VerifiedBuild 2026.06.26.1097863.1, CU2

Tag files describe themselves

Every shipped tag carries a blay layout section holding its own field names, type names, and enum option names. The definitions for all 101 tag groups are recoverable from the shipped data alone, without touching the engine binary.

Why this matters

The expected route to a tag editor was recovering field definitions from HaloSimulation_tag_release.dll, since Blam tag builds historically embed their definition tables. That work is no longer on the critical path. The strings below came out of a shipped weapon tag, and they are the same human-readable names Guerilla displayed:

object flags
long flags
does not cast shadow
search cardinal direction lightmaps on failure
rounds total maximum
early mover localized physics

Because each tag carries its own layout, a reader can be entirely generic. No hand-written per-group parsers and no hardcoded offsets are required, which is the failure mode that makes most Halo tag tooling brittle across builds.

One shape, all the way down

The entire tag body is built from a single repeating shape: a 12-byte header of a four-CC magic, a version, and a content size, followed by that many bytes of content. The size excludes the header. Sections chain as siblings and nest as children at every level, so one generic walker reads the whole file.

SectionKindContentsEvidence
blaylayoutThe tag's own field definitionsVerified
tglycontainerHolds every definition tableVerified
str*blobNUL-separated UTF-8 strings, referenced by byte offsetVerified
optionstableString offsets, one per enum or bitfield optionVerified
tgfttableTypes: name, on-disk size, composite flagVerified
grastableFields: name, type indexVerified
blv2tableBlocks: name, maximum element countVerified
stv4tableStructs: 16-byte GUID and nameVerified
bdatdataThe tag's actual field valuesVerified
tgblcontainerWraps the data payloadVerified

Strings are referenced by byte offset into the blob rather than by index, and an offset pointing at a NUL resolves to the empty string, which the data uses for unnamed fields such as padding. The blob is byte-packed, so nothing is reliably dword aligned.

A group decoded end to end

camera_track is the smallest group and decodes completely. This is the real Halo definition, including the engine's actual 16 control-point limit. Terminator fields delimit struct boundaries, so the field list is a flattened tree rather than a nested one.

types:  [0] block            12b  composite
        [1] real vector 3d   12b
        [2] real quaternion  16b
        [3] terminator X      0b

fields: +0   12b  real vector 3d   position
        +12  16b  real quaternion  orientation
        +28   0b  terminator X     <unnamed>
        +28  12b  block            control points
        +40   0b  terminator X     <unnamed>

blocks: camera_track_control_point_block  max 16
        camera_track_block                max 1

Definition size by group

All 101 groups parse. Groups with no enums or bitfields carry an empty option table.

GroupFour-CCStringsFields
characterchar1,0381,248
bipedbipd1,031582
weaponweap783503
chud_definitionchdt899612
collision_modelcoll270388
camera_tracktrak105

What is not solved yet

Hypothesis

Zero-size types carry payload elsewhere

Five types report a size of zero: array, custom, pad, struct, and terminator. Pad in particular must consume bytes, so the field record's auxiliary word is presumed to carry the length, the struct target, and the option run an enum or bitfield owns. That mapping is not yet established.

The data walk is not yet proven

The decisive test is walking the bdat payload with the field list and asserting it is consumed exactly, for all 12,290 shipped tags. Until that passes, field offsets into the data are inferred rather than confirmed.

A previous revision of this page reported that the option table overran its own section in 16 of 101 groups. That was a misreading: the word in question is a byte size, not an entry count. Read correctly, no group overruns.

Tooling

A Rust workspace under crates/ reads containers and layouts directly. ue-iostore parses UE 5.5 IoStore containers, blam-tag parses the container header and section tree, blam-defs holds the shared definition model, and blam-cli exposes it as the mjolnir command. It parses 101 of 101 groups across all 12,290 shipped payloads and recovers a complete field list for every one.

54 distinct field type names appear across the corpus, and every one has the same size in every group it appears in. That consistency is the strongest available check that the type table decode is correct.

Reproduce

Read-only against your own installed copy. Any oo2core_9_win64.dll from a local Unreal Engine install works, since UE 5.5 statically links Oodle and the game ships no separate DLL. Nothing is written to disk.

$env:HCE_PAKS = "<install>\Meteorite\Content\Paks"
$env:OODLE    = "<UE>\Engine\Binaries\DotNET\AutomationTool\oo2core_9_win64.dll"

# Every group, with its definition table sizes
cargo run --release -p blam-cli -- groups

# One group's section tree and type, block, and struct tables
cargo run --release -p blam-cli -- layout --group camera_track --tables

# A group's resolved field list with running offsets
cargo run --release -p blam-cli -- fields --group weapon

# The field type vocabulary and its sizes
cargo run --release -p blam-cli -- types

Full working notes: tag_body_format.md. Container and packaging detail: Blam tag data.