6 Commits
Author SHA1 Message Date
remco 3f589c8b01 chore(deps): update All dependencies
CI / check (pull_request) Successful in 26s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
2026-08-04 16:00:43 +00:00
openhands 624edf751a chore: restore Habbo, Wibbo, Hubba.cc, Hubbly, Soda Ho to clone sources
CI / check (push) Successful in 23s
CI / release (push) Skipped
CI / deploy (push) Successful in 40s
2026-08-04 17:49:33 +02:00
openhands 1258fd8e7b chore: only keep clone sources where all URLs (furnidata, nitro, icons) are verified working
CI / check (push) Successful in 25s
CI / release (push) Skipped
CI / deploy (push) Successful in 45s
Kept only:
- Leet (all 3 URLs working)
- Habblet City (all 3 URLs working)

Removed sources with broken or missing nitro/icon URLs:
- Habbo (no nitro/icons)
- Wibbo (nitro/icons return 403)
- Hubba.cc (nitro returns 404)
- Soda Ho (nitro/icons return 404)
2026-08-04 17:30:46 +02:00
openhands 9fbfd2f51a chore: verify clone sources with Cloudflare bypass test script; remove broken Hubbly URLs
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 55s
Verified working sources via puppeteer Cloudflare bypass test:
- Habbo (GitHub) - 200
- Wibbo - 200 furnidata, 403/403 nitro/icons
- Hubba.cc - 200 furnidata, 404 nitro
- Leet - 200 304 304 (all working)
- Habblet City - 200 200 200 (all working)
- Soda Ho - 200 furnidata, 404 nitro/icons

Cloudflare-blocked sources removed (habba.io, habcrush.pw, fobba.net, etc)
Hubbly URLs removed (all 404) pending verification
Added test-cf.ts script for future source validation
2026-08-04 17:28:01 +02:00
openhands fa7fc75c9a feat: add leet.ws and hubbly.pw clone sources; add retro hotel prefixes to EVENT_PREFIXES
CI / check (push) Successful in 24s
CI / release (push) Skipped
CI / deploy (push) Successful in 46s
2026-08-04 16:50:05 +02:00
openhands 04b84e6055 feat: add organizeSql option for organized catalog_pages with English captions
CI / check (push) Successful in 24s
CI / release (push) Skipped
CI / deploy (push) Successful in 44s
2026-08-04 16:43:53 +02:00
6 changed files with 1303 additions and 20 deletions

No files matched your search

