Recognize imported furniture by normalized classname and local IDs
This commit is contained in:
1 parent
9dc6b8a261
commit
a78d8256e0
7 files changed
+254
-46
No files matched your search
@@ -94,4 +94,50 @@ describe("furniture inspection", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect((await response.json()).items[0].furnidataReadable).toBe(false);
|
||||
});
|
||||
it("recognizes existing SQL and furnidata across classname casing", async () => {
|
||||
mocks.execute
|
||||
.mockResolvedValueOnce([
|
||||
[
|
||||
{
|
||||
id: 2000037258,
|
||||
spriteId: 2000037258,
|
||||
classname: "recycler_kintsugib",
|
||||
name: "Local",
|
||||
type: "s",
|
||||
},
|
||||
],
|
||||
[],
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
[
|
||||
{
|
||||
id: 3,
|
||||
pageId: 4,
|
||||
classname: "RECYCLER_KINTSUGIB",
|
||||
credits: 3,
|
||||
points: 0,
|
||||
},
|
||||
],
|
||||
[],
|
||||
]);
|
||||
mocks.read.mockResolvedValue({
|
||||
roomitemtypes: {
|
||||
furnitype: [
|
||||
{ id: 2000037258, classname: "recycler_kintsugib", name: "Local" },
|
||||
],
|
||||
},
|
||||
wallitemtypes: { furnitype: [] },
|
||||
});
|
||||
mocks.stat.mockImplementation(async (filename) => {
|
||||
if (String(filename).includes("kintsugib"))
|
||||
return { size: 256, isFile: () => true };
|
||||
throw { code: "ENOENT" };
|
||||
});
|
||||
const result = await (await POST(request(["recycler_kintsugiB"]))).json();
|
||||
expect(result.items[0].sql[0].id).toBe(2000037258);
|
||||
expect(result.items[0].catalog).toHaveLength(1);
|
||||
expect(result.items[0].furnidata).toHaveLength(1);
|
||||
expect(result.items[0].nitro.exists).toBe(true);
|
||||
expect(result.items[0].icon.exists).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { sql } from "drizzle-orm";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { db } from "@/lib/db";
|
||||
import { normalizeClassname } from "@/lib/furni/classname";
|
||||
import {
|
||||
type FurnitureInspection,
|
||||
validateClassnames,
|
||||
@@ -12,7 +13,10 @@ import { PERMS } from "@/lib/permission-slugs";
|
||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||
import { readFurniData } from "@/lib/services/furni-data";
|
||||
|
||||
async function asset(directory: string, name: string) {
|
||||
async function asset(
|
||||
directory: string,
|
||||
name: string,
|
||||
): Promise<{ exists: boolean | null; bytes: number }> {
|
||||
try {
|
||||
const stat = await fs.stat(path.join(directory, name));
|
||||
return { exists: stat.isFile(), bytes: stat.size };
|
||||
@@ -23,6 +27,16 @@ async function asset(directory: string, name: string) {
|
||||
};
|
||||
}
|
||||
}
|
||||
async function assetCandidates(directory: string, names: string[]) {
|
||||
const results = await Promise.all(
|
||||
[...new Set(names)].map((name) => asset(directory, name)),
|
||||
);
|
||||
return (
|
||||
results.find((result) => result.exists === true) ??
|
||||
results.find((result) => result.exists === null) ??
|
||||
results[0]
|
||||
);
|
||||
}
|
||||
// Read-only inspection lives outside import routes so it cannot enqueue a Git export.
|
||||
export const POST = withAdmin(
|
||||
{ permission: PERMS.ASSETS_IMPORT },
|
||||
@@ -32,17 +46,17 @@ export const POST = withAdmin(
|
||||
if (!names)
|
||||
return apiError("Provide 1–500 valid furniture classnames", 400);
|
||||
const namesSql = sql.join(
|
||||
names.map((name) => sql`${name}`),
|
||||
names.map((name) => sql`${normalizeClassname(name)}`),
|
||||
sql`, `,
|
||||
);
|
||||
const [rows] = (await db.execute(
|
||||
sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type FROM items_base WHERE item_name IN (${namesSql})`,
|
||||
sql`SELECT id, sprite_id AS spriteId, item_name AS classname, public_name AS name, type FROM items_base WHERE LOWER(TRIM(item_name)) IN (${namesSql})`,
|
||||
)) as unknown as [
|
||||
Array<FurnitureInspection["sql"][number] & { classname: string }>,
|
||||
unknown,
|
||||
];
|
||||
const [offers] = (await db.execute(
|
||||
sql`SELECT ib.item_name AS classname, ci.id, ci.page_id AS pageId, ci.cost_credits AS credits, ci.cost_points AS points FROM items_base ib JOIN catalog_items ci ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) > 0 WHERE ib.item_name IN (${namesSql})`,
|
||||
sql`SELECT ib.item_name AS classname, ci.id, ci.page_id AS pageId, ci.cost_credits AS credits, ci.cost_points AS points FROM items_base ib JOIN catalog_items ci ON FIND_IN_SET(ib.id, REPLACE(ci.item_ids, ';', ',')) > 0 WHERE LOWER(TRIM(ib.item_name)) IN (${namesSql})`,
|
||||
)) as unknown as [
|
||||
Array<FurnitureInspection["catalog"][number] & { classname: string }>,
|
||||
unknown,
|
||||
@@ -74,8 +88,9 @@ export const POST = withAdmin(
|
||||
continue;
|
||||
}
|
||||
const name = String(entry.classname ?? "");
|
||||
if (!names.includes(name)) continue;
|
||||
const values = entries.get(name) ?? [];
|
||||
const key = normalizeClassname(name);
|
||||
if (!names.some((value) => normalizeClassname(value) === key)) continue;
|
||||
const values = entries.get(key) ?? [];
|
||||
values.push({
|
||||
id: Number(entry.id),
|
||||
name: String(entry.name ?? ""),
|
||||
@@ -83,7 +98,7 @@ export const POST = withAdmin(
|
||||
revision: Number(entry.revision ?? 0),
|
||||
type,
|
||||
});
|
||||
entries.set(name, values);
|
||||
entries.set(key, values);
|
||||
}
|
||||
}
|
||||
const dirs = await getFurniAssetDirs();
|
||||
@@ -92,16 +107,39 @@ export const POST = withAdmin(
|
||||
items.push(
|
||||
...(await Promise.all(
|
||||
names.slice(offset, offset + 20).map(async (classname) => {
|
||||
const base = classname.split("*")[0];
|
||||
const variants = [
|
||||
classname,
|
||||
...rows
|
||||
.filter(
|
||||
(row) =>
|
||||
normalizeClassname(row.classname) ===
|
||||
normalizeClassname(classname),
|
||||
)
|
||||
.map((row) => row.classname),
|
||||
];
|
||||
const [nitro, icon] = await Promise.all([
|
||||
asset(dirs.nitroDir, `${base}.nitro`),
|
||||
asset(dirs.iconDir, `${classname.replace(/\*/g, "_")}_icon.png`),
|
||||
assetCandidates(
|
||||
dirs.nitroDir,
|
||||
variants.map((name) => `${name.split("*")[0]}.nitro`),
|
||||
),
|
||||
assetCandidates(
|
||||
dirs.iconDir,
|
||||
variants.map((name) => `${name.replaceAll("*", "_")}_icon.png`),
|
||||
),
|
||||
]);
|
||||
return {
|
||||
classname,
|
||||
sql: rows.filter((row) => row.classname === classname),
|
||||
catalog: offers.filter((row) => row.classname === classname),
|
||||
furnidata: entries.get(classname) ?? [],
|
||||
sql: rows.filter(
|
||||
(row) =>
|
||||
normalizeClassname(row.classname) ===
|
||||
normalizeClassname(classname),
|
||||
),
|
||||
catalog: offers.filter(
|
||||
(row) =>
|
||||
normalizeClassname(row.classname) ===
|
||||
normalizeClassname(classname),
|
||||
),
|
||||
furnidata: entries.get(normalizeClassname(classname)) ?? [],
|
||||
furnidataReadable: readable,
|
||||
nitro,
|
||||
icon,
|
||||
|
||||
Reference in new issue
Block a user