Input: pads and player commands¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Static reading in Ghidra
(2026-10-07); runtime claims are those of the pages linked.
Purpose¶
This page indexes the Device/ps2/ code that turns the two DualShock 2 pads into what the game reads: the libpad
state machine that brings a pad into analog and pressure mode, the pad queries over the 8-sample button history, the
Lua pad handlers, and the per-player record that turns buttons into command ids. What the commands mean and how
they are matched is on Combat, Commands; the pad record and the per-frame pad update are on
Front end, Pad record and Input on the front end; the per-player record
is on Characters. Every function from 0x00144a08 to 0x00148358 and from 0x00149520 to
0x00149ef8 is listed here.
Original structure¶
The code sits in Device/ps2/, between the stub at 0x001449e8 and DS_PS2Device.cpp's stub 0x001485a8, with
the libpad layer after the file systems (0x00149520-0x00149ef8). No string names its files, so the file is
inferred from position (Source map). Names are ours; evidence is confirmed (code) at the address
unless the row says otherwise.
Pad queries¶
pad is a pad record; cur is the current button word, prev(n) the word n samples
back. The matchers of the command tables are the ones marked "table".
| Address | Name | Returns | Evidence |
|---|---|---|---|
0x00144a08 |
Pad_GetHistory(pad, n) |
prev(n), n clamped to 7 |
confirmed (code) |
0x00144a80 |
Pad_Pressed(pad, 0) |
cur & ~prev(1) |
confirmed (code) |
0x00144b88 |
Pad_IsHeld(pad, mask) |
cur & mask != 0 (table 1) |
confirmed (code) |
0x00144ba8 |
Pad_IsNewlyReleased(pad, mask) |
none of mask down now, some down one sample back (table 3) |
confirmed (code) |
0x00144bf0 |
Pad_IsNewlyPressed(pad, mask) |
some of mask down now and none one sample back (table 2) |
confirmed (code) |
0x00144c38 |
Pad_PressedRepeatMask(pad, mask) |
newly pressed, or a d-pad direction in mask whose hold counter is 15 (the masked form of the menus' auto-repeat) |
confirmed (code) |
0x00144ce0 |
Pad_TappedShort(pad, mask) |
newly released, and up in at least one of samples 2-7 back: a press of 1 to 6 samples (table 5) | confirmed (code) |
0x00144d60 |
Pad_TapOrHold4(pad, mask) |
newly released after 1 to 3 samples down, or down for exactly the 4th sample (table 6) | confirmed (code), from the instructions |
0x00144e50 |
Pad_HeldFrames(pad, mask) |
down in each of the last N samples (N = 0x0050b708, CfgButtonHeldFrames) and up in one of the samples N to 7 back (table 7) |
confirmed (code) |
0x00144ef8 |
Pad_ComboHeld(pad, a, b) |
every button of a and b down now and not all of them one sample back (table 8) |
confirmed (code) |
0x00144f48 |
Pad_ComboPress(pad, a, b) |
every button of a and b down now and not all of b one sample back (table 9) |
confirmed (code) |
0x00144f98 |
Pad_SetCurrentButtons(pad, word) |
overwrites cur: the movie skip and PlayerRec_ClearButtons use it |
confirmed (code) |
0x001476f0 |
Pad_GetPressure(pad, button) |
the pressure byte of a one-bit button mask, by the order table 0x0050b768 (right, left, up, down, triangle, circle, cross, square, L1, R1, L2, R2); 0 for other masks |
confirmed (code) |
Pads, handlers and the device hooks¶
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00144fb0 / 0x001454a8 |
Pad_Update / Pads_Update |
the per-frame update (Input on the front end) | confirmed (code) |
0x00145670 |
Pads_ResetStateMachines |
calls 0x00149ef8: every libpad record back to its first state (PM_Greet_Enter calls it) |
confirmed (code) |
0x00145698 |
Pad_SetLuaHandler(player, button, function) |
PadSetHandler: takes the pad index of the HUD's virtual pad (HUD_GetVirtualPad) for player player != 0 when that pad record's player (+0x42) is the same, otherwise player itself as the pad index, and stores the script reference at 0x005dda90 + pad × 0x40 + 4 × b, b the highest set bit of button |
confirmed (code) |
0x00145780 |
Pads_SetDisconnectCallback(fn) |
stores fn in 0x0050b704, called when a pad is pulled (State machine); Pads_Init sets Player_UnbindPad (0x0022a980) |
confirmed (code) |
0x00149520 |
Pads_Init |
scePadInit; for each of the 8 libpad records: clear, port i >> 2, slot i & 3, first state; pad records' +0x42, +0x44 = −1; opens port 0 slot 0 and port 1 slot 0 with their DMA buffers (0x005de3c0, 0x005de8c0); clears the 0x200-byte Lua handler table; sets the disconnect callback; one Pads_Update |
confirmed (code); the libpad calls are inferred from their arguments |
0x00149668 |
Pad_SetVibration(record, on, strength) |
sets the small motor (on) and the big motor's strength in the libpad record, only when the pad belongs to a player and vibration is on in the options (GameState_IsVibrationActive); otherwise both 0 |
confirmed (code) |
0x00149ef0 |
Pads_UpdateHook |
empty; Pads_Update calls it |
confirmed (code) |
0x00149ef8 |
Pads_ResetStates |
the 8 records' state back to 0x001499e8 |
confirmed (code) |
The libpad state machine¶
Each of the 8 records at 0x005de3c0 + i × 0x140 holds the libpad DMA buffer, the state function at +0x100, port
+0x104, slot +0x108, connected +0x10c, the stick bytes +0x110-+0x113 (left x, y, right x, y), the button
word +0x114, 12 pressure bytes +0x116 and the actuator settings +0x122, +0x128. Pad_Read (0x001498a8)
steps it and copies the result into the pad record. The libpad entry points are unnamed
in Ghidra; their roles below are inferred from their arguments and the SDK's documented sequence.
| Address | State | What it does | Evidence |
|---|---|---|---|
0x00149770 |
PadPort_Step |
reads the port's state: disconnected → back to the first state and, if the pad belonged to a player (+0x42 ≠ −1), calls the disconnect callback once; stable → connected; error → first state; then runs the state function |
confirmed (code); the state numbers' meanings inferred |
0x001499e8 |
first | asks the current controller id: digital (4) → 0x00149ab0, DualShock (7) → 0x00149bc0, else 0x00149a88 |
confirmed (code) |
0x00149a88 |
unsupported | neutral data, not connected | confirmed (code) |
0x00149ab0 |
digital | when the pad has other modes, → 0x00149b08; else unsupported |
confirmed (code) |
0x00149b08 / 0x00149b58 |
set analog / wait | asks for analog mode, locked; waits for the request, then starts over (the pad now reports 7) | confirmed (code) |
0x00149bc0 / 0x00149c30 |
DualShock / wait | with actuators, sets their alignment (+0x128), waits |
confirmed (code) |
0x00149c98 |
pressure check | pressure mode available → 0x00149cf0, else 0x00149e58 |
confirmed (code) |
0x00149cf0 / 0x00149d38 |
enter pressure / wait | enters pressure mode, waits | confirmed (code) |
0x00149da0 |
read with pressure | buttons, sticks and the 12 pressure bytes; sends the motors | confirmed (code) |
0x00149e58 |
read | buttons and sticks; sends the motors | confirmed (code) |
0x00149720 |
PadPort_ClearData |
sticks to 0x80 (centre), buttons and pressures 0, motors off | confirmed (code) |
So a pad that has only a digital mode gives no input (inferred), and a DualShock 2 ends in pressure mode, which the
sprint's CfgPlayerRunButton option and Pad_GetPressure read.
Commands and the per-player record¶
The script bindings are on character bindings; the tables and the matching
order on Combat, Commands; the record (0x00660f50 + i × 0x2c) on
Characters. The nine tables live at 0x005ddd10-0x005de130 with their counts at
0x0050b740-0x0050b760.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00145fa0 |
PlayerRecord_Init |
pad −1, not pad-controlled, both stick buffers 0, command cleared, input unlocked | confirmed (code) |
0x00146000 |
PlayerRecord_ClearCommand |
clears the current stick buffer, +0x1c, +0x1d and the command +0x20; with a pad, also the pad record's +0x40, +0x41 |
confirmed (code) |
0x00146058 |
PlayerRecord_ClearCommandThunk |
the same, from Brain_InstallHandlers |
confirmed (code) |
0x00146078 |
PlayerRecord_Update |
flips the buffer; with a connected pad and pad control: copies the buttons, stores the stick's angle and length (π/2 and 0 when input is locked), passes +0x1c, +0x1d to the pad record (+0x41, +0x40), then Commands_Match; without a pad or pad control it calls PlayerRecord_ClearCommand instead |
confirmed (code) |
0x001461e0 |
Commands_Reset |
ResetCommands: every entry of the nine tables to mask 0xff, command 0 |
confirmed (code) |
0x001463e0 |
PlayerCommand_EnableAllForHuman(human, on) |
EnableCommands: sets or clears the human's pad bit in the mask of every bound entry |
confirmed (code) |
0x001467a8 |
PlayerCommand_EnableForHuman(human, id, on) |
EnableCommand: the same for the entries of one command |
confirmed (code) |
0x00146b90 |
PlayerCommand_Remove(id) |
DelCommand: in each table, removes the first entry with that command and moves the rest down (the scan ends after a removal, so a second entry of the same command in one table stays) |
confirmed (code) |
0x00147218 |
Commands_IsBound(id) |
whether a command id 1-57 is in any table (AddCommand checks it) |
confirmed (code) |
0x00147640 |
PlayerRecord_ForceCommand(record, id, ms) |
the pending command +0x24 and its end time +0x28 (game time + ms) |
confirmed (code) |
0x00147660 |
PlayerCommand_ForceForHuman |
ForceCommand: for a pad-controlled human and an id 1-57 |
confirmed (code) |
0x00147738 |
PlayerCommand_IsEnabled(record, id) |
the record's pad bit in the mask of the first entry with that command; HUD_Update asks for 0xa (Crimes) |
confirmed (code) |
0x00147940 |
Commands_Match(record) |
the matcher (Combat, Commands) | confirmed (code) |
0x00147ef8 |
PlayerRecord_GetCommand |
+0x20 |
confirmed (code) |
0x00147f00 |
PlayerRecord_GetCommandOrPending |
+0x20, else +0x24 (PadSetHandlerEx) |
confirmed (code) |
0x00147f18 |
PlayerRecord_GetPressure(record, i) |
pressure byte i (0-11) of the record's pad |
confirmed (code) |
0x00147f50 |
PlayerRecord_AnyButtonDown |
the pad's current button word is not 0 (MessageBox_ClaimPad) |
confirmed (code) |
0x00147f98 |
PlayerRec_IsPadHeld(record, mask) |
Pad_IsHeld on the record's pad; 0 without a pad |
confirmed (code) |
0x00147fd0 |
PlayerRecord_IsNewlyPressed(record, mask) |
Pad_IsNewlyPressed on the record's pad |
confirmed (code) |
0x00148008 |
PlayerRecord_GetPadHistory(record, n) |
Pad_GetHistory on the record's pad (the button-tap mini-game) |
confirmed (code) |
0x00148040 |
PlayerRecord_ClearButtons(record, mask) |
removes mask from the pad's current word, so a scene's start does not also act on the press |
confirmed (code) |
0x001480a0 / 0x00148210 |
PadHandlerEx_Set / its thunk |
PadSetHandlerEx: keeps the script reference in 0x0050b73c |
confirmed (code) |
A quirk. PlayerCommand_IsEnabled scans table 5 (tapped) with ≤ its count, one entry past its end, where the
other tables use <. Confirmed (code) at 0x00147738; the extra entry is the next table slot, unused (mask 0xff,
command 0) unless table 5 is full (inferred).
Coney's implementation¶
Coney's pads are src/core/pad.h (Pad, the pad record and its queries), src/core/pads.h and
src/core/pad_handlers.h; the SDL layer replaces the libpad state machine and always reports an analog pad with
pressures. The command tables are not written yet.
Open questions¶
- The role of the pad record's
+0x44(set to −1 with+0x42;Player_UnbindPadcopies+0x42there before clearing it, so it may be the last player). - The
libpadstate numbers 1-5 inPadPort_Step(inferred from the SDK, not read in the IOP module).