goldbox.dos_layout¶
Declarative field table for the DOS Pool of Radiance character record.
goldbox/layout.py is this module’s model and its sibling: same Field, same
Confidence, same rule that every byte of the record belongs to exactly one
entry so an overlap cannot be introduced silently. What differs is the
record. The DOS one is 285 bytes to the C64’s 580, and it is rearranged
rather than translated. Both directions read this table now: goldbox/dos.py
decodes a DOS record through it and, since #26, encodes one too – the
player’s own files are still never written to.
Where the offsets come from¶
Every entry below was measured against 24 real specimens – 18 characters
in three played save slots and 6 exported .CHA files – from Donald’s Steam
copy of Forgotten Realms: The Archives. The write-up that was the working,
work/reports/dos-saves.md, is lost; tests/test_dossave.py and
tests/test_dosconvert.py are the assertions, and
docs/117-save-conversion.md is the plan the table serves.
Nothing here is transcribed from a hex-editing guide: the community notes in
work/coab-research/formats/ predicted nine of these fields and were right
about all nine, which is corroboration and is said as much in the notes.
Three shapes worth knowing before reading the table¶
The name is length-prefixed, not NUL-padded: one count byte then up to 15 ASCII. The C64 spends 20 NUL-padded bytes on the same field, which is the whole of the four-byte displacement between the two layouts’ early fields.
The spellbook is one byte per spell, 56 of them, where the C64 packs 56 bits. The ordering turns out to be identical – see
SPELLBOOK.The per-class level array is indexed by the class number, where the C64’s eight slots are indexed by the class bit. Same width, different meaning per slot;
goldbox/dos.pycarries the permutation.
Live-only state¶
The DOS engine saves its heap with the record. Several u16le values in
0x0C1-0x0FF and 0x104-0x10B are far pointers that move by 16 bytes
between two saves of the same party, and the item list is a chain through
0x02A of each item record. They are marked LIVE in the note and a
converter must ignore them; the C64 has fixed slots and no heap.
Module Attributes
The Pool of Radiance DOS character record. |
|
One count byte plus fifteen bytes of ASCII. |
|
One item, in three of the four titles. |
|
One active effect in a |
|
The DOS spellbook is one byte per spell for spell ids 1..56, and 56 is |
|
Gold Box Companion's class table, and the C64's own class codes -- the two agree entry for entry, which is why |
|
Race codes for Pool of Radiance and Curse only, shared with the C64 -- except that the C64 table is 1-based on the same names with |
|
the table is reordered and half-orc is gone, which is the switch to AD&D 2nd Edition, where the half-orc stopped being a player race. |
|
Silver Blades' table with |
|
Every one of the 285 bytes belongs to exactly one entry. |
|
Every one of the 63 item bytes, likewise. |
|
the table above, unchanged. |
|
every ability becomes a (base, current) pair, the memorised-spell region grows from 21 bytes to 84, and the spellbook grows from 56 entries to 100 -- which is |
|
Secret of the Silver Blades, 439 bytes. |
|
Pools of Darkness, 510 bytes, and the one with no C64 counterpart at all. |
|
285, 422, 439, 510. |
|
|
Every title's table, by key. |
Functions
|
Bytes of the 285 at each confidence level. |
|
|
|
The layout as a fixed-width table. |
|
|
Every record field in offset order. |
|
|
The field table for one title, built from Pool of Radiance's. |
|
The shape for a record size, a key, or a shape. |
Classes
One title's DOS character record, as a difference from Pool of Radiance's. |
Exceptions
A record size or title key that names no DOS Gold Box record. |
- goldbox.dos_layout.CLASS_NUMBERS = ('cleric', 'druid', 'fighter', 'paladin', 'ranger', 'mage', 'thief', 'monk', 'cleric/fighter', 'cleric/fighter/mage', 'cleric/ranger', 'cleric/mage', 'cleric/thief', 'fighter/mage', 'fighter/thief', 'fighter/mage/thief', 'mage/thief', 'monster')¶
Gold Box Companion’s class table, and the C64’s own class codes – the two agree entry for entry, which is why
char_classconverts by copying. Checked on all 24 specimens against the class bitmask at0x0B0.
- goldbox.dos_layout.CURSE_OF_THE_AZURE_BONDS = DosShape(key='curse-of-the-azure-bonds', title='Curse of the Azure Bonds', record_size=422, item_suffix='.SWG', effect_suffix='.FX', item_size=63, spellbook_spells=100, race_numbers=('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 84, 'spellbook': 100, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 5, 'spells_castable_magic_user': 5}, inserts={'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=8, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False),), 'spells_castable_cleric': (Field(offset=0, size=5, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False),), 'icon_colours': 1, 'heap_104': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False), 3)})¶
every ability becomes a (base, current) pair, the memorised-spell region grows from 21 bytes to 84, and the spellbook grows from 56 entries to 100 – which is
goldbox/spells.py’s Curse id space, 1..100, exactly. Then the record gains the fields a second title needs: a multi-class level beside the level, a former-level array beside the class-level one, a druid spell-slot array between the cleric’s and the magic-user’s, and four bytes of its own before the combat tail.Its items are in a `.SWG` file, not a `.ITM` one – measured in the running game rather than assumed (#113). Every shipped Curse pregen carries nothing, so no
.ITMwas ever missing from a directory anybody had; the first character to buy a battle axe gotCHRDATI1.SWGbesideCHRDATI1.SAV, 63 bytes, and no.ITMappeared at any point. This said.ITMuntil then, which madegoldbox.dos.read_characterhand back an item count of three and an empty item list – silently, because a sibling that is not there reads as empty. It corroborates#55: Gateway to the Savage Frontier’s 422-byte.GUYexports read through this table and keep their items in.SWGtoo.- Type:
Curse of the Azure Bonds, 422 bytes. Three things move it
- class goldbox.dos_layout.DosShape[source]¶
Bases:
objectOne title’s DOS character record, as a difference from Pool of Radiance’s.
- __init__(key, title, record_size, item_suffix='.ITM', effect_suffix='.SPC', item_size=63, spellbook_spells=56, race_numbers=('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human'), sizes=<factory>, inserts=<factory>)¶
- inserts: Mapping[str, int | Sequence[goldbox.layout.Field | int]]¶
A count of undecoded bytes, or a sequence that may mix
Field`s with counts -- `(paladin_cures, 3)is one named byte and three nobody has attributed.
- item_size: int = 63¶
Bytes an item record takes in that file. 63 in three of the four titles and 67 in Silver Blades, whose four extra bytes are at the end and zero in 12 of 12 (#113).
- race_numbers: Sequence[str] = ('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human')¶
What the
racebyte means, indexed by it. Per title, and not a shift: Pool of Radiance and Curse shareRACE_NUMBERS, Silver Blades reorders it and loses the half-orc, and Pools of Darkness drops another entry from the front (#237). Look a race up through this, never through the module-level default.
- exception goldbox.dos_layout.DosShapeError[source]¶
Bases:
ValueErrorA record size or title key that names no DOS Gold Box record.
- goldbox.dos_layout.EFFECT_SIZE = 9¶
One active effect in a
.SPCfile. Also constant across the family.
- goldbox.dos_layout.ITEM_LAYOUT: tuple[goldbox.layout.Field, ...] = (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='text_length', label='Rendered line length', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the longest observed is 40, '* Magic User Scroll With 3 Spells'", candidate=False), Field(offset=1, size=41, kind=<Kind.RAW: 'raw'>, name='text', label='Rendered inventory line (CACHE)', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the line the game last drew for this item -- readied marker, '*' for magic, the name. **A cache and never a source.** It goes stale: one specimen reads '11 Darts' over a quantity byte of 8, which is one of the two characters whose encumbrance identity misses", candidate=False), Field(offset=42, size=4, kind=<Kind.RAW: 'raw'>, name='next', label='Next item (LIVE)', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="far pointer to the next item in the character's chain, NULL on the last. The C64 keeps sixteen fixed slots; drop the chain, keep the order", candidate=False), Field(offset=46, size=1, kind=<Kind.U8: 'u8'>, name='type_index', label='Item type', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="indexes `ITEMS`, the 128 x 16 type table -- **and the DOS `ITEMS` file is byte-identical to the C64's in 126 of its 128 records**, the two that differ being dagger and dart differing in range with the class-usage flags equal. So the class restrictions, which live in byte +13 of the type record and not in the item, need no conversion at all. C64 +0", candidate=False), Field(offset=47, size=1, kind=<Kind.U8: 'u8'>, name='name1', label='Name word 1', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the three name words are **the C64's own ITEMNAMES indices**, not text: 48 is MAIL, 162 is +1, 208 is CLERICAL SCROLL, on both ports. C64 +1, +2, +3", candidate=False), Field(offset=48, size=1, kind=<Kind.U8: 'u8'>, name='name2', label='Name word 2', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=49, size=1, kind=<Kind.U8: 'u8'>, name='name3', label='Name word 3', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=50, size=1, kind=<Kind.I8: 'i8'>, name='plus', label='Plus', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="signed: +1, +2, +3 against the printed names, and -5 in both this byte and the next on a cursed necklace, which is the C64's +4/+5 pair exactly. C64 +4", candidate=False), Field(offset=51, size=1, kind=<Kind.I8: 'i8'>, name='plus_save', label='Save bonus', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='signed; accumulates into the saving-throw roll. C64 +5', candidate=False), Field(offset=52, size=1, kind=<Kind.U8: 'u8'>, name='readied', label='Readied', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='0 or 1. The C64 packs it as bit 7 of its +6, sharing that byte with the hidden-name mask -- one of the two places where the DOS record spends a byte on a C64 bit', candidate=False), Field(offset=53, size=1, kind=<Kind.U8: 'u8'>, name='hidden', label='Hidden-name mask', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='bit 0 hides name word 3, bit 1 word 2, bit 2 word 1. C64 +6 bits 0-2', candidate=False), Field(offset=54, size=1, kind=<Kind.U8: 'u8'>, name='cursed', label='Cursed', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="0 or 1; the C64's +7 bit 7", candidate=False), Field(offset=55, size=2, kind=<Kind.U16LE: 'u16le'>, name='weight', label='Weight', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='u16le, tenths of a pound. Confirmed by the encumbrance identity, which is arithmetic entirely inside the DOS files. C64 +8, +9', candidate=False), Field(offset=57, size=1, kind=<Kind.U8: 'u8'>, name='quantity', label='Quantity', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='C64 +10', candidate=False), Field(offset=58, size=2, kind=<Kind.U16LE: 'u16le'>, name='value', label='Cost in gold', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='u16le. C64 +11, +12', candidate=False), Field(offset=60, size=1, kind=<Kind.U8: 'u8'>, name='charges', label='Charges', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='the use-item routine spends the quantity byte while it is above one and then decrements this one, destroying the item at zero. Three WAND OF MAGIC MISSILES templates differ in this byte alone -- 20, 33, 35. C64 +13', candidate=False), Field(offset=61, size=1, kind=<Kind.U8: 'u8'>, name='effect', label='Effect', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='C64 +14', candidate=False), Field(offset=62, size=1, kind=<Kind.U8: 'u8'>, name='power', label='Power', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='C64 +15', candidate=False))¶
Every one of the 63 item bytes, likewise.
- goldbox.dos_layout.ITEM_SIZE = 63¶
One item, in three of the four titles. Not constant across the family, which this comment claimed until #113 measured a played Silver Blades game: Pool of Radiance, Curse and Pools of Darkness are 63 and Silver Blades is 67 – 804 bytes for twelve items, which 63 does not divide. Read the stride from
DosShape.item_sizerather than from here; this is the default that shape overrides, and slicing a.STFat 63 is the bug #113 closed.
- goldbox.dos_layout.LAYOUT: tuple[goldbox.layout.Field, ...] = (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='name_length', label='Name length', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='1-15 in all 24 specimens; the longest observed is 14 (ORATISI NOMOON, THRENDER GRONE) and 0x00F is zero in every one, so the field is a count byte and fifteen of text rather than sixteen of text', candidate=False), Field(offset=1, size=15, kind=<Kind.RAW: 'raw'>, name='name_text', label='Name', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='plain ASCII, no PETSCII anywhere in the DOS record. The bytes past the count are zero in all 24. Converting to the C64 is a re-padding, not a transliteration: 20 NUL-padded bytes there against 1+15 here, and that four-byte difference is the whole displacement between the two layouts up to exceptional strength', candidate=False), Field(offset=16, size=1, kind=<Kind.U8: 'u8'>, name='strength', label='STR', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the six abilities run 0x010-0x015 in the C64's own order -- STR INT WIS DEX CON CHA -- and every specimen is 3-18. **Pool of Radiance stores each ability once.** From Curse onwards the DOS record stores a (base, current) pair per ability and everything after shifts by 0x46, so this offset is per title and does not transfer", candidate=False), Field(offset=17, size=1, kind=<Kind.U8: 'u8'>, name='intelligence', label='INT', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=18, size=1, kind=<Kind.U8: 'u8'>, name='wisdom', label='WIS', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=19, size=1, kind=<Kind.U8: 'u8'>, name='dexterity', label='DEX', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=20, size=1, kind=<Kind.U8: 'u8'>, name='constitution', label='CON', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=21, size=1, kind=<Kind.U8: 'u8'>, name='charisma', label='CHA', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=22, size=1, kind=<Kind.U8: 'u8'>, name='exceptional_strength', label='STR %', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="nonzero only where strength is 18. Values seen: 17, 65, 68, 90, 100 -- the Gold Box '18/00 = 100' encoding the C64 uses at 0x01A", candidate=False), Field(offset=23, size=5, kind=<Kind.RAW: 'raw'>, name='gap_017', label='unallocated @0x017', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=28, size=16, kind=<Kind.RAW: 'raw'>, name='spells_memorised', label='Spells memorised', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="spell ids in the shared 1-56 numbering, **filled from the end of the region backwards**: ROLAND (cleric 3) holds 0x027-0x02B = 3 3 3 3 23, which read back to front is 23 3 3 3 3 -- descending, and exactly what the C64 writes forward from its own 0x020. GILES and ASTRID (mage 3) hold 21 21 34 in the last three, again magic-user ids only. No specimen has a cleric id in a magic-user's list or the reverse. **The Curse importer never reads this region** (0x17-0x2C is one of its seven skipped runs), so nobody else's code corroborates the width; sixteen is what the C64 declares and what tiles here. **The same end-alignment is measured in the two later titles** (#113), by memorising one spell at a time in DOSBox and diffing the whole record: Curse's 84 bytes take a level-2 spell at 0x071, then a level-1 at 0x070, and adding a level-3 shifts the pair down so the three read 15 34 47 ending at 0x071 -- ids ascending towards the last byte, a repeated memorisation stored as a repeated id, and casting one clears the *lower* address and leaves the rest against the end. Silver Blades' 75 bytes take a ranger's one druid spell at 0x068, its own last byte. Five saves in Curse, one in Silver Blades, and eight archive records in Pool of Radiance", candidate=False), Field(offset=44, size=1, kind=<Kind.RAW: 'raw'>, name='gap_02c', label='unallocated @0x02c', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=45, size=1, kind=<Kind.U8: 'u8'>, name='thac0_base', label='THAC0 base (60 - value)', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="40, 42 and 43 only, giving THAC0 20, 18 and 17. Gold Box Companion's Levels.txt states thac0_base = 40 for cleric 1 and 42 for cleric 4 -- the same biased number, in the same encoding, as the C64's 0x071", candidate=False), Field(offset=46, size=1, kind=<Kind.U8: 'u8'>, name='race', label='Race', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="0 monster, 1 dwarf, 2 elf, 3 gnome, 4 half-elf, 5 halfling, 6 half-orc, 7 human -- the C64's table exactly. Every race/class pair in the 24 is legal AD&D: both cleric/fighter/mages are half-elves and the fighter/mage/thief is an elf. **That numbering is Pool of Radiance's and Curse's, and no further** (#237): Silver Blades reorders the table and loses the half-orc, and Pools of Darkness drops an entry from the front of that. Look the byte up through `DosShape.race_numbers`, which is per title, rather than through `RACE_NUMBERS`", candidate=False), Field(offset=47, size=1, kind=<Kind.U8: 'u8'>, name='char_class', label='Class', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the 18-entry combined-class table, `CLASS_NUMBERS`. **It is the C64's own table**: 0 cleric, 2 fighter, 5 mage, 6 thief, 8 cleric/fighter, 9 cleric/fighter/mage, 11 cleric/mage, 12 cleric/thief, 13 fighter/mage, 14 fighter/thief, 15 fighter/mage/thief, 16 mage/thief, which is `goldbox/yaml_io.py`'s CLASS_CODES entry for entry. Checked against the class bitmask at 0x0B0 on all 24 -- BAKSHI 9 against bits 11, RHIANNON 13 against 9, ORATISI NOMOON 15 against 13. So the class byte converts by copying, multi-class included", candidate=False), Field(offset=48, size=2, kind=<Kind.U16LE: 'u16le'>, name='age', label='Age', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='16-52 for humans, half-elves and dwarves and 180 for both elves. 0x031 is zero in all 24, so the second byte is inferred from the C64 having two and from 180 fitting in one -- an elf mage over 255 would settle it. Read big-endian both elves come out 46080, which is one of the three readings that fixed the byte order', candidate=False), Field(offset=50, size=1, kind=<Kind.U8: 'u8'>, name='hp_max', label='Hit points maximum', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="4-70 across the specimens, and **one byte, not two**: 0x033 starts the spellbook, so there is no room for a high byte. The C64's field at 0x076 is genuinely wider", candidate=False), Field(offset=51, size=56, kind=<Kind.RAW: 'raw'>, name='spellbook', label='Spellbook', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="one byte per spell, 0 or 1, for spell ids 1..56 in order -- **the same ids the C64 bitmask at 0x078 indexes**, so the conversion is a pack, not a permutation. Three things say so together. The DOS array is grouped cleric-1 (8), mage-1 (13), cleric-2 (7), mage-2 (7), cleric-3 (9), mage-3 (11), which is `goldbox/spells.py`'s `_GROUPS_POOL` boundaries 1-8, 9-21, 22-28, 29-35, 36-44, 45-55 byte for byte. Every set byte in all 24 specimens falls in a group its owner's class can cast, with no crossover: a level-1 cleric sets exactly bytes 0-7, a level-3 cleric adds bytes 21-27, a level-3 magic-user sets 8-20 and 28-34. And the memorised list at 0x01C, which is written as *ids* rather than as a mask, carries the same numbers -- 3 CURE LIGHT WOUNDS for the cleric, 21 SLEEP for the mages. **Byte 55 is spell id 56, RESTORATION**, and the C64's seven-byte mask has no bit for it: 56 bits hold ids 0-55 and id 0 does not exist. It is zero in all 24; a converter reports it rather than dropping it silently", candidate=False), Field(offset=107, size=1, kind=<Kind.U8: 'u8'>, name='attack_level', label='Fighting level', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the fighter class level where there is one and 1 otherwise -- 3 for a fighter 3, 1 for a cleric 3. The C64 keeps the same value at 0x098, and Curse's importer recomputes it as `SkillLevel(Fighter)` or 1, which is this rule in the engine's own hand", candidate=False), Field(offset=108, size=1, kind=<Kind.U8: 'u8'>, name='icon_dimension', label='Icon dimension', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="1 in all 24. The C64's 0x099 aligns here but does **not** mean this: it carries the icon *size* (small/large), which DOS keeps separately at 0x0C0. Two DOS fields, one C64 byte, and the C64's is the second of the two, one lower -- see `goldbox/layout.py`'s `size_small`", candidate=False), Field(offset=109, size=1, kind=<Kind.U8: 'u8'>, name='save_paralysis', label='Save vs paralysis', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the five saving throws in the C64's order, 0x06D-0x071. ROLAND, a cleric 3, reads 10 13 14 16 15, which is Gold Box Companion's save_1..save_5 for cleric 1-3 exactly", candidate=False), Field(offset=110, size=1, kind=<Kind.U8: 'u8'>, name='save_petrification', label='Save vs petrification', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=111, size=1, kind=<Kind.U8: 'u8'>, name='save_wands', label='Save vs wands', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=112, size=1, kind=<Kind.U8: 'u8'>, name='save_breath', label='Save vs breath', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=113, size=1, kind=<Kind.U8: 'u8'>, name='save_spell', label='Save vs spell', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=114, size=1, kind=<Kind.U8: 'u8'>, name='movement', label='Base movement', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='12 in all 24 -- unencumbered human speed. The encumbered value lives in the combat tail at 0x11C, where SILAS in plate mail reads 6', candidate=False), Field(offset=115, size=1, kind=<Kind.U8: 'u8'>, name='level', label='Level', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the highest class level: 3 for a cleric 3, 4 for the fighter 4 / thief 1. Curse's importer calls the same byte hit dice and sets its own `multiclassLevel` from it, which is why the DOS Curse record has one byte here that Pool of Radiance does not", candidate=False), Field(offset=116, size=1, kind=<Kind.U8: 'u8'>, name='levels_drained', label='Levels lost', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="zero in all 24; named from Curse's importer, which copies it to the field it calls levels lost, and from the C64's 0x0A1 aligning", candidate=False), Field(offset=117, size=1, kind=<Kind.U8: 'u8'>, name='hp_lost_to_drain', label='Hit points lost', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='zero in all 24; same two sources as 0x074', candidate=False), Field(offset=118, size=1, kind=<Kind.U8: 'u8'>, name='turn_class', label='Turn-undead row', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="**the row of the turning matrix the creature answers to, not the power of a cleric turning it** -- the C64's `turn_class` at 0x0A3, and DOS has no counterpart to the C64's `turn_power` at 0x0A4 at all. This was named `turn_power` at PROBABLE until #288, on the grounds that a party with no undead in it and no cleric above level 3 could not tell the C64's two turning bytes apart. The code and the monster files can. CONFIRMED, three ways:\n* `GAME.OVR:0x139CD`, the turn-undead routine, takes the **caster's** level out of `class_levels[0]` at record 0x096 and bands it -- 1 to 8 as it stands, 9 to 13 as 9, otherwise 10 -- as the matrix *column*, and at `0x13A2A` reads `es:[di+0x76]` off the **target** and multiplies it by ten as the *row*. So there is no caster-side byte in a DOS record at all, and the menu builder at `GAME.OVR:0x9F8C` gates the word `Turn ` on the cleric level rather than on this.\n* Eleven of the 103 distinct DOS Pool of Radiance monster records carry a non-zero value here and every one is undead, at the published AD&D rows: skeleton 1, zombie 2, ghoul 3, wight 5, wraith 7, giant skeleton 8, mummy 8, juju zombie 9, spectre 9, vampire 10, and FERRAN MARTINEZ 9. `tools/turncensus.py --dos` re-runs it.\n* Every player character in either port reads 0, including ROLAND, a cleric 3, whose C64 counterpart's `turn_power` would read 1.\nSo a converter writes **zero** here for a player character and takes nothing from the caster's strength, which the DOS engine works out for itself. docs/178-turning-undead.md, #297", candidate=False), Field(offset=119, size=1, kind=<Kind.I8: 'i8'>, name='thief_pick_pockets', label='Pick pockets', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="eight percentages, 0x077-0x07E, in the C64's order. Nonzero for the thief, for the fighter/mage/thief, and for SILAS -- a fighter 4 who also carries thief level 1 in the per-class array, which reads as a dual-class human. Signed, as on the C64, where a halfling's read-languages sits at -5", candidate=False), Field(offset=120, size=1, kind=<Kind.I8: 'i8'>, name='thief_open_locks', label='Open locks', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=121, size=1, kind=<Kind.I8: 'i8'>, name='thief_find_traps', label='Find traps', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=122, size=1, kind=<Kind.I8: 'i8'>, name='thief_move_silently', label='Move silently', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=123, size=1, kind=<Kind.I8: 'i8'>, name='thief_hide_in_shadows', label='Hide in shadows', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=124, size=1, kind=<Kind.I8: 'i8'>, name='thief_hear_noise', label='Hear noise', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=125, size=1, kind=<Kind.I8: 'i8'>, name='thief_climb_walls', label='Climb walls', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=126, size=1, kind=<Kind.I8: 'i8'>, name='thief_read_languages', label='Read languages', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=127, size=4, kind=<Kind.RAW: 'raw'>, name='effect_chain', label='Effect list pointer (LIVE)', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="a four-byte far pointer, and one of the seven runs Curse's importer refuses to read (0x7F-0x82). The 0x081/0x082 pair moves with the heap between two saves of the same party; the active effects themselves are in the sibling `.SPC` file, one node per record, each carrying the next in its own last four bytes. LIVE, and NULL is the right thing to write: the engine allocates the nodes on load and sets this pointer itself, measured under DOSBox-X (#61)", candidate=False), Field(offset=131, size=5, kind=<Kind.RAW: 'raw'>, name='field_83_87', label='unnamed @0x083', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note="00 00 01 00 00 in 101 of 101 engine-written Pool of Radiance records (#235; was 24) -- the engine hands back whatever is staged there and the sheet is pixel-identical either way, so nothing a player sees is driven by it. Curse's importer copies the run verbatim to its own 0xF6-0xFA without naming it either. The third-party names 'Morale' at 0x084 and 'TreasureShare' at 0x085 are neither confirmed nor refuted -- a player character has no morale and takes one share, which is consistent but not evidence. See docs/141-dos-savegame.md", candidate=True), Field(offset=136, size=2, kind=<Kind.U16LE: 'u16le'>, name='copper', label='Copper', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="seven u16le coin counts, 0x088-0x095, in the C64's order: copper, silver, electrum, gold, platinum, gems, jewelry. Three independent checks. A fresh export carries only gold. Between saves B and A all six party members gained the same amount of every kind, which is Gold Box splitting treasure evenly. And the encumbrance identity at 0x102 balances only with this reading and only little-endian. **Curse's importer discards the lot** and writes 300 platinum instead -- a fact about Curse's import, not about this field", candidate=False), Field(offset=138, size=2, kind=<Kind.U16LE: 'u16le'>, name='silver', label='Silver', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=140, size=2, kind=<Kind.U16LE: 'u16le'>, name='electrum', label='Electrum', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=142, size=2, kind=<Kind.U16LE: 'u16le'>, name='gold', label='Gold', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=144, size=2, kind=<Kind.U16LE: 'u16le'>, name='platinum', label='Platinum', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=146, size=2, kind=<Kind.U16LE: 'u16le'>, name='gems', label='Gems', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=148, size=2, kind=<Kind.U16LE: 'u16le'>, name='jewelry', label='Jewelry', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='', candidate=False), Field(offset=150, size=8, kind=<Kind.RAW: 'raw'>, name='class_levels', label='Per-class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="eight slots **indexed by the class number**: cleric 0, druid 1, fighter 2, paladin 3, ranger 4, mage 5, thief 6, monk 7. Confirmed on the four classes the specimens use -- a cleric 3 sets slot 0, a mage 3 sets slot 5, a thief 1 sets slot 6, BAKSHI the cleric/fighter/mage sets 0, 2 and 5 at once. The C64's eight slots at 0x0C9 are indexed by the class **bit** instead (magic-user, cleric, thief, fighter, knight, -, paladin, ranger), so this is a permutation and not a copy. Druid and monk have no C64 slot at all", candidate=False), Field(offset=158, size=1, kind=<Kind.U8: 'u8'>, name='sex', label='Sex', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="1 for ARGORA, ASTRID, RHIANNON, DARKSTAR and FLORENTZ and 0 for the other nineteen, which is female = 1 on the C64's encoding too. PROBABLE because no specimen names its own sex on screen here", candidate=False), Field(offset=159, size=1, kind=<Kind.RAW: 'raw'>, name='gap_09f', label='unallocated @0x09f', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=160, size=1, kind=<Kind.U8: 'u8'>, name='alignment', label='Alignment', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="0-based on the C64's own table: LAWFUL GOOD 0, LAWFUL NEUTRAL 1, LAWFUL EVIL 2, NEUTRAL GOOD 3, TRUE NEUTRAL 4, NEUTRAL EVIL 5, CHAOTIC GOOD 6, CHAOTIC NEUTRAL 7, CHAOTIC EVIL 8. Values 0, 1, 3, 4, 6 and 7 appear across the 24 and nothing outside 0-8 does, which is what a nine-entry table looks like from a party with no evil characters in it. The offset is fixed by the run either side: the eight attack-form bytes follow it and the per-class level array precedes it, both at the same +0x38 displacement to the C64", candidate=False), Field(offset=161, size=8, kind=<Kind.RAW: 'raw'>, name='attack_forms', label='Attack forms', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the C64's four parallel two-entry arrays at 0x0D9 -- attacks per round doubled, damage dice, die size, signed modifier, each holding form 0 then form 1. 0x0A1 is 2 in all 24, which is one attack in the family's halves encoding and is what Gold Box Companion's Levels.txt writes. A player character uses one form; the monster records are where the second is visible", candidate=False), Field(offset=169, size=1, kind=<Kind.U8: 'u8'>, name='armour_class_base', label='Base AC (60 - value)', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="50 in all 24, which on the family's 60 - value encoding is armour class 10: unarmoured, before dexterity and before any item. Exactly what the C64's 0x0E1 holds for every player character it has ever been read from, at the offset this one aligns to", candidate=False), Field(offset=170, size=1, kind=<Kind.U8: 'u8'>, name='strength_bonus', label='Strength bonus flag', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="1 in all 24 shipped records and in the six this project rolled in the game's own creation screens. The C64 keeps a flag of the same name and shape one byte further on, at 0x0E3, and its LIBRARY reads it at $375C to decide whether the AD&D strength tables apply at all; so the C64's counterpart of this byte is strength_bonus_flag, and `goldbox.c64_codec.write` sets it. PROBABLE rather than CONFIRMED for the DOS half: the C64 gate is read off the machine, and no DOS routine reading 0x0AA has been found, so the two are matched by name, position and a value that never varies. What is certainly *not* its counterpart is the C64's aligned byte 0x0E2, a strength *index* holding 15-22, which is why the pair were read as unrelated for a long time; neither should be copied onto the other. #277 (A DOS character converted to the C64 loses the strength bonus to hit and damage, because 0x0E3 is written zero)", candidate=False), Field(offset=171, size=1, kind=<Kind.U8: 'u8'>, name='unnamed_0ab', label='unnamed @0x0AB', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the byte that tells two characters of the same name apart. Stable per character across the A/B save pair and different for every character -- 165, 204, 0, 120, 154, 231 for the party. **ADD CHARACTER TO PARTY refuses a candidate whose name *and* this byte both match a character already in the party**, and that is measured in the running game rather than argued: #216 offered one party two records of the same name differing in this byte alone, and the second was refused at 0x00 against 0x00 and let in at 0x42. The write site is character creation, one call to the random routine, which is a code read corroborated by the shipped records' entropy. It is not experience (that starts at 0x0AC and decodes), not hit points rolled (0x0B1), and not any level. Two limits: what the engine calls it is unknown, which is why the name still says unnamed, and the read site is the only one in a memory image dumped at an encounter and after a fight -- an overlay not resident then could hold another", candidate=False), Field(offset=172, size=3, kind=<Kind.UINT_LE: 'uint_le'>, name='experience', label='Experience', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='u24le. 5333 -> 7670 for a fighter whose level byte says 3 and 7349 -> 9686 for one whose says 4, and all six party members gained the identical 2337 between saves B and A -- the even experience split. Read big-endian the same bytes put a level-3 fighter past fourteen million, which no Pool of Radiance character can reach', candidate=False), Field(offset=175, size=1, kind=<Kind.RAW: 'raw'>, name='gap_0af', label='unallocated @0x0af', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=176, size=1, kind=<Kind.U8: 'u8'>, name='class_bits', label='Class bitmask', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="bit 0 magic-user, bit 1 cleric, bit 2 thief, bit 3 fighter -- **the C64's bit order unchanged**. 24 of 24 decompose to the class byte at 0x02F, the multi-class values 9, 11 and 13 included", candidate=False), Field(offset=177, size=1, kind=<Kind.U8: 'u8'>, name='hp_rolled', label='Hit points rolled', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='hit points before the constitution bonus: it tracks hit points maximum across a level gain, offset by a per-character constant', candidate=False), Field(offset=178, size=3, kind=<Kind.RAW: 'raw'>, name='spells_castable_cleric', label='Cleric spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="how many spells of each level the character may memorise, one byte per spell level, cleric here and magic-user at 0x0B5. ROLAND, a cleric 3 with wisdom 18, reads 4 3 0; GILES, a magic-user 3, reads 0 0 0 here and 2 1 0 at 0x0B5, which is the game's own table for magic-user 3. **The C64 packs the same six numbers into three bytes**, cleric in the high nibble and magic-user in the low, at 0x0EE -- so this is an unpack, not a copy", candidate=False), Field(offset=181, size=3, kind=<Kind.RAW: 'raw'>, name='spells_castable_magic_user', label='Magic-user spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='', candidate=False), Field(offset=184, size=3, kind=<Kind.RAW: 'raw'>, name='gap_0b8', label='unallocated @0x0b8', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=187, size=1, kind=<Kind.U8: 'u8'>, name='portrait_head', label='Portrait head', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="**a one-based position in the creation menu's fourteen heads, not a block number** (#57). The menu's own table of art ids is in the game's own `START.EXE`, and the same fourteen bytes in the same order are in the C64's `GEN` on POOL3; the C64 record stores the id where this stores the position. CONFIRMED in DOSBox: position 1 draws `$00`, 7 draws `$16`, 12 draws `$39` and 14 draws `$44`, each matched pixel for pixel against the block rendered out of `HEAD<n>.DAX` -- `<n>` there is the disk the art is packed on, not anything this byte names. `goldbox/portraits.py` reads the table.\n**The four bytes at 0x0BB were one GUESS field called `icon_choice`; they are two pairs.** 0x0BB and 0x0BC are the sheet portrait and 0x0BD and 0x0BE the small combat icon, `CHEAD.DAX` and `CBODY.DAX`. Three things say so together. The community's per-title tables in `work/coab-research/formats/` name exactly this split and only Pool of Radiance has all four: from Curse onwards the first pair is gone and Pools of Darkness carries the second alone, which is what a sheet portrait dropped between titles looks like. The C64 record has `portrait_head` and `portrait_body` adjacent at 0x0FE and 0x0FF for the same reason. And the values fit: 1-11 here and 1-10 at 0x0BC across the 18, against 0-13 and 3-31 for the icon pair, which is two small sets and one larger one.\nHead and body move independently, and each byte moves its own half: BRUTUS ships as (12, 3) and draws a dark-haired head on a bare torso; (1, 3) draws a *different head* on **the same torso**; (1, 1) draws that same head on an armoured torso", candidate=False), Field(offset=188, size=1, kind=<Kind.U8: 'u8'>, name='portrait_body', label='Portrait body', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='see `portrait_head`: the body half of the same experiment', candidate=False), Field(offset=189, size=1, kind=<Kind.U8: 'u8'>, name='icon_head', label='Combat icon head', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the combat icon's half of the pair, and the half every title keeps. 0-13 across the 18", candidate=False), Field(offset=190, size=1, kind=<Kind.U8: 'u8'>, name='icon_body', label='Combat icon body', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='3-31 across the 18. Pools of Darkness reaches 31', candidate=False), Field(offset=191, size=1, kind=<Kind.U8: 'u8'>, name='party_order', label='Combat icon slot', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="**which of the eight loaded combat icons is this character's**, and not the marching order, which this was called until 2026-09-05 (#305). It sits inside the icon block for the same reason `icon_head` and `icon_colours` do.\n**CONFIRMED out of the shipped `GAME.OVR`, not from a specimen.** Pool of Radiance's overlay holds a five-instruction loop -- `mov byte ptr es:[di+0BFh], 0` / `cmp byte ptr es:[di+0BFh], 8` / `mov al, es:[di+0BFh]` used as an index into an eight-entry array of taken-slot flags / `inc byte ptr es:[di+0BFh]` -- which is an allocation of the lowest free slot of eight. Curse's overlay holds the same loop byte for byte with the displacement changed to its own 0x143. Elsewhere the same overlay stores **12** here around a far call and puts the old value back, and stores **255** and **10**: no marching position in a six-character party is ever 10, 12 or 255. `tools/dosbyteimm.py <GAME.OVR> --offset 0x0BF` is the scan.\n**Why 0-5 in file order was never evidence either way.** The engine's LOAD SAVED GAME walks the six saved filenames in order and calls the allocator once per character, so character *i* of a party with no NPC gets slot *i* -- the marching position, arrived at as a side effect. It stops agreeing the moment a character is dropped and another added, or the party is reordered, because the slot stays with the character and the file order does not. The largest value in the corpus is 7, in the two eighth-slot records of Treasures of the Savage Frontier's shipped save.\n**So the engine derives it on load and what a conversion writes here cannot survive**; the writer still writes it, because a value the destination overwrites costs nothing and the round trip needs it. `simeonpilgrim/coab` names it `icon_id` at Curse 0x143 and its `AssignPlayerIconId` (`ovr017`, `sub_4A60A`) is the loop above; `draw_combat_icon` and `chead_cbody_comspr_icon` are the readers. Curse's own Pool of Radiance importer copies 0x0BD, 0x0BE and 0x0C0 into its 0x141, 0x142 and 0x144 and **skips 0x0BF**, which is what a field the loader reallocates looks like.\n**The identifier is still `party_order` and is a misnomer.** It is the *neutral* record's field name, and `goldbox.dos.DIRECT` requires the two to be spelled the same; renaming it means renaming the neutral field, which `goldbox/amiga.py`, `goldbox/c64_codec.py`, `goldbox/yaml_io.py` and the window's own `field_party_order` all read. The value the conversion carries is right either way -- the C64 keeps its own 0-7 slot index at `goldbox/layout.py` 0x10D and both whole-save directions renumber by position -- so this is a spelling to fix in one pass, not a defect a player can reach", candidate=False), Field(offset=192, size=1, kind=<Kind.U8: 'u8'>, name='size', label='Size', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='1 small, 2 medium: 1 for the dwarf and the halfling, 2 for everyone else, in all 24. The C64 stores the same distinction one lower -- 0 small, 1 large -- at 0x099', candidate=False), Field(offset=193, size=6, kind=<Kind.RAW: 'raw'>, name='icon_colours', label='Icon colours', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="**six bytes of two 4-bit colour indices each, not heap pointers** (#57). They were read as far pointers because 0x0C4 and 0x0C6 are equal for every character in a slot; they are equal for every character in every slot of every *title*, which is not what a heap segment does. 42 of the 54 shipped records across the four titles read `91 A2 B3 C4 E6 F7` -- high nibbles 9 A B C E F, low nibbles 1 2 3 4 6 7 -- and the six that differ are the six **played** Pools of Darkness characters, each with its own set. The community tables name the pairs body, arm, leg, hair/face, shield and weapon.\n**CONFIRMED in the running game** (#112). The game's own icon editor reads `PARTS COLOR-1 COLOR-2 SIZE EXIT` and its colour menu names `WEAPON BODY HAIR SHIELD ARM LEG`. Relabelling one character's whole block with six colours no default uses gives 0x0C1 body, 0x0C2 arm, 0x0C3 leg, 0x0C4 hair *and* skin, 0x0C5 shield, 0x0C6 weapon, and moving one byte alone moves one part. The **low** nibble is the main colour and the high one the highlight -- COLOR-1 writes the low, COLOR-2 the high -- and nibble 0 is EGA 8.\n**Zero is not neutral, which is why a conversion may not write it.** The engine does not rebuild these: `docs/117` records that its own resave kept our zeros. A zeroed block paints all six parts EGA 8, dark grey -- the combat floor's own colour -- so the figure is about 64 black outline pixels on a background of exactly its own shade and reads as not being there. Measured over three fights, 4 zeroed icons against 9 default ones; a box round a zeroed figure holds two colours where the same box round a default one holds fourteen. `goldbox.dos.WRITE_DEFAULTS` writes the shipped set instead", candidate=False), Field(offset=199, size=1, kind=<Kind.U8: 'u8'>, name='item_count', label='Item count', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='24 of 24: count x 63 is the exact size of the sibling `.ITM` file. **Always 0 in an export**, which is the one systematic difference between a save slot and a `.CHA`', candidate=False), Field(offset=200, size=56, kind=<Kind.RAW: 'raw'>, name='item_chain', label='Item pointer block (LIVE)', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note="the item list as the engine holds it: a pointer block at 0x0CC that Curse's importer copies as 0x34 = 52 raw bytes, and that is the whole of what it does with items. The C64 has sixteen fixed slots and no chain, so the pointers go and the order stays. LIVE", candidate=False), Field(offset=256, size=1, kind=<Kind.U8: 'u8'>, name='hands_used', label='Hands used', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="first byte of the combat tail Curse's importer names hands used, weight, health status, in-combat, team, hit bonus, armour class, attacks left, dice, damage bonuses, current hit points, movement. 2 for the fighters and 1 for the mages here", candidate=False), Field(offset=257, size=1, kind=<Kind.RAW: 'raw'>, name='gap_101', label='unallocated @0x101', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note='zero in every specimen held', candidate=False), Field(offset=258, size=2, kind=<Kind.U16LE: 'u16le'>, name='encumbrance', label='Encumbrance', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="u16le, and **the single most useful number in the file**:\n```\nencumbrance = cp + sp + ep + gp + pp + gems + jewelry\n + sum(item weight x quantity)\n```\nIt balances exactly for 16 of the 18 saved characters and for all six exports, and the two that miss carry a stack of darts whose rendered name disagrees with the quantity byte, so one of those two is stale rather than the identity being wrong. That one sum confirms the money block, the 63-byte item stride, the weight offset and the byte order together. **Derived, not stored** -- Curse's `reclac_player_values` computes it the same way -- so the C64, which has no such field, loses nothing", candidate=False), Field(offset=260, size=8, kind=<Kind.RAW: 'raw'>, name='heap_104', label='unnamed @0x104 (LIVE)', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note="another of the runs Curse's importer skips; 0x106/0x107 move with the heap. LIVE", candidate=False), Field(offset=268, size=4, kind=<Kind.RAW: 'raw'>, name='field_10c_10f', label='status, active, side, quickfight', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the combat tail (#235), stored state rather than scratch -- twelve values staged across two boots, including 04 00 00 00 and 06 00 00 00, all came back byte for byte from the engine's own ENCAMP > SAVE. 0x10C is the status, 0-based: 0 Okay .. 8 Gone, the order of nine length-prefixed strings in START.EXE from file offset 0xD191, CONFIRMED against the sheet showing STATUS OKAY and STATUS UNCONSCIOUS for the same character staged 0 and 4. 0x10D is an active flag, CONFIRMED: 0 draws the name red in the party panel and 1 does not, 3 of 3 against 9 of 9 across two boots -- and it is the engine's own `in_combat`, which is the same field rather than a rival reading of it (#305). Curse's Pool of Radiance importer, in its shipped `GAME.OVR` at file offset 0x01D55F, copies this byte into Curse's 0x196 in the instruction pair after the one that copies 0x10C into Curse's 0x195; `simeonpilgrim/coab` calls Curse 0x195 `health_status` and 0x196 `in_combat`, and its `displayPlayerName` paints a character whose `in_combat` is false in colour 0x0C, EGA light red -- which is the red name the staging measured. 0x10E is the combat side, 1 for the enemy's, CONFIRMED from the DOS engine's own combat code and from a party member staged to it, which attacked his own side and was dropped from the party when the fight ended -- docs/169-dos-combat-side.md. 0x10F is the quickfight flag, CONFIRMED: staged 1, a fight ran to its end with QUICK never pressed; staged 0, the same encounter stopped at the command bar. The default 00 01 00 00 is what a freshly made character carries, not a constant every specimen holds -- see docs/141-dos-savegame.md", candidate=False), Field(offset=272, size=1, kind=<Kind.U8: 'u8'>, name='thac0_current', label='THAC0 current (60 - value)', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="40-47, one above the base for the fighter with 18/17 strength. Derived from the readied weapon. The C64's roster keeps the same number in the same encoding at +0x0E", candidate=False), Field(offset=273, size=1, kind=<Kind.U8: 'u8'>, name='armour_class', label='Armour class current (60 - value)', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="`60 - AC`, the family's encoding, and a **negative** armour class is what it is meant to give: SILAS reads 63, which is AC -3, and AD&D 1st edition puts a fighter in plate mail (AC 3) with a shield +1 (-2) and dexterity 18 (-4) at exactly -3. This was read as 'unsettled bias' while the negative value looked like an error. The C64 keeps the same number at record 0x10F and roster +0x0F", candidate=False), Field(offset=274, size=9, kind=<Kind.RAW: 'raw'>, name='roster_tail', label='Armour bonus and current attack form', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="**the C64's nine-byte roster tail, one for one**: the armour bonus at +0x10 then the eight running attack-form bytes at +0x11 -- two attack counts, two dice counts, two die sizes, two damage bonuses. The whole DOS combat tail 0x110-0x11C lines up on the C64's roster block 0x10E-0x11B at a displacement of -2, with the one-byte widening of hit points at the end accounting for the rest", candidate=False), Field(offset=283, size=1, kind=<Kind.U8: 'u8'>, name='hp_current', label='Hit points current', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note='equals hit points maximum for 22 of the 24 and is lower for the two wounded characters', candidate=False), Field(offset=284, size=1, kind=<Kind.U8: 'u8'>, name='movement_current', label='Movement current', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='12 for everyone unencumbered and 6 for SILAS, who wears plate mail. Derived from encumbrance; recompute rather than copy', candidate=False))¶
Every one of the 285 bytes belongs to exactly one entry.
- goldbox.dos_layout.NAME_SIZE = 16¶
One count byte plus fifteen bytes of ASCII.
- goldbox.dos_layout.POOLS_OF_DARKNESS = DosShape(key='pools-of-darkness', title='Pools of Darkness', record_size=510, item_suffix='.THG', effect_suffix='.EFX', item_size=63, spellbook_spells=125, race_numbers=('elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 141, 'spellbook': 125, 'levels_drained': 0, 'hp_lost_to_drain': 0, 'field_83_87': 4, 'copper': 0, 'silver': 0, 'electrum': 0, 'gold': 0, 'class_levels': 7, 'gap_09f': 0, 'strength_bonus': 0, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 9, 'spells_castable_magic_user': 9, 'gap_0b8': 2, 'portrait_head': 0, 'portrait_body': 0}, inserts={'char_class': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False),), 'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False), Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='highest_class_levels', label='Highest class levels', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='a third copy of the level array, and what a title with level drain that matters needs: the level to restore to. Zero in every shipped record, so PROBABLE from its position and from the highest-experience field that sits beside experience for the same reason', candidate=False)), 'experience': 5, 'spells_castable_cleric': (Field(offset=0, size=9, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False),), 'icon_colours': 2, 'heap_104': 2})¶
Pools of Darkness, 510 bytes, and the one with no C64 counterpart at all. It is the later engine: no drained-level pair, no
modifiedbyte, notype, no monk, no experience-per-hit-point award – and only three money slots, platinum, gems and jewelry, where every earlier title has seven. It gains a highest-level array and a highest-experience field beside the current ones, which is what a title with level drain that matters looks like.
- goldbox.dos_layout.POOLS_OF_DARKNESS_RACE_NUMBERS = ('elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster')¶
Silver Blades’ table with
tribbledropped from the front, so seven entries rather than eight. CONFIRMED the same two ways (#237) –GAME.EXEat file offset 0x00A9D4, stride 9, and Gold Box Companion’s04. Pools of Darkness/Game.datat 0x0863C4.Three entries are pinned by class legality alone, without either table: ORATISI NOMOON and SEV STAREKIAN read 0 and CHERROD reads 1, all three fighter/magic-user/thieves, which only an elf or a half-elf may be; ORATISI is an elf in Pool of Radiance, so 0 is
elfand 1 ishalf-elf. Four paladins read 5, and only a human may be a paladin.Index 2 is confirmed in a running game, on the other port: the Amiga Pools of Darkness table in
goldbox/amiga.pyis the same six names in the same order, andHALF-ELFandDWARFare two of the indices a probe there put on screen. The Amiga tuple stops athuman, having nomonsterentry to name.Treasures of the Savage Frontier’s 510-byte records read through this table too, and only through this one: its fourteen shipped characters include paladins at 5 and a fighter/magic-user/thief at 1, which are a human and a half-elf here and a halfling and a dwarf under
RACE_NUMBERS. Its ownGAME.EXEcarries the same seven names. Not a title this project reads, and one more thing the record size settles.- Type:
Pools of Darkness’ race codes
- goldbox.dos_layout.POOL_OF_RADIANCE = DosShape(key='pool-of-radiance', title='Pool of Radiance', record_size=285, item_suffix='.ITM', effect_suffix='.SPC', item_size=63, spellbook_spells=56, race_numbers=('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human'), sizes={}, inserts={})¶
the table above, unchanged. Present so callers can treat all four alike.
- Type:
Pool of Radiance itself
- goldbox.dos_layout.RACE_NUMBERS = ('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human')¶
Race codes for Pool of Radiance and Curse only, shared with the C64 – except that the C64 table is 1-based on the same names with
monsterat 0 on both. The two later titles renumber the races and this is not their table: readDosShape.race_numbers, never this, unless the title is known. It stays the module-level name so nothing that read it before #237 changed meaning, and it is the defaultDosShapefield.
- goldbox.dos_layout.RECORD_SIZE = 285¶
The Pool of Radiance DOS character record. A save slot (
CHRDAT<slot><n>.SAV) and an export (<NAME>.CHA) are the same 285 bytes in the same order; the only systematic difference is that an export zeroes the item count.
- goldbox.dos_layout.SECRET_OF_THE_SILVER_BLADES = DosShape(key='secret-of-the-silver-blades', title='Secret of the Silver Blades', record_size=439, item_suffix='.STF', effect_suffix='.SFX', item_size=67, spellbook_spells=117, race_numbers=('tribble', 'elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 75, 'spellbook': 117, 'field_83_87': 4, 'class_levels': 7, 'gap_09f': 0, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 7, 'spells_castable_magic_user': 7}, inserts={'char_class': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False),), 'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False),), 'spells_castable_cleric': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False), Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='spells_castable_unattributed', label='A fourth spell-slot array', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note="Silver Blades is the only title of the four with a **fourth** slot array -- 28 bytes where Curse has 15 and Pools of Darkness 27 -- and no shipped character sets a byte of it. Cleric, druid and magic-user account for the other three, and it is **not the paladin's**: Pools of Darkness puts a paladin's spells in the cleric array. UNKNOWN. A played Silver Blades save with a caster the shipped party does not have would settle it", candidate=False)), 'icon_colours': 3, 'heap_104': 1})¶
Secret of the Silver Blades, 439 bytes. Curse’s record plus a spellbook of 117 –
goldbox/spells.py’s Silver Blades id space, 1..117 – seven spell slot levels rather than five, and two undecoded slot arrays between the cleric’s and the magic-user’s rather than one. It drops the monk level slot and thetypebyte, and its memorised region is smaller than Curse’s at 75 bytes.Its items are 67 bytes each in a `.STF` file – measured in the running game (#113), where the mayor of New Verdigris hands the party twelve magic items in its first minute and the file that appears beside the record is
CHRDATC1.STF, 804 bytes for an item count of 12. 804 is 12 x 67 and is not divisible by 63. Every field is still at Pool of Radiance’s offset – the weights are the published AD&D figures in tenths of a pound,quantityis 30 on the line reading30 Arrows, and aMAGE SCROLL 3 SPELLScarries three ids inside this title’s 1..117 space in the three special bytes – so the four extra bytes are0x03F-0x042and are zero in 12 of 12.
- goldbox.dos_layout.SHAPES_BY_SIZE: dict[int, goldbox.dos_layout.DosShape] = {285: DosShape(key='pool-of-radiance', title='Pool of Radiance', record_size=285, item_suffix='.ITM', effect_suffix='.SPC', item_size=63, spellbook_spells=56, race_numbers=('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human'), sizes={}, inserts={}), 422: DosShape(key='curse-of-the-azure-bonds', title='Curse of the Azure Bonds', record_size=422, item_suffix='.SWG', effect_suffix='.FX', item_size=63, spellbook_spells=100, race_numbers=('monster', 'dwarf', 'elf', 'gnome', 'half-elf', 'halfling', 'half-orc', 'human'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 84, 'spellbook': 100, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 5, 'spells_castable_magic_user': 5}, inserts={'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=8, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False),), 'spells_castable_cleric': (Field(offset=0, size=5, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False),), 'icon_colours': 1, 'heap_104': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False), 3)}), 439: DosShape(key='secret-of-the-silver-blades', title='Secret of the Silver Blades', record_size=439, item_suffix='.STF', effect_suffix='.SFX', item_size=67, spellbook_spells=117, race_numbers=('tribble', 'elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 75, 'spellbook': 117, 'field_83_87': 4, 'class_levels': 7, 'gap_09f': 0, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 7, 'spells_castable_magic_user': 7}, inserts={'char_class': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False),), 'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False),), 'spells_castable_cleric': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False), Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='spells_castable_unattributed', label='A fourth spell-slot array', confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, note="Silver Blades is the only title of the four with a **fourth** slot array -- 28 bytes where Curse has 15 and Pools of Darkness 27 -- and no shipped character sets a byte of it. Cleric, druid and magic-user account for the other three, and it is **not the paladin's**: Pools of Darkness puts a paladin's spells in the cleric array. UNKNOWN. A played Silver Blades save with a caster the shipped party does not have would settle it", candidate=False)), 'icon_colours': 3, 'heap_104': 1}), 510: DosShape(key='pools-of-darkness', title='Pools of Darkness', record_size=510, item_suffix='.THG', effect_suffix='.EFX', item_size=63, spellbook_spells=125, race_numbers=('elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster'), sizes={'strength': 2, 'intelligence': 2, 'wisdom': 2, 'dexterity': 2, 'constitution': 2, 'charisma': 2, 'exceptional_strength': 2, 'gap_017': 0, 'spells_memorised': 141, 'spellbook': 125, 'levels_drained': 0, 'hp_lost_to_drain': 0, 'field_83_87': 4, 'copper': 0, 'silver': 0, 'electrum': 0, 'gold': 0, 'class_levels': 7, 'gap_09f': 0, 'strength_bonus': 0, 'experience': 4, 'gap_0af': 0, 'spells_castable_cleric': 9, 'spells_castable_magic_user': 9, 'gap_0b8': 2, 'portrait_head': 0, 'portrait_body': 0}, inserts={'char_class': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='paladin_cures', label='Cure disease uses left', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="how many times the paladin may still CURE DISEASE. **1 for every paladin and 0 for everybody else**, in 8 paladin records and 71 others across four record shapes and six titles: Curse's MATHEW, MARK and DEMELTINA and Gateway to the Savage Frontier's JERRICUS at the 422-byte shape's 0x191, Silver Blades' Guy de Valois and DEMELTINA, Pools of Darkness' Guy de Valois and DEMELTINA and Treasures of the Savage Frontier's MAXWELL and JERRICUS at the byte immediately after `char_class`. Pool of Radiance has no such byte -- it has no paladins.\n**Named from the game's own code**: `simeonpilgrim/coab`, the decompilation of the DOS Curse overlays this project already reads for `docs/117-save-conversion.md`, declares `paladinCuresLeft` at record 0x191 of a `StructSize = 0x1A6` player, which is the 422 bytes of the record. Character creation writes 1 (`ovr018`), CURE DISEASE is offered only while it is above zero and decrements it (`ovr020`), and the refresh sets `((paladinLevel - 1) / 5) + 1` (`ovr013`) -- so a Curse paladin of 11 or better could hold 3, and no specimen here holds anything but 0 or 1.\n**The offset moves between the engines and the field does not**: the 422-byte shape keeps it in the combat tail and the 439- and 510-byte shapes keep it beside the class. It stays 1 after HUMAN CHANGE CLASSES: DEMELTINA is a cleric 1 with former paladin 5 in `WISH-SPEC-curse-234-dualclassed` and still reads 1.\n**Silver Blades does not use it the way Curse's code does, and that is measured rather than assumed** (#299). Staged on Guy de Valois in DOS Silver Blades under DOSBox: at 0 the sheet still offers `CURE`, so it does not gate the command the way `CanCastCureDiseases` gates Curse's; at 2 one use ends the offer, so it does not count uses either; and one use takes a staged 2 to **0** in the engine's own resave, so the engine clears it rather than decrementing it. What is CONFIRMED for the later titles is therefore narrower than the name: the byte is the paladin's cure-disease bookkeeping, cleared when he cures, and it is 1 for every paladin and 0 for everybody else. Whether it is literally Curse's own field is PROBABLE and no more", candidate=False),), 'level': (Field(offset=0, size=1, kind=<Kind.U8: 'u8'>, name='former_level', label='Level left the old class at', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the level a dual-classed character left the old class at, again -- the same number `former_class_levels` holds at the old class's slot, in a single byte with no slot to look up. CONFIRMED on the same two engine-written transitions: Curse's DEMELTINA goes 0 -> 5 here exactly as slot 3 of former_class_levels does, and Silver Blades' PAINE goes 0 -> 8 exactly as slot 4 does. Curse's own `GAME.OVR` at 0x03BD7B writes this byte and the array entry a few instructions apart in the same routine, both from `es:[di+0xE5]` (`level`, before it is reset to 1). The reader checks the two agree and warns when they do not; the writer this project does not yet have would write both from the one neutral value", candidate=False),), 'class_levels': (Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='former_class_levels', label='Former class levels', confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, note="the per-class level array again, indexed by class number the same way, holding what a dual-classed character *was*. CONFIRMED by two engine-written transitions, one action apart from their own before-state and read at this shape's own offsets: Curse's DEMELTINA, a human paladin 5 put through HUMAN CHANGE CLASSES at the training hall, has slot 3 (the class he left) go 0 -> 5 while class_levels[paladin] goes 5 -> 0 and class_levels[cleric] goes 0 -> 1; Silver Blades' PAINE, a human ranger 8 trained the same way, has slot 4 go 0 -> 8 while class_levels[ranger] goes 8 -> 0 and class_levels[magic-user] goes 0 -> 1. Pool of Radiance has no such array", candidate=False), Field(offset=0, size=7, kind=<Kind.RAW: 'raw'>, name='highest_class_levels', label='Highest class levels', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note='a third copy of the level array, and what a title with level drain that matters needs: the level to restore to. Zero in every shipped record, so PROBABLE from its position and from the highest-experience field that sits beside experience for the same reason', candidate=False)), 'experience': 5, 'spells_castable_cleric': (Field(offset=0, size=9, kind=<Kind.RAW: 'raw'>, name='spells_castable_druid', label='Druid spell slots', confidence=<Confidence.PROBABLE: 'PROBABLE'>, note="the slot array between the cleric's and the magic-user's, one byte per spell level. **CONFIRMED by the rangers**, six of them across two titles: Silver Blades' PAINE, ARGORA and RWELLYN are level 8 and each holds 1 here and nothing in the other arrays, which is AD&D's ranger getting his first *druid* spell at 8; Pools of Darkness' CLARISSA, ARGORA and RWELLYN are level 13 and hold 2 1 here **and** 2 1 in the magic-user array, which is the same ranger at 13. Both match `goldbox/spells.py`'s ranger grant table, read out of the C64 `GEN`. Paladins do **not** use this array -- Pools of Darkness' Guy de Valois, a paladin 12, holds 2 2 in the *cleric* array", candidate=False),), 'icon_colours': 2, 'heap_104': 2})}¶
285, 422, 439, 510.
- Type:
The record size identifies the title on its own
- goldbox.dos_layout.SILVER_BLADES_RACE_NUMBERS = ('tribble', 'elf', 'half-elf', 'dwarf', 'gnome', 'halfling', 'human', 'monster')¶
the table is reordered and half-orc is gone, which is the switch to AD&D 2nd Edition, where the half-orc stopped being a player race. Entry 0 is the game’s own joke, and
tribbleis what the executable says.CONFIRMED, read two independent ways and cross-checked against the records (#237).
tools/dosraces.pyreads it out ofSTART.EXE, where the eight entries are counted strings on a stride of 9 beginning at file offset 0x00E721, bounded above by the eighteenth class name and below by the alignment table’sLawful Good; Gold Box Companion’sGBC/Games/03. Secret of the Silver Blades/Game.datcarries the same list at 0x0863C4. The same reader run against Pool of Radiance and Curse reproducesRACE_NUMBERSentry for entry, which is what makes it a measurement rather than a reading of a list that happened to be there.Three entries are exercised by a shipped record: 1 by ORATASI NOMOON, a fighter/magic-user/thief, which is an elf-or-half-elf class and which is race 2 = elf in Pool of Radiance – so 0 is a slot ahead of
elfand the table really does start attribble; 3 by MALACHITE, a fighter/thief; and 6 by nine characters including the paladins DEMELTINA and Guy de Valois, which is human-only. The other five rest on the two sources above.The C64 side of this repository already knew.
goldbox/games.py’sRACES_SILVER_BLADESis 1 elf, 2 half-elf, 3 dwarf, 4 gnome, 5 halfling, 6 human – the same numbering, read out of the C64GENand confirmed on the shipped pre-generated characters – andgoldbox/levels.pyreaches the same conclusion from a third direction, since Silver Blades’ race-indexed level-limit table atGEN $178Ahas the dwarf’s row at 3 and refuses to look one up at 6 or above, which is the human having no limit. So this module and that one disagreed about the same game until #237.- Type:
Secret of the Silver Blades’ race codes. Not a shift of the above
- goldbox.dos_layout.SPELLBOOK_FIRST_ID = 1¶
The DOS spellbook is one byte per spell for spell ids 1..56, and 56 is
RESTORATION, which the C64’s seven-byte bitmask cannot hold.
- goldbox.dos_layout.layout_for(what)[source]¶
The field table for one title, built from Pool of Radiance’s.
Every field keeps its name, its meaning and its note; what a title moves is where it lands. A shape whose widths do not add up to its own record size raises here rather than handing back a table that reads the wrong bytes – which is the check that makes a new title cheap to try.
What that check does not catch, and what does. It is a sum, so two compensating mistakes in one shape – a field short by n and a later one long by n – add up correctly and pass, mis-placing every field between them. What catches that is the per-specimen work in
tests/test_dosconvert.py: every record rebuilding byte for byte, the encumbrance identity balancing, and the class bitmask agreeing with the level arrays, all 54 of 54 today.Those tests need the player’s own archives and skip without them, so CI does not run them. On a machine with no
FR_ARCHIVESthe only thing standing behind Curse, Silver Blades and Pools of Darkness is this sum andtest_each_shape_tiles_its_own_record, which checks total width and that offsets increase – not that any field is in the right place. So an edit to one of those three shapes is only really tested where the archives are. Say in the commit that you ran it somewhere they exist.- Parameters:
what (int | str | goldbox.dos_layout.DosShape)
- Return type:
- goldbox.dos_layout.shape_for(what)[source]¶
The shape for a record size, a key, or a shape.
The size names the shape, and among these four it names the title too – no two of the four are the same length – which is what lets a reader identify a file it was handed with no title.
It does not name the title beyond them, and the machine that holds these archives holds the counter-example (#53). Treasures of the Savage Frontier’s fourteen shipped records are 510 bytes and read straight through the Pools of Darkness table: 14 of 14 rebuild byte for byte and 14 of 14 balance the encumbrance identity, on characters carrying real items, 400 to 1413. Gateway to the Savage Frontier’s twelve
.GUYexports are 422 bytes and read through the Curse table – 12 of 12 rebuild, and 11 of 12 balance once its.SWGfile is read as the item file (KAUNAS is 10 over and is not diagnosed). Neither is a title this project reads, and neither has a row here; what they establish is that a size answers “which shape”, and only the directory answers “which game”.- Parameters:
what (int | str | goldbox.dos_layout.DosShape)
- Return type: