The format reference is published, with every page cross-linked, at creators.coreprime.net.
A toolkit for working with Total Annihilation game assets. KBot provides a unified CLI for managing the various proprietary file formats used by the game, along with a browser-based studio (asset explorer, map editor, unit viewer, and live sandbox). There are also development tools, allowing advanced debugging and processing of various formats, including the COB/BOS scripting language.
go install github.com/coreprime/kbot/cmd/kbot@latestOr build from source:
git clone https://github.com/coreprime/kbot.git
cd kbot
task build # builds the Studio web assets and kbot binary
task install # installs kbot to $GOPATH/binTo run the visual explorer for Total Annihilation:
- Perform the installation steps
- Launch
kbot studio ~/games/totalaand open the Files tab to browse, preview, and inspect every asset in the install
To develop/code against the project:
- Create a flattened version of your Total Annihilation gmae assets with
kbot mount flatten ~/games/totala ~/games/totala-flattened - Clone
.envto.env.localand set your paths task build/task lintwill run the code quality checks.
- Installation
- File-Format Reference — deep dive into HPI, GAF, TAF/TSF, PCX, PAL, FNT, SCT, TNT, 3DO, COB/BOS, TDF/FBI/OTA, TA:K GUI
- Commands
kbot ctx— Working-Directory Contextskbot cob— COB/BOS Scriptingkbot hpi— Archive Fileskbot gaf— Sprite Animationskbot taf— Truecolor Animationskbot tsf— Animation Text Scriptskbot pcx— PCX Imageskbot fnt— Bitmap Fontskbot sct— Map Sectionskbot crt— TA: Kingdoms Scenarioskbot pal— Palettes & Lookup Tableskbot tnt— TNT Mapskbot zrb— Smacker Videokbot mount— Asset Explorerkbot pack— Static Asset Packskbot studio— KBot Studio (Web Workbench)kbot document— Reference Catalogue Generatorkbot mcp— Model Context Protocol Server
- Shell Completion
- Project Structure
- Development
- License
Register named Total Annihilation installs (packed or flattened) so the other
kbot subcommands that rely on a virtual filesystem can pick them up
automatically — no more typing --vfs ~/games/totala on every command.
Contexts persist to ~/.kbot (a small JSON file, conceptually equivalent to
~/.kube/config). One context is marked current at any time; the
KBOT_CONTEXT environment variable overrides the persisted current for the
running process so you can flip between installs per-shell without editing the
file.
# Register a packed TA install and mark it current
kbot ctx add ~/games/totala --alias ta-gog --game totala --version 3.1c
# Register a TA: Kingdoms install alongside
kbot ctx add ~/games/tak --alias kingdoms --game takingdoms
# Register a flattened directory you keep for development
kbot ctx add ~/ta-flattened --alias ta-flat --game totala --version 3.1c-flat
# Adopt the current directory. If it's already registered, just switch
# to it; otherwise launch a small interactive prompt for alias, game,
# and version.
cd ~/games/totala
kbot ctx here
# Show every registered context (the current one is starred).
# `kbot ctx` with no subcommand does the same; use `kbot ctx --help`
# for the full help text.
kbot ctx
kbot ctx list
# Print the active context's path — handy in shell substitution
cd "$(kbot ctx path)"
ls "$(kbot ctx path)/units"
# Print a specific context's path without switching to it
kbot ctx path --alias kingdoms
# Switch the persisted current context
kbot ctx use kingdoms
# Per-shell override (does not touch ~/.kbot)
KBOT_CONTEXT=ta-flat kbot tnt preview "metal heck.tnt" --target preview.png
# Replace an existing entry (e.g. you moved the game folder)
kbot ctx add ~/new/totala --alias ta-gog --game totala --version 3.1c --replace
# Remove a context
kbot ctx delete kingdoms--game must be one of totala, takingdoms, or custom. custom is the
escape hatch for directories that look like a TA layout but aren't an official
release (mod test-beds, partial extractions, etc.).
Where contexts apply. Any subcommand that mounts a VFS will fall back to the active context when its path argument is omitted:
| Command | Without an arg/flag |
|---|---|
kbot mount |
Mounts the active context. |
kbot mount flatten --target … |
Flattens the active context. |
kbot tnt preview <file.tnt> |
Uses the active context as the VFS root for sprites and the sister .ota. |
kbot mcp |
Registers the active context as the default game-data folder. |
kbot cob lint |
Lints the active context's tree when no path is passed. |
kbot cob roundtrip |
Scans the active context's tree when no path is passed. |
kbot gaf roundtrip |
Scans the active context's tree when no path is passed. |
Explicit arguments (a positional path, --vfs, or --game-data) always
override the context. Use KBOT_CONTEXT=<alias> to pick a different
registered context just for one invocation.
The file format is plain JSON, so you can hand-edit it if you need to:
{
"current": "ta-gog",
"contexts": {
"ta-gog": { "path": "/Users/me/games/totala", "game": "totala", "version": "3.1c" },
"kingdoms": { "path": "/Users/me/games/tak", "game": "takingdoms" },
"ta-flat": { "path": "/Users/me/ta-flattened", "game": "totala", "version": "3.1c-flat" }
}
}Work with compiled unit scripts (COB bytecode) and their BOS source.
# Decompile COB to BOS source
kbot cob decompile unit.cob
kbot cob decompile unit.cob --target unit.bos
# Compile BOS source to COB (TA script unless the source says .version 6)
kbot cob compile unit.bos --target unit.cob
kbot cob compile unit.bos --target unit.cob --strict # fail on warnings
# Disassemble COB to assembly listing
kbot cob disassemble unit.cob
kbot cob disassemble unit.cob -a # annotated with flow arrows
kbot cob disassemble unit.cob -s Create # single script
# Assemble back to COB
kbot cob assemble unit.coba --target unit.cob
# Lint for common issues
kbot cob lint unit.cob
kbot cob lint scripts/ # lint a whole directory
kbot cob lint scripts/ -q # summary only
kbot cob lint scripts/ --ci > cob.sarif # SARIF 2.1.0 for CI ingest
# Roundtrip validation (byte-perfect decompile→compile and disassemble→assemble)
kbot cob roundtrip scripts/
kbot cob roundtrip scripts/ --detailedAll commands support --stream to read from stdin and --target to write to a file (default: stdout).
compile and assemble write what TA 3.1c runs. A source compiles as a TA
script (COB version 4) unless it declares .version 6 (TA: Kingdoms). In a TA
script the TA: Kingdoms constructs (play-sound, Mission-Command,
__tak_math_*, .sound_name) and % are errors: TA faults on the Kingdoms
instructions and has no modulo instruction (0x10037000 is its bitwise XOR).
Anything that still compiles but deserves a look (a function defined twice binds
every call to the first definition, as the game does) and every finding of the
ta-* lint rules below is printed to stderr as a warning; --strict makes
warnings fail the command. Listings are disassembled with the game's opcode names
(XOR, NOT, XOR_ALT, NAME@0x… for a word with stray low bits); the
assembler still reads the older names MOD, BITWISE_XOR, BITWISE_NOT and
LOGICAL_XOR as the words they were written for.
Lint rules:
| Rule | Severity | Description |
|---|---|---|
unused-piece |
Piece declared but never used | |
unused-static |
Global variable never accessed | |
unused-local |
Local variable allocated but unused | |
always-true |
ℹ️ info | if(1) / while(1) — always-true condition |
dead-code |
if(0) / while(0) — unreachable code |
|
long-function |
Function exceeds 100 instructions | |
high-complexity |
Cyclomatic complexity > 15 | |
invalid-call |
❌ error | call-script to non-existent function |
speed-zero |
move/turn with speed <0> — never completes |
|
empty-function |
ℹ️ info | Function body is only return 0 |
duplicate-animation |
Identical sequential animation command | |
sleep-only-guard |
ℹ️ info | if block contains only a sleep |
duplicate-if |
ℹ️ info | Back-to-back identical if conditions |
raw-signal |
ℹ️ info | Signal uses raw number (BOS only) |
unnamed-global |
ℹ️ info | Static var uses global_N naming (BOS only) |
signal-never-signalled |
set-signal-mask watches a signal nobody sends |
|
recursive-call |
call-script forms a cycle |
|
duplicate-function |
Two scripts share a name; the game only calls the first | |
malformed-cob |
❌ error / |
File does not load or a script's code is truncated (error); damaged name tables the reader tolerated (warning) |
Every COB that does not declare the TA: Kingdoms version (6) is also checked against what TA 3.1c runs. These rules are errors, and appear in the SARIF output like the others:
| Rule | Severity | Description |
|---|---|---|
ta-kingdoms-opcode |
❌ error | TA: Kingdoms instruction (PLAY_SOUND, MISSION_COMMAND, TAK_MATH_*); TA faults on it |
ta-unknown-opcode |
❌ error | Opcode word no game runs |
ta-push-flags |
❌ error | PUSH/POP flag the game faults on (PUSH takes 1, 2 or 4; POP 2 or 4) |
ta-stack-limit |
❌ error | More than the game's 32 stack slots |
ta-get-arguments |
❌ error | GET with fewer than 5 pending values; it takes the rest from the function's locals |
ta-stack-underflow |
❌ error | An instruction pops more values than are pending |
ta-discard-call |
❌ error | DISCARD_CALL (0x10063000) with more than 4 arguments |
Manage HPI, UFO, and CCX archive files.
# List archive contents
kbot hpi list archive.hpi
kbot hpi list archive.hpi -v # verbose (sizes, compression)
kbot hpi list archive.hpi -p "*.wav" # filter by pattern
# Extract files
kbot hpi extract archive.hpi
kbot hpi extract archive.hpi -t ./output # target directory
kbot hpi extract archive.hpi -p "sounds/*" # extract matching files
# Pack a directory into an archive (LZ77, key 0xBF, Cavedog trailer)
kbot hpi pack ./content --target archive.hpi
kbot hpi pack ./content --target mod.ufo --method none --key 0 # stored, unencrypted
# Show archive details, including "TA 3.1c would mount: yes/no, reason"
kbot hpi info archive.hpiAll read commands support --stream to read the archive from stdin.
extract writes every file inside the target folder: an entry whose stored
name has a . or .. segment is skipped with a message, and of several
entries whose paths differ only in letter case only the last — the one the
game reads — is extracted.
Work with GAF animation files containing sprite sequences.
# List sequences (frame count, whether the game loops it, duration)
kbot gaf list sprites.gaf
# Export a sequence as an animated PNG (default) or GIF
kbot gaf export sprites.gaf
kbot gaf export sprites.gaf --format gif --sequence 3
# Dump all sequences and frames to a folder (PNG by default)
kbot gaf dump sprites.gaf --target ./sprites
# Build a GAF from a dump folder
kbot gaf build ./sprites --target rebuilt.gaf
# Check that decode→encode and dump→build keep what the game reads
kbot gaf roundtrip ./animsFrames are exported as the game draws them: a raw frame's key pixels and a compressed frame's skipped pixels are transparent, palette index 0 is opaque black, frames show for their duration in 1/30 s ticks, and an animation loops only when the sequence's loop byte is set.
Each sequence folder of a dump holds the frame images, a frames.csv (size, origin, key, duration, storage and the +11 byte of every frame) and a sequence.csv (position, exact name, loop word and +4 word). The build command reads both back, so a dump/build cycle keeps what the game reads. A sequence without a sequence.csv loops, like every stock sequence. Frames without a storage are compressed, except that every frame written to textures/*.gaf or anims/vismasks.gaf is raw, because the game reads those as plain pixel arrays (--storage overrides). Composite (layered) frames are written flattened. A frame with no pixels gets no image; its frames.csv row rebuilds it. A sub-folder with neither CSV file is skipped with a warning; any other sub-folder the build cannot read stops the build rather than leaving that sequence out.
Work with TA: Kingdoms truecolor (16-bit ARGB) animation files. Unlike GAF, a TAF carries its own colour and alpha, so no palette is needed. See docs/formats/taf.md for the format.
# Inspect
kbot taf info anims/manabomb_1555.taf # per-frame table
kbot taf list anims/manabomb_1555.taf # one-line summary
kbot taf lint anims/manabomb_1555.taf # structural validation
# Render
kbot taf render anims/bluefire_4444.taf --frame 4 -o frame4.png
kbot taf sheet anims/manabomb_1555.taf --cols 5 --bg "#202830" -o sheet.png
kbot taf export anims/fireballa_1555.taf --format apng -o anim.png # or --format gif
# Decompile / compile round-trip (TAF <-> TSF + PNG layers)
kbot taf decompile anims/manabomb_1555.taf --target ./manabomb
kbot taf compile ./manabomb/manabomb.tsf --images ./manabomb -o rebuilt.taf
kbot taf diff anims/manabomb_1555.taf rebuilt.taf # verify identical
kbot taf roundtrip anims/manabomb_1555.taf # byte-for-byte check
# Import from common formats
kbot taf from-gif boom.gif --format argb4444 --name Boom -o boom.taf
kbot taf from-sheet strip.png --frame-width 64 --frame-height 64 --count 8 -o strip.tafexport defaults to APNG (full alpha); --format gif produces a 1-bit-cutout preview. Every command resolves bare filenames through the active kbot context or an explicit --vfs mount.
Inspect and validate TSF documents — the human-readable, brace-delimited text form of a TAF that the TA: Kingdoms GUI loader reads directly for menu backgrounds. kbot taf decompile emits TSF and kbot taf compile reads it back.
# Summarise the animation, frames and layer images
kbot tsf info anims/titlescreen.tsf
# Check the document matches the compiler's expectations
kbot tsf lint anims/titlescreen.tsflint checks document shape only (one animation, one Filename'd layer per frame, recognised pixel formats); use kbot taf compile to exercise the full image-loading pipeline.
Inspect and convert PCX image files.
# Describe a PCX file (detailed metadata, then what TA 3.1c will do with it)
kbot pcx describe image.pcx
# Convert to PNG, GIF, or BMP
kbot pcx convert image.pcx --format png
kbot pcx convert image.pcx --format png --target output.png
# One-line info summary
kbot pcx info image.pcxTA 3.1c reads PCX files differently from image editors: it loads version 5 only, ignores BytesPerLine (padded rows shift), decodes everything as 8-bit single-plane data and takes the palette from the last 768 bytes. kbot pcx describe, the MCP pcx_describe tool and the studio's asset explorer (a badge next to the preview) report every such difference. See docs/formats/pcx.md.
Inspect and render Total Annihilation 1-bpp bitmap fonts. Each .fnt file
holds up to 256 glyphs sharing a single height; widths vary per glyph.
# One-line summary
kbot fnt info comix.fnt
# Detailed metadata (height, baseline, first character code, glyph
# count, character-code coverage)
kbot fnt describe comix.fnt
kbot fnt describe comix.fnt --list # also enumerate every defined glyph
# Render a string to PNG, laid out as the game does (text converted to
# Windows-1252 by default; --codepage picks another, raw passes bytes through)
kbot fnt render comix.fnt --text "Hello TA" --target hello.png
kbot fnt render armfont.fnt --text "Commander" --fg "#ffff00" --bg transparent
kbot fnt render comix.fnt --text "Привет" --codepage cp1251 --target ru.png
# Render every defined glyph as a 16-column sprite sheet
kbot fnt sheet comix.fnt --target sheet.png
# Dump one PNG per glyph (named U+00XX.png) into a directory
kbot fnt dump comix.fnt --target ./glyphsColors accept #rrggbb, #rrggbbaa, or the literal transparent / none.
Rendering follows the game: each character byte selects the glyph with that code, a glyph advances by exactly its width, a character with no glyph adds nothing, and drawing stops at the first newline.
Inspect and render Total Annihilation .SCT map sections — the reusable tile
blocks the official map editor stitches together to build .TNT maps.
# One-line summary
kbot sct info hill01.sct
# Detailed metadata (header, tile/attr counts, height stats)
kbot sct describe hill01.sct
# Render the full tile grid (32 px per tile) to PNG
kbot sct image hill01.sct --target hill01.png
# Export the elevation grid (16 px resolution) as a normalised grayscale PNG
kbot sct heightmap hill01.sct --target hill01-h.png
# Export the embedded 128x128 minimap as PNG
kbot sct minimap hill01.sct --target hill01-mini.pngInspect the .crt scenario sidecar that pairs with a TA: Kingdoms .tnt
map. It holds the units placed before play, the per-player rule engine
and named trigger regions. Multiplayer maps ship an empty stub; campaign
and special maps populate every section. See
docs/formats/takmap.md for the binary layout.
# Unit-type breakdown, rule counts and trigger regions
kbot crt describe "king of the hill.crt"
# Add every placement with its position, owner and facing
kbot crt describe --verbose "savannah hunt.crt"Inspect and convert Total Annihilation .PAL palettes, plus the related
color-index lookup tables built from them: .ALP (65,536 bytes, 256×256
half-and-half blends) and .SHD / .LHT (8,192 bytes, 32 shade or light
levels of 256 colors). The game ignores a table of any other size, and so
does kbot pal lookup.
# One-line summary (size, unique colors, duplicates, TA-style flag)
kbot pal info palette.pal
# Every entry with hex + RGB
kbot pal describe palette.pal
# 16x16 PNG swatch grid (index 0 hatched with magenta so it stands out;
# the game draws it as ordinary black in terrain and backdrops)
kbot pal swatch palette.pal --target palette.png --cell 16
# Convert to editor-friendly formats
kbot pal convert palette.pal --target palette.gpl # GIMP Palette
kbot pal convert palette.pal --target palette.txt --format jasc # JASC-PAL text
kbot pal convert palette.pal --target re-emitted.pal --format pal # binary TA .PAL
# Render a lookup table as a PNG swatch (.ALP 256×256 cells, .SHD/.LHT
# 256×32 cells) using the embedded palette (or pass --palette, a .pal or
# .pcx; TA: Kingdoms keeps most palettes in PCX files next to their tables)
kbot pal lookup palette.alp --target alp.png
kbot pal lookup palette.lht --palette palette.pal --target lht.png
kbot pal lookup palettes/aramon.alp --palette palettes/aramon.pcx --target aramon-alp.pngInspect, render, unpack and pack Total Annihilation .TNT map terrain.
# Summary on the console
kbot tnt describe "metal heck.tnt"
# Render artefacts
kbot tnt image "metal heck.tnt" --target map.png # full RGBA map (1:1, 32 px / tile)
kbot tnt heightmap "metal heck.tnt" --target height.png # 8-bit grayscale (16 px / cell)
kbot tnt buildmap "metal heck.tnt" --target build.png # per-cell buildability classification
kbot tnt voidmap "metal heck.tnt" --target voids.png # engine-void mask (Feature == 0xFFFC only)
kbot tnt minimap "metal heck.tnt" --target mini.png # embedded minimap
# Same as `image`, but composite feature sprites and draw numbered StartPos
# markers from the sister .ota. --vfs points at a flattened TA install (or
# any directory containing features/*.tdf and anims/*.gaf).
kbot tnt preview "metal heck.tnt" --vfs ~/ta-flattened --target preview.png
# Pick a different schema's StartPos set: the one the game finds as
# "Schema 2" (default 0; a schema after a gap in the numbering draws none).
kbot tnt preview "metal heck.tnt" --schema 2 --target preview-s2.png
# --vfs may be omitted when a kbot context is active (see `kbot ctx`)
kbot tnt preview "metal heck.tnt" --target preview.png
# Tiny ASCII rendering (dev-joke / quick sanity check)
kbot tnt ascii "metal heck.tnt" --cols 64
# Decompose into editable files
kbot tnt unpack "metal heck.tnt" --target ./metal_heck
# Preserve the original feature table verbatim so the unpack/pack round-trip
# is byte-identical (default unpack drops trailing scratch bytes Cavedog's
# tooling left in feature-name buffers; pack rebuilds the table from the
# unique names referenced in features.csv).
kbot tnt unpack "metal heck.tnt" --target ./metal_heck --lossless
# Reverse the unpack
kbot tnt pack ./metal_heck --target metal_heck.tntUnpack layout (--target <dir>):
<dir>/
map.png full RGBA render of the tile grid
heightmap.png 8-bit grayscale, pixel = raw elevation byte
minimap.png paletted PNG of the embedded minimap
tiles/<n>.png paletted 32×32 PNG per unique tile
tilemap.csv 2D grid of tile indices (rows = y, cols = x)
features.csv feature_index,name,attr_x,attr_y per placement
metadata.json header constants + round-trip info (feature table when --lossless)
heightmap.png is unnormalised so the pixel value equals the raw elevation byte — round-trip safe. Pass --normalize to stretch the range for human viewing. minimap.png is paletted so palette indices survive the round-trip.
kbot tnt unpack is lossy by default: the feature name table is omitted from metadata.json and kbot tnt pack rebuilds it from the unique names in features.csv. This loses any trailing scratch bytes the original tooling left in the table. Pass --lossless to record the original feature table (and the raw feature bytes as feature_raw_b64) in metadata.json so the directory packs back to a byte-identical TNT.
Buildmap classification (kbot tnt buildmap):
The PNG is at attribute-cell resolution (one pixel per 16×16 cell, same as heightmap). Each cell is classified, in priority order:
| Colour | Meaning |
|---|---|
| Black | engine-void — TileAttr.Feature == 0xFFFC |
| Red | a feature is placed in the cell (rocks, trees, geo-vents, ...) |
| Blue | underwater — Height is below sea level |
| Yellow | cliff edge — |Δheight| to a 4-neighbour exceeds 32 game units |
| Green | buildable |
By default the .tnt header's SeaLevel field drives the underwater check; pass --sealevel <n> to override (0 disables the check). 0xFFFD / 0xFFFE are not classified as void — see docs/formats/tnt.md for the rationale (Metal Heck and a few other early Cavedog maps mark steam vents this way, and those cells are demonstrably buildable in-engine).
Voidmap (kbot tnt voidmap):
A transparent PNG at attribute-cell resolution. Cells with Feature == 0xFFFC are opaque red; everything else is transparent — overlay it on a kbot tnt image render to highlight just the engine-void areas.
The Studio's Advanced › Export menu offers the same artefacts plus the minimap and the active-schema preview:
Menu item Backend CLI / MCP equivalent Export Minimap client canvas snapshot (n/a) Export Full Render tnt.RenderTileMap+tntpreview.ComposeWith(active schema's StartPos markers)kbot tnt preview --schema <ActiveSchema>/tnt_previewExport Map Image tnt.RenderTileMap(bare tile grid, no overlays)kbot tnt image/tnt_imageExport Buildmap tnt.RenderBuildMapFor(placed features block their cells)kbot tnt buildmap/tnt_buildmapExport Voidmap tnt.RenderVoidMapkbot tnt voidmap/tnt_voidmap
Linting a map:
kbot tnt lint runs the same checks Studio's Quality Checker dialog
applies, plus the tile-pool diagnostics that kbot tnt optimize is
built on. No files are modified — the command is read-only and
returns exit-1 when any issue is reported (handy for CI).
By default the command mounts the active kbot context (kbot ctx) as a virtual filesystem, so the path argument can be a bare
basename, a virtual path inside an HPI / GP3 archive, or an absolute
disk path. The sibling .ota and the metal-proximity feature
registry are picked up from the same VFS automatically.
# Bare basename — searched against maps/ in the active context VFS.
kbot tnt lint "metal heck.tnt"
# Virtual path — addresses a file inside an HPI archive directly.
kbot tnt lint "maps/the pass.tnt"
# Local file on disk (absolute or relative to cwd) still works.
kbot tnt lint ./my-edit.tnt
# Override the mount root for one invocation.
kbot tnt lint --vfs ~/ta-flattened "metal heck.tnt"
# Skip the quality pass; just report tile-pool savings.
kbot tnt lint --no-quality "metal heck.tnt"
# Override the sister .ota lookup (also accepts virtual paths).
kbot tnt lint --ota ./my-edit.ota "metal heck.tnt"
# Emit SARIF 2.1.0 on stdout for CI ingest (GitHub, GitLab, Harness…).
kbot tnt lint --ci "metal heck.tnt" > tnt-lint.sarifRelative paths like ./maps/Metal Heck.tnt are resolved against the
mounted VFS (lowercased and ./ stripped); any .. segment is
rejected so a path argument can't escape the kbot workspace.
Checks (each emits a row, ✅ /
| Group | Rule | Catches |
|---|---|---|
| Tile pool | duplicate-tiles |
Byte-identical tile graphics |
| Tile pool | similar-tiles |
Visually-similar graphics sharing a heightmap footprint (configurable via --similarity) |
| Tile pool | unused-tiles |
Tile graphics no cell references |
| Map data | bad-tile-index |
Tile map cells naming a tile beyond the tile set (the game reads past its tile set there) |
| Map data | unresolved-feature |
Cells whose feature word names no entry of the feature table (the game places nothing there) |
| Map data | interchange-bounds |
A map larger than other map tools accept |
| Map data | minimap |
No stored minimap, or one too small for the game's radar |
| Quality | dedupTiles |
Same as duplicate-tiles, surfaced via the Studio quality dialog id |
| Quality | otaFields |
Lobby-required OTA metadata missing |
| Quality | startsInBounds |
Start positions outside the map or sitting on a void cell |
| Quality | schemaSlots |
An advertised numplayers count the schema the game picks for it cannot host (the game reads Network 1..4 schemas from Schema 0 up to the first gap; schemas it never reads are listed) |
| Quality | metalProximity |
Start without a metal-producing feature within 24 tiles (skipped on metal-rich schemas; feature metal read as the game stores it, so metal=56.8 is 56) |
| Quality | voidIslands |
Passable cells unreachable from any start (≥ 20 cells) |
| Quality | heightDiscontinuities |
Cliffs > 32 height units between adjacent cells — likely to block ground pathing |
The shared implementation lives in internal/maplint so the CLI, the studio dialog, and the MCP tool all run identical logic against identical thresholds. The MCP tool name is tnt_lint and accepts path, ota_path (optional), similarity (optional, defaults to 1.0), quality (optional, defaults to true), and game_data (optional, picks the VFS used for the metal-proximity check).
Work with Smacker (.smk/.zrb) video files.
# Show video information (stored and display size, present audio tracks)
kbot zrb info video.smk
# Convert to MP4 at the height the game shows (640x240 interlaced → 640x480)
kbot zrb to-mp4 video.smk video.mp4
kbot zrb to-mp4 video.smk video.mp4 --line-double # repeat lines instead of black
kbot zrb to-mp4 video.smk video.mp4 --stored-height # keep 640x240Requires FFmpeg for conversions. There is no MP4 → Smacker conversion: stock FFmpeg has no Smacker encoder or muxer and kbot has no Smacker writer, so kbot zrb from-mp4 reports that and stops. Make SMK2 movies for TA with RAD Game Tools' Smacker tools.
Work with Bink (.bik) video files — the cutscene format TA: Kingdoms uses in place of the original game's Smacker (.zrb) videos. See docs/formats/bik.md for the format.
# Show video information (native header parse — no ffmpeg needed)
kbot bik info movies/takmission14_ph.bik
# Decode to MP4
kbot bik to-mp4 movies/takmission14_ph.bik intro.mp4to-mp4 requires FFmpeg (which ships a Bink decoder). Conversion is
decode-only: no open-source Bink encoder exists, so there is no
from-mp4. Smacker is no different in practice: kbot zrb from-mp4 only
reports that no Smacker encoder exists.
Browse game files interactively in a terminal.
# Terminal browser
kbot mount ~/ta-content
# Flatten (extract all files to disk)
kbot mount ~/ta-content flatten --target ./flat
# Omit the path to mount the active kbot context (see `kbot ctx`)
kbot mount flatten --target ./flatTerminal commands: ls, cd, pwd, cat, describe, archives, stats, exit
Total Annihilation installs are mounted in TA 3.1c's archive order (loose
files, rev31.gp3, *.ccx, *.ufo, the first ten *.hpi, then the
*.hpi past that limit — see HPI load order),
so every path resolves to the copy the game reads. archives and stats
list the mount order and every archive the game would skip (no Cavedog
trailer, a TA: Kingdoms archive, an unreadable file), with the reason.
For a graphical asset explorer, use the Files tab in kbot studio (see
below). It browses the VFS with per-folder listings and adds rich per-format
previews:
- View GAF animations with APNG preview, sequence/frame controls, and transparency toggles
- View PCX/FNT images with format options, and palettes as a swatch grid
- Play SMK/ZRB/Bink video transcoded to MP4 with an animated poster
- View TNT/SCT maps with minimap/heightmap/tilemap/buildmap/voidmap renders
- Inspect COB disassembly/decompilation, TDF/FBI/OTA structure, and raw hex
- Background cache warming with live websocket progress
Extract a game install into a static asset pack: the same JSON/PNG payloads
KBot Studio serves on demand, written once as plain files. Serve the output
directory over any static HTTP host and point @coreprime/kbot-game3d's
HttpPackProvider at its base URL — the renderer runs with no studio
server (the seam a replayer or any embedding site consumes).
# Pack every unit in a TA install
kbot pack ~/games/totala ./ta-pack
# A minimal pack for two units plus one map
kbot pack ~/games/totala ./ta-pack --units armcom,armpw --maps "greenhaven"
# TA:Kingdoms
kbot pack ~/games/kingdoms ./tak-pack --game takA pack contains manifest.json (game id, sides, unit list, content hash),
unitdb.json (per-unit movement class + motion domain, build/HP/weapon
stats, corpse chain, build-pic path and slot-ordered weapon ids; footprint,
terrain limits and standing orders are resolved as TA 3.1c resolves them,
and the economy keeps metalMake, extractsMetal and makesMetal apart),
palette.json, weapons.json (every weapon units can name in the game's
ID-indexed weapon table, built from weapons/*.tdf as the game builds it —
render data: rendertype, colors
— resolved RGB plus raw indices — beam duration, velocity, trajectory and
guided-flight fields — turn rate, water weapon, acceleration, flight time —
blast diameter, range, sound stems), features.json (every map-feature
definition: category, footprint, height, 3DO object link and GAF sprite
dims — what the renderer's 3D feature stand-ins size themselves from; the
maps' referenced object models pack under models/), and per-asset files under
models/ (authored geometry), models-enhanced/ (hidden-face
reconstruction, the Enhanced Mesh variant), textures/, cob/, sounds/,
weaponbitmaps/, unitpics/, cursors/, groundtiles/ and maps/
(map JSON + tile atlas + authentic minimap PNG). The whole weapon
catalogue's projectile meshes, sounds and sprite strips pack regardless of
the unit subset, so a replay can draw any weapon a recording names.
Output is deterministic — the same install always produces a byte-identical
pack, and the manifest's contentHash is the pack's identity. See
kbot pack --help for the full layout.
import { createWorld, HttpPackProvider } from '@coreprime/kbot-game3d'
const world = await createWorld(canvas, {
assets: new HttpPackProvider('https://cdn.example.com/packs/ta-31c'),
})
await world.addUnit('armcom', { x: 0, z: 0, side: 1 })A browser-based workbench for Total Annihilation and TA: Kingdoms. It mounts a
game install (or the active kbot ctx) over a VFS and opens a tabbed
single-page app whose welcome screen offers several workflows.
# Open against a packed TA install
kbot studio ~/games/totala
# Or use the active kbot context (see `kbot ctx`)
kbot studio
# Custom port (default 8100)
kbot studio --port 9000Explorer — the full Files tab / VFS browser: table and icon listings with live thumbnails, search, and a layer/source override, plus rich per-format previews — GAF/PCX/FNT/palette images, TNT/SCT maps, COB/BOS code (decompile, disassembly, call graph, lint), AI profiles, TDF/FBI/OTA structure, SMK/ZRB/BIK video, and audio — with a metadata, hex, and layering tab on every file and a download menu that offers rendered variants (PNG/GIF, MP4) alongside the raw bytes. A background cache warmer renders preview assets on startup and reports progress live over a websocket.
Map Creator — a visual TNT/OTA map editor. Start from a blank canvas or
open an existing map, then paint terrain from .sct sections, place and scatter
features, sculpt the heightmap, mark engine voids, and set per-schema start
positions. Split panes, undo/redo, a ruler, and symmetry tools aid editing; a
Quality Checker lints the map (with auto-fixes) before you save. The export
menu bundles a downloadable map archive (or loose TNT/OTA) plus full renders,
minimaps, heightmaps, buildmaps, and voidmaps. A Total Annihilation map
downloads as <map>.ufo: TA 3.1c mounts every .ufo, above every .hpi,
while a .hpi competes for the game's ten *.hpi slots. TA: Kingdoms maps
download as .hpi.
Workspaces and mod export — the picker's Export mod… dialog packs a
workspace's changed files into one archive. For Total Annihilation choose
.ufo (the default: no count limit, ranks above every .hpi), .ccx (ranks
above every .ufo) or .hpi. Before the download it checks the archive
against the base install's mount order: its position, any retail *.hpi it
would push past the ten-archive limit, and every file a higher-priority
archive or a loose file of the install also provides — the game ignores the
mod's copy of those, so they must ship as loose files in the game directory.
Every exported archive ends with the Cavedog trailer the game requires.
Saving an opened map edits its
.ota in place: only the values you change are rewritten, and everything the
editor does not show (victory conditions, pre-placed units and features,
fractions, other keys) is kept; an .ota it cannot read is left unchanged.
Schemas and start positions are read the way the game reads them, and the
saved .tnt carries a minimap laid out as the game's own (with its minimap
flag set).
Sandbox Mode — drop units onto a battlefield and test them live. A WASM physics core simulates movement, commands, weapon fire, and damage with team colours and particle effects. Run a local in-browser scene, host an authoritative match, or join a live hosted sandbox for shared multiplayer testing. Units carry the game's resolved data (movement-class terrain limits, standing orders — the commanders start on Hold Position — and weapons from the ID-indexed weapon table); a map loads with the schema a two-player skirmish would use, its SurfaceMetal under every plot and player 1 at StartPos1.
Unit Creator — open a unit to inspect and test it: an orbiting 3D model viewer with per-piece animation and weapon hardpoints, a piece tree, texture and weapon galleries, and a COB/BOS thread debugger that steps scripts with synced source/assembly, locals, and runtime port values.
A Scripting workflow and further tools are previewed on the welcome screen as planned additions.
Generate the markdown reference catalogues that live in two standalone sibling repos:
- coreprime/reference-ta — Total Annihilation (278 units, 198 weapons, 45 builders).
- coreprime/reference-tak — TA: Kingdoms + Iron Plague (203 units, 198 weapons, 32 builders).
# Total Annihilation — uses the active kbot context for --source
kbot document --target ~/go/src/github.com/coreprime/reference-ta
# TA: Kingdoms
kbot document --game takingdoms \
--source ~/tak-flat \
--target ~/go/src/github.com/coreprime/reference-tak
# Explicit source + skip the portrait re-conversion (TA only — TA:K
# has no unitpics/ to convert)
kbot document --source ~/ta-flat --target ./reference --skip-portraits
# Force a fresh PCX → PNG batch (default skips if PNG already exists)
kbot document --target ./reference --force-portraitsThe TA catalogue reads the data the way TA 3.1c does. FBIs and TDFs are
decoded with kbot-io's codec, so comments are blanked, a value runs to the
next ; and numbers are read as the game reads them (BuildCostMetal=40.28
costs 40). Weapons come only from the .tdf files directly in weapons/
(gamedata/weapons.tdf is not a weapon source) and are placed in the game's
256-slot table by ID=; each unit's Weapon1/2/3= is resolved through that
table. A unit gets a [CANBUILD] list only with Builder=1, read up to the
first gap in the canbuildN numbering, skipping names that match no unit and
keeping at most 30; each download/*.tdf adds its first five sections,
whatever their names, and a build menu holds at most 31 units. Entries the
game leaves out are listed at the end of ta-weapons.md and
ta-buildtree.md (retail data has none).
Output layout under --target (TA):
<target>/
├── ta-units.md 278-row unit catalogue
├── ta-weapons.md 198-row weapon catalogue + reverse cross-ref
├── ta-buildtree.md per-builder 2×3 menu grids + visual reverse index
└── img/ta-units/*.png 282 unit portraits (Objectname-keyed, lowercased)
Output layout under --target (TA: Kingdoms):
<target>/
├── tak-units.md per-side unit catalogue (Aramon/Taros/Veruna/Zhon/Creon/Mon/Lif/NPC)
├── tak-weapons.md inline [WEAPONn] sections harvested from every FBI
└── tak-buildtree.md per-builder canbuild/ listings (linear Priority order)
Implementation lives in internal/documentor;
text/template files are embedded under
internal/documentor/templates/ (one set per game). The TA:K extractor
(extract_tak.go) is separate
from the TA extractor because TA:K inlines weapons in FBIs, has
4 sides (5 with Iron Plague), and uses canbuild/<builder>/*.tdf
directories rather than gamedata/sidedata.tdf + download/*.tdf.
Convenience targets:
| Task | What it does |
|---|---|
task document |
TA → ../reference-ta (uses $TA_UNPACKED_PATH) |
task document-tak |
TA:K → ../reference-tak (uses $TAK_UNPACKED_PATH) |
Note on output licensing. The
kbot documenttool itself is MIT-licensed (it ships in this repo). Its output — the rendered markdown tables and the copied/converted unit portraits — is derivative of Cavedog/Humongous game data and is not covered by kbot's MIT licence. The reference repos (coreprime/reference-ta / coreprime/reference-tak) intentionally ship without aLICENSEfile for that reason; see their READMEs for the attribution notice.
Expose kbot's TA tooling to AI assistants (Claude Code, Claude Desktop, Cursor, etc.) over MCP. The assistant can then decompile scripts, lint COB, inspect HPI archives, render GAF/PCX, and so on directly against your game install.
# Register a TA install as a virtual filesystem (recommended)
kbot mcp --game-data ~/games/totala
# Name it explicitly and add a TA: Kingdoms install alongside
kbot mcp \
--game-data totala=~/games/totala \
--game-data kingdoms=~/games/tak
# Raw mount roots without the VFS layer (back-compat; assistant must pass absolute paths)
kbot mcp --mount ~/games/totala --mount /tmp/kbot-out
# Long-lived HTTP transport for multi-client setups
kbot mcp --http 127.0.0.1:8765 --game-data ~/games/totala
# Without --game-data or --mount, kbot mcp registers the active kbot
# context (see `kbot ctx`) as the default game-data folder
kbot mcp--game-data NAME=PATH (or just PATH, name derived from the basename) registers a Total Annihilation / TA: Kingdoms install as a named virtual filesystem. The folder is scanned once and its .hpi / .ufo / .ccx / .gp3 archives are layered under its physical files in the order the game mounts them (for Total Annihilation, TA 3.1c's order: rev31.gp3, *.ccx, *.ufo, the first ten *.hpi, then the *.hpi past that limit; see HPI load order). vfs_game_data lists the mount order and any archive the game would skip, with the reason. The first --game-data is the default the assistant uses when a tool call omits game_data. Each game-data base is added implicitly to the path guard, so on-disk paths inside it also resolve.
Once a game-data folder is configured, every tool's path argument accepts:
- an absolute on-disk path (e.g.
/Users/me/games/totala/units/ARMCOM.fbi), - a virtual path inside the VFS (e.g.
units/ARMCOM.fbi; the game's ownunits\ARMCOM.fbispelling works on every host), or - a bare filename (e.g.
ARMCOM.bos) that the resolver searches for across every archive and physical file. Ambiguous matches return a list so the assistant can pick.
When a hit lives inside an archive, kbot extracts it to a temp file for the duration of the call and cleans up afterwards. Physical files are passed through directly.
--mount is the older, simpler gate: it just restricts paths the assistant passes to lie inside the given root, with no VFS layering. Use it when you want to expose a non-game-data directory (such as an output scratch dir) without the cost of walking and indexing archives. Without any --mount or --game-data, the server runs in permissive mode and accepts any absolute path — fine for local development, unsafe on shared hosts.
These tools render various artefacts from a .tnt to PNG. Each accepts path (the .tnt argument, resolved through the same VFS rules as every other tool) and output (the destination PNG path):
| Tool | Purpose |
|---|---|
tnt_image |
Full tile-grid render at 32 px per tile. Used by the studio's Export Full Render menu item. |
tnt_heightmap |
8-bit grayscale elevation grid (round-trip safe by default; normalize=true for human viewing). |
tnt_buildmap |
Per-cell buildability classification — black/red/blue/yellow/green key (see the CLI section above). Optional sealevel overrides the .tnt header's value (0 disables the underwater check). |
tnt_voidmap |
Engine-void mask — cells with Feature == 0xFFFC painted red, everything else transparent. |
tnt_minimap |
Embedded 252×252 minimap (paletted PNG when paletted=true). |
tnt_preview |
tnt_image plus composited feature sprites and numbered StartPos markers for the schema the game finds as "Schema schema" (defaults to 0). Requires game_data so feature sprites and the sister .ota resolve. |
These tools inspect and render TA: Kingdoms truecolor animations (see docs/formats/taf.md). taf_render and taf_sheet return the rendered PNG inline so the assistant can show it directly; taf_export writes an animated file to output.
| Tool | Purpose |
|---|---|
taf_info |
Per-frame JSON: name, size, origin, pixel format and duration for every frame. |
taf_list |
One-line JSON summary (name, frame count, duration, format). |
taf_render |
Render a single frame and return it inline as PNG (optional output also saves it). |
taf_sheet |
Render every frame into a grid sprite sheet, returned inline as PNG. |
taf_export |
Write an animated GIF (format: gif) or APNG (format: apng/png, default) to output. |
taf_lint |
Structural validation findings as JSON. |
tsf_info |
Summarise a TSF document — animation, frames and referenced layer images. |
tsf_lint |
Shape-check a TSF document against the compiler's expectations. |
These tools inspect and convert TA: Kingdoms Bink cutscenes (see docs/formats/bik.md).
| Tool | Purpose |
|---|---|
bik_info |
Header JSON: geometry, frame count, frame rate, duration, alpha/grayscale flags and per-track audio. Native parse, no ffmpeg. |
bik_to_mp4 |
Decode a .bik to MP4 (H.264/AAC) via ffmpeg and write it to output. Decode-only — no reverse conversion exists. |
These tools inspect and convert the original TA Smacker/ZRB cutscenes (see docs/formats/smacker.md).
| Tool | Purpose |
|---|---|
zrb_info |
Header JSON: signature, stored and display geometry, frame count, frame rate, duration and the present audio tracks (rate, channels, sample size, compression). Native parse, no ffmpeg. |
zrb_to_mp4 |
Decode a .zrb/.smk to MP4 (H.264/AAC) via ffmpeg at the height the game shows (640x240 interlaced → 640x480) and write it to output. |
zrb_from_mp4 |
Reports that no Smacker encoder exists: stock ffmpeg has no Smacker encoder or muxer and kbot has no Smacker writer (only an ffmpeg that lists both is tried). |
| Tool | Purpose |
|---|---|
crt_describe |
Summarise a TA: Kingdoms .crt scenario: placed-unit breakdown, per-player rule counts and named trigger regions, returned as JSON. Pass units: true for the full per-unit placement list. |
| Tool | Purpose |
|---|---|
hpi_list |
List the files in an HPI/UFO/CCX archive (glob filter), with a game_mount line saying whether TA 3.1c would mount it. |
hpi_info |
Header and content summary; game_mount reports whether TA 3.1c would mount the file and why not (no Cavedog trailer, TA: Kingdoms version 2 archive, unreadable directory), the header key as the game reads it and the trailer year. |
hpi_extract_file |
Extract one archive entry to a file. |
These tools let the assistant query the virtual filesystem directly:
| Tool | Purpose |
|---|---|
vfs_game_data |
List registered game-data folders, their base paths, archive counts and file counts, the archive discovery mode, the mounted archives in lookup order, and every archive that was not mounted with the reason. |
vfs_find |
Locate files by bare name (ARMCOM.bos), basename glob (*.bos) or full-path glob (units/ARM*.fbi). Returns the virtual path and the source archive for every hit. |
vfs_list |
List virtual files and subdirectories under a given directory. |
vfs_stat |
Show every layer that contains a file — useful when a mod overrides a base-game asset. |
So the user can ask "where is ARMCOM.bos?" and the assistant calls vfs_find with query: "ARMCOM.bos".
Register kbot as a user-scoped MCP server so it loads in every session:
claude mcp add -s user kbot kbot mcp --game-data /path/to/total-annihilationIf the claude CLI is not on your PATH, add the entry directly to ~/.claude.json:
{
"mcpServers": {
"kbot": {
"type": "stdio",
"command": "kbot",
"args": [
"mcp",
"--game-data",
"/path/to/total-annihilation"
]
}
}
}Restart any open Claude Code sessions; new sessions will pick up the server automatically. Use /mcp inside Claude Code to verify the connection and list the available tools.
For Claude Desktop, add the same mcpServers block to its config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS) and restart the app.
kbot supports tab completion for all commands, subcommands, and flags.
# Generate and install (uses brew --prefix to find the correct path):
kbot completion zsh > $(brew --prefix)/share/zsh/site-functions/_kbot
# Ensure your .zshrc includes:
autoload -Uz compinit && compinitNote: On Apple Silicon Macs (M1/M2/M3), Homebrew installs to
/opt/homebrew. On Intel Macs, it's/usr/local. Using$(brew --prefix)handles both automatically.
You may need to restart your terminal or run exec zsh for changes to take effect.
# Requires bash-completion (install via Homebrew if needed):
brew install bash-completion@2
# Generate and install:
kbot completion bash > $(brew --prefix)/etc/bash_completion.d/kbot
# Ensure your .bash_profile includes:
[[ -r "$(brew --prefix)/etc/profile.d/bash_completion.sh" ]] && . "$(brew --prefix)/etc/profile.d/bash_completion.sh"kbot completion fish > ~/.config/fish/completions/kbot.fishkbot completion powershell | Out-String | Invoke-Expression
# Or save permanently:
kbot completion powershell > kbot.ps1
# Add `. /path/to/kbot.ps1` to your PowerShell profilekbot/
├── cmd/kbot/ CLI entry point and subcommands
├── formats/ Public format packages
│ ├── ai/ AI opponent profiles
│ ├── bik/ Bink video (TA:K cutscenes; header parse + ffmpeg decode)
│ ├── crt/ TA:K scenario sidecars (placed units, rules, triggers)
│ ├── fnt/ Bitmap fonts (1bpp, MSB-first)
│ ├── gaf/ Sprite animations + writer
│ ├── hpi/ HPI/UFO/CCX archives
│ ├── pal/ TA .PAL palettes + .ALP/.LHT/.SHD lookup tables
│ ├── pcx/ PCX images
│ ├── scripting/ COB/BOS bytecode
│ │ ├── assembly/ Assembler + disassembler
│ │ ├── compiler/ BOS → COB compiler
│ │ ├── decompiler/ COB → BOS decompiler
│ │ ├── linter/ Static analysis (17 rules)
│ │ └── parser/ Lexer, parser, preprocessor
│ ├── sct/ Map sections
│ ├── smacker/ Smacker video
│ ├── tdf/ Text data files
│ ├── tdo/ 3DO models
│ └── tnt/ Map terrain
├── filesystem/ Virtual filesystem (HPI layering)
└── internal/
├── assetrender/ Shared VFS render/describe engine (studio Files tab)
├── assets/ Embedded TA palette
├── cache/ On-disk file cache
├── mount/ Terminal asset browser (kbot mount)
├── studio/ Web workbench + Files explorer (kbot studio)
└── testutil/ Asset-path helpers for tests
task # build + vet + lint + test
task build # build Studio web assets + kbot binary
task install # install kbot to $GOPATH/bin
task test # run all tests
task test-race # tests with race detector
task lint # golangci-lint + eslint
task coverage # generate coverage.html- Go 1.24+
- Node.js 18+ and npm (for the Studio web assets)
- Task (
go install github.com/go-task/task/v3/cmd/task@latest) - golangci-lint (for
task lint) - FFmpeg (optional, for video conversions)
Many tests require a copy of the Total Annihilation game assets. You need:
- Original game installation — from GOG, Steam, or original CD media (version 3.1c recommended)
- Flattened (unpacked) assets — extracted from the HPI/UFO/CCX archives
Use kbot mount flatten to extract the packed archives into a flat directory:
# First build kbot
task build
# Flatten the game assets (adjust the source path to your TA install location)
./bin/kbot mount ./path/to/total-annihilation --flatten --target ./ta-flattenedThen create a .env.local file in the project root (this file is gitignored):
# .env.local — local paths to game assets for tests
TA_UNPACKED_PATH=/path/to/ta-flattened
TA_PACKED_PATH=/path/to/total-annihilation
TAK_UNPACKED_PATH=/path/to/ta-kingdoms-flattened| Variable | Description | Used by |
|---|---|---|
TA_UNPACKED_PATH |
Flattened TA game assets (scripts, maps, textures, etc.) | Most format tests |
TA_PACKED_PATH |
Original packed TA archives (HPI, UFO, CCX, GP3) | HPI/VFS tests |
TAK_UNPACKED_PATH |
Flattened TA: Kingdoms assets | TA:K format, adapter, and combat tests |
ALLOW_SKIP_ASSETS |
If true, tests skip when assets are missing. If false (default), tests fail. |
All asset tests |
By default, missing game assets cause test failures to ensure developers have a complete test environment. Set ALLOW_SKIP_ASSETS=true in CI or environments where game assets are not available.
kbot supports Total Annihilation and Total Annihilation: Kingdoms.
The format packages (formats/…) are game-agnostic — a TNT or COB announces
its own dialect through version words and parsing dispatches on what the
bytes say. Everything that instead depends on which game an install is
lives behind a per-game adapter, registered in two registries:
- Go —
gamesdefines theGame/Adapterinterfaces (palette resolution, unit sounds, cursor palettes, tilesets, map terrain groups, FBI→unit-meta building).games/totalaandgames/takingdomsregister themselves from packageinit; sessions resolve one adapter per mounted install viagames.Resolve(id). - JS —
@coreprime/kbot-game-totalaand@coreprime/kbot-game-takingdomscarry the studio's per-game surfaces: weapon-script conventions, COB quick-action catalogues, branding, welcome theming, and theview3dtables injected into the game-agnostic@coreprime/kbot-game3drenderer at boot. The studio resolves the active adapter throughui/common/game-registry.js.
Engine behaviour that varies per unit rather than per game — TA's per-slot
AimPrimary/FirePrimary scripts versus TA:K's shared parameterized
AimWeapon/FireWeapon set — is resolved from what each COB actually
exports (engine/sim/weapon_convention.go), so the simulation never consults
a game id and mixed-convention content just works.
To build a custom game on the TA formats:
- Go: implement
games.Game(usually embedding thetotalaadapter and overriding what differs), callgames.Registerfrom your packageinit, and blank-import the package where the studio wires adapters (internal/studio/palette.go). - JS: create a package composing over
@coreprime/kbot-game-totalathe way@coreprime/kbot-game-takingdomsdoes (spread the baselinegameobject, override fields), and add it to the registry list ininternal/studio/web/ui/common/game-registry.js. The required surface is pinned by the contract tests inpackages-js/game-takingdoms/contract.test.js(task test-js). - Context: register the install with
kbot ctx add <path> --game custom. Unregistered ids resolve to the Total Annihilation baseline in both registries, so a partially-adapted game degrades to TA behaviour instead of failing.
Having played Total Annihilation since its release in 1996 religiously, it was one of the formative games of my youth. Back in 1999, a 14 year old version of me logged onto the PlanetAnnihilation forums. They were dead, quiet. I was annoyed that there was lots of modding and activity going on, but nobody was updating the site, similar to how PlanetQuake/PlanetHalfLife were being maintained well. With poor wording, a young version of me unleashed some harsh words.
Instead of telling me to get stuffed, Frank "DMFrank" Rogan responded. He basically offered me the keys to the site, and said if I wanted it that badly, I can give it a go. I spent the next few years learning how to write editorial content, connecting with mod developers, the basics of software engineering to build new features for the site. A few years of building a community, and promoting the work of other developers was good for the soul.
Then life, or more specifically University hit. That plus the age of the game meant that the rate of new and interesting things slowed down. I went away for a while. I relocated to Australia from the UK. Whilst I never really disappeared or stop reading the forums, tracking projects, I no longer had time to be present/engage with them.
I came back in 2014, having realised I could reverse engineer the network protocols behind the defunct Boneyards.net game service a journey I partially documented over at TAUniverse. However it was an immense amount of work, and between trialling out one or two folks to help, none of whom had the means or ability to really lighten the load - the project again, succumbed to life.
Speaking with one of my colleagues at work, the idea came up to try this again - but with the power of AI to help drive the process. Part of the work for rebuilding Boneyards.net involves lots of work with the TA game formats, such as creating new Galactic maps, packing them into HPI files, being able to reason about the planets/maps being referenced.
This project is an open-source implementation of the various formats. I have archived some of these files in another repository. A longstanding bug-bear of mine was the incomplete or tricky to use tooling, so I've endevoured to make these as robust as I can and will continue to polish them.
© Steve Gray
The code is released under the MIT License — see LICENSE for details.
