Skip to content

PCSX2 recipes

Each recipe below is one thing an engineer often needs to do in the original running in PCSX2: find a human, move one, change a field, call one of the game's functions or a script binding, load a checkpoint. Every recipe has been run in PCSX2 and says when, on which copy and from which state; each names the Ghidra address that justifies it and the research page that explains it. The list is generated from research/pcsx2-recipes.toml by coney-tools pcsx2 recipe render; edit that file, never this page.

Run a recipe on a copy you hold a claim on (Driving PCSX2): coney-tools pcsx2 claim --agent ID, coney-tools pcsx2 launch --agent ID STATE, then coney-tools pcsx2 recipe run NAME --copy COPY --agent ID --arg name=value. recipe list lists them and recipe show NAME prints one. The runner talks to the copy's PINE port (and its debug server for the emulation steps), so the pcsx2 MCP server must be connected in mode: "debug", which leaves PINE free. It checks first that the copy runs, that you hold its claim, that the emulation is running or paused as the recipe needs, and that the running state holds every patch the recipe names: a patch goes into a copy of a state with coney-tools pcsx2 prepare-state SOURCE OUT --patch NAME ... before PCSX2 starts on it.

Many recipes call the game through the call-brains call hook (research/traces/patches/hooks-core.toml): the runner writes a function address and its arguments into the call block, and the game makes the call on its own thread at the start of the next brains update. The lua recipe and the recipes built on it run a Lua chunk in the level's script state the same way, so every script binding (HuCreate, GiveMoney, MenuLoadLevel, ...) can be used as a level script uses it; the script bindings list them with their arguments.

Input: prefer recordings. For runtime evidence that depends on input, drive the original with a PCSX2 input recording (pcsx2 record --recording, Input recordings): it replays the same pad frame by frame. The scripted-pad recipes below are for closed-loop drivers that decide each input from what the game shows, which a recording cannot do.

Finding humans

  • humans: List every human in use: table address, handle index, name, player number, position and heading.
  • find-player: The player's human (player number 0) and its tables: record, per-player record, brain and transform.
  • find-human: A human by name, with the same tables as find-player.

humans

List every human in use: table address, handle index, name, player number, position and heading.

The 60 humans live in one static array at 0x00640c80, 0x6d0 bytes each, built by 0x00228730. A human is in use when its record pointer (+0xd4) is set; its name is at +0x80, its player number at +0x1b0 (0 for player 1, -1 for an AI human) and its handle index at +0x92, which indexes the transform table at 0x00714b00 (position at +0x0, rotation quaternion at +0x10). The heading is 2 * atan2(qz, qw), 0 facing +y, anticlockwise.

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00228730 Humans_StaticInit, 0x0021b0b8 Object_SetPosition
Research research/characters.md#the-human-object
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); 9 humans listed, the player Rembrandt at index 2
uv run --project python coney-tools pcsx2 recipe run humans --copy COPY --agent ID

Steps:

{ humans = true }

find-player

The player's human (player number 0) and its tables: record, per-player record, brain and transform.

The player is the human in use whose +0x1b0 is 0. In later levels it is not human 0, and the per-human tables are indexed by the handle index +0x92, never by the table position: the per-player record at 0x00660f50 + index * 0x2c, the brain at 0x006d53f0 + index * 0x2f0 and the transform at 0x00714b00 + index * 0x20. The record is the pointer at +0xd4 (0x0065a540 + i * 0x180).

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00228730 Humans_StaticInit, 0x00146078 PlayerRecord_Update
Research research/characters.md#the-human-object, research/characters.md#the-record, research/ai.md#brain
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run find-player --copy COPY --agent ID

Steps:

{ let = { h = "player" } }
{ show = "h", label = "human" }
{ text = "h + 0x80", label = "name", size = 16 }
{ show = "index(h)", label = "index", format = "int" }
{ show = "rec(h)", label = "record" }
{ show = "prec(h)", label = "per_player" }
{ show = "brain(h)", label = "brain" }
{ show = "tf(h)", label = "transform" }
{ show = "u32(h + 0x90)", label = "handle" }

find-human

A human by name, with the same tables as find-player.

Names are the ones level scripts give HuCreate (+0x80, up to 15 characters); humans lists them. Pick a puppet or a target by name rather than by distance: the nearest human changes as pedestrians walk.

Parameters who (human): the human's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00235010 Human_FindByName
Research research/characters.md#the-human-object
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); PoizoCiv
uv run --project python coney-tools pcsx2 recipe run find-human --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who" } }
{ show = "h", label = "human" }
{ show = "index(h)", label = "index", format = "int" }
{ show = "s8(h + 0x1b0)", label = "player_number", format = "int" }
{ show = "rec(h)", label = "record" }
{ show = "prec(h)", label = "per_player" }
{ show = "brain(h)", label = "brain" }
{ show = "tf(h)", label = "transform" }
{ show = "u32(h + 0x90)", label = "handle" }

Moving and editing humans

  • position: Read a human's position, heading, speed and gait.
  • teleport: Move a human to x, y, z and turn it to a heading (degrees), by writing its transform.
  • drop-human: Teleport a human high above a point and let it fall: read its height as it drops and where it lands.
  • health: Read a human's health, maximum, power, stamina and rage.
  • set-health: Set a human's health (record +0x144, s16).
  • set-rage: Fill (or empty) a human's rage meter (human +0x650, s32).
  • brain: A human's AI: brain type, gang, goal stack (type ids), target and whether it is suspended.
  • held-object: What a human holds: the handle in its hand (+0x338) and the object being picked up (+0x33c).

position

Read a human's position, heading, speed and gait.

The transform table (0x00714b00 + index * 0x20) is the authoritative position and rotation; the human copies it into +0x10 / +0x20 at the start of each update. Speed across the ground is +0x1ac (m/s) and the gait +0x1a8 (0 standing to 5 sprint).

Parameters who (human): the human's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x0021b0b8 Object_SetPosition
Research research/characters.md#ground, research/characters.md#the-human-object
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run position --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who" } }
{ show = "f32(tf(h) + 0x0)", label = "x" }
{ show = "f32(tf(h) + 0x4)", label = "y" }
{ show = "f32(tf(h) + 0x8)", label = "z" }
{ show = "wrap(deg(heading(h)))", label = "heading_deg" }
{ show = "f32(h + 0x1ac)", label = "speed" }
{ show = "s32(h + 0x1a8)", label = "gait", format = "int" }

teleport

Move a human to x, y, z and turn it to a heading (degrees), by writing its transform.

Write the position (+0x0, +0x4, +0x8) and the rotation as a quaternion about z (qz = sin(h / 2) at +0x18, qw = cos(h / 2) at +0x1c, x and y 0) into the human's transform; the human picks it up at the start of its next update. Unlike the script's Teleport (0x00385bb8, which calls Human_SetTransform 0x0023d440) it does not snap to the ground or break a grab, so give a z on the ground (read one with position) and do not move a human that is holding or held. For the script's own teleport, run lua with Teleport(...).

