feat: add auto furniture interaction detection
- New auto-interaction.ts: detects canSit/canLay/canStand from classname/name keywords - Maps keywords to interaction types (bench, chair, bed, table, etc.) - Returns clickLimit and interactionModesCount for proper click/interaction limits - Nitro editor: added auto-detect button with preview of detected settings - furni-import.ts: auto-sets interaction_modes_count during import - upload-import.ts: auto-detects for direct insert and SQL migrations - clone-import.ts: auto-detects when cloning from other hotels
This commit is contained in:
1 parent
0b4fc4559e
commit
dee843106f
8 files changed
+596
-25
No files matched your search
+11
-9
@@ -1,17 +1,19 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: "epicnextcms",
|
name: 'epic-next-app',
|
||||||
script: "pnpm",
|
script: 'node_modules/next/dist/bin/next',
|
||||||
args: "start",
|
args: 'start',
|
||||||
instances: "max",
|
cwd: '/var/www/atom-nexst',
|
||||||
exec_mode: "cluster",
|
instances: 1,
|
||||||
node_args: "--v8-pool-size=4",
|
autorestart: true,
|
||||||
max_memory_restart: "1G",
|
watch: false,
|
||||||
|
max_memory_restart: '1G',
|
||||||
|
node_args: '--max-old-space-size=1024',
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: 'production',
|
||||||
PORT: 3002
|
PORT: 3002
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -32,9 +32,14 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { adminFetch } from "@/lib/admin-fetch";
|
import { adminFetch } from "@/lib/admin-fetch";
|
||||||
|
import {
|
||||||
|
autoDetectInteraction,
|
||||||
|
getInteractionTypeLabel,
|
||||||
|
} from "@/lib/furni/auto-interaction";
|
||||||
|
|
||||||
interface NitroEditorDialogProps {
|
interface NitroEditorDialogProps {
|
||||||
classname: string;
|
classname: string;
|
||||||
|
name?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -122,6 +127,7 @@ function CopyClassname({ value }: { value: string }) {
|
|||||||
|
|
||||||
export function NitroEditorDialog({
|
export function NitroEditorDialog({
|
||||||
classname,
|
classname,
|
||||||
|
name = "",
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: NitroEditorDialogProps) {
|
}: NitroEditorDialogProps) {
|
||||||
@@ -142,8 +148,22 @@ export function NitroEditorDialog({
|
|||||||
cansiton: false,
|
cansiton: false,
|
||||||
canlayon: false,
|
canlayon: false,
|
||||||
});
|
});
|
||||||
|
const [autoDetectResult, setAutoDetectResult] = useState<ReturnType<
|
||||||
|
typeof autoDetectInteraction
|
||||||
|
> | null>(null);
|
||||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleAutoDetect = () => {
|
||||||
|
const result = autoDetectInteraction(classname, name);
|
||||||
|
setAutoDetectResult(result);
|
||||||
|
setFlags({
|
||||||
|
canstandon: result.canStand,
|
||||||
|
cansiton: result.canSit,
|
||||||
|
canlayon: result.canLay,
|
||||||
|
});
|
||||||
|
toast.success(`Auto-detected: ${result.reason}`);
|
||||||
|
};
|
||||||
|
|
||||||
const patchMeta = (path: string, value: unknown) => {
|
const patchMeta = (path: string, value: unknown) => {
|
||||||
setMetadata((prev) =>
|
setMetadata((prev) =>
|
||||||
prev == null ? prev : updateNested(prev, path, value),
|
prev == null ? prev : updateNested(prev, path, value),
|
||||||
@@ -497,6 +517,53 @@ export function NitroEditorDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAutoDetect}
|
||||||
|
className="gap-1.5 w-full"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
|
Auto-detect from name
|
||||||
|
</Button>
|
||||||
|
{autoDetectResult && (
|
||||||
|
<div className="rounded-md bg-muted/50 p-3 text-xs space-y-1">
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
autoDetectResult.canLay ? "default" : "secondary"
|
||||||
|
}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
{autoDetectResult.canLay ? (
|
||||||
|
<Layers className="w-2.5 h-2.5" />
|
||||||
|
) : autoDetectResult.canSit ? (
|
||||||
|
<MousePointerClick className="w-2.5 h-2.5" />
|
||||||
|
) : (
|
||||||
|
<MousePointerClick className="w-2.5 h-2.5" />
|
||||||
|
)}
|
||||||
|
{getInteractionTypeLabel(
|
||||||
|
autoDetectResult.interactionType,
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
{autoDetectResult.clickLimit > 0 && (
|
||||||
|
<Badge variant="outline" className="gap-1">
|
||||||
|
Click limit: {autoDetectResult.clickLimit}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{autoDetectResult.interactionModesCount > 0 && (
|
||||||
|
<Badge variant="outline" className="gap-1">
|
||||||
|
Modes: {autoDetectResult.interactionModesCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{autoDetectResult.reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1374,6 +1374,7 @@ export function StudioClient({
|
|||||||
|
|
||||||
<NitroEditorDialog
|
<NitroEditorDialog
|
||||||
classname={editingClassname ?? ""}
|
classname={editingClassname ?? ""}
|
||||||
|
name={detail?.name ?? ""}
|
||||||
open={editingClassname !== null}
|
open={editingClassname !== null}
|
||||||
onOpenChange={(open) => !open && setEditingClassname(null)}
|
onOpenChange={(open) => !open && setEditingClassname(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { eq, inArray, sql } from "drizzle-orm";
|
import { eq, inArray, sql } from "drizzle-orm";
|
||||||
import { db, ItemsBase } from "@/lib/db";
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
|
import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
|
||||||
import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel";
|
import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { extractFurniIconPng } from "@/lib/services/clone-icon";
|
import { extractFurniIconPng } from "@/lib/services/clone-icon";
|
||||||
@@ -324,16 +325,21 @@ export async function cloneSingleFurni(params: {
|
|||||||
// allow_stack: derived from stackHeight (mirrors furni-import.ts `dims.z > 0`).
|
// allow_stack: derived from stackHeight (mirrors furni-import.ts `dims.z > 0`).
|
||||||
// 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
|
||||||
|
const autoInteraction = autoDetectInteraction(classname, entry.name);
|
||||||
|
const interactionModesCount = autoInteraction.interactionModesCount;
|
||||||
|
|
||||||
let newId: number;
|
let newId: number;
|
||||||
try {
|
try {
|
||||||
newId = await allocateItemsBaseId(async (nextId) => {
|
newId = await allocateItemsBaseId(async (nextId) => {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO items_base
|
INSERT INTO items_base
|
||||||
(id, sprite_id, public_name, item_name, type, width, length, stack_height,
|
(id, sprite_id, public_name, item_name, type, width, length, stack_height,
|
||||||
allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams)
|
allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, interaction_modes_count, customparams)
|
||||||
VALUES
|
VALUES
|
||||||
(${nextId}, ${nextId}, ${entry.name}, ${classname}, ${itemType}, ${entry.xdim}, ${entry.ydim}, ${stackHeight},
|
(${nextId}, ${nextId}, ${entry.name}, ${classname}, ${itemType}, ${entry.xdim}, ${entry.ydim}, ${stackHeight},
|
||||||
${allowStack}, ${entry.cansiton ? "1" : "0"}, ${entry.canlayon ? "1" : "0"}, ${entry.canstandon ? "1" : "0"}, 'default', ${entry.customparams})`);
|
${allowStack}, ${entry.cansiton ? "1" : "0"}, ${entry.canlayon ? "1" : "0"}, ${entry.canstandon ? "1" : "0"}, 'default', ${interactionModesCount}, ${entry.customparams})`);
|
||||||
return nextId;
|
return nextId;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const IMPORTED_PAGE_CAPTION = "Imported Furniture";
|
|||||||
export { CATEGORY_PAGE, CLASSNAME_RULES } from "@/lib/furni/auto-catalog";
|
export { CATEGORY_PAGE, CLASSNAME_RULES } from "@/lib/furni/auto-catalog";
|
||||||
|
|
||||||
import { CATEGORY_PAGE } from "@/lib/furni/auto-catalog";
|
import { CATEGORY_PAGE } from "@/lib/furni/auto-catalog";
|
||||||
|
import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -117,7 +118,9 @@ export function buildItemsBaseEnrichmentUpdate(params: {
|
|||||||
canstandon: boolean;
|
canstandon: boolean;
|
||||||
allowStack: string;
|
allowStack: string;
|
||||||
interactionType: string;
|
interactionType: string;
|
||||||
|
interactionModesCount?: number;
|
||||||
}): SQL {
|
}): SQL {
|
||||||
|
const modesCount = params.interactionModesCount ?? 0;
|
||||||
return sql`
|
return sql`
|
||||||
UPDATE items_base
|
UPDATE items_base
|
||||||
SET width = ${params.dims.x}, length = ${params.dims.y}, stack_height = ${params.dims.z},
|
SET width = ${params.dims.x}, length = ${params.dims.y}, stack_height = ${params.dims.z},
|
||||||
@@ -125,7 +128,8 @@ export function buildItemsBaseEnrichmentUpdate(params: {
|
|||||||
allow_lay = ${params.canlayon ? "1" : "0"},
|
allow_lay = ${params.canlayon ? "1" : "0"},
|
||||||
allow_walk = ${params.canstandon ? "1" : "0"},
|
allow_walk = ${params.canstandon ? "1" : "0"},
|
||||||
allow_stack = ${params.allowStack},
|
allow_stack = ${params.allowStack},
|
||||||
interaction_type = ${params.interactionType}
|
interaction_type = ${params.interactionType},
|
||||||
|
interaction_modes_count = ${modesCount}
|
||||||
WHERE id = ${params.id}
|
WHERE id = ${params.id}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -864,6 +868,10 @@ export async function importSingleFurni(params: {
|
|||||||
// allow_stack: if it has height and you can put things on it
|
// allow_stack: if it has height and you can put things on it
|
||||||
const allowStack = dims.z > 0 ? "1" : "0";
|
const allowStack = dims.z > 0 ? "1" : "0";
|
||||||
|
|
||||||
|
// Auto-detect interaction modes count from classname/name
|
||||||
|
const autoInteraction = autoDetectInteraction(classname, bestName);
|
||||||
|
const interactionModesCount = autoInteraction.interactionModesCount;
|
||||||
|
|
||||||
// Update items_base with ALL enriched data
|
// Update items_base with ALL enriched data
|
||||||
try {
|
try {
|
||||||
const update = buildItemsBaseEnrichmentUpdate({
|
const update = buildItemsBaseEnrichmentUpdate({
|
||||||
@@ -874,6 +882,7 @@ export async function importSingleFurni(params: {
|
|||||||
canstandon,
|
canstandon,
|
||||||
allowStack,
|
allowStack,
|
||||||
interactionType,
|
interactionType,
|
||||||
|
interactionModesCount,
|
||||||
});
|
});
|
||||||
await db.execute(update);
|
await db.execute(update);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { existsSync, promises as fs } from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { db, ItemsBase } from "@/lib/db";
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
|
import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
|
||||||
import { getFurniAssetWriteTargets } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetWriteTargets } from "@/lib/services/furni-asset-dirs";
|
||||||
import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data";
|
import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
@@ -158,6 +159,7 @@ async function writeSqlMigration(params: {
|
|||||||
customparams: string;
|
customparams: string;
|
||||||
price: { credits: number; points: number; pointsType: number };
|
price: { credits: number; points: number; pointsType: number };
|
||||||
categoryKey: string;
|
categoryKey: string;
|
||||||
|
interactionModesCount: number;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const num = await findNextMigrationNumber();
|
const num = await findNextMigrationNumber();
|
||||||
const padded = String(num).padStart(4, "0");
|
const padded = String(num).padStart(4, "0");
|
||||||
@@ -193,12 +195,12 @@ async function writeSqlMigration(params: {
|
|||||||
"",
|
"",
|
||||||
`INSERT INTO items_base`,
|
`INSERT INTO items_base`,
|
||||||
` (id, sprite_id, public_name, item_name, type, width, length, stack_height,`,
|
` (id, sprite_id, public_name, item_name, type, width, length, stack_height,`,
|
||||||
` allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams)`,
|
` allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, interaction_modes_count, customparams)`,
|
||||||
`VALUES`,
|
`VALUES`,
|
||||||
` (@next_id, @next_id, ${escapeSql(params.name)}, ${escapeSql(params.classname)},`,
|
` (@next_id, @next_id, ${escapeSql(params.name)}, ${escapeSql(params.classname)},`,
|
||||||
` ${escapeSql(params.itemType)}, ${params.xdim}, ${params.ydim}, ${params.stackHeight},`,
|
` ${escapeSql(params.itemType)}, ${params.xdim}, ${params.ydim}, ${params.stackHeight},`,
|
||||||
` ${params.allowStack}, ${params.cansiton ? "1" : "0"}, ${params.canlayon ? "1" : "0"}, ${params.canstandon ? "1" : "0"},`,
|
` ${params.allowStack}, ${params.cansiton ? "1" : "0"}, ${params.canlayon ? "1" : "0"}, ${params.canstandon ? "1" : "0"},`,
|
||||||
` ${escapeSql(params.interactionType)}, ${escapeSql(params.customparams)});`,
|
` ${escapeSql(params.interactionType)}, ${params.interactionModesCount}, ${escapeSql(params.customparams)});`,
|
||||||
"",
|
"",
|
||||||
`INSERT INTO catalog_items`,
|
`INSERT INTO catalog_items`,
|
||||||
` (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)`,
|
` (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)`,
|
||||||
@@ -328,16 +330,20 @@ export async function uploadSingleFurni(params: {
|
|||||||
|
|
||||||
const price = autoPriceFurni(classname);
|
const price = autoPriceFurni(classname);
|
||||||
|
|
||||||
|
// Auto-detect interaction modes count from classname/name
|
||||||
|
const autoInteraction = autoDetectInteraction(classname, name);
|
||||||
|
const interactionModesCount = autoInteraction.interactionModesCount;
|
||||||
|
|
||||||
let newId: number;
|
let newId: number;
|
||||||
try {
|
try {
|
||||||
newId = await allocateItemsBaseId(async (nextId) => {
|
newId = await allocateItemsBaseId(async (nextId) => {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO items_base
|
INSERT INTO items_base
|
||||||
(id, sprite_id, public_name, item_name, type, width, length, stack_height,
|
(id, sprite_id, public_name, item_name, type, width, length, stack_height,
|
||||||
allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams)
|
allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, interaction_modes_count, customparams)
|
||||||
VALUES
|
VALUES
|
||||||
(${nextId}, ${nextId}, ${name}, ${classname}, ${itemType}, ${xdim}, ${ydim}, ${stackHeight},
|
(${nextId}, ${nextId}, ${name}, ${classname}, ${itemType}, ${xdim}, ${ydim}, ${stackHeight},
|
||||||
${allowStack}, ${cansiton ? "1" : "0"}, ${canlayon ? "1" : "0"}, ${canstandon ? "1" : "0"}, ${userInteractionType}, ${customparams})`);
|
${allowStack}, ${cansiton ? "1" : "0"}, ${canlayon ? "1" : "0"}, ${canstandon ? "1" : "0"}, ${userInteractionType}, ${interactionModesCount}, ${customparams})`);
|
||||||
return nextId;
|
return nextId;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -402,6 +408,7 @@ export async function uploadSingleFurni(params: {
|
|||||||
customparams,
|
customparams,
|
||||||
price,
|
price,
|
||||||
categoryKey: classifyFurni(classname, itemType),
|
categoryKey: classifyFurni(classname, itemType),
|
||||||
|
interactionModesCount,
|
||||||
});
|
});
|
||||||
warnings.push(`SQL migration written: ${sqlFile}`);
|
warnings.push(`SQL migration written: ${sqlFile}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+99
-9
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user