9 Commits
Author SHA1 Message Date
remco 9ba8dc26a0 chore(deps): update All dependencies
CI / check (pull_request) Failing after 13s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
2026-08-05 16:00:31 +00:00
openhands af8aaaa512 fix: rebuild asset sets after repair to accurately report stillMissing
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m4s
2026-08-05 17:17:25 +02:00
openhands 00b5a6f5c3 feat: add back Leet and Hubbly presets
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 40s
2026-08-05 16:45:37 +02:00
openhands 02ad9c21f2 feat: remove non-working hotel presets, add verified custom hotels
CI / check (push) Successful in 33s
CI / release (push) Skipped
CI / deploy (push) Successful in 46s
2026-08-05 16:42:27 +02:00
openhands 101c73df1b feat: add 25 more hotel presets to clone sources
CI / check (push) Successful in 34s
CI / release (push) Skipped
CI / deploy (push) Successful in 46s
2026-08-05 16:28:00 +02:00
openhands d1dafba2c9 feat(clone): add more hotel presets for furnidata sources
CI / check (push) Successful in 32s
CI / release (push) Skipped
CI / deploy (push) Successful in 43s
- Added 8 official Habbo hotel presets (NL, DE, FR, ES, FI, BR, TR, COM)
  alongside the existing IT preset, each with their habbo_gamedata_hotel
  mapping so official furnidata enrichment uses the correct locale
- Habbo (IT) renamed to Habbo (IT) for clarity
- Wibbo, Hubba, Soda Ho, Leet, Hubbly, Habblet City presets unchanged
2026-08-05 16:24:57 +02:00
openhands 936053cca6 feat(clone): add hotel field to clone source presets
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 47s
- 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
2026-08-05 16:21:45 +02:00
openhands 79b82e0a31 fix: badge import uses configured gamedata root for ExternalTexts.json
CI / check (push) Successful in 30s
CI / release (push) Skipped
CI / deploy (push) Successful in 44s
- import-badge.ts, badges route, and badges edit route now resolve
  ExternalTexts.json via getGamedataRoot() instead of the hardcoded
  public/nitro-assets/gamedata/ path
- writeBadgeToExternalTexts creates the file (and parent dirs) when
  missing, so fresh deployments no longer fail with ENOENT
- upload-import SQL maker now classifies furni via classifyFurni and
  generates correct category sub-pages (imp_<catKey>) instead of
  hardcoded imp_other
2026-08-05 15:34:40 +02:00
openhands 4d4cbd2211 fix: SQL maker creates wrong categories and badge import fails when ExternalTexts.json missing
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m5s
- writeSqlMigration now classifies furni via classifyFurni and generates
  correct category sub-pages (imp_<catKey>) instead of hardcoded imp_other
- writeSqlMigration generates idempotent INSERT...SELECT WHERE NOT EXISTS
  for parent and category catalog_pages, with correct numeric page_id
- writeBadgeToExternalTexts creates ExternalTexts.json (and parent dirs)
  when missing instead of failing with ENOENT
2026-08-05 15:25:07 +02:00
14 changed files with 691 additions and 63 deletions

No files matched your search

