Skip to content

Stop silently swallowing script load/update/on_event errors - #31

Merged
diego3 merged 1 commit into
mainfrom
claude/log-swallowed-script-errors
Aug 1, 2026
Merged

Stop silently swallowing script load/update/on_event errors#31
diego3 merged 1 commit into
mainfrom
claude/log-swallowed-script-errors

Conversation

@diego3

@diego3 diego3 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

WorldScene.updateScripts (frameengine/view/scene/world_scene.go) discarded every error from
DoFile/DoString and CallScriptUpdate with a bare continue — no log, nothing. The three
CallOnEvent call sites (script-emitted events, BeginContact, EndContact) threw their error
away with _ =. Practical effect: a Python/Lua script with a syntax error or a runtime exception
left its entity silently frozen, with zero signal anywhere about why. Worse, a failed load never
marked loadedScripts[s.Path] = true, so the engine retried loading the same broken file every
frame, forever
, just as silently.

Changes

  • A load failure (DoFile/DoString/fs.ReadFile) is now logged once and the script is marked
    permanently failed (scriptLoadFailed), which also stops the per-frame retry loop — one fix
    covers both the missing log and the wasted repeated work.
  • A CallScriptUpdate failure is logged only on the success→failure transition (and again on
    recovery), not on every frame it keeps failing — keeps this compliant with CLAUDE.md's "no
    per-frame logging" rule even for a script that errors continuously.
  • The three CallOnEvent call sites (ScriptEmitted, BeginContact, EndContact) now log their
    error instead of discarding it. These fire per-event, not per-frame, so no rate-limiting is
    needed there.
  • New test file world_scene_script_errors_test.go: a minimal script.Engine test double
    (per CLAUDE.md's "mock interfaces, not concrete types" convention) exercising updateScripts
    directly, covering: a failing load isn't retried every frame, a load failure is logged, an
    update failure logs once (not once per frame) while it keeps failing, and recovery is logged.

A real bug this immediately surfaced

Running games/demo1 live during validation now prints:

script: update(0.0167) failed in "games/demo1/scripts/knight_gamelogic.lua": script: global "update" is not a function (got nil)

games/demo1/scripts/knight_gamelogic.lua is an empty (0-line) stub. LuaEngine.CallScriptUpdate
calls the named function unconditionally without checking it's defined first — unlike
PythonEngine, which no-ops when the function is missing (per the Engine interface's own
documented contract: "Returns nil if the function is not defined (not an error)"). This predates
this change — the error was always being returned and always being silently discarded — so it's
not a regression from this PR. Left as-is deliberately: demo1's Lua scripts are legacy per
CLAUDE.md ("do not update or maintain them"), and fixing the Lua/Python behavioral inconsistency
itself is a separate, unrelated change. Flagging it here since it's now visible for the first
time. games/metalslug_demo (Python, the actively-developed game) runs clean with no such errors.

Validation

  • go build ./..., go vet ./..., gofmt -l . — clean
  • go test ./... — all pass; go test -race ./... — same result, except the pre-existing,
    unrelated box2d-go checkptr failure under -race (reconfirmed not caused by this change)
  • New tests pass in isolation and as part of the full suite
  • GOOS=js GOARCH=wasm go build — clean
  • Live smoke run of both games/demo1 and games/metalslug_demo — see finding above; metalslug
    clean, demo1 now surfaces its pre-existing dead script (expected, not a regression)

Generated by Claude Code

WorldScene.updateScripts discarded every error from DoFile/DoString and
CallScriptUpdate (continue, no log), and CallOnEvent's error was thrown
away with `_ =` at all three call sites. A script with a syntax error or a
runtime exception left its entity silently frozen with zero diagnostic
signal, and a failed load was never marked as attempted, so the broken
file was retried (and would now be logged) every single frame forever.

Now: a load failure is logged once and marks the script permanently
failed (stops the per-frame retry loop); a CallScriptUpdate failure is
logged only on the success->failure transition (and again on recovery),
not every frame it keeps failing, keeping this compliant with the
"no per-frame logging" rule even for a script erroring continuously; the
three CallOnEvent call sites now log their error instead of discarding it
(these are already event-driven, not per-frame, so no rate-limiting is
needed there).

A live smoke run surfaced a real, previously invisible bug this fix was
meant to catch: games/demo1/scripts/knight_gamelogic.lua is an empty
stub, so its "update" global is nil — LuaEngine.CallScriptUpdate calls it
unconditionally without checking, unlike PythonEngine, which no-ops when
the function isn't defined. This predates this change (the error was
always returned and always discarded) and demo1's Lua scripts are legacy
per CLAUDE.md ("do not update or maintain"), so left as-is; noting it here
since it's now visible in logs where it wasn't before. metalslug_demo
(Python, actively developed) produces a clean run with no such errors.
@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@diego3
diego3 merged commit 419b37e into main Aug 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants