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**
|
- [ ] **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.
|
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**
|
- [ ] **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.
|
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**
|
- [ ] **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.
|
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**
|
- [ ] **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.
|
Expected: PASS.
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ Expected: PASS.
|
|||||||
Run:
|
Run:
|
||||||
|
|
||||||
```text
|
```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 typecheck
|
||||||
pnpm test
|
pnpm test
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
type: "text",
|
type: "text",
|
||||||
placeholder: "E:\\path\\to\\Nitro-Files",
|
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",
|
"DEV",
|
||||||
"Nitro-Files",
|
"Nitro-Files",
|
||||||
);
|
);
|
||||||
|
export const DEFAULT_GAMEDATA_ROOT =
|
||||||
|
process.platform === "win32" ? "" : "/var/www/Gamedata";
|
||||||
export const DEFAULT_FURNI_ICON_DIR = path.join(
|
export const DEFAULT_FURNI_ICON_DIR = path.join(
|
||||||
/*turbopackIgnore: true*/ process.cwd(),
|
/*turbopackIgnore: true*/ process.cwd(),
|
||||||
"public/swf/dcr/hof_furni/icons",
|
"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 {
|
function dirsFromNitroFilesRoot(root: string): FurniAssetDirs {
|
||||||
return {
|
return {
|
||||||
swfDir: path.join(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"),
|
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> {
|
export async function getFurniAssetDirs(): Promise<FurniAssetDirs> {
|
||||||
const [swfDir, iconDir, nitroDir] = await Promise.all([
|
const [swfDir, iconDir, nitroDir] = await Promise.all([
|
||||||
siteSettings
|
siteSettings
|
||||||
@@ -99,10 +122,16 @@ export async function getFurniAssetDirs(): Promise<FurniAssetDirs> {
|
|||||||
|
|
||||||
export async function getFurniAssetWriteTargets(): Promise<FurniAssetWriteTargets> {
|
export async function getFurniAssetWriteTargets(): Promise<FurniAssetWriteTargets> {
|
||||||
const primary = await getFurniAssetDirs();
|
const primary = await getFurniAssetDirs();
|
||||||
const nitroFilesRoot = await getNitroFilesRoot();
|
const [nitroFilesRoot, gamedataRoot] = await Promise.all([
|
||||||
const mirrorCandidates = nitroFilesRoot
|
getNitroFilesRoot(),
|
||||||
? [dirsFromNitroFilesRoot(nitroFilesRoot)]
|
getGamedataRoot(),
|
||||||
: [];
|
]);
|
||||||
|
const mirrorCandidates = [
|
||||||
|
...(nitroFilesRoot ? [dirsFromNitroFilesRoot(nitroFilesRoot)] : []),
|
||||||
|
...(gamedataRoot
|
||||||
|
? [dirsFromGamedataRoot(gamedataRoot, primary.swfDir)]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
const primaryKey = {
|
const primaryKey = {
|
||||||
swfDir:
|
swfDir:
|
||||||
@@ -119,18 +148,26 @@ export async function getFurniAssetWriteTargets(): Promise<FurniAssetWriteTarget
|
|||||||
: primary.nitroDir,
|
: primary.nitroDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const seenCandidates = new Set<string>();
|
||||||
const mirrorDirs = mirrorCandidates.filter((dirs) => {
|
const mirrorDirs = mirrorCandidates.filter((dirs) => {
|
||||||
const swfDir = normalizeDir(dirs.swfDir, dirs.swfDir);
|
const swfDir = normalizeDir(dirs.swfDir, dirs.swfDir);
|
||||||
const iconDir = normalizeDir(dirs.iconDir, dirs.iconDir);
|
const iconDir = normalizeDir(dirs.iconDir, dirs.iconDir);
|
||||||
const nitroDir = normalizeDir(dirs.nitroDir, dirs.nitroDir);
|
const nitroDir = normalizeDir(dirs.nitroDir, dirs.nitroDir);
|
||||||
return (
|
const isDifferent =
|
||||||
(process.platform === "win32" ? swfDir.toLowerCase() : swfDir) !==
|
(process.platform === "win32" ? swfDir.toLowerCase() : swfDir) !==
|
||||||
primaryKey.swfDir ||
|
primaryKey.swfDir ||
|
||||||
(process.platform === "win32" ? iconDir.toLowerCase() : iconDir) !==
|
(process.platform === "win32" ? iconDir.toLowerCase() : iconDir) !==
|
||||||
primaryKey.iconDir ||
|
primaryKey.iconDir ||
|
||||||
(process.platform === "win32" ? nitroDir.toLowerCase() : nitroDir) !==
|
(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 {
|
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 { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
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 type { HabboItFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
import type { FurniMetadata } from "@/lib/services/swf-to-nitro";
|
import type { FurniMetadata } from "@/lib/services/swf-to-nitro";
|
||||||
@@ -31,7 +34,10 @@ async function getFurnitureDataWritePaths(): Promise<string[]> {
|
|||||||
const primary = await getFurnitureDataPath();
|
const primary = await getFurnitureDataPath();
|
||||||
const configuredMirror =
|
const configuredMirror =
|
||||||
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
|
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
|
||||||
const nitroFilesRoot = await getNitroFilesRoot();
|
const [nitroFilesRoot, gamedataRoot] = await Promise.all([
|
||||||
|
getNitroFilesRoot(),
|
||||||
|
getGamedataRoot(),
|
||||||
|
]);
|
||||||
const mirror = configuredMirror.trim()
|
const mirror = configuredMirror.trim()
|
||||||
? configuredMirror
|
? configuredMirror
|
||||||
: nitroFilesRoot
|
: 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>();
|
const seen = new Set<string>();
|
||||||
return paths.filter((p) => {
|
return paths.filter((p) => {
|
||||||
const key = process.platform === "win32" ? p.toLowerCase() : p;
|
const key = process.platform === "win32" ? p.toLowerCase() : p;
|
||||||
|
|||||||
Reference in new issue
Block a user