Common Modding & Parsing Pitfalls
A single-page cheat-sheet of the most-bitten gotchas spread across the kbot format docs. If you're hitting a bug that makes no sense, start here. Each item links to the deeper explanation on the relevant format page.
Tip
Looking for "my mod doesn't load"? The TDF semicolon rule (#tdf--fbi--ota, first item) accounts for more "silent breakage" reports than every other gotcha combined.
HPI / UFO / CCX / GP3
- Loose files in the install root override every archive. Drop
units/MYUNIT.fbion disk and the engine prefers it over anything intotala1.hpi. The layering diagram in hpi.md shows the full priority stack. - Among archives, the first one mounted wins, and only ten
*.hpimount. TA 3.1c mountsrev31.gp3, then*.ccx,*.ufoand the first ten*.hpithat open, each group in ASCII upper-case name order. A mod shipped aszz_mod.hpiranks below every retail archive and, on an install that already has ten*.hpisorting before it, mounts only through the disc scan, below everything. Ship mods as.ufo. - HPI v1 pointers are absolute to file start, not to the directory
start. Subtract nothing. The canonical-parser trick is to allocate
a
DirectorySize-byte buffer and decrypt the directory into it at offsetStartso pointers index the buffer directly. See Directory tree. - HPI v1 cipher seed advances with file position. You can't decrypt a buffer in isolation — you have to know which absolute file offset the buffer came from. See Deriving the cipher key.
- If
DecryptKey == 0, don't apply the XOR cipher at all. Some custom archives ship plaintext; running the cipher anyway scrambles what's already readable. - SQSH chunk checksum is computed over the encrypted bytes. Verify before unscrambling. See Compressed chunks (SQSH).
- TA: Kingdoms uses HPI v2 (
Version == 0x00020000) with a different layout — no XOR, single SQSH chunk per file. See the TA: Kingdoms HPI v2 section before trying the v1 parser on a TAK archive. BANK-marker save games are a different beast — they reuse the HPI envelope but the contents are serialised game state, not a filesystem. kbot does not decode them..gp3requires the rev3.1 patch. It's still HPI inside, but unpatched TA refuses to mount it.
TDF / FBI / OTA
- Every value must end with
;. Missing semicolons silently merge values across lines and produce wildly wrong stats. This is the #1 cause of "my unit has weird HP / missing description / wrong cost". - Keys are case-insensitive, values are not.
Side=armworks for most lookups but breaks any mod code doing string-equality onSide. - List values are space-separated (
Category=COMMANDER MOBILE WEAPON) — not comma-separated. The exception is OTA'snumplayers=2,4,6,8,10which uses commas. - Booleans are integer
1/0, nottrue/false. The engine accepts only the integer forms. - For mods, never edit
sidedata.tdfto add units. Use adownload/<UNITNAME>.tdf[MENUENTRY]registration instead — the same mechanism Core Contingency uses. See TDF:[MENUENTRY]and the modding tutorial. Bmcode=0hides a unit from the build menu even if everything else is valid. Almost always you wantBmcode=1.- A unit not listed under any constructor cannot be built, even if its FBI is otherwise valid. The most common "my new unit exists but isn't buildable" bug. See modding step 5.
- Nested-section depth is convention, not spec. Some tooling
assumes 2-deep; weapon TDFs go 3 deep (
[WEAPON][DAMAGE][category]).
TNT / SCT — map terrain
- TNT
WidthandHeightare 16-px attribute cells, not tiles and not pixels. Divide by 2 for tile counts; multiply by 16 for pixel dimensions. See TNT header. SeaLevel == 0does NOT mean "no water" — it means the smallest possible value is on the threshold, so nothing is underwater. UseSeaLevel = 1for genuinely-dry maps.- TNT feature words place a feature only below the feature-table
count.
0xFFFCis void;0xFFFE(Lava Run, AC02),0xFFFDand any other word at or past the table size place nothing, and the cell stays buildable. - TNT minimap padding uses palette index
0x64, and the map region is sized from the map (longer side 252, the visible map's aspect), not found by scanning for padding. The game reads the stored minimap only when bit 0 of the header word at0x2cis set. See TNT. - TA: Kingdoms
.tntusesIDVersion == 0x4000instead of TA's0x2000(TA also reads an older0x1020layout).kbot tnt imageproduces garbage against TAK TNTs; the tile decoder is TA-specific. See TA:K maps. - SCT sections must be a multiple of 4 in both dimensions to load in TAE. The on-disk format permits other sizes but the editor rejects them with "section misaligned".
- SCT V2 has 8-byte height cells, V3 has 4-byte. kbot accepts both; mods built against the editor see only V3.
3DO — models
- Y is up, not Z. Importing a 3DO into a Z-up engine (Blender, modern Unreal) without swapping axes leaves every unit lying sideways.
- Untextured primitives are invisible unless
IsColored != 0— the pseudocodevisible = (texture != "") || (IsColored != 0)catches both cases. Most common 3DO rendering bug. OffsetToSelectionPrimis only meaningful on the root object; child objects must set it to-1(0xFFFFFFFF).- Fixed-point scale is
1 / 65536. A vertex written0x00010000= 1.0 world units; the low 16 bits are the fractional part. - Vertices are local to the object, not global. Two objects can have identical vertex arrays — that's mirrored geometry, not shared data.
COB / BOS — scripts
call-scriptopcode is0x10062000, NOT0x10063000as some pre-3.1 docs claim. Old decompilers using0x10063000produce nonsense — see the opcode reference.- Piece numbers in opcodes are indices into the piece name table, not strings. The COB carries names as a string pool; the runtime uses indices for speed.
SET_SIGNAL_MASKonly registers interest — it doesn't kill anything. Sendingsignal Xis what stops scripts whose mask hasXset. Thesignal-never-signalledlint rule catches scripts that set a mask nobody ever signals.- Cavedog's Scriptor inlines
CURRENT_SPEEDas the literal29, breakingMoveWatcher-style scripts after a round-trip. kbot's decompiler restores the symbolic name; if you must round-trip through Scriptor, parenthesise:(get CURRENT_SPEED) > 5. - Jump offsets are byte offsets relative to
OffsetToScriptCode, not relative to the current instruction.JUMP 0x4Cmeans "jump toOffsetToScriptCode + 0x4C". GET_UNIT_VALUEport numbers vary between TA and TA:K. TA's catalogue (kbot decompiler labels ports 1–20) is documented in Appendix B.
GAF — sprite animations
- A sequence loops only when the low byte of its +2 word is set; with 0 it plays once and stops. Every stock sequence stores 1, so a tool that writes zero freezes animated features.
- Frames in
textures/*.gafandanims/vismasks.gafmust be raw. The game reads them as plain pixel arrays. - Some sequences ship with frame durations of
0; the game shows such a frame for one tick. Don't "fix" them to a default. - Palette index 0 is not transparent by itself. Only a raw frame's key and a compressed frame's skip runs are.
- Compressed pixel rows can be shorter than
Width. The game reads on into the next bytes; kbot pads the row with transparent pixels and warns. No stock file has such rows. - Layer origins are hotspots: each layer of a composite frame is placed so its origin lands on the frame's origin.
anims/terrain.gafandanims/vismasks.gafhaveVersion == 0in their headers. The game ignores the version word.- TA: Kingdoms
.tafcomes in two kinds. A paletted.tafIS a GAF — same on-disk format, except TAK pulls the palette from a per-side.pcxrather than the global TA palette. A truecolor.tafreuses the same container but stores 16-bit ARGB pixels and needs no palette. The frame format byte (0/1= paletted,4/5= truecolor), not the extension, is authoritative — see TAF / TSF.
TAF / TSF — truecolor animations
.tafis overloaded — paletted (read as GAF) vs truecolor (read withkbot taf). Always check the frame format byte, never the filename, though most TAK truecolor files carry a_4444/_1555suffix by convention.- GIF export is lossy. GIF holds only a 1-bit cutout and 255
colours, so soft ARGB4444 alpha becomes hard edges. Use APNG
(
kbot taf export --format apng) to keep the real alpha channel. - The frame name field and the
0x0Bflag byte can hold junk. Preserve them verbatim for a byte-exact round-trip; don't normalise. Delay/Durationof0is legal — like GAF, a zero-tick frame is event-driven (static menu backgrounds use it). Don't "fix" it.
PCX — images
- TA ignores
BytesPerLine. Standard readers drop the padding whenBytesPerLine > Width; TA 3.1c decodes exactlyWidthbytes per row, so padded files shift in the game. WriteBytesPerLine == Width, and check withkbot pcx describe. - TA loads version 5 only, and decodes every file as 8-bit single-plane data.
- Literal bytes with the top two bits set must be RLE-escaped. A
bare byte of value
0xC0–0xFFis illegal as a literal; emit it as a 1-count run (0xC1, 0xFFfor a single0xFF). Mishandling desynchronises decode after one row. - The game takes the trailing 768 bytes as the palette with or without
the
0x0Cmarker; standard readers need the marker.
PAL / ALP / LHT / SHD — palettes & lookup tables
- The 4th byte per entry is NOT alpha. It's an unused padding
byte Cavedog always set to
0x00. Open-source palette tools that treat it as alpha refuse to render the palette (alpha=0 → all transparent). - Index 0 is black, not transparent. Terrain, minimaps and backdrops draw it opaque; sprites pick their own transparent key.
- An empty or missing
palette.palis replaced bypalette.pcx(its last 768 bytes); a.pallonger than 1,024 bytes is read up to byte 1,024. .alp/.lht/.shdare NOT RGB palettes —.alpis a 256×256 and.shd/.lhtare 32×256 tables of palette indices, and the game ignores (and rebuilds) a table of any other size. Rendering them withkbot pal describeshows garbage; usekbot pal lookupinstead.- The TA palette has 13 duplicate RGB triplets. If your editor deduplicates on import (some do), you lose entries and the palette silently becomes 243-entry.
FNT — bitmap fonts
Offsets[c] == 0means "no glyph", not "glyph at offset 0". Offset 0 lands in the middle of the header — Cavedog reuses zero as the sentinel.- Bit order is MSB-first within each byte. LSB-first is more common in modern bitmap fonts; don't assume.
- Bit stream is continuous between rows — no padding at end of each scan line.
AI profiles
- TA: Kingdoms AI files have no
plandirectives — just bareweight/limitlines. kbot's parser opens an implicitdefaultplan; readers should tolerate the missing block. See TA:K — plan-less profiles. - No validation anywhere. Misspelled unit names, broken weight
values, lines outside
plan— all silently ignored. First sign of a broken profile is "the AI behaves strangely", with no log message. - Category aliases (
PLANT,LEVEL3) only "exist" if at least one unit declares them inCategory=. MisspellLEVL3and the directive silently does nothing.
Sounds
- A missing WAV is silent, not an error. Misspell
select1=cmsdel1(transposed letters) and the engine plays nothing — no log, no fallback. - The
.wavextension is implied in TDF values — never write it.select1=cmdsel1.wavbecomessounds/cmdsel1.wav.wavand finds nothing.
TA: Kingdoms specifics
- TAK requires
-disablecavedogverificationas a command-line argument to load most modded files. Without it the engine rejects anything whose hash isn't on its allowlist. - TAK has 8 side codes in the data, not 4: ARA / TAR / VER / ZON for the four playable sides, plus CRE (Creon, Iron Plague), MON (monsters), LIF (wildlife), NPC (campaign characters). Unit-list filtering needs to handle all of them.
- TAK weapons live inline in each FBI as
[WEAPON1]–[WEAPON3]sub-sections, not in a separateweapons/*.tdfdirectory. - TAK build menus are linear, not 2×3. Each builder has a
directory
canbuild/<builder>/<unit>.tdfwith a single[Menu] { Priority = N; }block.
See also
- Modding tutorial — end-to-end TA unit-mod walkthrough.
- Glossary — definitions of every cross-format term referenced above.
- Quickstart — five kbot commands to try first.