VersionCheck-v1.5.0
What's new
VersionCheck-1.0
[v1.5.0] (2026-09-16) -- RequestFrom, RequestAbout, the GreenWall leg fixed, and two copies of the library finally talk (MINOR 14)
MINOR 13 -> 14: three public methods and one reserved wire key added, so an embedded copy of 14 must win LibStub registration over a copy of 13. 370 specs green, 713/713 executable lines (100.00%) on the run after the last change. Harness pin 226c0e0 -> 799ae17.
The API surface added at MINOR 14, in one place
Additive and feature-detectable, like MINOR 13's. Nothing at MINOR 13 changed meaning.
| Name | What it is |
|---|---|
VC:RequestFrom(name) |
The request, whispered to ONE player; true when handed to the transport |
VC:RequestAbout(addonName) |
The broadcast, naming ONE registered host; RequestCheck's return shape and clock |
VC:FlushGreenWall() |
Release the parked GreenWall request from the caller's own hardware event |
__all |
Reserved wire key: "answer about everything you have" -- the GreenWall trigger |
Changed -- GuildRoster is ## OptionalDeps:, not ## Dependencies: (thread 1b732c42)
The user asked GuildRoster (2026-09-15) to make VersionCheck a hard dependency: its sister-guild
sync now finds a peer through RequestAbout, so a GuildRoster without VersionCheck cannot sync.
This repo's standing directive made GuildRoster a hard dependency of VersionCheck. Both at once is a
TOC cycle, and the client answers a cycle by loading NEITHER addon -- so one side had to move, and
the newer instruction decides which. VersionCheck-1.0.toc now reads ## Dependencies: Ace3, LibAceGUIWidgets and ## OptionalDeps: GuildRoster.
What that keeps: load order (GuildRoster still loads first whenever present) and the CurseForge
install (libguildroster stays in .pkgmeta's required-dependencies -- CurseForge's install list
has no cycle rule, and the .pkgmeta comment now says so, since it used to say the two lists must
agree). What it costs: nothing in the code, because every use of LibGuildRoster was already
LibStub("LibGuildRoster-1.0", true) at call time with a nil branch; a player who somehow has
VersionCheck without GuildRoster gets a roster window of answered players only and transport-spelled
names, which is what an embedded copy without it already got.
RELEASE ORDER, and it matters for players this time: v1.5.0 must be published BEFORE (or the same day as) the GuildRoster release that makes VersionCheck a hard dependency. A player holding released v1.4.0 (hard on GuildRoster) who updates to that GuildRoster has the cycle on their machine and loads neither addon. Said on the GuildRoster thread as well.
New -- VC:FlushGreenWall(): send the parked GreenWall request from a caller's click (thread ec8f7fed)
The GreenWall half of a request is parked until the player's next chat keystroke, because
GreenWall's send bottoms out in SendChatMessage and the client only allows that from a hardware
event. That is right for the login batch and wrong for LibGuildRoster's sister sync, which the user
designed around a click overlay ("i don't want anything waiting for someone to say something"). GR
had been reaching into VC.gwPendingPayload from outside and sending our bytes under our name;
VC:FlushGreenWall() is those three lines as a method, and the ChatEdit_ParseText hook now calls
it too, so the two cannot drift. Returns true when something went out. The caller guarantees the
hardware event; the library cannot check it and does nothing else.
Fixed -- the GreenWall leg was truncated and answered by nobody
Found by LibGuildRoster on 2026-09-15 (inbox thread 73cb2669), measured against GreenWall's source
and confirmed here: GreenWall wraps our payload as E#<guild id>##VersionCheck-1.0:<base64> and
cuts the segment at 255 characters with strsub (Channel.lua:322, GW_MAX_MESSAGE_LENGTH) --
no chunking, no reassembly. With the user's 17 registered hosts the full batch is 444 bytes, 592 in
base64. So every login broadcast on the GreenWall leg since the transport was added had been cut
mid-payload, failed Deserialize on arrival, stamped the receiver's dedup clock on the way, and
been answered by nobody. The confederation reach this changelog has described since v1.2 was dead
for any player with more than a handful of hosts, and nothing said so. My defect; the AceComm leg
chunks and never had it.
The user's direction: "the broadcast doesn't need to be comprehensive, you just need to elicit
whisper responses." So the GreenWall leg is now a trigger: the control fields, a new reserved
key __all, and as many hosts as still fit a 150-byte budget, in name order (GreenWallTrigger;
the budget is derived on the constant from GreenWall's envelope with room for a 32-character guild
id, since the id is not readable through GreenWallAPI). The AceComm leg is unchanged and still
carries everything.
Receiver side, at MINOR 14: HandleRequest answers a request carrying __all about every host
it has, under the same answer-on-change gate, in addition to the intersection for the names it did
carry. At MINOR 13 and earlier: the key is ignored (reserved-prefix filtering at 13, hosts
lookup at 12) and the receiver answers the intersection with the names that fit -- what it would
have done with a full batch that arrived intact, and more than it did with one that never arrived.
Replies go by AceComm whisper, which chunks, so nothing about their size changes.
A push on a trigger is about every host. HandleRequest's push loop iterated the request's
keys, so a Notify carried on the GreenWall leg would have made confederation peers decide only about
the three or four names that fit. With __all present it iterates the receiver's hosts instead; a
push without __all decides about the names carried, as before. Both pinned in trigger_spec.
Found by my own honesty pass, not by the thread.
KNOWN COST, pre-existing and now reaching more players, stated rather than hidden. The login
batch carries __resync, and so does its trigger. A guildmate hears the AceComm leg at login and the
GreenWall leg whenever the user next types in chat; inside 20 seconds their dedup drops the second,
after that they answer it -- in full, because of __resync -- so one duplicate round of whispers
per login is possible from same-guild peers. This was already the behaviour for any player whose
batch fitted the segment; it now applies to everyone, because the trigger always fits. Dropping
__resync from the trigger would silence confederation-only peers after a reload, which is the
case the key exists for, so it stays. Re-fires (TPM's scanner) carry no __resync and are
answered on change only. The 20-second dedup TTL is unchanged.
New -- VC:RequestAbout(addonName): the broadcast, about one addon
The second half of the same thread. LibGuildRoster's question is "who runs GuildRoster, and which version", asked of the guild AND the confederation before it syncs a roster with someone -- and a one-addon request fits GreenWall's segment with room to spare, so no trigger mechanism is needed: every receiver at every MINOR answers the intersection, which is exactly that one addon.
Same two transports and the same return shape and clock as RequestCheck (true, 0; false, N
seconds to wait; false, 0 for nothing to send). false, 0 also covers an addon that is not a
registered host: the request carries our own version of it, and we have none for an addon we do not
run. __resync is always set, as for RequestFrom. Not a round of the popup decision -- no
collection window, VersionResponses untouched; answers arrive through OnPeerVersion.
BroadcastRefusal is the shared guard, and SendRequest the shared send, so RequestCheck and
RequestAbout cannot drift on either.
New -- VC:RequestFrom(name): the request, whispered to one player
Asked for by LibGuildRoster (inbox thread 754c66fd, 2026-09-15). Before it pulls a roster from a
player /who listed, it asks THEM what they run and pulls from the most compatible responder -- and
a sister-guild member never hears our GUILD batch. Until now GR built the request itself, from a
copy of VC10_REQ and the __v / __resync / __from keys: this library's private wire,
maintained in two places. GR already feature-detects VC.RequestFrom, so the copy can go the moment
this ships.
VC:RequestFrom(name) sends the same payload FireBatch broadcasts, on VC10_REQ over WHISPER
to name, and returns true when a request was handed to the transport. RESYNC_KEY is always
set: a targeted ask wants a full answer, and without it a MINOR 13+ peer we had already heard from
would stay quiet by design. A bare name gains our own realm through the same branch the reply path
uses; a realm-qualified one is sent as given. It refuses (false) with no name, no registered
hosts, or our own name in either spelling -- the receiving side would drop that as an own echo, so
false now beats sending and hearing nothing.
Deliberately NOT done, and documented on the function: it does not touch VC.lastBatchAt (the GUILD
dedup clock, which is about what guildmates suppress), and it does not open a collection window or
clear VersionResponses -- it is not a round of the popup decision. The answer arrives through
HandleResponse -> RecordPeerVersion -> OnPeerVersion, which is what a consumer reads. Known
and documented rather than worked around: a guildmate asked inside GW_REQ_DEDUP_TTL of our own
broadcast drops the whisper as a duplicate. That is their dedup working, and a spec pins it.
Changed -- one request builder for both transports
FireBatch built the payload inline. BuildRequest(opts) now builds it for the GUILD broadcast and
the targeted whisper alike -- every host's version, then __v, __from, and __resync / __push
on request -- so the two can never carry different shapes. The reply path's realm-qualifying branch
is QualifyName, shared with RequestFrom for the same reason. No wire change: every payload the
broadcast sends is byte-for-byte what it sent at MINOR 13.
Tests -- two REAL copies of the library answer each other (Tests/peers_spec.lua)
The harness delivered wow.client() to this repo's request (thread 9bade997, pin 1037beb), and
this is the test the suite could never write: Alice's copy broadcasts, Bob's REAL copy answers by
whisper after HIS jitter to a name HIS code built, and Alice pops exactly one dialog naming
Bob-Testrealm. Then the negatives self-replay cannot reach: Bob older (Alice not warned, Bob is --
by Alice's real code), Bob without the addon (no reply, no dialog), a peer with addons we do not
have (answers only about the shared one; we still harvest the rest from his broadcast), three
peers (only the highest named, on every copy behind it), and neither copy answering its own echo.
RequestFrom is specced the same way, against a Bob who never heard Alice's broadcast, and
RequestAbout against a Bob with more addons than were asked about. Tests/trigger_spec.lua pins
the GreenWall trigger's shape and budget, the __all receiver rule, and RequestAbout on one
instance.
Three things learned building it, recorded in the spec's header so nobody re-derives them:
- Load everyone, then let everyone enter the world.
freshVCadvances the clock 10s for ChatThrottleLib's start-up throttle, so a client that enters before the next one loads has its login batch fire mid-load, before the peer'sRegisterComm-- the peer never sees it and the version arrives by REQ harvest from THEIR batch, not by RSP. A real late-login shape, not the one under test. Measured with a probe. _G.AceComm30Framekeeps the last client's whole world alive acrossframes.reset(). AceComm names its frame; the widget registry is weak by design and keeps whatever is still referenced; a global the harness does not own is such a reference, and through the frame's OnEvent it holds that client's AceComm, its callbacks and this library, all still listening. The stale copy heard the NEXT example's GUILD echo and answered by whisper into its own deadsentlog, routed to the live Alice. Nil it inafter_each, withace.reset()on the host so later spec files get a fresh Ace3 load.env.deserializeresolves AceSerializer through LibStub, so after that teardown the host has none and a payload is read as the client that owns a registry.
Docs
README.md:RequestFrom,RequestAboutandFlushGreenWallin the API table, each with its own section; the GreenWall section says what now travels on that leg and when it goes out; Requirements says what GuildRoster's optional declaration means.docs/Curseforge_Description.html: the v1.5.0 entry (New / Changed / Fixed), the Requirements wording, GuildRoster in the "who depends on this" list, and the developer paragraph for MINOR 14.CLAUDE.md: the GreenWall budget rule and the two-client spec's traps.
Inbox
Six threads read. The retracted 38000 finding needed nothing -- the changelog already records the
retraction. frames.layerMismatch() and GetMouseButtonClicked were already adopted on the
previous pin; the RowList onRowClick(…, button, rowFrame) delivery (LibAceGUIWidgets at MINOR 29
on disk) is consumed, and the two feature-detected fallbacks STAY: two lines that keep right-click
working on a stale standalone widget library, per the backwards-compatibility directive.
[v1.4.0] (2026-09-10) -- the peer-review findings, answered (MINOR 13)
All seven open findings in docs/AUDIT.md, answered, and the roster window built. 330 specs
green, 645/645 executable lines (100.00%) -- observed on the run after the last change, not
carried forward. MINOR 12 -> 13: the API surface grew, so an embedded copy of 13 must win
LibStub registration over a copy of 12.
The API surface added at MINOR 13, in one place
Everything below is additive. Every name is feature-detectable (if VC.GetRoster then) for a
consumer that may be loaded beside an older embedded copy, and nothing at MINOR 12 changed meaning.
| Name | What it is |
|---|---|
VC:RegisterCheck(nameOrHost[, version]) |
Enable plus an explicit version |
VC.version, VC.minor |
The loaded LibStub minor, as a number |
VC:GetPeerVersions([addonName]) |
Every observation this session: sender -> { version, via, at } |
VC:GetRoster(addonName) |
One row per player with a four-state state; the window's model |
VC:RequestCheck([push]) |
Rate-limited broadcast; true, or false plus seconds remaining |
VC:IsLeadership() |
LibGuildRoster's officer-permission predicate, for the player |
VC:NewestKnownVersion(addonName) |
Highest build anyone (us included) is known to run |
VC:IsBehind(entry, addonName) |
true/false, or nil when the row has no version |
VC:WhisperUpdate(entry, addonName) |
The canned whisper; returns the text sent |
VC:ToggleRoster() |
Open or close the roster window; what /vc calls |
VC:RefreshRoster() |
Repaint an open window from the model; sends nothing |
VC.callbacks |
CallbackHandler: OnPeerVersion, OnCheckComplete, OnRosterChanged |
__v, __resync, __from, __push |
Reserved wire keys; consumers must not name an addon __… |
New -- RegisterCheck, a name two consumers were already calling
VC:RegisterCheck(nameOrHost[, version]) registers a host exactly as Enable does, and records the
version string you hand it.
The name had never existed here, but Grouper and BijouRR both open with
if VC.RegisterCheck then VC:RegisterCheck(NAME, VERSION) elseif VC.Enable then VC:Enable(self) end.
The elseif rescued both, so the dead branch was invisible and every version check worked -- which
made it a trap that would arm itself the moment this library defined the name for any purpose at
all. Both consumers would have switched to the first branch, stopped calling Enable, registered
no host and never warned again, with nothing erroring, nothing logged, and no spec here able to see
it (every one of them drives Enable). Defining it to mean what those call sites plainly intend is
what makes the name impossible to re-purpose underneath them.
The version is recorded as hostEntry.declaredVersion, not stamped onto hostEntry.host: for
the object form that table is the caller's own addon object, and a library assigning a Version
field into it is a side effect nobody asked for. HostVersion precedence is now the live addon
object, then declaredVersion, then the TOC, then "unknown".
New -- the library learns from traffic that was already on the wire
VC.peerVersions[sender][addonName] = { version, at, via }, read through
VC:GetPeerVersions([addonName]). Every REQ is broadcast to the whole guild carrying the sender's
complete addon-and-version list, and HandleRequest iterated it as for addonName in pairs(batch)
-- key only -- discarding the versions at the language level. They are recorded now.
It costs no traffic. Not one extra message is sent; this is pure listening, and a spec asserts it. That is the whole reason it is allowed to exist: the library was already receiving a complete version announcement from every player who logs in, and throwing it away.
Four things it fixes, none of which needed a protocol change:
- Late loggers. Someone who logs in after your collection window closed previously never appeared at all. They now announce themselves by doing nothing but logging in.
- Addons you do not have. Their REQ lists their hosts, not the overlap, so this is where you learn a guildmate runs something you have never installed. The reply is still limited to the intersection -- that is a property of the reply, not of what we may learn, and conflating the two is what discarded the data.
- Confederation peers. A REQ arriving over GreenWall from a non-connected realm gets a WHISPER reply that can never reach them. Their versions still arrive here, so they are no longer invisible.
- Late replies.
HandleResponserecords the observation even whenVersionCheckActiveis false. A reply landing outside a window is useless to the popup decision, not untrue.
VersionResponses is untouched and remains the popup's current-round input, which is why
FireBatch still empties it. The accumulated view and the decision input are now two tables with
two jobs, rather than one table doing both.
Changed -- one identity model, borrowed rather than invented
VersionCheck had three opinions about who a player is: GW_StripRealm, a realm-appending branch in
HandleRequest, and raw sender strings as store keys. That is how two characters sharing a base
name on different realms came to suppress each other. LibGuildRoster already solves this properly,
so VC now uses it: CanonName for names arriving over the wire, feature-detected on the method
rather than the library, falling back to the raw string when an older embedded copy lacks it.
Adopting it alone made things worse, and four specs caught that within a minute. CanonName is
pure -- a bare name stays bare and never gains a realm, which is exactly right for something you
persist and exactly wrong on its own here. One player arriving as Bob over GreenWall and
Bob-Whitemane over AceComm stopped merging and became two rows.
The rule needs both halves. LibGuildRoster's own phrasing is "the sender qualifies, the receiver
canonicalizes", and VC was doing neither: it never put its own qualified name on the wire, because
the sender string always came from the transport. It does now, in a reserved __from field, from
GetNormalizedPlayer() where the roster library is present and built by hand from UnitName and
GetRealmName otherwise. If the realm has not resolved yet -- a real window at login -- the field is
omitted rather than guessed: a half-built identity is worse than none, because peers would key
us under a name that is not ours and never merge it later.
Backwards compatible in both directions. An older copy ignores __from exactly as it ignores every
other reserved key, and a peer too old to send one is keyed on the transport's string, which is what
VC has always used.
The two heuristics were NOT deleted, and that is deliberate. They answer a different question.
The transport dedup runs before the payload is deserialized -- pinned by a spec, because parsing
first would let a peer flooding unreadable messages make us deserialize every one -- so it cannot
see __from and must work from the transport's sender string alone. Heuristic for the cheap
rate-limiting question, canonical identity for the stored answer.
Changed -- __-prefixed keys are reserved on the wire, from this release onward
Nothing sends one. This is here so that a future version can, without breaking any copy of the library already installed -- and it only works if it lands in the same release as harvesting.
The REQ payload is a flat addonName -> version map, so the only compatible way to add a protocol
control field is to put it in that map and rely on every reader ignoring it. A copy at MINOR 12 or
earlier already does: it looks the key up in VC.hosts, finds nothing, and neither replies about it
nor records it. A copy at MINOR 13 harvests every key it receives, so without this guard the day
any control field were introduced, every MINOR 13 client in every guild would begin showing a
phantom addon named after it -- in a roster window whose entire value is being trustworthy.
Filtered on receive, deliberately, because that is what protects clients which will never be
upgraded from a decision nobody has made yet. Enable is untouched, so a host that registers such a
name still works locally; peers simply do not harvest it. Replies about real addons in the same
payload are unaffected, so introducing a control field later cannot look like the version check
breaking.
New -- callbacks, and a refresh entry point that will not fire into silence
VC.callbacks (CallbackHandler-1.0, which ships with Ace3 and which AceComm already depends on --
no new dependency, and the same idiom LibGuildRoster uses):
OnPeerVersion(sender, addonName, version, via, changed)as each observation arrives, so a roster view fills in live instead of sitting blank until a window closes.changedis the argument that matters: re-hearing the same version from someone who logged in twice is the ordinary case and should not read as news.OnCheckComplete()when the collection window has closed and every host has been processed. Deliberately not "we found everyone" -- nothing can know that. It means we stopped waiting.
VC:RequestCheck() returns true when it broadcast, or false plus the seconds remaining.
The cooldown is GW_REQ_DEDUP_TTL and it is not a taste decision: peers suppress a repeat REQ from
the same player for that long, so an earlier broadcast is answered by nobody and leaves the roster
emptier than before it was pressed, because FireBatch clears VersionResponses on the way out.
It counts the login batch too, not just manual refreshes -- a peer's dedup does not care why we
sent, so a cooldown tracking only manual checks would let the very first press fire into silence.
New -- GetRoster, the four-state roster model, and OnRosterChanged
VC:GetRoster(addonName) returns one row per player for one addon, sorted by name: the model the
window renders, and the thing a consumer building its own view should read instead of
GetPeerVersions. Each row carries name, account (the alt-grouping key from LibGuildRoster,
supplied rather than acted on -- the view decides whether to collapse alts), rank, rankIndex,
classFile (named for what RowList's class colouring reads), online, isSelf, and when a
version is known version, via and at.
Every row has a state, and the four values are the point. A version checker that cannot
separate them lies -- RCLootCouncil's window collapses everything without a version into one "Not
installed ?", which reads as fact and is a guess:
reported-- we have a version for them.silent-- in the guild, ONLINE, has never reported. Probably has not got the addon, but that is an inference and the UI renders it as one.offline-- in the guild, offline, never reported. Says nothing either way.unknown-- not in the roster at all: a confederation peer, someone who has left, or the roster library absent.
Guild membership is read first so a player who has never answered still gets a row -- without that a window can only ever show people who replied, which is the question nobody is asking. The roster is read here and NOWHERE in the popup path: a guildless player must still be told they are out of date, and a spec pins it.
OnRosterChanged fires on VC.callbacks whenever LibGuildRoster reports a member joining,
leaving, coming online, going offline, changing rank, or the roster being rebuilt -- seven of its
events folded into one "repaint" signal, so a consumer does not have to learn that library's
vocabulary. Two things in the wiring came from LibGuildRoster's own answer and are easy to get
wrong: register on the library with a DOT (GR.RegisterCallback(VC, ...)), and there is NO REPLAY
of OnRosterReady -- VersionCheck wires up at PLAYER_ENTERING_WORLD, routinely after the roster
is ready, so it asks IsReady() and does the first fill itself. The library also calls its own
repaint directly rather than registering itself on VC.callbacks: CallbackHandler keys
registrations by target, so the library registering itself would silently replace any consumer
that used VC as its target -- which is what happened, and three specs caught it.
New -- the roster window, and /vc to open it
/vc, /versioncheck and /vcroster all open it. Registered by the library rather than by each
host: the window is inherently cross-addon and no single host knows the others exist, so none of
them could own the command.
LibAceGUIWidgets' ClearFrame -- the suite's main window, the same chrome FGI and Dibs open --
with one tab per registered host and a sortable, virtual-scrolling list of everyone in the guild:
name, rank, and either their version or which kind of not-knowing this is (Not seen, Offline,
Unknown). Four states rather than one "not installed", because collapsing them turns three
different facts into one confident guess; the info icon on the status bar explains them.
ClearFrame and not AceGUI's stock Frame, on instruction -- "use the gui lib for this, all that
functionality is in the lib" -- and the difference is not cosmetic: it exposes its bottom bar
(statusbg, info, settings, statustext) on the widget table, so nothing here reaches into an
AceGUI internal. A draft on the stock Frame had to find the bar through statustext:GetParent() and
hand-measure Close's geometry to sit a button beside it. Both reaches are gone.
It is built on first open and never before. ~20 addons embed this library and most of their users will never type the command; they should not pay for a window they do not open.
And it adds no traffic. Every row comes from observations the library was already receiving --
the login REQ every peer broadcasts, and the replies to our own. RefreshRoster sends nothing, and
a spec asserts it; switching tabs sends nothing either, since one request already fills every tab
and a second inside the peer dedup window would be answered by nobody while clearing what we had.
Rows appear live off OnPeerVersion and off LibGuildRoster's own roster events.
The list is LibAceGUIWidgets' RowList, not a hand-rolled stack of labels -- that was the plan
until its author pointed out RowList existed. Tabs are sorted: pairs order is not stable between
sessions, so they would silently rearrange on every login otherwise.
Every column has a header and every header sorts. Click Name, Rank or Version; click again to
flip. The list opens sorted by Version. The rank and version columns sort on a composite
<value>\0<name> field and display the plain value through RowList's display-only format,
because the healthy case for a guild is that everybody runs the same build and most people share a
rank -- both columns are almost entirely ties, and RowList's comparator is deterministic but not
stable, so without a unique secondary the rows would swap places on every repaint. The version
column is 170 wide because a released host reports the packager's substitution of
VersionCheck-v1.5.0, which is the whole git tag (TOGBankClassic-v1.2.3); at 90 every one of them
truncated to the addon's own name.
The status bar shows the library's version -- the TOC version when the standalone addon is
loaded (VersionCheck-v1.5.0 on a dev build, as FGI shows it), or the LibStub minor when only an
embedded copy is present and there is no TOC of our own to read.
LibGuildRoster-1.0 and LibAceGUIWidgets-1.0 are now hard dependencies, alongside Ace3 --
on the TOC's ## Dependencies: line, which the client enforces, AND in .pkgmeta's
required-dependencies, which is what CurseForge installs. The second was missed until the
release check: with only the TOC changed, a player installing from CurseForge would have got an
addon that refuses to load for want of two libraries CurseForge never fetched.
New -- a refresh button that will not lie to you
The window's refresh button, on the bottom row beside the info icon and Close, rate-limited through
LibAceGUIWidgets' named cooldowns: while the cooldown runs the caption becomes the seconds
remaining, the text dims, the highlight is hidden and the click is refused at the UI layer rather
than fired and discarded somewhere deeper. It hangs off ClearFrame's exposed info and the status
bar is re-anchored to end at it -- the same 6 px and the same anchor ClearFrame's own
SetSettingsButton uses when it parks a control there.
It does not start a cooldown for a request that never left. RequestCheck declines for reasons
that are not always the clock -- a guildless player has no GUILD channel to send on -- and stamping
regardless would leave the button dead for twenty seconds having sent nothing, which reads to the
user as the button being broken. The cooldown length is supplied as a function precisely because the
widget library evaluates it after the click, so it can report what the send actually did.
The cooldown key is namespaced with the library's major version: those cooldowns live in one
library-wide table shared by every addon using LibAceGUIWidgets, so a bare "refresh" would have
this button and some other addon's counting each other down.
Older embedded copies of LibAceGUIWidgets that predate named cooldowns simply get no button; the window still opens and still works.
Fixed before release -- what a self-audit of the window found
Found by reading RequestCheck against the three documents describing it, the evening the release
was declared ready. All fixed and specced; the counts at the top of this entry are post-fix.
RequestCheckonly ever refused for the clock, yet its comment, this changelog, the README and the CurseForge page all described a second refusal -- "nothing to send to", a player outside a guild -- that did not exist. With no hosts registered it returnedtrue, 0for a request that never went out. Now:IsInGuild()(feature-detected -- an absent global reads as "cannot tell", never as "guildless") and an empty batch both answerfalse, 0;FireBatchreturns whether it sent.Tests/callbacks_spec.luahad asserted thetrue-- a spec that had ratified the defect -- and is corrected with the reason beside it.- A click inside the dedup window did nothing visible.
RequestCheckreturns the seconds remaining precisely so a button can count down for them, and the button threw the number away: for twenty seconds after every login batch, Refresh was a dead click. It now counts down for the real remainder (a spec pins19sat t+6), and stays available only when nothing could be sent. - The info tooltip described
Unknownwrongly as "reported, but not for this addon". It means not in the guild roster at all -- a confederation member or someone who has left. - Right-clicking your own row offered to whisper you that you were out of date whenever a peer was ahead. It now says "This is you".
- A dev build was "behind" everyone.
VersionCheck-v1.5.0carries no digits andCompareVersionreads a digitless string as0.0.0, soIsBehindflagged a developer running from source and the menu offered to whisper them about it. The popup path already treats the sentinel specially; the whisper path now does too: a digitless version is never a candidate for newest and never compared --IsBehindanswersnil, the menu says "No version known". - Our own request, echoed back, was answered. Some server cores deliver a player's own GUILD
addon message to them.
HandleRequesthad no self check, so on those cores we whispered a reply to ourselves on every login (pre-existing) and, with__push, ran the update decision against our own version (harmless, noise). Identity is compared againstSelfName()after the payload is read, so a realm-qualified echo still matches. Twotransport_specexamples that replayed OUR OWN chunks "as Bob" stopped passing the moment this landed -- the same fiction the reply-side identity fix had already exposed on the other path -- and now send a genuinely foreign payload through the real AceComm chunking. - The roster fake lived in three spec files and the third copy had already diverged from the
first. One
env.installRoster(fields)/env.removeRoster()inTests/env_vc.lua.
New -- right-click a name to whisper them about it
Right-click any row and the menu offers one thing, decided by whether that player is behind the newest build anyone is known to run (ours included): "Whisper: Alpha is out of date", which sends "Your Alpha is out of date (you have 1.0.0, latest is 1.5.0), please update." -- the user's words, with both versions added so the recipient can see at a glance whether it is true. A player who is level gets "Up to date"; a player we have never heard from gets "No version known", because "not behind" would be a claim about someone who has never told us anything.
The whisper goes through C_ChatInfo.SendChatMessage when the client has it and the bare global
otherwise -- on Classic Era the bare global is a deprecation fallback that forwards to the
namespaced one, so the order is the client's own preference. Specs drive both branches.
From LibAceGUIWidgets MINOR 28, onRowClick passes the mouse button and the row frame -- asked
as thread ddb1caf9af5a and delivered the same hour. An older embedded copy passes neither, so
both are feature-detected per call: the button falls back to the client's GetMouseButtonClicked
(which Classic Era's restricted environment whitelists) and the anchor to the row most recently
hovered. Specs drive both shapes. The harness delivered GetMouseButtonClicked with a
wow.mouseButton tunable in the same hour (thread 64fa224f3a25), unpushed; Tests/env_vc.lua
staged it until the pin moved, which it did the same evening (see the 226c0e0 adoption below).
New -- a Notify button for guild leadership, and the __push key behind it
For the player only when LibGuildRoster's IsOfficer() says so -- the officer-note permission,
which a GM granted deliberately, not a rank index, which is a position in a list the GM can arrange
any way at all. GMs hold every permission, so it covers them.
What it does, and why a plain refresh could not. A peer already harvests every version a REQ
carries, but it only decides -- compares and pops the update popup -- when its own collection
window closes, at its own login or when one of its hosts re-triggers a batch. So an officer's
refresh could not make an out-of-date guildmate see anything until that guildmate's next batch.
Notify sends the same REQ with one extra reserved key, __push = "1", and a receiver that
understands it runs ProcessVersionResponsesForHost on receipt for the addons it shares with the
sender. No new popup path. Same comparison, same lastShown dedup, same dev-build suppression,
same combat deferral -- only when the existing decision runs changes. OnCheckComplete is not
fired, because no window of ours closed.
Backwards compatible the same way __from is: a MINOR 12 receiver looks the key up in its
hosts table, finds nothing, and ignores it -- it answers as it always has and decides at its own
next window, which is exactly what it did before the key existed. A receiver at this MINOR filters
it out of harvesting like every other __ key.
Both buttons carry a tooltip in LibAceGUIWidgets' title-and-body shape (AttachTooltip), saying
the one thing a caption cannot: what a click costs, and for Notify who it reaches and who sees it.
Attached before the cooldown binding and asserted through a real hover while the caption is
counting down, since the library's hooks and the cooldown's caption both live on the same button.
Refresh and Notify share one cooldown, on purpose. The thing being rate-limited is the peers' dedup window, keyed on us, which does not care which button we pressed; two independent cooldowns would let Notify fire five seconds after Refresh and be dropped unread by everyone. Both buttons count down together the moment either sends.
New -- the library can report its own version
VC.version and VC.minor both carry the loaded MINOR as a number.
LibStub:NewLibrary records the minor in LibStub.minors[major] and never writes it onto the
library table, so a consumer holding only that table could not ask. Three spellings across two
consumers all read nil, permanently -- /grouper libs printed Loaded but version unknown on
every client, in every session. That is the fleet's one diagnostic whose subject is versions.
Both field names are populated because both are read in the wild. _G.VersionCheck is still not
assigned and will not be; the library stays reachable only through LibStub.
Changed -- the update popup says what you are running
Was: Player 'Bob' is using version 2.0.0. / Please update to 2.0.0 -- the newer version twice and
the player's own never, so a popup for a version they had since installed read identically to a live
one. Now: You have 1.0.0; player 'Bob' has 2.0.0.
ShowUpdatePopup's fourth parameter was threaded through the entire combat-deferral state machine
and then discarded. Every popup spec passed "1.0.0" as that argument and no assertion anywhere
mentioned it -- a fixture constant nothing read, including the spec whose title enumerates the
message's contents. It is asserted now, through the deferral round trip as well. Where our own
version cannot be resolved (HostVersion returns the literal "unknown"), the old single-version
phrasing is kept: "You have unknown" is worse than not saying it.
Fixed -- two different players who share a base name
GW_StripRealm reduces Bob-Whitemane to Bob and the 20-second REQ suppression was keyed on
that. The strip is correct for the case it exists for -- one player arriving over both AceComm GUILD
and GreenWall in two name shapes -- but character names are unique per realm, not per
connected-realm group, so Bob-Whitemane and Bob-Faerlina collapsed to one key. Whichever asked
second inside the TTL was dropped before their payload was read: no reply, no retry (this
library has none), and the only trace was a debug line calling them a duplicate. Their collection
window closed without our version in it.
The full name is now stored alongside the expiry. Two senders sharing a base name are the same person only when either arrived without a realm suffix (the realmless spelling of the other) or the realms match. When a bare name is suppressed against a realm-qualified one the stored spelling is upgraded to the qualified form, so a third character on a different realm is compared against a realm we know rather than against a base name that matches everybody.
Unchanged -- 38000 stays, and this entry records an attempt to "fix" it that was WRONG
38000 is still 38000. It was briefly changed to 38002 during this session and changed
back. Recorded rather than quietly reverted, because the reasoning that produced the change was
persuasive, wrong, and will look persuasive again.
The argument for changing it, which was all true and still insufficient: reading every
## Interface: in the installed AddOns tree, 38000/38001/38002 appear across roughly sixty
TOCs, essentially all *_Wrath.toc -- DBM, WeakAuras, Details, Questie, Ace3, !BugGrabber,
TomTom, LibSharedMedia. The actively-maintained ones are all on 38002 while the stale ones sit on
38000, and DeltaSync.toc already carries 38002.
Why it was wrong anyway. Every one of those facts is about what OTHER addons target. None of
them is evidence about which build the operator's server reports, and that is the only thing
this number has to match. Asked directly, the answer was that Whitemane does report 38000. So
38002 would have flagged this addon as out of date on the one server it was "corrected" for.
v1.3.1's warning was right and is restated here rather than weakened: 38000 is the Whitemane
private server, it appears in no Blizzard product list by design, and a pass that verifies each
number against Blizzard's own products -- or against what the rest of the fleet declares -- will
conclude it is bogus and silently drop support for a live target. Absence from Blizzard's sources
is not evidence against a private server; it is what a private server looks like.
The transferable rule, which the fleet-wide sweep did not supply: a private server's build number is a fact about that server. It cannot be derived from other addons, from Blizzard's docs, or from the number's neighbours -- only from the operator or from the client itself. Ask.
Fixed -- five luacheck warnings on correct code
.luacheckrc listed SlashCmdList and StaticPopupDialogs as read_globals, with a comment
asserting that luacheck permits assigning their fields. It does not -- W122 "setting read-only
field" fired on all five sites, which are the normal and only way an addon registers a slash command
or a dialog. Both are now in globals. The stale comment claiming otherwise is corrected in place.
This surfaced only because luacheck is in fact runnable here, through writ. CLAUDE.md said it was
not installed on this box and that the language server's diagnostics were the working check; that is
no longer true and the note is updated.
Adopted the WoWAPITesting harness at 226c0e0
Pin moved c9f3199 -> 226c0e0 the same day, once the harness pushed the two things this session
asked it for. 226c0e0 is a docs-only commit naming 2406895, which carries the code; both are on
the harness's origin/main. Suite re-run against the new pin: 320 passed, 0 failed, 625/625
executable lines (100.00%) -- unchanged by the move itself.
Two adoptions, both from that entry:
frames.layerMismatch(), now asserted nil in the setup of both widget-layer spec files. It is an identity check on_G.CreateFrame, built by the harness from this repo's own report: the one probe that can see awow.reset()run afterframes.reset(), which a capability probe cannot and which cost an afternoon here.GetMouseButtonClicked()withwow.mouseButton, to the exact contract this repo filed. The stand-in inTests/env_vc.luais deleted and the two right-click specs setwow.mouseButton.
Checked the rest of the entry for branches that would run for the first time: this library
feature-detects neither GetPlayerInfoByGUID nor canaccessvalue, asserts no bank-slot or
pinned-object count, and classifies no invite outcome. Nothing else reaches here.
Adopted the WoWAPITesting harness at c9f3199
Pin moved f58da5e -> c9f3199 (2026-09-09), 103 commits, a clean ancestor. Suite re-run
against the new pin at the time of the move: 171 passed, 0 failed, 292/292 executable lines
(100.00%) -- unchanged by the adoption itself, and no stand-in in Tests/env_vc.lua became
redundant, so nothing was deleted. (The release figure at the top of this entry is higher
because the window and its specs landed after this.)
c9f3199 is reachable from the harness's origin/main and that repo has nothing unpushed, so the
submodule pointer resolves for anyone cloning this one -- checked rather than assumed, because the
newest Adoption log entry sitting in the harness's working tree makes "is this pinnable" a live
question rather than a formality.
Finding 6 measured this as 0813bf6, 136 commits behind main 59c4280. That was already stale
when it was written -- v1.3.1 moved the pin to f58da5e on 2026-08-13. Same as the finding's TOC
quote showing 120007 where the file said 120100: round 2 was written against the pre-v1.3.1
tree. The real gap was 103 commits, not 136.
c9f3199 rather than "latest" on purpose. The harness's newest Adoption log entry (2026-09-10, two
guild-invite refusal strings) is marked IN THE WORKING TREE ONLY, NOT PUSHED, DO NOT PIN FOR THIS
YET, and those changes are uncommitted in the harness repo -- so c9f3199, which is both HEAD
and origin/main, is the newest pinnable commit and correctly excludes them.
Two adoption entries in that range could have reached this library, and neither does:
RegisterAddonMessagePrefixnow returnsEnum.RegisterAddonMessagePrefixResult, nottrue. Consumers asserting a truthy return go red. Nothing here calls it -- AceComm registers on our behalf -- and no spec of ours asserts on it. The oneassert.is_true(...)on that call lives in the harness's ownspec/messaging_spec.luaand was fixed inside the pin we just took.GetServerTime,wow.pendingTimerCount(),wow.flushTimers(), carrying a 2026-09-10 correction that an earlier "delete your stand-in" instruction was unsafe for any consumer driving its own clock.Tests/env_vc.luadrives no clock of its own (that stand-in went in August), so there was nothing to keep and nothing to delete.
Checked by reading Tests/env_vc.lua and every Tests/*_spec.lua for those four names, not by
trusting that a green suite meant a clean adoption.
Still open
Nothing. All seven findings from docs/AUDIT.md are answered and closed.
Not part of the download
Tests/api_spec.lua is new -- the outward-facing API contract, which is the surface a spec suite
cannot reach on its own because the consumers live in other repos. New dedup and popup cases went
into Tests/request_spec.lua and Tests/popup_spec.lua. All excluded from the packaged zip.
This repo was also onboarded to writ, and docs/AUDIT.md and Tests/HARNESS_CONTRACT.md became the
historical record: findings and contracts now travel as threads in writ's inbox rather than as
writes to a markdown board somebody has to be watching. Neither file was edited. There is no
watcher.
[v1.3.1] (2026-08-13) — Retail 12.1.0 compatibility (no MINOR bump)
No library behaviour changed. VersionCheck-1.0.lua is untouched and MINOR stays at 12 —
deliberately, because a MINOR bump exists to make LibStub prefer a newer embedded copy, and there is
nothing here for it to prefer. The only shipped file that moved is the .toc.
Fix — flagged out of date on the current Retail client
## Interface: declared 120007 (Retail 12.0.7). The installed Retail client is 12.1.0, so
the addon list showed VersionCheck as out of date on every Retail install. Now 120100.
Verified against C:\Program Files (x86)\World of Warcraft\.build.info, which reports the real
version of each installed product, rather than from memory. The other six entries are unchanged and
38000 in particular must not be "corrected" — it is the Whitemane private server, it appears in
no Blizzard product list by design, and a pass that verifies each number against Blizzard's own
products will conclude it is bogus and silently drop support for a live target.
Not part of the download
Everything below is Tests/ and docs/, both excluded from the packaged zip by .pkgmeta.
Recorded because the test suite is how the last two shipped bugs were found, and it is worth knowing
what state it is in.
Adopted the WoWAPITesting harness at 0813bf6
Pin moved 47dd048 → 0813bf6. All five contracts this addon raised on 2026-08-04 came back
DELIVERED, so their local reference implementations in Tests/env_vc.lua are deleted rather
than kept alongside the harness's own — a local copy that survives adoption is a second source of
truth that drifts silently while the suite stays green. env_vc.lua drops from 287 to 199 lines;
the specs now use wow.inCombat, wow.popups and wow.advanceTime directly.
Two of the five needed more than deletion:
wow.popupsrecordsinfo— the live dialog table — rather than a copiedtextfield, so assertions moved topopups[i].info.text. It is a reference, so it reads the dialog's current text rather than a snapshot taken at show time.StaticPopup_Showraises on an unregistered dialog name, it does not returnnil. The harness checked Blizzard's current Classic EraStaticPopup.lua; thereturn nilthis addon's contract quoted came from older FrameXML and was wrong. The correction is stricter in exactly the direction the contract asked for — a registered/shown name mismatch now fails loudly at the call site instead of returning a nil somebody forgot to check.
The monotonic-clock change, flagged upstream as the one that can break a green suite, broke nothing
here: no spec asserted an absolute GetTime().
Found while adopting — a harness defect, raised as a contract
wow.reset() empties wow.frames, the registry advanceTime ticks OnUpdate on. But
ChatThrottleLib's frame is created once, when Ace3 first loads inside whichever spec asks
first. From the second reset onward nothing ticks it, so a message CTL chooses to queue is
never despooled: bQueueing latches true and wow.sent stays empty with no error.
It hides almost perfectly, because CTL sends directly whenever it has bandwidth — every ordinary
spec passes. It bites only when CTL genuinely queues, which is precisely the case the OnUpdate
ticking was added to make testable. Written up in Tests/HARNESS_CONTRACT.md;
Tests/env_vc.lua re-registers that one frame as a deliberately narrow local workaround until it
lands.
Suite: 151 specs, 100% line coverage of VersionCheck-1.0.lua (269/269), unchanged by the move.
Adopted the harness at f58da5e — and the sixth contract came back DELIVERED
Pin moved 0813bf6 → f58da5e, nine days and 138 commits. The suite is 151 passed, 0 failed
and 269/269 executable lines (100.00%) both before and after, so nothing upstream broke us.
The CTL workaround above is deleted — it landed in 9f0dd38. That commit was authored on
2026-08-04 but sits after the 0813bf6 pin adopted the same day, which is why the local
re-registration in Tests/env_vc.lua stayed live for nine days rather than being an oversight.
wow.frames is now weak-valued and reset() compacts it instead of emptying it, so a frame
lives exactly as long as something holds it — which is what the client does.
Deleting it was not merely tidying: with the harness fixed, the stand-in was actively wrong.
compactFrames() leaves the registry dense up to a private frameHigh, so
wow.frames[#wow.frames + 1] = CTL.Frame wrote one slot past it — a slot forEachFrame never
visits and the next CreateFrame silently overwrites. The regression guard for the harness's fix is
messaging_spec's "delivers the batch once a throttled client stops refusing", which still passes
with the stand-in gone. Tests/env_vc.lua now holds only what is genuinely this addon's own: the
GreenWall API fake, the jitter freeze, the payload helpers, and the library reload.
Checked and not applicable, rather than assumed: the strsplit behaviour change (c3129ba,
which stopped dropping a trailing empty field and would shift every field in a packed record) does
not reach this library — it never calls it, and neither does AceComm-3.0 or AceSerializer-3.0, the
only two libraries it loads. Same for securecall and strlenutf8, both new upstream and neither
used here.
Protocol adoption, both overdue. Tests/HARNESS_CONTRACT.md had all six contracts sitting under
## Open with not one answer — the file is an outbox, and responses are only ever written into the
harness's docs/contracts/VersionCheck-1.0.md, so a delivered contract was indistinguishable from an
ignored one. Each request now carries an appended > **Delivered — …** pointer and the file opens
with a status table; nothing was moved or re-titled, because the order is part of the record. And
docs/AUDIT.md — the standing peer-review file, carrying seven open findings from two rounds, none
of them answered — is now tracked and pointed at from CLAUDE.md, which is the only file loaded
unconditionally every session.
.luacheckrc gained 122/math under files["Tests"]: env_vc.fixJitter deliberately swaps
math.random to make a jittered reply's timing assertable, and env_vc.reset puts the real function
back.
[v1.3.0] (2026-08-04) — offline test suite, and a version-reporting fix it found (MINOR 12)
First release with an offline unit-test suite. 151 specs, 100% line coverage of
VersionCheck-1.0.lua (269/269 executable lines), run locally with lua Tests/wowapi/run.lua.
Fix — hosts registered by name reported unknown to everyone else
VC:Enable("MyAddon") (the plain-string form, documented in the README and used by hosts that have
no AceAddon object) produced a host entry with no .host. Three places needed that host's version
and each resolved it differently:
FireBatchfell back to the TOC —hostEntry.host.VersionorGetAddOnMetadata(name, "Version")or"unknown";ProcessVersionResponsesForHostdid the same;HandleRequestdid not — it went straight fromhostEntry.host.Versionto"unknown".
So a string-registered host announced its real version in its own request and answered "unknown"
to every request it received. "unknown" contains no digits, so CompareVersion parses it as
0.0.0 and sorts it below every real version: such a host could never be the highest version any
guildmate saw, and nobody was ever prompted to update on account of it. Silent, and it only affected
other people's clients, which is why it survived this long.
All three sites now call one HostVersion(hostEntry) helper, so they cannot drift again. The helper
is also nil-guarded on GetAddOnMetadata — FireBatch and ProcessVersionResponsesForHost called
it unguarded, which would have been a hard error on any client carrying neither the C_AddOns
namespace nor the bare global (the IS_DEV_BUILD check already guarded; those two did not).
Fix — the "no version responses" notice was chat spam, not a diagnostic
ProcessVersionResponsesForHost printed "[VersionCheck] No version responses received for <addon>."
with a bare print, so it went to the user's chat window unconditionally. Every other diagnostic in
the file goes through the debug-gated VCPrint; this one line did not, which reads as an oversight
rather than a decision.
It fires whenever nobody else in the guild answers — the normal state for a niche addon, and
nothing the user can act on. And it is not a one-off login notice: hosts re-trigger FireBatch
periodically (TOGProfessionMaster's scanner every ~10 minutes) and each batch clears
noResponseLogged, so it repeated for the whole session, once per host. A user with three
VC-hosting addons and no guildmates running them saw three lines every ten minutes.
Now routed through VCPrint, so it is still there under /vcdon where it is actually useful. This
restores the library's stated contract that its only user-facing output is the update popup.
Not changed — the response jitter
Recorded here because it was investigated and the answer is worth not re-deriving. HandleRequest
spreads replies with math.random(0, 8) and never seeds the RNG, which looks like a bug: an
unseeded Lua 5.1 RNG yields the same sequence every run, which would defeat the whole point.
Checked against Blizzard's global API listing rather than assumed: math.randomseed is not
exposed to addons at all — the list runs math.rad, math.random, math.sin. No addon can seed
it, so the common math.randomseed(time()) defence is impossible in WoW. Blizzard's own UI relies
on math.random varying across sessions (/castrandom picks a spell with it) and never seeds it
either, so the client must seed it internally. That last step is inference about the binary, which
the Lua source cannot prove — but it is the only reading under which Blizzard's own features work.
fastrandom exists as a client global and Blizzard use it for exactly this kind of jitter, but
swapping to it buys nothing over a seeded math.random and its range semantics are not something
this session verified. Left alone, with the finding recorded in a comment at the call site.
Test suite
Built on the shared WoWAPITesting harness, added as
the submodule Tests/wowapi (pinned at 47dd048). Specs drive the real event and comm paths —
PLAYER_ENTERING_WORLD on the library's own frame, the real AceComm CHAT_MSG_ADDON receive
handler, real AceSerializer payloads, real ChatThrottleLib — rather than calling methods directly.
That distinction matters here: TriggerVersionCheck is a shim that deliberately does nothing until
PEW has fired, so calling it proves nothing and firing the event proves everything.
Covered: LibStub registration and upgrade-in-place, the PEW wiring, Enable's input handling,
batch construction and the 12s collection window, request answering with its 20s cross-transport
dedup and 0-8s reply jitter, response collection, CompareVersion, the warn/stay-quiet decision
including lastShown and both dev-build suppressions, the popup and its per-host combat deferral,
the whole GreenWall transport including the echo-driven second round, the slash commands, and the
three legacy shims.
Line coverage is not branch coverage, and two of the spec files exist because of that gap rather than to raise a percentage:
Tests/metadata_spec.lua—local GetAddOnMetadata = (C_AddOns and C_AddOns.GetAddOnMetadata) or _G.GetAddOnMetadatais one line with three outcomes, and the harness defines both spellings, so every other spec took theC_AddOnsbranch and only that. The line read as covered while two of its three paths had never run — including the nil guard added in this release. That line is the v1.2.8 fix for a login crash on WoW 11.x clients, so getting the fallback wrong breaks older clients silently. Now exercised on a modern client (namespace only), a legacy client (bare global only), and a client with neither.Tests/transport_spec.lua— the batch on a real wire. A payload above 255 bytes (about a dozen registered hosts, which is a realistic install) is split by AceComm into multipart chunks; the split, the reassembly and the equally-multipart reply are now round-tripped through the real AceComm rather than asserted on a hand-built payload. Alongside it, the sends the client refuses:NotInGroupfor a player who is not in a guild — every login for anyone who installs a host addon before joining one — and a throttled send, which is the normal case at login when every addon broadcasts at once. The library never inspects a send result, so what mattered was that it degrades quietly and still closes its collection window rather than stranding every host inVersionCheckActivefor the session.
Tests/env_vc.lua carries local reference implementations of four things the shared harness does
not provide yet — InCombatLockdown, the StaticPopup surface, WoW's chat-frame print, and a clock
that ticks frame OnUpdate so ChatThrottleLib despools. All four are written up in
Tests/HARNESS_CONTRACT.md and are waiting on a harness session; the suite runs green without them.
TOC interface versions refreshed
## Interface: was 11509, 11507, 20505, 30405, 38000, 40402, 50503, 50502, 120001, 110207, 120000
and is now 11509, 20506, 30405, 38000, 40402, 50504, 120007 — one current number per target,
which is what the packager's enable-toc-creation needs to emit one TOC per flavour. This matches
the list DeltaSync already carries, so the two libraries no longer disagree about which clients
the suite supports.
| Target | Was | Now | Why |
|---|---|---|---|
| Retail | 120001, 120000, 110207 |
120007 |
The installed client is 12.0.7. Nothing in the old list reached it |
| Anniversary / TBC | 20505 |
20506 |
The installed client is 2.5.6 |
| Mists | 50503, 50502 |
50504 |
50504 is current; both old entries are superseded and same-family |
| Classic Era | 11509, 11507 |
11509 |
Installed client is 1.15.9; 11507 is superseded and same-family |
| Whitemane | 38000 |
38000 |
Kept. A private server, so it appears in no Blizzard product list — see below |
| Wrath | 30405 |
30405 |
Unchanged — not installed here, so left as found |
| Cataclysm | 40402 |
40402 |
Unchanged — not installed here, so left as found |
Retail and Anniversary were checked against the installed clients' own .build.info; Mists and
Whitemane against the interface numbers the rest of the AddOns tree already uses.
38000 is Whitemane, a private server, and was very nearly deleted from this list on the grounds
that no Blizzard client has that interface number. That is true and irrelevant: verifying against
.build.info and Blizzard's UI source can only ever see Blizzard's own products, so a private
server is invisible to that method by construction, and "I could not find it" is not "it does not
exist". The corroborating evidence was on disk the whole time — sibling addons in the tree carry
## Interface: 38000 on its own and as 30405, 38000, which is what a Wrath-era private server
target looks like.
Duplicate numbers within one flavour family are still worth avoiding: the packager generates a
per-flavour TOC from this list, so two Mainline entries make which one it picks a coin toss. That
reasoning does not extend to 38000, which is its own target rather than a duplicate of 30405.
Repository maintenance
.pkgmeta— addedTests; removed nine dot-entries (.git,.github,.vscode,.claude,.luarc.json,.markdownlint.json,.markdownlintignore) that were no-ops, since the packager prunes everything beginning with.unconditionally.wow-version-replication.ps1— two fixes, both verified with-DryRun. A bare folder name in.pkgmetacompiled to^docs$, which matches only a file nameddocs, sodocs/had been replicating into every other WoW flavor despite being listed; bare entries are now resolved against the repo and treated as a prefix match. And the always-skip list now covers any path component beginning with a dot, which is the script's own responsibility (dot-entries in.pkgmetaare no-ops, so it cannot learn them from there) — without it the.pkgmetacleanup above would have started replicating.claude/,.vscode/,.bustedand the.gitgitdir pointer.- Added
.busted(the two-line shared-config shim) and.luacheckrc;.luarc.jsongained the busted DSL globals andTests/wowapiinworkspace.ignoreDir.
[v1.2.9] (2026-07-28) — Classic Era interface bump to 11509
TOC-only change. The leading Classic Era interface number in VersionCheck-1.0.toc moved 11508 → 11509 to match the current Classic Era client build; the rest of the multi-flavor interface list (including the 11507 fallback) is unchanged.
No library code changed, so MINOR stays at 11.
[v1.2.8] (2026-05-23) — fix GetAddOnMetadata crash on WoW 11.x clients (MINOR 11)
Login-time crash reported from Silvermoon:
VersionCheck-1.0/VersionCheck-1.0.lua:39: attempt to call a nil value
WoW 11.x moved GetAddOnMetadata into the C_AddOns namespace and removed the bare global. Classic Era picked up the same change in its current client build, so every call site that hit the global was nil-dereferencing — first at chunk-load time (the IS_DEV_BUILD check at line 39), and then at FireBatch (VersionCheck-1.0.lua:191) and ProcessVersionResponsesForHost (VersionCheck-1.0.lua:375).
Resolved once at chunk-load with a local shim: local GetAddOnMetadata = (C_AddOns and C_AddOns.GetAddOnMetadata) or _G.GetAddOnMetadata. The three downstream call sites bind to the local naturally and need no further changes. _G.GetAddOnMetadata fallback keeps older clients (where C_AddOns doesn't exist) working. The IS_DEV_BUILD check additionally gates on GetAddOnMetadata and … so a hypothetical client missing both APIs degrades to "not a dev build" rather than crashing chunk-load.
.luarc.json updated: C_AddOns added to Lua.diagnostics.globals.
[v1.2.7] (2026-05-23) — GreenWall transport, popup hardening, debug-state SV fix (MINOR 10)
Bundles a long-paused feature branch with three bug fixes in the popup and debug-state paths. First release on MINOR 10.
GreenWall optional confederation transport
Version checks now reach every linked co-guild in a GreenWall confederation, not just the player's literal <guild>. AceComm GUILD remains the sole transport for users without GreenWall installed.
Implementation rides hooksecurefunc("ChatEdit_ParseText") to issue GreenWallAPI.SendMessage from inside the user's chat-keystroke scope. An echo of our own segment re-arms a fresh 12s collection window so confederation peers' RSPs land in round 2 of ProcessAllHosts. A 20s REQ dedup window suppresses the same sender arriving on both transports (which happens for peers in the user's own guild AND the confederation).
Same-realm-cluster reach is unchanged. Confederation peers on a different server cluster will receive the REQ but their WHISPER reply won't reach back — an acknowledged design limitation of AceComm WHISPER, not a bug.
Popup bug 1 — lastShown now persists across batches
Pre-2.2.7 the in-progress popup-dedup work wiped hostEntry.lastShown = nil at the top of every FireBatch. Hosts that re-trigger FireBatch periodically (TOGProfessionMaster's scanner does this every ~10 minutes for its own user-count refresh) were re-popping the same outdated-version popup on every cycle. The lastShown reset is gone; the field persists across batches and a strictly-newer version still surfaces via the strict-greater check in ProcessVersionResponsesForHost. UI reload clears the field naturally (in-memory only, no SavedVariable).
Popup bug 2 — popups defer in combat
ShowUpdatePopup now checks InCombatLockdown() and stashes pending args on hostEntry._pendingPopup. A single library-scoped frame registers PLAYER_REGEN_ENABLED and drains all hosts' pending entries in one pass when combat ends. Per-host (not library-scoped) stash matters: simultaneous hosts queued in the same combat would otherwise clobber each other.
Debug-state SavedVariable fix
The debug flag now reads VersionCheck10_DebugEnabled lazily on every access (isDebug() helper) instead of caching a VC.debugEnabled field at chunk-load. When both standalone and embedded copies of the library coexist at the same MINOR, and the embedded copy wins LibStub registration first, the standalone chunk that owns the SV declaration never runs past its if not VC then return end guard — so a cached read at the embedded chunk's load time saw the SV as nil and /vcdon silently reverted to off on every /reload. Lazy reads sidestep the race entirely: whichever code path eventually populates the global takes effect immediately for all subsequent prints and toggles.
Other
Debug-state migrated from _G.VC_DebugEnabled (bare global) to the VersionCheck10_DebugEnabled SavedVariable, with a proper SetDebug() helper. Standalone TOC declares the SV; embedded hosts that want /vcdon to survive /reload need to declare it in their own TOC (documented in README.md).
Removed leftover [VersionCheck-DIAG] chunk-load prints from the earlier SV-load-order investigation that produced the lazy-read fix.
Repo maintenance: added CLAUDE.md, .claude/, LICENSE (MIT), CHANGELOG.md, docs/Curseforge_Description.html. Renamed read.me → README.md. Updated .pkgmeta ignores accordingly.
[v1.2.6] (2026-04-02)
TOC interface-version line refresh — adds support for the then-current WoW Classic and retail Interface numbers.
[v1.2.5] (2026-03-30) — fix VersionCheck-v1.5.0 detection when :Enable is called with a string
When VC:Enable was called with a plain string name (instead of an addon object), hostEntry.host was nil and myVersion fell back to "unknown", bypassing the VersionCheck-v1.5.0 dev-build suppression check. Now both FireBatch and ProcessVersionResponsesForHost fall back to GetAddOnMetadata(addonName, "Version") so dev builds are detected correctly regardless of which :Enable form the host used.
[v1.2.4] (2026-03-04) — split RegisterComm from FireBatch, add 5s broadcast delay (MINOR 9)
Two bugs causing "no responses received" when both clients had the newer code:
RegisterCommwas insideFireBatch, called at the same time as the broadcast. If another player'sVC10_REQarrived during the startup window (beforeFireBatchran), we were not yet registered to receive it and silently missed it. Fix:RegisterCommnow happens immediately atPLAYER_ENTERING_WORLDso incoming broadcasts are never missed.FireBatchwas called directly fromPLAYER_ENTERING_WORLDwith no delay. The GUILD addon-message channel is not guaranteed to be fully available at that exact moment in Classic Era — the broadcast was going out into nothing. Fix: 5sC_Timer.Afterdelay before the broadcast only;RegisterCommstill happens immediately.
[v1.2.3] (2026-03-03)
Workspace config cleanup — removed stale folder reference, updated WoW API extension to ketho.wow-api-0.22.3.
[v1.2.2] (2026-03-03) — performance: stuttering, thundering herd, init freeze
- Defer
RegisterCommcalls intoC_Timer.After(10)to preventC_ChatInfo.RegisterAddonMessagePrefixfrom causing a 2–5s freeze during the post-login addon-load phase. - Add random 0–8s jitter before each client sends its
VCRESPwhisper to spread responses over time (thundering-herd fix). - Move collection timer to the broadcast site (12s window = 8s jitter + 4s buffer);
OnCommReceivednow just accumulates responses. - Extract
ProcessVersionResponsesForHosthelper to clean up result logic. - Guard
debugstack()behindVC_DebugEnabledto avoid unnecessary call-stack walking in production.
[v1.2.1] (2026-03-03)
Packaging metadata fixes — .pkgmeta, workspace, and TOC adjustments to align with the standalone-addon release flow.
[v1.2.0] (2026-03-02) — Ace3 dependency, not embedded
Removed embedded Ace3 from the library. VersionCheck-1.0 now uses the global Ace3 libraries provided by users' standalone Ace3 install. Reduces zip size and avoids version conflicts when other addons embed Ace3 at a different version.
Pre-history (v1.1.0 and earlier)
Initial public releases of the batched cross-guild version-check library. See git tag history for commit-level detail.
This mod has no additional files

