Skip to content

Commit 535b98a

Browse files
committed
Add Font reader and refine IW4 bridge logic
Add a Font asset/reader (FastFileLib/Iw4/Font.cs) and register it as a top-level reader so IW4 pointer-walks advance past fonts. Extend the IW4 bridge result with Complete, DeclaredLocalizeCount and DeclaredMaterialCount to distinguish full vs. partial walks. Update MainWindowForm to treat a complete walk as authoritative (replace scanner results) and to accept partial walks for specific types when the walk resolved every declared entry (localized/materials). Update Readers.cs to register FontReader and refresh docs to explain the new font reader and the bridge's full/partial-walk behavior.
1 parent a3f00c1 commit 535b98a

5 files changed

Lines changed: 142 additions & 27 deletions

File tree

Call of Duty FastFile Editor/Services/Iw4AssetBridge.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ public sealed class Iw4BridgeResult
1717
public List<MenuList> MenuLists { get; } = new();
1818
public List<StructuredDataDefAsset> StructuredDataDefs { get; } = new();
1919
public List<MaterialAsset> Materials { get; } = new();
20+
21+
/// <summary>True when the IW4 walk read every asset body (no stop / no error).</summary>
22+
public bool Complete { get; set; }
23+
24+
/// <summary>Number of localize-typed pool entries the zone declares (resolved or not).</summary>
25+
public int DeclaredLocalizeCount { get; set; }
26+
27+
/// <summary>Number of material-typed pool entries the zone declares (resolved or not).</summary>
28+
public int DeclaredMaterialCount { get; set; }
2029
}
2130

2231
/// <summary>
@@ -61,11 +70,14 @@ public static class Iw4AssetBridge
6170
return null;
6271
}
6372

64-
// Only trust a complete walk; a stop/error means later assets weren't read.
65-
if (walk.Error != null || walk.StoppedAtType != null)
66-
return null;
67-
68-
var result = new Iw4BridgeResult();
73+
// A complete walk replaces the pattern scanner wholesale; a partial walk (stopped at an
74+
// un-ported type) is still usable for any asset type it read IN FULL — see the caller.
75+
var result = new Iw4BridgeResult
76+
{
77+
Complete = walk.Error == null && walk.StoppedAtType == null,
78+
DeclaredLocalizeCount = walk.AssetList.Assets.Count(a => a.Type == FastFileLib.Iw4.XAssetType.Localize),
79+
DeclaredMaterialCount = walk.AssetList.Assets.Count(a => a.Type == FastFileLib.Iw4.XAssetType.Material),
80+
};
6981
var iw4MenuLists = new List<FastFileLib.Iw4.MenuList>();
7082

7183
// Every top-level asset body's start offset. The byte spans of inline data (e.g. a

Call of Duty FastFile Editor/UI/MainWindowForm.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -746,29 +746,45 @@ private void LoadAssetRecordsData(bool forcePatternMatching = false, bool loadRa
746746
var iw4 = Iw4AssetBridge.TryRead(zone);
747747
if (iw4 != null)
748748
{
749-
if (loadRawFiles)
749+
if (iw4.Complete)
750750
{
751-
var iw4Names = new HashSet<string>(iw4.RawFileNodes.Select(n => n.FileName), StringComparer.OrdinalIgnoreCase);
752-
var scannerOnly = _rawFileNodes.Where(n => !iw4Names.Contains(n.FileName)).ToList();
753-
_rawFileNodes = iw4.RawFileNodes
754-
.Where(r => !menuListNames.Contains(r.FileName))
755-
.Concat(scannerOnly)
756-
.ToList();
751+
// A complete walk is authoritative for every type it covers — replace the scan.
752+
if (loadRawFiles)
753+
{
754+
var iw4Names = new HashSet<string>(iw4.RawFileNodes.Select(n => n.FileName), StringComparer.OrdinalIgnoreCase);
755+
var scannerOnly = _rawFileNodes.Where(n => !iw4Names.Contains(n.FileName)).ToList();
756+
_rawFileNodes = iw4.RawFileNodes
757+
.Where(r => !menuListNames.Contains(r.FileName))
758+
.Concat(scannerOnly)
759+
.ToList();
760+
}
761+
if (loadLocalizedEntries)
762+
_localizedEntries = iw4.LocalizedEntries;
763+
if (loadStringTables)
764+
_stringTables = iw4.StringTables;
765+
if (loadWeapons)
766+
_weapons = iw4.Weapons;
767+
if (loadTechSets)
768+
_techSets = iw4.TechSets;
769+
if (loadMenuFiles && iw4.MenuLists.Count > 0)
770+
_menuLists = iw4.MenuLists;
771+
_structuredDataDefs = iw4.StructuredDataDefs;
757772
}
758-
if (loadLocalizedEntries)
773+
else if (loadLocalizedEntries
774+
&& iw4.LocalizedEntries.Count > 0
775+
&& iw4.LocalizedEntries.Count == iw4.DeclaredLocalizeCount)
776+
{
777+
// Partial walk, but it read EVERY declared localize entry before stopping
778+
// (e.g. code_post_gfx_mp.ff: the whole localize block sits before the first
779+
// un-ported type, Sound). Use the pointer-walk localize instead of the
780+
// FF-marker pattern scan. Other types stay on the scanner for this zone.
759781
_localizedEntries = iw4.LocalizedEntries;
760-
if (loadStringTables)
761-
_stringTables = iw4.StringTables;
762-
if (loadWeapons)
763-
_weapons = iw4.Weapons;
764-
if (loadTechSets)
765-
_techSets = iw4.TechSets;
766-
if (loadMenuFiles && iw4.MenuLists.Count > 0)
767-
_menuLists = iw4.MenuLists;
768-
_structuredDataDefs = iw4.StructuredDataDefs;
769-
// Top-level materials are parsed by the IW4 walk too; prefer them (they carry
770-
// texture/constant counts + detail) over the scan's name-only entries when present.
771-
if (iw4.Materials.Count > 0)
782+
}
783+
784+
// Materials surface from a full read of the material pool (complete walk, or a
785+
// partial walk that still resolved every declared material).
786+
if (iw4.Materials.Count > 0
787+
&& (iw4.Complete || iw4.Materials.Count == iw4.DeclaredMaterialCount))
772788
_materials = iw4.Materials;
773789
}
774790
}

