OpenPhotex reads the data formats of Terminal Reality games: Monster Truck Madness 1 and 2, CART Precision Racing, Terminal Velocity, Fury3, Hellbender, 4x4 Evolution 1 and 2, Fly! and Nocturne. It is written in TypeScript and runs in browsers and Node.js, and it ships a command-line tool, openphotex, for people, scripts and AI agents.
The aim is for OpenPhotex to be the one place where understood format behaviour lives. Viewers, converters and tools use it rather than carrying their own parsers, and new reverse-engineering findings are added here with tests. JSPod, JSTrackViewer, JSTruckViewer and JSMTM2Converter all read their game data through it.
Version 1.0. Every reader is checked against the retail archives of the game it belongs to, and every writer by reading its output back. The formats below are complete as listed; docs/ROADMAP.md records how the four apps moved onto the library.
Each format has one example on the command line, using a stock file name; run it from the folder holding the archive. read prints a short summary; add --json for the reader's full result (see Command line). An entry can be named by its path, by its file name when that is unique, or as #<index>.
| Format | Games | What OpenPhotex does | Example |
|---|---|---|---|
| POD1 | MTM1, MTM2, CPR, TV, Fury3, Hellbender | List, extract, write (library: writePod1) |
openphotex pod list TRUCK2.POD --filter '*.TRK' |
| POD2 | 4x4 Evolution 1 and 2, Nocturne | List, extract, check CRCs, read the edit history | openphotex pod verify PEAK.pod |
openphotex pod audit PEAK.pod --filter '*.act' |
|||
| EPD | Fly! | List, extract | openphotex pod info SC07.EPD |
| Any of them | Extract one entry or all of them | openphotex pod extract TRUCK2.POD TRUCK/BIGFOOT.TRK -o bigfoot.trk |
Details: docs/POD.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.RAW with .ACT |
MTM, CPR, TV, Fury3, Hellbender, Evo, Fly! | 8-bit texture drawn through its palette, to PNG | openphotex texture GAME.POD ART/01BAKGLS.RAW -o bakgls.png |
.RAW with a named palette |
Fly! charts, anything without a same-stem .ACT |
The same, with the palette chosen | openphotex texture SC07.EPD SC07N1.RAW --palette SC07N.ACT -o chart.png |
.OPA |
4x4 Evolution | Opacity plane merged into the texture's alpha | openphotex texture ASPEN.POD AS3PINE1.RAW --opa AS3PINE1.OPA -o pine.png |
.ACT |
All | 256-colour palette, 6- or 8-bit | openphotex read GAME.POD 01BAKGLS.ACT |
.RAW size |
All | The square side a texture of that size has | openphotex read GAME.POD 01BAKGLS.RAW |
.TIF |
4x4 Evolution 2 | Palette art and _BUMP normal maps, as RGBA |
openphotex read TRUCK.POD 31X10_08L.TIF |
texture writes only square .RAW art. A .TIF decodes through read, and Nocturne's 640x480 backdrops are not rendered by the command line yet. Palette choice (paletteCandidates) and the four stock METALCR2 and VGA palettes (bundledPalette) are library functions. Details: docs/RAW_ACT.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.BIN |
MTM1, MTM2, CPR, TV, Fury3, Hellbender, Fly! | MRGL model: vertices, faces, textures, materials, animated frame lists | openphotex read TRUCK2.POD BIGFOOT.BIN |
.SMF |
4x4 Evolution 1 and 2 | Model with every animation frame and its materials | openphotex read PEAK.pod CO1DROP.SMF |
.CMD |
CPR | High-detail car model, with its wing packages | openphotex read RACECAR.POD AARACE1.CMD |
.BSP |
Fly! | Structure such as a bridge, as one model | openphotex read NYMODELS.EPD NYBRIG01.BSP |
.DFM |
Nocturne | Skeletal character mesh with its LODs | openphotex read enemy.pod ARMOUR.DFM |
.KFM |
Nocturne | Key-frame/morph prop and cloth model, binary or text | openphotex read ACT1.POD A1-BOTTLE.KFM |
Details: docs/BIN.md, docs/EVO.md, docs/TRUCKS.md, docs/FLY.md, docs/NOCTURNE.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.TRK (truck) |
MTM1, MTM2, MTM2.1 | Truck manifest: models, wheels, scrape points, lights | openphotex read TRUCK2.POD TRUCK/BIGFOOT.TRK |
.TRK (vehicle) |
4x4 Evolution 1 and 2 | Vehicle manifest with specs and paint schemes | openphotex read TRUCK.POD AJWS.TRK |
.CAR |
CPR | Car manifest | openphotex read RACECAR.POD AARACE1.CAR |
Details: docs/TRUCKS.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.SIT, .SI2 |
MTM1, MTM2, CPR | Scene script: ramps, boxes, checkpoints, courses, trucks | openphotex read SUMMIT1.POD SUMMIT1.SIT |
.LVL |
MTM1, MTM2, CPR | Level file: terrain, colours, palette, textures, sky, sun | openphotex read SUMMIT1.POD SUMMIT1.LVL |
.TEX |
MTM, CPR, TV family | Terrain texture list | openphotex read SUMMIT1.POD SUMMIT1.TEX |
.TTY |
MTM, CPR, TV family | Terrain texture types | openphotex read SUMMIT1.POD SUMMIT1.TTY |
.TRK (track) |
CPR | Road layer: 20-point cross sections, walls, textures | openphotex read LAGUNA.POD LAGUNA.TRK |
.TTX |
CPR | Road texture list with surface types | openphotex read LAGUNA.POD LAGUNA.TTX |
Heightfields (8-bit, and CPR's 10.6 fixed point) and ground boxes (.RA0, .RA1, .CL0) are decoded by the library (decodeHeightSample, decodeGroundBoxes); a lone grid means nothing without its level, so read does not take them. Details: docs/LEVELS.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.KLP |
MTM2 | Loop points for the .WAV of the same name |
openphotex read SOUND.POD ACCEL3B.KLP |
SOUNDnnn.TXT |
MTM2 | A level's ambient sounds, checkpoint and lap sounds | openphotex read SOUND.POD SOUND002.TXT |
SUN.TXT |
MTM2 | Sun position and lens-flare layers | openphotex read STARTUP.POD SUN.TXT |
.LOC |
MTM2 | TRI Message System replacements (joke and Pig Latin wordings) | openphotex read UI.POD MTM2-PIG.LOC |
.TRN |
MTM1, MTM2 | Tournaments: name, description file, drag flag, events with laps; writeTrn writes them |
openphotex read STARTUP.POD DOUG.TRN |
.MOD |
MTM2 | ProTracker modules (parseMod, and renderMod plays one into stereo PCM) |
library only |
POWERBIG.200/.400/.480 |
MTM2 | Cockpit layout per screen height | openphotex read COCKPIT.POD POWERBIG.480 |
Details: docs/MTM2_FILES.md. The game's own simulation rules are in the library as mtm2Sim (docs/SIM_MTM2.md).
| Format | Games | Contents | Example |
|---|---|---|---|
.LVL |
TV, Fury3, Hellbender | Level header: every side file, sky, sun | openphotex read CDROM.POD ALIEN-T1.LVL |
.DEF |
TV, Fury3, Hellbender | Object definitions and placements | openphotex read CDROM.POD ALIEN-T1.DEF |
.NAV |
TV, Fury3 | Navigation points | openphotex read CDROM.POD ALIEN.NAV |
.NAV |
Hellbender | Navigation points, Hellbender's record | openphotex read GAME.POD FLOAT.NAV |
.PUP |
TV, Fury3, Hellbender | Powerup placements | openphotex read CDROM.POD ALIEN-T1.PUP |
.TDF |
TV, Fury3, Hellbender | Tunnel definitions | openphotex read CDROM.POD ALIEN-T1.TDF |
.ANI |
TV, Fury3 | Animated textures | openphotex read CDROM.POD ALIEN-T1.ANI |
.TXT briefing |
Hellbender | Mission briefing labels | openphotex read GAME.POD FLOAT.TXT --as hb-briefing |
Hellbender's cavern (.RA2, .RA3, .CL1) and the sky gradient are library functions (decodeHbUnderground, skyGradient). Details: docs/LEVELS.md.
| Format | Games | Contents | Example |
|---|---|---|---|
.SIT |
Evo 1 and 2 | Scene: placements, courses, vehicles | openphotex read PEAK.pod PEAK.SIT |
.LVL |
Evo 1 and 2 | Level settings, water, light | openphotex read PEAK.pod PEAK.LVL |
.WAT |
Evo 2 | Animated water material | openphotex read BAJBEACH.pod BAJBEACH.WAT |
.TEX |
Evo 1 and 2 | Terrain texture records | openphotex read PEAK.pod PEAK.TEX |
.VEG |
Evo 2 | Tree placements | openphotex read BAJBEACH.pod BAJBEACH.VEG |
AI line .TXT |
Evo 1 and 2 | A recorded lap for a computer driver (community tracks) | openphotex read TERRAMAR.pod 'AI\CLASS1\AI_11TERRAMAR.TXT' |
Details: docs/EVO.md.
| Format | Contents | Example |
|---|---|---|
.SCF |
A scenery set's manifest | openphotex read SANFRAN.SCF |
SCENERY.Sxx |
Object placements in a quadrant | openphotex read CHMODELS.EPD 'DATA\D192161\SCENERY.S10' |
.ALT |
Terrain heights | openphotex read CHICAGO1.EPD G00.ALT |
.TYP |
Terrain cell types | openphotex read CHICAGO1.EPD G00.TYP |
.TEX |
Terrain texture names | openphotex read CHICAGO1.EPD G00.TEX |
.REF |
Texture references, laid out by the .TYP |
openphotex read CHICAGO1.EPD G00.REF |
.AL2 |
Detail heights, laid out by the .TYP |
openphotex read CHICAGO1.EPD G00.AL2 |
A .REF or .AL2 is read from its archive, because its layout comes from the quadrant's .TYP. The globe tile grid and texture names are library functions (flyTileAt, parseFlyTextureName). Details: docs/FLY.md.
| Format | Contents | Example |
|---|---|---|
.SKL |
Skeleton and animation bank | openphotex read enemy.pod ARMOUR.SKL |
.CTH |
Cloth properties for a .KFM |
openphotex read enemy.pod BATWING.CTH |
.SET |
Scene manifest: fog, lights, cameras | openphotex read DUNGEON.POD DUNGEON.SET |
.GEO |
Collision geometry grid | openphotex read DUNGEON.POD DUNGEON.GEO |
.FOG |
Per-camera fog density | openphotex read DUNGEON.POD DUN03.FOG |
.THM |
Location thumbnails | openphotex read DUNGEON.POD DUNGEON.THM |
.ZTH |
Per-camera depth thumbnails | openphotex read DUNGEON.POD DUNGEON.ZTH |
.DFM and .KFM are under Models. Details: docs/NOCTURNE.md.
The writers are library functions; the command line does not write game files. Each output is checked by reading it back with OpenPhotex's own reader.
| Writes | Function |
|---|---|
| POD1 archives | writePod1, buildPod1Directory |
.BIN models |
writeBin |
.RAW with its own .ACT |
encodeRawTexture |
MTM2 .SIT, .LVL, .TEX, empty side files |
writeMtm2Sit, writeMtm2Lvl, writeTexList, writeEmptyList |
MTM2 .LTE light grid and ground-box grids |
buildMtm2Lte, emptyGroundBoxGrids |
MTM2 level palette and FOG\<track>.MAP |
mtm2LevelPalette, buildFogMap |
MTM2 2.1 truck .TRK |
writeMtm2Trk |
Details: docs/BIN.md, docs/RAW_ACT.md, docs/LEVELS.md, docs/TRUCKS.md.
import { parsePod, findPodEntry, readPodEntry } from "openphotex";
const bytes = new Uint8Array(await file.arrayBuffer()); // or fs.readFileSync(path)
const pod = parsePod(bytes);
pod.format; // "pod1" | "pod2" | "epd"
pod.comment; // header comment, e.g. "MTM2 Trucks"
pod.entries.length;
const entry = findPodEntry(pod, "truck/bigfoot.trk"); // case and / vs \ insensitive
const data = readPodEntry(bytes, entry); // a copy of the payloadThe core is environment-neutral. It takes a Uint8Array or ArrayBuffer and returns plain data. It never touches the DOM, OPFS, fetch or the file system, so reading files is left to the caller. A parsed archive holds no reference to the bytes, so it can be posted between workers or serialized as JSON.
| Export | Description |
|---|---|
parsePod(bytes, { byteLength? }) |
Parse the header and directory and return a PodArchive. Throws PodFormatError. |
podDirectoryEnd(prefix, byteLength) |
How many leading bytes parsePod needs, for reading only the directory of a large file (see below). |
readPodEntry(bytes, entry) |
A copy of an entry's payload. Pass { offset, length } to read part of one. |
findPodEntry(pod, path) |
Look up an entry by path, ignoring case and / vs \. |
findPodEntryByTitle(pod, name) |
The first entry whose file name matches. |
findPodEntriesByExtension(pod, ".RAW") |
Entries whose file name ends with the suffix. |
normalizePodPath, podPathTitle |
The path normalization that lookups use. |
buildPod1Directory(comment, entries) |
Encode a POD1 header and directory for { name, length, paletteName? } entries. The payloads follow it in the same order. Throws PodWriteError. |
writePod1(comment, entries) |
A complete POD1 from { name, data, paletteName? } entries. |
pod1DirectoryEntries(entries) |
A parsed archive's entries in the form the writer takes. Rebuilding a stock POD1 from it reproduces the original directory byte for byte. |
verifyPodChecksums(bytes, pod) |
POD2: compare the stored archive and entry CRCs with the data. crc32Mpeg2 is the checksum itself. |
readPod2AuditTrail(bytes, pod) |
POD2: the packer's edit history, one add, remove or change record per edit. |
decodeActPalette(bytes) |
An .ACT as 768 bytes of 8-bit RGB, scaling a 6-bit VGA table (see RAW_ACT.md). actPaletteDepth reports which reading applies. |
decodeRawTexture(raw, palette, { family?, cutout? }) |
A .RAW texture as { width, height, rgba, hasAlpha }. family is classic or evo. cutout applies the classic color key. |
rawTextureSide(byteLength, family?) |
The side of a square texture of this size, or 0. |
applyOpacityPlane(image, opa) |
Put a 4x4 Evolution .OPA into the alpha channel. |
parseEvoSit, parseEvoLvl, parseEvoWat, parseEvoTex, parseEvoVeg |
4x4 Evolution world files as plain objects; isEvoSit tells an Evo .SIT from an MTM one. |
parseSmf(bytes, name) |
An .SMF model in Evo's own axes (Y up) and the file's winding: groups with every animation frame as Float64Arrays, indexed faces, and verbatim material fields. smfTextureReference normalizes a texture field. |
parseEvoTrk(bytes, name) |
A v6/v7 vehicle manifest: wheel anchors, scrape points, lights, paint schemes, counted lists and every other field (specs, rawValues, fieldOrder). truckManifestLines prepares any truck manifest's lines. |
parseBin(bytes) |
A .BIN model as the file states it: raw vertex words, faces in file order with the texture, colour and material in force for each, decoded materials, or an animated model's frame names. MRGL, MRGLMAT, MRGLMAT2, BIN_GEOMETRY_DIVISOR and friends name the constants. |
parseTruckManifest(bytes) |
Any truck or car manifest, as { kind, manifest } with kind mtm, cpr-car or evo. detectTruckManifest, parseMtmTrkLines and parseCprCarLines are the parts. |
parseCprCmd(bytes, name) |
A CPR .CMD car model: parts with their raw fixed-point vertices, normals and faces. CMD_POSITION_SCALE, CMD_NORMAL_SCALE and CMD_UV_SCALE convert them; cmdWingPackages and cmdFaceTriangles hold the aero-package and quad-split rules. |
parseDfm, parseSkl, parseKfm, parseCth |
Nocturne character meshes, skeleton animations, morph models and cloth definitions. |
parseNocturneSet, parseNocturneGeo, parseNocturneFog |
Nocturne pre-rendered scene manifests, collision grids and per-camera fog data. |
parseNocturneThm, parseNocturneZth |
Nocturne location thumbnails and per-camera depth thumbnails. |
parseMtmSit, parseMtmLvl, parseTexList, parseTty |
MTM1, MTM2 and CPR scene scripts and level files; detectSitOrigin tells the three apart. |
parseKlp, parseMtmAmbientSounds, parseMtmSun, parseLoc, parseCockpitLayout, parseMod, renderMod |
MTM2 loop points, ambient sounds, sun and flare, message replacements and cockpit layouts. |
mtm2Sim |
The MTM2 simulation: terrain, surfaces, water, courses, checkpoints, truck parameters, controls and drivetrain (docs/SIM_MTM2.md). |
evoSim |
The 4x4 Evolution 1 and 2 vehicle model on the MTM2 world: rules, surface and tread tables, .TRK figures, driveline and step (docs/SIM_EVO.md). |
parseCprTrk, parseCprTtx |
CPR's road layer and its texture list, with the cross-section schema (CPR_POINT_NAMES, CPR_WALL_LAYERS, cprVisibleSlots, cprTrackIsClosed). |
parseTvLvl, parseDef, parseNavPoints, parseHbNavPoints, parsePowerups, parseTunnelDefs, parseAnimations, parseHbBriefing |
Terminal Velocity, Fury3 and Hellbender level files; placementToEditor converts their coordinates. |
decodeHeightSample, decodeGroundBoxes, decodeHbUnderground, skyGradient |
Heightfields, ground boxes, Hellbender's cavern and the sky gradient. |
parseEvoAiLine, lapRuns, evoHeightAt |
4x4 Evolution AI lines, the course runs that make a lap, and terrain sampling. |
paletteCandidates, bundledPalette |
Which .ACT a texture should use, ranked, and the four stock METALCR2 and VGA palettes. |
parseFlyScf, parseFlySceneryObjects, parseFlyQuadrant, parseFlyBsp, flyTileAt |
Fly! scenery manifests, object placements, terrain quadrants, structures and the globe tile grid. |
writeBin, encodeRawTexture, writeMtm2Sit, writeMtm2Lvl, writeMtm2Trk, buildMtm2Lte, mtm2LevelPalette, buildFogMap |
The writers listed under Writers. |
decodeTiff(bytes, name) |
An Evo 2 .TIF as RGBA, with kind palette (diffuse art) or rgb (_BUMP normal maps). |
PodFormatError |
Has .code (TOO_SMALL, BAD_ENTRY_COUNT, DIRECTORY_OUT_OF_BOUNDS, BAD_ENTRY_NAME, ENTRY_OUT_OF_BOUNDS) and .entryIndex. |
PodWriteError |
Has .code (BAD_ENTRY_COUNT, BAD_NAME, NAME_TOO_LONG, DUPLICATE_NAME, BAD_PALETTE, BAD_TEXT, TOO_LARGE) and .entryIndex. |
PodArchive |
format, comment (EPD: its title, the archive's own stem), entries, byteLength, directoryOffset, directoryEnd, checksum, auditCount. The last two are POD2 only and null otherwise. |
PodEntry |
index, name (as stored), normalizedName, title, length, offset, recordOffset, paletteName (POD1 .RAW only), timestamp (POD2 and EPD), crc (POD2 only). |
To index a large File or Blob without loading it:
let prefix = new Uint8Array(0);
for (;;) {
const need = podDirectoryEnd(prefix, file.size);
if (need <= prefix.length) break;
prefix = new Uint8Array(await file.slice(0, need).arrayBuffer());
}
const pod = parsePod(prefix, { byteLength: file.size });
// then read payloads with file.slice(entry.offset, entry.offset + entry.length)The build is plain ES modules with relative imports. Projects served as static files, such as JSTrackViewer, vendor the core:
npm run build
npm run vendor -- ../JSTrackViewer/src/vendor/openphotexThis copies dist/ without the CLI, adds the license, and writes a VERSION file naming the version and commit. The copy is generated output: change OpenPhotex and re-vendor it rather than editing the copy.
npm install && npm run build && npm link # puts `openphotex` on PATHopenphotex pod info GAME.POD
openphotex pod list GAME.POD
openphotex pod list GAME.POD --filter '*.RAW' # file-name pattern
openphotex pod list GAME.POD --filter 'art/*' --json # path pattern
openphotex pod extract GAME.POD TRUCK/BIGFOOT.TRK # writes ./BIGFOOT.TRK
openphotex pod extract GAME.POD BIGFOOT.TRK -o out.trk # a file name works if unique
openphotex pod extract GAME.POD '#12' --stdout | xxd | head
openphotex pod extract GAME.POD --all -o extracted/
openphotex pod verify TRACK.POD # POD2: check the stored CRCs
openphotex pod audit TRACK.POD --filter '*.act' # POD2: who changed what, and when
openphotex texture GAME.POD ART/GRASS.RAW -o grass.png # render a texture to PNG
openphotex texture TRACK.POD ART/PINE.RAW --opa ART/PINE.OPA -o pine.png
openphotex texture LOOSE.RAW --act LOOSE.ACT -o loose.png
openphotex read LAGUNA.POD LAGUNA.TRK # what a file says, through its reader
openphotex read GAME.POD FLOAT.NAV --json # the reader's result as JSON
openphotex read TRUCK2.POD BIGFOOT.BIN --json --full # typed arrays in full
openphotex read LOOSE.LVL --as tv-lvl # a loose file, reader namedread picks the reader from the file's extension, then its content, then the archive it came from: .SIT is 4x4 Evolution or MTM by content, a CPR road .TRK by its CRaceTrack.trackCount label, a Hellbender .NAV by its !priority,time sections, and a .LVL by the archive (POD2 is Evo; a POD1 carrying a .SIT or .SI2 is MTM, any other is the Terminal Velocity family). A .TEX inside an EPD is a Fly! texture list. A Fly! .REF or .AL2 is laid out by its quadrant's .TYP, so it is read from its archive, never loose. A loose .LVL has no archive to ask, so give --as. openphotex --help lists every format id. Files with no standalone reader (grids such as .CLR and .RA0, sound, video) fail with UNSUPPORTED_FORMAT.
texture draws with, in order:
--palette <entry>or--act <file>, when given;- the same-stem
.ACTin the archive; - the POD1 palette record.
With none of these it fails with PALETTE_REQUIRED rather than guessing. Its JSON output reports which palette it used and why. The texture's size rules come from the POD (POD2 means evo) unless --family says otherwise. --cutout applies the classic color key.
An <entry> can be given three ways:
- an archive path, in any case, with
/or\; - a file name that matches exactly one entry;
#<index>.
Output files are never overwritten unless --force is given. pod list --raw adds each directory record's bytes as hex.
--json prints one JSON document on stdout and nothing else. The document has these properties:
- Every key is always present, with
nullwhen a value does not apply, and keys come in a fixed order. - Entries come in directory order.
schemaVersionchanges only on an incompatible change, never for an added field.
The pod list shape:
{
"schemaVersion": 1,
"command": "pod list",
"file": "TRUCK2.POD",
"filter": null,
"archive": {
"format": "pod1", "comment": "MTM2 Trucks", "byteLength": 5515571, "entryCount": 439,
"directoryOffset": 84, "directoryEnd": 17644, "checksum": null, "auditCount": null
},
"entries": [
{
"index": 420, "name": "TRUCK\\BIGFOOT.TRK", "normalizedName": "TRUCK/BIGFOOT.TRK",
"title": "BIGFOOT.TRK", "offset": 5420955, "length": 6156, "recordOffset": 16884,
"paletteName": null, "timestamp": null, "crc": null
}
]
}pod info: printsschemaVersion,command,fileandarchive.pod extract: printsextracted: [{ index, name, normalizedName, length, output }].pod verify: printsformat,ok,archive: { stored, computed, ok },entriesCheckedandmismatches: [{ index, name, stored, computed }]. The command exits with 5 whenokis false.pod audit: printsfilter,auditCountandrecords: [{ index, offset, user, timestamp, actionCode, action, path, oldTimestamp, oldSize, newTimestamp, newSize }].read: printsfile,entry(null for a loose file),format,reader(the library function used) anddata, which is that function's return value: its shape is the function's documented type. Typed arrays appear as{ "typedArray": "Int32Array", "length": n }unless--fullis given.texture: printsfile,entry,family,width,height,hasAlpha,palette: { source, name, depth },opacityandoutput. The palettesourceis one ofact-file,palette-entry,same-stemorpalette-record.
Errors go to stderr. With --json an error is also a JSON document, and stdout stays empty:
{ "schemaVersion": 1, "error": { "code": "ENTRY_NOT_FOUND", "message": "No entry 'X' in the archive.", "selector": "X" } }| Exit | Meaning | error.code |
|---|---|---|
| 0 | Success | |
| 1 | Bad usage | USAGE |
| 2 | File or I/O problem | FILE_NOT_FOUND, IO_ERROR, OUTPUT_EXISTS |
| 3 | Not a readable POD, or the wrong kind of data for the command | TOO_SMALL, BAD_ENTRY_COUNT, DIRECTORY_OUT_OF_BOUNDS, BAD_ENTRY_NAME, ENTRY_OUT_OF_BOUNDS (with entryIndex), UNSUPPORTED_FORMAT |
| 4 | Entry or palette not found, or ambiguous | ENTRY_NOT_FOUND, AMBIGUOUS_ENTRY (with candidates), PALETTE_REQUIRED (with paletteName) |
| 5 | A check failed | CHECKSUM_MISMATCH |
OpenPhotex is meant to replace one-off parsers written in the middle of an investigation. Agents get their operating instructions from the openphotex skill, which is kept in the user's personal skills folder (~/.claude/skills/openphotex/SKILL.md, symlinked for Codex) so that it applies in every repository. In short:
- Reading a game file: run
openphotex read <archive> <entry> --json, orpodandtexture, never a parser written for the occasion. That includes disposable Python scripts; Python is for analysing OpenPhotex's JSON, not for decoding the bytes. - Bulk work: import the library from a Node script rather than running the command line thousands of times.
- Unknown bytes: exploring a format or field OpenPhotex does not read yet is fine, with the bytes taken from
openphotex pod extract --stdout. - Something missing: when OpenPhotex lacks information an investigation needs, extend it here with tests.
AGENTS.md covers working on OpenPhotex itself.
Requires Node.js 22.18 or newer, because the tests run TypeScript directly through Node's type stripping.
npm install
npm run build # dist/: the core and dist/cli/
npm test # builds, then runs node --test test/
npm run typecheck # core, CLI and testssrc/ core library: no DOM, no Node; compiled against the bare ES2022 lib
pod/ POD1, POD2 and EPD parsing, POD1 writing, lookup, path rules, POD2 integrity data
texture/ .RAW, .ACT, .OPA and .TIF decoding, palette ranking, bundled palettes, encoding
model/ .BIN reading and writing
truck/ truck and car manifests
mtm/ MTM and CPR .SIT and .LVL, MTM2 writers, level palette
cpr/ CPR .CMD models and the .TRK road layer
tv/ Terminal Velocity, Fury3 and Hellbender level files and coordinates
terrain/ heightfields, ground boxes, Hellbender's cavern
evo/ 4x4 Evolution world files, models, manifests, AI lines
fly/ Fly! scenery, terrain quadrants, globe grid, structures
nocturne/ Nocturne models, animation, cloth and scene files
cli/ the openphotex command (Node only); imports the core as "openphotex"
test/ node:test suites; fixtures/ builds synthetic PODs byte by byte
docs/ format documentation
scripts/ vendor.mjs
The tests use synthetic archives built in test/fixtures/build.ts, and no game data is committed. test/stock.test.ts also checks retail archives when they are present in ~/games/<game>/, or in the folder named by OPENPHOTEX_GAMES, and skips itself otherwise.
The core is kept portable in two ways. tsconfig.json gives src/ no DOM or Node types, and test/portability.test.ts fails if src/ uses Node or browser APIs or third-party imports. The core has no runtime dependencies.
When adding format knowledge:
- Write a fixture test that states the behaviour.
- Implement it.
- Record the evidence and any remaining uncertainty in
docs/.
Preserve behaviour that real archives depend on, and change it only with evidence.
Apache 2.0. See LICENSE.