Parameters who (human): the human's name; empty for the player; x (number): metres; y (number): metres; z (number): metres (the ground's height; read it with position); facing (number, default 0): the heading in degrees, 0 faces +y, anticlockwise
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x0021b0b8 Object_SetPosition, 0x0023d440 Human_SetTransform
Research research/characters.md#ground, research/characters.md#teleport
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); player moved 3 m along x and turned to 90 degrees; read back after 30 updates
uv run --project python coney-tools pcsx2 recipe run teleport --copy COPY --agent ID --arg who=<who> --arg x=<x> --arg y=<y> --arg z=<z> --arg facing=0

Steps:

{ let = { h = "who" } }
{ write = "tf(h) + 0x0", type = "f32", value = "x" }
{ write = "tf(h) + 0x4", type = "f32", value = "y" }
{ write = "tf(h) + 0x8", type = "f32", value = "z" }
{ write = "tf(h) + 0x10", type = "f32", value = "0" }
{ write = "tf(h) + 0x14", type = "f32", value = "0" }
{ write = "tf(h) + 0x18", type = "f32", value = "sin(rad(facing) / 2)" }
{ write = "tf(h) + 0x1c", type = "f32", value = "cos(rad(facing) / 2)" }
{ wait = 2 }
{ show = "f32(tf(h) + 0x0)", label = "x" }
{ show = "f32(tf(h) + 0x4)", label = "y" }
{ show = "wrap(deg(heading(h)))", label = "heading_deg" }

drop-human

Teleport a human high above a point and let it fall: read its height as it drops and where it lands.

Writes the human's transform as teleport does, with z height metres above the ground height you give, then reads z after 1, 10, 20 and 40 updates. A human that is not standing on anything falls under the character step's gravity: in the run below the height went 5.223, 4.596, 2.244 and 0.223 at 1, 10, 20 and 40 updates, the ground it stopped on. Pick a pedestrian by name (humans lists them); one that is walking is moved off its route.

Parameters who (human): the human's name; empty for the player; x (number): metres; y (number): metres; ground (number): the ground's height at x, y (read it with position); height (number, default 5): metres above the ground to drop from
Preconditions patches: none; emulation running
Evidence confirmed (runtime)
Ghidra 0x0021b0b8 Object_SetPosition, 0x0023d440 Human_SetTransform
Research research/characters.md#ground, research/characters.md#teleport
Confirmed at runtime 2026-10-21, copy pcsx2, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run drop-human --copy COPY --agent ID --arg who=<who> --arg x=<x> --arg y=<y> --arg ground=<ground> --arg height=5

Steps:

{ let = { h = "who" } }
{ write = "tf(h) + 0x0", type = "f32", value = "x" }
{ write = "tf(h) + 0x4", type = "f32", value = "y" }
{ write = "tf(h) + 0x8", type = "f32", value = "ground + height" }
{ wait = 1 }
{ show = "f32(tf(h) + 0x8)", label = "z_after_1" }
{ wait = 9 }
{ show = "f32(tf(h) + 0x8)", label = "z_after_10" }
{ wait = 10 }
{ show = "f32(tf(h) + 0x8)", label = "z_after_20" }
{ wait = 20 }
{ show = "f32(tf(h) + 0x8)", label = "z_after_40" }

health

Read a human's health, maximum, power, stamina and rage.

Health and its maximum are s16s in the human's record (+0x144, +0x146), the power meter is +0x148 and stamina +0x14a; rage is the human's s32 at +0x650 (up to the class maximum, 78 for class 6).

Parameters who (human): the human's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00237848 Human_SetHealth, 0x00264cf8 Human_AddRage
Research research/characters.md#the-record, research/combat-rage.md#rage
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run health --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who" } }
{ show = "s16(rec(h) + 0x144)", label = "health", format = "int" }
{ show = "s16(rec(h) + 0x146)", label = "health_max", format = "int" }
{ show = "s16(rec(h) + 0x148)", label = "power", format = "int" }
{ show = "s16(rec(h) + 0x14a)", label = "stamina", format = "int" }
{ show = "s32(h + 0x650)", label = "rage", format = "int" }

set-health

Set a human's health (record +0x144, s16).

A plain write sets the value as HuSetHealth (0x00237848) stores it, without the binding's checks: 0 or less is stored as given, and the human dies at its next damage update. Keep it at or under the maximum (+0x146, health reads it); HuSetMaxHealth (0x00237be0) changes that. For a death through the game's own path, run lua with HuKill(handle).

Parameters who (human): the human's name; empty for the player; health (number): the new health
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x00237848 Human_SetHealth
Research research/characters.md#the-record
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run set-health --copy COPY --agent ID --arg who=<who> --arg health=<health>

Steps:

{ let = { h = "who" } }
{ write = "rec(h) + 0x144", type = "s16", value = "health" }
{ wait = 2 }
{ show = "s16(rec(h) + 0x144)", label = "health", format = "int" }

set-rage

Fill (or empty) a human's rage meter (human +0x650, s32).

Rage is the s32 at human +0x650, added to by Human_AddRage (0x00264cf8) up to the class maximum (78 for the player's class 6). Each gain sets the hold timer +0x648 to the game time plus 5000 ms, and the meter drains once it has passed, so the recipe sets the timer too: the meter then holds for 5 s, long enough for L1 + R1 to start rage (Rage_Enter 0x00284340), as the rage pass did.

Parameters who (human): the human's name; empty for the player; rage (number, default 78): the new value (78 is full for class 6)
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x00264cf8 Human_AddRage, 0x00284340 Rage_Enter
Research research/combat-rage.md#rage
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run set-rage --copy COPY --agent ID --arg who=<who> --arg rage=78

Steps:

{ let = { h = "who" } }
{ write = "h + 0x650", type = "s32", value = "rage" }
{ write = "h + 0x648", type = "s32", value = "u32(game_time) + 5000" }
{ wait = 2 }
{ show = "s32(h + 0x650)", label = "rage", format = "int" }

brain

A human's AI: brain type, gang, goal stack (type ids), target and whether it is suspended.

The brain (0x006d53f0 + index * 0x2f0) holds the type at +0x04 (0 player, 1 cop, 2 gang, 3 Warrior), the dead and suspended bytes +0x09 / +0x0a, the goal stack top at +0x2c (-1 empty) over ten goal pointers at +0x40 (their type ids are goal types), the target handle at +0x124 and the gang pointer at +0x20c (gang records of 0xb10 from 0x005e6e30, id at +0x30). To change the AI, use the script's own bindings through lua (BrFlushGoals, GoalFight, BrSetType, BrSuspend, ...), which keep the brain's lists consistent.

Parameters who (human): the human's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00293b28 Brains_Update, 0x0028f928 Brain_OnEvent
Research research/ai.md#brain, research/ai-gangs.md#gang-record
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run brain --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who", b = "brain(who)" } }
{ show = "s32(b + 0x04)", label = "brain_type", format = "int" }
{ show = "u8(b + 0x09)", label = "dead", format = "int" }
{ show = "u8(b + 0x0a)", label = "suspended", format = "int" }
{ show = "min(u32(b + 0x20c), 1) * (s16(u32(b + 0x20c) + 0x30) + 1) - 1", label = "gang", format = "int" }
{ show = "s8(b + 0x2c)", label = "goal_top", format = "int" }
{ show = "goal_at(b, 0)", label = "goal_0", format = "int" }
{ show = "goal_at(b, 1)", label = "goal_1", format = "int" }
{ show = "goal_at(b, 2)", label = "goal_2", format = "int" }
{ show = "hidx(u32(b + 0x124))", label = "target_index", format = "int" }

held-object

What a human holds: the handle in its hand (+0x338) and the object being picked up (+0x33c).

The object in a human's hand is the handle at +0x338 (the null handle 0xffffffff when empty), the one being picked up +0x33c, and a carried prop +0x348 / +0x34c. To give a weapon, put the weapon object in front of the human and let it pick it up (the weapon pickup scenarios do), or spawn one with lua (ObjSpawn): HuGiveWeapon (0x00299470) is called by no script and what it feeds is not traced.

Parameters who (human): the human's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (code)
Ghidra 0x00299470 Human_SetPendingWeapon
Research research/characters.md#the-human-object, research/combat-moves.md
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); empty hands: 0xffffffff
uv run --project python coney-tools pcsx2 recipe run held-object --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who" } }
{ show = "u32(h + 0x338)", label = "in_hand" }
{ show = "hidx(u32(h + 0x338))", label = "in_hand_index", format = "int" }
{ show = "u32(h + 0x33c)", label = "picking_up" }

