goldbox.commissions

The City Council’s books: the reward ledger, the offer board, the summons.

Everything here reads the 224 persistent flag bytes at $4A20-$4AFF, which live at offset $0100 in SAVEDGAME0 (load address $4900) and are the same bytes in a running machine. No transport and no Qt, so this works from a save file, from a live read, or from a test’s own array.

Two structures in ECL08 (the Phlan City Hall, disk 3) carry the whole state:

$4AA6 + i, i = 0..25 the reward ledger. The area script writes 254 when

the job is done; the clerk pays and writes 255. 0 is untouched, and four entries also keep a progress marker between 1 and 253 in the same byte.

$4AC1 commissions completed – bumped by the clerk for the

ten jobs that count as major.

The index is the quest: each has the clerk’s own speech in the 26-entry ONGOSUB [$6E79], 26 table at ECL08 $9D55, which is where the names below come from. The clerk pays on exactly 254 ($9D1C COMPARE [$6E7A], 254), so any other value sits there untouched by her.

Only four entries ever hold a value between 1 and 253; the other twenty-two are 0, 254 or 255 and nothing else. MARKERS below carries those four, each read off the script that writes it. A ledger byte the clerk will not pay for is a real state, not a decoding error.

255 does not always mean the party was paid. Two entries are closed at 255 without a reward: index 3 when the party fled the Buccaneer’s Base and left the boy behind (ECL08 $9EAD, beside “YOUR BUNGLING THE BIVANT RESCUE HAS COST US”), and index 18 when the party broke the seal on Cadorna’s iron box, which ECL08 $9AAF closes on entry to the City Hall while Cadorna confronts them.

Index 22 is dead. No instruction in any of the thirty scripts reads or writes $4ABC, and its handler is a bare RETURN — but its row in the clerk’s four payout tables at ECL08 $B5E0/$B5F7/$B60E/$B625 is not empty, so it was a commission once. It can never be anything but 0.

offered() re-implements the board at ECL08 $A84D: sixteen candidates tested in a fixed order, each a COMPARE/IF/GOTO $A890 gate, at most three offered per visit. The cap counter is $4A05, in the per-script scratch page, so it resets when the party leaves — and it is never reset inside the script, which is why a second approach to the clerk on the same visit says only “BACK SO SOON? I CAN ONLY REPEAT THE EARLIER OFFERS” and lists nothing.

Three things the board does that a pure predicate cannot show:

  • The graveyard commission is offered before the sixteen (ECL08 $A584) and does not count against the cap. graveyard_offer() has it.

  • Being offered a job writes flags. Candidates 3, 4, 5, 12, 13, 14 and 15 set $4AB0, $4A97, $4A9B, $4A8C, $4A98, $4A99/$4A6B and $4A9A as the clerk speaks. offered() predicts, it does not simulate.

  • Running the board dry advances the plot. If the loop reaches candidate 16 without filling the three slots, $A842/$AF2C writes 254 into $4ABE — ledger index 24, “Cadorna exposed as a traitor” — so the next visit gets that speech.

The write-ups, work/reports/quest-flags.md and work/reports/commissions.md, are lost; the plan and its evidence table are docs/103-quest-log-panel.md. Nothing outside those two structures and the named appointment flags is exposed, because nothing else in the region is confirmed to the same standard.

Module Attributes

SLUM_WANDERING

How the 25 splits, for anything that has to explain the number.

QUEST_UNSEEN

what a panel says about a side quest is #158 step 4 and is Donald's to word.

Functions

appointments(source)

flags(source)

The flag block, out of whichever container the caller happens to hold.

graveyard_offer(source[, party_strength])

ECL08 $A584: the graveyard commission, offered before the sixteen.

ledger(source)

ledger_name(index)

The clerk's own words, or the index where the clerk has no speech.

marker_text(index, value)

What a ledger byte between 1 and 253 means, or None if it means nothing.

offered(source[, limit, party_strength])

What the clerk would offer on the next visit, in the board's order.

read(source[, party_strength])

Everything, from the flag block.

scratch(source)

$4A00-$4A1F out of whichever container the caller holds.

side_quests(source)

What a save says about each side quest, as far as it can say anything.

summary_lines(source)

The panel as text, for a terminal.

Classes

Appointment

A place the Council has told the party to go, or a job it has handed out.

