Phase C of #188 — the last phase of server-side multi-language support.
Adds Session.I18n(), a cached per-session i18n table resolver built via
getLangStringsFor(s.Lang()). The pointer is stable until SetLang
invalidates the cache, so hot-path handlers pay zero allocations on
repeated calls. All 51 s.server.i18n.* call sites across commands,
guild, guild scout, cafe, and cast-binary handlers now route through
s.I18n().*, so chat replies, guild invite mail templates, cafe reset
notices, and quest-timer broadcasts are served in the player's
preferred language instead of the server-wide default.
Scenario JSON gets the same plain-or-map LocalizedString treatment
that quests received in phase B: subheader Strings and inline entry
Text accept either a plain string (backwards compatible) or a
language-keyed object. CompileScenarioJSON takes the compiling
session's language, loadScenarioBinary passes s.Lang(), and
ParseScenarioBinary emits plain-string LocalizedStrings so existing
.bin files round-trip byte-for-byte through the JSON path.
World-wide broadcasts (Raviente siege announcements via
BroadcastRaviente) intentionally stay on the server default — they
have no single-session context to resolve against.
Add TestScenarioRoundTrip_RealFiles covering 7 real scenario files
(cat=0/1/3, T101/T103, with and without chunk1). Tests skip gracefully
when game data is absent so CI stays green.
Decoded metadata structure from analysis of 145k+ real scenario files:
- Chunk0 m[0]/m[1] = CategoryID/MainID; m[5] = str0_len (offset to
str1); m[6] = MainID (cat=0) or 0xFFFF; m[8] = constant 5.
- Chunk1 m[9]=cumOff[2], m[10]=cumOff[1], m[14]=cumOff[3],
m[15]=total_string_bytes; m[11-13]/m[16-17] = dialog script offsets
beyond the 0xFF sentinel; m[20] = constant 5; m[21] ≈ data_size.
Update docs/scenario-format.md with full field tables for both chunks.
Closes#172. Scenario files in bin/scenarios/ can now be authored as
.json instead of .bin — the server compiles them to wire format on
load, falling back to .bin if no .json is present.
- Add ParseScenarioBinary / CompileScenarioJSON in scenario_json.go;
supports sub-header format (strings as UTF-8, metadata as base64),
inline format, and raw JKR blobs.
- Add PackSimple JKR type-3 (LZ77) compressor in jpk_compress.go,
ported from ReFrontier JPKEncodeLz.cs; round-trip tested against
UnpackSimple.
- Fix off-by-one in processDecode (jpk.go): last literal byte was
silently dropped for data that does not end on a back-reference.
- Wire loadScenarioBinary into handleMsgSysGetFile replacing the
inline os.ReadFile call; mirrors the existing loadQuestBinary pattern.
- Rewrite docs/scenario-format.md with full container/sub-header spec
and JSON schema examples.