Features
- Save Menus now support Shadows for text options
03d939655eA single shadow type can be set per-option. Two shadow colors can be set per-option; one for normal display, and one when the option is 'selected' (matching the two text colors)
- Save Menus now have 'Selected X/Y Offset'
f3456217a8The option the cursor is on can be offset by a specified X/Y. This allows ex. indenting the selected option.
- Save Menus now have 'Misc Texts'
14d156444eThis allows drawing arbitrary non-option text to the menu. This can be used for things like menu titles ('Game Over'), or flavor text ('You failed to save the land...')
Editor
- Ctrl+Click Color Swatches uses previous color
d076e3e217This mimics how Ctrl+Click works for Tile/Combo swatches, but now for colors. Should be especially useful when setting up the same color repeatedly in places such as Save Menus.
- Try each recent quest at startup until one loads
065e21f813DiscordWhen "Open Last Quest" is enabled and the most recent quest is missing or fails to load, the editor now silently tries the next quest in the recent list, and so on. Only when none of them can be loaded does it fall back to creating a new quest, without showing any error. Previously it gave up after the first entry, which was annoying when using ZC from an external drive that gets assigned a different path on each computer.
ZScript
- @ScriptInfo and @ScriptSetup annotations
c7f6da771bSpecify an info/description string and a setup instruction string for your scripts! These can be read in the Editor in the script setup dialog.
Bug Fixes
- File dialogs opening on a folder that does not exist
ea5ec359e1Exporting a tile page could open the file dialog on a bogus folder, so it listed nothing at all - not even a ".." to navigate out of. Any file dialog whose starting folder no longer exists now falls back to the closest folder that does.
The bogus folder came from a stale export path saved in the config by versions before 2.55.10 (9df104fd95), which was never cleaned up. That value is now repaired on the next launch.
- checkPath misreporting special files
a2fdd56776
Player
- Big 4-directional enemy fire/attack frames overlapping walk tiles
95dd9b2eafThe 'Big + Firing' enemy animation styles placed their firing frames too close to the walking frames, so a big enemy's attack animation displayed walking tiles from the wrong direction instead of its own attack frames. The fire frame offset now clears the full block of walking tiles.
Broken when added in 2.55-alpha-100 (9753e33c).
ZScript
- npcdata and messagedata arrays used the wrong object
848bc89dd5DiscordEvery array on an npcdata read and wrote enemy 0 instead of the npcdata the script loaded, so
npcdata->Flags[],->Attributes[],->Defense[],->InitD[],->Shield[],->BFlags[],->MoveFlags[]and the two weapon arrays all reported the "(None)" enemy's values - usually zero. Scalar members like->Animwere unaffected.messagedata->Flags[]and->Margins[]had the same problem, always acting on message 0.Regressed in 3.0.0-prerelease.115+2025-06-30 (df3ce2079c); messagedata in 3.0.0-prerelease.172+2026-04-14 (d7523f4f81).
save_menu->Flags[]bad index bounde521e3dc49- Bad results from an overly long number in a ZASM tag
93215b2d26 - Backslashes lost from script metadata help text
eec081f811Help text on a script's attributes, flags or InitD - anything written in a doc comment - lost its backslashes when the quest was reloaded, mangling the text after them. "C:\new" came back as "C:" followed by a newline and "ew".
- Restore
lweapon->Max()anditemsprite->Max()39a038d6ebBoth lost their deprecated function form when it was converted to
@deprecated_getter:itemspritenever got the name, andlweaponwas given the name of a ZASM register rather than the function.Regressed in 3.0.0-prerelease.139+2025-10-24 (c2a70e4d5c).
- Deprecated getters were not callable on most classes
5c24245adcCalling the
@deprecated_getterform of a variable failed to compile with "has not been declared" for every class that uses a ref variable -npc,eweapon,lweapon,itemsprite,combodataandmessagedata. Sonpc->Max(),combodata->PosX()and friends could not be used at all, despite being documented and warned about as merely deprecated.Regressed in 3.0.0-prerelease.139+2025-10-24 (c2a70e4d5c).
- Backslashes eaten from annotation strings
b36567cb55A string in an annotation lost any backslash it contained, along with the character after it: "C:\new\table" was stored as "C:ewable".
- Strings longer than 65535 chars no longer broken
3210067f3eInternal string and array helpers used 16-bit loop indices, so operating on an array with more than 65535 elements would wrap the index back to zero. For example,
strcmp()compared only the first 65536 chars (and never reached the null terminator), and writing such an array could hang the game. Arrays can hold up to 214748 elements.This bug has existed since ZScript arrays were added (2.50-era).
- Call-heavy scripts ran far slower than the interpreter on Apple Silicon
adb0a5e4f7The AArch64 JIT resumed execution after a script function call or wait by comparing against every resume point in the function, one at a time, on every entry. Scripts with many function calls in one big function - common in decompiled 2.50-era quests - re-walked that chain on every return from a call, making the JIT up to 30x slower than the interpreter (one quest's replay: 10+ minutes, now 15 seconds), and getting worse the deeper into the function the game's active call sites sit.
Functions with more than 32 resume points now resume via an indirect branch like the x64 backend. The comparison dispatch is kept for small functions, where its per-edge register fixups win.
Regressed recently in 3.0.0-prerelease.201+2026-07-19 (489060ad94).
Documentation
- Update jit.md for hybrid a64 resume dispatch and direct calls
d645501fd9
ZScript
- Explain @deprecated_getter with a note
1d03f22bd0DiscordVariables that also have an older function form printed a bare "deprecated_getter Foo" line, which didn't explain what it meant. They now get a note that names the function, with the parentheses, and says to prefer the variable.
- Explain alias and future-deprecation annotations
458edd42cbDiscordFollowing the same fix for deprecated getters, the remaining alias annotations printed a bare tag line that didn't explain what it meant. Aliases now get a note after the description that names them, with parentheses for functions, and says whether the name is deprecated. Symbols planned to be deprecated later get a "Deprecated in a future version" callout instead of a bare tag.
Also documents the future-deprecation annotation in the binding files, which was missing.
Chores
- Remove unused path validation helpers
4e56792d6b - Fix manage_commits.py reporting wrong conflict-free cherry-picks
021f3f1e90The conflict dry-run based its temporary worktree on the current HEAD, so running the script from the main checkout tested picks against main instead of releases/2.55 - reporting 16 of 21 candidates as clean when a real cherry-pick onto 2.55 found 5, including two that would have misapplied (one silently recreating a file 2.55 doesn't have, one patching a function 2.55 doesn't have). The worktree is now always based on releases/2.55, matching the rest of the script.
Clean picks whose touched paths don't exist on 2.55 now print a warning (a pick can create the file or remap it via rename detection), and the output notes that applying cleanly is not the same as compiling - a pick can land on lookalike context that references identifiers 2.55 lacks.
Refactors
Player
- Levels with palette cycling ran much slower than necessary
ffeed52acePalette cycling advances up to three cycles at independent speeds, so a single level can produce a few hundred distinct palettes. The color lookup tables rebuilt on every palette change (~1ms each) were cached, but the cache only held 64 palettes - and cycling revisits palettes in order, which is the worst case for an undersized cache: entries were evicted right before they were needed again. One tested quest rebuilt tables 42,000 times for only 385 distinct palettes, eating ~40% of frame time.
Now only the expensive RGB lookup table is cached per palette (5x smaller entries, so ~640 palettes fit in 20MB), and the cheap translucency tables are derived from it when the active palette changes. Replaying a palette-cycling-heavy quest went from 158s to 96s.
Palette cycling has always had this cost; the cache added in 2.55.4 / 3.0.0-prerelease.55 (c3889779) was too small to help it.
ZScript
- Derive scripting array ref registers from the object type
b262acee18 - Call directly between JIT-compiled script functions
48fc5b7218Script function calls under the JIT previously round-tripped through the C++ driver on every call and return - a function lookup, a resume lookup, and a full function re-entry each time. Scripts making many small function calls (std.zh-heavy quests reach thousands per frame) spent more time in that overhead than in the scripts themselves.
Calls to functions that can never yield now call the compiled code directly on the machine stack. Yielding callees, not-yet-compiled callees, and calls past a native depth cap keep the driver path. Across the benchmark set, total script time drops ~30% (yuurand_riviere -62%, freedom_in_chains -35%, 100_rooms_of_wisdom -29%).
- Reduce per-call and per-array-access script engine overhead
60deb04a95Script engine time on a call- and array-heavy replay (Yuurand) drops about 6%.
- Fast paths for the hottest script registers
01776fc9d6Script engine time on Yuurand dropped by 10%.
Tests
- Unit tests for the util string and path helpers
166deb0c99Covers everything fixed in the recent util.cpp work: the two fixed-size buffers that could be overrun (ffparse2 and unstringify_vector), the escape round-trips that ate backslashes (unescape_characters, disallow_escapes), escape_string leaving quotes and backslashes unescaped, checkPath treating special files as regular ones, and nearest_existing_directory.
- Fix replay_uploads.py failing on Windows with too many replays
c22ea5d8e6Running the uploaded replays passed every replay path as its own command line argument. At a few hundred replays that exceeds the ~32k character command line limit on Windows, and the run died before starting with "[WinError 206] The filename or extension is too long".
Pass the arguments via a file instead.
CI
- Vscode extension tests failed to launch VS Code on mac
797afd4bd9
Misc.
- Fix measure_performance.py timings on Windows
3267b80569