Skip to content

Latest commit

 

History

History
389 lines (312 loc) · 17.7 KB

File metadata and controls

389 lines (312 loc) · 17.7 KB

Command line

.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).

info

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
}

list

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).

extract

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).

replace

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.

unpack, pack

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"}.

verify

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": []
}

rebuild

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
}

Signatures

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

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.

patch

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, index

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.

texpack

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.