Read Virtools behavior scripts out of a .cmo / .nmo / .vmo file — on any
OS, without the Virtools editor.
Virtools (the 3D engine behind many 2000s games, kiosks and web experiences) stores its gameplay logic as a graph of CKBehavior "building blocks" wired by links. Normally you need the Windows Virtools Dev editor to look at those scripts. This tool reconstructs the graph straight from the file bytes and prints it as readable text or Graphviz:
- the behavior graphs ("scripts") in the file,
- each behaviour's execution links (
A.Out -> B.In), - and decoded parameter values — floats, vectors, bools, strings, object references, and resolved message and attribute names.
SCRIPT 'controller' (#1287, 19 sub-behaviors)
--- nodes ---
#1290 [graph] intro Up=(0, 1, 0), Message=msg_intro_go, MIN=5.4, MAX=5.6
#1296 [BB] Wait Message Message=msg_ready (<-Message)
#1301 [BB] Send Message Message=msg_start, Dest=@Level
#1340 [BB] Has Attribute Attribute=player_ready
--- execution links ---
intro.Out 0 -> Send Message.In
Wait Message.Out -> Has Attribute.In
Existing tools that surface Virtools scripts (e.g. SuperScriptMaterializer, VirtoolsScriptDeobfuscation) run inside Virtools Dev on Windows. This one needs only the file and a terminal — useful for game preservation, modding, asset archaeology and reverse engineering on Linux/macOS.
It drives a small patched build of yyc12345's LibCmo
Unvirt (MIT). LibCmo already parses the
CMO container; the patch (libcmo-virtools-script-dump.patch) adds three things:
- CRC-tolerant loading — some files ship a tampered/zeroed header CRC that
makes stock
Unvirtreject them; the patch downgrades the CRC mismatch to a warning (the compressed body is intact). - A generic chunk dump (repurposing the
testcommand): every object'sCKStateChunkidentifiers as raw dwords, plus the manager chunks. - A
CKStateChunk::GetObjectList()accessor.
cmo_script_dump.py then decodes those dumps offline. Modern Virtools (4.0)
packs each behaviour/link/IO/parameter into a CK_STATESAVE_*_NEWDATA blob with
inline object references stored as file indices:
| element | layout |
|---|---|
| link | [delay, inIO_idx, outIO_idx] |
| IO flag | 1 = input, 2 = output (for link orientation) |
| behaviour | [flags, (typeGUID if leaf BB), scalars…, count-prefixed ref-lists] |
| param value | [guid, mode, …] — raw buffer / object-ref / manager index |
Message names come from the Message Manager chunk; attribute names from the Attribute Manager chunk. On the game this was developed against the reconstruction was validated globally: 24553/24555 IOs resolve to exactly one owning behaviour and 13114/13117 link endpoints resolve.
Note: the parameter-type GUIDs and the behaviour-blob heuristics were recovered from one Virtools 4.0 title. They cover the standard Virtools types, but exotic custom parameter types may show as raw hex. PRs with more GUIDs welcome.
You need the patched Unvirt binary, Python 3.10+, and script(1) (util-linux);
graphviz only if you want to render the dot output.
A patched Unvirt is built by CI and published here:
https://github.com/HeiseMo/libcmo21/releases/tag/unvirt-linux
curl -L -o unvirt.tgz \
https://github.com/HeiseMo/libcmo21/releases/download/unvirt-linux/unvirt-linux-x64.tar.gz
tar xzf unvirt.tgz
export UNVIRT="$PWD/unvirt-linux-x64/run-unvirt.sh" # wrapper finds bundled libs
export CMO_FILE=/path/to/your.cmo # or pass --cmo each timeThe patched source lives in the fork https://github.com/HeiseMo/libcmo21
(LibCmo v0.4.0 + the included libcmo-virtools-script-dump.patch). Needs
CMake ≥ 3.23 and a C++23 compiler; the exact dependency pins and build steps are
in .github/workflows/unvirt-release.yml there (YYCCommonplace v2.0.0, zlib
v1.3.1, stb). Then:
export UNVIRT=/path/to/your/build/Unvirtcmo_script_dump.py scripts # list the behavior-graph "scripts"
cmo_script_dump.py script <index|name> # render one: nodes + params + exec links
cmo_script_dump.py dot <index|name> | dot -Tsvg -o script.svg
cmo_script_dump.py json [index|name] # one script (or all) as JSON — diff/tooling
cmo_script_dump.py search <text> # any object by name
cmo_script_dump.py messages # Message Manager table (index -> name)
cmo_script_dump.py attributes [filter] # Attribute Manager table (index -> name)
cmo_script_dump.py histogram # CK class-id counts
cmo_script_dump.py chunk <index> # raw CKStateChunk for one objectThe first scripts/script call runs Unvirt once and caches the decoded dump
next to the script (.cache_<file>_*.tsv); later calls are instant. Use
--refresh-graph to rebuild.
Works on .cmo, .nmo and .vmo (Virtools 2.1+ / NeMo "Nemo Fi" containers);
tested across several files of different types and content. Exotic custom
parameter types that aren't recognised fall back to showing both int and float
interpretations (i=…/f=…) or raw hex rather than guessing.
MIT (see LICENSE). The included patch modifies
libcmo21 by yyc12345, also MIT.
This tool reads file formats; it ships no game assets. Respect the copyright of any files you open with it.