ashg: compressed sprites and textures
- Source
docs/formats/ashg.md- SHA-256
c6df9f8c…697d4c0b
Where they are · Header · Stream · Sprite packs · Image entries · How the game reads it · Replacing a sprite or texture · Rebuilding · Leads
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 |
- confirmed: a unit sprite file is always a
pakdof two entries: an uncompressed sprite pack, then anashgholding another sprite pack. The two sheets differ in size, 512 × 384 (1,960 sheets) against 1,024 × 256 (1,965) in most files. Source: C# scan. - likely: the unit's draw code picks one of the two by a flag of the unit (
+0x53, passed to0x1402e5180). Entry#0is used in place. Entry#1is unpacked into a slot. Source:0x14033e6c0(Taco::Application::UNIT::vfunction10),0x1402e5180,0x1402e5220.
Header
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. |
- confirmed: as a
pakdentry anashgis stored with 1–16 zero bytes after it, toalign16(packed_size + 1): the usual entry padding (containers.md). As a whole payload (menu/face/,menu/shopbg/) the payload is exactlypacked_sizebytes. Source: C# scan, all 4,285.
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.
- likely: method 1 (unused): a control byte
cgives a count(c & 0x7F) + 1. With bit 7 set the next byte is repeated that many times; otherwise that many literal bytes follow. Source:0x140385610. - likely: method 0 (unused):
unpacked_sizebytes are copied from byte 16. Source:0x140384ff0. - likely: the decompressor checks nothing. There is no bound on the output, and a match can reach before the start. It reads input past the length it was given from its second input pointer, which every caller passes as null. So a stream that runs past
packed_size, or past the entry, would crash the game. Source:0x140384ff0,0x1403852b0; all five callers pass 0 as the second buffer.
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 |
- confirmed: the mask is always the sheet's size. The aura is 8 × 8 in most packs, and 96 × 144 or 112 × 144 in others. Source: C# scan; the
btxwidthandheight(btx.md). - confirmed: the header's
unk_header_endword and padding are zeros, and each entry is padded as usual. The pack's own size (pack_size) is a multiple of 16. Source: C# scan. - likely: the game reads only the first ID. With
0x1FF10000it registers entry#0alone. With0x1FF10001it registers#0as the sheet, then#1as<name>_auraand#2as<name>_mask, at the offsets in the header. With any other first ID it registers nothing. It never readscountor the other IDs. Source:0x1402702a0.
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.
- likely: a texture is created without its clut, as a texture of 8-bit indices (
0x14026fb20passes no clut to0x14019e500). The clut is given to the draw separately, by index into the image's clut list (0x14026f250). Unit and weapon sheets have no clut of their own. Their colours come from the 65 cluts ofbattle_data_release_2#2/10and the 16 cluts ofweapon_graphics#1/0, chosen by the draw code. The weapon draw0x1402e5f20registers#1/0and picks a clut with0x14026f250.
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: there is one decompressor,
0x140384ff0. It reads the 16-byte header, then dispatches onmethod. It reads its input as two pieces, a buffer of a given length and a second buffer for the rest; every caller passes null as the second. It never checks the magic,crcorunk_0x0F, nor whether the output fits.0x140385900returnsunpacked_size. Source:0x140384ff0,0x1403852b0,0x140385610; its five callers are in the table below (exe.calls). A search of the decompiled code for the match-code arithmetic finds no other decoder. - likely: every loader finds its
ashgby position: an entry index, apakdID or a whole file. It unpacks into a fixed slot, except the shop, which allocatesunpacked_sizebytes. A bigger resource would overflow its slot without any check.
| 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 |
- likely: unit sprite files are cached whole in 105 slots of
0x11E000(1,171,456) bytes. A bigger file takes several slots in a row, so there's no limit per file, only105 × 0x11E000in all. Source:0x1402e4bf0. - likely: a unit's two
object/files (the IDs in entry_unit_1) are read as follows. The bundle (group 30) is read whole. Each entry goes by itspakdID: kind 0 (high 16 bits) is a unit sprite file, cached under the low 16 bits; kind 1 is a face, cached the same way. The plain file (group 20) is read only when no bundle entry already gave that sprite ID. Source:0x1402efac0,0x14031eeb0. - likely: an event scene finds its entries by
pakdID through0x140350380, which subtracts a base per kind: 3 →0x11000(faces), 4 →0x12000(sprite and animation), 5 →0x50000(unit sprite files); kind 8 (0x13000) is the scene's sound banks (sabf.md). Source:0x140350380and its callers0x1403593b0,0x140323530,0x1403267c0. - likely: everything unpacked or found ends up in one register routine,
0x14026f4b0, which picks by the first bytes.btx\0as the u320x787462goes to0x14026fe10and on to thebtxloader (btx.md).p,a,kgoes to the sprite pack reader0x1402702a0. Anything else is read as an image entry by0x1402701a0. The image reader creates the texture through0x14026fb20→0x14019e500and keeps a pointer to each clut.
So the game reads these fields and nothing else:
ashg:packed_size,unpacked_size,method,window_bits, the stream, and (weapon sprites only) the first three bytes.- Sprite pack:
offsets[0]–offsets[2]andids[0]. - Image entry:
header_size;texture_offset,clut_base,clut_table_offset,clut_count,frame_width,frame_height; the texture'sdata_size,data_offset,command_offset(and bit 0 of the word there),width,height,bpp,texture_keyand pixels; each clut'sdata_size,data_offsetand colours.
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.
- The
ashgheader must be valid, but needn't be byte-identical.unpacked_sizemust be exact, andmethod0, 1 or 2 with a stream that ends inside the entry.crcandunk_0x0Faren't checked, but writing the CRC byte and 0 keeps the file like the originals. Method 0 (stored) is the simplest valid choice. Its stream is just the data, sopacked_size= 16 +unpacked_size. - Stay inside the slot sizes in the table above: unpacked unit variant-1 packs up to
0x1403F8bytes, weapons0x50000, faces0x48000unpacked and0x28000stored. The game doesn't check, so a bigger one would overwrite memory. - Keep a sprite pack's shape. Keep the first ID
0x1FF10001, or the aura and mask aren't loaded. Keep the sheet, aura and mask in that order, and keep the mask the size of the sheet, as in every file. - Keep the image's fixed values. Keep
header_size0x10,texture_offset0x20,data_offset0x30andbpp8 (or 4 or 32). Keep pixels linear, with bit 0 of the first command word clear.data_sizemust coverwidth×height. - Change
texture_keywhen the pixels change. The game reuses a texture it already has under the same key in the same sprite object, so an edited sheet that keeps its old key may show the old picture. The key is shared by every copy of the same sheet. For example, theDra_Darkvariant-1 sheet has one key in 12object/files and an event scene. Any value no other sheet uses should do. - Keep where the quads are. The animations draw rectangles of the sheet by pixel position (sas3.md): units by the 25 shared sets in
battle_data_release.dat#0(unit_animation_sets), event actors by theSAS3next to their pack. A redrawn sheet must keep each quad's rectangle, or come with an editedSAS3. - Clut-less sheets take their colours from elsewhere. Units' indices go through the shared cluts in
battle_data_release_2#2/10, and weapons' throughweapon_graphics#1/0. 1,956 sheets carry one 256-colour clut of their own, as many as there are scene actor packs (scene ID0x12000+ n), so probably those.
Rebuilding
- confirmed: the original encoder can be reproduced. It works greedily: at each byte it takes the longest match of at least 3 bytes, and among matches of that length the one farthest back; bytes with no match collect into literal runs of at most 127. It encodes with each
window_bitsfrom 7 to 10 and keeps the shortest stream; no two window sizes ever tie. With the header filled in as above (packed_sizefrom the stream,method2, the CRC byte, 0), this gives back everyashgin the game byte for byte. Source: C# scan,encode2.cs: 4,285 of 4,285 identical. - likely: to rebuild a file, unpack each
ashg, rebuild what's inside, then re-encode it this way. An unchanged entry comes back byte-identical, and a changed one gets a valid stream. Sprite packs follow thepakdrules (containers.md). Image entries keep their fixed values and are padded to 16 bytes.
Leads
- Confirm in game (R7) that a sheet repacked with method 0, or with a new
texture_key, loads; and what happens to an edited sheet that keeps its key. - How the quads of the 25 shared unit animation sets map onto the unit sheets. Their rectangles reach 1,024 × 384 (#0/0) and 1,024 × 512 (#0/7–9), while the sheets are 512 × 384 (variant 0) and 1,024 × 256 (variant 1). The draw divides a quad's pixel position by the sheet texture's width and height (
0x1402e3340). Which set and which variant a unit uses has to be traced from the unit and class code. - What decides variant 0 or 1 of a unit sprite (the unit's
+0x53,0x1402e5180). - How the draw picks a unit's clut among the 65 in
battle_data_release_2#2/10, and what the aura and mask layers do (0x14026f2f0,0x14026f360,0x14026f3d0,0x14026f440bind them). - How scene unit sprite files (ID
0x50000+ n) reach the unit sprite code:0x140323530stores the pointer, and the rest is not traced. - The clut-only images: who reads
battle_data_release_2#2/10and how the 65 cluts are numbered. - The
bg/files start with the same 16-byte file header withunk_0x0C0 (394 entries). That format belongs withbg/. - "Image entry" comes from Gibbed's
ImageFile; the only game support is that the unit graphics code isUnit\UnitImage.cpp. A name of the game's for these entries would replace it.