feat(utils): split-aware gamedata loader with tiered merge

Introduces loadGamedata(url, options?) and mergeGamedata(a, b) in
@nitrots/utils. The loader transparently accepts:

- a single-file URL (legacy) -> parsed as before
- a directory URL ending with '/' -> tier-merged from core/custom/seasonal,
  each tier driven by its own manifest.json5

Merge rules:

- arrays of objects sharing an id key (id, classname, name): merged by id,
  later layers overriding earlier ones
- arrays without an id key: concatenated
- plain objects: recursive merge per key
- anything else: later value wins

All gamedata consumers (FurnitureDataLoader, ProductDataLoader,
EffectAssetDownloadManager, AvatarRenderManager actions+figuredata,
LocalizationManager) are migrated to loadGamedata. Behaviour is unchanged
for single-file URLs, so existing deployments need no config changes;
opt-in to split mode by appending '/' to the URL once the layout is in
place.

README updated with the directory layout, merge table and programmatic
usage example. The companion CLI splitter that produces the core/ tier
from legacy files lives in the Nitro V3 client repo.
This commit is contained in:
medievalshell
2026-05-18 21:19:54 +02:00
parent 2a00365862
commit ae9bc8bfce
8 changed files with 265 additions and 101 deletions
@@ -1,7 +1,7 @@
import { ILocalizationManager } from '@nitrots/api';
import { BadgePointLimitsEvent, GetCommunication } from '@nitrots/communication';
import { GetConfiguration } from '@nitrots/configuration';
import { parseConfigJsonFromResponse } from '@nitrots/utils';
import { loadGamedata } from '@nitrots/utils';
import { BadgeBaseAndLevel } from './BadgeBaseAndLevel';
export class LocalizationManager implements ILocalizationManager
@@ -26,28 +26,15 @@ export class LocalizationManager implements ILocalizationManager
url = GetConfiguration().interpolate(url);
let response: Response;
try
{
response = await fetch(url);
}
catch(fetchErr)
{
throw new Error(`Could not fetch localization file "${ url }" — check "external.texts.url" in ui-config.json (${ fetchErr.message })`);
}
if(response.status !== 200) throw new Error(`Failed to load localization file "${ url }" — server returned HTTP ${ response.status }. Check "external.texts.url" in ui-config.json`);
let data: any;
try
{
data = await parseConfigJsonFromResponse(response, url);
data = await loadGamedata(url);
}
catch(parseErr)
catch(err)
{
throw new Error(`Invalid localization file "${ url }" — JSON/JSON5 parse failed. Check "external.texts.url" in ui-config.json (${ parseErr.message })`);
throw new Error(`Could not load localization file "${ url }" — check "external.texts.url" in ui-config.json (${ err?.message || err })`);
}
this.parseLocalization(data);