Skip to content

Engine architecture ​

Prerequisite: How the game works.

This doc is about this project — how the TypeScript code is organised — as opposed to the game's file formats (those get their own docs). If you want to find where something lives in the source, start here.

The packages, and which way they point ​

The repository is an npm workspace of packages, and the arrangement is one rule: nothing shared knows which game it is.

PackageWhat it isImports
engine/the DreamFactory engine — containers, interpreter, runtime, the browser layer around themnothing
chrome/what every page shares, the front door, the editors and the games alike: the top bar and its menus, the version, the registry of games, editions, the bug report, the UI-language axisengine
speedrun/the speedrun workbench any game can carry: the sheet language, the run loop, the drivers, the aim sweep, the panel and the verbs that name no gameengine
site/the project's own web presence: the front door and the format editorsengine, chrome
taoot/Titanic: four pages, six editions and the demo, its own tools, the suites that play it to the endengine, chrome, speedrun
dust/Dust: three pages, one disc, its own tools and suitesengine, chrome, speedrun
timelapse/Timelapse: one page and four discs — its own palette, its own title card, and a boot log in a panel the page opens over the pictureengine, chrome
skullcracker/Skull Cracker: the game on one page, walk.html beside it as the bench a level is opened on, and its own disassembler for SC.EXEengine, chrome
redjack/RedJack: one page on three discs, DreamFactory 5, played to its end by its machine suitesengine, chrome
lunicus/Lunicus: one page, DreamFactory 0; the game itself ported from LUNICUS.EXE into src/game/engine, chrome
jumpraven/Jump Raven: the same kind of package, ported from RAVEN.EXE, with a player that wins itengine, chrome

The architecture map draws the same packages from the code, and goes down from them to the folders, the files and the classes.

site/tests/layering.ts enforces this: it fails the build if engine/, chrome/ or speedrun/ reaches for a game, or if any game reaches for another.

Everything below ## Where a game's own code lives is a game shell. Everything above it is the engine, where most of the code is.

Two layers inside the engine: "read the files" vs "run the game" ​

engine/src/ splits in two, and the two halves have very different sources (a port of DFET versus behaviour recovered from the game). A third directory, engine/src/web/, puts the result on a canvas.

engine/src/df/ — the format library ("how to read the files") ​

This is a faithful TypeScript port of the decoding logic in DFET. Its only job is: given the raw bytes of a game file, produce plain data structures — a list of scenes, a decoded image, a palette, a chunk of audio samples. It knows nothing about how the game plays. Every file in here corresponds to a format doc:

