save-reader: real tag names (byte-confirmed), 36 tests, strict 3/3
This commit is contained in:
parent
c68ac17106
commit
436a51fa04
6 changed files with 242 additions and 18 deletions
|
|
@ -49,4 +49,4 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| determinism oracle | verify | verified | high | 100% | 2026-09-07 | BYTE-IDENTICAL across 5 runs incl. cross-process: (Autosave).sav 978041ac…, (Autosave EndTurn).sav bb4fd9ac…; gzip MTIME=0; only loaded-post-turn re-save differs (Player.Status 4->0, Summary.Checksum). findings/subsystems/determinism-oracle.md |
|
||||
| engine: shim trace/compare emitter | engine | verified | high | 100% | 2026-09-07 | merged (sots-engine 9e110b4): emitter byte-exact vs mkfixture, tracer, snapshot/diff, Hook<Descriptor>; ctest 18/18; tracecmp exits 0/1/2 as specified; shim links |
|
||||
| engine parity: first-wins keys + tokenizer rules | engine | in-progress | — | 0% | 2026-09-07 | fix mars/text + mars/parse + Python oracle to confirmed Mars::Script/GlobalConsts semantics; re-verify 100% |
|
||||
| save-format tag corrections | verify | in-progress | — | 0% | 2026-09-07 | otnF/nextid/FAIDes/DHide/DWep/DName/ords/wpts into SAVE_FORMAT + save_reader; strict must stay 3/3 |
|
||||
| save-format tag corrections | verify | verified | high | 100% | 2026-09-07 | byte-confirmed at offsets (otnF x62, nextid, FAIDes/DHide/DWep/DName x43, ords, wpts, paths); reader A()-matches them; 36 tests; strict 3/3, infos 259->197; SAVE_FORMAT §10 changelog |
|
||||
|
|
|
|||
|
|
@ -37,3 +37,4 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
|
|||
- **RESOLVED: End-Turn oracle** — deterministic and cross-process: compare autosaves by sha256, no masking. Only when diffing a re-save of a *loaded post-turn* autosave: mask `Player.Status` (→0) and recompute/ignore `Summary.Checksum` (additive). (from [[determinism-oracle]])
|
||||
- **SAVE_FORMAT tag corrections (fix Python reader + spec)** — real on-disk tags: `otnF` (not `ontF`) in Odes/Owep/Otch, `nextid` (not `nextId`) in NdGr2, Design = `FAIDes/DHide/DWep/DName`; `ords`/`wpts` are real tags. Python's positional R() matching hid these. RNG: float mapping `(float)(y*2^-32)`, `next_int` mask, and lazy-vs-eager twist at `left==0` still need binary confirmation. (from [[mars-stream]])
|
||||
- **RESOLVED: tech `allows` default** — unlisted species = 1.0 (confirmed in `FUN_005822d0` tree-creation roll, strategic-turn-internals §2); `game/data` uses 100. Still open from game/data: what the engine's converter does with the 34 malformed tokens (`force_right o`, `crew false`, `1.0f`, `0-5`, ``90\``); repeated scalars in a block are last-wins per the sequential if/else consumers (loader-prototypes §M3) — confirm on a fixture in compare mode. (from [[game-data]])
|
||||
- **RESOLVED: SAVE_FORMAT tag names** — all corrections confirmed by bytes and applied to the Python reader + spec (§10). Residual doc debt: `findings/objects/save-editor-structs.md` and `verify/design-rules/SHIP_DESIGN_RULES.md` still quote R1's `ontF/faiDes/nextId` spellings (R1-provenance; annotate rather than rewrite). (from [[SAVE_FORMAT]])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# save_reader --strict against real save turn2-state.sav (padding=joint, 0 resyncs, 0 hint-failures, 2503 raw bytes = opaque RNG blob)
|
||||
# result: strict exit 0 -- 0 errors, 0 warnings, 259 info
|
||||
# after findings/objects/schema-gaps-resolved.md §10 (reader: string values never ASCII-tested, len-0 strings;
|
||||
# schema: NVO.indi unconditional, PrNSp iff PrMax>0, zdsc pairs, turnstats/tch int, Summary/CreateParams tags, CDT framed)
|
||||
# same result on turn1-state.sav (257 info) and turn3-state.sav (259 info)
|
||||
259x [info] tag '.' read positionally as <R() name> (NULL-named items: Summary Settings/FxCrID, CreateParams MapP planets, Sim id lists, Vector3 bodies)
|
||||
# result: strict exit 0 -- 0 errors, 0 warnings, 197 info
|
||||
# after SAVE_FORMAT.md §10 tag-name corrections (from the C++ round-trip writer, re-verified in the bytes):
|
||||
# otnF (not ontF) leads every odes/owep/otch element; nextid (not nextId) in NdGr2;
|
||||
# Des header tags FAIDes DHide DWep DName; ords / wpts / paths are named framed arrays -> all A() now
|
||||
# previous revision (schema-gaps-resolved.md §10): 259 info, of which 62 were "tag 'otnF' read positionally as 'ontF'"
|
||||
# same result on turn1-state.sav (197 info) and turn3-state.sav (197 info; turn3 is the only save with an FPlan/wpts)
|
||||
197x [info] tag '.' read positionally as <R() name> (NULL-named items only: Summary Settings/FxCrID, CreateParams MapP planets, Sim id lists, Vector3 bodies)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ editors (R1 Bardez, R2 SOTSedit; `save-editor-structs.md`), the binary member ta
|
|||
`findings/objects/schema-gaps-resolved.md` (round 3), which settled every item that was
|
||||
marked VERIFY in the previous revision of this file. The synthetic fixture
|
||||
(`save_writer_stub.py`) round-trips under the same assumptions (`test_save_reader.py`).
|
||||
The C++ typed writer (which reproduces the real saves byte-identically) later exposed a
|
||||
handful of tag names this file had wrong or left unnamed; they are corrected throughout
|
||||
and listed in §10.
|
||||
|
||||
## 1. Container
|
||||
|
||||
|
|
@ -102,7 +105,9 @@ NumLeg NumNotes NumPR NSprj Nexp NWeapXcl ndeflay rdtc numcreps ninv NumFlts Num
|
|||
NumMnF NVO NVE NVs NumPlgs2 PopNG mnsp nadct haltc PrNSp NTH nply ncls seno2 spc zdsc NumIDs
|
||||
PlayerIDs DesignIDs SystemIDs FleetIDs ShipIDs TradeIDs AllExc AllExcCF`. Count and element
|
||||
tags inside framed arrays (`Players`, `GOWinPly`, `sacq`, `slost`, `dipstats`, `preps`,
|
||||
`odes`, `owep`, `otch`, `cme2`, `Ojvs`, `MapP` planets …) are always `"."` (§3).
|
||||
`odes`, `owep`, `otch`, `cme2`, `Ojvs`, `ords`, `wpts`, `paths`, `MapP` planets …) are
|
||||
always `"."` (§3). `ords` (BuildQueue), `wpts` (FlightPlan) and `paths` (NdGr2) are real
|
||||
framed-array tags, not positional items (§10).
|
||||
|
||||
Uncounted lists: `hist` holds n × `stats` frames until its END; `Sim` holds 7 × (`ISsp`
|
||||
string, `ISsu` float) after the players; the root holds one `CD` frame per custom-data id.
|
||||
|
|
@ -116,9 +121,9 @@ string, `ISsu` float) after the players; the root holds one `CD` frame per custo
|
|||
| system-level `hindi` true | `indi` frame (IndependenceInfo) after `NVs` |
|
||||
| `NVO` entry | `indi` frame is **always** present after `isind` (inline member of the map value; `isind` does not gate it) |
|
||||
| `PrMax > 0` | `PrNSp` + n × (`PrSp`, `PrNum`); with `PrMax == 0` the `PrisH` frame ends right after `PrMax` |
|
||||
| `HFPlan` true | `FPlan` frame |
|
||||
| `HFPlan` true | `FPlan` frame: `wpts{"." n, n×"."{Wpt Tp nrt{nrp nrf nrt}}} FPsp2 FPeta2 FPogn2 FPdpos pnd` (turn3 only) |
|
||||
| `HLay` true | `Lay` frame (opaque) |
|
||||
| `hbq` true | `BQ2` frame; `hsp` true → `pop`, `ppop` Population frames |
|
||||
| `hbq` true | `BQ2` frame (same body as `BQ`: `ords{"." n, n×"."{desID con conleft sav ordID}}`); `hsp` true → `pop`, `ppop` Population frames |
|
||||
| `HasAIR` true | `AIR` frame (opaque) |
|
||||
| system `PID` non-null | `BQ` frame present (reader: optional by name) |
|
||||
| `SvSctOb` pointer non-NULL | `SvSctOb` frame before `zdsc` (present in the three real saves; `Opt`) |
|
||||
|
|
@ -155,6 +160,14 @@ IsFxAv FxAv Tag Pwd(string) Team Settings{4×"." int}`.
|
|||
`stats` (PlayerTurnStats): `pop`(int64) `sacq{}` `slost{}` (VectorHelper<SystemEvent>: `set ses seop
|
||||
senp seno2 n×seot2`) `trn almem inc tdinc sav col bat tch`(int) `ncls` 3 × (`cls shpt shpl shpk satt
|
||||
satl satk`).
|
||||
`Des` (design header, under `NumDes`/`NumLeg` n × (`DesID`, `Des{}`)): `FAIDes`(bool) `DHide`(bool)
|
||||
`DWep`(int) `DName`(string), then the `DSec` section frames (kept generic).
|
||||
Objective records (Player `odes`/`owep`/`otch`, each a framed VectorHelper of `"."` elements):
|
||||
`odes` = `otnF otnL odid opid`; `owep` = `otnF otnL odet owep`(string) `owith`; `otch` = `otnF otnL odet
|
||||
otch`(string) `owith`.
|
||||
`NdGr2` (node grid): `paths{"." n, n×"."{npt npid npfr npto npctm npcby npdtn npdtf npenp npuse nptf}}`
|
||||
then `nextid`(int).
|
||||
`BQ` (system build queue): `ords{"." n, n×"."{desID con conleft sav ordID}}` — count 0 in turn1..3.
|
||||
|
||||
Everything not covered by a shape (TechTree body, Events, ShipRecs, spy2, civr, comms, Ojvs,
|
||||
Attrib, sprjs, SvSctOb, trdmgr, spymgr, CD, …) is kept as the generic
|
||||
|
|
@ -182,6 +195,9 @@ applied over R1/R2:
|
|||
* `Team` appears twice in Player: an int, later a nested `{ALid AL NA CF}` frame (typed key `Alliances`)
|
||||
* `GOWinPly` is a framed `VectorHelper<uint>` (`"."` count + n × `"."` int), empty in the real saves
|
||||
* `CreateParams`/`Summary` tag case is as written above (R1's camel-case names were wrong)
|
||||
* Objective lead tag `otnF`, node-grid `nextid`, design header `FAIDes DHide DWep DName`, and the
|
||||
framed arrays `ords`/`wpts`/`paths` are `A()` (byte-confirmed, §10); the reader keeps R1's
|
||||
keys `faiDes dHide dWep dName` in its typed output for `stock_designs.py`
|
||||
|
||||
## 8. Reader output conventions
|
||||
|
||||
|
|
@ -207,3 +223,34 @@ applied over R1/R2:
|
|||
5. `--padding auto` picks joint; `resyncs == 0` and `hint_failures == 0` on turn1/2/3;
|
||||
the only raw bytes are the `RNG` blob. Remaining unknowns are the bodies of the
|
||||
opaque frames listed in §6 (kept generic, not a parsing gap).
|
||||
6. Every tag the reader matches by name is spelled as on disk (§10); the only positional
|
||||
(`R()`) items left are NULL-named `"."` items and R1-named fields inside `crep`,
|
||||
`NdGr2.paths` elements, `MapP` and `Waypoint.nrt`.
|
||||
|
||||
## 10. Tag-name corrections (changelog)
|
||||
|
||||
Found by the C++ typed save writer, which has to emit the real tag strings to round-trip
|
||||
the saves byte-identically. The Python reader had matched these fields **positionally**
|
||||
(`R()`), so a wrong spelling in the spec never failed a parse — the walker reported each as
|
||||
an *info* ("tag 'otnF' read positionally as 'ontF'") and the typed output silently used the
|
||||
wrong name. Each entry below was re-verified by inflating the saves and searching the
|
||||
byte stream for `[int32 len][ascii]` (offsets are inflated offsets, decimal).
|
||||
|
||||
| was (spec / R1) | is (on disk) | where | evidence |
|
||||
|---|---|---|---|
|
||||
| `ontF` | **`otnF`** | first field of every `odes`/`owep`/`otch` element | `otnF` ×62 in turn2 (first @72288, in `otch`; @123344 region `odes` element `otnF otnL odid opid`); ×60 turn1, ×62 turn3. `ontF` ×0 in all three |
|
||||
| `nextId` | **`nextid`** | last item of `NdGr2` | `nextid` ×1: turn1 @564432, turn2 @575500, turn3 @580152 (value 44 in turn2, after `paths`). `nextId` ×0 |
|
||||
| `faiDes dHide dWep dName` | **`FAIDes DHide DWep DName`** | first four items of every `Des` frame | ×43 each in turn2 (first Des frame @67820: `FAIDes` @67832, `DHide` @67844, `DWep` @67856, `DName` @67868); ×41 turn1, ×43 turn3. Camel-case forms ×0 |
|
||||
| unnamed/positional | **`ords`** | framed VectorHelper inside `BQ` | ×3 in each save (turn2 @531144, 548092, 550044; each `BQ` @−12); count 0 in all |
|
||||
| unnamed/positional | **`wpts`** | framed VectorHelper inside `FPlan` | turn3 only (the only save with `HFPlan` = 1): `FPlan` @591476, `wpts` @591492, element `Wpt` @591528 `Tp` @591540 `nrt` @591552. Absent from turn1/turn2 because no fleet has a flight plan there (`HFPlan` = 0 ×6 / ×7) |
|
||||
| positional | **`paths`** | framed VectorHelper inside `NdGr2` | ×1 per save (turn2 @568072, `NdGr2` @568056); spelled as R1 had it but now matched by name |
|
||||
|
||||
Reader changes (`save_reader.py`): the fields above are `A()`; `ords`/`wpts`/`paths` lost
|
||||
the `flex` inline hedge (the game always frames them); the never-occurring `dName`/`faiDes`/
|
||||
`dHide` entries were dropped from `MANUAL_KINDS`. Typed-output keys are unchanged
|
||||
(`faiDes dHide dWep dName` via `key=`; `otnF`, `nextid`, `ords`, `wpts`, `paths` were never
|
||||
consumed under the old names). Effect on the real saves: strict still exits 0 with 0
|
||||
resyncs / 0 hint-failures; the info count drops from 257/259/259 to 197 on turn1/2/3 — the
|
||||
60/62/62 `otnF` "read positionally" notes are gone, and every remaining info is a `"."`
|
||||
item. `test_save_reader.py::TagNameCorrectionsTest` pins each correction and fails a
|
||||
frame that uses the old spelling.
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ MoraleEvent = Shape(None, [
|
|||
BuildOrder = Shape(None, [
|
||||
A("desID", "int"), A("con", "int"), A("conleft", "int"), A("sav", "int"), A("ordID", "int"), Rest(),
|
||||
])
|
||||
BuildQueue = Shape("BQ", [R("ords", CArr(BuildOrder), flex=True), Rest()])
|
||||
BuildQueue = Shape("BQ", [A("ords", CArr(BuildOrder)), Rest()]) # `ords` = framed VectorHelper (§10)
|
||||
IndependenceInfo = Shape("indi", [
|
||||
A("indsp", "int"), A("indcl", PlayerColor), A("indnm", "string"),
|
||||
A("indav", "string"), A("indba", "string"), Rest(),
|
||||
|
|
@ -479,12 +479,19 @@ Prep = Shape(None, [
|
|||
A("ncol", "int"), A("mpwr", "int"), A("mcls", "int"), A("mmsl", "int"),
|
||||
A("nshp", "int"), A("nsat", "int"), Rest(),
|
||||
])
|
||||
Odes = Shape(None, [R("ontF", "int"), R("otnL", "int"), R("odid", "int"), R("opid", "int"), Rest()])
|
||||
Owep = Shape(None, [R("ontF", "int"), R("otnL", "int"), R("odet", "int"), R("owep", "string"), R("owith", "int"), Rest()])
|
||||
Otch = Shape(None, [R("ontF", "int"), R("otnL", "int"), R("odet", "int"), R("otch", "string"), R("owith", "int"), Rest()])
|
||||
# Objective records (elements of the framed odes/owep/otch arrays). The lead tag
|
||||
# is `otnF` on disk (R1 had `ontF`; positional matching hid the typo -- caught by
|
||||
# the C++ round-trip writer and confirmed in the bytes, SAVE_FORMAT.md §10).
|
||||
Odes = Shape(None, [A("otnF", "int"), A("otnL", "int"), A("odid", "int"), A("opid", "int"), Rest()])
|
||||
Owep = Shape(None, [A("otnF", "int"), A("otnL", "int"), A("odet", "int"), A("owep", "string"), A("owith", "int"), Rest()])
|
||||
Otch = Shape(None, [A("otnF", "int"), A("otnL", "int"), A("odet", "int"), A("otch", "string"), A("owith", "int"), Rest()])
|
||||
Note = Shape("Nts", [A("NtSys", "int"), A("NtTxt", "string"), A("NtTrn", "int"), Rest()])
|
||||
# Design header: on-disk tags are FAIDes/DHide/DWep/DName (R1's camel-case names
|
||||
# were wrong). The typed-dict keys keep R1's spelling because
|
||||
# verify/design-rules/stock_designs.py consumes them.
|
||||
Design = Shape("Des", [
|
||||
R("faiDes", "bool"), R("dHide", "bool"), R("dWep", "int"), R("dName", "string"), Rest("sections"),
|
||||
A("FAIDes", "bool", key="faiDes"), A("DHide", "bool", key="dHide"),
|
||||
A("DWep", "int", key="dWep"), A("DName", "string", key="dName"), Rest("sections"),
|
||||
])
|
||||
ConMods = Shape("ConMods", [
|
||||
A("ConMod", "float", key="ConMod0"), A("SavMod", "float", key="SavMod0"),
|
||||
|
|
@ -546,7 +553,7 @@ Player = Shape("Player", [
|
|||
NodeRoute = Shape("nrt", [A("nrp", "int"), A("nrf", "int"), A("nrt", "int"), Rest()])
|
||||
Waypoint = Shape(None, [A("Wpt", "int"), A("Tp", "int"), R("nrt", NodeRoute, flex=True), Rest()])
|
||||
FlightPlan = Shape("FPlan", [
|
||||
R("wpts", CArr(Waypoint), flex=True), A("FPsp2", "float"), A("FPeta2", "int"),
|
||||
A("wpts", CArr(Waypoint)), A("FPsp2", "float"), A("FPeta2", "int"), # `wpts` = framed VectorHelper (§10)
|
||||
A("FPogn2", "vec3"), A("FPdpos", "vec3"), A("pnd", "int"), Rest(),
|
||||
])
|
||||
PrisonerHold = Shape("PrisH", [ # PrisonerHold::Write @0x0056ec00
|
||||
|
|
@ -597,7 +604,9 @@ NodePath = Shape(None, [
|
|||
R("npcby", "int"), R("npdtn", "int"), R("npdtf", "int"), R("npenp", "int"),
|
||||
R("npuse", "int"), R("nptf", "int"), Rest(),
|
||||
])
|
||||
NodeGrid = Shape("NdGr2", [R("paths", CArr(NodePath), flex=True), R("nextId", "int"), Rest()])
|
||||
# NdGr2: `paths` (framed VectorHelper) then `nextid` -- all lower-case on disk
|
||||
# (R1 had `nextId`; hidden by positional matching, see SAVE_FORMAT.md §10).
|
||||
NodeGrid = Shape("NdGr2", [A("paths", CArr(NodePath)), A("nextid", "int"), Rest()])
|
||||
|
||||
# -- turn statistics (schema-gaps-resolved.md §6) --------------------------------
|
||||
SystemEvent = Shape(None, [ # SystemEvent::Write @0x008189a0, element tag "."
|
||||
|
|
@ -664,8 +673,9 @@ ROOT = Seq([
|
|||
MANUAL_KINDS = {
|
||||
# tech tree / designs (R1 §7)
|
||||
"TNm": "string", "tfc": "bool", "tResCost": "int", "tResDone": "int", "tAcq": "int",
|
||||
"tiAcq": "int", "tUnlck": "int", "dName": "string", "wfn": "string", "bId": "bool",
|
||||
"faiDes": "bool", "dHide": "bool",
|
||||
# (R1's `dName`/`faiDes`/`dHide` never occur on disk; the real Design tags
|
||||
# FAIDes/DHide/DWep/DName are registered from the Design shape)
|
||||
"tiAcq": "int", "tUnlck": "int", "wfn": "string", "bId": "bool",
|
||||
# objectives / comms / research / encounters (R1 §5, §6, §11)
|
||||
"cmp": "bool", "dsc": "string", "xcsn": "string", "nm": "string", "ntg": "string",
|
||||
"wep": "string", "gmch": "int", "drad": "float", "cst": "float",
|
||||
|
|
|
|||
|
|
@ -409,6 +409,170 @@ class SchemaRoundTripTest(unittest.TestCase):
|
|||
sr.read_bytes(w.bytes(), padding="joint", strict=True, schema=sr.Seq([sr.R("p", sr.PrisonerHold)]))
|
||||
|
||||
|
||||
class TagNameCorrectionsTest(unittest.TestCase):
|
||||
"""SAVE_FORMAT.md §10: tags the C++ round-trip writer had to emit to reproduce
|
||||
the real saves byte-for-byte, which the spec/reader had wrong or unnamed.
|
||||
Positional R() matching used to hide the misspellings; the fields are now
|
||||
A() and a wrong spelling is a strict failure."""
|
||||
|
||||
REAL = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "results", "saves")
|
||||
|
||||
@staticmethod
|
||||
def field_names(shape):
|
||||
return [(f.name, f.auth) for f in shape.fields if isinstance(f, sr.Field)]
|
||||
|
||||
def test_schema_declares_the_real_tags(self):
|
||||
for shape in (sr.Odes, sr.Owep, sr.Otch):
|
||||
names = self.field_names(shape)
|
||||
self.assertEqual(names[0], ("otnF", True)) # not R1's "ontF"
|
||||
self.assertTrue(all(auth for _, auth in names), names)
|
||||
self.assertEqual(self.field_names(sr.Design),
|
||||
[("FAIDes", True), ("DHide", True), ("DWep", True), ("DName", True)])
|
||||
# typed-dict keys keep R1's spelling (verify/design-rules/stock_designs.py reads them)
|
||||
self.assertEqual([f.key for f in sr.Design.fields if isinstance(f, sr.Field)],
|
||||
["faiDes", "dHide", "dWep", "dName"])
|
||||
self.assertEqual(self.field_names(sr.NodeGrid), [("paths", True), ("nextid", True)])
|
||||
self.assertEqual(self.field_names(sr.BuildQueue), [("ords", True)])
|
||||
self.assertEqual(self.field_names(sr.FlightPlan)[0], ("wpts", True))
|
||||
for f in (sr.BuildQueue.fields[0], sr.FlightPlan.fields[0], sr.NodeGrid.fields[0]):
|
||||
self.assertFalse(f.flex, f) # framed VectorHelper, no inline hedge
|
||||
|
||||
def test_catalog_knows_only_the_real_spellings(self):
|
||||
for wrong in ("ontF", "nextId", "faiDes", "dHide", "dWep", "dName"):
|
||||
self.assertNotIn(wrong, sr.GLOBAL_KINDS)
|
||||
self.assertEqual(sr.GLOBAL_KINDS["otnF"], "int")
|
||||
self.assertEqual(sr.GLOBAL_KINDS["nextid"], "int")
|
||||
self.assertEqual((sr.GLOBAL_KINDS["FAIDes"], sr.GLOBAL_KINDS["DHide"],
|
||||
sr.GLOBAL_KINDS["DWep"], sr.GLOBAL_KINDS["DName"]),
|
||||
("bool", "bool", "int", "string"))
|
||||
for name in ("ords", "wpts", "paths"):
|
||||
self.assertIsInstance(sr.GLOBAL_SHAPES[name], sr.CArr)
|
||||
|
||||
@staticmethod
|
||||
def objectives(lead):
|
||||
"""Player-level odes/owep/otch arrays as the real saves lay them out."""
|
||||
w = sw.SaveWriter()
|
||||
w.begin("odes"); w.int(".", 1)
|
||||
w.begin("."); w.int(lead, 2); w.int("otnL", 2); w.int("odid", 18); w.int("opid", 32); w.end()
|
||||
w.end()
|
||||
w.begin("owep"); w.int(".", 0); w.end()
|
||||
w.begin("otch"); w.int(".", 1)
|
||||
w.begin("."); w.int(lead, 1); w.int("otnL", 1); w.int("odet", 0)
|
||||
w.string("otch", "WEP_RedLas"); w.int("owith", 1); w.end()
|
||||
w.end()
|
||||
return w.bytes()
|
||||
|
||||
def test_objective_records_use_otnF(self):
|
||||
schema = sr.Seq([sr.A("odes", sr.CArr(sr.Odes)), sr.A("owep", sr.CArr(sr.Owep)),
|
||||
sr.A("otch", sr.CArr(sr.Otch))])
|
||||
res = sr.read_bytes(self.objectives("otnF"), padding="joint", strict=True, schema=schema)
|
||||
self.assertEqual(norm(res.typed["odes"]), [{"otnF": 2, "otnL": 2, "odid": 18, "opid": 32}])
|
||||
self.assertEqual(res.typed["owep"], [])
|
||||
self.assertEqual(norm(res.typed["otch"]),
|
||||
[{"otnF": 1, "otnL": 1, "odet": 0, "otch": "WEP_RedLas", "owith": 1}])
|
||||
self.assertFalse([i for i in res.issues if "read positionally" in i.msg])
|
||||
# R1's misspelling is no longer accepted silently
|
||||
with self.assertRaises(sr.SaveFormatError):
|
||||
sr.read_bytes(self.objectives("ontF"), padding="joint", strict=True, schema=schema)
|
||||
|
||||
@staticmethod
|
||||
def design(tags):
|
||||
w = sw.SaveWriter()
|
||||
w.begin("Des")
|
||||
w.bool(tags[0], True); w.bool(tags[1], False); w.int(tags[2], 1); w.string(tags[3], "Honor Lance")
|
||||
w.begin("DSec"); w.int("ga", 1); w.end() # sections stay generic
|
||||
w.end()
|
||||
return w.bytes()
|
||||
|
||||
def test_design_header_tags(self):
|
||||
schema = sr.Seq([sr.A("Des", sr.Design)])
|
||||
data = self.design(("FAIDes", "DHide", "DWep", "DName"))
|
||||
wk, root = walk(data)
|
||||
self.assertTrue(all(c.hinted for c in root.children[0].children[:4])) # typed from the Des shape
|
||||
res = sr.read_bytes(data, padding="joint", strict=True, schema=schema)
|
||||
d = res.typed["Des"]
|
||||
self.assertEqual((d["faiDes"], d["dHide"], d["dWep"], d["dName"]), (True, False, 1, "Honor Lance"))
|
||||
self.assertEqual([s["_name"] for s in d["sections"]], ["DSec"])
|
||||
self.assertNotIn("FAIDes", d) # key stays R1's; only the tag changed
|
||||
with self.assertRaises(sr.SaveFormatError): # camel-case R1 spelling is not on disk
|
||||
sr.read_bytes(self.design(("faiDes", "dHide", "dWep", "dName")),
|
||||
padding="joint", strict=True, schema=schema)
|
||||
|
||||
@staticmethod
|
||||
def node_grid(tag):
|
||||
w = sw.SaveWriter()
|
||||
w.begin("NdGr2")
|
||||
w.begin("paths"); w.int(".", 1)
|
||||
w.begin(".")
|
||||
for i, n in enumerate(("npt", "npid", "npfr", "npto", "npctm", "npcby",
|
||||
"npdtn", "npdtf", "npenp", "npuse", "nptf")):
|
||||
w.int(n, i)
|
||||
w.end()
|
||||
w.end()
|
||||
w.int(tag, 44)
|
||||
w.end()
|
||||
return w.bytes()
|
||||
|
||||
def test_node_grid_nextid(self):
|
||||
schema = sr.Seq([sr.A("NdGr2", sr.NodeGrid)])
|
||||
res = sr.read_bytes(self.node_grid("nextid"), padding="joint", strict=True, schema=schema)
|
||||
g = res.typed["NdGr2"]
|
||||
self.assertEqual(g["nextid"], 44)
|
||||
self.assertNotIn("nextId", g)
|
||||
self.assertEqual((g["paths"][0]["npt"], g["paths"][0]["nptf"]), (0, 10))
|
||||
with self.assertRaises(sr.SaveFormatError):
|
||||
sr.read_bytes(self.node_grid("nextId"), padding="joint", strict=True, schema=schema)
|
||||
|
||||
def test_build_queue_ords_and_flight_plan_wpts(self):
|
||||
w = sw.SaveWriter()
|
||||
w.begin("BQ"); w.begin("ords"); w.int(".", 1)
|
||||
w.begin("."); w.int("desID", 5); w.int("con", 1); w.int("conleft", 2); w.int("sav", 3); w.int("ordID", 7); w.end()
|
||||
w.end(); w.end()
|
||||
w.begin("FPlan"); w.begin("wpts"); w.int(".", 1)
|
||||
w.begin("."); w.int("Wpt", 272); w.int("Tp", 1)
|
||||
w.begin("nrt"); w.int("nrp", -1); w.int("nrf", 0); w.int("nrt", 0); w.end()
|
||||
w.end(); w.end()
|
||||
w.float("FPsp2", 2.5); w.int("FPeta2", 3)
|
||||
w.vec3("FPogn2", 1.0, 2.0, 3.0, named=True); w.vec3("FPdpos", 4.0, 5.0, 6.0, named=True)
|
||||
w.int("pnd", 0)
|
||||
w.end()
|
||||
schema = sr.Seq([sr.A("BQ", sr.BuildQueue), sr.A("FPlan", sr.FlightPlan)])
|
||||
res = sr.read_bytes(w.bytes(), padding="joint", strict=True, schema=schema)
|
||||
self.assertEqual(norm(res.typed["BQ"]["ords"]),
|
||||
[{"desID": 5, "con": 1, "conleft": 2, "sav": 3, "ordID": 7}])
|
||||
fp = res.typed["FPlan"]
|
||||
self.assertEqual((fp["wpts"][0]["Wpt"], fp["wpts"][0]["Tp"], fp["wpts"][0]["nrt"]["nrp"]), (272, 1, -1))
|
||||
self.assertEqual((fp["FPsp2"], fp["FPeta2"], fp["FPogn2"], fp["pnd"]), (2.5, 3, [1.0, 2.0, 3.0], 0))
|
||||
# an empty queue is a framed count of 0 (what turn1..3 carry)
|
||||
w = sw.SaveWriter()
|
||||
w.begin("BQ"); w.begin("ords"); w.int(".", 0); w.end(); w.end()
|
||||
res = sr.read_bytes(w.bytes(), padding="joint", strict=True, schema=sr.Seq([sr.A("BQ", sr.BuildQueue)]))
|
||||
self.assertEqual(res.typed["BQ"]["ords"], [])
|
||||
# the inline (unframed) hedge the old flex R() allowed is not what the game writes
|
||||
w = sw.SaveWriter()
|
||||
w.begin("BQ"); w.int("ords", 0); w.end()
|
||||
with self.assertRaises(sr.SaveFormatError):
|
||||
sr.read_bytes(w.bytes(), padding="joint", strict=True, schema=sr.Seq([sr.A("BQ", sr.BuildQueue)]))
|
||||
|
||||
@unittest.skipUnless(os.path.exists(os.path.join(REAL, "turn3-state.sav")), "real saves not present")
|
||||
def test_real_saves_carry_the_corrected_tags(self):
|
||||
res = sr.read_save(os.path.join(self.REAL, "turn2-state.sav"), padding="joint", strict=True)
|
||||
players = [p["Player"] for p in res.typed["sim"]["players"]]
|
||||
first = players[0]["designs"][0]["Des"]
|
||||
self.assertEqual((first["dName"], first["faiDes"], first["dHide"], first["dWep"]), ("Armor", False, False, 0))
|
||||
self.assertEqual((players[0]["otch"][0]["otnF"], players[0]["otch"][0]["otch"]), (1, "WEP_RedLas"))
|
||||
self.assertTrue(any(o["odid"] == 18 and o["opid"] == 32 for p in players for o in p["odes"]))
|
||||
self.assertEqual(res.typed["sim"]["NdGr2"]["nextid"], 44)
|
||||
self.assertEqual(len(res.typed["sim"]["NdGr2"]["paths"]), 43)
|
||||
# every remaining positional match is a NULL-named "." item
|
||||
self.assertFalse([i for i in res.issues if "read positionally" in i.msg and "tag '.'" not in i.msg])
|
||||
# turn3 is the only save with an active flight plan
|
||||
res3 = sr.read_save(os.path.join(self.REAL, "turn3-state.sav"), padding="joint", strict=True)
|
||||
plans = [f["Flt"]["FPlan"] for f in res3.typed["sim"]["fleets"] if f["Flt"]["HFPlan"]]
|
||||
self.assertEqual(len(plans), 1)
|
||||
self.assertEqual((plans[0]["wpts"][0]["Wpt"], plans[0]["wpts"][0]["nrt"]["nrp"]), (272, -1))
|
||||
|
||||
|
||||
class CliTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue