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.
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. The GetGuildRosterInfo iteration is also silently filtered by the guild panel's "show offline" toggle on retail — flip it off and your "roster" only contains currently-online members.
This library solves both. It gates the OnRosterReady callback behind a stabilization phase that requires two consecutive builds at the same member total, and brackets every roster scan so the show-offline filter can never truncate it — forcing the flag on just long enough to read, then restoring whatever the player had set. Joins fire from the authoritative CHAT_MSG_SYSTEM "has joined the guild" message, never from a roster diff that could be confused by a partial snapshot.
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. 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 }. 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.
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.
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 (your addon discovers and syncs them). The lib only stores, normalizes, hashes, diffs, queries, and ages presence; it does no networking and persists nothing. 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, rank }tables;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, default 120s).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.
Presence is a separate overlay from membership: a roster resync never wipes liveness, and the lib never asserts "offline" — presence simply ages out and self-corrects. Persist sister rosters in your own SavedVariables and re-feed them on login; 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 ...).
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, so polling is the only replacement.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 and v0.5.1 is 16.
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, that feature stops working and there is currently no supported replacement.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.
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)
- Cataclysm Classic (4.4.x)
- Mists of Pandaria Classic (5.5.x)
- Mainline / Retail (12.x, Midnight)
Required dependency
Ace3 — supplies LibStub and CallbackHandler-1.0. CurseForge installs Ace3 automatically when you install this addon, so most players don't need to do anything special.
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):
Libs\LibGuildRoster-1.0\LibGuildRoster-1.0.lua
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 = UnitName("target")
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
v0.5.1 — The library stops writing your "Show Offline Members" setting
LibStub MINOR 15 → 16. One fix reported from the field, plus two defects the pre-release self-audit found in this release's own code. Nothing for a consumer to change.
- An FPS collapse while recruiting is fixed. A FastGuildInvite user went from 110 fps to about 20 over seven minutes of inviting, cleared by a
/reload, and it only happened to players with Show Offline Members unticked. The cause was this library: every roster scan forced that checkbox on and put it straight back, andSetGuildRosterShowOfflinefiresGUILD_ROSTER_UPDATE— so two writes per scan fed the same event that caused the scan. An accepted guild invite is what starts the cycle, which is why recruiting sustained it. - The workaround was never necessary in the first place. It existed because the roster iteration was believed to be filtered by that checkbox. Measured on a live Classic Era client, 997-member guild, box unticked: the iteration returned 997 of 997 rows. Blizzard's own Classic guild UI reaches the same conclusion by construction — it reads both the total and online counts and picks which to iterate to, which would be pointless if the data were filtered.
- Retail was measured too, and it matches Classic. On a live retail client, 900-member guild, with the flag forced off and the write verified rather than assumed, the iteration still returned 900 of 900. Neither flavour filters. Worth knowing if you ever measure this yourself: on retail the Communities panel's "Show Offline Members" checkbox is decoupled from
GetGuildRosterShowOffline— unticking it in the UI leaves the accessor readingtrue, so a measurement taken by clicking the box tests nothing. - Two defects found by the pre-release self-audit, in this release's own code. The load-time counter that reports how many chat patterns built could have read a perfect
7/7while silently ignoring an eighth message type, because its total was a literal sitting beside a positional table — it now derives from a single list, so the failure direction is a visible alarm rather than an invisible all-clear. And both roster scans were wrapped in apcallthat re-raised immediately, left over from the removed workaround, which truncated the traceback on any error; an error now reports the line that actually failed. - The in-game addon list description was wrong. All six manifests still described "wipe+rebuild semantics", which v0.5.0 made false.
One test is left pending rather than quietly adjusted to pass: the offline harness still modelled retail as filtered, so asserting the measured behaviour would have failed against the test environment rather than against the library. That is a harness change and it has been raised there.
v0.5.0 — The roster is built once and never rebuilt, and four locales come back to life
LibStub MINOR 12 → 15. The largest behaviour change this library has had. Everything is source-compatible — nothing renamed, nothing errors — but two changes are silent, meaning your addon keeps running and quietly stops doing something. Those two are marked SILENT below.
- The micro-stutter when a guildmate logs in or out is gone. The library used to rebuild its whole roster every time the game fired
GUILD_ROSTER_UPDATE— an event the client's own calendar code re-triggers in a loop, so it arrives in bursts of three while you are standing still and six to nine when someone logs off. On a 978-member guild each rebuild cost 3–47 ms, and one guildmate logging out cost about 141 ms of frame time in this library alone. The roster is now built once during login and kept up to date from chat messages; after that, the same event costs 0.004 ms and a 990-member roster still comes up complete. Both numbers are measured in a live Classic Era client, not estimated. - Kicks and rank changes now work on Russian, French, Korean and German clients. Blizzard writes some locales' chat messages with reordered arguments and with grammar directives — declensions, articles, postpositions — that the game resolves before you ever see the line. The library matched neither, so on those clients the messages it needed were rejected before they were examined: a Russian player kicked from the guild produced no event at all, on every version that has shipped, and promotions and demotions were silent on Russian, French and Korean. This mattered far more after the change above, because there is no longer a periodic rebuild to quietly correct it. All eleven locales the game ships are now covered by an automated test.
- SILENT —
OnMemberLevelChangedno longer fires, and nothing warns you. Its only source was the roster rebuild that this release removes, and no chat message announces a guildmate levelling up. See the callback list above for the full explanation. If you announce guild level-ups from it, that feature stops working;member.levelstill reads throughGetMember, so polling is the only replacement. - SILENT —
OnRosterUpdatednow fires only during the login stream, because that is the only time a full rebuild happens. Its meaning is unchanged — "a full rebuild completed" — but if you were treating it as the general "the roster changed" hook, you go permanently deaf after login. Use the per-member callbacks, which stay live all session. - A player who joins is added to the roster immediately, from the join announcement itself, instead of waiting for a roster round-trip that no longer happens. Their class, rank and level are filled in from a single targeted row read shortly afterwards, and it gives up after 60 seconds if the server never produces one.
OnMemberRankChangedis now driven by the promote/demote chat messages rather than the rebuild diff. Those messages name the new rank, so the index is resolved through a rank-name map learned from the roster. A promotion into a rank nobody currently holds has no index to resolve to:member.rankNameis still updated and the callback stays silent rather than reporting a guessed index.OnRosterHashChangedarrives sooner — it fires from the join and leave branches now, when the change happens, instead of at the next rebuild. No signature change and nothing to do.member.lastOnlineno longer freezes or contradictsisOnline. Because roster rows are only read at login, the field had three ways to go silently wrong over a session. Presence transitions now maintain it. Read the note onGetMemberabove before displaying it — its resolution is "as of login, or as of the moment we saw them log off".- The login retry obeys the server's throttle. The library was re-requesting the roster while the server had already said it would ignore the request, and counting those against its retry budget — so the budget could run out having made no effective attempt. On a bad login that could have left an empty roster with nothing to correct it.
Retail only: during a Mythic+, raid encounter or rated PvP match the game protects system chat and this library stops parsing it, so a join, leave or kick announced inside that window is now missed for the session rather than picked up late. Presence recovers on its own the next time that member speaks. Classic-flavour clients have no such system.
v0.4.0 — Micro-stutter fix, IsOfficer, and two login-window bugs
- The library was the cause of a micro-stutter when guildmates logged in and out, and it no longer is. Measured on a 978-member guild: a full roster pass cost around 27 ms, of which the game's own roster reads were 1.3 ms — the other 95% was this library. Two things were doing it. The membership digest was recomputed on every roster event, and it sorts every member key, joins them into a ~10,000-character string and hashes it a byte at a time; a guildmate logging in cannot change a membership digest, so that work was pure waste. And
NormalizeNamewas re-deriving the same few hundred names on every pass, about 5,900 string allocations each time. The digest is now computed only when the membership set can actually have moved, and name normalization is cached. No callback behaviour changed —OnRosterHashChangedfires exactly when it did before. - New
lib:IsOfficer([name])— one officer check for everyone, and it uses the permission, never the rank number. A rank index is just a position in a list your GM arranges however they like, sorankIndex <= 2means nothing in particular — in the very common GM / Officer / Alt layout it lets every alt through. An addon shipped exactly that and an alt could wipe their guild's entire world-buff dataset. This asks whether the GM actually granted the officer-note permission. If you have a rank-number check anywhere, this is what to replace it with. It answers for you; for anyone else it returnsnil, meaning "unknown" rather than "no", because the game exposes no way to ask about another member's permissions. - Guild and officer chat now count as proof someone is online. The "has come online" message is only seen if your client was listening, so a guildmate who logged in before you did, or during a
/reload, stayed marked offline while visibly talking. A message from them now corrects that immediately, and costs a couple of table lookups — no roster request, which is the expensive thing this avoids. It is one-directional: talking proves online, silence proves nothing. - Fixed: normalizing a name could throw a Lua error during login. For a moment after you log in the game has not yet told the client its own realm name, and in that window
NormalizeNameraised "attempt to concatenate a nil value" out of the library and into whatever called it. It now returns the plain name for that window instead, exactly asGetNormalizedPlayeralways has. - Fixed: cross-guild rosters fed during that same window kept unrealmed names and published a membership digest no other client could match — which is the signal that tells a sister client whether to re-sync, so two clients holding identical members would have disagreed forever. Those rosters are now re-keyed the moment the realm arrives.
- Fixed: TBC and Classic Anniversary were loading the Vanilla manifest. The file meant to serve them was named
GuildRoster_BCC.toc, which is not a filename any client recognises, so those clients fell through to the Classic Era file and flagged the addon out of date. It is nowGuildRoster_TBC.toc. The Wrath manifest's interface number was also two patches stale. - For addon authors: feature-detect
IsOfficer(if GR.IsOfficer then) rather than assuming it — this library is embedded in several addons and LibStub hands out whichever copy loaded first, so older copies without it are still in circulation.
v0.3.0 — CanonName, for names that arrive from another player
- New
lib:CanonName(name)— the one to use for a name that came from another client. It cleans a name up exactly likeNormalizeNamedoes, with one difference: if the name has no realm on it, it leaves it alone instead of adding yours. That sounds small and it is the whole point.NormalizeNameadds your realm to a bare name, which is correct for a name you read out of the game yourself — a name is only ever bare there when that character is on your realm. It is wrong for a name that arrived over the wire, because your realm is not necessarily the sender's. On a connected realm,"Thrall"sent by one player became"Thrall-Fairbanks"on one receiver and"Thrall-Whitemane"on another — one person stored as two records, with nothing afterwards to say they were the same. The rule is where the name came from: read it from the game yourself, useNormalizeName; received it from someone else, useCanonName. See the Quick example above. - Two addons hit this independently before it was fixed. One had guild-policy elections disagreeing across a connected realm, because a bare name in a saved list resolved differently on each player's client. If you keep any list of names that syncs between players, this is worth a look.
NormalizeNameis unchanged for every normal call and the library's own roster keys are exactly as before, so nothing needs updating unless you want the new behaviour. Three edge cases did change: passing something that is not a string (a number, say) now returnsnilinstead of a made-up name like"123-YourRealm"; a stray:in a name is now removed; and"unknown"still comes back as"Unknown"without a realm attached.
v0.2.5 — No more phantom level-ups
- Fixed: the library could announce a level-up that never happened. The game sometimes hands back a guild roster entry with a name but no level. The library used to record that member at level 1; on the next roster update their real level arrived and it reported a level-up — from 1 to 60 — for someone who had simply been listed incompletely. An announcer built on
OnMemberLevelChangedwould congratulate them in guild chat. It now reuses the level it already had for that member, and for a member it has never seen carrying a level it holds the change back until a real one arrives. Genuine level-ups after that are reported normally. If you wrote a workaround that ignores jumps from level 1, you can remove it.
v0.2.4 — Level-change callback, and a more robust roster
- New
OnMemberLevelChanged(name, oldLevel, newLevel, wasOnline, isOnline)callback. Fires when a guildmate's character level changes between roster rebuilds — the piece you needed for a "grats on level 60!" announcer, without having to snapshot the whole roster yourself. It reports the raw transition rather than deciding for you: it fires on a decrease as well as an increase, and it isn't gated on whether the member is online. The member's presence before and after the rebuild comes with the callback, so your addon can apply whatever rule it wants without caching presence of its own. Detected from the rebuild diff under the same guard as the rank callback, so the login roster stream can't misfire it. - "Show Offline Members" is no longer your problem — or the library's. That checkbox filters what the guild roster API returns, so a roster read while it's off contains only online members: offline guildmates look like they aren't in the guild, and each one looks like a brand-new join when they next log in. Previous versions forced the checkbox on at login, which overwrote a setting players may have chosen on purpose and still broke if another addon flipped it afterwards. Now each roster read forces the flag on just long enough to scan and restores whatever the player had — so the roster is always complete, on every game version, and the player's setting is never changed. Addon authors: stop calling
SetGuildRosterShowOffline(true)at init — it's no longer needed and only clobbers a preference. - Fixed: a guildmate who left could trigger a "came online" event. When the server sent a stale roster that still listed someone who had just left the guild, the library announced them as coming online.
- Fixed: the "roster is ready" signal could fire on a half-loaded roster after changing guilds. Leftover state from the previous guild could make the very first roster update in a new guild look settled, so addons acted on an incomplete member list.
- Corrected the callback registration example in the docs. The old snippet used a form that errors on load and silently kills every callback in the consuming addon. See the Quick example above for the correct form.
v0.2.3 — Mists of Pandaria Classic support, and a 12.0.1 API refresh
- Now supports Mists of Pandaria Classic. The library ships a new TOC for the MoP Classic client (interface 50504), so it loads cleanly on the current Classic-progression realms. It now covers six flavours: Classic Era, BCC, Wrath, Cataclysm, Mists, and Retail.
- Interface versions refreshed to the latest builds. Every TOC was bumped to its current client version (Classic Era 1.15.9, Cataclysm 4.4.2, Retail 12.0.7, plus the new MoP 5.5.4), so the addon no longer flags as "out of date."
- The
isMobilemember field is retired. Blizzard removed the WoW Companion "mobile" flag from the guild roster in patch 12.0.1, soGetMember(...).isMobilenow readsfalseon current clients. The field is kept for backwards compatibility, but treat it as deprecated — don't rely on it. - Re-verified against the 12.0.1 client. The whole API surface was re-audited on every supported flavour after the 12.0.1 patch. Aside from the retired mobile flag, everything the library depends on is unchanged, and the retail taint-safety guard remains correctly scoped (Classic clients gained the lockdown API in 12.0.1 but never mark guild chat as protected, so the behaviour is unchanged there).
v0.2.2 — In-game name now matches the CurseForge name
- Shows as
Lib: LibGuildRosterin-game. The addon now displays asLib: LibGuildRosterin the in-game AddOns list and in BugSack, matching this project's CurseForge name. Previously it showed asLib: GuildRoster, which made it hard to find when another addon reportedGuildRosteras a missing dependency and a CurseForge search for "GuildRoster" didn't surface this page. Nothing else changed — the download, the folder, and the library itself are identical to v0.2.1, so no action is needed if you already have it installed.
v0.2.1 — Retail taint safety, and faster message scanning
- No longer taints retail execution during instanced content. On retail, system chat lines are flagged protected ("secret") while you're in an active Mythic+, raid encounter, or rated PvP match. Reading one with a string operation taints execution and shows up as unrelated Blizzard errors (money frames, world-map tooltips, and the like). The lib now checks Blizzard's
C_ChatInfo.InChatMessagingLockdownstate and skips parsing while it's active, so it never touches a protected value. Guild presence, joins, and leaves work normally everywhere else — only those locked-down windows are skipped, and membership catches up on the next roster rebuild. Classic, BCC, Wrath, and Cata have no such system and are unaffected. - Cheaper
CHAT_MSG_SYSTEMscanning. The handler now rejects the flood of unrelated system messages (loot, achievements, instance notices, ...) with a single cheap text check before doing any heavy pattern work, so the library does far less work per chat line. This matters because it sits in the chat hot path of every addon that embeds it.
v0.2.0 — Cross-guild (sister-roster) support
- Track sister guilds alongside your own. A new multi-roster store lets a consumer feed in one or more sister-guild rosters (membership + presence) and query them the same way as the home guild. Everything from the previous release is unchanged and home-only; all the new behaviour is additive.
- New methods:
GetHomeGuildKey,SetSisterRoster,RemoveSisterRoster,MarkOnline,GetOnlineMembersScoped,IsInAnyRoster,IsInGuildScoped,GetRoster,GetRosterMeta,GetKnownRosters,GetRosterHash. - New
OnRosterHashChanged(guildKey, newHash)callback and aguildKey2nd argument onOnMemberJoined/OnMemberLeft(one-arg consumers are unaffected). - Presence is a separate, transient overlay — a roster resync never wipes liveness, and the lib never asserts "offline"; presence ages out on its own. Nothing is persisted by the lib.
v0.1.0 — Player normalization helper, documented API
- New
lib:GetNormalizedPlayer()getter. Returns the local player's ownName-Realmstring in the same form as the roster keys, so you can match yourself againstGetMember/GetAllMemberswithout rebuilding the string by hand. Falls back to the bare name before the realm resolves. NormalizeNameandGetRealmNameare now documented. Both have always been available; they're now listed as public API for consumers that buildName-Realmkeys or compare against roster keys.
v0.0.4 — Retail "secret string" taint fix
- Retail (TWW+)
CHAT_MSG_SYSTEMtaint errors silenced. Some retail system messages carry a protected "secret" flag, and touching one with a string operation throws a taint error that could cause the lib to miss later guild events. This release suppressed the visible error. (This quieted the crash but did not fully prevent the underlying taint — that was properly fixed in v0.2.1.)
v0.0.3 — Ace3 dependency, install fix
- Standalone install no longer errors. v0.0.1 and v0.0.2 could log a Lua warning about missing
LibStub.luaon first load; this is resolved by depending on Ace3 (which suppliesLibStubandCallbackHandler-1.0). - Ace3 is now a required dependency. CurseForge installs it automatically when you install this addon.
v0.0.2 — Locale fix, expanded member data, rank-change callback
- Non-English clients now work properly. Online, offline, join, and leave events fire in real time on every locale; previously the hardcoded English chat patterns silently failed on German, French, Russian, Chinese, and other clients.
- More data on every guild member.
GetMembernow returns zone, public note, officer note, AFK/DND status, and offline duration (years/months/days/hours). - New
lib:IsReady()getter. Consumers loading after the lib (or registering callbacks after the first roster build) can now check readiness and bootstrap themselves without missingOnRosterReady. - New
OnMemberRankChanged(name, oldRankIndex, newRankIndex)callback. Fires when a guildmate is promoted or demoted, detected from the rebuild diff. Safe against partial-roster login streams.
v0.0.1 — Initial release
- First standalone release of LibGuildRoster-1.0 on CurseForge, extracted from its prior home as a vendored copy inside FastGuildInvite.
- Supports Classic Era, Burning Crusade Classic, Wrath Classic, Cataclysm Classic, and Mainline / Retail.
- Full public API and callback surface as documented above.
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 five 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. - 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.
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, so polling is the only replacement.- 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
A library has no UI, 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=7/7, 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 7/7 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.
Contact
Bug reports, feature requests, questions, or just chatting: Join the Discord.