FileReadsDoc
container.tsthe shared container skeletonDFile container
binary.tslow-level byte reading (endianness, strings)DFile container
version.tswhich DreamFactory wrote a file — 1 (Dust) or 4 (Titanic). Both engines put the tag as an i32 at container 0 + 0x02, the one field that never moved, so the version is asked rather than guessedDFile container
image.tscompressed frames, palettes, depth mapsImage codec
set.tsSET rooms/scenes/viewsSET
set-patch.tsthe SET write path — the copy-on-write patches the set editor makes, kept out of the reader the runtime loadsSET
set-v1.tsa DreamFactory 1 SET. Its own reader, not a branch: a v1 set has no turn rings and no roads but a grid of cells and one flat transition table in which a turn and a walk are the same recordSET
set-v1-to-v4.tsthat grid rearranged into the SetFile the viewer already knows, so there is one viewer rather than twoSET
set-any.tsopen a SET without knowing which engine wrote it — a tagged union, so the compiler asks which model you are holdingSET
shp.tsSHP propsSHP
mov.tsMOV movies — the segment chain, and the patches the movie editor writesMOV
mov-v1.tsa DreamFactory 1 movie. The chain-of-segments model survives; almost every mechanic under it differs, and more of it is behaviour than layout — advance is an authored goto, a frame can block until the sound it started finishes, hotspots are typed records, two palette indices are transparent. The format where the two engines diverge most. The record layout is DF.EXE's own, read out of its movie loopMOV
mov-pace.tshow fast a movie plays, in one place so the player and the editor cannot disagreeMOV
mov-sound.tsa segment's bed: which chunks, how much of the authored loop order, and whether it repeats — beside the pacing, and shared for the same reasonMOV
stg.tsSTG stage/UISTG
cst.tsCST casts (actor sprite sets)PUP / CST
pup.tsPUP puppets (conversation close-ups)PUP / CST
audio.tsTRK/SFX/11K audio banksAudio
banks.tsshared chunk-directory walk for TRK/MOV banksAudio
snd.ts.SND — the same role a .TRK plays, as DreamFactory 1 spells it. Same audio containers, no chunk tables: the names are inline in container 0 and the theme is the trailing run of consecutively numbered chunksDust's music & sound
script.tsthe compiled-script binaryScript container
script-asm.tsthe way back: source text → the tokens encodeScript writesScript container
opcodes.tsthe opcode-ID → name table the script decoder and the disassembly tools shareScript container
savegame.ts + savegame-read.ts, savegame-patch.ts, savegame-layout.ts.ti saved games: the container file and the record types; the decoders; the patch-writer; the record offsets and strides both read bySavegame
savegame-v1.ts.rtd saved games — the same container file three years earlier, so this module is only the delta: the offsets inside containers 0 and 1, and four record strides that movedSavegame, DF1
save-vars.tsthe variable list and string pool, the one part of a save that did not change between the two engines, so both readers share it rather than each carrying a copySavegame
text.tsthe character set the text bytes are in — which no DF file declares, so it comes from the language treeLanguages
build.ts + *-build.tsthe write half: a container accumulator and the field writers, plus one builder per format (set, shp, stg, mov, pup, cst, banks)Writing one back

engine/src/runtime/ — the runtime ("how the game behaves") ​

This part has no counterpart in DFET: it is the actual game engine. Its behaviour is reconstructed from observing the real game and from disassembling TI.EXE. Key files:

FileResponsibility
parser.tsturns a decoded script's tokens into a syntax tree
ast.tsthe syntax-tree node types the parser emits and the interpreter walks
interp.tsthe interpreter — runs the scripts; owns the builtin registry
builtins/the engine commands, grouped by family — core (pure language helpers), dispatch (the sendto* special forms), scene, props, audio, timing, actors, puppets, pointer, helpers, savegame — with context.ts as the shared plumbing and index.ts registering everything against a session (duplicates throw). The full command inventory is the builtin reference
setscripts.tsbinds one SET's scripts to the interpreter and routes the event chain
props.tsthe prop runtime — visibility, animation, compositing
actors.tsthe actor (CST) runtime — walking characters and their poses
geometry.tsshared 3D math — projection, depth, occlusion, compass bearing
audio.tsplayback channels (sound / voice / theme) and the sound library
session.tsGameSession — ties everything together and owns cross-set state
clock.tsthe fine time base behind delay(n) waits
scheduler.tsthe heartbeat — loops (makeloop), crickets, walks, looping sounds
puppet.tsPuppetController — PUP conversation close-ups
stage.tsStageController — the STG UI band / full-screen screens
saveload.tssaving/loading .ti games at the session level (what goes in, how a load restores)
saveload-v1.tsthe same two halves for Dust's .rtd: the same choreography, and the two ends that genuinely differ — a .rtd gives a world position where a .ti names a scene and a view
masks.tswhich globals are a counter rather than the story, in one place because three things ask: both playthrough hosts, and the debug panel's "what just moved" list
signature.tsa running hash of everything the next frame would be drawn from, so a composite can be skipped when the picture has not changed — hashing the inputs rather than counting mutations, because a revision counter can be forgotten at a write site
point.tsthe packed-point format (x<<16)|y scripts pass coordinates in
input.tsthe event queue — input made while the engine was mid-gesture, and what flushevents() discards (recovered from the binary)
bootplan.tswhat a game's boot needs, read out of its own BOOTFILE — the resource list, the landing room and the disc volumes, rather than TAOOT filenames hardcoded in the host (the boot plan)
rng.tsthe seedable source behind the session's two streams — script random() and the engine's own ambient draws — which is what makes a run reproducible (why two)
trace.tsthe state snapshot a playthrough asserts at each story beat

