Reverse-Engineering Dossier · Toolchain V5

How Steel Panthers Stores a Battle

A consolidated engineering study of the binary scenario formats used by Steel Panthers 1, Steel Panthers 2, and the modern SPCAMO (SPWW2 / SPMBT) engine — the containers, the records, the coordinate systems, and the conversion and import pipelines that move data between them.

3 game formats 20 source modules Scope: scenarios, units, leaders, maps, obstacles, entrenchments Basis: the V5 reader / editor codebase
SECTION 00

Overview — three games, one lineage

All three games describe the same kind of object — a tactical scenario: a hex map, two orders of battle, leaders, victory locations, and battlefield obstacles. What differs is how those records are physically laid out on disk, and that difference grew more sophisticated with each generation.

SP1
Steel Panthers (1995)

A single flat binary blob (~510 KB). No header, no compression — every field lives at a hard-coded absolute offset. Data is stored as parallel arrays: all names together, all positions together, all armor together.

no magic · fixed offsets
SP2
Steel Panthers 2

A transitional two-headed format. Early v1.0 files are flat (like SP1); later v1.1 files adopt a sectioned, RLE-compressed container. The reader auto-detects which one it is holding.

STEEL2_SAVE_V100 · or flat
SPCAMO
SPWW2 / SPMBT (CAMO engine)

The fully modern SPCTS v3 container — sectioned, RLE-compressed, with explicit per-record structural fields (side, validity, formation links). This is the format the editor reads and writes.

SPCTS_SAVE_V100

The toolchain's job is to read the two legacy formats, normalize them into a portable JSON intermediate, and then write that JSON into a live SPCAMO file — converting the map geometry and translating every nation, unit class, and weapon index between the games' incompatible numbering schemes along the way.

Reading note Every offset and record size in this document is taken directly from the reverse-engineered readers in the V5 codebase. Sections still marked “unknown / TBD” in the source are flagged as such here rather than guessed at.
SECTION 01

Common building blocks

SP2 (compressed) and SPCAMO share the same container DNA — an artifact of the SSI → CAMO engine lineage. Understanding these three primitives unlocks both formats at once.

The section container

A compressed file is a 20-byte file header followed by a stream of self-describing sections. There is no central directory; you parse sequentially until you run out of bytes.

0x0016 B
MAGICASCII id
0x104 B
0x00reserved
0x14sections →

Per-section header — 9 bytes

+0u32 LE
sec #number
+4u32 LE
lenbytes·*
+81 B
cmp0=raw 1=RLE
+9len B
datapayload

* The length field is stored signed and is sometimes negative; readers take the absolute value. After the payload, the next section header begins immediately.

The RLE codec

Section payloads marked compressed use a trivially simple run-length scheme. The single lead byte decides everything:

// decompress: lead byte selects run vs. literal
while (i < data.length) {
  lead = data[i++];
  if (lead > 0x80) {            // RUN
    run = lead - 0x80;
    val = data[i++];
    emit run copies of val;
  } else {                     // LITERAL
    emit next lead bytes verbatim;
  }
}

The encoder is the mirror image: runs of identical bytes (up to 127) become a 0x80+n / value pair, everything else is copied as literal packets of up to 128 bytes. Because the editor must round-trip files, it ships both halves of this codec and rebuilds only the sections it touched, copying all others through byte-for-byte.

The hex coordinate system

Maps are stored column-major. The constant that ties everything together in SPCAMO is the stride MAPSIZE = 200: any per-hex array is indexed as hex = x · 200 + y. Two map grids coexist — the standalone map file is 100 columns wide, while a full scenario grid is 160 columns × 200 rows, which is why map import has to expand geometry (see §07).

Off-map sentinels A unit that isn't on the map (off-map artillery, a passenger in a transport) is marked by a sentinel coordinate: 255 in SP1, 0xFFFF in SP2 and SPCAMO. Empty victory-location slots use 0xDE (SPCAMO) or 0xFF (SP2).
SECTION 02

SP1 — the flat-file era

SP1 has no container at all. The scenario is one large fixed-size image of the game's memory, and the reader simply reaches into known absolute offsets. Records are stored as parallel arrays rather than structs: to assemble unit #7 you gather its name, class, position, weapons, ammo and armor from six different regions of the file.

File size ≈ 509–510 KB Unit slots 199 (100 P1 + 99 P2) Leaders up to 500 Victory hexes 21

Scenario header block

