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 {
|
import {
|
||||||
autoDetectInteraction,
|
autoDetectInteraction,
|
||||||
getInteractionTypeLabel,
|
getInteractionTypeLabel,
|
||||||
|
nitroAnimationStatesCount,
|
||||||
} from "@/lib/furni/auto-interaction";
|
} from "@/lib/furni/auto-interaction";
|
||||||
|
|
||||||
interface NitroEditorDialogProps {
|
interface NitroEditorDialogProps {
|
||||||
@@ -154,7 +155,20 @@ export function NitroEditorDialog({
|
|||||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleAutoDetect = () => {
|
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);
|
setAutoDetectResult(result);
|
||||||
setFlags({
|
setFlags({
|
||||||
canstandon: result.canStand,
|
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,
|
ensureDirectories,
|
||||||
getOrCreateCategoryPage,
|
getOrCreateCategoryPage,
|
||||||
} from "@/lib/services/furni-import";
|
} from "@/lib/services/furni-import";
|
||||||
|
import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction";
|
||||||
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
||||||
import {
|
import {
|
||||||
curlDownload,
|
curlDownload,
|
||||||
@@ -314,8 +315,12 @@ export async function cloneSingleFurni(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Validate it is a real Nitro bundle.
|
// Validate it is a real Nitro bundle.
|
||||||
|
let clonedNitroJson: Record<string, unknown> | null = null;
|
||||||
try {
|
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 {
|
} catch {
|
||||||
await fs.unlink(/*turbopackIgnore: true*/ nitroPath).catch(() => {});
|
await fs.unlink(/*turbopackIgnore: true*/ nitroPath).catch(() => {});
|
||||||
logger.warn("[clone-import] invalid .nitro bundle for", { classname });
|
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.
|
// allow_walk = canstandon, allow_sit = cansiton, allow_lay = canlayon.
|
||||||
const allowStack = stackHeight > 0 ? "1" : "0";
|
const allowStack = stackHeight > 0 ? "1" : "0";
|
||||||
|
|
||||||
// Auto-detect interaction modes count from classname/name
|
// Auto-detect interaction modes count. The source furnidata flags and the
|
||||||
const autoInteraction = autoDetectInteraction(classname, entry.name);
|
// 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;
|
const interactionModesCount = autoInteraction.interactionModesCount;
|
||||||
|
|
||||||
let newId: number;
|
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";
|
import type { SpriteImage } from "./image-decoder";
|
||||||
|
|
||||||
// ── Minimal PNG Encoder ────────────────────────────────────────────
|
// ── Minimal PNG Encoder ────────────────────────────────────────────
|
||||||
@@ -267,7 +267,7 @@ export function parseNitroBundle(buffer: Buffer): {
|
|||||||
offset += 4;
|
offset += 4;
|
||||||
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen);
|
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen);
|
||||||
offset += jsonCompressedLen;
|
offset += jsonCompressedLen;
|
||||||
const jsonStr = inflateSync(jsonCompressed).toString("utf-8");
|
const jsonStr = inflateJsonOrGzip(jsonCompressed).toString("utf-8");
|
||||||
const json = JSON.parse(jsonStr);
|
const json = JSON.parse(jsonStr);
|
||||||
|
|
||||||
// File 2: PNG
|
// File 2: PNG
|
||||||
@@ -279,7 +279,20 @@ export function parseNitroBundle(buffer: Buffer): {
|
|||||||
offset += 4;
|
offset += 4;
|
||||||
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen);
|
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen);
|
||||||
offset += pngCompressedLen;
|
offset += pngCompressedLen;
|
||||||
const png = inflateSync(pngCompressed);
|
const png = inflateJsonOrGzip(pngCompressed);
|
||||||
|
|
||||||
return { json, jsonFileName, png, pngFileName };
|
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,
|
||||||
IMPORTED_PAGE_CAPTION_SAVE,
|
IMPORTED_PAGE_CAPTION_SAVE,
|
||||||
} from "@/lib/services/furni-import";
|
} from "@/lib/services/furni-import";
|
||||||
|
import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction";
|
||||||
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
||||||
|
|
||||||
export interface UploadResult {
|
export interface UploadResult {
|
||||||
@@ -330,8 +331,15 @@ export async function uploadSingleFurni(params: {
|
|||||||
|
|
||||||
const price = autoPriceFurni(classname);
|
const price = autoPriceFurni(classname);
|
||||||
|
|
||||||
// Auto-detect interaction modes count from classname/name
|
// Auto-detect interaction modes count. Real flags + .nitro animation
|
||||||
const autoInteraction = autoDetectInteraction(classname, name);
|
// 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;
|
const interactionModesCount = autoInteraction.interactionModesCount;
|
||||||
|
|
||||||
let newId: number;
|
let newId: number;
|
||||||
|
|||||||
Reference in new issue
Block a user