Skip to content

fix(scripts): declare Runnable entry points with var so modern engines find them - #494

Open
ResurrectedTrader wants to merge 1 commit into
blizzhackers:restructurefrom
ResurrectedTrader:runnable-var
Open

ResurrectedTrader wants to merge 1 commit into
blizzhackers:restructurefrom
ResurrectedTrader:runnable-var

Conversation

@ResurrectedTrader

Copy link
Copy Markdown
Contributor

Problem

Loader resolves every script by name through global[script] (libs/core/Loader.js, and SoloPlay's LoaderOverrides.js does the same). All 84 entry points in libs/scripts are declared as const X = new Runnable(...).

On modern JS engines a top-level const/let is a binding in the global lexical environment, not a property of the global object, so global["Pindleskin"] is undefined and every script fails in the loader with:

Invalid script function name. Typeof: undefined Name: Pindleskin

Legacy SpiderMonkey treated top-level const as a read-only var on the global object, which is why this has been working so far. Quick reproduction on V8 via node's vm module:

const vm = require("vm"); const ctx = vm.createContext({}); ctx.global = ctx;
vm.runInContext("const Foo = 1; var Bar = 2; function Baz() {}", ctx);
vm.runInContext("global['Foo']", ctx); // undefined
vm.runInContext("global['Bar']", ctx); // 2
vm.runInContext("typeof global['Baz']", ctx); // "function"

Change

  • Declare the 84 Runnable entry points with var, which is a global-object property on every engine. Bare-identifier and typeof X access across files is unaffected.
  • Each declaration carries an inline eslint-disable-next-line no-var -- <reason> so the exemption is limited to exactly these lines; eslint.config.mjs is untouched and lint stays clean.
  • SoloPlay needs no change: its scripts are plain function declarations, and its loader only includes SoloPlay/Scripts/.

🤖 Generated with Claude Code

…s find them

Loader resolves each script as `global[script]`. On modern JS engines
such as V8 (d2bsn) a top-level `const` is a binding in the global
lexical environment, not a property of the global object, so
`global["Pindleskin"]` is undefined and every Runnable script dies with
"Invalid script function name. Typeof: undefined". Legacy SpiderMonkey
aliased top-level `const` to a read-only `var`, which is why this worked
there.

Switch the 84 script entry points in libs/scripts to `var`, which is a
global-object property on both engines, with an inline
`eslint-disable-next-line no-var` on each declaration explaining why.
SoloPlay is unaffected: its scripts are plain function declarations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ResurrectedTrader

Copy link
Copy Markdown
Contributor Author

@theBGuy

theBGuy commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

I know we've talked about this one before. Still don't like it, just feels wrong to make an exception for var.

@ResurrectedTrader

Copy link
Copy Markdown
Contributor Author

It's effectively a var now.
I guess I could do globalThis.foo = new Runnable, but is that any better?
The problem is how the discovery works I guess.

@ResurrectedTrader

Copy link
Copy Markdown
Contributor Author

Asked robot about globalThis:

globalThis does not exist natively on SpiderMonkey 24, but Polyfill.js line 887 defines it on both engines, and no other file references the script names as bare identifiers. So globalThis.Pindleskin = new Runnable(...) would lint clean without any disable.
But it changes runtime behaviour in a way that breaks a real path. Today's var, function, and SpiderMonkey's const all create non-configurable globals, so the Loader's delete global[script] has always been a silent no-op. A plain property assignment is configurable, so the delete would start succeeding. That matters because scripts get run twice in one game:
GetKeys calls Loader.runScript("Countess"), and Countess is commonly also enabled in the main list.
GetEssences runs Andariel, Duriel, Mephisto, Diablo and Baal the same way.
BaalHelper and BaalAssistant run Nihlathak and Diablo.
The runScript cleanup deletes the global after the nested run. When the main loop later reaches that script, include() returns true without re-executing the file on either engine, so global[script] is undefined and the loader throws "Invalid script function name". Making the assignment approach safe would mean also removing the four deletes in Loader.js and the three in SoloPlay's LoaderOverrides.js, which turns a one-line-per-script fix into a two-repo loader change.

Let me know how you want to proceed.

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