All format pages ↓
Tutorials & reference

Common Modding & Parsing Pitfalls

From coreprime/kbot @ 0ab97bf · Edit on GitHub

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.fbi on disk and the engine prefers it over anything in totala1.hpi. The layering diagram in hpi.md shows the full priority stack.
  • Among archives, the first one mounted wins, and only ten *.hpi mount. TA 3.1c mounts rev31.gp3, then *.ccx, *.ufo and the first ten *.hpi that open, each group in ASCII upper-case name order. A mod shipped as zz_mod.hpi ranks below every retail archive and, on an install that already has ten *.hpi sorting 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 offset Start so 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.
  • .gp3 requires 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=arm works for most lookups but breaks any mod code doing string-equality on Side.
  • List values are space-separated (Category=COMMANDER MOBILE WEAPON) — not comma-separated. The exception is OTA's numplayers=2,4,6,8,10 which uses commas.
  • Booleans are integer 1/0, not true/false. The engine accepts only the integer forms.
  • For mods, never edit sidedata.tdf to add units. Use a download/<UNITNAME>.tdf [MENUENTRY] registration instead — the same mechanism Core Contingency uses. See TDF: [MENUENTRY] and the modding tutorial.
  • Bmcode=0 hides a unit from the build menu even if everything else is valid. Almost always you want Bmcode=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 Width and Height are 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 == 0 does NOT mean "no water" — it means the smallest possible value is on the threshold, so nothing is underwater. Use SeaLevel = 1 for genuinely-dry maps.
  • TNT feature words place a feature only below the feature-table count. 0xFFFC is void; 0xFFFE (Lava Run, AC02), 0xFFFD and 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 at 0x2c is set. See TNT.
  • TA: Kingdoms .tnt uses IDVersion == 0x4000 instead of TA's 0x2000 (TA also reads an older 0x1020 layout). kbot tnt image produces 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 pseudocode visible = (texture != "") || (IsColored != 0) catches both cases. Most common 3DO rendering bug.
  • OffsetToSelectionPrim is only meaningful on the root object; child objects must set it to -1 (0xFFFFFFFF).
  • Fixed-point scale is 1 / 65536. A vertex written 0x00010000 = 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-script opcode is 0x10062000, NOT 0x10063000 as some pre-3.1 docs claim. Old decompilers using 0x10063000 produce 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_MASK only registers interest — it doesn't kill anything. Sending signal X is what stops scripts whose mask has X set. The signal-never-signalled lint rule catches scripts that set a mask nobody ever signals.
  • Cavedog's Scriptor inlines CURRENT_SPEED as the literal 29, breaking MoveWatcher-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 0x4C means "jump to OffsetToScriptCode + 0x4C".
  • GET_UNIT_VALUE port 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/*.gaf and anims/vismasks.gaf must 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.gaf and anims/vismasks.gaf have Version == 0 in their headers. The game ignores the version word.
  • TA: Kingdoms .taf comes in two kinds. A paletted .taf IS a GAF — same on-disk format, except TAK pulls the palette from a per-side .pcx rather than the global TA palette. A truecolor .taf reuses 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

  • .taf is overloaded — paletted (read as GAF) vs truecolor (read with kbot taf). Always check the frame format byte, never the filename, though most TAK truecolor files carry a _4444 / _1555 suffix 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 0x0B flag byte can hold junk. Preserve them verbatim for a byte-exact round-trip; don't normalise.
  • Delay / Duration of 0 is 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 when BytesPerLine > Width; TA 3.1c decodes exactly Width bytes per row, so padded files shift in the game. Write BytesPerLine == Width, and check with kbot 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–0xFF is illegal as a literal; emit it as a 1-count run (0xC1, 0xFF for a single 0xFF). Mishandling desynchronises decode after one row.
  • The game takes the trailing 768 bytes as the palette with or without the 0x0C marker; 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.pal is replaced by palette.pcx (its last 768 bytes); a .pal longer than 1,024 bytes is read up to byte 1,024.
  • .alp / .lht / .shd are NOT RGB palettes — .alp is a 256×256 and .shd / .lht are 32×256 tables of palette indices, and the game ignores (and rebuilds) a table of any other size. Rendering them with kbot pal describe shows garbage; use kbot pal lookup instead.
  • 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] == 0 means "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 plan directives — just bare weight/limit lines. kbot's parser opens an implicit default plan; 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 in Category=. Misspell LEVL3 and 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 .wav extension is implied in TDF values — never write it. select1=cmdsel1.wav becomes sounds/cmdsel1.wav.wav and finds nothing.

TA: Kingdoms specifics

  • TAK requires -disablecavedogverification as 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 separate weapons/*.tdf directory.
  • TAK build menus are linear, not 2×3. Each builder has a directory canbuild/<builder>/<unit>.tdf with 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.