FastFileLib/Iw4/Font.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// =============================================================================
2+
// IW4 (MW2 PS3) zone reader — ported from Jacob Schroeder's FastFile
3+
// https://github.com/jacob-schroeder/FastFile
4+
// Ports: FastFile.Logic/Assets/Readers/FontReader.cs and
5+
// FastFile.Models/Assets/Fonts/FontAsset.cs (FontAsset + FontGlyph).
6+
//
7+
// Font_s is a 24-byte root (name + pixelHeight + glyphCount + material + glowMaterial +
8+
// glyphs pointers); the name, glyph table, and materials all live in other (LARGE/shared)
9+
// blocks, i.e. Offset pointers. The local inline engine resolves only inline (-1) pointers,
10+
// so for a typical font nothing is consumed past the 24-byte root and the walk simply
11+
// advances to the next asset. This is what lets the body walk step past the fonts that sit
12+
// right before the big localize block in zones like code_post_gfx_mp.ff.
13+
// =============================================================================
14+
15+
namespace FastFileLib.Iw4;
16+
17+
public sealed class FontAsset : BaseAsset
18+
{
19+
public const int RootSize = 0x18; // 24
20+
public const int GlyphSize = 0x18; // 24
21+
22+
public FontAsset() : base(XAssetType.Font) { }
23+
24+
public ZonePointer<string>? NamePtr { get; set; }
25+
public string Name => NamePtr is { IsResolved: true } ? NamePtr.Result ?? string.Empty : string.Empty;
26+
public int PixelHeight { get; set; }
27+
public int GlyphCount { get; set; }
28+
public ZonePointer<Material>? Material { get; set; }
29+
public ZonePointer<Material>? GlowMaterial { get; set; }
30+
public ZonePointer<FontGlyph[]>? Glyphs { get; set; }
31+
32+
public override string? GetDisplayName => string.IsNullOrWhiteSpace(Name) ? Type.ToString() : Name;
33+
}
34+
35+
public sealed class FontGlyph
36+
{
37+
public ushort Letter;
38+
public byte X0, Y0, Dx, PixelWidth, PixelHeight, Padding;
39+
public float S0, T0, S1, T1;
40+
}
41+
42+
internal static class FontReader
43+
{
44+
public static FontAsset Read(ref ZoneReadContext context)
45+
{
46+
var asset = new FontAsset
47+
{
48+
Offset = context.Position,
49+
NamePtr = GenericReader.ReadStringPointer(ref context),
50+
PixelHeight = context.ReadInt32(),
51+
GlyphCount = context.ReadInt32(),
52+
Material = MaterialReader.ReadMaterialPointer(ref context),
53+
GlowMaterial = MaterialReader.ReadMaterialPointer(ref context),
54+
};
55+
56+
asset.Glyphs = context.ReadPointer<FontGlyph[]>(
57+
(ref ZoneReadContext pointerContext, ZonePointer<FontGlyph[]> pointer) =>
58+
{
59+
var glyphs = new FontGlyph[Math.Max(0, asset.GlyphCount)];
60+
for (var i = 0; i < glyphs.Length; i++)
61+
glyphs[i] = ReadGlyph(ref pointerContext);
62+
pointer.SetResult(glyphs);
63+
});
64+
65+
return asset;
66+
}
67+
68+
private static FontGlyph ReadGlyph(ref ZoneReadContext context) => new()
69+
{
70+
Letter = context.ReadUInt16(),
71+
X0 = context.ReadByte(),
72+
Y0 = context.ReadByte(),
73+
Dx = context.ReadByte(),
74+
PixelWidth = context.ReadByte(),
75+
PixelHeight = context.ReadByte(),
76+
Padding = context.ReadByte(),
77+
S0 = context.ReadFloat(),
78+
T0 = context.ReadFloat(),
79+
S1 = context.ReadFloat(),
80+
T1 = context.ReadFloat(),
81+
};
82+
}