The scenario parameters sit in a tight cluster just past offset 37,500. Battle type is encoded as a two-byte pair (bf1,bf2); e.g. (1,2) = “Player 1 Assaults”, (1,1) = “Meeting Engagement”.

SP1 scenario fields (absolute offsets)
OffsetFieldNotes
37513month1–12 index into month table
37514yearstored as year − 1900
37515weather0=Desert 1=Summer 2=Winter
37518 / 37519P1 / P2 nationalityindex into 18-entry nation table
37522 / 37523battle bf1 / bf2pair encodes advance/assault/meeting
37524max turns
37612visibilityhex range
37626location text19-byte ASCIIZ, leading-null tolerant

Unit parallel arrays

Each domain of unit data lives in its own striped region. Positions are 19-byte records split by side; soft units (infantry, guns) carry all-zero armor.

SP1 unit data regions
Region baseStrideHolds
293674 (P1) · 295574 (P2)19 Bposition: x@+0, y@+2, hull facing@+10, turret@+12
181044 Bweapon slot type IDs (×4)
189088 Bper-weapon HE/AP ammo counts
2051414 Barmor: FH,SH,RH,FT,ST,RT,TOP + pad
30501 / 30701 / 309011 Bsmoke / HVAP / HEAT ammo
The “dead slot” trap A slot having a non-empty name does not mean the unit is live. SP1 stops loading a side at the first formation byte of 0; slots past that gap are orphaned — real-looking data the game never instantiates. The reader exposes an isDead flag so consumers filter on it rather than on name presence.

Leaders & obstacles

Leaders are striped much like units: names at offset 0 (15 B each), ranks at 8500, rally at 9000, skills (infantry/artillery/armor) at 9500. Battlefield obstacles live in a 19-byte record array beginning at offset 301236 (0x49854), organized into blocks of 100 slots.

Entrenchments are SP1's most baroque structure: they are reconstructed from paired blocks — a position-marker record in one block and a graphic/type record in a parallel block share the same slot index. Block 2/3 hold P1/P2 position markers; block 6/7 hold the matching graphic tiles that name the emplacement (foxhole vs. sandbag).

SECTION 03

SP2 — one reader, two formats

SP2 sits on the fault line between eras. The reader sniffs the first 16 bytes and the file size to decide which layout it is dealing with, then presents a unified view either way.

v1.1 Compressed
magic “STEEL2_SAVE_V100”

The sectioned + RLE container from §01. This is already the SPWW2 v2.2 binary shape, which is why SP2 map conversion can hand it almost directly to the v2→v3 converter.

v1.0 Uncompressed
no header · ≈ 690–705 KB

Flat binary; sections concatenated at fixed absolute offsets. Detected when the file is ≥ 600 KB and lacks the magic. The reader synthesizes section views over the raw bytes.

Uncompressed section anchors

SectionAbs. offsetRecordCount
1 · units0220 B399
17 · positions46400919 B399
34 · leaders68705830 B399
35 · formations69905836 B150
37 · scenario703998135 B1
38 / 39 · map W/H705144 / 7051484 Bbyte[0]

The unit record (Section 1, 220 bytes)

Unlike SP1's parallel arrays, SP2 packs a unit into one contiguous record. Crucially, records are not split by side — the side is read from a single control byte at +178.

SP2 Section-1 unit record — selected fields
OffSizeField
016name (ASCIIZ)
20 / 211+1formation id / sub-id (P2 ids start at 64)
45–604×4per-weapon ammo (HE/AP/Sabot/HEAT)
118 / 1191+1experience / morale
1211status (Ready…Destroyed, 0–9)
1222bombardment ammo type
145 / 1472+2hull / turret facing (degrees)
1712transporter unit id (0xFFFF = none)
1781sideCtrl — 0=P1, 1=P2
2111altitude (0=Landed 1=NoE 2=High)

Obstacles & entrenchments in Section 17

SP2 folds both unit positions and battlefield obstacles into Section 17 — 5000 records of 19 bytes, organized as ten blocks of 500. Obstacle type is the byte at +14; the side at +15.

BlockRecordsContentsDiscriminator
21000–1499Dragon's Teethtyp=0, str = count×10
63000–3499Minestyp=1, str = count×10
(shared)Entrenchmentstyp=1 & str = 0xFFFF

The strength = 0xFFFF marker is what distinguishes a foxhole/sandbag emplacement from a destructive obstacle sharing the same block. Tile IDs name the type — sandbags 1430–1433, foxholes 1464–1466 — a numbering that SPCAMO inherits unchanged.

SECTION 04

SPCAMO — the SPCTS v3 container

This is the live format: the editor both reads and writes it, so its structure is documented in the most detail. It is the §01 container with magic SPCTS_SAVE_V100, and its records carry explicit structural metadata — side, validity, formation parentage — that the legacy formats only implied by position.

Section inventory

SPCAMO / SPCTS v3 sections (RLE unless noted)
#PurposeLayout
1Unit records261 B × 1000 (500 P1 + 500 P2)
8Per-hex terrain15 B / hex × 200 × width
17Unit positions + obstacles + entrenchments19 B records, blocks of 1000
34Leader data40 B × 999
35Formation data60 B × 400
37Scenario metadata594 B — primary edit target
38 / 39Map width / height4 B each (raw); byte[0] = count
9 / 52 / 56Per-hex flags: mine / dragon's-teeth / barbed-wire1 B / hex
54Map label text6400 B (100 × 64-byte records)
11 · 36 · 40 · 41 · 57 · 59Misc / partially-mapped blocksdocumented as TBD in source

The unit record (Section 1, 261 bytes)

Where SP1 implied side by slot range and SP2 used one control byte, SPCAMO makes everything explicit and self-consistent — which is what lets the editor insert, delete and re-side units safely.

SPCAMO Section-1 unit record — key fields
OffSzFieldMeaning
015nameASCIIZ
162classIDunit class index
20 / 222 / 1formID / subIDformation membership
1462pictureicon id
148 / 1502 / 2rotation 1/2hull / turret facing (degrees)
1611statModifiededitor-dirty / anti-cheat flag
162 / 1631 / 2obatNationID / obatIDorder-of-battle linkage
1651unitModebitmask: bit2=side, bits0-1=type
1752carrier0xFFFF = on map independently
1771validslot in use
1781entrper-unit dug-in flag (see §06)
1801isOffMap1 = off-map support
1821side0=P1 1=P2 (authoritative)
2151altitudelanded / NoE / high

Scenario metadata (Section 37)

594 decompressed bytes hold the whole scenario envelope. Victory locations are four parallel 21-entry arrays; an empty slot is 0xDE.

OffField
17 / 38 / 59 / 80VLOC x[] / y[] / value[] / control[] — 21 entries each
104wind direction (raw, decode TBD)
Month, year, weather, nations and battle-type pair occupy the leading bytes, mirroring the SP2 §37 layout.
Why writes are safe The editor never rebuilds a whole file. It decompresses only the sections it changes, patches the exact bytes, re-compresses just those, and copies every other section through verbatim. Unit edits flow through an in-memory model written back to Section 1; position edits patch Section 17 block 0 while leaving obstacle/entrenchment blocks untouched.
SECTION 05

Cross-format comparison

Read sideways, the same conceptual record takes three very different shapes. This is the table to keep open when writing a converter.

The same data domain across all three formats
DomainSP1SP2SPCAMO
Containerflat, no headerflat or STEEL2 sectionsSPCTS sections + RLE
Unit storageparallel arrays220 B record × 399261 B record × 1000
Side encodingslot range (0–99 / 100–198)byte @ +178byte @ +182 (+ unitMode bit)
Positions19 B, split by sideSection 17, 19 BSection 17 block 0, 19 B
Leadersstriped arrays (name/rank/skill)Section 34, 30 BSection 34, 40 B
Formationsimplicit (formation byte)Section 35, 36 BSection 35, 60 B
Victory locs21 slots near 0x9286§37 arrays, empty=0xFF§37 arrays, empty=0xDE
Obstacles19 B array @ 301236§17 blocks 2 / 6§17 block 2 / 6 + per-hex flags
Entrenchmentspaired pos + gfx blocks§17, str=0xFFFF3-way write (see §06)
Off-map markerx = 255x = 0xFFFFx = 0xFFFF / carrier

Two invariants survive every generation and are worth committing to memory: the 19-byte obstacle/position record and the entrenchment tile numbering (sandbags 1430–1433, foxholes 1464–1466). Everything else — container, striping, side encoding — was redesigned at least once.

SECTION 06

Entrenchment deep-dive

Unit-level entrenchment is the one place where a single game concept is spread across three independent sections that must agree. It was the hardest part of the format to pin down, and it was settled empirically by diffing matched “entrenched / not-entrenched” save pairs for each side.

