Compare commits
6
Commits
f64c68581b
...
3f589c8b01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f589c8b01 | ||
|
|
624edf751a | ||
|
|
1258fd8e7b | ||
|
|
9fbfd2f51a | ||
|
|
fa7fc75c9a | ||
|
|
04b84e6055 |
No files matched your search
+13
-10
@@ -35,11 +35,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.7.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.5.7",
|
"@hookform/resolvers": "^5.7.1",
|
||||||
"@tanstack/react-virtual": "^3.14.9",
|
"@tanstack/react-virtual": "^3.14.9",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"ioredis": "^5.11.1",
|
"ioredis": "^5.11.1",
|
||||||
"isomorphic-dompurify": "^3.20.0",
|
"isomorphic-dompurify": "^3.21.0",
|
||||||
"jpeg-js": "^0.4.4",
|
"jpeg-js": "^0.4.4",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
@@ -58,14 +58,14 @@
|
|||||||
"motion": "^12.43.0",
|
"motion": "^12.43.0",
|
||||||
"music-metadata": "^11.14.0",
|
"music-metadata": "^11.14.0",
|
||||||
"mysql2": "^3.23.2",
|
"mysql2": "^3.23.2",
|
||||||
"next": "^16.2.12",
|
"next": "^16.3.0",
|
||||||
"next-auth": "5.0.0-beta.32",
|
"next-auth": "5.0.0-beta.32",
|
||||||
"next-intl": "^4.13.4",
|
"next-intl": "^4.13.5",
|
||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-hook-form": "^7.83.0",
|
"react-hook-form": "^7.84.0",
|
||||||
"resend": "^6.18.1",
|
"resend": "^6.18.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.6",
|
"@biomejs/biome": "2.5.7",
|
||||||
"@next/bundle-analyzer": "^16.2.12",
|
"@next/bundle-analyzer": "^16.3.0",
|
||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
@@ -88,12 +88,15 @@
|
|||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"knip": "^6.30.0",
|
"knip": "^6.31.0",
|
||||||
"lint-staged": "^17.3.0",
|
"lint-staged": "^17.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.25",
|
||||||
|
"puppeteer": "^25.5.0",
|
||||||
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"tsx": "^4.23.1",
|
"tsx": "^4.23.5",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vite": "8.2.0",
|
"vite": "8.2.0",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
|
|||||||
Generated
+865
-165
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||||
@@ -125,6 +125,7 @@ export interface CatalogAuditOptions {
|
|||||||
repairNitros?: boolean;
|
repairNitros?: boolean;
|
||||||
sql?: boolean;
|
sql?: boolean;
|
||||||
applySql?: boolean;
|
applySql?: boolean;
|
||||||
|
organizeSql?: boolean;
|
||||||
repairFurniData?: boolean;
|
repairFurniData?: boolean;
|
||||||
repairStructure?: boolean;
|
repairStructure?: boolean;
|
||||||
}
|
}
|
||||||
@@ -316,6 +317,7 @@ export async function runCatalogAudit(
|
|||||||
classname: item.item_name,
|
classname: item.item_name,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
itemType: item.type,
|
itemType: item.type,
|
||||||
|
publicName: item.public_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,7 +654,9 @@ export async function runCatalogAudit(
|
|||||||
message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`,
|
message: `Generating catalog SQL for ${missingCatalogEntries.length} items…`,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const result = await generateCatalogSql(missingCatalogEntries);
|
const result = await generateCatalogSql(missingCatalogEntries, {
|
||||||
|
organizeSql: options?.organizeSql,
|
||||||
|
});
|
||||||
catalogSqlCount = result.count;
|
catalogSqlCount = result.count;
|
||||||
onEvent?.({
|
onEvent?.({
|
||||||
type: "sql_ready",
|
type: "sql_ready",
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import {
|
|||||||
} from "@/lib/services/furni-data";
|
} from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
autoPriceFurni,
|
autoPriceFurni,
|
||||||
|
CATEGORY_PAGE,
|
||||||
|
classifyFurni,
|
||||||
getOrCreateCategoryPage,
|
getOrCreateCategoryPage,
|
||||||
|
getOrCreateImportedParentPage,
|
||||||
} from "@/lib/services/furni-import";
|
} from "@/lib/services/furni-import";
|
||||||
|
|
||||||
export interface MissingCatalogEntry {
|
export interface MissingCatalogEntry {
|
||||||
classname: string;
|
classname: string;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
itemType: string;
|
itemType: string;
|
||||||
|
publicName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogSqlResult {
|
export interface CatalogSqlResult {
|
||||||
@@ -26,14 +30,280 @@ function escSqlLiteral(value: string): string {
|
|||||||
return value.replace(/'/g, "''");
|
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
|
* Build standalone `catalog_items` INSERT statements for items_base entries
|
||||||
* that exist (with assets) but have no catalog entry yet. Ids and prices are
|
* 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
|
* 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.
|
* 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(
|
export async function generateCatalogSql(
|
||||||
entries: MissingCatalogEntry[],
|
entries: MissingCatalogEntry[],
|
||||||
|
opts: { organizeSql?: boolean } = {},
|
||||||
): Promise<CatalogSqlResult> {
|
): Promise<CatalogSqlResult> {
|
||||||
const statements: string[] = [
|
const statements: string[] = [
|
||||||
"-- AtomCMS catalog repair",
|
"-- AtomCMS catalog repair",
|
||||||
@@ -42,6 +312,20 @@ export async function generateCatalogSql(
|
|||||||
"START TRANSACTION;",
|
"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
|
// The generation pass does not INSERT, so MAX(id) would never advance and
|
||||||
// per-entry allocateCatalogItemId would hand out the same id every time.
|
// per-entry allocateCatalogItemId would hand out the same id every time.
|
||||||
// Read the current max once and hand out sequential ids from there.
|
// 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];
|
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
||||||
let nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
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;
|
let generated = 0;
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
let pageId: number;
|
let pageId: number;
|
||||||
try {
|
if (organizeSql && parentPageId) {
|
||||||
pageId = await getOrCreateCategoryPage(entry.classname, entry.itemType);
|
const catKey = classifyFurni(entry.classname, entry.itemType);
|
||||||
} catch {
|
const captionSave = `imp_${catKey}`;
|
||||||
continue;
|
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 price = autoPriceFurni(entry.classname);
|
||||||
|
const displayName = entry.publicName || entry.classname;
|
||||||
statements.push(
|
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++;
|
nextId++;
|
||||||
generated++;
|
generated++;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface CloneSource {
|
|||||||
const KEY = "clone_sources";
|
const KEY = "clone_sources";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verified working sources (checked Aug 2026). Used as the default list and
|
* Verified sources. Used as the default list and as the fallback when the
|
||||||
* as the fallback when the `clone_sources` site setting is empty.
|
* `clone_sources` site setting is empty.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_SOURCES: CloneSource[] = [
|
export const DEFAULT_SOURCES: CloneSource[] = [
|
||||||
{
|
{
|
||||||
@@ -38,6 +38,34 @@ export const DEFAULT_SOURCES: CloneSource[] = [
|
|||||||
nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture",
|
nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture",
|
||||||
iconBaseUrl: "",
|
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(
|
export const DEFAULT_ICON_SOURCES: CloneSource[] = DEFAULT_SOURCES.filter(
|
||||||
|
|||||||
Reference in new issue
Block a user