The Scripting doc covers the interpreter and builtins in detail; the rest of this doc is about how the pieces run together.

engine/src/web/ — the browser layer ("put it on a screen") ​

The third directory of the engine package, and the part that is neither format knowledge nor recovered engine behaviour: it puts a session on a canvas. It is still game-agnostic — it may not mention Titanic or Dust — but it may mention document, and it is where a game shell attaches.

FileResponsibility
host.tsGameHost — what it means to run the game (set activation, prefetch, cold boot, resuming a save) with no reference to document; a shell passes its side in as a file source, five UI notifications and an AudioSink. See the browser host
viewer.tsSetViewer — the navigation state machine over a parsed SET (turn/walk/teleport, the geometry of its own hit-testing), and one optional RoomLayer of the screen. Rendering and the click priority chain are in the row below
screen-director.tsScreenDirector — who owns the screen, and who gets a click: the five-way arbitration between a movie, a conversation, a fade, the world and a held frame, the flat/room compositor, the CLUT, the per-frame service of the whole session, and the input priority chain (hittest, clicks, keys, the cursor). Held by the host, and it works with no room at all — which is what lets Timelapse, a game with no .SET on any of its four discs, draw and play its films
screen-presenter.tsScreenPresenter — the single persistent framebuffer every render path composites into, the fade overlays, and the signature check that skips a composite when the picture has not changed. Held by the host, so it outlives the viewer a set change replaces
screen.tsthe screen contract: how big the framebuffer is, and where a SET view sits inside it. 512×384 is the DF4 default (Titanic, Dust) rather than the law — Timelapse says 640×480 in every one of its stage headers
screen-gamma.tsthe per-channel power curve TI.EXE applies to every palette entry before it reaches the screen (pow(c/255, 0.65) * 255) — the reason a faithful port looks brighter than a verbatim one. DF.EXE has its own curve, exponent 0.75 and truncated rather than rounded, which Dust selects at boot
ring-cache.tsthe LRU of decoded turn/walk rings, on a byte budget — the viewer's memory story, on its own
movie-player.tsMoviePlayer — modal MOV playback: the segment chain, cutscenes, interactive close-ups, movie chains/calls, cues and the soundtrack
puppet-view.tsPuppetView — draws conversation close-ups (layer compositing, subtitles, choice bevels); the conversation logic is runtime/puppet.ts
fonts.tsthe canvas font stacks and wrapText — including breaking a line that has no spaces in it
cursors.tscursor(name) as something style.cursor will take: the 32×32 monochrome CURS.* art a DreamFactory build keeps in its own executable, resampled (nearest neighbour, never blended) to the size the picture is being shown at — a whole-number zoom is exact pixel doubling and a fractional one an even mix of one- and two-pixel rows, capped at the 128×128 past which browsers ignore a cursor image outright. All three games have their own set, extracted from their own build by tools/dumpcursors.ts — 15 for Timelapse, 11 for Titanic, 9 for Dust — because the sets differ: Timelapse navigates BY cursor (11,031 of its 13,200 cursor(...) calls are the two step arrows, and it redrew both), Dust's v1 build has no step arrows at all, and the two disagree about the pointing hand
photos-idb.tsthe IndexedDB store behind Timelapse's photo album (plugin("camera", …)) — the one thing this engine produces that the player made, and the one the original kept outside a saved game. Everything in it is allowed to fail: a blocked or full store costs persistence and nothing else
keys.tswhether a keypress belongs to whatever has focus or to the game. The page listens on window, so without this, typing mission into a filter box would toggle the minimap on the M and send every letter into the running game
save-browser.ts / save-store.tsthe saved-games UI and its IndexedDB "file system" — parameterised on a SaveKind, so Titanic's .ti and Dust's .rtd get a database each off one implementation
window-bar.tsa game window's menu bar, on the game's own frame: inside the element that holds the canvas, as wide as the picture and scaled with it, and in fullscreen over its top edge until the pointer reaches it. The classic grey Windows look, whatever the page's theme, because it is the game's window and not the page. The menus are data in a Win32 resource's own spelling (&Open...\tCtrl+O), which tools/rtmenu.ts reads out of an executable; Alt, F10, the arrows and the Ctrl accelerators work as they do on Windows, and while a menu is open the game hears no key. Lunicus has it (lunicus/src/menu.ts)
window-dialog.tsa game window's dialog box, the menu bar's companion on the same frame: a Win32 dialog template, which tools/rtdialog.ts reads out of an executable, drawn with every control where the template put it, in dialog units scaled with the picture. It is modal the way DialogBox was: a layer over the frame takes the pointer, and windowDialogOpen() tells the page to give the game no key. A button is a command to the page's procedure, Enter the default button, Esc IDCANCEL. Lunicus has it (lunicus/src/dialogs.ts)
debug-panel.ts / state-list.tsthe state pane behind X — a DreamFactory game's plot lives entirely in script globals, so a snapshot of the globals table IS the game state, and this renders the same snapshot the playthrough goldens hold. A list that patches rather than redraws (it polls; the engine has no "a global changed" event), filtered by masks.ts. The SPINE — the handful of variables shown above the rest — is the game's to name: Titanic's six come from a readout the original carries, Dust's two are what its own suites set
load-clock.tsthe load remover: how long the page has spent waiting on the NETWORK, so a run's clock can decline to count it. Fed each fetch by whichever store a game has (HostFiles.onWire), and only the ones somebody was waiting for
cache-warmup.tspulls a game's files through the browser cache before a timed run, so the fiftieth run of a leg costs what the second one did. WHICH files is the game's answer, not this module's