The core finding Setting the per-unit flag alone does nothing visible in-game. A unit that the engine treats as dug in is the product of three coordinated writes. A save where the flag was set but the emplacement and hex marker were absent rendered the unit as not entrenched.

The three writes (per entrenched unit)

① Section 1
the unit flag

S1[slot·261 + 178] = 1. The per-unit dug-in boolean. Necessary, but not sufficient on its own.

② Section 17
the emplacement

A 19-byte record appended in block 2 at the unit's hex: tile = foxhole/sandbag id, active=5, strength=0xFFFF, typ=1, side. Two records if a hex stacks both types.

③ Section 8
the hex marker

S8[(x·200+y)·15 + 8] |= bit — OR in 0x40 for a foxhole, 0x80 for a sandbag. A hex carrying both ends at 0xC0 (192).

Section-8 hex byte is a bitfield

0x4064 · foxhole
0x80128 · sandbag
0xC0192 · both

Because the byte is OR-combined rather than overwritten, the importer must read-modify-write: a hex that already holds a foxhole and gains a sandbag must end at 192, and a single hex may legitimately own two emplacement records in Section 17.

Verification Re-deriving an entrenched file from its plain baseline + a JSON description reproduced the game's own output byte-for-byte across four matched save pairs (P1 and P2, 26 to 121 units), in Section 1, Section 8, and the Section 17 emplacement set. The flag at offset 178 flips cleanly 0→1 on a genuine entrench; cases where it appeared pre-set turned out to be a deploy-time artifact, with the emplacement + hex marker remaining the authoritative signal.
SECTION 07

The map conversion chain

Maps are not converted in one hop. They are walked up a version ladder — SP1/SP2 geometry is first lifted into the intermediate SPWW2 v2.2 shape, then into v3, then wrapped into a full playable scenario.

SOURCE
SP1 / SP2 map

Raw scenario; map lives in tile-layer sections.

STAGE 1
SPWW2 v2.2

SP1_Map.js / SP2_Map.js emit a STEEL2_SAVE_V100 intermediate.

STAGE 2
SPCTS v3 map

SPCAMO_Map.js · convertV2toV3() rewrites tile records to v3.

STAGE 3
Full scenario

SP_ScenStub.js expands & wraps in a blank template.

The two source modules feed the same converter from different starting points. SP2 compressed files are already in v2.2 shape and pass through almost directly; SP2 uncompressed and all SP1 files have their map sections extracted and assembled into a synthetic STEEL2 blob first.

Geometry the stub must reconcile

  • The v3 map file is 100 columns wide; a playable scenario grid is 160 × 200. SP_ScenStub.js expands each tile layer from 100 → 160 columns, column-major.
  • v2.2 tile layers are 16,000 bytes each (sections 2–7); the elevation/extra layers use 12-, 13-, and 1-byte-per-hex records (sections 8, 28, 29).
  • The expanded map sections are merged with blank non-map sections from an embedded template so the result is a complete, loadable SPCTS scenario rather than a bare map.
SECTION 08

JSON import & cross-game translation

The reader app exports any SP1/SP2/SPCAMO scenario to a portable JSON. The editor's import handlers replay that JSON into a live SPCAMO file — and the difficult part is not the bytes, it is reconciling three games' incompatible numbering schemes.

Import surface
editor entry points

importScenJSON() — scenario parameters · importVlocJSON() — victory locations · importUnitsFromJSON() — units, leaders & formations · importEntrenchJSON() — obstacles, then optionally unit-personal entrenchments.

Translation tables
SPCAMO_Translation.js

Maps source-game indices to engine indices: SP1→SPWW2 and SP2→SPMBT for nations, with reverse lookups and human-readable name tables, plus unit-class mapping into the SPWW2/SPMBT OBAT id space.

Why translation is unavoidable

Nation, unit-class and weapon indices are per-game enumerations. SP2 nation 0 (“West Germany”) must become SPMBT nation 44 (“Germany”); an SP1 unit class must resolve to a concrete SPWW2 OBAT slot before its weapons and armor can be looked up. The unit importer therefore does more than copy fields — it re-homes each unit into the target game's order of battle, writes new Section 35 formation records, wires leader ranges, and lets the editor's patched writer lay down all structural Section-1 fields explicitly.

Entrenchment import — the two-phase flow