Spawning

  • spawn-human: Create a human in front of the player, of the type and gang of a human already there (a pedestrian or a gang member).

spawn-human

Create a human in front of the player, of the type and gang of a human already there (a pedestrian or a gang member).

The runner reads the player's position and heading, works out a point distance metres in front, and runs HuCreate(name, type, {x, y, z}, heading, nil, 0, gang, nil) through lua, as a level script creates its characters (0x00358428 -> Human_Create 0x00233d60: a free slot of the 60, Human_Init 0x00218008, a 2.5 m ground snap). The type (human +0xd0) and the gang id (brain +0x20c -> gang record +0x30) are copied from the human named by like, so the model is already resident: a Civilians* human gives a pedestrian, a gang member (Vermin, a Hurricane) a member of that gang. The new human faces the player. Its handle is left in the global coney_spawned (lua-globals coney); HuCreate returns the null handle when all 60 slots are taken. Remove it with lua HuDelete(coney_spawned).

Parameters name (text, default ConeyPed): the new human's name (15 characters at most); like (human, default Civilians0): an existing human whose type and gang the new one takes; distance (number, default 1.5): metres in front of the player
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x00358428 lua_HuCreate, 0x00233d60 Human_Create
Research research/characters.md#creation
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); a pedestrian like Civilians0 appeared 1.5 m in front of Rembrandt, facing him
uv run --project python coney-tools pcsx2 recipe run spawn-human --copy COPY --agent ID --arg name=ConeyPed --arg like=Civilians0 --arg distance=1.5

Steps:

{ let = { p = "player", a = "heading(player)" } }
{ let = { x = "f32(tf(p)) - distance * sin(a)", y = "f32(tf(p) + 0x4) + distance * cos(a)", z = "f32(tf(p) + 0x8) + 0.5" } }
{ let = { type = "s32(like + 0xd0)", gang = "s16(u32(brain(like) + 0x20c) + 0x30)", facing = "deg(a) + 180" } }
{ lua = "coney_spawned = HuCreate("{name}", {type}, {{{x:.3f}, {y:.3f}, {z:.3f}}}, {facing:.1f}, nil, 0, {gang}, nil)" }
{ require = "1 - min(result, 1)", message = "HuCreate's chunk failed" }
{ let = { h = "human(name)" } }
{ show = "h", label = "human" }
{ show = "index(h)", label = "index", format = "int" }
{ show = "f32(tf(h))", label = "x" }
{ show = "f32(tf(h) + 0x4)", label = "y" }
{ show = "wrap(deg(heading(h)))", label = "heading_deg" }

The crew

  • crew: Read a war chief's crew: the gang's tactic and the command it belongs to, the command bytes, and each member's goals.
  • crew-command: Give the crew a Warrior command from the script's side (IssueWarriorCommand), then read back the tactic and the members' goals.
  • crew-command-menu: Give the crew a Warrior command through the pad, as a player does: hold R2, push the right stick to a slot, let go.
  • spawn-crew: Add a Warrior to the war chief's crew: create one next to the player, in the player's gang, and read the crew back.

crew

Read a war chief's crew: the gang's tactic and the command it belongs to, the command bytes, and each member's goals.

A war chief (a player human whose +0x3ac is 1) orders his gang, the crew. His brain's gang pointer (+0x20c) is a gang record (0x005e6e30 + id * 0xb10): id +0x30, alert +0x32, the current tactic +0x40 (0 for none), the leader +0x44 and sixteen member handles from +0x48. A command's tactic is told by its vtable (+0x1c): follow 0x005438c0, attack 0x00544160, defend 0x00543800, hold 0x005439e0 (0x00543980 when hiding), scatter 0x00543d40, wreck 0x005441c0, which the command line gives as the command number (0 follow, 1 attack, 2 defend, 3 hold, 4 scatter, 5 wreck; -1 no tactic). The members come from members: the handle list's humans with the brain's goal stack bottom first (type ids, goal types: 52 is FollowAndAttack) and the target. The game state record (*0x0051489c) holds the command bytes: +0x411 commands locked, +0x414 + player the out-of-range flag, +0x41a + player the command in force (stored when its tactic starts), +0x41c + player the last command asked for (stored after the enabled check and before the range check: a command refused for range shows only here, a disabled one in neither), +0x41e + player * 7 + command the enabled bytes (a command is refused unless its byte is set; enabled gives them as bits, bit 0 follow) and +0x42e + player the menu lock.

Parameters who (human): the war chief's name; empty for the player
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x0041c4e0 WarChief_IssueCommand, 0x0016c388 Gang_Find, 0x00165640 Gang_SetTactic
Research research/ai-gangs.md#warrior-commands, research/ai-gangs.md#gang-record, research/ai-gangs.md#tactics
Confirmed at runtime 2026-10-21, copy pcsx2, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6), a Warrior spawned into the crew
uv run --project python coney-tools pcsx2 recipe run crew --copy COPY --agent ID --arg who=<who>

Steps:

