Skip to content

Commit f55819b

Browse files
committed
Add Ghosts asset layout/walker and UI support #65
Introduce GhostsZoneLayout (FastFileLib) to locate/parse Ghosts asset pools and wrapped headers, add unit tests (GhostsZoneLayoutTests), and replace the ad-hoc scanner with GhostsAssetWalker that pairs pool entries with headers and produces RawFileNode(s). Update GhostsZoneParser to use the new library, remove GhostsRawFileScanner, and modify UI (MainWindowForm/UIManager) to use the walker, surface named non-rawfile entries as a generic fallback, and label Ghosts fastfiles. These changes centralize Ghosts parsing logic in the library, improve asset naming/pairing, and simplify UI rawfile handling.
1 parent d1c05cd commit f55819b

7 files changed

Lines changed: 961 additions & 243 deletions

File tree

Call of Duty FastFile Editor/UI/MainWindowForm.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -617,20 +617,26 @@ private void LoadAssetRecordsData(bool forcePatternMatching = false, bool loadRa
617617
_assetPoolStartOffset = zone.AssetPoolStartOffset;
618618
_assetPoolEndOffset = zone.AssetPoolEndOffset;
619619

620-
// Ghosts: use a dedicated rawfile scanner that walks the (already inflated)
621-
// zone for the short-shape rawfile header pattern. The shared MW2-style
622-
// AssetRecordProcessor doesn't recognise IW6 asset shapes, and the
623-
// GhostsGameDefinition.ParseRawFile method returns null so feeding records
624-
// through it would produce an empty list. The library's TryDecompressGhosts
625-
// already expanded every inner zlib stream during decompression, so the
626-
// rawfile body is plaintext at known offsets.
620+
// Ghosts: walk the (already inflated) zone using the pool's type IDs as a
621+
// schedule, striding from one header to the next with a byte-scan fallback.
622+
// This pairs every pool entry with its located header so non-rawfile types
623+
// (scriptfile, stringtable, weapon, image, …) get named in the asset-pool
624+
// tab alongside rawfiles. The shared MW2-style AssetRecordProcessor doesn't
625+
// recognise IW6 asset shapes, so we bypass it entirely.
627626
if (_openedFastFile.IsGhostsFile)
628627
{
629628
_processResult = new AssetRecordCollection();
630629
_menuLists = new List<MenuList>();
631-
_rawFileNodes = loadRawFiles
632-
? GhostsRawFileScanner.Scan(zone.Data, scanStart: _assetPoolEndOffset)
633-
: new List<RawFileNode>();
630+
631+
if (loadRawFiles && _zoneAssetRecords != null && _zoneAssetRecords.Count > 0)
632+
{
633+
var walk = GhostsAssetWalker.Walk(zone.Data, _zoneAssetRecords, scanStart: _assetPoolEndOffset);
634+
_rawFileNodes = walk.RawFileNodes;
635+
}
636+
else
637+
{
638+
_rawFileNodes = new List<RawFileNode>();
639+
}
634640
RawFileNode.CurrentZone = zone;
635641
_localizedEntries = new List<LocalizedEntry>();
636642
_techSets = new List<TechSetAsset>();
@@ -4286,6 +4292,21 @@ private void LoadAssetPoolIntoListView()
42864292
imageIndex++;
42874293
}
42884294

4295+
// Generic fallback: if no typed parser claimed this record but the
4296+
// walker resolved its name + body offsets (Ghosts non-rawfile types
4297+
// land here), surface them directly from the ZoneAssetRecord.
4298+
if (!isParsed && !string.IsNullOrEmpty(record.Name) && record.HeaderStartOffset > 0)
4299+
{
4300+
isParsed = true;
4301+
name = record.Name;
4302+
dataStart = $"0x{record.AssetDataStartPosition:X}";
4303+
dataEnd = $"0x{record.AssetDataEndOffset:X}";
4304+
size = $"0x{record.Size:X}";
4305+
status = !string.IsNullOrEmpty(record.AdditionalData)
4306+
? $"{record.AdditionalData} (no content parser)"
4307+
: "Located, no content parser";
4308+
}
4309+
42894310
lvi.SubItems.Add(dataStart);
42904311
lvi.SubItems.Add(dataEnd);
42914312
lvi.SubItems.Add(size);

Call of Duty FastFile Editor/UI/UIManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public static void UpdateLoadedFileNameStatusStrip(ToolStripStatusLabel statusLa
4141
gameString = "COD5";
4242
else if (fastFile.IsMW2File)
4343
gameString = "MW2";
44+
else if (fastFile.IsGhostsFile)
45+
gameString = "Ghosts";
4446
else
4547
gameString = "Unknown";
4648

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using Call_of_Duty_FastFile_Editor.Models;
2+
using FastFileLib;
3+
using FastFileLib.GameDefinitions;
4+
using System.Diagnostics;
5+
using System.Text;
6+
7+
namespace Call_of_Duty_FastFile_Editor.ZoneParsers
8+
{
9+
/// <summary>
10+
/// Thin shim around <see cref="FastFileLib.GhostsZoneLayout"/>: drives the
11+
/// library's header scan + pool pairing, then translates the results into
12+
/// the editor's models — mutates each <see cref="ZoneAssetRecord"/> with
13+
/// resolved offsets/names and emits <see cref="RawFileNode"/>s for the
14+
/// rawfile-typed entries.
15+
/// </summary>
16+
public static class GhostsAssetWalker
17+
{
18+
public sealed class WalkResult
19+
{
20+
public List<RawFileNode> RawFileNodes { get; } = new();
21+
public int Resolved { get; set; }
22+
public int Unresolved { get; set; }
23+
public int UnpairedHeaders { get; set; }
24+
}
25+
26+
public static WalkResult Walk(byte[] zoneData, List<ZoneAssetRecord>? records, int scanStart)
27+
{
28+
var result = new WalkResult();
29+
if (zoneData == null || zoneData.Length == 0) return result;
30+
31+
var headers = GhostsZoneLayout.LocateAllHeaders(zoneData, scanStart < 0 ? 0 : scanStart);
32+
33+
// Base zones: no pool to pair against. Surface every located header
34+
// as a rawfile (per docs/Ghosts_FastFile_Format.md, "almost all"
35+
// wrapped assets in base zones are rawfiles).
36+
if (records == null || records.Count == 0)
37+
{
38+
foreach (var h in headers)
39+
result.RawFileNodes.Add(BuildRawFileNode(zoneData, h));
40+
result.UnpairedHeaders = headers.Count;
41+
Debug.WriteLine($"[GhostsAssetWalker] No pool — surfaced {headers.Count} located headers as rawfiles");
42+
return result;
43+
}
44+
45+
// Lift to library DTOs for pairing.
46+
var poolDtos = new List<GhostsPoolEntry>(records.Count);
47+
foreach (var r in records)
48+
{
49+
poolDtos.Add(new GhostsPoolEntry(
50+
recordOffset: r.AssetPoolRecordOffset,
51+
type: r.AssetType_Ghosts,
52+
pointerKind: GhostsPointerKind.Placeholder)); // pointer kind isn't used during pairing
53+
}
54+
55+
var pairing = GhostsZoneLayout.PairPoolWithHeaders(poolDtos, headers);
56+
57+
for (int i = 0; i < records.Count; i++)
58+
{
59+
int hIdx = pairing.PoolToHeader[i];
60+
if (hIdx < 0) continue;
61+
var h = pairing.Headers[hIdx];
62+
var record = records[i];
63+
64+
record.HeaderStartOffset = h.HeaderOffset;
65+
record.HeaderEndOffset = h.BodyStart;
66+
record.AssetDataStartPosition = h.BodyStart;
67+
record.AssetDataEndOffset = h.BodyEnd;
68+
record.AssetRecordEndOffset = h.BodyEnd;
69+
record.Name = h.Name;
70+
record.Size = h.DecompressedLen;
71+
record.AdditionalData = $"Ghosts pool-walked ({(h.IsLong ? "long" : "short")} header)";
72+
records[i] = record;
73+
74+
if (record.AssetType_Ghosts == GhostsAssetTypePS3.rawfile)
75+
result.RawFileNodes.Add(BuildRawFileNode(zoneData, h));
76+
}
77+
78+
result.Resolved = pairing.PairedCount;
79+
result.Unresolved = Math.Max(0, records.Count - pairing.PairedCount);
80+
result.UnpairedHeaders = pairing.UnpairedHeaders;
81+
82+
Debug.WriteLine($"[GhostsAssetWalker] Paired {result.Resolved}/{records.Count} pool entries with {headers.Count} located headers ({result.UnpairedHeaders} unpaired); {result.RawFileNodes.Count} rawfiles");
83+
return result;
84+
}
85+
86+
private static RawFileNode BuildRawFileNode(byte[] zone, GhostsAssetHeader h)
87+
{
88+
byte[] body = new byte[h.DecompressedLen];
89+
Buffer.BlockCopy(zone, h.BodyStart, body, 0, h.DecompressedLen);
90+
bool isText = LooksTextual(body);
91+
return new RawFileNode
92+
{
93+
FileName = h.Name,
94+
StartOfFileHeader = h.HeaderOffset,
95+
HeaderSize = h.BodyStart - h.HeaderOffset,
96+
MaxSize = h.DecompressedLen,
97+
CompressedSize = h.CompressedLen,
98+
IsCompressed = false,
99+
RawFileBytes = body,
100+
RawFileContent = isText ? Encoding.UTF8.GetString(body) : null,
101+
AdditionalData = $"Ghosts pre-scan ({(h.IsLong ? "long" : "short")} header)",
102+
};
103+
}
104+
105+
private static bool LooksTextual(byte[] body)
106+
{
107+
if (body.Length == 0) return true;
108+
int printable = 0;
109+
for (int i = 0; i < body.Length; i++)
110+
{
111+
byte b = body[i];
112+
if (b == 0x09 || b == 0x0A || b == 0x0D || (b >= 0x20 && b < 0x7F))
113+
printable++;
114+
}
115+
return printable * 10 >= body.Length * 9;
116+
}
117+
}
118+
}

Call of Duty FastFile Editor/ZoneParsers/GhostsRawFileScanner.cs

Lines changed: 0 additions & 151 deletions
This file was deleted.

0 commit comments

Comments
 (0)