3.3.35
What's new
GSE
3.3.35 (2026-10-13)
Full Changelog Previous Releases
- #1912 #1980 EllesmereUI late-built bar buttons get the right-click picker; Blizzard bar prefix lengths
EllesmereUI builds no buttons at login for a hidden bar with no key bound
(Bars 6-8 by default) and builds them when the bar first shows, after GSE's
one-time PLAYER_ENTERING_WORLD hook pass. Re-run the EABButton hook pass
whenever an EABBar_<key> frame shows; gseRightClickHooked prevents double
hooks.
overrideActionButton compared Blizzard bar prefixes with wrong lengths, so
only ActionButton and Gamepad buttons took the Blizzard path. Correct all
seven lengths.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01MgTiaCHhhMgQjtZtJBiN5e - Merge pull request #2144 from LarryThiessen/tree-footer-option-labels
#2143 Tree footer laid out like the editor's; shorter Action Bar Overrides labels - #2143 Key column: controller glyphs at 22px in a 110 column
At 16 the glyph atlases, which carry their own padding, read as specks.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - #2143 #2149 Key column glyphs from Blizzard's own gamepad keys; popup switch on every client
- Forever's gamepad bars: the Key column draws the glyphs the bar itself
draws. Each button's ButtonIcon.mappedButtonKey, and the bar's trigger
modifiers (left, right, both, as ActionBarTemplates.xml wires them),
through InputIconTextureSetUtility for the active controller style. The
bindings lookup it replaces found nothing: Blizzard draws these from fixed
keys, not bindings. - Binding keys are read in the action's own binding context, as Blizzard's
Key Bindings screen reads them. - Options: the Button Binding popup switch was registered only above
GameMode 10, on a comment that Classic needs another menu API. It does not:
the menu runs on every client through GSE.OpenContextMenu, so the switch is
shown on every client.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- Forever's gamepad bars: the Key column draws the glyphs the bar itself
- Merge master into tree-footer-option-labels (#2144)
- Merge pull request #2140 from LarryThiessen/import-compare-layout
#2139 Import and Sequence Compare windows: centred actions, even boxes - #2137 #2138 #2149 #2150 Bindings: binds by spec capability, spec and loadout layers, Button Bindings panel
Specs the binds are kept by, decided by what the client can do, not its
version (Profiles.lua):- A client with dual spec (C_SpecializationInfo.SetActiveSpecGroup: TBC
Anniversary, Wrath to WoD, Era, MoP Classic, Forever) keeps them by spec
group, Primary from the start and Secondary once learned, so the key does
not move when dual spec is learned. Secondary starts empty. - Otherwise several specialisations (Retail): by specialisation.
- Talent loadouts are Retail's only (GSE.HasTalentLoadouts).
Spec and talent loadout layers (#2149, #2150): - Right-click on an action button says when the active loadout supplies the
override, and offers All Talent Loadouts or "<loadout> only". All Talent
Loadouts also drops that loadout's own entry, so the change shows. - Drag changes the layer the button's override already comes from, and says
so when that is the loadout's. - Clear Override clears only the layer applied; it used to wipe the spec's
and every loadout's. - A loadout page lists the spec's binds it takes unchanged, with Override to
copy one in; a spec page marks rows a loadout replaces.
Button Bindings panel (#2137, #2138 -- the ideas, built fresh): - Button names as bar and slot, cut from the front where still too long.
- A read-only Key column: every key WoW's Key Bindings put on the button,
all of them in its tooltip. Forever's gamepad bars show the controller
input (crossbar modifiers + d-pad / face button) as glyphs. - State column only for Warrior, Rogue and Druid with a LibActionButton bar,
or a saved row that has a state.
Tree: Bindings > Button Bindings > <class> > <spec> > <loadout> By Type and
By Collection; By Class puts Bindings in the class's group. Tree paths read
the spec segment on every client -- on Forever it was read as a loadout.
Message listeners: each module registers on its own AceEvent embed, since
AceEvent keeps one handler per message per object.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- A client with dual spec (C_SpecializationInfo.SetActiveSpecGroup: TBC
- Merge pull request #2136 from LarryThiessen/macro-variable-layout
#2135 Macro and variable pages: layout tidy - #2135 Variable Notes - Help page: Save centred
As the Configuration page's Save / Delete Variable row is now.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - Merge master into macro-variable-layout (#2134)
Both pages: keep #2135's 8 spacer under the Configuration heading and
#2134's scopeDropdown argument to DrawElementVersionConfig.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - Merge pull request #2134 from LarryThiessen/config-one-page
#2133 Configuration page as one page; drag icon and base icon; macro/variable versions laid out the same - #2133 Review tidy: one compact edit box, overrides message, no dead code
- Sequence Name, Author and Help Link share compactEditBox; the Author
box's unused label-above branch and setEditBoxLabelGap are gone. - Unused constants removed: FIELD_WIDTH, METADATA_LAYOUT_GAP_ALLOWANCE,
ROW_HEIGHT, METADATA_BOTTOM_PADDING. - GSE.ReloadOverrides sends GSE_ACTIONBAR_OVERRIDES_UPDATED; the Config
drag icon listens for it instead of hooking GSE.CreateActionBarOverride,
so a removed override, or a spec or loadout change, re-mirrors it too. - Label widths measured by one widestText(fontObject, texts).
- DrawElementScope returns its dropdown and the variable and macro pages
pass it to DrawElementVersionConfig, instead of editframe.elementScopeDropdown.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- Sequence Name, Author and Help Link share compactEditBox; the Author
- #2133 First-step icon, Notes - Help pages, Forever contexts and specs
Sequence icon- A sequence's icon at rest is always its first compiled step's: the
per-player base icon and its right-click picker are gone, and
GSE.GetSequenceStartIcon reads the compiled step again. - The Config drag icon copies a bar button's look but always shows that
icon; it carries the override button's GSE watermark (one shared
helper, following the watermark option), with DRAG ME! and a caption
saying why it shows that icon below it.
Notes - Help - Sequences, variables and macros each get Configuration and Notes - Help
tree children; Notes - Help is the Help Link and a Help Information box
filling the pane (GSE.GUI.DrawNotesPage). Each keeps its notes rule. - Variables and macros gain MetaData.Helplink, sanitised on save. Drawing
the box no longer writes the default into an empty link. - A variable's two pages share one draft; the Notes page has its own Save.
WoW Forever - No Delves/Scenarios, Arena, Heroic, Mythic, Mythic+ or Timewalking:
dropped from contextVersionPriority, so no rows, never picked, never
blocking a version delete; the editor drops set values on Save. - Build 1.60.1.70338 removed the global GetNumSpecializationsForClassID,
so no talent-tree spec registered (1486 showed blank). Read from
C_SpecializationInfo first. - GetClassIDforSpec falls back to GSE.SpecIDClassList: picking
Protection filed the sequence as Global. Class-level ids are recorded. - Talent-tree icons recorded per spec; GSE.GetSpecIcon serves them to the
spec lists and the tree.
Spec lists - Specialization/Class ID dropdowns (sequence, variable, macro, the
tree's spec picker) list Global, then classes with icon and colour and
their specs under them, alphabetical, keyed by id (Holy is two specs). - Dropdown lists open from the box's left edge.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- A sequence's icon at rest is always its first compiled step's: the
- Forever: one GSE.IsForever check, set in Init.lua
The Forever rule is worked out once, where Init.lua already derives
GameMode from the version, and everything with Forever specifics reads
GSE.IsForever. GSE.KeybindingsEnabled() re-derived it from GetBuildInfo
and TOCFlavour, and is gone.
(cherry picked from commit def9a022f49b35097a5d2674d7205bc84ba33892, without
the Bindings-section removal it was stacked on)
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - #2133 Select Icon menu: a branch's "!" spell is offered
getAllConditionalBranchSpells read each branch with GetConditionalsFromString
and a whitespace trim, so "/cast [stance:1] !Maul; Swipe" looked up "!Maul",
found nothing, and the menu (and now the base icon menu through
GSE.GetActionIconCandidates) never offered Maul. It reads through
GSE.GetMacroClauseTarget like the other icon readers.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - Merge master into config-one-page (#2130's GSE.GetMacroClauseTarget)
- Merge pull request #2130 from LarryThiessen/editor-fixes
#2129 Editor fixes: reopen page, Button Bindings first draw, pooled edit box caption, checkbox hover, tree width, tooltip wrap, castsequence icons - #2129 Editor: every area reopens on the page last used; Bindings panels refit on show and resize
Variables, Macros and Bindings each remember the tree node last opened in
them (lastNodeByArea) and reopen by selecting it, as Sequences already did,
so the tree highlight matches the page. A node that no longer exists falls
back to the area root. Retires RestoreLastNode and lastArea/lastKey/lastClassId.
The Bindings chooser and the Keybind/Button Bindings panels re-measure on
OnShow/OnSizeChanged instead of relying on a next-frame select in
ShowKeyBindings.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - #2129 Macro clause target: one reader for conditionals, reset= and !
GSE.GetMacroClauseTarget (translator.lua) reads the spell or item a single
macro clause names through GetConditionalsFromString, which already moves a
castsequence reset= into the mods, then drops the "!" toggle guard. It
replaces the copies in Native (option preview), Storage and the Editor (icon
candidate normalisers) and the extra reset= strip in GetSpellsFromString,
which was dead code behind GetConditionalsFromString. A castsequence
"!Spell" element now resolves there too.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com - Merge pull request #2146 from LarryThiessen/keybind-combat-close
#2145 Key binding fields: no blocked-action report when the editor closes in combat - Merge pull request #2132 from LarryThiessen/dropdown-fill-frame
#2131 Dropdowns: Blizzard art fills its frame again; click area kept inside it - Merge pull request #2142 from LarryThiessen/qol-icon-picker
#2141 QoL icon picker: a GSE window with search; open to other menus - #2133 Configuration page as one page; drag icon and base icon; macro/variable versions laid out the same
- Config and Notes tabs merged: Sequence Name, Author, Help Link at the top;
Help Information box (GSE.Tools notes read-only, else MetaData.Help);
Specialization / Default Version centred; PvE | PvP each half the page,
dropdowns to their column's edge; Dependencies sized for three rows;
Disable Sequence centred at the bottom. - The drag icon: big, centred under PvP, copies the bar button's look and
key when the sequence is on a bar, "DRAG ME!" label (red on hover); the
drag shows its icon at once (button scan deferred a frame). - Right-click the drag icon: choose a base icon from the icons the
sequence's blocks offer (GSE.GetActionIconCandidates, shared with the
block icon menu), or any icon via GSE.ShowIconPicker when present. Kept
per PC (GSEOptions.SequenceBaseIcons); bar buttons show it while the
sequence is at rest, the step icons while it runs. - Macro and variable pages: Specialization, Default Version and PvE | PvP
laid out as on the sequence page; version bar controls aligned.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- Config and Notes tabs merged: Sequence Name, Author, Help Link at the top;
- Merge pull request #2148 from LarryThiessen/drag-scan-visible
#2147 Drag to action button: scan only shown frames under UIParent - Merge pull request #2154 from LarryThiessen/abo-show-empty-slots
#2153 Action bar overrides: stay visible with Always Show Buttons off - #2153 Action bar overrides: stay visible with Always Show Buttons off
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2147 Drag to action button: scan only shown frames under UIParent
Walking every frame with EnumerateFrames froze a Retail UI with many addons
for 6-8 seconds when a drag started. Hidden buttons are no drop target, so
walk down from UIParent and skip hidden branches.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2145 Key binding fields: no blocked-action report when the editor closes in combat
ControllerKeybinding's stopCapture runs on every release and set keyboard
propagation each time. SetPropagateKeyboardInput is protected in combat; the
pcall kept the error quiet but WoW still raised ADDON_ACTION_BLOCKED against
GSE_GUI. Set it only when it changes, and never in combat (at rest it is
already on).
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2143 Tree footer laid out like the editor's; shorter Action Bar Overrides labels
- The tree's list ends as high above its window's bottom as the editor's
main box does above its footer; the grouping button centred in that space
with All Sequences centred under it. The button reads "Sort By Type /
Class / Collection". - Options, Action Bar Overrides: "Enable Popup", "Show Watermark",
"Show Label", "Force Icon Coloring" (the heading already says it is about
button bindings). Saved settings are unchanged.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- The tree's list ends as high above its window's bottom as the editor's
- #2141 QoL icon picker: a GSE window with search; open to other menus
- The native icon picker now sits in a GSE window (title bar, X to close,
GSE skin) instead of the macro popup's frame; header and filter on top,
the grid under them, nothing overlapping the first row. - Search along the bottom: a number finds that spell ID's icon and that icon
file ID; text finds the player's spells whose names contain it. No match
turns the text red and leaves every icon showing. - GSE.ShowIconPicker(callback) exposes it, so other menus (the Configuration
page's base icon) can offer "Choose any icon..." when QoL is loaded.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- The native icon picker now sits in a GSE window (title bar, X to close,
- #2139 Import and Sequence Compare windows: centred actions, even boxes
- Import window, collection import and the Companion queue: buttons centred.
- Sequence Compare: the two boxes split the width 50/50, edge to edge; the
import action, new name and Continue stacked and centred under them, the
action dropdown compact so it sits level with its label.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- #2135 Macro and variable pages: layout tidy
- Configuration heading centred, with space above and below it.
- Macro box 7 lines; "Used by Sequences" full width, flush with the rows above.
- Variable page: Save / Delete Variable centred; Execute on Event row lined
up (labels on one line, boxes on one line, toggle centred in its space);
Add from List frame holds its art.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- #2131 Dropdowns: Blizzard art fills its frame again; click area kept inside it
Since #1914 the native dropdown's art was sized width - 40 and anchored 16
left, so its visible right edge stopped ~26 short of the widget's frame on
every Blizzard-style dropdown. width - 14 puts it on the frame's edge. The
click overlay (modern skin / long lists) is clamped to the widget's own width
so Blizzard's wider frame cannot take a click meant for the next control.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2129 Editor fixes: reopen on last page, Button Bindings first draw, pooled edit box caption, checkbox hover, tree width, tooltip wrap, castsequence icons
- Reopen after /reload lands on the last tree page (openMode follows tree clicks).
- Button Bindings drawn after the reopened editor is shown, not into a hidden one.
- A pooled EditBox put back its label layout on reuse (compact mode leaked).
- Checkbox hover grow only under the ElvUI skin.
- Tree width measured in the rows' real font.
- Long tooltips wrap at 360.
- GetSpellsFromString skips a castsequence reset= clause; single-spell macro icons found.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- #1828 Update TOC for 12.1.5
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1 - Merge pull request #2114 from LarryThiessen/notes-panel-links
#2113 Notes panel: full note every time, steady styling, links to copy - #2113 Notes panel: localized link strings, width for macros and variables
- The copy-link popup and the link hover tooltip go through the locale
table like the rest of the editor. - addRenderedNotesPanel's doc comment is back above the function; the link
helpers had been inserted between them. - The macro and variable pages now pass their editor frame, and
CreateReadOnlyNotesPanel derives the content width from it the way the
sequence page does. The panel measures the note's height at that width,
and without it a page not yet laid out fell back to the 120px minimum and
wrapped the note into a narrow column.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1
- The copy-link popup and the link hover tooltip go through the locale
- Merge master into notes-panel-links
Brings the branch up to master, which now has variables and macros storing
notes as sequences do (MetaData.Notes / MetaData.Help) and drawing them
through the same read-only panel this PR changes.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1 - Merge pull request #2116 from LarryThiessen/tree-height-refresh
#2115 Tree: refresh on a height change, not only on a width change - Merge pull request #2118 from LarryThiessen/chooser-hotspot-scroll
#2117 Bindings chooser: column highlights scroll with the page - Merge pull request #2120 from LarryThiessen/chooser-active-loadout
#2119 Bindings chooser: tiles go to, and light, the loadout you are in - Merge pull request #2121 from TimothyLuke/dependabot/npm_and_yarn/undici-6.29.0
Bump undici from 6.28.0 to 6.29.0 - Merge pull request #2126 from TimothyLuke/dependabot/npm_and_yarn/brace-expansion-5.0.12
Bump brace-expansion from 5.0.9 to 5.0.12 - Merge pull request #2127 from TimothyLuke/drag-sequence-to-actionbar
Drag a sequence from the editor onto any action button - Drag a sequence from the editor tree onto an action button
Dragging a sequence's row out of the editor tree now does what dragging the
icon on its Config tab does: GSE's copy of the icon follows the mouse, and
releasing over an action button puts the sequence on it.- The tree widget reports a held row leaving the tree: OnButtonDragOut on
the way out, OnButtonDragOutEnd on release, in place of the in-tree drop.
The held row keeps the mouse, so it is checked per frame while the button
is down; a release the row never hears about (hidden or recycled mid-drag)
still ends it. - Dragging within the tree is unchanged, so reordering versions still works.
- Only sequence rows start it; versions, variables, macros and headings do
nothing when dragged out.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1
- The tree widget reports a held row leaving the tree: OnButtonDragOut on
- Drag to action button: skip frames whose GetName is not a name
Starting a drag errored on Blizzard's damage meter: its rows define
GetName() to return their name FontString, and the target check passed that
to string.sub. Only a real string name counts now, and the check runs under
pcall, since every frame in the UI goes through it and one built in an
unexpected way must not stop the drag.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1 - Drag a sequence from the editor onto any action button
Long ago a sequence came with a /click macro, and dragging that macro to a
bar put the sequence on a button. WoW no longer lets that macro work. This
gives the gesture back without a macro: the Config tab shows the sequence's
icon top right, and dragging it draws GSE's own copy of the icon under the
mouse. Nothing goes on WoW's cursor and nothing goes into a slot. On release,
the action button under the mouse gets an action-bar override, as the
right-click picker would set.- The target is found by testing which named action button the mouse is
over (collected once at drag start, topmost wins), not by name and not by
mouse focus, so any bar works: Blizzard, Bartender, ElvUI, Dominos,
ConsolePort, Forever's gamepad bars. - Only an empty slot, or one GSE already overrides, takes a sequence; a real
action would win over the override and hide it, so the player is told to
clear it first. - Out of combat only; entering combat cancels. A mouse release anywhere ends
the drag if the editor closes mid-drag. - The icon is the sequence's first compiled step, as the override button
would show it (GSE.GetSequenceStartIcon). Greyed, with a reason, for a
sequence with no live button (unsaved, or another class).
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1
- The target is found by testing which named action button the mouse is
- Merge pull request #2124 from LarryThiessen/fix-abo-icon-colour
Action Bar Overrides: keep the override icon in full colour (#2123) - #2123 Action-bar overrides: full-colour icons are an option, off by default
Keeping the override icon in full colour overrides whatever the bar draws,
including the usable / GCD dimming a player may rely on, and a bar skin's
own option is the first place to fix a greyed icon. So it is opt-in:
"Keep Button Binding Icons in Full Colour" in GSE's options
(GSEOptions.actionBarKeepIconColour), off unless turned on.- The icon hooks go on only when the option is on; turning it on applies
them to every current override, and new overrides get them as they are
made. Turned off, they stay (hooksecurefunc cannot be undone) but do
nothing, and the bar's colouring returns on its next update. - The guard runs before hookButtonIconUpdates' once-only check, so a
button first hooked while the option was off still gets it.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01Bv33FsMBZQw2AuNy8LL2d1
- The icon hooks go on only when the option is on; turning it on applies
- #2109 Make the gamepad UI work with GSE to select sequences onto the Gamepad actionbar
- Bump brace-expansion from 5.0.9 to 5.0.12
Bumps brace-expansion from 5.0.9 to 5.0.12.
updated-dependencies:- dependency-name: brace-expansion
dependency-version: 5.0.12
dependency-type: indirect
...
Signed-off-by: dependabot[bot] support@github.com
- dependency-name: brace-expansion
- Action-bar overrides: keep the override icon in full colour
An override's slot is empty, so bar skins (HUI_ActionBars' "Not Usable"
colour, for one) and WoW treat it as unusable and grey or desaturate the
icon GSE draws there, from their own hooks and timers. Watch the icon
instead of racing them: while GSE owns the slot, any SetVertexColor or
SetDesaturated on it is put back to full colour. A real action dropped on
the slot keeps its own colouring, since the override yields to it.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2111 #2122 Fix button labels
- #2111 tree: grouped views keep sequences in spec order
By Class listed a class's sequences alphabetically across all its specs,
so Blood, Frost and Unholy were interleaved. Grouped views now sort
sequences as the by-type tree does -- class, then spec, then name
(GSE.SequenceKeyOrder) -- with the class's variables and macros after,
by name. By Collection sorts each collection's sequences the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 WoW Forever: keybinding is a switch, off unless turned on
Forever's gamepad handling is still a moving target, and binding sequences
to keys there is an unknown quantity; a sequence goes on an action-bar
button (Button Bindings) and WoW binds the button instead.- GSE.KeybindingsEnabled(): always on elsewhere; on Forever (TOCFlavour)
GSEOptions.ForeverKeybindings, an "Enable Keybindings" option shown only
there. Toggling re-applies binds at once -- off frees every key GSE bound. - A one-off turns it on for a character that already binds keys, so nothing
in use stops working; it only ever turns it on. - Turning it on warns that it is for power users who know WoW's keybinding
system and how to undo a keybind, and that controller keybinds are not
supported; Cancel leaves it off. Without the GUI the warning is printed. - Off: LoadKeyBindings releases and binds nothing; the editor's Bindings
shows Button Bindings only -- no chooser, no keybind pages, landing on the
current spec's Button Bindings; the tracker shows no keybinds; /gse bind
says to turn it on.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y
- GSE.KeybindingsEnabled(): always on elsewhere; on Forever (TOCFlavour)
- #2111 profiles: a class's spec setup shared by all its characters
GSEStore.profile[classID][specID] holds a spec's keybinds, action-bar
overrides and settings, used by every character of the class on any realm
or faction (per class before WoD, where binds are kept per class). Talent
loadout binds stay on the character -- their config ids are its own -- as
do the per-character click rate / Reset OOC overrides.- Migration, once per character: the first seeds each spec's profile; a
character whose binds match uses it; one whose binds differ keeps them,
opted out (GSE_C.ProfileOptOut). Waits until WoW can place each spec. - Binds and overrides load through GSE.SpecKeyBinds / SpecOverrides (the
profile's, or the character's own when opted out); deleting a sequence
clears it from every profile; profiles follow moved sequences. - Settings (click rate, Reset OOC, pause and reset modifiers, Skyriding
binds, action-bar display) follow the spec being played: a metatable on
GSEOptions reads the profile's value, else the account-wide one kept in
ProfileDefaults, so no reader changed. Table settings are copied into a
profile before they can be edited there. - The keybind and override panels have "Shared with every <class>
character" per spec. /gse forceclean opts the character out rather than
wiping the class's profiles. - Fixed: one-off 3202 cleared one spec at most, and for spec 2 cleared
spec 3 -- an error before its flag was set, so on every login. The
tracker now uses the binder's own spec and loadout keys (it lacked the
spec-group fallback), and the keybind editor opens on the current spec on
Legion to Shadowlands clients.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y
- Migration, once per character: the first seeds each spec's profile; a
- #2111 collections: every element records the collections it came through
An element's envelope carries Collections = { [key] = name }: the
collection's PlatformID, or "name:" .. name for a pasted one with no site
id (replaced by the id when the same collection comes from the site).
Envelope, not body, so sealed content records it too.- Recording: an import of a collection (payload.Collections) leaves an
expectation per member; the store settles it when the member is written
-- the out-of-combat queue, the import dialog -- and an identical re-import
settles it too. A member the user declines takes nothing (30 min expiry). - A single element in its container is not a collection. A Mod export is
one only when it has a package name and more than one element. - Editors show "From collections" (sequence Metadata tab, variable and
macro pages). - The tree groups By Type, By Class -- a class's sequences, variables and
macros together -- or By Collection, switched from its bottom row. Paths
under a group are the by-type paths, so every handler works unchanged;
a by-type selection is translated to where the element sits. - A collection's right-click menu exports it (named, all members
selected), removes what came through it alone, or forgets it.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y
- Recording: an import of a collection (payload.Collections) leaves an
- Merge origin/master: #2112 spell ranks, with macro ranks per version
A managed macro's Ranks (its casts written with a rank) describe the text
they were read from, so in the versioned shape they live on each version
beside managedMacro: UpgradeMacro moves a flat node's Ranks into
Versions[1], GSE.MacroRanks reads the running version's, and everything
that compiles a macro -- GSE.MacroText, the sealed-macro materialiser,
the macro editor -- compiles with them. The manage toggle reads each
version's ranks from its spell-name text before converting it, and a
macro exported from WoW alone carries its ranks on its version.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2111 variables and macros: scoped and versioned as sequences are
A variable and a macro now take a sequence's shape: MetaData for their
name, author, scope (SpecID), help (Notes/Help) and which version runs
where (Default and the contexts), Versions for what runs. Older flat
content is upgraded on every read (GSE.UpgradeVariable / UpgradeMacro),
so exports, shares and records not yet migrated keep working.- A variable compiles the version the current context selects, and is
recompiled when the context changes. A macro's WoW text is the version
that runs now. Macros still execute by their /macro name. - Scope files an element in its class; one of another class is neither
loaded nor written to this character's macros. - A macro GSE drives -- managed, or with more than one version -- is
written into WoW and never read back from it. Read back, a context
change copied the version WoW held over the one that now runs. - Snapshots of a managed or sealed macro no longer replace its source.
- The variable and macro editors gain the scope picker, a version bar
(label, copy, delete -- refused while a context selects it) and the
version configuration. A macro rename moves its stored entry with it. - Export and sharing send the current shape, never a macro's slot.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y
- A variable compiles the version the current context selects, and is
- Bump undici from 6.28.0 to 6.29.0
Bumps undici from 6.28.0 to 6.29.0.
updated-dependencies:- dependency-name: undici
dependency-version: 6.29.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] support@github.com
- dependency-name: undici
- #2112 support spell ranks on WoW Forever
Forever runs vanilla content on Retail's API: every rank is its own spell
ID and C_Spell.GetSpellInfo has no rank field, so ranks are read with
C_Spell.GetSpellSubtext -- the Retail equivalent of the GetSpellSubtext
call removed with Classic support in #1471.- A bare spell name stays rankless and casts the highest rank known.
- A written rank,
Frostbolt(Rank 2)orMaul(Rank 1)(Bear), is stored
as that rank's spell ID so it still translates between locales; the
block (or managed macro) records its ranked casts inRanks. - Compiling caps a written rank at the highest rank the character knows,
stepping up as ranks are trained and never past the one written. - Recompile on LEARNED_SPELL_IN_SKILL_LINE (SPELLS_CHANGED is Classic-only,
and Forever reports GameMode 12) and on SPELL_DATA_LOAD_RESULT for rank
text that was not loaded at compile time. - The macro box's focus-lost repaint, the Macro Manager, macro export and
the GSE button tooltip (rank, top right) all honourRanks.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
- #2111 embeds: an Embed block names its sequence by id and by label
GSE.ResolveEmbed runs the sequence with the block's SequenceID when this
machine has it, and otherwise the one its Sequence label finds. On save and on
load (GSE.NormaliseEmbeds) the id follows a move to a PlatformID, the label
follows a rename, and a label-only block gains the id it resolves to. Export
and sharing let only PlatformIDs leave: a local id becomes the PlatformID or
is dropped. The editor's Embed picker is keyed by id.
Variables and macros stay referenced by name.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2111 buttons: sequences run from a stable handle; binds hold ids
Each sequence's secure button is named GSES<n>, kept in GSEStore.button[id]:
short enough for a macro's /click line, unchanged by a rename, moved with the
sequence to its PlatformID, and never reused once its sequence is deleted.
Keybinds and action-bar overrides in GSE_C hold the sequence id; each
character's old name-keyed entries (and ids that have since moved) are brought
up to date at login by GSE.UpdateCharacterSequenceRefs. A rename therefore
touches no binds; a delete removes them by id. The keybind editor stores ids
and shows labels; the tracker maps a button back to its sequence.
Macros are still referenced by their /macro name: GSE's macro store is for
management, not execution.
Removed the stub-macro path nothing reached any more: CheckMacroCreated,
OOCCheckMacroCreated, CreateMacroIcon, DeleteMacroStub, CleanOrphanSequences
(which read a variable that was never set) and /gse cleanorphans.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2111 storage: sequences are identified by id, not name
GSE.Library[classid][id] and GSEStore.sequence[classid][id] are the runtime
shape; a sequence's Name is a label. FindSequenceId turns a name into an id
only for names that arrive from outside (import, commands, Embed, another
player). Rename and class move keep the id; a sealed body stays byte-identical.
A sequence made in game has a local id at once and moves to its PlatformID at
the next load, recorded in GSEStore.alias; an id held in any class is never
taken twice.
Sharing with other players is keyed by id and label, and offers variables and
macros as well as sequences -- including global ones, which ipairs never
listed. Protected content is never listed or sent.
Fixed on the way: the bridge refused to delete global sequences; Send and the
own-chat-link path passed keys that could not be read; a corrupt sequence's
delta fork was stranded on delete; FindSequence returned the raw tuple for
other classes; /gse movelostmacros read the wrong SpecID and only moved the
in-memory copy.
Buttons, keybinds, overrides and macro stubs still use the label (next step).
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01JdPcb24JAYCGmnki5FLo7Y - #2111 storage: record a PlatformID without rewriting the element
GSE.SetStoredPlatformID(kind, name, pid, classid) puts a GSE.Tools id into
the element's envelope through the sidecar view the round trip already uses;
ReconcileStore moves the element to it at logout. The body is read only when
nothing else names the author, and never written -- a sealed (!GSE3!+) body
must not be re-encoded in the clear. Every class is searched, global included.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - storage: variables and macros record the GSE build that saved them
Sequences carry MetaData.GSEVersion, stamped by the editor's Save. Variables
and macros carried nothing, so gse.tools could not say which build wrote them.
The variable editor now stamps GSEVersion beside LastUpdated, and UpdateMacro
stamps it where it already stamps LastUpdated.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2119 Bindings chooser: tiles go to, and light, the loadout you are in
Each tile lands on the saved loadout the player is in under its own area
instead of the spec, and hovering a tile lights that loadout's row in the tree
with its selection band. With no saved loadout the tiles go to the spec as
before.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2117 Bindings chooser: column highlights scroll with the page
The hotspots were anchored to the scroll frame's fixed viewport, so the
highlight stayed put while the tiles scrolled away under it. They now anchor
to the scroll content, offsets carried across from the viewport so they start
where they did, and move with the page.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2115 Tree: refresh on a height change, not only on a width change
The OnSizeChanged guard from #2014 skipped any resize that left the width
alone, so dragging the editor shorter never recalculated how many rows fit
and they spilled out below the panel. Height is not set by the tree, so a
height change now always refreshes; the width round-trip is still skipped.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2113 Notes panel: full note every time, steady styling, links to copy
The pool restores a label's word wrap on reuse, and the notes body takes its
height from a ruler rather than its own pre-layout measurement, so a note is
no longer cut to one line (most often on a Config/Notes tab swap). Gold base
text and the teal rail are set explicitly instead of depending on which
recycled widgets the panel drew. Addresses and Markdown links become
hyperlinks that open a popup with the address selected to copy.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com - #2111 storage: one GSEStore, keyed by identity, instead of three name-keyed tables
Sequences, variables and macros lived in three SavedVariables keyed by
NAME (GSESequences, GSEVariables, GSEMacros), with three more tables beside
them holding their GSE.Tools ids (GSEPlatformIDs and friends) -- the Mod
cannot write an id into a sealed body, so it had to live somewhere else.
#2111 gives macros and variables the metadata, versions and class/spec
flagging sequences have, which means one shape for all three. This is that
shape, and the move into it.
GSEStore = { v = 1,
sequence|variable|macro = { [classid] = { [id] = {
Name, Body, PlatformID, Author, Scope } } },
character = { [GUID] = { Label, ClassID, Macros = { [macroId] = slot } } } }
id is the GSE.Tools PlatformID once synced, "local-..." before. Body is
exactly what used to be stored under the name, and may be sealed; the
envelope beside it is plain, so the Mod can always record where an element
lives and what it is called even when it cannot re-encode the body.
The ~400 call sites that read the old shapes do not change here. GSE.Store
now returns WORKING VIEWS in those shapes, rebuilt from GSEStore once per
session and never saved; PLAYER_LOGOUT -- which fires before WoW writes
SavedVariables, on /reload and on exit -- folds them back. Moving the call
sites to ids is the next step. Every access goes through GSE.Store: the
variable, macro and PlatformID tables were rerouted the way sequences were
(146 more references), with the root initialisers removed by hand, since
GSE.isEmpty only matches nil and "".
Migration. The old SavedVariables stay declared in the TOC -- WoW only
loads names the TOC lists, so dropping them would leave nothing to migrate
from -- and are cleared once moved, so they drop out of the file.
Sequences keep their class. Every macro and variable goes global (class 0);
an author moves one by editing it. The old tables WIN over GSEStore when
both are present: after this build has run they are cleared at every
logout, so one reappearing was written while WoW was closed (the
Companion), and is the newer intent.
Characters. A character macro is referenced from its character's record,
not tagged with slots. It goes in its character's class when that is known
and global otherwise; one held by characters of more than one class, or any
of unknown class, is global; and a macro only ever moves TOWARDS global
automatically -- out of global is an author's edit. ClassID is learned at
login (UnitClass answers only for the current character).
Reconcile never deletes what the view could not show: two records with one
name cannot both appear in a name-keyed view, and the hidden one is not
read as deleted. A PlatformID arriving by the name-keyed sidecar is applied
to a character macro only when the name picks out exactly one macro and no
other holds that id, and a rekey never overwrites another macro's key.
Checked on a real retail library (80 sequences, 8 variables, 28 macros)
against the pre-upgrade backup: every item present, bodies byte-identical,
all six old tables gone from the file. The only differences were two items
the session itself changed after migrating (an import, and the Decursive
addon rewriting its macro), and 15 sidecar ids for macros that were not in
the library -- orphans, correctly dropped.
Tests: spec/gsestore_spec.lua, 28 cases, each property checked by breaking
the code on purpose -- which found and fixed two real faults: a character
macro never adopted a PlatformID from the sidecar, and a rekey would have
left its characters pointing at the old id. Existing storage specs now go
through the store. 540 busted, all Lua 5.1 specs, luacheck clean.
Ships with the Companion's GSEStore support (GSECompanion 295309d).
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2111 storage: one character key, and one door to the sequence store
Groundwork for #2111. Macros and variables are gaining the metadata,
versions and class/spec flagging that sequences already have, which means
reshaping the saved variables they live in. Two things had to be true first:
there has to be exactly one answer to "which character is this", and there
has to be exactly one place that knows where the stored content is.
Character key
The Mod had four recipes for "this character", and they disagreed.
On WoW Forever, UnitFullName's second return is the SURNAME, not the realm:
it returns "Bob", "Geldoff". Every call site read it as a realm, so Forever
characters were filed as "Bob-Geldoff". An earlier Forever build returned
the name whole, which is why that same character's saved bucket was keyed
"Bob Geldoff-Classic Beta PvE" -- a key the current build no longer
produces, so it was orphaned.
Retail was not consistent either. UnitFullName strips spaces from the
realm and GetRealmName does not, and the Mod used both: the storage paths
stripped them, Events.lua's startup did not. On any realm with a space in
its name -- Argent Dawn, Twisting Nether -- one character had two buckets.
GSE.CharacterKey() is now the player GUID, which carries no name or realm.
That is the WoW developer community's standard answer, and it held across
Forever's numbered overflow realms in testing. CharacterLabel() is
GetUnitName(true) plus realm, for display only -- it returns Forever's full
two-part name, which UnitFullName splits.
The 13 call sites collapse into CharacterMacroBucketKey(). Its first call
after login folds every legacy-keyed bucket for this character into the
GUID one, and never overwrites what is already there. A legacy key holding
a MACRO rather than a bucket is left alone: macro names can contain hyphens,
so "Bob-Geldoff" can be a real macro. A GUID cannot collide like that; it is
longer than WoW's 16-character macro name limit. Before the GUID exists it
answers with the key storage has always used, so an early write lands
where the post-login call will find and migrate it.
OneOffEvents' update 3201 keeps its old recipe on purpose: it migrates data
that recipe wrote.
Also fixed on the way through: "clear keybindings" wrote
KeyBindings["Char-Realm"][spec] -- the nesting update 3201 removed -- while
every reader looks at KeyBindings[spec]. GSE_C is SavedVariablesPerCharacter;
it never needed a character key.
Store accessor
GSE.Store(kind) returns the at-rest root for a kind of content, and the 112
places that named GSESequences directly now go through it. For sequences
this is a pure indirection -- the shape is unchanged -- so it changes no
behaviour. It exists so the next step can move the storage in one place.
It sits at the top of Storage.lua, which owns the saved variables. Nothing
calls it at file-load time, so its position in the TOC does not matter.
The root is created on demand, which cannot hide saved data: the TOC sets
LoadSavedVariablesFirst. The three root-initialisers were removed by hand
rather than renamed, because GSE.isEmpty only matches nil and "", so
"if isEmpty(root) then root = ..." never fires once the root exists.
LoadStorage's per-class loop below still creates every class table.
Tests: spec/characterkey_spec.lua lifts the shipped functions and replays
the real cases -- the Forever surname key, the orphaned earlier-build key,
both spellings of a spaced realm, GUID entries winning, a real macro
sharing a legacy key surviving, other characters untouched, the pre-login
fallback. 512 busted, all Lua 5.1 specs, luacheck clean.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01JdPcb24JAYCGmnki5FLo7Y - #2109 storage: GetMacroIcon read sequence.Metadata, which is always nil
GetMacroIcon reached for sequence.Metadata.SpecID -- lowercase d -- in three
places. The field is MetaData, so Metadata is always nil and indexing it
raises "attempt to index a nil value".
It fires for any sequence with no Icon when no icon id is passed. Only the
first of the three reads sits inside the @debug@ block; line 1775's check
is live code, so this could raise in release builds, not only debug.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - #2109 QoL: Forever runs the Skyriding/vehicle binds like retail
This block was excluded on WoW Forever because every part of it hands a
snippet to the restricted environment, and Forever's RestrictedExecution.lua
shipped without loadstring_untainted -- three "attempt to call a nil value"
errors at login on an install with no sequences at all.
Blizzard fixed it in 1.60.1.70009. Confirmed in game 2026-09-25: a secure
snippet run through SecureHandlerBaseTemplate:Execute now sets its
attribute and returns without error, and GSE loads cleanly.
The exclusion is removed, not version-gated. It was only ever there because
nothing could compile on Forever; beta clients auto-update, so a build older
than the fix is not something a player is left on. A short note replaces the
old explanation so the exclusion is not re-added.
Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session\_01JdPcb24JAYCGmnki5FLo7Y - Merge pull request #2060 from LarryThiessen/fix-helplink-filter
Refuse wowlazymacros.com as a Help Link, and heal stored sequences (#2059) - #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 - Refuse wowlazymacros.com as a Help Link, and heal stored sequences
Any form of the host -- scheme, subdomain, path, case -- is replaced by
the GSE United default. The rule lives in Storage.lua and runs on load
(migrateSequenceVersions, beside StampOriginKey, so existing sequences
are healed the first time they load), on store, and in the editor's Help
Link box as you type. Spec covers the matcher and the heal.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
This mod has no additional files