fix: map furni imports to production gamedata

This commit is contained in:
Simo committed 2026-08-02 13:16:51 +02:00
1 parent a81af2d71a
commit 01570874a8
6 files changed
+224 -15

No files matched your search

@@ -148,6 +148,14 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
type: "text",
placeholder: "E:\\path\\to\\Nitro-Files",
},
{
key: "gamedata_root",
label: "Production Gamedata root (server path)",
description:
"Filesystem root served at /gamedata. Auto-detected as /var/www/Gamedata when present.",
type: "text",
placeholder: "/var/www/Gamedata",
},
],
},
{
+87
View File
@@ -0,0 +1,87 @@
// @vitest-environment node
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { existsSyncMock, settingGetMock } = vi.hoisted(() => ({
existsSyncMock: vi.fn(),
settingGetMock: vi.fn(),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return { ...actual, existsSync: existsSyncMock };
});
vi.mock("@/lib/services/site-settings", () => ({
siteSettings: { get: settingGetMock },
}));
import { getFurniAssetWriteTargets } from "./furni-asset-dirs";
const primary = {
swfDir: path.join("test-assets", "primary", "swf"),
iconDir: path.join("test-assets", "primary", "icons"),
nitroDir: path.join("test-assets", "primary", "furniture"),
};
function configureSettings(values: Record<string, string>): void {
settingGetMock.mockImplementation(
async (key: string, fallback: string | null = null) =>
values[key] ?? fallback,
);
}
describe("getFurniAssetWriteTargets", () => {
beforeEach(() => {
vi.clearAllMocks();
existsSyncMock.mockReturnValue(false);
});
it("maps a configured production Gamedata root to the directories served by Nitro", async () => {
const gamedataRoot = path.join("test-assets", "Gamedata");
configureSettings({
furni_swf_dir: primary.swfDir,
furni_icon_dir: primary.iconDir,
furni_nitro_dir: primary.nitroDir,
nitro_files_root: "",
gamedata_root: gamedataRoot,
});
const targets = await getFurniAssetWriteTargets();
expect(targets.mirrorDirs).toContainEqual({
swfDir: primary.swfDir,
iconDir: path.join(gamedataRoot, "icons"),
nitroDir: path.join(gamedataRoot, "bundled", "furniture"),
});
});
it("does not add a production mirror when no root is configured or present", async () => {
configureSettings({
furni_swf_dir: primary.swfDir,
furni_icon_dir: primary.iconDir,
furni_nitro_dir: primary.nitroDir,
nitro_files_root: "",
gamedata_root: "",
});
const targets = await getFurniAssetWriteTargets();
expect(targets.mirrorDirs).toEqual([]);
});
it("does not duplicate a Gamedata layout already used as the primary target", async () => {
const gamedataRoot = path.join("test-assets", "Gamedata");
configureSettings({
furni_swf_dir: primary.swfDir,
furni_icon_dir: path.join(gamedataRoot, "icons"),
furni_nitro_dir: path.join(gamedataRoot, "bundled", "furniture"),
nitro_files_root: "",
gamedata_root: gamedataRoot,
});
const targets = await getFurniAssetWriteTargets();
expect(targets.mirrorDirs).toEqual([]);
});
});
+44 -7
View File
@@ -7,6 +7,8 @@ export const DEFAULT_NITRO_FILES_ROOT = path.join(
"DEV",
"Nitro-Files",
);
export const DEFAULT_GAMEDATA_ROOT =
process.platform === "win32" ? "" : "/var/www/Gamedata";
export const DEFAULT_FURNI_ICON_DIR = path.join(
/*turbopackIgnore: true*/ process.cwd(),
"public/swf/dcr/hof_furni/icons",
@@ -63,6 +65,16 @@ export async function getNitroFilesRoot(): Promise<string> {
: "";
}
export async function getGamedataRoot(): Promise<string> {
const configured = (await siteSettings.get("gamedata_root", "")) ?? "";
if (configured.trim()) return normalizeDir(configured, configured);
return DEFAULT_GAMEDATA_ROOT &&
existsSync(/*turbopackIgnore: true*/ DEFAULT_GAMEDATA_ROOT)
? normalizeDir(DEFAULT_GAMEDATA_ROOT, DEFAULT_GAMEDATA_ROOT)
: "";
}
function dirsFromNitroFilesRoot(root: string): FurniAssetDirs {
return {
swfDir: path.join(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"),
@@ -77,6 +89,17 @@ function dirsFromNitroFilesRoot(root: string): FurniAssetDirs {
};
}
function dirsFromGamedataRoot(
root: string,
primarySwfDir: string,
): FurniAssetDirs {
return {
swfDir: primarySwfDir,
iconDir: path.join(/*turbopackIgnore: true*/ root, "icons"),
nitroDir: path.join(/*turbopackIgnore: true*/ root, "bundled/furniture"),
};
}
export async function getFurniAssetDirs(): Promise<FurniAssetDirs> {
const [swfDir, iconDir, nitroDir] = await Promise.all([
siteSettings
@@ -99,10 +122,16 @@ export async function getFurniAssetDirs(): Promise<FurniAssetDirs> {
export async function getFurniAssetWriteTargets(): Promise<FurniAssetWriteTargets> {
const primary = await getFurniAssetDirs();
const nitroFilesRoot = await getNitroFilesRoot();
const mirrorCandidates = nitroFilesRoot
? [dirsFromNitroFilesRoot(nitroFilesRoot)]
: [];
const [nitroFilesRoot, gamedataRoot] = await Promise.all([
getNitroFilesRoot(),
getGamedataRoot(),
]);
const mirrorCandidates = [
...(nitroFilesRoot ? [dirsFromNitroFilesRoot(nitroFilesRoot)] : []),
...(gamedataRoot
? [dirsFromGamedataRoot(gamedataRoot, primary.swfDir)]
: []),
];
const primaryKey = {
swfDir:
@@ -119,18 +148,26 @@ export async function getFurniAssetWriteTargets(): Promise<FurniAssetWriteTarget
: primary.nitroDir,
};
const seenCandidates = new Set<string>();
const mirrorDirs = mirrorCandidates.filter((dirs) => {
const swfDir = normalizeDir(dirs.swfDir, dirs.swfDir);
const iconDir = normalizeDir(dirs.iconDir, dirs.iconDir);
const nitroDir = normalizeDir(dirs.nitroDir, dirs.nitroDir);
return (
const isDifferent =
(process.platform === "win32" ? swfDir.toLowerCase() : swfDir) !==
primaryKey.swfDir ||
(process.platform === "win32" ? iconDir.toLowerCase() : iconDir) !==
primaryKey.iconDir ||
(process.platform === "win32" ? nitroDir.toLowerCase() : nitroDir) !==
primaryKey.nitroDir
);
primaryKey.nitroDir;
if (!isDifferent) return false;
const candidateKey = [swfDir, iconDir, nitroDir]
.map((dir) => (process.platform === "win32" ? dir.toLowerCase() : dir))
.join("\0");
if (seenCandidates.has(candidateKey)) return false;
seenCandidates.add(candidateKey);
return true;
});
return {
+63
View File
@@ -0,0 +1,63 @@
// @vitest-environment node
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } =
vi.hoisted(() => ({
getGamedataRootMock: vi.fn(),
getNitroFilesRootMock: vi.fn(),
settingGetMock: vi.fn(),
}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
getGamedataRoot: getGamedataRootMock,
getNitroFilesRoot: getNitroFilesRootMock,
}));
vi.mock("@/lib/services/site-settings", () => ({
siteSettings: { get: settingGetMock },
}));
import { writeFurniData } from "./furni-data";
describe("writeFurniData production mirror", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "furni-data-paths-"));
getNitroFilesRootMock.mockResolvedValue("");
getGamedataRootMock.mockResolvedValue(path.join(tempDir, "Gamedata"));
settingGetMock.mockImplementation(
async (key: string, fallback: string | null = null) => {
if (key === "furni_data_url") {
return path.join(tempDir, "cms", "FurnitureData.json");
}
if (key === "furni_data_mirror_path") return "";
return fallback;
},
);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it("writes metadata into the config directory served by production", async () => {
const data = { roomitemtypes: { furnitype: [{ id: 123 }] } };
await writeFurniData(data);
const livePath = path.join(
tempDir,
"Gamedata",
"config",
"FurnitureData.json",
);
await expect(fs.readFile(livePath, "utf-8")).resolves.toBe(
JSON.stringify(data),
);
});
});
+17 -3
View File
@@ -1,6 +1,9 @@
import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { getNitroFilesRoot } from "@/lib/services/furni-asset-dirs";
import {
getGamedataRoot,
getNitroFilesRoot,
} from "@/lib/services/furni-asset-dirs";
import type { HabboItFurniEntry } from "@/lib/services/habbo-furnidata-cache";
import { siteSettings } from "@/lib/services/site-settings";
import type { FurniMetadata } from "@/lib/services/swf-to-nitro";
@@ -31,7 +34,10 @@ async function getFurnitureDataWritePaths(): Promise<string[]> {
const primary = await getFurnitureDataPath();
const configuredMirror =
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
const nitroFilesRoot = await getNitroFilesRoot();
const [nitroFilesRoot, gamedataRoot] = await Promise.all([
getNitroFilesRoot(),
getGamedataRoot(),
]);
const mirror = configuredMirror.trim()
? configuredMirror
: nitroFilesRoot
@@ -41,7 +47,15 @@ async function getFurnitureDataWritePaths(): Promise<string[]> {
)
: "";
const paths = [primary, mirror].filter(Boolean).map((p) => path.normalize(p));
const gamedataPath = gamedataRoot
? path.join(
/*turbopackIgnore: true*/ gamedataRoot,
"config/FurnitureData.json",
)
: "";
const paths = [primary, mirror, gamedataPath]
.filter(Boolean)
.map((p) => path.normalize(p));
const seen = new Set<string>();
return paths.filter((p) => {
const key = process.platform === "win32" ? p.toLowerCase() : p;