import { promises as fs } from 'node:fs' import { Prisma } from '@/generated/prisma/client' import { prisma } from '@/lib/prisma' import { getFurnitureDataPath } from '@/lib/services/furni-data' // Cache FurnitureData.json in memory with TTL to avoid repeated disk I/O let furniDataCache: { roomitemtypes?: { furnitype?: Array<{ id: number; description?: string; classname?: string; revision?: number }> } wallitemtypes?: { furnitype?: Array<{ id: number; description?: string; classname?: string; revision?: number }> } } | null = null let furniDataCacheTime = 0 const FURNI_CACHE_TTL = 30_000 // 30 seconds async function getFurnitureData() { if (furniDataCache && Date.now() - furniDataCacheTime < FURNI_CACHE_TTL) return furniDataCache const furniDataPath = await getFurnitureDataPath() try { const raw = await fs.readFile(furniDataPath, 'utf-8') furniDataCache = JSON.parse(raw) furniDataCacheTime = Date.now() return furniDataCache } catch { return null } } /** Invalidate the FurnitureData cache after writes */ export function invalidateFurniDataCache() { furniDataCache = null furniDataCacheTime = 0 } interface RawItem { id: number itemIds: string pageId: number offerId: number songId: number orderNumber: number catalogName: string costCredits: number costPoints: number pointsType: number amount: number limitedSells: number limitedStack: number extradata: string haveOffer: string clubOnly: string } interface BaseItem { id: number spriteId: number publicName: string itemName: string type: string width: number length: number stackHeight: number allowStack: number allowSit: number allowLay: number allowWalk: number allowGift: number allowTrade: number allowRecycle: number allowMarketplaceSell: number allowInventoryStack: number interactionType: string interactionModesCount: number vendingIds: string customparams: string effectIdMale: number effectIdFemale: number clothingOnWalk: string } export interface CatalogItemEnriched { id: number catalogName: string itemIds: string costCredits: number costPoints: number pointsType: number amount: number limitedSells: number limitedStack: number orderNumber: number offerId: number songId: number haveOffer: string clubOnly: string extradata: string baseName: string baseItemName: string spriteId: number baseItem: BaseItem | null } export interface CatalogItemsData { items: CatalogItemEnriched[] baseItems: { id: number; publicName: string; itemName: string; spriteId: number; type: string }[] catalogNameMap: Record furniDataIdList: number[] furniDescriptionMap: Record furniRevisionMap: Record interactionTypes: string[] allPages: { id: number; caption: string }[] } export async function loadCatalogItemsData(pageId: number): Promise { // Load items via raw query to work around pageId Int vs VARCHAR mismatch const pageIdStr = String(pageId) const rawItems = await prisma.$queryRaw>>` SELECT * FROM catalog_items WHERE page_id = ${pageIdStr} ORDER BY id ASC ` const items: RawItem[] = rawItems.map((r: Record) => ({ id: Number(r.id), itemIds: String(r.item_ids ?? ''), pageId: Number(r.page_id) || 0, offerId: Number(r.offer_id) || -1, songId: Number(r.song_id) || 0, orderNumber: Number(r.order_number) || 99, catalogName: String(r.catalog_name ?? ''), costCredits: Number(r.cost_credits) || 0, costPoints: Number(r.cost_points) || 0, pointsType: Number(r.points_type) || 0, amount: Number(r.amount) || 1, limitedSells: Number(r.limited_sells) || 0, limitedStack: Number(r.limited_stack) || 0, extradata: String(r.extradata ?? ''), haveOffer: String(r.have_offer ?? '1'), clubOnly: String(r.club_only ?? '0'), })) const [allPages, interactionTypesRaw] = await Promise.all([ prisma.catalogPages.findMany({ orderBy: { caption: 'asc' }, select: { id: true, caption: true }, }), prisma.$queryRaw>` SELECT DISTINCT interaction_type FROM items_base ORDER BY interaction_type ASC `, ]) const interactionTypes = interactionTypesRaw.map((r) => String(r.interaction_type)) // Resolve item names from items_base for enrichment + translate const itemIdStrings = items.map((i) => i.itemIds).filter(Boolean) const baseItemIds = [ ...new Set( itemIdStrings.flatMap((s) => s .split(';') .map(Number) .filter((n) => n > 0), ), ), ] // Raw query to work around ENUM('0','1') columns returning numeric values. // baseItemIds is built from numeric splits + filter(n > 0); joined via Prisma.join. const baseItems: BaseItem[] = baseItemIds.length > 0 ? ( await prisma.$queryRaw>>` SELECT id, sprite_id, public_name, item_name, type, width, length, stack_height, allow_stack, allow_sit, allow_lay, allow_walk, allow_gift, allow_trade, allow_recycle, allow_marketplace_sell, allow_inventory_stack, interaction_type, interaction_modes_count, vending_ids, customparams, effect_id_male, effect_id_female, clothing_on_walk FROM items_base WHERE id IN (${Prisma.join(baseItemIds)}) ` ).map((r) => ({ id: Number(r.id), spriteId: Number(r.sprite_id), publicName: String(r.public_name ?? ''), itemName: String(r.item_name ?? ''), type: String(r.type ?? 's'), width: Number(r.width) || 1, length: Number(r.length) || 1, stackHeight: Number(r.stack_height) || 0, allowStack: Number(r.allow_stack ?? 0), allowSit: Number(r.allow_sit ?? 0), allowLay: Number(r.allow_lay ?? 0), allowWalk: Number(r.allow_walk ?? 0), allowGift: Number(r.allow_gift ?? 1), allowTrade: Number(r.allow_trade ?? 1), allowRecycle: Number(r.allow_recycle ?? 0), allowMarketplaceSell: Number(r.allow_marketplace_sell ?? 0), allowInventoryStack: Number(r.allow_inventory_stack ?? 1), interactionType: String(r.interaction_type ?? 'default'), interactionModesCount: Number(r.interaction_modes_count) || 2, vendingIds: String(r.vending_ids ?? '0'), customparams: String(r.customparams ?? ''), effectIdMale: Number(r.effect_id_male) || 0, effectIdFemale: Number(r.effect_id_female) || 0, clothingOnWalk: String(r.clothing_on_walk ?? ''), })) : [] const baseItemMap = Object.fromEntries(baseItems.map((b) => [b.id, b])) // Build mapping: baseItemId -> catalogName const catalogNameMap: Record = {} for (const item of items) { const firstBaseId = item.itemIds .split(';') .map(Number) .find((n) => n > 0) if (firstBaseId && !catalogNameMap[firstBaseId]) { catalogNameMap[firstBaseId] = item.catalogName } } const catalogItems: CatalogItemEnriched[] = items.map((item) => { const firstBaseId = item.itemIds .split(';') .map(Number) .find((n) => n > 0) const base = firstBaseId ? baseItemMap[firstBaseId] : undefined return { id: item.id, catalogName: item.catalogName, itemIds: item.itemIds, costCredits: item.costCredits, costPoints: item.costPoints, pointsType: item.pointsType, amount: item.amount, limitedSells: item.limitedSells, limitedStack: item.limitedStack, orderNumber: item.orderNumber, offerId: item.offerId, songId: item.songId, haveOffer: item.haveOffer, clubOnly: item.clubOnly, extradata: item.extradata, baseName: base?.publicName || base?.itemName || '', baseItemName: base?.itemName || '', spriteId: base?.spriteId ?? 0, baseItem: base ? { ...base } : null, } }) // Build spriteId -> items_base.id mapping for FurnitureData.json lookup const spriteToBaseId = new Map() for (const b of baseItems) { spriteToBaseId.set(b.spriteId, b.id) } // Read FurnitureData.json (cached in memory) const foundBaseIds = new Set() const furniDescriptionMap: Record = {} const furniRevisionMap: Record = {} const furniData = await getFurnitureData() if (furniData) { for (const section of ['roomitemtypes', 'wallitemtypes'] as const) { if (furniData[section]?.furnitype) { for (const item of furniData[section].furnitype) { const baseId = spriteToBaseId.get(item.id) if (baseId !== undefined) { foundBaseIds.add(baseId) furniDescriptionMap[baseId] = item.description ?? '' furniRevisionMap[baseId] = { classname: item.classname ?? '', revision: item.revision ?? 0, } } } } } } return { items: catalogItems, baseItems: baseItems.map((b) => ({ id: b.id, publicName: b.publicName, itemName: b.itemName, spriteId: b.spriteId, type: b.type, })), catalogNameMap, furniDataIdList: [...foundBaseIds], furniDescriptionMap, furniRevisionMap, interactionTypes, allPages, } }