FastFileLib/Iw4/Readers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ internal static class XAssetReaderRegistry
177177
[XAssetType.StructuredDataDef] = StructuredDataReader.Read,
178178
[XAssetType.Weapon] = WeaponReader.Read,
179179
[XAssetType.Material] = MaterialReader.Read,
180+
[XAssetType.Font] = FontReader.Read,
180181
// NOTE: XModel/Fx readers exist (ported for weapon sub-assets) but are NOT registered
181182
// as top-level readers — they mis-read some standalone xmodels (e.g. mp_rust errors with
182183
// "Invalid boolean value 255" on the 4th top-level XModel). Since xmodels precede

docs/IW4_Zone_Read_Path.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ Ported from Jacob Schroeder's FastFile (https://github.com/jacob-schroeder/FastF
2020

2121
Walks an inflated zone the engine's way — following the IW pointer conventions, **not** pattern-scanning. The XFile header, script strings, and asset-pool type list are **always complete**; asset **body** reading is registry-dispatched (`XAssetReaderRegistry` in `Readers.cs`).
2222

23-
Ported body readers (per file under `Iw4/`): `rawfile`/`localize`/`techset`/`stringtable` (`Readers.cs`), `menufile` (full `MenuReader` — windows/items/statements/event-handlers, `MenuReader.cs`+`MenuModels.cs`), `material`+`image` (`MaterialReader.cs`+`MaterialModels.cs`), `structureddatadef` (`StructuredData.cs`), `weapon` (`Weapon.cs`), `xmodel` (`XModel.cs`), `fx` (`Fx.cs`), `tracer` (`Tracer.cs`).
23+
Ported body readers (per file under `Iw4/`): `rawfile`/`localize`/`techset`/`stringtable` (`Readers.cs`), `menufile` (full `MenuReader` — windows/items/statements/event-handlers, `MenuReader.cs`+`MenuModels.cs`), `material`+`image` (`MaterialReader.cs`+`MaterialModels.cs`), `font` (`Font.cs`, registered top-level), `structureddatadef` (`StructuredData.cs`), `weapon` (`Weapon.cs`), `xmodel` (`XModel.cs`), `fx` (`Fx.cs`), `tracer` (`Tracer.cs`).
24+
25+
### Font reader (`Font.cs`) — registered so localize zones walk
26+
27+
`FontReader` is a faithful port of the reference `FontReader.cs` + `FontAsset.cs`. `Font_s` is a 24-byte root (`RootSize = 0x18`: name + pixelHeight + glyphCount + material + glowMaterial + glyphs pointers); the name, glyph table (`FontGlyph` = `0x18` bytes each), and materials are all in other (LARGE/shared) blocks, i.e. Offset pointers the local inline engine resolves to `default`. So a font's inline footprint is just the 24-byte root, which is exactly enough to **advance the walk past the fonts**. This matters because in the language zone `code_post_gfx_mp.ff` the pool order is `… Techset → Font×10 → Localize×6354 → Sound …` — the 10 fonts are the *only* thing between the walk and the 6359 localize entries. Registering `Font` lets the walk read the fonts, then all 6359 localize entries, then stop cleanly at the first `Sound` (asset #6435 of 6750).
2428

2529
### Material reader (`MaterialReader.cs` / `MaterialModels.cs`)
2630

