Features
Editor
- Show walkability and flags on the combo pages
2cd3634267DiscordPress W or F in the combo pages (and the combo selector) to toggle showing each combo's walkability or inherent flag, the same as the "Show Walkability" and "Show Flags" view options.
ZScript
@ExportEngineValue("Level Item")75ee64a0feNew ExportEngineValue option for exporting a set of Level Item flags. Exports as a bitflags, so more than one level item can be selected.
Bug Fixes
- Memory leak when loading sound effects of older quests
5c6d195f1dDiscordLoading a quest saved in an older version kept an extra copy of all of its sound effects in memory that was never freed, about 21 MB for yuurand. It leaked again every time such a quest was loaded.
Regressed in 3.0.0-prerelease.160+2026-02-08 (58486c3f4a).
- Prevent crashes from long file names and paths
85a84fd6c4DiscordGrabbing colors from an image whose file name was longer than 64 characters crashed the editor. Fixed several similar crashes from long file names or paths:
- Grabbing tiles from a file with a very long name and no extension
- Changing the file type filter in the built-in file picker when the current file has a very long name
- Opening a quest with a very long path (Recent quests menu)
- File and music names with many non-English characters
- Very long paths for quests, images, MIDIs and music in the editor
The palette grab crash regressed in 2.55-alpha-113 (bc22503603), and the Recent quests crash was introduced when that menu was added in 2.55-alpha-103 (d530a2fba3). The rest are long-standing.
- Api server read oversized replay uploads into memory
a5c6204189Bug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
- Api server errored on malformed replay uploads
d39f2b352eBug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
Player
- Keep palette mixing and slopes identical across cpu architectures
55a86482b4Script palette mixing could produce colors one step off on arm64 compared to x86, and slope collision used the same calculation, desyncing replays recorded on the other architecture.
Bug introduced when AArch64 support was added in 3.0.0-prerelease.201+2026-07-19 (a720c27f87).
- Replays were never re-uploaded on Windows and Linux
f3c558544fA replay that grows after being uploaded (the player keeps playing that save) is meant to be uploaded again, but checking whether the file changed only worked on macOS. This also meant a save that was first seen before it had been played was never uploaded at all.
Bug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
- Replay never uploaded after an unexpected server response
148b1608b1Any error response other than a server error made the client give up on that replay forever - including ones that have nothing to do with the replay, like a 404 or 429 from a proxy or a bad server deploy. Now the replay is only given up on when the server rejects the replay itself, and is otherwise tried again later.
Bug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
- Replay of a copied save never uploaded
7d095864f7Copying a save file gives its replay a new identity the first time the copy is played. If replays were uploaded before then, the copy's replay was mistaken for one too old to upload, and was never uploaded - even after being played.
Bug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
- Leaked curl handle when a replay upload request failed
739b0763b1Bug introduced when replay uploading was added in 3.0.0-prerelease.58+2024-07-07 (36f5d08109).
Editor
- Every combo was saved with a lift weapon data block
578cf12b3dA combo's lift weapon data was compared against the wrong default when deciding whether the combo was untouched, so no combo ever counted as blank. Every combo written to a quest file carried a full block of default lift weapon data, making quest files a few percent larger than needed. The editor also treated every combo as in use: importing a combo file with overwrite protection never replaced anything, and "Effect Square Fix" skipped every combo.
Bug introduced when standardized weapon data settings were added in 3.0.0-prerelease.120+2025-07-19 (b43e006d74).
- Wrong error message for an invalid ZASM string or array
ac0684880fWhen assembling ZASM, an invalid string was reported as an invalid third parameter, and an invalid array as an invalid string.
Regressed in 3.0.0-prerelease.102+2025-05-04 (51ec0e0a51).
ZScript
- Stop JIT losing register values around function calls
09d7852b67Under the JIT, a value a script kept in a register could be lost when it was still needed after a function call or return - for example a value written before two calls and read after them, or one a function set before returning to a caller that reads it.
Seen in The Darkness Within, where a bridge put back the wrong combos as it retracted.
In practice only quests compiled by old versions hit this: 2.50's compiler kept a value in a register across function calls, a pattern the JIT did not anticipate. The generated ZASM was unsafe: it sometimes overwrote registers before they could be used (though it worked in the common case).
Regressed in 3.0.0-prerelease.135+2025-10-08 (68087da593).
- Debugger shows wrong locals with experimental optimizer
debeb75ee2With -optimize-ZASM-experimental, the debugger could show the wrong value for a function's local variables, or fail to evaluate them.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger crash when a console expression errors
7b618b6481With "Break on error" enabled, evaluating a console expression that raised a script error (such as reading a member of an invalid ffc) crashed the player.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger window corruption when calling functions
913917112eCalling a script function from the debugger (in the console, a watch, or a value-change breakpoint) could redraw the debugger window from inside the call. That could corrupt the window, or crash if a breakpoint was removed or the window closed at that moment.
Regressed in 3.0.0-prerelease.180+2026-05-02 (1663c510c6).
- Debugger assignment can free the assigned object
2c72cc2769Assigning an object to a variable in the debugger console could free that object when the variable held its only reference, as in
x = xornode = node->next, leaving the variable pointing at nothing.Bug introduced when assignment in debugger expressions was added in 3.0.0-prerelease.161+2026-02-16 (ef4820fa6d).
- Debugger shows wrong call stack for other scripts
dc2d4e146eSelecting a script other than the paused one in the debugger's Scripts panel showed the wrong call stack for it: every frame after the first was a "?" frame. Selecting one of those frames showed the wrong variables.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger shows wrong variables for recursive calls
d21617a531In a recursive function, selecting a call stack frame below the collapsed "... (xN)" row showed another frame's variables. Clicking that row itself also showed nonsense variables; it now does nothing.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger watch list stale after unwatching
42aae92d57Removing a watch with the "Unwatch" context menu item left its row in the Watch panel until something else refreshed it, and double-clicking a row to edit it could then edit a different watch.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger "Enable All" skips value-change breakpoints
f2d8e7b65fThe Breakpoints panel's "Enable All" and "Disable All" buttons did not affect breakpoints that pause when a value changes, though "Clear All" removes them.
Bug introduced when value-change breakpoints were added in 3.0.0-prerelease.216+2026-08-30 (f47b67abe4).
- Debugger watches can read a destroyed script
55caf48e1dAfter resuming from a pause, the debugger kept referring to the script it had paused in. Watches that call a function still evaluate every second while the game runs, and read that script's data even after its sprite (and so its script data) was gone, which could crash.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger crash dividing the minimum long by -1
56dd042d19Evaluating a long division whose result doesn't fit in a long, such as the minimum long value divided by -1L, crashed the player on x86.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger assignment to members miscounts references
89b9089a7eAssigning an object or array to a class member in the debugger console (
obj->arr = other), or a class instance to an element of an untyped array, didn't track the reference. The assigned value could later be freed while still in use, and the replaced value was never freed.Bug introduced when assignment in debugger expressions was added in 3.0.0-prerelease.161+2026-02-16 (ef4820fa6d).
- Debugger crash after a quest fails to load
a9c8161a58If a quest failed to load partway through while the debugger was open with breakpoints set, the Breakpoints panel could read the previous quest's freed script data and crash.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Debugger hover tooltip shows outdated array contents
7fef56a3c9Hovering an array or object in the debugger's source view kept showing its contents from the first time it was hovered, even after stepping changed them, as long as it was still the same array or object.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Prevent crash on exit with the debugger window open
d16b242cd0Quitting the player, or a replay finishing, while the debugger window was open crashed on macOS.
Bug introduced when the rich debugger was added in 3.0.0-prerelease.161+2026-02-16 (c7a2ccbb84).
- Script state leaked when a scripted eweapon reflected into an lweapon
fe1e594f8bA scripted enemy weapon that bounced off a mirror combo or a mirror shield became a player weapon in place. The state of the script it had been running, and every object and array that script owned, stayed filed under the enemy weapon type and was never released when the weapon was removed. They lingered until the game was restarted, as did any objects stored in the weapon's enemy weapon arrays, like InitD[].
Regressed in 3.0.0-prerelease.20+2024-01-01 (e3b23aa7da).
- JIT on arm64 could corrupt the caller of a huge function
7a6d0f5561On arm64, a script function compiled with the JIT whose spilled values take more than 16 KB of stack returned with a register the caller relies on overwritten. It went unnoticed only because the code calling into JIT-compiled functions happened not to keep anything there across the call.
Bug introduced when the native arm64 JIT was added in 3.0.0-prerelease.201+2026-07-19 (489060ad94).
- Reflected scripted eweapons ran an lweapon script
5cdd3436d5A scripted enemy weapon that bounced off the hero's mirror shield kept its script number after becoming a player weapon, so it started running whichever lweapon script happened to have the same number.
Regressed in 3.0.0-prerelease.20+2024-01-01 (e3b23aa7da).
- -no-JIT-print-asm and -no-JIT-fatal-compile-errors were ignored
96870399e9These two flags could only turn their config option on: the negated form did nothing, so a config that enabled one couldn't be overridden from the command line. They now work like the other JIT flags.
Documentation
ZScript
- Reorganize jit.md
9f04e745c2Group the JIT doc by audience: using the JIT, how the interpreter and the JIT work, then per-backend details, debugging, and a flags reference. Add a backend comparison table, split up the longest paragraphs, drop history asides, and fix x64-only wording that applies to both native backends.
Also correct -JIT-a64-max-vregs, which is now off by default.
- Fix stale and missing details in jit.md
aec2ff00a4
Build
- Initial support for FreeBSD and the other BSDs
aee91bfcc9ZQuest Classic now builds and runs on FreeBSD (other BSDs untested).
Chores
- Add Panoply of Calatia to the memory and performance scripts
6f1b270144Its decompiled 2.50-era global script puts ~12.7k call sites in one 149k-instruction function, the worst case seen for JIT compilation. measure_performance.py now accepts default replays relative to the repo root (under .tmp/, skipped when missing), like measure_memory.py's replay uploads.
- Update replay_uploads_known_good_replays.json
98ab49e2fb
Refactors
- Use CheckList typedef for vector
8c2d8a1cbb - Only allocate combo weapon data and script metadata when used
433ebb351cDiscordEvery script slot reserved memory for its metadata (such as names and help text for its InitD and attributes), even though most slots are unused. It is now only allocated when set, which saves about 11 MB of memory.
- Stop copying sound effects when moving them
86a18e175cDiscordLoading a quest briefly kept extra copies of its sound effects in memory, which raised the peak memory usage of both the player and the editor by up to about 20 MB.
Regressed in 3.0.0-prerelease.160+2026-02-08 (58486c3f4a).
Player
- Share one buffer between all blank tiles
19db464e5eDiscordEvery one of the 214500 tile slots had its own memory allocated, even though most are blank. Blank tiles now share a single buffer in the player, which saves about 50-60 MB of memory for most quests.
ZScript
- Reading mapdata FFCs no longer allocates them
1e94d42973DiscordScreens only allocate memory for as many FFCs as they use, but reading any FFC property of a screen through mapdata allocated all FFCs up to the one read. A script that reads every FFC of every screen (as Crucible Crest's does on startup) made every screen allocate all 128, which used about 437 MB of memory.
- Keep less JIT analysis data in memory
d6cffc8ae5DiscordThe JIT compiles a script's functions as they become hot, and kept an analysis of each script's control flow in memory for as long as the quest was loaded, so that any function could be compiled later. It now builds that for each function just before compiling it, which lowers memory usage by about 100 MB for yuurand.
- Correct a stale comment about JIT yielding functions
81e8b3c36d - Explain the ZASM liveness analysis
6cb2acfbe2A comment above zasm_run_liveness_analysis covering what it computes, who consumes it (optimizer and JIT register cache), the dataflow equations, how calls, returns and suspends are modeled, and the implicit reads.
- Faster JIT calls in functions with many call sites
5e55db05e4Script functions with thousands of function calls in them, like the global scripts of quests decompiled from 2.50, previously called other script functions through the slower path under the JIT. Those calls are now as fast as everywhere else: the global script of "The Adventure of Link and Zelda: Panoply of Calatia" spends 3.3x less time running scripts.
- Halve the memory used to hold compiled scripts
3c2cc3af83DiscordEvery instruction of a compiled script took 32 bytes, half of which were for the string or array that a few instructions carry. They now take 16 bytes, which lowers memory usage by about 200 MB for yuurand and 40 MB for Stellar Seas, in both the player and the editor.
The code of a script can now have at most 65535 strings and arrays, and compiling one with more shows an error.
- Use much less memory to JIT compile very large scripts
e649140be6Compiling a very large script function with the JIT could briefly take over a gigabyte of memory - the global script of "The Adventure of Link and Zelda: Panoply of Calatia" needed 1.4 GB. Such functions are now compiled in parts of a bounded size, so no single compile needs more than a few tens of MB. Panoply of Calatia's peak memory drops from 1.6 GB to 450 MB (260 MB with 2 compile threads), and its scripts compile 8x faster, with no change in how fast they run.
- Also split very long blocks when JIT compiling in pieces
9d4c7f9a33 - Remove far branches from the arm64 JIT
2059454ed8 - Always resume compiled code with an indirect branch on arm64
eb704d60ac - Remove JIT size limits made redundant by pieces
0e51cdcc8a
Tests
- Cover re-uploading a replay in test_upload
c95c286f21Uploads half of a replay, then appends the rest (as if the player kept playing the same save) and uploads again, checking that the server replaces its copy with the longer replay.
- Fetch re-uploaded replays and pin known good ones in replay_uploads.py
f2b4c42a54A replay can be re-uploaded under the same key with more frames appended, but replay_uploads.py never re-fetched a replay it already had locally. Now a local copy whose size differs from the bucket's is re-downloaded (and logged).
Re-downloading alone would reset the local copy that gets updated/asserted, so an --assert run could fail for reasons unrelated to the change being tested, and different people could end up validating different versions of the same replay. So the known good list now also records how much of each replay was validated (steps and frames). Since uploads are append-only, cutting the latest upload to that length recreates the validated replay exactly, so --only_known_good runs the same replays for everyone. Each local copy remembers what it was made from and is only recreated in --update mode when that changes; --assert warns instead. --update_known_good records new pins.
The existing list was converted using the local copies it was made from, which still match the bucket.
ZScript
- Report debugger test failures instead of aborting
02d89ec6f3A failed assertion inside a debugger test coroutine escaped it and terminated the whole test process, so nothing said which check failed, and no later test ran. Now the failure is recorded on the task (and passed up to any task awaiting it), reported, and the debugger is set playing with its breakpoints removed so the replay can finish.
The four replay tests share one driver.
CI
- Pull requests from forks no longer fail on the private ZScript database
93d4fad8e3 - Use the runner's cached Python 3.12 instead of downloading 3.12.8
e6b2b4ae6dThe runner images no longer carry 3.12.8 in their toolcache, so every job downloaded it and then let pip upgrade itself from PyPI. A PyPI blip during that upgrade killed two replay jobs in one run (a third died in the requirements install). With a '3.12' pin, setup-python uses the cached 3.12.x (3.12.14 on Ubuntu, 3.12.10 on Windows and macOS) and skips that download entirely.
release.yml keeps the exact pin for now.