goldbox.portraits¶
The character sheet’s portrait, and the one enumeration both ports share.
A Pool of Radiance character is drawn on the sheet as a head over a body, chosen at creation from a menu of fourteen heads and twelve bodies. Both ports store that choice in the character record, and they store it differently:
the C64 keeps the art’s own id –
portrait_headat0x0FEis the two hex digits of a filename, so$2Dis the fileHEAD2D(docs/140-loaded- files-cache.md, loader slots 13 and 14);DOS keeps the menu position, one-based –
portrait_headat0x0BBis 1 to 14 andportrait_bodyat0x0BCis 1 to 12.
The art itself is the same art with the same numbering. DOS packs it into one
.DAX container per disk instead of one file per portrait, and the block ids
inside HEAD<n>.DAX are exactly the ids of the HEAD* files on the C64’s
POOL<n>.D64 – 41 head ids and 21 body ids, equal as sets and equal disk by
disk, all sixteen containers (docs/117-save-conversion.md, “The portrait”).
So the two ports are joined by the menu: DOS position n is C64 art id
`heads[n - 1]`, and the table of ids is in the player’s own files on both
sides. This module can read it off either – tables_from_dos,
tables_from_c64, tables_from_disks – and also carries the twenty-six
numbers themselves, POOL_OF_RADIANCE_MENU, read off both ports once and
typed out, so a conversion needs no disk in reach to give a character his
own face. stored_tables is the fallback; the block above it says why a
handful of integers with their provenance is a measurement and not a copy
of a game file.
Module Attributes
The menu is fourteen heads and twelve bodies in both ports' binaries. |
|
The game keys whose C64 character sheet draws a portrait. |
|
the fourteen head ids and the twelve body ids in menu order, read out of the game's own binaries once and written down here as numbers, so a conversion needs no disk in reach to give a character his own face. |
|
The stored menu per game key. |
Functions
|
Every head and body id the DOS game directory's containers hold. |
|
Whether this title's C64 character sheet draws a portrait at all. |
|
The creation menu this module carries for |
|
The same tables out of the C64 |
|
The menu tables off the player's C64 game sides, whichever holds |
|
The menu tables out of the DOS game directory's own executable. |
Classes
The fourteen heads and twelve bodies the creation menu offers. |
Exceptions
The portrait tables could not be read out of the files given. |
- goldbox.portraits.HEAD_COUNT = 14¶
The menu is fourteen heads and twelve bodies in both ports’ binaries.
- goldbox.portraits.POOL_OF_RADIANCE_KEY = 'pool-of-radiance'¶
The game keys whose C64 character sheet draws a portrait.
Only Pool of Radiance’s does, and that is a fact about the engine rather than about the art on a player’s disks: its
LIBRARY $48A4readsportrait_headandportrait_bodyout of the live record, asks the loader for theHEAD<xx>andBODY<xx>files they name, and draws them withANIMATE00’s$8406and$8409entries at screen$CC44. Curse of the Azure Bonds’ and Secret of the Silver Blades’LIBRARYnever call the loader at all, and no file on any of their twelve sides calls either of those twoANIMATEentries – 558 and 571 files searched (tools/portraitdraw.py, which prints the census).Watched as well as read: a Curse sheet and a Silver Blades sheet with
$41– art that exists onCURSE_B– written into both record bytes draw exactly as they do with the pair at zero, and loader slots 13 and 14 stay$FF, so no art was even asked for. The runs are indocs/188-the-sheet-portrait-per-title.mdand the screenshots on#300.Kept here rather than in the conversion so both directions read one fact:
goldbox.dosdecides today withshape is POOL_OF_RADIANCEin the C64-to-DOS direction and does not decide at all in the other, which is why a Curse import reports a portrait it never could have written. Spelled out rather than imported fromgoldbox.games:goldbox.traitsdoes the same and says why – this package duck-types on.keyto keep the import graph acyclic.
- goldbox.portraits.POOL_OF_RADIANCE_MENU = PortraitTables(heads=(0, 8, 9, 13, 16, 18, 22, 34, 45, 51, 53, 57, 67, 68), bodies=(1, 2, 3, 4, 7, 8, 18, 24, 26, 33, 35, 37), source='the stored Pool of Radiance creation menu (goldbox/portraits.py)')¶
the fourteen head ids and the twelve body ids in menu order, read out of the game’s own binaries once and written down here as numbers, so a conversion needs no disk in reach to give a character his own face. Donald, 2026-09-06: “Can we not just store all these art id tables ourselves? … It’s just a handful of numbers, right? Just pull them from each title so you can cross reference them. Then you don’t need the disks at all.” And, asked whether that crossed
AGENTS.md’s line: “We don’t need to refuse game disks. Just store the IDs we would otherwise be looking up. They are 40 years old and they are not going to change.” And, the same day: “A table of 26 numbers doesn’t break any rules. It’s not art, it’s just two dozen numbers.” (.claude/rules/conversions.md, “A small table of numbers read out of the game is a measurement”.)Why this is a measurement and not a copy of a data file.
AGENTS.mdbans the game’s data files – a map, a table, a script, a record – as committed bytes, because a slice of a game file under a new name is the file. What is below is not a slice of anything: it is twenty-six integers thattables_from_c64()found inGENandtables_from_dos()found inSTART.EXE, by the shape of the run and by checking every value against the art beside it, and then typed out – the same class of thing as the field offsets ingoldbox/layout.py, the$49FFswitch below, and every address cited indocs/. It describes where the menu’s fourteenth head lives; it does not carry the head. No byte ofGEN, ofSTART.EXEor of anyHEAD<xx>file is here, and nothing here would let anybody rebuild one.Where the numbers came from, so anybody can re-derive them.
tools/portraitmenu.pyreads both binaries and prints this block;tools/portraitmenu.py --checksays whether the disks on the machine still agree with it. Read 2026-09-06 off:the C64’s
POOL3.D64:GEN, run at file offset 2877, bodies first – the one Pool of Radiance rip on this machine, present at two paths with the same SHA-256 (51b6fac7...);DOS’s
START.EXE, run at file offset 58297, heads first – the Forgotten Realms: The Archives release, and the same bytes again in Donald’s play directory.
The two ports agree byte for byte (
tests/test_portraits.py::test_the_menu_found_from_the_disks_is_the_one_dos_offers), andtest_the_stored_menu_is_what_the_disks_carrypins this block to what the disks say wherever the disks are present, so the two cannot drift apart in silence. One release of each port has been read. A release whoseGENorders the menu differently would make this table wrong for its owner; none is known, and the check above is how one would be found.Only Pool of Radiance needs one. Curse of the Azure Bonds’ and Secret of the Silver Blades’ C64 sheets draw no portrait for any character (
SHEET_PORTRAIT_TITLES,#300), neither title’sGENorSTART.EXEcarries a fourteen-and-twelve run –tables_from_disksandtables_from_dosboth raise on both, measured 2026-09-06 on every copy on this machine – so there is nothing to store and nothing a stored table would give a player.- Type:
Pool of Radiance’s creation menu
- exception goldbox.portraits.PortraitError[source]¶
Bases:
ValueErrorThe portrait tables could not be read out of the files given.
- class goldbox.portraits.PortraitTables[source]¶
Bases:
objectThe fourteen heads and twelve bodies the creation menu offers.
headsandbodiesare art ids in menu order, so the index into either is the DOS record’s value less one, and the value at that index is the C64 record’s byte.- __init__(heads, bodies, source)¶
- agrees_with(other)[source]¶
Whether another port’s tables are the same menu in the same order.
- Parameters:
other (goldbox.portraits.PortraitTables)
- Return type:
- goldbox.portraits.STORED_MENUS: dict[str, goldbox.portraits.PortraitTables] = {'pool-of-radiance': PortraitTables(heads=(0, 8, 9, 13, 16, 18, 22, 34, 45, 51, 53, 57, 67, 68), bodies=(1, 2, 3, 4, 7, 8, 18, 24, 26, 33, 35, 37), source='the stored Pool of Radiance creation menu (goldbox/portraits.py)')}¶
The stored menu per game key. A title with no entry has no sheet portrait to convert, and
stored_tables()answersNonefor it.
- goldbox.portraits.dos_art_ids(game)[source]¶
Every head and body id the DOS game directory’s containers hold.
- goldbox.portraits.draws_sheet_portrait(game=None)[source]¶
Whether this title’s C64 character sheet draws a portrait at all.
gameis agoldbox.games.Game, anything else carrying akey, or the key itself. A title this answers False for has nothing to convert and nothing to report: a character arriving there without a face is a character arriving correct, because the engine draws none for any character, including one it made itself.`None` means Pool of Radiance, which is the answer every other resolver in this package gives –
goldbox.spells.for_game,goldbox.levels.for_game,goldbox.traits.for_game,goldbox.c64_save.container_forandgoldbox.c64_codec.record_shapeall resolve it that way, because every caller that passes no title predates the second game and means the first. Answering False forNonehere would have been the one predicate in the family that disagreed, and it would disagree in the direction that silently drops a portrait Pool of Radiance really does draw.- Return type:
- goldbox.portraits.stored_tables(game=None)[source]¶
The creation menu this module carries for
game, orNone.gameis agoldbox.games.Game, anything else carrying akey, or the key itself, andNonemeans Pool of Radiance for the same reasondraws_sheet_portrait()says it does. This is what a conversion falls back to when nobody handed it tables read off the player’s own disks:goldbox.dos.to_neutralasks here before it gives up on the portrait, so a DOS Pool of Radiance party converts with every face its own whether or not aPOOL<n>.D64is anywhere in reach. Reading the player’s disks (tables_from_disks()) is still there for anybody who wants the table off their own copy.- Return type:
- goldbox.portraits.tables_from_c64(disk)[source]¶
The same tables out of the C64
GENoverlay, for a cross-check.GENis onPOOL3, the character-creation side, beside theHEAD*andBODY*files its two tables name. The order is the other way round there – bodies first – which is the only difference between the ports.This checks a found table against the one disk given, which is right for
POOL3–GENand the art it names are both there. A title that keeps them on different sides needstables_from_disks, which checks against every side of the title instead of just this one.- Parameters:
disk (str | pathlib.Path)
- Return type:
- goldbox.portraits.tables_from_disks(disks)[source]¶
The menu tables off the player’s C64 game sides, whichever holds
GEN.The import direction – a DOS save becoming a
.d64– needs the tables to turn the DOS record’s menu position into the art id the C64 record stores, and the thing it has in its hand is the directory this title’s sides live in – the onegoldbox.dos.write_dos_savealready reads the combat icon andANIMATE00out of.tables_from_c64wants the one side that carriesGEN; this finds it, for whichever of the three importable titles the directory turns out to hold (#300).Every side of the matching title is tried and the first that answers wins, rather than hardcoding
POOL3or aPOOL<n>.D64name: which side carriesGENis the game’s business, and Curse keepsGENon one side and itsHEAD*/BODY*art on the others. So the ids a found table is checked against are pooled from every side of the title, not only the oneGENhappened to be on – a table found is still a table naming portraits that exist somewhere on the player’s own disks.- Parameters:
disks (str | pathlib.Path)
- Return type:
- goldbox.portraits.tables_from_dos(game)[source]¶
The menu tables out of the DOS game directory’s own executable.
gameis the directory holdingSTART.EXEand theHEAD<n>.DAXfiles – the onegoldbox.dos.write_dos_savealready takes so the party’s own area script can be staged.- Parameters:
game (str | pathlib.Path)
- Return type: