Tekken 4’s moveset files, decoded and edited in game

The compression codec, the Character Data format, and four edits proven in PCSX2

How Tekken 4 (PS2) stores each fighter’s moves: an LZSS-compressed “Character Data” file of twenty tables. The format is decoded well enough to rebuild all 22 stock movesets byte for byte, and edits made with it work in game: a move’s damage changed, a button rerouted, a brand-new move added, and one fighter’s whole moveset run on another’s body.

A fighting game is mostly its movesets: which button does what, how much it hurts, what can follow it. Tekken 4 ships them as one compressed file per character, and nothing about them had been documented for the PS2 release. This entry describes the format as far as it is decoded, how each step was proven, and the four edits that showed the decoding is right in the running game.

Addresses are PS2 Emotion Engine virtual addresses in the US release (SLUS-20328). Byte dumps of Namco’s data are deliberately left out: the structure is described, not the content.

Where a moveset lives

Each fighter variant is a 28-byte row in a table at 0x30E100 (112 rows). A row names six files in the game’s DATA.BIN archive (see Tekken 4’s 2.57 GB archive keeps its index in the executable): file kN of row r is archive record 319 + 6r + N.

FileHolds
k0The Character Data (CD): the moveset, LZSS-compressed
k1The motion bank: animation clips
k2A character model used only in the Tekken Force modes
k3The character model used in normal fights
k4The sound bank, sent to the sound processor
k5Audio data kept in main memory

The moveset belongs to the character, not the costume: every group of rows that shares a k0 also shares its k1, with no exceptions across records 319 to 930.

Health is not per character at all. It’s a flat table of 21 entries at 0x309C50, every one of them 165.0 in 16.16 fixed point.

The compression codec

The decompressor is at 0x197CC8. It’s LZSS:

  • A flag byte carries seven flags, least significant bit first, behind a sentinel bit.
  • Flag 1 is a literal byte.
  • Flag 0 is a two-byte big-endian token. The distance is token & 0x7FF (0 means 2048) and the length is (token >> 11) & 31 (0 means 32). It copies forward byte by byte, so overlapping copies work.
  • A flag byte of zero ends the stream, and stock files then pad with zeros to a multiple of four.

The Character Data format

A decompressed CD starts with a 0x1A0-byte header. Offsets inside the file are relative to the end of that header.

  • A version number. Values of 8 or more make the loader skip clip relocation.
  • A “relocated” flag.
  • Strings, a list of 33 alias move ids, and per-character constants.
  • Nineteen (offset, count) pairs for tables T0 to T18, then pointers to an embedded clip bank and four more banks. The notes disagree on whether there is a twentieth table pair or that slot is the clip bank.

When the game loads a CD, a relocator at 0x19FA08 turns every offset into a pointer.

The tables that matter most are these:

  • T8, moves (52 bytes each). Each entry has a clip reference, flags, hit level, links into T5 (transitions) and T2 (damage), a frame count written by the loader, and an active-frame window. The clip reference’s top bits say where the animation lives: flag 0x4000 means the fighter’s own motion bank, 0x8000 the common bank, and neither means a clip embedded in the CD itself. A standard character has between 950 and 2,296 moves.
  • T2, hit data. Damage is T2[T8.t2] + 4, masked with 0x3FFF. At runtime it lands in the fighter structure at +0x152.
  • T5 and T6, transitions (20 bytes each). The target move id is the 16-bit value at +0x08. The last entry of a list holds the fallback move. Special values mark the end (0x8000), jumps (0x8003) and returns (0x8004), and anything above that is a motion command.
  • T1, conditions. A stream of (opcode, argument) pairs ending at opcode 0x107, dispatched through a 263-entry jump table at 0x30B270. Opcodes with bit 15 set are actions that run when a transition fires. Stock data uses 131 condition opcodes and 39 action ids; they were decoded by hand, and many are still partial or unknown.

One rule of the move lookup shapes every edit. When a move id is at or above a fighter’s own move count, getMove (0x1A0250) reads it from the opponent’s table. So a modified moveset must never have fewer moves than the stock one, and new moves are appended, never inserted, so every existing index stays put.

