GuildRoster-v1.2.1
What's new
Changelog
All notable changes to LibGuildRoster-1.0 are documented here.
1.2.1 -- NormalizeName follows the regional-names switch
The LibStub MINOR moves 32 -> 33. No API change.
Fixed
NormalizeName's cache now keys onRegionalNames()as well as the realm. It wiped only whenGetRealmName()changed, so a name normalized whileRegionalUniqueNamesEnabled()was true kept its bare spelling after the switch went false, whileGetNormalizedPlayer(uncached) followed the switch -- the two then disagreed for the same character. A flip of the switch now wipes the cache and re-keys the sister rosters exactly as a realm change does (lib.nameCacheRegional).RegionalNames' docstring, which said the answer was "not cached", now says the name cache keys on it. Found by FastGuildInvite (peer review740ceb25): its Forever specs flip the switch, and a later spec file saw a roster row keyedMeagainst a player key ofMe-Testrealm, 27 failures under a shuffled order. On a live client the switch is not expected to change within a session (not verified), so the player-visible impact is believed to be none.- Specs:
normalize_spec"re-spells a cached name when the switch flips, agreeing with GetNormalizedPlayer", andsister_spec"re-keys a fed roster both ways when the regional-names switch flips" -- with the switch on,CanonNamedrops the realm half, so a storedAna-Testrealmre-keys toAnaand back again. The HOME roster is not re-keyed after login (the documented build-once margin), so a mid-session flip, if a live client can make one (not verified), leaves its keys in the old spelling.
Changed
- Removed: the three dev-only boards --
docs/AUDIT.md,docs/LIBRARY_CONTRACTS.mdandTests/HARNESS_CONTRACT.md. Their content was imported into the writ inbox, where every item is closed, and git history keeps the files. None of them shipped (docsandTestsare in.pkgmetaignore:).
1.2.0 -- "ready" and "complete" are separate answers
The LibStub MINOR moves 31 -> 32. Nothing changed shape: three additions, and the server's "No player named X" now reads as known offline (see Changed).
Request from FastGuildInvite (contract a19005f7ee7b, 2026-10-09). FGI judges
a guild-log departure the live route never saw by asking IsReady /
IsInGuild, and IsReady goes true on a SHORT login build: the stabilization
block counts a short scan as complete once retryCount >= MAX_RETRIES, so the
build goes ready on the rows it has. A current member that build never read
then reads as gone. LoginBuildIsShort finishes such a build later, but
nothing told a consumer when -- and it gives up after MAX_LOGIN_REPAIRS (3)
without telling anyone either.
New
lib:IsRosterComplete()-- ready AND the held roster came from a scan that saw as many rows asGetNumGuildMemberscounted. Backed bylib.rosterShort, set from the scan's ownshortwhen the login build goes ready and on every repair: a repair that reads every row clears it, one that comes out short sets it (including the retry path, which has already rewritten the roster with the partial rows). Reset on leaving a guild. A build still short when the repair cap runs out stays incomplete for the session, which is the honest answer.OnRosterCompleted()-- fires on each incomplete -> complete edge: right afterOnRosterReady(and afterOnRosterUpdatedon the same event) when the login build read every row, or when a repair finishes a short build. One handler therefore covers both cases. A repair of a build that was already complete does not fire it again.OnMemberLeft(name, guildKey, kicked)--kickedistruewhen the home message matched the kick patterns,falsefor "has left", and absent for the sister-roster diff, where nothing says why. FGI asked for it as an option so it can drop its guild-log round trip for live departures.lib:GetSisterPresence(guildKey, name)--trueonline,falseknown offline,nilnobody said, for one sister member. Asked for by DeltaSync (contract90f5fd1913f4):GetOnlineMembersScopedcannot tell "known offline" from "nobody said", and DeltaSync's sister-whisper guard declined both. Same sources as that list and no/who("candidates, not presence"). The home guild answers from the roster:isOnlinefor a member,nilfor anyone else.
Changed
The server's "No player named X" now records X as known OFFLINE.
OnPeerUnreachableused to clear the sighting stamp, leaving "nobody said", so a consumer's guard let X straight through again. It now also stampslib.offline[lower(guildKey)][X]in every sister roster holding X, andsisterOnlinereads it newest-wins: a strictly newer sighting or stream "on" beats it, a sighting in the same instant keeps "on" (the clock cannot order them), an aged relay sighting from before it loses. No TTL. Dropped with the guild byRemoveSisterRoster. The guild tab rows and the level readings use the same rule, so they show the member offline too.A repair's retries count as ONE repair. Found by Peer Review (G1, 2026-10-10) before this shipped: a repair that came out short retried like the login build, and every retry event spent one of the
MAX_LOGIN_REPAIRS(3). Three short events spent the cap, so the event that finally carried every row was never read andIsRosterCompletestayed false for the session.lib.repairInProgressnow spans a repair's retries andloginRepairscounts repairs started.lib.repairEventsis the hard bound on events any repair may read (MAX_LOGIN_REPAIRS * (MAX_RETRIES + 1), 18), so a server that never lifts its request throttle cannot keep the library scanning. KNOWN COST: a client that never fills its rows now reads up to 8 times instead of 3 before giving up (one repair's six events plus two more repairs).
Tests
buildonce_spec: seven specs for completeness -- complete login build fires once after ready; short build is ready but incomplete, then a repair completes it and fires once; a repair that comes out short goes incomplete and the next completes it; a short repair's retries count once, so the full event after them completes it (G1); a repair the server never lets retry stops at the event bound; the repair cap running out short never fires; a guild change resets it. The cap spec now asserts reads equal the events read and stop at the bound.chat_specpinskickedfor both messages.livestream_spec: six specs forGetSisterPresence-- nil when nobody said; false after a not-found with no earlier sighting (and absent fromGetOnlineMembersScoped); a newer sighting or stream "on" beats it and an older relayed one does not; no/whoinput; the home guild and bad arguments; dropped with the guild.- 947 specs pass; line coverage 100% on the library and both env layers.
Not covered
- Run in no client. How often a login build actually goes ready short in play is not known here.
1.1.4 -- no more "secret string value" error on WoW: Forever
The LibStub MINOR moves 30 -> 31. No API changed shape.
Field report via FastGuildInvite (peer review 457f6869, Discord, WoW
Forever, count 5): "LibGuildRoster-1.0.lua:2587: attempt to compare local
'message' (a secret string value, while execution tainted by
'GuildRoster')", ParseSystemMessage <- OnChatMsgSystem <- OnEvent.
Fixed
- I gated secret chat payloads on the flavour instead of on the value.
OnChatMsgSystemandOnChatMsgGuildskipped a payload only whenIS_RETAIL(WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) andC_ChatInfo.InChatMessagingLockdown()were both true. WoW: Forever runs the retail client -- itsChatInfoDocumentation.luaflagsCHAT_MSG_SYSTEM,CHAT_MSG_GUILDandCHAT_MSG_OFFICERSecretInChatMessagingLockdownand leavesplayerNamewithoutNeverSecret, and itsFrameScriptDocumentationhasissecretvalue-- but it is notWOW_PROJECT_MAINLINE, so the secret message reachedmessage == "". Both handlers now return whenisSecret(arg)(1.1.3's helper) says the argument is secret, on every flavour. Classic clients haveInChatMessagingLockdownbut never a secret payload, so they keep parsing through a lockdown as before. chat_specmodels a secret payload withissecretvalueon both a Classic and a mainline env, for the system and the guild handler; the old lockdown-flag specs are replaced.- Forever got Classic's 5 s
/whofloor instead of retail's 8 s.IS_RETAILnow also matchesWOW_PROJECT_ID18, which Forever reports: observed in a client on 2026-10-01 and recorded in FastGuildInvite'sFGI_Compatibility.lua(Blizzard's forever tree defines no constant for it). FGI'sfn.getMinScanIntervalalready gives Forever 8 s, so the two libraries now hold their sends apart by the same floor there.WHO_SEND_FLOORis the only thing that readsIS_RETAIL;sistersync_specpins 5 / 8 / 8 for Classic, Forever and Mainline. - 934 passed; coverage 100% on the library,
env_guild.luaandenv_e2e.lua.
Not covered
- Not run on a WoW: Forever client (there is none on this box).
1.1.3 -- no more "secret string value" error on retail
The LibStub MINOR moves 29 -> 30. No API changed shape.
Field report, 2026-09-30, retail: "LibGuildRoster-1.0.lua:1287: attempt to
compare local 'name' (a secret string value, while execution tainted by
'GuildRoster')", from UnitKeyName under OnUnitLevel (UNIT_LEVEL).
Fixed
- 1.1.0's level reading compared unit data that retail can hand back as a
secret value. On retail 12.0+
UnitName,UnitLevel,UnitIsUnitandUnitIsConnectedmay return secrets, and comparing or testing one from addon code is a Lua error. A file-localisSecret(the client'sissecretvalue, absent = nothing is secret) now guards all four:UnitKeyNamereturns nil for a secret name or surname/realm, andOnUnitLeveldrops a unit whose identity, connection or level is secret. The reading is lost for that unit, which is the same outcome as any unit we cannot name. Classic flavours have noissecretvalueand take the old path unchanged. sisterlevel_specmodels a secret as a valueissecretvaluenames and asserts none of the four is read;.luacheckrcand.luarc.jsondeclare the global.
Changed
- Comments only, no behaviour: the GreenWall send helper and
livestream_specnow record that GreenWall 1.14.0'sSendMessagetried a channel send before its relay and raisedADDON_ACTION_BLOCKED52 times on Classic Era on 2026-09-30, and that GreenWall's next release sends API data relay-only with the same signature (peer reviewd49c5edb3f3f), so nothing here changes. - The
Tests/wowapiharness pin moves1ccef14->cc816a0; the whole suite is green on it (930 passed).
Seen in a retail client, 2026-10-01, in a delve: the same line on "target"
printed false Valeera Sanguinar out of combat and true nil in a real
fight, and OnUnitLevel("target") raised nothing either time. Per the
Warcraft Wiki's Secret Values page, a unit's name is secret in combat when
it is neither player-controlled nor in the party or raid -- so the reported
crash was most likely a target or nameplate unit's UNIT_LEVEL.
Not covered
- Which unit was targeted in the in-combat run was not recorded. The secret
UnitLevel/UnitIsUnit/UnitIsConnectedguards were exercised by the spec alone.
1.1.2 -- WoW: Forever whispers go to "First Last", no realm
The LibStub MINOR moves 28 -> 29. No API changed shape; one helper added
(lib:WhisperTarget(name)).
FastGuildInvite contract 54277063, 2026-09-29: on WoW Forever a player saw
three lines of "No player named 'Quebeldorf Flamehammer-ClassicBetaPvE2' is
currently playing." while that player was online and in their guild, and the
reporter "was doing literally nothing". FGI's own whispers were one source; it asked
whether ours were too. Which sends produced those three lines is not known.
Fixed
- Every whisper this library sends addressed the realm-suffixed name, which
the Forever server refuses. 1.1.1 fixed the KEYS on Forever but left the
sends addressing AceComm's raw
sender, which carries the realm. Every WHISPER send now goes throughlib:WhisperTarget(name): the baton'shereanswer to a guildmate'shi, and every sister-pull message (the guild offer, the pull request, the refusal,unchanged,no copy we can vouch for, the delta, the ack and the roster) -- plus the name handed to VersionCheck'sRequestFrom. On a client whereRegionalUniqueNamesEnabled()is true it isCanonName(name), the full name with no realm; everywhere else the name goes through untouched, so no other flavour's addressing changes. That the Forever server accepts the bareFirst Lastis what Blizzard's own whisper UI implies; not verified on a Forever client.
Tests
normalize_spec:WhisperTargetdrops the realm on Forever and leaves a name untouched on every other client.livestream_spec: thehereanswer is addressed exactly as the sender arrived on a normal client (a same-realm sender is bare,Zed), and toZed Stonefistfor a Forever senderZed Stonefist-ClassicBetaPvE2. The sister-pull sites share the same call and are covered by the existing sister-sync and e2e specs on the non-Forever path only.
Docs
- README and the CurseForge page add
lib:WhisperTargetto the API list and a "whisper through it" line to the Forever section, a 1.1.2 section each, and the MINOR moves to 29 in the README header, the feature-detect list anddocs/HEALTHCHECK.md.
1.1.1 -- WoW: Forever names: "First Last", no realm
The LibStub MINOR moves 27 -> 28. No API changed shape; three small
helpers added (lib:RegionalNames(), lib:SurnameSeparator(),
lib:UnitKeyName(unit)).
The user, 2026-09-28: "we may need to look at the forever name normalization, there are no relams, and the characters have a first and last name" / "we probably don't want to break things with normalization".
Fixed
- A Forever character is keyed by its full name, with no realm. On WoW:
Forever a character is "First Last", unique across the region, and realms
are not shown to players (Blizzard, "Create a Name of Your Own in WoW:
Forever").
NormalizeNameappended the viewer's internal realm to every bare name, so one character could key differently on two clients with different internal realms -- the same divergenceCanonNameexists to prevent on connected realms. Now, when the client's ownRegionalUniqueNamesEnabled()says so,NormalizeNamenever appends a realm andCanonNamedrops a-Realmsuffix an API added, so"Drie Stonefist"and"Drie Stonefist-Realm"are one key. Chomp and Total RP 3 made the same call. - The player's own key lost the surname.
GetNormalizedPlayerread onlyUnitName("player")'s first return; on Forever the second return is the SURNAME (Blizzard'sCamelot/NameUtil.luajoins exactly those two), so the player was keyed asFirst-Realm. It is nowFirst Last. - A party member's level reading treated the surname as a realm.
OnUnitLevelbuiltname .. "-" .. secondfromUnitName(unit), which on Forever madeFirst-Last. It goes throughlib:UnitKeyName(unit)now, which joins with the client's surname separator on Forever and with a hyphen everywhere else.
Every other client is untouched: RegionalUniqueNamesEnabled is absent there,
so the switch reads false and every name takes the path it took in 1.1.0.
The separator is read from the engine's
Constants.CharacterNameSeparatorConsts.CHARACTERNAME_SURNAME_SEPARATOR,
with " " when it is absent (Chomp's fallback, and what Blizzard's whisper
parser implies). If a client ever separates the surname with a hyphen,
CanonName keeps the whole string rather than cutting the surname off as a
realm.
Tests
normalize_spec: eight cases for the Forever path -- the switch off by default, no realm appended, a realm suffix dropped, a hyphen separator kept, the player's full name, the missing-separator fallback, a player with no surname, andUnitKeyNameon both kinds of client.env_guild.useRegionalNames(first, surname, sep)models a Forever client, andresetStateclears both globals so nothing leaks into later files. The harness does not model regional names; asked of WoWAPITesting.
Docs
- README and the CurseForge page each gain a "Forever" section, the three
helpers in the API list, and a Forever line in the features list. Both
usage examples now read a unit through
lib:UnitKeyNamerather than bareUnitName, which on Forever returns the surname second and elsewhere drops a cross-realm target's realm when only the first return is kept.
Dev tooling
wow-version-replication.ps1(not shipped): a Deleted whose source file exists again is copied as Changed instead of removed from every replica install, so an editor's save-by-replace cannot delete a live file from the other WoW installs (TOGProfessionMaster's peer review, thread 99d78fe0).
Not covered
- Nothing here has run on a Forever client -- there is no install on this
box. What
GetGuildRosterInfo, chat senders, AceComm senders and/whorows return there is engine-side and was not read from any source; the fix handles both "First Last" and "First Last-Realm" so either is safe. - The separator's actual value is not in Blizzard's Lua source (the constant is engine-supplied). The space is Chomp's fallback, not a reading.
- Sister-guild KEYS (
Guild-Realm) are unchanged; whether Forever's guild names need the same treatment is not known.
1.1.0 -- a live sister-guild presence stream, level-ups, and one shared level refresh
The LibStub MINOR moves 26 -> 27. Two callbacks with one signature, a
request/release pair and the baton's methods added; nothing an addon calls
changed shape. Two new addon-message prefixes, LibGRxlvl
(lib.LEVEL_PREFIX) and LibGRxlive (lib.LIVE_PREFIX), and GreenWall
messages under the addon id GuildRoster.
All seven TOCs now carry ## Category: Library, so the in-game AddOns list
files the library under the "Library" heading with the fleet's other
libraries instead of among the ungrouped addons (VersionCheck-1.0's request,
thread 3ecb0b4bb88f). ## X-Category: Libraries stays for CurseForge. The
title is now in the fleet's brand orange, |cffFF8000Lib: LibGuildRoster|r,
on every flavour (DeltaSync thread 3f323604089b); toc_spec pins both.
Added
THE BATON: the home guild's live presence, carried to the sister guilds. The user, 2026-09-27: "deltasync requires near real time liveness and you do this well, for your host guild. but with cross guild syncs, this is an issue we'll want to tackle", and on the shape, "it would have to be airtight and FAST".
- Who holds it. The online library user in the guild whose name sorts
first (
IsBatonHolder). Every client works it out from the same facts, so agreeing costs no messages.- At roster-ready,
StartLivesendshion GUILD. - Each online library user who sorts before the newcomer whispers
here. They are the only ones who could outrank it. - A ding or zone message on GUILD also proves a guildmate runs the library.
- At roster-ready,
- Handover. It needs no message and no timer. The holder's logoff is the server's own "has gone offline" message, which every client hears at once. They drop the holder from the peer list, and the next name holds the baton. That client's first act is to forward the old holder's logoff.
- What the holder sends over GreenWall (
ForwardLive):on:<name>/off:<name>for each home logon and logoff.lvl:<level>:<name>for a ding by a guildmate who runs no library. One who runs it sends their own ding. A client holding a level refresh sends these too, holder or not -- only such a client reads those dings, so tying them to the baton lost every one whenever the holder ran no consumer that asked for the refresh (Peer Review, thread aabe287416b7, finding D). A ding sent twice changes nothing: the held level only rises, andlivestream_specpins that a repeat fires no second callback. The cost: one ding is one GreenWall message per online client holding a level refresh (each TOGTools user), not one per guild.snap:<id>:<i>/<n>:<names>chunks of who is online (SendLiveSnapshot,SNAP_CHUNKcharacters each). These answer thewantthat every client sends once when it comes up.- A name on the sender's own realm goes bare (
WireName), and the receiver puts the SENDER's realm back on it, not its own. Peer Review asked for it (same thread): a full name can fill a snapshot chunk alone.
- The receiving side (
OnGreenWallMessage):- It places the sender in a listed sister guild, and every member a message names must be in that same roster.
- It records
on/offinlib.live, and a complete snapshot marks everyone it omits offline, except its sender. - It fires
OnSisterLocationsUpdated. GetOnlineMembersScopednow counts a streamonwhile that guild's stream has spoken withinLIVE_STREAM_TTL. A streamoffclears the member's sighting, so a later sighting still wins.- The server's "No player named X" reply (
OnPeerUnreachable) now also overrides a streamon. - The guild tab rows let a newer stream entry beat an older
/whosighting.
- Known gap, stated plainly. When a guild's last library user logs off,
nobody sends their
off, or anyone's after it. The receiver's copy of that stream stops counting afterLIVE_STREAM_TTLand falls back to sightings. It never falls back to a stale "online". - Also asked of DeltaSync (contract 69fba6d764ec): guard directed sends to
sister members with
IsInAnyRoster+GetOnlineMembersScoped. Its guard reads only the homeGetMembertoday.
- Who holds it. The online library user in the guild whose name sorts
first (
Library users report their own zone (
OnZoneChanged). OnZONE_CHANGED_NEW_AREAthe client sends onezone:<text>message on GUILD, and every other client sets that member's zone. The server has no message for a guildmate changing zone. The level refresh (below) now also refreshes an ONLINE member's zone from the rows it already reads. An offline row's zone is the login build's to set.OnHomeMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source), the home guild's half, asked for by TOGTools (contract 9c178c96f673) so Gratz can drop its 60-secondC_GuildInfo.GuildRosterpoll. The operator: "this is the opportunity to have GR get lvl 'live' like it does for presence ... if the GR could get that info for free like it does presence, we wouldn't have to do expensive lookups and compares". What is free, looked for across the per-flavour trees and the web: no server message announces another player's level on any flavour;PLAYER_LEVEL_UPcarries no unit and is the player's own;UNIT_LEVELnames a unit token, so it covers group and target units; the "Level N" guild-achievement broadcast exists only where achievements do (never Era), every tenth level, and was not confirmed to announce -- not used. So there are two sources."comm": every library user sends their own ding on GUILD (OnPlayerLevelUp, one short message per level) and every other library user'sOnLevelCommtakes it from a home member -- proof of a live ding, and proof of presence the same as guild chat."unit":UNIT_LEVELfor a grouped home member (OnUnitLevel), which is a ding only when their held level was read while they were online and they have not come online since (levelStale, set inMarkMemberOnline). The held level ismember.level, soGetMemberstays current and a ding seen by both sources fires once. A mid-session joiner's first level is a baseline (levelUnknown). The player's own ding updates their record and fires nothing. Nothing polls and nothing re-reads the roster; build-once stands. A guildmate without the library, outside the player's group, is not seen.UNIT_LEVELfor a grouped sister member is an online reading for the sister callback below (source"unit").A player's own ding also goes to the sister guilds, over GreenWall (TOGTools contract 58cbd95c507a). Seen in game on 2026-09-27: a sister member dinged 40 and no callback fired, because no
/whoor pull had read their level before the ding, so the first reading after it could only be a baseline.OnPlayerLevelUpnow also sendslvl:<level>withGreenWallAPI.SendMessage("GuildRoster", ...)while a sister guild is listed, when GreenWall's API is version 2 (which sends with no key press, GreenWallAPI.lua:37-38) andC_AddOnsknows this library's id (GreenWall asserts it,API.lua:59; an embedded copy with no GuildRoster folder skips).OnGreenWallLevelplaces the sender by the sister rosters we hold (GreenWall's handler says only whether a message is from our own guild), marks them online, and firesOnSisterMemberLevelChangedsource"comm"-- proof of a live ding, so it needs no earlier online reading and, for a member no reading has placed a level on, fires witholdLevelnil. A sender we cannot place, or in an unlisted guild, is dropped. Not changed: a/whosighting still does not treat a roster copy's level as the "before" reading -- that copy does not say the member was online, so a higher/wholevel could be a ding from hours earlier.lib:RequestLevelRefresh(owner)/lib:ReleaseLevelRefresh(owner), the one exception to build-once (TOGTools contract 7e447fb6d5c7). The operator, 2026-09-27: "ok, then we should probably move the 60s recheck into guildroster, shouldn't we". While any consumer holds it, aGUILD_ROSTER_UPDATEafter ready re-reads the rows' LEVELS (ReadRosterLevels, source"roster") when it answers our request or whenLEVEL_REFRESH_INTERVAL(60 s) has passed since the last read, and asks the server again (OnLevelRefreshEvent) at most once per interval, only when the event'scanRequestRosterUpdatesays it may, and never on the event whose login verification just asked. One request per client whoever holds it. No timer: the first cut ran the request from aC_Timer.NewTickerand was refused, because the operator's rule is no timer that touches the WoW API; so the cadence follows the guild's own events, and a quiet guild refreshes less often than every 60 s. Levels only -- membership, presence, rank and notes stay with their chat events; the show-offline setting is never written; with no holder the event is still ignored and reads no row (buildonce_specunchanged). A "roster" reading fires only for a member our presence model has online and who has not come online since the held level was read; a row with no level is skipped, never a baseline; the held level never drops, so a cache behind a"comm"ding does not fire it twice.OnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source), asked for by TOGTools (contract 2f198123af10) so Gratz can congratulate a sister-guild ding. The operator: "the user though i should detect level ups in the SISTER guild and grats members in the SISTER guild, not just the guild i'm in". Every/whorow about a sister member (recordLocation, source"who") and every member of aSetSisterRosterlanding (source"roster") is a reading, filed in the new in-memorylib.sisterLevels[lower(guildKey)][charKey] = { level, online }bynoteSisterLevel. It fires only when the level is higher than the one held AND the member was known online at the last reading and this one. A/whorow is online by definition; a roster reading is online when presence (MarkOnline) or a/whorow saw them withinPRESENCE_TTL(knownOnline), and otherwise says nothing about presence rather than "offline" -- a roster's level for somebody offline is last-known and must never read as a live ding. A clean location answer that did not list a member (TakeLocateAnswer) marks them offline. The first reading in a session and a member who just joined are baselines; the held level only rises, so one ding seen by a/whoand then a pull fires once; the home guild is never recorded.guildKeyis the listed spelling. Readings are forgotten with the guild (RemoveSisterRoster, an unlisting).Tests/sisterlevel_spec.lua, 33 specs across both callbacks, the refresh and the GreenWall forwarding;Tests/livestream_spec.lua, 25 specs for the baton, the stream and zones. Both against a stub GreenWall; the harness has no GreenWall model. Writing them found one defect before it shipped: a complete snapshot that did not name its own sender marked the sender offline, though the message itself proves them online.One rule for "is this sister member online" (
sisterOnline), from the session's self-audit. It had been three separate rules:GetOnlineMembersScoped, the guild tab rows'/whooverride, andknownOnline. Now the newest statement wins among a sighting, the stream and (for rows and level readings only) a/whorow.GetOnlineMembersScopedstill leaves/whorows out on purpose, because a/whorow says the member is online, not that they can answer (the candidates rule). The same audit found two gaps, both fixed:- The server's "No player named X" reply did not override an older
/who"online" (OnPeerUnreachablenow marks the/whorecord offline). gwSendcounted a message GreenWall refused (ok = false) as sent.
- The server's "No player named X" reply did not override an older
Not covered
- Not run in a client. The addon-channel announcement,
UNIT_LEVEL, the level refresh, the baton and zone reports are exercised only against the harness. How often a real guild's events let the refresh run is not measured. SNAP_CHUNKwas 120 and every chunk would have been refused. Peer Review (thread aabe287416b7, finding A) pointed out that GreenWall's relay refuses, rather than cuts, a payload over 255 minus its header (Relay.lua:236-243). It is now 64.livestream_specruns our real sends through GreenWall's realRelay.lua,Constants.luaandBase64BCA.luafrom a worst-case sender, and asserts each one is accepted. The worst-case sender has a 12-letter name on a 24-letter realm and a 12-character guild id; that guild-id length is an assumption. At that worst case (members on another realm, sent in full) a chunk holds one name, so 31 online members make 30 GreenWall messages per snapshot. Same-realm names go bare, and 31 ten-letter ones make 6.- Sister readings come only as often as
/whoanswers and pulls (five minutes apart per guild), so a sister ding is seen minutes late, or missed when the member logs off in between -- accepted by the operator ("it would be slower") -- for sister members who do not run the library; those who do announce their own ding over GreenWall (above). The GreenWall send fromPLAYER_LEVEL_UPis not verified in a client; GreenWall's own API.lua:37-38 says API version 2 needs no key press.
1.0.1 -- the guild tab is called Members, and three retail fixes
The LibStub MINOR moves 25 -> 26. Nothing an addon calls changed shape.
Added on lib: the constant GUILD_TAB_NAME, and BuildGuildTabInvite /
SyncGuildTabInvite (internal to the tab; listed because they are on lib).
Changed
- The tab is "Members" on both windows (
lib.GUILD_TAB_NAME; the Communities tab's tooltip, the classic bottom tab's text, the "i" help's title and the/grcheckbox label all read it). It was "Sisters" / "Guild and sister guilds". Players without a sister guild -- most of them -- read that as "not for you" and asked for it gone. The user, 2026-09-27: "it needs to not have the sister guild theme, focus on it's just a better guild tab than the game gives you." The "i" help (GUILD_TAB_HELP) now leads with the search filter and Reset button, the two things it adds over the game's own roster (sortable columns are not mentioned: the game's tab has them too), puts sister guilds in one optional paragraph, and ends with how to turn the tab off in/gr. The/guildroster tabstatus line and the diagnostics are unchanged.
Fixed
- Retail: no bottom border under the Chat-box patch. After switching from
the Chat tab, the strip that covers the Chat page's input box stopped at the
window's
Bg, which on retail ends 2 above the window's bottom (MainlineSharedUIPanelTemplates.xml:603), and retail has noBottomBorder-- its frame is a NineSlice child (:550). The strip ended short with no border under it. On a window with noBottomBorderand aNineSlice.BottomEdge, the strip now runs to the bottom of that edge piece andrec.footEdgeredraws it over our stone: same atlas (or texture and coords), same tiling,SetAllPointson theirs. Clients with aBottomBorderare unchanged. Two specs inrostersisters_spec.lua(atlas and texture edges). The user confirmed the border in game on 2026-09-27. - The Chat page's control button showed through over the "i". The strip
ended 10 past the chat box, and the control-frame button beside Invite Member
(
CommunitiesFrame.xml:583) poked out between it and Invite Member, over our "i". The strip's right edge is now Invite Member's LEFT (the old box-plus-margin edge only on a window without that button), andLiftGuildTabPageliftsrec.infoabove the strip on every show. The user confirmed it in game on retail on 2026-09-27. - Retail: no Invite Member when the tab was opened from Roster. Blizzard's
InviteButtonbelongs to the Chat display mode only (CommunitiesFrame.lua:544-563), so over the Roster page it is hidden and our tab showed no Invite Member at all. We may not Show theirs, soBuildGuildTabInvitemakes one of ours on their rectangle that calls what theirs calls (CommunitiesUtil.OpenInviteDialog(clubId, streamId),CommunitiesInvitationFrame.lua:111-118-- the game's own Add Guild Member popup for a guild), andSyncGuildTabInviteshows it on everyShowGuildTabonly while theirs is hidden, enabled byCanGuildInvite()as theirs is. Not built on a client withoutCommunitiesUtil. The user confirmed it in game on retail on 2026-09-27.
1.0.0 -- where sister members are, from /who, and the /gr list draws again
Released as 1.0.0 rather than 0.10.0 -- the user, 2026-09-26: "lets go ahead and make this v1.0.0". Nothing about the number changes behaviour.
The LibStub MINOR moves 24 -> 25. The user, 2026-09-26: "add the ability to somehow click on an sister guild member and have it do a /who query to update where they are, following /who rules in FGI ... we could have it do a /who <sister guild name> and get 50 members, and some way to do the leaf split like FGI", and "especially automating it so people don't have to push the /who button ... we just need to ensure they don't conflict."
Added
- Sister members' zone, level and online state come from
/who.lib.locations[lower(guildKey)][charKey] = { zone, level, class, online, at }is the server's last word on each member. EVERYWHO_LIST_UPDATEfeeds it -- ours, the player's own, another addon's -- because every row is a free sighting. A COMPLETE answer that covered a member and did not list them writesonline = false. It is deliberately notlib.presence: presence picks who to pull a roster from and means "heard on the wire, runs the library"; a/whorow says only "online".BuildSisterRosterRowstakes the NEWER of the two for online (each good forPRESENCE_TTL), the/whozone while it still says online, and the/wholevel over the roster copy's. The Communities tab's Zone column now fills for sister members; the classic Sisters page shows the zone in place of the guild name once one is known. New callbackOnSisterLocationsUpdated(lowerGuildKey); both tabs repaint on it. - Automatic, from a click the player was making anyway. Opening either
guild tab (
ShowGuildTab,ShowClassicGuildTab) callslib:RefreshSisterLocations(force, onlyKey), which owes one/whoper listed guild, paced byLOCATE_INTERVAL(=WHO_INTERVAL). The existing click overlay sends it on the player's next click anywhere -- no button. - The leaf split, FGI's rules. A location answer cut at the server's fifty
(
total > n) is asked again in pieces bylib:SplitLocate(spec, levels): level band first, cut by a port of FGI'sfn.levelSplitPoint(functions.lua:6213-6229-- the upper median when one level holds half the rows, else the middle), then a single capped level by class using the client's ownWHO_TAG_CLASSand localised class names, limited to the classes the held copy has at that level. Bounded byLOCATE_LEAF_LIMIT(16) split queries per refresh; past it, or with nothing left to split on, the fifty rows are kept and the event ring says so. Absence from a PARTIAL answer proves nothing and marks nobody offline. - "Where is X? (/who)" and "Who is online in <guild>? (/who)" on a sister
row's right-click menu (
GuildTabRowMenuItems(name, row)-- the second argument is new; sister rows now carrysisterKey).lib:LocateMember(name, key)queues a/whoby bare name as URGENT and sends it inside the menu's own click when the floor allows, else first on the next click. lib:IsWhoOwed()-- true while a SYNC/whoof ours would go out on the next click (pastWHO_SEND_FLOOR, the Who list not the player's, not held by the guild step). The public form of what FGI's Wingman reads to yield its click (it readwhoSync.queuedirectly; threadc5a1a43e). Location queries are deliberately not counted. The guild-step half isheldByGuildStep, a read-only mirror ofwaitingOnGuildsharing the guildmate search (guildmateToAsk, split out ofPullFromGuildmates).lib:QueueLocate,lib:TakeLocateAnswer,lib:WhoPaneIsPlayersare public for the specs.- A Reset button beside each guild tab's search box. The user,
2026-09-26: "if i filter on a column, how do i get it back ot the 'default'
view ... without doing a /reload? if there isn't a way, we need a small reset
button somewhere in the strip in the top" -- there was no way: a header
click only ever moved the sort to another column or reversed it
(
SortByColumnIndex,SortClassicGuildTab).lib:ResetGuildTabView(rec)clears the sort, the direction and the search, and repaints: online first, then by name. Show Offline Members is left alone -- a saved setting with its own checkbox in the same strip. Each search box gave up the button's 64 px (GUILD_TAB_SEARCH_WIDTH300 -> 236,CLASSIC_TAB_SEARCH_WIDTH316 -> 252) so the strip ends where it did; whether the placeholder still fits on one line at 236 was checked in game on 2026-09-26: it does (it wrapped at 180).
Changed
- Location queries have their OWN queue,
whoSync.locateQueue, and are low priority. FGI v2.14.1 yields Wingman's/whostep wheneverwhoSync.queueis non-empty (fn.foreignWhoOwed), so a sixteen-leaf split there would take every Wingman click for over a minute. Order on a click: a member the player asked about, then the sync's queries, then location queries -- and those ALTERNATE with any other sender: the SendWho hook now files each send as ours (lastOursAt, flagged bywhoSync.sendingOursaround our own call) or foreign (lastForeignAt), and while a foreign sender was active withinLOCATE_FOREIGN_WINDOW(60 s) and the last send was ours, the location query waits a click. Stamps read at click time; no timer. The same text is never sent twice:QueueWhoabsorbs a waiting location query of the same text and the spec rides the sync's answer, andkeepLocatemoves a spec'd query to the location queue when the sync drops it (pruneWho,FinishGuildStep). - "The Who list is the player's" replaces "the Friends frame is open".
SendQueuedWhoheld every/whowhileFriendsFramewas shown -- which includes our own classic Sisters page, so a player looking at it could never be sent one. It now holds only while the Who tab is selected (FRIEND_TAB_WHO, ClassicFriendsFrame.lua:53): our registration is borrowed while a query is out, so the answer cannot reachWhoList_Update, the function that switches the frame to the Who tab (FriendsFrame.lua:1003). A client without the tab helpers keeps the old, wider rule. The status bar and event now say "the Who list" rather than "the Social window". - The registration debt settles when the WHO TAB shows, not when the Social
window opens --
HookWhoPanehooksWhoFrame's OnShow (the sub-frameFriendsFrame_ShowSubFrame("WhoFrame")shows, ClassicFriendsFrame.lua:479), falling back to FriendsFrame on a client without it. Opening Social on the Guild tab or our Sisters page used to hand Blizzard's handler back while another addon's/whocould still be out, and that answer would switch the window to the Who tab. Mirrors FGI v2.14.6'swhoPaneOf(threadc5a1a43e). - A guild dropped from the list also forgets its locations and any location
query waiting for it (
forgetSyncState). - An answer that is plainly somebody else's is no longer consumed as ours.
Our guild
/whoanswered with rows and none of them in that guild (FGI's level band, the player's own/who) is passed over:outstandingand theaskedFIFO stay as they were, so our real answer lands in its own slot instead of being read into whatever we send next. Peer Review's partial remedy (threadc5a1a43e, audit7033364662e7). Not airtight: an empty answer fits every question and is still taken, and because our plain-name query can match only non-members, one query is passed over at most once (whoSync.skippedHead) -- a wrong skip costs one answer, never the queue. Alongside it, a guild-wide answer now REPLACES the candidates of only the guild it asked about; rows for another sister guild in it merge. - Absence is taken only from a CLEAN location answer. A complete answer to
a guild or band query marks members offline only when every row in it is
that guild's. Found from the operator's in-game screen on 2026-09-26: sister
members heard on the wire (VersionCheck) showed online with a blank zone,
meaning an older
/whorecord had marked them offline -- and the only check before that was "some row is the guild's", which an FGI level-band answer read in our slot passes (a few of the guild's 60s among strangers). Whether that answer was the cause in that session is not verified; the events had scrolled out of the ring. A mixed answer's rows are still recorded as sightings. - A location
/whosent and never answered no longer blocks every later refresh.whoPendingcounted a text in theaskedFIFO as "on its way", so a refresh only filed its spec on it -- and an answer the server ate never came, so for the rest of the session no location/whowent out. Seen in game 2026-09-26: sister members showed online (from wire sightings) with blank zones, the event ring had no "asked the server" line, and the player's own/whofilled them. Now only a text still WAITING to be sent is pending; a resend costs one/who. - Absence needs an UNAMBIGUOUS answer, and a lost query is dropped at our
next send -- Peer Review's remedy for self-audit finding F1 (thread
7059e8cf). A lost answer shifted theaskedFIFO, so the answer to "Where is Cid?" could be read as the whole-guild query's and mark every other member offline. Now (a) absence is written only when exactly one query of ours was out and no other/whowas sent at or after it (the SendWho hook's stamps), else the rows are sightings only; and (b) at our next send -- a click -- any of our queries unanswered pastWHO_SEND_FLOORis counted lost and dropped (whoSync.askedAt), so two of ours are never out at once. KNOWN COST: a guild answer slower than the floor now lands in the next query's slot. - An "i" beside Invite Member on the Communities tab, with how the tab
works (
lib:BuildGuildTabInfo,GUILD_TAB_HELP): the same icon as LibAceGUIWidgets' ClearFrame info button, tooltip through itsAttachTooltip, parented to our page so it shows only with it. The user, 2026-09-26: "we need to add our famous i icon next to the invite member with the tooltips on how thit tab works" -- seen in game the same day. It breathes (W:Breathe, the fleet's pulse from TOGBankClassic's help icon) until the player hovers it once; remembered account-wide asLibGuildRosterDB.guildTabHelpSeen. "can we make it breath the first time someone opens the tab". - A paced refresh says so: "where sister members are: asked Ns ago, next /who in Ns (or right-click a member)", instead of doing nothing silently.
- The
/grwindow keeps itself current. Its list and its Diagnostics block repainted only on six library callbacks, none of which a sighting, a/whoanswer or a new sync event fires -- the user, 2026-09-26: "you need to make the GR UI refresh, it currently doesn't, i have to open/close/open it to see new stuff". The 1 s display ticker (already running while the window is open, retired when it closes) now repaints both when their content changed, and re-lays-out only then. It also listens toOnSisterLocationsUpdated. - The /who section header's "the ONE deferred call" sentence was stale since 0.9.2 and now says nothing there is deferred.
Fixed
- The
/grwindow's guild list drew no rows. Its host is an AceGUISimpleGroup, whoseLayoutFinishedsets its height to its AceGUI children's (AceGUIContainer-SimpleGroup.lua:27-30) -- and the list is a raw frame, so every layout pass wrote 0 over the 130. Harmless until LibAceGUIWidgets v0.2.4, whose RowList sizes itself from its parent: data, no rows, and a chat line from the widget library saying so. Measured offline by tracing theSetHeight(0)calls; fixed with AceGUI's ownnoAutoHeight. My defect since the window was built. Not seen in game. - WoW Forever: a sister-guild
/whoopened the Group Finder on its Who tab. Found by FastGuildInvite (peer review1a5ce19f) in theforevertree: the Group Finder'sLFGWhoListFrameregistersWHO_LIST_UPDATEin its OnLoad and, whileC_SocialUI.IsSystemEnabled()is false, answers withShowUIPanel(LFGParentFrame)(Blizzard_GroupFinder_VanillaStyle/Mainline/WhoList.lua:165,:236-239). The library held onlyFriendsFrame, so the Forever pane got every answer. My defect since 0.9.2 shipped a Forever TOC. Now every Blizzard Who host inlib.WHO_PANE_FRAMESthat exists is held (BorrowWhoPanes), the ones held are remembered inwhoSync.heldPanes, andGiveBackWhoListhands back exactly those. The Group Finder is load-on-demand, so a load under a standing debt is taken atADDON_LOADED(OnWhoPaneLoaded), and an answer that finds it listening takes it back as it already did for a third party's hand-back. The player opening that Who list settles the debt (an OnShow post-hook), and it being on screen holds our/who(WhoPaneIsPlayers). Named frames rather than FGI'sGetFramesRegisteredForEventwalk: the two hosts are known from the source, and the walk needs a harness model not yet delivered (FGI's contractf9c248f0). Not seen in game; there is no Forever client on this box.
Docs
- README and the CurseForge page brought level with the library. The
CurseForge page's Compatibility list had never gained World of Warcraft:
Forever (0.9.2); its Callbacks section lacked
OnMailAutocompleteChanged,OnGuildTabChangedandOnSisterLocationsUpdated; its MINOR list stopped at 0.9.2; and it named neitherGuildTabRowMenuItems,ResetGuildTabViewnor the Who-host methods. The README's Callbacks section lackedOnSisterLocationsUpdated, and its/whoparagraph named onlyFriendsFrame. Both now say thatOnSisterLocationsUpdatedcan fire with no guild key (the/whoanswer handler's finalFire).
Testing
- Harness pin
bb80c1b->1ccef14, taking0a1a896(contractf275ebec: a hollowwow.reset()evicts the widget layer's globals, so the two hand-written scroll-plumbing nils inTests/env_guild.luaare gone). Four search specs went red on it; every one was the env, not the library:Tests/env_guild.luastill stood in forC_Club.GetGuildClubId, the two Club enums andC_CreatureInfo.GetClassInfo-- all the harness's own since6770e26, which I asked for (inbox8e9aa2eb) and never adopted. The stand-in answered club id 7;frames.reset()now reinstalls the harness'sGetGuildClubId(id 1), so the member list answered an unknown club and the guildmate half of every search came back empty. Deleted; the member list is keyed onwow.guildClubId, read at call time.- The class column now reads the harness's DISPLAY name (
Warrior), as the client's does. The stand-in returned the token, and two classic-tab assertions had pinned that wrong value. ResetGuildTabViewcleared the query beforeSetText(""), whoseOnTextChangedthen wrote""back. Nothing drew differently (empty is no filter), but the state was not the "no query" the function promises; thenilis now set after the text.- The hollow scroll stand-ins are installed on every reset instead of only
where absent: the harness records a name as widget-owned only if nothing
held it before the widget layer's first install, so our stand-in made
those two names permanently unowned, the widget layer's copies survived
every later hollow reset, and the stand-in's body never ran (coverage
99.13%). Reported to WoWAPITesting (
dc070b16), fixed there as1ccef14, which this pin takes; the always-install stays, and is correct either way.
Tests/cfhtml_spec.lua, new: the CurseForge description goes through the harness's owncfhtml.lua(required as a module) on every run -- well-formed, under the 90,000-character ceiling, Discord the only link. The page had no checker at all until now. The desk has no route to that checker (filed with writ as8bf857eb), so it lives in the suite instead.- The suite is order-independent now.
run.lua --order reversewas red (5), and both causes were mine.zz_window_spec.lualoads LibAceGUIWidgets and nothing let it go, so every later spec saw a widget library the plain env lacks:/guildrosteropened the window instead of printing usage, and widget code ran on the hollow layer.env_guild's reset now forgets it, beside DeltaSync and VersionCheck. Andsister_spec's exact-PRESENCE_TTLboundary misjudged from a fractional clock (the harness clock never resets, and(t + 900) - tis not exactly 900 in floating point); it now starts on a whole second. At pin1ccef14: default,--order reverseand--order shuffle:1790463113are all 855/0, coverage 100% (4989/4989).
Not verified
- Seen in game 2026-09-26 (Classic Era, Communities tab): sister members
(The Other Gods) show their
/whozone in the Zone column and in the hover card, beside the guild name in the Rank column. The/grwindow's list draws its row again (The Other Gods, 288 members, 7 online, held), and a location/whowent out from a click and was answered with 8 rows -- under the cap, so no split ran. The Reset button works, and the search placeholder still fits on one line at 236. After thewhoPendingfix, the library's own location/whowent out from a click and filled the sister zones with no manual/who, the/grwindow showing it as it happened. Nothing below that line has been checked in a client yet. In particular the level-band form<guild name> <lo>-<hi>is unproven: the plain guild name is the form with evidence (0.8.0), and whether the server honours a band beside a multi-word plain name is not known. If it ignores the band every leaf returns the same fifty, the split walks to its 16-query limit and stops. WHO_TAG_CLASSis read at call time, and the class axis simply does not split on a client without it. Correction from Peer Review (thread7059e8cf): it IS on this box --F:\Blizzard API Docs\GlobalStrings\enUS.lua:23749,"c-"-- which flavour that folder describes is not verified.- The level-band split has never run in a client: in the operator's session
the only listed sister guild, The Other Gods, had 8 online (their
/grscreenshot, 2026-09-26), under the 50 that triggers it. - An answer that is somebody else's, read in our slot, is judged no further when it lists nobody of the asked guild or a guild row outside the asked band; an EMPTY foreign answer still reads as "nobody online" for what we asked (nobody can tell whose an empty answer is).
0.9.2 -- the Social pane stays shut beside Wingman, and a WoW Forever TOC
The LibStub MINOR moves 23 -> 24. Both items came in on the inbox from
FastGuildInvite's session on 2026-09-20: a peer-review finding (c5a1a43e)
and a contract (d1bf13d8).
Fixed
- The Social pane popped open during Wingman use when both addons had a
/whoout from the same click. Two field reports the same day (the user on retail, rocky on Discord: "when wingman is enabled, /who pops up automatically"). Blizzard'sFriendsFrameis the only frame registered forWHO_LIST_UPDATE, and its handler ends inWhoList_Update->ShowUIPanel(FriendsFrame). Both libraries borrow that registration off while a background/whois out, and each handed it back when ITS OWN answer arrived -- mine one frame after the event (RestoreFriendsFrame). Nobody can tell whose answer aWHO_LIST_UPDATEis, so with one query of ours and one of FGI's out at once, whichever answer landed first re-registered the frame while the other query was still out, and the second answer reached Blizzard's handler alone. My half of the race; FGI's own half (a written-off query's late answer) is fixed in its v2.14.1. - The registration now goes back when the PLAYER wants the pane, never on
an answer -- the rule FGI v2.14.1 adopted, mirrored here so the hole is
closed from both sides. The debt (
whoSync.borrowed) stands across as many queries as it takes;lib:GiveBackWhoList()settles it on exactly two occasions, from two post-hookslib:HookWhoPane()installs at the first borrow: FriendsFrame's OnShow (opening the pane repaints the Who list from the client's buffer by itself --FriendsFrame_OnShow->FriendsFrame_Update->WhoList_Update, ClassicFriendsFrame.lua:442,:480), andC_FriendList.SendWhowith a non-nilorigin: every Blizzard call site passes anEnum.SocialWhoOrigin(SlashCommands.lua:1111Chat,FriendsFrame.lua:1233Social,ItemRefHandlers.lua:50Item, in all four flavour trees; the argument isNilable = trueinFriendListDocumentation.lua) and no addon does, so an addon-origin send is one that borrowed the registration itself and is left alone. Both hooks are keyed to the objects, not booleans, because the suite reinstallsFriendsFrameandC_FriendListbetween spec files. - The send borrows unconditionally rather than only while
borrowedis false, and an answer that finds Blizzard listening under a standing debt takes the registration back for whatever is still out -- both for the case where a third party (an FGI older than v2.14.1) re-registered the frame under us. - The "close the pane our answer opened" step is gone with the
hand-back, and it had a defect of its own: a pane the player opened while
our query was out was "up but not at the send", so their own Who list was
shut in their face. Its OnShow now settles the debt first, and the answer
reads the settled debt as "the player wanted this". This also removes the
one
C_Timer.After(0, ...)the/whopath had. - KNOWN COST, stated rather than hidden: an FGI older than v2.14.1 alongside this release still hands the registration back on its own answer, and nothing here closes the pane that lets our answer open. That fix is FGI's, in the same day's release.
- A
/whois held inside the server's floor of the last/whoANYONE sent (WHO_SEND_FLOOR: 8 s on retail, 5 s elsewhere -- the operator's own measurements recorded in FGI'sfn.getMinScanInterval: "on my classic pretty empty server, it's at about 4s that it doesn't eat a /who. we know on retail we need 8s as a minimum"). FGI's reply on the same thread named the hole that remains once the pane stays shut: neither library can tell whoseWHO_LIST_UPDATEit is, so with a query of ours and one of FGI's out together the first answer is consumed by BOTH -- ours reads a level band's fifty strangers as the asked guild's answer ("nobody online" for the round), FGI credits its band as searched against our guild rows. FGI's half already refuses a send inside the floor measured from any send its hook saw, ours included; this is our half. TheSendWhopost-hook stamps every send whatever its origin,SendQueuedWhostamps its own as well (a client the hook could not be installed on), and a click inside the floor is held with the queue intact and the status bar saying how long -- the next click past it sends. The hooks are installed atPLAYER_LOGINas well as at the first borrow for this: FGI's Wingman and our overlay fire on the SAME click, so its send precedes our first borrow, and a send never seen never stamps the floor -- the server would have eaten our first query. A click-time read of a stamp, exactly likelastWho; nothing waits on it. The two libraries can now only overlap when an answer takes longer than the floor. Two of OUR OWN queries cannot go out inside it either, which the server was already refusing silently.
New
GuildRoster_Camelot.toc(## Interface: 16001), the_Mainlinefile list verbatim, for World of Warcraft: Forever (Battle.net productwow_classic_beta, codename Camelot): the retail client -- Menu API,C_*namespaces,Blizzard_FriendsFrame/Mainline,Blizzard_Communities-- running classic-era content. The client reads<Addon>_Camelot.tocahead of_Mainline.toc(warcraft.wiki.gg, TOC format: standard, mists, cata, wrath, tbc, camelot, vanilla, ..., mainline, classic, then the plain file). FastGuildInvite v2.14.0 ships its own_CamelotTOC and hard-depends on this library, so without ours it did not load there.WOW_PROJECT_IDisWOW_PROJECT_MAINLINEon Forever (no constant of its own), soIS_RETAILtakes the retail path everywhere, which is right: the Communities tab's install is the one Forever takes, and the classic tab's template check from 0.9.1 -- whose first field report came from a Forever client -- keeps the classic tab off. WhatGetNormalizedRealmNameand/whorows return on a server with no realm names is unpublished and untouched.
Docs
- README: the
/whoparagraph names the new rule and the two public methods; the dependency table has the seventh row. The CurseForge page's dependency table has it too, and its Recent Updates keeps the last five releases -- the v0.8.0 block moved to the full changelog.CLAUDE.md: the/whobullet carries the rule and the known cost, the TOC sync rule names seven files, and the no-timers bullet no longer claims a deferred hand-back exists. - The 0.8.0 section of this file (727 lines, 49,369 characters) moved whole
to
CHANGELOG_ARCHIVE.md, at 11,552 characters of headroom under the release-body ceiling -- about two release blocks on this project.
Testing
sistersync_spec: the answer test now asserts the debt STANDS; a new test drives the three settlement cases (OnShow settles it; aSendWhowith an origin settles it before the answer; an addon's send -- ours included -- settles nothing) and that each hook is installed once per object; a new test plays a third party re-registering the frame under us; the old closer test is rewritten as the case it got wrong (a pane the player opened mid-query is left alone, and the next/whois theirs); the degrade test's "withoutC_Timer" arm became "withouthooksecurefuncorHookScript" -- borrow and answer still work, the debt simply has nothing to settle it. A new test drives the floor: the login install of the hooks, a foreign send holding our first click, the status line, the click past the floor sending, and two of our own held apart. AfloorPassedhelper moves the clock where a spec means "the player clicks again" (six sites).Tests/env_e2e.lua'sanswerWhomoves the shared clock to the first moment a send is allowed before each click -- a click inside the floor is held, and a player's next click is seconds away anyway.toc_spec: seven files in the table,_Camelotin the closed suffix probe, and the three doc tables read back with the new row.- 823 specs, 100% line coverage on the library,
env_guild.luaandenv_e2e.lua. - Not run in a client, either item. The
/whomechanism is traced in both codebases and in the Blizzard source; the pop is not reproduced here and FGI's side is also unverified in a client. Nothing of this library has run on a Forever client -- there is no install on this box, and whether CurseForge accepts a 16001 TOC is not known from this workspace (FGI v2.14.0 shipped one; the user can say whether that release went through).
0.9.1 -- the classic tab no longer errors at login on retail
The LibStub MINOR moves 22 -> 23. One fix; nothing an addon calls changed.
Fixed
- A Lua error at every login on a retail client,
CreateFrame(): Couldn't find inherited node "GuildFrameColumnHeaderTemplate"fromInstallClassicGuildTab, reported twice on 2026-09-19/20: by a FastGuildInvite user on WoW Forever (relayed by FastGuildInvite's session as peer review960cc9f9) and by the user on retail proper. My defect, from 0.9.0: the classic tab's install guarded onFriendsFrame,FriendsFrameTab4,frame.Bg, theFauxScrollFrame_*helpers andPanelTemplates_SelectTab, and retail has every one of those -- the same frame, a fourth tab (Quick Join,Blizzard_FriendsFrame/Mainline/FriendsFrame.xml:1119), the same helpers. What retail has not got is the Classic Guild page's two templates,GuildFrameColumnHeaderTemplateandFriendsFrameGuildPlayerStatusButtonTemplate(Blizzard_UIPanels_Game/Classic/FriendsFrame.xml:576,:408; in theclassic_era,classicandclassic_anniversarytrees of the Blizzard source and not inlive).CreateFrameon a template the client lacks is an error, not nil, so the header loop threw with the tab, page, inset and offline box already built and nothing settingrec.failed. - The fix asks the client for its templates before building anything:
hasTemplates(...)onC_XMLUtil.GetTemplateInfo, documented inXMLUtilDocumentation.luaon all four flavours withMayReturnNothing, so an unknown template answers nil.InstallClassicGuildTabasks for its five (FriendsFrameTabTemplate,InsetFrameTemplate,GuildFrameColumnHeaderTemplate,FriendsFrameGuildPlayerStatusButtonTemplate,FauxScrollFrameTemplate) andInstallGuildTabfor its two (CommunitiesFrameTabTemplate,CommunitiesMemberListFrameTemplate) -- the Communities install's mixin guards are a proxy for the same files and had the same shape of hole, so both paths go through the one helper. On retail the classic install now answers false and builds nothing; the Communities tab is unaffected. - A client with no
C_XMLUtilat all is answered TRUE, not false. Peer Review's proposed fix returned false there. Every Blizzard client has the API (all four documentation trees), so that branch is a private server on an older build -- Whitemane's 38000 family -- where the classic window and its templates exist and the tab built and worked before this check. Refusing there would remove a working tab to guard against a client that does not exist. - The
/grwindow's help tooltip said "Nothing here sends a /who". Untrue since 0.8.0, when Sync now began sending the owed/whofrom the click; the line predates that and was never swept. It now says a/whogoes out only from the player's own click, and that nobody unseen is whispered.
Docs
docs/Curseforge_Description.html: the loaded-MINOR list stopped at 0.8.1 (20) and now runs to 0.9.1 (23); the slash-command backup list namesmail,tabanddiagbesidesisters,pullandsync(the README's matching list too); the v0.7.0 and v0.6.1 blocks left "Recent Updates" for the full changelog, keeping the last five releases -- 7,806 characters of headroom under the 90,000 ceiling was one release block from a rejected page.
Testing
Tests/env_guild.lua'sinstallFriendsFrameModelnow modelsC_XMLUtil.GetTemplateInfo(the harness has none): a table for each name inguild.templates, which starts as the eight templateswrapGuildWindowTemplatesroutes, nil otherwise. A spec removes a name to play a client without it.classictab_spec: retail's Friends frame (every guard passing, the header template absent) builds nothing -- no tab, page, inset or offline box -- and the row template alone absent answers the same; noC_XMLUtil, and aC_XMLUtilwithoutGetTemplateInfo, both build the tab. The file's header said the tab was MINOR 23; it shipped at 22 in 0.9.0, corrected.rostersisters_spec: the Communities install answers false with either of its two templates absent, alongside its existing four-piece check.- Seen in game on 2026-09-20, both sides: the user logged in on retail with the fix synced -- no error -- and on Classic Era, where the Sisters tab is still on the Friends frame. The user: "i ran it in retail, fixed the issue. I ran it in classic, no issue."
0.9.0 -- a guild and sister guilds tab on BOTH guild windows
The LibStub MINOR moves 21 -> 22. Additive: nothing an addon calls changed shape; methods and one callback were added (listed below).
The user, 2026-09-17: "like how we did the mailbox integration with the sister guilds ... can we populate the wow guild roster with alt guildies?" Asked whether it could be done without taint: "lets try it, and see, then we can back it out if it's taint or fix it." The try is recorded under "What was tried first"; the measurement backed it out, and what ships is a tab of our own on the Communities window -- and, the next day, a second one on the classic Friends-frame guild window.
Both tabs ship here. The classic tab was offered on 2026-09-18 as "option
1, as its own release after 0.9.0" and chosen with "do 1"; it was then
built, and seen working in game, the same day 0.9.0 was being prepared. The
user, that evening: "they are both working, lets prep for a release, lets get
the changelog and .html updated for v0.9.0". Between those two, this file
briefly held the classic tab in an "Unreleased" section on Peer Review's
reading of the earlier words (thread fd0ed643); the later words settle it.
Added
- A fourth tab on the Communities guild window, below Blizzard's Chat,
Roster and Guild Info tabs, built from their own
CommunitiesFrameTabTemplateand anchored underGuildInfoTabas theirs are under each other. Its page is a frame of OURS inheritingCommunitiesMemberListFrameTemplate, so Blizzard's own mixin draws Blizzard's own row template -- it looks like the Roster page -- from tables that are all ours. The user's words for the shape: "can we rebuild the frame exactly and replace it with ours, and then modify it?", "i'd rather match the games", and on a separate window, "but that just opens another window". - Its rows: guildmates from
C_Club.GetClubMembers/GetMemberInfo(the pair Blizzard's list reads, so zone, level, rank and note are live), each a COPY withisSister,onlineand a realm-qualifiedcharKeyadded; the library's own home roster when the client lacks that API. Sister members fromBuildSisterRosterRowsas before. One list, sorted by the clicked column (Blizzard's Rank column sorts onguildRankOrder, which our rows do not carry, so it reads the rank text), default online first then by name. - The widget library's search box above the columns (
W:CreateSearchBox, filtering withW:SearchMatchacross name, key, rank or sister-guild name, note and zone). The user: "add our libaceguiwidgets search box to filter through the list". Without LibAceGUIWidgets there is no box and the query is ignored. - Clicks: left whispers through
ChatFrameUtil.SendTell(the real Classic Era API; the bareChatFrame_SendTellis only a deprecation fallback behindloadDeprecationFallbacks, used when the namespaced form is absent); right opens a Whisper / Invite to group menu, the invite throughC_PartyInfo.InviteUnit(Classic Era has no bareInviteUnit). Officer actions stay on Blizzard's Roster tab:SetGuildRankOrder,SetNoteandRemoveFromGuildareHasRestrictions, so they cannot run from our frame. - Settings, account-wide at the root of
LibGuildRosterDB:guildTab(the tab, default ON),guildTabDefault(the guild window opens on our tab, default OFF -- the user: "when you push the J key it opens our replication instead of theirs, turned on/off in a setting"), andguildTabOffline(our page's own Show Offline, so Blizzard'scommunitiesShowOfflineCVar is never written). Two checkboxes in/gr,/guildroster tab on|off|default|nodefault, and a "Guild window tab ... Built ... Opens on it" diagnostics line. - API:
IsGuildTabEnabled/SetGuildTab,IsGuildTabDefault/SetGuildTabDefault,IsGuildTabShowOffline/SetGuildTabShowOffline, theOnGuildTabChanged(enabled)callback,BuildHomeGuildRows,BuildGuildTabRows(opts),BuildSisterRosterRows, and the frame half --InstallGuildTab,ShowGuildTab,HideGuildTab,RefreshGuildTab,SyncGuildTabVisibility,OnGuildWindowShow,InitGuildTabRow,OnGuildTabRowClick,GuildTabRowMenuItems,WhisperName,InviteName. - Install at
PLAYER_LOGINand onADDON_LOADEDforBlizzard_Communities, recorded onlib.guildTabso a LibStub upgrade does not build a second tab. - The CLASSIC guild window gets a tab too: a fifth across the bottom of
the Friends frame, for players on "Use Classic Guild UI" -- who, with the
tab above, still saw nothing. The user, 2026-09-18: "on the classic UI,
there is nothing, can we add a tab to that like we did the new UI?", and
of three shapes offered (a fifth tab; a third view on their Guild page,
which writes their menu; a button of ours swapping lists in place), "do
1". "Sisters", built from their
FriendsFrameTabTemplateafter Raid at their -14 overlap, shown exactly when their Guild tab is (theirFriendsFrame_UpdateGuildTabVisibilityandInGuildCheckpost-hooked). A lookalike we manage ourselves: not registered with PanelTemplates, which would writeFriendsFrame.numTabs; pressed and un-pressed with the samePanelTemplates_SelectTab/DeselectTabtheir code uses (widget calls, on ours and on the one of theirs left pressed -- theirselectedTabis never written). The page covers theirBg's rectangle down to the top of their bottom border in the window's stone, and is laid out from their Guild page's own XML with their own templates: the Show Offline box, fourGuildFrameColumnHeaderTemplateheaders (Name 83, Zone 120 -- 105 with a scrollbar -- Lvl 32, Class 92), thirteenFriendsFrameGuildPlayerStatusButtonTemplaterows from (3,-82), aFauxScrollFrameTemplate296x237 at (-32,-87) with their scrollbar art, the totals line, the horizontal bar, and our search box where their page has the MOTD. Rows come from the sameBuildGuildTabRows; a sister member's guild is in the Zone column, having no zone. Painted the wayGuildStatus_Updatepaints theirs (gold name and white columns online, all grey offline). Same settings, same click actions, officer actions on their Guild tab. Two things the read-through after green added: their own tab pass (InGuildCheckrunsPanelTemplates_UpdateTabs) re-presses their tab under our open page, soSyncClassicTabVisibilitypresses ours again after it; and the Show Offline box follows the shared setting on every repaint, not only drives it. And one the self-audit found: with the window already open, a KEYBIND (ToggleFriendsFrame(tab),OpenFriendsFrame) moves their tab throughPanelTemplates_SetTaband a directFriendsFrame_OnShow()call -- no tab click, no OnShow script -- so ours stayed over their new page. A post-hook onPanelTemplates_SetTabputs ours away when the Friends frame's tab really moved from the one ours was opened over, and leaves it when the same tab is re-asserted (GuildStatus_Updatedoes that on every roster event). Peer Review enumerated the paths that reach it from the Era tree -- the O key (TOGGLEFRIENDSTAB),TOGGLEWHOTAB, theGUILD_ROSTERslash command (SlashCommandsOverrides.lua:193), the chat social button,ShowWhoPanel-- and every one callsPanelTemplates_SetTabbefore its directFriendsFrame_OnShow()(FriendsFrame.lua:1210, :1223, :1238; the tab template's OnClick at xml:727), so the one hook covers all of them.InstallClassicGuildTab,ShowClassicGuildTab,HideClassicGuildTab,PressClassicTab,RefreshClassicGuildTab,PaintClassicRow,SortClassicGuildTab,SyncClassicTabVisibility,OnFriendsFrameShow; a "Classic tab built" clause on the diagnostics line. Same MINOR as the Communities tab: one release, one increment.
How it stays out of Blizzard's taint
Four rules, each the direct lesson of the measurement below, written in full in the section header:
- Never write into a Blizzard table -- member arrays, data provider, CallbackRegistry.
- Never call a Blizzard frame's Show/Hide/SetShown, nor
CommunitiesFrame:SetDisplayMode. A Show from tainted code runs that frame's OnShow tainted, andCommunitiesMemberListMixin:OnShowregisters callbacks on CommunitiesFrame. So our page is drawn OVER their content area with its own opaque background; their page stays shown underneath. Measured, not argued: switching their window to its Roster page before ours showed was tried in game (the user's suggestion, after the Chat page's input box drew through ours), andselect(2, issecurevariable(CommunitiesFrame.MemberList, "sortedMemberList"))then read"GuildRoster". The switch was removed the same day; the measured lift below covers the chat box instead. - Post-hooks only:
hooksecurefunconSetDisplayMode(a Blizzard tab click puts our page away) and onUpdateCommunitiesTabs(our tab follows the Guild Info tab's visibility, so a community or the finder shows none), andHookScripton the window's OnShow (opens on our page or theirs, decided afresh each time). - The template's scripts are replaced before the page can show or hide
anywhere. The page is created under a hidden HOLDER of ours that answers
the three club questions with nil, because the template's OnLoad runs
inside CreateFrame and reaches its "communities frame" through GetParent;
only once OnShow / OnHide / OnUpdate / OnEvent are ours does it move to the
guild window. Every acquired row has its events unregistered and its OnShow
cleared, so it never re-reads itself through
C_Club.GetMemberInfo.
Two layout decisions, both from the first in-game looks:
- The page's frame level is MEASURED on every show, and its strata is always HIGH: the highest shown frame anywhere in the guild window's tree (at HIGH or below), plus 20. Two guesses failed in game first -- the window's level plus 20 lost to their list's border, pinned at 100, and a fixed 300 lost to the chat box at 1200. Then the strata: taking HIGH only while something shown was in it (the chat pane's scrollbar), the page opened from the Roster or Guild Info tab was measured shown, MEDIUM, level 531, over a window at level 1 whose member list was at 2 -- and their page still showed through, while from Chat, in HIGH, ours showed. What was over us in MEDIUM is not established (nothing in Blizzard_Communities' XML puts a roster or info frame above it); HIGH is the one strata that leaves the window's DIALOG popups above us, so the page takes it every time. Confirmed in game the same day: the user, "i confirm the tab switching works now correctly".
- The slider column left of the list is redrawn whole. Their
communities-list scrollbar sits there over the window inset's marble, and
the Chat page's input box hangs its corner art into it. Three patches round
the art each looked wrong in game; the user: "we need to fully redraw over
this slider to fix our issue, just do that". So the column is the window
inset's marble, aligned to the inset's own
Bg, and it runs only BETWEEN the inset's own top and bottom border pieces (theTopEdge/BottomEdgetextures on the inset's NineSlice child): the column sits above the window's inset, so one running the inset's full height painted those two lines out, and copies of the tiles drawn back on the column were wrong too (the user, with crops of the game beside ours: "see how you covered up the natural top borders of the scrollbar, you shouldn't", the same for the bottom, and of the copy, "a strip you added in there? it shouldn't"). Nothing of ours draws on either line now. The window's lighter stone was tried twice in between, each time read off a screenshot, and sent back both times ("you regressed to this") -- on our page, and on it aMinimalScrollBarof our own at exactly their bar's anchors that mirrors theirs -- extent, allowed, position, shown -- from ahooksecurefuncon their bar'sUpdate, and again on every show of ours. Ours takes no mouse, so a click falls through to their real bar and their list scrolls as before; nothing of theirs is written. - The tab hangs under the LAST button below Guild Info, not under Guild
Info itself. GRM puts its roster button there (
GRM_UI.lua:16688) and ours sat on it. It keeps the same centre-to-centre spacing as Blizzard's own tabs (a 32-high tab plus their 20 gap), re-placed on every tab pass. - The Chat page's input box is painted out with Blizzard's own textures,
lined up with Blizzard's own. Left shown under us, the box hangs below the
list and its left corner art reaches 10 into the column left of our list,
where the communities-list scrollbar sits (
CommunitiesChatFrame.xml:65-69). Two things cover it: the window'sBgstone, clipped to the strip under our list (from under the window inset's own bottom border piece -- a strip starting at the inset's rect shaved the lowest row off that line -- down to the top of the window's bottom border, not the Bg's bottom, which painted over that border), and the slider column redrawn whole (above), which stops at our list's edge where our own InsetFrame's border -- the sameInsetFrameTemplateedge the game has beside that column -- draws above it. Four patches preceded that, each wrong in game: the list's marble over the box (wrong colour, 4 short of the corners), a 2-wide marble strip beside the corner art, marble and an edge tile over the art -- a black block over the grey border itself (the user: "the black background is overlapping and covering the 'issues' and it looks bad. we need the fix to be us deploying over the actual grey border edge, not extending the background over the border") -- and then a second copy of our list's border, all eight NineSlice pieces drawn over everything, which doubled the edge.MinimalScrollBaritself has no background or border of its own -- only track and arrow atlases -- which is why the redraw is a whole bar.
The one widget call made on a Blizzard frame is SetChecked(false) on their
four tabs when ours is clicked. Nothing protected runs from a tab, and
UpdateCommunitiesTabs only ever writes that state.
What was tried first, and why it is gone
The first cut appended sister rows to Blizzard's own Roster page with a
post-hook on CommunitiesFrame.MemberList.RefreshListDisplay, and a post-hook
on the entry mixin's Init set isInvitation on our rows to make their clicks
dead. It worked in game -- the user: "it's working, they populate" -- and
two things were found there:
- A negative
memberIdwas rejected by the client, 40 BugSack errors from Blizzard's row handler re-reading each member onGUILD_ROSTER_UPDATE. The id base became2147483647counting down; it is kept, positive, on the sister rows today. - Health-check command 8 (
issecurevariableper row, naming the tainting addon) read"GuildRoster"on EVERY row, the player's own guildmates included. My containment argument -- that the scroll box recycles frames by identity, so a guildmate's row is never re-initialised inside our insert -- was wrong: the scroll box's own Lua bookkeeping is rewritten inside the insert, and every later secure refresh reads it. The rows also snapped the scroll position on each roster event, from the same cause.
AppendSisterRosterRows, OnRosterEntryInit, InstallRosterSisters,
IsRosterSistersEnabled / SetRosterSisters, OnRosterSistersChanged,
/guildroster roster and health-check command 8 are removed. None of them was
ever released. The saved rosterSisters key is no longer read.
Not covered, said plainly
- The classic window's tab, seen in game on 2026-09-18 -- the user, after opening both: "they are both working". It was built from the Guild page's own XML with 25 specs on a model of the window that has no layout, so that first look was the geometry check; what the user ran beyond opening it (sorting, Show Offline, scrolling, the O key with ours up, an officer note edit on their Guild tab) was not itemised and is not claimed here.
PanelTemplates_DeselectTabon one of their tabs callstab:Enable()and re-anchors its text -- widget calls, the same class asSetCheckedon the Communities tabs. Peer Review searched the Era AddOns tree for a secure reader of a Friends tab's enabled state or ofFriendsFrame.numTabsand found none; a grep absence, stated as such. A widget's enabled state is not a Lua variable, soissecurevariablecannot measure it; an officer note edit on their Guild tab after using ours is the check, and it has not been run on the classic window.- Seen in game on 2026-09-18, on a Classic Era client with GRM
installed: the tab under GRM's button; guildmates and sister members in
one list with the search box and online count in the band above it; the
page opening from the Chat, Roster and Guild Info tabs (the last two only
after the strata fix above, measured); the page's background, border and
the redrawn slider column matching Blizzard's own Roster tab through nine
rounds of screenshots; column sorting (the Rank and Note headers were
clicked in the user's shots); and
/dump select(2, issecurevariable( CommunitiesFrame.MemberList, "sortedMemberList"))printing nothing after the page switch was removed -- with the switch in, it had printed"GuildRoster". And the check that matters, done last: the user, an officer, edited a member's note on Blizzard's own Roster tab after using ours -- no error -- and the same/dumpafterwards printed nothing. The officer path is clean with the tab in use. - Not seen in game, said plainly: typing in the search box; a left-click
whisper and the right-click Whisper / Invite menu; the slider replica
scrolling their list; the rank dropdown (the note edit is the same
restricted path,
SetNotebesideSetGuildRankOrder, and it passed)./guildroster tab offis the back-out.
Fixed
- A sister member who logged off kept reading online for as long as two
guildmates kept relaying (TOGBankClassic's peer review
653048d5688e, 2026-09-17, confirmed atOnSisterRosterComm's twoMarkOnlinecalls). The relay'socarried bare names and every receiver stamped them at receive time, so a name any one member saw once circulated: A relays X, B stamps X fresh, B relays X 270 s later, A stamps X fresh, indefinitely -- and the same-hash relay restarts the pull clock, so no first-hand pull ever replaced it.onow carries each name's AGE --{ ["Name-Realm"] = seconds since the relayer last saw them }on the GUILD relay and on a guildmate's offer -- andMarkOnlinestamps a relayed namenow - age, never moving an existing stamp backwards and dropping an age pastPRESENCE_TTL. A first-hand sighting (an array of names, as before) is still stamped now. A bare name list from a 0.8.x relayer is not stamped: it cannot be dated, and stamping it fresh would put this client back into the loop. So in a guild mixed between 0.8.x and 0.9.0, a 0.9.0 client sees only first-hand presence until the relayers update; that cost is taken over keeping the loop alive.pendingPull.presenceand a guild offer'socarry the map through to the roster landing. The new tab's sister rows read the same presence, which is why this is in the same release.
Changed
- The mailbox autocomplete hook is tainted too, and that is now written
down in its header:
AutoComplete_Updatecalls our wrapper, so the five suggestion buttons and the dropdown's selection state are tainted for that keystroke. The Send button reads the To: box fresh from the widget and callsSendMail(MailFrame.lua:864-866) without touching anything we wrote, so the restricted call never runs tainted by that trace. Not measured with a taint log.
Testing
- Harness pin
acac151->bb80c1b. The window specs were red atacac151before any change this session: LibAceGUIWidgets moved its resize framework intoLibAceGUIWidgets-Resize.lua, the harness manifest learned the file in2aac908, and this repo's pin predated that. Every entry between the two pins was read; none asked anything of this addon beyond running the suite, which is green. Tests/rostersisters_spec.lua(new): the three row builders pure, the settings, the install and its refusals, showing and hiding against Blizzard's tabs, the rows' disarmed events and their clicks, and the page's own controls. Each of Blizzard's template scripts and methods that the library must replace records that it ran, so "none of theirs ran through a whole open-show-hide cycle" is an assertion; a companion spec proves the modelled row really re-reads itself and raises on a negative id, so the "no handler ran" assertions are not vacuous. The widget-library examples (search, search box, menu) run on env.frames' widget layer with the real LibAceGUIWidgets -- on the hollow layer its search box indexes a field that layer answers with a function.Tests/classictab_spec.lua(new, 25): the classic window's tab -- the install and its refusals (no Friends frame, no Raid tab, no scroll helpers, setting off, no bottom border, noWhoFrameColumn_SetWidth), visibility following their two passes and the guild, showing and hiding against their four tabs with theirselectedTabasserted untouched, the keybind move putting ours away while a same-tab re-assert and another panel'sSetTabdo not, the painted rows (names, sister guild in Zone, level, class, the online and offline colours, the scrollbar's column widths), paging through the FauxScrollFrame, header sorts, Show Offline on our setting with their CVar asserted unmoved, the click actions, the missing-helper fallbacks, the library's signals, the open-on-ours rule that ignores the Friends tab, the search box on the widget layer, and the diagnostics clause. Every script and helper of Blizzard's the library must not run records that it ran.Tests/env_guild.luamodels the classic window too:FriendsFramewithBgandBottomBorder, its four tabs whose OnClick setsselectedTabthe wayPanelTemplates_Tab_OnClickdoes, thePanelTemplates_*helpers by global name, theFauxScrollFrame_*helpers,WhoFrameColumn_SetWidth,FriendsFrame_UpdateGuildTabVisibilityon aclassicUIswitch andInGuildCheck; the classic templates route to amodelWidget(a model frame with a global name, sizes, text, an id, enable state, font strings and button textures), the row template registering its four named regions. And a reset fix the new file's sort position exposed: the widget layer'sCreateScrollBoxListLinearViewandScrollUtilsurvive the hollow reset, so after any spec that switched layers the hollow model stood aside -- which model ran was a function of spec-file order. The hollow reset now clears both first. That is a leak the harness owns (env/frames.luainstalls both andwow.resetleaves them); it is filed to WoWAPITesting's inbox as contract586d96c4a0f0, and the local clear stays until the pin that closes it.Tests/env_guild.luamodels the guild window:CommunitiesFramewith its four tabs,SetDisplayModeandUpdateCommunitiesTabs; the tab template; the member-list template with its scroll box, Show Offline button and column header; and the row template, all through a CreateFrame wrapper (wrapGuildWindowTemplates, re-applied by a spec that switches layers). PlusC_Club.GetGuildClubId/GetClubMembers/GetMemberInfo(a fresh copy per call, raising on a negative id as the client did),C_CreatureInfo.GetClassInfo, the Club enums, andChatFrameUtil.SendTellas a FIELD on the harness's own table. The invite is the harness's ownC_PartyInfo.InviteUnit(wow.invites).Tests/showoffline_spec.luaclearsCommunitiesFrameafter switching to the widget layer, whose CreateFrame no longer routes the window's templates; those specs are about the show-offline setter, and the tab's no-window branch is covered elsewhere.Tests/healthcheck_spec.lua: seven commands again; command 8 and its four specs went with the in-list rows.
0.8.2 -- sister guild ranks, and same-realm sister members are heard
The LibStub MINOR moves 20 -> 21. Additive: nothing an addon calls changed
shape, and one method was added (GetRosterRankHash).
The user, 2026-09-16, on VersionCheck's window showing a blank Rank for
Testthese-Azuresong beside ranked home members: "can you capture this data so
it's included in the sister roster sync?" -- and "we'll make that part of
v0.8.2". The blank was not a capture gap in the home roster: VersionCheck reads
rank through GetMember, which is home-only, and the sister wire had
deliberately carried no rank since 0.7.0.
Added
- Rank on the sister wire. Every served member now carries
rn(rankName) andri(rankIndex) besiden,c,l,pn, omitted when absent. Built inwireMember, read and TYPED inwireToFeed(a string name, a number index, anything else dropped -- it is concatenated into a hash). - Sister members carry
rankNameandrankIndex, the home record's own spellings, so a consumer reads a sister member the way it readsGetMember.SetSisterRostertakes them; the older free-formrankis still stored as-is and fillsrankNamewhen it is a string orrankIndexwhen it is a number.PersistSisterRostersaves both, so a relog keeps them. lib:GetRosterRankHash(key)-- the third "has anything changed" hash, overcharKey \1 index \1 namefor members with a rank, sorted. It is carried everywhere the note hash is:rhon the relay, the guild ask's entries, a guild offer, aq = 2request and a delta;rsbesidehs/nson aq = 1request (kept on a request held for a/who). Absent means "cannot judge", as for notes, so a 0.8.1 peer is never re-pulled over ranks it cannot send.- Why a separate hash rather than rank folded into the note hash: a 0.8.1 client hashes notes alone, so folding rank in would make every 0.8.1 / 0.8.2 pair disagree on every round, and the stamp ordering would then pass the rankless copy back and forth.
Changed
detailsMatch(self, key, noteHash, rankHash)replaces the notes half of the "same copy" comparison, which was written out at six sites. Five call it now (answerGuildAsk,OnSisterRosterComm,serveRoster,takeGuildOffer,PullFromGuildOffers);BroadcastSisterRosters' suppression compares two stored hashes rather than a peer's against ours, and gained its rank half inline.- The relay suppression records
ranksbesidenotes, so a rank-only change is re-relayed rather than swallowed.
Fixed
- A roster served by a SAME-REALM sister member was always dropped as
"we did not ask them" (TOGBankClassic's peer review GR-SAMEREALM-001, found by
its four-client fleet on real AceComm). AceComm hands a receiver
Ambiguate(sender, "none")-- bare for a player on our realm -- while every record the sync compares it with (pendingPull[key].peer,askedPeers, a roster charKey) isName-Realm, and the comparison usedCanonName, which never supplies a realm. My defect: the pull gate (PR-2, earlier the same day) made it fatal, because until then nothing checked who answered. Fixed at the boundary --OnSisterPullComm,OnSisterRosterCommandOnSisterConfigCommnormalize the sender once, on AceComm's contract that a bare addon-channel sender is on our realm -- and every sender comparison (askedFor,consumeAskedPeer,mutual,OnPeerVersion,ResolvePendingVerify,ResolveNameVerify,OnPeerUnreachable) goes through onesenderKey, so a name that arrives from/whoor VersionCheck in either spelling matches too. Also stored bare before this:rosterMeta.via, a refusal'speer, a held asker'ssender. - Every same-realm member ranked "never reported" to VersionCheck and was
never pulled from: VersionCheck keys a peer by its arrival spelling (bare for
our realm) and
PeerLibraryVersionlooked it up asName-Realm. It now tries the bare form for a name on our realm, and only for our realm. - Why no spec saw either:
Tests/env_e2e.luadelivered every message with the sender asName-Realm, the one spelling a real client never gives a same-realm sender. It now delivers it bare (arrivalName); with only the comparison fixes in place, 20 e2e scenarios went red under that change, which is how the stored-bare fields above were found. The harness's ownAmbiguatereturns the full name for"none"whatever the realm, filed with WoWAPITesting. NOT verified in game. - A delta is verified on notes and ranks, not only membership.
r = 2now carriesnhandrh, andtakeServedRosterchecks both over the result before storing it; a mismatch asks for the full roster, as a membership mismatch always did. The base a delta is computed from is snapshotted when the MEMBERSHIP hash changes, so it need not hold the notes the taker holds: a taker whose copy differed only in notes (or now ranks) from that snapshot was sent an empty delta, verified it on membership, stored the same copy and asked again every round. Found by readingserveRosterwhile adding the rank hash; the rank case is the one the new e2e scenario drives (a copy saved before ranks: empty delta, refused, one full pull). The note case takes the same code path and was not separately reproduced.
Tests
hash_spec:GetRosterRankHash-- shape, order, independence from membership and notes, movement on a promotion, a renamed rank and a rank on another member, home rows and fed rosters agreeing, and the free-formrank.sistersync_spec: a relay whose ranks differ is a miss; our relay and request carryrh; a rank-only change is re-relayed; the served wire carriesrn/riand neverrank.e2e_spec: rank crosses on a pull; a returning player's rankless saved copy gets ranks from a guildmate (empty delta refused, then a full pull, nothing sent to the sister guild) and persists them; a promotion crosses as a verified delta with the membership hash unchanged; then "no change". The guild-first scenarios' saved copies now carry the harness's rank, because a copy saved by this version does.sistersync_spec: a same-realm peer VersionCheck recorded bare is found forName-OurRealmand bare, and never for another realm.- Harness pin
34107ac->acac151. Adoptsf5d2383,Ambiguate's context table (our contractf578e2bf):env_e2e.lua'sarrivalNamenow calls the harness'sAmbiguate(name, "none")instead of returning the bare name by hand. The move turned three timer-driven specs red, and the cause is harness-side and MEASURED: a 300 s ticker due at10639.000000000ran withGetTime()=10638.999999996, so the library's own(GetTime() - last) >= SISTER_PULL_INTERVALpacing read the round as too soon. Filed as contractadcdd67f; the harness fixed it the same hour in98c7d86(a timer runs atmax(clock, at)and the clock never moves back), and the local stand-in written for the gap was deleted on adopting it. The three specs are green on the harness's own timers with no margin added. installShowOfflineEchois retired -- the local copy of the show-offline setter'sGUILD_ROSTER_UPDATEedge that the harness shipped as contract 7 long ago. It had stayed because the harness's edge reaches onlyenv.frameswidget-layer frames and nothing here loaded that layer. The three feedback-loop specs inshowoffline_spec.luanow load the library on the widget layer, deliver events withframes.fireEvent, count writes by wrapping the real setter, and the third asserts the harness's setter really reaches the library's frame, so the other two cannot pass on a mechanism that never fired.- 722 specs green; coverage 100% on the library,
env_guild.luaandenv_e2e.lua.
Older versions (0.8.1 and earlier) are in CHANGELOG_ARCHIVE.md.
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include