Skip to content

Commit 1c8986d

Browse files
committed
docs: expand Ghosts (IW6) format & support notes
Enrich Ghosts (IW6) documentation and SupportedFormats entry. Ghosts_FastFile_Format.md: add detailed zone/pool layout, tagCount/assetCount rules, four pointer conventions (including IW6 0x40 flag), luafile layout and Lua 5.1 custom bytecode notes (format byte 0x0D), LuaBytecodeInspector behavior, and library/editor wiring (FastFileLib.GhostsZoneLayout, walkers, and editor shims). Clarify which asset types are parsed (rawfile/scriptfile/luafile) vs. unparsed flat binaries, update sample table and known-unknowns. SupportedFormats.md: change Ghosts status to note partial support with Lua extraction and summarize read-only capabilities, pool-location rules, and header shapes.
1 parent 0a37c81 commit 1c8986d

2 files changed

Lines changed: 189 additions & 70 deletions

File tree

docs/Ghosts_FastFile_Format.md

Lines changed: 182 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
# Ghosts FastFile Format — Research Notes
22

3-
Verified against 4 retail PS3 samples: `ghosts_patch_common_mp.ff` (525 KB
4-
patch), `patch_homecoming.ff` (195 KB patch), `patch_mp_dome_ns.ff` (145 KB
5-
patch), `ghosts ps3 common.ff` (29.8 MB base). All Ghosts (IW6) version
6-
`0x22E`. No PC, Xbox 360, or Wii-U samples have been tested; layout details
7-
below apply to PS3 only.
3+
Verified against PS3 retail samples: small patch FFs (`patch_mp_prisonbreak.ff`,
4+
`patch_homecoming.ff`, `patch_mp_dome_ns.ff`), DLC-updated zones
5+
(`mp_character_room_dlc_updated.ff`), UI patches (`patch_ui_mp.ff`,
6+
1256 pool entries), and base zones (`ghosts ps3 common.ff`, 29.8 MB).
7+
All Ghosts (IW6) version `0x22E`. No PC, Xbox 360, or Wii-U samples
8+
have been tested; layout details below apply to PS3 only.
89

910
**Current implementation status:**
1011
- ✅ Detect Ghosts via `IWff0100` outer magic + version `0x22E` + `IWffS100` inner magic
1112
- ✅ Decompress and inflate to zone in one pass (`FastFileProcessor.TryDecompressGhosts`)
1213
- Outer raw-deflate blocks (2-byte BE srcSize + payload, 64 KB out each)
1314
- Inner per-asset zlib streams expanded inline against decoded `compressedLen` from each asset header
14-
- 🟡 Walk the asset pool (`GhostsZoneParser`) — works for **patch FFs only**. Patches use 8-byte `[FF*4][type BE u32]` pool entries starting at `0x40`; base FFs use a different layout (a long run of pointer placeholders with type information stored elsewhere) and aren't walked correctly yet.
15-
- ✅ Asset pool tab populates in the editor for patch FFs (type names from `GhostsAssetTypePS3`); base FFs decompress + inflate fine but the asset-pool tab stays empty.
16-
- ❌ Per-asset content parsers (rawfile body, scriptfile body, weapon struct, etc.) — not implemented; all `Parse*` methods on `GhostsGameDefinition` return null
17-
- ❌ Recompression / re-signing — not implemented and not viable without IW's RSA-2048 private key
15+
- ✅ Walk the asset pool (`FastFileLib.GhostsZoneLayout`) — header-counts-driven, works for **patch FFs, DLC-updated zones, and base zones**. Reads `tagCount` @ `0x28` and `assetCount` @ `0x30` to navigate past the tag-string region; falls back to a brute scan when header counts are missing or layout is unexpected.
16+
- ✅ Asset pool tab populates in the editor for every tested zone type, with type names from `GhostsAssetTypePS3`.
17+
- ✅ Pair pool entries with asset bodies for **rawfile** (zlib-wrapped short header), **scriptfile** (zlib-wrapped long header), and **luafile** (flat 16-byte header). Editor surfaces names + offsets + body bytes for these in both the Asset Pool tab and the Raw Files tab. Flat-binary types (xmodel/image/sound/techset/weapon/material/…) are listed but not opened — they'd each need their own struct parser.
18+
- ✅ Luafile bodies feed `FastFileLib.LuaBytecodeInspector`, which surfaces an extracted-strings summary in the editor's text viewer (Lua source isn't in the FF — IW6 ships compiled bytecode with custom format byte `0x0D`).
19+
- ❌ Per-asset content parsers for other types (weapon struct, image, sound, …) — not implemented; `GhostsGameDefinition.Parse*` methods all return null.
20+
- ❌ Recompression / re-signing — not implemented and not viable without IW's RSA-2048 private key.
1821

