diff --git a/src/app/admin/import/clone/import-clone-client.tsx b/src/app/admin/import/clone/import-clone-client.tsx
index 095fb34e08..da1dd32b45 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 8b3fcca3cf..1c235bf56b 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 7b06f4cd09..dd5859755f 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 19f0f02ecd..280004aefd 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 f5bc789034..605ecc924a 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;