promotional banner

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.34-9-gee85046

File nameGSE-3.3.34-9-gee85046.zip
Uploaded
Sep 23, 2026
Downloads
134
Size
2.6 MB
Flavors
MoP ClassicRetailClassic TBCClassicForever
File ID
8951535
Type
A
Alpha
Supported game versions
  • 12.1.0
  • 12.0.7
  • 5.5.4
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

GSE

3.3.34-9-gee85046 (2026-09-23)

Full Changelog Previous Releases

  • #2109 keybinds: dual spec needs its own binds, not the other build's
    Keybindings and action bar overrides are partitioned by saved talent loadout,
    which works on retail because a loadout is 1:1 with the build being played. WoW
    Forever has no saved loadouts. It has DUAL SPEC -- Primary and Secondary -- and
    one specialisation per class, so GetSpec() is always "1" there and
    GetLastSelectedSavedConfigID has nothing to return. Both builds produced an
    identical key, shared one set of binds, and switching between them rebuilt the
    bindings to exactly what they already were.
    This is the one place on that client where the distinction is load-bearing.
    Sequences do not care: SpecID is a label and nothing gates execution on it. Binds
    and overrides genuinely partition, and dual spec is Forever's only 1:1 with the
    build you are on.
    GetBindingLoadoutKey is now the single answer to "which loadout is this", used
    by the change-detection context and by both storage sites. A real saved loadout
    still wins. The active spec group stands in only when there is no loadout AND
    the client reports more than one group, so retail keeps its existing keys
    untouched -- a retail character without a loadout still keys on tostring(nil)
    exactly as before rather than having its saved binds orphaned under a new one.
    The group is prefixed, so a group of 1 or 2 can never be read as a config id of
    1 or 2.
    Three sites in Events.lua computed this independently and now share it. Two of
    them were wrapped 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

This mod has no additional files