1922
## TL;DR — IW6 PS3 vs the previous IW signed-FF design
2023

@@ -162,53 +165,71 @@ trailing data that follows the last complete block.
162165

163166
## Zone format (after the outer deflate, before inner-zlib inflation)
164167

168+
Same shape as CoD4/WaW/MW2: a fixed XFile header gives counts that drive
169+
navigation through the tag pointer table and tag strings, then the asset
170+
pool follows, then asset bodies. The layout is uniform across patch / DLC /
171+
base zones — only the count values differ.
172+
165173
```
166-
0x000 XFile header
167-
0x040 Asset pool (8 bytes per entry, terminated by pattern break)
168-
.... Per-asset data (back-to-back asset entries)
174+
0x00..0x27 Fixed XFile fields (zone size, block sizes, …)
175+
0x28..0x2B tagCount (BE u32)
176+
0x2C..0x2F placeholder
177+
0x30..0x33 assetCount (BE u32)
178+
0x34..0x37 placeholder
179+
0x38..? Either count3+placeholder (zones with tagCount > 0)
180+
or first pool entry (patch FFs where tagCount == 0)
181+
... Tag pointer placeholders (4 bytes × tagCount)
182+
... Tag strings (tagCount null-terminated ASCII)
183+
... Asset pool (8 bytes × assetCount)
184+
... Asset bodies (back-to-back)
169185
```
170186

