diff --git a/docker-compose.yml b/docker-compose.yml index ddf9f6c0..9f217c27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,7 +82,7 @@ services: - PORT=3002 byparr: - image: ghcr.io/thephaseless/byparr:v3.0.4 + image: ghcr.io/thephaseless/byparr:main # `latest` is een drijvende tag. Zonder pull_policy haalt compose de # bestaande image nooit opnieuw op en blijft byparr hangen op de eerste # versie die ooit werd binnengehaald. diff --git a/src/app/api/admin/import/furni/nitro-editor/route.ts b/src/app/api/admin/import/furni/nitro-editor/route.ts index 05f382e1..323ff26f 100644 --- a/src/app/api/admin/import/furni/nitro-editor/route.ts +++ b/src/app/api/admin/import/furni/nitro-editor/route.ts @@ -14,6 +14,7 @@ import { withFurniDataLock, writeFurniData, } from "@/lib/services/furni-data"; +import { autoDetectInteraction, nitroAnimationStatesCount } from "@/lib/furni/auto-interaction"; import { rcon } from "@/lib/services/rcon"; import { computeDefaultDir, @@ -164,9 +165,27 @@ export const PUT = withAdmin( // 5. Update items_base (best-effort) try { + const autoInteraction = autoDetectInteraction( + rawClassname, + typeof metadata?.name === "string" ? metadata.name : rawClassname, + { + cansiton, + canlayon, + canstandon, + logicType: + typeof metadata?.logic?.logicType === "string" + ? metadata.logic.logicType + : undefined, + animationStates: nitroAnimationStatesCount(metadata), + hasActionData: true, + }, + ); + const interactionModesCount = autoInteraction.interactionModesCount; + await db.execute(sql` UPDATE items_base SET width = ${dims.x}, length = ${dims.y}, stack_height = ${dims.z}, + interaction_modes_count = ${interactionModesCount}, allow_sit = ${cansiton ? "1" : "0"}, allow_lay = ${canlayon ? "1" : "0"}, allow_walk = ${canstandon ? "1" : "0"} diff --git a/src/lib/furni/studio-inspection.ts b/src/lib/furni/studio-inspection.ts index 9b6c6d11..d276d32a 100644 --- a/src/lib/furni/studio-inspection.ts +++ b/src/lib/furni/studio-inspection.ts @@ -1,6 +1,6 @@ export interface FurnitureInspection { classname: string; - sql: Array<{ id: number; spriteId: number; name: string; type: string }>; + sql: Array<{ id: number; spriteId: number; name: string; type: string; width?: number; length?: number; stackHeight?: number; interactionModesCount?: number }>; catalog: Array<{ id: number; pageId: number; @@ -13,6 +13,10 @@ export interface FurnitureInspection { description: string; revision: number; type: string; + width?: number; + length?: number; + stackHeight?: number; + interactionModesCount?: number; }>; furnidataReadable: boolean; nitro: { exists: boolean | null; bytes: number }; @@ -32,11 +36,20 @@ export function validateClassnames(value: unknown): string[] | null { return [...new Set(value)]; } export function compareFurniture( - source: { name: string; description: string; revision: number; type: string }, + source: { + name: string; + description: string; + revision: number; + type: string; + width?: number; + length?: number; + height?: number; + interactionModesCount?: number; + }, local: FurnitureInspection, ) { - const fd = local.furnidata[0], - sql = local.sql[0]; + const fd = local.furnidata[0]; + const sql = local.sql[0]; const values = [ ["Name", sql?.name ?? fd?.name, source.name], ["Description", fd?.description, source.description], @@ -46,16 +59,23 @@ export function compareFurniture( sql ? (sql.type === "i" ? "wallitem" : "flooritem") : fd?.type, source.type, ], - ] as const; - const fields = values.map(([label, before, after]) => ({ - label, - before, - after, - changed: - before !== undefined && - after !== undefined && - String(before) !== String(after), - })); + ["Width", sql?.width ?? fd?.width, source.width], + ["Length", sql?.length ?? fd?.length, source.length], + ["Height", sql?.stackHeight ?? fd?.stackHeight, source.height], + ["Interaction Modes Count", sql?.interactionModesCount ?? fd?.interactionModesCount, source.interactionModesCount], + ]; + const fields = values.map((item) => { + const [label, before, after] = item; + return { + label, + before, + after, + changed: + before !== undefined && + after !== undefined && + String(before) !== String(after), + }; + }); const state = hasFurnitureConflict(local) ? "conflict" : !local.furnidataReadable @@ -65,7 +85,7 @@ export function compareFurniture( : fields.some((f) => f.changed) ? "changed" : "present"; - return { state, fields } as const; + return { state, fields }; } export function furnitureHealth(local: FurnitureInspection) { const missing: string[] = []; diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index ef8d8ff6..09048e51 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -1009,61 +1009,6 @@ export async function importSingleFurni(params: { }; } - // ── Insert into DB with enriched data ────────────────────────────── - await onProgress?.("writing_db"); - try { - if (preservingExisting) { - warnings.push( - `Existing furniture #${itemId}: SQL settings and catalog offers preserved`, - ); - } else if (isUpdate) { - // NOTE: live Arcturus `items_base` has no description/revision/rare columns — - // those metadata fields are persisted in FurnitureData.json (see entry below), - // not in the DB. Only refresh the columns this import manages. - await db.execute(sql` - UPDATE items_base SET sprite_id = ${spriteId}, public_name = ${bestName}, item_name = ${classname}, type = ${itemType} - WHERE id = ${spriteId} - `); - } else { - itemId = await reserveFurnitureId( - originalId, - classname, - async (allocatedId) => { - await db.execute( - sql`INSERT INTO items_base (id, sprite_id, public_name, item_name, type) VALUES (${allocatedId}, ${allocatedId}, ${bestName}, ${classname}, ${itemType})`, - ); - }, - ); - spriteId = itemId; - if (spriteId !== originalId) - warnings.push( - `Source ID ${originalId} is occupied; imported with local ID ${spriteId}`, - ); - } - } catch (err) { - return { - ok: false, - warnings, - error: `DB ${isUpdate ? "update" : "insert"} failed: ${(err as Error).message}`, - }; - } - - await Promise.all([ - mirrorFileToDirs(swfPath, `${safeSwfName}.swf`, mirrorSwfDirs, warnings), - mirrorFileToDirs( - iconPath, - `${iconFileName}_icon.png`, - mirrorIconDirs, - warnings, - ), - mirrorFileToDirs( - resolveNitro() ?? nitroPath, - `${safeNitroName}${LOCAL_BUNDLE_EXT}`, - mirrorNitroDirs, - warnings, - ), - ]); - // ── Build enriched FurnitureData entry ──────────────────────────── const dims = conversionResult?.dimensions || @@ -1144,6 +1089,65 @@ export async function importSingleFurni(params: { }); const interactionModesCount = autoInteraction.interactionModesCount; + // ── Insert into DB with enriched data ────────────────────────────── + await onProgress?.("writing_db"); + try { + if (preservingExisting) { + warnings.push( + `Existing furniture #${itemId}: SQL settings and catalog offers preserved`, + ); + } else if (isUpdate) { + // NOTE: live Arcturus `items_base` has no description/revision/rare columns — + // those metadata fields are persisted in FurnitureData.json (see entry below), + // not in the DB. Only refresh the columns this import manages. + await db.execute(sql` + UPDATE items_base + SET sprite_id = ${spriteId}, public_name = ${bestName}, item_name = ${classname}, type = ${itemType}, + width = ${dims.x}, length = ${dims.y}, stack_height = ${dims.z}, + interaction_modes_count = ${interactionModesCount}, interaction_type = ${interactionType} + WHERE id = ${spriteId} + `); + } else { + itemId = await reserveFurnitureId( + originalId, + classname, + async (allocatedId) => { + await db.execute( + sql`INSERT INTO items_base (id, sprite_id, public_name, item_name, type) VALUES (${allocatedId}, ${allocatedId}, ${bestName}, ${classname}, ${itemType})`, + ); + }, + ); + spriteId = itemId; + if (spriteId !== originalId) + warnings.push( + `Source ID ${originalId} is occupied; imported with local ID ${spriteId}`, + ); + } + } catch (err) { + return { + ok: false, + warnings, + error: `DB ${isUpdate ? "update" : "insert"} failed: ${(err as Error).message}`, + }; + } + + await Promise.all([ + mirrorFileToDirs(swfPath, `${safeSwfName}.swf`, mirrorSwfDirs, warnings), + mirrorFileToDirs( + iconPath, + `${iconFileName}_icon.png`, + mirrorIconDirs, + warnings, + ), + mirrorFileToDirs( + resolveNitro() ?? nitroPath, + `${safeNitroName}${LOCAL_BUNDLE_EXT}`, + mirrorNitroDirs, + warnings, + ), + ]); + + // When the .nitro logicType produced no specific handler, fall back to the // auto-detected type (classname mechanic + keyword + real flags) exactly // like the post-import verification sweep, so newly imported items carry the diff --git a/src/lib/services/furniture-inspection.ts b/src/lib/services/furniture-inspection.ts index b58c9747..d7d625c4 100644 --- a/src/lib/services/furniture-inspection.ts +++ b/src/lib/services/furniture-inspection.ts @@ -46,7 +46,7 @@ export async function inspectFurniture( const rows = await queryRows< FurnitureInspection["sql"][number] & { classname: string } >( - sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type FROM items_base WHERE LOWER(TRIM(item_name)) IN (${namesSql})`, + sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type, width, length, stack_height AS stackHeight, interaction_modes_count AS interactionModesCount FROM items_base WHERE LOWER(TRIM(item_name)) IN (${namesSql})`, ); const offers = await queryRows< FurnitureInspection["catalog"][number] & { classname: string } @@ -89,6 +89,10 @@ export async function inspectFurniture( description: String(entry.description ?? ""), revision: Number(entry.revision ?? 0), type, + width: Number(entry.xdim ?? 0), + length: Number(entry.ydim ?? 0), + stackHeight: Number(entry.zdim ?? entry.defaultdir ?? 0), + interactionModesCount: Number(entry.interaction_modes_count ?? 0), }); entries.set(key, values); }