Debugging OpenMW Lua gets dramatically easier when failures are allowed to remain specific.
The worst debugging architecture is one that catches the original error, keeps running with half-updated state, and finally fails somewhere unrelated three callbacks later.
Start with the first wound.
Preserve the traceback
If an error crosses a boundary you intentionally protect, preserve enough information to reconstruct the call path.
DreamScripts contains multiple historical fixes whose entire lesson is that protected execution without a useful traceback is not useful error handling. Commits c8737a3 and 0e7eb8d corrected error paths so the traceback was actually emitted.
A useful failure report should normally identify:
- the original message;
- traceback when available;
- script/context involved;
- object, record, event, path, or registration ID involved;
- whether execution stopped or continued;
- any version/build information needed to reproduce the engine behavior.
Do not replace all of that with something went wrong.
Reproduce before theorizing
Reduce the problem to the smallest repeatable action.
If the failure appears to depend on:
- entering a cell;
- loading a save;
- switching UI modes;
- one record type;
- a particular event ordering;
- one configuration field;
make that dependency explicit before changing architecture.
A reliable reproducer is worth more than a plausible theory.
Ask which layer is wrong
A symptom in Lua does not prove the bug is in Lua.
The relevant layers may include:
- your script state;
- OpenMW's Lua API contract;
- the C++ binding representation;
- engine state or game data;
- LuaJIT behavior;
- a fork/runtime-specific facility.
The GMST optimization failure documented under Paid For With Blood is a perfect example: code that looked numerically trivial was actually interacting with userdata-backed values.
Check context before API semantics
When require 'openmw.foo' fails or Cod3x reports an unavailable module, first confirm the active script context.
Do not spend an hour debugging a function signature when the module cannot exist in that sandbox.
See Script Contexts and Context Availability Is a Contract.
Check object validity and lifetime
OpenMW objects are engine-backed references. A reference can outlive the state in which it was useful.
Before blaming an unrelated accessor, ask:
- is this object still valid?
- is it loaded?
- does it still have a cell?
- was this work deferred across a transition?
- are we holding a transient object where an ID would be safer?
Stale work is a lifecycle bug until proven otherwise.
Use logging to establish ordering
When the bug depends on event/lifecycle ordering, log the transitions rather than every local variable.
Useful logs look like:
transition=17 cell=Vivec start
transition=17 presence-request sent
transition=18 cell=Ald-ruhn start
transition=17 presence-response rejected stale=true
That tells you which invariant held or failed.
A wall of x = 1, x = 2, x = 3 rarely does.
Profile performance bugs, do not debug them by aesthetic preference
If the bug is "this is slow":
- reproduce the slowdown;
- count the work;
- time the work;
- identify allocations if relevant;
- inspect bytecode/traces only if Lua execution remains material;
- inspect engine-bound calls if they dominate.
See Measure First and Pr0f1l3r.
Reduce, then source-dive
Once the mechanism is narrow enough, inspect OpenMW source instead of inventing semantics from observations.
A five-minute source read is often cheaper than a twenty-commit workaround around an assumption that was never true.
See Source Diving.