goldbox.neutral¶
One character, in no port’s terms, and the report every codec fills in.
Three formats in two directions is six converters; three codecs around one neutral record is three readers and three writers, and a fourth format then costs two pieces rather than four. This module is the middle.
DOS file –reader–> NeutralCharacter –writer–> C64 record
A reader decodes one port’s bytes into named neutral values. A writer encodes those values into another port’s bytes. Neither knows the other exists: the DOS reader names DOS offsets, the C64 writer names C64 fields, and what passes between them is this record.
What a neutral value carries¶
Not just a number. Each Value carries
the decoded value, in the neutral convention the vocabulary below states;
a confidence, taken from the grade the source port’s field table gives the field it was read from. A writer asks for what it is willing to stand behind (
NeutralCharacter.take()) and gets nothing rather than a guess – refusing to write is the point of the grades, not a decoration on them;an origin, the reader’s one-line phrase for where the value came from, which is what the writer’s provenance report quotes.
Who says what¶
The split that keeps a codec honest is: a reader says where a value came
from, a writer says where it went and what it could not take. So the DOS
reader’s origin for the spellbook is “DOS spellbook @0x033, one byte per
spell” and the C64 writer’s line is that phrase plus “packed to one bit” –
its own packing rule, not the reader’s. port names the source port so a
writer can say whose value it is turning away without knowing anything else
about it.
Reporting¶
Report is the one shape every direction reports in: a provenance for
every output byte, the fields left behind, and the conversions that changed
something. disposition() builds the table that makes a silent drop
impossible – a field the source declares and the codec names nowhere.
Module Attributes
every field a codec may set, and what it means. |
|
The seven abilities in the order every port stores them, which is also the order |
|
a codec handed a name outside this tuple has a bug, and a codec that cannot spell one of them in its own port reports the loss rather than choosing the nearest thing. |
Functions
|
Every field a codec knows about and what it does with it. |
|
|
Classes
One character as named neutral values, with a report line for each. |
|
How a value's report line reads, which is also what kind of value it is. |
|
Where each byte of the output came from, and what was left behind. |
|
One field of the neutral record: the value, where it came from, how far it is trusted, and the shape of the sentence that will report it. |
|
The take-refuse-report protocol every writer shares. |
Exceptions
A field name the neutral vocabulary does not declare. |
- goldbox.neutral.ABILITIES: tuple[str, ...] = ('strength', 'intelligence', 'wisdom', 'dexterity', 'constitution', 'charisma', 'exceptional_strength')¶
The seven abilities in the order every port stores them, which is also the order
abilities_secondis keyed in. One tuple, because three modules need the same order and a permutation between two of them would be silent.
- goldbox.neutral.FIELDS: dict[str, str] = {'abilities_second': 'ability name -> the second of the two copies of that score the later titles keep. Curse of the Azure Bonds and every title after it store each ability twice, on both ports, and the two copies are equal in every record measured -- 406 DOS pairs and six C64 records. **Which copy the engine treats as current is not established**, so a codec carries both rather than choosing, and a codec whose port keeps one copy takes nothing from this', 'active': "false for a character the game has taken out of the party and marks as such. Both ports draw the name **red** in the party panel for it, and the C64 also leaves the character out of the party's own strength; they hold the flag at opposite polarities and each codec converts. True is the ordinary state", 'age': 'age in years', 'alignment': 'law * 3 + morality', 'armour_class': 'armour class as the game last computed it, stored 60 - value (63 is AC -3)', 'armour_class_base': 'armour class with nothing worn, stored 60 - value', 'attack_forms': 'the eight attack-form bytes', 'attack_level': 'the level the attack tables are read at', 'char_class': 'the single class code, in the shared 18-entry order', 'charisma': 'CHA', 'class_bits': 'one bit per class held, in the shared bit order', 'constitution': 'CON', 'copper': 'copper pieces', 'dexterity': 'DEX', 'electrum': 'electrum pieces', 'encumbrance': 'total weight carried, in tenths of a pound', 'exceptional_strength': 'the 18/xx percentile, 0 when there is none', 'experience': 'experience points', 'former_levels': "class name -> the level a dual-classed character left that class at. Empty for everybody else. AD&D 1st edition lets a human leave one class for another once, keeping the old class's level and using neither its hit dice nor its abilities again, and a title that allows it keeps the old level so the character can still be that class in the parts of the rules that ask", 'gems': 'gems, counted not valued', 'gold': 'gold pieces', 'granted_effects': 'whole effect records that never expire and are not innate -- what a readied ring, girdle or cloak granted. Nine bytes each, in the shared shape `goldbox/dos.py` reads: the effect id, a little-endian duration of zero, the value the effect carries, the flag the engine reads when the item comes off, and a next pointer left NULL because the engine rebuilds the chain on load', 'hostile': "true for a combatant on the enemy's side. Never true for a character the engine saved into a party: both ports write 0 for every player character, and it is here because both ports store it beside the character and a conversion copies a field rather than assuming it", 'hp_current': 'hit points now', 'hp_lost_to_drain': 'hit points lost with those levels', 'hp_max': 'maximum hit points', 'hp_rolled': 'hit points rolled, before the constitution bonus', 'infravision': 'infravision range in feet; a property of the race, which some ports store and others derive', 'innate_effects': 'effect ids that are properties of the character rather than spells running on it', 'intelligence': 'INT', 'inventory': 'the items carried, each in the shared sixteen-byte item shape `goldbox/items.py` reads', 'jewelry': 'pieces of jewelry, counted not valued', 'level': 'the character level the game itself keeps', 'levels': "class name -> that class's level, in the source's slot order", 'levels_drained': 'levels lost to undead and not yet restored', 'movement': 'movement rate unencumbered', 'movement_current': 'movement rate as the game last computed it', 'name': "the character's name, plain text", 'npc': 'true for a companion the party picked up rather than one the player made', 'npc_control_byte': "the full control byte both the C64 and DOS keep for a companion -- C64 record 0x0B8, DOS's field_83_87 second byte in Pool of Radiance and Curse of the Azure Bonds, first in Secret of the Silver Blades. Bit 7 is always set (npc is true); the low seven bits are morale, stored halved. Neither port decodes it -- a converter copies it unchanged -- and it is set only when npc is true; a player character has no morale to carry (#303)", 'party_order': 'position in the marching order', 'platinum': 'platinum pieces', 'portrait_body': 'the body half of the same pair, one of twelve', 'portrait_head': "which of the fourteen creation-menu heads the character sheet draws, as the art's own id -- the C64 record's spelling; the DOS codec converts its menu position to and from it", 'quickfight': "true once the player has chosen QUICK for the character; neither port ever clears it, so it is the state the character's next fight starts in", 'race': 'race index, in the shared Gold Box order', 'roster_tail': 'the derived combat block the roster keeps beside the record', 'save_breath': 'save vs breath weapon', 'save_paralysis': 'save vs paralysis, poison and death magic', 'save_petrification': 'save vs petrification and polymorph', 'save_spell': 'save vs spell', 'save_wands': 'save vs rod, staff and wand', 'sex': '0 male, 1 female', 'silver': 'silver pieces', 'size_small': '0 small, 1 large', 'spells_castable': "class name -> slots free per spell level, ascending. How many levels and how many classes is the source title's: Pool of Radiance keeps three levels for the cleric and the magic-user, Curse of the Azure Bonds five for those two and the druid, Secret of the Silver Blades seven", 'spells_known': 'spell ids in the spellbook, ascending', 'spells_memorised': 'spell ids memorised, highest first', 'status': 'the state the game puts into words on the character sheet, as one of the names in STATUS_NAMES. A name and not a number because the ports number these differently and DOS holds two the C64 has no value for at all', 'strength': 'STR, 3-18 (25 for a monster)', 'thac0_base': "the class-and-level THAC0, before anything carried, in the family's stored 60 - value encoding", 'thac0_current': 'THAC0 as the game last computed it, stored 60 - value', 'thief_climb_walls': 'climb walls, per cent', 'thief_find_traps': 'find and remove traps, per cent', 'thief_hear_noise': 'hear noise, per cent', 'thief_hide_in_shadows': 'hide in shadows, per cent', 'thief_move_silently': 'move silently, per cent', 'thief_open_locks': 'open locks, per cent', 'thief_pick_pockets': 'pick pockets, per cent', 'thief_read_languages': "read languages, per cent (a halfling's is negative)", 'turn_power': "the cleric's turning strength", 'unnamed_0ab': "the identity draw the engine makes at creation and uses for exactly one thing -- telling two characters of the same name apart when one is added to a party. DOS keeps one byte of it at 0x0AB; Pool of Radiance's C64 GEN draws two at 0x0E6-0x0E7 and never rewrites them; an Amiga Pool of Radiance record holds its own copy at the same offset DOS does, re-cut straight across by `goldbox.amiga.to_dos_record` (#378, An Amiga character converted to DOS loses the identity byte his own record has always held). None of the three ports ever reads it back once written, so a writer with no source for it is free to derive one instead, which is what a DOS destination with no supplied value does (#216, Every converted DOS character carries the same identity byte at 0x0AB) -- and what it still does for a pure DOS-to-DOS conversion even though DOS holds the byte too, since `goldbox.dos.IDENTITY_HELD_PORTS` trusts every port but DOS's own (#378). Curse of the Azure Bonds and Secret of the Silver Blades never draw the C64 pair at all, so a source from either has none to give (#258, The C64 side of 0x0AB is unnamed, so the conversion drops it with no issue behind it)", 'wisdom': 'WIS'}¶
every field a codec may set, and what it means.
A name here is the thing, not any port’s storage of it, and a reader that invents a name outside this table is refused – a typo would otherwise be a field silently unread by every writer. Where a value needs a convention (an order, a unit, an encoding) the entry states it, and that convention is the neutral one: a port whose own encoding differs converts on the way in.
- Type:
The neutral vocabulary
- class goldbox.neutral.NeutralCharacter[source]¶
Bases:
objectOne character as named neutral values, with a report line for each.
portnames the port a reader took this from – “DOS”, “C64”, “Amiga” – so a writer can say whose value it is turning away.droppedis what the reader itself could not carry: fields of the source with no neutral home, named on the way past rather than lost.gameis the title whose tables the port-relative indices were read in –race,char_classandclass_bitsare numbers into a table that is not the same in every Gold Box game, so a writer that wants a name asksgoldbox/games.pywith this in hand. None means Pool of Radiance’s, which is what a caller with no title in hand means.- fields: dict[str, goldbox.neutral.Value]¶
- set(name, value, origin, confidence=Confidence.CONFIRMED, how=Provenance.COPIED, dropped=())[source]¶
- Parameters:
name (str)
value (Any)
origin (str)
confidence (goldbox.layout.Confidence)
- Return type:
None
- take(name, floor=Confidence.GUESS)[source]¶
The value, or None when the reader trusts it less than
floor.This is how a codec refuses to write what it does not understand: it asks for a field at the grade it is willing to stand behind, and a field graded below that comes back as nothing to write and something to report, never as a plausible-looking guess.
- Parameters:
name (str)
floor (goldbox.layout.Confidence)
- Return type:
goldbox.neutral.Value | None
- unwritten(taken)[source]¶
Neutral fields a writer did not consume, in the order they were set.
The other half of
disposition(): that one catches a source field no codec names, this one catches a neutral value no writer took.
- exception goldbox.neutral.NeutralError[source]¶
Bases:
KeyErrorA field name the neutral vocabulary does not declare.
- class goldbox.neutral.Provenance[source]¶
Bases:
enum.EnumHow a value’s report line reads, which is also what kind of value it is.
The separator is the whole of it, and the three of them are the three honest sentence shapes a provenance line has: this byte came from somewhere, this byte is something, or this byte is the same value in the destination’s own shape.
- COMPUTED = ': '¶
Derived by a rule; the source port does not store it at all.
infravision: computed from race ...
- COPIED = ' <- '¶
Taken from a named field of the source port.
thac0 <- DOS ...
- RESHAPED = ', '¶
The same value, re-cut to the destination’s shape.
name, re-padded from ...
- class goldbox.neutral.Report[source]¶
Bases:
objectWhere each byte of the output came from, and what was left behind.
docs/117-save-conversion.mdmakes this the test that replaces a round trip: for any offset in the output, say where that byte came from. Each port’s codec subclasses this to say how much of its output has to be explained – every byte, or only the non-zero ones.- __init__(total=0, sources=<factory>, dropped=<factory>, warnings=<factory>)¶
- goldbox.neutral.STATUS_NAMES: tuple[str, ...] = ('okay', 'animated', 'temporarily gone', 'running', 'unconscious', 'dying', 'dead', 'stoned', 'gone')¶
a codec handed a name outside this tuple has a bug, and a codec that cannot spell one of them in its own port reports the loss rather than choosing the nearest thing.
The order is DOS’s own numbering – nine length-prefixed words in
START.EXEfrom file offset 0xD191, which is the enumeration DOS record 0x10C indexes – so the DOS reader may use the index and no other codec may. The C64 numbers seven of these differently in the low three bits of record 0x100 and has no value at all foranimatedortemporarily gone;goldbox/c64_codec.py’sSTATUS_BITSis that port’s own table.- Type:
Every value the neutral
statusfield may hold, and the whole of it
- class goldbox.neutral.Value[source]¶
Bases:
objectOne field of the neutral record: the value, where it came from, how far it is trusted, and the shape of the sentence that will report it.
- __init__(value, origin, confidence=Confidence.CONFIRMED, how=Provenance.COPIED, dropped=())¶
- Parameters:
value (Any)
origin (str)
confidence (goldbox.layout.Confidence)
- Return type:
None
- confidence: goldbox.layout.Confidence = 'CONFIRMED'¶
- dropped: tuple[str, ...] = ()¶
What the reader had to leave behind to produce this value – the running spells left over when the innate ones were picked out, say. A writer emits them where it consumes the field, so a drop is reported beside the thing it was dropped from.
- how: goldbox.neutral.Provenance = ' <- '¶
- line(destination, extra='')[source]¶
The provenance line for a destination field this value fed.
extrais the writer’s own half – the rule it applied on the way in, which the reader cannot know.
- value: Any¶
- class goldbox.neutral.Writer[source]¶
Bases:
objectThe take-refuse-report protocol every writer shares.
Hoisted from
goldbox/c64_codec.write, whereuseandemitwere closures a second writer would have copied by hand – which is exactly whatgoldbox/amiga.pydid, against a different middle, and the mistake this class exists to end. A writer constructs one around the character and its own report and gets four things it would otherwise re-implement:use()– take a field at the floor, and turn a refusal into a report line rather than silence. A refused value’s owndroppedlist still reaches the report: what a reader had to leave behind to produce a value is a fact about the source whether or not the value is written.emit()– the provenance note for the bytes a value became.get()– a plain value for a derivation, at the same floor.NeutralCharacter.getdoes not apply one, and a writer that computes a byte from a field it would have refused to copy is standing behind the value twice as hard, not half as hard.finish()– the closing sweep: neutral fields this writer took nothing from, then the reader’s own drops and warnings.
droppedis the codec’s own(name, why)table of fields it takes nothing from, so that the sweep reports this conversion’s reason for leaving a field behind rather than a generic sentence. It reports what the character actually carries, which is why the whole-contract statement lives in the codec’sfield_disposition()and is tested there instead.- __init__(char, report, into, floor=Confidence.GUESS, dropped=())[source]¶
- Parameters:
report (goldbox.neutral.Report)
into (str)
floor (goldbox.layout.Confidence)
- Return type:
None
- emit(v, destination, offset, size, extra='')[source]¶
- get(name, default=None)[source]¶
A bare value for a rule to compute from, floor applied.
Does not count as taking the field: a writer that derives one byte from
raceand also copiesracereports the copy, and a writer that only derives must stillusethe field once if it wants the field counted as consumed.
- use(name)[source]¶
The value, if the reader stands behind it at the floor.
A field graded below the floor comes back as nothing to write and something to report, never as a plausible-looking guess.
- Parameters:
name (str)
- Return type:
goldbox.neutral.Value | None
- goldbox.neutral.disposition(direct, transformed, dropped, into, derived=(), constants=())[source]¶
Every field a codec knows about and what it does with it.
The tables are the codec’s whole account of itself, and the test that keeps it honest is
undeclared(): a field the source declares and none of them names would be a field dropped in silence, whichdocs/117-save-conversion.mdforbids.derivedandconstantsare the two a destination does not need to be given – a field it recomputes on load, and one that holds the same value in every record anybody has read. They report asderived:andconstant:rather thandropped:, because a player is told about a loss and neither is one (#324 (The import pane tells a player nine fields could not be converted that the C64 recomputes for itself)). They are optional so a codec that has measured none still calls the one builder rather than growing a second.