The entrenchment importer runs map obstacles first (dragon's teeth + mines from the JSON obstacles array), then prompts before touching units. On confirmation it reads the unitPersonalEntrenchments array, matches each record strictly by hex + side to an on-map unit, and performs the three §06 writes — honoring the source tile id so a foxhole stays a foxhole.

Scenario packaging

Distribution scenarios travel in ZIP packs. SP_Zip.js opens a pack, finds every .DAT, and pairs it with its .CMT sidecar — a 200-byte fixed record whose leading ASCIIZ string is the human-readable scenario title — matching by case-insensitive filename stem.

SECTION 09

File inventory

What each module in the V5 release is responsible for. The data is intentionally fragmented across many files so the apps can lazy-load only the tables they need.

Applications
FileRole
1_SP_READER_v18.htmReads SP1/SP2/SPCAMO scenarios; renders OOB; exports the JSON + OOB text intermediate.
2_SP_EDITOR_SPCAMO_v51.htmLoads, edits and re-writes SPCAMO files; hosts every JSON import handler.
0_SPCONVERT_MAP_v5…htmlMap-conversion test harness / display.
Format readers & the container
FileRole
SP1_ScenData.jsSP1 flat-file reader — scenario, units, leaders, vlocs, bombardment, obstacles, entrenchments.
SP2_ScenData.jsSP2 dual-format reader (compressed + uncompressed auto-detect).
SPCAMO_ScenData.jsSPCTS v3 reader/writer core: section parser, RLE codec, _rebuildFile, all S1/S17/S34/S37 constants.
Map & packaging pipeline
FileRole
SP1_Map.jsSP1 map → SPWW2 v2.2.
SP2_Map.jsSP2 map → v2.2 (direct for compressed, assembled for flat).
SPCAMO_Map.jsv2.2 → v3 (convertV2toV3).
SP_ScenStub.jsv3 map → full 160-col scenario via blank template.
SP_Zip.jsZIP pack reader; DAT↔CMT title pairing (needs fflate).
Import logic & lookup data
FileRole
SPCAMO_Import.jsScenario-parameter & victory-location import.
SPCAMO_UnitImport.jsUnit / leader / formation import with OBAT re-homing.
SPCAMO_Import_Entrench.jsObstacle import + the two-phase unit-personal entrenchment import.
SPCAMO_Translation.jsNation / class cross-game translation + name tables.
SPCAMO_UnitsMBT.js · SPCAMO_UnitsWW2.jsOrder-of-battle tables (the bulk of the data weight).
SPCAMO_Weapons_V2.jsWeapon name + max-range by [nation][slot].
SPCAMO_Icons_MBT.js · SPCAMO_Icons_WW2.jsUnit icon glyph data.
SPCAMO_UnitImport_Names.jsName resolution support for import.
SECTION 10

Offset appendix

A consolidated quick-reference of the magic numbers most often needed when writing tooling against these formats.

Magic & structural constants

ConstantValueWhere
SP2 compressed magicSTEEL2_SAVE_V100SP2 v1.1, SPWW2 v2.2
SPCAMO magicSPCTS_SAVE_V100v3 scenario / map
File header size0x14 (20)both sectioned formats
Section header size94+4+1
RLE run threshold0x80lead > 0x80 ⇒ run
Hex stride (MAPSIZE)200hex = x·200+y
Scenario grid160 × 200SP_ScenStub
Obstacle record size19 Ball formats

Entrenchment write recipe (SPCAMO)

// for each entrenched unit at (x,y), side s:
S1[ slot*261 + 178 ] = 1;                       // ① dug-in flag
S17.block2.append({ x, y,                          // ② emplacement
     tile:pictureID, active:5,
     strength:0xFFFF, typ:1, side:s });
S8[ (x*200+y)*15 + 8 ] |= isFoxhole ? 0x40 : 0x80;  // ③ hex marker (OR)

Entrenchment tile ids (shared SP1 / SP2 / SPCAMO)

TileTypeS8 bit
1430–1433Sandbag Emplacement (variants A–D)0x80
1464Foxhole (E)0x40
1465Foxhole (NE)0x40
1466Foxhole (NW)0x40

Steel Panthers Data Format Engineering Study — compiled from the V5 reader/editor toolchain.
Offsets reflect the reverse-engineered readers as shipped; sections marked TBD in source remain open questions. Game data and formats © their respective authors; this document describes interoperability behavior only.

↑ TOP