Where a game's own code lives ​

The packages above the engine. None of them adds engine behaviour; they say which disc, which pages, and what the page around the canvas looks like. skullcracker/, redjack/, lunicus/ and jumpraven/ are laid out in their own sections of these docs.

site/ — the shared web presence ​

FileResponsibility
editors/the eight format editors and the page that lists them — see the browser editors
front.ts / games.tsthe front door, and the registry of which games exist and what it takes to read one's data — here rather than in a game, because the editors need it and a shared package must not depend on a consumer
chrome.cssthe topbar, pickers, titles, controls and panels every page shares. Not one colour in it: every value is a role, and the four palettes — Titanic's abyss-and-brass, Dust's dusk-and-ember, Timelapse's glass-and-chrome, the project's black-and-green — are four implementations of that one contract
editions.tsthe edition axis: which gamefiles/ tree a page is showing. Remembered per game and carried across the play page, the editors and the collection
ui-languages.ts / lang-menu.ts / locales/the UI-language axis, which is a different question: what language the words on the page are in. Pages carry their English inline, so nothing is fetched for an English reader
site.ts / version.ts / bug-report.tswhere the site's root is from a page at any depth, the version in the top bar, and the Report-bug button that prefills a GitHub issue with the game, the build number and the screen

speedrun/ — the speedrun workbench ​

