Merge branch 'main' of ssh://5.231.31.84:2222/remco/EpicNext-Cms
CI / check (push) Failing after 37s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / tests-imaging-browsers (push) Skipped
CI / tests-quality (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped

This commit is contained in:
Remco
2026-10-28 20:49:49 +01:00
5 changed files with 134 additions and 72 deletions
+1 -1
View File
@@ -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.
@@ -4,6 +4,10 @@ import type { NextRequest } from "next/server";
import { apiError, apiOk } from "@/lib/api";
import { withAdmin } from "@/lib/api-handler";
import { db, ItemsBase, queryRows } from "@/lib/db";
import {
autoDetectInteraction,
nitroAnimationStatesCount,
} from "@/lib/furni/auto-interaction";
import { resolveBundleInDir } from "@/lib/furni/bundle-file";
import { logger } from "@/lib/logger";
import { PERMS } from "@/lib/permissions";
@@ -173,9 +177,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"}
+48 -15
View File
@@ -1,6 +1,15 @@
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 +22,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 +45,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 +68,27 @@ 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 +98,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[] = [];
+58 -55
View File
@@ -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,64 @@ 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
+5 -1
View File
@@ -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);
}