diff --git a/src/app/api/admin/import/furni/route.ts b/src/app/api/admin/import/furni/route.ts index 06681d8c23..d7696487d4 100644 --- a/src/app/api/admin/import/furni/route.ts +++ b/src/app/api/admin/import/furni/route.ts @@ -391,7 +391,7 @@ export const GET = withAdmin( type: item.itemType === "i" ? "wallitem" : "flooritem", name: item.name, description: item.description, - revision: 0, + revision: Number(item.revision) || 0, category: String(item.category ?? ""), ...presence, nitroExists, diff --git a/src/lib/services/clone-import.test.ts b/src/lib/services/clone-import.test.ts index 43e0fd274e..b059e17750 100644 --- a/src/lib/services/clone-import.test.ts +++ b/src/lib/services/clone-import.test.ts @@ -304,3 +304,14 @@ describe("fetchSourceFurnidata", () => { ).rejects.toThrow(/Cloudflare challenge/); }); }); + +it("preserves numeric source revisions for asset downloads", () => { + const [entry] = parseFurnidata({ + roomitemtypes: { + furnitype: [ + { id: 16940, classname: "recycler_kintsugiB", revision: "73706" }, + ], + }, + }); + expect(entry.revision).toBe(73706); +}); diff --git a/src/lib/services/clone-import.ts b/src/lib/services/clone-import.ts index 7b82ba89bb..a133fae37e 100644 --- a/src/lib/services/clone-import.ts +++ b/src/lib/services/clone-import.ts @@ -69,6 +69,10 @@ function toSourceFurni( return { ...raw, id: Number(raw.id ?? 0), + revision: + Number.isSafeInteger(Number(raw.revision)) && Number(raw.revision) > 0 + ? Number(raw.revision) + : 0, classname: String(raw.classname ?? ""), name: String(raw.name ?? raw.classname ?? ""), description: String(raw.description ?? ""), diff --git a/src/lib/services/furni-import-repair.test.ts b/src/lib/services/furni-import-repair.test.ts index d111cd0bd8..d1c006fbbb 100644 --- a/src/lib/services/furni-import-repair.test.ts +++ b/src/lib/services/furni-import-repair.test.ts @@ -4,6 +4,8 @@ import { MySqlDialect } from "drizzle-orm/mysql-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ + official: vi.fn(), + download: vi.fn(), execute: vi.fn(), select: vi.fn(), stat: vi.fn(), @@ -43,14 +45,14 @@ vi.mock("@/lib/services/furni-data", () => ({ appendFurniEntry: mocks.append, })); vi.mock("@/lib/services/habbo-furnidata-cache", () => ({ - lookupOfficialHabboFurni: async () => null, + lookupOfficialHabboFurni: mocks.official, })); vi.mock("@/lib/services/habbo-gamedata-hotel", () => ({ getHabboGamedataHotel: async () => "it", })); vi.mock("@/lib/services/import/core/download", () => ({ downloadFile: vi.fn(), - tryDownloadCandidates: async () => false, + tryDownloadCandidates: mocks.download, })); vi.mock("@/lib/services/swf-to-nitro", () => ({ parseNitroBundle: () => ({ json: {} }), @@ -76,6 +78,8 @@ function queries() { describe("completion of existing furniture", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.official.mockResolvedValue(null); + mocks.download.mockResolvedValue(false); const rows = [ [{ id: 2000037257, spriteId: 9001, type: "s" }], [{ itemName: input.classname }], @@ -140,13 +144,22 @@ it("imports recycler_kintsugiB with an allocated ID throughout SQL and furnidata return [[{ id: 123, page_id: "15" }], []]; return [[], []]; }); + mocks.official.mockResolvedValue({ revision: 73706, xdim: 1, ydim: 1 }); const result = await importSingleFurni({ ...input, + revision: 0, id: 16940, classname: "recycler_kintsugiB", }); expect(result.ok).toBe(true); expect(result.itemId).toBe(2000037258); + expect(mocks.download).toHaveBeenCalledWith( + expect.arrayContaining([ + "https://images.habbo.com/dcr/hof_furni/73706/recycler_kintsugiB.swf", + ]), + expect.any(String), + "swf", + ); expect(mocks.build).toHaveBeenCalledWith( expect.objectContaining({ id: 2000037258, diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index fa4c78b6f6..fdfd92a787 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -659,7 +659,7 @@ export async function importSingleFurni(params: { } // ── Derive file-safe names ──────────────────────────────────────── - const rev = revision || 0; + const rev = bestRevision; const assetTargets = await getFurniAssetWriteTargets(); const { swfDir, iconDir, nitroDir } = assetTargets; const starIdx = classname.indexOf("*");