DDirectorWikiDirector & Lingo Encyclopedia

Pitfalls, Debugging, and Porting

The classic Lingo traps, debugging techniques, and rules for porting Lingo to and from modern languages.

Classic traps

1. Missing the

Built-in system properties are read with the. Omitting it silently creates a local variable:

lingo
set stageColor = 0        -- creates a LOCAL named stageColor. Does nothing.
set the stageColor = 0    -- sets the actual stage color

When old code "has no effect", check for a missing the first.

2. Parentheses on function calls

lingo
put rollover        -- useless: names the function
put rollover()      -- calls it

Any call whose return value is used needs parentheses. count(list), random(6), the result idioms exist because Director 4 lacked general call syntax.

3. Lists are references

Passing a list to a handler passes the same list. Mutation leaks to the caller. Copy with duplicate(). See Lists.

4. getProp vs getaProp

getProp errors on a missing key; getaProp returns VOID. Old code depends on both behaviors deliberately.

5. Message routing surprises

A handler in a sprite behavior swallows the event for lower levels unless it calls pass. A keyDown handler on an editable field that does not pass eats the keystroke and the field never types. See Message hierarchy.

6. beginSprite limitations

Inside beginSprite, calculated sprite properties like rect may not be ready, and go, play, and updateStage are disabled. Initialization that needs geometry belongs in the first prepareFrame.

7. The itemDelimiter is global

Change it, and every item expression in every script sees the change until restored. Wrap and restore religiously.

8. and / or do not short-circuit

if objectP(x) and x.ready then still evaluates x.ready when x is VOID, and errors. Classic Lingo nests ifs instead.

9. Globals persist across movies

go to movie does not reset globals, the actorList, timeout objects, or open windows. Movies that misbehave on re-entry usually left state behind.

10. TRUE is 1, but any nonzero integer passes

if n then passes for any nonzero integer. But strings/lists/VOID in a condition are type errors, not falsy. Do not port JavaScript truthiness in either direction.

Debugging technique

lingo
put someExpression                   -- print to Message window
put "reached here", x, y             -- multiple values
showGlobals()                        -- dump globals
showLocals()                         -- dump locals (inside a handler)
the traceScript = TRUE               -- log executed lines
trace(expr)                          -- MX 2004 message-window output
alert "value:" && string(x)          -- modal fallback
  • The Message window (Ctrl-M) evaluates any Lingo you type; it is a REPL against the running movie.
  • Breakpoints + the Debugger/Watcher exist in the authoring tool; protected content has no source, so runtime put logging and alert remain the tools of last resort.
  • the lastError-style diagnostics do not exist in classic Lingo; script errors surface as modal dialogs (authoring/projector) or silent halts (Shockwave with the alertHook set).
  • the alertHook: a script object whose alertHook handler intercepts error dialogs; returning 1 suppresses them. Preservation runtimes should implement it, as shipped games use it to swallow errors.

Version differences that matter

AreaOld (D4-D6)New (D7-MX 2004)
SyntaxVerbose only (the x of sprite 1)Dot syntax added; both valid
Continuation¬ character\ in listings
Event stopdontPassEventstopEvent() (dontPassEvent still parses)
Timersthe timer, the timeoutLength familytimeout objects
Text membersFields (bitmap-era)Fields + anti-aliased Text members (D7+)
Imagingnoneimage object, copyPixels (D8+)
Integer coercion of bad stringslenient (0)strict (VOID) in MX 2004
WindowsMIAW APIsame + _player.windowList facades
Scripting objectsthe globals_movie, _player, _mouse, _key, _sound, _system

When emulating a specific title, pin the player version it shipped against and test coercion, event, and text behaviors against that version's rules, not MX 2004's, where they differ.

Porting Lingo to modern languages

  • Lingo is 1-based everywhere (strings, lists, channels, frames). Off-by-one bugs dominate naive ports.
  • repeat with i = 1 to n is inclusive on both ends.
  • No null: the empty value is VOID; voidP() is the test. VOID propagates through arithmetic as errors, not NaN.
  • Property lists preserve insertion order; modern JS objects do too, but Python dicts pre-3.7 did not; order-dependent code exists.
  • Case-insensitive identifiers: symbol keys #Foo and #foo collide; two handlers differing only in case are the same handler.
  • String comparison is case-insensitive by default; port with explicit lower() on both sides.
  • Integer division truncates; mod follows the dividend's sign (C-style, not Python-style).
  • value() is an expression evaluator; do not translate as parseInt.
  • Chunk expressions have no direct equivalent; word splitting on any whitespace run and item splitting on a single-char delimiter need faithful reimplementation (empty items between consecutive delimiters are real items).
  • Sprite/score access has hidden state (puppeting, score authority per frame); a port that keeps game logic but replaces rendering must reproduce the event lifecycle or subtle logic breaks.

Reading decompiled Lingo

Decompilers emit mechanical but accurate source. Expect:

  • Auto-named locals and loop variables where the name table was stripped selectively.
  • Verbose syntax for property access even for MX-era movies (bytecode does not record which syntax was typed).
  • tell, me plumbing, and return structure faithfully recovered; comments are gone forever.
  • Handler names always survive (they live in the name table, needed at runtime for message dispatch).

A fast orientation strategy for a decompiled game: list all movie scripts, find startMovie/prepareMovie for the boot path, grep for sendSprite/sendAllSprites/call( to map the messaging web, then grep global names to find the managers.