SAS3: sprite animations
- Source
docs/formats/sas3.md- SHA-256
0d909785…aae274f4
Where they are · Header · Animation table · Sprite table · Sprites · Animations · Padding · How the game reads it · Replacing or editing animations · Rebuilding · Leads
A set of 2D animations for one sprite sheet. The sprites list rectangles (quads) of the sheet. Each animation is a timed list of frames that show a sprite, move it, play a sound or jump. Each animation has up to four directions, and a direction can reuse another's frames mirrored. It drives the units in battle (25 shared sets in battle/battle_data_release.dat #0), the actors of event scenes, the shop and class-change screens, and one picture of the Warren Report. The sheets it draws from are sprite packs or plain btx textures. Gibbed's SequenceFile reads it (and the PSP's SAS\0, version 0).
| Based on | Every SAS3 in the game's 10,016 RotXor + ZIP .dat files: 1,995 (Where they are). Gibbed's SequenceFile.cs, Sprite.cs, Cel.cs, Frame.cs, FrameData.cs, FrameType.cs and Frames/*.cs (reference_tools/Gibbed.LetUsClingTogether/projects/Gibbed.TacticsOgre.AnimationFormats/), checked 2026-10-11. The unpacked executable, SHA-256 7b0230a11498f40396c9048345e7cfe0ef442cb7ca8faffc6f197920d2669599. |
| Checked | 2026-10-11 |
| Method | A single-file C# program over the decrypted files, using the toolchain's Containers library. It finds every pakd entry starting SAS3 and walks its header, animation table, sprite table, sprites and animations. It checks every frame's fields by type and marks every byte it explains. Static analysis with Ghidra 12.1.4 of the animation player and draw code (0x1402e2820, 0x1402e3b80, 0x1402e3cb0, 0x1402e1d00, 0x1402e3340; the decompiled code in local/exe/decomp/). Not yet decoded or rebuilt by the toolchain, and no in-game test. |
Where they are
confirmed (C# scan, all 10,016 .dat files): a SAS3 is always a pakd entry, never a whole payload or inside an ashg.
| Where | Entry | Count | Its sheet |
|---|---|---|---|
battle/battle_data_release.dat #0 (unit_animation_sets) |
#0/0–#0/24 |
25 | The units' sheets in object/ (ashg.md), not traced which |
event/scene/*.dat, scene ID 0x12000 + n |
#n/1 |
1,956 | The sprite pack at #n/0 |
menu/sequence/shop_*.dat |
#1, and #1, #3, #5, #7 in shop_item_blend_tubo.dat |
12 | The btx in the entry before it |
menu/sequence/classchange.dat |
#0 |
1 | The btx entries #1, #2, not traced |
WarrenReport/WarrenReport.dat |
#86 |
1 | Not traced |
Header
16 bytes. Every offset in a SAS3 is from its start (its magic).
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 4 | raw[4] | magic |
SAS3. The game doesn't check it: every loader takes the entry by position. |
likely | C# scan (1,995); no loader compares it (How the game reads it); Gibbed SignatureV3. |
| 0x04 | 1 | u8 | scale |
Positions and quad sizes are divided by it when drawn: 4 in 1,994 files, 1 in WarrenReport.dat #86. Must not be 0. |
likely | 0x1402e3cb0, 0x1402e1d00 (1.0 / scale); C# scan; Gibbed Scale. |
| 0x05 | 3 | raw[3] | unk_0x05 |
Zeros in every file. Not read. | likely | C# scan; Gibbed requires zeros. |
| 0x08 | 4 | u32 | sprite_table_offset |
The sprite table. It also ends the animation table. | confirmed | C# scan: 16 + 16 × animations in all 1,995. |
| 0x0C | 4 | u32 | animation_table_count |
Animations × 4: the number of u32 entries in the animation table. | confirmed | C# scan: all 1,995. The actor code likely reads it as "has an animation" when over 3 (0x1403263d0). Gibbed DirectionCount. |
Animation table
At 0x10, up to sprite_table_offset: one row of four u32 per animation, one per direction, 0x10 + 16a + 4d for animation a and direction d. confirmed (C# scan): 1–118 animations per file. 14 of the 25 unit sets (#0/0–#0/11, #0/23, #0/24) have 118; the other 11 have 1–13.
| Bits | Name | Meaning | Confidence | Source |
|---|---|---|---|---|
| 1–31 | animation_offset |
The animation for this direction. 0 means none; the game treats a whole entry below 2 as none. Directions often share an animation. | likely | 0x1402d9bf0 (returns SAS3 + (entry >> 1), or none below 2); C# scan: no entry is 1. |
| 0 | mirrored |
Draw the animation mirrored left to right. The game negates x positions and swaps each frame's flip_x. |
likely | 0x1402e3cb0, 0x1402e3340; Gibbed IsFlipped. |
Sprite table
At sprite_table_offset. confirmed (C# scan): 1–137 sprites per file.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 4 | u32 | sprite_count |
Number of sprites. Not read: a frame picks a sprite by an 8-bit index, so only the first 256 can be shown. | likely | C# scan; 0x1402e3cb0, 0x1402e1d00. |
0x04 + 4n, n = 0–sprite_count − 1 |
4 | u32 | sprite_offset[n] |
Where sprite n starts. | likely | 0x1402e1d00; C# scan. |
Sprites
Each at its sprite_offset. confirmed (C# scan): the sprites follow the table back to back, in table order.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 1 | u8 | sheet |
0 in 4,679 sprites, 1 in 166. Every sprite with 1 is in one of 11 unit sets (#0/0–#0/9, #0/23). The game copies it into the animation (+0x1B) when the sprite is shown; what it selects isn't traced. |
guess | 0x1402e3cb0; C# scan. Gibbed SheetIndex. |
| 0x01 | 1 | u8 | quad_count |
Number of quads: up to 9 in the files. | likely | 0x1402e1d00; C# scan. |
| 0x02 | 2 | s16 | unk_0x02 |
Not read by the draw code found. Gibbed reads it as a centre's x. | — | 0x1402e1d00 doesn't read it. Gibbed CenterX. |
| 0x04 | 2 | s16 | unk_0x04 |
As unk_0x02; Gibbed's centre y. |
— | Gibbed CenterY. |
| 0x06 + 12n | 12 | — | quad[n] |
See Quads. | likely | 0x1402e1d00 (sprite + 6 + 12n). |
Quads
One quad (a textured rectangle) each; the draw builds one quad per record (0x1402e3340).
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 2 | s16 | x |
Where the quad is drawn, from the animation's position, in 1/scale pixels. |
likely | 0x1402e3340; Gibbed RelativeX. |
| 0x02 | 2 | s16 | y |
As x, downwards. |
likely | 0x1402e3340; Gibbed RelativeY. |
| 0x04 | 2 | u16 | width |
The quad's size, both on the sheet (in texels) and on screen (in 1/scale pixels). 3 quads in the files are 0 wide or high. |
likely | 0x1402e3340; C# scan. |
| 0x06 | 2 | u16 | height |
As width. |
likely | 0x1402e3340. |
| 0x08 | 2 | u16 | u |
The quad's left edge on the sheet, in texels. The game divides it by the sheet texture's width. | likely | 0x1402e3340; Gibbed SheetX. |
| 0x0A | 2 | u16 | v |
The top edge, divided by the texture's height. | likely | 0x1402e3340; Gibbed SheetY. |
Animations
Each at an animation_offset. confirmed (C# scan): the animations follow the sprites in ascending order of offset. They lie back to back except for 190 gaps of one or two frames (14 or 28 bytes) in 12 of the unit sets. The gaps hold well-formed frames (types 0–3) that no animation reaches: dead data, kept for a byte-identical rebuild.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 2 | u16 | frame_count |
Number of frames. The game reads only the low byte, as a signed count, so at most 127. In the files it's at least 1 and never over 127, so the high byte is always 0. | likely | 0x1402e3b80, 0x1402e2820; C# scan. |
| 0x02 | 2 | u16 | duration |
The animation ends when its tick counter reaches this, after the last frame has run. In every file it's at least the last frame's time. |
likely | 0x1402e3b80; C# scan (equal in 404 animations, greater in 3,559). |
| 0x04 + 14n | 14 | — | frame[n] |
See Frames. | likely | 0x1402e3b80 (animation + 4 + 14n). |
Frames
14 bytes. The arguments at 0x04–0x0D mean different things by type (next table). Unused argument bytes are 0 in every frame (C# scan, all 1,995 files).
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 2 | u16 | time |
The tick at which the frame runs. Frames run in order, each once the counter reaches its time. Never decreasing within an animation. |
likely | 0x1402e3b80; C# scan. |
| 0x02 | 1 | enum8 | type |
What the frame does. See Frame types. Values over 14 are skipped. | likely | 0x1402e3cb0 (switch over 0–14). |
| 0x03 | 1 | u8 | unk_0x03 |
0 in every frame. Not read. | likely | C# scan; Gibbed requires 0. |
| 0x04 | 1 | u8 | arg_0x04 |
Types 0, 2: the sprite index. Type 11: the frame index to go to. Types 10, 14: a value (0 or 1). With types 8 and 9, the low byte of a u16. | likely | 0x1402e3cb0. |
| 0x05 | 1 | s8 | flip_x |
Types 0, 2: 1 draws the sprite mirrored left to right (in type-0 frames, 0 in 11,660 and 1 in 682). A mirrored animation swaps it. With types 8 and 9, the high byte of the u16 at 0x04. |
likely | 0x1402e3cb0, 0x1402e3340. |
| 0x06 | 1 | u8 | flip_y |
Types 0, 2: 1 draws the sprite upside down. | likely | 0x1402e3cb0, 0x1402e3340. |
| 0x07 | 1 | s8 | unk_0x07 |
Types 0, 2: kept in the animation as a signed 16-bit value (+0x40); nonzero in 1,473 frames. Meaning not traced. |
likely | 0x1402e3cb0; C# scan. |
| 0x08 | 2 | s16 | x |
Types 0–3, 12: a position, in 1/scale pixels. |
likely | 0x1402e3cb0. |
| 0x0A | 2 | s16 | y |
As x. |
likely | 0x1402e3cb0. |
| 0x0C | 2 | s16 | z |
Types 2, 3: a third coordinate, mixed into x and y by constants (a height in the battle view). | likely | 0x1402e3cb0. |
Frame types
What 0x1402e3cb0 does with each type, so each row is likely. Counts are frames in the files (confirmed, C# scan). Gibbed's names are given where they differ from what the code does.
| Type | Count | Does | Uses |
|---|---|---|---|
| 0 | 12,342 | Show sprite arg_0x04 at (x, y), and mark the animation visible. |
arg_0x04, flip_x, flip_y, unk_0x07, x, y |
| 1 | 1,944 | Move to (x, y). |
x, y |
| 2 | 1,498 | Show a sprite, as 0, at a 3D offset from x, y and z. |
as 0, and z |
| 3 | 782 | Move to a 3D offset, as 2. | x, y, z |
| 7 | 314 | Hide: clears the visible mark. Gibbed: LoopEnd. |
— |
| 8 | 118 | Play a sound, by the u16 at 0x04: 56, 57 and 2,012–2,126 in the files. Gibbed: LoopStart. |
u16 at 0x04 |
| 9 | 1,406 | By the u16 at 0x04: 10 and 11 set and clear a switch (+0x66); 100 or more starts a loop of (value − 100) at this frame; 0–7 set that bit of the animation's flag byte (+0x48). Gibbed: SetFlag. |
u16 at 0x04 |
| 10 | 204 | Store arg_0x04 at +0x46 and set bit 0 of +0x49. |
arg_0x04 |
| 11 | 362 | Go to frame arg_0x04, setting the tick counter to that frame's time. Every target in the files is inside its animation. Gibbed: GoTo. |
arg_0x04 |
| 12 | 28 | Set a draw offset of (x, y), mirrored with the animation. |
x, y |
| 14 | 204 | Store arg_0x04 at +0x47 and set bit 1 of +0x49. |
arg_0x04 |
| 4–6, 13 | 0 | Nothing. | — |
Padding
confirmed (C# scan, all 1,995): the SAS3 ends with the last animation's last frame. It isn't aligned, and as a pakd entry it is followed by 1–16 zero bytes, to align16(end + 1) (containers.md). Every byte before the end is the header, a table, a sprite, an animation or a dead frame in a gap.
How the game reads it
Every fact here is static analysis of the unpacked executable (Ghidra 12.1.4, the decompiled code in local/exe/decomp/), so at most likely. The functions are in the names list.
- likely: the game reads a
SAS3in place, through pointers, and never checks its magic, sizes or counts. Source: the functions below. - likely: the battle loader stores a pointer to each entry of
unit_animation_sets(#0) in a table of 25 at0x140D69DD0. It stops after 25, so a 26th entry would never be used. A unit's animation points at one slot of that table. Source:0x1402d4a30→0x1402d9ca0;0x1402e65e0. - likely: an event actor (scene ID
0x12000+ n) registers its sprite pack (#n/0) and plays theSAS3beside it. Source:0x140323530,0x1403267c0→0x1403263d0;0x140350380finds the entry. - likely: the shop gives
0x1403ed560theSAS3(entry#1) and abtx(entry#0) of amenu/sequence/file; thebtxbecomes the sheet. Source:0x14030c300→0x1403ed560. - likely: to start an animation the game takes an animation index (8 bits) and a direction (0–3), and looks up the table entry at
0x10 + 16a + 4d. Source:0x1402e2820,0x1402d9bf0. - likely: each tick it runs every frame whose
timehas come (0x1402e3b80→0x1402e3cb0). After the last frame the animation lasts until the counter reachesduration. An animation with no direction at all ends after 60 ticks. Source:0x1402e3b80. - likely: to draw, it takes the shown sprite,
SAS3+sprite_offset[index], and builds one quad per quad record (0x1402e1d00→0x1402e3340). Each quad's place is the animation's position plus the record'sxandy, and its size iswidth×height, all times 1/scale. Its texture coordinates areu,v,u + widthandv + height, divided by the sheet texture's width and height.flip_xandflip_ymirror the quad. Source:0x1402e1d00,0x1402e3340.
Replacing or editing animations
What static analysis says an edited SAS3 must keep; likely, untested in game.
- Offsets must point right. The game follows
sprite_table_offset, eachsprite_offsetand eachanimation_offset(stored doubled, with the mirror bit) without checking them. Keep the animation table at0x10, and keepsprite_table_offset=0x10+ 16 × animations. - Stay within the 8-bit indices. A frame can show only sprites 0–255, and a frame count over 127 reads as negative. Animation indices are 8-bit too. Keep
scalenonzero. - Keep every sprite index in range. The game doesn't check one.
#0/3already has three frames that show sprite 27 or 28 of its 27 sprites (0–26), which read past its sprite table. - Keep quads inside the sheet. Their
u,v,widthandheightare texels of the sheet they're drawn with, so aSAS3and its sheet are edited together (ashg.md). - The unit sets are shared. Every unit drawn with one of the 25
unit_animation_setssets uses its sprites and quads, so editing a set changes every unit that uses it.
Rebuilding
- likely: the layout is fully positional: header, animation table, sprite table, sprites in table order, then animations in offset order. A rebuild that writes them back in that order, keeps the dead frames in the gaps (or the offsets they imply), and pads the entry as
pakdentries are gives back the original. The C# scan accounts for every byte this way. Source: C# scan, all 1,995; not yet round-tripped by the toolchain (R30).
Leads
- How the 25 unit sets' quads map onto the unit sheets. Their rectangles reach 1,024 × 384 and 1,024 × 512, larger than the 512 × 384 and 1,024 × 256 sheets (ashg.md). The same goes for what
sheet1 selects in 11 of the sets, and which set each class uses. - Gibbed names
#0/23dorgaluaand#0/24dorgalua_xl; the other 23 sets are unnamed. - "Direction": the four columns of the animation table are Gibbed's directions (also his native
actor_set_direction). The code takes a column 0–3 (0x1402e2820) from object byte+0x30, a menu bool (0x1403ecb00:+0xE5 != 0) or constants; nothing read shows it is a facing. A trace ofUNIT+0x30(from the facing natives) would settle the word. - "Sprite", "sheet", "frame", "animation" are generic words the references use too; none is the game's word for these exact structures.
- How a type-9 loop (value 100 or more) loops back, and what the flag bits and the
+0x46/+0x47/+0x49/+0x66values (types 9, 10, 14) do: the animation state is read in0x1402e29b0and by the unit and event code. - That type 8 plays a sound effect: it calls
0x140288830in the sound code with the u16. Match the IDs against the sound tables (sabf.md). - Which code plays
menu/sequence/classchange.dat(0x1402bc560loads it) andWarrenReport.dat#86.