{ let = { h = "who" } }
{ let = { g = "u32(brain(h) + 0x20c)", p = "s8(h + 0x1b0)" } }
{ let = { t = "u32(g + 0x40)" } }
{ show = "s32(h + 0x3ac)", label = "war_chief", format = "int" }
{ show = "s16(g + 0x30)", label = "gang", format = "int" }
{ show = "g", label = "gang_record" }
{ show = "s16(g + 0x32)", label = "alert", format = "int" }
{ show = "hidx(u32(g + 0x44))", label = "leader_index", format = "int" }
{ show = "t", label = "tactic" }
{ show = "u32(t + 0x1c) * min(t, 1)", label = "tactic_vtable" }
{ show = "u32(t + 0x4) * min(t, 1)", label = "tactic_started", format = "int" }
{ show = "warrior_command(t)", label = "command", format = "int" }
{ show = "u8(game_state + 0x41a + p)", label = "command_in_force", format = "int" }
{ show = "u8(game_state + 0x41c + p)", label = "last_asked", format = "int" }
{ show = "u8(game_state + 0x41e + p * 7) | (u8(game_state + 0x41f + p * 7) << 1) | (u8(game_state + 0x420 + p * 7) << 2) | (u8(game_state + 0x421 + p * 7) << 3) | (u8(game_state + 0x422 + p * 7) << 4) | (u8(game_state + 0x423 + p * 7) << 5) | (u8(game_state + 0x424 + p * 7) << 6)", label = "enabled", format = "int" }
{ show = "u8(game_state + 0x411)", label = "commands_locked", format = "int" }
{ show = "u8(game_state + 0x42e + p)", label = "menu_locked", format = "int" }
{ show = "u8(game_state + 0x414 + p)", label = "out_of_range", format = "int" }
{ members = "g + 0x48" }

crew-command

Give the crew a Warrior command from the script's side (IssueWarriorCommand), then read back the tactic and the members' goals.

The same path as the command menu and WCIssueCommand: the dispatcher 0x0041c4e0 refuses a command that is disabled for the player (the enabled bytes of crew; a level's script disables them, level 99's street has all seven off), so the recipe first enables it with WCEnableCommand(handle, command, true) when enable is 1. Then IssueWarriorCommand(command, forced) runs as player 1's chief and the gang's tactic is replaced by the command's, whose Start gives each member his goal on the first gang update after. forced 1 restarts the command even when it is the current one and says nothing; 0 behaves as the menu does (the chief speaks and plays his gesture, the crew's range is checked). Commands: 0 follow, 1 attack, 2 defend, 3 hold, 4 scatter, 5 wreck. The chief's own brain switches attack and defend back to follow 1.5 s after no member has an enemy (WarChief_AutoCommand 0x00303988, while game state +0x431 is set), which in a street with nobody to fight happens within seconds; keep 1 switches that off first with WCEnableAutomaticSwitching(false) (restore it with lua WCEnableAutomaticSwitching(true)). The recipe waits 10 updates and runs crew's readings.

Parameters command (number): 0 follow, 1 attack, 2 defend, 3 hold, 4 scatter, 5 wreck; forced (number, default 1): 1 restarts it silently, 0 as the menu does (the chief speaks); enable (number, default 1): 1 to enable the command for the chief first; keep (number, default 1): 1 to switch the chief's automatic switching back to follow off first
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x0041c4e0 WarChief_IssueCommand, 0x0041c2d0 GameState_IssueWarriorCommand, 0x0041dbe8 WCEnableCommand
Research research/ai-gangs.md#warrior-commands, references/bindings/character.md#issuewarriorcommand
Confirmed at runtime 2026-10-21, copy pcsx2, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6), a Warrior spawned into the crew
uv run --project python coney-tools pcsx2 recipe run crew-command --copy COPY --agent ID --arg command=<command> --arg forced=1 --arg enable=1 --arg keep=1

Steps:

{ let = { h = "player" } }
{ lua = "if {keep} == 1 then WCEnableAutomaticSwitching(0) end if {enable} == 1 then WCEnableCommand(HuGetPlayer(0), {command}, 1) end IssueWarriorCommand({command}, {forced})" }
{ require = "1 - min(result, 1)", message = "the Lua chunk failed" }
{ wait = 10 }
{ let = { g = "u32(brain(h) + 0x20c)" } }
{ show = "warrior_command(u32(g + 0x40))", label = "command", format = "int" }
{ show = "u8(game_state + 0x41a + s8(h + 0x1b0))", label = "command_in_force", format = "int" }
{ members = "g + 0x48" }

crew-command-menu

Give the crew a Warrior command through the pad, as a player does: hold R2, push the right stick to a slot, let go.

The command menu is Player_UseItemCommand (0x002843f8): R2 held (pad command 1) opens the display (WarCommandDisplay_Open 0x001a6d28, which also switches the camera's right stick off), the display reads the right stick every update and highlights a slot, and R2's release (pad command 2) issues the highlighted slot through the dispatcher 0x0041c4e0 (unforced, so the chief speaks and the crew's range is checked). The six slots lie 60 degrees apart, clockwise from up: 0 up follow, 1 up-right defend, 2 down-right scatter, 3 down hold, 4 down-left wreck, 5 up-left attack. A push counts only past about 87 % deflection (radius 110 of 127), so the stick is held at full deflection toward the slot; for the two vertical slots it is nudged 4 % to the right, because a stick exactly up (raw x of 128) leaves the highlight where it was (exactly down works; the nudge does no harm there). With scripted-pad and right-stick the recipe writes R2 and the stick into the pad bytes (as pad does), holds them hold updates, reads the display (the shown flag +0x1554 and the highlight +0x1540 of the panel's display at +0x1ef0), then releases R2 and lets the stick go. enable 1 first enables what the menu needs: the slot's command (WCEnableCommand), the display (HUDShowWarCommand) and the pad commands 1 and 2 for the player (EnableCommand; level 99's street leaves R2 off, and a disabled command is matched but kept in the record's pending +0x24, so the human never sees it). The state needs the scripted-pad, right-stick and call-brains patches.

Parameters slot (number): the menu slot 0-5: 0 up (follow), 1 up-right (defend), 2 down-right (scatter), 3 down (hold), 4 down-left (wreck), 5 up-left (attack); hold (number, default 20): updates R2 is held with the stick pushed; enable (number, default 1): 1 to enable the slot's command for the chief first
Preconditions patches: scripted-pad, right-stick, call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x002843f8 Player_UseItemCommand, 0x001a6d28 WarCommandDisplay_Open, 0x001a6c58 WarCommandDisplay_Issue
Research research/hud.md#warrior-command-menu, research/ai-gangs.md#warrior-commands
Confirmed at runtime 2026-10-21, copy pcsx2, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6), a Warrior spawned into the crew
uv run --project python coney-tools pcsx2 recipe run crew-command-menu --copy COPY --agent ID --arg slot=<slot> --arg hold=20 --arg enable=1

Steps:

{ let = { h = "player" } }
{ let = { cmd = "(0x153420 >> (4 * slot)) & 0xf", sx = "100 * sin(rad(60 * slot)) + 4 * (1 - min(slot % 3, 1))", sy = "100 * cos(rad(60 * slot))" } }
{ lua = "if {enable} == 1 then WCEnableCommand(HuGetPlayer(0), {cmd}, 1) HUDShowWarCommand(1, 0) EnableCommand(HuGetPlayer(0), 1, 1) EnableCommand(HuGetPlayer(0), 2, 1) end" }
{ write = "0x005de3aa", type = "u16", value = "((0xffff ^ buttons("r2")) >> 8) | (((0xffff ^ buttons("r2")) & 0xff) << 8)" }
{ write = "0x005de3ac", type = "u32", value = "stick(sx) | (stick(0 - sy) << 8) | (stick(0) << 16) | (stick(0) << 24)" }
{ wait = "hold" }
{ let = { d = "u32(0x00600848 + 4 * u32(h + 0x380)) + 0x1ef0" } }
{ show = "u32(d + 0x1554)", label = "menu_shown", format = "int" }
{ show = "u32(d + 0x1540)", label = "highlighted_slot", format = "int" }
{ write = "0x005de3aa", type = "u16", value = "0xffff" }
{ wait = 1 }
{ write = "0x005de3ac", type = "u32", value = "0x80808080" }
{ wait = 10 }
{ let = { g = "u32(brain(h) + 0x20c)" } }
{ show = "cmd", label = "slot_command", format = "int" }
{ show = "warrior_command(u32(g + 0x40))", label = "command", format = "int" }
{ show = "u8(game_state + 0x41a)", label = "command_in_force", format = "int" }
{ show = "u8(game_state + 0x41c)", label = "last_asked", format = "int" }
{ members = "g + 0x48" }

