goldbox.c64_codec

The C64 codec: a 580-byte C64 record to and from a neutral character.

Both halves of the pair goldbox/neutral.py describes. write() turns a neutral character into the 580 bytes; read() turns the 580 bytes into a neutral character. The module knows the C64 record and nothing else: which neutral field goes to which entry of goldbox/layout.py, what the C64 stores that no source supplies, and what the C64 has no room for. Where a value came from is a source reader’s business and reaches write() only as the phrase goldbox.neutral.Value carries.

Every byte of the output is justified – it came from a neutral value, or it was computed from one by a named rule, or it is a documented constant – which is what Report.unaccounted asserts.

Evidence for the fields themselves is in goldbox/layout.py; for the conversion, docs/117-save-conversion.md.

Module Attributes

DIRECT

Neutral field -> the C64 field it becomes, where the value crosses unchanged.

POOL_OF_RADIANCE_RECORD

81 memorised slots, `0x020`-`0x070`, and no second ability array, no dual-class pair.

CURSE_RECORD

69 memorised slots, `0x020`-`0x064`, a second ability array at 0x065, the dual-class pair, and no spell-slot array at all.

SILVER_BLADES_RECORD

74 memorised slots, `0x01B`-`0x064` -- the only title measured whose list does not start at 0x020.

LEVEL_FIELDS

Class name -> the C64 field holding that class's level.

INFRAVISION

Race name -> the stored 0x0D5 byte (tens of feet).

ITEM_SLOTS

How many item slots the C64 record has.

NO_SLOT_ARRAY_FOR

What a player reads when a title's C64 record has no home for something the source held.

NO_SPELL_SLOTS

both proofs that produced it -- #192 step 3 and #193 step 3 -- watched the memorise screen enforce a spell ceiling nothing in the converted save wrote, so a Curse or Silver Blades character arriving with no spells_castable array loses nothing.

STATUS_BITS

Neutral status name -> the low three bits of C64 record 0x100.

STATUS_BY_BITS

The same table read the other way, for read().

OUT_OF_PLAY

Bit 7 of record 0x100, and it is a flag in its own right rather than part of the status: LIBRARY $38BE masks it off with AND #$07 before drawing the word, $1BF6 skips a slot carrying it when it sums the party's strength, and $3E4A picks the greyed colour for the party panel with CMP #$80 -- and the colour it picks is 2, red, not grey.

NO_C64_STATUS

What a player is told when the source's status has no C64 value.

TRANSFORMED

Neutral fields the writer takes by a rule rather than by a copy.

DROPPED

Neutral fields the C64 writer takes nothing from, and why.

READ_DROPPED

A save slot holds 256 of the record's 580 bytes; the roster block and the item page hold the rest.

READ_DROPPED_SILENT

What a player reads for each name in READ_DROPPED -- the read side's twin of goldbox.dos.DROPPED_PLAYER_TEXT.

READ_DERIVED

C64 fields the reader leaves behind because the value is recomputed rather than genuinely lost -- the read side's twin of `goldbox.dos.

READ_TARGETS

What read() does with every named field of the C64 layout -- the layout-wide account the DOS writer of #26 called for, so a C64 field nothing reads cannot be dropped in silence.

Functions

field_disposition()

Every neutral field and what write() does with it.

get_memorised(rec[, game])

The whole memorised-spell list, as wide as this title reads it.

memorised_span([game])

(offset, size) of this title's memorised-spell list.

read(rec[, roster, inventory, game, source])

Read one C64 record into the neutral record.

record_shape([game])

The record shape for a title, Pool of Radiance's when none is given.

set_memorised(rec, data[, game])

Write the whole list back.

span_of(names)

(offset, size) of a run of declared fields, refusing a gap in it.

strength_index(strength, percentile)

The C64's strength_index: STR below 18, else 18 plus the band.

write(char[, icon])

Build a 580-byte C64 character record from a neutral one.

Classes

RecordShape

The parts of the 580-byte record the titles do not agree about.

Report

A C64 conversion's provenance: every byte has to be explained.

