mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(scripts): add split-gamedata.mjs CLI splitter
Companion tool for the split-aware gamedata loader added in
@nitrots/utils. Takes a legacy single-file gamedata JSON/JSON5 and
produces the directory layout the loader expects:
<out>/
manifest.json5 (root manifest, tier order)
core/
manifest.json5 (file list in load order)
<part>.json5 ...
The tool auto-detects the gamedata type from top-level keys and applies
the right split strategy:
- EffectMap -> one file per effect type (dance, fx, ...)
- FigureData -> palettes + one file per setType
- FigureMap -> chunked libraries (default 500/file)
- FurnitureData -> floor/wall, chunks of furnitype (default 300)
- HabboAvatarActions -> grouped by state
- ProductData -> chunked products (default 500)
- ExternalTexts/UITexts-> grouped by key prefix
Only the core/ tier is generated; custom/ and seasonal/ are operator-
owned and the loader auto-discovers them when their manifest.json5
exists.
Flags: --input, --output, --type, --chunk-size, --json (legacy emit),
--force, --help.
README extended with a 'Splitting gamedata' section covering the layout,
the strategy table, CLI usage and the renderer-config migration step.
This commit is contained in:
@@ -75,6 +75,94 @@ Useful workflow flags:
|
|||||||
`install.mjs` is idempotent: re-running it keeps any `*.json` config files
|
`install.mjs` is idempotent: re-running it keeps any `*.json` config files
|
||||||
that already exist and only patches the URL keys you pass on the CLI.
|
that already exist and only patches the URL keys you pass on the CLI.
|
||||||
|
|
||||||
|
## Splitting gamedata
|
||||||
|
|
||||||
|
The renderer can load gamedata files (FigureData, FurnitureData, FigureMap,
|
||||||
|
EffectMap, ProductData, HabboAvatarActions, ExternalTexts, UITexts) either as
|
||||||
|
a single legacy JSON/JSON5 file or as a **directory of small files** organised
|
||||||
|
in three tiers: `core/` (vendor baseline), `custom/` (your additions / overrides),
|
||||||
|
`seasonal/` (date-bound content such as Christmas or Easter).
|
||||||
|
|
||||||
|
The split layout is much easier to maintain — you edit a small focused file
|
||||||
|
instead of a 43 MB FurnitureData.json — and lets you keep vendor and operator
|
||||||
|
content cleanly separated.
|
||||||
|
|
||||||
|
### Directory layout
|
||||||
|
|
||||||
|
```
|
||||||
|
nitro-assets/gamedata/furnidata/
|
||||||
|
manifest.json5 # { "tiers": ["core", "custom", "seasonal"] }
|
||||||
|
core/
|
||||||
|
manifest.json5 # { "files": ["floor-001.json5", ..., "wall-001.json5"] }
|
||||||
|
floor-001.json5
|
||||||
|
floor-002.json5
|
||||||
|
wall-001.json5
|
||||||
|
custom/ # OPTIONAL — created by you
|
||||||
|
manifest.json5 # { "files": ["my-rares.json5"] }
|
||||||
|
my-rares.json5
|
||||||
|
seasonal/ # OPTIONAL — created by you
|
||||||
|
manifest.json5
|
||||||
|
xmas-2026.json5
|
||||||
|
```
|
||||||
|
|
||||||
|
Each tier is loaded in order. Within a tier, files load in the order listed in
|
||||||
|
its `manifest.json5`. Items in later layers override items in earlier layers
|
||||||
|
when they share the same identifier (`id`, `classname`, `name`, or the
|
||||||
|
top-level key for flat dictionaries).
|
||||||
|
|
||||||
|
### Generating the `core/` tier from a legacy file
|
||||||
|
|
||||||
|
Use the bundled CLI splitter:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/split-gamedata.mjs \
|
||||||
|
--input ~/legacy-gamedata/FurnitureData.json \
|
||||||
|
--output ~/nitro-assets/gamedata/furnidata
|
||||||
|
```
|
||||||
|
|
||||||
|
It auto-detects the gamedata type from the file's top-level keys and applies
|
||||||
|
the strategy that makes the most sense:
|
||||||
|
|
||||||
|
| Type | Split strategy |
|
||||||
|
|----------------------|---------------------------------------------|
|
||||||
|
| EffectMap | one file per effect `type` (dance, fx, ...) |
|
||||||
|
| FigureData | one `palettes.json5` + one file per setType |
|
||||||
|
| FigureMap | chunks of `libraries` (default 500/file) |
|
||||||
|
| FurnitureData | floor / wall, chunks of `furnitype` (300) |
|
||||||
|
| HabboAvatarActions | grouped by `state` (or single file if ≤1) |
|
||||||
|
| ProductData | chunks of products (default 500) |
|
||||||
|
| ExternalTexts/UITexts| grouped by key prefix (e.g. `gamecenter.*`) |
|
||||||
|
|
||||||
|
Useful flags: `--type=<name>` to force the type, `--chunk-size=N` to override
|
||||||
|
the default chunk size, `--json` to emit standard JSON instead of JSON5,
|
||||||
|
`--force` to overwrite an existing output directory. Full reference:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/split-gamedata.mjs --help
|
||||||
|
```
|
||||||
|
|
||||||
|
We only ship the `core/` tier with vendor baselines — `custom/` and `seasonal/`
|
||||||
|
are operator-owned: create their manifests when you need them and the loader
|
||||||
|
picks them up automatically.
|
||||||
|
|
||||||
|
### Pointing the renderer at a directory
|
||||||
|
|
||||||
|
In `public/configuration/renderer-config.json`, replace the legacy file URL
|
||||||
|
with the directory URL (note the trailing slash — that's how the loader
|
||||||
|
detects split mode):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
// single file (legacy, still supported):
|
||||||
|
"furnidata.url": "https://example.com/nitro-assets/gamedata/FurnitureData.json",
|
||||||
|
|
||||||
|
// directory (split mode):
|
||||||
|
"furnidata.url": "https://example.com/nitro-assets/gamedata/furnidata/",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both styles work; you can migrate one gamedata file at a time.
|
||||||
|
|
||||||
## Installation (manual)
|
## Installation (manual)
|
||||||
|
|
||||||
- First you should open terminal and navigate to the folder where you want to clone Nitro and Nitro-Renderer
|
- First you should open terminal and navigate to the folder where you want to clone Nitro and Nitro-Renderer
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
|
import { dirname, basename, resolve, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const HELP = `
|
||||||
|
Nitro V3 — gamedata splitter
|
||||||
|
|
||||||
|
Takes a legacy single-file gamedata JSON (EffectMap, FigureData, FigureMap,
|
||||||
|
FurnitureData, HabboAvatarActions, ProductData, ExternalTexts, UITexts) and
|
||||||
|
produces the directory layout consumed by the split-aware loader:
|
||||||
|
|
||||||
|
<output>/
|
||||||
|
manifest.json5 tier order (defaults to core/custom/seasonal)
|
||||||
|
core/
|
||||||
|
manifest.json5 files list, in load order
|
||||||
|
<part1>.json5
|
||||||
|
<part2>.json5
|
||||||
|
...
|
||||||
|
|
||||||
|
Custom and seasonal tiers are NOT generated — those are operator-owned and
|
||||||
|
will be auto-discovered by the loader if their manifest.json5 exists.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node scripts/split-gamedata.mjs --input <file> --output <dir> [flags]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--input <path> Path to the legacy JSON (or JSON5) file
|
||||||
|
--output <dir> Target directory (will contain core/, manifest.json5)
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--type <name> Force the gamedata type (effectmap, figuredata,
|
||||||
|
figuremap, furnidata, avatar-actions, productdata,
|
||||||
|
external-texts, ui-texts). Default: auto-detect
|
||||||
|
--chunk-size <N> Items per chunk for the bucket-based splits.
|
||||||
|
Defaults: figuremap=500, furnidata=300, productdata=500,
|
||||||
|
external-texts/ui-texts split by prefix instead
|
||||||
|
--json (or --legacy) Emit standard JSON instead of JSON5 (no comments)
|
||||||
|
--force Overwrite the output directory if it already exists
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/split-gamedata.mjs \\
|
||||||
|
--input ~/gamedata/FurnitureData.json \\
|
||||||
|
--output ~/nitro-assets/gamedata/furnidata
|
||||||
|
|
||||||
|
node scripts/split-gamedata.mjs \\
|
||||||
|
--input ./EffectMap.json --output ./effectmap --chunk-size 50
|
||||||
|
|
||||||
|
After splitting, point your renderer-config at the directory (note the
|
||||||
|
trailing slash):
|
||||||
|
|
||||||
|
"furnidata.url": "https://example.com/nitro-assets/gamedata/furnidata/"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const opts = {
|
||||||
|
input: null,
|
||||||
|
output: null,
|
||||||
|
type: null,
|
||||||
|
chunkSize: null,
|
||||||
|
asJson5: true,
|
||||||
|
force: false,
|
||||||
|
help: false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = args[i];
|
||||||
|
if (a === '--help' || a === '-h') opts.help = true;
|
||||||
|
else if (a === '--input') opts.input = args[++i];
|
||||||
|
else if (a === '--output') opts.output = args[++i];
|
||||||
|
else if (a === '--type') opts.type = args[++i];
|
||||||
|
else if (a === '--chunk-size') opts.chunkSize = parseInt(args[++i], 10);
|
||||||
|
else if (a === '--json' || a === '--legacy') opts.asJson5 = false;
|
||||||
|
else if (a === '--json5') opts.asJson5 = true;
|
||||||
|
else if (a === '--force') opts.force = true;
|
||||||
|
else if (a.startsWith('--input=')) opts.input = a.slice('--input='.length);
|
||||||
|
else if (a.startsWith('--output=')) opts.output = a.slice('--output='.length);
|
||||||
|
else if (a.startsWith('--type=')) opts.type = a.slice('--type='.length);
|
||||||
|
else if (a.startsWith('--chunk-size=')) opts.chunkSize = parseInt(a.slice('--chunk-size='.length), 10);
|
||||||
|
else {
|
||||||
|
process.stderr.write(`Unknown flag: ${ a }\n`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.help) {
|
||||||
|
process.stdout.write(HELP);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.input || !opts.output) {
|
||||||
|
process.stderr.write('Missing --input or --output. Use --help for usage.\n');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(opts.input)) {
|
||||||
|
process.stderr.write(`Input file not found: ${ opts.input }\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectType = (data) => {
|
||||||
|
if (!data || typeof data !== 'object') return null;
|
||||||
|
const keys = new Set(Object.keys(data));
|
||||||
|
if (keys.has('roomitemtypes') || keys.has('wallitemtypes')) return 'furnidata';
|
||||||
|
if (keys.has('palettes') && keys.has('setTypes')) return 'figuredata';
|
||||||
|
if (keys.has('libraries')) return 'figuremap';
|
||||||
|
if (keys.has('effects')) return 'effectmap';
|
||||||
|
if (keys.has('actions')) return 'avatar-actions';
|
||||||
|
if (keys.has('productdata')) return 'productdata';
|
||||||
|
// Flat dict heuristic: many top-level scalar keys with dots → texts
|
||||||
|
if (keys.size > 30 && Object.values(data).every(v => typeof v === 'string')) {
|
||||||
|
const sampleKey = Object.keys(data)[0] || '';
|
||||||
|
return sampleKey.includes('.') ? 'external-texts' : 'ui-texts';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitArrayInChunks = (arr, size) => {
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < arr.length; i += size) chunks.push(arr.slice(i, i + size));
|
||||||
|
return chunks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitByPrefix = (dict, maxPerBucket = 800) => {
|
||||||
|
const buckets = new Map();
|
||||||
|
for (const key of Object.keys(dict)) {
|
||||||
|
const prefix = key.split('.')[0] || '_other';
|
||||||
|
if (!buckets.has(prefix)) buckets.set(prefix, {});
|
||||||
|
buckets.get(prefix)[key] = dict[key];
|
||||||
|
}
|
||||||
|
// Merge small buckets into a shared one to reduce manifest noise
|
||||||
|
const out = [];
|
||||||
|
const small = {};
|
||||||
|
let smallCount = 0;
|
||||||
|
for (const [name, content] of buckets) {
|
||||||
|
const size = Object.keys(content).length;
|
||||||
|
if (size < 50) {
|
||||||
|
Object.assign(small, content);
|
||||||
|
smallCount += size;
|
||||||
|
} else {
|
||||||
|
// split further if oversized
|
||||||
|
if (size > maxPerBucket) {
|
||||||
|
const entries = Object.entries(content);
|
||||||
|
const slices = splitArrayInChunks(entries, maxPerBucket);
|
||||||
|
slices.forEach((slice, idx) => {
|
||||||
|
const part = {};
|
||||||
|
for (const [k, v] of slice) part[k] = v;
|
||||||
|
out.push([ `${ name }-${ String(idx + 1).padStart(3, '0') }`, part ]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
out.push([ name, content ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (smallCount > 0) out.push([ '_misc', small ]);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeName = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
||||||
|
|
||||||
|
const ext = () => opts.asJson5 ? 'json5' : 'json';
|
||||||
|
|
||||||
|
const writePart = (filePath, data, headerComment) => {
|
||||||
|
const body = JSON.stringify(data, null, 4);
|
||||||
|
const out = opts.asJson5 && headerComment
|
||||||
|
? `// ${ headerComment }\n${ body }\n`
|
||||||
|
: `${ body }\n`;
|
||||||
|
writeFileSync(filePath, out, 'utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSplits = (type, data) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'effectmap': {
|
||||||
|
const effects = data.effects || [];
|
||||||
|
const byType = new Map();
|
||||||
|
for (const e of effects) {
|
||||||
|
const t = sanitizeName(e.type || 'other');
|
||||||
|
if (!byType.has(t)) byType.set(t, []);
|
||||||
|
byType.get(t).push(e);
|
||||||
|
}
|
||||||
|
return Array.from(byType, ([ t, items ]) => ({
|
||||||
|
name: `effects-${ t }.${ ext() }`,
|
||||||
|
comment: `Effects of type "${ t }" (${ items.length } items)`,
|
||||||
|
content: { effects: items }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case 'figuredata': {
|
||||||
|
const parts = [];
|
||||||
|
if (data.palettes?.length) {
|
||||||
|
parts.push({
|
||||||
|
name: `palettes.${ ext() }`,
|
||||||
|
comment: `Color palettes (${ data.palettes.length })`,
|
||||||
|
content: { palettes: data.palettes }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const st of (data.setTypes || [])) {
|
||||||
|
const t = sanitizeName(st.type);
|
||||||
|
parts.push({
|
||||||
|
name: `settype-${ t }.${ ext() }`,
|
||||||
|
comment: `setType "${ st.type }" (paletteId=${ st.paletteId })`,
|
||||||
|
content: { setTypes: [ st ] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
case 'figuremap': {
|
||||||
|
const libs = data.libraries || [];
|
||||||
|
const size = opts.chunkSize || 500;
|
||||||
|
const chunks = splitArrayInChunks(libs, size);
|
||||||
|
return chunks.map((chunk, idx) => ({
|
||||||
|
name: `libraries-${ String(idx + 1).padStart(3, '0') }.${ ext() }`,
|
||||||
|
comment: `libraries ${ idx * size + 1 }..${ idx * size + chunk.length } of ${ libs.length }`,
|
||||||
|
content: { libraries: chunk }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case 'furnidata': {
|
||||||
|
const parts = [];
|
||||||
|
const size = opts.chunkSize || 300;
|
||||||
|
const floor = data.roomitemtypes?.furnitype || [];
|
||||||
|
const wall = data.wallitemtypes?.furnitype || [];
|
||||||
|
const floorChunks = splitArrayInChunks(floor, size);
|
||||||
|
floorChunks.forEach((chunk, idx) => {
|
||||||
|
parts.push({
|
||||||
|
name: `floor-${ String(idx + 1).padStart(3, '0') }.${ ext() }`,
|
||||||
|
comment: `Floor furniture ${ idx * size + 1 }..${ idx * size + chunk.length } of ${ floor.length }`,
|
||||||
|
content: { roomitemtypes: { furnitype: chunk } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const wallChunks = splitArrayInChunks(wall, size);
|
||||||
|
wallChunks.forEach((chunk, idx) => {
|
||||||
|
parts.push({
|
||||||
|
name: `wall-${ String(idx + 1).padStart(3, '0') }.${ ext() }`,
|
||||||
|
comment: `Wall furniture ${ idx * size + 1 }..${ idx * size + chunk.length } of ${ wall.length }`,
|
||||||
|
content: { wallitemtypes: { furnitype: chunk } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
case 'avatar-actions': {
|
||||||
|
const actions = data.actions || [];
|
||||||
|
const byState = new Map();
|
||||||
|
for (const a of actions) {
|
||||||
|
const s = sanitizeName(a.state || 'other');
|
||||||
|
if (!byState.has(s)) byState.set(s, []);
|
||||||
|
byState.get(s).push(a);
|
||||||
|
}
|
||||||
|
if (byState.size <= 1) {
|
||||||
|
return [ {
|
||||||
|
name: `actions.${ ext() }`,
|
||||||
|
comment: `All avatar actions (${ actions.length })`,
|
||||||
|
content: { actions }
|
||||||
|
} ];
|
||||||
|
}
|
||||||
|
return Array.from(byState, ([ s, items ]) => ({
|
||||||
|
name: `actions-${ s }.${ ext() }`,
|
||||||
|
comment: `Actions in state "${ s }" (${ items.length })`,
|
||||||
|
content: { actions: items }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case 'productdata': {
|
||||||
|
const products = data.productdata?.product || [];
|
||||||
|
const size = opts.chunkSize || 500;
|
||||||
|
const chunks = splitArrayInChunks(products, size);
|
||||||
|
return chunks.map((chunk, idx) => ({
|
||||||
|
name: `products-${ String(idx + 1).padStart(3, '0') }.${ ext() }`,
|
||||||
|
comment: `Products ${ idx * size + 1 }..${ idx * size + chunk.length } of ${ products.length }`,
|
||||||
|
content: { productdata: { product: chunk } }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
case 'external-texts':
|
||||||
|
case 'ui-texts': {
|
||||||
|
const buckets = splitByPrefix(data, opts.chunkSize || 800);
|
||||||
|
return buckets.map(([ name, content ]) => ({
|
||||||
|
name: `${ sanitizeName(name) }.${ ext() }`,
|
||||||
|
comment: `${ name } (${ Object.keys(content).length } keys)`,
|
||||||
|
content
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown gamedata type: ${ type }. Use --type to force one.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const raw = readFileSync(opts.input, 'utf8');
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const JSON5 = require('json5');
|
||||||
|
data = JSON5.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
process.stderr.write(`Could not parse ${ opts.input } as JSON nor JSON5: ${ e.message }\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = opts.type || detectType(data);
|
||||||
|
if (!type) {
|
||||||
|
process.stderr.write('Could not auto-detect gamedata type. Pass --type=<name>. See --help.\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outDir = resolve(opts.output);
|
||||||
|
const coreDir = join(outDir, 'core');
|
||||||
|
|
||||||
|
if (existsSync(outDir)) {
|
||||||
|
if (!opts.force) {
|
||||||
|
process.stderr.write(`Output directory already exists: ${ outDir }. Use --force to overwrite.\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
rmSync(outDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
mkdirSync(coreDir, { recursive: true });
|
||||||
|
|
||||||
|
const parts = buildSplits(type, data);
|
||||||
|
if (!parts.length) {
|
||||||
|
process.stderr.write(`No content produced for type ${ type }. Input may be empty.\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
writePart(join(coreDir, part.name), part.content, part.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreManifest = {
|
||||||
|
files: parts.map(p => p.name)
|
||||||
|
};
|
||||||
|
const coreManifestBody = opts.asJson5
|
||||||
|
? `// Auto-generated by split-gamedata.mjs from ${ basename(opts.input) }\n// Type: ${ type } — ${ parts.length } files, ${ parts.reduce((n, p) => n + JSON.stringify(p.content).length, 0).toLocaleString() } chars total\n${ JSON.stringify(coreManifest, null, 4) }\n`
|
||||||
|
: `${ JSON.stringify(coreManifest, null, 4) }\n`;
|
||||||
|
writeFileSync(join(coreDir, `manifest.${ ext() }`), coreManifestBody, 'utf8');
|
||||||
|
|
||||||
|
const rootManifest = { tiers: [ 'core', 'custom', 'seasonal' ] };
|
||||||
|
const rootManifestBody = opts.asJson5
|
||||||
|
? `// Root manifest — load order of tiers (later overrides earlier by id/classname).\n// Drop a custom/manifest.${ ext() } and/or seasonal/manifest.${ ext() } to add\n// override tiers without touching core/.\n${ JSON.stringify(rootManifest, null, 4) }\n`
|
||||||
|
: `${ JSON.stringify(rootManifest, null, 4) }\n`;
|
||||||
|
writeFileSync(join(outDir, `manifest.${ ext() }`), rootManifestBody, 'utf8');
|
||||||
|
|
||||||
|
process.stdout.write([
|
||||||
|
`[split-gamedata] ${ type } -> ${ outDir }`,
|
||||||
|
` ${ parts.length } file(s) in core/`,
|
||||||
|
` tiers: core (always loaded), custom (optional), seasonal (optional)`,
|
||||||
|
` point renderer-config at: ${ outDir.replace(/\\/g, '/') }/`,
|
||||||
|
''
|
||||||
|
].join('\n'));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (e) {
|
||||||
|
process.stderr.write(`[split-gamedata] ${ e.message }\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user