spawn-crew

Add a Warrior to the war chief's crew: create one next to the player, in the player's gang, and read the crew back.

Runs HuCreate(name, type, {x, y, z}, heading, nil, 0, gang, nil) through lua as spawn-human does, with the gang taken from the player's brain (+0x20c -> gang record +0x30) and the type from the human named by like, which must be a Warrior whose model is resident (Ash, a member of the player's gang in level 99; the type is human +0xd0). The new human is a Warrior brain (type 3) in the crew and takes the goals of the command in force within 10 updates (hold one with crew-command, which turns the chief's automatic switching off). Its handle is left in the global coney_spawned. Remove it with lua HuDelete(coney_spawned). The recipe waits 10 updates and runs crew's member list.

Parameters name (text, default ConeyCrew): the new human's name (15 characters at most); like (human, default Ash): a Warrior whose type the new human takes; distance (number, default 3): metres in front of the player
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x00358428 lua_HuCreate, 0x00233d60 Human_Create
Research research/characters.md#creation, research/ai-gangs.md#gang-record
Confirmed at runtime 2026-10-21, copy pcsx2, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); Crew1 appeared 3 m in front of Rembrandt as a type 3 brain in gang 1, listed among the members, and with attack in force (automatic switching off) had its goal 52 ten updates later
uv run --project python coney-tools pcsx2 recipe run spawn-crew --copy COPY --agent ID --arg name=ConeyCrew --arg like=Ash --arg distance=3

Steps:

{ let = { p = "player", a = "heading(player)" } }
{ let = { x = "f32(tf(p)) - distance * sin(a)", y = "f32(tf(p) + 0x4) + distance * cos(a)", z = "f32(tf(p) + 0x8) + 0.5" } }
{ let = { type = "s32(like + 0xd0)", gang = "s16(u32(brain(p) + 0x20c) + 0x30)", facing = "deg(a) + 180" } }
{ lua = "coney_spawned = HuCreate("{name}", {type}, {{{x:.3f}, {y:.3f}, {z:.3f}}}, {facing:.1f}, nil, 0, {gang}, nil)" }
{ require = "1 - min(result, 1)", message = "HuCreate's chunk failed" }
{ wait = 10 }
{ show = "index(human(name))", label = "index", format = "int" }
{ show = "s32(brain(human(name)) + 0x04)", label = "brain_type", format = "int" }
{ show = "goal_at(brain(human(name)), 0)", label = "goal_0", format = "int" }
{ members = "u32(brain(p) + 0x20c) + 0x48" }

Player state

  • inventory: A player's inventory counts: flash, money, spray paint, handcuffs, cuff keys and loot.
  • give-item: Give player 1 money or any inventory item through the game's own path (callbacks and limits included).
  • stats: Player 1's statistics: the score and the six category counters' first words.

inventory

A player's inventory counts: flash, money, spray paint, handcuffs, cuff keys and loot.

The game state is *(0x0051489c) (W_GameState, 0x57c0 bytes). It holds one inventory block of 0x3f4 bytes per player from +0x480; item i is the 0x2c-byte entry at block +0x04 + i * 0x2c, its count at +0x20. Item 1 is the flash, 2 money, 3 spray-paint charges, 5 handcuffs, 6 handcuff keys and 10 stolen loot. The money is what the HUD shows; the cash a human carries for a mugging is another field (human +0x370, HuGetMoney).

Parameters slot (number, default 0): the player: 0 for player 1, 1 for player 2
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x0041e5b0 Inventory_AddItem, 0x0041ee88 Inv_GetMoney
Research research/player-state.md#inventory
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); money read 36, the HUD's $36
uv run --project python coney-tools pcsx2 recipe run inventory --copy COPY --agent ID --arg slot=0

Steps:

{ let = { inv = "u32(0x0051489c) + 0x480 + slot * 0x3f4 + 0x04" } }
{ show = "s32(inv + 1 * 0x2c + 0x20)", label = "flash", format = "int" }
{ show = "s32(inv + 2 * 0x2c + 0x20)", label = "money", format = "int" }
{ show = "s32(inv + 3 * 0x2c + 0x20)", label = "spray_paint", format = "int" }
{ show = "s32(inv + 5 * 0x2c + 0x20)", label = "handcuffs", format = "int" }
{ show = "s32(inv + 6 * 0x2c + 0x20)", label = "cuff_keys", format = "int" }
{ show = "s32(inv + 10 * 0x2c + 0x20)", label = "loot", format = "int" }

give-item

Give player 1 money or any inventory item through the game's own path (callbacks and limits included).

Runs GiveMoney(amount, 1) (0x0037b458 -> GameState_GiveMoney 0x0041ef00) for money, or for another item InvAddItem's worker Inventory_AddItem(inventory, player, item, amount, notify) (0x0041e5b0) through call, which keeps the count within the item's limits and runs the pickup callbacks as a pickup does. A negative amount takes. Writing the count straight into the inventory (see inventory) skips the limits and the callbacks.

Parameters item (number, default 2): the item id: 1 flash, 2 money, 3 spray paint, 5 handcuffs, 6 cuff keys; amount (number, default 100): how many (negative takes)
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x0041e5b0 Inventory_AddItem, 0x0041ef00 GameState_GiveMoney
Research research/player-state.md#pickup-callback, research/player-state.md#inventory
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); item 2, 100: money 36 to 136; item 1, 1: flash 0 to 1 (inventory read back)
uv run --project python coney-tools pcsx2 recipe run give-item --copy COPY --agent ID --arg item=2 --arg amount=100

Steps:

{ let = { gs = "u32(0x0051489c)" } }
{ call = "0x0041e5b0", args = ["gs + 0x480", "0", "item", "amount", "1"] }
{ show = "s32(gs + 0x480 + 0x04 + item * 0x2c + 0x20)", label = "count", format = "int" }

stats

Player 1's statistics: the score and the six category counters' first words.

The stats object at 0x006fe490 keeps a 0xc0-byte live record per player (+0x000 player 1, +0x0c0 player 2; checkpoint copies at +0x180 and +0x240, Stats_Construct 0x004227d8). The score is the record's +0x00 (the HUD's number); each category keeps its event counters from mission +0x08, bonus +0x14, style +0x24, combat +0x58, crime +0x78 and harmony +0xa8. What each event counts is in Statistics. To add to one through the game, run lua with StatAdd(HuGetPlayer(0), group, stat, amount) (0x004224d8).

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x004227d8 Stats_Construct, 0x004224d8 Script_StatAdd
Research research/player-state.md#statistics
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); score read 1133, the HUD's 0001133
uv run --project python coney-tools pcsx2 recipe run stats --copy COPY --agent ID

