United Offensive BSP Sepecification¶
Format verdict¶
Call of Duty: United Offensive 1.51 uses the Call of Duty 1 bsp
layout family. The file is little-endian IBSP version 59 (0x3b),
has a fixed 272-byte header, and contains 33 lump descriptors. Every
fixed record size proved by the CoD:UO collision and renderer loaders
matches the corresponding record size on the CoD 1 format page. It is
not the substantially different CoD 2 version-4 format.
The equivalence claim is deliberately bounded: the container, directory, records, and cross-references documented below match. It does not imply that the CoD and CoD:UO map compilers always emit identical payloads, nor that their entity and script content is interchangeable.
The CoD:UO recovery resolves two points left unclear by the older CoD 1 page:
A lump descriptor stores
fileLengthfirst andfileOffsetsecond.Lump 32 is part of the 33-entry header. It is an optional, fixed-topology light-visibility cache, not data outside the lump directory.
Slots 5 and 31 remain unidentified because no recovered CoD:UO runtime loader consumes them. They are kept as honest unknowns rather than assigned a plausible name.
Binary conventions¶
Unless a field says otherwise:
Integers and IEEE-754 floats are little-endian.
Offsets are absolute byte offsets from the start of the file.
int16andint32are signed;uint8,uint16, anduint32are unsigned.vec2is two consecutive 32-bit floats;vec3is three.Record arrays are tightly packed according to the sizes below.
The stock
CM_SaveLumpwriter lays lumps out in index order and aligns each payload start to four bytes. Readers use the offsets in the directory and do not otherwise require physical index order.
Header¶
The header occupies bytes 0x000 through 0x10f.
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
ASCII |
|
4 |
|
|
|
|
264 |
|
|
Thirty-three 8-byte descriptors. |
Each directory entry is:
Relative offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Payload length in bytes. |
|
4 |
|
|
Absolute payload offset. |
Equivalent C declarations are:
typedef struct {
int32_t fileLength;
int32_t fileOffset;
} Lump;
typedef struct {
int32_t ident;
int32_t version;
Lump lumps[33];
} BspHeader; /* 272 bytes / 0x110 */
The retail loaders copy and byte-normalize all 68 header words and
compare the version with 59. The collision loader does not reject a
wrong ident, but a format reader should still require IBSP.
Lump directory at a glance¶
Variable means the payload is not a homogeneous fixed-record array.
A dash means that the CoD:UO runtime supplies no proved record size or
interpretation.
Index |
Lump |
Unit size |
Primary consumer |
Summary |
|---|---|---|---|---|
0 |
Materials / shaders |
72 |
Collision and renderer |
Name plus surface and content flags. |
1 |
Lightmaps |
786,432 |
Renderer |
One 512 x 512 RGB image per unit. |
2 |
Planes |
16 |
Collision and renderer |
Normal and distance. |
3 |
Brush sides |
8 |
Collision |
Axial distance or plane index, plus material. |
4 |
Brushes |
4 |
Collision |
Side count and material; side spans are implicit. |
5 |
Unknown / unused |
None found |
Directory slot exists; runtime meaning unresolved. |
|
6 |
Triangle-soup surfaces |
16 |
Renderer |
Material, lightmap, vertex span, and index span. |
7 |
Draw vertices |
44 |
Renderer |
Position, UVs, lightmap UVs, normal, and color. |
8 |
Draw indices |
2 |
Renderer |
Signed local vertex indexes. |
9 |
Cull groups |
32 |
Renderer |
Bounds and surface span. |
10 |
Cull-group indexes |
4 |
Renderer |
Indexes selected by cells. |
11 |
Portal vertices |
12 |
Renderer |
Shared 3D vertex pool. |
12 |
Occluders |
20 |
Renderer |
Plane, edge, and vertex spans. |
13 |
Occluder plane indexes |
4 |
Renderer |
Indexes into lump 2. |
14 |
Occluder edges |
4 |
Renderer |
Two local plane and two local vertex indexes. |
15 |
Occluder indexes |
2 |
Renderer |
Indexes selected by cells. |
16 |
AABB trees |
12 |
Renderer |
Surface spans and implicit child counts. |
17 |
Cells |
52 |
Renderer |
Bounds and spans of portals, groups, and occluders. |
18 |
Portals |
16 |
Renderer |
Plane, destination cell, and vertex span. |
19 |
Light indexes |
2 |
Renderer |
Per-leaf light references and sun sentinel. |
20 |
Nodes |
36 |
Collision and renderer |
BSP plane, children, and integer bounds. |
21 |
Leaves |
36 |
Collision and renderer |
Cluster, area, terrain, brush, cell, and light spans. |
22 |
Leaf-brush indexes |
4 |
Collision |
Indexes into lump 4. |
23 |
Leaf-surface indexes |
4 |
Collision |
CoD:UO collision uses these as terrain-patch indexes. |
24 |
Terrain patches |
16 |
Collision |
Curve or indexed-terrain descriptor. |
25 |
Terrain vertices |
12 |
Collision |
Shared 3D vertex pool. |
26 |
Terrain indexes |
2 |
Collision |
Signed local vertex indexes for terrain triangles. |
27 |
Models |
48 |
Collision and renderer |
Bounds plus surface, leaf-surface, and brush spans. |
28 |
Visibility |
Variable |
Collision |
Two-word header plus cluster visibility rows. |
29 |
Entities |
Variable |
Collision and renderer |
NUL-terminated text entity definitions. |
30 |
Static lights |
72 |
Renderer |
Typed light record with a 16-byte parameter union. |
31 |
Unknown / unused |
None found |
Directory slot exists; runtime meaning unresolved. |
|
32 |
Light-visibility cache |
12 |
Renderer |
Optional 8192 x 32-entry cache; 3,145,728 bytes when complete. |
Lump details¶
Lump 0 - Materials / shaders¶
Each 72-byte material record is shared directly by the collision and renderer loaders.
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
64 |
|
|
NUL-terminated material or shader path. |
|
4 |
|
|
Surface behavior and material class flags. |
|
4 |
|
|
Collision/content mask. |
The fixed name field can hold at most 63 bytes plus its terminator.
Lump 1 - Lightmaps¶
The lump is a concatenation of 512 x 512, three-channel lightmaps. Channels are stored as one byte each, giving:
512 * 512 * 3 = 786,432 bytes per lightmap
The renderer derives the expected number of images from the maximum
nonnegative lightmapNum used by lump 6. A nonempty lump must contain
exactly that many images. At load time, the original 512-square RGB
images may be packed into larger RGBA GPU atlases; that atlas is runtime
state, not part of the file.
Lump 2 - Planes¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
12 |
|
|
Plane normal. |
|
4 |
|
|
Distance from the origin. |
The plane equation used by collision traversal is
dot(normal, point) - distance.
Lumps 3 and 4 - Brush sides and brushes¶
Each 4-byte brush record is:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Number of consecutive records in lump 3. |
|
2 |
|
|
Index into lump 0 for the brush contents. |
Brushes do not store a first-side index. Their spans are concatenated in lump 3, so a reader maintains a running side cursor across lump 4.
Each 8-byte brush-side record is:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Axial distance for the first six sides of a brush; lump-2 plane index for later sides. |
|
4 |
|
|
Index into lump 0 for this side. |
The first six sides encode the axis-aligned bounds. Additional sides describe non-axial clipping planes. A valid collision brush therefore has at least six sides.
Lump 5 - Unknown / runtime-unused¶
The slot is present in the 33-entry header but is not passed to any CoD:UO collision or renderer loader recovered in this tree. No record size or meaning is assigned here.
Lump 6 - Triangle-soup surfaces¶
The older CoD 1 page calls these triangle soups. CoD:UO uses one 16-byte record per render surface:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Index into lump 0. |
|
2 |
|
|
Lightmap index; negative values select non-lightmapped modes. |
|
4 |
|
|
First record in lump 7. |
|
2 |
|
|
Number of vertices in the surface-local span. |
|
2 |
|
|
Number of indexes in the surface-local span. |
|
4 |
|
|
First record in lump 8. |
indexCount is normally a multiple of three. Each selected lump-8
value is local to the surface and is added to firstVertex.
Lump 7 - Draw vertices¶
Each renderer vertex is 44 bytes:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
12 |
|
|
World-space position. |
|
8 |
|
|
Base material texture coordinates. |
|
8 |
|
|
Lightmap texture coordinates. |
|
12 |
|
|
Vertex normal. |
|
4 |
|
|
Packed vertex color, including alpha. |
This compact 44-byte layout is one of the clear differences from the 68-byte CoD 2 vertex record.
Lump 8 - Draw indexes¶
The lump is an array of signed 16-bit indexes. Indexes are relative to
the owning lump-6 surface’s firstVertex, not absolute indexes into
the complete vertex lump.
Lump 9 - Cull groups¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
12 |
|
|
Minimum bounds. |
|
12 |
|
|
Maximum bounds. |
|
4 |
|
|
First lump-6 surface. |
|
4 |
|
|
Number of surfaces. |
Lump 10 - Cull-group indexes¶
The lump is an array of 32-bit indexes into lump 9. Cells select
contiguous spans of this index array through firstCullGroup and
cullGroupCount.
Lump 11 - Portal vertices¶
The lump is a shared array of vec3 positions, 12 bytes each. Lump 18
portals and lump 12 occluders both select spans from this pool.
Lump 12 - Occluders¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
First record in lump 13. |
|
2 |
|
|
Number of occluder planes. |
|
2 |
|
|
Number of occluder edges. |
|
4 |
|
|
Runtime destination offset for the edge span. |
|
4 |
|
|
First vertex in lump 11. |
|
2 |
|
|
Number of vertices. |
|
2 |
padding |
Natural trailing padding; not consumed. |
There is one original-loader subtlety: the renderer restarts its input
lookup at lump-14 edge zero for each occluder, while firstEdge
selects the runtime destination span. Tools seeking retail behavior
should preserve that access pattern instead of automatically adding
firstEdge to the input index.
Lump 13 - Occluder plane indexes¶
The lump is an array of signed 32-bit indexes into lump 2. Lump-14 plane bytes are local indexes into the owning occluder’s selected plane span.
Lump 14 - Occluder edges¶
Each 4-byte record is:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Two local indexes into the owning occluder’s planes. |
|
2 |
|
|
Two local indexes into the owning occluder’s vertices. |
Lump 15 - Occluder indexes¶
The lump is an array of signed 16-bit indexes into lump 12. Cells select
contiguous spans through firstOccluder and occluderCount.
Lump 16 - AABB trees¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
First lump-6 surface for a terminal tree. |
|
4 |
|
|
Number of terminal surfaces. |
|
4 |
|
|
Number of immediate child records. |
Tree bounds are not serialized in this lump. Records are stored in
depth-first order. When childCount is nonzero, the child records
immediately follow the parent; the renderer recursively derives bounds
from descendants. A terminal record has childCount == 0 and derives
bounds from its surfaces.
Lump 17 - Cells¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
12 |
|
|
Minimum bounds. |
|
12 |
|
|
Maximum bounds. |
|
4 |
|
|
Index into lump 16. |
|
4 |
|
|
First record in lump 18. |
|
4 |
|
|
Number of portals. |
|
4 |
|
|
First record in lump 10. |
|
4 |
|
|
Number of cull-group indexes. |
|
4 |
|
|
First record in lump 15. |
|
4 |
|
|
Number of occluder indexes. |
Lump 18 - Portals¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Index into lump 2. |
|
4 |
|
|
Destination cell in lump 17. |
|
4 |
|
|
First vertex in lump 11. |
|
4 |
|
|
Number of portal vertices. |
Portals are directed records. The owning cell is determined by the
cell’s portal span; cellIndex identifies the cell reached through
the portal.
Lump 19 - Light indexes¶
The lump is an array of signed 16-bit indexes into lump 30. Each leaf
selects a span through firstLightIndex and lightCount. If the
first selected value is negative, the renderer treats it as a sunlight
marker, skips that element, and uses the remaining indexes as ordinary
static-light references.
Lump 20 - Nodes¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Split plane in lump 2. |
|
8 |
|
|
Nonnegative node indexes; negative leaf encodings. |
|
12 |
|
|
Serialized integer minimum bounds. |
|
12 |
|
|
Serialized integer maximum bounds. |
A negative child value n selects leaf -1 - n, so -1 is leaf
0. The CoD:UO collision and renderer loaders advance over the bounds but
do not use them when building their runtime node records.
Lump 21 - Leaves¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Visibility cluster; negative means no cluster. |
|
4 |
|
|
Collision-area index. |
|
4 |
|
|
First record in lump 23. |
|
4 |
|
|
Number of lump-23 entries. |
|
4 |
|
|
First record in lump 22. |
|
4 |
|
|
Number of lump-22 entries. |
|
4 |
|
|
Renderer cell in lump 17. |
|
4 |
|
|
First record in lump 19. |
|
4 |
|
|
Number of lump-19 entries, including a possible sun marker. |
The renderer consumes cluster, cell, and light fields. Collision consumes cluster, area, terrain-patch, and brush fields. The shared 36-byte layout is therefore proved by two independent loader paths.
Lump 22 - Leaf-brush indexes¶
The lump is an array of signed 32-bit indexes into lump 4. Leaves and models select contiguous spans from it.
Lump 23 - Leaf-surface indexes¶
The historical name is leaf surfaces, but the CoD:UO collision loader treats each 32-bit entry as an index into lump 24. Leaves and models select spans of terrain/curve collision patches through this array.
Lump 24 - Terrain and curve collision patches¶
Every 16-byte record begins with a common four-byte prefix:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Index into lump 0. |
|
1 |
|
|
|
|
1 |
padding |
Alignment before the 12-byte union. |
When collisionMode == 0, bytes 0x04 through 0x0f are:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Curve-grid width. |
|
2 |
|
|
Curve-grid height. |
|
4 |
|
|
Integer subdivision-error threshold. |
|
4 |
|
|
First grid point in lump 25. |
When collisionMode != 0, the union is:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
2 |
|
|
Number of selected lump-25 vertices. |
|
2 |
|
|
Number of selected lump-26 indexes. |
|
4 |
|
|
First vertex in lump 25. |
|
4 |
|
|
First index in lump 26. |
Lumps 25 and 26 - Terrain vertices and indexes¶
Lump 25 is a shared array of 12-byte vec3 positions. Lump 26 is an
array of signed 16-bit indexes used by terrain-mode lump-24 records.
Those indexes are local to the selected vertex span and are consumed in
triangle groups.
Curve-mode records use a rectangular width * height sequence from
lump 25 and do not select lump-26 indexes.
Lump 27 - Models¶
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
12 |
|
|
Minimum bounds. |
|
12 |
|
|
Maximum bounds. |
|
4 |
|
|
First lump-6 render surface. |
|
4 |
|
|
Number of render surfaces. |
|
4 |
|
|
First record in lump 23. |
|
4 |
|
|
Number of lump-23 entries. |
|
4 |
|
|
First record in lump 22. |
|
4 |
|
|
Number of lump-22 entries. |
Model 0 is the world model. Later records are inline brush models
addressed by the engine as *1, *2, and so on.
Lump 28 - Visibility¶
A nonempty visibility lump begins with:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Number of visibility rows. |
|
4 |
|
|
Bytes in each row. |
|
Variable |
|
|
|
CM_ClusterPVS(cluster) returns data + cluster * bytesPerCluster.
An empty lump is allowed; the collision system then creates an
all-visible fallback row whose byte count is the cluster count rounded
up to a 32-byte boundary.
Lump 29 - Entities¶
This lump is a byte string containing brace-delimited, quoted key/value entity definitions. A minimal shape is:
{
"classname" "worldspawn"
"ambient" "0.2"
}
The format should end the payload with a NUL byte. The renderer reads
lighting keys from worldspawn and recognizes renderer-only entities
such as misc_model and corona; the game module parses the same
text for gameplay entities. Entity classes and keys are content
conventions layered on the BSP container, not additional binary records.
Lump 30 - Static lights¶
Each light record is 72 bytes:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Light type, values below. |
|
12 |
|
|
Source RGB light color. |
|
12 |
|
|
Point/spot origin. |
|
12 |
|
|
Sun or spot direction. |
|
16 |
union |
|
Type-selected attenuation/spot parameters. |
|
16 |
bytes |
|
Present in the record; no CoD:UO renderer read proves its meaning. |
Proved type values are:
Value |
Type |
Parameter use |
|---|---|---|
1 |
Sun |
Direction; no union member used. |
2 |
Point |
Position; implicit quadratic attenuation 1.0. |
3 |
Linear point |
|
4 |
Custom point |
|
5 |
Spot |
|
6 |
Color-only / exact source name unresolved |
Common color terms only. |
7 |
Custom spot |
Quadratic at |
8 |
Diffuse sun |
No additional disk fields consumed by the static-light loader. |
The last 16 bytes are intentionally left unnamed. Their presence and extent are proved, but no field-specific read in the CoD:UO renderer establishes a source meaning.
Lump 31 - Unknown / runtime-unused¶
As with lump 5, the directory slot exists but no recovered CoD:UO collision or renderer path consumes it. No format claim beyond that fact is made here.
Lump 32 - Light-visibility cache¶
This optional cache is part of the normal 33-lump directory. When fully compiled, it contains 8192 hash buckets with 32 entries per bucket. Each entry is 12 bytes, so the complete payload is:
8192 * 32 * 12 = 3,145,728 bytes (3 MiB)
Each disk entry is:
Offset |
Size |
Type |
Field |
Meaning |
|---|---|---|---|---|
|
4 |
|
|
Packed Y/X/cluster lookup key. |
|
1 |
|
|
|
|
5 |
|
|
Results for diffuse-sun step counts 1 through 5. |
|
2 |
|
|
Per-leaf light visibility bits; bit |
The key and bucket are computed as:
key = ((uint32_t)gridY << 22)
| (((uint32_t)gridX & 0x3ff) << 12)
| ((uint32_t)cluster & 0xfff);
bucket = (reverse_bits((uint32_t)gridZ)
+ (uint32_t)gridY * 0x0c41
- (uint32_t)gridX * 0x0c3d) & 0x1fff;
The renderer selects one of the five diffuse-sun bytes according to its current sampling setting when it converts the disk cache to the smaller runtime cache. A zero-length lump means that no precompiled cache is present.
The shipped loader contains a notable validation bug: its cache initializer returns success even when the payload size is wrong, making the caller’s “funny lump size” error unreachable. Format tools should require either zero bytes or the complete 3 MiB payload rather than copying that bug.
Cross-lump relationships¶
Source |
Fields |
Target |
|---|---|---|
Lump 3 brush side |
|
Lumps 0 and 2 |
Lump 4 brush |
|
Lumps 0 and 3 |
Lump 6 surface |
material, lightmap, vertex, and index fields |
Lumps 0, 1, 7, and 8 |
Lump 9 cull group |
surface span |
Lump 6 |
Lump 10 index |
cull-group index |
Lump 9 |
Lump 12 occluder |
plane, edge, and vertex spans |
Lumps 13, 14, and 11 |
Lump 13 index |
plane index |
Lump 2 |
Lump 14 edge |
local plane and vertex indexes |
Owning lump-12 spans |
Lump 15 index |
occluder index |
Lump 12 |
Lump 16 AABB tree |
surface span or implicit children |
Lump 6 or following lump-16 records |
Lump 17 cell |
AABB tree and index spans |
Lumps 16, 18, 10, and 15 |
Lump 18 portal |
plane, destination cell, and vertex span |
Lumps 2, 17, and 11 |
Lump 19 index |
light index or negative sun marker |
Lump 30 |
Lump 20 node |
plane and child indexes |
Lump 2, lump 20, and lump 21 |
Lump 21 leaf |
terrain, brush, cell, and light spans |
Lumps 23, 22, 17, and 19 |
Lump 22 index |
brush index |
Lump 4 |
Lump 23 index |
terrain-patch index |
Lump 24 |
Lump 24 patch |
material and vertex/index spans |
Lumps 0, 25, and 26 |
Lump 27 model |
render-surface, leaf-surface, and brush spans |
Lumps 6, 23, and 22 |
Relationship to the CoD 1 and CoD 2 pages¶
The older CoD 1 page and the CoD:UO loaders agree on version 59, all named lump indexes, and every fixed size listed there. CoD:UO therefore belongs to that same format generation. This recovery adds field layouts, cross-reference semantics, the length-before-offset directory order, and the identity of lump 32.
The CoD 2 page describes a different format generation: version 4, different lump numbering, additional light-grid/collision sections, and a 68-byte draw vertex. Those CoD 2 structures must not be applied to CoD:UO maps.
Evidence and confidence¶
This specification is derived from maintained recovered types and direct loader behavior, not from a guessed extension of the two wiki pages.
Primary CoD:UO evidence:
src/qcommon/bsp_types.h: version, descriptor order, 33-lump enumeration, and shared disk types.src/qcommon/collision_map_types.h: 72-byte material record.src/collision/collision_map_load.c: collision-owned records and cross-lump behavior. Its loader graph is matched against CoDUOMP.exe0x0041c400..0x0041d7d1andcoduo_lnxded0x0804a06c..0x0804bc57.src/client/engine/renderer/backend.handsrc/client/engine/renderer/renderer_world_load.c: renderer-owned records and load graph. The Windows world-loader range is0x0050ab70..0x0050e774across its direct helpers.src/client/engine/renderer/renderer_lightmap.c: 512-square RGB lightmaps.src/client/engine/renderer/renderer_light_visibility.c: lump-32 topology, record fields, key, hash, and save path.docs/MAP_VALIDATOR_RULES.md: stricter validation rules for safely accepting CoD:UO multiplayer maps.
The checked Linux dedicated binary is SHA-256
24b0d8269a1ae97e9fe29cd338288e18d804678bdc184a43990a29dad77961f1. At
0x0804b58e it copies 0x110 header bytes; at 0x0804b616 it
compares the version with 0x3b; and at 0x0804b65f..0x0804b750 it
dispatches the collision lump descriptors at the indexes documented
above.
Unresolved claims are kept narrow:
No runtime meaning is claimed for lumps 5 or 31.
No semantic field names are claimed for the final 16 bytes of each lump-30 light record.
This is a runtime format specification, not a full description of every map-compiler intermediate or authoring rule.