TO:R Data Map

ashg: compressed sprites and textures

Source
docs/formats/ashg.md
SHA-256
c6df9f8c…697d4c0b

An ashg is a 16-byte header and an LZ77-style stream that unpacks to one resource: a btx texture (portraits, class faces, shop pictures) or a sprite pack, a pakd of a sprite sheet and two btx layers. The sheet is an image entry with no magic, which Gibbed reads as ImageFile. Sprite packs are also stored uncompressed, often in the same files. This doc covers the ashg wrapper, sprite packs and image entries. The textures are in btx.md, and the animations that draw the sheets are in sas3.md. Gibbed calls the compression RLE.

Based on Every ashg, sprite pack and image entry in the game's 10,016 RotXor + ZIP .dat files: 4,285 ashg, 6,196 sprite packs (2,259 of them inside an ashg) and 6,180 image entries (Where they are). Gibbed's RLE.cs, ImageFile.cs, Images/Image.cs, Images/Texture.cs, Images/Palette.cs (reference_tools/Gibbed.LetUsClingTogether/projects/Gibbed.TacticsOgre.FileFormats/) and Gibbed.TacticsOgre.ExportImage/Program.cs, checked 2026-10-11, written for the PSP release. The unpacked executable, SHA-256 7b0230a11498f40396c9048345e7cfe0ef442cb7ca8faffc6f197920d2669599.
Checked 2026-10-11
Method Single-file C# programs over the decrypted files, using the toolchain's Containers library. They find every ashg (as a payload or a pakd entry at any depth), check its header, decompress it, and record what it holds. They find every sprite pack and image entry, compressed or not, and account for their bytes. They re-encode every ashg and compare it with the original. Static analysis with Ghidra 12.1.4 of the decompressor (0x140384ff0), its five callers and the sprite registration code (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): an ashg starts a pakd entry or a whole payload, never anything else. Every one unpacks to a btx (2,026) or a sprite pack (2,259). Entry paths are as in containers.md.

