DDirectorWikiDirector & Lingo Encyclopedia

Building a Director-Compatible Engine

The emulation landscape, architecture priorities, ECS mapping, compatibility traps, and the test checklist for a modern Director runtime.

The landscape

Active approaches to running Director content today:

ApproachExamplesNotes
Native players in compatibility environmentsOriginal projectors under Windows compat modes, VMs, WineHighest fidelity, worst portability/longevity; Shockwave-in-browser is gone.
Open-source reimplementationScummVM's Director engine (targets D2-D6 era and expanding), community engines (dirplayer and others)The sustainable path; fidelity varies by era and feature.
Decompile-and-portPer-title rewrites from decompiled LingoLabor-intensive; loses the runtime but preserves the game.
Bytecode/format toolingDecompilers, cast extractors, format parsers (ProjectorRays, Shockky lineage)Foundation layer for everything above.

For per-title preservation, the deciding questions are: which Director version built it, which Xtras it needs, and how deep its dependence on exact runtime semantics goes (inks, event order, text metrics).

Target mental model

Do not model Director as "a scripting language with graphics". Model it as a timeline-driven multimedia runtime with a scripting layer:

text
container loader (RIFX/afterburner)
  -> cast libraries and members
  -> score: frames, channels, spans, keyframes
  -> script compiler/interpreter (Lingo VM)
  -> Director object model APIs (the facades)
  -> frame scheduler + event dispatcher
  -> renderer / media / input / network / Xtra backends

Minimum viable compatibility core

Build these first; everything else layers on top:

  1. Cast model: lookup by number and name (search order!), media metadata, member scripts. Casts
  2. Score and playhead: frames, markers, go/play/play done, sprite spans, channel order. Score
  3. Sprites: number/name lookup, member binding, geometry, ink/blend, behavior instance lists, puppeting authority rules. Sprites
  4. Lingo: case-insensitive identifiers, 1-based lists/strings, chunk expressions, symbols, globals/properties, no short-circuit logic, VOID. Language section
  5. Event dispatch: full lifecycle order, hierarchy with pass/stopEvent, primary handlers, sendSprite/sendAllSprites/call, behavior order. Events section
  6. Input: _mouse/_key state properties, hit-testing rules, editable-field key pass-through. Input
  7. Renderer: channel-ordered compositing, regPoints, Copy/Matte/BackgroundTransparent/Blend inks minimum, with streams kept separate per the native model.
  8. Memory/streaming surface: preLoad family accepted, frameReady/mediaReady truthful. Memory

Then: parent scripts/ancestors/actorList/timeouts, text and field metrics, sound channels and cue points, film loops, network operations, MIAWs, an Xtra registry, and media backends (video/Flash/3D) or honest stubs.

Mapping to a modern engine (ECS or otherwise)

text
Director movie   -> world/session
Cast member      -> asset + optional script asset
Sprite span      -> entity template over a frame interval
Active sprite    -> entity instance
Behavior         -> script component instance (ordered!)
Frame script     -> frame-scoped system
Movie script     -> global script system
Child object     -> plain script object (no entity)

The one non-negotiable rule: your engine's processing order must never override Director's orders. Draw order is channel/locZ order; dispatch order is the message hierarchy; per-frame order is the event lifecycle. If your ECS wants to iterate by component type, it still has to deliver observable effects in Director order.

Implementation traps

Each of these is a documented failure mode from real emulator work:

  • Do not treat events as DOM-style bubbling: primary handlers, behavior order, pass, and stopEvent are their own model.
  • Do not clear globals or the actorList on movie branch.
  • Do not make lists 0-based anywhere, including internal representations that leak.
  • Do not adopt JavaScript truthiness or short-circuit evaluation for Lingo source.
  • Do not bake ink/matte transparency into reusable member bitmaps (why).
  • Do not treat backing canvas size as the stage coordinate system (why).
  • Do not resync score state after Lingo runs for the frame; sync at frame entry only.
  • Do not cull offscreen sprites: parked-at-negative-coordinates sprites are alive by design.
  • Do not silently ignore Xtra calls; return deterministic unsupported diagnostics.
  • Do not trust member numbers to be stable across authoring versions of a title; support name lookup faithfully (first-match cast order).
  • Do not let media decode timing affect event order.
  • Word splitting: whitespace runs only; item splitting: single-char delimiter with empty items preserved.

Compatibility test checklist

Fixture movies (or synthetic tests) every engine should pass before optimizing:

  1. Movie start order: prepareMovie -> beginSprite -> prepareFrame -> startMovie.
  2. Frame order with span boundaries: beginSprite / stepFrame / prepareFrame / enterFrame / input+idle / exitFrame / endSprite.
  3. Stop order: endSprite -> stopMovie; cross-movie state persistence.
  4. Behavior attachment order on one sprite; stopEvent in the first blocks the rest.
  5. pass from sprite behavior reaches cast member, frame, then movie script.
  6. mouseDownScript fires before sprite handlers; passes by default.
  7. Editable field typing requires pass in keyDown.
  8. sendSprite hits all target behaviors then continues the hierarchy; returns FALSE when unhandled.
  9. actorList receives stepFrame on advance AND updateStage(); survives movie branch.
  10. beginSprite restrictions (rect unready; go/play/updateStage inert).
  11. Channel order and locZ override drawing; ties break by channel.
  12. Matte ink changes the hit area; Background Transparent does not.
  13. getProp errors on missing key, getaProp returns VOID; duplicate propList keys tolerated.
  14. Sorted lists insert in order on later add().
  15. integer("42" & numToChar(2)) matches your target player version's strictness.
  16. the itemDelimiter is global and takes effect immediately.
  17. Blend 0..100 maps to the fixed-point 0..256 formula; arithmetic inks ignore blend.
  18. Palette-index colors resolve at draw time against the active palette.

Debugging strategy for fidelity work

The layer-classification discipline from the native research applies to any engine. For a wrong sprite: decode -> source prep -> property mapping -> dirtying -> operation selection -> coverage -> final math -> hit policy; find which layer diverged before changing code. For a wrong click: coordinates -> candidates -> hit policy -> target retention -> primary handler -> propagation -> timing. Build a per-sprite operation report (channel, member, ink, blend, colors, palette, depth, coverage flags, chosen paths) and diff it against expected state; most "mystery" bugs localize instantly.

The overriding rule: fix shared runtime semantics, never individual content. A per-room or per-window patch is a bug you have chosen to ship everywhere else.