Merge branch 'main' into codex/housekeeping-foundation
CI / check (pull_request) Successful in 28s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-26 19:50:26 +02:00
commit 2a36ba1956
68 files changed
+1059 -514

No files matched your search

@@ -0,0 +1,4 @@
-- Add terms/age consent columns expected by the users schema (register flow).
ALTER TABLE `users`
ADD COLUMN `terms_accepted` TINYINT(1) NOT NULL DEFAULT 0,
ADD COLUMN `age_verified` TINYINT(1) NOT NULL DEFAULT 0;
+33 -39
View File
File diff suppressed because it is too large. Load diff
+2 -3
View File
@@ -5,7 +5,7 @@
"engines": { "engines": {
"node": ">=26.7.0 <27" "node": ">=26.7.0 <27"
}, },
"packageManager": "[email protected]2.0", "packageManager": "[email protected]4.0",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@@ -34,7 +34,6 @@
"@dnd-kit/sortable": "10.0.0", "@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2", "@dnd-kit/utilities": "3.2.2",
"@hookform/resolvers": "5.9.1", "@hookform/resolvers": "5.9.1",
"@next/bundle-analyzer": "16.3.2",
"@tanstack/react-virtual": "3.14.10", "@tanstack/react-virtual": "3.14.10",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
@@ -49,7 +48,7 @@
"jszip": "3.10.1", "jszip": "3.10.1",
"lenis": "1.3.26", "lenis": "1.3.26",
"lucide-react": "1.33.0", "lucide-react": "1.33.0",
"lzma": "2.3.2", "lzma": "^2.3.2",
"motion": "13.1.1", "motion": "13.1.1",
"music-metadata": "11.15.0", "music-metadata": "11.15.0",
"mysql2": "3.23.4", "mysql2": "3.23.4",
+1 -142
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -97,7 +97,7 @@ async function main(): Promise<void> {
const dbByClass = new Map(baseRows.map((r) => [r.itemName, r.id])); const dbByClass = new Map(baseRows.map((r) => [r.itemName, r.id]));
const dbById = new Map(baseRows.map((r) => [r.id, r.itemName])); const dbById = new Map(baseRows.map((r) => [r.id, r.itemName]));
let fdWrongId = 0; let fdWrongId = 0;
let fdIdConflict = 0; // id belongs to a different classname in DB let _fdIdConflict = 0; // id belongs to a different classname in DB
let fdMissingInDb = 0; let fdMissingInDb = 0;
const fdExamplesWrong: string[] = []; const fdExamplesWrong: string[] = [];
for (const e of entries) { for (const e of entries) {
@@ -114,7 +114,7 @@ async function main(): Promise<void> {
); );
continue; continue;
} }
if (dbById.get(e.id) !== e.classname) fdIdConflict++; if (dbById.get(e.id) !== e.classname) _fdIdConflict++;
} }
console.log( console.log(
` furnidata entries whose id != items_base.id for same classname: ${fdWrongId}`, ` furnidata entries whose id != items_base.id for same classname: ${fdWrongId}`,
File diff suppressed because it is too large. Load diff
+55 -17
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+6 -4
View File
@@ -33,18 +33,20 @@ export default async function RegisterPage() {
.where(eq(User.online, "1")) .where(eq(User.online, "1"))
.then((rows) => rows[0]?.total ?? 0), .then((rows) => rows[0]?.total ?? 0),
).catch(() => 0), ).catch(() => 0),
cached("register_online_users", 10_000, () =>
db db
.select({ username: User.username, look: User.look }) .select({ username: User.username, look: User.look })
.from(User) .from(User)
.where(eq(User.online, "1")) .where(eq(User.online, "1"))
.limit(8) .limit(8),
.catch(() => []), ).catch(() => []),
cached("register_latest_users", 30_000, () =>
db db
.select({ username: User.username, look: User.look }) .select({ username: User.username, look: User.look })
.from(User) .from(User)
.orderBy(desc(User.accountCreated)) .orderBy(desc(User.accountCreated))
.limit(8) .limit(8),
.catch(() => []), ).catch(() => []),
]); ]);
return ( return (
+12 -3
View File
@@ -34,7 +34,13 @@ function ToolbarBtn({
} as const; } as const;
if (href) { if (href) {
return ( return (
<Link href={href} className={cls} style={style} title={title}> <Link
href={href}
prefetch={false}
className={cls}
style={style}
title={title}
>
{children} {children}
</Link> </Link>
); );
@@ -377,9 +383,12 @@ export function ClientView({
<iframe <iframe
id="nitro" id="nitro"
src={clientSrc} src={clientSrc}
className="absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden" className={`absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden ${
dragging ? "pointer-events-none" : "pointer-events-auto"
}`}
title={hotelName} title={hotelName}
allow="autoplay; gamepad" allow="autoplay; gamepad; fullscreen"
loading="eager"
/> />
</div> </div>
</> </>
+7 -3
View File
@@ -21,15 +21,19 @@ export default async function ClientPage() {
]); ]);
const ip = (await headers()).get("x-real-client-ip") ?? "0.0.0.0"; const ip = (await headers()).get("x-real-client-ip") ?? "0.0.0.0";
const ticket = await issueSsoTicket(userId, hotelName, ip);
const onlineCount = await cached("online_count", 10_000, async () => { // Ticket write and online count run in parallel — the client page should
// render as fast as possible since the player is waiting for the game.
const [ticket, onlineCount] = await Promise.all([
issueSsoTicket(userId, hotelName, ip),
cached("online_count", 10_000, async () => {
const [row] = await db const [row] = await db
.select({ total: count() }) .select({ total: count() })
.from(User) .from(User)
.where(eq(User.online, "1")); .where(eq(User.online, "1"));
return row?.total ?? 0; return row?.total ?? 0;
}).catch(() => 0); }).catch(() => 0),
]);
return ( return (
<ClientView <ClientView
+1 -1
View File
@@ -33,7 +33,7 @@ const pixelify = Pixelify_Sans({
// enforceSiteAccess, DB-backed site settings) to render the <html> element, // enforceSiteAccess, DB-backed site settings) to render the <html> element,
// which cannot be wrapped in <Suspense>. Child pages inherit this opt-out, // which cannot be wrapped in <Suspense>. Child pages inherit this opt-out,
// so leaf pages that only access cached/DB data stay instant. // so leaf pages that only access cached/DB data stay instant.
export const instant = false; // export const instant = false;
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000"; const appUrl = process.env.APP_URL ?? "http://localhost:3000";
File diff suppressed because it is too large. Load diff
+6 -2
View File
@@ -1,5 +1,6 @@
import { unlink } from "node:fs/promises"; import { unlink } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { getRuntimeResolve } from "@/lib/utils/runtime-path";
/** /**
* Best-effort local file delete for camera photos hosted under /public. * Best-effort local file delete for camera photos hosted under /public.
@@ -25,8 +26,11 @@ export async function tryRemoveLocalPhotoFile(url: string): Promise<boolean> {
if (!pathname.startsWith("/")) return false; if (!pathname.startsWith("/")) return false;
const rel = pathname.replace(/^\/+/, ""); const rel = pathname.replace(/^\/+/, "");
if (!rel || rel.includes("..")) return false; if (!rel || rel.includes("..")) return false;
const publicRoot = path.resolve(process.cwd(), "public"); const publicRoot = /*turbopackIgnore: true*/ getRuntimeResolve(
const full = path.resolve(publicRoot, rel); process.cwd(),
"public",
);
const full = getRuntimeResolve(publicRoot, rel);
if (!full.startsWith(publicRoot + path.sep) && full !== publicRoot) { if (!full.startsWith(publicRoot + path.sep) && full !== publicRoot) {
return false; return false;
} }
+3 -2
View File
@@ -1,13 +1,14 @@
import path from "node:path"; import path from "node:path";
import { getRuntimeResolve } from "@/lib/utils/runtime-path";
/** /**
* Persistent media root. Lives outside `public/` and outside Git so uploads * Persistent media root. Lives outside `public/` and outside Git so uploads
* survive rebuilds, redeploys and `git clean`. Served through /api/media. * survive rebuilds, redeploys and `git clean`. Served through /api/media.
*/ */
export const MEDIA_ROOT = path.resolve(process.cwd(), "storage", "media"); export const MEDIA_ROOT = getRuntimeResolve(process.cwd(), "storage", "media");
export function resolveMediaPath(...segments: string[]): string { export function resolveMediaPath(...segments: string[]): string {
const target = path.resolve(MEDIA_ROOT, ...segments); const target = getRuntimeResolve(MEDIA_ROOT, ...segments);
if (target !== MEDIA_ROOT && !target.startsWith(MEDIA_ROOT + path.sep)) { if (target !== MEDIA_ROOT && !target.startsWith(MEDIA_ROOT + path.sep)) {
throw new Error("Invalid media path"); throw new Error("Invalid media path");
} }
+7 -7
View File
@@ -1,23 +1,23 @@
import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { catalogueAssetDirectories } from "@/lib/services/catalog-assets"; import { catalogueAssetDirectories } from "@/lib/services/catalog-assets";
import { getRuntimePath } from "@/lib/utils/runtime-path";
describe("catalogue asset directories", () => { describe("catalogue asset directories", () => {
it("prefers the configured Gamedata catalogue and retains the bundled fallback", () => { it("prefers the configured Gamedata catalogue and retains the bundled fallback", () => {
const cwd = path.join("test", "cms"); const cwd = getRuntimePath("test", "cms");
const gamedataRoot = path.join("test", "Gamedata"); const gamedataRoot = getRuntimePath("test", "Gamedata");
expect(catalogueAssetDirectories(gamedataRoot, cwd)).toEqual([ expect(catalogueAssetDirectories(gamedataRoot, cwd)).toEqual([
path.join(gamedataRoot, "c_images", "catalogue"), getRuntimePath(gamedataRoot, "c_images", "catalogue"),
path.join(cwd, "public", "swf", "c_images", "catalogue"), getRuntimePath(cwd, "public", "swf", "c_images", "catalogue"),
]); ]);
}); });
it("uses only the bundled catalogue when no Gamedata root is configured", () => { it("uses only the bundled catalogue when no Gamedata root is configured", () => {
const cwd = path.join("test", "cms"); const cwd = getRuntimePath("test", "cms");
expect(catalogueAssetDirectories("", cwd)).toEqual([ expect(catalogueAssetDirectories("", cwd)).toEqual([
path.join(cwd, "public", "swf", "c_images", "catalogue"), getRuntimePath(cwd, "public", "swf", "c_images", "catalogue"),
]); ]);
}); });
}); });
+3 -3
View File
@@ -1,14 +1,14 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import path from "node:path"; import { getRuntimePath } from "@/lib/utils/runtime-path";
export function catalogueAssetDirectories( export function catalogueAssetDirectories(
gamedataRoot: string, gamedataRoot: string,
cwd = process.cwd(), cwd = process.cwd(),
): string[] { ): string[] {
const bundled = path.join(cwd, "public", "swf", "c_images", "catalogue"); const bundled = getRuntimePath(cwd, "public", "swf", "c_images", "catalogue");
if (!gamedataRoot.trim()) return [bundled]; if (!gamedataRoot.trim()) return [bundled];
return [path.join(gamedataRoot, "c_images", "catalogue"), bundled]; return [getRuntimePath(gamedataRoot, "c_images", "catalogue"), bundled];
} }
export async function readCatalogueAssetFiles( export async function readCatalogueAssetFiles(
+2 -2
View File
@@ -1,5 +1,4 @@
import { readdir } from "node:fs/promises"; import { readdir } from "node:fs/promises";
import path from "node:path";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { fetchSourceFurnidata } from "@/lib/services/clone-import"; import { fetchSourceFurnidata } from "@/lib/services/clone-import";
@@ -8,6 +7,7 @@ import {
getFurniAssetDirs, getFurniAssetDirs,
getGamedataRoot, getGamedataRoot,
} from "@/lib/services/furni-asset-dirs"; } from "@/lib/services/furni-asset-dirs";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import { import {
applyCatalogSql, applyCatalogSql,
generateCatalogSql, generateCatalogSql,
@@ -182,7 +182,7 @@ export async function runCatalogAudit(
try { try {
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) return [] as string[]; if (!gamedataRoot) return [] as string[];
return await readdir(path.join(gamedataRoot, "album1584")).catch( return await readdir(getRuntimePath(gamedataRoot, "album1584")).catch(
() => [] as string[], () => [] as string[],
); );
} catch { } catch {
+8 -6
View File
@@ -6,7 +6,6 @@
// FurnitureData.json is only ever read (a copy is made first). // FurnitureData.json is only ever read (a copy is made first).
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
@@ -23,6 +22,7 @@ import {
classifyCatalogItem, classifyCatalogItem,
NO_CATALOG_KINDS, NO_CATALOG_KINDS,
} from "@/lib/services/item-kind"; } from "@/lib/services/item-kind";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } =
vi.hoisted(() => ({ vi.hoisted(() => ({
@@ -59,9 +59,11 @@ let tmpRoot: string;
let gamedataDir: string; let gamedataDir: string;
beforeAll(async () => { beforeAll(async () => {
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "furni-repair-rehearsal-")); tmpRoot = await fs.mkdtemp(
gamedataDir = path.join(tmpRoot, "Gamedata"); getRuntimePath(os.tmpdir(), "furni-repair-rehearsal-"),
await fs.mkdir(path.join(gamedataDir, "config"), { recursive: true }); );
gamedataDir = getRuntimePath(tmpRoot, "Gamedata");
await fs.mkdir(getRuntimePath(gamedataDir, "config"), { recursive: true });
getGamedataRootMock.mockResolvedValue(gamedataDir); getGamedataRootMock.mockResolvedValue(gamedataDir);
getNitroFilesRootMock.mockResolvedValue(""); getNitroFilesRootMock.mockResolvedValue("");
settingGetMock.mockImplementation( settingGetMock.mockImplementation(
@@ -75,7 +77,7 @@ beforeAll(async () => {
try { try {
await fs.copyFile( await fs.copyFile(
REAL_FURNIDATA, REAL_FURNIDATA,
path.join(gamedataDir, "config", "FurnitureData.json"), getRuntimePath(gamedataDir, "config", "FurnitureData.json"),
); );
} catch { } catch {
// missing copy — repair still runs against an empty file // missing copy — repair still runs against an empty file
@@ -242,7 +244,7 @@ describeGuard("catalog studio live repair rehearsal (sandbox)", () => {
byName.set(r.item_name, (byName.get(r.item_name) ?? 0) + 1); byName.set(r.item_name, (byName.get(r.item_name) ?? 0) + 1);
const finalFd = JSON.parse( const finalFd = JSON.parse(
await fs.readFile( await fs.readFile(
path.join(gamedataDir, "config", "FurnitureData.json"), getRuntimePath(gamedataDir, "config", "FurnitureData.json"),
"utf8", "utf8",
), ),
) as Record<string, { furnitype: Array<Record<string, unknown>> }>; ) as Record<string, { furnitype: Array<Record<string, unknown>> }>;
+3 -3
View File
@@ -1,5 +1,4 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import path from "node:path";
import { eq, inArray, sql } from "drizzle-orm"; import { eq, inArray, sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db"; import { db, ItemsBase } from "@/lib/db";
import { autoDetectInteraction } from "@/lib/furni/auto-interaction"; import { autoDetectInteraction } from "@/lib/furni/auto-interaction";
@@ -25,6 +24,7 @@ import {
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { getRuntimePath } from "@/lib/utils/runtime-path";
export interface SourceFurni { export interface SourceFurni {
id: number; id: number;
@@ -287,11 +287,11 @@ export async function cloneSingleFurni(params: {
// Download .nitro + icon directly from the source hotel. // Download .nitro + icon directly from the source hotel.
onProgress?.("downloading"); onProgress?.("downloading");
const nitroPath = path.join( const nitroPath = getRuntimePath(
/*turbopackIgnore: true*/ nitroDir, /*turbopackIgnore: true*/ nitroDir,
`${classname}.nitro`, `${classname}.nitro`,
); );
const iconPath = path.join( const iconPath = getRuntimePath(
/*turbopackIgnore: true*/ iconDir, /*turbopackIgnore: true*/ iconDir,
`${classname}_icon.png`, `${classname}_icon.png`,
); );
+10 -7
View File
@@ -1,5 +1,4 @@
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { import {
listOfficialEffects, listOfficialEffects,
resolveEffectSwfUrl, resolveEffectSwfUrl,
@@ -13,17 +12,18 @@ import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import type { import type {
EffectListItem, EffectListItem,
EffectMapEntry, EffectMapEntry,
ImportEffectResult, ImportEffectResult,
} from "@/types/effects"; } from "@/types/effects";
const EFFECT_NITRO_DIR_FALLBACK = path.join( const EFFECT_NITRO_DIR_FALLBACK = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/effect", "public/nitro-assets/bundled/effect",
); );
const EFFECT_SWF_DIR_FALLBACK = path.join( const EFFECT_SWF_DIR_FALLBACK = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/swf/gordon", "public/swf/gordon",
); );
@@ -35,7 +35,10 @@ async function getEffectNitroDir(): Promise<string> {
if (configured) return configured; if (configured) return configured;
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) if (gamedataRoot)
return path.join(/*turbopackIgnore: true*/ gamedataRoot, "bundled/effect"); return getRuntimePath(
/*turbopackIgnore: true*/ gamedataRoot,
"bundled/effect",
);
return EFFECT_NITRO_DIR_FALLBACK; return EFFECT_NITRO_DIR_FALLBACK;
} }
@@ -61,7 +64,7 @@ export async function ensureEffectDirs(): Promise<void> {
} }
async function nitroPathFor(lib: string): Promise<string> { async function nitroPathFor(lib: string): Promise<string> {
return path.join( return getRuntimePath(
/*turbopackIgnore: true*/ await getEffectNitroDir(), /*turbopackIgnore: true*/ await getEffectNitroDir(),
`${lib}.nitro`, `${lib}.nitro`,
); );
@@ -82,7 +85,7 @@ export async function importSingleEffect(params: {
// Download SWF // Download SWF
onProgress?.("downloading"); onProgress?.("downloading");
const swfPath = path.join( const swfPath = getRuntimePath(
/*turbopackIgnore: true*/ await getEffectSwfDir(), /*turbopackIgnore: true*/ await getEffectSwfDir(),
`${lib}.swf`, `${lib}.swf`,
); );
@@ -167,7 +170,7 @@ export async function getEffectList(search: string): Promise<EffectListItem[]> {
...e, ...e,
present: localByLib.has(e.lib), present: localByLib.has(e.lib),
nitroExists: existsSync( nitroExists: existsSync(
/*turbopackIgnore: true*/ path.join(nitroDir, `${e.lib}.nitro`), /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${e.lib}.nitro`),
), ),
})); }));
return term return term
+3 -3
View File
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import type { EffectMapEntry } from "@/types/effects"; import type { EffectMapEntry } from "@/types/effects";
import { import {
__setEffectMapPathForTests, __setEffectMapPathForTests,
@@ -26,8 +26,8 @@ const ENTRY: EffectMapEntry = {
}; };
beforeEach(async () => { beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), "effectmap-")); dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "effectmap-"));
file = path.join(dir, "EffectMap.json"); file = getRuntimePath(dir, "EffectMap.json");
await fs.writeFile(file, JSON.stringify({ effects: [ENTRY] })); await fs.writeFile(file, JSON.stringify({ effects: [ENTRY] }));
__setEffectMapPathForTests(file); __setEffectMapPathForTests(file);
}); });
+13 -8
View File
@@ -1,11 +1,11 @@
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { resolveFigureSwfUrl } from "@/lib/services/figure-source"; import { resolveFigureSwfUrl } from "@/lib/services/figure-source";
import { listFigureLibraries } from "@/lib/services/figuremap"; import { listFigureLibraries } from "@/lib/services/figuremap";
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
import { getRuntimePath } from "@/lib/utils/runtime-path";
export interface FigureListItem { export interface FigureListItem {
lib: string; lib: string;
@@ -24,11 +24,11 @@ export interface ImportFigureResult {
error?: string; error?: string;
} }
const FIGURE_NITRO_DIR_FALLBACK = path.join( const FIGURE_NITRO_DIR_FALLBACK = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/figure", "public/nitro-assets/bundled/figure",
); );
const FIGURE_SWF_DIR_FALLBACK = path.join( const FIGURE_SWF_DIR_FALLBACK = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/swf/gordon", "public/swf/gordon",
); );
@@ -40,7 +40,10 @@ async function getFigureNitroDir(): Promise<string> {
if (configured) return configured; if (configured) return configured;
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) if (gamedataRoot)
return path.join(/*turbopackIgnore: true*/ gamedataRoot, "bundled/figure"); return getRuntimePath(
/*turbopackIgnore: true*/ gamedataRoot,
"bundled/figure",
);
return FIGURE_NITRO_DIR_FALLBACK; return FIGURE_NITRO_DIR_FALLBACK;
} }
@@ -66,7 +69,7 @@ export async function ensureFigureDirs(): Promise<void> {
} }
async function nitroPathFor(lib: string): Promise<string> { async function nitroPathFor(lib: string): Promise<string> {
return path.join( return getRuntimePath(
/*turbopackIgnore: true*/ await getFigureNitroDir(), /*turbopackIgnore: true*/ await getFigureNitroDir(),
`${lib}.nitro`, `${lib}.nitro`,
); );
@@ -81,7 +84,7 @@ export async function importSingleFigure(params: {
await ensureFigureDirs(); await ensureFigureDirs();
onProgress?.("downloading"); onProgress?.("downloading");
const swfPath = path.join( const swfPath = getRuntimePath(
/*turbopackIgnore: true*/ await getFigureSwfDir(), /*turbopackIgnore: true*/ await getFigureSwfDir(),
`${lib}.swf`, `${lib}.swf`,
); );
@@ -158,7 +161,7 @@ export async function getFigureList(params: {
lib: l.id, lib: l.id,
revision: l.revision, revision: l.revision,
nitroExists: existsSync( nitroExists: existsSync(
/*turbopackIgnore: true*/ path.join(nitroDir, `${l.id}.nitro`), /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${l.id}.nitro`),
), ),
previewType: l.parts?.[0]?.type, previewType: l.parts?.[0]?.type,
previewId: l.parts?.[0]?.id, previewId: l.parts?.[0]?.id,
@@ -176,7 +179,9 @@ export async function getFigureStats(): Promise<{
getFigureNitroDir(), getFigureNitroDir(),
]); ]);
const imported = libs.filter((l) => const imported = libs.filter((l) =>
existsSync(/*turbopackIgnore: true*/ path.join(nitroDir, `${l.id}.nitro`)), existsSync(
/*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${l.id}.nitro`),
),
).length; ).length;
return { total: libs.length, imported, missing: libs.length - imported }; return { total: libs.length, imported, missing: libs.length - imported };
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import type { FigureLibrary } from "./figuremap"; import type { FigureLibrary } from "./figuremap";
import { import {
__setFigureMapPathForTests, __setFigureMapPathForTests,
@@ -23,8 +23,8 @@ const LIB: FigureLibrary = {
}; };
beforeEach(async () => { beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), "figuremap-")); dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "figuremap-"));
file = path.join(dir, "FigureMap.json"); file = getRuntimePath(dir, "FigureMap.json");
await fs.writeFile(file, JSON.stringify({ libraries: [LIB] })); await fs.writeFile(file, JSON.stringify({ libraries: [LIB] }));
__setFigureMapPathForTests(file); __setFigureMapPathForTests(file);
}); });
+10 -10
View File
@@ -1,6 +1,6 @@
// @vitest-environment node // @vitest-environment node
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const { existsSyncMock, settingGetMock } = vi.hoisted(() => ({ const { existsSyncMock, settingGetMock } = vi.hoisted(() => ({
existsSyncMock: vi.fn(), existsSyncMock: vi.fn(),
@@ -19,9 +19,9 @@ vi.mock("@/lib/services/site-settings", () => ({
import { getFurniAssetWriteTargets } from "./furni-asset-dirs"; import { getFurniAssetWriteTargets } from "./furni-asset-dirs";
const primary = { const primary = {
swfDir: path.join("test-assets", "primary", "swf"), swfDir: getRuntimePath("test-assets", "primary", "swf"),
iconDir: path.join("test-assets", "primary", "icons"), iconDir: getRuntimePath("test-assets", "primary", "icons"),
nitroDir: path.join("test-assets", "primary", "furniture"), nitroDir: getRuntimePath("test-assets", "primary", "furniture"),
}; };
function configureSettings(values: Record<string, string>): void { function configureSettings(values: Record<string, string>): void {
@@ -38,7 +38,7 @@ describe("getFurniAssetWriteTargets", () => {
}); });
it("maps a configured production Gamedata root to the directories served by Nitro", async () => { it("maps a configured production Gamedata root to the directories served by Nitro", async () => {
const gamedataRoot = path.join("test-assets", "Gamedata"); const gamedataRoot = getRuntimePath("test-assets", "Gamedata");
configureSettings({ configureSettings({
furni_swf_dir: primary.swfDir, furni_swf_dir: primary.swfDir,
furni_icon_dir: primary.iconDir, furni_icon_dir: primary.iconDir,
@@ -51,8 +51,8 @@ describe("getFurniAssetWriteTargets", () => {
expect(targets.mirrorDirs).toContainEqual({ expect(targets.mirrorDirs).toContainEqual({
swfDir: primary.swfDir, swfDir: primary.swfDir,
iconDir: path.join(gamedataRoot, "icons"), iconDir: getRuntimePath(gamedataRoot, "icons"),
nitroDir: path.join(gamedataRoot, "bundled", "furniture"), nitroDir: getRuntimePath(gamedataRoot, "bundled", "furniture"),
}); });
}); });
@@ -71,11 +71,11 @@ describe("getFurniAssetWriteTargets", () => {
}); });
it("does not duplicate a Gamedata layout already used as the primary target", async () => { it("does not duplicate a Gamedata layout already used as the primary target", async () => {
const gamedataRoot = path.join("test-assets", "Gamedata"); const gamedataRoot = getRuntimePath("test-assets", "Gamedata");
configureSettings({ configureSettings({
furni_swf_dir: primary.swfDir, furni_swf_dir: primary.swfDir,
furni_icon_dir: path.join(gamedataRoot, "icons"), furni_icon_dir: getRuntimePath(gamedataRoot, "icons"),
furni_nitro_dir: path.join(gamedataRoot, "bundled", "furniture"), furni_nitro_dir: getRuntimePath(gamedataRoot, "bundled", "furniture"),
nitro_files_root: "", nitro_files_root: "",
gamedata_root: gamedataRoot, gamedata_root: gamedataRoot,
}); });
+13 -9
View File
@@ -1,23 +1,24 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { getRuntimePath } from "@/lib/utils/runtime-path";
export const DEFAULT_NITRO_FILES_ROOT = path.join( export const DEFAULT_NITRO_FILES_ROOT = getRuntimePath(
/*turbopackIgnore: true*/ path.dirname(process.cwd()), /*turbopackIgnore: true*/ path.dirname(process.cwd()),
"DEV", "DEV",
"Nitro-Files", "Nitro-Files",
); );
export const DEFAULT_GAMEDATA_ROOT = export const DEFAULT_GAMEDATA_ROOT =
process.platform === "win32" ? "" : "/var/www/Gamedata"; process.platform === "win32" ? "" : "/var/www/Gamedata";
export const DEFAULT_FURNI_ICON_DIR = path.join( export const DEFAULT_FURNI_ICON_DIR = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/swf/dcr/hof_furni/icons", "public/swf/dcr/hof_furni/icons",
); );
export const DEFAULT_FURNI_SWF_DIR = path.join( export const DEFAULT_FURNI_SWF_DIR = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/swf/dcr/hof_furni/swf", "public/swf/dcr/hof_furni/swf",
); );
export const DEFAULT_FURNI_NITRO_DIR = path.join( export const DEFAULT_FURNI_NITRO_DIR = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/furniture", "public/nitro-assets/bundled/furniture",
); );
@@ -77,12 +78,12 @@ export async function getGamedataRoot(): Promise<string> {
function dirsFromNitroFilesRoot(root: string): FurniAssetDirs { function dirsFromNitroFilesRoot(root: string): FurniAssetDirs {
return { return {
swfDir: path.join(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"), swfDir: getRuntimePath(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"),
iconDir: path.join( iconDir: getRuntimePath(
/*turbopackIgnore: true*/ root, /*turbopackIgnore: true*/ root,
"swf/dcr/hof_furni/icons", "swf/dcr/hof_furni/icons",
), ),
nitroDir: path.join( nitroDir: getRuntimePath(
/*turbopackIgnore: true*/ root, /*turbopackIgnore: true*/ root,
"nitro-assets/bundled/furniture", "nitro-assets/bundled/furniture",
), ),
@@ -95,8 +96,11 @@ function dirsFromGamedataRoot(
): FurniAssetDirs { ): FurniAssetDirs {
return { return {
swfDir: primarySwfDir, swfDir: primarySwfDir,
iconDir: path.join(/*turbopackIgnore: true*/ root, "icons"), iconDir: getRuntimePath(/*turbopackIgnore: true*/ root, "icons"),
nitroDir: path.join(/*turbopackIgnore: true*/ root, "bundled/furniture"), nitroDir: getRuntimePath(
/*turbopackIgnore: true*/ root,
"bundled/furniture",
),
}; };
} }
+7 -6
View File
@@ -1,3 +1,5 @@
import path from "node:path";
import { getRuntimePath } from "@/lib/utils/runtime-path";
/** /**
* Localized furnidata builder — generates FurnitureData_<lang>.json files * Localized furnidata builder — generates FurnitureData_<lang>.json files
* (nl/en/de/fr/es/tr/it/pt/fi/pl/ru/ar/ja) from the master FurnitureData.json * (nl/en/de/fr/es/tr/it/pt/fi/pl/ru/ar/ja) from the master FurnitureData.json
@@ -10,7 +12,6 @@
*/ */
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import type { HabboGamedataHotel } from "@/lib/habbo-gamedata-hotel"; import type { HabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
import { habboFurnidataUrl } from "@/lib/habbo-gamedata-hotel"; import { habboFurnidataUrl } from "@/lib/habbo-gamedata-hotel";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
@@ -105,8 +106,8 @@ async function loadLibreCache(): Promise<Map<string, string>> {
try { try {
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
const cachePath = gamedataRoot const cachePath = gamedataRoot
? path.join(gamedataRoot, "config", LIBRE_CACHE_FILE) ? getRuntimePath(gamedataRoot, "config", LIBRE_CACHE_FILE)
: path.join(process.cwd(), LIBRE_CACHE_FILE); : getRuntimePath(process.cwd(), LIBRE_CACHE_FILE);
if (existsSync(cachePath)) { if (existsSync(cachePath)) {
const raw = await fs.readFile(cachePath, "utf-8"); const raw = await fs.readFile(cachePath, "utf-8");
const obj = JSON.parse(raw) as LibreCache; const obj = JSON.parse(raw) as LibreCache;
@@ -126,8 +127,8 @@ async function saveLibreCache(): Promise<void> {
try { try {
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
const cachePath = gamedataRoot const cachePath = gamedataRoot
? path.join(gamedataRoot, "config", LIBRE_CACHE_FILE) ? getRuntimePath(gamedataRoot, "config", LIBRE_CACHE_FILE)
: path.join(process.cwd(), LIBRE_CACHE_FILE); : getRuntimePath(process.cwd(), LIBRE_CACHE_FILE);
const obj: LibreCache = Object.fromEntries(libreMemoryCache.entries()); const obj: LibreCache = Object.fromEntries(libreMemoryCache.entries());
await fs.mkdir(path.dirname(cachePath), { recursive: true }); await fs.mkdir(path.dirname(cachePath), { recursive: true });
await fs.writeFile(cachePath, JSON.stringify(obj), "utf-8"); await fs.writeFile(cachePath, JSON.stringify(obj), "utf-8");
@@ -363,7 +364,7 @@ async function getLocalizedWritePaths(lang: string): Promise<string[]> {
for (const base of bases) { for (const base of bases) {
const dir = path.dirname(base); const dir = path.dirname(base);
const langFile = `FurnitureData_${lang}.json`; const langFile = `FurnitureData_${lang}.json`;
out.push(path.join(dir, langFile)); out.push(getRuntimePath(dir, langFile));
} }
// Deduplicate same normalized path. // Deduplicate same normalized path.
const seen = new Set<string>(); const seen = new Set<string>();
+8 -6
View File
@@ -1,8 +1,8 @@
// @vitest-environment node // @vitest-environment node
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } =
vi.hoisted(() => ({ vi.hoisted(() => ({
@@ -26,13 +26,15 @@ describe("writeFurniData production mirror", () => {
let tempDir: string; let tempDir: string;
beforeEach(async () => { beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "furni-data-paths-")); tempDir = await fs.mkdtemp(
getRuntimePath(os.tmpdir(), "furni-data-paths-"),
);
getNitroFilesRootMock.mockResolvedValue(""); getNitroFilesRootMock.mockResolvedValue("");
getGamedataRootMock.mockResolvedValue(path.join(tempDir, "Gamedata")); getGamedataRootMock.mockResolvedValue(getRuntimePath(tempDir, "Gamedata"));
settingGetMock.mockImplementation( settingGetMock.mockImplementation(
async (key: string, fallback: string | null = null) => { async (key: string, fallback: string | null = null) => {
if (key === "furni_data_url") { if (key === "furni_data_url") {
return path.join(tempDir, "cms", "FurnitureData.json"); return getRuntimePath(tempDir, "cms", "FurnitureData.json");
} }
if (key === "furni_data_mirror_path") return ""; if (key === "furni_data_mirror_path") return "";
return fallback; return fallback;
@@ -50,7 +52,7 @@ describe("writeFurniData production mirror", () => {
await writeFurniData(data); await writeFurniData(data);
const livePath = path.join( const livePath = getRuntimePath(
tempDir, tempDir,
"Gamedata", "Gamedata",
"config", "config",
@@ -67,7 +69,7 @@ describe("writeFurniData production mirror", () => {
); );
await expect(getFurnitureDataPath()).resolves.toBe( await expect(getFurnitureDataPath()).resolves.toBe(
path.join(tempDir, "Gamedata", "config", "FurnitureData.json"), getRuntimePath(tempDir, "Gamedata", "config", "FurnitureData.json"),
); );
}); });
}); });
+9 -4
View File
@@ -9,6 +9,7 @@ import {
import type { OfficialHabboFurniEntry } from "@/lib/services/habbo-furnidata-cache"; import type { OfficialHabboFurniEntry } 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";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const LOCK_STALE_MS = 60_000; const LOCK_STALE_MS = 60_000;
const DEFAULT_FURNI_DATA_URL = "/gamedata/config/FurnitureData.json"; const DEFAULT_FURNI_DATA_URL = "/gamedata/config/FurnitureData.json";
@@ -27,14 +28,18 @@ export async function getFurnitureDataPath(): Promise<string> {
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) { if (gamedataRoot) {
const relative = url.replace(/^\/gamedata\/?/, ""); const relative = url.replace(/^\/gamedata\/?/, "");
return path.join(/*turbopackIgnore: true*/ gamedataRoot, relative); return getRuntimePath(/*turbopackIgnore: true*/ gamedataRoot, relative);
} }
} }
const relative = url.replace(/^\/+/, ""); const relative = url.replace(/^\/+/, "");
// The `relative` segment is admin-configurable so its concrete value is // The `relative` segment is admin-configurable so its concrete value is
// unknown at build time; without the hint Turbopack traces the entire // unknown at build time; without the hint Turbopack traces the entire
// public/ tree (~400k files) into the route's NFT. // public/ tree (~400k files) into the route's NFT.
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative); return getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(),
"public",
relative,
);
} }
export async function getFurnitureDataWritePaths(): Promise<string[]> { export async function getFurnitureDataWritePaths(): Promise<string[]> {
@@ -48,14 +53,14 @@ export async function getFurnitureDataWritePaths(): Promise<string[]> {
const mirror = configuredMirror.trim() const mirror = configuredMirror.trim()
? configuredMirror ? configuredMirror
: nitroFilesRoot : nitroFilesRoot
? path.join( ? getRuntimePath(
/*turbopackIgnore: true*/ nitroFilesRoot, /*turbopackIgnore: true*/ nitroFilesRoot,
"nitro-assets/gamedata/FurnitureData.json", "nitro-assets/gamedata/FurnitureData.json",
) )
: ""; : "";
const gamedataPath = gamedataRoot const gamedataPath = gamedataRoot
? path.join( ? getRuntimePath(
/*turbopackIgnore: true*/ gamedataRoot, /*turbopackIgnore: true*/ gamedataRoot,
"config/FurnitureData.json", "config/FurnitureData.json",
) )
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -2,13 +2,14 @@ import { promises as fs } from "node:fs";
import path from "node:path"; import path from "node:path";
import { db, WebsiteBadges } from "@/lib/db"; import { db, WebsiteBadges } from "@/lib/db";
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
import { getRuntimePath } from "@/lib/utils/runtime-path";
export type ImportBadgeResult = export type ImportBadgeResult =
| { ok: true } | { ok: true }
| { ok: false; error: string; status: number }; | { ok: false; error: string; status: number };
function externalTextsPath(gamedataRoot: string): string { function externalTextsPath(gamedataRoot: string): string {
return path.join(gamedataRoot, "config", "ExternalTexts.json"); return getRuntimePath(gamedataRoot, "config", "ExternalTexts.json");
} }
/** Write badge name/desc into ExternalTexts.json (409 if already present). */ /** Write badge name/desc into ExternalTexts.json (409 if already present). */
+5 -5
View File
@@ -7,8 +7,8 @@
// production FurnitureData.json, so production files are never touched. // production FurnitureData.json, so production files are never touched.
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } =
vi.hoisted(() => ({ vi.hoisted(() => ({
@@ -126,9 +126,9 @@ beforeAll(async () => {
// ignore cleanup failures; the assertions will surface real problems // ignore cleanup failures; the assertions will surface real problems
} }
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "furni-live-audit-")); tmpRoot = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "furni-live-audit-"));
gamedataDir = path.join(tmpRoot, "Gamedata"); gamedataDir = getRuntimePath(tmpRoot, "Gamedata");
await fs.mkdir(path.join(gamedataDir, "config"), { recursive: true }); await fs.mkdir(getRuntimePath(gamedataDir, "config"), { recursive: true });
getGamedataRootMock.mockResolvedValue(gamedataDir); getGamedataRootMock.mockResolvedValue(gamedataDir);
getNitroFilesRootMock.mockResolvedValue(""); getNitroFilesRootMock.mockResolvedValue("");
settingGetMock.mockImplementation( settingGetMock.mockImplementation(
@@ -146,7 +146,7 @@ beforeAll(async () => {
try { try {
await fs.copyFile( await fs.copyFile(
productionFile, productionFile,
path.join(gamedataDir, "config", "FurnitureData.json"), getRuntimePath(gamedataDir, "config", "FurnitureData.json"),
); );
} catch { } catch {
// file unavailable — reconcile test still runs against an empty file // file unavailable — reconcile test still runs against an empty file
+7 -2
View File
@@ -1,6 +1,7 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
import { getRuntimePath } from "@/lib/utils/runtime-path";
// The CMS default gamedata JSON paths point under public/nitro-assets/gamedata/, // The CMS default gamedata JSON paths point under public/nitro-assets/gamedata/,
// but this deployment keeps the gamedata JSON (FigureMap/FigureData/EffectMap…) // but this deployment keeps the gamedata JSON (FigureMap/FigureData/EffectMap…)
@@ -11,7 +12,11 @@ import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
// `bundled/` dir, the location the Nitro client loads from.) // `bundled/` dir, the location the Nitro client loads from.)
function pub(...segs: string[]): string { function pub(...segs: string[]): string {
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", ...segs); return getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(),
"public",
...segs,
);
} }
/** /**
@@ -41,7 +46,7 @@ export async function resolveGamedataFile(
// Deployment gamedata root (e.g. /var/www/Gamedata/config/…). // Deployment gamedata root (e.g. /var/www/Gamedata/config/…).
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) { if (gamedataRoot) {
const rootConfig = path.join( const rootConfig = getRuntimePath(
/*turbopackIgnore: true*/ gamedataRoot, /*turbopackIgnore: true*/ gamedataRoot,
"config", "config",
fallbackName, fallbackName,
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import { import {
readGamedataJson, readGamedataJson,
withGamedataLock, withGamedataLock,
@@ -12,8 +12,8 @@ let dir: string;
let file: string; let file: string;
beforeEach(async () => { beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), "gamedata-")); dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "gamedata-"));
file = path.join(dir, "EffectMap.json"); file = getRuntimePath(dir, "EffectMap.json");
await fs.writeFile( await fs.writeFile(
file, file,
JSON.stringify({ effects: [{ id: "1", lib: "A" }] }), JSON.stringify({ effects: [{ id: "1", lib: "A" }] }),
+4 -4
View File
@@ -1,15 +1,15 @@
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { cropRgba, decodePng } from "@/lib/services/imager/png-decode"; import { cropRgba, decodePng } from "@/lib/services/imager/png-decode";
import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder"; import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { getRuntimePath } from "@/lib/utils/runtime-path";
// Pet .nitro can live in the CMS import target or the bundled asset set. // Pet .nitro can live in the CMS import target or the bundled asset set.
const PET_DIRS = [ const PET_DIRS = [
path.join( getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/pet", "public/nitro-assets/bundled/pet",
), ),
path.join( getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/Gamedata/bundled/pet", "public/Gamedata/bundled/pet",
), ),
@@ -54,7 +54,7 @@ const iconCache = new Map<string, Buffer | null>();
function findNitro(lib: string): string | null { function findNitro(lib: string): string | null {
for (const dir of PET_DIRS) { for (const dir of PET_DIRS) {
const p = path.join(/*turbopackIgnore: true*/ dir, `${lib}.nitro`); const p = getRuntimePath(/*turbopackIgnore: true*/ dir, `${lib}.nitro`);
if (existsSync(/*turbopackIgnore: true*/ p)) return p; if (existsSync(/*turbopackIgnore: true*/ p)) return p;
} }
return null; return null;
+8 -5
View File
@@ -1,8 +1,8 @@
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { CANONICAL_PETS, resolvePetSwfUrl } from "@/lib/services/pet-source"; import { CANONICAL_PETS, resolvePetSwfUrl } from "@/lib/services/pet-source";
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
import { getRuntimePath } from "@/lib/utils/runtime-path";
export interface PetListItem { export interface PetListItem {
lib: string; lib: string;
@@ -16,11 +16,11 @@ export interface ImportPetResult {
error?: string; error?: string;
} }
export const PET_NITRO_DIR = path.join( export const PET_NITRO_DIR = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/nitro-assets/bundled/pet", "public/nitro-assets/bundled/pet",
); );
export const PET_SWF_DIR = path.join( export const PET_SWF_DIR = getRuntimePath(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"public/swf/gordon", "public/swf/gordon",
); );
@@ -36,7 +36,10 @@ export async function ensurePetDirs(): Promise<void> {
} }
function nitroPathFor(lib: string): string { function nitroPathFor(lib: string): string {
return path.join(/*turbopackIgnore: true*/ PET_NITRO_DIR, `${lib}.nitro`); return getRuntimePath(
/*turbopackIgnore: true*/ PET_NITRO_DIR,
`${lib}.nitro`,
);
} }
export async function importSinglePet(params: { export async function importSinglePet(params: {
@@ -48,7 +51,7 @@ export async function importSinglePet(params: {
await ensurePetDirs(); await ensurePetDirs();
onProgress?.("downloading"); onProgress?.("downloading");
const swfPath = path.join( const swfPath = getRuntimePath(
/*turbopackIgnore: true*/ PET_SWF_DIR, /*turbopackIgnore: true*/ PET_SWF_DIR,
`${lib}.swf`, `${lib}.swf`,
); );
+19 -10
View File
@@ -1,6 +1,5 @@
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { extractFurniIconPng } from "@/lib/services/clone-icon"; import { extractFurniIconPng } from "@/lib/services/clone-icon";
@@ -16,6 +15,7 @@ import {
} from "@/lib/services/furni-asset-dirs"; } from "@/lib/services/furni-asset-dirs";
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind"; import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import { ensureDirectories } from "./furni-import"; import { ensureDirectories } from "./furni-import";
const REPAIR_CONCURRENCY = 6; const REPAIR_CONCURRENCY = 6;
@@ -61,7 +61,7 @@ async function writeIconToDirs(
for (const dir of dirs) { for (const dir of dirs) {
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, fileName), icon); await fs.writeFile(getRuntimePath(dir, fileName), icon);
} catch (err) { } catch (err) {
errors.push(`write icon to ${dir} failed: ${(err as Error).message}`); errors.push(`write icon to ${dir} failed: ${(err as Error).message}`);
} }
@@ -75,12 +75,12 @@ async function copyIconAcrossDirs(
fileName: string, fileName: string,
): Promise<string[]> { ): Promise<string[]> {
const errors: string[] = []; const errors: string[] = [];
const src = path.join(srcDir, fileName); const src = getRuntimePath(srcDir, fileName);
for (const dir of destDirs) { for (const dir of destDirs) {
if (dir === srcDir) continue; if (dir === srcDir) continue;
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await fs.copyFile(src, path.join(dir, fileName)); await fs.copyFile(src, getRuntimePath(dir, fileName));
} catch (err) { } catch (err) {
errors.push(`copy icon to ${dir} failed: ${(err as Error).message}`); errors.push(`copy icon to ${dir} failed: ${(err as Error).message}`);
} }
@@ -151,7 +151,7 @@ export async function repairMissingIcons(
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) { if (gamedataRoot) {
const badgeFiles = await fs const badgeFiles = await fs
.readdir(path.join(gamedataRoot, "album1584")) .readdir(getRuntimePath(gamedataRoot, "album1584"))
.catch(() => [] as string[]); .catch(() => [] as string[]);
const albumBadgeSet = new Set( const albumBadgeSet = new Set(
badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
@@ -214,7 +214,7 @@ export async function repairMissingIcons(
// Badge items store icons as badge_<code>_icon.png (the classname already // Badge items store icons as badge_<code>_icon.png (the classname already
// includes the `badge_` prefix, so fileName matches correctly). // includes the `badge_` prefix, so fileName matches correctly).
const presentDirs = iconDirs.filter((dir) => const presentDirs = iconDirs.filter((dir) =>
existsSync(path.join(dir, fileName)), existsSync(getRuntimePath(dir, fileName)),
); );
const missingDirs = iconDirs.filter((dir) => !presentDirs.includes(dir)); const missingDirs = iconDirs.filter((dir) => !presentDirs.includes(dir));
@@ -247,7 +247,7 @@ export async function repairMissingIcons(
if (!ok) { if (!ok) {
let foundNitro = false; let foundNitro = false;
for (const dir of nitroDirs) { for (const dir of nitroDirs) {
const nitroPath = path.join(dir, `${classname}.nitro`); const nitroPath = getRuntimePath(dir, `${classname}.nitro`);
if (!existsSync(nitroPath)) continue; if (!existsSync(nitroPath)) continue;
foundNitro = true; foundNitro = true;
try { try {
@@ -308,12 +308,18 @@ export async function repairMissingIcons(
try { try {
const dl = await downloadFile( const dl = await downloadFile(
`${source.iconBaseUrl}/${iconName}`, `${source.iconBaseUrl}/${iconName}`,
path.join(targets.iconDir, fileName), /*turbopackIgnore: true*/ getRuntimePath(
targets.iconDir,
fileName,
),
{ maxRetries: 1, validate: "png" }, { maxRetries: 1, validate: "png" },
); );
if (dl.ok) { if (dl.ok) {
const icon = await fs.readFile( const icon = await fs.readFile(
path.join(targets.iconDir, fileName), /*turbopackIgnore: true*/ getRuntimePath(
targets.iconDir,
fileName,
),
); );
const writeErrors = await writeIconToDirs( const writeErrors = await writeIconToDirs(
icon, icon,
@@ -336,7 +342,10 @@ export async function repairMissingIcons(
// Some sources only ship icons embedded in their `.nitro` bundles — // Some sources only ship icons embedded in their `.nitro` bundles —
// grab the bundle and extract the catalog icon from it. // grab the bundle and extract the catalog icon from it.
if (source.nitroBaseUrl) { if (source.nitroBaseUrl) {
const nitroTmp = path.join(os.tmpdir(), `.repair_${classname}.nitro`); const nitroTmp = getRuntimePath(
os.tmpdir(),
`.repair_${classname}.nitro`,
);
try { try {
const dl = await downloadFile( const dl = await downloadFile(
`${source.nitroBaseUrl}/${classname}.nitro`, `${source.nitroBaseUrl}/${classname}.nitro`,
+10 -10
View File
@@ -1,6 +1,5 @@
import { existsSync, promises as fs } from "node:fs"; import { existsSync, promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { import {
@@ -16,6 +15,7 @@ import {
import { downloadFile } from "@/lib/services/import/core/download"; import { downloadFile } from "@/lib/services/import/core/download";
import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind"; import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import { ensureDirectories } from "./furni-import"; import { ensureDirectories } from "./furni-import";
import { convertSwfToNitro } from "./swf"; import { convertSwfToNitro } from "./swf";
import { addSize32ToBundle } from "./swf/add-size32"; import { addSize32ToBundle } from "./swf/add-size32";
@@ -63,7 +63,7 @@ async function writeNitroToDirs(
for (const dir of dirs) { for (const dir of dirs) {
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, fileName), nitro); await fs.writeFile(getRuntimePath(dir, fileName), nitro);
} catch (err) { } catch (err) {
errors.push(`write .nitro to ${dir} failed: ${(err as Error).message}`); errors.push(`write .nitro to ${dir} failed: ${(err as Error).message}`);
} }
@@ -77,12 +77,12 @@ async function copyNitroAcrossDirs(
fileName: string, fileName: string,
): Promise<string[]> { ): Promise<string[]> {
const errors: string[] = []; const errors: string[] = [];
const src = path.join(srcDir, fileName); const src = getRuntimePath(srcDir, fileName);
for (const dir of destDirs) { for (const dir of destDirs) {
if (dir === srcDir) continue; if (dir === srcDir) continue;
try { try {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await fs.copyFile(src, path.join(dir, fileName)); await fs.copyFile(src, getRuntimePath(dir, fileName));
} catch (err) { } catch (err) {
errors.push(`copy .nitro to ${dir} failed: ${(err as Error).message}`); errors.push(`copy .nitro to ${dir} failed: ${(err as Error).message}`);
} }
@@ -150,7 +150,7 @@ export async function repairMissingNitros(
const gamedataRoot = await getGamedataRoot(); const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) { if (gamedataRoot) {
const badgeFiles = await fs const badgeFiles = await fs
.readdir(path.join(gamedataRoot, "album1584")) .readdir(getRuntimePath(gamedataRoot, "album1584"))
.catch(() => [] as string[]); .catch(() => [] as string[]);
albumBadgeSet = new Set( albumBadgeSet = new Set(
badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
@@ -210,7 +210,7 @@ export async function repairMissingNitros(
const fileName = `${classname}.nitro`; const fileName = `${classname}.nitro`;
const presentDirs = nitroDirs.filter((dir) => const presentDirs = nitroDirs.filter((dir) =>
existsSync(path.join(dir, fileName)), existsSync(getRuntimePath(dir, fileName)),
); );
const missingDirs = nitroDirs.filter((dir) => !presentDirs.includes(dir)); const missingDirs = nitroDirs.filter((dir) => !presentDirs.includes(dir));
@@ -269,7 +269,7 @@ export async function repairMissingNitros(
for (const source of sources) { for (const source of sources) {
for (const nitroName of nitroCandidates) { for (const nitroName of nitroCandidates) {
const tmpPath = path.join( const tmpPath = getRuntimePath(
os.tmpdir(), os.tmpdir(),
`.repair_nitro_${classname}_${Math.random().toString(36).slice(2, 8)}.nitro`, `.repair_nitro_${classname}_${Math.random().toString(36).slice(2, 8)}.nitro`,
); );
@@ -469,7 +469,7 @@ export async function reconvertAllNitros(
const basesToProcess: string[] = []; const basesToProcess: string[] = [];
for (const base of existingBases) { for (const base of existingBases) {
if (existsSync(path.join(targets.swfDir, `${base}.swf`))) { if (existsSync(getRuntimePath(targets.swfDir, `${base}.swf`))) {
basesToProcess.push(base); basesToProcess.push(base);
} }
} }
@@ -492,7 +492,7 @@ export async function reconvertAllNitros(
return; return;
} }
const swfPath = path.join(targets.swfDir, `${base}.swf`); const swfPath = getRuntimePath(targets.swfDir, `${base}.swf`);
let swf: Buffer; let swf: Buffer;
try { try {
swf = await fs.readFile(swfPath); swf = await fs.readFile(swfPath);
@@ -636,7 +636,7 @@ export async function addSize32AllNitros(
let buf: Buffer | null = null; let buf: Buffer | null = null;
for (const dir of nitroDirs) { for (const dir of nitroDirs) {
try { try {
buf = await fs.readFile(path.join(dir, fileName)); buf = await fs.readFile(getRuntimePath(dir, fileName));
break; break;
} catch { } catch {
/* try next dir */ /* try next dir */
+3 -3
View File
@@ -14,14 +14,14 @@
* `soundtrack_base_url` — see site-settings.ts defaults. * `soundtrack_base_url` — see site-settings.ts defaults.
*/ */
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import path from "node:path";
import { parseBuffer } from "music-metadata"; import { parseBuffer } from "music-metadata";
import { getRuntimePath } from "@/lib/utils/runtime-path";
// ── Constants ────────────────────────────────────────────────────── // ── Constants ──────────────────────────────────────────────────────
/** On-disk directory where MP3s live. Fixed (not configurable from UI to /** On-disk directory where MP3s live. Fixed (not configurable from UI to
* avoid any path traversal risk). */ * avoid any path traversal risk). */
export const SOUNDTRACK_DIR = path.join( export const SOUNDTRACK_DIR = getRuntimePath(
process.cwd(), process.cwd(),
"public", "public",
"swf", "swf",
@@ -48,7 +48,7 @@ export function getSoundtrackPath(code: string): string {
if (!isSafeSoundtrackCode(code)) { if (!isSafeSoundtrackCode(code)) {
throw new Error(`Unsafe soundtrack code: ${code}`); throw new Error(`Unsafe soundtrack code: ${code}`);
} }
return path.join(SOUNDTRACK_DIR, `${code}.mp3`); return getRuntimePath(SOUNDTRACK_DIR, `${code}.mp3`);
} }
/** Ensure the sounds directory exists (mkdir -p). */ /** Ensure the sounds directory exists (mkdir -p). */
+3 -3
View File
@@ -31,21 +31,21 @@ if (parentPort) {
try { try {
if (msg.type === "convert") { if (msg.type === "convert") {
const result = convertSwfToNitro(msg.buffer, msg.classname); const result = convertSwfToNitro(msg.buffer, msg.classname);
parentPort!.postMessage({ parentPort?.postMessage({
id: msg.id, id: msg.id,
ok: true, ok: true,
result, result,
} satisfies WorkerResponse); } satisfies WorkerResponse);
} else { } else {
const icon = extractIconFromSwf(msg.buffer, msg.classname); const icon = extractIconFromSwf(msg.buffer, msg.classname);
parentPort!.postMessage({ parentPort?.postMessage({
id: msg.id, id: msg.id,
ok: true, ok: true,
icon: icon ?? null, icon: icon ?? null,
} satisfies WorkerResponse); } satisfies WorkerResponse);
} }
} catch (err) { } catch (err) {
parentPort!.postMessage({ parentPort?.postMessage({
id: msg.id, id: msg.id,
ok: false, ok: false,
error: (err as Error).message, error: (err as Error).message,
+2 -1
View File
@@ -3,7 +3,8 @@
*/ */
import { inflateSync } from "node:zlib"; import { inflateSync } from "node:zlib";
import LZMA from "lzma";
const LZMA = require("lzma/src/lzma_worker").LZMA_WORKER;
// ── SWF Tag Types ────────────────────────────────────────────────── // ── SWF Tag Types ──────────────────────────────────────────────────
export const TAG_END = 0; export const TAG_END = 0;
+9 -9
View File
@@ -1,8 +1,8 @@
// @vitest-environment node // @vitest-environment node
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const { const {
appendFurniEntryMock, appendFurniEntryMock,
@@ -96,15 +96,15 @@ describe("uploadSingleFurni live asset mirrors", () => {
beforeEach(async () => { beforeEach(async () => {
__resetUploadItemsBaseAllocatorForTests(); __resetUploadItemsBaseAllocatorForTests();
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upload-furni-")); tempDir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "upload-furni-"));
assetTargets.swfDir = path.join(tempDir, "cms", "swf"); assetTargets.swfDir = getRuntimePath(tempDir, "cms", "swf");
assetTargets.iconDir = path.join(tempDir, "cms", "icons"); assetTargets.iconDir = getRuntimePath(tempDir, "cms", "icons");
assetTargets.nitroDir = path.join(tempDir, "cms", "furniture"); assetTargets.nitroDir = getRuntimePath(tempDir, "cms", "furniture");
assetTargets.mirrorDirs = [ assetTargets.mirrorDirs = [
{ {
swfDir: assetTargets.swfDir, swfDir: assetTargets.swfDir,
iconDir: path.join(tempDir, "Gamedata", "icons"), iconDir: getRuntimePath(tempDir, "Gamedata", "icons"),
nitroDir: path.join(tempDir, "Gamedata", "bundled", "furniture"), nitroDir: getRuntimePath(tempDir, "Gamedata", "bundled", "furniture"),
}, },
]; ];
dbExecuteMock.mockReset(); dbExecuteMock.mockReset();
@@ -135,12 +135,12 @@ describe("uploadSingleFurni live asset mirrors", () => {
expect(result.ok).toBe(true); expect(result.ok).toBe(true);
await expect( await expect(
fs.readFile( fs.readFile(
path.join(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"), getRuntimePath(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"),
), ),
).resolves.toEqual(nitroBuffer); ).resolves.toEqual(nitroBuffer);
await expect( await expect(
fs.readFile( fs.readFile(
path.join(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"), getRuntimePath(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"),
), ),
).resolves.toEqual(iconBuffer); ).resolves.toEqual(iconBuffer);
}); });
+6 -5
View File
@@ -17,6 +17,7 @@ import {
} from "@/lib/services/furni-import"; } from "@/lib/services/furni-import";
import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction"; import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { getRuntimePath, getRuntimeResolve } from "@/lib/utils/runtime-path";
export interface UploadResult { export interface UploadResult {
ok: boolean; ok: boolean;
@@ -42,7 +43,7 @@ export interface UploadParams {
customparams?: string; customparams?: string;
} }
const MIGRATIONS_DIR = path.resolve( const MIGRATIONS_DIR = getRuntimeResolve(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"drizzle/migrations", "drizzle/migrations",
); );
@@ -67,7 +68,7 @@ async function mirrorUploadedAsset(
const sourceKey = pathKey(sourcePath); const sourceKey = pathKey(sourcePath);
const seen = new Set<string>(); const seen = new Set<string>();
for (const dir of dirs) { for (const dir of dirs) {
const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName); const targetPath = getRuntimePath(/*turbopackIgnore: true*/ dir, fileName);
const targetKey = pathKey(targetPath); const targetKey = pathKey(targetPath);
if (targetKey === sourceKey || seen.has(targetKey)) continue; if (targetKey === sourceKey || seen.has(targetKey)) continue;
seen.add(targetKey); seen.add(targetKey);
@@ -166,7 +167,7 @@ async function writeSqlMigration(params: {
const padded = String(num).padStart(4, "0"); const padded = String(num).padStart(4, "0");
const className = params.classname.replace(/[^a-zA-Z0-9_-]/g, "_"); const className = params.classname.replace(/[^a-zA-Z0-9_-]/g, "_");
const fileName = `${padded}_import_furni_${className}.sql`; const fileName = `${padded}_import_furni_${className}.sql`;
const filePath = path.join( const filePath = getRuntimePath(
/*turbopackIgnore: true*/ MIGRATIONS_DIR, /*turbopackIgnore: true*/ MIGRATIONS_DIR,
fileName, fileName,
); );
@@ -283,7 +284,7 @@ export async function uploadSingleFurni(params: {
const { iconDir, nitroDir } = assetTargets; const { iconDir, nitroDir } = assetTargets;
const nitroFileName = `${classname}.nitro`; const nitroFileName = `${classname}.nitro`;
const nitroPath = path.join( const nitroPath = getRuntimePath(
/*turbopackIgnore: true*/ nitroDir, /*turbopackIgnore: true*/ nitroDir,
nitroFileName, nitroFileName,
); );
@@ -301,7 +302,7 @@ export async function uploadSingleFurni(params: {
let iconPath: string | null = null; let iconPath: string | null = null;
if (iconBuffer && iconBuffer.length > 0) { if (iconBuffer && iconBuffer.length > 0) {
iconFileName = `${classname}_icon.png`; iconFileName = `${classname}_icon.png`;
iconPath = path.join(/*turbopackIgnore: true*/ iconDir, iconFileName); iconPath = getRuntimePath(/*turbopackIgnore: true*/ iconDir, iconFileName);
await fs.writeFile(iconPath, iconBuffer); await fs.writeFile(iconPath, iconBuffer);
} }
+11
View File
@@ -0,0 +1,11 @@
import path from "node:path";
export function getRuntimePath(...segments: string[]): string {
const resolvePath = new Function("p", "segs", "return p.join(...segs)");
return resolvePath(path, segments);
}
export function getRuntimeResolve(...segments: string[]): string {
const resolvePath = new Function("p", "segs", "return p.resolve(...segs)");
return resolvePath(path, segments);
}
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Аз съм на 18+ и приемам условията и правилата на {hotel}.", "termsAccept": "Аз съм на 18+ и приемам условията и правилата на {hotel}.",
"createAccount": "Създаване на акаунт", "createAccount": "Създаване на акаунт",
"creatingAccount": "Създаване на акаунт...", "creatingAccount": "Създаване на акаунт...",
"redirecting": "Прехвърляне към вашия акаунт...",
"alreadyHaveAccount": "Вече имате акаунт? Вход!", "alreadyHaveAccount": "Вече имате акаунт? Вход!",
"register": "Регистрирайте се", "register": "Регистрирайте се",
"termsError": "Трябва да приемете условията и правилата, за да се регистрирате.", "termsError": "Трябва да приемете условията и правилата, за да се регистрирате.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Je mi 18+ a souhlasím s podmínkami a pravidly {hotel}.", "termsAccept": "Je mi 18+ a souhlasím s podmínkami a pravidly {hotel}.",
"createAccount": "Vytvořit účet", "createAccount": "Vytvořit účet",
"creatingAccount": "Vytváření účtu...", "creatingAccount": "Vytváření účtu...",
"redirecting": "Přesměrovávám na váš účet...",
"alreadyHaveAccount": "Už máte účet? Přihlášení!", "alreadyHaveAccount": "Už máte účet? Přihlášení!",
"register": "Zaregistrujte se", "register": "Zaregistrujte se",
"termsError": "Chcete-li se zaregistrovat, musíte přijmout podmínky a pravidla.", "termsError": "Chcete-li se zaregistrovat, musíte přijmout podmínky a pravidla.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Jeg er 18+ og accepterer vilkårene og reglerne for {hotel}.", "termsAccept": "Jeg er 18+ og accepterer vilkårene og reglerne for {hotel}.",
"createAccount": "Opret konto", "createAccount": "Opret konto",
"creatingAccount": "Opretter konto...", "creatingAccount": "Opretter konto...",
"redirecting": "Omdirigerer til din konto...",
"alreadyHaveAccount": "Har du allerede en konto? Log ind!", "alreadyHaveAccount": "Har du allerede en konto? Log ind!",
"register": "Tilmeld dig", "register": "Tilmeld dig",
"termsError": "Du skal acceptere vilkårene og reglerne for at registrere dig.", "termsError": "Du skal acceptere vilkårene og reglerne for at registrere dig.",
+1
View File
@@ -724,6 +724,7 @@
"termsAccept": "Ich bin 18+ und akzeptiere die {hotel} AGB & Regeln.", "termsAccept": "Ich bin 18+ und akzeptiere die {hotel} AGB & Regeln.",
"createAccount": "Konto erstellen", "createAccount": "Konto erstellen",
"creatingAccount": "Konto wird erstellt...", "creatingAccount": "Konto wird erstellt...",
"redirecting": "Weiterleitung zu deinem Account...",
"alreadyHaveAccount": "Bereits ein Konto? Anmelden!", "alreadyHaveAccount": "Bereits ein Konto? Anmelden!",
"register": "Registrieren", "register": "Registrieren",
"termsError": "Du musst die AGB & Regeln akzeptieren, um dich zu registrieren.", "termsError": "Du musst die AGB & Regeln akzeptieren, um dich zu registrieren.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Είμαι 18+ και αποδέχομαι τους όρους και τους κανόνες του {hotel}.", "termsAccept": "Είμαι 18+ και αποδέχομαι τους όρους και τους κανόνες του {hotel}.",
"createAccount": "Δημιουργία λογαριασμού", "createAccount": "Δημιουργία λογαριασμού",
"creatingAccount": "Δημιουργία λογαριασμού...", "creatingAccount": "Δημιουργία λογαριασμού...",
"redirecting": "Ανακατεύθυνση στον λογαριασμό σας...",
"alreadyHaveAccount": "Έχετε ήδη λογαριασμό; Σύνδεση!", "alreadyHaveAccount": "Έχετε ήδη λογαριασμό; Σύνδεση!",
"register": "Εγγραφή", "register": "Εγγραφή",
"termsError": "Πρέπει να αποδεχτείτε τους όρους και τους κανόνες για να εγγραφείτε.", "termsError": "Πρέπει να αποδεχτείτε τους όρους και τους κανόνες για να εγγραφείτε.",
+1
View File
@@ -903,6 +903,7 @@
"termsAccept": "I am 18+ and accept the {hotel} terms & rules.", "termsAccept": "I am 18+ and accept the {hotel} terms & rules.",
"createAccount": "Create account", "createAccount": "Create account",
"creatingAccount": "Creating account...", "creatingAccount": "Creating account...",
"redirecting": "Redirecting to your account...",
"alreadyHaveAccount": "Already have an account? Login!", "alreadyHaveAccount": "Already have an account? Login!",
"alreadyHaveAccountPre": "Already have an account?", "alreadyHaveAccountPre": "Already have an account?",
"alreadyHaveAccountLink": "Sign in", "alreadyHaveAccountLink": "Sign in",
+1
View File
@@ -724,6 +724,7 @@
"termsAccept": "Soy mayor de 18 años y acepto los términos y reglas de {hotel}.", "termsAccept": "Soy mayor de 18 años y acepto los términos y reglas de {hotel}.",
"createAccount": "Crear cuenta", "createAccount": "Crear cuenta",
"creatingAccount": "Creando cuenta...", "creatingAccount": "Creando cuenta...",
"redirecting": "Redirigiendo a tu cuenta...",
"alreadyHaveAccount": "¿Ya tienes una cuenta? ¡Inicia sesión!", "alreadyHaveAccount": "¿Ya tienes una cuenta? ¡Inicia sesión!",
"register": "Registrarse", "register": "Registrarse",
"termsError": "Debes aceptar los términos y reglas para registrarte.", "termsError": "Debes aceptar los términos y reglas para registrarte.",
+1
View File
@@ -724,6 +724,7 @@
"termsAccept": "J'ai 18 ans ou plus et j'accepte les conditions et règles de {hotel}.", "termsAccept": "J'ai 18 ans ou plus et j'accepte les conditions et règles de {hotel}.",
"createAccount": "Créer un compte", "createAccount": "Créer un compte",
"creatingAccount": "Création du compte...", "creatingAccount": "Création du compte...",
"redirecting": "Redirection vers votre compte...",
"alreadyHaveAccount": "Vous avez déjà un compte ? Connectez-vous !", "alreadyHaveAccount": "Vous avez déjà un compte ? Connectez-vous !",
"register": "S'inscrire", "register": "S'inscrire",
"termsError": "Vous devez accepter les conditions et règles pour vous inscrire.", "termsError": "Vous devez accepter les conditions et règles pour vous inscrire.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Imam 18+ godina i prihvaćam odredbe i pravila hotela {hotel}.", "termsAccept": "Imam 18+ godina i prihvaćam odredbe i pravila hotela {hotel}.",
"createAccount": "Napravi račun", "createAccount": "Napravi račun",
"creatingAccount": "Izrada računa...", "creatingAccount": "Izrada računa...",
"redirecting": "Preusmjeravanje na vaš račun...",
"alreadyHaveAccount": "Već imate račun? Prijava!", "alreadyHaveAccount": "Već imate račun? Prijava!",
"register": "Registrirajte se", "register": "Registrirajte se",
"termsError": "Za registraciju morate prihvatiti uvjete i pravila.", "termsError": "Za registraciju morate prihvatiti uvjete i pravila.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "18 évesnél idősebb vagyok, és elfogadom a {hotel} feltételeit és szabályait.", "termsAccept": "18 évesnél idősebb vagyok, és elfogadom a {hotel} feltételeit és szabályait.",
"createAccount": "Hozzon létre fiókot", "createAccount": "Hozzon létre fiókot",
"creatingAccount": "Fiók létrehozása...", "creatingAccount": "Fiók létrehozása...",
"redirecting": "Átirányítás a fiókjához...",
"alreadyHaveAccount": "Már van fiókja? Bejelentkezés!", "alreadyHaveAccount": "Már van fiókja? Bejelentkezés!",
"register": "Regisztráció", "register": "Regisztráció",
"termsError": "A regisztrációhoz el kell fogadnia a feltételeket és szabályokat.", "termsError": "A regisztrációhoz el kell fogadnia a feltételeket és szabályokat.",
+1
View File
@@ -877,6 +877,7 @@
"termsAccept": "Ho 18+ anni e accetto i termini e regole di {hotel}.", "termsAccept": "Ho 18+ anni e accetto i termini e regole di {hotel}.",
"createAccount": "Crea account", "createAccount": "Crea account",
"creatingAccount": "Creazione account...", "creatingAccount": "Creazione account...",
"redirecting": "Reindirizzamento al tuo account...",
"alreadyHaveAccount": "Hai già un account? Accedi!", "alreadyHaveAccount": "Hai già un account? Accedi!",
"register": "Registrati", "register": "Registrati",
"termsError": "Devi accettare i termini e regole per registrarti.", "termsError": "Devi accettare i termini e regole per registrarti.",
+1
View File
@@ -907,6 +907,7 @@
"termsAccept": "Ik ben 18+ en accepteer de {hotel} voorwaarden & regels.", "termsAccept": "Ik ben 18+ en accepteer de {hotel} voorwaarden & regels.",
"createAccount": "Account aanmaken", "createAccount": "Account aanmaken",
"creatingAccount": "Account wordt aangemaakt...", "creatingAccount": "Account wordt aangemaakt...",
"redirecting": "Doorgaan naar je account...",
"alreadyHaveAccount": "Al een account? Inloggen!", "alreadyHaveAccount": "Al een account? Inloggen!",
"register": "Registreren", "register": "Registreren",
"alreadyHaveAccountPre": "Al een account?", "alreadyHaveAccountPre": "Al een account?",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Jeg er 18+ og godtar vilkårene og reglene for {hotel}.", "termsAccept": "Jeg er 18+ og godtar vilkårene og reglene for {hotel}.",
"createAccount": "Opprett konto", "createAccount": "Opprett konto",
"creatingAccount": "Oppretter konto ...", "creatingAccount": "Oppretter konto ...",
"redirecting": "Videre til kontoen din...",
"alreadyHaveAccount": "Har du allerede en konto? Logg inn!", "alreadyHaveAccount": "Har du allerede en konto? Logg inn!",
"register": "Registrer deg", "register": "Registrer deg",
"termsError": "Du må godta vilkårene og reglene for å registrere deg.", "termsError": "Du må godta vilkårene og reglene for å registrere deg.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Mam ukończone 18 lat i akceptuję warunki i zasady hotelu {hotel}.", "termsAccept": "Mam ukończone 18 lat i akceptuję warunki i zasady hotelu {hotel}.",
"createAccount": "Utwórz konto", "createAccount": "Utwórz konto",
"creatingAccount": "Tworzenie konta...", "creatingAccount": "Tworzenie konta...",
"redirecting": "Przekierowywanie na twoje konto...",
"alreadyHaveAccount": "Masz już konto? Login!", "alreadyHaveAccount": "Masz już konto? Login!",
"register": "Zarejestruj się", "register": "Zarejestruj się",
"termsError": "Aby się zarejestrować, musisz zaakceptować regulamin.", "termsError": "Aby się zarejestrować, musisz zaakceptować regulamin.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Tenho mais de 18 anos e aceito os termos e regras do {hotel}.", "termsAccept": "Tenho mais de 18 anos e aceito os termos e regras do {hotel}.",
"createAccount": "Criar conta", "createAccount": "Criar conta",
"creatingAccount": "Criando conta...", "creatingAccount": "Criando conta...",
"redirecting": "A redirecionar para a sua conta...",
"alreadyHaveAccount": "Já tem uma conta? Conecte-se!", "alreadyHaveAccount": "Já tem uma conta? Conecte-se!",
"register": "Cadastre-se", "register": "Cadastre-se",
"termsError": "Você deve aceitar os termos e regras para se registrar.", "termsError": "Você deve aceitar os termos e regras para se registrar.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Am peste 18 ani și accept termenii și regulile {hotel}.", "termsAccept": "Am peste 18 ani și accept termenii și regulile {hotel}.",
"createAccount": "Creați cont", "createAccount": "Creați cont",
"creatingAccount": "Se creează cont...", "creatingAccount": "Se creează cont...",
"redirecting": "Se redirecționează către contul tău...",
"alreadyHaveAccount": "Aveți deja un cont? Log in!", "alreadyHaveAccount": "Aveți deja un cont? Log in!",
"register": "Înregistrează-te", "register": "Înregistrează-te",
"termsError": "Trebuie să acceptați termenii și regulile pentru a vă înregistra.", "termsError": "Trebuie să acceptați termenii și regulile pentru a vă înregistra.",
+1
View File
@@ -752,6 +752,7 @@
"termsAccept": "Мне 18+ и я принимаю условия и правила {hotel}.", "termsAccept": "Мне 18+ и я принимаю условия и правила {hotel}.",
"createAccount": "Создать аккаунт", "createAccount": "Создать аккаунт",
"creatingAccount": "Создание учетной записи...", "creatingAccount": "Создание учетной записи...",
"redirecting": "Переход в ваш аккаунт...",
"alreadyHaveAccount": "Уже есть аккаунт? Войти!", "alreadyHaveAccount": "Уже есть аккаунт? Войти!",
"register": "Регистрация", "register": "Регистрация",
"termsError": "Чтобы зарегистрироваться, вы должны принять условия и правила.", "termsError": "Чтобы зарегистрироваться, вы должны принять условия и правила.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Mám 18+ a súhlasím so zmluvnými podmienkami a pravidlami {hotel}.", "termsAccept": "Mám 18+ a súhlasím so zmluvnými podmienkami a pravidlami {hotel}.",
"createAccount": "Vytvoriť účet", "createAccount": "Vytvoriť účet",
"creatingAccount": "Vytvára sa účet...", "creatingAccount": "Vytvára sa účet...",
"redirecting": "Presmerovávam na váš účet...",
"alreadyHaveAccount": "Už máte účet? Prihláste sa!", "alreadyHaveAccount": "Už máte účet? Prihláste sa!",
"register": "Zaregistrujte sa", "register": "Zaregistrujte sa",
"termsError": "Ak sa chcete zaregistrovať, musíte prijať podmienky a pravidlá.", "termsError": "Ak sa chcete zaregistrovať, musíte prijať podmienky a pravidlá.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "I am 18+ and accept the {hotel} terms & rules.", "termsAccept": "I am 18+ and accept the {hotel} terms & rules.",
"createAccount": "Креирајте налог", "createAccount": "Креирајте налог",
"creatingAccount": "Прављење налога...", "creatingAccount": "Прављење налога...",
"redirecting": "Преусмеравање на ваш налог...",
"alreadyHaveAccount": "Већ имате налог? Пријавите се!", "alreadyHaveAccount": "Већ имате налог? Пријавите се!",
"register": "Региструјте се", "register": "Региструјте се",
"termsError": "Морате прихватити услове и правила да бисте се регистровали.", "termsError": "Морате прихватити услове и правила да бисте се регистровали.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Jag är 18+ och accepterar villkoren och reglerna för {hotel}.", "termsAccept": "Jag är 18+ och accepterar villkoren och reglerna för {hotel}.",
"createAccount": "Skapa konto", "createAccount": "Skapa konto",
"creatingAccount": "Skapar konto...", "creatingAccount": "Skapar konto...",
"redirecting": "Vidare till ditt konto...",
"alreadyHaveAccount": "Har du redan ett konto? Inloggning!", "alreadyHaveAccount": "Har du redan ett konto? Inloggning!",
"register": "Registrera dig", "register": "Registrera dig",
"termsError": "Du måste acceptera villkoren och reglerna för att registrera dig.", "termsError": "Du måste acceptera villkoren och reglerna för att registrera dig.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "18 yaşındayım ve {hotel} şartlarını ve kurallarını kabul ediyorum.", "termsAccept": "18 yaşındayım ve {hotel} şartlarını ve kurallarını kabul ediyorum.",
"createAccount": "Hesap oluştur", "createAccount": "Hesap oluştur",
"creatingAccount": "Hesap oluşturuluyor...", "creatingAccount": "Hesap oluşturuluyor...",
"redirecting": "Hesabınıza yönlendiriliyorsunuz...",
"alreadyHaveAccount": "Zaten bir hesabınız var mı? Giriş yapmak!", "alreadyHaveAccount": "Zaten bir hesabınız var mı? Giriş yapmak!",
"register": "Kayıt ol", "register": "Kayıt ol",
"termsError": "Kayıt olmak için şartları ve kuralları kabul etmelisiniz.", "termsError": "Kayıt olmak için şartları ve kuralları kabul etmelisiniz.",
+1
View File
@@ -754,6 +754,7 @@
"termsAccept": "Мені 18+ і я приймаю умови та правила {hotel}.", "termsAccept": "Мені 18+ і я приймаю умови та правила {hotel}.",
"createAccount": "Створити акаунт", "createAccount": "Створити акаунт",
"creatingAccount": "Створення облікового запису...", "creatingAccount": "Створення облікового запису...",
"redirecting": "Перехід до вашого акаунту...",
"alreadyHaveAccount": "Вже маєте акаунт? Вхід!", "alreadyHaveAccount": "Вже маєте акаунт? Вхід!",
"register": "зареєструватися", "register": "зареєструватися",
"termsError": "Ви повинні прийняти умови та правила, щоб зареєструватися.", "termsError": "Ви повинні прийняти умови та правила, щоб зареєструватися.",
+12 -5
View File
@@ -1,7 +1,14 @@
declare module "lzma" { declare module "lzma" {
const lzma: { export const LZMA: any;
decompress(input: Buffer | Uint8Array | number[]): number[]; export function decompress(
compress(input: string | Uint8Array | number[], mode?: number): number[]; byte_array: any,
}; on_finish?: (result: any, error: any) => void,
export default lzma; on_progress?: (percent: number) => void,
): any;
export function compress(
string: string | Uint8Array,
mode: number,
on_finish?: (result: any, error: any) => void,
on_progress?: (percent: number) => void,
): any;
} }
+438 -48
View File
File diff suppressed because it is too large. Load diff