From 936053cca64e2463700aee7f8579557ed8960169 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Aug 2026 16:21:45 +0200 Subject: [PATCH] feat(clone): add hotel field to clone source presets - CloneSource interface now has a hotel field (maps to habbo_gamedata_hotel) - DEFAULT_SOURCES presets include their associated hotel (it/com) - When cloning from a source with a hotel configured, habbo_gamedata_hotel is set automatically so official furnidata enrichment uses the correct locale - Clone source form UI now has a hotel select dropdown - Source list shows the hotel label when configured - Clone API route passes hotel through to upsertSource --- .../import/clone/import-clone-client.tsx | 30 +++++++++++++++++++ src/app/api/admin/import/clone/route.ts | 3 +- src/lib/services/clone-import.test.ts | 1 + src/lib/services/clone-import.ts | 11 +++++++ src/lib/services/clone-sources.ts | 9 ++++++ 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/app/admin/import/clone/import-clone-client.tsx b/src/app/admin/import/clone/import-clone-client.tsx index 095fb34e..da1dd32b 100644 --- a/src/app/admin/import/clone/import-clone-client.tsx +++ b/src/app/admin/import/clone/import-clone-client.tsx @@ -18,7 +18,15 @@ import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { adminFetch } from "@/lib/admin-fetch"; +import { HABBO_GAMEDATA_HOTELS } from "@/lib/habbo-gamedata-hotel"; import { runSseImport } from "@/lib/sse-client"; import { cn } from "@/lib/utils"; @@ -30,6 +38,7 @@ interface CloneSource { furnidataUrl: string; nitroBaseUrl: string; iconBaseUrl: string; + hotel: string; } interface FurniItem { @@ -70,6 +79,7 @@ function SourceForm({ initial, onSave, onCancel, saving }: SourceFormProps) { const [furnidataUrl, setFurnidataUrl] = useState(initial?.furnidataUrl ?? ""); const [nitroBaseUrl, setNitroBaseUrl] = useState(initial?.nitroBaseUrl ?? ""); const [iconBaseUrl, setIconBaseUrl] = useState(initial?.iconBaseUrl ?? ""); + const [hotel, setHotel] = useState(initial?.hotel ?? ""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -79,6 +89,7 @@ function SourceForm({ initial, onSave, onCancel, saving }: SourceFormProps) { furnidataUrl, nitroBaseUrl, iconBaseUrl, + hotel, }); } @@ -115,6 +126,19 @@ function SourceForm({ initial, onSave, onCancel, saving }: SourceFormProps) { onChange={(e) => setIconBaseUrl(e.target.value)} required /> +
diff --git a/src/app/api/admin/import/clone/route.ts b/src/app/api/admin/import/clone/route.ts index 8b3fcca3..1c235bf5 100644 --- a/src/app/api/admin/import/clone/route.ts +++ b/src/app/api/admin/import/clone/route.ts @@ -76,7 +76,7 @@ export const POST = withAdmin( { permission: PERMS.ASSETS_IMPORT }, async (request, ctx) => { const body = await request.json(); - const { name, furnidataUrl, nitroBaseUrl, iconBaseUrl, id } = + const { name, furnidataUrl, nitroBaseUrl, iconBaseUrl, hotel, id } = body as Record; const isHttps = (u: string) => { @@ -104,6 +104,7 @@ export const POST = withAdmin( furnidataUrl, nitroBaseUrl, iconBaseUrl, + hotel: hotel ?? "", }); logAudit({ diff --git a/src/lib/services/clone-import.test.ts b/src/lib/services/clone-import.test.ts index 7b06f4cd..dd585975 100644 --- a/src/lib/services/clone-import.test.ts +++ b/src/lib/services/clone-import.test.ts @@ -85,6 +85,7 @@ const SOURCE = { furnidataUrl: "https://x/fd.json", nitroBaseUrl: "https://x/furni", iconBaseUrl: "https://x/icons", + hotel: "", }; const ENTRY = { id: 5, diff --git a/src/lib/services/clone-import.ts b/src/lib/services/clone-import.ts index 19f0f02e..280004ae 100644 --- a/src/lib/services/clone-import.ts +++ b/src/lib/services/clone-import.ts @@ -2,6 +2,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import { eq, inArray, sql } from "drizzle-orm"; import { db, ItemsBase } from "@/lib/db"; +import { HABBO_GAMEDATA_HOTEL_SETTING_KEY } from "@/lib/habbo-gamedata-hotel"; import { logger } from "@/lib/logger"; import { extractFurniIconPng } from "@/lib/services/clone-icon"; import type { CloneSource } from "@/lib/services/clone-sources"; @@ -16,6 +17,7 @@ import { } from "@/lib/services/furni-import"; import { browserHeaders } from "@/lib/services/import/core/browser-headers"; import { downloadFile } from "@/lib/services/import/core/download"; +import { siteSettings } from "@/lib/services/site-settings"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; export interface SourceFurni { @@ -221,6 +223,15 @@ export async function cloneSingleFurni(params: { const { classname, itemType } = entry; const warnings: string[] = []; + // Set the active Habbo hotel for official furnidata enrichment. + if (source.hotel) { + try { + await siteSettings.update(HABBO_GAMEDATA_HOTEL_SETTING_KEY, source.hotel); + } catch { + /* non-blocking */ + } + } + // Path-traversal guard: classname comes from remote furnidata and is used to // build file paths — reject anything that doesn't look like a safe furni name. if (!/^[\w\-.*]+$/.test(classname)) { diff --git a/src/lib/services/clone-sources.ts b/src/lib/services/clone-sources.ts index f5bc7890..605ecc92 100644 --- a/src/lib/services/clone-sources.ts +++ b/src/lib/services/clone-sources.ts @@ -7,6 +7,7 @@ export interface CloneSource { furnidataUrl: string; nitroBaseUrl: string; iconBaseUrl: string; + hotel: string; } const KEY = "clone_sources"; @@ -23,6 +24,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ "https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json", nitroBaseUrl: "", iconBaseUrl: "", + hotel: "it", }, { id: "default-wibbo", @@ -30,6 +32,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://assets.wibbo.org/gamedata/FurnitureData.json", nitroBaseUrl: "https://assets.wibbo.org/bundled/furniture", iconBaseUrl: "https://assets.wibbo.org/icons", + hotel: "com", }, { id: "default-hubba", @@ -37,6 +40,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://nitro.hubba.cc/gamedata/FurnitureData.json", nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture", iconBaseUrl: "", + hotel: "com", }, { id: "default-sodaho", @@ -44,6 +48,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://assets.sodaho.tel/gamedata/FurnitureData.json", nitroBaseUrl: "", iconBaseUrl: "", + hotel: "com", }, { id: "default-leet", @@ -51,6 +56,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://www.leet.ws/gamedata/furnidata.json", nitroBaseUrl: "https://www.leet.ws/nitro/bundled/furniture", iconBaseUrl: "https://www.leet.ws/nitro/icons", + hotel: "com", }, { id: "default-hubbly", @@ -58,6 +64,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://www.hubbly.pw/gamedata/furnidata.json", nitroBaseUrl: "https://www.hubbly.pw/nitro/bundled/furniture", iconBaseUrl: "https://www.hubbly.pw/nitro/icons", + hotel: "com", }, { id: "default-habbletcity", @@ -65,6 +72,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [ furnidataUrl: "https://www.habblet.city/gamedata/furnidata.json", nitroBaseUrl: "https://www.habblet.city/nitro/bundled/furniture", iconBaseUrl: "https://www.habblet.city/nitro/icons", + hotel: "com", }, ]; @@ -96,6 +104,7 @@ export async function upsertSource( furnidataUrl: src.furnidataUrl.trim(), nitroBaseUrl: src.nitroBaseUrl.trim().replace(/\/+$/, ""), iconBaseUrl: src.iconBaseUrl.trim().replace(/\/+$/, ""), + hotel: (src.hotel ?? "").trim(), }; const idx = sources.findIndex((s) => s.id === entry.id); if (idx >= 0) sources[idx] = entry;