Steps:

{ let = { rec = "0x006fe490" } }
{ show = "s32(rec + 0x00)", label = "score", format = "int" }
{ show = "u16(rec + 0x08)", label = "mission_0", format = "int" }
{ show = "u16(rec + 0x14)", label = "bonus_0", format = "int" }
{ show = "u16(rec + 0x24)", label = "style_0", format = "int" }
{ show = "u16(rec + 0x58)", label = "combat_0", format = "int" }
{ show = "u16(rec + 0x78)", label = "crime_0", format = "int" }
{ show = "u16(rec + 0xa8)", label = "harmony_0", format = "int" }

Input

  • pad: Hold buttons and both sticks on player 1's pad for some updates, then let go (closed-loop drivers).
  • stick-magnitude: Make full-deflection stick input read as a chosen magnitude, by rewriting the stick table's ends.
  • puppet-command: Make an AI human act on a command id for one update (attack, grab, block), as a puppet.

pad

Hold buttons and both sticks on player 1's pad for some updates, then let go (closed-loop drivers).

With the scripted-pad patch (and right-stick for the right stick) the pad read takes player 1's buttons and sticks from spare bytes after the scePadRead buffer: the lbu loads at 0x00149dd4, 0x00149dd8, 0x00149de8 and 0x00149df0 (DualShock 2 path; 0x00149e8c... in the digital path) read offsets 0x22, 0x23, 0x26, 0x27 instead of 2, 3, 6, 7. So 0x005de3aa / 0x005de3ab are the active-low buttons (SELECT ... LEFT, then L2 ... SQUARE) and 0x005de3ac-0x005de3af the right stick's x, y and the left stick's x, y. The bytes stay until written again. Sticks are given as an input script's percentages (y up) and turned into bytes exactly as Coney does (160 + round(0.95 v) above 0, 95 - round(0.95 |v|) below, 128 at rest), so both games see the same stick; use partial deflections (Drive it like a gamepad). The patch must be in the state before the code is compiled: pcsx2 prepare-state SOURCE OUT --patch scripted-pad right-stick, then launch OUT. Find the player as the human whose +0x1b0 is 0 (find-player); the per-player record is indexed by its +0x92. For input that does not depend on what the game shows, use an input recording instead (Input recordings).

Parameters press (text): buttons held, Coney's names joined by + (cross+l2); empty for none; lx (number, default 0): left stick x, percent (-100 to 100); ly (number, default 0): left stick y, percent, up positive; rx (number, default 0): right stick x, percent; ry (number, default 0): right stick y, percent, up positive; updates (number, default 30): how many character updates to hold it (1/30 s each)
Preconditions patches: scripted-pad, right-stick; emulation running
Evidence confirmed (runtime)
Ghidra 0x00149da0 PadPort_ReadPressure
Research guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); left stick 60 % left for 30 updates: the pad record read -0.6, the player walked (gait 2, 1.63 m/s) and stood after the release
uv run --project python coney-tools pcsx2 recipe run pad --copy COPY --agent ID --arg press=<press> --arg lx=0 --arg ly=0 --arg rx=0 --arg ry=0 --arg updates=30

Steps:

{ let = { held = "0xffff ^ buttons(press)" } }
{ write = "0x005de3aa", type = "u16", value = "(held >> 8) | ((held & 0xff) << 8)" }
{ write = "0x005de3ac", type = "u32", value = "stick(rx) | (stick(0 - ry) << 8) | (stick(lx) << 16) | (stick(0 - ly) << 24)" }
{ wait = 1 }
{ show = "f32(0x005dd810 + 0x08)", label = "stick_x" }
{ show = "f32(0x005dd810 + 0x0c)", label = "stick_y" }
{ wait = "updates" }
{ show = "f32(player + 0x1ac)", label = "speed" }
{ show = "s32(player + 0x1a8)", label = "gait", format = "int" }
{ write = "0x005de3aa", type = "u16", value = "0xffff" }
{ write = "0x005de3ac", type = "u32", value = "0x80808080" }

stick-magnitude

Make full-deflection stick input read as a chosen magnitude, by rewriting the stick table's ends.

The game turns each raw stick byte into a float through a 256-entry float table at 0x0050b8d0 (entry 0 is -1.0, entry 255 is +1.0); it is data, so a write takes effect at once. Writing -m to entry 0 and +m to entry 255 (the table's address plus 0x3fc) makes a fully deflected stick (a keyboard key, a pad at its stop) read as magnitude m; the pad record (0x005dd810, the stick as floats at 0x08 and 0x0c) shows what the game uses. The camera's right-stick code (0x00129050) reads the raw bytes, so the table does not change it. Run it again with magnitude=1 to restore the table. With the scripted-pad patch, pad gives partial values directly and this is not needed. State the magnitude with every runtime claim that depends on input.

Parameters magnitude (number, default 1): the stick value full deflection gives (1 restores the table)
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x00129050 Cam_Follow_RightStick
Research guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); 0.5 written and read back, then restored to 1.0
uv run --project python coney-tools pcsx2 recipe run stick-magnitude --copy COPY --agent ID --arg magnitude=1

Steps:

{ write = "0x0050b8d0", type = "f32", value = "0 - magnitude" }
{ write = "0x0050b8d0 + 0x3fc", type = "f32", value = "magnitude" }
{ show = "f32(0x0050b8d0)", label = "entry_0" }
{ show = "f32(0x0050b8d0 + 0x3fc)", label = "entry_255" }

puppet-command

Make an AI human act on a command id for one update (attack, grab, block), as a puppet.

Player_UpdateActions (0x00146078) runs for every human whose per-player +0x1e is 0, AI humans included, and acts on the command id in its per-player record (0x00660f50 + index * 0x2c, +0x20). For a human with no pad, 0x00146000 clears +0x20 every update; the puppet patch puts a nop over that sw zero,0x20(a0) at 0x00146030, so a command written there stays. The recipe writes command for one update, then for the next and 0 after, which is one tap (commands: a cross tap is 0x12, then 0x10, then 0). A grab needs a target: write the player's handle (human +0x90) to the puppet's human +0xc8 and its brain's +0x124 (target=1 does), or command 70 misses. A held button (R1 to block) is read from a pad record instead: give the puppet pad index 4 (per-player +0x19) and write the button into all eight u16 entries of pad record 4's history (0x005dd950 + 0x1c) every update. Set CfgAutoLockAndCombat (0x005104b8) to 0 when the player must not turn to face the attacker. Pick the puppet by name: an ally's hits play reactions without damage.