+2 -2
View File
@@ -41,7 +41,7 @@
"jpeg-js": "0.4.4",
"json5": "2.2.3",
"jszip": "3.10.1",
"lenis": "1.3.25",
"lenis": "1.3.26",
"lucide-react": "1.28.0",
"lzma": "2.3.2",
"motion": "12.43.0",
@@ -82,7 +82,7 @@
"pino-pretty": "^13.1.3",
"postcss": "^8.5.25",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.5",
"tsx": "^4.23.8",
"typescript": "^7.0.2",
"vite": "8.2.0",
"vitest": "4.1.10"
+28 -17
View File
@@ -74,8 +74,8 @@ importers:
specifier: 3.10.1
version: 3.10.1
lenis:
specifier: 1.3.25
version: 1.3.25([email protected])
specifier: 1.3.26
version: 1.3.26([email protected])
lucide-react:
specifier: 1.28.0
version: 1.28.0([email protected])
@@ -192,17 +192,17 @@ importers:
specifier: ^4.3.3
version: 4.3.3
tsx:
specifier: ^4.23.5
version: 4.23.5
specifier: ^4.23.8
version: 4.23.8
typescript:
specifier: ^7.0.2
version: 7.0.2
vite:
specifier: 8.2.0
version: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].5)([email protected])
version: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].8)([email protected])
vitest:
specifier: 4.1.10
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected]))
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected]))
packages:
@@ -2329,8 +2329,8 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
[email protected]5:
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
[email protected]6:
resolution: {integrity: sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
react: '>=17.0.0'
@@ -2976,6 +2976,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-8W675THjbzfFmLOQzjDBIBna+WjqMGIxmSZ1mMc1+o9qoVsEuAgQu5j5ueLhau8inOkDu9OslVg0FmfBs1RIHw==}
engines: {node: '>=18.0.0'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
engines: {node: '>=16.20.0'}
@@ -4250,7 +4255,7 @@ snapshots:
obug: 2.1.4
std-env: 4.2.0
tinyrainbow: 3.1.1
vitest: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected]))
vitest: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected]))
'@vitest/[email protected]':
dependencies:
@@ -4261,13 +4266,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected]))':
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected]))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].5)([email protected])
vite: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].8)([email protected])
'@vitest/[email protected]':
dependencies:
@@ -4607,7 +4612,7 @@ snapshots:
yaml: 2.9.0
zod: 4.4.3
[email protected]5([email protected]):
[email protected]6([email protected]):
optionalDependencies:
react: 19.2.8
@@ -5238,6 +5243,12 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
[email protected]:
dependencies:
esbuild: 0.25.12
optionalDependencies:
fsevents: 2.3.3
[email protected]:
optionalDependencies:
'@typescript/typescript-aix-ppc64': 7.0.2
@@ -5296,7 +5307,7 @@ snapshots:
[email protected]: {}
[email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected]):
[email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected]):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.5
@@ -5308,13 +5319,13 @@ snapshots:
esbuild: 0.25.12
fsevents: 2.3.3
jiti: 2.7.0
tsx: 4.23.5
tsx: 4.23.8
yaml: 2.9.0
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected])):
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected])):
dependencies:
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected])([email protected])([email protected].5)([email protected]))
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected])([email protected])([email protected].8)([email protected]))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -5331,7 +5342,7 @@ snapshots:
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
vite: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].5)([email protected])
vite: 8.2.0(@types/[email protected])([email protected])([email protected])([email protected].8)([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 26.1.2
@@ -0,0 +1,402 @@
{
"badge_name_Z75": "Training camp!",
"badge_desc_Z75": "Per esser diventato un guerriero DOC!",
"badge_name_Z67": "Coniglio Scaccia-Mito",
"badge_desc_Z67": "La Sicurezza in Habbo non è una leggenda!",
"badge_name_Z64": "Topo da Laboratorio",
"badge_desc_Z64": "Beta Tester II",
"badge_name_Z63": "Topo da Laboratorio",
"badge_desc_Z63": "Beta Tester I",
"badge_name_Z39": "Hotel Bau",
"badge_desc_Z39": "Tutti a casa finalmente!",
"badge_name_Z38": "Direttore Hotel Bau",
"badge_desc_Z38": "Hotel a ***3 stelle",
"badge_name_Z37": "Direttore Hotel Bau",
"badge_desc_Z37": "Hotel a **2 stelle",
"badge_name_Z36": "Direttore Hotel Bau",
"badge_desc_Z36": "Hotel a *1 stella",
"badge_name_Z35": "Hotel Bau",
"badge_desc_Z35": "Per aver ritrovato i cuccioli",
"badge_name_Z26": "Futuro Re",
"badge_desc_Z26": "Sarai tu il prossimo Re?",
"badge_name_Z16": "Campagna di Sicurezza Gennaio 2017",
"badge_desc_Z16": "Per essere un esperto nel proteggere il mio account!",
"badge_name_Z09": "In Campeggio",
"badge_desc_Z09": "Campagna Campeggio con i Lupetti - 2009",
"badge_name_Z05": "Ex Habbo eXpert",
"badge_desc_Z05": "Gli Habbo X erano guide volontarie scelte dello Staff tra il 2006 e il 2008.",
"badge_name_Z02": "W la Terra",
"badge_desc_Z02": "Per i veri amanti della natura!",
"badge_name_Z01": "Wow or not?",
"badge_desc_Z01": "Hai impressionato la giuria con la tua sfilata!",
"badge_name_YAK": "x",
"badge_desc_YAK": "x",
"badge_name_XXX": "x",
"badge_desc_XXX": "x",
"badge_name_XRLTD": "Distintivo Palla di Vetro con Neve",
"badge_desc_XRLTD": "Per avere acquistato questo Raro LTD di Natale",
"badge_name_XMSR1": "Macchina Magica",
"badge_desc_XMSR1": "Per comprare una Macchina Rara Magica",
"badge_name_XMS14": "Gnomi all'avventura!",
"badge_desc_XMS14": "Per aver vinto la Competizione Video!",
"badge_name_XMS13": "Cin cin!",
"badge_desc_XMS13": "Attento a non versartelo addosso",
"badge_name_XMS12": "Organizzatore di Feste",
"badge_desc_XMS12": "... da urlo!",
"badge_name_XMS10": "Postazione di Controllo",
"badge_desc_XMS10": "Per comprare una Postazione di Controllo Rara",
"badge_name_XMS07": "Babbo Natale e i 3 Re Magi",
"badge_desc_XMS07": "Babbo Natale ha molte conoscenze...",
"badge_name_XMS06": "Natale in Europa",
"badge_desc_XMS06": "Un Natale dei più tipici!",
"badge_name_XMS05": "Natale sulla Spiaggia",
"badge_desc_XMS05": "Festeggia come un Brasiliano!",
"badge_name_XMS04": "Natale senza Natale",
"badge_desc_XMS04": "Festeggia come in Turchia",
"badge_name_XMS03": "Decoratore di Alberi Natalizi",
"badge_desc_XMS03": "Per avere decorato l'Albero di Natale Habbo 2013",
"badge_name_XMS01": "Bottega di Babbo Natale",
"badge_desc_XMS01": "Per avere acquistato l'Affare Stanza natalizio",
"badge_name_XMH20": "Pupazzo Gatto Scheletrico",
"badge_desc_XMH20": "Per avere acquistato il Pupazzo Gatto Scheletrico",
"badge_name_XMH19": "Pupazzo Tristo Mietitore",
"badge_desc_XMH19": "Per avere acquistato il Pupazzo Tristo Mietitore",
"badge_name_XMH18": "Pupazzo Punk Rock",
"badge_desc_XMH18": "Per avere acquistato il Pupazzo Punk Rock",
"badge_name_XMH17": "Pupazzo Ragazza Habbo",
"badge_desc_XMH17": "Per avere acquistato il Pupazzo Ragazza Habbo",
"badge_name_XMH16": "Pupazzo Lucha Libre",
"badge_desc_XMH16": "Per avere acquistato il Pupazzo Lucha Libre",
"badge_name_XMH15": "Pupazzo Scheletro Tatuato",
"badge_desc_XMH15": "Per avere acquistato il Pupazzo Scheletro Tatuato",
"badge_name_XMH14": "Pupazzo Ragazzo Habbo",
"badge_desc_XMH14": "Per avere acquistato il Pupazzo Ragazzo Habbo",
"badge_name_XMH13": "Pupazzo in Giacca e Cravatta",
"badge_desc_XMH13": "Per avere acquistato il Pupazzo in Giacca e Cravatta",
"badge_name_XMH12": "Pupazzo Catrina",
"badge_desc_XMH12": "Per avere acquistato il Pupazzo Catrina",
"badge_name_XMH11": "Pupazzo Mariachi",
"badge_desc_XMH11": "Per avere acquistato il Pupazzo Mariachi",
"badge_name_XMB": "Mr.Pupazzo",
"badge_desc_XMB": "Con cilindro e carota è il signore delle nevi!",
"badge_name_XmasRoom_Top100": "Top 100 Castello di Natale",
"badge_desc_XmasRoom_Top100": "Per essere rientrat@ nella top 100 della Competizione Stanza Castello di Natale",
"badge_name_XmasRoom_Top10": "Top 10 Castello di Natale",
"badge_desc_XmasRoom_Top10": "Per essere rientrat@ nella top 10 della Competizione Stanza Castello di Natale",
"badge_name_XmasRoom": "Partecipante Castello di Natale",
"badge_desc_XmasRoom": "Per aver partecipato alla Competizione Stanza Castello di Natale",
"badge_name_XMAS2": "Ninnolo",
"badge_desc_XMAS2": "Livello III",
"badge_name_XMAS1": "Ninnolo",
"badge_desc_XMAS1": "Livello II",
"badge_name_XMAS0": "Ninnolo",
"badge_desc_XMAS0": "Livello I",
"badge_name_XMA": "Smile Congelato",
"badge_desc_XMA": "Il suo sorriso ti scalda il cuore",
"badge_name_XM9": "Palla di Neve",
"badge_desc_XM9": "Deve ancora fare parecchia strada per diventare un pupazzo!",
"badge_name_XM8": "Boccale di Birra",
"badge_desc_XM8": "Habbo Natale 2007",
"badge_name_XM7": "Moneta",
"badge_desc_XM7": "Habbo Natale 2007",
"badge_name_XM6": "Robot di Santa 3000",
"badge_desc_XM6": "Habbo Natale 2007",
"badge_name_XM5": "Fiocco di Neve",
"badge_desc_XM5": "Habbo Natale 2007",
"badge_name_XM4": "Natale 2006",
"badge_desc_XM4": "Ero su Habbo a Natale del 2006!",
"badge_name_XM3": "Renna",
"badge_desc_XM3": "Habbo Natale 2005",
"badge_name_XM2": "DJ-Bling",
"badge_desc_XM2": "Habbo Natale 2005",
"badge_name_XM10E": "FREEZE!",
"badge_desc_XM10E": "Hai costruito un'Arena Fantastica!",
"badge_name_XM10D": "Campione Wired-Freeze",
"badge_desc_XM10D": "L'ennesima potenza del divertimento!",
"badge_name_XM10C": "Regalo Natalizio Fuori Stagione",
"badge_desc_XM10C": "Saluti dai Caraibi!",
"badge_name_XM10B": "Pattino d'Oro",
"badge_desc_XM10B": "Il Miglior Palazzetto del Ghiaccio",
"badge_name_XM10A": "Città Natalizia!",
"badge_desc_XM10A": "Per gli addobbi Natalizi più Originali!",
"badge_name_XM1": "Rasta Santa",
"badge_desc_XM1": "Habbo Natale 2005 e 2006",
"badge_name_XGH1": "Il Fantasma del Natale",
"badge_desc_XGH1": "Ho trovato il Fantasma del Natale",
"badge_name_X535": "Conduttore di slitta - Sig.ra Claus",
"badge_desc_X535": "Oh, che bello andare su una slitta trainata da un cavallo..ehi!",
"badge_name_X534": "Conduttore di slitta - Habbo Natale",
"badge_desc_X534": "Oh, che bello andare su una slitta trainata da un cavallo..ehi!",
"badge_name_X533": "Pranzo di Natale - Sig.ra Claus",
"badge_desc_X533": "Non c'è di niente di meglio di un bel Pranzo di Natale",
"badge_name_X532": "Pranzo di Natale - Habbo Natale",
"badge_desc_X532": "Non c'è di niente di meglio di un bel Pranzo di Natale",
"badge_name_X531": "Silent night - Sig.ra Claus",
"badge_desc_X531": "Non hai svegliato nessun Habbo, hai sfiorato la perfezione come la Sig.ra Claus",
"badge_name_X530": "Silent night - Habbo Natale",
"badge_desc_X530": "Non hai svegliato nessun Habbo, hai sfiorato la perfezione come Habbo Natale",
"badge_name_X529": "Preparazione Regali di Natale - Sig.ra Claus",
"badge_desc_X529": "Ti sei assicurato che quest'anno tutti ricevano il giusto regalo di Natale",
"badge_name_X528": "Preparazione Regali di Natale - Habbo Natale",
"badge_desc_X528": "Ti sei assicurato che quest'anno tutti ricevano il giusto regalo di Natale",
"badge_name_X527": "Mercato di Natale - Sig.ra Claus",
"badge_desc_X527": "Per aver completato il labirinto del mercato di Natale",
"badge_name_X526": "Mercato di Natale - Habbo Natale",
"badge_desc_X526": "Per aver completato il labirinto del mercato di Natale",
"badge_name_X525": "Proprietario di Renna - Sig.ra Claus",
"badge_desc_X525": "Vixen, la renna della Sig.ra Claus, è stata riportata sana e salva nella stalla",
"badge_name_X524": "Proprietario di Renna - Habbo Natale",
"badge_desc_X524": "Dasher, la renna preferita di Habbo Natale, è stata riportata sana e salva nella stalla",
"badge_name_X523": "Film di Natale - Sig.ra Claus",
"badge_desc_X523": "Sei un vero Fan dei film di Natale",
"badge_name_X522": "Film di Natale - Habbo Natale",
"badge_desc_X522": "Sei un vero Fan dei film di Natale",
"badge_name_X521": "Meraviglioso Albero di Natale - Sig.ra Natale",
"badge_desc_X521": "Oh Albero di Natale, Oh Albero di Natale, Le tue foglie son così immutabili",
"badge_name_X520": "Meraviglioso Albero di Natale - Habbo Natale",
"badge_desc_X520": "Oh Albero di Natale, Oh Albero di Natale, Le tue foglie son così immutabili",
"badge_name_X519": "Canti Natalizi - Sig.ra Claus",
"badge_desc_X519": "Jingle bell, jingle bell, jingle bell rock",
"badge_name_X518": "Canti Natalizi - Habbo Natale",
"badge_desc_X518": "Haaaabbo Natale sta arrivando in città!",
"badge_name_X2535": "L'Alimentatore Preferito di Esophagor",
"badge_desc_X2535": "",
"badge_name_X2534": "Stazione dei Treni",
"badge_desc_X2534": "",
"badge_name_X2533": "Collezione Paese delle Meraviglie di Cacao",
"badge_desc_X2533": "",
"badge_name_X2532": "Affare Stanza Ufficio di Habbo Natale",
"badge_desc_X2532": "",
"badge_name_X2531": "Affare Stanza Natale Accogliente di Habbo",
"badge_desc_X2531": "",
"badge_name_X2530": "Uovo Glassato LTD",
"badge_desc_X2530": "",
"badge_name_X2529": "Borsa Kitty Rara",
"badge_desc_X2529": "",
"badge_name_X2528": "Cuscino di Ghiaccio Raro",
"badge_desc_X2528": "",
"badge_name_X2527": "Habbo Night Hotel Raro",
"badge_desc_X2527": "",
"badge_name_X2526": "Coda di cavallo Ghiacciata Scintillante Rara",
"badge_desc_X2526": "",
"badge_name_X2525": "Buon Natale 2025!",
"badge_desc_X2525": "",
"badge_name_X2521": "A caccia di vacanze!",
"badge_desc_X2521": "",
"badge_name_X2520": "Alimentatore di Esophagor",
"badge_desc_X2520": "",
"badge_name_X2518": "Battaglia con Palle di neve!",
"badge_desc_X2518": "",
"badge_name_X2516": "Spirito delle Feste",
"badge_desc_X2516": "",
"badge_name_X2515": "Straordinario Creatore delle Feste",
"badge_desc_X2515": "",
"badge_name_X2514": "Felice & Moderno",
"badge_desc_X2514": "",
"badge_name_X2513": "Eroe delle Tradizioni Natalizie",
"badge_desc_X2513": "",
"badge_name_X2512": "Corona dello Scontro di Natale",
"badge_desc_X2512": "",
"badge_name_X2511": "Habubu Glam",
"badge_desc_X2511": "",
"badge_name_X2510": "Habubu Cozy",
"badge_desc_X2510": "",
"badge_name_X2509": "Habubu Dream",
"badge_desc_X2509": "",
"badge_name_X2508": "Habubu Calm",
"badge_desc_X2508": "",
"badge_name_X2507": "Habubu Luck",
"badge_desc_X2507": "",
"badge_name_X2506": "Habubu Hope",
"badge_desc_X2506": "",
"badge_name_X2505": "Habubu Happy",
"badge_desc_X2505": "",
"badge_name_X2504": "Habubu Fun",
"badge_desc_X2504": "",
"badge_name_X2503": "Habubu Love",
"badge_desc_X2503": "",
"badge_name_X2502": "Squadra - DJ Bling",
"badge_desc_X2502": "",
"badge_name_X2501": "Squadra - Rasta Santa",
"badge_desc_X2501": "",
"badge_name_X2330": "Competizione Anatroccolo",
"badge_desc_X2330": "",
"badge_name_X2329": "Competizione Stanza Sweet Suite NL, TR",
"badge_desc_X2329": "",
"badge_name_X2328": "Offerta Crafting",
"badge_desc_X2328": "",
"badge_name_X2327": "Trono Bianco",
"badge_desc_X2327": "",
"badge_name_X2326": "Buon 2024!",
"badge_desc_X2326": "",
"badge_name_X2325": "Buon Natale!",
"badge_desc_X2325": "",
"badge_name_X2324": "Goditi il Natale Habbo",
"badge_desc_X2324": "",
"badge_name_X2323": "Aiuta Frank Wobbah a cucinare",
"badge_desc_X2323": "",
"badge_name_X2322": "Salvataggio delle ricette di Frank Wobbah",
"badge_desc_X2322": "",
"badge_name_X2321": "Missione Babbo Natale per un giorno",
"badge_desc_X2321": "",
"badge_name_X2320": "Missione Brilla come un Biglietto d'oro",
"badge_desc_X2320": "",
"badge_name_X2319": "Missione Delizie Arcobaleno",
"badge_desc_X2319": "",
"badge_name_X2318": "Missione Frutti di Bosco",
"badge_desc_X2318": "",
"badge_name_X2317": "Missione Mentine Rinfrescanti",
"badge_desc_X2317": "",
"badge_name_X2316": "Pazzo per la Missione del Cioccolato",
"badge_desc_X2316": "",
"badge_name_X2315": "Torta Trono Rara",
"badge_desc_X2315": "",
"badge_name_X2314": "Offerta di Capodanno 2023",
"badge_desc_X2314": "",
"badge_name_X2313": "Offerta Indumenti Natale 2023",
"badge_desc_X2313": "",
"badge_name_X2312": "Carosello di Cioccolato Artigianale LTD",
"badge_desc_X2312": "",
"badge_name_X2311": "Corona d'Oro 24K LTD",
"badge_desc_X2311": "",
"badge_name_X2310": "Ali sulla Testa Mitiche Rare",
"badge_desc_X2310": "",
"badge_name_X2309": "Seduta Aerea Rara",
"badge_desc_X2309": "",
"badge_name_X2308": "Nastro Scartami RARO",
"badge_desc_X2308": "",
"badge_name_X2307": "Albero di Zucchero Filato RARO",
"badge_desc_X2307": "",
"badge_name_X2306": "Affare Stanza di Capodanno",
"badge_desc_X2306": "",
"badge_name_X2305": "Affare Stanza Pista di Pattinaggio sul Ghiaccio",
"badge_desc_X2305": "",
"badge_name_X2304": "Affare Stanza Ritrovo dell'Elfo",
"badge_desc_X2304": "",
"badge_name_X2303": "Bottega Moderna di Babbo Natale",
"badge_desc_X2303": "",
"badge_name_X2302": "Emporio del Cioccolato Magico",
"badge_desc_X2302": "",
"badge_name_X2301": "Collezione Paese delle Meraviglie di Cacao",
"badge_desc_X2301": "",
"badge_name_X2234": "Sanrio Christmas Room Competition",
"badge_desc_X2234": "",
"badge_name_X2232": "Affare Stanza Salone da Pranzo",
"badge_desc_X2232": "",
"badge_name_X2231": "Offerta Bevande per soldi Reali",
"badge_desc_X2231": "",
"badge_name_X2230": "Renna Abile",
"badge_desc_X2230": "",
"badge_name_X2229": "Pinguino Abile",
"badge_desc_X2229": "",
"badge_name_X2228": "Labirinto dei Pinguini: Livello Facile - Completato",
"badge_desc_X2228": "",
"badge_name_X2227": "Labirinto dei Pinguini: Livello Medio - Completato",
"badge_desc_X2227": "",
"badge_name_X2226": "Labirinto dei Pinguini: Livello Folle - Completato",
"badge_desc_X2226": "",
"badge_name_X2224": "Natale Habbo 2022 - Pinguino di Tokyo",
"badge_desc_X2224": "",
"badge_name_X2223": "Natale Habbo 2022 - Pinguino Timido",
"badge_desc_X2223": "",
"badge_name_X2222": "Natale Habbo 2022 - Pinguino Accademico",
"badge_desc_X2222": "",
"badge_name_X2220": "Natale Habbo 2022 - Pinguino Unicorno",
"badge_desc_X2220": "",
"badge_name_X2219": "Natale Habbo 2022 - Pinguino Albino",
"badge_desc_X2219": "",
"badge_name_X2218": "Natale Habbo 2022 - Pinguino Fantasma",
"badge_desc_X2218": "",
"badge_name_X2217": "Natale Habbo 2022 - Pinguino Egizio",
"badge_desc_X2217": "",
"badge_name_X2216": "Natale Habbo 2022 - Pinguino Artista",
"badge_desc_X2216": "",
"badge_name_X2215": "Natale Habbo 2022 - Pinguino Addormentato",
"badge_desc_X2215": "",
"badge_name_X2214": "Natale Habbo 2022 - Pinguino Testa a Molla",
"badge_desc_X2214": "",
"badge_name_X2213": "Offerta Cibo Soldi Veri",
"badge_desc_X2213": "",
"badge_name_X2212": "Uovo Habberge Palla di Neve LTD",
"badge_desc_X2212": "",
"badge_name_X2211": "Ali d'Angelo LTD",
"badge_desc_X2211": "",
"badge_name_X2210": "RARO Husky Addestrato",
"badge_desc_X2210": "",
"badge_name_X2209": "Raro Sauna Nordica",
"badge_desc_X2209": "",
"badge_name_X2208": "RARO Capelli con Treccine",
"badge_desc_X2208": "",
"badge_name_X2207": "Raro Look da Albero Festivo",
"badge_desc_X2207": "",
"badge_name_X2206": "Affare Stanza Sauna Finlandese",
"badge_desc_X2206": "",
"badge_name_X2205": "Affare Stanza Snowboard sulle Alpi in Inverno",
"badge_desc_X2205": "",
"badge_name_X2204": "Affare Stanza Natale Bavarese",
"badge_desc_X2204": "",
"badge_name_X2203": "Affare Stanza Inverno ad Habbo Stonehenge",
"badge_desc_X2203": "",
"badge_name_X2202": "Affare Stanza Baita del Resort Invernale",
"badge_desc_X2202": "",
"badge_name_X2201": "Affare Stanza Resort degli Sport Invernali",
"badge_desc_X2201": "",
"badge_name_X2139": "Buon Natale!",
"badge_desc_X2139": "",
"badge_name_X2138": "Sulla lista dei buoni di Babbo Natale!",
"badge_desc_X2138": "",
"badge_name_X2137": "La Squadra di Babbo Natale",
"badge_desc_X2137": "",
"badge_name_X2136": "Chi è il vero Babbo Natale?",
"badge_desc_X2136": "",
"badge_name_X2135": "Il Coro Natalizio di Habbo",
"badge_desc_X2135": "",
"badge_name_X2134": "Battaglia di Palle di Neve!",
"badge_desc_X2134": "",
"badge_name_X2133": "Oh Albero di Natale",
"badge_desc_X2133": "",
"badge_name_X2132": "Si prepara una Tempesta!",
"badge_desc_X2132": "",
"badge_name_X2131": "Pattinando sul Ghiaccio Sottile",
"badge_desc_X2131": "",
"badge_name_X2130": "Piante Malridotte dall'Inverno",
"badge_desc_X2130": "",
"badge_name_X2129": "Nobile Uccello d'Oro",
"badge_desc_X2129": "",
"badge_name_X2128": "Caso Risolto!",
"badge_desc_X2128": "",
"badge_name_X2127": "Passi nella neve",
"badge_desc_X2127": "",
"badge_name_X2126": "La Grande Fuga",
"badge_desc_X2126": "",
"badge_name_X2125": "Il Sospetto Sbagliato",
"badge_desc_X2125": "",
"badge_name_X2124": "Scappato appena in tempo!",
"badge_desc_X2124": "",
"badge_name_X2123": "Identità Segreta",
"badge_desc_X2123": "",
"badge_name_X2122": "Una Mappa Segreta",
"badge_desc_X2122": "",
"badge_name_X2121": "Zzzz..Zzzz..",
"badge_desc_X2121": "",
"badge_name_X2120": "Si è intrufolato nella stanza del commesso viaggiatore",
"badge_desc_X2120": "",
"badge_name_X2119": "Un Look strepitoso!",
"badge_desc_X2119": "",
"badge_name_X2118": "Chiavi prestate",
"badge_desc_X2118": "",
"badge_name_X2117": "Il Treno sta arrivando",
"badge_desc_X2117": "",
"badge_name_X2116": "Bandito del Treno",
"badge_desc_X2116": "",
"badge_name_X2115": "Il Treno Habbo Express",
"badge_desc_X2115": "",
"badge_name_X2114": "Uovo Habberge Art Deco LTD",
"badge_desc_X2114": "",
"badge_name_X2113": "Locomotiva a Vapore Rara",
"badge_desc_X2113": "",
"badge_name_X2112": "Costume da Treno Raro",
"badge_desc_X2112": "",
"badge_name_X2111": "Macchina della Cioccolata Calda di Lusso Rara",
"badge_desc_X2111": ""
}
@@ -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
/>
<Select value={hotel} onValueChange={setHotel}>
<SelectTrigger>
<SelectValue placeholder="Hotel (optional)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">None</SelectItem>
{HABBO_GAMEDATA_HOTELS.map((h) => (
<SelectItem key={h.value} value={h.value}>
{h.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button type="submit" size="sm" disabled={saving}>
@@ -261,6 +285,12 @@ function SourcesManager({
<div className="text-xs text-muted-foreground truncate">
{src.furnidataUrl}
</div>
{src.hotel ? (
<div className="text-[10px] text-muted-foreground/70">
{HABBO_GAMEDATA_HOTELS.find((h) => h.value === src.hotel)
?.label ?? `habbo.${src.hotel}`}
</div>
) : null}
</div>
</button>
<div className="flex items-center gap-1 shrink-0">
@@ -3,11 +3,11 @@ import path from "node:path";
import { withAdmin } from "@/lib/api-handler";
import { apiError, apiOk } from "@/lib/api-response";
import { PERMS } from "@/lib/permissions";
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
const EXTERNAL_TEXTS_PATH = path.join(
process.cwd(),
"public/nitro-assets/gamedata/ExternalTexts.json",
);
function externalTextsPath(gamedataRoot: string): string {
return path.join(gamedataRoot, "config", "ExternalTexts.json");
}
/** GET - Read badge name/desc from ExternalTexts.json */
export const GET = withAdmin(
@@ -16,8 +16,11 @@ export const GET = withAdmin(
const code = request.nextUrl.searchParams.get("code")?.toUpperCase();
if (!code) return apiError("Missing code", 400);
const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) return apiError("gamedata_root not configured", 500);
try {
const raw = await fs.readFile(EXTERNAL_TEXTS_PATH, "utf-8");
const raw = await fs.readFile(externalTextsPath(gamedataRoot), "utf-8");
const texts: Record<string, string> = JSON.parse(raw);
const name =
+12 -9
View File
@@ -4,13 +4,13 @@ import { withAdmin } from "@/lib/api-handler";
import { apiError, apiOk } from "@/lib/api-response";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
import { searchBadges } from "@/lib/services/habboassets";
import { importBadgeSynced } from "@/lib/services/import-badge";
const EXTERNAL_TEXTS_PATH = path.join(
process.cwd(),
"public/nitro-assets/gamedata/ExternalTexts.json",
);
function externalTextsPath(gamedataRoot: string): string {
return path.join(gamedataRoot, "config", "ExternalTexts.json");
}
// ── Search badges via official Habbo external texts ────────────────
export const GET = withAdmin(
@@ -33,11 +33,14 @@ export const GET = withAdmin(
// Check which badges already exist in ExternalTexts.json
const existingCodes = new Set<string>();
try {
const raw = await fs.readFile(EXTERNAL_TEXTS_PATH, "utf-8");
const texts = JSON.parse(raw);
for (const badge of badges) {
if (texts[`badge_name_${badge.code}`] !== undefined) {
existingCodes.add(badge.code);
const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) {
const raw = await fs.readFile(externalTextsPath(gamedataRoot), "utf-8");
const texts = JSON.parse(raw);
for (const badge of badges) {
if (texts[`badge_name_${badge.code}`] !== undefined) {
existingCodes.add(badge.code);
}
}
}
} catch {
+2 -1
View File
@@ -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<string, string>;
const isHttps = (u: string) => {
@@ -104,6 +104,7 @@ export const POST = withAdmin(
furnidataUrl,
nitroBaseUrl,
iconBaseUrl,
hotel: hotel ?? "",
});
logAudit({
+20 -2
View File
@@ -183,8 +183,8 @@ export async function runCatalogAudit(
}
})(),
]);
const iconSet = new Set(iconFiles);
const nitroSet = new Set(nitroFiles);
let iconSet = new Set(iconFiles);
let nitroSet = new Set(nitroFiles);
// Badge detection: `items_base.type = 'b'` is authoritative. The album1584
// `.gif` listing is kept as a fallback for hotels that store badges with a
// non-standard type.
@@ -466,6 +466,15 @@ export async function runCatalogAudit(
}
});
// Re-read directories so the set reflects files written
// by the repair pass.
const [repairedIconFiles, repairedNitroFiles] = await Promise.all([
readdir(iconDir).catch(() => [] as string[]),
readdir(nitroDir).catch(() => [] as string[]),
]);
iconSet = new Set(repairedIconFiles);
nitroSet = new Set(repairedNitroFiles);
// Re-check which of the previously-missing icons are still missing
// and drop the ones the repair fixed from the issue list. The ones
// left over cannot be restored from any source → legacy/broken.
@@ -530,6 +539,15 @@ export async function runCatalogAudit(
}
});
// Re-read directories so the set reflects files written
// by the repair pass.
const [repairedIconFiles2, repairedNitroFiles2] = await Promise.all([
readdir(iconDir).catch(() => [] as string[]),
readdir(nitroDir).catch(() => [] as string[]),
]);
iconSet = new Set(repairedIconFiles2);
nitroSet = new Set(repairedNitroFiles2);
// Re-check which of the previously-missing nitros are still missing
// and drop the ones the repair fixed from the issue list. The ones
// left over cannot be restored from any source → legacy/broken.
+1
View File
@@ -85,6 +85,7 @@ const SOURCE = {
furnidataUrl: "https://x/fd.json",
nitroBaseUrl: "https://x/furni",
iconBaseUrl: "https://x/icons",
hotel: "",
};
const ENTRY = {
id: 5,
+11
View File
@@ -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)) {
+99 -1
View File
@@ -7,6 +7,7 @@ export interface CloneSource {
furnidataUrl: string;
nitroBaseUrl: string;
iconBaseUrl: string;
hotel: string;
}
const KEY = "clone_sources";
@@ -18,11 +19,76 @@ const KEY = "clone_sources";
export const DEFAULT_SOURCES: CloneSource[] = [
{
id: "default-habbo",
name: "Habbo",
name: "Habbo (IT)",
furnidataUrl:
"https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "it",
},
{
id: "default-habbo-com",
name: "Habbo (COM)",
furnidataUrl: "https://www.habbo.com/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com",
},
{
id: "default-habbo-nl",
name: "Habbo (NL)",
furnidataUrl: "https://www.habbo.nl/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "nl",
},
{
id: "default-habbo-de",
name: "Habbo (DE)",
furnidataUrl: "https://www.habbo.de/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "de",
},
{
id: "default-habbo-fr",
name: "Habbo (FR)",
furnidataUrl: "https://www.habbo.fr/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "fr",
},
{
id: "default-habbo-es",
name: "Habbo (ES)",
furnidataUrl: "https://www.habbo.es/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "es",
},
{
id: "default-habbo-fi",
name: "Habbo (FI)",
furnidataUrl: "https://www.habbo.fi/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "fi",
},
{
id: "default-habbo-br",
name: "Habbo (BR)",
furnidataUrl: "https://www.habbo.com.br/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com.br",
},
{
id: "default-habbo-tr",
name: "Habbo (TR)",
furnidataUrl: "https://www.habbo.com.tr/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com.tr",
},
{
id: "default-wibbo",
@@ -30,6 +96,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 +104,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 +112,7 @@ export const DEFAULT_SOURCES: CloneSource[] = [
furnidataUrl: "https://assets.sodaho.tel/gamedata/FurnitureData.json",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com",
},
{
id: "default-leet",
@@ -51,6 +120,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 +128,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 +136,32 @@ 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",
},
{
id: "default-habbo-retro",
name: "Habbo Retro",
furnidataUrl:
"https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com",
},
{
id: "default-habbo-classic",
name: "Habbo Classic",
furnidataUrl: "https://www.habbo.com/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com",
},
{
id: "default-habbo-original",
name: "Habbo Original",
furnidataUrl: "https://habbo.com/gamedata/furnidata_json/1",
nitroBaseUrl: "",
iconBaseUrl: "",
hotel: "com",
},
];
@@ -96,6 +193,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;
+44 -21
View File
@@ -1,45 +1,68 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { db, WebsiteBadges } from "@/lib/db";
const EXTERNAL_TEXTS_PATH = path.join(
process.cwd(),
"public/nitro-assets/gamedata/ExternalTexts.json",
);
import { getGamedataRoot } from "@/lib/services/furni-asset-dirs";
export type ImportBadgeResult =
| { ok: true }
| { ok: false; error: string; status: number };
function externalTextsPath(gamedataRoot: string): string {
return path.join(gamedataRoot, "config", "ExternalTexts.json");
}
/** Write badge name/desc into ExternalTexts.json (409 if already present). */
export async function writeBadgeToExternalTexts(
code: string,
name: string,
description: string,
): Promise<ImportBadgeResult> {
const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) {
return {
ok: false,
error: "gamedata_root not configured",
status: 500,
};
}
const extPath = externalTextsPath(gamedataRoot);
const nameKey = `badge_name_${code}`;
const descKey = `badge_desc_${code}`;
let texts: Record<string, string>;
try {
const raw = await fs.readFile(EXTERNAL_TEXTS_PATH, "utf-8");
const texts = JSON.parse(raw) as Record<string, string>;
const nameKey = `badge_name_${code}`;
const descKey = `badge_desc_${code}`;
if (texts[nameKey] !== undefined) {
const raw = await fs.readFile(extPath, "utf-8");
texts = JSON.parse(raw) as Record<string, string>;
} catch (err) {
// File not present yet (fresh deployment) — start with an empty object.
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
console.warn(
"[import-badges] Failed to update ExternalTexts.json:",
(err as Error).message,
);
return {
ok: false,
error: "Badge already exists in ExternalTexts.json",
status: 409,
error: "Failed to update ExternalTexts.json",
status: 500,
};
}
texts = {};
}
texts[nameKey] = name;
texts[descKey] = description || "";
if (texts[nameKey] !== undefined) {
return {
ok: false,
error: "Badge already exists in ExternalTexts.json",
status: 409,
};
}
await fs.writeFile(
EXTERNAL_TEXTS_PATH,
JSON.stringify(texts, null, 4),
"utf-8",
);
texts[nameKey] = name;
texts[descKey] = description || "";
try {
await fs.mkdir(path.dirname(extPath), { recursive: true });
await fs.writeFile(extPath, JSON.stringify(texts, null, 4), "utf-8");
return { ok: true };
} catch (err) {
console.warn(
+4
View File
@@ -62,6 +62,8 @@ vi.mock("@/lib/services/furni-import", () => ({
async (insertFn: (nextId: number) => Promise<unknown>) => insertFn(701),
),
autoPriceFurni: vi.fn(() => ({ credits: 3, points: 0, pointsType: 0 })),
CATEGORY_PAGE: {},
classifyFurni: vi.fn(() => "other"),
ensureDirectories: vi.fn(async () => {
const dirs = [
assetTargets.swfDir,
@@ -76,6 +78,8 @@ vi.mock("@/lib/services/furni-import", () => ({
await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })));
}),
getOrCreateCategoryPage: getOrCreateCategoryPageMock,
IMPORTED_PAGE_CAPTION: "Imported Furniture",
IMPORTED_PAGE_CAPTION_SAVE: "imported_furni",
}));
vi.mock("@/lib/services/swf/nitro-builder", () => ({
+28 -5
View File
@@ -7,8 +7,12 @@ import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data";
import {
allocateCatalogItemId,
autoPriceFurni,
CATEGORY_PAGE,
classifyFurni,
ensureDirectories,
getOrCreateCategoryPage,
IMPORTED_PAGE_CAPTION,
IMPORTED_PAGE_CAPTION_SAVE,
} from "@/lib/services/furni-import";
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
@@ -153,6 +157,7 @@ async function writeSqlMigration(params: {
interactionType: string;
customparams: string;
price: { credits: number; points: number; pointsType: number };
categoryKey: string;
}): Promise<string> {
const num = await findNextMigrationNumber();
const padded = String(num).padStart(4, "0");
@@ -163,11 +168,29 @@ async function writeSqlMigration(params: {
fileName,
);
const catKey = params.categoryKey;
const captionSave = `imp_${catKey}`;
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
const lines: string[] = [
`-- Migration: ${fileName}`,
`-- Description: Import furniture "${params.classname}"`,
`-- Generated: ${new Date().toISOString()}`,
"",
`-- Ensure the "Imported Furniture" parent page exists`,
`INSERT INTO catalog_pages (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)`,
`SELECT ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)}, ${escapeSql(IMPORTED_PAGE_CAPTION)}, 'default_3x3', -1, '1', 9999, 263, '1', '1', '', '', '', '', '', '', '', ''`,
`WHERE NOT EXISTS (SELECT 1 FROM catalog_pages WHERE caption_save = ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)});`,
"",
`-- Ensure the category sub-page exists (${catKey})`,
`INSERT INTO catalog_pages (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)`,
`SELECT ${escapeSql(captionSave)}, ${escapeSql(catInfo.label)}, 'default_3x3', p.id, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', ''`,
`FROM catalog_pages p`,
`WHERE p.caption_save = ${escapeSql(IMPORTED_PAGE_CAPTION_SAVE)}`,
` AND NOT EXISTS (SELECT 1 FROM catalog_pages c WHERE c.caption_save = ${escapeSql(captionSave)} AND c.parent_id = p.id);`,
"",
`SET @next_id = (SELECT COALESCE(MAX(id), 0) + 1 FROM items_base);`,
"",
`INSERT INTO items_base`,
` (id, sprite_id, public_name, item_name, type, width, length, stack_height,`,
` allow_stack, allow_sit, allow_lay, allow_walk, interaction_type, customparams)`,
@@ -177,13 +200,12 @@ async function writeSqlMigration(params: {
` ${params.allowStack}, ${params.cansiton ? "1" : "0"}, ${params.canlayon ? "1" : "0"}, ${params.canstandon ? "1" : "0"},`,
` ${escapeSql(params.interactionType)}, ${escapeSql(params.customparams)});`,
"",
`SET @next_id = LAST_INSERT_ID();`,
"",
`INSERT INTO catalog_items`,
` (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)`,
`VALUES`,
` (NULL, ${escapeSql("imp_other")}, @next_id, ${escapeSql(params.classname)},`,
` ${params.price.credits}, ${params.price.points}, ${params.price.pointsType}, 1, 1, '-1', '');`,
`SELECT (SELECT COALESCE(MAX(id), 0) + 1 FROM catalog_items), p.id, @next_id, ${escapeSql(params.classname)},`,
` ${params.price.credits}, ${params.price.points}, ${params.price.pointsType}, 1, 1, '-1', ''`,
`FROM catalog_pages p`,
`WHERE p.caption_save = ${escapeSql(captionSave)};`,
"",
];
@@ -379,6 +401,7 @@ export async function uploadSingleFurni(params: {
interactionType: userInteractionType,
customparams,
price,
categoryKey: classifyFurni(classname, itemType),
});
warnings.push(`SQL migration written: ${sqlFile}`);
} catch (err) {