171-
### XFile header
172-
173-
The first 64 bytes (`0x00..0x3F`) contain a sequence of u32 BE fields:
174-
zone size at `0x00`, several block-size slots at `0x08..0x2F`, then count
175-
+ pointer-placeholder pairs around `0x28..0x37`. The exact field layout
176-
has not been fully mapped; in particular the layout differs between
177-
patch and base zones.
178-
179-
**Patch zones** (e.g. `patch_homecoming.zone`, `ghosts_patch_common_mp.zone`):
180-
the asset pool starts at `0x40` immediately after the XFile header, with
181-
8-byte `[ptr][type]` entries terminated by the pattern breaking. The pool
182-
walker (`GhostsZoneParser`) finds it by scanning the first `0x200` bytes
183-
for the longest contiguous run of valid
184-
`[FFFFFFFF][type BE u32 ≤ 0x35]` 8-byte records.
185-
186-
**Base zones** (e.g. `common.zone`): different layout that hasn't been
187-
reverse-engineered. Bytes from `0x40` onwards are a long run of pointer
188-
placeholders (continuous `FF FF FF FF`) rather than 8-byte `[ptr][type]`
189-
records, suggesting the type information is stored separately (perhaps a
190-
parallel type array later in the zone). The current pool walker doesn't
191-
recognise this layout and the asset-pool tab stays empty for base zones.
192-
193-
### Asset pool (patch zones only)
194-
195-
In patch zones each entry is 8 bytes. Pool entries use
196-
**`[pointer placeholder][type ID]`** order (pointer first), matching
197-
MW2 PS3 — not the `[type][pointer]` order used by MW2 PC.
187+
### XFile header counts
188+
189+
| Sample | tagCount | assetCount | Pool offset |
190+
|---|---:|---:|---:|
191+
| `patch_mp_prisonbreak.zone` | 0 | 4 | `0x38` |
192+
| `patch_mp_dome_ns.zone` | 0 | 4 | `0x38` |
193+
| `patch_homecoming.zone` | 0 | 4 | `0x38` |
194+
| `ghosts_patch_common_mp.zone` | 0 | 122 | `0x38` |
195+
| `mp_character_room_dlc_updated.zone` | 212 | 1880 | `0xED5` |
196+
| `patch_ui_mp.zone` | 249 | 1256 | `0x1997` |
197+
| `ghosts ps3 common.zone` | varies | varies | varies |
198+
199+
**Pool location rule** (implemented in `FastFileLib.GhostsZoneLayout.LocatePool`):
200+
201+
1. **`tagCount == 0`** → pool starts at `0x38` immediately after the
202+
`assetCount` placeholder. This covers most patch FFs.
203+
2. **`tagCount > 0`** → skip `tagCount × 4` placeholder bytes starting at
204+
`0x3C`, then skip `tagCount` null-terminated tag strings; the pool
205+
follows. A 32-byte probe window forward of the strings handles a
206+
small trailing field (purpose unknown — verified in
207+
`mp_character_room_dlc_updated.zone`: bytes `00 00 00 30` sit between
208+
the last tag's null and pool[0]).
209+
3. **Fallback** brute-scan for the longest run of valid pool entries —
210+
used when header counts are missing or layout is unexpected.
211+
212+
### Asset pool
213+
214+
Each entry is 8 bytes. Pool entries use **`[pointer placeholder][type ID]`**
215+
order (pointer first), matching MW2 PS3 — not the `[type][pointer]` order
216+
used by MW2 PC.
198217

199218
```
200-
FF FF FF FF 00 00 00 XX
201-
└─ pointer ┘ └─ type ─┘ (BE u32, high 3 bytes zero, low byte = type ID)
219+
PP PP PP PP 00 00 00 XX
220+
└─ pointer ┘ └─ type ─┘ (type word: BE u32, high 3 bytes zero, low byte = type ID ≤ 0x35)
202221
```
203222

204-
The pool starts at `0x40` immediately after the XFile header. Verified
205-
for the 3 patch samples — `patch_homecoming.zone` (4 entries, all
206-
scriptfile), `patch_mp_dome_ns.zone` (4 entries — 2 rawfile + 2
207-
scriptfile), `ghosts_patch_common_mp.zone` (122 entries — 120
208-
scriptfile + 2 rawfile).
223+
The pointer field has **four observed conventions** — any 4-byte value is
224+
accepted by the pool walker; the strict type-word structure plus the
225+
header's `assetCount` cap are what delimit the pool:
209226

210-
Base zones use a different layout (see XFile header section above) and
211-
this 8-byte `[ptr][type]` walking doesn't apply.
227+
| Pattern | Meaning |
228+
|---|---|
229+
| `FF FF FF FF` | Standard inline placeholder (most common) |
230+
| `00 00 00 00` | NULL — seen on the first scriptfile entry of `patch_mp_prisonbreak.zone` |
231+
| `80 XX XX XX` | High-bit-set resolved pointer (CoD4/WaW convention) |
232+
| `40 XX XX XX` | **0x40-flagged resolved pointer** — used by IW6, e.g. `40 1F DF 85` in `patch_ui_mp.zone` (material → image references). Earlier code that only accepted the `0x80`-flag form found 87 / 1256 entries in this zone. |
212233

213234
### Asset type IDs (IW6 PS3)
214235

@@ -360,8 +381,74 @@ inline as part of decompression, so a zone produced by this codebase has
360381
no remaining `78 XX` streams (verified: 0 residual streams in all four
361382
samples after `TryDecompressGhosts` completes).
362383

384+
### Luafile bodies (flat 16-byte header — *not* zlib-wrapped)
385+
386+
`luafile` (type `0x32`) uses a different in-zone layout than rawfile /
387+
scriptfile — flat header, body is plain Lua bytecode (compiled, not
388+
compressed). Verified against `patch_ui_mp.zone` (85 luafile pool entries,
389+
86 located bodies including one trailing entry not referenced by the pool).
390+
391+
```
392+
[FF*4][size BE u32][unk u32][FF*4]<name>\0<Lua 5.1 bytecode>
393+
```
394+
395+
- 16 bytes header.
396+
- `size` is the exact byte count of the bytecode body that follows the name.
397+
- `unk` is consistently `0x02000000` across every observed entry. Purpose
398+
unconfirmed — possibly a fixed type tag or chunk marker. The luafile
399+
scanner doesn't depend on its value.
400+
- Name is path-style ASCII ending in `.lua`. The walker requires this
401+
suffix to distinguish luafiles from any other flat-header asset types.
402+
403+
Bodies always start with the Lua 5.1 signature `1B 4C 75 61 51` (`\x1B
404+
LuaQ`). The signature check is what makes the scan safe against random
405+
`[FF*4][u32][u32][FF*4]` byte sequences in dense asset data.
406+
407+
Stride from one luafile to the next is `nameEnd + 1 + size`. Handled by
408+
`FastFileLib.GhostsZoneLayout.LocateAllLuaFiles`.
409+
410+
### Lua bytecode (IW6 custom format byte `0x0D`)
411+
412+
The Lua source is **not** in the FF — IW6's build pipeline compiles `.lua`
413+
→ bytecode and ships only the bytecode. Recovering readable source needs
414+
an external Lua 5.1 decompiler (e.g. `luadec`).
415+
416+
The 12-byte Lua header is standard except for the format byte:
417+
418+
| Offset | Value | Meaning |
419+
|---|---|---|
420+
| `0x00` | `1B` | escape |
421+
| `0x01..0x03` | `Lua` | signature |
422+
| `0x04` | `51` | Lua version 5.1 |
423+
| `0x05` | **`0D`** | **format byte — non-zero = IW6 custom dialect** |
424+
| `0x06` | `00` | endianness flag (declared BE — but length fields in the chunk body are actually LE) |
425+
| `0x07` | `04` | sizeof(int) |
426+
| `0x08` | `04` | sizeof(size_t) |
427+
| `0x09` | `04` | sizeof(Instruction) |
428+
| `0x0A` | `04` | sizeof(lua_Number) — **single-precision** (stock Lua 5.1 default is 8) |
429+
| `0x0B` | `00` | integral flag (floating-point lua_Number) |
430+
431+
The chunk body past the 12-byte header doesn't follow stock Lua 5.1 chunk
432+
layout — IW6 uses a customized chunk format whose string-constant length
433+
prefix is 1 byte (not `sizeof(size_t)`) at least for the type-registry
434+
strings (`TNIL`, `TBOOLEAN`, `TLIGHTUSERDATA`, …) at the chunk start. Full
435+
chunk layout hasn't been reverse-engineered.
436+
437+
`FastFileLib.LuaBytecodeInspector` works around the custom format by
438+
**not** parsing chunks: it reads the 12-byte header for metadata and then
439+
ASCII-scans the body for every printable null-terminated run of length
440+
3–256. That surfaces the useful signal (menu / widget / function /
441+
identifier names) without depending on chunk structure — e.g. for
442+
`ui/lui/mp_menus/clandetails.lua` it produces ~803 extracted strings
443+
including `OnCreate`, `UpdateClanDetails`, `clan_details_main`,
444+
`MenuBuilder`, etc., letting a reader understand what the menu does.
445+
363446
## How the editor handles Ghosts
364447

448+
All Ghosts pool-layout, header-scan, pairing, and Lua-summary logic lives in
449+
`FastFileLib`. The editor classes are thin shims that adapt library DTOs to
450+
the editor's model types — same pattern as `RawFileScanner``RawFileParser`.
451+
365452
| Component | Behaviour |
366453
|---|---|
367454
| `FastFileInfo.DetectGameVersion` | Recognises version `0x22E` as Ghosts; reports platform as PS3 (signed magic with this version is not Xbox 360) |
@@ -370,27 +457,54 @@ samples after `TryDecompressGhosts` completes).
370457
| `FastFileProcessor.Decompress` | Short-circuits to `TryDecompressGhosts` before the shared CoD4/WaW/MW2 dispatch |
371458
| `FastFileProcessor.TryDecompressGhosts` | Two passes: outer raw-deflate blocks → raw zone, then `InflateGhostsZoneAssets` expands inner zlib streams inline |
372459
| `FastFileProcessor.TryReadGhostsAssetHeader` | Recognises both "long" (8 trailing FFs) and "short" (4 trailing FFs) header shapes; reads `compLen` from the first u32 after the leading-FF block |
460+
| `FastFileLib.GhostsZoneLayout` | Library home for pool location (`LocatePool` + `WalkPool`), wrapped-asset header scan (`LocateAllHeaders`), luafile scan (`LocateAllLuaFiles`), and positional pool↔header pairing (`PairPoolWithHeaders`). Header-counts-driven, handles patch + DLC + base zones. |
461+
| `FastFileLib.LuaBytecodeInspector` | Parses Lua 5.1 header + ASCII-scans body for null-terminated printable strings. Format-agnostic to handle IW6's custom format byte `0x0D`. |
373462
| `ZoneFile.Load` | For Ghosts, skips `ReadHeaderFields` / `StructureBasedZoneParser` (those depend on PS3-MW2 header offsets) and runs `GhostsZoneParser` instead |
374-
| `GhostsZoneParser` | Searches first `0x200` bytes for the longest run of valid `[FFFFFFFF][type ≤ 0x36 BE u32]` entries, then walks until the pattern breaks |
375-
| `GhostsGameDefinition` | Maps the 54 IW6 PS3 asset type IDs to names. All `Parse*` content methods return null — pool listing only |
376-
| `MainWindowForm.OpenFastFile` | Skips the asset-selection dialog for Ghosts (no content parsers, nothing to select); otherwise runs the normal flow so the asset-pool tab populates |
377-
| `ZoneHexViewForm` | Uses `GhostsGameDefinition.GetAssetTypeName` for the type column; per-asset content panels stay empty |
463+
| `Editor: GhostsZoneParser` | Thin shim over `GhostsZoneLayout.ParsePool` — translates `GhostsPoolEntry` DTOs to `ZoneAssetRecord` and writes them onto the `ZoneFile`. |
464+
| `Editor: GhostsAssetWalker` | Thin shim over `GhostsZoneLayout.LocateAllHeaders` + `LocateAllLuaFiles` + `PairPoolWithHeaders`. Mutates each `ZoneAssetRecord` with resolved offsets/names; emits `RawFileNode`s for rawfile + luafile entries (the luafile's `RawFileContent` is the `LuaBytecodeInspector` summary). |
465+
| `GhostsGameDefinition` | Maps the 54 IW6 PS3 asset type IDs to names. All `Parse*` content methods return null — content is sourced via the walker instead. |
466+
| `MainWindowForm.OpenFastFile` | Skips the asset-selection dialog for Ghosts (most types still have no content parsers); otherwise runs the normal flow so the asset-pool tab populates. |
467+
| `UIManager.UpdateLoadedFileNameStatusStrip` | Includes `IsGhostsFile` in the `gameString` branch so the status bar shows `Ghosts: <name>` for IW6 files. |
468+
| `ZoneHexViewForm` | Uses `GhostsGameDefinition.GetAssetTypeName` for the type column; per-asset content panels stay empty for non-wrapped/non-lua types |
378469

379470
## Known unknowns
380471

381-
- **XFile header structure.** Block-size fields, `AssetCount` location, and any
382-
additional metadata fields between the XFile header and the asset pool are
383-
not mapped. The pool walker works around this by pattern-matching pool entries.
472+
- **XFile header field semantics.** Counts at `0x28` and `0x30` are mapped
473+
(tagCount + assetCount); other u32s in `0x00..0x27` are zone size + block
474+
size slots but exact roles unconfirmed. The pool walker doesn't need them.
475+
- **Trailing field between tag strings and pool.** DLC zones have 4 bytes
476+
between the last tag string's null and pool[0] (`00 00 00 30` in
477+
`mp_character_room_dlc_updated.zone`). Purpose unconfirmed — `LocatePool`
478+
probes a 32-byte forward window to skip past it.
479+
- **Asset pool trailing entry.** Header `assetCount` is consistently one
480+
greater than the count my walker locates (1879 vs 1880 in DLC zone, 1255
481+
vs 1256 in patch_ui_mp, 122 vs 122 in ghosts_patch_common_mp where they
482+
match). The last "entry" doesn't have a valid type byte and reads like a
483+
sentinel; impact is cosmetic.
384484
- **Base FF index table.** The 12 KB table between the outer header and
385485
`IWffS100` in base FFs has a clear repeating structure (offset pairs +
386486
markers) but its record format and purpose are not reverse-engineered.
487+
It's not needed for decompression.
387488
- **"LO" region.** Bit-7 is deliberately zero across all 112 KB but the
388489
encoding/meaning of the remaining 7-bit-per-byte payload is unknown.
389490
- **Per-asset header's third u32 (long shape).** Not a size. Values seen
390491
range from less than `decLen` (3 vs 6) to several times `decLen` (12459
391492
vs 3031). Interpretation unconfirmed.
392-
- **Asset-content layouts.** No internal struct parsing for any asset type
393-
on Ghosts. `GhostsGameDefinition.Parse*` all return null.
493+
- **Luafile `unk` field.** Consistently `0x02000000`. Possibly a fixed
494+
chunk-type tag or flag word. Treated as opaque metadata.
495+
- **IW6 Lua bytecode chunk format.** Custom format byte `0x0D`. Past the
496+
12-byte header, chunk layout doesn't follow stock Lua 5.1 — string
497+
constants use a 1-byte length prefix in the type-registry preamble at
498+
least. Full chunk layout not reverse-engineered;
499+
`LuaBytecodeInspector` works around this with an ASCII-run scan.
500+
- **Asset-content layouts for non-wrapped types.** No internal struct
501+
parsing for xmodel / image / sound / weapon / techset / material /
502+
stringtable / etc. Each would need its own struct reverse-engineering.
503+
- **Pointer flag bits.** Pool pointers use both `0x80......` (CoD4/WaW
504+
high-bit convention) and `0x40......` (IW6-specific). Meaning of the
505+
flag bits — whether they're heap region tags, alignment hints, or
506+
something else — isn't confirmed; the masked-off offset value usually
507+
lands inside the zone so they're probably both runtime address tags.
394508
- **Non-PS3 platforms.** Xbox 360, Wii-U, and PC variants of IW6 use shifted
395509
asset type IDs (Xbox 360 has no `vertexshader`; PC adds `computeshader`,
396510
`hullshader`, `domainshader`, `vertexdecl`; Wii-U has `fonticon` at `0x1A`).
@@ -400,12 +514,15 @@ samples after `TryDecompressGhosts` completes).
400514