Parameters who (human): the puppet's name (humans lists them); command (number): the first update's command id (combat.md#commands), such as 0x12; then (number, default 0): the second update's command id (0x10 after 0x12 for a cross tap); 0 none; target (number, default 0): 1 to aim it at the player first (human +0xc8, brain +0x124)
Preconditions patches: puppet; emulation running
Evidence confirmed (runtime)
Ghidra 0x00146078 PlayerRecord_Update, 0x00146000 PlayerRecord_ClearCommand
Research guides/research-workflow.md#driving-pcsx2, research/combat.md#commands
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); PoizoCiv given 0x12 then 0x10, aimed at the player: its anim id went from 0x29f to 11, ANIM_ATTACK_X1
uv run --project python coney-tools pcsx2 recipe run puppet-command --copy COPY --agent ID --arg who=<who> --arg command=<command> --arg then=0 --arg target=0

Steps:

{ let = { h = "who", aim = "target * u32(player + 0x90) + (1 - target) * u32(who + 0xc8)" } }
{ write = "h + 0xc8", type = "u32", value = "aim" }
{ write = "brain(h) + 0x124", type = "u32", value = "target * aim + (1 - target) * u32(brain(who) + 0x124)" }
{ write = "prec(h) + 0x1e", type = "u8", value = "0" }
{ write = "prec(h) + 0x20", type = "u32", value = "command" }
{ wait = 1 }
{ write = "prec(h) + 0x20", type = "u32", value = "then" }
{ wait = 1 }
{ write = "prec(h) + 0x20", type = "u32", value = "0" }
{ wait = 2 }
{ show = "s32(rec(h) + 0x20)", label = "clip" }

Calling the game

  • call: Make the game call one of its own functions with up to six integer or pointer arguments.
  • lua: Run a Lua chunk in the level's script state, as a level script would: any script binding, any global.
  • lua-globals: List the script's global variables (the mission script's state) whose names hold a pattern.
  • human-event: Send a game event (an event record with an id) to a human: its script handler, its gang, then its brain.

call

Make the game call one of its own functions with up to six integer or pointer arguments.

The call-brains hook sits at Brains_Update's entry (0x00293b28, step 5 of the character step) and checks the call block at 0x000af000 every update: the function's address at +0x0 (0 when none waits), six arguments from +0x4 (passed in a0-a3, t0, t1), the result at +0x1c and a count of calls made at +0x20. The runner writes the arguments, then the address; the hook clears the address, makes the call on the game's thread, stores v0 and counts it. Only integer and pointer arguments: a function that takes a float in f12 gets whatever is there, so call such functions through their script binding with lua. The state must have the hook installed (pcsx2 prepare-state ... --patch call-brains) and the game must be running.

Parameters function (expr): the function's address, such as 0x0041abe8; a0 (expr, default 0): first argument (an expression: 0x10, player, u32(player + 0x90)); a1 (expr, default 0): second argument; a2 (expr, default 0): third argument; a3 (expr, default 0): fourth argument
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x00293b28 Brains_Update
Research guides/research-workflow.md#hooks
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); GetCheckPoint's worker 0x0041abe8 returned the checkpoint the game state holds
uv run --project python coney-tools pcsx2 recipe run call --copy COPY --agent ID --arg function=<function> --arg a0=0 --arg a1=0 --arg a2=0 --arg a3=0

Steps:

{ call = "function", args = ["a0", "a1", "a2", "a3"] }
{ show = "result", label = "result" }

lua

Run a Lua chunk in the level's script state, as a level script would: any script binding, any global.

ScriptSystem *0x00512b04 holds the level's lua_State at +0x04; its vtable slot +0x2c is ScriptSystem_DoString (0x00356a78), which runs a string with lua_dobuffer. The runner writes the code into spare memory at 0x000c0000 (zeros in every state checked) and has the call hook call ScriptSystem_DoString(system, code, 0); the result is 0 when the chunk ran without error. A chunk can call every binding the level scripts use (script bindings): HuCreate, HuKill, GiveMoney, StatAdd, SetCheckPoint, MenuLoadLevel, GoalFight, Teleport, and the level's own functions (P2.SetupLesson1()). Lua 4 syntax: local, %upvalues, no #. The text is a Python format string: {name} takes a bound value, so write a Lua table constructor's braces doubled, {{x, y, z}}.

Parameters code (text): the Lua chunk, such as 'GiveMoney(100)'
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x00356a78 ScriptSystem_DoString, 0x00293b28 Brains_Update
Research research/scripting.md, guides/research-workflow.md#hooks
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); set a global and read it back with lua-globals; GiveMoney(5) took the inventory money from 191 to 196
uv run --project python coney-tools pcsx2 recipe run lua --copy COPY --agent ID --arg code=<code>

Steps:

{ lua = "{code}" }
{ show = "result", label = "lua_dobuffer" }

lua-globals

List the script's global variables (the mission script's state) whose names hold a pattern.

The globals table is ScriptSystem *0x00512b04 -> lua_State +0x04 -> globals +0x44. A Lua 4 table (Hash) holds its node array at +0x0 and its node count as the u16 at +0x6 (the u16 at +0x4 is its tag, 4); a node is 40 bytes: the key TObject (type at +0x0, value at +0x8), the value TObject at +0x10 (type +0x10, value +0x18) and the chain pointer at +0x20. Types: 1 nil, 2 number (a double), 3 string (its text at +0x10 of the string), 4 table, 5 function. Level scripts keep their state in globals (bWarriorsFight, vFenceSection), so this shows how far a mission has gone. A table value prints as its address; its nodes read the same way.

Parameters pattern (text): part of the names to show (case ignored); empty for all
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x00356a78 ScriptSystem_DoString, 0x00328a30 Lua_CallCClosure
Research research/scripting.md, guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); 1505 globals with a non-nil value; the node count u16 at +0x6 read 3500 (0xdac) and the node array held valid nodes up to there
uv run --project python coney-tools pcsx2 recipe run lua-globals --copy COPY --agent ID --arg pattern=<pattern>

Steps:

{ globals = "pattern" }

human-event

Send a game event (an event record with an id) to a human: its script handler, its gang, then its brain.

Human_OnEvent (0x0021d480, human vtable 0x0053f088 slot +0x44) delivers an event record to the human's Lua message handler (ScriptHandler_Deliver 0x00384c38, which reads the id at +0x20), then to its gang (Gang_OnEvent 0x00164c20), then to its brain (Brain_OnEvent 0x0028f928), stopping at the first that consumes it; the result is 1 when one consumed it. The brain's own handler is the function at brain +0x28 (with this adjusted by the s16 at +0x24): WarriorBrain_OnEvent 0x003063b0 for a Warrior, PlayerBrain_OnEvent 0x00303e90 for the player. The runner builds a record at 0x000c8000 (spare memory): +0x00 the human who caused it, as the human's address (confirmed at runtime for event 0xb, which a Warrior keeps at brain +0x27c), +0x04 the other party, +0x20 the event id (the event record: 0 the prompt, 1 hit (with +0x11 set to count as an attack), 2 reset, 0xb a new enemy, ...). The brain's goals may be refilled in the same update, so check an effect that stays (a Warrior's noted enemy, its target).

Parameters who (human): the human's name; empty for the player; event (number): the event id (ai.md#event-record); causer (expr, default 0): record +0x00: the human who caused it (an address), 0 none; other (expr, default 0): record +0x04: the other party, 0 none
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x0021d480 Human_OnEvent, 0x0028f928 Brain_OnEvent, 0x00384c38 ScriptHandler_Deliver
Research research/ai.md#event-record, research/ai.md#events
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); event 0xb to Ash (a Warrior) with Civilians2 as causer: consumed, brain +0x27c held Civilians2's address and +0x280 was 1
uv run --project python coney-tools pcsx2 recipe run human-event --copy COPY --agent ID --arg who=<who> --arg event=<event> --arg causer=0 --arg other=0