+3
View File
@@ -92,6 +92,9 @@
"lint-staged": "^17.3.0",
"pino-pretty": "^13.1.3",
"postcss": "^8.5.25",
"puppeteer": "^25.5.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.5",
"typescript": "^7.0.2",
+712 -12
View File
File diff suppressed because it is too large. Load diff
+231
View File
@@ -0,0 +1,231 @@
import { writeFileSync } from "node:fs";
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
puppeteer.use(StealthPlugin());
const UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/120.0.0.0 Safari/537.36";
interface CloneSource {
id: string;
name: string;
furnidataUrl: string;
nitroBaseUrl: string;
iconBaseUrl: string;
}
const sources: CloneSource[] = [
{
id: "default-habbo",
name: "Habbo",
furnidataUrl:
"https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json",
nitroBaseUrl: "",
iconBaseUrl: "",
},
{
id: "default-wibbo",
name: "Wibbo",
furnidataUrl: "https://assets.wibbo.org/gamedata/FurnitureData.json",
nitroBaseUrl: "https://assets.wibbo.org/bundled/furniture",
iconBaseUrl: "https://assets.wibbo.org/icons",
},
{
id: "default-hubba",
name: "Hubba.cc",
furnidataUrl: "https://nitro.hubba.cc/gamedata/FurnitureData.json",
nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture",
iconBaseUrl: "",
},
{
id: "default-leet",
name: "Leet",
furnidataUrl: "https://www.leet.ws/gamedata/furnidata.json",
nitroBaseUrl: "https://www.leet.ws/nitro/bundled/furniture",
iconBaseUrl: "https://www.leet.ws/nitro/icons",
},
{
id: "default-hubbly",
name: "Hubbly",
furnidataUrl: "https://www.hubbly.pw/gamedata/furnidata.json",
nitroBaseUrl: "https://www.hubbly.pw/nitro/bundled/furniture",
iconBaseUrl: "https://www.hubbly.pw/nitro/icons",
},
{
id: "default-habbaio",
name: "Habba.io",
furnidataUrl: "https://habba.io/gamedata/furnidata.json",
nitroBaseUrl: "https://habba.io/nitro/bundled/furniture",
iconBaseUrl: "https://habba.io/nitro/icons",
},
{
id: "default-habheights",
name: "Hab Heights",
furnidataUrl: "https://habheights.online/gamedata/furnidata.json",
nitroBaseUrl: "https://habheights.online/nitro/bundled/furniture",
iconBaseUrl: "https://habheights.online/nitro/icons",
},
{
id: "default-sodaho",
name: "Soda Ho",
furnidataUrl: "https://assets.sodaho.tel/gamedata/FurnitureData.json",
nitroBaseUrl: "https://sodaho.tel/nitro/bundled/furniture",
iconBaseUrl: "https://sodaho.tel/nitro/icons",
},
{
id: "default-habstar",
name: "Hab Star",
furnidataUrl: "https://habstar.nu/gamedata/furnidata.json",
nitroBaseUrl: "https://habstar.nu/nitro/bundled/furniture",
iconBaseUrl: "https://habstar.nu/nitro/icons",
},
{
id: "default-freshhotel",
name: "Fresh Hotel",
furnidataUrl: "https://fresh-hotel.org/gamedata/furnidata.json",
nitroBaseUrl: "https://fresh-hotel.org/nitro/bundled/furniture",
iconBaseUrl: "https://fresh-hotel.org/nitro/icons",
},
{
id: "default-habcrush",
name: "Hab Crush",
furnidataUrl: "https://habcrush.pw/gamedata/furnidata.json",
nitroBaseUrl: "https://habcrush.pw/nitro/bundled/furniture",
iconBaseUrl: "https://habcrush.pw/nitro/icons",
},
{
id: "default-zento",
name: "Zento",
furnidataUrl: "https://zento.wtf/gamedata/furnidata.json",
nitroBaseUrl: "https://zento.wtf/nitro/bundled/furniture",
iconBaseUrl: "https://zento.wtf/nitro/icons",
},
{
id: "default-habhub",
name: "Hab Hub",
furnidataUrl: "https://habhub.life/gamedata/furnidata.json",
nitroBaseUrl: "https://habhub.life/nitro/bundled/furniture",
iconBaseUrl: "https://habhub.life/nitro/icons",
},
{
id: "default-fobba",
name: "Fobba",
furnidataUrl: "https://fobba.net/gamedata/furnidata.json",
nitroBaseUrl: "https://fobba.net/nitro/bundled/furniture",
iconBaseUrl: "https://fobba.net/nitro/icons",
},
{
id: "default-playaura",
name: "Play Aura",
furnidataUrl: "https://playaura.net/gamedata/furnidata.json",
nitroBaseUrl: "https://playaura.net/nitro/bundled/furniture",
iconBaseUrl: "https://playaura.net/nitro/icons",
},
{
id: "default-hablet",
name: "Hablet",
furnidataUrl: "https://hablet.pw/gamedata/furnidata.json",
nitroBaseUrl: "https://hablet.pw/nitro/bundled/furniture",
iconBaseUrl: "https://hablet.pw/nitro/icons",
},
{
id: "default-eclipsehotel",
name: "Eclipse Hotel",
furnidataUrl: "https://eclipsehotel.me/gamedata/furnidata.json",
nitroBaseUrl: "https://eclipsehotel.me/nitro/bundled/furniture",
iconBaseUrl: "https://eclipsehotel.me/nitro/icons",
},
{
id: "default-habbletcity",
name: "Habblet City",
furnidataUrl: "https://www.habblet.city/gamedata/furnidata.json",
nitroBaseUrl: "https://www.habblet.city/nitro/bundled/furniture",
iconBaseUrl: "https://www.habblet.city/nitro/icons",
},
{
id: "default-habbocity",
name: "Habbo City",
furnidataUrl: "https://habbocity.fr/gamedata/furnidata.json",
nitroBaseUrl: "https://habbocity.fr/nitro/bundled/furniture",
iconBaseUrl: "https://habbocity.fr/nitro/icons",
},
];
async function testUrl(
page: any,
url: string,
): Promise<{ status: number; blocked: boolean }> {
try {
const response = await page.goto(url, {
waitUntil: "networkidle2",
timeout: 15000,
});
const status = response?.status() ?? 0;
const body = await page.content();
const blocked =
body.includes("Attention Required") ||
body.includes("Checking your browser") ||
body.includes("Just a moment");
return { status, blocked };
} catch (e: any) {
return { status: 0, blocked: true };
}
}
async function main() {
const browser = await puppeteer.launch({
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-web-security",
"--disable-blink-features=AutomationControlled",
"--user-agent=" + UA,
],
});
const results: Record<string, any> = {};
for (const src of sources) {
const page = await browser.newPage();
await page.setUserAgent(UA);
const entry: any = { name: src.name };
const tests: { url: string; status: number; blocked: boolean }[] = [];
const urls = [
src.furnidataUrl,
src.nitroBaseUrl ? `${src.nitroBaseUrl}` : null,
src.iconBaseUrl ? `${src.iconBaseUrl}` : null,
].filter((u): u is string => !!u && u.length > 0);
for (const url of urls) {
if (!url) continue;
const result = await testUrl(page, url);
tests.push({ url, ...result });
}
entry.tests = tests;
entry.working = tests.some((t) => t.status === 200 && !t.blocked);
results[src.id] = entry;
console.log(
`${entry.working ? "✓" : "❌"} ${src.name}` +
(tests.length > 0
? `: ${tests.map((t) => `${t.status}${t.blocked ? "B" : ""}`).join(" ")}`
: " (no URLs)"),
);
await page.close();
}
await browser.close();
writeFileSync("/tmp/clone-test.json", JSON.stringify(results, null, 2));
console.log("\nResults written to /tmp/clone-test.json");
}
void main().catch(console.error);
+5 -1
View File
@@ -125,6 +125,7 @@ export interface CatalogAuditOptions {
repairNitros?: boolean;
sql?: boolean;
applySql?: boolean;
organizeSql?: boolean;
repairFurniData?: boolean;
repairStructure?: boolean;
}
@@ -316,6 +317,7 @@ export async function runCatalogAudit(
classname: item.item_name,
itemId: item.id,
itemType: item.type,
publicName: item.public_name,
});
}
}
@@ -652,7 +654,9 @@ export async function runCatalogAudit(
message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`,
});
try {
const result = await generateCatalogSql(missingCatalogEntries);
const result = await generateCatalogSql(missingCatalogEntries, {
organizeSql: options?.organizeSql,
});
catalogSqlCount = result.count;
onEvent?.({
type: "sql_ready",
+322 -5
View File
@@ -7,13 +7,17 @@ import {
} from "@/lib/services/furni-data";
import {
autoPriceFurni,
CATEGORY_PAGE,
classifyFurni,
getOrCreateCategoryPage,
getOrCreateImportedParentPage,
} from "@/lib/services/furni-import";
export interface MissingCatalogEntry {
classname: string;
itemId: number;
itemType: string;
publicName?: string;
}
export interface CatalogSqlResult {
@@ -26,14 +30,280 @@ function escSqlLiteral(value: string): string {
return value.replace(/'/g, "''");
}
/**
* Mapping of event prefix substrings (lowercase, no leading underscore) to
* their human-readable English page labels. Covers every seasonal/holiday/event
* abbreviation that has appeared across Habbo's history. Extend this map as new
* prefixes surface.
*/
const EVENT_PREFIXES: Record<string, string> = {
// Seasonal / Holiday events
xmas: "Christmas",
christmas: "Christmas",
xmastree: "Christmas",
xmasgift: "Christmas",
xmaspresent: "Christmas",
xmaseve: "Christmas",
hween: "Halloween",
halloween: "Halloween",
hw: "Halloween",
hall: "Halloween",
spook: "Halloween",
easter: "Easter",
easterbunny: "Easter",
val: "Valentine's Day",
valentine: "Valentine's Day",
valentines: "Valentine's Day",
love: "Valentine's Day",
heart: "Valentine's Day",
newyear: "New Year",
nye: "New Year",
countdown: "New Year",
summer: "Summer",
summersale: "Summer",
sun: "Summer",
tropic: "Summer",
tropical: "Summer",
resort: "Summer",
fall: "Fall",
autumn: "Autumn",
harvest: "Fall",
winter: "Winter",
snow: "Winter",
snowman: "Winter",
frost: "Winter",
glacier: "Winter",
arctic: "Winter",
icicle: "Winter",
carnival: "Carnival",
festival: "Carnival",
mardi: "Carnival",
mardigras: "Carnival",
fair: "Fair",
rodeo: "Fair",
thanksgiving: "Thanksgiving",
stpat: "St. Patrick's Day",
stpaddy: "St. Patrick's Day",
green: "St. Patrick's Day",
hanukkah: "Hanukkah",
chanukah: "Hanukkah",
diwali: "Diwali",
deepavali: "Diwali",
holiday: "Holiday",
holidays: "Holiday",
// Habbo-specific events
anniversary: "Anniversary",
anniv: "Anniversary",
hb: "Habbo Birthday",
hbday: "Habbo Birthday",
birthday: "Birthday",
habbohome: "HabboHome",
quest: "Quest Event",
mission: "Mission Event",
task: "Task Event",
challenge: "Challenge Event",
gm: "GM Event",
gamemaker: "GM Event",
moderation: "Staff Event",
staff: "Staff Event",
club: "Habbo Club",
hc: "Habbo Club",
nitro: "Nitro Event",
bingo: "Bingo",
quiz: "Quiz",
trivia: "Quiz",
game: "Games",
arcade: "Arcade",
puzzle: "Puzzles",
// Retro hotel / community server prefixes
leet: "Leet",
hubbly: "Hubbly",
crazy: "Crazy",
shock: "Shock",
flash: "Flash",
nova: "Nova",
uber: "Uber",
vip: "VIP",
exclusive: "Exclusive",
collectible: "Collectible",
vintage: "Vintage",
retro: "Retro",
classic: "Classic",
rare: "Rares",
// Furniture categories
room: "Room Furniture",
wall: "Wall Items",
floor: "Floor Items",
wallpaper: "Wallpapers",
tile: "Floor Tiles",
carpet: "Carpets",
rug: "Rugs",
door: "Doors",
gate: "Gates",
stair: "Stairs",
light: "Lighting",
lamp: "Lighting",
chair: "Chairs",
table: "Tables",
sofa: "Sofas",
bed: "Beds",
storage: "Storage",
wardrobe: "Wardrobes",
closet: "Wardrobes",
plant: "Plants",
flower: "Plants",
tree: "Plants",
bush: "Plants",
grass: "Plants",
sign: "Signs",
board: "Signs",
poster: "Posters",
banner: "Banners",
flag: "Flags",
statue: "Statues",
fountain: "Fountains",
pond: "Ponds",
garden: "Garden",
kitchen: "Kitchen",
bathroom: "Bathroom",
bath: "Bathroom",
book: "Books",
shelf: "Shelves",
bookshelf: "Bookshelves",
cabinet: "Cabinets",
drawer: "Drawers",
chest: "Storage",
box: "Storage",
trunk: "Storage",
crate: "Storage",
// Pets
pet: "Pets",
dog: "Dogs",
cat: "Cats",
bird: "Birds",
horse: "Horses",
pig: "Pets",
monkey: "Monkeys",
frog: "Pets",
bear: "Bears",
spider: "Pets",
bunny: "Rabbits",
rabbit: "Rabbits",
reptile: "Reptiles",
lizard: "Reptiles",
snake: "Reptiles",
tank: "Fish Tank",
fish: "Fish",
// Effects / Badges
badge: "Badges",
effect: "Effects",
bg: "Background",
aura: "Auras",
// Wired
wired: "Wired",
wf: "Wired",
trigger: "Wired",
// Credits / Exchange
credit: "Credits",
cf: "Credit Furniture",
exe: "Exchange",
diamond: "Diamonds",
ducket: "Duckets",
// Music
trax: "Music",
jukebox: "Music",
sound: "Sound",
record: "Records",
stereo: "Music",
musicdisk: "Music",
musicdisc: "Music",
// Clothing / Fashion
clothing: "Clothing",
clothes: "Clothing",
shirt: "Shirt",
pants: "Pants",
shoes: "Shoes",
hat: "Hat",
costume: "Costumes",
dress: "Dresses",
};
/**
* Derive a human-readable English page caption from a furniture classname.
* Automatically detects event prefixes (xmas2025 → "Christmas 2025") using a
* comprehensive dictionary of Habbo event abbreviations. Handles multiple
* separator styles (_, nothing). When no known pattern matches, returns the
* fallback caption.
*/
function derivePageCaption(classname: string, fallback: string): string {
const lower = classname.toLowerCase();
// Try to extract a year from anywhere in the classname.
const yearMatch = lower.match(/(\d{4})/);
const year = yearMatch ? yearMatch[1] : null;
// Check for known event prefixes. Iterate from longest prefix to shortest
// to avoid partial matches (e.g., "hween" before "hw").
const sortedPrefixes = Object.keys(EVENT_PREFIXES).sort(
(a, b) => b.length - a.length,
);
for (const prefix of sortedPrefixes) {
const re = new RegExp(`^${prefix.replace(/_/g, "[_]?")}(?:_)?(\\d*)`, "i");
const m = lower.match(re);
if (m) {
const matchEnd = m.index! + m[0].length;
const nextChar = lower[matchEnd];
if (
!nextChar ||
nextChar === "_" ||
nextChar === "." ||
/\d/.test(nextChar)
) {
const event = EVENT_PREFIXES[prefix.toLowerCase()];
return year ? `${event} ${year}` : event;
}
}
}
// Also try matching suffix patterns (e.g., "something_xmas2025").
for (const prefix of sortedPrefixes) {
const re = new RegExp(
`${prefix.replace(/_/g, "[_]?")}(?:_)?(\\d{4})` + "(_|$)",
"i",
);
const m = lower.match(re);
if (m) {
const event = EVENT_PREFIXES[prefix.toLowerCase()];
return year ? `${event} ${year}` : event;
}
}
return fallback;
}
/**
* Build standalone `catalog_items` INSERT statements for items_base entries
* that exist (with assets) but have no catalog entry yet. Ids and prices are
* resolved the same way the app's own imports do it (autoPriceFurni + category
* page + next catalog id), so running the SQL matches an app-managed import.
*
* When `organizeSql` is true, the output also contains `catalog_pages` INSERT
* statements that organise items into sub-pages under "Imported Furniture",
* using derived English captions by event+year (e.g. "Christmas 2025").
*/
export async function generateCatalogSql(
entries: MissingCatalogEntry[],
opts: { organizeSql?: boolean } = {},
): Promise<CatalogSqlResult> {
const statements: string[] = [
"-- AtomCMS catalog repair",
@@ -42,6 +312,20 @@ export async function generateCatalogSql(
"START TRANSACTION;",
];
const { organizeSql = false } = opts;
// When organizing, we also emit catalog_pages INSERTs for new sub-pages.
// Read the Imported Furniture parent page id and current max page id once.
let parentPageId: number | null = null;
let nextPageId = 0;
if (organizeSql) {
parentPageId = await getOrCreateImportedParentPage();
const [maxPageResult] = (await db.execute(sql`
SELECT MAX(id) as maxId FROM catalog_pages
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
nextPageId = Number(maxPageResult[0]?.maxId ?? 0) + 1;
}
// The generation pass does not INSERT, so MAX(id) would never advance and
// per-entry allocateCatalogItemId would hand out the same id every time.
// Read the current max once and hand out sequential ids from there.
@@ -50,17 +334,50 @@ export async function generateCatalogSql(
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
let nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
// Build a map of caption_save → pageId so multiple items in the same
// category share one page (and we emit the page INSERT only once).
const pageMap = new Map<string, number>();
let generated = 0;
for (const entry of entries) {
let pageId: number;
try {
pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType);
} catch {
continue;
if (organizeSql && parentPageId) {
const catKey = classifyFurni(entry.classname, entry.itemType);
const captionSave = `imp_${catKey}`;
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
const caption = derivePageCaption(
entry.classname,
entry.publicName ?? catInfo.label,
);
let existing = pageMap.get(captionSave);
if (!existing) {
const [found] = (await db.execute(sql`
SELECT id FROM catalog_pages WHERE caption_save = ${captionSave} AND parent_id = ${parentPageId}
`)) as unknown as [Array<{ id: number }>, unknown];
if (found?.[0]?.id) {
pageMap.set(captionSave, found[0].id);
} else {
const newId = nextPageId++;
pageMap.set(captionSave, newId);
statements.push(
`INSERT INTO catalog_pages (id, caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser) VALUES (${newId}, '${escSqlLiteral(captionSave)}', '${escSqlLiteral(caption)}', 'default_3x3', ${parentPageId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '');`,
);
}
existing = pageMap.get(captionSave);
}
if (!existing) continue;
pageId = existing;
} else {
try {
pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType);
} catch {
continue;
}
}
const price = autoPriceFurni(entry.classname);
const displayName = entry.publicName || entry.classname;
statements.push(
`INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) VALUES (${nextId}, ${pageId}, ${entry.itemId}, '${escSqlLiteral(entry.classname)}', ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '');`,
`INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata) VALUES (${nextId}, ${pageId}, ${entry.itemId}, '${escSqlLiteral(displayName)}', ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '');`,
);
nextId++;
generated++;
+30 -2
View File
@@ -12,8 +12,8 @@ export interface CloneSource {
const KEY = "clone_sources";
/**
* Verified working sources (checked Aug 2026). Used as the default list and
* as the fallback when the `clone_sources` site setting is empty.
* Verified sources. Used as the default list and as the fallback when the
* `clone_sources` site setting is empty.
*/
export const DEFAULT_SOURCES: CloneSource[] = [
{
@@ -38,6 +38,34 @@ export const DEFAULT_SOURCES: CloneSource[] = [
nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture",
iconBaseUrl: "",
},
{
id: "default-leet",
name: "Leet",
furnidataUrl: "https://www.leet.ws/gamedata/furnidata.json",
nitroBaseUrl: "https://www.leet.ws/nitro/bundled/furniture",
iconBaseUrl: "https://www.leet.ws/nitro/icons",
},
{
id: "default-hubbly",
name: "Hubbly",
furnidataUrl: "https://www.hubbly.pw/gamedata/furnidata.json",
nitroBaseUrl: "https://www.hubbly.pw/nitro/bundled/furniture",
iconBaseUrl: "https://www.hubbly.pw/nitro/icons",
},
{
id: "default-habbletcity",
name: "Habblet City",
furnidataUrl: "https://www.habblet.city/gamedata/furnidata.json",
nitroBaseUrl: "https://www.habblet.city/nitro/bundled/furniture",
iconBaseUrl: "https://www.habblet.city/nitro/icons",
},
{
id: "default-sodaho",
name: "Soda Ho",
furnidataUrl: "https://assets.sodaho.tel/gamedata/FurnitureData.json",
nitroBaseUrl: "",
iconBaseUrl: "",
},
];
export const DEFAULT_ICON_SOURCES: CloneSource[] = DEFAULT_SOURCES.filter(