@@ -48,7 +52,7 @@ The walk **stops cleanly at the first asset type without a ported reader** (an a
4852

4953
So MW2 PS3 **rawfile, localize, stringtable, weapon, techset, AND menufile** all come from the pointer-following reader instead of pattern-scanning (i.e. **no pattern matching for a fully-walked MW2 PS3 zone** — the Asset Pool "Status" column reads "IW4 pointer-walk" for every parsed type), populating both their dedicated tabs and the Asset Pool list view (the walk returns assets in pool order, so each list lines up with the pool records of that type). The IW4 rawfile `Offset`/`DataOffset` match `RawFileScanner`'s header/data offsets, so the in-place save path is unchanged.
5054

51-
It's used **only when the full IW4 walk completes** (a partial walk would miss assets → falls back to the scanner), and only for MW2 PS3 (`IsMW2File && !IsPC && !IsXbox360`). The IW4 reader classifies `.csv` files as stringtables (not rawfiles), so scanner-only rawfiles (the `.csv` tables) are merged back in so they stay editable in the Raw Files tab. The IW4 top-level reader registry covers rawfile/localize/stringtable/menufile/structureddatadef/techset/weapon/**material**; **image/xmodel are sub-assets only** (the walk stops at a top-level image), so any zone whose full walk completes contains none of those as pool entries — images stay on the `AssetRecordProcessor` (pattern-scan) path, as do all other games and platforms. Top-level `material` pool entries are now parsed by the walk (so a material no longer halts it) **and** mapped by `Iw4AssetBridge` (`Material`→`MaterialAsset`, with the root texture/constant/state-bit counts, the technique-set name, and per-texture `semantic : image` / per-constant detail when those tables are stored inline). They feed a **lazily-built "Materials" tab** (`MainWindowForm.PopulateMaterials`, double-click → `MaterialViewerForm`). For all other games the same tab is populated from the pattern scan (`AssetRecordProcessor` → `MaterialParser`), which only recovers the name — so the count/detail columns show `-` there.
55+
The bridge is used for MW2 PS3 only (`IsMW2File && !IsPC && !IsXbox360`). A **complete** walk is authoritative and replaces the scanner for every type it covers (rawfile/localize/stringtable/weapon/techset/menu/structdata). A **partial** walk (stopped at an un-ported type) is still used for any type it read *in full* — `Iw4BridgeResult.Complete` plus the declared per-type counts (`DeclaredLocalizeCount`/`DeclaredMaterialCount`, counted from the pool's `XAsset[]`) gate this: if the walk resolved **every** declared localize entry before stopping, the editor uses the pointer-walk localize instead of the `FF`-marker pattern scan (this is what makes `code_post_gfx_mp.ff`'s 6359 strings come from the reader even though the walk stops at `Sound` afterwards); same for materials. Types the partial walk didn't fully cover stay on the scanner for that zone (so nothing past the stop is lost). The IW4 reader classifies `.csv` files as stringtables (not rawfiles), so scanner-only rawfiles (the `.csv` tables) are merged back in so they stay editable in the Raw Files tab. The IW4 top-level reader registry covers rawfile/localize/stringtable/menufile/structureddatadef/techset/weapon/**material**/**font**; **image/xmodel are sub-assets only** (the walk stops at a top-level image), so any zone whose full walk completes contains none of those as pool entries — images stay on the `AssetRecordProcessor` (pattern-scan) path, as do all other games and platforms. Top-level `material` pool entries are now parsed by the walk (so a material no longer halts it) **and** mapped by `Iw4AssetBridge` (`Material`→`MaterialAsset`, with the root texture/constant/state-bit counts, the technique-set name, and per-texture `semantic : image` / per-constant detail when those tables are stored inline). They feed a **lazily-built "Materials" tab** (`MainWindowForm.PopulateMaterials`, double-click → `MaterialViewerForm`). For all other games the same tab is populated from the pattern scan (`AssetRecordProcessor` → `MaterialParser`), which only recovers the name — so the count/detail columns show `-` there.
5256

5357
**Why a map zone's materials don't parse yet (e.g. `mp_rust.ff`):** IW4 asset order is `… xmodel → material → techset → image → sound …`, so the body walk must read every **XModel** before it reaches the materials. `XModelReader.Read` exists (ported for weapon sub-assets) but mis-reads some *standalone* xmodels — registering it as a top-level reader makes `mp_rust` error with "Invalid boolean value 255" on the 4th XModel, still **before** the 466 materials. So `XModel`/`Fx` are deliberately **not** registered, the walk stops cleanly at the first XModel (asset #42 of 819), the bridge falls back to the scanner, and the materials stay typed-but-unparsed. Reaching a map zone's materials requires **completing the XModel body reader** first; after that the material reader (already correct) and the Materials-tab plumbing light up automatically. `MW2GameDefinition.IsMaterialType` (id `0x05` on all platforms) is overridden so the Asset Pool labels these as a known view-only type ("External reference …") rather than "Not parsed (unsupported type)". `MW2GameDefinition.IsTechSetType` is overridden (techset id PS3 `0x08`/Xbox360 `0x07`/PC `0x09`) so the pool view and per-type counts recognise techset records.
5458

0 commit comments

Comments
 (0)