goldbox.c64_codec.DIRECT: tuple[tuple[str, str], ...] = (('strength', 'strength'), ('intelligence', 'intelligence'), ('wisdom', 'wisdom'), ('dexterity', 'dexterity'), ('constitution', 'constitution'), ('charisma', 'charisma'), ('exceptional_strength', 'exceptional_strength'), ('thac0_base', 'thac0_base'), ('race', 'race'), ('char_class', 'char_class'), ('age', 'age'), ('hp_max', 'hp_max'), ('attack_level', 'attack_level'), ('save_paralysis', 'save_paralysis'), ('save_petrification', 'save_petrification'), ('save_wands', 'save_wands'), ('save_breath', 'save_breath'), ('save_spell', 'save_spell'), ('movement', 'movement'), ('level', 'level'), ('levels_drained', 'levels_drained'), ('hp_lost_to_drain', 'hp_lost_to_drain'), ('thief_pick_pockets', 'thief_pick_pockets'), ('thief_open_locks', 'thief_open_locks'), ('thief_find_traps', 'thief_find_traps'), ('thief_move_silently', 'thief_move_silently'), ('thief_hide_in_shadows', 'thief_hide_in_shadows'), ('thief_hear_noise', 'thief_hear_noise'), ('thief_climb_walls', 'thief_climb_walls'), ('thief_read_languages', 'thief_read_languages'), ('copper', 'copper'), ('silver', 'silver'), ('electrum', 'electrum'), ('gold', 'gold'), ('platinum', 'platinum'), ('gems', 'gems'), ('jewelry', 'jewelry'), ('sex', 'sex'), ('alignment', 'alignment'), ('armour_class_base', 'armour_class_base'), ('experience', 'experience'), ('class_bits', 'class_bits'), ('hp_rolled', 'hp_rolled'), ('party_order', 'party_order'), ('hp_current', 'hp_current'), ('thac0_current', 'thac0'), ('armour_class', 'armour_class'), ('movement_current', 'roster_movement'))

Neutral field -> the C64 field it becomes, where the value crosses unchanged. The names differ in only two places, and both times because the C64 name is a storage detail: thac0 and roster_movement are what the roster block calls the current THAC0 and the current movement rate.

goldbox.c64_codec.DROPPED: tuple[tuple[str, str], ...] = (('infravision', "the C64 computes its own from race, so a source's value is recomputed rather than copied"), ('encumbrance', 'derived -- the C64 has no such field and recomputes what it needs'))

Neutral fields the C64 writer takes nothing from, and why. Reported by Writer.finish for any character that carries one, never silent.

goldbox.c64_codec.INFRAVISION = {'dwarf': 6, 'elf': 6, 'gnome': 6, 'half-elf': 6, 'half-orc': 6, 'halfling': 3, 'human': 0, 'monster': 0}

Race name -> the stored 0x0D5 byte (tens of feet). The C64 stores this at 0x0D5 and it is a property of the race, not of any save: DOS does not store it, the Amiga derives what it needs, so the C64 writer computes it.

