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 fileLength first and fileOffset second.

  • 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.

  • int16 and int32 are signed; uint8, uint16, and uint32 are unsigned.

  • vec2 is two consecutive 32-bit floats; vec3 is three.

  • Record arrays are tightly packed according to the sizes below.

  • The stock CM_SaveLump writer 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.

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

0x00

64

char[64]

name

NUL-terminated material or shader path.

0x40

4

int32

surfaceFlags

Surface behavior and material class flags.

0x44

4

int32

contentFlags

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

0x00

12

float[3]

normal

Plane normal.

0x0c

4

float

distance

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

0x00

2

int16

numSides

Number of consecutive records in lump 3.

0x02

2

int16

materialIndex

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

0x00

4

float or int32

plane

Axial distance for the first six sides of a brush; lump-2 plane index for later sides.

0x04

4

int32

materialIndex

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

0x00

2

int16

materialIndex

Index into lump 0.

0x02

2

int16

lightmapNum

Lightmap index; negative values select non-lightmapped modes.

0x04

4

int32

firstVertex

First record in lump 7.

0x08

2

uint16

vertexCount

Number of vertices in the surface-local span.

0x0a

2

uint16

indexCount

Number of indexes in the surface-local span.

0x0c

4

uint32

firstIndex

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

0x00

12

float[3]

position

World-space position.

0x0c

8

float[2]

st

Base material texture coordinates.

0x14

8

float[2]

lightmap

Lightmap texture coordinates.

0x1c

12

float[3]

normal

Vertex normal.

0x28

4

uint8[4]

color

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

0x00

12

float[3]

mins

Minimum bounds.

0x0c

12

float[3]

maxs

Maximum bounds.

0x18

4

int32

firstSurface

First lump-6 surface.

0x1c

4

int32

surfaceCount

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

0x00

4

int32

firstPlane

First record in lump 13.

0x04

2

int16

planeCount

Number of occluder planes.

0x06

2

int16

edgeCount

Number of occluder edges.

0x08

4

int32

firstEdge

Runtime destination offset for the edge span.

0x0c

4

int32

firstVertex

First vertex in lump 11.

0x10

2

int16

vertexCount

Number of vertices.

0x12

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

0x00

2

uint8[2]

planeIndexes

Two local indexes into the owning occluder’s planes.

0x02

2

uint8[2]

vertexIndexes

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

0x00

4

int32

firstSurface

First lump-6 surface for a terminal tree.

0x04

4

int32

surfaceCount

Number of terminal surfaces.

0x08

4

int32

childCount

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

0x00

12

float[3]

mins

Minimum bounds.

0x0c

12

float[3]

maxs

Maximum bounds.

0x18

4

int32

aabbTreeIndex

Index into lump 16.

0x1c

4

int32

firstPortal

First record in lump 18.

0x20

4

int32

portalCount

Number of portals.

0x24

4

int32

firstCullGroup

First record in lump 10.

0x28

4

int32

cullGroupCount

Number of cull-group indexes.

0x2c

4

int32

firstOccluder

First record in lump 15.

0x30

4

int32

occluderCount

Number of occluder indexes.

Lump 18 - Portals

Offset

Size

Type

Field

Meaning

0x00

4

int32

planeIndex

Index into lump 2.

0x04

4

int32

cellIndex

Destination cell in lump 17.

0x08

4

int32

firstVertex

First vertex in lump 11.

0x0c

4

int32

vertexCount

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

0x00

4

int32

planeIndex

Split plane in lump 2.

0x04

8

int32[2]

children

Nonnegative node indexes; negative leaf encodings.

0x0c

12

int32[3]

mins

Serialized integer minimum bounds.

0x18

12

int32[3]

maxs

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

0x00

4

int32

cluster

Visibility cluster; negative means no cluster.

0x04

4

int32

area

Collision-area index.

0x08

4

int32

firstTerrainPatch

First record in lump 23.

0x0c

4

int32

terrainPatchCount

Number of lump-23 entries.

0x10

4

int32

firstLeafBrush

First record in lump 22.

0x14

4

int32

leafBrushCount

Number of lump-22 entries.

0x18

4

int32

cellIndex

Renderer cell in lump 17.

0x1c

4

int32

firstLightIndex

First record in lump 19.

0x20

4

int32

lightCount

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

0x00

2

int16

materialIndex

Index into lump 0.

0x02

1

uint8

collisionMode

0 selects a quadratic curve grid; nonzero selects indexed terrain.

0x03

1

padding

Alignment before the 12-byte union.

When collisionMode == 0, bytes 0x04 through 0x0f are:

Offset

Size

Type

Field

Meaning

0x04

2

int16

width

Curve-grid width.

0x06

2

int16

height

Curve-grid height.

0x08

4

int32

maxError

Integer subdivision-error threshold.

0x0c

4

uint32

firstVertex

First grid point in lump 25.

When collisionMode != 0, the union is:

Offset

Size

Type

Field

Meaning

0x04

2

int16

vertexCount

Number of selected lump-25 vertices.

0x06

2

int16

indexCount

Number of selected lump-26 indexes.

0x08

4

uint32

firstVertex

First vertex in lump 25.

0x0c

4

uint32

firstIndex

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

0x00

12

float[3]

mins

Minimum bounds.

0x0c

12

float[3]

maxs

Maximum bounds.

0x18

4

int32

firstSurface

First lump-6 render surface.

0x1c

4

int32

surfaceCount

Number of render surfaces.

0x20

4

int32

firstLeafSurface

First record in lump 23.

0x24

4

int32

leafSurfaceCount

Number of lump-23 entries.

0x28

4

int32

firstLeafBrush

First record in lump 22.

0x2c

4

int32

leafBrushCount

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

0x00

4

int32

clusterCount

Number of visibility rows.

0x04

4

int32

bytesPerCluster

Bytes in each row.

0x08

Variable

uint8[]

data

clusterCount * bytesPerCluster row bytes.

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

0x00

4

int32

type

Light type, values below.

0x04

12

float[3]

color

Source RGB light color.

0x10

12

float[3]

position

Point/spot origin.

0x1c

12

float[3]

direction

Sun or spot direction.

0x28

16

union

parameters

Type-selected attenuation/spot parameters.

0x38

16

bytes

unconsumed

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

float linearAttenuation at 0x28.

4

Custom point

float quadraticAttenuation at 0x28, float constantAttenuation at 0x2c.

5

Spot

float cutoffCos at 0x28, int32 exponent at 0x2c.

6

Color-only / exact source name unresolved

Common color terms only.

7

Custom spot

Quadratic at 0x28, constant at 0x2c, cutoff cosine at 0x30, exponent at 0x34.

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

0x00

4

uint32

key

Packed Y/X/cluster lookup key.

0x04

1

uint8

sampleState

0 empty, 1 valid, 2 blocked.

0x05

5

uint8[5]

diffuseSunVisibility

Results for diffuse-sun step counts 1 through 5.

0x0a

2

uint16

visibleLightBits

Per-leaf light visibility bits; bit 0x8000 represents diffuse sun.

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

materialIndex, non-axial planeIndex

Lumps 0 and 2

Lump 4 brush

materialIndex; implicit running side span

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.exe 0x0041c400..0x0041d7d1 and coduo_lnxded 0x0804a06c..0x0804bc57.

  • src/client/engine/renderer/backend.h and src/client/engine/renderer/renderer_world_load.c: renderer-owned records and load graph. The Windows world-loader range is 0x0050ab70..0x0050e774 across 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.

External references