feat: auto interaction detection from real furniture data
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:
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,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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user