This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+219
-180
@@ -14,41 +14,47 @@ const LOCK_STALE_MS = 60_000;
|
||||
* is unset.
|
||||
*/
|
||||
export async function getFurnitureDataPath(): Promise<string> {
|
||||
const url =
|
||||
(await siteSettings.get("furni_data_url", "/nitro-assets/config/FurnitureData.json5")) ??
|
||||
"/nitro-assets/config/FurnitureData.json5";
|
||||
if (path.isAbsolute(url) && !url.startsWith("/")) return url;
|
||||
const relative = url.replace(/^\/+/, "");
|
||||
// The `relative` segment is admin-configurable so its concrete value is
|
||||
// unknown at build time; without the hint Turbopack traces the entire
|
||||
// public/ tree (~400k files) into the route's NFT.
|
||||
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative);
|
||||
const url =
|
||||
(await siteSettings.get(
|
||||
"furni_data_url",
|
||||
"/nitro-assets/config/FurnitureData.json5",
|
||||
)) ?? "/nitro-assets/config/FurnitureData.json5";
|
||||
if (path.isAbsolute(url) && !url.startsWith("/")) return url;
|
||||
const relative = url.replace(/^\/+/, "");
|
||||
// The `relative` segment is admin-configurable so its concrete value is
|
||||
// unknown at build time; without the hint Turbopack traces the entire
|
||||
// public/ tree (~400k files) into the route's NFT.
|
||||
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative);
|
||||
}
|
||||
|
||||
async function getFurnitureDataWritePaths(): Promise<string[]> {
|
||||
const primary = await getFurnitureDataPath();
|
||||
const configuredMirror = (await siteSettings.get("furni_data_mirror_path", "")) ?? "";
|
||||
const nitroFilesRoot = await getNitroFilesRoot();
|
||||
const mirror = configuredMirror.trim()
|
||||
? configuredMirror
|
||||
: nitroFilesRoot
|
||||
? path.join(/*turbopackIgnore: true*/ nitroFilesRoot, "nitro-assets/gamedata/FurnitureData.json")
|
||||
: "";
|
||||
const primary = await getFurnitureDataPath();
|
||||
const configuredMirror =
|
||||
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
|
||||
const nitroFilesRoot = await getNitroFilesRoot();
|
||||
const mirror = configuredMirror.trim()
|
||||
? configuredMirror
|
||||
: nitroFilesRoot
|
||||
? path.join(
|
||||
/*turbopackIgnore: true*/ nitroFilesRoot,
|
||||
"nitro-assets/gamedata/FurnitureData.json",
|
||||
)
|
||||
: "";
|
||||
|
||||
const paths = [primary, mirror].filter(Boolean).map((p) => path.normalize(p));
|
||||
const seen = new Set<string>();
|
||||
return paths.filter((p) => {
|
||||
const key = process.platform === "win32" ? p.toLowerCase() : p;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const paths = [primary, mirror].filter(Boolean).map((p) => path.normalize(p));
|
||||
const seen = new Set<string>();
|
||||
return paths.filter((p) => {
|
||||
const key = process.platform === "win32" ? p.toLowerCase() : p;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function getFurnitureDataReadPath(): Promise<string> {
|
||||
const paths = await getFurnitureDataWritePaths();
|
||||
const existing = paths.find((p) => existsSync(/*turbopackIgnore: true*/ p));
|
||||
return existing ?? paths[0];
|
||||
const paths = await getFurnitureDataWritePaths();
|
||||
const existing = paths.find((p) => existsSync(/*turbopackIgnore: true*/ p));
|
||||
return existing ?? paths[0];
|
||||
}
|
||||
|
||||
// ── FurnitureData.json lock + helpers ────────────────────────────────
|
||||
@@ -56,185 +62,218 @@ async function getFurnitureDataReadPath(): Promise<string> {
|
||||
let furniDataLock: Promise<void> = Promise.resolve();
|
||||
|
||||
async function acquireDiskLock(lockPath: string): Promise<void> {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (true) {
|
||||
try {
|
||||
const fh = await fs.open(lockPath, "wx");
|
||||
await fh.writeFile(String(process.pid));
|
||||
await fh.close();
|
||||
return;
|
||||
} catch (err) {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e.code !== "EEXIST") throw err;
|
||||
try {
|
||||
const stat = await fs.stat(lockPath);
|
||||
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
||||
await fs.unlink(lockPath).catch(() => {});
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
/* lock vanished between checks */
|
||||
}
|
||||
if (Date.now() > deadline) throw new Error("FurnitureData lock timeout");
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (true) {
|
||||
try {
|
||||
const fh = await fs.open(lockPath, "wx");
|
||||
await fh.writeFile(String(process.pid));
|
||||
await fh.close();
|
||||
return;
|
||||
} catch (err) {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e.code !== "EEXIST") throw err;
|
||||
try {
|
||||
const stat = await fs.stat(lockPath);
|
||||
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
||||
await fs.unlink(lockPath).catch(() => {});
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
/* lock vanished between checks */
|
||||
}
|
||||
if (Date.now() > deadline) throw new Error("FurnitureData lock timeout");
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function withFurniDataLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let release: () => void;
|
||||
const acquired = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
const prev = furniDataLock;
|
||||
furniDataLock = acquired;
|
||||
await prev;
|
||||
const lockBase = await getFurnitureDataReadPath();
|
||||
await fs.mkdir(path.dirname(lockBase), { recursive: true });
|
||||
const lockPath = `${lockBase}.lock`;
|
||||
await acquireDiskLock(lockPath);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await fs.unlink(lockPath).catch(() => {});
|
||||
release!();
|
||||
}
|
||||
let release: () => void;
|
||||
const acquired = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
const prev = furniDataLock;
|
||||
furniDataLock = acquired;
|
||||
await prev;
|
||||
const lockBase = await getFurnitureDataReadPath();
|
||||
await fs.mkdir(path.dirname(lockBase), { recursive: true });
|
||||
const lockPath = `${lockBase}.lock`;
|
||||
await acquireDiskLock(lockPath);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await fs.unlink(lockPath).catch(() => {});
|
||||
release?.();
|
||||
}
|
||||
}
|
||||
|
||||
export async function readFurniData(): Promise<Record<string, unknown>> {
|
||||
const target = await getFurnitureDataReadPath();
|
||||
const raw = await fs.readFile(/*turbopackIgnore: true*/ target, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
const target = await getFurnitureDataReadPath();
|
||||
const raw = await fs.readFile(/*turbopackIgnore: true*/ target, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
export async function writeFurniData(data: Record<string, unknown>): Promise<void> {
|
||||
const targets = await getFurnitureDataWritePaths();
|
||||
const payload = JSON.stringify(data);
|
||||
for (const target of targets) {
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
const tmp = `${target}.tmp`;
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ tmp, payload, "utf-8");
|
||||
await fs.rename(/*turbopackIgnore: true*/ tmp, target);
|
||||
}
|
||||
export async function writeFurniData(
|
||||
data: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const targets = await getFurnitureDataWritePaths();
|
||||
const payload = JSON.stringify(data);
|
||||
for (const target of targets) {
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
const tmp = `${target}.tmp`;
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ tmp, payload, "utf-8");
|
||||
await fs.rename(/*turbopackIgnore: true*/ tmp, target);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildFurniEntry(params: {
|
||||
id: number;
|
||||
classname: string;
|
||||
revision: number;
|
||||
category: string;
|
||||
name: string;
|
||||
description: string;
|
||||
spriteId: number;
|
||||
dims: { x: number; y: number; z: number };
|
||||
metadata: FurniMetadata | null;
|
||||
habboIt?: HabboItFurniEntry | null;
|
||||
id: number;
|
||||
classname: string;
|
||||
revision: number;
|
||||
category: string;
|
||||
name: string;
|
||||
description: string;
|
||||
spriteId: number;
|
||||
dims: { x: number; y: number; z: number };
|
||||
metadata: FurniMetadata | null;
|
||||
habboIt?: HabboItFurniEntry | null;
|
||||
}): Record<string, unknown> {
|
||||
const { id, classname, revision, category, name, description, spriteId, dims, metadata, habboIt } = params;
|
||||
const {
|
||||
id,
|
||||
classname,
|
||||
revision,
|
||||
category,
|
||||
name,
|
||||
description,
|
||||
spriteId,
|
||||
dims,
|
||||
metadata,
|
||||
habboIt,
|
||||
} = params;
|
||||
|
||||
// Merge partcolors: prefer habbo.it (has real hex colors), fallback to .nitro extraction
|
||||
const partcolors = habboIt?.partcolors?.color?.length
|
||||
? habboIt.partcolors
|
||||
: (metadata?.partcolors ?? { color: [] });
|
||||
// Merge partcolors: prefer habbo.it (has real hex colors), fallback to .nitro extraction
|
||||
const partcolors = habboIt?.partcolors?.color?.length
|
||||
? habboIt.partcolors
|
||||
: (metadata?.partcolors ?? { color: [] });
|
||||
|
||||
return {
|
||||
id,
|
||||
classname,
|
||||
revision: revision || habboIt?.revision || 0,
|
||||
category: category || habboIt?.category || "unknown",
|
||||
defaultdir: metadata?.defaultdir ?? habboIt?.defaultdir ?? 0,
|
||||
xdim: dims.x,
|
||||
ydim: dims.y,
|
||||
partcolors,
|
||||
name,
|
||||
description: description || "",
|
||||
adurl: "",
|
||||
offerid: spriteId,
|
||||
buyout: !!habboIt?.bc,
|
||||
rentofferid: -1,
|
||||
rentbuyout: false,
|
||||
bc: habboIt?.bc ?? false,
|
||||
excludeddynamic: false,
|
||||
customparams: habboIt?.customparams ?? "",
|
||||
specialtype: metadata?.specialtype ?? habboIt?.specialtype ?? 0,
|
||||
canstandon: metadata?.canstandon ?? habboIt?.canstandon ?? false,
|
||||
cansiton: metadata?.cansiton ?? habboIt?.cansiton ?? false,
|
||||
canlayon: metadata?.canlayon ?? habboIt?.canlayon ?? false,
|
||||
furniline: habboIt?.furniline ?? "",
|
||||
environment: habboIt?.environment ?? "",
|
||||
rare: habboIt?.rare ?? false,
|
||||
};
|
||||
return {
|
||||
id,
|
||||
classname,
|
||||
revision: revision || habboIt?.revision || 0,
|
||||
category: category || habboIt?.category || "unknown",
|
||||
defaultdir: metadata?.defaultdir ?? habboIt?.defaultdir ?? 0,
|
||||
xdim: dims.x,
|
||||
ydim: dims.y,
|
||||
partcolors,
|
||||
name,
|
||||
description: description || "",
|
||||
adurl: "",
|
||||
offerid: spriteId,
|
||||
buyout: !!habboIt?.bc,
|
||||
rentofferid: -1,
|
||||
rentbuyout: false,
|
||||
bc: habboIt?.bc ?? false,
|
||||
excludeddynamic: false,
|
||||
customparams: habboIt?.customparams ?? "",
|
||||
specialtype: metadata?.specialtype ?? habboIt?.specialtype ?? 0,
|
||||
canstandon: metadata?.canstandon ?? habboIt?.canstandon ?? false,
|
||||
cansiton: metadata?.cansiton ?? habboIt?.cansiton ?? false,
|
||||
canlayon: metadata?.canlayon ?? habboIt?.canlayon ?? false,
|
||||
furniline: habboIt?.furniline ?? "",
|
||||
environment: habboIt?.environment ?? "",
|
||||
rare: habboIt?.rare ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function upsertEntryInSection(
|
||||
section: { furnitype: Array<Record<string, unknown>> },
|
||||
entry: Record<string, unknown>,
|
||||
section: { furnitype: Array<Record<string, unknown>> },
|
||||
entry: Record<string, unknown>,
|
||||
) {
|
||||
const classname = entry.classname as string | undefined;
|
||||
if (classname) {
|
||||
const idx = section.furnitype.findIndex((e) => e.classname === classname);
|
||||
if (idx >= 0) {
|
||||
section.furnitype[idx] = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
section.furnitype.push(entry);
|
||||
const classname = entry.classname as string | undefined;
|
||||
if (classname) {
|
||||
const idx = section.furnitype.findIndex((e) => e.classname === classname);
|
||||
if (idx >= 0) {
|
||||
section.furnitype[idx] = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
section.furnitype.push(entry);
|
||||
}
|
||||
|
||||
export async function appendFurniEntry(entry: Record<string, unknown>, itemType: string): Promise<void> {
|
||||
await withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>;
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.wallitemtypes, entry);
|
||||
} else {
|
||||
if (!furniData.roomitemtypes) furniData.roomitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.roomitemtypes, entry);
|
||||
}
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
});
|
||||
export async function appendFurniEntry(
|
||||
entry: Record<string, unknown>,
|
||||
itemType: string,
|
||||
): Promise<void> {
|
||||
await withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>;
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.wallitemtypes, entry);
|
||||
} else {
|
||||
if (!furniData.roomitemtypes) furniData.roomitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.roomitemtypes, entry);
|
||||
}
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeFurniEntry(classname: string): Promise<boolean> {
|
||||
return withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>;
|
||||
let removed = false;
|
||||
return withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>;
|
||||
let removed = false;
|
||||
|
||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||
if (furniData[section]?.furnitype) {
|
||||
const before = furniData[section].furnitype.length;
|
||||
furniData[section].furnitype = furniData[section].furnitype.filter((e) => e.classname !== classname);
|
||||
if (furniData[section].furnitype.length < before) removed = true;
|
||||
}
|
||||
}
|
||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||
if (furniData[section]?.furnitype) {
|
||||
const before = furniData[section].furnitype.length;
|
||||
furniData[section].furnitype = furniData[section].furnitype.filter(
|
||||
(e) => e.classname !== classname,
|
||||
);
|
||||
if (furniData[section].furnitype.length < before) removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
}
|
||||
return removed;
|
||||
});
|
||||
if (removed) {
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
}
|
||||
return removed;
|
||||
});
|
||||
}
|
||||
|
||||
export async function appendFurniEntriesBatch(
|
||||
entries: Array<{ entry: Record<string, unknown>; itemType: string }>,
|
||||
entries: Array<{ entry: Record<string, unknown>; itemType: string }>,
|
||||
): Promise<void> {
|
||||
await withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<string, { furnitype: unknown[] }>;
|
||||
for (const { entry, itemType } of entries) {
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.wallitemtypes as { furnitype: Array<Record<string, unknown>> }, entry);
|
||||
} else {
|
||||
if (!furniData.roomitemtypes) furniData.roomitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.roomitemtypes as { furnitype: Array<Record<string, unknown>> }, entry);
|
||||
}
|
||||
}
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
});
|
||||
await withFurniDataLock(async () => {
|
||||
const furniData = (await readFurniData()) as Record<
|
||||
string,
|
||||
{ furnitype: unknown[] }
|
||||
>;
|
||||
for (const { entry, itemType } of entries) {
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes)
|
||||
furniData.wallitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(
|
||||
furniData.wallitemtypes as {
|
||||
furnitype: Array<Record<string, unknown>>;
|
||||
},
|
||||
entry,
|
||||
);
|
||||
} else {
|
||||
if (!furniData.roomitemtypes)
|
||||
furniData.roomitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(
|
||||
furniData.roomitemtypes as {
|
||||
furnitype: Array<Record<string, unknown>>;
|
||||
},
|
||||
entry,
|
||||
);
|
||||
}
|
||||
}
|
||||
await writeFurniData(furniData as Record<string, unknown>);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user