Details
Expansion
Categories
Developers
Having issues?
LibGuildRoster-1.0 — Reliable Guild Roster Tracking for WoW Addons
LibGuildRoster-1.0 is a small, drop-in library for World of Warcraft addons that need to know the current state of the player's guild — who's in it, who's online, and who just joined or left. It handles the awkward edge cases that bite anyone who tries to use GetGuildRosterInfo directly: the streaming login roster, the partial-snapshot misfires, and the stale server responses that briefly re-list a player you just watched leave.
Features at a glance
What the library gives your addon. Each item is covered in detail further down.
- A complete guild roster, built once at login.
IsReady,GetMember,GetAllMembers,GetOnlineMembers,IsInGuild,IsOnline. Nothing is handed out until the login stream has settled, a login build that provably stopped short is finished once, and after that the roster is kept up to date from chat messages and never rebuilt — soGUILD_ROSTER_UPDATEcosts your addon nothing. - Live membership callbacks. Joins, leaves, kicks, promotions and demotions, online and offline (a member speaking in guild chat counts as proof they are online). Parsed from Blizzard's own localized strings and tested in all eleven locales the game ships.
- Names that survive connected realms.
NormalizeNamefor a name read from the game,CanonNamefor a name that arrived over the wire,GetNormalizedPlayerfor yourself. On WoW: Forever a character is known by their full "First Last" name, with no realm. - An officer check that rank order cannot fool.
IsOfficerreads the permission the guild master granted, never a rank index. - Sister-guild rosters you read like your own.
GetRoster(key),IsInAnyRoster,GetOnlineMembersScoped. A sister member carriesname,class,level,rankName,rankIndex, its publicnoteandguild, andGetRosterHash/GetRosterNoteHash/GetRosterRankHashtell you what changed. - The sister-guild sync, run by the library. An officer sets the list once and the guild receives it. Rosters are pulled over a mutual, time-stamped handshake with no timeouts; your own guild is asked before the sister guild; DeltaSync sends only the changes when it is installed; copies are saved between sessions. Sister members are found with a
/whosent from the player's click, or through VersionCheck with no 50-row limit, and the most compatible version is asked first.OnSisterConfigChangedandOnSisterRosterUpdatedtell you when anything lands. - Alt groups. Which characters belong to one account, across sister rosters too —
SetAltGroup,IsSameAccount,IsAltOfRosterMember. - Sister-guild names in the mailbox. The Send Mail "To:" box completes sister-guild members as it does guildmates; the library installs it.
- A better guild tab on both guild windows, called "Members". New in 0.9.0. Your guild with a search filter and a Reset button — a fourth tab on the Communities guild window and a fifth on the classic Friends-window guild tab — drawn with the game's own templates and writing nothing into the game's frames. If your guild lists a sister guild, its members join the list too. Left-click whispers; right-click whispers or invites to group; an "i" beside Invite Member explains it. Turn it off in
/gr. - Where sister-guild members are. New in 1.0.0. Zone, level and online state for sister members, from
/who: asked automatically when a guild tab opens (on the player's next click), split by level and class when the server cuts an answer at fifty, and on demand from a member's right-click menu. - Level-ups, guild and sister guilds, without polling. New in 1.1.0.
OnHomeMemberLevelChangedandOnSisterMemberLevelChanged: each copy of the library announces its own player's level-up to the guild and, over GreenWall, to the sister guilds; group members are read as they level;RequestLevelRefreshkeeps every guildmate's level current while an addon asks for it; and sister members are also read from the library's/wholookups and roster pulls. - Sister guilds' online players, live. New in 1.1.0. With GreenWall, one library user per guild (the "baton") passes every logon and logoff to the sister guilds and hands the job on when they log off, so
GetOnlineMembersScopedfor a sister guild is near real time — what a cross-guild sync needs to stop whispering players who have gone. Library users' zones stay current in your own guild too. - The library's
/whonever pops a window open. The game's Who list is kept shut while a lookup of the library's is out — the Social window everywhere, and the Group Finder's Who list on World of Warcraft: Forever — and handed back the moment the player opens it or types a/whoof their own. - A
/grwindow for setting up sister guilds, pulling, watching the sync and reading diagnostics, with/guildrostertext commands as the backup. - Diagnostics you can paste.
/guildroster diagin chat, plus the health-check/dumpcommands in the developer section below. - Every client from one file. Classic Era, TBC Classic and Anniversary, Wrath, Cataclysm, Mists, retail, and World of Warcraft: Forever (the retail client running classic-era content).
Why this exists
Tracking the guild roster sounds simple. It isn't. On retail, the roster streams in across multiple GUILD_ROSTER_UPDATE events after login or /reload. Naively diffing those events tells you that 150 of your guildmates just joined when really the previous event captured a partial roster.
This library gates the OnRosterReady callback behind a stabilization phase that requires two consecutive builds at the same member total, so no consumer ever acts on a partial snapshot. Joins fire from the authoritative CHAT_MSG_SYSTEM "has joined the guild" message, never from a roster diff that could be confused by one.
It used to do something about the guild panel's "Show Offline Members" toggle as well, and that was a mistake. The belief — widespread, and stated on this page until v0.5.1 — was that unticking that box filters what GetGuildRosterInfo returns. It does not, on either flavour: measured on a live Classic Era client with the box unticked, the iteration returned 997 of 997 rows, and on retail with the flag forced off and the write verified, 900 of 900. There was nothing to work around, and the workaround was itself harmful: writing that setting fires GUILD_ROSTER_UPDATE, so every scan fed the event that caused it, which cost a recruiting player 110 fps down to 20. v0.5.1 removed it.
There is a third problem, and it is the one v0.5.0 exists for: GUILD_ROSTER_UPDATE is not a "something changed" signal, and it is not rare. Its only payload is a flag meaning "the server throttle has lifted, you may ask again" — so any addon that answers it with a request creates a loop, and the client's own calendar code does exactly that. Measured on a 978-member guild: bursts of three events while standing still, six to nine when a guildmate logs off. An addon that rebuilds its roster on each one pays that cost every time.
So the roster is built once, during the login stream, and maintained in place from chat events for the rest of the session. After that the event is ignored outright — it reads no roster rows, allocates nothing and fires no callbacks, which is why its frequency stopped mattering. (The one exception, since v0.8.0: a login build that provably stopped short — the server's total exceeds what was read — is finished on the next event. A healthy session never takes that path; see the v0.8.0 notes.) Membership comes from the join/leave/kick messages, presence from the online/offline messages plus guild chat as proof-of-life, and rank from the promote/demote messages. Stale server responses still can't resurrect someone you watched leave; that protection moved rather than disappeared.
What that trades away, stated plainly: chat parsing is now the only correctness path, so there is no periodic rebuild quietly fixing a missed message. That is why v0.5.0 also went through all seven roster messages in all eleven locales the game ships and put them under test — a locale that silently failed to parse used to cost seconds of latency and would now cost the rest of the session.
Public API
lib:IsReady()— boolean. True once the first stabilized full roster build has completed (whenOnRosterReadyhas fired).lib:IsInGuild(name)— boolean. Accepts short names orName-Realm.lib:IsOfficer([name])— boolean or nil. New in 0.4.0. Whether you hold officer privileges, decided by the officer-note permission your GM granted and never by your rank index — a rank index is only a position in a list, sorankIndex <= 2lets every alt through in the common GM / Officer / Alt layout.falsewhen you are not in a guild. With a name it answers for your own character only; for anyone else it returnsnil, meaning unknown rather than "not an officer", because the client exposes no API for another member's permissions.nilis falsy, soSetShownstill hides safely.lib:IsOnline(name)— boolean.lib:GetMember(name)— table or nil:{ name, class, level, rankIndex, rankName, isOnline, zone, publicNote, officerNote, status, isMobile, lastOnline, guild }.guildis new in 0.6.0 and names the roster this record belongs to — the home guild key, or the sister-roster key it was fed under — so it is the per-character answer to "which guild is this alt in". The same character can hold a home record and a sister record whoseguildfields differ;lib:IsInAnyRoster(name)is the authoritative one and prefers home. Note onlastOnline: since v0.5.0 it reads as of login, or as of the moment the library saw them log off. Someone who logged off while you were playing has an accurate all-zeroes tuple; someone offline the whole time keeps their login-time value, which under-reports by up to the length of your session. Treat that one as a floor rather than a reading.lib:GetAllMembers()— array ofName-Realmstrings.lib:GetOnlineMembers()— array of onlineName-Realmstrings.lib:GetNormalizedPlayer()— string or nil. The local player's ownName-Realm, in the same form as the roster keys (compare it directly againstGetMember/GetAllMembers). Falls back to the bare name before the realm resolves.lib:NormalizeName(name)— string or nil. The same normalization the lib applies to roster keys; use it to build a key that matches.lib:CanonName(name)— string or nil. New in 0.3.0. The same normalization, except that a name with no realm is left alone instead of having yours added.lib:GetRealmName()— string. The connected-realm-aware realm name, cached after login.lib:RegionalNames()— boolean. New in 1.1.1. True on a WoW: Forever client, where names are "First Last" and carry no realm.lib:SurnameSeparator()— string. New in 1.1.1. What the client puts between first name and surname (a space when the client does not say).lib:UnitKeyName(unit)— string or nil. New in 1.1.1. A unit's name ready forNormalizeNameon every client: name and surname on Forever, name and realm everywhere else.lib:WhisperTarget(name)— string. New in 1.1.2. The name to whisper someone by: on Forever the realm is removed (the server refuses a whisper toFirst Last-Realm); everywhere else the name comes back unchanged.
NormalizeName or CanonName? It depends where the name came from
New in 0.3.0. The two do identical cleaning and differ in one thing: whether a name with no realm gets yours appended. Pick by where the name came from, not by what you plan to do with it.
- You read it from the game — a guild roster row, a unit, the player. A name is bare here only when that character is on your own realm, so adding your realm is correct. Use
NormalizeName. This is what the library uses for its own roster keys, and it is unchanged. - It arrived over the wire — an addon message, a sync payload, a string saved by another client. That name lost its realm context in transit, and your realm may not be the sender's. Use
CanonName.
Why it matters on connected realms: "Thrall" sent by a player on one realm becomes "Thrall-Fairbanks" on one receiver and "Thrall-Whitemane" on another. That is one player stored as two different records, with no way to tell afterwards that they were ever the same person. CanonName gives every client the same string.
CanonName never consults your realm, is safe to call twice on its own output, and returns nil for nil, a non-string, an empty name, or a name missing the part before the hyphen.
WoW: Forever — "First Last", no realm
New in 1.1.1. On World of Warcraft: Forever a character has a first name and a surname, the full name is unique across the region, and realms are hidden. When the client says so (lib:RegionalNames()), both functions key a character by the full name with no realm: NormalizeName("Drie Stonefist") and CanonName("Drie Stonefist-Realm") both give "Drie Stonefist", and GetNormalizedPlayer() includes your surname.
- Read a unit's name with
lib:UnitKeyName(unit), notUnitName(unit). On Forever the second valueUnitNamereturns is the surname, not a realm. - Whisper through
lib:WhisperTarget(name)(new in 1.1.2). The sender name an addon message arrives with carries a realm, and the Forever server answers a whisper to it with "No player named ... is currently playing". Every whisper the library sends goes through it. - Every other client is unchanged and keeps
Name-Realm. - Not yet run on a Forever client. Sister-guild keys (
Guild-Realm) are unchanged.
Cross-guild API (sister rosters)
New in 0.2.0. The library can track one or more sister guilds alongside your own — useful when a player belongs to two allied guilds and wants to share data across both. Your own guild stays self-scanned and authoritative; sister rosters are fed in from outside. Since 0.7.0 the library does the feeding itself — the list, the sync and the persistence are below under Sister-guild sync — so an addon normally only reads these. The store methods stay for an addon with its own source. All of this is additive — the methods above are unchanged.
lib:GetHomeGuildKey()— string or nil. Your guild's key,"Faction-GuildName"(e.g."Horde-The Brave Ones"); nil when guildless.lib:SetSisterRoster(guildKey, members, meta)— wipe-and-replace a sister guild's roster.membersare"Name-Realm"strings or{ name, class, level, rankName, rankIndex, note }tables (note, since 0.8.0, is the member's public note;rankNameandrankIndex, since 0.8.2, matchGetMember; the library's own pull fills all three). A sister member read back throughlib:GetRoster(guildKey)[charKey]hasname,class,level,rankName,rankIndex,noteandguild— the rank fields are nil when the member who served the roster runs 0.8.1 or older, and there is noisOnline(askGetOnlineMembersScoped);metais optional opaque data the lib stores but never interprets (a re-feed that omits it keeps the previous value; onlyRemoveSisterRosterclears it).lib:RemoveSisterRoster(guildKey)— stop tracking a sister guild.lib:MarkOnline(guildKey, names)— stamp a last-seen time for sister members.lib:GetOnlineMembersScoped(guildKey)— array of onlineName-Realm(home: live status; sister: members seen withinlib.PRESENCE_TTL, 900s since 0.7.0, and since 1.1.0 members the sister guild's live stream reports online while that stream has spoken withinlib.LIVE_STREAM_TTL). The newest word wins: a streamed logoff, or the server's "No player named X" reply to a whisper, takes a member off the list at once./whorows are deliberately not counted here — a/whoproves someone is logged in, not that they can answer an addon message.lib:IsInAnyRoster(name)— guildKey or nil; your own guild takes precedence.lib:IsInGuildScoped(guildKey, name)— boolean.lib:GetRoster(guildKey)— the roster table, or nil.lib:GetRosterMeta(guildKey)— the opaquemetayou passed toSetSisterRoster, or nil.lib:GetKnownRosters()— array of every guildKey currently tracked.lib:GetRosterHash(guildKey)— a stable digest of the membership set only (excludes presence/rank/status), so two clients with the same members produce the same hash.lib:GetRosterNoteHash(guildKey)andlib:GetRosterRankHash(guildKey)(new in 0.8.2, MINOR 21) do the same for public notes and ranks.
Presence is a separate overlay from membership: a roster resync never wipes liveness, and a sighting ages out and self-corrects. Since 1.1.0 the library does say "offline" for a sister member, but only on direct evidence: that guild's live stream reporting a logoff or leaving them out of a complete snapshot, or the server answering a whisper with "No player named X". Since 0.7.0 the library persists a fed roster for any listed sister guild and re-feeds it at the next login itself; the first feed for a guild is treated as a baseline, so it won't re-welcome every member. Because the lib ships embedded in several addons, depend on a copy at MINOR 6 or newer and feature-detect each method (if lib.SetSisterRoster then ...).
Sister-guild sync — the list, the wire, persistence
New in 0.7.0 (MINOR 18). Until now an addon that wanted an allied guild's roster ran the whole thing itself: the officer-edited list of sister guilds, the gossip that spreads it through the home guild, a sync host to pull the roster, a saved copy and a login re-feed. TOGProfessionMaster did — and when TOGTools and TOGBankClassic wanted the same rosters, three copies of that would have been three lists that can disagree feeding one store. It is now in the library, once, ported from TOGProfessionMaster's working code, and every addon reads it. All additive; feature-detect with if lib.GetSisterGuildNames then ... end.
lib:GetSisterGuildNames()— sorted array of the configured guild names. Empty means nothing is configured, which is now distinguishable from "configured, roster not pulled yet".lib:SetSisterGuildNames(names)— replace the list. Officer-only, decided bylib:IsOfficer(); members receive the list by gossip instead. Takes an array or newline-separated text; trims, de-duplicates case-insensitively, stamps server time, tears down any guild dropped from the list, and gossips the result. Returnsok, reason.lib:GetSisterGuildsTs(),lib:GetSisterGuildKeys(),lib:IsSisterGuildKey(key)— the stamp, the"Faction-GuildName"keys, and the gate every accept, persist, relay and pull path calls. The gate is case-insensitive on purpose: the list is typed, the keys on the wire are spelled by the provider's client, and one capital letter must not unlist a guild.lib:PullSisterRoster(peer[, guildKey])— pull a sister guild's roster from a named online member of it (giveName-Realmfor a member on another realm). The manual bootstrap. The library's own two messages over the addon channel — nothing beyond Ace3 is needed on either end.lib:RequestSisterRosters([force])— one automatic round (the library runs it every five minutes): each listed guild is pulled from its freshest known-online member, never from a member with no presence stamp.force(what "Sync now" passes since 0.8.0) ignores the five-minute pacing; the second return names every listed guild not pulled and why.lib:GetSyncStatusText()— new in 0.8.0. One line saying what the sync is doing right now, per listed guild, and the last thing it did — what the/grstatus bar shows.lib:OnPeerUnreachable(name)is public too: forward the server's "No player named X" message if your addon whispers sister members itself.lib:BroadcastSisterConfig(),lib:BroadcastSisterRosters(),lib:RefeedSisterRosters(),lib:PersistSisterRoster(key)— the timed jobs, callable on demand (a "Sync now" button).lib:AskGuildForSisterRosters(),lib:PullFromGuildOffers(key),lib:PullFromGuildmates(key)andlib:FinishGuildStep(key)— new in 0.8.1 (MINOR 20), the guild-first step: ask your own guild what it holds, pull the best guildmate offer, whisper the best guildmate able to answer, and end the step (true when the guild's copy turned out current). The library runs all four itself at login; an addon needs them only to drive the sync by hand.lib:IsSisterSyncAvailable()— is the pull path up? True once the roster is ready in a guild and AceComm is present.lib:GetSisterDb()— this home guild's record inLibGuildRosterDB, the library's SavedVariables. Read it; write throughSetSisterGuildNames.lib:GetSisterStatus()— one row per configured guild: name, key, whether its roster is held, member count, and who has been seen online recently. What the window and the slash listing both draw from.OnSisterConfigChanged(names, ts, source)— a new callback, fired when the list changes by an officer's edit ("local") or by adopting a guildmate's gossip (the sender).OnSisterRosterUpdated(guildKey, source)— a new callback, fired when a sister roster lands, by"pull"or"relay".lib:HandleSlash(msg)andlib:ToggleSisterWindow()— the slash command body and the window, public so your addon can route its own command to them.lib:BuildDiagnosticLines()returns the lines/guildroster diagprints.lib:IsMailAutocompleteEnabled(),lib:SetMailAutocomplete(bool),lib:GetSisterRecipients(),lib:MergeRecipients(results, text, max, candidates, priority)and theOnMailAutocompleteChanged(enabled)callback — new in 0.8.0 (MINOR 19), the mailbox autocomplete below. The library installs the hook itself; an addon needs none of these unless it wants to show or change the setting.lib:RefreshSisterLocations(force, guildKey),lib:LocateMember(name, guildKey),lib:IsWhoOwed()and theOnSisterLocationsUpdated(guildKey)callback — new in 1.0.0 (MINOR 25). Sister members' zone, level and online state from/who; the rows fromBuildSisterRosterRowscarryzone.IsWhoOwedis for an addon that shares clicks with the library's/who: true while one of its own would go out on the next click.lib:SendQueuedWho()andlib:SendConfederationAsk()send what is owed, and must be called from a click (what Sync now does).lib:GiveBackWhoList()andlib:HookWhoPane()(new in 0.9.2),lib:BorrowWhoPanes()andlib.WHO_PANE_FRAMES(new in 1.0.0) — how the library keeps the game's Who list shut while its own/whois out. Every frame named inWHO_PANE_FRAMESthat exists (the Social window's, and the Group Finder's Who list on World of Warcraft: Forever) is held, and exactly those are handed back when the player opens the Who list or sends a/whoof their own. Public for an addon that sends/whotoo; nothing needs calling for ordinary use.lib:IsBatonHolder(),lib:ForwardLive(kind, name, level),lib:SendLiveSnapshot(),lib:StartLive(),lib:WireName(charKey),lib:OnGreenWallMessage(...)andlib:OnZoneChanged()— new in 1.1.0 (MINOR 27), the live stream. The holder of the baton is the online library user in your guild whose name sorts first; nobody negotiates it, and the server's own "has gone offline" message hands it on. The holder sendson/offfor each logon and logoff and a chunked snapshot of who is online when a sister client comes up; any client holding a level refresh also forwards level-ups of guildmates without the library. Same-realm names travel without their realm and the receiver puts the sender's realm back. Everything goes over GreenWall's API version 2 (GreenWall 1.14.0 or later) under the addon idGuildRoster, sized to fit its relay. The library runs all of it; nothing needs calling.lib:IsGuildTabEnabled()/lib:SetGuildTab(bool)(governs both windows),lib:IsGuildTabDefault()/lib:SetGuildTabDefault(bool),lib:IsGuildTabShowOffline()/lib:SetGuildTabShowOffline(bool)(our page's own Show Offline box; the game's setting is never written), theOnGuildTabChanged(enabled)callback, the row builderslib:BuildSisterRosterRows(includeOffline),lib:BuildHomeGuildRows(includeOffline)andlib:BuildGuildTabRows({ includeOffline, query, sortKey, reverse })(rows in theClubMemberInfoshape, each withisSister,online, a realm-qualifiedcharKey, andsisterGuildon a sister row), and two row actions,lib:WhisperName(name)andlib:InviteName(name), withlib:GuildTabRowMenuItems(name[, row])for the right-click menu they sit in — new in 0.9.0 (MINOR 22), the guild-window tabs below.lib:ResetGuildTabView(lib.guildTab or lib.classicTab)(new in 1.0.0) is what the tabs' Reset button calls: no search, no column sort. The library installs both tabs itself; the row builders and actions are public for an addon drawing its own roster.
No code needed: type /gr. Officers and the guild master add sister guilds; everyone sees each guild's member count, who is online and whether its roster has arrived, with Pull from, Remove, Sync now and a Diagnostics section. The text backup (/guildroster or /libgr): /guildroster sisters [add|remove|clear Guild], pull Name-Realm, sync, mail on|off, tab on|off|default|nodefault and diag.
How it works:
- Your own guild first (0.8.1). At login your guild is asked whether anyone has a newer copy — guildmates on 0.8.1 answer a broadcast, and the best guildmate on 0.8.0 or later is whispered. A newer copy comes from them; only when nobody in your guild has a current one is the sister guild asked.
- Finding the sister guild. A
/who Guild Namefrom your next click (a/whois only allowed from a click; your click still lands where you aimed), then VersionCheck asks who runs the library and the roster comes from the member closest to your version. If you already hold a roster you pulled, the/whois skipped and one confederation-wide question goes out instead, with no 50-row limit. Nothing prints to chat, the Who window stays shut — and on World of Warcraft: Forever, the Group Finder's Who list too — (it opens again only when you open it or send a/whoyourself), the library waits its turn behind any/whoyou or another addon sent in the last few seconds, and nobody is whispered who has not been seen online. - Both guilds must list each other (0.8.0), and a roster is only accepted from someone you asked. A refusal says why, so you know who has to fix it.
- Handshakes, not timeouts. A large roster reads as "receiving 289 members", never as a dead peer. With DeltaSync on both ends only the changes are sent. The guild relay carries a small fingerprint, not the roster.
- No timestamp, not accepted (0.8.1). A sister roster that does not carry when it was fetched is ignored, so an old copy can never overwrite a newer one.
- Sister-guild names in the mailbox (0.8.0). The Send Mail "To:" box autocompletes sister-guild members. On by default; switch it off in
/gror with/guildroster mail off. - The Members tab on the guild window (0.9.0). A tab at the bottom of the tab column (below Guild Info, and below any button another addon such as Guild Roster Manager has put there) shows your guild in a list that looks like the Roster page, with a search filter and a Reset button added — and any sister guild's members too, once one is listed. Click a name to whisper; right-click to whisper or invite. Hover the "i" beside Invite Member for how the tab works. Invite Member is there however you reach the tab. Officer actions stay on the game's own Roster tab. On by default; switch it off, or make the guild window open on it, in
/gror with/guildroster tab. If you use the classic guild window ("Use Classic Guild UI" in the Social options), a "Members" tab sits across its bottom after Raid with the same list in the classic style, on the same settings. - Where sister members are (1.0.0). Opening either guild tab asks the server, on your next click, where each sister guild's members are: the tab then shows their zone, level and whether they are online. A guild with more than fifty online is asked again in smaller pieces, by level and then by class. Right-click a sister member to look up just them, or their whole guild. A Reset button beside the search box puts the list back the way it opened.
Alt groups — which characters share one account
New in 0.6.0 (MINOR 17). An alt group is account state: the set of characters one player owns. The library stores and queries them; it never discovers them, because no client API can — a consumer feeds them, usually from its own sync channel. All of it is additive, so feature-detect with if lib.SetAltGroup then ... end.
lib:SetAltGroup(ownerName, altNames[, meta])— wipe-and-replace, stored sorted. Returnstrue,true, "unchanged",false, "stale"or a barefalse; see below, they mean different things.lib:RemoveAltGroup(ownerName)— stop tracking a group.lib:GetAltGroup(ownerName)— sorted array of names, or nil.lib:GetAltGroupMeta(ownerName)— the canon you passed, verbatim.lib:GetAltOwner(altName)— the owner key a character is filed under, or nil.lib:IsSameAccount(nameA, nameB)— boolean.lib:GetKnownAltOwners()— sorted array of owner keys.lib:IsAltOfRosterMember(name[, rosterKey])— does somebody else in that roster vouch for this character? OmitrosterKeyto ask about home plus every sister guild.
Which guild an alt is in is not a method — it is member.guild, on the member record. An alt group spans guilds by design, so a guild view needs the answer per character, and per-character data belongs on the character rather than in a second table that can drift from it:
local home = lib:GetHomeGuildKey()
for _, name in ipairs(lib:GetAltGroup(owner)) do
local m = lib:GetMember(name)
if m and m.guild == home then
-- this alt is in your guild
end
end
An alt with no member record is unplaceable, not unguilded. The library knows only the rosters it has been given — the guild it scans, plus sister rosters you fed it — so a character in a guild nobody fed it looks exactly like one in no guild at all. If you hide the ones you cannot place, you hide both. Feed the missing roster and that same character gains a record with nothing else changing, which is why absence could never have meant unguilded. There is no client API answering "what guild is this arbitrary character in", so this is a limit rather than an omission.
Feeding a group: stamp a canon and read the return
Only relevant if your addon syncs alt data between players. If you only read alt groups, skip this. The alt store is deliberately shared — every addon in the client reads and writes one store — so without a canon no feeder can tell its own stale data from another addon's fresher claim. The three fields follow DeltaSync's "canonical hashes — compute once, at save, and never again":
-- SAVE — the one and only place any of this is produced.
function MyAddon:SaveAltGroup(owner, alts)
local record = { owner = owner, alts = alts }
record.setAt = time() -- stamp the datestamp FIRST…
record.hash = nil -- …exclude the field from its own input…
record.hash = host:ComputeHashV2(record) -- …then stamp the canon.
GuildRoster:SetAltGroup(owner, alts, {
source = "MyAddon",
setAt = record.setAt,
hash = record.hash,
})
end
setAt must be inside the hashed input. That is what makes an equal hash mean the same publish event rather than merely the same names — two saves of coincidentally identical content are two versions, and one number has to be able to say so. Everywhere else, read and forward; never recompute. A hash you recomputed is your opinion of someone else's version, and once two clients can each hold an opinion nobody is authoritative. The library holds to this too: it stores your meta verbatim and computes no hash of its own, ever.
true— stored. Nothing to do.true, "unchanged"— same hash, same publish event. Nothing was written: no wipe, no reverse-index churn, no callback. This is the return that stops peers re-syncing data everybody already holds.false, "stale"— an oldersetAtthan the standing record. Refused, and not an error: somebody fresher got there first. Do not retry, and do notRemoveAltGroupto force it through.falsewith no reason — bad input. The alt list was not a table, or the owner name could not be canonicalized. That one is a bug in your caller.
Identity and ordering are different questions. The hash says whether two versions differ; it cannot say which is newer, and that is setAt's only job. Do not compare setAt to decide whether something changed — it is not a second identity channel, it is the same datestamp already sealed inside the hash, exposed so the ordering rule has something to read.
Adopt whenever you like. Both checks require both sides to carry the fields, so a caller passing no meta — or a pre-canon one — gets exactly the last-write-wins behaviour it always had. Mixed adoption is safe and nothing has to move at once.
Two traps, both from real consumer bugs
- The stale check protects the key you are WRITING, not one you are deleting. If you sweep for stale owner keys and call
RemoveAltGroupon a different owner, checkGetAltGroupMetaon that other key first. - If your owner key can move, use ONE rule in every store that holds it. Found by FastGuildInvite's own test on its first run, so this is a defect that actually happened. Their owner key is the alphabetically-first character, so it moves when a player rolls a character sorting earlier — and they then held the same relationship in two places, this library and their persisted store. The correct sweep is drop any previous group holding any of the incoming characters. The narrow version that looks equivalent and is not: "did the old group name the new owner?" — never true in exactly the case that matters, because the new owner did not exist as a key when the old group was filed. The stale entry survives, restore feeds both, each feed's sweep removes the other, and the account lands under a different key every login depending on table-iteration order. It presents as "a group that keeps changing owner for no reason". Note the asymmetry that makes your own store the harder problem despite looking like the easier one:
GetAltOwnercan tell you who currently owns a name, and your own store cannot.
Upgrading to v0.5.0 — what a consumer has to change
Everything is source-compatible; nothing was renamed and nothing errors. The list is short because the redesign is almost entirely internal — but two of these are silent, so your addon keeps working and quietly stops doing something.
OnMemberLevelChangednever fires — SILENT. If you announce guild level-ups, that feature is gone.member.levelstill reads throughGetMember. Since v1.1.0 the replacement isOnHomeMemberLevelChanged, withRequestLevelRefreshfor guildmates who run no library.OnRosterUpdatedis login-stream only — SILENT. If you used it as "the roster changed", you go deaf after login. Move to the per-member callbacks.OnRosterHashChangedfires sooner. Nothing to do — it now arrives when a join or leave happens rather than at the next rebuild.member.lastOnlinehas a stated resolution. Read the note onGetMemberabove before displaying it.member.levelcan stay1for a joiner if the server never produces their row within 60 seconds. A rebuild used to correct it.lib:IsOfficer()exists (since 0.4.0). Replace anyrankIndex <= Nofficer test with it.
Feature-detect, don't assume. This library ships inside several addons and LibStub hands out whichever copy loaded first, so an older MINOR may be the one you get. Guard anything added after your minimum — if lib.IsOfficer then ... end. LibStub.minors["LibGuildRoster-1.0"] is the loaded MINOR if you need to branch on it; v0.5.0 is 15, v0.5.1 is 16, v0.6.0 and v0.6.1 are both 17 — 0.6.1 changed no behaviour, so it took no new MINOR — v0.7.0 is 18, v0.8.0 is 19, v0.8.1 is 20, v0.8.2 is 21, v0.9.0 is 22, v0.9.1 is 23, v0.9.2 is 24, v1.0.0 is 25, v1.0.1 is 26, v1.1.0 is 27, v1.1.1 is 28, v1.1.2 is 29, v1.1.3 is 30 and v1.1.4 is 31.
Callbacks (via CallbackHandler-1.0)
OnRosterReady()— fired once after the first stabilized full build. This is when consumers can trustIsInGuild,GetMember, and friends.OnRosterUpdated()— fired after every full rebuild, which since v0.5.0 means only during the login stream. In practice it fires a handful of times as you log in and then never again for the session, so do not use it as a general "the roster changed" hook — useOnMemberJoined,OnMemberLeft,OnMemberOnline,OnMemberOfflineandOnMemberRankChanged, which stay live all session.OnMemberOnline(name)/OnMemberOffline(name)— presence transitions.OnMemberOnlinehas two sources as of 0.4.0: the "has come online" system message, and a member speaking in guild or officer chat while still recorded as offline. The second exists because the announcement is only seen if your client was listening — someone who logged in before you did, or during a/reload, would otherwise stay marked offline while visibly talking. It is one-directional: chat proves online, silence proves nothing, so there is no matching inference for going offline. If you announce on this callback, expect it for someone who was already online but whom the library had not yet seen.OnMemberJoined(name, guildKey)— home guild: fires only on theCHAT_MSG_SYSTEM"has joined the guild" message, never from roster diffs; sister guild: fires from theSetSisterRosterdiff. TheguildKey2nd argument is new in 0.2.0; one-arg consumers ignore it.OnMemberLeft(name, guildKey)— home: "has left the guild" or "has been kicked out of the guild"; sister: theSetSisterRosterdiff.OnMemberRankChanged(name, oldRankIndex, newRankIndex)— fires when a home member is promoted or demoted. Since v0.5.0 this comes from the promote/demote system messages rather than a roster diff. Those messages name the new rank, so the index is resolved through a rank-name map the library learns from the roster; a promotion into a rank nobody currently holds updatesmember.rankNamebut stays silent on the callback rather than reporting a guessed index.OnMemberLevelChanged(name, oldLevel, newLevel, wasOnline, isOnline)— no longer fires as of v0.5.0. It was produced only by the roster-rebuild diff, and v0.5.0 stopped rebuilding the roster. No system message announces a guildmate levelling, so the only way to detect it is to re-read the whole roster — the exact work that release removed, which cost up to 140 ms of frame time per guildmate logging in or out. Registering for it is harmless; it simply never fires.member.levelis still populated and readable throughGetMember, so a consumer that polls still works. If you were announcing guild level-ups from this callback, move toOnHomeMemberLevelChanged(1.1.0), below.OnRosterHashChanged(guildKey, newHash)— fires when a roster's membership set changes (not presence): the home roster when a member joins or leaves, a sister roster on eachSetSisterRosterthat alters the set.OnSisterConfigChanged(names, ts, source)— new in 0.7.0. The sister-guild list changed:namesis the sorted list,tsits server-time stamp, andsourceis"local"for an officer's edit on this client or the sender's name when a guildmate's gossip was adopted.OnSisterRosterUpdated(guildKey, source)— new in 0.7.0. A sister roster landed from the wire,sourcebeing"pull"(this client asked a member of that guild) or"relay"(a guildmate passed it on). It does not fire for the login re-feed of the saved copy — that arrives beforeOnRosterReadyand shows up as the baselineOnRosterHashChanged— nor for your ownSetSisterRostercall.OnMailAutocompleteChanged(enabled)— new in 0.8.0. The player switched the mailbox autocomplete of sister-guild names on or off. Not fired for a repeat of the current value.OnGuildTabChanged(enabled)— new in 0.9.0. The player changed one of the guild-window tab's two settings (the tab itself, or opening the guild window on it);enabledis whether the tab is on. Not fired for a repeat, nor by the Show Offline box.OnSisterLocationsUpdated(guildKey)— new in 1.0.0. A/whoanswer changed what the library knows about a sister guild's members — zone, level or online.guildKeyis lower-cased, andnilwhen the change is not tied to one guild.OnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source)— new in 1.1.0 (MINOR 27). A sister-guild member was seen at a higher level than the library held,sourcebeing"who"(a/whorow) or"roster"(a roster landing). It fires only when the member was known online at the previous reading and this one, so a saved roster's last-known level for someone offline never reads as a live level-up. The first reading in a session and a member who just joined are baselines; one level-up seen by both sources fires once; never for your own guild. Only as frequent as the library's/whoanswers and pulls, so a level-up can be minutes late, or missed if the member logs off first. A sister member in your group is also read as they level ("unit"). A sister member who runs the library announces their own level-up over GreenWall the moment it happens ("comm"), and a sister client holding a level refresh reports its guildmates' level-ups the same way ("relay"), sooldLevelcan benilfor either. Several clients may send the same level-up; it fires once.OnHomeMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source)— new in 1.1.0 (MINOR 27). The home guild's half, same signature, so one handler serves both. No game message announces a guildmate levelling on any client, so the sources are"comm"— the guildmate's own copy of the library tells the guild the moment they level, one short addon message — and"unit"— a guildmate in your group, counted only when they have been online since their level was last read. A mid-session joiner's first level is a baseline; one level-up fires once; never for yourself. A guildmate without the library is seen only while grouped with you, unless an addon holds the level refresh below (source"roster").lib:RequestLevelRefresh(owner)/lib:ReleaseLevelRefresh(owner)— new in 1.1.0 (MINOR 27).owneris any non-nil value, such as your addon table. While anyone holds it, the library re-reads the guild roster's levels at most once a minute and asks the server for a fresh roster at most once a minute — once per player, however many addons hold it. The request rides the roster event's own "you may ask" flag, never a timer, so a quiet guild refreshes less often. Levels only; it never touches Show Offline. Release it when you no longer need it; with nobody holding it, nothing is re-read.
Compatibility
- Classic Era (1.15.x)
- Burning Crusade Classic and Classic Anniversary (2.5.x) — fixed in 0.4.0; before that these clients silently loaded the Classic Era manifest and showed the addon as out of date
- Wrath Classic (3.4.x), plus the 380xx Wrath-line builds — 38000, which the Whitemane server reports (restored in 0.6.1; 0.6.0 had dropped it and would not load there), and 38002, the number the major addons carry on their Wrath manifests (added in 0.6.0)
- Cataclysm Classic (4.4.x)
- Mists of Pandaria Classic (5.5.x)
- Mainline / Retail (12.x, Midnight) — 12.1.0 declared as of 0.6.0; before that the addon showed as out of date on 12.1.0 and would not load unless you ticked "Load out of date AddOns"
- World of Warcraft: Forever (the retail client running classic-era content) — added in 0.9.2; not yet run on a Forever client
Required dependencies
Ace3 — supplies LibStub, CallbackHandler-1.0 and, for the sister-guild sync, AceComm-3.0 and AceSerializer-3.0. LibAceGUIWidgets (since 0.7.0) — the sister-guild window. VersionCheck-1.0 (since 0.8.0) — how the sync finds a member of the other guild who can answer, and on which version. CurseForge installs all three automatically when you install this addon, so most players don't need to do anything special. If you install by hand, take VersionCheck-1.0 v1.5.0 or later (released 16 September 2026): earlier versions of it required GuildRoster, and two addons that each require the other load neither.
Optional: GreenWall. When it is installed and your guilds are confederated, the sister-guild sync asks the whole confederation at once who runs this library, with no 50-member /who limit. With GreenWall 1.14.0 or later on both sides (since 1.1.0) it also carries the live stream of who is online and level-ups between the guilds. Without it everything still works through /who and one-to-one whispers. DeltaSync is optional too: with it on both ends, a re-sync sends only what changed.
Embedding in your addon
The recommended path is to reference this lib as an external in your addon's .pkgmeta:
externals:
Libs/LibGuildRoster-1.0:
url: https://github.com/Pimptasty/GuildRoster
tag: latest-release
Then load it from your .toc after LibStub and CallbackHandler-1.0 (provided by your own embeds, Ace3, or another lib):
LibsLibGuildRoster-1.0LibGuildRoster-1.0.lua
If you want the sister-guild sync, depend on the standalone addon instead of embedding. Two things since 0.7.0 live in the standalone TOCs and not in the Lua file. ## SavedVariables: LibGuildRosterDB — the library reads and writes that one global and nothing else, so only a TOC that declares it makes the sister list and rosters survive a logout; an embedded copy in an addon that does not declare it keeps them in memory for the session (the sync still runs, the gossip restores the list from a guildmate after login, and the first roster waits for the first relay or pull — not an error, it is the pre-0.7.0 behaviour). And ## Dependencies: Ace3, LibAceGUIWidgets, the second for the /gr window — without it ToggleSisterWindow() returns nil, /gr prints the slash usage instead, and everything else works. The shape that gets both is ## Dependencies: Ace3, GuildRoster (CurseForge slug libguildroster, which pulls LibAceGUIWidgets in with it) and a plain LibStub("LibGuildRoster-1.0"). Embedding stays right for an addon that only reads the home roster.
Quick example
local lib = LibStub("LibGuildRoster-1.0")
lib.RegisterCallback(self, "OnRosterReady", function()
print("Guild roster ready,", #lib:GetAllMembers(), "members.")
end)
lib.RegisterCallback(self, "OnMemberJoined", function(_, name)
print("Welcome,", name)
end)
Handling a name: which function, and why it matters
This is the one decision worth getting right, and it depends entirely on where the name came from.
-- YOU read it from the game -> NormalizeName.
-- A name is bare here only when that character is on your own realm,
-- so filling in your realm is correct.
local unitName = lib:UnitKeyName("target") -- "Thrall", or "Drie Stonefist" on Forever
local key = lib:NormalizeName(unitName) -- "Thrall-YourRealm"
local member = lib:GetMember(key)
-- It came from ANOTHER PLAYER -> CanonName.
-- Their realm may not be yours, so adding yours would invent a
-- different identity on every receiver.
local function OnAddonMessage(prefix, payload, channel, sender)
local who = lib:CanonName(payload) -- "Thrall" stays "Thrall"
if not who then return end -- nil = unusable, don't store it
myDatabase[who] = (myDatabase[who] or 0) + 1
end
Getting this backwards is silent, not loud. Using NormalizeName on a received name does not error — it produces a perfectly valid-looking key that simply differs from the one every other player computed. You find out later, when two players' data will not reconcile.
CanonName returns nil for anything unusable — nil, a non-string, an empty name, or a name missing the part before the hyphen — so check it before storing. It is safe to call on its own output, and gives the same answer on every client regardless of realm or language.
Register on lib.RegisterCallback (a dot), not lib.callbacks:RegisterCallback. lib.callbacks is the CallbackHandler registry — it owns Fire, while RegisterCallback / UnregisterCallback are mixed into the library table itself. The registry form raises "attempt to call method 'RegisterCallback' (a nil value)" at file scope, which silently kills every callback registration in the consuming addon.
The first argument is the object you are registering as (any table; it is the handle you unregister with later). A function callback receives the event name as its first parameter — that is why the handlers above take _ before the real arguments.
Caveats
- Show Offline Members needs no attention from you, and as of v0.5.1 the library never writes it. Up to v0.5.0 it bracketed each of its own scans — forcing that checkbox on just long enough to read, then restoring it — on the belief that the checkbox filters what the guild roster API returns. Measured on a live Classic Era client with the box unticked, the roster iteration returned 997 of 997 rows: it is not filtered, so there was nothing to work around, and the bracket itself was firing a roster event every time it ran. It is gone. Earlier versions also asked retail consumers to call
SetGuildRosterShowOffline(true)at init; that is no longer needed and now only overwrites a deliberate preference. - Recently-left dedup — a 60-second window after
OnMemberLeftsuppressesOnMemberJoinedfor the same player. A legitimate rejoin within 60 seconds will not fireOnMemberJoined. - Feature-detect anything added after the version you pinned. This library is embedded in several addons and
LibStubhands out whichever copy loaded first, so an older one genuinely circulates. Writeif GR.IsOfficer thenrather than assuming the method exists. The library header marks each addition with the MINOR that introduced it; "the API has been stable since MINOR 5" means nothing has been removed or reshaped, not that the list stopped growing. NormalizeNamereturns a bare name for a moment after login. Until the client knows its own realm,lib:NormalizeName("Bob")gives"Bob"rather than"Bob-YourRealm". That is deliberate and does not raise (it raised before 0.4.0 — that was a bug). Keys built in that window correct themselves: the login stream rebuilds the home roster on every event until it stabilizes, and fed sister rosters are re-keyed when the realm arrives. If you save a key, save it afterOnRosterReady.OnMemberOnlinecan fire from guild chat, not only from a login. If you announce on that callback, expect it for a member who was already online but whom the library had not yet seen — for instance after a/reloadduring which they never spoke.levelis always a number, but can briefly read 1. The game occasionally hands back a roster row with a name and no level. When that happens for someone the library has not seen before — a player who has just joined — it stores 1 as a placeholder so the field never becomesnil. During login a later row corrects it; for someone who joins mid-session the library makes one targeted row read for them (within about 60 seconds), and if that row never arrives the level stays 1 for the session — since v0.5.0 there is no periodic rebuild to catch it later. No level-change callback is fired for the correction. If you display levels, a brand-new member can show as 1.
Recent Updates
v1.1.4 — WoW: Forever error fix
LibStub MINOR 30 → 31. Nothing an addon calls has changed shape.
Fixed
- On WoW: Forever, guild or system chat during a boss fight, Mythic+ or PvP match could raise an "attempt to compare ... a secret string value" error from the library. It now skips chat the game keeps hidden, on every version of the game.
- On WoW: Forever, the library waited too short a time between its
/whosearches, so the server could answer one with nothing. It now waits the same 8 seconds it does on retail.
v1.1.3 — retail error fix
LibStub MINOR 29 → 30. Nothing an addon calls has changed shape.
Fixed
- On retail, fighting a mob could raise an "attempt to compare ... a secret string value" error from the library. It no longer reads names or levels the game keeps hidden during combat.
v1.1.2 — WoW: Forever whispers
LibStub MINOR 28 → 29. Nothing an addon calls has changed shape. Addon developers get lib:WhisperTarget(name), the name to whisper someone by on every client.
Fixed
- On WoW: Forever, the library's messages to other players could fail with "No player named ... is currently playing" while that player was online, because it addressed them with a realm the server does not accept. It now addresses them by their full name.
v1.1.1 — WoW: Forever character names
LibStub MINOR 27 → 28. Nothing an addon calls has changed shape.
Fixed
- On WoW: Forever, where characters have a first and last name and no realm, a character is now known by their full name everywhere, the same on every player's client. Before, the library could add a hidden realm to the name, so two players could disagree about who someone was, and your own character was known by your first name only.
v1.1.0 — Live sister-guild presence, and level-ups for both guilds
LibStub MINOR 26 → 27. Two callbacks (OnHomeMemberLevelChanged, OnSisterMemberLevelChanged) and two methods (RequestLevelRefresh, ReleaseLevelRefresh) added for addon developers; nothing an addon calls has changed shape.
New
- Your sister guilds see who in your guild is online, as it happens. With GreenWall installed, one player running the library in each guild passes logons and logoffs to the sister guilds. When that player logs off, the next one takes over automatically. Addons that sync data between guilds can then skip players who have gone offline instead of messaging them.
- Guildmates' zones stay current. Players running the library tell the guild when they change zone, and everyone else's zone is refreshed along with the guild list when an addon asks for it.
- Addons can now congratulate guildmates on a level-up without scanning the guild list. When you level, your copy of the library tells your guild in one short message, so other players' addons hear about it the moment it happens. A guildmate in your group is noticed as they level too, and while an addon asks for it the library keeps everyone's level current from the guild list, once a minute at most, however many addons ask.
- The same for sister-guild members. With GreenWall installed, your level-up reaches your sister guilds too, the moment it happens, and so do level-ups of guildmates who don't run the library, whenever someone in your guild has an addon keeping levels current. Otherwise a sister member's level-up is noticed from the library's lookups of the sister guild, so it may be a few minutes late.
Changed
- The library now shows in orange under the "Library" heading of the in-game AddOns list, alongside the other libraries, on every game version.
v1.0.1 — The guild tab is now called Members
LibStub MINOR 25 → 26. Nothing an addon calls has changed shape; the tab's name is the new constant GUILD_TAB_NAME.
Changed
- The guild tab is now called "Members" on both guild windows (it was "Sisters" on the classic window). It is a better guild roster for every guild — a search filter and a Reset button on top of the game's own list — and sister guilds only join it if your guild lists one. Its "i" help leads with that, and ends with how to turn the tab off in
/gr.
Fixed
- Retail: switching from the Chat tab to the Members tab no longer leaves a gap with no border along the bottom of the window, or a chat button showing over the tab's "i" help icon.
- Retail: Invite Member is now on the Members tab however you reach it; opened from the Roster tab, it used to be missing.
v1.0.0 — See where sister-guild members are
LibStub MINOR 24 → 25. Methods added for addon developers (RefreshSisterLocations, LocateMember, IsWhoOwed) and one callback (OnSisterLocationsUpdated); nothing an addon calls has changed shape.
New
- Sister-guild members show their zone. On the guild tab, sister members now have a zone, an up-to-date level and a correct online state, taken from
/who. Opening the tab asks the server for you on your next click anywhere — there is no button to press. - Big sister guilds are covered too. The server lists at most fifty players per
/who; when a sister guild has more online, the library asks again by level and then by class, the way FastGuildInvite's scanner does. - Right-click a sister member for "Where is …? (/who)" to look up just them, or "Who is online in …? (/who)" to refresh their whole guild now.
- An "i" beside Invite Member on the guild tab: hover it for how the tab works.
- A Reset button beside the search box on both guild tabs puts the list back the way it opened — no search, no column sort, online members first — without a
/reload. - Plays nicely with FastGuildInvite's Wingman. These lookups wait behind anything more important, take turns with other addons sending
/who, and never hold up Wingman's scanning.
Changed
- Having the Friends window open no longer stops the library's
/who— only the Who list being open does. Before, looking at the Sisters tab itself kept the sync from asking the server anything.
Fixed
- The
/grwindow's list of sister guilds could be blank with a recent version of LibAceGUIWidgets, and print a message about it in chat. - On World of Warcraft: Forever, the Group Finder no longer opens on its Who tab each time the library looks up a sister guild. Not yet tried on a Forever client.
v0.9.2 — The Social window stays shut beside Wingman, and World of Warcraft: Forever
LibStub MINOR 23 → 24. Two methods added for addon developers (GiveBackWhoList, HookWhoPane); nothing an addon calls has changed shape.
New
- The library loads on World of Warcraft: Forever. A seventh manifest for that client (the retail game running classic-era content) ships alongside the six existing ones, so an addon that depends on this library — FastGuildInvite from v2.14.0 — can load there. The library has not yet been run on a Forever client.
Fixed
- The Social (Who) window no longer pops open on its own when this library and FastGuildInvite's Wingman both send a
/whofrom the same click. Both keep the window shut while a background/whois out, and both used to let the game have it back the moment their own answer arrived — but the game cannot say whose answer is whose, so the first answer let the second one open the window. The window now comes back only when you open it yourself or send a/whoof your own. FastGuildInvite v2.14.1 makes the same change on its side; with an older FastGuildInvite the window can still appear from that side. - A
/whoof the library's own waits its turn. The server ignores a second/whosent within a few seconds of the last one (about 8 on retail, 5 elsewhere), whoever sent it — so if you, or another addon, just sent one, the library's goes out on your next click after that gap instead, and the status bar says how long. Two addons' queries can no longer be answered as each other's.
v0.9.1 — The classic tab no longer errors at login on retail
LibStub MINOR 22 → 23. One fix; nothing an addon calls has changed.
Fixed
- A Lua error at every login on a retail client — Couldn't find inherited node "GuildFrameColumnHeaderTemplate" — is gone. Retail's Friends frame passed every check the classic Friends-frame tab made before building, but does not carry the Classic guild page's row and column-header pieces, and asking the game for a piece it lacks is an error rather than an empty answer. The library now asks the game whether it has each piece before building either tab, and builds nothing where one is missing. The Communities-window tab on retail is unaffected; the classic Friends-frame tab was never meant to appear there.
v0.9.0 — A guild and sister guilds tab on both guild windows
LibStub MINOR 21 → 22. Nothing an addon calls has changed; the additions are listed above.
New
- A new tab on the guild window lists your guildmates and your sister guilds' members together. It sits at the bottom of the tab column, below the Guild Info tab and below any button another addon (such as Guild Roster Manager) has put there, and looks just like the Roster page: class, level, zone, rank (or the sister guild's name for a sister member), public note, and who is online. Column headers sort, and "Show Offline Members" works as usual.
- The classic guild window gets a tab as well. If you use "Use Classic Guild UI" in the Social options, a "Sisters" tab sits across the bottom of that window after Raid, with the same list in the classic style: Name, Zone, Lvl and Class, the classic scrollbar, the search box, and the same clicks and settings. A sister member's guild shows in the Zone column. Clicking Friends, Who, Guild or Raid — or pressing the key that opens one of them — brings the game's own page back.
- A search box above the list filters it by name, rank, guild, note or zone as you type.
- Left-click a name to whisper them; right-click to whisper or invite them to your group. Promoting, removing and editing notes stay on the game's own Roster or Guild tab, because the game only allows them there.
- Settings in
/gr: the tab itself (on by default, for both windows), and "Open the guild window on that tab" (off by default) so the J key opens straight onto it./guildroster tab on,off,defaultandnodefaultdo the same. The tab's own "Show Offline Members" box is remembered for your account and shared by both windows; the game's own setting is never changed by it.
Changed
- The game's own Roster page is left exactly as it was. An early build of this release added sister members to it directly, and a check in game showed that could block officer actions on that page, so the list moved to its own tab instead.
Fixed
- A sister-guild member who had logged off could show as online for hours. Guildmates passing a sister roster around also passed along who they had seen, and each copy counted as a brand-new sighting, so one real sighting kept bouncing between two players. The relay now says how long ago each member was seen, and that sighting expires on the same schedule everywhere. Guildmates still on 0.8.x send undated sightings, which are ignored until they update. Reported by the TOGBank team.
Older release notes (v0.8.2 and earlier) are in the addon's full changelog.
For developers and AI coding assistants
If you are integrating this library — or you are an AI assistant writing code that does — these are the things that are actually got wrong, in the order they bite. Every one of them comes from a real consumer bug, not from imagining what might go wrong.
The eight that cost someone a debugging session
- Register with a dot, not a colon on the registry.
lib.RegisterCallback(self, "Event", fn). Writinglib.callbacks:RegisterCallback(...)raises "attempt to call method 'RegisterCallback' (a nil value)" at file scope, which silently kills every callback registration in your addon — including the ones after it. - A function callback receives the EVENT NAME first.
function(event, name) ... end, notfunction(name) ... end. This is CallbackHandler's contract, not ours, and getting it wrong gives you a member called"OnMemberJoined". - Choose
NormalizeNamevsCanonNameby where the name CAME FROM, never by what you plan to do with it. Read from the client (a roster row, a unit, the player) —NormalizeName, which appends your realm, correct because a bare name there means your realm. Arrived over the wire (an addon message, a sync payload, a saved string from another client) —CanonName, which leaves it bare. Getting this backwards does not error: it builds a valid-looking key that differs from the one every other player computed, and you find out weeks later when two clients' data will not reconcile and neither is wrong on its own terms. The rule needs both ends: the SENDER qualifies, the RECEIVER canonicalizes. Only the sender knows its own realm, so it putslib:GetNormalizedPlayer()(orNormalizeNameon a locally-read name) on the wire; the receiver runsCanonNameon what arrives. Both return the same string for an already-qualified name, so that round trip is lossless. And noteNormalizeNameis not pure: it appendsGetRealmName(), which isnilbetween login and the realm resolving, so the same bare name normalizes bare early and qualified later. The library re-keys its own stores when the realm lands; it cannot re-key yours. If you persistNormalizeNameoutput, take it afterIsReady(). - Feature-detect anything added after the version you pinned. This library ships inside several addons and
LibStubhands out whichever copy loaded first, so an older one genuinely circulates. Writeif GR.IsOfficer then. "The API has been stable since MINOR 5" means nothing has been removed or reshaped — not that the list stopped growing. - Do not test for officer status with a rank index. Use
lib:IsOfficer(). A rank index is a position in a list the guild leader arranges however they like, sorankIndex <= 2means nothing in particular — in the very common GM / Officer / Alt layout it lets every alt through. A consumer shipped exactly that and an alt wiped the guild's dataset. - If you feed alt groups, read what
SetAltGroupreturns, and use one owner-key rule across every store that holds it. New in 0.6.0. Four returns that mean four different things, and afalse, "stale"is success with nothing to do rather than a failure to retry. The owner-key trap is the one that has already bitten a consumer: a key derived from the group's contents moves, and a sweep that asks "did the old group name the new owner?" is never true in exactly the case that matters. Both are written out in the alt-group section above. - If you register callbacks late, check
IsReady()and do the initial fill yourself. There is no replay.OnRosterReadyfires exactly once per session, from one site, and the login handler returns early once the roster is initialized — so an addon that wires up atPLAYER_ENTERING_WORLD, or on/reload, or simply loads after this library, registers after the event has already gone. The pattern is two lines:lib.RegisterCallback(self, "OnRosterReady", "OnReady")thenif lib:IsReady() then self:OnReady() end. Get it wrong and the consequence is worse than it used to be: since v0.5.0 there is no next full rebuild, so the window is not "empty for a bit", it is empty for the session, with nothing to explain why. SetSisterRosteris wipe-and-replace, and you are probably not the only feeder. It replaces the whole roster for thatguildKeyon every call — there is no merge. DeltaSync and TOGProfessionMaster both feed sister rosters, and a third addon has asked about doing so. If two of you feed the same key, last writer wins and the loser's members vanish, and it presents as members intermittently disappearing rather than as an error. The library cannot arbitrate that; settle it between the feeders before shipping. And feed realm-qualified names: a sister guild is by definition not yours, so a bare name is filed under your realm, which for a cross-realm guild is a character that may not exist.
Taking this library as a hard dependency
New guidance in 0.6.0, because a consumer did it and the first thing that happened was a false alarm. A ## Dependencies: entry narrows your addon to the client versions both of you declare: on any client where this library is flagged out of date, it does not load, your dependency is unsatisfied, and your addon does not load either. So before you take the dependency, check the intersection. This library declares seven per-flavour TOCs — GuildRoster.toc is Classic Era only, not the whole set — and as of 0.9.2 the numbers are:
| TOC | ## Interface: | Client |
|---|---|---|
GuildRoster.toc | 11509 | Classic Era 1.15.9 |
GuildRoster_TBC.toc | 20506 | Burning Crusade Classic 2.5.6 |
GuildRoster_Wrath.toc | 30405, 38000, 38002 | Wrath Classic 3.4.5, plus the 380xx Wrath-line builds: 38000 is what the Whitemane server reports, 38002 is what DBM and WeakAuras ship on their Wrath manifests |
GuildRoster_Cata.toc | 40402 | Cataclysm Classic 4.4.2 |
GuildRoster_Mists.toc | 50504 | Mists of Pandaria Classic 5.5.4 |
GuildRoster_Camelot.toc | 16001 | World of Warcraft: Forever (the retail client running classic-era content; read ahead of _Mainline.toc, same file list) |
GuildRoster_Mainline.toc | 120005, 120007, 120100 | Retail 12.0.5, 12.0.7 and 12.1.0 |
If your addon declares a number that is not here, either raise it with us or accept that your addon will not load on that client while the dependency stands. These seven files are pinned by the test suite (Tests/toc_spec.lua), so a number cannot go stale silently again — that is how the retail one did, and a consumer found it before we did. The same spec reads this table back and fails if it disagrees with the manifests, so the table is checked rather than just typed. The spec is ~300 lines of plain file-reading Lua that needs only the runner's describe/it/assert — nothing from the WoW environment model — so if you ship per-flavour TOCs yourself it is worth copying, and it also pins the two .pkgmeta mistakes that ship a broken zip while reporting success.
Things that are true now and were not before v0.5.0
If you are working from an older example, a cached answer, or another addon's code, these are the claims most likely to be stale:
- The roster is built ONCE, at login. There is no periodic rebuild and no self-healing. Anything that used to be "corrected on the next rebuild" is now permanent for the session.
OnRosterUpdatedis not a "something changed" hook. It fires a handful of times during login and then never again. Use the per-member callbacks, which stay live all session.OnMemberLevelChangednever fires. Registering is harmless; it simply never runs.member.levelis still readable; since v1.1.0 useOnHomeMemberLevelChanged.- Never call
SetGuildRosterShowOffline. Not to "make sure the roster is complete" — it is complete, measured on both flavours — and not for any other reason. That setter firesGUILD_ROSTER_UPDATE, so writing it from anything reached by that event is a positive feedback loop. It cost a player 110 fps down to 20, and removing it is what v0.5.1 is.
How to check it is actually working
The library's work happens out of sight, so "no Lua errors" is not evidence that it works — every serious defect this library has shipped failed silently. Paste this in game after login, once the roster has settled:
/dump (function() local M="LibGuildRoster-1.0" local S=LibStub(M) return {minor=LibStub.minors[M],ready=S:IsReady(),members=#S:GetAllMembers(),online=#S:GetOnlineMembers(),patterns=S.chatPatternsBuilt.."/"..S.CHAT_PATTERNS_TOTAL} end)()Healthy is ready=true, patterns=8/8 (7/7 before 0.8.0), and members matching your guild's size. ready=false after login means the roster was never built and everything downstream is empty and silent. patterns below the full count means a chat message type cannot be detected on that client at all. A minor lower than you expect means an older embedded copy won the load race and is the one running.
patterns=8/8 is not proof that parsing works — it counts patterns that BUILT, not patterns that MATCH. It read the full count on a Russian client for the entire time ruRU kicks were dead.
If you feed alt groups, this one says whether they are arriving and whether the feeders are stamping a canon — an unstamped write succeeds silently and looks identical to a stamped one from the outside, so this is the only place it is visible in game:
/dump (function() local S=LibStub("LibGuildRoster-1.0") local o,s,h=S:GetKnownAltOwners(),0,0 for _,k in ipairs(o) do local m=S:GetAltGroupMeta(k) if m then s=s+1 if m.hash then h=h+1 end end end return {owners=#o,canon=s,hashed=h} end)()canon < owners is the mixed-adoption state and is expected while adopters roll out — those groups get no identity check, so an unchanged re-broadcast is applied as a fresh write instead of answering "unchanged". Ordering is still protected by an internal high-water mark that an unstamped write cannot clear, so it degrades rather than breaks. hashed < canon is the one to chase: setAt alone gives ordering but not identity, so every re-feed of an unchanged group looks like a new publish — usually a feeder computing setAt but never calling its hash function, or stamping it somewhere other than its save path.
Contact
Bug reports, feature requests, questions, or just chatting: Join the Discord.
You must be logged in to leave a comment.