GSE: Sequences, Variables, Macros

GSE is an advanced macro compiler that is an alternative to the limits provided by the default macro editor.
Back to Files

3.3.35

File nameGSE-3.3.35.zip
Uploaded
Oct 16, 2026
Downloads
21.2K
Size
2.7 MB
Flavors
MoP ClassicRetailClassic TBCClassicForever
File ID
9202472
Type
R
Release
Supported game versions
  • 12.1.5
  • 12.1.0
  • 5.5.4
  • 2.5.6
  • 1.60.1
  • 1.15.9

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
  • 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
  • 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
  • #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
  • 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
  • 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
  • #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
  • #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
  • 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
  • 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
  • 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
  • #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
  • 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
  • #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
  • #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
  • 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
  • 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
  • #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) or Maul(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 in Ranks.
    • 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 honour Ranks.
      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 in if 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'
    with loadstring_untainted=nil in 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. The GameMode >= 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 carries SET 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 = true was the documented way to turn the widget pool
    off for soak-testing, and _G.GSE_NoLayoutBatch did 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 = true no
    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