fix(catalog): resolve classname candidates using public_name
This commit is contained in:
1 parent
8c47c3c158
commit
ca6aa97eb2
5 files changed
+305
-211
No files matched your search
@@ -162,16 +162,18 @@ export async function repairMissingNitros(
|
||||
let items: Array<{
|
||||
id: number;
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
type: string;
|
||||
interaction_type: string;
|
||||
}>;
|
||||
try {
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT id, item_name, type, interaction_type FROM items_base ORDER BY id
|
||||
SELECT id, item_name, public_name, type, interaction_type FROM items_base ORDER BY id
|
||||
`)) as unknown as [
|
||||
Array<{
|
||||
id: number;
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
type: string;
|
||||
interaction_type: string;
|
||||
}>,
|
||||
@@ -200,16 +202,37 @@ export async function repairMissingNitros(
|
||||
const total = items.length;
|
||||
onEvent?.({ type: "started", total });
|
||||
|
||||
const processItem = async (classname: string) => {
|
||||
// A row can have its real classname in either `item_name` or `public_name`.
|
||||
const classnameCandidates = (item: (typeof items)[number]): string[] => {
|
||||
const out: string[] = [];
|
||||
const add = (n?: string | null) => {
|
||||
if (n && !out.includes(n)) out.push(n);
|
||||
};
|
||||
add(item.item_name);
|
||||
if (
|
||||
item.public_name &&
|
||||
item.public_name !== item.item_name &&
|
||||
!item.public_name.includes(" ") &&
|
||||
item.public_name !== "0"
|
||||
) {
|
||||
add(item.public_name);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const processItem = async (item: (typeof items)[number]) => {
|
||||
const classname = item.item_name;
|
||||
// Special items (badges, pets, effects, bots, sticky notes) don't have
|
||||
// furni .nitro bundles to repair. Skip them.
|
||||
if (skipSet.has(classname)) {
|
||||
return { status: "skipped" } as const;
|
||||
}
|
||||
const fileName = `${classname}.nitro`;
|
||||
const names = classnameCandidates(item);
|
||||
const fileNameFor = (n: string) => `${n}.nitro`;
|
||||
const fileNames = names.map(fileNameFor);
|
||||
|
||||
const presentDirs = nitroDirs.filter((dir) =>
|
||||
existsSync(getRuntimePath(dir, fileName)),
|
||||
fileNames.some((f) => existsSync(getRuntimePath(dir, f))),
|
||||
);
|
||||
const missingDirs = nitroDirs.filter((dir) => !presentDirs.includes(dir));
|
||||
|
||||
@@ -223,18 +246,19 @@ export async function repairMissingNitros(
|
||||
// 1) Copy across mirrors — the .nitro already exists in one target (e.g.
|
||||
// gamedata), so just fill the ones that are missing it.
|
||||
if (presentDirs.length > 0) {
|
||||
try {
|
||||
const copyErrors = await copyNitroAcrossDirs(
|
||||
presentDirs[0],
|
||||
missingDirs,
|
||||
fileName,
|
||||
);
|
||||
warnings.push(...copyErrors);
|
||||
ok = copyErrors.length === 0;
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`copy across .nitro dirs failed: ${(err as Error).message}`,
|
||||
);
|
||||
for (const f of fileNames) {
|
||||
const src = nitroDirs.find((d) => existsSync(getRuntimePath(d, f)));
|
||||
if (!src) continue;
|
||||
const dst = missingDirs.filter((d) => d !== src);
|
||||
if (dst.length === 0) continue;
|
||||
try {
|
||||
const copyErrors = await copyNitroAcrossDirs(src, dst, f);
|
||||
warnings.push(...copyErrors);
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`copy across .nitro dirs failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,58 +272,51 @@ export async function repairMissingNitros(
|
||||
}
|
||||
if (sources.length === 0) sources = DEFAULT_NITRO_SOURCES;
|
||||
|
||||
const nitroCandidates = [
|
||||
`${classname}.nitro`,
|
||||
// Variant classnames (e.g. `table_silo_med*10`) often only exist
|
||||
// on sources under their base name (e.g. `table_silo_med.nitro`)
|
||||
// or with the `*` replaced by `_` (e.g. `table_silo_med_10.nitro`).
|
||||
...(classname.includes("*")
|
||||
? [
|
||||
`${classname.split("*")[0]}.nitro`,
|
||||
`${classname.replace(/\*/g, "_")}.nitro`,
|
||||
]
|
||||
: []),
|
||||
// Classnames with dots (`post.it`) are stored on disk with
|
||||
// underscores (`post_it.nitro`).
|
||||
...(classname.includes(".")
|
||||
? [`${classname.replace(/\./g, "_")}.nitro`]
|
||||
: []),
|
||||
];
|
||||
for (const n of names) {
|
||||
const nitroCandidates = [
|
||||
fileNameFor(n),
|
||||
...(n.includes("*")
|
||||
? [fileNameFor(n.split("*")[0]), fileNameFor(n.replace(/\*/g, "_"))]
|
||||
: []),
|
||||
...(n.includes(".") ? [fileNameFor(n.replace(/\./g, "_"))] : []),
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
for (const nitroName of nitroCandidates) {
|
||||
const tmpPath = getRuntimePath(
|
||||
os.tmpdir(),
|
||||
`.repair_nitro_${classname}_${Math.random().toString(36).slice(2, 8)}.nitro`,
|
||||
);
|
||||
try {
|
||||
const dl = await downloadFile(
|
||||
`${source.nitroBaseUrl}/${nitroName}`,
|
||||
tmpPath,
|
||||
{ maxRetries: 1 },
|
||||
for (const source of sources) {
|
||||
for (const nitroName of nitroCandidates) {
|
||||
const tmpPath = getRuntimePath(
|
||||
os.tmpdir(),
|
||||
`.repair_nitro_${classname}_${Math.random().toString(36).slice(2, 8)}.nitro`,
|
||||
);
|
||||
if (dl.ok) {
|
||||
// Validate it is a real Nitro bundle before writing it
|
||||
// anywhere (mirrors clone-import.ts).
|
||||
const buffer = await fs.readFile(tmpPath);
|
||||
parseNitroBundle(buffer);
|
||||
const writeErrors = await writeNitroToDirs(
|
||||
buffer,
|
||||
missingDirs,
|
||||
fileName,
|
||||
try {
|
||||
const dl = await downloadFile(
|
||||
templateUrl(source.nitroBaseUrl, n, n),
|
||||
tmpPath,
|
||||
{ maxRetries: 1 },
|
||||
);
|
||||
warnings.push(...writeErrors);
|
||||
ok = writeErrors.length === 0;
|
||||
break;
|
||||
if (dl.ok) {
|
||||
// Validate it is a real Nitro bundle before writing it
|
||||
// anywhere (mirrors clone-import.ts).
|
||||
const buffer = await fs.readFile(tmpPath);
|
||||
parseNitroBundle(buffer);
|
||||
const writeErrors = await writeNitroToDirs(
|
||||
buffer,
|
||||
missingDirs,
|
||||
fileNameFor(n),
|
||||
);
|
||||
warnings.push(...writeErrors);
|
||||
ok = writeErrors.length === 0;
|
||||
break;
|
||||
}
|
||||
warnings.push(`${nitroName} not found on ${source.name}`);
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`download from ${source.name} failed: ${(err as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
await fs.unlink(tmpPath).catch(() => {});
|
||||
}
|
||||
warnings.push(`${nitroName} not found on ${source.name}`);
|
||||
} catch (err) {
|
||||
warnings.push(
|
||||
`download from ${source.name} failed: ${(err as Error).message}`,
|
||||
);
|
||||
} finally {
|
||||
await fs.unlink(tmpPath).catch(() => {});
|
||||
}
|
||||
if (ok) break;
|
||||
}
|
||||
if (ok) break;
|
||||
}
|
||||
@@ -316,7 +333,7 @@ export async function repairMissingNitros(
|
||||
|
||||
await runPool(items, REPAIR_CONCURRENCY, async (item, i) => {
|
||||
const classname = item.item_name;
|
||||
const result = await processItem(classname);
|
||||
const result = await processItem(item);
|
||||
if (result.status === "skipped") {
|
||||
skipped++;
|
||||
onEvent?.({
|
||||
@@ -360,6 +377,14 @@ export async function repairMissingNitros(
|
||||
return { succeeded, failed, skipped };
|
||||
}
|
||||
|
||||
/** Expand %libname% / %param% templates used by some retro asset mirrors. */
|
||||
function templateUrl(base: string, libname: string, param: string): string {
|
||||
if (!base) return "";
|
||||
return base
|
||||
.replace(/%libname%/g, encodeURIComponent(libname))
|
||||
.replace(/%param%/g, encodeURIComponent(param));
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-convert every furniture `.nitro` we produced from a local SWF so the
|
||||
* bundles include size 32 (and every other visualization size the converter
|
||||
|
||||
Reference in new issue
Block a user