Read off the character generator’s own race table, races 1-7, in both POOL3.D64:GEN ($0E5C, read by LDY #$D5 / TAX / LDA $0E5C,X / STA $6B00,Y at $094F) and CURSE_A.D64:GEN ($0C4B, read at $0C11, written to $7CD5 at $0C14) – both titles ship the same seven values: dwarf 6, elf 6, gnome 6, half-elf 6, halfling 3, half-orc 6, human 0. tools/infravision.py table reads and disassembles both off the player’s own disks; #392 (A converted halfling gets sixty feet of infravision, where the C64's own generator gives him thirty) is where the halfling’s was found wrong – it had been 6 on the AD&D 1st-edition argument that gnome, halfling and half-orc all get the same 60 feet, and the game does not agree for the halfling.

Secret of the Silver Blades’ GEN writes 0 into this byte at $2278, the only write to it anywhere in that overlay – but as one STA inside a loop that zeroes a wider range of the record ($7CD3-$7CD5) from an A set to 0 well before it, not a race-indexed table read. A separate fact, not folded into this table, and not yet acted on beyond being noted here.

Keyed by name, not by the record’s race number (#287, A converted Silver Blades human sees in the dark, because the infravision table is keyed by Pool of Radiance’s race numbers). The number is an index into the record’s own title’s race table, and the titles disagree: human is 7 in Pool of Radiance and Curse and 6 in Silver Blades, which used to be the half-orc’s slot – so every converted Silver Blades human read the half-orc’s 6. _infravision() looks the number up through goldbox.games.race_table for the record’s own title before this table is asked, so the name and not the number is what travels between titles.

goldbox.c64_codec.LEVEL_FIELDS: dict[str, str] = {'cleric': 'level_cleric', 'fighter': 'level_fighter', 'knight': 'level_knight', 'magic-user': 'level_magic_user', 'paladin': 'level_paladin', 'ranger': 'level_ranger', 'thief': 'level_thief'}

Class name -> the C64 field holding that class’s level. The C64 indexes its eight slots by the class bit; a class with no bit has no slot, and a level for one is reported rather than written somewhere plausible.

goldbox.c64_codec.NO_C64_STATUS: dict[str, str] = {'animated': 'Animated by a spell: the character arrives as they were before it -- the C64 game has no such state, and the nearest thing it has is a dead creature the game runs as a monster rather than a member of the party', 'temporarily gone': 'Temporarily gone from the party: the character arrives with it -- the C64 game has no such state'}

What a player is told when the source’s status has no C64 value.

PROPOSED, not yet approved. .claude/rules/gui-text.md makes every word a player reads Donald’s; this is the working proposal, written so it can be seen running rather than only described. No file offset and no issue number, which tests/test_dosconvert.py’s two guard tests enforce for the DOS table and this one follows.

goldbox.c64_codec.OUT_OF_PLAY = 128

Bit 7 of record 0x100, and it is a flag in its own right rather than part of the status: LIBRARY $38BE masks it off with AND #$07 before drawing the word, $1BF6 skips a slot carrying it when it sums the party’s strength, and $3E4A picks the greyed colour for the party panel with CMP #$80 – and the colour it picks is 2, red, not grey. Set means the character is out of play, which is the same thing DOS says at 0x10D with the opposite polarity, and DOS draws its own name red too.

CONFIRMED, and independent of the low three bits (#235): one boot with $81 – OK with the flag set – beside $05 – unconscious with it clear – and three controls at $01 drew OK in red for the first and UNCONSIOUS in the panel’s ordinary colour for the second. Two of two red against three of three not, partitioning on bit 7 and on nothing else.

goldbox.c64_codec.READ_TARGETS: dict[str, str] = {'abilities_second': "dropped: not a field of its own in Pool of Radiance -- these seven bytes are part of that title's memorised list and are read with it (#268, A character with more than sixteen memorised spells loses the rest, because the layout gives the list sixteen bytes and the game gives it eighty-one), and they are zero in every Pool of Radiance specimen. Curse of the Azure Bonds and Secret of the Silver Blades keep a real second ability array at the same offset and `read` takes it whole whenever the record has one -- see the `abilities_second in out` guard below -- so this entry only ever fires for Pool of Radiance", 'age': 'read as neutral age', 'alignment': 'read as neutral alignment', 'armour_class': 'read as neutral armour_class', 'armour_class_base': 'read as neutral armour_class_base', 'attack_forms': 'read as neutral attack_forms', 'attack_level': 'read as neutral attack_level', 'char_class': 'read as neutral char_class', 'charisma': 'read as neutral charisma', 'class_bits': 'read as neutral class_bits', 'combat_side': 'bit 0 read as neutral hostile and bit 7 as neutral quickfight', 'constitution': 'read as neutral constitution', 'copper': 'read as neutral copper', 'dexterity': 'read as neutral dexterity', 'dual_class_level': 'see dual_class_slot: the two are one field', 'dual_class_slot': 'with dual_class_level, read as neutral former_levels -- named by the class the slot belongs to -- in a title whose shape has dual_class and only when dual_class_level is non-zero; a slot naming no class is warned about rather than guessed at', 'electrum': 'read as neutral electrum', 'exceptional_strength': 'read as neutral exceptional_strength', 'experience': 'read as neutral experience', 'flags_0b8': 'bit 7 read as neutral npc, and the whole byte read again as neutral npc_control_byte when it is set (#303)', 'gems': 'read as neutral gems', 'gold': 'read as neutral gold', 'hp_current': 'read as neutral hp_current', 'hp_lost_to_drain': 'read as neutral hp_lost_to_drain', 'hp_max': 'read as neutral hp_max', 'hp_rolled': 'read as neutral hp_rolled', 'identity_pair': 'the first byte read as neutral unnamed_0ab, in Pool of Radiance only; Curse of the Azure Bonds and Secret of the Silver Blades never draw it (#258)', 'infravision': 'read as neutral infravision', 'intelligence': 'read as neutral intelligence', 'inventory': "read as neutral inventory, from the save's item page or the record's sixteen slots", 'item_effects': 'zeroes stripped into neutral innate_effects', 'jewelry': 'read as neutral jewelry', 'level': 'read as neutral level', 'level_cleric': 'read into neutral levels, named by the class bit', 'level_fighter': 'read into neutral levels, named by the class bit', 'level_knight': 'read into neutral levels, named by the class bit', 'level_magic_user': 'read into neutral levels, named by the class bit', 'level_paladin': 'read into neutral levels, named by the class bit', 'level_ranger': 'read into neutral levels, named by the class bit', 'level_thief': 'read into neutral levels, named by the class bit', 'levels_drained': 'read as neutral levels_drained', 'missile_attack_adjustment': "derived: a cache of what dexterity is worth to hit at range. COM.PREP $1633 rebuilds it from the record's own dexterity, which is converted, at the start of every fight and before anything reads it", 'movement': 'read as neutral movement', 'name': 'read as neutral name', 'party_order': 'read as neutral party_order', 'platinum': 'read as neutral platinum', 'portrait_body': 'read as neutral portrait_body', 'portrait_head': 'read as neutral portrait_head', 'race': 'read as neutral race', 'region_220': 'dropped: the combat icon: 18 CHARPIC00 screen codes and 18 colours, a C64 character set no other port can draw. #320 (A C64 party converted to DOS arrives with no combat figure at all, because the table only runs one way) is the reverse table this needs and does not yet have', 'roster_in_use': 'the low three bits read as neutral status and bit 7 as neutral active; zero is an empty roster slot and is neither', 'roster_movement': 'read as neutral movement_current', 'roster_spell_counts': "derived: the roster block's +0x03-+0x0B: how many spells the character has memorised at each spell level. `COM.PREP $15ED` clears all nine and rebuilds them from the character's own memorised-spell list at record 0x020 at the start of every fight, and nothing else in the game ever writes them", 'roster_tail': "read as neutral roster_tail, from the roster block's +0x10-+0x18 or the record", 'save_breath': 'read as neutral save_breath', 'save_paralysis': 'read as neutral save_paralysis', 'save_petrification': 'read as neutral save_petrification', 'save_spell': 'read as neutral save_spell', 'save_wands': 'read as neutral save_wands', 'sex': 'read as neutral sex', 'silver': 'read as neutral silver', 'size_small': 'read as neutral size_small', 'spells_castable': 'nibbles unpacked into neutral spells_castable', 'spells_known': "the spellbook mask's low seven bytes, unpacked into neutral spells_known", 'spells_known_high': "the same mask's high nine bytes, 0x07F-0x087, unpacked into the same neutral spells_known. How far into them the reader goes is the title's goldbox.spells.SpellTable.spellbook_size -- 7, 13 or 16 -- so Pool of Radiance reads none of them and Curse stops at 0x084", 'spells_memorised': "zeroes stripped into neutral spells_memorised, over the span goldbox.c64_codec.memorised_span gives for the title -- 81 bytes from 0x020 in Pool of Radiance, 69 in Curse of the Azure Bonds, 74 from 0x01B in Secret of the Silver Blades -- and not the declared field's 69 (#268)", 'strength': 'read as neutral strength', 'strength_bonus_flag': "derived: the strength-adjustment gate LIBRARY's roster recompute reads at 0x0E3. Every character any port creates holds 1 there -- GEN writes it at creation and only the monsters that share the record layout read 0 -- so `write` sets it from the same rule rather than from a source value, and a C64 record read here and written back keeps it", 'strength_index': 'derived: derived from strength and the percentile; a writer that wants it recomputes it', 'thac0': 'read as neutral thac0_current', 'thac0_base': 'read as neutral thac0_base', 'thief_climb_walls': 'read as neutral thief_climb_walls', 'thief_find_traps': 'read as neutral thief_find_traps', 'thief_hear_noise': 'read as neutral thief_hear_noise', 'thief_hide_in_shadows': 'read as neutral thief_hide_in_shadows', 'thief_move_silently': 'read as neutral thief_move_silently', 'thief_open_locks': 'read as neutral thief_open_locks', 'thief_pick_pockets': 'read as neutral thief_pick_pockets', 'thief_read_languages': 'read as neutral thief_read_languages', 'turn_class': "derived: zero for every player character -- the undead's row, not the caster's", 'turn_power': 'read as neutral turn_power', 'wisdom': 'read as neutral wisdom'}

What read() does with every named field of the C64 layout – the layout-wide account the DOS writer of #26 called for, so a C64 field nothing reads cannot be dropped in silence. tests/test_doswriter.py checks it against goldbox/layout.py’s named fields.

class goldbox.c64_codec.Report[source]

Bases: goldbox.neutral.Report

A C64 conversion’s provenance: every byte has to be explained.

Not merely the non-zero ones. A zero the C64 record wants is as much a decision as a value copied into it, and docs/117-save-conversion.md makes accounting for all 580 the test that replaces a round trip.

__init__(total=580, sources=<factory>, dropped=<factory>, warnings=<factory>, has_portrait=False)
Parameters:
Return type:

None

has_portrait: bool = False

True when this character’s own sheet portrait crossed – both portrait_head and portrait_body written from a source, not left at whatever CharacterRecord.blank() starts with. Kept separate from reading the record’s own bytes back: HEAD00 is a real portrait, so a written zero and an unwritten zero are the same byte and only the report can tell them apart (#57).

summary_notes()[source]

Lines a port’s own report adds between the count and the warnings.

Return type:

list[str]

total: int = 580

How many bytes the provenance covers.

property unaccounted: list[int]

C64 offsets this conversion cannot explain. Should be empty.

goldbox.c64_codec.STATUS_BITS: dict[str, int] = {'dead': 3, 'dying': 4, 'gone': 2, 'okay': 1, 'running': 6, 'stoned': 7, 'unconscious': 5}

Neutral status name -> the low three bits of C64 record 0x100. The C64’s own enumeration, read off the routine that draws the STATUS line: LIBRARY $38BE is LDA $6C00 / AND #$07 / CLC / ADC #$29 / TAX, and the string table at LIBRARY $3439/$347B holds ids $29-$30 as HITPOINTS, OK, GONE, DEAD, DYING, UNCONSIOUS – the game’s own spelling – RUNNING, STONED. Index 0 is unreachable: zero at 0x100 means the slot is empty, which is what DROP CHARACTER writes (CAMP $0C0B), so the word the table holds there is never drawn.

CONFIRMED three ways (#235): the display arithmetic above; a character an orc took to 0 hit points in a driven fight, whose byte went $01 -> $84 and then $84 -> $85 as the fight ended, which is what the engine saved; and each of $82-$87 staged into a copy of that save and read back off the sheet as GONE, DEAD, UNCONSIOUS, RUNNING and STONED.

Two neutral names are missing from it and that is the finding, not an omission: animated and temporarily gone are DOS states with no C64 value. SPELLE04 $AA11 writes $03 beside creature type 4, undead, for Animate Dead – but $03 is DEAD with bit 7 clear, written on a thing the same routine marks as not a player character, so it is the nearest thing rather than the same thing.

goldbox.c64_codec.STATUS_BY_BITS: dict[int, str] = {1: 'okay', 2: 'gone', 3: 'dead', 4: 'dying', 5: 'unconscious', 6: 'running', 7: 'stoned'}

The same table read the other way, for read().

goldbox.c64_codec.TRANSFORMED: tuple[tuple[str, str], ...] = (('name', "re-padded into the C64's 20 NUL-padded bytes at 0x000"), ('levels', "permuted onto the C64's eight slots, which are indexed by the class bit; a class with no bit is reported"), ('spells_known', 'packed into the C64 mask at 0x078, as many bytes of it as the title uses; an id past the last the mask has a bit for is warned about'), ('spells_memorised', "into the slots the title's own C64 record has, filled from the start: 81 in Pool of Radiance and 69 in Curse of the Azure Bonds"), ('spells_castable', 'repacked cleric-high/magic-user-low into three bytes, in the titles whose C64 record has the array; reported in the ones that do not'), ('abilities_second', 'written to the second ability array in the titles that keep one, and part of the memorised list in Pool of Radiance, which does not'), ('former_levels', "the one class with a level becomes the C64's dual_class_slot and dual_class_level; two would be reported. Whether the old class has been regained is not itself a neutral field -- it is computed here, against neutral level, and written into the old class's own level slot (PROBABLE, GEN $20A3, #256 M2)"), ('size_small', "copied to the C64's size byte"), ('thac0_base', "**recomputed, not copied**: rebuilt from the class levels through this title's own table (`GEN $1EF3`), because a source's own port may have written the byte through a different one -- DOS's magic-user and thief rows disagree with the C64's at low level (#366)"), ('turn_power', "**computed, not copied**: the C64's caster turning byte at 0x0A4 is what this title's own GEN writes from the cleric and paladin levels, because no port a conversion reads keeps the value -- DOS works it out when Turn is pressed and stores nothing, and a copied zero costs a cleric the word TURN on the combat bar (#288)"), ('attack_forms', 'copied as a block to 0x0D9'), ('innate_effects', "the first ten ids, into the C64's trait slots; the rest are warned about"), ('granted_effects', "each id, into a free trait slot after the innate ones, filled from slot 9 down the way the item's own READY would fill it (#252); the rest are warned about. The record's duration, value and removal flag have no C64 counterpart and need none -- see the write itself. Not something the C64 reader can give back as this name, because a trait slot the converter filled and one READY filled are the same byte to the engine's own compare"), ('inventory', "the first sixteen items, into the C64's fixed slots; the rest are warned about"), ('roster_tail', "copied as a block into the C64's roster tail"), ('npc', 'bit 7 of 0x0B8, the byte the game itself counts player characters with; a player character gets 0x00 there whatever the source held (#303)'), ('npc_control_byte', 'written unchanged to 0x0B8 when npc is true -- bit 7 plus the low seven bits of morale, stored halved; nothing to write when npc is false (#303)'), ('status', "the name indexed into the C64's own seven-value table, into the low three bits of record 0x100; a state the C64 does not have is reported and the character arrives OK"), ('active', "bit 7 of that same byte, set when the character is out of play -- the opposite polarity to DOS's own flag"), ('hostile', "bit 0 of record 0x10C -- 0 the party's side, 1 the enemy's"), ('quickfight', 'bit 7 of the same byte, set by QUICK and never cleared'), ('portrait_head', "the art's own id, copied to 0x0FE; a character with no id gets none written and the sheet draws no face"), ('portrait_body', "the art's own id, copied to 0x0FF -- see portrait_head"), ('unnamed_0ab', "the first byte into the C64's identity_pair at 0x0E6, second byte zero; reported instead in Curse of the Azure Bonds or Secret of the Silver Blades, whose GEN never draws the pair (#258, The C64 side of 0x0AB is unnamed, so the conversion drops it with no issue behind it)"))

Neutral fields the writer takes by a rule rather than by a copy.

goldbox.c64_codec.field_disposition()[source]

Every neutral field and what write() does with it.

The neutral-vocabulary twin of goldbox.dos.field_disposition, which asks the same question of the DOS layout. Writer.finish catches a value no writer took one character at a time; this catches a name the writer has never been taught, which is the failure that rots silently – a field added to goldbox/neutral.py’s FIELDS and never wired up here.

Return type:

dict[str, str]

goldbox.c64_codec.get_memorised(rec, game=None)[source]

The whole memorised-spell list, as wide as this title reads it.

Parameters:

rec (goldbox.record.CharacterRecord)

Return type:

bytes

goldbox.c64_codec.memorised_span(game=None)[source]

(offset, size) of this title’s memorised-spell list.

Every reader of the list goes through here, and so does the writer. goldbox/layout.py declares spells_memorised as the 69 bytes at 0x020 that all three measured titles agree about, which is not any one title’s list: Pool of Radiance runs 81 bytes to 0x070, Curse of the Azure Bonds stops at 69 because 0x065 is its second ability array, and Secret of the Silver Blades starts five bytes earlier at 0x01B and runs 74. Asking the record for the declared field instead is what lost a Pool of Radiance character everything past their sixteenth memorised spell (#268).

Return type:

tuple[int, int]

goldbox.c64_codec.read(rec, roster=None, inventory=None, game=None, source=None)[source]

Read one C64 record into the neutral record.

roster is the character’s roster block, which is where a save slot keeps the four current combat numbers – a slot record stores only 256 of the 580 bytes and stops short of them. inventory is the sixteen-byte item records off the save’s item page, for the same reason. Either may be None, and then the value is read from rec when it holds it and left unset when it does not: a field nobody supplied is absent rather than zero, so a writer reports it instead of writing a plausible nothing.

game is the title whose race and class tables the record’s indices are in; it travels on the neutral record so a writer can name them.

Parameters:
Return type:

goldbox.neutral.NeutralCharacter

goldbox.c64_codec.record_shape(game=None)[source]

The record shape for a title, Pool of Radiance’s when none is given.

Duck-typed on .key the way goldbox.spells.for_game is, so a goldbox.games.Game, a key or None all work and this module still does not import goldbox/games.py.

A title with a key that names no measured row raises (#274, A C64 title nobody has measured is read with Pool of Radiance’s record shape, silently) – goldbox/c64_save.py’s container_for is the precedent: handing back Pool of Radiance’s spell span, ability layout and dual-class answer for a title nobody has read the overlays of is inventing that title’s geometry, silently, exactly the way a title with no fast-travel row is refused rather than given Pool of Radiance’s coordinates (automap/fasttravel.py). Only None – a caller with no title in hand at all, not a title that is unmeasured – still means Pool of Radiance.

Return type:

goldbox.c64_codec.RecordShape

goldbox.c64_codec.set_memorised(rec, data, game=None)[source]

Write the whole list back. data must be the title’s own width.

Parameters:
Return type:

None

goldbox.c64_codec.span_of(names)[source]

(offset, size) of a run of declared fields, refusing a gap in it.

Parameters:

names (tuple[str, ...])

Return type:

tuple[int, int]

goldbox.c64_codec.strength_index(strength, percentile)[source]

The C64’s strength_index: STR below 18, else 18 plus the band.

Equals strength below 18; 18/01-18/50 give 19 and 20, 18/80 and 18/81 give 21, 18/98 gives 22 – the AD&D exceptional-strength bands collapsed to one number. PROBABLE, and it is computed rather than copied because no source port has been found to store it.

Parameters:
  • strength (int)

  • percentile (int)

Return type:

int

goldbox.c64_codec.write(char, icon=None)[source]

Build a 580-byte C64 character record from a neutral one.

icon is the 36-byte combat icon. No port outside the C64 has one – it is a C64 character set – so with none given the field is left zero and reported.

Parameters:
Return type:

tuple[goldbox.record.CharacterRecord, goldbox.c64_codec.Report]