automap.window¶
The PyQt6 map window.
Deliberately thin. All the geometry is in render.py and all the knowledge is
in state.py; this paints primitives and forwards key presses. Keeping it that
way is what lets the map be developed and tested without a display – see
to_svg, which draws exactly the same primitives.
Module Attributes
How a combatant's square is filled, by |
|
And what the hit points inside it are written in. |
|
Said in the Messages panel when there are no game disks configured at all, so there is nothing for the grid to draw -- the one failure that used to go to stderr and nowhere else, then to nowhere at all once the paint that once put it on the grid was removed without a word ( |
|
Said in the status line while the party is on the travel grid, where none of the loaded maps reach -- |
|
Said in the Messages panel when the map the machine is drawing is a Gold Box map and none of the configured title's -- so the disks the window is set up for are not the game that is running. |
Functions
|
The |
Classes
The map, which opens whether or not there is a game to watch. |
|
Paints the fight, in the area map's own language. |
|
Paints the current map. |
- class automap.window.AutomapBinding[source]¶
Bases:
PyQt6.QtCore.QObjectThe map, which opens whether or not there is a game to watch.
Three ordinary states, none of them an error: no emulator yet, an emulator with no save loaded, and a live party. The window sits in the first two showing an empty grid and saying what it is waiting for, and moves between them on its own. Quitting VICE and restarting it is handled the same way – the connection is simply re-established on the next tick.
- COMBAT_LOG = True¶
Keep the combat messages the game paints over. One extra burst per tick while a fight is running and nothing at all outside one.
- COMBAT_LOG_EVERY = 1¶
How many combat ticks per read of the message panel. One is a starting point, not an answer: a message lives about a second of emulated time (
COMBAT $28C3, delay$49FC, default 2), so there is room to poll less often if the extra ~14.3 ms a tick turns out to stutter the fight. Left here rather than inSettingsso it can be raised without a release; it belongs in the settings file once the measurement says what it costs.
- LIVE_EVERY = 5¶
How many map ticks per read of the live party. See
poll_live.
- SIDE_SQUEEZED = 160¶
And the narrowest. A squeezed window still shows enough of a note or a commission to say which one it is; without a floor here the column was 270px of fixed width whatever the screen was (#41).
- SIDE_WIDTH = 460¶
The width the notes, quest log and messages column opens at. It was a cap until
#162made the divider draggable, and a cap is the one thing a draggable column cannot have; the number itself did not move. Defined once, inpanel.ColumnSplitter, which is what divides the tab.
- __init__(root, mapper, interval_ms=200, connect=None, settings=None, drive=True, disks=None)[source]¶
- Parameters:
interval_ms (int)
settings (automap.config.Settings | None)
drive (bool)
disks (str | None)
- ask(question)[source]¶
A yes/no the player has to answer. A method so a test can answer it, the same way
ActionBar.askis.
- bus_guard¶
One byte of
$DD00before each tick, for a target whose reads stop the processor (halts_on_read): the tick is skipped while the drive is mid-transfer, because a stop inside a load hangs the C64 Ultimate –#375 (Wish has to work around the Ultimate freezing the C64 mid-load, which hangs the game while the automapper follows along). It reduces the rate and cannot end it; seeautomap/busguard.py.
- columns¶
The two dividers down the tab, and the widths the user drags them to. Built before the panels so that a column restored shut is shut on the first frame rather than after one at the default width.
- edit_note(x, y, index=None)[source]¶
Open the popover on a square.
indexedits an existing note.A popover and not a dialog: notes are made while playing, and a modal box in front of the map is an interruption for something that should cost one keystroke.
- log_combat(messages, battle=None)[source]¶
Combat lines into the Messages panel, each with its dice under it.
Passes `dedup=False`, and that is the whole point of the feature:
MessagesPanel.saydrops a line identical to the one before it, which is right for “waiting for the game” on every tick and wrong for two “MAGNUS MISSES.” in a row – and just as wrong for the identical roll line under each of them. The log has already deduplicated, on consecutive identical frames, which is the only rule that can tell the two apart.battleis who was in the fight; it is passed explicitly because the last flush of a fight happens afterself.battlehas already gone to None, and without it the last message of every fight would lose its dice.This is also where the shouting stops. The game prints in capitals because the C64’s character set is capitals;
combatlog.recaseturns the line into ordinary prose with the combatants’ names capitalised, and the combatant list is what says which words those are. It runs here rather than in the log becauseMessage.textis the evidence of what the game actually printed. The tooltip keeps the rows verbatim for the same reason.- Return type:
None
- no_maps¶
No maps at all is its own state, and not the same as no emulator: an emulator will not fill the grid either. Said in the Messages panel, alongside
Waiting to connect...and the rest – the grid itself carries nothing (Donald’s ruling, 2026-09-04, on#214 (The automapper's empty grid never says there are no game disks, though the code and a test believe it does)).
- note_here()[source]¶
N: a note on the party’s own square, if we know where that is.- Return type:
None
What a right-click on a square with notes offers, as data.
Data rather than a
QMenuso the offer can be tested without a display –note_menuis four lines on top of this.No “add another”. A square holds one note. It still lists whatever is on the square rather than assuming one, because a file written by a build that allowed several has to stay editable and deletable.
- other_maps: Callable[[], dict[str, dict]] | None¶
Set by the host to a zero-argument callable returning every other configured title’s own maps,
{title: {area: Geo}}–None(the default) means “no correction available”, which is what a hosted binding with no window behind it gets, and_check_the_gamethen behaves exactly as it did before step 4 of#357 (The automapper reads the shared Game disks folder, so setting a title's own folder does not make it map that title).
- poll_battle()[source]¶
Swap to the combat canvas while a fight is running. True if one is.
Gated on `$6E11`, never on the screen. Checked once a second while the party is in the world – one more round trip on the tick that reads the party anyway – and on every tick once a fight has started, because that is when the map is worth looking at. The area map is not polled at all during a fight: the party is not moving through the world, and its explored squares sit untouched until the fight ends.
- Return type:
- poll_combat_log()[source]¶
Read the message panel and keep whatever it finished saying.
- Return type:
None
- poll_live()[source]¶
Refresh the roster and the strip, every
LIVE_EVERYticks.Not every tick, and the reason is measurable: a poll’s cost is the round trip, and under VICE each one hands the emulation ~14.3 ms of extra emulated time. The map’s own fix is one trip; the party is one more (two reads inside a single resume, see
ViceTarget.read_blocks). Doing it every fifth tick is once a second at the default interval, which is faster than hit points change and a fifth of the disturbance.Only the visible tab polls at all – the host hands the target to whichever tab is showing – so a hidden map costs nothing.
- Return type:
None
- set_maps(maps, title=None, disks=None)[source]¶
New game disks: draw their maps instead, without a restart.
These maps are also the signature the running game is checked against (#21), so changing them takes the window’s verdict on the title back to “no idea” – see
Automapper.use_maps.
- show_strength(save0_bytes, roster_bytes)[source]¶
Recompute party strength and show it under the strip.
Live data only, and deliberately: the number is what the running game would compute, so a save file on disk would be the wrong answer the moment anybody readied anything. Same two blocks as the poll.
The slums count comes with it because it is the one scaled encounter watched end to end –
(strength / 3) * 2,ECL14 $B1B0– and a bare 13 says nothing about what it costs. Seedocs/114-party-strength.md.
- shutdown()[source]¶
Save what must survive. Idempotent, and safe with no connection.
Split out of
closeEventbecause a hosted window is never closed on its own – the host closes, and the notes still have to be written. The connection is only ours to close when we opened it.- Return type:
None
- tick()[source]¶
Read one fix and redraw if anything moved.
Hosted (
drive=False), trouble is raised rather than absorbed: the host owns the connection and is the only thing that can reattach.- Return type:
None
- titleObserved¶
Emitted with a title’s name when
_check_the_gamefinds the machine running a different configured title than the one this window is set up for –#357 (The automapper reads the shared Game disks folder, so setting a title's own folder does not make it map that title)step 4. The host, not this class, owns switching to that title’s own folder: it is the one holdingSettings.game_folders.alias of
str
- waiting(text, alarm=False)[source]¶
Say what is being waited for. The host sets this; alone, we do.
alarmcolours it red, for the one case that is not ordinary waiting: something else is holding the emulator’s monitor, so the game is running and we cannot read it. Not a dialog – it clears on its own when the other client lets go.
- automap.window.COMBATANT_FILL = {'enemy': PyQt6.QtGui.QColor, 'helpless': PyQt6.QtGui.QColor, 'party': PyQt6.QtGui.QColor}¶
How a combatant’s square is filled, by
combat.Combatant.kindwith any-dimtaken off. Unknown kinds fall back to the enemy red, which is what the oldstartswith("party")test did for everything that was not the party.
- class automap.window.CombatCanvas[source]¶
Bases:
PyQt6.QtWidgets.QWidgetPaints the fight, in the area map’s own language.
Same graph paper, same ink, same line art – a player should not feel they have changed program because a fight started. What is new is the colour: the party green, the enemy red and a helpless enemy gold, with current hit points written in the square, because mid-fight that is the number you look for.
The geometry is
automap/combat.py, which has no Qt in it; this paints what it yields and answers the tooltip.- MIN_LABEL_PIXELS = 4¶
Below this pixel size nothing reads as a digit – the floor
tools/combatbarsheet.py’sfit_fontuses for the same reason.
- automap.window.HP_INK = {'hp-dim': PyQt6.QtGui.QColor, 'hp-ink': PyQt6.QtGui.QColor}¶
And what the hit points inside it are written in. Paper on the dark fills, ink on the light one,
FADEDon a dimmed square whatever colour it was.
- class automap.window.MapCanvas[source]¶
Bases:
PyQt6.QtWidgets.QWidgetPaints the current map.
- property cell: int¶
How big a square is drawn, for the room the widget has been given.
Derived from the size rather than remembered from a resize event, because a widget that is not on screen is not sent one until it is shown – and a click, a tooltip and a note anchor all have to agree with what was painted whether that event has arrived or not.
- flash: tuple[int, int] | None¶
The square a notes-panel row asked to be pointed at, until the next click anywhere. Not a selection – nothing here is selectable.
- mousePressEvent(event)[source]¶
Note where the press landed. Nothing opens here.
The popover used to open on the press, while the button was still down, and on Windows it closed itself the instant it appeared: the debug log showed it receiving a bare
Closewhile still visible and still the active window, with no mouse event, noFocusOutand noWindowDeactivatebefore it. That is Qt dismissing a popup, and the only thing left for it to be dismissed by is the release of the very click that opened it – a popup grabs the mouse, so the release lands on a popup that was not there when the button went down.Opening on the release instead means no button is down by the time the popup exists, and there is nothing left to straddle it.
- property origin: tuple[int, int]¶
Where the grid’s top-left corner sits in the widget.
The grid stays square and centred: a widget wider than it is tall spends the difference on paper, not on rectangles for squares.
- automap.window.NO_DISKS = 'No game disks found. Set the game folder in File > Preferences…'¶
Said in the Messages panel when there are no game disks configured at all, so there is nothing for the grid to draw – the one failure that used to go to stderr and nowhere else, then to nowhere at all once the paint that once put it on the grid was removed without a word (
#214 (The automapper's empty grid never says there are no game disks, though the code and a test believe it does)). Donald’s wording, exactly.
- automap.window.OUTDOORS_STATUS = 'Outdoors, no map'¶
Said in the status line while the party is on the travel grid, where none of the loaded maps reach –
#205 (A party that walks out onto the travel grid leaves the automapper's marker behind). Donald’s ruling, 2026-09-04: the grid itself carries nothing, so this andOUTDOORS_WHERE/OUTDOORS_AREAinautomap/state.pyare the whole of what tells a player the map cannot follow them. The[status]/[memory]source suffix is added the same way the ordinary status line adds it.
- automap.window.WRONG_GAME = 'ERROR: Wrong game disk loaded. Disabling functionality to protect from corruption.'¶
Said in the Messages panel when the map the machine is drawing is a Gold Box map and none of the configured title’s – so the disks the window is set up for are not the game that is running. Donald’s wording, exactly.
It names neither game on purpose, and it could not name the second one anyway: the check that fires it validates the title we believe rather than identifying the one that is there. What was believed, and what was seen, go to the debug log –
Automapper._contradicted.