401515
## Reference samples
402516

403-
| Sample | Size | Variant | Block count | Inflated zone |
404-
|---|---|---|---|---|
405-
| `patch_mp_dome_ns.ff` | 145,493 B | patch | 1 | 153,118 B |
406-
| `patch_homecoming.ff` | 194,709 B | patch | 2 | 179,579 B |
407-
| `ghosts_patch_common_mp.ff` | 525,354 B | patch | 9 | 799,277 B |
408-
| `ghosts ps3 common.ff` | 29,796,135 B | base | 717 | 47,689,203 B |
409-
410-
All four are PS3 retail. All four produce 0 residual zlib streams after
517+
| Sample | Size | Variant | Pool entries | Notable |
518+
|---|---|---|---:|---|
519+
| `patch_mp_prisonbreak.ff` | small | patch | 4 | 1 scriptfile + 3 rawfile. First scriptfile uses NULL pointer in pool entry |
520+
| `patch_mp_dome_ns.ff` | 145 KB | patch | 4 | 2 rawfile + 2 scriptfile |
521+
| `patch_homecoming.ff` | 195 KB | patch | 4 | all scriptfile |
522+
| `ghosts_patch_common_mp.ff` | 525 KB | patch | 122 | 120 scriptfile + 2 rawfile |
523+
| `patch_ui_mp.ff` | 31 MB | DLC patch | 1256 | tagCount=249, mixed types (510 image + 476 material + 85 luafile + 68 rawfile + 57 techset + …). First sample to exercise `0x40`-flagged pool pointers |
524+
| `mp_character_room_dlc_updated.ff` | 26 MB | DLC updated | 1880 | tagCount=212, mostly xmodel (1846) + techset (29) + xanim (3) + rawfile (1). First sample with non-zero `tagCount` |
525+
| `ghosts ps3 common.ff` | 29.8 MB | base | varies | full character / weapon / vfx pool |
526+
527+
All PS3 retail. All produce 0 residual zlib streams after
411528
`FastFileProcessor.TryDecompress` completes.

0 commit comments

Comments
 (0)