fix: map furni imports to production gamedata
This commit is contained in:
1 parent
a81af2d71a
commit
01570874a8
6 files changed
+224
-15
No files matched your search
@@ -47,7 +47,7 @@ Also assert that an absent unconfigured root adds no Gamedata mirror and that a
|
||||
|
||||
- [ ] **Step 2: Run the resolver test and verify RED**
|
||||
|
||||
Run: `pnpm test src/lib/services/furni-asset-dirs.test.ts`
|
||||
Run: `pnpm exec vitest run --coverage=false src/lib/services/furni-asset-dirs.test.ts`
|
||||
|
||||
Expected: FAIL because `gamedata_root` is not read and the Gamedata mirror is absent.
|
||||
|
||||
@@ -61,7 +61,7 @@ Make `getFurnitureDataWritePaths()` include `<gamedata_root>/config/FurnitureDat
|
||||
|
||||
- [ ] **Step 5: Run resolver tests and verify GREEN**
|
||||
|
||||
Run: `pnpm test src/lib/services/furni-asset-dirs.test.ts`
|
||||
Run: `pnpm exec vitest run --coverage=false src/lib/services/furni-asset-dirs.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
@@ -92,7 +92,7 @@ await expect(fs.readFile(path.join(liveIconDir, "chair_icon.png"))).resolves.toE
|
||||
|
||||
- [ ] **Step 2: Run the upload test and verify RED**
|
||||
|
||||
Run: `pnpm test src/lib/services/upload-import.test.ts`
|
||||
Run: `pnpm exec vitest run --coverage=false src/lib/services/upload-import.test.ts`
|
||||
|
||||
Expected: FAIL because manual uploads currently write only to the primary CMS directories.
|
||||
|
||||
@@ -102,7 +102,7 @@ Resolve all write targets, create their directories through the existing `ensure
|
||||
|
||||
- [ ] **Step 4: Run the upload test and verify GREEN**
|
||||
|
||||
Run: `pnpm test src/lib/services/upload-import.test.ts`
|
||||
Run: `pnpm exec vitest run --coverage=false src/lib/services/upload-import.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
@@ -111,7 +111,7 @@ Expected: PASS.
|
||||
Run:
|
||||
|
||||
```text
|
||||
pnpm test src/lib/services/furni-asset-dirs.test.ts src/lib/services/upload-import.test.ts src/lib/services/furni-import.test.ts
|
||||
pnpm exec vitest run --coverage=false src/lib/services/furni-asset-dirs.test.ts src/lib/services/upload-import.test.ts src/lib/services/furni-import.test.ts
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
pnpm build
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user