Skip to content

Research Report Cmfmt

LopeKinz edited this page Sep 26, 2026 · 1 revision

cm (Collision): Layout, Round-Trip, Box-Hull, Binding

Worker w-cmfmt-a2 (Attempt 2/3; Attempt 1 paused, Attempt 2 cut off by session limit), 2026-09-23. Files: tools/probe_cmfmt.py, re_probes/cmfmt/, this report.

Result in One Sentence

cm is a small id-prefix (Surface Records) plus a Havok Binary Tagfile 2016 (hknp); the parser reads every byte typed, and the writer writes back 5166/5166 Retail Payloads from 3 Archives byte-identically, 19 mutation classes are accepted 0/299, and a new Box Hull is generated which is structurally equivalent to a Retail func_collision_only Box. Not tested at runtime.

1. What Was Wrong with Attempt 1 (Corrected Before Each Number)

The aborted run placed two unmeasured Polytope rules into the parser; consequently, the Round-Trip fell to 5145/5166. Additionally, the Box was incorrect.

Claim Attempt 1 Measured Now
Vertex w = 0x3F000000 | i for all vertices 21 files break this Array padded to a multiple of 4 with exact copies of the last real vertex, 23004/23004
Faces tile the index array 116 Polytope splits index runs (e.g., in _combo/world.hkshape) each Face-Range in the array, summing all indices
vertexEdges[v] = first outgoing edge 0/22998 (last: also 0) only "one edge starting at v" holds (22998/22998); Box carries the Retail Array of identical topology
Box carries Records from Template Template amb_volume_hull_1 is a Trigger (0x4000) Default Record = that of Retail Solid Brushes
Mass = Template values, "half floats" Incorrect: packed int16 vectors Recalculated from Box geometry (Section 5)
TNA1 "not decoded" (old layout note) Outdated convex_layout.txt deleted

2. Layout (Code Evidence from re_probes/runtime_87400/module.bin)

Type cm -> idHavokShapeResource (Registration .text 0xC0B64, String .rdata 0x2654790). Loader .text 0x723C30, called from 0x7243AF with version = entry+0x68 (all 71672 cm entries across all archives: 21).

u32 n
n x { u32 f0 -> rt+0x08, f1 -> rt+0x10, f2 -> rt+0x0C,
      f3 -> rt+0x14 (only version >= 0x12), f4 -> rt+0x18, f5 -> rt+0x1C, f6 -> rt+0x20 }
      (Runtime-Stride 0x38, rt+0 = pointer to the resource)
u32 blob_size           (must land exactly at the Payload end)
blob                    Havok "TAG0", SDKV 20160200

After Havok Load: 0x72428B..0x7242AD replaces for each Shape shape+0x30 (hknpShape.userData) with records + userData*0x38. userData is the Record Index.

Tagfile, sections always TAG0(SDKV DATA TYPE(TPTR TSTR TNA1 FSTR TBDY THSH TPAD) INDX(ITEM PTCH)) (5166/5166). Fully decoded: Type table (Names, Templates, Parents, Subtype Flags, Sizes, Members), DATA over the Type table (each byte leaf, pointer/array slot, or checked null), ITEM (12 bytes: type|flag, offset, count), PTCH (Offsets of slots that an ITEM-index refers to). The writer generates Layout, ITEM, PTCH, TSTR/FSTR/TNA1/TBDY/THSH and all section headers from the object graph; only leaf values are inherited.

Root types: hknpCompoundShape 3514, hknpCompressedMeshShape 1256, hknpConvexPolytopeShape 396. Havok is indeed it - but without opaque bytes.

Record Fields

Names from tables in the image:

Field Meaning Source Evidence
f0 idContents Bitmask Pointer table .rdata 0x2EEC880, Index = Bit ("unused bit 9" at 9, "unused bit 24" at 24) Names with trigger/volume: 334/334 have bit 14 trigger (Base 7 %); fx_func_water = water
f1 surfType_t .rdata 0x248A9A8, 0..38 glass names 66/68 GLASS* (Base 5 %), fabric 15/16 (4 %), rock 13/21 (7 %); water = LIQUID
f2 SURF_*-Flags .rdata 0x248AD68 only 6 values: 0, COLLISION, NOIMPACT, NOAREAS, NOIMPACT|NOAREAS, SLICK|NOIMPACT|STEEP
f3 0 - 21673/21673
f4..f6 -1.0f - 21673/21673, meaning open

f0/f1 are correlation proofs, not consumer code interpretation. The first f1 correlation (only userData-Records) was negative and incorrectly configured; Record usage also occurs via other paths (referenced in 4729 files, no userData some Records - presumably Shape-Tags in Compressed Meshes, unaccounted for).

3. Round-Trip - the controlling mechanism

python tools/probe_cmfmt.py --roundtrip -> re_probes/cmfmt/roundtrip.txt

Archive Result
base/chunk_8.resources 907/907 byte-identical
base/chunk_16.resources 1469/1469
dlc/dlc_2/base/chunk_22.resources 2790/2790 (5 empty cm entries)
total 5166/5166, 108 MB Payload

