The Infostand Borders merge (origin/Dev 4b7d04d, upstream commit) added
user.borderId = (wrapper.bytesAvailable ? wrapper.readInt() : 0);
inside the per-user loop in RoomUnitParser (the parser for the
RoomUsersComposer packet — header 3920 — which ships the full roster
on room enter). The guard is unsafe inside a loop: `bytesAvailable`
is a boolean meaning "any bytes left in the WHOLE packet?", not
"any bytes left in THIS user record". For every user except the
last one, `bytesAvailable === true` because the NEXT user's bytes
still follow, so the parser reads an int and steals 4 bytes from
the next user — cascade corruption of the entire roster.
Symptom in production: users don't see each other on first room
sight. The roster arrives, the parser sfasa, RoomEngine drops the
malformed records.
Fix: stop reading borderId inside the loop. The per-user border id
is shipped separately via RoomUnitInfoParser (single-user packet,
no loop), where the bytesAvailable guard is safe. The roster
packet's last-tail extension story stays clean for any future
trailing block the same way other parsers do — but only when the
guard is the LAST read in the packet, not a per-record one.
This also makes the renderer wire-compatible with both old
emulators (no borderId at all) and the new Arcturus version that
ships borderId in RoomUsersComposer — the latter just has 4 extra
trailing bytes per user that the parser ignores. A follow-up change
on Arcturus' RoomUsersComposer can drop the borderId append, or
keep it and the client simply doesn't read it from the roster
(which is fine — the infostand re-fetch via RoomUnitInfoParser
gives the authoritative border).
mvn-equivalent: yarn compile:fast clean, vitest 138/138.
Nitro Renderer
nitro-renderer is a Javascript library for rendering Nitro in the browser using PixiJS
Installation
npm
npm install @nitrots/nitro-renderer
yarn
yarn add @nitrots/nitro-renderer
JSON / JSON5 configuration parser
Every configuration file and gamedata file loaded by the renderer (figuredata,
furnidata, productdata, effectmap, avatar actions, etc.) goes through
@nitrots/utils → JsonParser.ts. The parser supports three modes, selected at
the host build time through the compile-time constant __NITRO_JSON_MODE__:
| Mode | Behaviour |
|---|---|
legacy |
Strict JSON.parse only. Comments / trailing commas raise a clear error. |
json5 |
JSON5.parse only. Accepts comments, trailing commas, single quotes. |
auto |
Try strict JSON first, fall back to JSON5. Default when the flag is unset. |
URL hints are still honoured: files ending in .json5 (or served with a
application/json5 content-type) always go through JSON5, regardless of mode.
Wiring the flag into a host
The renderer does not ship its own build for the flag — the host application (typically Nitro V3) defines it via its bundler. Example with Vite:
// vite.config.mjs in the host
export default defineConfig({
define: {
__NITRO_JSON_MODE__: JSON.stringify('json5') // or 'legacy' / 'auto'
}
});
If the constant is not defined the parser falls back to auto, which preserves
the original behaviour of older releases — so existing hosts keep working
without any change.
Using the parser directly
import { parseConfigJson, fetchConfigJson } from '@nitrots/utils';
const data = parseConfigJson<MyConfig>(rawText, '/configuration/ui-config.json');
const data2 = await fetchConfigJson<MyConfig>('/configuration/ui-config.json5');
Errors carry the source URL and, in legacy mode, a hint about switching to
JSON5 — making misconfigurations easy to diagnose in production logs.
Split-aware gamedata loader
@nitrots/utils also exports loadGamedata, the loader that backs every
gamedata consumer in the renderer (FurnitureDataLoader, ProductDataLoader,
EffectAssetDownloadManager, AvatarRenderManager, LocalizationManager). It
accepts either a single-file URL (legacy) or a directory URL (split
mode) — detected automatically by the trailing slash.
Directory layout
<gamedata-dir>/
manifest.json5 # OPTIONAL — { "tiers": ["core", "custom", "seasonal"] }
core/
manifest.json5 # REQUIRED — { "files": ["a.json5", "b.json5", ...] }
a.json5
b.json5
custom/ # OPTIONAL tier
manifest.json5
overrides.json5
seasonal/ # OPTIONAL tier
manifest.json5
xmas.json5
If the directory manifest.json5 is absent, the loader falls back to the
default tier order core → custom → seasonal. Each tier is skipped silently
if its manifest.json5 is missing.
Merge semantics
mergeGamedata(a, b) (also exported) implements the rules below; tiers and
files within a tier are merged in order, with later layers overriding
earlier ones:
| Combination | Result |
|---|---|
| Two plain objects | recursive merge, key by key |
| Two arrays of objects sharing an id key | merged by id (later overrides earlier) |
| Two arrays without an id key | concatenated |
| Anything else | later value wins |
Recognised id keys (in priority order): id, classname, name. Pass
mergeArrayIdKeys in the options object to extend or override them.
Programmatic usage
import { loadGamedata, mergeGamedata } from '@nitrots/utils';
// host code never needs to care whether the URL is split or not
const furnidata = await loadGamedata('https://example.com/gamedata/furnidata/');
// merge ad-hoc if you load tiers manually
const merged = mergeGamedata(coreData, customData);
A CLI splitter for legacy single-file gamedata lives in the Nitro V3 client
repo at scripts/split-gamedata.mjs — see the Nitro V3 README for usage.