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
Neutral field -> the C64 field it becomes, where the value crosses unchanged. |
|
|
81 memorised slots, `0x020`-`0x070`, and no second ability array, no dual-class pair. |
|
69 memorised slots, `0x020`-`0x064`, a second ability array at |
|
74 memorised slots, `0x01B`-`0x064` -- the only title measured whose list does not start at |
Class name -> the C64 field holding that class's level. |
|
Race name -> the stored |
|
|
How many item slots the C64 record has. |
|
What a player reads when a title's C64 record has no home for something the source held. |
|
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 |
Neutral status name -> the low three bits of C64 record |
|
The same table read the other way, for |
|
Bit 7 of record |
|
What a player is told when the source's status has no C64 value. |
|
Neutral fields the writer takes by a rule rather than by a copy. |
|
Neutral fields the C64 writer takes nothing from, and why. |
|
|
A save slot holds 256 of the record's 580 bytes; the roster block and the item page hold the rest. |
|
What a player reads for each name in |
|
C64 fields the reader leaves behind because the value is recomputed rather than genuinely lost -- the read side's twin of `goldbox.dos. |
What |
Functions
Every neutral field and what |
|
|
The whole memorised-spell list, as wide as this title reads it. |
|
|
|
Read one C64 record into the neutral record. |
|
The record shape for a title, Pool of Radiance's when none is given. |
|
Write the whole list back. |
|
|
|
The C64's |
|
Build a 580-byte C64 character record from a neutral one. |
Classes
|
The parts of the 580-byte record the titles do not agree about. |
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:
thac0androster_movementare 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.finishfor 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
0x0D5byte (tens of feet). The C64 stores this at0x0D5and 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 byLDY #$D5 / TAX / LDA $0E5C,X / STA $6B00,Yat$094F) andCURSE_A.D64:GEN($0C4B, read at$0C11, written to$7CD5at$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 tablereads 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’
GENwrites 0 into this byte at$2278, the only write to it anywhere in that overlay – but as oneSTAinside a loop that zeroes a wider range of the record ($7CD3-$7CD5) from anAset 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 throughgoldbox.games.race_tablefor 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.mdmakes 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, whichtests/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 $38BEmasks it off withAND #$07before drawing the word,$1BF6skips a slot carrying it when it sums the party’s strength, and$3E4Apicks the greyed colour for the party panel withCMP #$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 at0x10Dwith 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$01drew 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.pychecks it againstgoldbox/layout.py’s named fields.
- class goldbox.c64_codec.Report[source]¶
Bases:
goldbox.neutral.ReportA 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.mdmakes accounting for all 580 the test that replaces a round trip.- __init__(total=580, sources=<factory>, dropped=<factory>, warnings=<factory>, has_portrait=False)¶
- has_portrait: bool = False¶
True when this character’s own sheet portrait crossed – both
portrait_headandportrait_bodywritten from a source, not left at whateverCharacterRecord.blank()starts with. Kept separate from reading the record’s own bytes back:HEAD00is a real portrait, so a written zero and an unwritten zero are the same byte and only the report can tell them apart (#57).
- 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 $38BEisLDA $6C00 / AND #$07 / CLC / ADC #$29 / TAX, and the string table atLIBRARY $3439/$347Bholds ids$29-$30as HITPOINTS, OK, GONE, DEAD, DYING, UNCONSIOUS – the game’s own spelling – RUNNING, STONED. Index 0 is unreachable: zero at0x100means 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->$84and then$84->$85as the fight ended, which is what the engine saved; and each of$82-$87staged 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:
animatedandtemporarily goneare DOS states with no C64 value.SPELLE04 $AA11writes$03beside creature type 4, undead, for Animate Dead – but$03is 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.finishcatches 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 togoldbox/neutral.py’sFIELDSand never wired up here.
- goldbox.c64_codec.get_memorised(rec, game=None)[source]¶
The whole memorised-spell list, as wide as this title reads it.
- Parameters:
- Return type:
- 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.pydeclaresspells_memorisedas the 69 bytes at0x020that all three measured titles agree about, which is not any one title’s list: Pool of Radiance runs 81 bytes to0x070, Curse of the Azure Bonds stops at 69 because0x065is its second ability array, and Secret of the Silver Blades starts five bytes earlier at0x01Band 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).
- goldbox.c64_codec.read(rec, roster=None, inventory=None, game=None, source=None)[source]¶
Read one C64 record into the neutral record.
rosteris 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.inventoryis 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 fromrecwhen 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.gameis 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:
source (str | None)
- Return type:
- 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
.keythe waygoldbox.spells.for_gameis, so agoldbox.games.Game, a key or None all work and this module still does not importgoldbox/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’scontainer_foris 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). OnlyNone– 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.
datamust be the title’s own width.- Parameters:
data (bytes)
- Return type:
None
- goldbox.c64_codec.span_of(names)[source]¶
(offset, size)of a run of declared fields, refusing a gap in it.
- 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.
- goldbox.c64_codec.write(char, icon=None)[source]¶
Build a 580-byte C64 character record from a neutral one.
iconis 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:
icon (bytes | None)
- Return type:
tuple[goldbox.record.CharacterRecord, goldbox.c64_codec.Report]