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
@@ -4,6 +4,11 @@ import { eq, sql } from "drizzle-orm";
|
|||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
import { db, ItemsBase } from "@/lib/db";
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
|
import { normalizeClassname } from "@/lib/furni/classname";
|
||||||
|
import {
|
||||||
|
indexLocalFurniture,
|
||||||
|
localFurnitureStatus,
|
||||||
|
} from "@/lib/furni/local-presence";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getCloneList } from "@/lib/services/clone-import";
|
import { getCloneList } from "@/lib/services/clone-import";
|
||||||
@@ -298,6 +303,11 @@ export const GET = withAdmin(
|
|||||||
const page = parseInt(request.nextUrl.searchParams.get("page") || "1", 10);
|
const page = parseInt(request.nextUrl.searchParams.get("page") || "1", 10);
|
||||||
|
|
||||||
// ── Browse a custom clone source (retro hotel furnidata) ────────
|
// ── Browse a custom clone source (retro hotel furnidata) ────────
|
||||||
|
const localRows = await db
|
||||||
|
.select({ id: ItemsBase.id, itemName: ItemsBase.itemName })
|
||||||
|
.from(ItemsBase);
|
||||||
|
const localIndex = indexLocalFurniture(localRows);
|
||||||
|
const importedSet = new Set(localIndex.keys());
|
||||||
const sourceId = request.nextUrl.searchParams.get("source");
|
const sourceId = request.nextUrl.searchParams.get("source");
|
||||||
if (sourceId) {
|
if (sourceId) {
|
||||||
const source = await getSource(sourceId);
|
const source = await getSource(sourceId);
|
||||||
@@ -325,7 +335,7 @@ export const GET = withAdmin(
|
|||||||
...(fd.roomitemtypes?.furnitype ?? []),
|
...(fd.roomitemtypes?.furnitype ?? []),
|
||||||
...(fd.wallitemtypes?.furnitype ?? []),
|
...(fd.wallitemtypes?.furnitype ?? []),
|
||||||
]) {
|
]) {
|
||||||
if (e.classname) furniDataSet.add(e.classname);
|
if (e.classname) furniDataSet.add(normalizeClassname(e.classname));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* best effort */
|
/* best effort */
|
||||||
@@ -345,7 +355,13 @@ export const GET = withAdmin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const enriched = items.map((item) => {
|
const enriched = items.map((item) => {
|
||||||
const imported = item.present;
|
const presence = localFurnitureStatus(
|
||||||
|
item.classname,
|
||||||
|
localIndex,
|
||||||
|
furniDataSet,
|
||||||
|
catalogIdSet,
|
||||||
|
);
|
||||||
|
const imported = presence.alreadyImported;
|
||||||
let nitroExists = false;
|
let nitroExists = false;
|
||||||
if (imported) {
|
if (imported) {
|
||||||
const starIdx = item.classname.indexOf("*");
|
const starIdx = item.classname.indexOf("*");
|
||||||
@@ -357,7 +373,17 @@ export const GET = withAdmin(
|
|||||||
/*turbopackIgnore: true*/ nitroDir,
|
/*turbopackIgnore: true*/ nitroDir,
|
||||||
`${base}.nitro`,
|
`${base}.nitro`,
|
||||||
);
|
);
|
||||||
nitroExists = existsSync(/*turbopackIgnore: true*/ p);
|
nitroExists =
|
||||||
|
existsSync(/*turbopackIgnore: true*/ p) ||
|
||||||
|
(localIndex.get(normalizeClassname(item.classname)) ?? []).some(
|
||||||
|
(row) =>
|
||||||
|
existsSync(
|
||||||
|
path.join(
|
||||||
|
/*turbopackIgnore: true*/ nitroDir,
|
||||||
|
`${row.itemName.split("*")[0]}.nitro`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -367,10 +393,9 @@ export const GET = withAdmin(
|
|||||||
description: item.description,
|
description: item.description,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
category: String(item.category ?? ""),
|
category: String(item.category ?? ""),
|
||||||
alreadyImported: imported,
|
...presence,
|
||||||
nitroExists,
|
nitroExists,
|
||||||
hasFurnidata: imported ? furniDataSet.has(item.classname) : undefined,
|
|
||||||
hasCatalogEntry: imported ? catalogIdSet.has(item.id) : undefined,
|
|
||||||
iconUrl: sourceIconBase
|
iconUrl: sourceIconBase
|
||||||
? `${sourceIconBase}/${encodeURIComponent(item.classname)}_icon.png`
|
? `${sourceIconBase}/${encodeURIComponent(item.classname)}_icon.png`
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -389,16 +414,6 @@ export const GET = withAdmin(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull every imported classname once so we can both (a) sort non-imported
|
|
||||||
// items to the top *before* pagination and (b) skip the per-page lookup
|
|
||||||
// below. Single-column indexed query — fast even at 10k+ rows.
|
|
||||||
const allImported = await db
|
|
||||||
.select({ itemName: ItemsBase.itemName })
|
|
||||||
.from(ItemsBase);
|
|
||||||
const importedSet = new Set(
|
|
||||||
allImported.map((e) => e.itemName).filter((n): n is string => !!n),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bulk-load FurnitureData and catalog references for status checks
|
// Bulk-load FurnitureData and catalog references for status checks
|
||||||
const furniDataSet = new Set<string>();
|
const furniDataSet = new Set<string>();
|
||||||
const catalogIdSet = new Set<number>();
|
const catalogIdSet = new Set<number>();
|
||||||
@@ -411,7 +426,7 @@ export const GET = withAdmin(
|
|||||||
...(fd.roomitemtypes?.furnitype ?? []),
|
...(fd.roomitemtypes?.furnitype ?? []),
|
||||||
...(fd.wallitemtypes?.furnitype ?? []),
|
...(fd.wallitemtypes?.furnitype ?? []),
|
||||||
]) {
|
]) {
|
||||||
if (e.classname) furniDataSet.add(e.classname);
|
if (e.classname) furniDataSet.add(normalizeClassname(e.classname));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* best effort */
|
/* best effort */
|
||||||
@@ -438,7 +453,13 @@ export const GET = withAdmin(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const enriched = result.items.map((item) => {
|
const enriched = result.items.map((item) => {
|
||||||
const imported = importedSet.has(item.classname);
|
const presence = localFurnitureStatus(
|
||||||
|
item.classname,
|
||||||
|
localIndex,
|
||||||
|
furniDataSet,
|
||||||
|
catalogIdSet,
|
||||||
|
);
|
||||||
|
const imported = presence.alreadyImported;
|
||||||
let nitroExists = false;
|
let nitroExists = false;
|
||||||
if (imported) {
|
if (imported) {
|
||||||
const starIdx = item.classname.indexOf("*");
|
const starIdx = item.classname.indexOf("*");
|
||||||
@@ -450,14 +471,22 @@ export const GET = withAdmin(
|
|||||||
/*turbopackIgnore: true*/ nitroDir,
|
/*turbopackIgnore: true*/ nitroDir,
|
||||||
`${base}.nitro`,
|
`${base}.nitro`,
|
||||||
);
|
);
|
||||||
nitroExists = existsSync(/*turbopackIgnore: true*/ p);
|
nitroExists =
|
||||||
|
existsSync(/*turbopackIgnore: true*/ p) ||
|
||||||
|
(localIndex.get(normalizeClassname(item.classname)) ?? []).some(
|
||||||
|
(row) =>
|
||||||
|
existsSync(
|
||||||
|
path.join(
|
||||||
|
/*turbopackIgnore: true*/ nitroDir,
|
||||||
|
`${row.itemName.split("*")[0]}.nitro`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
alreadyImported: imported,
|
...presence,
|
||||||
nitroExists,
|
nitroExists,
|
||||||
hasFurnidata: imported ? furniDataSet.has(item.classname) : undefined,
|
|
||||||
hasCatalogEntry: imported ? catalogIdSet.has(item.id) : undefined,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -94,4 +94,50 @@ describe("furniture inspection", () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect((await response.json()).items[0].furnidataReadable).toBe(false);
|
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 { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
|
import { normalizeClassname } from "@/lib/furni/classname";
|
||||||
import {
|
import {
|
||||||
type FurnitureInspection,
|
type FurnitureInspection,
|
||||||
validateClassnames,
|
validateClassnames,
|
||||||
@@ -12,7 +13,10 @@ import { PERMS } from "@/lib/permission-slugs";
|
|||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import { readFurniData } from "@/lib/services/furni-data";
|
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 {
|
try {
|
||||||
const stat = await fs.stat(path.join(directory, name));
|
const stat = await fs.stat(path.join(directory, name));
|
||||||
return { exists: stat.isFile(), bytes: stat.size };
|
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.
|
// Read-only inspection lives outside import routes so it cannot enqueue a Git export.
|
||||||
export const POST = withAdmin(
|
export const POST = withAdmin(
|
||||||
{ permission: PERMS.ASSETS_IMPORT },
|
{ permission: PERMS.ASSETS_IMPORT },
|
||||||
@@ -32,17 +46,17 @@ export const POST = withAdmin(
|
|||||||
if (!names)
|
if (!names)
|
||||||
return apiError("Provide 1–500 valid furniture classnames", 400);
|
return apiError("Provide 1–500 valid furniture classnames", 400);
|
||||||
const namesSql = sql.join(
|
const namesSql = sql.join(
|
||||||
names.map((name) => sql`${name}`),
|
names.map((name) => sql`${normalizeClassname(name)}`),
|
||||||
sql`, `,
|
sql`, `,
|
||||||
);
|
);
|
||||||
const [rows] = (await db.execute(
|
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 [
|
)) as unknown as [
|
||||||
Array<FurnitureInspection["sql"][number] & { classname: string }>,
|
Array<FurnitureInspection["sql"][number] & { classname: string }>,
|
||||||
unknown,
|
unknown,
|
||||||
];
|
];
|
||||||
const [offers] = (await db.execute(
|
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 [
|
)) as unknown as [
|
||||||
Array<FurnitureInspection["catalog"][number] & { classname: string }>,
|
Array<FurnitureInspection["catalog"][number] & { classname: string }>,
|
||||||
unknown,
|
unknown,
|
||||||
@@ -74,8 +88,9 @@ export const POST = withAdmin(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const name = String(entry.classname ?? "");
|
const name = String(entry.classname ?? "");
|
||||||
if (!names.includes(name)) continue;
|
const key = normalizeClassname(name);
|
||||||
const values = entries.get(name) ?? [];
|
if (!names.some((value) => normalizeClassname(value) === key)) continue;
|
||||||
|
const values = entries.get(key) ?? [];
|
||||||
values.push({
|
values.push({
|
||||||
id: Number(entry.id),
|
id: Number(entry.id),
|
||||||
name: String(entry.name ?? ""),
|
name: String(entry.name ?? ""),
|
||||||
@@ -83,7 +98,7 @@ export const POST = withAdmin(
|
|||||||
revision: Number(entry.revision ?? 0),
|
revision: Number(entry.revision ?? 0),
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
entries.set(name, values);
|
entries.set(key, values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dirs = await getFurniAssetDirs();
|
const dirs = await getFurniAssetDirs();
|
||||||
@@ -92,16 +107,39 @@ export const POST = withAdmin(
|
|||||||
items.push(
|
items.push(
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
names.slice(offset, offset + 20).map(async (classname) => {
|
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([
|
const [nitro, icon] = await Promise.all([
|
||||||
asset(dirs.nitroDir, `${base}.nitro`),
|
assetCandidates(
|
||||||
asset(dirs.iconDir, `${classname.replace(/\*/g, "_")}_icon.png`),
|
dirs.nitroDir,
|
||||||
|
variants.map((name) => `${name.split("*")[0]}.nitro`),
|
||||||
|
),
|
||||||
|
assetCandidates(
|
||||||
|
dirs.iconDir,
|
||||||
|
variants.map((name) => `${name.replaceAll("*", "_")}_icon.png`),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
classname,
|
classname,
|
||||||
sql: rows.filter((row) => row.classname === classname),
|
sql: rows.filter(
|
||||||
catalog: offers.filter((row) => row.classname === classname),
|
(row) =>
|
||||||
furnidata: entries.get(classname) ?? [],
|
normalizeClassname(row.classname) ===
|
||||||
|
normalizeClassname(classname),
|
||||||
|
),
|
||||||
|
catalog: offers.filter(
|
||||||
|
(row) =>
|
||||||
|
normalizeClassname(row.classname) ===
|
||||||
|
normalizeClassname(classname),
|
||||||
|
),
|
||||||
|
furnidata: entries.get(normalizeClassname(classname)) ?? [],
|
||||||
furnidataReadable: readable,
|
furnidataReadable: readable,
|
||||||
nitro,
|
nitro,
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { indexLocalFurniture, localFurnitureStatus } from "./local-presence";
|
||||||
|
|
||||||
|
const index = indexLocalFurniture([
|
||||||
|
{ id: 2000037258, itemName: "recycler_kintsugib" },
|
||||||
|
]);
|
||||||
|
describe("local furniture recognition", () => {
|
||||||
|
it("recognizes casing differences and uses the allocated catalog ID", () => {
|
||||||
|
expect(
|
||||||
|
localFurnitureStatus(
|
||||||
|
"recycler_kintsugiB",
|
||||||
|
index,
|
||||||
|
new Set(["recycler_kintsugib"]),
|
||||||
|
new Set([2000037258]),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
alreadyImported: true,
|
||||||
|
localId: 2000037258,
|
||||||
|
hasFurnidata: true,
|
||||||
|
hasCatalogEntry: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("does not confuse the base furniture with its named variant", () => {
|
||||||
|
expect(
|
||||||
|
localFurnitureStatus(
|
||||||
|
"recycler_kintsugi",
|
||||||
|
index,
|
||||||
|
new Set(),
|
||||||
|
new Set([16940]),
|
||||||
|
).alreadyImported,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
it("does not identify a source-ID collision as a matching catalog offer", () => {
|
||||||
|
expect(
|
||||||
|
localFurnitureStatus(
|
||||||
|
"recycler_kintsugiB",
|
||||||
|
index,
|
||||||
|
new Set(),
|
||||||
|
new Set([16940]),
|
||||||
|
).hasCatalogEntry,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
it("retains separate color variants and duplicate SQL records", () => {
|
||||||
|
const rows = indexLocalFurniture([
|
||||||
|
{ id: 1, itemName: "chair*0" },
|
||||||
|
{ id: 2, itemName: "CHAIR*0" },
|
||||||
|
{ id: 3, itemName: "chair*1" },
|
||||||
|
]);
|
||||||
|
expect(rows.get("chair*0")).toHaveLength(2);
|
||||||
|
expect(rows.get("chair*1")).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
localFurnitureStatus("chair*0", rows, new Set(), new Set()).localId,
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { normalizeClassname } from "./classname";
|
||||||
|
|
||||||
|
export function indexLocalFurniture<T extends { id: number; itemName: string }>(
|
||||||
|
rows: T[],
|
||||||
|
) {
|
||||||
|
const index = new Map<string, T[]>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = normalizeClassname(row.itemName);
|
||||||
|
const matches = index.get(key) ?? [];
|
||||||
|
matches.push(row);
|
||||||
|
index.set(key, matches);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
export function localFurnitureStatus(
|
||||||
|
classname: string,
|
||||||
|
local: Map<string, Array<{ id: number; itemName: string }>>,
|
||||||
|
furnidata: Set<string>,
|
||||||
|
catalogIds: Set<number>,
|
||||||
|
) {
|
||||||
|
const key = normalizeClassname(classname);
|
||||||
|
const rows = local.get(key) ?? [];
|
||||||
|
return {
|
||||||
|
alreadyImported: rows.length > 0,
|
||||||
|
localId: rows.length === 1 ? rows[0].id : undefined,
|
||||||
|
hasFurnidata: furnidata.has(key),
|
||||||
|
hasCatalogEntry: rows.some((row) => catalogIds.has(row.id)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|||||||
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";
|
||||||
|
import { normalizeClassname } from "@/lib/furni/classname";
|
||||||
import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel";
|
import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { extractFurniIconPng } from "@/lib/services/clone-icon";
|
import { extractFurniIconPng } from "@/lib/services/clone-icon";
|
||||||
@@ -510,21 +511,24 @@ export async function getCloneList(params: {
|
|||||||
.select({ itemName: ItemsBase.itemName })
|
.select({ itemName: ItemsBase.itemName })
|
||||||
.from(ItemsBase)
|
.from(ItemsBase)
|
||||||
.where(inArray(ItemsBase.itemName, allNames));
|
.where(inArray(ItemsBase.itemName, allNames));
|
||||||
for (const r of rows) present.add(r.itemName);
|
for (const r of rows) present.add(normalizeClassname(r.itemName));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered =
|
const filtered =
|
||||||
filter === "missing"
|
filter === "missing"
|
||||||
? searched.filter((e) => !present.has(e.classname))
|
? searched.filter((e) => !present.has(normalizeClassname(e.classname)))
|
||||||
: filter === "present"
|
: filter === "present"
|
||||||
? searched.filter((e) => present.has(e.classname))
|
? searched.filter((e) => present.has(normalizeClassname(e.classname)))
|
||||||
: searched;
|
: searched;
|
||||||
|
|
||||||
const total = filtered.length;
|
const total = filtered.length;
|
||||||
const start = (Math.max(page, 1) - 1) * perPage;
|
const start = (Math.max(page, 1) - 1) * perPage;
|
||||||
const slice = filtered.slice(start, start + perPage);
|
const slice = filtered.slice(start, start + perPage);
|
||||||
return {
|
return {
|
||||||
items: slice.map((e) => ({ ...e, present: present.has(e.classname) })),
|
items: slice.map((e) => ({
|
||||||
|
...e,
|
||||||
|
present: present.has(normalizeClassname(e.classname)),
|
||||||
|
})),
|
||||||
meta: { page: Math.max(page, 1), perPage, total },
|
meta: { page: Math.max(page, 1), perPage, total },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -540,9 +544,11 @@ export async function getCloneStats(
|
|||||||
.select({ itemName: ItemsBase.itemName })
|
.select({ itemName: ItemsBase.itemName })
|
||||||
.from(ItemsBase)
|
.from(ItemsBase)
|
||||||
.where(inArray(ItemsBase.itemName, classnames));
|
.where(inArray(ItemsBase.itemName, classnames));
|
||||||
for (const r of rows) have.add(r.itemName);
|
for (const r of rows) have.add(normalizeClassname(r.itemName));
|
||||||
}
|
}
|
||||||
const present = all.filter((e) => have.has(e.classname)).length;
|
const present = all.filter((e) =>
|
||||||
|
have.has(normalizeClassname(e.classname)),
|
||||||
|
).length;
|
||||||
return { total: all.length, present, clonable: all.length - present };
|
return { total: all.length, present, clonable: all.length - present };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +567,9 @@ export async function getClonableClassnames(
|
|||||||
.select({ itemName: ItemsBase.itemName })
|
.select({ itemName: ItemsBase.itemName })
|
||||||
.from(ItemsBase)
|
.from(ItemsBase)
|
||||||
.where(inArray(ItemsBase.itemName, classnames));
|
.where(inArray(ItemsBase.itemName, classnames));
|
||||||
for (const r of rows) have.add(r.itemName);
|
for (const r of rows) have.add(normalizeClassname(r.itemName));
|
||||||
}
|
}
|
||||||
return all.filter((e) => !have.has(e.classname)).map((e) => e.classname);
|
return all
|
||||||
|
.filter((e) => !have.has(normalizeClassname(e.classname)))
|
||||||
|
.map((e) => e.classname);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeClassname } from "@/lib/furni/classname";
|
||||||
/**
|
/**
|
||||||
* Furniture search & listing — powered by official Habbo furnidata.
|
* Furniture search & listing — powered by official Habbo furnidata.
|
||||||
*
|
*
|
||||||
@@ -37,7 +38,9 @@ export async function listFurniture(
|
|||||||
const perPage = Math.min(options.perPage || 50, 100);
|
const perPage = Math.min(options.perPage || 50, 100);
|
||||||
const page = options.page || 1;
|
const page = options.page || 1;
|
||||||
const search = options.search?.toLowerCase().trim() || "";
|
const search = options.search?.toLowerCase().trim() || "";
|
||||||
const importedSet = options.importedSet;
|
const importedSet = options.importedSet
|
||||||
|
? new Set([...options.importedSet].map(normalizeClassname))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const map = await getOfficialHabboFurnidata();
|
const map = await getOfficialHabboFurnidata();
|
||||||
@@ -58,8 +61,8 @@ export async function listFurniture(
|
|||||||
if (importedSet) {
|
if (importedSet) {
|
||||||
// Non-imported first (id desc), then imported (id desc).
|
// Non-imported first (id desc), then imported (id desc).
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
const aImp = importedSet.has(a.classname) ? 1 : 0;
|
const aImp = importedSet.has(normalizeClassname(a.classname)) ? 1 : 0;
|
||||||
const bImp = importedSet.has(b.classname) ? 1 : 0;
|
const bImp = importedSet.has(normalizeClassname(b.classname)) ? 1 : 0;
|
||||||
if (aImp !== bImp) return aImp - bImp;
|
if (aImp !== bImp) return aImp - bImp;
|
||||||
return b.id - a.id;
|
return b.id - a.id;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user