163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
/**
|
|
* habbo.it furnidata cache — downloads once, keeps in memory.
|
|
* Provides instant lookup by classname for enriching imports.
|
|
*/
|
|
|
|
export type { HabboItFurniEntry } from '@/types/furni'
|
|
|
|
import type { HabboItFurniEntry } from '@/types/furni'
|
|
|
|
const FURNIDATA_URL = 'https://www.habbo.it/gamedata/furnidata_json/1'
|
|
const CACHE_TTL = 30 * 60 * 1000 // 30 minutes
|
|
|
|
let cache: Map<string, HabboItFurniEntry> | null = null
|
|
let cacheTimestamp = 0
|
|
let loading: Promise<Map<string, HabboItFurniEntry>> | null = null
|
|
|
|
/**
|
|
* Fetches and parses the full habbo.it furnidata JSON.
|
|
* Results are cached in memory for 30 minutes.
|
|
*/
|
|
export async function getHabboItFurnidata(): Promise<Map<string, HabboItFurniEntry>> {
|
|
const now = Date.now()
|
|
|
|
// Return cache if still valid
|
|
if (cache && now - cacheTimestamp < CACHE_TTL) {
|
|
return cache
|
|
}
|
|
|
|
// Deduplicate concurrent requests
|
|
if (loading) return loading
|
|
|
|
loading = (async () => {
|
|
try {
|
|
console.debug('[habbo-it] Fetching furnidata from habbo.it...')
|
|
const res = await fetch(FURNIDATA_URL, {
|
|
signal: AbortSignal.timeout(20000),
|
|
headers: { Accept: 'application/json' },
|
|
})
|
|
|
|
if (!res.ok) {
|
|
console.warn(`[habbo-it] Fetch failed: ${res.status}`)
|
|
return cache || new Map()
|
|
}
|
|
|
|
const data = await res.json()
|
|
const map = new Map<string, HabboItFurniEntry>()
|
|
|
|
// Process room items (floor furniture)
|
|
const roomTypes = data?.roomitemtypes?.furnitype
|
|
if (Array.isArray(roomTypes)) {
|
|
for (const item of roomTypes) {
|
|
if (!item.classname) continue
|
|
map.set(item.classname, {
|
|
id: item.id ?? 0,
|
|
classname: item.classname,
|
|
revision: item.revision ?? 0,
|
|
category: item.category ?? '',
|
|
defaultdir: item.defaultdir ?? 0,
|
|
xdim: item.xdim ?? 1,
|
|
ydim: item.ydim ?? 1,
|
|
partcolors: item.partcolors ?? { color: [] },
|
|
name: item.name ?? '',
|
|
description: item.description ?? '',
|
|
specialtype: item.specialtype ?? 0,
|
|
canstandon: item.canstandon ?? false,
|
|
cansiton: item.cansiton ?? false,
|
|
canlayon: item.canlayon ?? false,
|
|
furniline: item.furniline ?? '',
|
|
environment: item.environment ?? '',
|
|
rare: item.rare ?? false,
|
|
tradeable: item.tradeable,
|
|
bc: item.bc,
|
|
customparams: item.customparams,
|
|
itemType: 's',
|
|
})
|
|
}
|
|
}
|
|
|
|
// Process wall items
|
|
const wallTypes = data?.wallitemtypes?.furnitype
|
|
if (Array.isArray(wallTypes)) {
|
|
for (const item of wallTypes) {
|
|
if (!item.classname) continue
|
|
map.set(item.classname, {
|
|
id: item.id ?? 0,
|
|
classname: item.classname,
|
|
revision: item.revision ?? 0,
|
|
category: item.category ?? '',
|
|
defaultdir: item.defaultdir ?? 0,
|
|
xdim: item.xdim ?? 1,
|
|
ydim: item.ydim ?? 1,
|
|
partcolors: item.partcolors ?? { color: [] },
|
|
name: item.name ?? '',
|
|
description: item.description ?? '',
|
|
specialtype: item.specialtype ?? 0,
|
|
canstandon: item.canstandon ?? false,
|
|
cansiton: item.cansiton ?? false,
|
|
canlayon: item.canlayon ?? false,
|
|
furniline: item.furniline ?? '',
|
|
environment: item.environment ?? '',
|
|
rare: item.rare ?? false,
|
|
tradeable: item.tradeable,
|
|
bc: item.bc,
|
|
customparams: item.customparams,
|
|
itemType: 'i',
|
|
})
|
|
}
|
|
}
|
|
|
|
console.debug(
|
|
`[habbo-it] Cached ${map.size} items (${roomTypes?.length ?? 0} room + ${wallTypes?.length ?? 0} wall)`,
|
|
)
|
|
cache = map
|
|
cacheTimestamp = now
|
|
return map
|
|
} catch (err) {
|
|
console.warn('[habbo-it] Fetch error:', (err as Error).message)
|
|
return cache || new Map()
|
|
} finally {
|
|
loading = null
|
|
}
|
|
})()
|
|
|
|
return loading
|
|
}
|
|
|
|
/**
|
|
* Lookup a single item by classname from habbo.it furnidata.
|
|
* Tries exact match first, then base classname (without color variant).
|
|
*/
|
|
export async function lookupHabboIt(classname: string): Promise<HabboItFurniEntry | null> {
|
|
const map = await getHabboItFurnidata()
|
|
|
|
// 1. Exact match
|
|
const exact = map.get(classname)
|
|
if (exact) return exact
|
|
|
|
// 2. If classname has color variant (e.g. "chair*4"), try base
|
|
if (classname.includes('*')) {
|
|
const base = classname.split('*')[0]
|
|
// Try base with *0 first (most common default)
|
|
const variant0 = map.get(`${base}*0`)
|
|
if (variant0) return variant0
|
|
// Try just the base
|
|
const baseMatch = map.get(base)
|
|
if (baseMatch) return baseMatch
|
|
}
|
|
|
|
// 3. If no variant in classname, try *0
|
|
if (!classname.includes('*')) {
|
|
const withVariant = map.get(`${classname}*0`)
|
|
if (withVariant) return withVariant
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/** Clear the cache (useful after manual edits) */
|
|
export function clearHabboItCache(): void {
|
|
cache = null
|
|
cacheTimestamp = 0
|
|
}
|