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
@@ -0,0 +1,23 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { catalogueAssetDirectories } from "@/lib/services/catalog-assets";
|
||||
|
||||
describe("catalogue asset directories", () => {
|
||||
it("prefers the configured Gamedata catalogue and retains the bundled fallback", () => {
|
||||
const cwd = path.join("test", "cms");
|
||||
const gamedataRoot = path.join("test", "Gamedata");
|
||||
|
||||
expect(catalogueAssetDirectories(gamedataRoot, cwd)).toEqual([
|
||||
path.join(gamedataRoot, "c_images", "catalogue"),
|
||||
path.join(cwd, "public", "swf", "c_images", "catalogue"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses only the bundled catalogue when no Gamedata root is configured", () => {
|
||||
const cwd = path.join("test", "cms");
|
||||
|
||||
expect(catalogueAssetDirectories("", cwd)).toEqual([
|
||||
path.join(cwd, "public", "swf", "c_images", "catalogue"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function catalogueAssetDirectories(
|
||||
gamedataRoot: string,
|
||||
cwd = process.cwd(),
|
||||
): string[] {
|
||||
const bundled = path.join(cwd, "public", "swf", "c_images", "catalogue");
|
||||
if (!gamedataRoot.trim()) return [bundled];
|
||||
|
||||
return [path.join(gamedataRoot, "c_images", "catalogue"), bundled];
|
||||
}
|
||||
|
||||
export async function readCatalogueAssetFiles(
|
||||
directories: string[],
|
||||
): Promise<string[]> {
|
||||
const files = new Set<string>();
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
for (const file of await fs.readdir(directory)) files.add(file);
|
||||
} catch {
|
||||
// A missing optional source must not hide assets from the other source.
|
||||
}
|
||||
}
|
||||
return [...files];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getGamedataRoot,
|
||||
getNitroFilesRoot,
|
||||
} from "@/lib/services/furni-asset-dirs";
|
||||
import type { HabboItFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import type { OfficialHabboFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import type { FurniMetadata } from "@/lib/services/swf-to-nitro";
|
||||
|
||||
@@ -150,7 +150,7 @@ export function buildFurniEntry(params: {
|
||||
spriteId: number;
|
||||
dims: { x: number; y: number; z: number };
|
||||
metadata: FurniMetadata | null;
|
||||
habboIt?: HabboItFurniEntry | null;
|
||||
officialHabbo?: OfficialHabboFurniEntry | null;
|
||||
}): Record<string, unknown> {
|
||||
const {
|
||||
id,
|
||||
@@ -162,20 +162,20 @@ export function buildFurniEntry(params: {
|
||||
spriteId,
|
||||
dims,
|
||||
metadata,
|
||||
habboIt,
|
||||
officialHabbo,
|
||||
} = params;
|
||||
|
||||
// Merge partcolors: prefer habbo.it (has real hex colors), fallback to .nitro extraction
|
||||
const partcolors = habboIt?.partcolors?.color?.length
|
||||
? habboIt.partcolors
|
||||
// Prefer the configured official hotel's real hex colors, then .nitro metadata.
|
||||
const partcolors = officialHabbo?.partcolors?.color?.length
|
||||
? officialHabbo.partcolors
|
||||
: (metadata?.partcolors ?? { color: [] });
|
||||
|
||||
return {
|
||||
id,
|
||||
classname,
|
||||
revision: revision || habboIt?.revision || 0,
|
||||
category: category || habboIt?.category || "unknown",
|
||||
defaultdir: metadata?.defaultdir ?? habboIt?.defaultdir ?? 0,
|
||||
revision: revision || officialHabbo?.revision || 0,
|
||||
category: category || officialHabbo?.category || "unknown",
|
||||
defaultdir: metadata?.defaultdir ?? officialHabbo?.defaultdir ?? 0,
|
||||
xdim: dims.x,
|
||||
ydim: dims.y,
|
||||
partcolors,
|
||||
@@ -183,19 +183,19 @@ export function buildFurniEntry(params: {
|
||||
description: description || "",
|
||||
adurl: "",
|
||||
offerid: spriteId,
|
||||
buyout: !!habboIt?.bc,
|
||||
buyout: !!officialHabbo?.bc,
|
||||
rentofferid: -1,
|
||||
rentbuyout: false,
|
||||
bc: habboIt?.bc ?? false,
|
||||
bc: officialHabbo?.bc ?? false,
|
||||
excludeddynamic: false,
|
||||
customparams: habboIt?.customparams ?? "",
|
||||
specialtype: metadata?.specialtype ?? habboIt?.specialtype ?? 0,
|
||||
canstandon: metadata?.canstandon ?? habboIt?.canstandon ?? false,
|
||||
cansiton: metadata?.cansiton ?? habboIt?.cansiton ?? false,
|
||||
canlayon: metadata?.canlayon ?? habboIt?.canlayon ?? false,
|
||||
furniline: habboIt?.furniline ?? "",
|
||||
environment: habboIt?.environment ?? "",
|
||||
rare: habboIt?.rare ?? false,
|
||||
customparams: officialHabbo?.customparams ?? "",
|
||||
specialtype: metadata?.specialtype ?? officialHabbo?.specialtype ?? 0,
|
||||
canstandon: metadata?.canstandon ?? officialHabbo?.canstandon ?? false,
|
||||
cansiton: metadata?.cansiton ?? officialHabbo?.cansiton ?? false,
|
||||
canlayon: metadata?.canlayon ?? officialHabbo?.canlayon ?? false,
|
||||
furniline: officialHabbo?.furniline ?? "",
|
||||
environment: officialHabbo?.environment ?? "",
|
||||
rare: officialHabbo?.rare ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ vi.mock("@/lib/services/furni-data", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
||||
lookupHabboIt: vi.fn(),
|
||||
lookupOfficialHabboFurni: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/import/core/download", () => ({
|
||||
|
||||
@@ -2,14 +2,16 @@ import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
|
||||
import { logger } from "@/lib/logger";
|
||||
import {
|
||||
appendFurniEntry,
|
||||
buildFurniEntry,
|
||||
removeFurniEntry,
|
||||
} from "@/lib/services/furni-data";
|
||||
import type { HabboItFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||
import type { OfficialHabboFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { lookupOfficialHabboFurni } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
|
||||
// Re-export the download helpers now owned by the shared import core.
|
||||
// Existing furni callers keep importing them from here.
|
||||
@@ -396,22 +398,23 @@ export async function importSingleFurni(params: {
|
||||
const itemType = type === "wallitem" ? "i" : "s";
|
||||
const spriteId = originalId;
|
||||
|
||||
// ── Cross-reference with habbo.it furnidata for enrichment ────────
|
||||
let habboIt: HabboItFurniEntry | null = null;
|
||||
// ── Cross-reference with configured official Habbo furnidata ──────
|
||||
let officialHabbo: OfficialHabboFurniEntry | null = null;
|
||||
try {
|
||||
habboIt = await lookupHabboIt(classname);
|
||||
if (habboIt) {
|
||||
warnings.push(`Enriched from habbo.it: "${habboIt.name}"`);
|
||||
const hotel = await getHabboGamedataHotel();
|
||||
officialHabbo = await lookupOfficialHabboFurni(classname);
|
||||
if (officialHabbo) {
|
||||
warnings.push(officialHabboEnrichmentWarning(hotel, officialHabbo.name));
|
||||
}
|
||||
} catch {
|
||||
// Non-blocking: habbo.it enrichment is optional
|
||||
// Non-blocking: official Habbo enrichment is optional
|
||||
}
|
||||
|
||||
// Best values: use habbo.it furnidata for enrichment
|
||||
const bestName = name || habboIt?.name || classname;
|
||||
const bestDescription = description || habboIt?.description || "";
|
||||
const bestRevision = revision || habboIt?.revision || 0;
|
||||
const bestCategory = category || habboIt?.category || "unknown";
|
||||
// Best values: use configured official Habbo furnidata for enrichment.
|
||||
const bestName = name || officialHabbo?.name || classname;
|
||||
const bestDescription = description || officialHabbo?.description || "";
|
||||
const bestRevision = revision || officialHabbo?.revision || 0;
|
||||
const bestCategory = category || officialHabbo?.category || "unknown";
|
||||
|
||||
// ── Insert into DB with enriched data ──────────────────────────────
|
||||
onProgress?.("writing_db");
|
||||
@@ -581,15 +584,15 @@ export async function importSingleFurni(params: {
|
||||
// ── Build enriched FurnitureData entry ────────────────────────────
|
||||
const dims =
|
||||
conversionResult?.dimensions ||
|
||||
(habboIt
|
||||
? { x: habboIt.xdim, y: habboIt.ydim, z: 0 }
|
||||
(officialHabbo
|
||||
? { x: officialHabbo.xdim, y: officialHabbo.ydim, z: 0 }
|
||||
: { x: 1, y: 1, z: 0 });
|
||||
const meta = conversionResult?.metadata ?? null;
|
||||
|
||||
// Merge interaction flags: prefer .nitro extraction, fallback to habbo.it
|
||||
const cansiton = meta?.cansiton ?? habboIt?.cansiton ?? false;
|
||||
const canlayon = meta?.canlayon ?? habboIt?.canlayon ?? false;
|
||||
const canstandon = meta?.canstandon ?? habboIt?.canstandon ?? false;
|
||||
// Prefer .nitro flags, then the configured official Habbo furnidata.
|
||||
const cansiton = meta?.cansiton ?? officialHabbo?.cansiton ?? false;
|
||||
const canlayon = meta?.canlayon ?? officialHabbo?.canlayon ?? false;
|
||||
const canstandon = meta?.canstandon ?? officialHabbo?.canstandon ?? false;
|
||||
|
||||
// Derive interaction_type. Priority:
|
||||
// 1. Wired LOGIC items (wf_act_/wf_trg_/wf_cnd_/wf_slc_/wf_xtra_/wf_var_)
|
||||
@@ -657,7 +660,7 @@ export async function importSingleFurni(params: {
|
||||
spriteId,
|
||||
dims,
|
||||
metadata: meta,
|
||||
habboIt,
|
||||
officialHabbo,
|
||||
});
|
||||
|
||||
// ── Add to FurnitureData.json (or defer for batch) ────────────────
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// @vitest-environment node
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { getHabboGamedataHotelMock } = vi.hoisted(() => ({
|
||||
getHabboGamedataHotelMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/habbo-gamedata-hotel", () => ({
|
||||
getHabboGamedataHotel: getHabboGamedataHotelMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/logger", () => ({
|
||||
logger: { warn: vi.fn() },
|
||||
}));
|
||||
|
||||
import * as cacheModule from "./habbo-furnidata-cache";
|
||||
|
||||
function fixture(classname: string, name: string) {
|
||||
return {
|
||||
roomitemtypes: {
|
||||
furnitype: [
|
||||
{
|
||||
id: 1,
|
||||
classname,
|
||||
name,
|
||||
description: "",
|
||||
revision: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
wallitemtypes: { furnitype: [] },
|
||||
};
|
||||
}
|
||||
|
||||
describe("official Habbo furnidata cache", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
const cacheApi = cacheModule as unknown as {
|
||||
clearOfficialHabboFurnidataCache?: () => void;
|
||||
clearHabboItCache?: () => void;
|
||||
};
|
||||
(
|
||||
cacheApi.clearOfficialHabboFurnidataCache ?? cacheApi.clearHabboItCache
|
||||
)?.();
|
||||
});
|
||||
|
||||
it("exports a generic API and isolates cached data by configured hotel", async () => {
|
||||
const generic = cacheModule as unknown as {
|
||||
getOfficialHabboFurnidata?: () => Promise<Map<string, { name: string }>>;
|
||||
};
|
||||
expect(generic.getOfficialHabboFurnidata).toBeTypeOf("function");
|
||||
if (!generic.getOfficialHabboFurnidata) return;
|
||||
|
||||
getHabboGamedataHotelMock
|
||||
.mockResolvedValueOnce("it")
|
||||
.mockResolvedValueOnce("nl");
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(fixture("italian_chair", "Sedia"))),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(fixture("dutch_chair", "Stoel"))),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const italian = await generic.getOfficialHabboFurnidata();
|
||||
const dutch = await generic.getOfficialHabboFurnidata();
|
||||
|
||||
expect(italian.get("italian_chair")?.name).toBe("Sedia");
|
||||
expect(dutch.get("dutch_chair")?.name).toBe("Stoel");
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://www.habbo.nl/gamedata/furnidata_json/1",
|
||||
expect.objectContaining({ headers: { Accept: "application/json" } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
* (`habbo_gamedata_hotel`). Downloads once per hotel, keeps in memory.
|
||||
*/
|
||||
|
||||
export type { HabboItFurniEntry } from "@/types/furni";
|
||||
export type { OfficialHabboFurniEntry } from "@/types/furni";
|
||||
|
||||
import {
|
||||
type HabboGamedataHotel,
|
||||
@@ -11,21 +11,21 @@ import {
|
||||
} from "@/lib/habbo-gamedata-hotel";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
import type { HabboItFurniEntry } from "@/types/furni";
|
||||
import type { OfficialHabboFurniEntry } from "@/types/furni";
|
||||
|
||||
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
let cache: Map<string, HabboItFurniEntry> | null = null;
|
||||
let cache: Map<string, OfficialHabboFurniEntry> | null = null;
|
||||
let cacheHotel: HabboGamedataHotel | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
let loading: Promise<Map<string, HabboItFurniEntry>> | null = null;
|
||||
let loading: Promise<Map<string, OfficialHabboFurniEntry>> | null = null;
|
||||
|
||||
/**
|
||||
* Fetches and parses the full official Habbo furnidata JSON for the
|
||||
* configured hotel. Results are cached in memory for 30 minutes.
|
||||
*/
|
||||
export async function getHabboItFurnidata(): Promise<
|
||||
Map<string, HabboItFurniEntry>
|
||||
export async function getOfficialHabboFurnidata(): Promise<
|
||||
Map<string, OfficialHabboFurniEntry>
|
||||
> {
|
||||
const hotel = await getHabboGamedataHotel();
|
||||
const now = Date.now();
|
||||
@@ -51,7 +51,7 @@ export async function getHabboItFurnidata(): Promise<
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const map = new Map<string, HabboItFurniEntry>();
|
||||
const map = new Map<string, OfficialHabboFurniEntry>();
|
||||
|
||||
const roomTypes = data?.roomitemtypes?.furnitype;
|
||||
if (Array.isArray(roomTypes)) {
|
||||
@@ -137,10 +137,10 @@ export async function getHabboItFurnidata(): Promise<
|
||||
* Lookup a single item by classname from official Habbo furnidata.
|
||||
* Tries exact match first, then base classname (without color variant).
|
||||
*/
|
||||
export async function lookupHabboIt(
|
||||
export async function lookupOfficialHabboFurni(
|
||||
classname: string,
|
||||
): Promise<HabboItFurniEntry | null> {
|
||||
const map = await getHabboItFurnidata();
|
||||
): Promise<OfficialHabboFurniEntry | null> {
|
||||
const map = await getOfficialHabboFurnidata();
|
||||
|
||||
const exact = map.get(classname);
|
||||
if (exact) return exact;
|
||||
@@ -162,7 +162,7 @@ export async function lookupHabboIt(
|
||||
}
|
||||
|
||||
/** Clear the cache (e.g. after changing habbo_gamedata_hotel). */
|
||||
export function clearHabboItCache(): void {
|
||||
export function clearOfficialHabboFurnidataCache(): void {
|
||||
cache = null;
|
||||
cacheHotel = null;
|
||||
cacheTimestamp = 0;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { logger } from "@/lib/logger";
|
||||
import type { FurniListResult, HabboFurniItem } from "@/types/furni";
|
||||
import { getHabboItFurnidata } from "./habbo-furnidata-cache";
|
||||
import { getOfficialHabboFurnidata } from "./habbo-furnidata-cache";
|
||||
|
||||
export type { FurniListResult, HabboFurniItem };
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function listFurniture(
|
||||
const importedSet = options.importedSet;
|
||||
|
||||
try {
|
||||
const map = await getHabboItFurnidata();
|
||||
const map = await getOfficialHabboFurnidata();
|
||||
if (map.size === 0) return empty;
|
||||
|
||||
// Convert to array and optionally filter by search
|
||||
@@ -108,12 +108,12 @@ export async function searchFurniture(
|
||||
|
||||
/**
|
||||
* Get a single furniture item by classname.
|
||||
* Uses the cached habbo.it furnidata.
|
||||
* Uses cached furnidata from the configured official Habbo hotel.
|
||||
*/
|
||||
export async function getFurnitureByClassname(
|
||||
classname: string,
|
||||
): Promise<HabboFurniItem | null> {
|
||||
const map = await getHabboItFurnidata();
|
||||
const map = await getOfficialHabboFurnidata();
|
||||
|
||||
// Try exact match
|
||||
let entry = map.get(classname);
|
||||
|
||||
Reference in new issue
Block a user