diff --git a/findings/objects/ship-design-catalogue.md b/findings/objects/ship-design-catalogue.md new file mode 100644 index 0000000..1abfd38 --- /dev/null +++ b/findings/objects/ship-design-catalogue.md @@ -0,0 +1,326 @@ +# The ship-design catalogue: how designs persist, hull size, and the `0x400` flag + +Lane D2, 2026-09-08. Static reading against the exe, checked against all 11 saves. + +Blocker #4 on lane Y's path to a byte-matching turn. Three lanes had brushed against the first +question and each left it open; this closes it, and the answer corrects a published finding. + +--- + +## 1. How ship designs persist — the serializer three lanes could not find + +**`Game::ShipDesign::Write` exists, is `0x008325e0`, and makes four stream calls.** The published +finding — "`Game::ShipDesign::Write` (`0x008747a0`) makes no stream call at all" — is **wrong in its +address**. `0x008747a0` is not this class's writer and is not a serializer of any kind: lane V2's +exact vtable inversion puts it in **no vftable at all** (`vtable_map.py who 0x008747a0` → +`is in no vftable`), and its body pops a parser stack. The serializer sweep attributed it to +`ShipDesign` by a slot mapping that did not hold for this class. + +The class is not the shape the earlier note assumed either. RTTI `0x00a894a8`: + +``` +Game::ShipDesign : Game::ShipDesignDef, Mars::IStreamable, Mars::RefCounted, Mars::NetworkObject +``` + +`ShipDesignDef` is the **base**, and it is *not* an `IStreamable` — it declares its own three +virtuals (`Read`, `Write`, dtor, vftable `0x009fef64`) in a different order from `IStreamable`'s. +`ShipDesign` overrides all three; the `IStreamable` sub-object at `+0x9c` (vftable `0x00a32710`) +holds three adjustor thunks back to the primary vftable `0x00a32720`. That thunk layer is why a +slot-indexed sweep mis-keyed the class, and it is a **general warning for the serializer tool**: a +class that inherits an interface *second* reaches its own writer through a thunk, and the thunk is +what a slot walk finds. + +So a design persists as **two serializers, a base and a derived one**, exactly the `*Impl` shape +lane G generalised — except the concrete class is a *derived class*, not a separate `Impl` class. +Lane V2's caution was right: check the shape, do not assume it. + +### The full `Des` record, both writers, in disk order + +| # | tag | primitive | member | writer | +|---|---|---|---|---| +| 1 | `FAIDes` | **bool** | `+0x4` | `ShipDesignDef::Write` `0x00827390` | +| 2 | `DHide` | **bool** | `+0x5` | " | +| 3 | `DWep` | **bool** | `+0x6` | " | +| 4 | `DName` | string | `+0x8` (`Mars::string`, `0x1c`) | " | +| 5 | `DSec` | frame | `+0x4c` — **command** | " (via `StreamableHelper`) | +| 6 | `DSec` | frame | `+0x24` — **mission** | " | +| 7 | `DSec` | frame | `+0x74` — **engine** | " | +| 8 | `Dtc` | int | `+0x134` | `ShipDesign::Write` `0x008325e0` | +| 9 | `Dwgv` | **bool** | `+0x16c` | " | +| 10 | `Dwg` | frame, **only when `Dwgv`** | `+0x170`, `StreamableHelper` | " | + +`ShipDesign::Read` `0x008324f0` mirrors it, with one extra: it reads an int `DRefCnt` into a **null +destination** — read and discarded — that the writer never emits. A network stream writes it; the +save stream does not, and the tag-addressed reader tolerates the absence. + +### Three corrections this forces + +**(a) Three sections, not five.** `SHIP_DESIGN_RULES.md` §1 said "five section slots, three used; +slots 3-4 are absent in every design (reserved)". There are **three**, and there is no reserved +slot. Two independent enumerations (rule 5): + +* the writer emits exactly three `DSec` frames — a straight-line body, no loop; +* the constructor `0x00874c70` runs `eh_vector_constructor_iterator(this+0x24, stride 0x28, + count 3)`, and `0x24 + 3*0x28 = 0x9c`, which is exactly where `ShipDesign`'s own `IStreamable` + vptr sits. `sizeof(Game::ShipDesignDef) == 0x9c`. + +The "two extra slots" were **`Dtc` and `Dwgv`**, swept into the section list by the reference Python +reader's trailing `Rest("sections")` and then decoded by `stock_designs.py` as two empty sections. +This is rule 8 in its exact form: the reader and its consumer agreed with each other and were both +wrong, and nothing downstream could see it because an empty slot is a no-op for every rule. Fixed +openly (§6). + +**(b) `DWep` and `Dwgv` are bools, not ints.** Both writers call the bool primitive. With their +four-character tags a bool item and an int item are the same size on the wire and the values 0 and 1 +are the same bytes, so **no save can distinguish them** — the same class of defect as lane G's +`ObservedTech.odet`, and byte-neutral for the same reason. Corrected in both readers; the engine's +typed round trip stays byte-identical on all 11 saves. + +**(c) Offset order ≠ write order, again.** The section array is `[+0x24, +0x4c, +0x74]`; the wire is +`[+0x4c, +0x24, +0x74]`. Reading the array in memory order gives **mission, command, engine**; +reading the wire gives **command, mission, engine**. Both orders matter below — the wire order is +what a save reader needs, the memory order is what decides hull size. + +--- + +## 2. Hull size and the `0x400` flag + +Both live on `ShipDesign`, **neither is on the wire**, and both are recomputed from the data files +by `ShipDesign::UpdateDerivedStats` `0x0087e7c0` whenever a design changes or is loaded. + +### `0x400` = `defence_platform` + +`ShipSectionDef` carries a **64-bit role-flag word** at `+0x298`/`+0x29c`. The `.shipsection` parser +`0x005744e0` sets it one bit at a time: each boolean role key is a call to +`SetRoleFlagBit(&flags, loMask, hiMask, value)` `0x0056e780`. At `0x005749b7`, immediately after the +`strcmp` against the literal `defence_platform` at `0x009fea10`: + +``` +push ecx ; the parsed value +push 0x0 ; hi mask +lea edx,[esi+0x298] +push 0x400 ; lo mask +push edx +call 0x0056e780 +``` + +**`0x400` is `defence_platform`.** The rest of the word, from the same scan: `refueling_capacity` +`0x2`, `repair_capacity` `0x4`, `refinery` `0x8`, `mining_capacity` `0x10`, `scanrange` / +`rebelai_scanrange` `0x20`, `gateship` `0x40`, `ewar` `0x800`, `ramscoop` `0x1000`, `aicontrol` +`0x2000`, `command_quota` `0x10000`, `node_bore` `0x20000`, `prisoner_capacity` `0x40000`, +`freighter` `0x80000`, `gravboat_bonus` `0x200000`, `construction_capacity` `0x400000`, `science` +`0x1000000`, `tradingpost` `0x2000000`, `freighterQ` `0x8000000`, `police` `0x10000000`, `spy` +`0x40000000`, `spytender` `0x80000000`; high dword `colony_trap` `0x1`, `mining_trap` `0x2`, +`monitor` `0x4`, `propaganda` `0x10`. + +The design's word is the **OR** across its resolved sections +(`AggregateSectionStats` `0x00826af0`, `out[4] |= GetRoleFlags(section)`), so one flagged section +flags the whole design. In the shipped data the 49 sections carrying `defence_platform` are exactly +the `{DE,CR,DN}DefencePlatform`, `{CR,DN}TorpedoPlatform`, `{CR,DN}DronePlatform`, +`CRDeepScanPlatform` families and `_VNeumannSatellite` — all standalone mission sections — so no +shipped design mixes a flagged section with unflagged ones. That the rule is an OR is therefore +**inferred from the instruction and untestable on the corpus** (rule 6); it is unit-tested in the +engine on a synthetic design instead. + +**This is not lane B5's `0x400`.** A *fleet* gets flag `0x400` when the retreat pipeline creates it, +and that flag reaches the wire as `FtFlg` — `turn1-state.sav`'s first fleet has `FtFlg 1024`. It is +a different word on a different object. The numeral is a coincidence of two bit layouts, and the +brief was right to say "check rather than assume". + +### Hull size = `section_class` + +`ShipSectionDef+0x260` is the parsed `section_class`, and the parse is a three-name table: + +``` +ParseShipClassName 0x0056e1c0: _stricmp against "Destroyer", "Cruiser", "Dreadnought" + stores the index it stopped at -> 0, 1, 2 +``` + +Case-insensitive, which the data needs — the shipped catalogs write both `cruiser` and `Cruiser`. +The wrapper `0x0056e250` logs `" [%s] unrecognized ship class"` and stores **0** on no match, and +a section with no `section_class` key at all never reaches the parser and keeps the constructed 0. +So **absent or unrecognised is a destroyer, not an error**. 16 shipped sections have no +`section_class` (the `_Biomissile` / `_BoardingPod` families and three `_NPC` stems). + +The design's hull size is `AggregateSectionStats`'s `out[0x1d] = sectionDef->+0x260`, copied to +`ShipDesign+0x12c`. That is an **assignment inside the per-section loop, not a max and not an OR**, +so the *last resolved section in memory slot order* wins — i.e. engine, else command, else mission. +Rule A6 makes every shipped design class-homogeneous, so first-wins and last-wins agree on **all 503 +design records in the 11 saves** (measured, §4); the assignment order is implemented anyway because +it is what the original does, and it is unit-tested on a hand-built mixed-class design. + +### One corroboration the census does not need + +The same `0x400` bit picks the design's **default hull health** when no section overrides it at +`+0x2ac` (`0x00826af0`, `out[2]`): + +| hull size | without `0x400` | with `0x400` | +|---|---|---| +| 0 destroyer | 500 | 100 | +| 1 cruiser | 3000 | 500 | +| 2 dreadnought | 15000 | 1000 | + +A defence platform is a fifth to a fifteenth of the ship's hull. That is an independent reading of +the same two words and it agrees: `0x400` separates *platforms* from *ships*, and `+0x12c` is the +three-step hull-size ladder. + +--- + +## 3. The census — what the two words feed + +`ShipCensusByHullClass` `0x00818a50` takes a player and an `int[8]`: + +``` +for each fleet in server.fleets where fleet->owner(+0x58) == player: + for each ship in fleet->ships(+0xa4..+0xa8): + design = ship->+0x14 + if (design->+0xb8 & 0x400) == 0: ++out[0]; ++out[2 + design->+0x12c] + else: ++out[1]; ++out[5 + design->+0x12c] +``` + +`out[0]`/`out[1]` are the two grand totals, which the caller computes and **discards**. The six +breakdowns land at `turnRecord+0x2a..+0x34` and reach the wire as the three `cls` groups' **`shpt`** +(ships) and **`satt`** (satellites — the game's own word for a defence platform). + +Note what the loop does *not* do: it never looks at the ship's own `PlrID`, only at the fleet's +owner, and it never looks at the ship's health or state. Every ship in an owned fleet counts. + +--- + +## 4. Measurement: 480 census leaves, 0 mismatched + +Rebuilt from the save's own state plus the section catalog, compared against the record the game +archived for that save's own frame (the record is rebuilt at end of turn *and* on load, so it never +has to survive a round trip). + +| saves | designs classified | leaves compared | **mismatched** | +|---|---|---|---| +| 11 | 503 | **480** | **0** | + +Computed twice, independently: once in Python off `save_reader.py` + `design_rules.py`, once in C++ +off the engine's own `shapes` reader and `game::data::Catalog`. The two disagree about how many +sections a design has (the Python reader's five vs the engine's three, §1a) and still produce the +same 480 numbers — so they are not the same check wearing two hats. + +**Coverage, reported as loudly as the result (rule 15).** Only **32 of the 480 leaves are nonzero in +the archive**. A zero leaf agrees for free. + +| leaf | nonzero of 80 | +|---|---| +| cls 0 `shpt` — destroyer ships | **18** | +| cls 0 `satt` — destroyer platforms | **3** | +| cls 1 `shpt` — cruiser ships | 0 | +| cls 1 `satt` — cruiser platforms | 0 | +| cls 2 `shpt` — dreadnought ships | **11** | +| cls 2 `satt` — dreadnought platforms | 0 | + +**Three of the six census leaves are unexercised, not verified.** No save in the corpus contains a +cruiser of any kind or a dreadnought-class platform, so the cruiser column and the dreadnought +platform cell are hypotheses (rule 6). They are cheap to exercise — a save taken after +`IND_CruisCon` is researched and one cruiser is built would close the whole cruiser column, and +`zuul-turn23-fleet23.sav`'s 27-destroyer fleet shows the destroyer path is genuinely loaded. + +Other honest residuals from the same run: + +* 0 ships whose design id is missing from the save's design tables; +* 0 designs none of whose sections resolve against the catalog; +* 0 designs where first-resolved and last-resolved section disagree on hull size (rule A6 predicts + 0, and this is the measurement that says so rather than the assumption). + +--- + +## 5. What this closes, and what it does not + +Against lane Y's baseline — turn1→turn2 **209→204**, turn2→turn3 **108→103**, 5 closed / 0 +regressed, with the 204 broken down `/Sim/players` 82, `/Sim/systems` 80, `/Sim/turnstats` 24, +`/Sim/SvSctOb` 8, plus 10 singletons: + +**closed: 0. regressed: 0.** Reported separately, and both are zero *in the standalone's divergence +list*, because this lane wrote no phase. + +That is the honest number and it needs the reason spelled out. The census leaves live under +`/Sim/turnstats`, and the turn record is built by `src/app/turn_record.cpp`, which **lane A2 holds** +this cycle (the alliance mask `almem` is a field of the same record). Committing a census field into +that file now is exactly the cross-lane collision the fragment discipline exists to prevent. So this +lane did what lane S2's convention prescribes for a phase whose formula is held but whose write-out +is not this lane's: **evaluate and report, do not write**. + +What is now unblocked, quantified: + +* **6 of the 24 `/Sim/turnstats` leaves per pair** — `shpt` ×3 and `satt` ×3 — have a formula that + reproduces the archived value on 480 of 480 observations. Wiring them is one call to + `game::design::ShipCensus` inside `BuildTurnRecord`, and the earlier `--commit-blocked` run's + "three leaves of the ship census" among its 17 regressions were exactly these, wrong because the + record committed zeros. They should now commit clean. +* **The ship records** (`Player.ShipRecs`) and **tail phase T24** both need the same two words per + design; they are now available as `DesignStats::hull_size` / `DesignStats::defence_platform` and + as the standalone `classify_design()`. +* The turn record's other four unmodelled fields are **not** touched: `almem` is lane A2's, + `tdinc` waits on per-system money output, `bat` is a tail stub, and `sacq`/`slost` have count 0 in + every save (rule 6). + +Nothing here was compared against the live game. `verified` in the phase catalog means "compared +against the running game", and this is not that — it is a comparison against bytes the original +wrote, which is the strongest instrument a lane holding no VM has. + +--- + +## 6. Fixes to the oracles, made openly (rule 12) + +Two readers carried the same wrong record shape; both are fixed, with a test per defect and +byte-neutrality shown. + +**`verify/save-reader/save_reader.py`** — the `Des` shape ended in `Rest("sections")`. Now: three +`DSec` frames as a `Repeat`, then named `Dtc` (int) and `Dwgv` (bool), then the conditional `Dwg`; +`DWep` retyped int → bool. All 11 saves still read strict-clean; the reader's own suite is **49/49** +(was 48; three existing tests asserted the old shape and were corrected, one test added that pins +"exactly three `DSec`" against the real saves). + +**`verify/design-rules/stock_designs.json`** regenerated from the same three saves. The diff is +*only* `raw_slots` 5 → 3 and `dWep` `0/1` → `false/true`; every other field of all 127 designs is +byte-for-byte the same, and `test_design_rules.py` is **32/32** with the same ground truth +(127/127 structural, 197/197 `DOpts`, 121/127 gating with the six Tarkas hidden-rider warns). + +**`sots-engine` `src/mars/stream/shapes.h`** — `Design::dWep` and `Design::dwgv` retyped to bool, +the conditional `Dwg` node added, and the comment that said the derived writer made no stream call +replaced with the recovered layout. The typed round trip is still **byte-identical on all 11 saves** +at 100% named coverage. + +The engine's `stock_designs.cpp` fixture loader now accepts 3–5 `raw_slots` so an old fixture still +loads. `kSlotCount` is deliberately **left at 5**: the extra two array entries are inert and +touching the enum reaches `rules.cpp` and another lane's tests for no behavioural gain. The comment +in `design.h` is corrected instead — the record is three sections, and the fifth-slot slack is this +engine's own, not the game's. + +--- + +## 7. Open, and what would close it + +1. **The cruiser column and the dreadnought-platform cell** are unexercised (§4). One save with a + cruiser in it and one with a DN defence platform close three leaves. Cheapest of everything on + this page. +2. **`Dtc`** (`+0x134`) is on the wire and is stamped by at least one caller immediately before the + recompute; its meaning is not read here. It is 1 on the stock designs. +3. **`Dwgv`/`Dwg`** — weapon groups — are false in every save. The conditional branch is implemented + from the instruction and is unexercised. +4. **The other 24 role-flag bits** are decoded (§2) but only `0x400` is consumed by anything this + lane read. `0x80000` (`freighter`) and `0x8000000` (`freighterQ`) drive a *second* classifier + `0x0081a430`/`0x0082c7c0` over the same two words — worth reading if a freighter count turns up + missing elsewhere. +5. **The remaining ~24 fields of the aggregator's output struct** (`+0x18`..`+0x64` → + `ShipDesign+0xc0`..`+0x118`) are the rest of the derived-stat block — mass, cost, capacities, + speeds. `SHIP_DESIGN_RULES.md` §6 derives most of them from the data with the formulas marked as + assumptions; `0x00826af0` is where every one of those assumptions can be settled, field by field, + and it is now a named function. + +--- + +## Addresses + +`ghidra/addresses.d/lane-d2.json` (19 entries, no collision with any existing fragment). +Key ones: `ShipDesignDef::Write` `0x00827390`, `ShipDesign::Write` `0x008325e0`, +`ShipDesign::UpdateDerivedStats` `0x0087e7c0`, `AggregateSectionStats` `0x00826af0`, +`ShipSectionDef::GetRoleFlags` `0x0056ee80`, `ParseShipClassName` `0x0056e1c0`, +`SetRoleFlagBit` `0x0056e780`, `ShipCensusByHullClass` `0x00818a50`; +offsets `ShipDesign+0xb8` (role flags), `+0x12c` (hull size), `ShipSectionDef+0x260` +(`section_class`), `+0x298` (role flags). diff --git a/ghidra/addresses.d/lane-d2.json b/ghidra/addresses.d/lane-d2.json new file mode 100644 index 0000000..6250a14 --- /dev/null +++ b/ghidra/addresses.d/lane-d2.json @@ -0,0 +1,156 @@ +{ + "entries": [ + { + "name": "Game_ShipDesignDef_Write", + "addr": "0x00827390", + "convention": "thiscall", + "prototype": "void (Game_ShipDesignDef* this, Mars::Stream* s) // THE DESIGN SERIALIZER LANE D SAID DID NOT EXIST. Slot 1 of the ShipDesignDef vftable 0x009fef64. Writes, in DISK order: WriteBool 'FAIDes' this+0x4, WriteBool 'DHide' this+0x5, WriteBool 'DWep' this+0x6 (a BOOL, not an int -- the campaign schema had it as int; byte-neutral because a 4-char tag makes both items 12 bytes), WriteString 'DName' this+0x8, then THREE 'DSec' frames through StreamableHelper at this+0x4c, this+0x24, this+0x74 in that order. THREE sections, not five: the ctor 0x00874c70 runs eh_vector_constructor_iterator(this+0x24, stride 0x28, count 3). MEMORY ORDER != WRITE ORDER: the array is [+0x24, +0x4c, +0x74] and the wire is [+0x4c (command), +0x24 (mission), +0x74 (engine)]", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesignDef_Read", + "addr": "0x00827240", + "convention": "thiscall", + "prototype": "void (Game_ShipDesignDef* this, Mars::Stream* s) // slot 0 of vftable 0x009fef64. Mirrors Write field for field, same tags, same three DSec frames in the same order", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_Write", + "addr": "0x008325e0", + "convention": "thiscall", + "prototype": "void (Game_ShipDesign* this, Mars::Stream* s) // CORRECTS 'Game::ShipDesign::Write (0x008747a0) makes no stream call at all': 0x008747a0 is in NO vftable and is not this class's writer. Game::ShipDesign derives from Game::ShipDesignDef (RTTI 0x00a894a8: ShipDesign, ShipDesignDef, IStreamable, RefCounted, NetworkObject; IStreamable sub-object at +0x9c, NetworkObject at +0xa0). Primary vftable 0x00a32720 slot 1; the +0x9c IStreamable vftable 0x00a32710 reaches it through an adjustor thunk at 0x00874de0. Body: direct call to ShipDesignDef::Write 0x00827390 (the base part), then WriteInt 'Dtc' this+0x134, WriteBool 'Dwgv' this+0x16c, and ONLY IF that flag is set a 'Dwg' frame through StreamableHelper at this+0x170. Dwgv is false in all 11 saves, so the Dwg branch is unexercised (rule 6)", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_Read", + "addr": "0x008324f0", + "convention": "thiscall", + "prototype": "void (Game_ShipDesign* this, Mars::Stream* s) // primary vftable 0x00a32720 slot 0. Calls ShipDesignDef::Read, then ReadInt 'DRefCnt' into a NULL destination (read and discarded; the WRITER never emits it, so it is a network-stream field the tag-addressed reader tolerates), then 'Dtc', 'Dwgv' and the conditional 'Dwg'", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_ctor", + "addr": "0x00874c70", + "convention": "thiscall", + "prototype": "Game_ShipDesign* (Game_ShipDesign* this) // installs ShipDesignDef::vftable at +0, then IStreamable/NetworkObject at +0x9c/+0xa0 and the three ShipDesign vftables. Constructs the DName string at +0x8 and eh_vector_constructor_iterator(this+0x24, 0x28, 3) -- the three section records. Enumerates sizeof(Game_ShipDesignDef) == 0x9c", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_UpdateDerivedStats", + "addr": "0x0087e7c0", + "convention": "fastcall", + "prototype": "void (Game_ShipDesign* this) // THE RECOMPUTE THAT PRODUCES BOTH CENSUS WORDS. 16 call sites; one of them stamps this+0x134 (Dtc) and calls straight in. (1) resolves each of the three section records this+0x24/+0x4c/+0x74 to a ShipSectionDef* through the catalog lookup 0x0056edd0 and caches them at this+0xac/+0xb0/+0xb4, IN MEMORY SLOT ORDER; (2) builds a 3-element context array (stride 0x124) and hands it to the aggregator 0x00826af0 together with this+0x130; (3) copies the aggregator's ~0x7c-byte output struct into the design: struct+0x10 -> this+0xb8 (role flags, low dword), struct+0x14 -> this+0xbc (high dword), struct+0x18..+0x64 -> this+0xc0..+0x118, struct+0x74 -> this+0x12c (HULL SIZE), struct+0x78 -> this+0x1a4, struct+0x00..+0x0c -> this+0x138..+0x144. Neither this+0xb8 nor this+0x12c is on the wire: they are rebuilt from the data files whenever a design changes or is loaded", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_AggregateSectionStats", + "addr": "0x00826af0", + "convention": "cdecl", + "prototype": "void (DesignStatBlock* out, SectionContext ctx[3], void* techCtx, char flag) // walks the three section contexts (stride 0x124, ctx[i]+0 = the ShipSectionDef*, null = empty slot). out[4]/out[5] (the 64-bit role-flag word) |= ShipSectionDef::GetRoleFlags 0x0056ee80 per section -- an OR, so ONE flagged section flags the whole design. out[0x1d] (hull size) = sectionDef+0x260 -- an ASSIGNMENT, so the LAST resolved section in memory slot order wins; every shipped design is section_class-homogeneous so first-wins and last-wins agree on all 503 design records in the corpus. Also: out[6] x5 when any section carries tech 0x2756; out[4] &= ~0x80 for tech 0x2757; out[4] |= 0x100000 when EVERY section carries tech 0x2742. Default hull health (out[2]) is chosen by the SAME 0x400 bit when no section overrides it at +0x2ac: without 0x400 hull 0/1/2 -> 500/3000/15000, with 0x400 -> 100/500/1000", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipSectionDef_GetRoleFlags", + "addr": "0x0056ee80", + "convention": "thiscall", + "prototype": "unsigned __int64 (Game_ShipSectionDef* this, void* ctx) // returns CONCAT(this+0x29c, this+0x298) -- the section's 64-bit role-flag word straight out of the parsed .shipsection -- with bit 0x20 of the low dword OR-ed in when ctx is non-null, ctx+0xfc is set and this+0x304 > 0. That conditional bit is the only part of the word that is not pure file data", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipSectionCatalog_FindByID", + "addr": "0x0056edd0", + "convention": "thiscall", + "prototype": "Game_ShipSectionDef* (SectionCatalog* this, Game_ShipSectionID* id) // linear scan of the vector at this+0x8/+0xc matching def+0x4 == id->species and def+0x8 == id->sectionId. Returns null for the (0,0) empty slot without scanning", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ParseShipClassName", + "addr": "0x0056e1c0", + "convention": "cdecl", + "prototype": "bool (int* out, const char* name) // THE HULL-SIZE DEFINITION. _stricmp against \"Destroyer\", \"Cruiser\", \"Dreadnought\" in that order and stores the index it stopped at: 0, 1, 2. Returns false without writing on no match", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipSectionDef_ParseSectionClass", + "addr": "0x0056e250", + "convention": "cdecl", + "prototype": "bool (int* out, const char* name) // wraps 0x0056e1c0 for the `section_class` key; on failure logs \" [%s] unrecognized ship class\" and stores 0, so an unknown OR ABSENT section_class is a destroyer, not an error", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_SetRoleFlagBit", + "addr": "0x0056e780", + "convention": "cdecl", + "prototype": "void (unsigned __int64* flags, unsigned int loMask, unsigned int hiMask, bool value) // the .shipsection parser's flag setter. Every boolean role key in the section parser 0x005744e0 is one call to this with its own mask pair; `defence_platform` is (lo 0x400, hi 0) at 0x005749b7, `monitor` is (lo 0, hi 0x4), `refinery` 0x8, `mining_capacity` 0x10, `scanrange`/`rebelai_scanrange` 0x20, `gateship` 0x40, `ewar` 0x800, `ramscoop` 0x1000, `aicontrol` 0x2000, `command_quota` 0x10000, `node_bore` 0x20000, `prisoner_capacity` 0x40000, `freighter` 0x80000, `gravboat_bonus` 0x200000, `construction_capacity` 0x400000, `science` 0x1000000, `tradingpost` 0x2000000, `freighterQ` 0x8000000, `police` 0x10000000, `spy` 0x40000000, `spytender` 0x80000000, `refueling_capacity` 0x2, `repair_capacity` 0x4; high dword: `colony_trap` 0x1, `mining_trap` 0x2, `monitor` 0x4, `propaganda` 0x10", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ServerPlayer_ShipCensusByHullClass", + "addr": "0x00818a50", + "convention": "cdecl", + "prototype": "void (ServerPlayer* p, int out[8]) // THE CENSUS. Zeroes out[0..7], then walks the server's fleet vector (p+0x8 -> S, S+0x60/+0x64), keeps fleets whose owner (fleet+0x58) is p, and for every ship in fleet+0xa4/+0xa8 takes design = ship+0x14. If (design+0xb8 & 0x400) == 0: ++out[0] and ++out[2 + design->hullSize(+0x12c)]; else ++out[1] and ++out[5 + hullSize]. So out[0]/out[1] are the two grand totals (computed and DISCARDED by the caller), out[2..4] are ships by hull size 0/1/2 and out[5..7] are defence platforms by hull size. The six land at turnRecord+0x2a..+0x34 and reach the wire as the three `cls` groups' `shpt` and `satt`", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "Game_ShipDesign_IsMobileWarshipClass", + "addr": "0x0081a430", + "convention": "cdecl", + "prototype": "int (Game_ShipDesign* design) // a SECOND classifier over the same two words, kept because it shows the flag word is a role set and not a single bit: returns -1 when the design lacks flag 0x80000 (`freighter`), else for hull size 1 returns 0 when 0x8000000 (`freighterQ`) is set and 1 otherwise, and 2 for any other hull size. 0x0082c7c0 is the matching counter over a fleet list. NOT the census -- neither reads 0x400", + "status": "unverified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "ShipDesign_off_RoleFlagsLow", + "offset": "0xb8", + "convention": "offset", + "prototype": "unsigned int // low dword of the design's 64-bit role-flag word, the OR of its sections'. Bit 0x400 = `defence_platform`. High dword at +0xbc. NOT on the wire; rebuilt by 0x0087e7c0", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "ShipDesign_off_HullSize", + "offset": "0x12c", + "convention": "offset", + "prototype": "int // 0 destroyer / 1 cruiser / 2 dreadnought, from the last resolved section's `section_class`. NOT on the wire; rebuilt by 0x0087e7c0", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "ShipDesign_off_Dtc", + "offset": "0x134", + "convention": "offset", + "prototype": "int // the wire field `Dtc`, written by ShipDesign::Write and stamped by at least one caller immediately before it calls the recompute 0x0087e7c0", + "status": "unverified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "ShipSectionDef_off_SectionClass", + "offset": "0x260", + "convention": "offset", + "prototype": "int // parsed `section_class`: 0 destroyer / 1 cruiser / 2 dreadnought, 0 when absent or unrecognised", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + }, + { + "name": "ShipSectionDef_off_RoleFlagsLow", + "offset": "0x298", + "convention": "offset", + "prototype": "unsigned int // low dword of the section's 64-bit role-flag word (high dword at +0x29c), one bit per boolean role key in the .shipsection file", + "status": "verified", + "source": "findings/objects/ship-design-catalogue.md" + } + ] +} diff --git a/verify/design-rules/SHIP_DESIGN_RULES.md b/verify/design-rules/SHIP_DESIGN_RULES.md index 9e0fdbb..3598e1f 100644 --- a/verify/design-rules/SHIP_DESIGN_RULES.md +++ b/verify/design-rules/SHIP_DESIGN_RULES.md @@ -29,15 +29,31 @@ From the save (`Player.designs[]` / `Player.legacyDesigns[]`, fully decoded in `stock_designs.py`): ``` -Des { faiDes bool, dHide bool, dWep int, dName string, - sections[5] : DSec { DSec { int species_idx, int section_id }, +Des { faiDes bool, dHide bool, dWep bool, dName string, + sections[3] : DSec { DSec { int species_idx, int section_id }, DGbnk2 { int n_banks, n x ( DW2 { bool bID, wid int | wfn string, int did } ) }, - DOpts { int n, n x tech-name string } } } + DOpts { int n, n x tech-name string } }, + dtc int, dwgv bool, [ Dwg {...} only when dwgv ] } ``` -* **Five section slots, three used**: slot 0 = command, 1 = mission, 2 = engine; - slots 3-4 are absent in every design (reserved). An empty slot is - `(species 0, section 0)`; ids in `_shipsections.txt` start at 1. +> **CORRECTION (lane D2, 2026-09-08).** This section used to read `sections[5]` and +> "five section slots, three used; slots 3-4 are absent in every design +> (reserved)". **There are three sections and no reserved slots.** The two +> extra entries were `Dtc` and `Dwgv` — the derived writer's own tail — swept +> into the section list by `save_reader.py`'s trailing `Rest("sections")` and +> then decoded by `stock_designs.py` as two empty sections. Both writers are +> now recovered: `Game::ShipDesignDef::Write 0x00827390` emits the four scalars +> and exactly three `DSec` frames, `Game::ShipDesign::Write 0x008325e0` appends +> `Dtc`, `Dwgv` and the conditional `Dwg`. The constructor confirms the count +> independently (`eh_vector_constructor_iterator(this+0x24, stride 0x28, +> count 3)`, and `0x24 + 3*0x28 = 0x9c = sizeof(ShipDesignDef)`). `dWep` is a +> **bool** on the wire, not an int — byte-indistinguishable, writer-decidable. +> See `findings/objects/ship-design-catalogue.md`. + +* **Three section slots**: disk order 0 = command, 1 = mission, 2 = engine. An + empty slot is `(species 0, section 0)`; ids in `_shipsections.txt` start at 1. + (In memory the array is ordered mission, command, engine — offset order is not + write order here, and the memory order is what decides hull size.) * `species_idx` indexes the save's species table `Human, Hiver, Tarkas, Liir, _NPC, Zuul, Morrigi` (`SPECIES_INDEX`); `section_id` is the race's `_shipsections.txt` id. Every design in the saves uses one species for all @@ -260,6 +276,30 @@ Zuul sections) -- last value taken; engine behaviour unknown. 4x gauss + `mis`), hidden `Default Assault Shuttle`. The `Independent Colony` NPC has a Tarkas `Defence Platform` with `las_red` x4 + `mis`. +## 7A. Hull size and the class flag (lane D2, code-settled) + +Two derived words the design caches, **neither on the wire**, both rebuilt from +this catalog by `ShipDesign::UpdateDerivedStats 0x0087e7c0` whenever a design +changes or is loaded. They are what the turn record's per-hull-class ship +census counts by. Full derivation in +`findings/objects/ship-design-catalogue.md`. + +| word | rule | +|---|---| +| **hull size** (`ShipDesign+0x12c`) | the `section_class` of the **last resolved section in memory slot order** (engine, else command, else mission), mapped `Destroyer 0 / Cruiser 1 / Dreadnought 2` case-insensitively. Absent or unrecognised -> **0**, with a log line, not an error — settles the 16 shipped sections with no `section_class`. Assignment, not max: A6 makes first-wins and last-wins agree on all 503 design records in the 11 saves | +| **role flags** (`ShipDesign+0xb8`/`+0xbc`, 64-bit) | the **OR** of every resolved section's own flag word. One bit per boolean role key; `defence_platform` is `0x400` of the low dword. The OR is read from the instruction and is untestable on shipped data — no design mixes a flagged with an unflagged section | + +The same `0x400` bit also picks the design's default hull health when no section +overrides it: without it `500 / 3000 / 15000` by hull size, with it +`100 / 500 / 1000`. So the flag separates *platforms* from *ships*, which is +exactly how the census uses it (`shpt` vs `satt`). + +This also answers §8 question 1 in part: **A6 is not enforced by this path.** +The aggregator assigns the class from whichever section it visits last and never +compares the three, so a mixed-class design would be silently classified by its +engine section rather than rejected here. Whether the *designer UI* rejects it +is still open. + ## 8. Open questions for the Ghidra side 1. **Class-equality (A6)** -- enforced in `ShipDesignDef` validation, or only diff --git a/verify/design-rules/stock_designs.json b/verify/design-rules/stock_designs.json index 23e93a0..442d985 100644 --- a/verify/design-rules/stock_designs.json +++ b/verify/design-rules/stock_designs.json @@ -93,7 +93,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -137,20 +137,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -212,7 +198,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -247,20 +233,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -322,7 +294,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -357,20 +329,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -434,7 +392,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -474,20 +432,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -538,7 +482,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -567,20 +511,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -631,7 +561,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -647,20 +577,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -767,7 +683,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -811,20 +727,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -887,7 +789,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -924,20 +826,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -1000,7 +888,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -1037,20 +925,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -1117,7 +991,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -1163,20 +1037,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -1227,7 +1087,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -1256,20 +1116,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -1322,7 +1168,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -1340,20 +1186,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -1479,7 +1311,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -1495,20 +1327,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -1636,7 +1454,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -1654,20 +1472,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -2297,7 +2101,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -2315,20 +2119,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -2658,7 +2448,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -2684,20 +2474,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -3031,7 +2807,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -3062,20 +2838,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -3411,7 +3173,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -3446,20 +3208,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -3794,7 +3542,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -3827,20 +3575,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -4176,7 +3910,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -4211,20 +3945,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -4557,7 +4277,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -4586,20 +4306,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -4952,7 +4658,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -5025,20 +4731,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -5394,7 +5086,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -5470,20 +5162,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -5842,7 +5520,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -5888,20 +5566,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -6261,7 +5925,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -6324,20 +5988,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -6671,7 +6321,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -6717,20 +6367,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -7070,7 +6706,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -7116,20 +6752,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -7469,7 +7091,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -7515,20 +7137,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -7868,7 +7476,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -7914,20 +7522,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -8267,7 +7861,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -8313,20 +7907,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -8656,7 +8236,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -8682,20 +8262,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -9030,7 +8596,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -9064,20 +8630,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -9718,7 +9270,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -9752,20 +9304,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -10428,7 +9966,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -10505,20 +10043,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -10874,7 +10398,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -10915,20 +10439,6 @@ "IND_RefCoat" ], "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -11259,7 +10769,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -11296,20 +10806,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -11643,7 +11139,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -11682,20 +11178,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -12019,7 +11501,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -12037,20 +11519,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -12385,7 +11853,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -12419,20 +11887,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -13051,7 +12505,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -13085,20 +12539,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -13415,7 +12855,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -13444,20 +12884,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -13564,7 +12990,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -13608,20 +13034,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -13683,7 +13095,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -13718,20 +13130,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -13793,7 +13191,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -13828,20 +13226,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -13905,7 +13289,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -13945,20 +13329,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -14009,7 +13379,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -14038,20 +13408,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -14102,7 +13458,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -14118,20 +13474,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -14238,7 +13580,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -14282,20 +13624,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -14358,7 +13686,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -14395,20 +13723,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -14471,7 +13785,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -14508,20 +13822,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -14588,7 +13888,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -14634,20 +13934,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -14698,7 +13984,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -14727,20 +14013,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -14813,7 +14085,7 @@ "list": "designs", "faiDes": true, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -14859,20 +14131,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -14919,7 +14177,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -14937,20 +14195,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -15076,7 +14320,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -15092,20 +14336,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -15233,7 +14463,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -15251,20 +14481,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -15894,7 +15110,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -15912,20 +15128,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -16255,7 +15457,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -16281,20 +15483,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -16628,7 +15816,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -16659,20 +15847,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -17008,7 +16182,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -17043,20 +16217,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -17391,7 +16551,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -17424,20 +16584,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -17773,7 +16919,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -17808,20 +16954,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -18154,7 +17286,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -18183,20 +17315,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -18549,7 +17667,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -18622,20 +17740,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -18991,7 +18095,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -19067,20 +18171,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -19439,7 +18529,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -19485,20 +18575,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -19858,7 +18934,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -19921,20 +18997,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -20268,7 +19330,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -20314,20 +19376,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -20667,7 +19715,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -20713,20 +19761,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -21066,7 +20100,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -21112,20 +20146,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -21465,7 +20485,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -21511,20 +20531,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -21864,7 +20870,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -21910,20 +20916,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -22253,7 +21245,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -22279,20 +21271,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -22627,7 +21605,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -22661,20 +21639,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -23015,7 +21979,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -23060,20 +22024,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -23714,7 +22664,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -23748,20 +22698,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -24424,7 +23360,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -24501,20 +23437,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -24870,7 +23792,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -24911,20 +23833,6 @@ "IND_RefCoat" ], "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -25255,7 +24163,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -25292,20 +24200,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -25639,7 +24533,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -25678,20 +24572,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -26015,7 +24895,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -26033,20 +24913,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -26381,7 +25247,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -26415,20 +25281,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -27047,7 +25899,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -27081,20 +25933,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -27411,7 +26249,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -27440,20 +26278,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -27560,7 +26384,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -27604,20 +26428,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -27679,7 +26489,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -27714,20 +26524,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -27789,7 +26585,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -27824,20 +26620,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -27901,7 +26683,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -27941,20 +26723,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Human" @@ -28005,7 +26773,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -28034,20 +26802,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -28098,7 +26852,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -28114,20 +26868,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -28234,7 +26974,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -28278,20 +27018,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -28354,7 +27080,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -28391,20 +27117,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -28467,7 +27179,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -28504,20 +27216,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -28584,7 +27282,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -28630,20 +27328,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -28694,7 +27378,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -28723,20 +27407,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -28809,7 +27479,7 @@ "list": "designs", "faiDes": true, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 2, @@ -28855,20 +27525,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "Tarkas" @@ -28915,7 +27571,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -28933,20 +27589,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -29072,7 +27714,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -29088,20 +27730,6 @@ "options": [], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -29229,7 +27857,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": true, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -29247,20 +27875,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -29890,7 +28504,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -29908,20 +28522,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -30251,7 +28851,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -30277,20 +28877,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -30624,7 +29210,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -30655,20 +29241,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -31004,7 +29576,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -31039,20 +29611,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -31387,7 +29945,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -31420,20 +29978,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -31769,7 +30313,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -31804,20 +30348,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -32150,7 +30680,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -32179,20 +30709,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -32545,7 +31061,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -32618,20 +31134,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -32987,7 +31489,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -33063,20 +31565,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -33435,7 +31923,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -33481,20 +31969,6 @@ "rider_design_ids": [ 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -33854,7 +32328,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -33917,20 +32391,6 @@ 0, 0 ] - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -34264,7 +32724,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -34310,20 +32770,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -34663,7 +33109,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -34709,20 +33155,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -35062,7 +33494,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -35108,20 +33540,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -35461,7 +33879,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -35507,20 +33925,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -35860,7 +34264,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -35906,20 +34310,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -36249,7 +34639,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -36275,20 +34665,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -36623,7 +34999,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -36657,20 +35033,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -37011,7 +35373,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -37056,20 +35418,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -37710,7 +36058,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -37744,20 +36092,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -38420,7 +36754,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -38497,20 +36831,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -38866,7 +37186,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 4, @@ -38907,20 +37227,6 @@ "IND_RefCoat" ], "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 } ], "player_race": "_NPC" @@ -39251,7 +37557,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -39288,20 +37594,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -39635,7 +37927,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -39674,20 +37966,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -40011,7 +38289,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -40029,20 +38307,6 @@ ], "bank_count": 0 }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -40377,7 +38641,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -40411,20 +38675,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -41043,7 +39293,7 @@ "list": "designs", "faiDes": false, "dHide": false, - "dWep": 1, + "dWep": true, "raw_slots": [ { "species": 0, @@ -41077,20 +39327,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, @@ -41407,7 +39643,7 @@ "list": "legacyDesigns", "faiDes": false, "dHide": false, - "dWep": 0, + "dWep": false, "raw_slots": [ { "species": 0, @@ -41436,20 +39672,6 @@ 0 ] }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, - { - "species": 0, - "section_id": 0, - "weapons": [], - "options": [], - "bank_count": 0 - }, { "species": 0, "section_id": 0, diff --git a/verify/save-reader/SAVE_FORMAT.md b/verify/save-reader/SAVE_FORMAT.md index 7971dad..0eed709 100644 --- a/verify/save-reader/SAVE_FORMAT.md +++ b/verify/save-reader/SAVE_FORMAT.md @@ -161,7 +161,20 @@ IsFxAv FxAv Tag Pwd(string) Team Settings{4×"." int}`. 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). +`DWep`(**bool**) `DName`(string), then **exactly three** `DSec` section frames — command, mission, +engine in that disk order, kept generic — then `Dtc`(int) `Dwgv`(**bool**) and, only when `Dwgv` is +set, a `Dwg` weapon-group frame (`Dwgv` is false in every save available). + +> **Confirmed against the binary (2026-09-08, lane D2).** A design is written by +> **two** serializers: `Game::ShipDesignDef::Write` `0x00827390` (the four scalars and the three +> `DSec` frames, in wire order `+0x4c +0x24 +0x74`) and its subclass `Game::ShipDesign::Write` +> `0x008325e0` (`Dtc`, `Dwgv`, conditional `Dwg`). This **corrects two published claims**: +> `Game::ShipDesign::Write` is not `0x008747a0` and does not "make no stream call" — that address is +> in no vftable at all; and the record has **three** sections, not five. The reader's old +> `Rest("sections")` swept `Dtc` and `Dwgv` into the section list, which is where the "slots 3-4 are +> reserved and always empty" claim came from. `DWep` and `Dwgv` are `WriteBool`; with four-character +> tags a bool item and an int item are the same size and 0/1 the same bytes, so the change is +> byte-neutral and no save could have shown it. See `findings/objects/ship-design-catalogue.md`. 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`. diff --git a/verify/save-reader/save_reader.py b/verify/save-reader/save_reader.py index 7236ce6..a6259dc 100644 --- a/verify/save-reader/save_reader.py +++ b/verify/save-reader/save_reader.py @@ -520,9 +520,28 @@ CivilianRatios = Shape("civr", [A("smx", "float"), A("spe", SpeciesRatios), 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. +# +# TWO writers, a base and a derived one (lane D2): the base emits the four +# scalars then EXACTLY THREE `DSec` frames -- command, mission, engine, in that +# order -- and the derived one appends `Dtc`, the `Dwgv` flag and, only when +# that flag is set, the `Dwg` weapon-group frame. +# +# `DWep` and `Dwgv` are BOOLs, not ints: both writers call the bool primitive. +# With four-character tags a bool item and an int item are the same size on the +# wire and the values 0/1 are the same bytes, so no save can tell them apart -- +# this is the same class of defect as `ObservedTech.odet`, and it is byte-neutral. +# +# CORRECTION (lane D2): this shape used to end in `Rest("sections")`, which swept +# `Dtc` and `Dwgv` into the section list. That is where the campaign's "five +# section slots, of which slots 3 and 4 are reserved and always empty" claim came +# from -- there are three sections and no reserved slots. Design = Shape("Des", [ A("FAIDes", "bool", key="faiDes"), A("DHide", "bool", key="dHide"), - A("DWep", "int", key="dWep"), A("DName", "string", key="dName"), Rest("sections"), + A("DWep", "bool", key="dWep"), A("DName", "string", key="dName"), + Repeat(A("DSec", "any"), key="sections"), + A("Dtc", "int", key="dtc"), A("Dwgv", "bool", key="dwgv"), + If("dwgv", A("Dwg", "any", key="weaponGroups")), # never set in any save available + Rest(), ]) ConMods = Shape("ConMods", [ A("ConMod", "float", key="ConMod0"), A("SavMod", "float", key="SavMod0"), diff --git a/verify/save-reader/test_save_reader.py b/verify/save-reader/test_save_reader.py index b1b91e9..e40d7e3 100644 --- a/verify/save-reader/test_save_reader.py +++ b/verify/save-reader/test_save_reader.py @@ -426,11 +426,16 @@ class TagNameCorrectionsTest(unittest.TestCase): names = self.field_names(shape) self.assertEqual(names[0], ("otnF", True)) # not R1's "ontF" self.assertTrue(all(auth for _, auth in names), names) + # Lane D2: the derived writer's Dtc/Dwgv are named fields, not part of the + # section run; the run itself is a Repeat and holds exactly the three DSec frames. self.assertEqual(self.field_names(sr.Design), - [("FAIDes", True), ("DHide", True), ("DWep", True), ("DName", True)]) + [("FAIDes", True), ("DHide", True), ("DWep", True), ("DName", True), + ("Dtc", True), ("Dwgv", 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"]) + ["faiDes", "dHide", "dWep", "dName", "dtc", "dwgv"]) + rep = [f for f in sr.Design.fields if isinstance(f, sr.Repeat)] + self.assertEqual([(r.lead, r.key) for r in rep], [("DSec", "sections")]) 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)) @@ -444,7 +449,8 @@ class TagNameCorrectionsTest(unittest.TestCase): 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")) + ("bool", "bool", "bool", "string")) # DWep is WriteBool, not WriteInt + self.assertEqual(sr.GLOBAL_KINDS["Dwgv"], "bool") # likewise for name in ("ords", "wpts", "paths"): self.assertIsInstance(sr.GLOBAL_SHAPES[name], sr.CArr) @@ -479,8 +485,9 @@ class TagNameCorrectionsTest(unittest.TestCase): 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.bool(tags[0], True); w.bool(tags[1], False); w.bool(tags[2], True); w.string(tags[3], "Honor Lance") w.begin("DSec"); w.int("ga", 1); w.end() # sections stay generic + w.int("Dtc", 1); w.bool("Dwgv", False) # the derived writer's tail w.end() return w.bytes() @@ -491,8 +498,10 @@ class TagNameCorrectionsTest(unittest.TestCase): 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((d["faiDes"], d["dHide"], d["dWep"], d["dName"]), (True, False, True, "Honor Lance")) + # the section run stops at Dtc: the tail is NOT two more "reserved slots" self.assertEqual([s["_name"] for s in d["sections"]], ["DSec"]) + self.assertEqual((d["dtc"], d["dwgv"]), (1, False)) 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")), @@ -779,6 +788,33 @@ class WireSchemaDefectsTest(unittest.TestCase): for d in pl["shipRecs"]["designRecords"]: self.assertEqual(set(d) - {"_off"}, {"srd", "src", "srb", "srl", "sri"}) + # --- lane D2: the Des record has THREE sections, and DWep/Dwgv are bools ---- + @unittest.skipUnless(os.path.exists(os.path.join(REAL, "turn3-state.sav")), "real saves not present") + def test_design_records_hold_exactly_three_sections(self): + """The campaign's "five slots, 3 and 4 reserved" was this reader's own + `Rest()` sweeping Dtc and Dwgv into the section list. Both design + writers are recovered: the base emits three DSec frames and nothing + else, the derived one appends Dtc and the Dwgv flag.""" + seen = 0 + for name in ("turn1-state.sav", "turn2-state.sav", "turn3-state.sav"): + path = os.path.join(self.REAL, name) + if not os.path.exists(path): + continue + res = sr.read_save(path, padding="joint", strict=True) + for pe in res.typed["sim"]["players"]: + pl = pe["Player"] + for key in ("designs", "legacyDesigns"): + for d in pl.get(key, []): + des = d["Des"] + self.assertEqual([x["_name"] for x in des["sections"]], + ["DSec", "DSec", "DSec"], des["dName"]) + self.assertIsInstance(des["dWep"], bool) + self.assertIsInstance(des["dwgv"], bool) + self.assertFalse(des["dwgv"]) # never set in any save available + self.assertNotIn("weaponGroups", des) + seen += 1 + self.assertGreater(seen, 100) + class CliTest(unittest.TestCase): def setUp(self):