3.3.34-9-gee85046
What's new
GSE
3.3.34-9-gee85046 (2026-09-23)
Full Changelog Previous Releases
- #2109 keybinds: dual spec needs its own binds, not the other build's
Keybindings and action bar overrides are partitioned by saved talent loadout,
which works on retail because a loadout is 1:1 with the build being played. WoW
Forever has no saved loadouts. It has DUAL SPEC -- Primary and Secondary -- and
one specialisation per class, so GetSpec() is always "1" there and
GetLastSelectedSavedConfigID has nothing to return. Both builds produced an
identical key, shared one set of binds, and switching between them rebuilt the
bindings to exactly what they already were.
This is the one place on that client where the distinction is load-bearing.
Sequences do not care: SpecID is a label and nothing gates execution on it. Binds
and overrides genuinely partition, and dual spec is Forever's only 1:1 with the
build you are on.
GetBindingLoadoutKey is now the single answer to "which loadout is this", used
by the change-detection context and by both storage sites. A real saved loadout
still wins. The active spec group stands in only when there is no loadout AND
the client reports more than one group, so retail keeps its existing keys
untouched -- a retail character without a loadout still keys on tostring(nil)
exactly as before rather than having its saved binds orphaned under a new one.
The group is prefixed, so a group of 1 or 2 can never be read as a config id of
1 or 2.
Three sites in Events.lua computed this independently and now share it. Two of
them were wrapped inif C_ClassTalents and ..., which would have skipped the
spec-group path entirely on a client with no loadout API at all; the guard now
sits inside the helper where it belongs.
Tests cover both directions, because the retail half is the half that can break
silently: a saved loadout still wins, a saved loadout beats a spec group,
Primary and Secondary no longer collide, a single-group client with no loadout
keeps its old key, and the prefix keeps a group distinct from a config id.
Verified they bite -- disabling the spec-group branch collides Primary with
Secondary, and dropping the prefix collides group 2 with config id 2.
Also declares GetNumSpecGroups in .luacheckrc.
luacheck 0 warnings / 0 errors across 67 files; busted 491 -> 497 passing;
lua5.1 spec/run51.lua (PUC 5.1.5, what CI runs) all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 statics: label sequences by talent tree on a one-spec-per-class client
WoW Forever reports ONE specialisation per class -- Paladin 1486, Mage 1482 --
and exposes Holy / Protection / Retribution as trait GROUPS on that spec's tree.
GetSpecializationInfoByID with a retail spec id returns nil there, so the
hardcoded list resolved nothing and a sequence could only ever be labelled with
its class. The editor offered "Paladin" where the talent frame plainly shows
three trees.
The groups now map onto retail spec ids by orderIndex, so the sequence stores an
id the whole ecosystem ALREADY understands. GetClassIDforSpec, the spec filters,
the website and the Companion keep working untouched; nothing new crosses the
wire, and a Protection Paladin sequence is recognisably that on either flavour.
The translation stays a Mod-internal detail, which is right, because the Mod is
the only place with a client to read it from.
Only ids are hardcoded -- 27 numbers across the nine classes that exist there.
Names and ordering come from the client, so the era-correct label appears by
itself: Forever's rogue tree reports "Combat" and is stored as 260, which retail
calls Outlaw. orderIndex is used rather than groupID because it needs no
per-class dump and survives renumbering, and rather than displayName because
that is localised.
Two positions are judgement calls no API can answer. Rogue Combat -> 260, the
slot retail later renamed Outlaw. Druid Feral -> 103; Guardian (104) split out of
Feral in a later expansion and has no tree here, so 104 never appears. Both are
commented where they live.
Gated on SHAPE, not version: one specialisation for the class AND exactly three
groups on its tree. That matters for retail, where a class has three or four
specs and those groups are HERO TALENTS -- mapping "Herald of the Sun" onto a
spec id would rename every spec in the editor. The test for that gate
deliberately supplies three groups, so it fails on the spec count and not
incidentally on the group count; verified by weakening the gate and watching it
fail.
The class-level spec is registered too (1486 -> "Paladin"), so the id
GetCurrentSpecID returns on such a client resolves to a real entry meaning "any
build of this class" rather than an unknown number. Sequences already tagged
that way keep working, so there is no migration.
Sequence execution is unaffected and deliberately so: SpecID is a label for the
editor, the website and provenance. It is consumed only by GetClassIDforSpec to
pick a class bucket -- being in Holy has never stopped anyone running a
Protection sequence on any flavour.
Also declares GetNumSpecializationsForClassID in .luacheckrc; its sibling
GetSpecializationInfoForClassID was already there.
luacheck 0 warnings / 0 errors across 67 files; busted 484 -> 491 passing;
lua5.1 spec/run51.lua (PUC 5.1.5, what CI runs) all specs passing -- the spec
uses type() rather than assert.is_table, which run51 does not implement.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 statics: a spec name can be absent on a client that has the spec API
Creating a sequence on WoW Forever threw:
GSE/API/Statics.lua:223: attempt to concatenate a nil value
Seven SpecIDList entries are disambiguated as "Frost - Mage", because Frost is
both a Mage and a Death Knight spec. Both halves come from the client's
CONTENT: GetSpecializationInfoByID for the spec, C_CreatureInfo.GetClassInfo
for the class. Forever runs Retail's API surface on vanilla content, so
GameMode is honestly 12 and this retail branch is entered -- but there is no
Frost Mage to return and no Death Knight class, and the nil went straight into
a concatenation. determineClassName already guarded its half; the spec half
never did.
specWithClass returns nil when the spec is absent and the bare spec name when
only the class is absent. nil rather than a placeholder deliberately: the key
is then never set, so a client without specialisations ends up with the
class-only list it should have, instead of rows reading "- Mage".
All seven sites were vulnerable, not just the one in the traceback -- 64, 65,
66, 105, 251, 257 and 264. The report happened to hit Mage first.
Tests lift the helper out of the shipped file and drive it with each half
present and absent. Verified they bite: removing the nil guard reproduces the
concatenation error.
This is the third instance of one pattern and the pattern is worth naming: on
Forever, an API being present says nothing about the content behind it.
GameMode answers "what API level" and is right; it cannot answer "does this
client have vehicles / specialisations / skyriding", and nine sites still gate
spec behaviour on GameMode (CharacterFunctions 8 and 58, Statics 153/178/203,
Events 58, Utils 2127, Tracker 1796 and 1860). Every one of them takes the
retail path on Forever. This commit fixes the crash, not the model.
luacheck 0 warnings / 0 errors across 67 files; busted 479 -> 484 passing;
lua5.1 spec/run51.lua all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 qol: the capability cannot be probed — gate the block instead
b555f0d0 was wrong in two ways and made the symptom worse: 4 errors at login
instead of 3.
First, it guarded only UpdateVehicleBar. Every part of that block hands a
snippet to the restricted environment -- the Execute in UpdateVehicleBar, the
_onattributechanged body, and RegisterAttributeDriver firing that body -- so
three sites error, not one, which is exactly the 3x originally reported.
Second, and the reason it went backwards: a trial :Execute() wrapped in pcall
still reports. The restricted environment calls the error handler directly
rather than raising something pcall can swallow, so the probe became a fourth
error carrying its own body, "GSEVehicleProbe = nil". Asking the question by
executing is itself the failure. The probe is gone.
Nor can API presence answer it. Forever ships the retail API surface whole:
HasVehicleActionBar, HasOverrideActionBar, GetBonusBarOffset and
C_ActionBar.GetVehicleBarIndex all exist and are callable on a client with no
vehicle, override or skyriding bar to report. That is the same fact that makes
GameMode honestly 12 and lets the >= 11 test through.
With execution and presence both ruled out, what is left is the flavour, so the
whole block is skipped on Forever. Stated plainly: this is a temporary
exclusion, not a claim the feature is meaningless there. [possessbar] IS valid
on vanilla content, so these binds would be genuinely useful on Forever -- they
simply cannot work while no snippet compiles, and three errors per login is the
worse of the two nothings. Remove the gate when Blizzard ships
loadstring_untainted and the feature works there unchanged.
Fails open: if the flavour cannot be determined the block runs exactly as
before, so no other client is affected.
luacheck 0 warnings / 0 errors across 67 files; busted 479 passing; lua5.1
spec/run51.lua all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 qol: don't trip over a restricted environment that cannot compile
A completely empty GSE install on WoW Forever threw this three times at login,
before the user had created anything:
RestrictedExecution.lua:79: attempt to call a nil value
... GSE_QoL/QoL.lua:1527: in function 'UpdateVehicleBar'
withloadstring_untainted=nilin the locals. Blizzard's restricted
environment on Forever has no loadstring_untainted, so every :Execute() fails
to compile its snippet. That is Blizzard's bug and GSE cannot fix it -- but
UpdateVehicleBar runs unconditionally at login, so GSE was the thing that made
it visible, three times, to someone who had not used the feature.
UpdateVehicleBar now asks whether secure snippets compile at all and returns
quietly if they do not. The feature cannot function without them, so there is
nothing to be gained from building a snippet to watch it fail.
Deliberately a CAPABILITY test, not another version test. TheGameMode >= 11
gate above it is right: GameMode answers "does this client have Midnight's API
level", and Forever genuinely does -- that is what #2109 established and why
this code now runs there at all. Whether a given API actually WORKS is a
separate question, and no version check can answer it. A Forever special-case
would be wrong again on the next client that ships the same hole.
Probed lazily rather than at file load, because the restricted environment is
not necessarily ready that early, and cached so a broken client pays one pcall.
The real Execute is deliberately left unguarded: once the probe says snippets
compile, a failure there is a genuine bug in the snippet or the user's binds
and should still surface.
NOT addressed here, and it matters more: GSE/API/Events.lua drives the secure
click model through the same mechanism (SHBT:Execute and :WrapScript). Those
run only when a sequence is armed, which is why an empty install saw just this
one -- but they compile through the same path, so GSE's core secure execution
is expected to fail on Forever too. Inferred from the shared code path, not yet
observed. Deciding whether that should go quiet or fail loudly is a product
call, not a bug fix.
luacheck 0 warnings / 0 errors across 67 files; busted 479 passing; lua5.1
spec/run51.lua (PUC 5.1.5, what CI runs) all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 toc: declare Forever's interface so GSE loads on it
Every GSE .toc declared 11509, 20506, 50504, 120007, 120100 -- Classic Era,
Anniversary, MoP, and two Midnight builds. Forever is none of those, so on a
Forever client GSE is out of date and does not load at all unless the player
ticks "Load out of date AddOns". Everything else done for #2109 -- the GameMode
mapping, the TOC flavour split, the expansion maps in the API, Companion and
interface -- is unreachable if the addon never loads.
Confirmed against a live install today: product wow_classic_beta, build
1.60.1.69893, running WowB.exe (the retail beta binary, not WowClassicB.exe,
which is a leftover from the 5.5.0 MoP beta that used to occupy that folder).
16001 is still DERIVED rather than read from the client -- the formula every
stream obeys, major10000 + minor100 + patch, so 1.15.9 -> 11509 and
5.5.0 -> 50500. That formula is now corroborated from the install itself: the
folder's Config.wtf carriesSET lastAddonVersion "50500"from its previous
life as the MoP beta, which is that CVar recording an interface version in
exactly this shape. Once a Forever login completes, lastAddonVersion rewrites
to the real number and this can be confirmed or corrected in one place per tree.
An extra entry costs nothing if it is wrong: an interface the client does not
recognise is ignored, so the worst case is the current behaviour. Not adding it
guarantees the addon does not load.
Inserted in ascending order and the files written with their CRLF endings
preserved, so the diff is one line per .toc and nothing else.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2106 options: surface the editor draw switches in Tools & Diagnostics
The widget pool and layout batching switches were reachable only from /gse.
A command you have to be told about is barely better than the loose global it
replaced -- someone hitting a redraw bug has no way to find it.
Both now appear under Tools & Diagnostics as "Editor Draw Diagnostics", and
deliberately not under Developer Debug: that subcategory is gated on
GSE.Developer and stripped from packaged builds by --@debug@, so it cannot
help the people who actually hit these bugs and file the reports. That is the
whole audience for a kill switch.
Both read positively -- checked means the optimisation is ON -- and store the
inverse. "Disable widget pool", checked, reads as a double negative on screen;
"Reuse editor widgets", unchecked, does not.
The tooltips say what the optimisation buys, that turning it off is a
diagnostic step rather than a preference, the equivalent slash command, and
when the change takes effect (reload for the pool, reopen the editor for
layout). Written as plain strings rather than L[] keys, like the other recent
additions in this file, so they do not depend on a locale entry that does not
exist yet -- luacheck cannot see a missing AceLocale key.
luacheck 0 warnings / 0 errors across 67 files; busted 479 passing; lua5.1
spec/run51.lua all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2106 debug switches: options with commands, not loose globals
/run GSE_NoWidgetPool = truewas the documented way to turn the widget pool
off for soak-testing, and_G.GSE_NoLayoutBatchdid the same for the editor's
per-chunk layout batching. Both were loose globals: set by poking the global
table, gone on every /reload, and present nowhere in the addon's own surface --
not in the options, not in /gse, not discoverable by anyone who had not read
the source or an issue thread.
Both are now GSEOptions fields driven by real commands:
/gse widgetpool on|off
/gse layoutbatch on|off
Either with no argument reports the current state. Being in GSEOptions they
survive a /reload, which is what a soak test actually needs -- the old globals
had to be re-set after every reload, and forgetting that looks exactly like
"the bug went away".
The widget pool has two call sites -- banking on Release, handing out on
Create -- so it reads through one local helper rather than testing GSEOptions
twice, or the pool could end up half-on.
Audited the rest of the global namespace for the same pattern. GSE_NoWidgetPool
and GSE_NoLayoutBatch were the only two behaviour switches; everything else
matching GSE_* is a frame name, a Statics constant, or -- like
GSE_ActionButtonUseKeyDown -- already a registered Settings proxy.
Note for anyone following the #2106 thread:/run GSE_NoWidgetPool = trueno
longer does anything. The equivalent is/gse widgetpool off, and it now
persists across the /reload the repro asks for.
luacheck 0 warnings / 0 errors across 67 files; busted 479 passing; lua5.1
spec/run51.lua all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2106 editor: guard the write, not the widget lifecycle
Still reproduced on 3.3.34 after e446d5b: deleting one Action block rewrote 36
others from {type="spell", spell=49998} into a macro carrying the localised
spell name, in 7 bursts, silently. @liuli195 captured the chain with SetText
hooks and it is not the path the previous fix closed.
e446d5b cleared stale callbacks in Release and stripped IndentationLib in
resetForReuse -- both aimed at the teardown fire (IndentationLib.disable ->
SetText -> live handler). The captured stacks contain neither IndentationLib
nor Release. The fire arrives from GSE's own deferred, chunked rebuild:
buildChunk draws 4 blocks a frame, so a block's text is injected several frames
after the delete, and SetText fires OnTextChanged synchronously through the
widget's engine bridge. buildGeneration already guards against writing into a
torn-down container, but not against a box firing a handler from a previous
life. 14 of the 36 rewrites were paired to one box instance receiving the
SetText and firing in the same instant -- one box rewrote 8 actions.
So this stops chasing lifecycle holes and makes the write itself fail-safe,
which was @liuli195's suggestion and is the right altitude: the handlers now
refuse to write unless the fire is a genuine user edit of the block the box
currently belongs to.
Two conditions, because there are two ways a fire is not that:
setTextQuietly flags the widget for the duration of a programmatic SetText,
which is synchronous, so our own injection cannot be recorded as an edit. This
matters most for the macro box, which holds the SPELL NAME for a spell action
while its handler stores .macro and clears .spell unconditionally -- one
spurious fire is all it takes to convert a working spell action into a macro
named after itself. Same shape as the GSEMacroEditorColoring guard already used
by RefreshMacroEditorColoredText.
editIsStale also compares the box's current owner against the action the
handler closed over. Every handler here captures a keyPath, and the pool hands
one box to several blocks inside a single rebuild, so a fire carrying a
previous life's handler writes through that life's keyPath -- a different
action. Ownership is claimed immediately after UI:Create, before anything
touches the text, because that is precisely the window the stale fire lands in.
Both handlers also bail when Actions[keyPath] is gone: a delete renumbers every
keyPath after it and these are captured, so indexing a removed action errors
and writing to a shifted one corrupts the wrong block silently, which is worse.
Tests lift both helpers out of the shipped file and drive them through a fake
box whose SetText fires synchronously, including the full shape of the bug -- a
box recycled to a second block while the first block's handler is still
attached. Verified they bite: removing the owner check fails 2, removing the
injection guard fails 5 of 9.
Does not touch e446d5b. Those holes were real; they were just not this one.
luacheck 0 warnings / 0 errors across 67 files; busted 470 -> 479 passing;
lua5.1 spec/run51.lua (PUC 5.1.5, what CI runs) all specs passing.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y
This mod has no additional files