Commissions

What the Council has asked for, and what it has already paid for.

Entry

One row of the reward ledger.

Flags

The 224 bytes, addressed the way the game addresses them.

Offer

A candidate on the board: what the clerk says, and what it settles.

QuestFlag

One byte at one value, and the instruction that puts it there.

Scratch

$4A00-$4A1F, the page an area change zeroes.

SideQuest

A quest on its own flag bytes, outside the council's ledger.

SideQuestState

What a save says about one side quest.

class goldbox.commissions.Appointment[source]

Bases: object

A place the Council has told the party to go, or a job it has handed out.

__init__(address, name, kind, value, state, outstanding)
Parameters:
Return type:

None

address: int
kind: str
name: str
outstanding: bool
state: str
value: int
class goldbox.commissions.Commissions[source]

Bases: object

What the Council has asked for, and what it has already paid for.

__init__(completed, ledger, offers, appointments)
Parameters:
Return type:

None

appointments: tuple[goldbox.commissions.Appointment, ...]
completed: int
property in_progress: tuple[goldbox.commissions.Entry, ...]

an area script’s own progress marker.

Type:

A non-zero byte below 254

ledger: tuple[goldbox.commissions.Entry, ...]
offers: tuple[goldbox.commissions.Offer, ...]
property outstanding: tuple[goldbox.commissions.Appointment, ...]
property paid: tuple[goldbox.commissions.Entry, ...]
property reward_waiting: tuple[goldbox.commissions.Entry, ...]

Done, and the money is still sitting at the City Hall.

class goldbox.commissions.Entry[source]

Bases: object

One row of the reward ledger.

__init__(index, address, name, source, value, major)
Parameters:
Return type:

None

address: int
property detail: str | None

What the progress marker means, for the entries that keep one.

property done: bool
index: int
major: bool
name: str
source: str | None
property state: str
value: int
class goldbox.commissions.Flags[source]

Bases: object

The 224 bytes, addressed the way the game addresses them.

__init__(data)[source]
Parameters:

data (bytes)

ledger(index)[source]
Parameters:

index (int)

Return type:

int

to_bytes()[source]
Return type:

bytes

class goldbox.commissions.Offer[source]

Bases: object

A candidate on the board: what the clerk says, and what it settles.

__init__(order, text, ledger)
Parameters:
Return type:

None

ledger: tuple[int, ...]
order: int
text: str
goldbox.commissions.QUEST_UNSEEN = 'not seen'

what a panel says about a side quest is #158 step 4 and is Donald’s to word.

Type:

A quest’s state, as an identifier. Not interface text

class goldbox.commissions.QuestFlag[source]

Bases: object

One byte at one value, and the instruction that puts it there.

__init__(address, value, meaning, durable, where)
Parameters:
Return type:

None

address: int
durable: bool
meaning: str
property scratch: bool

True when an area change wipes this byte.

value: int
where: str
goldbox.commissions.SLUM_WANDERING = 15

How the 25 splits, for anything that has to explain the number.

class goldbox.commissions.Scratch[source]

Bases: object

$4A00-$4A1F, the page an area change zeroes.

Kept apart from Flags on purpose: a byte in here means something only while the script that wrote it is still resident, so a caller has to ask for it by name rather than get it mixed into the persistent block.

__init__(data)[source]
Parameters:

data (bytes)

to_bytes()[source]
Return type:

bytes

class goldbox.commissions.SideQuest[source]

Bases: object

A quest on its own flag bytes, outside the council’s ledger.

__init__(key, name, script, area, accept, finish, progress=())
Parameters:
Return type:

None

accept: goldbox.commissions.QuestFlag
area: int
property durable: bool

True when the game itself remembers the whole quest.

finish: goldbox.commissions.QuestFlag
property flags: tuple[tuple[int, int, str], ...]

Every (address, value, meaning) this quest is made of.

key: str
name: str
progress: tuple[goldbox.commissions.QuestFlag, ...] = ()
script: str
class goldbox.commissions.SideQuestState[source]

Bases: object

What a save says about one side quest.

__init__(quest, state, accept_value, finish_value, progress_in_hand=False)
Parameters:
Return type:

None

accept_value: int | None
property ambiguous: bool

QUEST_UNSEEN here could equally mean “accepted, then left”.

