Testing a PS2 game with no one at the controller

Driving PCSX2 by script, and reading the whole console’s memory from savestates

How the Tekken 4 project proves changes in game without a person playing: PCSX2 is booted by script, steered with synthetic key presses, and asked for savestates, which hold all 32 MB of main memory plus sound and video memory. Every in-game result in the project was produced this way, with rules learned from the times it went wrong.

Reverse engineering a console game produces a lot of claims that only the running game can settle. “Not confirmed in game” is treated as “not working”, so the project needed a way to confirm things in game quickly, repeatably and without someone holding a controller. This entry is that method, written as a procedure that would carry over to other PS2 games.

Booting into a known state

PCSX2 starts in batch mode, straight from a savestate:

Terminal window
pcsx2-qt.exe -batch -logfile run.log -statefile fight.p2s -- game.iso

The run starts inside a fight in a known scene, so a test doesn’t have to play through menus every time. Two practical rules:

  • Headless mode (-nogui) hung in this setup. Batch mode works.
  • PCSX2 asks for confirmation on exit, which blocks a polite shutdown. The driver ends the process instead.

Pressing buttons

The driver posts key-down and key-up window messages to PCSX2’s window, so it doesn’t need focus and doesn’t steal the keyboard. A dedicated input profile maps those keys to controller 1. It’s switched on in the game’s settings only while the driver runs, and switched off afterwards.

Menu routes are recorded as key sequences with timings. For example, arcade mode is Start, 14 seconds after boot.

Seeing the result: savestates as probes

PCSX2’s remote-control interface, PINE, can save a state on request. A PCSX2 savestate (.p2s) is a zip archive holding:

  • the full 32 MB of main (EE) memory, identity-mapped, so an address in memory is an offset in the file;
  • a screenshot;
  • I/O-processor memory;
  • sound memory;
  • the vector unit’s microcode memory;
  • video (GS) memory.

PCSX2 2.x compresses members with Zstandard (zip method 93), which Python’s zipfile can’t read. The reader parses the zip headers itself.

A test therefore reads like this:

  1. Build a modified disc.
  2. Boot it into the scene by script.
  3. Press the buttons.
  4. Request a savestate.
  5. Compare memory against the prediction, offline.

The comparison is as exact as the claim needs. A moveset test diffs the loaded data word by word against the expected bytes (Tekken 4’s moveset files, decoded and edited in game). A stage test reads allocator state (How large can a Tekken 4 stage be? Bigger than predicted). A sound test compares sound memory with a tone bank.

About 2,000 savestates from these runs are kept. They answer later questions without booting again, because any address in any of them can be read at any time.

Proving a cheat

Cheats get their own checker:

  1. Boot into a saved fight.
  2. Wait for PCSX2’s log to report the patches active.
  3. For each enabled cheat, read the patched words back over PINE.
  4. Check the effect while time runs. A frozen round timer must hold while the play clock advances. An infinite-health cheat must restore health on the next frame after it’s forced down to 1.0.

Rules learned the hard way

Each of these cost a wrong conclusion first:

  • Anchor a memory search on a value you can see. The savestate screenshot shows the play clock, so compute the real time between two states rather than assuming it. An “anomaly” in the round timer was a wrong time delta: the states were 0.47 s apart.
  • Expect fixed point. Health was searched for as a small integer and never found. It’s 16.16 fixed point, with a maximum of 165.0.
  • Don’t rely on what’s on screen in a paused game. A manual loop that asked “did the health bar move?” failed because the pause menu hides the bars.
  • Don’t write to thousands of candidate addresses at once. Writing to 1,688 candidate addresses at once froze PCSX2.
  • PINE reports “running” while paused. Check that the play clock moves.
  • Check that the emulator applied the change at all. PCSX2 2.x only applies cheats that are named in the game’s own settings file. From late August to early October, the cheats under test were silently never applied, and the savestates showed every patched address still holding the original instruction.
  • Disassemble before trusting a constant. A raw scan for the number 180 found menu code, not health. The worst case of this is The “round timer” patch that was really editing rand() % 60.

Sharing one emulator

The project’s later research ran several sessions in parallel, but there is only one PCSX2. The rules that kept it sane:

  • Every boot is requested from a coordinator first, then reported done.
  • Automated runs use their own savestate slot and never touch the user’s.
  • The game’s settings file is restored after every boot.
  • The driver refuses to start if PCSX2 is already running.
  • No disc image is opened by two processes at once.
  • Desk work is done first, and boots are batched.

It’s slower than everyone booting at will, and it has never produced a result polluted by another session’s run.

↑ ↓ move, Enter open Search with filters