Steps:

{ let = { h = "who", record = "0x000c8000" } }
{ write = "record + 0x00", type = "u32", value = "causer" }
{ write = "record + 0x04", type = "u32", value = "other" }
{ write = "record + 0x08", type = "u32", value = "0" }
{ write = "record + 0x0c", type = "u32", value = "0" }
{ write = "record + 0x10", type = "u32", value = "0" }
{ write = "record + 0x14", type = "u32", value = "0" }
{ write = "record + 0x18", type = "u32", value = "0" }
{ write = "record + 0x1c", type = "u32", value = "0" }
{ write = "record + 0x20", type = "u32", value = "event" }
{ write = "record + 0x24", type = "u32", value = "0" }
{ show = "u32(brain(h) + 0x28)", label = "brain_handler" }
{ call = "0x0021d480", args = ["h", "record"] }
{ show = "result", label = "consumed", format = "int" }

Levels and missions

  • where: Which level and checkpoint the game is in, and the game state's address.
  • load-checkpoint: Reload a level at a checkpoint, as the story does after a save or a death: SetCheckPoint, then MenuLoadLevel.

where

Which level and checkpoint the game is in, and the game state's address.

The game state *(0x0051489c) keeps the level records (0x84 bytes each from +0x14d4, the name at record +0x14), the current record's index (u16 at +0x56dc, GetCurrentLevelIndex) and the checkpoint (u16 at +0x33a, GetCheckPoint 0x0041abe8). Record 1 is level99.

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x0041abe8 Script_GetCheckPoint, 0x0015c7b0 LevelFlow_SelectLevelByName
Research research/frontend.md#the-level-table, research/level-loading.md
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); level99, checkpoint 2; GetCheckPoint through call gave 2 too
uv run --project python coney-tools pcsx2 recipe run where --copy COPY --agent ID

Steps:

{ let = { gs = "u32(0x0051489c)" } }
{ show = "gs", label = "game_state" }
{ show = "u16(gs + 0x56dc)", label = "level_index", format = "int" }
{ text = "gs + 0x14d4 + u16(gs + 0x56dc) * 0x84 + 0x14", label = "level", size = 16 }
{ show = "u16(gs + 0x33a)", label = "checkpoint", format = "int" }

load-checkpoint

Reload a level at a checkpoint, as the story does after a save or a death: SetCheckPoint, then MenuLoadLevel.

SetCheckPoint(n) (0x0037b760 -> 0x0041abc0) stores the checkpoint in the game state (+0x33a) and copies the inventories, the statistics and the object list for it; MenuLoadLevel(name) (0x0036df48 -> 0x00160d78) finds the level's record by name (0x0015c7b0), sets mode 8's next level (0x005e5d90 + 0x20) and the game state's +0x14c to 3, and the front end then loads the level, whose script starts at that checkpoint (InitLevel). The recipe runs both in one lua chunk. The load takes several seconds; run where afterwards. A save state is the faster way to come back to the same place: make one with the debug server's save (pcsx2_save_state to a file of your own), never a quick-save slot.

Parameters level (text, default level99): the level's name (docs/references/levels.md); checkpoint (number, default 1): the checkpoint, 1-based
Preconditions patches: call-brains; emulation running
Evidence confirmed (runtime)
Ghidra 0x0041abc0 Script_SetCheckPoint, 0x00160d78 LevelFlow_LoadLevelByName
Research research/level-loading.md#initlevel, research/ai.md#level99-save
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); level99 checkpoint 3 from checkpoint 2: the level reloaded and where read checkpoint 3
uv run --project python coney-tools pcsx2 recipe run load-checkpoint --copy COPY --agent ID --arg level=level99 --arg checkpoint=1

Steps:

{ lua = "SetCheckPoint({checkpoint}) MenuLoadLevel("{level}")" }

Emulation

  • pause: Pause emulation as a whole (the debug server's pause), to read or write a frozen moment.
  • resume: Resume emulation after pause or advance.
  • step: Run a number of frames from a pause, then pause again, and report how far the game's tick count moved.

pause

Pause emulation as a whole (the debug server's pause), to read or write a frozen moment.

The PCSX2-MCP fork's debug server pauses and resumes emulation as a whole (emulation pause or resume), the pause advance steps from; PINE reads and writes still work while paused. This is not the debugger's CPU break (pcsx2_pause), during which frames do not advance. The game's own pause menu is another thing again (PauseMenu_Toggle 0x00154f28, START). The emulation state is what the recipes' emulation precondition checks.

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x00154f28 PauseMenu_Toggle
Research guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); the tick count 0x005104f4 stood still while paused
uv run --project python coney-tools pcsx2 recipe run pause --copy COPY --agent ID

Steps:

{ emulation = "pause" }
{ show = "u32(0x005104f4)", label = "tick", format = "int" }

resume

Resume emulation after pause or advance.

The debug server's emulation resume. A recipe that calls the game (call, lua) needs emulation running: the call hook makes the call from inside the game's own update.

Parameters none
Preconditions patches: none; running or paused
Evidence confirmed (runtime)
Ghidra 0x003a2ea0 TaskManager_TickGame, 0x00249108 Humans_Update
Research guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6)
uv run --project python coney-tools pcsx2 recipe run resume --copy COPY --agent ID

Steps:

{ emulation = "resume" }
{ wait = 1 }
{ show = "u32(0x005104f4)", label = "tick", format = "int" }

step

Run a number of frames from a pause, then pause again, and report how far the game's tick count moved.

The debug server's frame_advance runs N emulator frames (vsyncs) and pauses. The game counts its 60 Hz ticks at 0x005104f4, about one per emulator frame (10 frames moved it by 10, 2 by 3: a frame can catch a tick up), and steps the characters on each tick that makes the count even (Tasks), so a character update takes two frames: frames=2 is one update. Emulation must be paused first (pause).

Parameters frames (number, default 1): how many frames to run (1-600)
Preconditions patches: none; emulation paused
Evidence confirmed (runtime)
Ghidra 0x003a2ea0 TaskManager_TickGame, 0x00249108 Humans_Update
Research research/tasks.md#tick, guides/research-workflow.md#driving-pcsx2
Confirmed at runtime 2026-10-21, copy pcsx2-d, PCSX2 2.9.129 (PCSX2-MCP fork), level99 street (slot 6); frames=10 moved the tick count by 10, frames=2 by 3
uv run --project python coney-tools pcsx2 recipe run step --copy COPY --agent ID --arg frames=1

Steps:

{ let = { before = "u32(0x005104f4)" } }
{ advance = "frames" }
{ show = "u32(0x005104f4) - before", label = "ticks", format = "int" }