True whenever the accept flag is in the scratch page and the durable half says nothing. It is not a claim that the party accepted anything – it is the statement that this save cannot tell the two apart. Donald’s decision of 2026-09-04 settled what the panel does about that: it shows the errand once the potion is in hand and never from this scratch byte – see durable_state. All 16 saves on the machine that never met Ohlo read exactly like a party that accepted the errand and walked out of the Slums.

property durable_state: str

The same reading with durable=False flags ignored.

Donald’s decision, 2026-09-04: the Quest Log shows the errand once the game’s own durable bytes say so, and never shows QUEST_ACCEPTED from $4A04, which is scratch an area change wipes. So this never reads accept_value unless quest.accept is itself durable – for Ohlo it never is, and a page with $4A04 = 250 alone reads QUEST_UNSEEN here while state reads QUEST_ACCEPTED.

finish_value: int
progress_in_hand: bool = False

Whether a durable progress flag is at its value. Stored rather than recomputed so durable_state needs nothing but this dataclass’s own fields – no Flags kept around, no second read of the save.

quest: goldbox.commissions.SideQuest
state: str
goldbox.commissions.appointments(source)[source]
Return type:

tuple[goldbox.commissions.Appointment, …]

goldbox.commissions.flags(source)[source]

The flag block, out of whichever container the caller happens to hold.

Accepts a Flags, a SaveGame0 (or anything with to_bytes), the whole SAVEDGAME0 payload, the $4A00 page a memory dump tends to come in, or the 224 bytes themselves. Lengths are distinct, so no flag is needed.

Return type:

goldbox.commissions.Flags

goldbox.commissions.graveyard_offer(source, party_strength=None)[source]

ECL08 $A584: the graveyard commission, offered before the sixteen.

Four gates, in the script’s order: $4AC1 >= 4, the graveyard reward not already paid, party strength >= 19, and the commission not already accepted ($4A96 != 255). party_strength of None means “not known”, and the strength gate is taken as passed.

Parameters:

party_strength (int | None)

Return type:

goldbox.commissions.Offer | None

goldbox.commissions.ledger(source)[source]
Return type:

tuple[goldbox.commissions.Entry, …]

goldbox.commissions.ledger_name(index)[source]

The clerk’s own words, or the index where the clerk has no speech.

Parameters:

index (int)

Return type:

str

goldbox.commissions.marker_text(index, value)[source]

What a ledger byte between 1 and 253 means, or None if it means nothing.

None is the honest answer for a value no script writes: the four entries below are the only ones with markers, so anything else in 1..253 is a byte the game did not put there.

Parameters:
Return type:

str | None

goldbox.commissions.offered(source, limit=OFFER_LIMIT, party_strength=None)[source]

What the clerk would offer on the next visit, in the board’s order.

The graveyard commission comes first and does not count against limit, which is how ECL08 runs it: $A584 is reached before $A831 zeroes the board index, and $4A05 is only bumped inside the board’s own loop.

Parameters:
  • limit (int)

  • party_strength (int | None)

Return type:

tuple[goldbox.commissions.Offer, …]

goldbox.commissions.read(source, party_strength=None)[source]

Everything, from the flag block.

Parameters:

party_strength (int | None)

Return type:

goldbox.commissions.Commissions

goldbox.commissions.scratch(source)[source]

$4A00-$4A1F out of whichever container the caller holds.

None where the source cannot carry it – the 224 persistent bytes, or a Flags. Everything else flags() accepts does carry it: a $4A00 page and SAVEDGAME0 both start at or below $4A00.

Return type:

goldbox.commissions.Scratch | None

goldbox.commissions.side_quests(source)[source]

What a save says about each side quest, as far as it can say anything.

The durable flag is read from flags(); the accept flag is read from scratch() when the source carries it. A quest whose accept flag is in the scratch page and whose durable flag is still 0 reads as QUEST_UNSEEN from a save made outside its area whether or not the party accepted it. SideQuestState.durable_state is the reading Donald’s decision of 2026-09-04 asked for: it never looks at the scratch page, so this ambiguity never reaches it.

Return type:

tuple[goldbox.commissions.SideQuestState, …]

goldbox.commissions.summary_lines(source)[source]

The panel as text, for a terminal. Same content, no Qt.

Return type:

list[str]