feat: auto interaction detection from real furniture data
CI / check (push) Successful in 31s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m17s

Replace classname/name keyword guessing with real data so sit/lay/stand
has zero false positives:
- autoDetectInteraction() now accepts real flags (cansiton/canlayon/
  canstandon), logicType, and animation-state count; real data wins and
  keywords are only a fallback when no real data exists.
- New furni-real-interaction helper reads the local FurnitureData.json
  (highest-revision entries win; a true flag in any duplicate is kept)
  and falls back to the official Habbo furnidata.
- interaction_modes_count now uses emulator conventions: dice=6,
  gate/teleport=2, roller=1, generic multistate = animation-state count,
  chairs/beds=1, plain items=0.
- verifyAndFixInteractionModesCount only touches items with real data
  and skips everything else (no keyword overwrites).
- parseNitroBundle falls back to gunzip for gzip-compressed .nitro files.
- Wired through furni-import, upload-import, clone-import, and the nitro
  editor auto-detect button.
This commit is contained in:
openhands committed 2026-08-19 20:28:24 +02:00
1 parent 121dda7249
commit 3fa192a108
8 files changed
+626 -78

No files matched your search

@@ -35,6 +35,7 @@ import { adminFetch } from "@/lib/admin-fetch";
import {
autoDetectInteraction,
getInteractionTypeLabel,
nitroAnimationStatesCount,
} from "@/lib/furni/auto-interaction";
interface NitroEditorDialogProps {
@@ -154,7 +155,20 @@ export function NitroEditorDialog({
const [jsonError, setJsonError] = useState<string | null>(null);
const handleAutoDetect = () => {
const result = autoDetectInteraction(classname, name);
// Use real data when available: the DB flags for this item plus the
// .nitro animation states already loaded in the editor. Keyword
// detection only kicks in when neither is present — no false positives.
const nitroJson =
metadata && typeof metadata === "object"
? (metadata as Record<string, unknown>)
: null;
const result = autoDetectInteraction(classname, name, {
cansiton: flags.cansiton,
canlayon: flags.canlayon,
canstandon: flags.canstandon,
logicType: nitroJson?.logicType as string | undefined,
animationStates: nitroAnimationStatesCount(nitroJson),
});
setAutoDetectResult(result);
setFlags({
canstandon: result.canStand,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+16 -3
View File
@@ -16,6 +16,7 @@ import {
ensureDirectories,
getOrCreateCategoryPage,
} from "@/lib/services/furni-import";
import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction";
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
import {
curlDownload,
@@ -314,8 +315,12 @@ export async function cloneSingleFurni(params: {
}
}
// Validate it is a real Nitro bundle.
let clonedNitroJson: Record<string, unknown> | null = null;
try {
parseNitroBundle(await fs.readFile(/*turbopackIgnore: true*/ nitroPath));
const parsed = parseNitroBundle(
await fs.readFile(/*turbopackIgnore: true*/ nitroPath),
);
clonedNitroJson = parsed.json as Record<string, unknown>;
} catch {
await fs.unlink(/*turbopackIgnore: true*/ nitroPath).catch(() => {});
logger.warn("[clone-import] invalid .nitro bundle for", { classname });
@@ -367,8 +372,16 @@ export async function cloneSingleFurni(params: {
// allow_walk = canstandon, allow_sit = cansiton, allow_lay = canlayon.
const allowStack = stackHeight > 0 ? "1" : "0";
// Auto-detect interaction modes count from classname/name
const autoInteraction = autoDetectInteraction(classname, entry.name);
// Auto-detect interaction modes count. The source furnidata flags and the
// cloned .nitro animation states are real data — keyword guessing is only
// used when neither is available, so there are no false positives.
const autoInteraction = autoDetectInteraction(classname, entry.name, {
cansiton: entry.cansiton,
canlayon: entry.canlayon,
canstandon: entry.canstandon,
logicType: (clonedNitroJson?.logicType as string) || undefined,
animationStates: nitroAnimationStatesCount(clonedNitroJson),
});
const interactionModesCount = autoInteraction.interactionModesCount;
let newId: number;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+16 -3
View File
@@ -1,4 +1,4 @@
import { deflateSync, inflateSync } from "node:zlib";
import { deflateSync, gunzipSync, inflateSync } from "node:zlib";
import type { SpriteImage } from "./image-decoder";
// ── Minimal PNG Encoder ────────────────────────────────────────────
@@ -267,7 +267,7 @@ export function parseNitroBundle(buffer: Buffer): {
offset += 4;
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen);
offset += jsonCompressedLen;
const jsonStr = inflateSync(jsonCompressed).toString("utf-8");
const jsonStr = inflateJsonOrGzip(jsonCompressed).toString("utf-8");
const json = JSON.parse(jsonStr);
// File 2: PNG
@@ -279,7 +279,20 @@ export function parseNitroBundle(buffer: Buffer): {
offset += 4;
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen);
offset += pngCompressedLen;
const png = inflateSync(pngCompressed);
const png = inflateJsonOrGzip(pngCompressed);
return { json, jsonFileName, png, pngFileName };
}
/**
* Some bundled .nitro files (e.g. official /var/www/Gamedata furniture)
* gzip their contents instead of raw-deflate. Try inflate first, then fall
* back to gunzip so both formats parse.
*/
function inflateJsonOrGzip(input: Buffer): Buffer {
try {
return inflateSync(input);
} catch {
return gunzipSync(input);
}
}
+10 -2
View File
@@ -15,6 +15,7 @@ import {
IMPORTED_PAGE_CAPTION,
IMPORTED_PAGE_CAPTION_SAVE,
} from "@/lib/services/furni-import";
import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
export interface UploadResult {
@@ -330,8 +331,15 @@ export async function uploadSingleFurni(params: {
const price = autoPriceFurni(classname);
// Auto-detect interaction modes count from classname/name
const autoInteraction = autoDetectInteraction(classname, name);
// Auto-detect interaction modes count. Real flags + .nitro animation
// states win over keyword guessing — no false positives.
const autoInteraction = autoDetectInteraction(classname, name, {
cansiton,
canlayon,
canstandon,
logicType: (nitroMeta?.logicType as string) || undefined,
animationStates: nitroAnimationStatesCount(nitroMeta),
});
const interactionModesCount = autoInteraction.interactionModesCount;
let newId: number;