FileTable.bin: the game's file table
- Source
docs/formats/filetable.md- SHA-256
fffe581e…96ca7a1a
Layout · Header · Name records · Group records · Range records · File records · Path records · How the game reads it · Decoded data · Rebuilding · Leads
The index of every game file, at the root of Data/. The game looks a file up here by group and file ID (or by a hash of its name), and gets back its path under Data/ and its size; it then reads exactly that many bytes from the file. So a rebuilt file that changes size needs its record here changed to match (Rebuilding). The file is not wrapped (no RotXor or ZIP, containers.md). It has fields no code reads and record kinds no record uses; Gibbed reads them as the PSP game's. Terms are as in the glossary.
| Based on | FileTable.bin, 2,627,476 bytes, SHA-256 a1a3dfbafd6d24b69b7db85a5bff9ef08c64cdedd3054e95717166905089a213. The game's loader in the unpacked executable, SHA-256 7b0230a11498f40396c9048345e7cfe0ef442cb7ca8faffc6f197920d2669599 (Ghidra and rea). Gibbed's FileTableFile.cs and FileTable/*.cs (reference_tools/Gibbed.LetUsClingTogether/projects/Gibbed.TacticsOgre.FileFormats/) and Gibbed.Reborn.RebuildFileTable/Program.cs. |
| Checked | 2026-10-12 |
| Method | Static analysis of the loader and the read path with Ghidra 12.1.4, via rea and the decompiled code in local/exe/decomp/ (functions named by address below); disassembly with Xcode's objdump (LLVM); a single-file C# exploration program (exescan.cs, session scratchpad) that searches the executable for every call, jump or pointer to a given address; hex inspection; single-file C# exploration programs (filetable_check.cs, and ft.cs in the R31 review, session scratchpads) that walk the whole file as the loader does, check every count, offset, size and name hash against the file and Data/, and list each group's folders; Gibbed's source. 2026-10-12 (R18): the round trip, tor round-trip local/game/Data/FileTable.bin --docs docs, decoded the table with the layouts below, checked every count and offset against the Rebuilding rules, and rebuilt it from its rows byte for byte (1/1 file), with no decode problem reported; tor export and tor read-back (same as the original); the gate FileTableGivesEachFilesSizeAndTakesARebuiltOne (toolchain/tests/), which checks each file's size against Data/ and gives a rebuilt file a new size. The round trip confirms the layout and the Rebuilding rules, not what a field means: every fact about the loader is static analysis alone, so at most likely. No in-game tests yet. |
Layout
The parts follow each other with no gaps, in this order (confirmed: the R18 round trip finds each part ending where the next begins, and the path records ending at the end of the file; so did the exploration program):
| Part | Starts at | Record size | Count |
|---|---|---|---|
| Header | 0x0 | 0x44 | 1 |
| Name records | 0x44 | 8 | name_count |
| Group records | after the names | 24 | group_count |
| Range records | after the groups | 8 | sum of the groups' range_count |
| File records | file_records_offset |
4 | sum of the ranges' file_count |
| Path records | path_records_offset |
variable, 8-aligned | one per file record |
Each part's count and offset in the shipped file are in its page's byte map (FileTable.bin).
Header
The loader (0x1402b1490) reads only name_count, group_count, file_records_offset and path_records_offset. The fields no code reads are named by position, or as Gibbed reads them (guess).
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 2 | u16 | magic |
0x1EF1. The loader doesn't check it; one file, so the data can't show it marks a format. |
guess | Hex inspection; not read in 0x1402b1490. Gibbed FileTableFile (Signature). |
| 0x02 | 2 | u16 | name_count |
Number of name records. | likely | 0x1402b1490 (count of 8-byte records from 0x44); hex inspection. Agrees with Gibbed. |
| 0x04 | 2 | u16 | group_count |
Number of group records. | likely | 0x1402b1490 (stores it at slot +0x58, the bound of 0x1402b1350's search of 24-byte records); hex inspection. Gibbed dir_count. |
| 0x06 | 2 | u16 | unk_0x06 |
4. Not read by the loader; Gibbed rejects any other value. | — | Hex inspection. Gibbed. |
| 0x08 | 4 | u32 | file_records_offset |
Where the file records start: 0x44 + 8 × name_count + 24 × group_count + 8 × ranges. |
likely | 0x1402b1490 (slot +0x78); 0x1402b1350 adds a range's offset to it; exploration program (the sum matches). Gibbed file_table_offset. |
| 0x0C | 4 | u32 | path_records_offset |
Where the path records start, right after the file records: file_records_offset + 4 × file records. Gibbed calls it the total size, which it was on the PSP, where the file ended there. |
likely | 0x1402b1490 (slot +0x80); 0x1402b1340 adds 8 × path_offset to it; exploration program. Gibbed external_table_offset. |
| 0x10 | 16 | raw[16] | title_id_1 |
ULJM05753, NUL-padded: a product code. Not read by the loader. |
guess | Hex inspection. Gibbed (TitleId1, the PSP game's title ID). |
| 0x20 | 16 | raw[16] | title_id_2 |
NPJH50348, NUL-padded: a product code. Not read by the loader. |
guess | Hex inspection. Gibbed (TitleId2). |
| 0x30 | 2 | u16 | unk_0x30 |
0. Gibbed rejects any other value. | — | Hex inspection. Gibbed. |
| 0x32 | 1 | u8 | unk_0x32 |
0. Gibbed accepts 0 or 1. | — | Hex inspection. Gibbed. |
| 0x33 | 1 | u8 | parental_level |
5. Not read. | guess | Hex inspection. Gibbed's comment: used when writing save data on the PSP (PARENTAL_LEVEL in the SFO). |
| 0x34 | 16 | raw[16] | install_data_key |
All zero. Not read. | guess | Hex inspection. Gibbed (InstallDataCryptoKey, the PSP's install-data key). |
Name records
Let the game open a file by name: it hashes the name and looks the hash up here to get the file's group and file ID (0x1402af940, 0x1402b1700). If the hash isn't found, it opens the name as a path instead.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x0 | 4 | u32 | name_hash |
FNV-1 32-bit hash of the name: start at 0x811C9DC5, and for each byte multiply by 0x01000193, then XOR the byte (sign-extended). |
likely | 0x1402af940; exploration program (below). |
| 0x4 | 2 | u16 | group_id |
The file's group ID. | likely | 0x1402b1700 returns it, 0x1402af940 opens by it. Gibbed dir_id. |
| 0x6 | 2 | u16 | file_id |
The file's ID within the group. | likely | Same. |
- likely: the game searches every name record for the hash, not just one group's, and takes the first match. Source:
0x1402b1700(a linear search over allname_countrecords). - confirmed: the records are in group order, only for groups 100, 700 and 1001, matching those groups'
name_indexandname_count(each group's count is on its page); every other group hasname_count0 andname_index0xFFFF. Source: the R18 round trip (it derives both from the name records'group_ids); exploration program. - confirmed: for group 100, every hash is the FNV-1 hash of the file's path in its path record. For group 1001, every hash is the FNV-1 hash of the file's name in upper case, without folder or extension (
ENTRY_UNIT_001forbattle/entry/entry_unit_001.dat). Group 700's one record (WarrenReport/WarrenReport.dat) hashes none of that path's spellings tried (with or without folder, extension orData/, in each case and with either slash). Source: exploration programsnamehash.csandvariants.cs(R31, 2026-10-11); the toolchain'sNameHash. - confirmed: the name records cover only whole files: no hash is any spelling of a name Gibbed gives a
pakdentry (308 entry names inconfigs/reborn/filetables/*.toml, each tried alone, under its file's and group's paths, with and without extension, in each case and with either slash). So Gibbed's entry names (arms_data.xlc,battle_text.xlc, …) can't be checked against the game's hashes, and don't count as the game's names. The one other match,menu\mapname\fr-fr\stronghold_012.datagainst record 1001/405, is a collision: that record's hash is alsoENTRY_UNIT_+ its own number. Source:namehash.cs.
Group records
A group is the first of the two IDs a file is opened by. It isn't a folder: group 500 spans many menu/ folders and group 300 SaveThumb/common, ScreenShot, saveicon and system, while object/ is split over groups 20 and 30, event/scene/ over 901–909 and worldmap/ over 800 and 850 (exploration program ft.cs). No game string names it.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x00 | 2 | u16 | id |
Group ID. The game finds a group by searching for it. In ascending order. | likely | 0x1402b1350; exploration program (ascending). |
| 0x02 | 1 | bool8 | obfuscated |
0 or 1: 1 only in group 2000 (movie/*.pmf, the files the game XORs with key 1, pmf.md). Not read when looking a file up. |
guess | Exploration program (values); no reader in 0x1402b1350, 0x1402b1280, 0x1402b1650. Gibbed (IsEncrypted). |
| 0x03 | 1 | u8 | offset_shift |
4 in every group. Used only for record kinds 2 to 5: a file's data offset is the record's offset << offset_shift + base_offset. |
likely | 0x1402b1280; exploration program. Gibbed block_shift (DataBlockSize). |
| 0x04 | 4 | u32 | base_offset |
0 in every group. Used only for record kinds 2 to 5, as above; the offset it gives is then dropped by 0x1402af860. |
likely | 0x1402b1280; 0x1402b1490 sets it to 0 when mounting; exploration program. Gibbed data_base_offset. |
| 0x08 | 1 | u8 | unk_0x08 |
0 in every group. The game finds no file in a group where this isn't 0. | — | 0x1402b1350; exploration program. Gibbed rejects non-zero. |
| 0x09 | 1 | bool8 | in_install_data |
0 in groups 2000, 2700 and 2750, 1 in the others. Not read when looking a file up. | guess | Exploration program. Gibbed (IsInInstallData). |
| 0x0A | 2 | u16 | range_count |
Number of this group's ranges. | likely | 0x1402b1350. Gibbed batch_count. |
| 0x0C | 2 | u16 | name_count |
Number of this group's name records; 0 for most. | likely | 0x1402b1650; exploration program. Agrees with Gibbed. |
| 0x0E | 2 | u16 | name_index |
Index of its first name record, 0xFFFF when it has none. |
likely | 0x1402b1650. Agrees with Gibbed. |
| 0x10 | 4 | u32 | range_records_offset |
Byte offset of its first range record from the start of the range records: 8 × the ranges of the groups before it. | likely | 0x1402b1350 (adds it to slot +0x70). Gibbed batch_table_offset. |
| 0x14 | 4 | u32 | install_base_offset |
Differs per group: grows from 0 to 0xF4DA8000 in multiples of 0x8000. Not read when looking a file up. |
guess | Exploration program. Gibbed (DataInstallBaseOffset). |
Range records
A range is a run of consecutive file IDs in one group, sharing one record kind.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x0 | 2 | u16 | base_file_id |
First file ID in the range. | likely | 0x1402b1350. Agrees with Gibbed. |
| 0x2 | 2 | u16 | file_count |
Number of files; the range covers IDs base_file_id to base_file_id + file_count − 1. |
likely | Same. |
| 0x4 | 4 | bits[0:28] | file_record_offset |
Bits 0–28 of the u32 at 0x4: byte offset of the range's first file record from the start of the file records. | likely | 0x1402b1350 (& 0x1FFFFFFF, added to slot +0x78). Gibbed entry_offset. |
| 0x4 | 4 | bits[29:31] | record_kind |
Bits 29–31 of the u32 at 0x4: the record kind, which sets the file record's size: 4, 0, 4, 6, 6 or 8 bytes for kinds 0 to 5. 0 in every range. | likely | 0x1402b1350 (size table at 0x140c9a9e4: 04 00 04 06 06 08); 0x1402b1280 switches on it; exploration program (every range is kind 0). Gibbed entry_kind. |
- likely: the file record for ID
fis atfile_records_offset + file_record_offset + record_size × (f − base_file_id). Source:0x1402b1350. - confirmed: ranges are in group order, each group's
range_countof them from itsrange_records_offset, and their file records follow in the same order, back to back: eachfile_record_offsetis 4 × the files of the ranges before it. Source: the R18 round trip; exploration program.
File records
All of Reborn's file records are kind 0 (above): a 4-byte offset into the path records.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x0 | 4 | u32 | path_offset |
Where the file's path record is, in 8-byte units: at path_records_offset + 8 × path_offset. Records vary in size, so this isn't a record index. |
likely | 0x1402b1280, 0x1402b1340 (<< 3). Gibbed external_index. |
Record kinds 2 to 5 hold an offset (shifted by the group's offset_shift) and a size, 2 or 4 bytes each, into one big data file. The game still reads them (0x1402b1280), but no record in Reborn's file uses them; Gibbed reads them as the PSP game's.
Path records
One record per file record.
| Offset | Size | Type | Name | Meaning | Confidence | Source |
|---|---|---|---|---|---|---|
| 0x0 | n + 1 |
cstr | path |
The file's path under Data/, with / separators, in ASCII, NUL-terminated. |
likely | 0x1402b1340, 0x140295760 (opened under the game's data root); exploration program (paths). |
n + 1 |
4 | u32 | size |
The file's size in bytes, right after the NUL, not aligned. The game reads this many bytes from the file. | likely | 0x1402b1280; exploration program (below). |
n + 5 |
0 to 7 | raw | padding |
Zero bytes up to the next multiple of 8 from path_records_offset. |
likely | Exploration program. Agrees with Gibbed. |
- confirmed: records are back to back, in file-record order, each used by exactly one file record, each padded with zeros to the next multiple of 8, and the last ends at the end of the file. Every path is ASCII. Source: the R18 round trip; exploration program.
- confirmed: for every file record whose file is in
Data/,sizeis the file's size on disk. The others name files that aren't inData/, and all have size 0 (each group's count is on the file's page, under Size 0). Source: the gateFileTableGivesEachFilesSizeAndTakesARebuiltOneover the toolchain's decode (R18); exploration program over the game'sData/.
How the game reads it
How the executable turns a file ID or name into bytes: it loads this table, looks the file up in it, and reads the file from Data/. The functions are in the names list.
Loading the table
- likely: the game loads one file table, once, at startup: the game's setup (
0x1402c1970) callsFileTable_Mountwith no path, retrying until it succeeds, and nothing else callsFileTable_MountorFileTable_Load, directly or through a stored pointer. Source:0x1402c1970(calls at0x1402c1d8band0x1402c1da2);exe.calls; exploration programexescan.cs(no other call, jump or pointer to either function in the executable). - likely: that table is
Data\FileTable.binin the executable's folder. With no path,FileTable_Loadopens the bare nameFileTable.bin; no table holds its hash yet, so it's opened as a path under the data root, which is the executable's folder plusData\. Given a path, it would open the path andFileTable.binjoined with no separator (%s%s). Source:0x1402b1490(format string at0x14067dbc8),0x1402af940(fallback to a path),Mx::Blade::BladeFramework::vfunction70x14015d120(GetModuleFileNameW, thenData\, kept by the file manager at0x140f6c770),0x1402b0560(copies it to the data root at0x140ce3980). - likely:
FileTable.EDATis never loaded:FileTable_Loadpicks it overFileTable.binonly when its fourth argument is set, and its one caller,FileTable_Mount, always passes 0. Source:0x1402b05b0(callsFileTable_Load(slot, path, 1, 0, 0xFF)). - likely: the game reads the table whole, and checks only that it read as many bytes as the file has on disk; there is no hash or checksum over it. Source:
0x1402b1490(opens with0x1402af940, size from the file system via0x140295760with its size flag set, compares with the bytes read by0x140295990). - likely: mounting also sets every group's
base_offsetto 0 in memory (FileTable_Load's third argument); it is 0 in every group already. Source:0x1402b1490. - likely: the file system has 64 slots of
0x88bytes for tables, and beside them a list of pointers to the loaded ones, sorted after each mount by a signed 16-bit priority in the slot (at0x42), lowest first, with empty entries last (as0x7FFF). Every mount gets priority0xFF, so the sort decides nothing, andqsortleaves the order of equal entries unspecified. Opening by ID searches the sorted list; opening by name searches the slots in slot order, so the first table mounted (slot 0) first. Source:0x1402b05b0(first free slot, priority fromFileTable_Load's fifth argument, thenqsort),0x1402af4f0(comparator),0x1402af860(sorted list at0x2270),0x1402af940(slots from0x70); ISO C,qsort("the order of equivalent elements is unspecified"). - likely: so a mod can't add a table of its own without changing the executable: only
Data\FileTable.binis ever loaded. Code injected into the game to mount a second table wouldn't reliably win either: with the same priority as the game's table, whether it's searched first by ID is up toqsort, and by name the game's table is always searched first. Source: the facts above.
Opening a file by ID
- likely: given a group ID and file ID, the game finds the group, then the range holding the file ID, then its file record, and from the record the file's path and size. Source:
0x1402af860→0x1402b1350(group, range and file record),0x1402b1280(size),0x1402b1340(path). - likely: it joins the path to its data root and opens it without asking the file system for the size; the handle's size is set from the table, and that many bytes are read. No hash or checksum of the file is checked. Source:
0x140295760(size flag clear, root from0x1402af520),0x1402af860(stores the table's size in the handle at0x48),0x140295990(reads the handle's size).
Opening a file by name
- likely: given a name, the game hashes it with FNV-1 (32-bit) and searches the name records of every loaded file table for the hash; if it's found, it opens that group and file ID as above. If not, it opens the name as a path under the data root, with its size from the file system. Source:
0x1402af940(hash, search, fallback),0x1402b1700(name-record search).
Reading the bytes
What the game does with the table's size once a file is open, and so what happens when the file on disk is a different size.
- likely: reads are queued, not done at once:
0x1402af220adds a request (handle, buffer, size) to a list ordered by priority, and worker threads namedFileSystem::Read(0x1402b0140) andFileSystem::ReadStream(0x1402afef0) carry the requests out; a third,FileSystem::Decomp(0x1402af110), unpacks wrapped files (containers.md). Source:0x1402ae340(creates the three workers by name),0x1402af220. - likely: the size asked for is the handle's size, which for a file opened by ID is the table's
size, and the buffer the game reads into is allocated at exactly that size. Source:0x1402af860(storessizein the handle at0x48);0x1402959a0,0x140295b20(queue a read of the handle's size);0x140295bc0,0x140295c90(allocate a buffer of the handle's size,File\FileDesc.cpp). - likely: each read opens the file, seeks, and calls
freadfor the requested size; it fails only whenfreadreturns 0 bytes, so a short read counts as success. The layers above don't look at the count: the game records the requested size as the bytes read. Source:0x1402afd20(ignores the stream's result, sets bytes read at0x88to the requested size) →0x1402956d0→Mx::IO::Blade::BladeFile::vfunction3(0x140179470, advances by the requested size) →0x14010b190→bl::fio::FileIO::vfunction3(0x14012bea0:_fsopen,fseek,fread; error0x1Aonly when nothing was read). - likely: so the game never compares a file's real size with the table's. When the file is longer, only the first
sizebytes are read. When it's shorter, the read still succeeds, and the rest of the buffer is left as it was allocated (not cleared). Source: the three facts above. - likely: for a RotXor-and-ZIP
.dat, the game uses only the bytes up to the end of the member's compressed data; the central directory and end record after it are never read (containers.md). So anysizefrom the end of the compressed data (30 + n + csize) up works, and a smaller one cuts the compressed data short. AnMLZ0file is different: the game takessize − 8as its compressed length, so itssizemust be exact. Source:0x1401a2a00(reads the local header and inflatescompressed_sizebytes),0x1401a29e0; forMLZ0,0x1401a2a00(LZ4_decompress_safeofsize − 8bytes from byte 8).
Decoded data
tor export writes the table as a folder payload.filetable/ with one .jsonl per kind of record (toolchain/README.md (../../toolchain/README.md#decoded-data)). The counts and offsets the Rebuilding rules derive aren't columns; they're in the file's .bin.
| File | Rows |
|---|---|
header.jsonl |
One: every Header field but name_count, group_count, file_records_offset and path_records_offset. |
names.jsonl |
One per name record. |
groups.jsonl |
One per group record, every field but range_count, name_count, name_index and range_records_offset. |
ranges.jsonl |
One per range record: group_id (its group's id), then every field but file_record_offset. |
files.jsonl |
One per file record, with its path record: group_id, file_id (its range's base_file_id + its place in the range), path, size. |
The view exe.file_opens joins files.jsonl to the executable's calls that open files (The indexes).
Rebuilding
What a rebuild needs. confirmed for unchanged rows: the table rebuilds byte for byte from its rows, with every count and offset derived as below (see Method).
- A mod that changes a file's size changes the game's own
Data\FileTable.bin: the game loads no other table (Loading the table). - Parts are written in the order of Layout, with no gaps. Every count and offset is derived from the records:
name_count,group_count, each group'srange_count,range_records_offset,name_countandname_index(0xFFFFwhen it has no name records), eachfile_record_offset,file_records_offset,path_records_offset, and eachpath_offset. - Ranges follow their groups' order, and a group's name records are one run. Files follow their ranges' order, each range's
file_countof them with IDs from itsbase_file_idup. Rows that break this are refused with an error. - Only record kind 0 is written: the file records of the other kinds hold an offset and size into one big data file (File records), which Reborn doesn't use; a range of another kind is refused.
- Each path record is the path, a NUL, the 4-byte
size, and zero bytes up to the next multiple of 8 frompath_records_offset. A longer or shorter path moves the path records after it, and theirpath_offsets are worked out again. - Every other field is kept as it is in the original (the fields no code reads, the name hashes).
- For a rebuilt
.datwhose path doesn't change, only its path record's 4sizebytes change; nothing moves. confirmed: the gateFileTableGivesEachFilesSizeAndTakesARebuiltOnerebuildsbattle/battle_data_release.datwith a longer text key, sets itssizewith the toolchain (FileTable.SetSizes, by path), and the new table differs from the original only in those 4 bytes and decodes back with the new size. sizeis set to the rebuilt file's exact size. The game would also accept anything from the end of a ZIP member's compressed data up (Reading the bytes), but an exact size is what the game ships and the only value whose effect doesn't depend on that reading.- Gibbed's
RebuildFileTabledoes the same: it reads the table, sets each named file'ssizeto its size on disk, and writes the table again. Source: GibbedRebuildFileTable/Program.cs,FileTableFile.Serialize.
Leads
- What group 700's one name hash (
0x4F63C3BF,WarrenReport/WarrenReport.dat) is a hash of: the name0x14028bd80and other callers ofFileTable_OpenByNamepass would tell. - What the game uses
obfuscated,in_install_dataandinstall_base_offsetfor, if anything; nothing in the lookup reads them. A reader ofobfuscated(the movie path0x14026c260→0x1402b0970?) would settle it. - Title IDs:
ULJM05753andNPJH50348look like Sony PSP product codes; with a primary source for Sony's title-ID format,title_id_1/2could be named from the data. - The file records without a file in
Data/: which groups they are in, and whether they're other platforms' or cut files. - Confirm in game (R14) what Reading the bytes predicts: a
.datwhosesizeis a few bytes short of the file (cutting only its ZIP end records) still loads, and one cut into its compressed data doesn't. - What the game's setup sets right after mounting the table:
0x1402b05a0sets byte 1 of an 8-byte array at0x2B08in the file system, whichFileSystem_QueueRead(0x1402af220) compares per read channel with the array at0x2B10. 0x14028bd80looks names up by hash in group 500 only (0x1402b04d0→0x1402b1650), but the shipped table has name records only for groups 100, 700 and 1001: what it opens, and whether it ever finds anything.local/exe/decomp/is stale:0x1402ae180.cstill shows0x1402af220asFile_Read, several named functions show asFUN_…, and comments cite adocs/executable/file_loading.mdthat doesn't exist. Regenerate it after the names are applied.