goldbox.yaml_io

Export a save’s party to YAML and import it back.

The YAML codec, one of four around the record goldbox/neutral.py describes and not the thing the others convert through. entry_for reads a NeutralCharacter and never a CharacterRecord, so a C64 party and a DOS one render through one code path without either being converted to the other first; import_into is the other direction and writes a C64 save.

The design goal is a lossless round-trip: exporting a save and importing it unchanged must reproduce the original file byte for byte. That is what makes the tool safe to use on a real save, and it is asserted by the tests.

Only fields we actually understand are editable. Everything else — the ~82% of each record still unidentified, the party header, and everything in SAVEDGAME1 past its first page — is carried through untouched, because an edit must never destroy bytes whose meaning we do not know.

Pool of Radiance’s save is two files, and both are written. SAVEDGAME0 holds the character records; SAVEDGAME1 opens with eight roster blocks holding the values the game derives — armour class, THAC0, current hit points, movement and the damage bonus. Those appear under combat:, together with the three bytes at +0x03+0x05 whose meaning is not established, and are the only part of SAVEDGAME1 this module touches. Curse of the Azure Bonds and the four titles after it write one file with the same roster as its last page; which shape a disk has is goldbox/games.py’s business, not this module’s.

The document records the title it came from, as game:, and an import into a different title’s disk is refused. The container geometry differs between them and the race and class tables differ again, so a silent cross-title import would write a plausible-looking corrupt save.

Two fields the game stores twice are kept in step, because writing one without the other leaves a record no save has ever been seen in: the class code at 0x073 follows the bitmask at 0x0EB, and the character level at 0x0A0 follows the per-class level array.

Items keep their raw bytes in the YAML alongside friendly values. On import the raw bytes are the starting point and the friendly fields are applied over them, so an item survives a round-trip exactly while readied and quantity remain editable. Item names are indices into the game’s own table and are exported for reference only.

Module Attributes

LEVEL_FIELDS

Pool of Radiance's four, for a caller with no Game in hand.

NAME_CHANGE_REFUSED

Donald's wording, approved verbatim (#145) -- no slot, no name, no second sentence.

Functions

class_code_for(bits[, game])

The single class code matching a class bitmask.

comments_for(game)

FIELD_COMMENTS with the race and class lists this title actually has.

entry_for(char, slot_index, items, icon[, ...])

One goldbox.neutral.NeutralCharacter as plain data.

export_save(path[, game_disk, game])

Read a save disk and return the whole party as plain data.

import_into(save_path, data, out_path[, ...])

Apply a parsed YAML document to a save disk, writing to out_path.

level_fields(game)

Class name -> the record field holding that class's level, per title.

names_to_classes(value[, game])

strip_annotations(data)

The same document without its presentation-only keys.

to_yaml(data)

Emit the document with field comments and in-game ordering.

Exceptions

ValueError_

Raised with a message a person can act on.

goldbox.yaml_io.LEVEL_FIELDS = {'cleric': 'level_cleric', 'fighter': 'level_fighter', 'magic-user': 'level_magic_user', 'thief': 'level_thief'}

Pool of Radiance’s four, for a caller with no Game in hand.

goldbox.yaml_io.NAME_CHANGE_REFUSED = 'ERROR: Name field cannot be changed.'

Donald’s wording, approved verbatim (#145) – no slot, no name, no second sentence. Kept as one constant so it only needs changing in one place.

exception goldbox.yaml_io.ValueError_[source]

Bases: ValueError

Raised with a message a person can act on.

goldbox.yaml_io.class_code_for(bits, game=None)[source]

The single class code matching a class bitmask.

Three combinations have no code in the game’s table – magic-user/cleric/ thief, cleric/thief/fighter, and all four at once. Refuse them rather than write a code that means something else.

Delegates to goldbox.classcode.code_for (#310), which is the mask-only case of the same rule goldbox.dos.write and goldbox.c64_codec.read both repair a stale code with. game picks the title’s own table – None means Pool of Radiance’s, the table this function has always used.

Parameters:
Return type:

int

goldbox.yaml_io.comments_for(game)[source]

FIELD_COMMENTS with the race and class lists this title actually has.

The lists differ per title, and a comment naming Pool of Radiance’s races on a Silver Blades export would be an instruction to write a wrong number. An unknown list says so instead of listing nothing.

Parameters:

game (goldbox.games.Game | None)

Return type:

dict[str, str]

goldbox.yaml_io.entry_for(char, slot_index, items, icon, game=None, names=None, types=None, spell_names=None, block=None)[source]

One goldbox.neutral.NeutralCharacter as plain data.

The YAML writer: a codec beside the C64, DOS and Amiga ones rather than the thing they convert through. It reads neutral field names and never a CharacterRecord, so a C64 party and a DOS one render through one code path without either of them being converted to the other first.

items, icon and block come in beside the character because they are not the record: a save keeps items on their own page, icons in a table of their own, and the derived combat numbers in the roster.

Parameters:
Return type:

dict[str, Any]

goldbox.yaml_io.export_save(path, game_disk=None, game=None)[source]

Read a save disk and return the whole party as plain data.

The title is identified from the disk unless one is named.

Parameters:
Return type:

dict[str, Any]

goldbox.yaml_io.import_into(save_path, data, out_path, game_disk=None, game=None)[source]

Apply a parsed YAML document to a save disk, writing to out_path.

Returns a human-readable list of the changes made. The input file is never modified; the caller chooses the destination.

Parameters:
Return type:

list[str]

goldbox.yaml_io.level_fields(game)[source]

Class name -> the record field holding that class’s level, per title.

One entry per class the title has, because the level array has one slot per class bit. A class we can name but have no field for is left out rather than pointed at the wrong byte.

Parameters:

game (goldbox.games.Game | None)

Return type:

dict[str, str]

goldbox.yaml_io.names_to_classes(value, game=None)[source]
Parameters:

game (goldbox.games.Game | None)

Return type:

int

goldbox.yaml_io.strip_annotations(data)[source]

The same document without its presentation-only keys.

Keys beginning with _ carry text we render as a YAML comment rather than as data – currently just the item-type summary. They are derived from the game disk, not from the save, so they must not survive a round-trip.

Parameters:

data (dict[str, Any])

Return type:

dict[str, Any]

goldbox.yaml_io.to_yaml(data)[source]

Emit the document with field comments and in-game ordering.

Hand-rolled because PyYAML cannot write comments. yaml.safe_load reads it back identically – asserted in the tests – and every scalar is rendered by PyYAML itself, so quoting and escaping stay correct.

Parameters:

data (dict[str, Any])

Return type:

str