Where Entry Holds Count Loaded by
object/<Name>.dat (132 files, group 20) #0, #1 A unit sprite file: #0 a sprite pack, #1 an ashg holding a second sprite pack 132 + 132 opened by 0x1402efac0, 0x14031eeb0; cached by 0x1402e4bf0 (from a load callback such as 0x1402f0560); drawn through 0x1402e5220
object/<Name>NN.dat bundles (699 files, group 30) the entry with pakd ID kind 0 A unit sprite file, as above 614 the same
the same the entry with pakd ID kind 1 An ashg holding the unit's face btx 699 0x1402efac0, 0x14031eeb0 → 0x1402f2100
event/scene/*.dat ID 0x11000 + n An ashg holding a face btx 1,239 0x1403593b0 → 0x1402f28f0
the same ID 0x50000 + n A unit sprite file 1,216 0x140323530; the rest is not traced
the same ID 0x12000 + n A pakd: #0 an uncompressed sprite pack, #1 its SAS3 1,956 0x140323530, 0x1403267c0 → 0x1403263d0
battle/battle_data_release.dat #1 (weapon_graphics) #1/0 An image entry with 16 cluts and no texture 1 0x1402e5f20
the same #1/1 A sprite pack 1 0x1402e62b0
the same #1/2–#1/298 An ashg holding a sprite pack 297 0x1402e62b0
battle/battle_data_release.dat #2 (battle_data_release_2) #2/10 An image entry with 65 cluts and no texture (Gibbed's unit_cluts.spr) 1 not traced
the same, and #3–#10 10 entries of #2, #3/0–#10/0 A sprite pack of one btx 18 0x1402e6250 (for #2)
menu/menu_classface.dat #0–#79 An ashg holding a class face btx, found by its pakd ID 80 0x1402bd080, 0x1402bbcd0 → 0x1402f24c0
menu/face/*.dat (5), menu/shopbg/*.dat (3) the payload (ZIP member *.ash) An ashg holding a btx 8 0x14030c300

16 bytes. Offsets are from the start of the ashg.

Offset Size Type Name Meaning Confidence Source
0x00 4 raw[4] magic ashg. Only the weapon loader looks at it. It unpacks an entry starting ash and copies anything else as is. Every other loader unpacks without looking. likely 0x1402e62b0 (compares the first three bytes); the decompressor 0x140384ff0 doesn't read it; C# scan (4,285).
0x04 4 u32 packed_size Size of the ashg: the header plus the stream. The decompressor takes the smaller of this and the size its caller gives as the length of its input. confirmed C# scan: equals 16 + the bytes the stream uses in all 4,285; 0x140384ff0.
0x08 4 u32 unpacked_size Size of the output. The decompressor writes until it has this many bytes. The shop loader sizes its buffer from it. confirmed C# scan: equals the output size in all 4,285; 0x140384ff0, 0x140385900, 0x14030c300.
0x0C 1 u8 method 2 (LZ, Stream) in every file. The decompressor also handles 0 (stored) and 1 (run-length). Any other value writes nothing. likely 0x140384ff0 (switch), 0x1403852b0 (2), 0x140385610 (1); C# scan: 2 in all 4,285.
0x0D 1 u8 window_bits Method 2: how many bits of a match code are its offset. The low 5 bits are used. 7 in 2,222 files, 8 in 611, 9 in 1,451 and 10 in 1. confirmed 0x1403852b0 (& 0x1f); C# scan: every stream decodes with it.
0x0E 1 u8 crc The low byte of the CRC-32 of the unpacked data, the ZIP CRC-32 (reflected polynomial 0xEDB88320). The game doesn't read it. confirmed C# scan: matches in all 4,285; not read by 0x140384ff0.
0x0F 1 u8 unk_0x0F 0 in every file. Not read. likely C# scan; 0x140384ff0.

Stream

Method 2, from byte 16. Tokens follow one another until unpacked_size bytes are written. Every stored stream fits this description (C# scan, all 4,285 decode to exactly unpacked_size bytes using exactly packed_size − 16 bytes). The decoder is 0x1403852b0, so the token layout is also likely from the code.

First byte Bytes Meaning
0x00–0x7F 1 + n A literal run: the next n bytes (the first byte's value, 0–127) are copied to the output. A 0 run copies nothing.
0x80–0xFF 2 A match. With v = (first & 0x7F) << 8 | second: copy (v >> window_bits) + 3 bytes from (v & (2^window_bits − 1)) + 1 bytes back in the output, byte by byte, so a match can overlap what it writes.

So with window_bits = w, a match reaches back at most 2^w bytes and copies 3 to (0x7FFF >> w) + 3 bytes (258 at w = 7). The files use runs of up to 127 bytes, matches of up to 258 bytes and offsets of up to 2^w.

Sprite packs

A pakd (containers.md) with an ids table whose first ID has 0x1FF1 in its high 16 bits. confirmed (C# scan, all 6,196):

Shape ids Entries Header Count
Sheet with layers 0x1FF10001, 0x1FF10002, 0x1FF10003 #0 an image entry (the sheet), #1 a btx named <name>_aura, #2 a btx named <name>_mask 0x30 bytes 6,178 (2,259 inside an ashg)
Texture only 0x1FF10000 #0 a btx 0x20 bytes 18, all in battle/battle_data_release.dat

Image entries

The sheet: a 16-byte file header, a 32-byte image header, then a texture, cluts and a clut table. confirmed (C# scan, all 6,180): in the file, the image header is followed by the texture, then the cluts back to back, then the clut table, then zeros to a 16-byte boundary. Every byte up to the entry's total_size is one of these. As a pakd entry it is then padded to align16(total_size + 1).

File header

Offset Size Type Name Meaning Confidence Source
0x00 4 u32 unk_0x00 0 in every image. Not read. Gibbed requires 0. likely C# scan; Gibbed ImageFile.
0x04 4 u32 total_size Size of the image entry without the pakd padding. Not read. confirmed C# scan: entry size is align16(total_size + 1) in all 6,180.
0x08 2 u16 frame_count 1 in every image. Not read. guess C# scan. Gibbed requires 1, and comments "probably frame count".
0x0A 2 u16 header_size 0x10: where the image header starts. The game reads it and finds the image header there. likely 0x1402701a0, 0x14026f8c0; C# scan.
0x0C 4 u32 unk_0x0C 3 in every image. Not read. — C# scan. Gibbed requires 3, and comments "probably version".

Image header

At header_size. Its offsets are from its own start.

Offset Size Type Name Meaning Confidence Source
0x00 4 u32 total_size The entry's total_size − 0x10. Not read. confirmed C# scan, all 6,180.
0x04 4 u32 texture_offset 0x20: the texture, right after this header. When it equals clut_base there is no texture, and the game then writes 0 here, changing the loaded data. likely 0x1402701a0, 0x14026f8c0; C# scan: 0x20 in all; equal to clut_base in the 2 clut-only images.
0x08 4 u32 clut_base Where the cluts start; the clut table's offsets count from here. It equals total_size when there are no cluts. likely 0x1402701a0; C# scan.
0x0C 4 u32 clut_table_offset The clut table: clut_count u32 offsets, from clut_base. It equals total_size when there are no cluts. likely 0x1402701a0; C# scan.
0x10 4 u32 clut_count 0 in 4,222 images, 1 in 1,956 (event sprites), 16 in weapon_graphics #1/0, and 65 in battle_data_release_2 #2/10. The game reads its low 16 bits as signed. likely 0x1402701a0; C# scan.
0x14 4 u32 frame_width 0 in every image. The game copies it into the sprite resource (+0x48). guess 0x1402701a0; C# scan. Gibbed FrameWidth.
0x18 4 u32 frame_height 0 in every image. Copied to +0x4C. guess 0x1402701a0; C# scan. Gibbed FrameHeight.
0x1C 4 u32 unk_0x1C 0 in every image. Not read. likely C# scan; Gibbed requires 0.

Texture

At the image header + texture_offset. Offsets are from the texture's start. confirmed (C# scan, all 6,178 textures): every value given as fixed below holds in every one.

Offset Size Type Name Meaning Confidence Source
0x00 4 u32 total_size data_offset + data_size + 32. Not read. confirmed C# scan.
0x04 4 u32 data_size width × height × bpp / 8. The game passes it to the converter as a limit. confirmed C# scan; 0x14026fb20.
0x08 4 u32 data_offset 0x30. The game finds the pixels here. Gibbed reads 0x20 as a PSP texture. likely 0x14026fb20; C# scan.
0x0C 4 u32 command_offset data_offset + data_size: the command words. The game reads bit 0 of the first word as a "swizzled" flag. likely 0x14026fb20; C# scan.
0x10 2 u16 block_width 16. Not read: the swizzle block size is fixed in the converter. likely C# scan; 0x14019e500.
0x12 2 u16 block_height 8. Not read. likely As block_width.
0x14 2 u16 width In pixels: 1,024 (1,977 textures), 512 (2,122), 256 (1,025), 128 (1,048), 64 (3), 192 (1) or 2,048 (2, 1,760 high). likely 0x14026fb20; C# scan.
0x16 2 u16 height In pixels, 64 to 1,760. likely 0x14026fb20; C# scan.
0x18 2 u16 bpp 8 in every texture: one clut index per byte. The converter also takes 4 (two indices per byte, low nibble first) and 32. With any other value it makes no pixels. likely 0x14026fb20 → 0x14019e500; C# scan.
0x1A 2 u16 unk_0x1A 0. Not read. likely C# scan; Gibbed requires 0.
0x1C 4 u32 unk_0x1C 0. Not read. likely C# scan; Gibbed requires 0.
0x20 8 u64 texture_key The key the game caches the uploaded texture under. 1,277 distinct values. A key never stands for two different pictures, and 156 pictures appear under more than one key, so it names the picture rather than hashing its pixels. It is not XXH64 of the pixels. likely 0x14026fb20 (looks the key up in the sprite object's texture map at +0x28, and reuses the texture it finds); C# scan.
0x28 8 u64 unk_0x28 0. Not read. likely C# scan.
0x30 data_size raw[n] data The pixels, row by row, width bytes a row: linear, not swizzled. likely 0x14019e500 (bpp 8, flag 0: memcpy); C# scan.
next 32 u32[8] commands See Command words. confirmed C# scan.

Cluts

Each at clut_base + its table offset. Offsets are from the clut's start. confirmed (C# scan, all 2,037 cluts): every one has 256 colours, so every one is 0x430 bytes.

Offset Size Type Name Meaning Confidence Source
0x00 4 u32 total_size 0x430. Not read. confirmed C# scan.
0x04 4 u32 data_size 0x400. The game takes data_size / 4 colours. likely 0x14026f250; C# scan.
0x08 4 u32 data_offset 0x10. The game reads the colours from here. likely 0x14026f250; C# scan.
0x0C 4 u32 command_offset 0x410. Not read. confirmed C# scan.
0x10 data_size u32[256] colors One colour per index: bytes R, G, B, A. likely Gibbed ExportImage (reads the u32 as ABGR); 0x14026f250 (copies them as they are).
0x410 32 u32[8] commands See Command words. confirmed C# scan.

Command words

Eight u32 PSP display-list words, the same in every texture and in every clut (C# scan): confirmed for the values.

Where Words
Texture C2000000, then seven 00000000
Clut C500FF03, B1000000, B0000000, C4000020, 0B000000, then three 00000000

On the PSP these are texture mode, clut mode and clut load commands (Gibbed Texture.ValidateGECommandsReborn, Palette.ValidateGECommands). The PC game reads only bit 0 of the texture's first word (0: not swizzled), so likely none of the rest matters. Source: 0x14026fb20.

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.

Resource Loader Input it passes Where it unpacks to Limit
Unit sprite, variant 1 (unit sprite file #1) 0x1402e5220 The entry: offsets[2] − offsets[1] of the unit sprite file One of 15 slots of 0x140400 bytes, after an 8-byte key unpacked_size ≤ 0x1403F8 (1,311,736); the files reach 0x1401C0
Weapon sprite (weapon_graphics #1/n) 0x1402e62b0 The entry One of 6 slots of 0x50000 bytes, in each of 2 sets unpacked_size ≤ 0x50000; the files reach 0x3C1C0
Unit face (bundle kind 1) 0x1402f2100 copies it into a cache, then 0x1402f27d0 / 0x1402f2860 → 0x1402f24c0 / 0x1402f28f0 The cached entry and its size One of 2 slots of 0x48000 bytes, in each of 2 sets The stored entry ≤ 0x28000 bytes (0x1402f2100 silently skips a bigger one); unpacked_size ≤ 0x48000; the files reach 0x234C0 and 0x40050
Class face (menu/menu_classface.dat) 0x1402bbcd0 → 0x1402f24c0 The entry, with its size given as 0x28000 As unit faces packed_size ≤ 0x28000, unpacked_size ≤ 0x48000
Scene face (scene ID 0x11000 + n) 0x1403593b0 → 0x140350380, 0x1402f28f0 The entry As unit faces unpacked_size ≤ 0x48000
Shop face and background (menu/face/, menu/shopbg/) 0x14030c300 The whole payload A buffer of unpacked_size bytes None found

So the game reads these fields and nothing else:

Replacing a sprite or texture

What static analysis says a replacement must keep. All of it is likely; none of it has been tried in game.

Rebuilding

Leads