Cm.parse is strict: type table fully resolvable, layout rule per ITEM, gaps null, non-field bytes null, every reference type-checked, PTCH == found slots, polytope count consistent.

4. Negative Controls

python tools/probe_cmfmt.py --negative -> negative_controls.txt, 299 Payloads (100 per archive, distributed by size). Accepts 0 for all 19 classes: Record count +-1, blob_size +-4, end -1/-16/+8, ITEM-Count +1, Primitive-Stride +4/+1, PTCH-Offset +4, byte in field padding, object pointer on array, SDKV, TNA1 count, Record-Stride 32/24, ITEM-Stride 16/8. The writer may fail: 16->8 Array Alignment changes 298/299. Additionally at the Box-File: broken Vertex w and Face-Range outside are rejected.

5. Box-Hull (new, RUNTIME UNTESTED)

python tools/probe_cmfmt.py --box OUT.cm --half-extents 32,32,64 -> re_probes/cmfmt/box_64x64x128.cm (4532 B, sha256 0f35940e…df3d, deterministic).

Inherited from the template: type table, root scalars (Flags, Type, Dispatch, convexRadius 0.01, Property-Key) and vertexEdges (identical topology; Havoks choice of outgoing edge is not reconstructed). Newly generated: Vertices, planes, faces, indices, faceLinks (twin half-edges, 22883/22883 Retail), mass, penetration, records, object graph.

Mass, measured on 1762 retail axis boxes:

  • Volume = product of edge lengths including 2x convexRadius, Mass = 1000 x Volume;
  • Inertia = 1.5 x full cuboid tensor, 1762/1762 within 4 pack quanta (1742 in 1);
  • hkPackedVector3: 3x int16 + upper half of a float scaler, largest component in [2^14, 2^15): packs 45980/45980 retail vectors byte-exactly, zero vector 0000000000008019 (28/28); majorAxisSpace = 00800080008030f5 (1762/1762).
  • Template rebuilt: inertia byte-identical, mass/volume 2 ulp off.

maxAllowedPenetration (hkHalf16 = upper 16 bits of float32): Value/(smallest half-edge + radius) in [0.3288, 0.3313] over 1762/1762. Exact rule not found; round16(0.3305 x Basis) hits 1673/1762 bit-exactly.

Record Default (0x1 solid, 1 METAL, 0x40 SURF_COLLISION, 0, -1, -1, -1): exactly the records from maps/game/dlc/c02/c2v1/c2v1_geo_lobby_func_collision_only_2_1509497262.hkshape, a retail box of identical file size (4532 B, 1 polytope, 8 vertices/6 faces, half-edges ~0.06 x 0.40 x 0.40). This retail box passes the Box Checker; its face order is different - both are thus retail practice.

Checks (check_box, --selftest): strict parse + round-trip, corners, every plane supporting, winding outside, connectivity, mass, penetration; the same checker rejects false mass (32,32,65). None of this proves that the engine loads the file.

6. What a Hull Binds

Exclusively the Name, built in code:

  • Model: <model> + Suffixes in fixed order (.text 0x725280): $scale=%e,%e,%e $filterclip=%x $overridematerial=%s $simplify=%i $convexdecomposition=%i,%g $part=%d $filteroccluders=true.
  • Entity with clipModelName -> <clipModelName>.hkshape (.hkshape referenced at .text 0xAF3317). Retail example, exact case for a custom box: c2v1_geo_lobby_func_collision_only_2_1509497262, CLIPMODEL_AUTO, clipModelName = "maps/game/dlc/c02/c2v1/c2v1_geo_lobby_func_collision_only_2_1509497262".
  • World: <map> + /_combo/world.hkshape (.text 0x4B0741, String .rdata 0x26232D0). c2v1: hknpCompoundShape from 34 Convex Polytopes + hkcdDynamicTree (69 Codec32 Nodes).
  • CLIPMODEL_BOX/CYLINDER/SPHERE/RENDERMODEL_BOUNDS/NONE do not carry a clipModelName in c2v1-3 (151/161/1/218/708) - thus no name lookup; the shape comes from size or similar.

--bind via entities of c2v1-3: 28151/28431 clipModelName resolves to an existing cm (Prefix level: exact suffix values are not reconstructed).

Evidence for Map Alias: the installed dlc/dlc_4/base/chunk_22.resources contains 0 of 6985 entries with c2v1_custom in the name, thus also no maps/game/dlc/c02/c2v1_custom/_combo/world.hkshape. The bundle re_probes/idclwriter/chunk_22.resources has it (byte copy of c2v1, parsed and round-tripped). Whether the engine searches for a world cm under an alias name follows from 0x4B0700 only if the alias name arrives there - unchecked.

7. Open

  • Rewriting World/Compound Collision requires the hkcdDynamicTree BVH and for Meshes the hknpCompressedMeshShape tree. Both are read and written byte-exactly but not recalculated. Reusing as a byte copy is possible; instances shifting or new world geometry requires a newly calculated BVH - not yet.
  • Exact rule for maxAllowedPenetration and Havoks choice in vertexEdges.
  • The semantics of f4..f6 and the path of records not referenced by any userData.
  • Runtime: everything above is "structurally valid after these checks".

Clone this wiki locally