A page feature built on the engine rather than part of it, so a package of its own, which any game can carry. The speedrun workbench, whole: the sheet grammar, the run loop, both drivers (Playwright's contract and the in-page one), the aim sweep, the panel's markup and stylesheet, and the verbs that name no game. A game supplies its own verbs and five facts about itself (workbench.ts, Workbench); Titanic and Dust each get a /speedrun/ page out of it. Which table a verb belongs in is argued in action.ts.

The workbench page is not a file in any game. speedrun/vite.ts makes it out of the game's own play page, in dev on every request and in a build as the speedrun entry: it moves the nodes that are the game into a row beside the panel, drops what only a player needs (the settings strip, the title card), opens the Details column and adds the workbench's own controls. So a game that wants a /speedrun/ page brings four things:

FileWhat it says
<game>/speedrun/workbench.tswhich of the play page's nodes are the game, which it has no use for, and the <meta> switches its main.ts reads
<game>/src/speedrun/page.tsone startWorkbench call: the game's key space, its verbs, and what it can warm or calculate
<game>/src/speedrun/actions.tsits own verbs, over the ones that name no game
<game>/src/speedrun.cssthe panel's stylesheet, imported, plus anything its own chrome has to be told

and one line in its vite.config.ts: ...speedrunPage({ root: HERE, ...WORKBENCH }).

taoot/ — Titanic's shell ​

Four pages: the front page, /play/, /collection/, and the unlisted /speedrun/ workbench.

FileResponsibility
main.tsthe play page: audio unlock, DOM wiring, the session and its host hooks, the automatic cold boot, input handlers, the rAF loop
files.tsFileStore — six editions, two CDs, per-disc basename collisions and an LRU for the 37 MB cutscenes
languages.ts / lang-chooser.tsthe table (codes, endonyms, code pages) and the authored lang.stg chooser — a real DreamFactory stage, not a dialog
collection.ts / booklet.ts / home.tsthe pages that are about the game rather than the game: the turnable box, the 32-page booklet, and the front page's top bar
save-seed.tsthe one-time seeding of the shipped .ti saves into the store
nightdive.tsthe intro film before the boot — a real MOV built by mknightdive.ts and played by the engine's own MoviePlayer, buttons and all
state-spine.ts / log-buffer.tsthe six variables the game's own debug readout names (a SCRIPT in TI.EXE, on the save panel's HELP button — so they are its author's choice, not this port's), and the bounded log a bug report carries the tail of. The pane that shows them is the engine's
speedrun/ + speedrun/page.tswhat is Titanic's about the workbench: this ship's own verbs (the coal lever, the wireless, the map's jumps, the smokestack, the inventory band) and the navigation its routes are written against. The workbench itself is the speedrun workspace; this names the disc. Deployed but unlisted — konami.ts is the door

dust/ — Dust's shell ​

Three pages: the game, /collection/, and the unlisted /speedrun/ workbench. Short on purpose — one volume, one edition, one copy of every name.

FileResponsibility
main.tsthe page: the boot, the films, the town, and the controls
files.tsthe CD as a HostFiles. Two things it has to get right: the BOOTFILE is at INSTALL/ALT31/BOOTFILE rather than in DATA/, and the boot's films need a room to draw through even though the boot opens none
speedrun/actions.ts + speedrun/page.tsthe workbench, for this disc: Dust's own verbs (goto, give, takeInHand, offer, doorAt, meet, talkOut, loadSave, blackjack) over the engine's core ones and the menu verbs. A verb is added when a route needs it
game.cssthe stage, the Start screen, the boot gauge and the log — a sheet of its own rather than the page's <style>, because the workbench boots the same main.ts and needs the same elements styled
saves.tsthe store's Dust dimension (.rtd, its own database) and the seeding of the saves that ship beside the disc, each offered once so a save added to the tree later still arrives. The earliest of them — found by frame, not by name — is also the base a fresh save is patched into, because a save is a serialized heap and cannot be written from nothing

timelapse/ — Timelapse's shell ​

One page, four discs. The shortest of the shells here, and the one whose cursors carry navigation, because this is the game that navigates by them.

FileResponsibility
main.tsthe page: the loader and its Enter button, the 640×480 plate blitted into a 1280×960 canvas, the input (mouse, keys, gestures), the position readout, and the boot log b opens
files.tsfour CDs as one flat basename index, every disc mounted at once — plus the fourteen installed files the manifest does not list, because the walker skips any directory called install
theme.cssthe palette, sampled from the title card: void, nacre, glass, chrome, and one warm stair for the one control that commits
cursor-art.tsthe fifteen CURS.* cursors out of tl.exe, written by tools/dumpcursors.ts — two 1bpp planes each, not PNGs

The GameSession: the thing that persists ​

GameSession (session.ts) is the top of the runtime. There is one interpreter whose variables live for the whole play session, so your inventory and progress survive when you walk from one set to another. The session owns:

  • the interpreter and its global variables,
  • the currently open SET (via a per-set binding, SetScripts),
  • the audio banks and playback channels,
  • the props runtime and the open "shop" (SHP) files,
  • the stage layer (STG) — the UI band and any full-screen screen.

When you travel to a new set, the set is swapped out but the session stays. That's the key architectural fact: sets are disposable, the session is not.

The cohesive sub-runtimes live in their own files — Clock, Scheduler, PuppetController, StageController — which the session composes (this.scheduler, this.stageCtrl, …).

The session does not forward to them. A caller addresses the subsystem it means — session.scheduler.makeLoop(...), session.stageCtrl.gotoFlat(...) — because a forwarder is a second name for one thing, and two names can drift apart. What stays on the session is what belongs to it: the interpreter, the cross-set state, and the fields more than one subsystem reads.

The render picture: layers on a 512×384 screen ​

The screen is 512×384. It is drawn back-to-front:

┌────────────────────────────────┐  y = 0
│                                │
│   the current SET view         │   ← 512 × 264, only when "set visible"
│   (a pre-rendered background)   │
│                                │
├────────────────────────────────┤  y = 264
│   UI band: menu, held item,     │   ← STG flat image + house.shp props
│   watch  (STG + SHP props)      │
└────────────────────────────────┘  y = 384
  1. A stage flat image (from an STG file) is the bottom layer / background.
  2. The current SET view is composited into the top 512×264 (when the set is visible — full-screen screens like the map hide it).
  3. Props (SHP) are drawn on top, ordered by depth so nearer things cover farther ones. In-world props are placed using the 3D projection recovered from TI.EXE; UI-band props sit at fixed screen positions.

How one mouse click flows through the system ​

The same "event travels down a chain" idea appears throughout the engine.

The diagram is the shape of the thing; the routing is the game's own, not the engine's. BOOTFILE 0001's mousedown is a hittest and a switch on result() into the six sendto* paths, and where a title ships that handler it decides where a click goes — the port's transcription is the fallback for one that doesn't (see the click priority chain).

The chain for a pointer event over a hotspot is object → scene → set main → stage, and off the end of it the event keeps climbing the containment chain — the file that holds the thing. Each level either handles the event (with an exitcode) or passes it on (passcode, or simply having no handler). The full event model is in the Scripting doc.

A keyboard event is dispatched differently, in two ways that matter.

It starts at the boot, not at the scene: TAOOT's boot holds a keydown that is a router — it maps the player's own movement keys (keynorth/keywest/keyeast, W/A/D by default and rebindable from the control panel) onto the arrows and then re-routes with sendtoscene(currentscene(), keydown(arg)). Everything else the press reaches, it reaches along that re-route, which is what carries the mapped value: scene → set main → stage → the boot library's own keydown, the default that turns "leftarrow" into currentscene("left"). Dispatching the boot's two containers side by side instead would hand the default the unmapped key, so the arrows would work and the W/A/D bindings would do nothing (#14).

That mapping is a script, so everything above it is key-blind — and the event queue is above it. TI.EXE posts the record in its window proc and pops it in the main loop, both of them before any script has said what the key means, so a press made mid-move waits its turn whether the player made it with an arrow or with the letter they bound. A queue in the arrow path, one level below the original's, would keep the arrows and drop the letters (#207).

And a link that merely finishes does not end the walk — only exitcode does. deckbd.set's keydown shows why: a ladder of if currentview() = "viewNN" & arg = "uparrow" … exitcode that falls off the end for every other key. Under the pointer event's rule that would consume the press, and no arrow would ever reach the default movement. (This is also why a scene script can quietly steal ↑ to send you through a door instead of walking — it takes the key with an exitcode.)

What keeps the router from resolving its own re-route back into itself is a re-entrancy check: a script already running a handler further up the dispatch stack is never given it again. Without it, reaching the boot recurses until it runs out of memory.

The boot is also where a title keeps its defaults, which is the other half of why events walk that far. They are written against target rather than me, because the boot is answering on something else's behalf — initprop hides a prop and zeroes it, resetactor disowns an actor:

code initprop ()                    code resetactor ()
    propvisible (target, false)         actorowner (target, "none")
    propvalue (target, 0)               actorvalue (target, 0)
    propdeg (target, 0)                 initactor ()

Almost everything relies on them: of the 72 props TAOOT's two always-open shops give you, only door and signs carry an initprop of their own, and no cast member in the tree carries a resetactor. So a prop or an actor answering nothing for an event is the normal case, not the broken one — and a stub target, with no script at all, still has to reach them. TAOOT ships one: the purser is an actor record with an eight-byte script container, and dropping his events as "target not loaded" leaves him holding the cufflink into the next game (#89).

And a press may never reach the chain at all, because two things are modal ahead of it: a playing movie and a suspended conversation. Both are places where the original's own wait loop is the one popping the event queue, so the key is answered there — ESC aborts the clip, ESC skips the line — and the scripts are never told (host, conversations).

The heartbeat and timed events ​

The engine has a heartbeat that ticks 20 times a second — one service step every 50 ms, which is TI.EXE's own rate. On each tick it services timed things: script-scheduled callbacks (makeloop), positional ambient sounds (makecricket), and looping sounds. This is the job of scheduler.ts. A separate, finer time base (1 tick = 1/60 s) in clock.ts drives delay(n) waits.

This timing layer is fully recovered from TI.EXE and implemented; the write-up is Timing. The two headline facts:

  • A "loop" is really a one-shot delayed callback — things appear to loop only because their handler re-schedules itself at the end.
  • A "cricket" is a positional ambient one-shot bound to the current set, with stereo panning based on where it is relative to the camera; setting it to fire again with no gap makes a seamless loop, with a gap makes an intermittent hiss.

The runtime subsystems each have their own deep-dive under Runtime — timing, the stage layer, characters, audio playback, and saving/loading.

Running and verifying ​

  • One dev server per root, so they can run at once. The two that are about the whole project come first and the games follow in the order they were ported: 5173 the front door and the editors (npm run dev), 5174 this documentation (npm run docs:dev), then 5175 Titanic, 5176 Dust, 5177 Timelapse, 5178 Skull Cracker, 5179 RedJack, 5180 Lunicus and 5181 Jump Raven (npm run dev -w <package>). A link from one to another 404s in dev with a page naming the server that would serve it — separate Vite roots cannot be one origin, and the deployed tree has no such problem.
  • On Titanic's server, /play/ cold boots itself into the game with nothing in front of it but the boot text. The saved-games browser reaches saves from the in-game menu and the editors reach every .SET under gamefiles/; there are no story-state presets or puzzle-jump buttons, since that is the playthrough's job. Assets are fetched on demand. See the browser host.
  • npm test — the Vitest suites across all packages: the end-to-end regression scenarios, savegame round-trips, recovered-builtin checks, the blackjack interpreter test, the editors' write path, the authoring and language suites, the text/audio encoding ones, Dust's movies and saves, and the layering rule itself. Prefer extending regression.ts over writing throwaway tests. The full map is the test reference.
  • npm run test:machine — every game played, headless; Titanic's from the boot to the ending, 27 segments carried as one session, asserting a recorded state trace per beat. What that buys, and the bugs it has caught that nothing else could, is how we know it's right.
  • tools/ has what works on any rip — the dumpers (dumpset, dumpshp, dumpscripts), parse, scancmds, scandeg, the manifest writer — and a tool that knows which game it is looking at lives in that game's own tools/: the TI.EXE mining tools and the flow-map generator are taoot/tools/. See the tool reference. The dev server also hosts the browser editors, one per container format.

Now that you know where things live, the two deep topics are the scripting language and, underneath all the data, the DFile container format.

Docs licensed GPL-3.0 (the decoder is ported from DFET). Game data © CyberFlix — not included.