Proving the decoding

The format was not accepted on reading alone. Each stage was checked against something that could have disagreed:

  1. Decode by output. The first decoder (written before the real decompressor was found) was compared with live memory from a savestate. 48,208 of 65,583 words matched, and every difference was a pointer the loader rewrites.
  2. The real decompressor, emulated as above, against every stock stream.
  3. A full rebuild. A parser and writer (cdtool) rebuild each CD from its parsed tables, recomputing every offset. All 22 distinct stock CDs come out byte-identical.
  4. The real relocator. The game’s relocator was emulated and its output compared with savestate memory for four characters in eight fighter slots: identical except one word set by a call the emulation stubs out.
  5. In game. The experiments below.

In game: experiments E1 and E2

Both ran on 2026-10-21 in PCSX2, driven by a script that boots the game, steers the menus and reads memory, with no human at the controller (see Testing a PS2 game with no one at the controller).

E1: does the game accept a recompressed moveset?

TestChangeCheckResult
C0A fighter’s moveset, recompressed by the project’s encoder, otherwise unchangedThe loaded CD compared word by word with the expected relocated CDPass. 0 words differ, before and after fighting
M0The same, padded to 135,996 bytes (the staging buffer holds 137,600)As C0, on both player sidesPass. 0 words differ
B1The largest stock moveset (504,124 bytes decoded) as player 2Both CDs relocate cleanlyPass, in Practice mode only

E2: do edits made with the format behave as predicted?

The baseline was read first: on the edited fighter, Triangle plays move 279 for 12 damage and Square plays move 276 for 5. The first round tested nothing useful, because the preset targeted a move that no plain button triggers. The second round:

TestEditObservedResult
M1Move 279’s damage, 12 → 60Move 279 lands for 60; the training display reads “DAMAGE 60”Pass
M317 transitions retargeted from move 279 to 276Triangle now plays move 276 for 5 damage, Square’s stock movePass
M5A new move, 1049: move 279’s animation with another move’s 17-damage hit data, reached from TriangleTriangle plays move 1049; the opponent’s health drops 165 → 148Pass
M4Another fighter’s whole moveset (row 52’s k0 and k1) under the custom fighter’s rowsThe body runs the donor’s move ids and the fight is stablePass, with limits

The limits are real and recorded:

  • All E2 tests were player 1 only.
  • M4 did not exercise throws or being hit.
  • The project’s notes disagree about which character row 52 is: it’s labelled Kazuya in the test log and Heihachi in the audio research.
  • Practice mode refills health between hits, so damage was read from the training display or a post-hit state.

Corrections along the way

The adversarial verification and the rebuild overturned several first readings. They’re listed because each one would have broken a mod:

  • The player 2 slot is smaller than player 1’s. The first report said each player has 504,128 bytes for a decoded moveset. Only player 1 does; player 2 has 348,416 bytes before running into player 1’s motion bank, so the fighter used as a template has about 86 KB of headroom, not 240 KB.
  • The compressed staging buffer is 137,600 bytes, so a literal-only “encoder” (no compression, 299,810 bytes) can’t be used.
  • The transition target is at +0x08, not +0x0C. +0x0C holds flags.
  • Decoded sizes were overstated by decoders that ignored the sentinel bit, by up to 33 bytes. One stock moveset that was “20 bytes over” the player 2 limit actually fits.
  • “No juggle scaling” was never established. A damage reduction keyed on fields of the hit record exists; what it does is still open.
  • Practice mode does not use the fixed versus-mode memory slots that the desk research assumed.

Limits for a modified moveset

LimitValue
Compressed (staging buffer)137,600 bytes; largest stock file 137,540
Decoded, player 1504,128 bytes
Decoded, player 2348,416 bytes
Move countAt least the stock count, or move lookups leak into the opponent’s table

Still unknown

Animation is the big gap. The clip decoder (0x184508) is located, and the pose builder runs it over 23 bones × 3 channels (20 rotation bones and 3 translation bones). But the bitstream and the bone-to-joint mapping are not decoded, so a new move can reuse any existing animation but not have one of its own. The open questions below list the rest.

↑ ↓ move, Enter open Search with filters