Browser rebuild of Dust: A Tale of the Wired West, plus a DreamFactory extractor.
Non-commercial fan reconstruction. Not a port of CyberFlix’s engine C. Playable client is Three.js. Game logic will run extracted DreamFactory tokens in a TypeScript VM. https://diamondback.town is a title chooser: Dust: Resurrected, Dust: Unlocked, Dust: Reimagined, The Picture Show, and The Sideshow.
Hosted: https://diamondback.town (GitHub Pages + CloudFront extract).
Repo root unless a cd is shown. Need Node 22+ for the client, Python 3.11+ for the tools, and a Dust tree at sources/dust.dbgl/ to extract.
Needs dfextract/out/SET/_TOWN (and _NITE plus interiors you walk into).
npm install
npm test
npm run test:playtest
npm run dev
Open http://localhost:5173 — title chooser (four titles plus The Sideshow). Cards switch in-page; they do not reload the document. Vite serves stills from dfextract/out/ at /extract/….
Dust: Resurrected (/?mode=resurrected): the VM game (Day 1 night through Day 5 endings). Original dashboard, CST sprites, PUP talking-heads. Skull HUD is the extracted score flat (Save / Open / Quit / Credits). Save writes a .json file (picker, or a named download). Open loads a .json / old .rtd file. localStorage is autosave only — Continue (/?mode=resurrected&continue=1) and a refresh of the play page restore it. When an autosave exists, New Game on the title (with a confirm) starts over. Needs a full extract (PUP/CST/FLT/PRP, not just SET stills). Boot skips intros unless &intro=1. Playback notes: src/play/README.md.
Dust: Unlocked (/?mode=unlocked): same PlayGame / VM as Resurrected, sandbox policy. Empty of story casts, every door open (debugging), minigame NPCs (Leroy at the range, Bolivar at the store, Dell at D7, Kid at G6, saloon blackjack/poker tables), farm animals including horses (not the dog). Bank sign cracks the safe; apoth bottles open compounding. N day/night; ?clock=1|2|3. Afternoon by default so shops and tables are up. Needs the same full extract as Resurrected (PUP/CST/FLT/PRP, not just SET stills).
Dust: Reimagined (/?mode=reimagined): 3D free-roam of Diamondback. Pointer-lock FPS, empty town, clickable swing doors, N day/night. Esc releases the look; Esc again returns to the chooser. On a phone it is a virtual stick under the left thumb, a drag on the right to look, a tap to open a door, and Jump / Night / Menu buttons — no pointer lock, no keys. Isolated from Resurrected / Unlocked — no VM, no SET stills. Details: src/reimagined/README.md.
The Picture Show (/?mode=movies): extracted playmovie reels in the browser (not movplay.py). Opening is selected first. /?mode=movies&reel=intro3 picks another. Not INVEN inspectables or stairs. Underground is the Yunni cave payoffs (fountain, skeleton, snake, flute, chest, tumble). Coming attractions lists the CD INFO/ attract reels (Jump Raven, Lunicus, Skull Cracker, Titanic).
The Sideshow (/?mode=sideshow): non-canon attractions, none of which touch the engine reconstruction. One landing card holds all of them; the attraction is &show=. First attraction is Chicken Blaster (/?mode=sideshow&show=blaster): the filmed streets, full of chickens, a finite belt of shells, and a cascade that travels tile by tile when you shoot one. A boss every fifth wave, drawn from the animals whose pose sets the extract actually has. Isolated from the play modes by an import wall (src/sideshow/boundary.test.ts); needs SET _TOWN, CST _EXTRA/_TARGET/_GANG and SND. Design and rationale: SIDESHOW.md.
| Key / click | Touch | Action |
|---|---|---|
| ← → or A / D | Drag the picture sideways | Turn. The still follows your finger, so dragging right turns you left |
| ↑ or W | Drag up | Walk one filmed block |
| N | Moon button in the toy bar | Day ↔ night stills (sandbox only; does not change the day number) |
| Click a door, then walk forward | Tap, then drag up | Open (if allowed) and go inside |
| Click a nearby person (play mode) | Tap | Talk (puppet + choices) |
| Click the skull (play mode) | Tap | Original menu: Save, Open, Quit, Credits, keys, volume |
| C (play mode) | Skull menu check box | Show / hide the black speech bar. Dust puppetparam (7): subtitles start off |
Swipes work anywhere on the page, not just on the letterboxed stage, so a portrait phone is not mostly dead glass. Nothing in the stills modes needs a keyboard.
/?mode=unlocked&clock=1|2|3 — morning / afternoon / night (Unlocked only).
Production-style local build (preview still mounts /extract from dfextract/out/):
npm run build
npm run preview
First-time setup (venv, PATH, wipe out/): dfextract/README.md. Default dump is scripts + audio + frames + SET Z planes (play needs FRAMES/z/). --video is opt-in (ffmpeg).
cd dfextract
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
python cli.py
macOS / Linux: source .venv/bin/activate and python3. From repo root you can also python -m dfextract.
| Command | What it does |
|---|---|
python cli.py |
Full dump → dfextract/out/ (~minutes, several GB) |
python cli.py --scripts --type boot |
Smoke test: out/BOOT/_BOOTFILE/Script 1.txt |
python cli.py --type set |
SET stills / graphs only (what the town walker needs) |
python cli.py --type mov --video |
MOV movie.mp4 (needs ffmpeg) |
python cli.py --jobs 1 |
One file at a time |
Remove-Item -Recurse -Force .\out then python cli.py |
Clean re-extract |
Output is gitignored and generated. Never hand-edit dfextract/out/
(or dustdecompile/out/). Fix dfextract/ / dustdecompile/ and
re-run, or fix src/ if the dump is already faithful. Catalog:
dfextract/docs/output-catalog.md.
Needs a dump with MOV FRAMES/ + timeline.json (default extract). pygame-ce is in dfextract/requirements.txt.
cd dfextract
python movplay.py out/MOV/_INTRO --scale 2
python movplay.py out/MOV/_DOG1
Esc or Q quits, Space pauses. --scale 1 is native 512×264. --video muxes movie.mp4 if you would rather open that in a normal player.
Reads DF.EXE / MOVPLAY.EXE / plugins. Not used by the browser client.
python -m dustdecompile
Output: dustdecompile/out/ (gitignored), including out/rsrc/cursors/
from DF.EXE. Details: dustdecompile/README.md.
A full Ghidra decompile is a separate, optional step (needs Ghidra and a JDK); it writes the decompiled C and function/string/data-ref tables into the same gitignored tree:
<ghidra>/support/analyzeHeadless <proj> DustProj -import DF.EXE -scriptPath dustdecompile/ghidra -postScript ExportDecomp.java dustdecompile/out/ghidra/DF
What that decompile proved about the script VM — types, operator
precedence, statements, engine hooks, timing, dialogue, mixer and the
.rtd save layout — is dustdecompile/docs/vm.md.
npm test
cd dfextract && python -m unittest discover -s tests -v
cd dustdecompile && python -m unittest discover -s tests -v
Python tests need sources/dust.dbgl/ (and dfextract/out/ for some decompile cases).
| Piece | Where |
|---|---|
| JS / HTML | GitHub Pages at https://diamondback.town |
| Stills / WAV / SET JSON | CloudFront https://d3en1dc3mw7cky.cloudfront.net (S3, uploaded once) |
| CI | .github/workflows/pages.yml — main pushes that touch src/, Vite, or package.json |
Repo variable VITE_EXTRACT_BASE must be the CloudFront origin (no trailing slash). Local npm run dev ignores it and keeps using /extract.
Extract cache (public/sw.js). Hosted builds register a service
worker that puts the film on the player's disk: cache-first for the
extract origin, and after boot the stills walker hands it every plate of
the current SET ordered breadth-first from the spawn, warmed in the
background at low priority. A second visit — and most of a first one —
reads from storage instead of the network, which is what Dust got from
the CD. Measured on a preview build: a six-move walk made 78 extract
requests, none of which touched the network.
It is deliberately narrow. It never intercepts HTML, JS or CSS, so a
Pages deploy behaves exactly as before and the worker cannot serve a
stale app shell. The extract origin is baked into the worker's URL
(sw.js?base=…), so changing VITE_EXTRACT_BASE installs a new worker
and drops the old cache — which is what makes the versioned-prefix
upload plan safe. EPOCH in sw.js is the manual wipe, and ?nosw=1
unregisters and clears. Dev never registers it: Vite serves /extract
with no-cache so a re-dump must show up on reload.
Pages Source must be GitHub Actions. Markdown and Python (dfextract/, dustdecompile/) do not trigger a deploy. Actions → Deploy Pages → Run workflow forces one. After a client change, push main; you do not re-upload S3 unless the extract itself changed.
If the canvas is blank on the live site: CloudFront CORS (GET/HEAD, Access-Control-Allow-Origin * or the Pages origin) or a key-prefix mismatch (SET/… vs extract/SET/…).
Original CD / DOSBox tree, extracted PNG/WAV, and dfextract / dustdecompile output stay on disk (copyrighted; too large for GitHub). CloudFront serves the extract the hosted chooser needs; the CD stays local.
Decisions, slices, and the asset pipeline: SPEC.md. How the
town stills play (strips, HQ, 20 Hz flipbook, doors):
src/world/set/README.md. Extract still
codec and palette: dfextract/docs/images.md.
Extractor from scratch: dfextract/README.md.
Engine binaries: dustdecompile/docs/findings.md.