Skip to content

Lua hot reload: callbacks told apart by their order reload, @onHotReload hooks, smaller payloads - #1551

Merged
Frotty merged 5 commits into
masterfrom
lua-hot-reload-callbacks-and-hooks
Oct 24, 2026
Merged

Frotty merged 5 commits into
masterfrom
lua-hot-reload-callbacks-and-hooks

Conversation

@Frotty

@Frotty Frotty commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Three changes aimed at the edits a real map makes most: callbacks which share an identity reload, a map can run setup code again after a reload (@onHotReload), and a payload no longer carries checks of the whole program. Design and limits: docs/LUA_HOT_RELOAD.md.

Callbacks told apart by their order reload

Closures which share an identity (several anonymous closures of one interface in one function, such as two doAfter callbacks) are told apart by their order. #1549 froze them: editing any of their bodies, adding one or removing one needed a restart. In Castle Fight, 1,050 of the 2,712 closures are in such groups (CallbackSingle 490 of 1,310, EventListener 74 of 222, ArgHandler 49 of 50).

  • Editing their bodies keeps the order, and so does adding one after them: both reload.
  • A closure whose source is now what another one of its group had has moved (a swap, or one inserted before the others), and an object made from the closure at its old place would run the other one's body: that is refused, as is removing one. Each closure is compared by its source as written (Function.closureSource), since each one's Lua names its own class, against the baseline's and the one an earlier payload sent last (History.groupShapes).
  • A copy of a body another closure keeps moves nothing: copying a callback and appending it, or editing one into a copy of another, reloads (copiedCallbacksAreNoMove). One inserted before identical callbacks is still refused.
  • Identities now name the classes a nested class is declared in. Two inner classes of one name in one package (A.N, B.N) shared the identity Test:N; allowing a group to grow exposed it (nestedClassFieldTypeChangesRequireARestart).

Reload hooks

A package init does not run again, so setup it did (UI frames, registered events, values copied from constants) stays as it was. A function annotated @onHotReload runs after each payload installed everything else, with the code the payload has, in package initialisation order; one which fails does not stop the others and is reported in the payload's status. The validator requires it to be static and take no parameters. The annotation is declared in the standard library (wurstscript/WurstStdlib2#518).

In a -luaHotReload build the hooks are entry points of the tree shaker, the call relations and the Lua garbage removal; in any other build hotReloadHooks() is empty, so the output is unchanged and a hook nothing calls is not emitted (reloadHookIsNotEmittedOutsideHotReloadBuilds). With -stacktraces every function takes the position of its call, a hook too, so the payload passes one: without it a trace taken in a hook showed nil (reloadHookFrameNamesTheReloadInStackTraces).

Payloads carry what an edit can have changed

A payload of Castle Fight with nothing changed was 403,624 bytes, past the 200,000 characters the delivery was measured with on the 3.0.1.24371 client. 379,578 of them were the initializer order check listing every one of the map's initializers, and 23,547 storage guards whose conditions were constant true. It is now 93 bytes:

  • The started map ran its initializers in the baseline's order, so the order check lists them only when an edit puts two of them in another order (swappedInitializersOfTheStartedMapWarnInTheMap); otherwise it lists those payloads added, as before (mapRemembersOrderOfPayloadAddedInitializers).
  • A storage guard is emitted only for a unit with storage the started map lacks (payloadChecksNoUnchangedInitializers).

Evidence

Measured on the 3.0.1.24371 client, single player, in a test map built with -build ... -lua -luaHotReload and a project's default run arguments (-runcompiletimefunctions -injectobjects -stacktraces), its session driven by LuaHotReloadSession start|send. Ten payloads in one game, 51 checks, all passed. Phases 1-9 as in #1550, plus:

Phase Edit In the game
10 of three callbacks of one interface made in one function, the second's body edited and a third appended the second, made when the map started, returns its new 20; the first keeps its 1; the appended one returns 3
every phase an @onHotReload function counts the payloads and copies a constant into a variable, as setup in an init does it ran after each of the nine payloads which applied and not at the start; after phase 7 the copy holds the tuned 250; phase 10 edited the hook and ran the new body (108: eight times 1, then 100)

Every payload which applied printed nothing but "applied payload n". An earlier run with phase 10 but without the hook passed 47 of 47.

Unit tests, Windows, on the final code: LuaHotReloadTests (103), DeterministicChecks (37), LuaBackendAuditTests (297), LuaTranslationTests (152), ClosureTests (68), TreeShakerTests (25), AnnotationTests (8), HotReloadPipelineTests (6), RunArgsTests (5). Each new test fails before its change; removing the move detection fails the swap and the middle-insertion tests.

Real edits

Castle Fight's last 30 commits which change Wurst code, each built with this branch and its payload made against the commit before it: 8 accepted, 22 refused. The causes, ranked, are in the docs ("Edits of a real project"); the two largest are a constant array literal changing length (14 commits, the only cause in 4) and a constant starting or stopping being read (7, the only cause in 2), and fixing both would accept 17 of 30. Accepted payloads also ship functions which differ only in the flatten's temporary names, numbered across the program: one of 5 changed files shipped 621 functions, 550 of them for that. Both are left for the next changes.

Not covered

  • Moving a callback among the others of its group, and removing one, still need a restart.
  • The editor's part (the language server's hot run) was not measured again; the in-game run builds from the command line. Linux is covered by PR CI; macOS is unverified.
  • A payload of a real map was not delivered in the game; the largest delivered was the test map's 93.7 KB phase.

…oad hooks, smaller payloads

- Closures which share an identity are no longer frozen: editing their bodies and appending one reload; a move (a
  closure now with another's source) and a removal still need a restart. Identities name enclosing classes.
- Functions annotated @onHotReload run after each payload installed everything else; static, no parameters. Entry
  points only in -luaHotReload builds. With -stacktraces the payload passes them a position.
- A payload lists the initializer order only when an edit swaps the started map's initializers, and guards storage only
  for units with storage the started map lacks: a no-change Castle Fight payload went from 403,624 to 93 bytes.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T12:38:14.717483Z 0459fec Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Frotty

Frotty commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@Frotty

Frotty commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b284fa204

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Frotty

Frotty commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 960e10ab64

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Frotty

Frotty commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 0459fec42a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant