fix: use configured furni source and catalog assets
This commit is contained in:
1 parent
aed70db81f
commit
c78f8812ad
25 files changed
+368
-129
No files matched your search
@@ -7,8 +7,10 @@ import {
|
||||
Check,
|
||||
CheckCheck,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FileWarning,
|
||||
FolderSync,
|
||||
Globe2,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Loader2,
|
||||
@@ -52,6 +54,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import type { FurniImportSource } from "@/lib/habbo-gamedata-hotel";
|
||||
import { NitroEditorDialog } from "./nitro-editor-dialog";
|
||||
|
||||
interface FurniItem {
|
||||
@@ -146,7 +149,7 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
failed: "Failed",
|
||||
};
|
||||
|
||||
export function ImportFurniClient() {
|
||||
export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [activeSearch, setActiveSearch] = useState("");
|
||||
const [items, setItems] = useState<FurniItem[]>([]);
|
||||
@@ -936,6 +939,31 @@ export function ImportFurniClient() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-card px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-md bg-primary/10 p-2 text-primary">
|
||||
<Globe2 className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Official furnidata source
|
||||
</p>
|
||||
<p className="text-sm font-medium">{source.label}</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Furniture assets: {source.assetCdnHost}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={source.furnidataUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
{source.host}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
{/* Dashboard Statistics */}
|
||||
{!stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as hotelHelpers from "@/lib/habbo-gamedata-hotel";
|
||||
|
||||
describe("furni import source metadata", () => {
|
||||
it("describes the configured hotel without changing the global asset CDN", () => {
|
||||
const helpers = hotelHelpers as unknown as {
|
||||
buildFurniImportSource?: (hotel: string) => {
|
||||
hotel: string;
|
||||
label: string;
|
||||
host: string;
|
||||
furnidataUrl: string;
|
||||
assetCdnHost: string;
|
||||
};
|
||||
officialHabboEnrichmentWarning?: (hotel: string, name: string) => string;
|
||||
};
|
||||
|
||||
expect(helpers.buildFurniImportSource).toBeTypeOf("function");
|
||||
expect(helpers.officialHabboEnrichmentWarning).toBeTypeOf("function");
|
||||
if (
|
||||
!helpers.buildFurniImportSource ||
|
||||
!helpers.officialHabboEnrichmentWarning
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(helpers.buildFurniImportSource("nl")).toEqual({
|
||||
hotel: "nl",
|
||||
label: "Netherlands (habbo.nl)",
|
||||
host: "habbo.nl",
|
||||
furnidataUrl: "https://www.habbo.nl/gamedata/furnidata_json/1",
|
||||
assetCdnHost: "images.habbo.com",
|
||||
});
|
||||
expect(helpers.officialHabboEnrichmentWarning("nl", "Stoel")).toBe(
|
||||
'Enriched from habbo.nl: "Stoel"',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { buildFurniImportSource } from "@/lib/habbo-gamedata-hotel";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
import { ImportFurniClient } from "./import-furni-client";
|
||||
|
||||
export default async function ImportFurniPage(_props: {
|
||||
@@ -10,5 +12,6 @@ export default async function ImportFurniPage(_props: {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
return <ImportFurniClient />;
|
||||
const hotel = await getHabboGamedataHotel();
|
||||
return <ImportFurniClient source={buildFurniImportSource(hotel)} />;
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiOk } from "@/lib/api-response";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
|
||||
const CATALOGUE_DIR = path.join(
|
||||
/*turbopackIgnore: true*/ process.cwd(),
|
||||
"public",
|
||||
"swf",
|
||||
"c_images",
|
||||
"catalogue",
|
||||
);
|
||||
import {
|
||||
catalogueAssetDirectories,
|
||||
readCatalogueAssetFiles,
|
||||
} from "@/lib/services/catalog-assets";
|
||||
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
|
||||
|
||||
export const GET = withAdmin(
|
||||
{ permission: PERMS.CATALOG_VIEW },
|
||||
@@ -23,18 +18,14 @@ export const GET = withAdmin(
|
||||
const offset = Math.max(Number(sp.get("offset") || 0) || 0, 0);
|
||||
const search = (sp.get("search") || "").trim().toLowerCase();
|
||||
|
||||
let icons: number[] = [];
|
||||
try {
|
||||
const files = await fs.readdir(CATALOGUE_DIR);
|
||||
icons = files
|
||||
.map((f) => /^icon_(\d+)\.(?:png|gif)$/i.exec(f))
|
||||
.filter((m): m is RegExpExecArray => Boolean(m))
|
||||
.map((m) => Number(m[1]))
|
||||
.filter((n) => Number.isFinite(n))
|
||||
.sort((a, b) => a - b);
|
||||
} catch {
|
||||
icons = [];
|
||||
}
|
||||
const directories = catalogueAssetDirectories(await getGamedataRoot());
|
||||
const files = await readCatalogueAssetFiles(directories);
|
||||
let icons = files
|
||||
.map((f) => /^icon_(\d+)\.(?:png|gif)$/i.exec(f))
|
||||
.filter((m): m is RegExpExecArray => Boolean(m))
|
||||
.map((m) => Number(m[1]))
|
||||
.filter((n) => Number.isFinite(n))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (search) {
|
||||
icons = icons.filter((id) => String(id).includes(search));
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiOk } from "@/lib/api-response";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
|
||||
const CATALOGUE_DIR = path.join(
|
||||
/*turbopackIgnore: true*/ process.cwd(),
|
||||
"public",
|
||||
"swf",
|
||||
"c_images",
|
||||
"catalogue",
|
||||
);
|
||||
import {
|
||||
catalogueAssetDirectories,
|
||||
readCatalogueAssetFiles,
|
||||
} from "@/lib/services/catalog-assets";
|
||||
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
|
||||
|
||||
export const GET = withAdmin(
|
||||
{ permission: PERMS.CATALOG_VIEW },
|
||||
@@ -25,20 +20,17 @@ export const GET = withAdmin(
|
||||
const type = sp.get("type") || "all";
|
||||
|
||||
let names: string[] = [];
|
||||
try {
|
||||
const files = await fs.readdir(CATALOGUE_DIR);
|
||||
const seen = new Set<string>();
|
||||
for (const file of files) {
|
||||
if (/^icon_\d+\./i.test(file)) continue;
|
||||
const base = file.replace(/\.(png|gif|jpg|jpeg|webp)$/i, "");
|
||||
if (!base || seen.has(base)) continue;
|
||||
seen.add(base);
|
||||
names.push(base);
|
||||
}
|
||||
names.sort((a, b) => a.localeCompare(b));
|
||||
} catch {
|
||||
names = [];
|
||||
const directories = catalogueAssetDirectories(await getGamedataRoot());
|
||||
const files = await readCatalogueAssetFiles(directories);
|
||||
const seen = new Set<string>();
|
||||
for (const file of files) {
|
||||
if (/^icon_\d+\./i.test(file)) continue;
|
||||
const base = file.replace(/\.(png|gif|jpg|jpeg|webp)$/i, "");
|
||||
if (!base || seen.has(base)) continue;
|
||||
seen.add(base);
|
||||
names.push(base);
|
||||
}
|
||||
names.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
if (type === "header") {
|
||||
names = names.filter((n) => /header/i.test(n));
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
readFurniData,
|
||||
} from "@/lib/services/furni-data";
|
||||
import { downloadFile, ensureDirectories } from "@/lib/services/furni-import";
|
||||
import { getHabboItFurnidata } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
|
||||
|
||||
@@ -62,7 +62,7 @@ export const POST = withAdmin(
|
||||
}> = [];
|
||||
|
||||
// Load furnidata once for all items
|
||||
const furnidata = await getHabboItFurnidata();
|
||||
const furnidata = await getOfficialHabboFurnidata();
|
||||
|
||||
// Load existing FurnitureData.json classnames for dedup
|
||||
const existingFD = (await readFurniData()) as {
|
||||
@@ -204,7 +204,7 @@ export const POST = withAdmin(
|
||||
spriteId: dbItem?.spriteId || 0,
|
||||
dims: conversion.dimensions || { x: 1, y: 1, z: 1 },
|
||||
metadata: conversion.metadata || null,
|
||||
habboIt: fd || null,
|
||||
officialHabbo: fd || null,
|
||||
});
|
||||
furniDataEntries.push({ entry, itemType });
|
||||
fdClassnames.add(baseClassname);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getFurnitureDataPath,
|
||||
readFurniData,
|
||||
} from "@/lib/services/furni-data";
|
||||
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { lookupOfficialHabboFurni } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
|
||||
/**
|
||||
@@ -86,9 +86,9 @@ export const POST = withAdmin(
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const classname = row.itemName;
|
||||
let habboIt = null;
|
||||
let officialHabbo = null;
|
||||
try {
|
||||
habboIt = await lookupHabboIt(classname);
|
||||
officialHabbo = await lookupOfficialHabboFurni(classname);
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
@@ -96,10 +96,10 @@ export const POST = withAdmin(
|
||||
const entry = buildFurniEntry({
|
||||
id: row.id,
|
||||
classname,
|
||||
revision: habboIt?.revision ?? 0,
|
||||
category: habboIt?.category || "unknown",
|
||||
name: row.publicName || habboIt?.name || classname,
|
||||
description: habboIt?.description || "",
|
||||
revision: officialHabbo?.revision ?? 0,
|
||||
category: officialHabbo?.category || "unknown",
|
||||
name: row.publicName || officialHabbo?.name || classname,
|
||||
description: officialHabbo?.description || "",
|
||||
spriteId: row.spriteId,
|
||||
dims: {
|
||||
x: row.width,
|
||||
@@ -107,7 +107,7 @@ export const POST = withAdmin(
|
||||
z: Math.round(Number(row.stackHeight)),
|
||||
},
|
||||
metadata: null,
|
||||
habboIt,
|
||||
officialHabbo,
|
||||
});
|
||||
|
||||
entries.push({ entry, itemType: row.type === "i" ? "i" : "s" });
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
IMPORTED_PAGE_CAPTION_SAVE,
|
||||
importSingleFurni,
|
||||
} from "@/lib/services/furni-import";
|
||||
import { getHabboItFurnidata } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { listFurniture } from "@/lib/services/habbofurni";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
|
||||
@@ -183,7 +183,7 @@ export const GET = withAdmin(
|
||||
const pageItems = missing.slice(start, start + perPage);
|
||||
|
||||
// Enrich with revision from official furnidata
|
||||
const furnidata = await getHabboItFurnidata();
|
||||
const furnidata = await getOfficialHabboFurnidata();
|
||||
const items = pageItems.map((item) => {
|
||||
const fd =
|
||||
furnidata.get(item.item_name) ||
|
||||
@@ -434,7 +434,7 @@ export const PATCH = withAdmin(
|
||||
// Resolve revision: try furnidata first, then spriteId as fallback
|
||||
let rev = item.revision || 0;
|
||||
if (rev === 0) {
|
||||
const furnidata = await getHabboItFurnidata();
|
||||
const furnidata = await getOfficialHabboFurnidata();
|
||||
const fd =
|
||||
furnidata.get(item.classname) ||
|
||||
furnidata.get(baseClassname) ||
|
||||
@@ -516,7 +516,7 @@ export const PATCH = withAdmin(
|
||||
.from(ItemsBase)
|
||||
.where(eq(ItemsBase.itemName, item.classname))
|
||||
.limit(1);
|
||||
const furnidata = await getHabboItFurnidata();
|
||||
const furnidata = await getOfficialHabboFurnidata();
|
||||
const fd =
|
||||
furnidata.get(item.classname) || furnidata.get(baseClassname);
|
||||
const itemType = dbItem?.type === "i" ? "i" : "s";
|
||||
@@ -531,7 +531,7 @@ export const PATCH = withAdmin(
|
||||
spriteId: dbItem?.spriteId || 0,
|
||||
dims: conversion.dimensions || { x: 1, y: 1, z: 1 },
|
||||
metadata: conversion.metadata || null,
|
||||
habboIt: fd || null,
|
||||
officialHabbo: fd || null,
|
||||
});
|
||||
await appendFurniEntry(entry, itemType);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user