.venv/bin/python -m opent5 COMMAND ... (or `opent5 COMMAND ...` once installed)
Every command runs headless. Add --json to any command for one JSON object on stdout;
without it the output is plain text for people. Errors go to stderr (and, with --json, into
the object as "ok": false, "error": "...").
| Exit code | Meaning |
|---|---|
| 0 | done |
| 1 | failed: the zone does not read, an edit is refused, verification found a problem, or an output path is not allowed. The message says what was expected and what was found |
| 2 | usage: unknown command, missing argument, replace without ASSET FILE pairs |
Where output may go. No command writes into the game folders named in .env
(OPENT5_ZONES, OPENT5_PATCH_ZONES, OPENT5_DLC_ZONES, and the folder of OPENT5_ELF), and
no command writes over the zone it reads. Such a path is refused before anything is written.
opent5 --version prints the version and the licence notice, with its line breaks.
Assets on the command line (ASSET): an index in the zone's asset list (40), a name
(mp/gametypestable.csv), or type:name when a name is used by more than one type
(image:~-gus_art_streetsigns_c). Names also find assets loaded inside other assets (an
image inside a material, a material inside the world), which have no index of their own.
A ZONE argument may be a path or a bare zone name such as mp_nuked, which is looked up in
the folders named in .env (base disc first, then the update, then the DLC).
opent5 info ZONE [--json]
What the zone is and what it holds.
$ opent5 info patch_mp.ff
patch_mp (.../patch_mp.ff)
file 1196128 bytes, sha1 499ce6547392fe44ae0a52186712591e770e19e2
signed yes (console signature at 0x3c)
content 3715811 bytes in 77 chunks
blocks 0xa98 0x0 0x0 0x72580 0x2c46d1 0x0 0xb7b4
assets 1337 in the asset list, 1087 loaded inline
parse exact
types
445 localize
...
signed says the header carries a field that decodes under the console key. An edited zone
still carries the original field, which no longer matches its content.
{
"ok": true,
"zone": "patch_mp",
"path": ".../patch_mp.ff",
"bytes": 1196128,
"sha1": "499ce6547392fe44ae0a52186712591e770e19e2",
"signed": true,
"content_bytes": 3715811,
"chunks": 77,
"block_sizes": [2712, 0, 0, 468352, 2901713, 0, 47028],
"assets": 1337,
"inline_assets": 1087,
"type_counts": {"localize": 445, "xanim": 328, "...": 0},
"parse_exact": true,
"parse_problems": [],
"seconds": 0.9
}opent5 list ZONE [--type TYPE] [--match GLOB] [--inline] [--json]
The asset list, optionally only one type, only names matching a glob (case-sensitive,
fnmatch rules), and with --inline also the assets loaded inside other assets.
$ opent5 list patch_mp.ff --type stringtable
8 stringtable 46 mp/defaultstringtable.csv
40 stringtable 4423 mp/gametypestable.csv
...
17 asset(s)
Columns: index, type, size in content bytes, name.
{
"ok": true,
"zone": "patch_mp",
"count": 1,
"assets": [
{"index": 40, "type": "stringtable", "name": "mp/gametypestable.csv", "size": 4423,
"offset": 324206, "editable": ["table", "fields", "hex"]}
]
}An inline asset has "index": ["inline", <type number>, <name>] and "parent", the index of
the top-level asset that loads it. editable names the editors that apply (docs/edit-api.md).
opent5 extract ZONE OUTDIR [--type TYPE] [--name GLOB] [--previews] [--json]
Without --type / --name: the full export of opent5.export (docs/extract.md): images as
PNG, rawfiles, stringtables as CSV, localize as JSON, world and models as OBJ, map entities,
and every other asset as JSON, with manifest.json. --previews also renders previews.
With a filter: only the selected assets, one file each:
| Type | File |
|---|---|
| rawfile | rawfiles/<name as stored>, the bytes as the game reads them (scripts inflated, without their final NUL) |
| stringtable | stringtables/<name>.csv |
| localize | localize/<NAME>.txt, the value |
| image | images/<name>.png, level 0 (from the zone, or from the .pak beside the zone) |
| map_ents, col_map | map_ents/<name>.ents, the entity string |
| anything else | <type>/<name>.json, the asset's typed fields |
These are the files replace takes back.
$ opent5 extract mp_nuked.ff out/nuked --type col_map_mp
$ opent5 extract mp_nuked.ff out/nuked --type image --name '*streetsigns*'
{
"ok": true, "zone": "mp_nuked", "outdir": "out/nuked", "mode": "selected", "assets": 1,
"files": ["out/nuked/map_ents/mp_nuked.d3dbsp.ents"],
"failures": []
}Full export: {"ok", "zone", "outdir", "mode": "full export", "files", "failures", "manifest"}
(files and failures are counts; the manifest lists them).
opent5 replace ZONE ASSET FILE [ASSET FILE ...] -o OUT [--no-verify] [--share split|all]
[--allow-shared] [--resize] [--json]
Replaces assets with the content of files and saves a new zone. Every length is allowed: the zone is re-laid out and every offset pointer remapped (docs/edit-api.md, Saving).
| Asset type | FILE |
|---|---|
| rawfile | the new file (any bytes; a .gsc / .csc is compressed the way the game stores scripts) |
| stringtable | CSV with the same number of columns; rows may be added or removed at the end, and every changed cell is rehashed and the index re-sorted |
| localize | the new value (one final newline is dropped) |
| image | PNG of the same width and height, or a DDS of the same format, size and at least the same mip count. Pixels in the zone (inline or deferred) or streamed from .pak files (below); with --resize, a streamed image may take another power-of-two size |
| map_ents, col_map | the new entity string |
Shared strings. The zone linker stores identical strings once, so several localize keys (or
stringtable cells, or other string fields) can read the same stored text; for example
MENU_PLAYER_MATCH_CAPS and MPUI_PLAYER_MATCH_CAPS share "PLAYER MATCH" in
code_post_gfx_mp. --share split (the default) changes only the named asset: the other
fields keep the old text as their own copy. --share all changes the stored string, so
every field that shares it changes too (fields that read only the end of it, or that are an
asset's name, keep the old text). Each change's detail says which happened (share=split or
share=all). It applies to localize values and stringtable cells.
$ opent5 replace code_post_gfx_mp MPUI_PLAYER_MATCH_CAPS new.txt --share all -o out/cpg.ff
change: asset 4162 localize share=all: the stored string changed for 2 field(s) (also MENU_PLAYER_MATCH_CAPS)
Streamed images (docs/edit-api.md, docs/research/pak.md 9). The new parts are written to
.pak files beside OUT: <OUT stem>.pak for the level's own pak (copy it into the game
folder with the .ff; the game opens <zone>.pak beside <zone>.ff). A part in a shared
pak (images_low, common, ui_mp, img_patch; for most images the small mip tail in
images_low.pak) is left as it is unless --allow-shared, and the change's detail names it.
--allow-shared writes that pak too, under its own name beside OUT, and the report prints
warning: images_low.pak is shared: every zone that streams from it reads it, so a changed copy changes this image wherever it is used. An image whose parts are all in shared paks is
refused without the flag. Every written pak is listed:
$ opent5 replace mp_nuked.ff 'image:~-gmp_nuked_townsign_c' sign.png -o out/d/mp_nuked.ff
...
pak: out/d/mp_nuked.pak (172005376 bytes, sha1 cb4ac63f...); entries 702, 703, 704 written; 2050 of 2053 identical
--resize accepts a PNG or DDS of another power-of-two size for a streamed image (pak.md
9.1): the mip tail keeps its size, each larger level becomes one part in the level pak (new
entries are added after its last one), and the image's header in the zone changes, so the
zone is rebuilt and its console signature no longer matches. Without the flag another size
is refused with a message naming it.
$ opent5 replace mp_nuked.ff 'image:~-gmp_nuked_manneq_head_male_01_c' head_256x512.png \
-o out/j/mp_nuked.ff --resize
change: asset image:~-gmp_nuked_manneq_head_male_01_c (inline) image size 128x256 (9 mips) -> 256x512 (10 mips), 4 parts; level pak entries added: 2053; ...
pak: out/j/mp_nuked.pak (172070912 bytes, sha1 cd2d037b...); entries 1281, 1282, 2053 written, 1 added; 2051 of 2054 identical
After saving, the file is verified (unless --no-verify): reopened, parsed exactly, every
asset not edited identical to the source (or identical apart from remapped pointers that
each name the same thing), every edited asset read back as edited.
$ opent5 replace mp_nuked.ff col_map_mp:maps/mp/mp_nuked.d3dbsp moved.ents -o out/mp_nuked.ff
mp_nuked -> out/mp_nuked.ff (36349760 bytes, sha1 3740413f440542d7c02eb85395aeb753eaa8d94e)
replaced col_map_mp maps/mp/mp_nuked.d3dbsp from moved.ents (text)
change: asset 407 text
verified: 529 assets; 455 identical, 73 identical apart from 16512 remapped pointers, 1 edited and read back
note: The console signature at 0x3c is the original one and no longer matches the content: ...
{
"ok": true,
"zone": "mp_nuked",
"output": "out/mp_nuked.ff",
"bytes": 36349760,
"sha1": "3740413f440542d7c02eb85395aeb753eaa8d94e",
"source_sha1": "6d5a4a0e5c031c0d8fcd555413730b014e9c0c5d",
"replaced": [
{"asset": {"index": 407, "type": "col_map_mp", "name": "maps/mp/mp_nuked.d3dbsp", "...": ""},
"file": "moved.ents", "as": "text"}
],
"changes": [{"index": 407, "kind": "text", "detail": ""}],
"verified": true,
"problems": [],
"verification": {
"assets_checked": 529, "identical": 455, "identical_after_pointer_remap": 73,
"pointers_checked": 16512, "edited": 1, "edited_read_back": 1,
"shared_string_copies_in": []
},
"signature_note": "The console signature at 0x3c is the original one and no longer matches ..."
}changes[].detail says when a string another asset shared was given its own copy (the
zone's linker shares identical strings; an edit changes only the asset it names).
shared_string_copies_in lists the assets that received such a copy.
opent5 unpack ZONE DIR [--json]
opent5 pack DIR -o OUT [--keep-size] [--json]
Container level only: unpack writes the header, the decompressed content and a chunk
manifest to DIR; pack builds a fastfile from such a folder (the zone length field is
derived from the content unless --keep-size). An unmodified folder packs to the original
file byte for byte. JSON: {"ok", "zone", "directory", "chunks", "content_bytes", "log"} and
{"ok", "directory", "output", "bytes", "sha1", "log"}.
opent5 verify ZONE [--against SOURCE] [--json]
Checks a zone: the container (every chunk decrypts and inflates, four terminators, the
length field), an exact parse, and that the parse writes back to the same content. With
--against, compares it asset by asset with its source and lists what changed. A zone
that passes exits 0 whether or not it differs from the source.
$ opent5 verify out/mp_nuked.ff --against mp_nuked.ff
mp_nuked: OK (sha1 3740413f440542d7c02eb85395aeb753eaa8d94e)
container: 1405 chunks, 68848473 content bytes, 4 terminators
parse: exact
rewrite from the parse: identical
against mp_nuked.ff: 455 identical, 73 identical apart from remapped pointers, 1 changed
changed: 407 col_map_mp maps/mp/mp_nuked.d3dbsp: expected 2451471 bytes as in the source, found 2451487 at 0x1e82252
{
"ok": true,
"zone_path": "out/mp_nuked.ff",
"sha1": "...",
"checks": {
"container": {"chunks": 1405, "content_bytes": 68848473, "terminators": 4,
"padded_as_original_writer": true},
"parse_exact": true,
"rewrites_identically": true
},
"zone": "mp_nuked",
"signed": true,
"against": {
"source": "mp_nuked.ff", "source_sha1": "...", "comparable": true,
"identical": 455, "identical_after_pointer_remap": 73, "pointers_checked": 16512,
"changed": [{"index": 407, "type": "col_map_mp", "name": "maps/mp/mp_nuked.d3dbsp",
"reason": "expected 2451471 bytes as in the source, found 2451487 at 0x1e82252"}]
},
"problems": []
}opent5 rebuild ZONE -o OUT [--recompress] [--json]
Parses the zone, writes every asset back from its parsed form, and packs the container. For
an unmodified zone the result is byte-identical to the source, and the command says so (and
exits 1 if it is not). Chunks whose content is unchanged are carried as stored;
--recompress deflates every chunk afresh instead, which also gives the identical file.
$ opent5 rebuild mp_nuked.ff -o out/mp_nuked.ff
mp_nuked: 529 assets parsed and written back
content identical
file byte-identical (1404 chunks carried, 0 deflated afresh)
source 6d5a4a0e5c031c0d8fcd555413730b014e9c0c5d mp_nuked.ff
output 6d5a4a0e5c031c0d8fcd555413730b014e9c0c5d out/mp_nuked.ff
{
"ok": true, "zone": "mp_nuked", "source": "mp_nuked.ff",
"source_sha1": "6d5a4a0e5c031c0d8fcd555413730b014e9c0c5d",
"output": "out/mp_nuked.ff", "sha1": "6d5a4a0e5c031c0d8fcd555413730b014e9c0c5d",
"bytes": 36349760, "assets": 529, "content_identical": true, "file_identical": true,
"chunks_carried": 1404, "chunks_deflated": 0, "seconds": 4.4
}Every retail zone is signed at 0x3c and the signature cannot be regenerated. A zone whose
content changed loads only on a client with the signature check patched out; replace says
so in signature_note every time.
Convert a map built with the PC Mod Tools into a PS3 zone: the base zone's world assets (gfx_map, col_map_mp and its entities, com_map, game_map_mp) are replaced with the PC map's, everything else is kept. The base is only read; the result goes to OUTDIR. Details, supported gametypes and limits: docs/convert.md.
opent5 convert PC_MAP.ff --base mp_nuked -o OUTDIR [--lighting baked|flat|sunlit|keep]
[--modes sd,dom,...|all] [--pc-game DIR] [--new-material NAME|all]
[--name mp_NAME [--copy-pak]] [--register --patch-mp PATCH_MP.ff
[--title TEXT] [--description TEXT] [--ui-slot N]] [--json]
--lighting baked (the default) converts the PC map's own cod2rad lightmaps, reflection
probes and outdoor image into the map's zone; flat, sunlit and keep light it from the
base map's lightmaps instead (docs/convert.md 3.5, 10). Every conversion also adds the
minimap corners and a compass material and image of the map's own inside its zone
(docs/convert.md 10.4).
Gametypes: every one of the twelve is checked against the map's entities (spawns and
objectives, docs/convert.md 9.4) and reported in convert.json (objectives, with what
each lacks and the script lines that need it). Team Deathmatch and Free-for-all must be
ready or the conversion stops; --modes adds more that must be ready (--modes all: every
one). The text output lists the ready and the not-ready modes.
Materials and props: a material the base zone has is reused by name; any other is built
into the map's own zone with its textures (docs/convert.md 11). Their .iwi images are read
from --pc-game (repeatable; default: the PC game folder the map was built in, when the PC
map is its zone/<language>/ file). --new-material NAME builds a material anew even
when the base has one of that name. Static models (Radiant misc_model) place the base
zone's XModels by name; a prop the base lacks stops the conversion with its name
(docs/convert.md 12).
--name gives the map its own zone name (mp_NAME.ff, every internal occurrence renamed;
docs/research/map-registration.md lists them); --copy-pak writes mp_NAME.pak beside it.
--register is opt-in and writes a NEW copy of patch_mp.ff with one map-table row added: the
game offers maps only from that table, so a custom map cannot be started without it (see
docs/convert.md, the own-files rule, and docs/demo-box-named.md). The source patch_mp.ff is
only read.
Share a mod as the difference from a stock zone, without redistributing game files. A patch holds only the changed assets; applying it checks the user's copy is the right source and reproduces the edited zone.
opent5 patch create STOCK EDITED -o mod.o5patch
opent5 patch apply mod.o5patch STOCK -o OUT.ff [--no-verify]
opent5 patch info mod.o5patch
apply refuses a source whose hash does not match the patch (expected vs found).
A text edit makes a patch of a few hundred bytes. Format: docs/patch-format.md.
Search names and text (scripts, string tables, localised text, entity strings) across every configured zone at once, from a disk cache that makes repeat searches instant.
opent5 search TERM [--names] [--type T] [--kind K] [--regex] [--case]
[--zone GLOB] [--limit N] [--rebuild]
opent5 index build [--rebuild]
opent5 index status
The first search builds the cache (about a minute for 178 zones); later ones are instant. Details: docs/search-index.md.
Batch-replace a zone's textures from a folder of images named after the images they replace (naming rule and map-file format in docs/texture-pack.md).
opent5 texpack list ZONE [--template FILE] [--replaceable]
opent5 texpack apply ZONE PACKDIR -o OUTDIR [--map FILE] [--resize]
[--allow-shared] [--dry-run]
list dumps a zone's image names and can write a map template. apply replaces
matching images and writes OUTDIR/.ff (and .pak when streamed images
changed), verified; --dry-run previews the mapping. Shared-pak parts are left
alone unless --allow-shared. Per-file failures are reported; only a failed save
verification fails the command. All take --json.