5.11 POK trainers¶
A .pok file is a list of cheats for one game: "Infinite lives", "Infinite
ammo", "Start on level 5". Each entry (a trainer) is a few bytes to write into
the running game's memory. The format is the one World of Spectrum made
standard, and the files are found next to a game's tape or snapshot in most
collections. JNEXT opens one, shows its trainers, applies the ones you pick to
the machine that is running, undoes them again, and lets you save the result as
a snapshot.
A .pok file changes the program that is running. It does not start one: load
the game first (see 5. Running programs), reach the point you want,
then open the trainers.
Using one¶
- Load and start the game.
- Choose Machine ▸ POK Trainers…, or File ▸ Load NEX File… (Alt+O) and
pick the
.pokfile (the file dialog lists POK Trainers (*.pok)). A.pokopened with File ▸ Load NEX File… does not reset the machine: it opens the trainers and leaves the game exactly where it was. - Pick a trainer in the list and press Apply. Its Status reads applied, and the game now behaves differently: lives stop going down, say.
- Press Remove to undo it.
- Close the window and carry on playing. To keep the state, save it with File ▸ Save Snapshot… (5.9 Snapshots): the snapshot holds the changed bytes, so loading it later gives you the game with the trainer already in it.
A game loaded from a ZIP archive that also holds a .pok gets its trainers
without any of this: File ▸ Load NEX File… makes them the open list (not shown), the
status bar says so, and Machine ▸ POK Trainers… shows them
(5. Running programs).
Machine ▸ POK Trainers… with no file open asks for one; with a file open it shows the trainers again. Open POK File… in the window opens another file. While the window is open the game keeps running behind it, but the main window takes no keys until you close it.
What the Status column says¶
| Status | It means |
|---|---|
| not applied | Memory does not hold this trainer's bytes. Apply is available. |
| applied | You applied it and its bytes are still in memory. Remove puts back what it replaced. |
| applied (found in memory) | You did not apply it in this session, but memory already holds all of its bytes, for example after loading a snapshot taken with it applied. Remove needs the file to give the original bytes (see below). |
| invalid - line N: … | The trainer has a malformed line, and the reason names the line. It cannot be applied. |
The status is worked out from memory, never remembered. The list is refreshed when you press Apply or Remove, select a row, or the machine is rebuilt. If the game has overwritten a poked byte (a level change that resets the lives counter, say), the next refresh shows not applied, and Remove says so instead of writing. Press Apply again to poke it back: Remove then undoes the whole trainer, putting every byte the game had not touched back to what it was before the first Apply, and leaving a byte the game itself changed as the game has it.
Trainers that ask for a value¶
Some trainers leave a number open: "Lives" without saying how many. Apply then asks for it, offering the byte memory holds at the first such place (the game's current lives). The number you give is written everywhere the trainer leaves a value open, once for the trainer, not once per poke. Cancel applies nothing. A trainer that asks for a value is never shown as found in memory, because the number you chose cannot be known afterwards.
What Remove puts back¶
Remove writes back exactly the bytes Apply replaced, at exactly the places it wrote them, even if the game has paged other memory in since. That is why it is exact, and why it is only available for a trainer you applied in this session.
For a trainer found in memory, JNEXT has no record of what was there. A .pok
line can carry the original byte as its last number, "used to remove a POKE"
from a game that was already poked, and JNEXT uses that. Most .pok files do
not give it (the number is 0), and then Remove is refused with a message
naming the line and the address.
When the undo is forgotten¶
The file stays open for as long as the main window does, but what Remove needs to undo a trainer is kept only while the machine you applied it to is running. These forget it, though the changes stay in memory:
- Machine ▸ Power Reset, a machine-type change, File ▸ Load NEX File… of a program or snapshot, Quick Load, and a program that resets the machine itself. The row shows applied (found in memory) if memory still holds the bytes, not applied if not.
- A program sent by a debugger: a ZRCP
smartloadorload-binary, or a new DeZog (DZRP) or GDB connection, which is when those load their program. The trainer's bytes may still be in memory, but what they replaced belonged to the old program, so the row shows applied (found in memory) or not applied, never applied. Memory writes a debugger makes in a connection that is already open do not do this. - Opening another
.pokfile.
Where a poke lands¶
Each line of a .pok carries a bank number as well as an address. JNEXT
follows the hardware:
| The line says | It writes to |
|---|---|
| bank 8-15 (the format's "ignore the bank", always used for 48K games) | the RAM the memory map shows at that address at the moment you press Apply |
| bank 0-7, address 0xC000-0xFFFF | that 16 KB bank, whatever is paged in there |
| bank 0-7, address below 0xC000 | the bank is ignored, as for 8-15: only the top 16 KB of the address space is banked, so this is the memory map again |
Banks 0-7 are the 128K RAM banks, so they work on the 128K, +3, Pentagon and the Next. On a 48K the only bank that exists at 0xC000 is bank 0; a line naming another bank at 0xC000 or above is refused, with a message (below 0xC000 the bank is ignored, as on every machine, and the line pokes the memory map). A trainer whose lines say bank 8 writes into whatever bank the game has paged in at that moment, so apply it while the game is running normally.
A trainer is applied all or nothing. If one of its pokes cannot land (an address that is ROM in the current memory map, a bank the machine does not have, a slot with nothing mapped) the trainer is refused with a message naming the line, and none of its bytes are written.
Other emulators, Fuse for one, index inside the named bank for every address.
The two only differ for a bank number on a line below 0xC000, which is rare in
the files in circulation. In the one checked against the real game, the 128K
Starglider's "Infinite Missiles (Bank#0)", reading the bank number there would
miss: its first line (address 44796) is the byte the CPU sees at 0xAEFC, the 1
of an LD A,1 that sets the missile counter.
Things that stop it¶
- An RZX recording is playing or being made. A change the recording does not contain would make its playback diverge, so Apply and Remove are refused with a message that says so. Stop the recording or the playback first.
- Rewind. Applying or removing a trainer is a change made from outside the program, like a memory edit in the debugger. Step Back across it is refused; Frame Back to before it undoes it. See Backward execution (rewind).
A file with a malformed line¶
JNEXT reads the format strictly, so a damaged line costs only the trainer it
belongs to. That trainer is listed as invalid - line N: … with the reason (a
bank outside 0-15, an address outside 16384-65535, a value outside 0-256, a
line that does not have four numbers, a trainer that ends without its last
line), and every other trainer works. Lines that belong to no trainer are listed
below the list with their line numbers, twenty at most and then "and N more".
Blank lines, CRLF or LF line ends, names padded with spaces, and a missing final
Y line are all fine. A file with no trainer at all, or larger than 1 MiB, is
refused with a message.
A trainer's name is shown as UTF-8 text when the bytes are valid UTF-8 and as Latin-1 otherwise, which covers both kinds of file in circulation.
Without the window¶
The window is part of the Qt build. The SDL-only build's window and the headless
runs have no menus, and --load game.pok is refused. Two options apply a .pok
file there:
--delayed-pok-frames N FILEapplies trainers fromFILEafterNemulated frames, once the program has loaded. Frames are counted from the start of the run (a 50 Hz machine runs 50 a second).--pok-trainers LISTsays which ones: their numbers in the file, 1 for the firstNline, comma-separated, eachTorT=VALUE. A trainer that asks for a value (a256in the file) needsT=VALUE, 0 to 255; any other trainer takes none.1,3=9,7applies trainer 1, trainer 3 with the value 9, and trainer 7, in that order.
$ jnext --headless --machine 48k game.tap \
--delayed-pok-frames 1200 game.pok --pok-trainers 1,3=9 \
--delayed-snapshot trained.sna --delayed-snapshot-frames 1210 \
--delayed-automatic-exit-frames 1300
The numbers are the order of the N lines in the file (grep '^N' game.pok | cat -n
numbers them), and the log names each trainer as it is applied. Pick the frame by
when the game has reached the point you want, and look with a
--delayed-screenshot at a guessed frame: a tape loaded with the fast loader is
ready after a few hundred frames (the 48K Starglider shows its menu at 150), one
loaded in real time (--tape-realtime) takes as many frames as it has seconds
of tape times 50 at 50 Hz. With a snapshot, --delayed-pok-frames 0 applies the
trainers in the first frame, right after the snapshot has loaded.
Everything that can be checked is checked before the machine starts, and a failure exits with status 1: a file that cannot be read, is larger than 1 MiB or has no trainer; a number that names a trainer the file does not have or one whose lines are malformed; a value missing for a trainer that asks for one, or given to one that does not; either option without the other, or given twice (one file per run). The Qt window refuses both options, since it has the trainers window.
The trainers go in by the same path as the window's Apply: every write is a
MUTATE line in the log, a trainer is applied whole or not at all, a bank-3
line at 0xC000 or above on a 48K or an address that is ROM in the current memory map is refused,
and so is anything while an RZX is recorded or played. A refusal, or an
automatic exit that comes before the frame, makes the run exit non-zero once it
ends, and the trainers before the refused one stay applied. A trainer whose
bytes memory already holds, from a snapshot saved with it, is left as it is.
For anything the options do not do (undoing a trainer later, applying at several moments, a condition), a debugger script does the writes in any build:
; "Infinite lives": Z 8 32771 0 53 (bank 8 = the memory map; value 0)
; Pick the frame by when the game has reached the point you want.
on frame 2000 once do
set mem[32771] = 0
save_snapshot "game-trained.sna"
end
on frame 2003 once do exit 0 end
$ jnext --headless --machine 48k --load game.tap --script trainer.jds
For a line with a bank 0-7 at 0xC000 or above, write to the physical page
(Changing the machine):
page 2*bank + ((address >> 13) & 1), offset address & 0x1FFF. The line
Z 3 49443 90 0 (bank 3, address 49443, value 90) becomes:
var bank = 3
var addr = 49443
var poke_value = 90
on frame 2000 once do
set phys[2 * bank + ((addr >> 13) & 1), addr & 0x1FFF] = poke_value
save_snapshot "game-trained.sna"
end
on frame 2003 once do exit 0 end
Every write is logged as a MUTATE line, in the window too: the trainers go
through the same write path as the debugger's own memory edits, which is also
why an RZX recording refuses them.