promotional bannermobile promotional banner

Ultimate Guild Roster

Ultimate Guild Roster - Guild Roster Management, Shared Across Your Guild
Back to Files

UltimateGuildRoster-v0.4.0

File nameUltimate_Guild_Roster-UltimateGuildRoster-v0.4.0.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 18, 2026
Downloads
18
Size
269.7 KB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9235694
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 5.5.4
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

Changelog

All notable changes to Ultimate Guild Roster. Work in progress sits under [Unreleased]; the version number is decided at release prep.

[0.4.0] - 2026-10-17

Changed: the log is cleared by type (LOG-07, todo #4, docs/design/event-log.md "Clearing by type")

  • The Log tab's three clear buttons and the date-range dialog are replaced by one "Clear by type" button that any guild member may use on their own copy. It opens a dialog with a tick box per category and its count, asks with the total, and clears up to the moment of the question (EventLog:Clear(categories, upTo, bound), EventLog:Counts). Every type of a cleared category records logClearedAt, so Add and the GRM import never bring an older line of it back (EventLog:Cleared).
  • Three categories are split to match GRM at a minimum: rank into promote and demote, leave into left and kicked, alts into alt links and main changes. Migration 8 (SCHEMA_VERSION 8) moves each saved log and chat switch onto its parts with the same value. The 20 filter chips fit two rows at the pane's width by shorter labels, CHIP_PAD 2 and a 100 px search box (only measured with the harness's text metric).
  • Peer Review F2 (thread dc2520d3): a roster comparison's change is stored even when the game's guild log dates it before a clear, since it is new by construction (AddChanges does not ask Cleared). Peer Review N1 (thread 8d43f4c2): such a change arriving while the question is up is not removed, because Clear looks only at the entries Counts counted (EL-23b). F3 is a known cost: a live line in the same server second as a clear is refused.
  • Peer Review A6 (thread 8d43f4c2): Tests/settings_spec.lua reads the category list out of docs/design/settings.md rather than keeping a third copy, and asserts SavedVariables.CATEGORIES matches it in order.

Added: SY-54, whole-client login catch-up over the hash protocol

  • The last pending sync spec, which "waited on the protocol", is written: three examples in Tests/sync_two_peers_spec.lua. A new client with nothing ends with every set's canon equal to an established client's, and receives a rename marker with no by (an old group holding only a tombstone) with the sender's canon (Peer Review thread c04b4fd1 reply 8444c31d, item 2); two clients each holding a no-push part the other lacks both end with the union and equal canons after one announce; and a third client relays an officer's part to a fourth while the officer is quiet.
  • No code change: the protocol it waited on was already built. Example 1 and 3 passed on their first run.
  • The union example went red in the harness's default "sync" delivery (run 20261017-141226.201): Alice's offer reached Bob inside his own BroadcastItemHashes send, before DeltaSync opens the collect window after that call returns, so the offer was dropped. The game never delivers a reply inside the sender's own send, so SY-54 runs with wow.echoMode = "queued" and pumps each second, and passes (run 20261017-141343.635). The relay example's whole-set compare with the quiet officer was dropped, with the reason in the spec: the fourth client's own no-push login history never reaches a client that is not syncing.
  • Whole suite green, run 20261017-141723.062 (1601 passed, 0 failed, 2 pending). Not verified in game.

Added: sister guilds share members and the log, each guild's in its own store (SY-65, docs/design/sync.md "Sister guilds")

  • Sharing with sister guilds is back on, the way decision 8 has it: the hash list goes out over GreenWall and negotiation and data go by whisper. Sync.SHARED is { members = true, log = true }, which also turns the GreenWall bridge on in Sync:Start.
  • Each guild's copy of a shared set is its own DeltaSync item, "<set>@<GuildId>" (Sync:WireKey, Sync:GuildId: ComputeHashV2 of LibGuildRoster's "Faction-GuildName" key in lower case). Our own guild's wire key reads our set; a sister guild's copy is kept at sync.sisters[<guild key, lower case>][<set>], and Sync:Merge calls no applier and sends no UGR_SYNC_APPLIED for it, so a sister guild's leaver never becomes our former member and their log lines never enter our log (the case Peer Review S2 found, thread 51800e63). Nothing shows a sister guild's copy yet.
  • Sync:IsSetPeer is replaced by Sync:Takes and Sync:Serves: plain set keys stay between home members; our guild's wire key is served to a sister guild's members and never taken from them; a sister guild's wire key is taken from that guild's members only and served to nobody. answerLeg3 merges a leg-3 give only from someone it takes from, askLeg3 gives only to someone it serves, and a live push is taken only for a plain key.
  • Sync:MyHashes lists our guild's wire keys and our copy of each configured sister's after the plain sets; hasContent accepts exactly those keys.
  • Peer Review S1 (thread 51800e63) fixed in UGR: Sync:OnOfferReceived now drops, from the table DeltaSync's P2P reads next (DeltaSync.lua _HandleOffer), every hash-list item we would not serve the sender and every hash-offer item we would not take from them. A sister member's offer of a plain set is never recorded, so it cannot crowd a guildmate's out of a dispatch. KNOWN COST: that the hook may edit data.items is what DeltaSync's code does today, not a documented contract (DeltaSync thread f3ec1986).
  • KNOWN COST: the bridge sends at most 10 messages a broadcast, sorted by key, and the list is the plain sets plus two entries per guild; with three or more sister guilds the keys sorting last may not be bridged (not measured against GreenWall's limit).
  • Specs: SY-65 in Tests/sync_sets_spec.lua (twelve examples), written red (run 20261017-124944.753: 13 failed, WireKey missing and the bridge off). Tests/sync_spec.lua's bridge example now asserts the bridge is on, and Tests/sync_hash_scan_spec.lua allows Sync:GuildId as a hash site, as it does Sync:Bucket, since it hashes an identifier and never content; both changes say why in the spec. Whole suite green, run 20261017-125844.563 (1588 passed, 0 failed, 3 pending); Sync.lua 687/687 lines.

Fixed: a sister guild's copy is dropped when the guild leaves the sister list (SY-67)

  • Once a guild left LibGuildRoster's sister list, its copy at sync.sisters[<key>] stayed in the saved data for good (self-audit, thread 6efe3151 item 4). Sync:PruneSisters now drops every copy whose guild GetSisterGuildKeys no longer names, case-insensitively, and releases any open pull of that guild's wire keys (OnItemCompleted), so DeltaSync's slot is not held to its timeout; Sync.asking entries now keep the provider's name as DeltaSync gave it for that. It runs on LibGuildRoster's OnSisterConfigChanged and at the start of Sync:Announce (so at login too), and does nothing while LibGuildRoster's home key is unresolved, when its list reads empty and every copy would go.
  • Spec SY-67 in Tests/sync_sets_spec.lua (five examples, through LibGuildRoster's real SetSisterGuildNames): red first (run 20261017-134100.170, 4 failed: the copy stayed, PruneSisters missing, nothing released), green in run 20261017-134129.123. Whole suite green, run 20261017-134508.434 (1598 passed, 0 failed, 3 pending); Sync.lua 706/706 lines.
  • Peer Review follow-ups (thread a93c588f). N1: PruneSisters also drops nothing while LibGuildRoster holds no list at all (GetSisterGuildsTs() == 0, its saved data lost or fresh), which read as an empty list and dropped every copy before gossip could bring the list back; an officer emptying the list leaves a stamp above 0 and still drops them. N2: UGR_GUILD_CHANGED releases every open pull's DeltaSync slot before it resets Sync.asking, through one release helper shared with PruneSisters. Three new SY-67 examples, red first (run 20261017-160019.106), and SY-51's 'merges nothing from the old guild's provider's leg 4' now expects the guild change's one release and still asserts the stale leg 4 completes nothing. P1: a mutant without string.lower on the listed keys turns the case example red (run 20261017-160127.998).
  • Peer Review S3 (thread 777f80a0): PruneSisters called GetSisterGuildsTs unguarded, so a LibGuildRoster too old to have it raised there. It now feature-detects the method, as Compat.CanWriteNotes does, and drops nothing without it: an unknown stamp must not read as an empty list. New SY-67 example, red first (run 20261017-172411.387, Sync.lua:390 calling a nil method), green in run 20261017-172727.611.

Changed: SY-66 runs on the harness's GreenWall model (WoWAPITesting pin 42bebe3)

  • Tests/wowapi moved from 6bfbb37 to 42bebe3, which adds env/greenwall.lua, GreenWall's own relay behaviour, built from UGR's request (WoWAPITesting thread 566f9c60). Tests/greenwall_relay.lua, UGR's stand-in, is deleted (in writ's trash) and SY-66 installs env.greenwall with Test Guild and Sister Guild, delivering with wow.pumpAddonMessages().
  • The model sends the hash list inside Sync:Announce, so SY-66 now counts GreenWall sends from before the announce. It counts a send only when GreenWall's ok says another guild was reached, and a red run names each refusal's reason and size. Red first in run 20261017-135418.726: the count started after the announce, so it saw no sends. Green in run 20261017-135521.314.
  • The model's four differences from the stand-in, checked against DeltaSync's bridge: a too-long refusal is split and retried (DeltaSync.lua _BridgeHashList, :2187); ok == false ("no-peers") counts as not sent (:2200); the sender's own echo and the home-guild copies are dropped (OnGreenWallMessage, if echo or guild, :2223); and the 10-second dedupe only drops an identical repeat. No finding for DeltaSync. Not verified in game.
  • Whole suite green, run 20261017-135923.536 (1598 passed, 0 failed, 3 pending); Sync.lua 706/706 lines.
  • Peer Review N1 and N2 (thread dc18be64): SY-66 counts only the reader's own relay sends (from), and now also asserts that nothing a refused send carried was lost, by decoding each bridge message with AceSerializer and checking every refused key went out in a send that reached. Not "nothing refused": that went red in run 20261017-163054.944 on six too-long refusals, which are DeltaSync's designed halving split. Green in run 20261017-163303.284.
  • Peer Review N1 (thread 777f80a0): that check could not see a key DeltaSync drops past maxMessages, because DeltaSync then never calls GreenWall, so the key is in no send at all. SY-66 now records every item key the reader's host hands _BridgeHashList during the announce (watching the real call) and reports as lost each one no send that reached carried. Green in run 20261017-171647.207; a mutant capping the reader's bridge at one message turns both SY-66 examples red with seven lost keys (run 20261017-171952.288).

Fixed: sister-guild sync follow-ups from Peer Review (thread 6efe3151)

  • G1: with 'Only send to ranks at or above mine' on, we offered a set to players we then refused to answer, so their negotiation hung on every broadcast. I had the rank check in OnDataRequest and not in the offer filter. Sync:Serves now holds IsSendPeer and OnDataRequest calls Serves alone, so the offer and the answer are one predicate. With the setting on, a sister guild's member is served nothing (their rank is not in our roster; SY-14: an unknown rank refuses). Red run 20261017-131258.988.
  • N3: a pull of a sister guild's copy now ends after one round. We give a copy's source nothing, so one round's leg 4 brings everything of theirs that is newer; asking again ran every round for nothing when our copy was the newer one. Red run 20261017-131412.564 (two asks).
  • N1: a comment at Sync:OnOfferReceived's filter says it covers only the hash protocol's two message types, so a switch to DeltaSync's numbered protocol must extend it. N2 is written into sync.md as a KNOWN COST: a sister guild's copy is never passed between home members.
  • SY-66 in Tests/sync_two_peers_spec.lua: two whole clients in two guilds that list each other as sister guilds, the hash list carried by Tests/greenwall_relay.lua (a GreenWallAPI v2 stand-in, until the harness models GreenWall). A sister member's join, leave and log line reach our sync.sisters copy with the author's canon; nothing reaches our sets, members, former members or config; both ways round. It passed on its first real run, since SY-65's code already existed; a mutant that stores a sister copy in our own sets turns it red (run 20261017-131207.882).
  • Self-audit, a closed leak: with the rank setting on, a pull from a lower-ranked guildmate used to hand them our groups in leg 3, because askLeg3 checked Sync:Serves, which then began with IsPeer and ignored rank. That is a send SY-14 forbids. Since G1, Serves holds the rank rule, so we give a lower-ranked guildmate nothing in leg 3 (Peer Review, thread f97de5af). I had not specced or noted that, and it brought N3's wasted rounds back for home peers. The one-round rule is now general: onLeg finishes after leg 4 whenever we do not serve the provider, which covers both cases. Spec in SY-65, red with three asks (run 20261017-132737.758), green with two (leg 1 and leg 3).
  • Whole suite green, run 20261017-133140.035 (1593 passed, 0 failed, 3 pending); Sync.lua 689/689 lines.

Fixed: no data set is taken from a sister guild; the GreenWall bridge is wired and off (SY-64, docs/design/sync.md "Sister guilds")

  • Sync:IsPeer already passed a sister guild's members (IsInAnyRoster), and the receive path took any set from any peer. So a sister guild's config (the approved list) could merge into ours and grant admin here, and since no members part names the guild it was observed in, a sister guild's leaver would become our former member through Members adopt and be relayed to our guild (Peer Review S2, thread 51800e63). New Sync.SHARED (empty) and Sync:IsSetPeer(setKey, name): a sister member is a peer for no set. OnDataReceived and OnDataRequest check it; Sync:Ask checks it and releases a refused offer with p2p:OnItemCompleted (DeltaSync frees the slot by item key alone, P2PSession.lua:904-922).
  • DeltaSync v4.6.0 / MINOR 26 (thread 3551240e) can send the hash broadcast to the co-guilds over GreenWall. Sync:Start passes greenwall = C.FOLDER (GreenWall's addon id must be the TOC name, not the namespace) only while Sync.SHARED names a set, so today it is off: a bridged list could only start offers that are always refused. Sharing waits on a sister guild's data having its own store.
  • Specs: SY-64 in Tests/sync_sets_spec.lua (four examples) and Tests/sync_spec.lua (one). First written with members and log shared and the bridge on (red run 20261017-120946.020, green run 20261017-121344.178); rewritten the same session, with the reason in the spec comment, once adopt was traced: red run 20261017-123136.813 (4 failed), whole suite green run 20261017-123506.960 (1576 passed, 0 failed, 3 pending).

Tests: harness pinned to WoWAPITesting 6bfbb37

  • The harness now installs C_GuildInfo.GuildControlGetRankFlags and guild.setRankFlags (UGR's request, inbox 44877338). Tests/member_history_sync_spec.lua's stand-in is gone; MD-44 sets rank flags through guild.setRankFlags and still removes the call for the older-client case. The only other entry crossed (around_each_file comment) asks nothing. Whole suite green on the new pin (run 20261017-122509.836, 1576 passed, 0 failed, 3 pending).

Added: member history syncs (writ todo #14, docs/design/sync.md "Member history syncs", MD-37 to MD-44)

  • Decision 11 for what the roster comparison records. New members-set parts of the "observation" class (Sync.ClassOf(setKey, groupId, partId) now takes the part id): any client may save them; a delete is still the admin gate. hist:join:<YYYYMMDD>, hist:left:<YYYYMMDD>, hist:rank:<YYYYMMDD>:<rankIndex> carry { name, date, kind, actor } (and rankIndex, rankName); info carries { name, guid, class, race, level, rankIndex, rankName, publicNote, seen }.
  • Sync:SavePart gains nopush (sixth argument): observations reach other clients by negotiation only.
  • Modules/Roster.lua: each join, rejoin, leave (part kind "left", "kicked" or "leftUnknown") and rank entry is shared through Members:ShareObserved (only when no part is held under that id). Members:ShareInfo runs after every comparison for each current member, on a leave (where seen counts) and in the live join, rank and level handlers. Entries go in through Members:Record, which replaces another client's copy of the same event instead of adding a second.
  • Modules/Members.lua applier: adds each received observation the record lacks as source = "sync", verified = false, in date order; never changes an entry this client recorded; the same family within one day is the same event ("joined" and "rejoined" count as one kind; a leave and a rejoin on consecutive days stay two entries). info fills a former member's fields and a current member's nil ones (type-checked). A group for nobody whose newest join or left part, officer's or observed, is a leave is adopted as a former member. applyDate no longer fills a peer's observation with an officer's date.
  • Tombstones: ClearJoinDate / ClearPromotionDate also tombstone the observed ids of that event within one day; the applier removes the entry a tombstoned id names unless it is verified or another live part of that event remains.
  • Backfill: Members:ShareHistory, once per guild after the first complete comparison (tracking.historyShared, a new AceDB default, no migration: migrations run in OnInitialize, before the guild key resolves), saves in one Sync:SaveParts batch a part for every dated entry of every record (sync-added and officer-corrected entries excluded) at its own setAt, and one info per record at its newest setAt.
  • Officer notes travel only to ranks that may view them (Peer Review P1, thread 81e1e4c4): a fifth set, onotes, group onote:<name lower-cased>, part note = { name, text }, saved with push off by a player who can view officer notes, only when changed. A player who may not view officer notes neither lists, offers, asks for, takes nor answers it (Sync.lua holdsSet); one who may answers and pushes it only to Sync:IsNotePeer: a send peer at or above the player's rank whose rank flags do not bar viewing (ns.Compat.RankCanViewOfficerNote, flag 11 of C_GuildInfo.GuildControlGetRankFlags). Sync.APPLIERS.onotes fills a former member's officerNote. sync.md's log section records that officer-note text in log lines must follow the same rule when log sync is built.
  • A v0.3.0 client (read at the tag, not run) stores and relays the new members-set parts without applying them, and never offers or takes onotes, which it does not list.
  • Refinements written into sync.md: a received "joined" is held once any "joined" is, a "seenAtRank" once a rank entry at that rank is (install-day first sightings would otherwise be extra joins).
  • Specs: Tests/member_history_sync_spec.lua (new), red first (runs 20261017-110518.019, 14 failed; 20261017-114820.140, 4 failed for the onotes set), then green (whole suite 20261017-115938.101, 1571 passed, 0 failed). Changed because the login comparison now writes the members set: bans_spec BN-01 and grm_import_spec IM-12 compare before/after; sync_protocol_spec, sync_sets_spec, sync_tab_spec empty the members set after login; sync_sets_spec's set list (SY-43) and applier list gain onotes. The harness has no GuildControlGetRankFlags; the spec stages a stand-in.
  • Harness pin moved dd620e5 to 549bbc9.
  • Not verified: in game; with a real v0.3.0 client; whether a non-officer reads other ranks' real flags; a re-announce between two online harness clients did not start a negotiation (offer arrived after the collect window), so MD-39 starts it as onSyncAccepted does.

Added: the macro tool, a Macro tab and its run dialog (todo #13, docs/design/macro-tool.md MC-07, MC-11 to MC-17)

  • Vishiswaz's request. Modules/MacroQueue.lua gains MACRO_BYTES (255, a chosen bound; the real macrotext limit is not measured), Scan(action, now) (reads MacroRules:Rules once per pass and queues each member a rule matches, never one at or above the player's rank or the player), MacroText(items), Peek(), Drop(item), ClearFinished() and the UGR_MACRO_QUEUE_CHANGED message. Peer Review Q1 (thread 18d2d5bc): Check now sends it when an item's status changes and StartBatch always does, so the tab repaints a landed action and an open run dialog re-lists.
  • APICompat.lua: Compat.GuildActMacro builds /gremove, /gpromote or /gdemote from the client's SLASH_GUILD_UNINVITE1 / PROMOTE1 / DEMOTE1 globals (names confirmed from the classic_era source by Peer Review; the enUS fallback spellings are not verified). GuildRemoveMacro now calls it.
  • GUI/MacroRun.lua (new): the run dialog, a session-owned form dialog holding a secure button. Its macro text is empty at rest. Out of combat preClick settles the queue, then peeks; if the list differs from the one shown it re-lists and runs nothing, otherwise it starts the batch and carries exactly the items StartBatch returned. In combat it does nothing. postClick empties the text again. Peer Review F1, M1-M3 and N1 on thread 18d2d5bc. No saved macro is created or edited.
  • GUI/Tabs/Macro.lua and GUI/MacroRuleDialog.lua (new): the tab (Remove / Promote / Demote chips, Scan, Rules, Run, Clear finished, the queue list with right-click Remove from queue) and the rule editor. Wired into TAB_ORDER between bans and sync, GUI/Tabs.lua, the guild window's top tabs, MainWindow:RefreshLocale, /ugr macro, the locale file and all five TOCs.
  • Spec assertions changed only because a tab was added: load_spec (TAB_ORDER), bans_tab_spec, window_spec (tab counts), guild_tab_spec (top-tab keys, Sync's slot), slash_spec (BUILT_IN), toc_spec (EXPECTED_FILES).
  • Not verified in a client: that one press runs several guild commands, that a PreClick attribute reaches the same click, the 255-byte bound, the tab icon, and that the combat-start hide lands before lockdown.
  • Tests/alts_tab_spec.lua: its spec ids are now AL-35 to AL-37, because docs/design/alts.md used AL-17 to AL-19 twice (Peer Review D3, thread d090e16c). Names only, no assertion changed.

Fixed: one click of the macro run button could carry out a batch the dialog never showed (Peer Review X1, X2, thread 18d2d5bc)

  • X1: W:CreateSecureActionButton registers its button for the down and the up (LibAceGUIWidgets-1.0.lua:3794), so one physical click can run PreClick and PostClick twice, and the secure handler acts on one half only (classic_era SecureTemplates.lua:805-808). The down started the listed batch, the down's PostClick emptied the text and re-listed the NEXT batch, and the up's PreClick started that next batch, which the up's click then ran. For a remove batch that removes members the officer was never shown. Whether the down reaches PreClick in a client is not verified; the fix is right either way. GUI/MacroRun.lua: the down's preClick starts the batch, the down's postClick does nothing, the up's preClick keeps the batch (MacroRun.downPressed), and only the up's postClick empties the text and re-arms. An up with no down before it is still a whole click. A down whose up never comes leaves its batch for the next press's Settle; the new MacroRun:Rest() empties the button on Open and at combat start, and the queue's change message does not re-arm between a down and its up.
  • The Roster tab's kick dialog had the matching flaw: its postClick hid the dialog on the down, so a mouse click (secure action on the up) put the dialog away before Remove ran (not verified in a client). It now hides on the up only (GUI/Tabs/Roster.lua KickDialog).
  • X2: MacroQueue:Settle sends UGR_MACRO_QUEUE_CHANGED when it marks an item failed, so the Macro tab stops showing it as waiting.
  • Specs, red first (run 20261017-094853.692, four failures for the reasons above), then green (run 20261017-095056.933, 84 passed): two new MC-15 examples drive the down and the up (Tests/macro_tab_spec.lua), one Settle example in Tests/macro_queue_spec.lua, and the kick example in Tests/roster_menu_spec.lua now clicks the down (dialog stays, Remove armed) before the up. GUI/MacroRun.lua, Modules/MacroQueue.lua and GUI/Tabs/Roster.lua are at 100% line coverage (run 20261017-095427.649). docs/design/macro-tool.md gains "One click, one batch".
  • Follow-ups from Peer Review on thread 8edab7cf: the combat-start comment in GUI/MacroRun.lua now cites warcraft.wiki.gg API_InCombatLockdown (lockdown begins after PLAYER_REGEN_DISABLED fires, so Rest()'s SetAction applies at once; not measured in a client) (F1). The abandoned-down example in Tests/macro_tab_spec.lua asserts behaviour instead of the internal downPressed field: after combat starts, a lone up presses (one Settle) (F3; green run 20261017-100819.548). An up-only option for W:CreateSecureActionButton was requested from LibAceGUIWidgets (inbox d761a47e), so the two hand-written workarounds can go when it ships (F2).
  • LibAceGUIWidgets v0.6.0 (MINOR 44) shipped it: the macro run button and the kick dialog's Remove button now pass clicks = "up", one PreClick/PostClick pair per click. The down/up handling stays for a player on an older copy, which ignores the option. New assertions on the registration ({ "AnyUp" }) in both specs, red first (run 20261017-173125.025).
  • Peer Review, thread 18d2d5bc: on an older library, a down whose up never came left its batch on the button, and a later up with no down (a press begun off the button and let go on it) skipped the press and ran that text even after the queue had changed, so an item the officer had since taken out could still be carried out. The queue's change message now forgets a pending down and empties the button (MacroRun:Rest()) before it re-arms, so that up is a whole click over the list then shown. New MC-15 example, red first (run 20261017-173640.989: the abandoned remove stayed on the button), green in run 20261017-173941.958.
  • Changed the same day (spec changed with its reason in the MC-15 comment): that up no longer presses at all. The secure handler acts on the up for a mouse click but on the DOWN for a key-binding click with ActionButtonUseKeyDown on (classic_era SecureTemplates.lua:805-808); there the down had already run its batch, and the up's press started the next batch and recorded it sent, ranks and all, with nothing to run it. A queue change now marks the click abandoned (MacroRun.abandoned), and its up empties the button and does nothing; the officer clicks again over the new list. Neither client retries the down's batch or records any rank twice. Red first (run 20261017-180227.782), green in run 20261017-182343.812.

Added: "Allow all ranks" in the Lowest officer rank list (Discord thread 56775f07)

  • Vishiswaz: "sometimes guild masters add new ranks. This way they would not have to change this setting." sync.officerRank may now be saved as "all" (Admin.ALL_RANKS), read by Admin:OfficerRank() as the guild's lowest rank at that moment, so a rank added later is inside the gate; Admin:AllRanks() is new. A saved rank index keeps meaning that rank, so no migration. The guild info lock still wins (Settings:Set refuses, the dropdown shows the lock's rank greyed). Every reader goes through OfficerRank(), so the ban list, approved list, macro tab and sync follow. The setting is per client and does not sync. v0.3.0 reads "all" as rank 1 (fails closed).
  • Modules/Admin.lua, Modules/Settings.lua (accepts "all" before the ranks load), GUI/SettingsPanel.lua (the entry, sorted last), Locale/enUS.lua ("Allow all ranks"), docs/design/settings.md. Specs red first (run 20261017-095944.363, 11 failures), then green (run 20261017-100048.984, 89 passed): a new "Allow all ranks" block in Tests/admin_gate_spec.lua, two ST-05 examples in Tests/settings_spec.lua, and the ST-12 values example in Tests/settings_panel_spec.lua now expects the entry last (that is the change asked for).
  • The guild info block carries it too (writ todo te506e75f786fdf73): with Allow all ranks in force Sync:GuildInfoBlock() gives UGR{o=all} instead of today's lowest rank number, which would have put a rank added after the paste outside the lock. Sync.ParseGuildInfo reads o=all (lower case, the o letter only) as Admin.ALL_RANKS; Sync:GuildLock() turns it into the guild's lowest rank as it is read; the new Sync:GuildLocksAll() says the lock is all, so Admin:AllRanks() is true under it and the dropdown shows "Allow all ranks" greyed. Sync:CheckGuildInfo() counts o=2 becoming o=all as a change even when rank 2 is already the lowest. v0.3.0 reads o=all as no o and falls back to its own setting (fails to the local setting, never open). The admin_gate_spec example that pinned the old UGR{o=2} as a known cost now expects UGR{o=all} (that is the change). Specs red first (run 20261017-102925.862, 4 failures), then green (run 20261017-103027.479, 87 passed); the CheckGuildInfo example was added after the code and was not seen red.
  • KNOWN COST: with it on, every rank sees and holds the ban list, may change the approved list, and may do every admin action.

Changed: a chat line that already opens with the sender's nickname no longer shows it twice (ALT-09b, Discord thread c59e3510)

  • Vishiswaz: "Can we do anything to prevent seeing the same name twice in chat for someone using both add-ons?" TOG Tools' name prefix writes the sender's nickname into the message (TOGTools/Modules/NamePrefix/NamePrefix.lua:157), so a UGR reader saw it in the message and beside the name. Nicknames.SenderNameFilter and the fallback ChatFilter now pass the message to ChatTag(name, msg), and the tag leaves the nickname out (keeping the main) when the message, after spaces, opens with one bracket and that nickname in any case, the next character not a letter or digit, or with the nickname directly followed by a colon. A bare leading word keeps the tag (Peer Review F3, thread 87e0b05f: a nickname such as "Ok" vanished from "ok, pulling"; red run 20261017-101543.195, green run 20261017-101556.927). The message is never edited. A secret message is not read (Compat.CanRead, new, over the client's canaccessvalue). Not on a whisper the player sent (CHAT_MSG_WHISPER_INFORM), where the message is the player's own and starting it with the recipient's nickname is addressing them (found in this session's self-audit; red run 20261017-101252.442, green run 20261017-101313.310).
  • Spec CT-07 in Tests/chat_tags_spec.lua, red first (run 20261017-100921.341: the nickname was still drawn), then green with the nickname specs (run 20261017-101046.752, 35 passed). docs/design/alts.md gains the rule.

Removed: the docked-window fallback on the guild window's Members tab (MD-33; Peer Review N1, thread 0514da61)

  • No released LibGuildRoster could reach it: the click handler and the panes both arrive in MINOR 34 (GuildRoster 1.3.0), so GuildTab:Enable returned before the fallback on any library without panes. Removed: GuildTab:AddButton, Label, the buttons table, the docked case of PanelPage, MainWindow's dock helper, dockedTo and CloseDocked, the page argument of MainWindow:Open, and the locale key "Open Ultimate Guild Roster beside the guild window.". On a library with the click handler but no panes UGR registers its click, menu and tooltip and puts nothing on the page.
  • Tests/guild_tab_spec.lua: the MD-33 block now pins that (no frame added to the page, the window opens free-standing). MD-34's check that no button went on the page counts the page's children instead, since the buttons table is gone. The MD-33 and MD-35 "library without panes" setups now unregister the pane path's OnGuildTabPageShown handler, because the new Enable returns early and no longer overwrites it; in game Enable runs once against one library. Red run 20261017-094752.969 (two failures, the old button), green run 20261017-095137.097 (185 passed).
  • docs/design/member-details.md: MD-33 describes the removal, and the pane paragraph no longer says the pane hosts a TabGroup.
  • The Rekey spec, also numbered MD-33 (0.3.0's entry calls it that), is now MD-36 in member-details.md, Tests/members_spec.lua and the Members:Restore comment. Name only.

Added: alt links sync again (todo t9d0544d16dc3fd6a, Discord thread 01e41d4e, docs/design/alts.md AL-20 to AL-34)

  • Operator: "you're telling the customer we aren't syncing log or alt data. that's wrong, we need to sync that data." v0.3.0 stopped syncing alt groups when they moved to LibGuildRoster's alt store; groups still come from the library, and UGR now syncs its fed copy as config/alt parts (admin class, Sync.lua ClassOf) and feeds received groups back into the library.
  • Modules/Alts.lua: Alts.KeyFor; a view state (read-time checks, newest canon wins per character, viewOf, settle, which retires a key only where the library still credits it to UGR); commit saves views, never raw parts; edit plans carry old; HeldAt, Apply (through Sync:SaveParts), FeedAll; the applier OnSynced registered as APPLIERS["config/alt"].
  • Migrations.lua: migration 7 "alt links sync" moves the saved copies into parts; a copy filed under a character other than its main is parked in parkedAltGroups, since it would fail the read-time check. Constants.lua: SCHEMA_VERSION = 7 and ALT_GROUP_MAX = 75, shared by Alts.MAX and the migration. SavedVariables.lua: the altGroups default is gone.
  • Found while turning green: the applier walked ipairs({before, now}), which stops at a nil before, so a newly arrived group never re-checked the groups it overlapped. Fixed.
  • KNOWN COST: two officers who link different characters to the same owner at the same moment write one part, and the older link is lost (same owner, same id). An import's knock-on shrink or tombstone of another group pushes, through SavePart/DeletePart.
  • Specs: new Tests/alts_sync_spec.lua (AL-20 to AL-30, AL-34), AL-31 and AL-32 in sync_two_peers_spec.lua (AL-32's v0.3.0 client is modelled as this build without the applier), AL-33 in migrations_spec.lua. Changed, each with its reason in a comment: alts_spec, alts_auto_spec, grm_import_spec (IM-13) read the config part; migrations_spec's version-6 examples run the list to 6; sync_sets_spec's applier key list gains config/alt; nicknames_spec and nickname_display_spec receive groups as pushed parts, since the admin gate refuses Apply at rank 2. Red run 20261017-095950.848 (25 failed), whole suite green with 100% on every touched file, run 20261017-102516.849 (1550 passed, 0 failed, 3 pending). Not verified in game, against a real v0.3.0 client, or for list order over the real DeltaSync wire.

Docs: alt-link sync design, Peer Review D4 and D5 (thread d090e16c)

  • docs/design/alts.md: a view's owner when it wins no main is its first won character in the stored part's members order, the same on every client because receivers store the part verbatim (D4; that the wire keeps a list's order is not verified, and AL-26 now asserts the key on a received group). The unlink KNOWN COST now covers any save: a re-save restamps its group as newest, so a newer group its author has not received loses the shared character (D5).

Docs: the 0.3.0 entry and page corrected after release

  • Peer Review G1 (thread ec30d4c5): the CurseForge page's rename lines (Bans tab, and the two under 0.3.0) now say "with GuildRoster 1.3.0", since rename detection needs the GUID that library version supplies. The 0.3.0 section below gets the same note. The same condition is added where the page promised the Write alt notes button (it writes through Members:SetGameNote, which refuses without a GUID, Modules/Members.lua:544), renames recorded while offline, and opening from the guild window. Discord got a correction post for the release announcement.
  • Peer Review F2 and F2-B (thread 0514da61): Modules/Alts.lua's return nil after the owner-key loop. GuildRoster 1.3.0's CanonName trimmed before stripping a stray colon, so "E :" was filed under owner key "E " and the real library reached that line; the alts_spec example briefly drove the real library with that name (run 20261017-083511.780). GuildRoster then fixed CanonName in its working tree as 1.3.1 / MINOR 35 (GuildRoster thread f1827f75), and the suite loads that sibling file with no pin, so the example went red (run 20261017-084342.988, alts_spec.lua:108). It is now a defensive pin again: it replaces GetAltGroup on the real library object for one name and depends on no colon handling (run 20261017-084451.780, 63 passed). The line stays, because a library that reports a group with no matching owner key must read as no group.
  • Docs: docs/design/sync.md no longer exempts zone, online status and last online from syncing (Peer Review H6, thread 715d94a8); decision 11 says all data UGR uses syncs, and the cost of syncing presence is recorded as a KNOWN COST for the design to bound. What is built: last online syncs as the seen day in a member's info part; zone and online status are planned and not built, and every passage of sync.md now says that rather than that they do not sync. docs/design/sync-redesign.md's "Read from the game, never synced" paragraph is marked superseded in place. Its "From LibGuildRoster" line now names only sister guilds' presence, and docs/design/alts.md marks its two "not synced" lines as v0.3.0, superseded by decision 11 (Peer Review A6 and N1, thread 47377ab4).
  • Spec change, with its reason (Peer Review A1, thread 47377ab4): alts_spec's "registers no sync applier: alt groups never sync" pinned the behaviour the operator rejected (decision 11: alt links sync). It is now a pending stating the wanted behaviour, until todo t9d0544d16dc3fd6a designs the record shape; the file header says the same.
  • Comments: GUI/GuildTab.lua's header names the MD-33 removal todo; GUI/ExportWindow.lua's set field points at ExportWindow.SETS, which also holds deaths.
  • Whole suite and coverage on the v0.3.0 tree, before the alts_spec change: 1468 passed, 0 failed, 3 pending; 7052/7052 lines over all 39 shipped files (coverage run 20261017-083350.400). After the change, a coverage run that took the whole suite: 1468 passed, 0 failed, 3 pending, with Modules/Alts.lua 448/448 (run 20261017-083909.366); the other files' figures from that run were not read.
  • Self-audit F1 (thread ec30d4c5): the 0.3.0 section still said the sync was "being built", that SY-41 to SY-62 were "next", and that the alts port had no whole-suite run. Those lines now say what shipped, and the "log syncs" design bullet says the log set has no writer in 0.3.0. The v0.3.0 GitHub release body was published from the old text and is not changed by this.

Event log clear modes, the import dialog's prompt, race sync trace

  • Removed: EventLog's range and all clear modes and EventLog:Clearable (Peer Review F1, thread ebe08e19). Nothing outside Tests called them; the Log tab clears only the rows it shows, so Clear now takes "filtered" with rows and raises on any other mode. The A4 range-error spec went with the mode. Specs that cleared everything with Clear("all") (admin_gate, export_window, log_tab) now pass every entry as rows. New spec: the module exposes no Clearable, and "range", "all" and "most" raise; red first (run 20261017-085212.223). docs/design/event-log.md updated.
  • The A2 snapshot clear (only the entries counted when the question opened are removed) is now proven: a mutant that cleared every entry at OK turned both A2 specs red (run 20261017-085346.791), green after restoring (run 20261017-085422.537).
  • New spec: in combat, one Escape on the range dialog's confirmation leaves the range dialog and the window up. It was green on its first run against the sibling LibAceGUIWidgets working tree, so it was never seen red.
  • The GRM import dialog now opens through ns.UI.Prompt (Peer Review F3/F4, thread ebe08e19), so a guild change closes it like every other UGR question. It is marked free: closing the window or the Members-tab pane leaves it up, as U5 / MD-34 decided (thread d0f49e1b), and ns.UI.ClosePrompt(all) closes a free prompt only when all is true (GUI/UI.lua's own guild-change handler passes it; it sat in the Log tab's handler until Peer Review N1, thread ebe08e19, so a prompt the Log tab never opened no longer depends on that tab). Four specs in grm_import_spec, red first (run 20261017-085600.692), including one UGR prompt replacing another (the old one's hide runs once, the new one is tracked). A first-time import offer closed by a guild change counts as "Not now" for that guild (docs/design/grm-import.md).
  • Docs: docs/design/roster-tracking.md says a member record's race does not travel over sync (only date, name, rank name and note parts do), and that Refresh would correct a non-token at the next comparison if one ever arrived (Peer Review, thread b65fa228). The Refresh race fix, its RT-44 spec and the locale spec's exemptions from ns.Migrations.list were already in the code. RT-44 is now proven: with the guard put back to r.race == nil it went red on the stored "Night Elf" (run 20261017-091101.085), and green again once restored (run 20261017-091129.178).
  • Whole suite under coverage: 1470 passed, 0 failed, 4 pending; GUI/UI.lua, GUI/Tabs/Log.lua, GUI/Tabs/Bans.lua, GUI/Tabs/Sync.lua, GUI/MainWindow.lua, GUI/ImportDialog.lua, Modules/EventLog.lua and APICompat.lua at 100% (run 20261017-090705.451).

[0.3.0] - 2026-10-16

Removed: the minimap button

The operator, 2026-10-16: "we need to remove the Minimap Button (for now), as our main UI will be through the guild&communites UI". The way in is now the guild window's Members tab (GUI/GuildTab.lua), the key binding and /ugr.

  • Core.lua registers no LibDataBroker launcher and no LibDBIcon button; ns.dataBroker, ns.SetMinimapShown, Constants.LDB_NAME and Constants.ICON are gone, and so are the settings page's "Show minimap button" switch and its four locale strings.
  • profile.minimap is no longer a default. No migration deletes a saved one, so a player's hide choice is still there if the button comes back (savedvariables_spec pins that).
  • LibDBIcon-1.0 is no longer a dependency of any TOC or of .pkgmeta, and the Forever TOC no longer embeds Libs/LibDataBroker-1.1 and Libs/LibDBIcon-1.0; the Libs folder is deleted. The test env drops the LibDBIcon library row and the eviction of the two embedded libraries.
  • Specs changed first, with this reason: toc_spec (no LibDBIcon dependency or embed on any client), window_spec (no launcher, no show/hide call, no saved switch), load_spec, savedvariables_spec and settings_panel_spec. They ran red (run 20261016-222222.700), then green (run 20261016-222432.720, 175 passed). Core.lua, Constants.lua, SavedVariables.lua and GUI/SettingsPanel.lua are at 100% line coverage (run 20261016-224204.718).
  • Two alts_spec examples cover the last two lines of Modules/Alts.lua: a merge with the newer group's character named first keeps the older group, and a group the library reports under a name with no owner key for it reads as no group. Both pin today's behaviour, so neither ran red.
  • Peer Review (thread 0514da61): F1, window_spec's two library lookups sat behind if ldb then / if icon then, which no longer hold, so they never ran; they now assert both libraries are absent. N2, comments and spec titles that still named the minimap (GUI/MainWindow.lua, Locale/enUS.lua, locale_strings_spec, guild_tab_spec) and docs/FEATURES.md SET-01 / UI-03, docs/design/settings.md ST-02 and docs/design/member-details.md are corrected. N1: the MD-33 button path in GUI/GuildTab.lua is reached by no released LibGuildRoster (the click handler and panes both arrive in 1.3.0); its comment says so, and removing it with its specs is a todo.

Changed: the sync is rewritten on data sets (docs/design/sync.md)

The first build gave every field of every member its own DeltaSync item on numbered P2P, so the key count grew as members times fields, and several workarounds (the equal-time tie-break stopgap, the handshake wrappers, three MINOR 25 asks of DeltaSync) followed from that one choice (docs/design/sync-redesign.md, "What was wrong"). The operator's design of 2026-10-16 replaces it:

  • Four data sets (members, log, config, bans), each one DeltaSync item with one master canon.
  • Part canons stamped once at save as <DTS><hash>, the hash over the content alone; group, bucket and set canons derived from stored canons (Sync:Derive), as TOGBank's request log does.
  • Union with tombstones for every set; the greater canon wins one id.
  • Buckets by a hash of the group id: 32 to start, doubling past an average of 100, never shrinking.
  • DeltaSync's hash P2P, with a four-leg exchange that runs both ways in one negotiation and stops after 3 rounds.
  • A line relayed by any guild or sister-guild member is taken; the admin gate applies at save.
  • The log syncs, trued up from the game's guild event log, with duplicates removed by a content key and tombstones rather than a time window. Not built in 0.3.0: the log set has no writer, so the event log stays local to each player (below).
  • Alt groups move to LibGuildRoster.

Specs SY-41 to SY-63 were written red first (below; SY-54 is pending). The 3-round limit and the bucket numbers are Claude's proposals. Two choices the operator has not made are banked: any client may save an observed log line, and the log's Clear becomes a guild-wide delete (bank :4804). One DeltaSync host for all four sets instead of a separate ban host (bank :4805).

A Sizes section, from the real saved data (1,029 members, 84 log lines in two days) and the wire encoding: legs 1 and 2 stay small, but DeltaSync's delivery watchdog bounds a whole negotiation at 180 seconds from the accept (P2PSession.lua:595), about 120 KB, and a GRM-imported roster is about 226 KB. So no leg is one send of more than Sync.LEG_BYTES (32 KB), batches merge as they arrive, and a bucket the asker holds nothing in is answered with its groups (SY-63). Claude's proposals, bank :4843. The game event log's cap is dropped from Not verified at the operator's word.

Tests/sync_sets_spec.lua (SY-41 to SY-48) is written, red: part canons, derived canons, the empty set, merging and its convergence both ways, buckets, receiving from any peer, and who may save. Its header pins what the design left open: a group with no parts and no tombstones is not stored (so two clients holding the same data hold the same groups), Merge returns the changed group ids sorted, and AUTHORS is keyed by the part's kind and called with the player, the group id and the content.

The core is built and every store is moved onto it (the last bullets below). The saved data moves to the new format on load, and none of it has been tried in game.

  • Sync.lua is rewritten in place on the data sets: Set, Stamp, Derive, Canon, Merge, Bucket, SavePart, SaveParts, DeletePart, DeleteGroup, IsPeer, IsSendPeer, Push, MyHashes, Announce, OnOfferReceived, Held, and CheckBanAccess in place of CheckBanHost. The saved sync default is { sets = {}, records = {} }.
  • Merge finds a set's applier under APPLIERS["<set>/<group prefix>"] before APPLIERS[<set>], because config holds both admin: and rules: groups.
  • Modules/Admin.lua's approved list is on the config set as admin:<name> groups, and Tests/admin_gate_spec.lua is rewritten to it. Before this, OnEnable stopped with an error for every player below the officer rank, because the approved list still called the removed record API (Peer Review P1, thread e2e12b6e).
  • Admin:Approve refuses a bare name until the player's realm resolves ("Your realm has not loaded yet. Try again in a moment."), except on WoW Forever, where names are regional. Before the realm resolves LibGuildRoster leaves a bare name bare, so the entry was filed as admin:<name>. Once the realm resolved, KeyFor gave admin:<name>-<realm>, so the entry could not be listed or removed and it synced forever (Peer Review L2, thread e2e12b6e; admin_gate_spec, red first). The comment on canon now says that NormalizeName appends the connected-realm name (Peer Review L1). Approve("Unknown") says "Enter a name." rather than the realm message, since LibGuildRoster returns "Unknown" bare whatever the realm (Peer Review F4, thread 33dfc47f; red first).
  • Re-saving a part whose content is unchanged no longer stamps it again or pushes it to the guild (Peer Review N1). A whole-group delete over a group that already had a part tombstone now leaves two clients with the same stored data (Peer Review N3). Both fixes were made before their specs, which is the wrong order for this repo; the specs were added after and pass.
  • The four legs, the 3-round limit, the ban gate on legs, live push and batching are built: Sync:Ask, OnDataRequest answering legs 1 and 3, OnDataReceived taking legs 2 and 4 and live pushes, onSyncAccepted and a gated hasContent in InitP2P. Tests/sync_protocol_spec.lua (SY-49 to SY-53, SY-63) passes 28 of 28 with the examples below (run 20261016-143415.069).
  • Leg 2 also carries diff, the buckets that differ, beyond sync.md's first table: a bucket the provider holds nothing in otherwise never told the asker to give its group there. sync.md's table and a spec example now pin it. That example passed on its first run because the code came first.
  • Fixed (Peer Review C1, thread 95c2d3f6): a negotiation moved a differing group only from the side with the greater group canon. A group canon is its newest DTS plus a hash, so when two officers edited different parts of one member, the newer part on the lesser side never synced, and the pair re-ran three rounds on every broadcast. A differing group is now both wanted and given. The new SY-50 example (each side holding one newer part, both canon orders) ran red first. The leg-3 and leg-4 examples were changed to match: they had pinned the one-way rule.
  • Fixed (Peer Review F6): leg 3's want list and every leg's tombs rode whole on the last message, so a leg 3 wanting 1,000 groups was one message of about 45 KB. They are now split across the numbered messages like the groups, and the provider collects want until the last one. The new SY-63 example ran red first.
  • Peer Review F7: a new SY-52 example pins that a provider who fails the admin gate answers no bans leg. The code already did this, so the example passed on its first run.
  • Every store is moved onto the data sets. Modules/Members.lua saves join: and promo: parts (verified dates only) and cnote parts of a member's group; left: parts come only from the GRM import; nicknames are a nick part in the same members set; Modules/Bans.lua saves to the bans set and the separate ban lane is gone (bans ride the one host, gated by CheckBanAccess); the macro tool's rules are config groups rules:<action> and the admin gate's approved list is config groups admin:<name>; the GRM import saves its whole batch with one SaveParts; Guild.lua's adoptPending no longer calls the removed Receive (Peer Review F1, thread e2e12b6e); the Sync tab reads Held() and the in-step count; migration 5 (SY-62) moves each guild's saved records and bans into the members, config and bans sets, drops alt records and removed or dateless join/promo records, parks what it cannot file (an old log: record among them) in sync.parked, and deletes sync.numbers*, banSync, sync.records and the old bans table. The log set has no shipped writer yet (the event log is not synced), so it is always empty. Alt groups no longer sync. The transitional Sync.KEYS and CheckBanHost shims are removed. Tests/sync_records_spec.lua is deleted and the older sync specs are rewritten or retired, each with its reason in the spec header. SY-54 (two clients catching up with each other) is pending.
  • Fixed (NK-03): deleting a part or a group in the same second as its newest save could leave the part alive, so a nickname set and cleared twice in one second came back. A delete is now stamped no earlier than the newest part it removes (deleteDts). The new sync_sets_spec SY-41 example ran red first.
  • Fixed (Peer Review N1, thread bf8341d7): a provider's leg 2 carrying a tombstone that is not a time raised a Lua error inside the comm handler. Such tombstones are now dropped where leg 2 is read and before a whole group's canon is derived. Two sync_protocol_spec examples ran red first.
  • Fixed (Peer Review P1 and F3, threads 95c2d3f6 and bf8341d7): a batched leg was answered when the message numbered last arrived, so parts arriving out of order lost the parts after it, and a repeated first part reset what had been collected. A leg is now answered once every part has arrived, in any order, and a repeated part counts once (allIn). The new SY-63 example ran red first.
  • Peer Review F10: a mutation run (run 20261016-151035.317) removed the provider's admin gate and leg 2's diff in turn, and the SY-52 and SY-50 diff examples each went red, so both examples catch the bug they were written for. Two SY-53 examples (the whisper path of a push, an unknown message type ignored) were added for coverage; they passed on their first run.
  • Whole suite after the store move: 1424 passed, 9 failed, 3 pending (run 20261016-154831.920); the 9 are the guild-window pane specs MD-34 and MD-35, whose code is not written yet. Sync.lua and Modules/Admin.lua are at 100% line coverage. Nothing has been tried in game.
  • Fixed (Peer Review F1 and R1, thread bf319dc5): the provider collected a leg 3's parts per round, so a half-finished leg 3 from the same asker could complete a later negotiation early with a partial want list, and when the part count differed the old leg's wants were answered too. Sync:Ask now gives each negotiation an id, nid (a number drawn once per session, a dot, and a per-session counter; no timer and no clock, and the random part keeps ids from before a /reload from matching), every leg-3 part carries it, and the provider collects by nid. A leg 3 with a missing or malformed nid (Sync.NID_MAX 32 characters) is ignored. Three new SY-63 examples ran red first (run 20261016-155912.899): answering before the second negotiation's part 1, answering 4 wants instead of 1, and answering a leg 3 with no nid. sync.md's leg-3 row shows the field. A client without nid gets no leg-3 answer; the hash protocol has not shipped, so no released client sends one (not verified against a released build).
  • Fixed (thread 00173eb7 F4, the asker's twin of F1): the asker matched legs 2 and 4 by round only, so a part from an earlier negotiation at the same round could count toward the next. Leg 1 now carries nid too, the provider echoes it on every leg-2 and leg-4 part, and onLeg drops a part whose nid is not the open negotiation's. A leg 1, 2 or 4 with a missing or malformed nid is ignored, so a client without nid gets no leg-2 answer either. sync_protocol_spec pins it: the echo example, the stale leg-2 and leg-4 part examples, and the missing/malformed/foreign nid example. nid's draw is math.random, which Blizzard's own UI calls in all five clients' source trees (Blizzard_SharedXMLBase/TableUtil.lua:518; :479 in the Forever tree).
  • Fixed (Peer Review G1): a guild change left open negotiations in place, so a leg 4 from the old guild's provider merged into the new guild's set. UGR_GUILD_CHANGED now clears Sync.asking and Sync.answering with Sync.advertised. Two SY-51 examples ran red first (run 20261016-160543.442).
  • Peer Review R2: no change. Only the settings page's "Approve a player" box calls WhyNotApprove and Approve (GUI/SettingsPanel.lua:103-104), with what the officer types, so the game's localized "Unknown" name cannot reach it from a unit or a roster row.
  • Sync.lua 585/585, Modules/Admin.lua 100/100 (coverage run 20261016-161249.669, whole suite 1444 passed, 0 failed, 3 pending).
  • Peer Review F6 (thread 00173eb7): /ugr bans asked ns.Bans:CanSee() while the tab strip and the guild window's icon tabs ask MainWindow.Offered, so the two could disagree if the rule changed. The slash command now asks MainWindow.Offered("bans") (Slash.lua:84-87). The new slash_spec example ran red first (run 20261016-162839.587) and green with bans_spec and bans_tab_spec (run 20261016-164522.551). Peer Review S1: bans_spec's BN-11 is now titled for sync's wire, since the ban host is retired.
  • Fixed: Members:Quietly turned the members set's applier off, ran its function, then turned it on, so a function that raised left the applier off and every members part received for the rest of the session was dropped. The switch is now a count, released through pcall and the error raised again with error(a, 0) (Modules/Members.lua:181-200), so a nested call cannot turn it back on early either. Two members_spec MD-19 examples ran red first (run 20261016-164338.861) and green (run 20261016-164818.965). The re-raised error keeps its message but not the original stack. The comment on Members:Rekey now says the members-set parts stay under the old name on a rename, a gap filed as a todo.
  • Peer Review (thread e2e12b6e): the sync_sets_spec example on applier keys asserted only n >= 4, so a module that forgot to register would sync nothing and stay green. It now asserts the exact sorted list { "bans", "config/admin", "config/rules", "members" } (Modules/Bans.lua, Modules/Admin.lua, Modules/MacroRules.lua, Modules/Members.lua). Red first with Admin's registration removed (run 20261016-164533.590), green restored (run 20261016-164709.459). The log set has no applier, as above. docs/design/sync.md now gives Sync:SaveParts(batch) (it said (setKey, batch)) with the batch's { set, group, part, content, dts } entries, names the "<set>/<prefix>" applier key, and says the asker drops a leg-2 or leg-4 part whose nid is not the open negotiation's.
  • Fixed (todo #12): a rename or realm transfer moved the member's record but left their members-set group (join, promotion and leave dates, custom note, nickname) under the old name, so parts received later for the old name found no record and later edits split across two groups. Members:Rekey now re-files the group locally (refile, Modules/Members.lua:798-811): each live part is copied to the new name's group with the new name, stamped by Sync:Stamp at the old part's own DTS with its by kept, and the part and group tombstones carry over, straight into Sync:Merge. Every client sees the rename in the roster and derives the same copies, so nothing is saved through the admin gate and nothing is pushed. The old group is kept, because a delete would be stamped at each client's own clock. The decision is in docs/design/member-details.md (MD-33) and docs/design/sync.md. Four members_spec MD-33 examples ran red first, including two clients ending with equal members-set canons. Members:Restore (a former member rejoining under a new name) re-files the same way; its MD-33 example ran red first (run 20261016-175512.703) and green with roster_tracking_spec (run 20261016-175550.324). A rejoin under a new name now tells the alt and ban lists too (the entry below).
  • Fixed (self-audit thread c04b4fd1 F2, ROSTER-09): a former member who rejoined under a new name kept their alt group and their ban under the old name, so the rejoin hooks (Alts:OnMemberRejoined, Bans:OnBannedPresent) looked under the new name and found neither. Members:Restore now tells Alts:OnMemberRenamed and Bans:OnMemberRenamed when the name changed, before APPLY.rejoin (Modules/Roster.lua:388-398) runs those hooks. Both rename paths go through one local function, tellRenamed (Modules/Members.lua:91-96), called from Rekey and Restore; the fix had first been written into APPLY.rejoin as a second copy, and was merged there. ROSTER-09's two roster_tracking_spec examples ran red first. Whole suite 1468 passed, 0 failed, 3 pending (run 20261016-224930.539, with Modules/Alts.lua at 448/448); Modules/Members.lua 456/456 and Modules/Roster.lua 447/447 (run 20261016-224204.718).
  • Fixed (Peer Review R1, thread 306a2e93): a banned player who left and came back under a new name was not flagged as banned. Roster:Diff found the former member by GUID but asked the ban list about the new name only. It now asks about the former name too (Modules/Roster.lua:222-224). RT-16 in roster_tracking_spec was the red spec (failing at :316 in run 20261016-183446.265) and is green in run 20261016-184441.765.
  • Every rename path above needs the member's GUID: a rename is matched by GUID (Modules/Roster.lua:313) and a rejoin with no GUID by the same name and class (:203). LibGuildRoster reads the GUID only from 1.3.0 (MINOR 34, per Peer Review G1, thread ec30d4c5), so with GuildRoster 1.2.1 a member back under a new name arrives as a new member, unbanned and without their alts or dates. The CurseForge page says "with GuildRoster 1.3.0" on those lines.
  • Fixed (self-audit thread c04b4fd1 F1): after a renamed rejoin the old name's group is kept, and its newest dated part is a leave. A client that never saw the rename (a fresh install's first sync, or a late part saved under the old name) found no record for that group, so the applier made a duplicate former member under the old name, and the late part never reached the renamed member. A re-file now writes a rename marker into the new group: part from:<old group id>, content { from = <old group id>, name = <new name> } (markerOf, Modules/Members.lua:629-643, written at :808). It is stamped at the newest DTS the old group holds (parts, part tombstones, group tombstone) with the by of the old group's newest live part, so every client that re-files the same old group derives the same marker (Peer Review 9f6acf34). A group no record is filed under goes to the held record whose group holds its live marker (heirOf, :658-670; current members first, then former, each in name order) and is re-filed into it. If the old group arrives first, the former member it makes is marked adopted, and it is taken back once the new group and its marker show up (takeBack, :745-758). A chain of renames copies earlier markers like any part. No tombstone is ever made for this, so a client that receives a tombstone before it sees the rename cannot drop the copied parts (Peer Review's hazard, reply 7ff93c2b). The saved shape does not change: adopted is a new field where absent means false. An earlier version of this fix matched the old group by finding exact re-filed copies of its parts. Peer Review G1 (thread c04b4fd1) showed that "copy join date to alts" makes byte-identical join parts, so a departed alt's group would have been merged into another alt and synced out. The marker replaced that heuristic and its per-apply matching cost. Known costs: a rename made before this build carries no marker, so its old group can still make a duplicate; duplicates made by earlier builds carry no adopted mark and are never taken back; and any guild member's client can send a false marker, as with any other part, because receiving is not gated. Specs in members_spec (marker shape, DTS and by, two re-filing players deriving one marker, the G1 case, a rename chain, no marker meaning no re-file) ran red first (run 20261016-182722.284, 6 failed; G1 failed at members_spec.lua:1145 with no former X) and green (run 20261016-182922.266). Modules/Members.lua 453/453 (run 20261016-183446.265).

Changed: alt groups live in LibGuildRoster's alt store (todo #11, docs/design/alts.md)

  • Modules/Alts.lua is rewritten to the design alts.md and alts_spec already described. Groups are read from LibGuildRoster, keyed by owner. A bare name another addon fed reads as that member. An owner that did not list itself still counts as one of the group's characters. A main counts only while UGR fed it and the group still lists it. One character with no main is not a group. New: Alts:SameAccount (the library's IsSameAccount) and Alts:HeldAt.
  • One save path writes an edit's groups to UGR's saved copy and to the library, takes the saved characters out of every other copy, and fires one UGR_ALTS_CHANGED with the sorted owner keys it touched. A shrunk copy changes the library only where the library still credits that owner to UGR. When two groups merge, the one with a main wins, then the larger, then the older, then the lesser owner key.
  • On a LibGuildRoster without any of the six alt functions, everything reads and writes the saved copy alone and nothing raises.
  • Removed: Alts:Store, the old merged view, the "<player>:<now>" and "rejoin:A+B" ids and the altGroup cache.
  • Migration 6 (Migrations.AltGroupsByOwner, SCHEMA_VERSION 6) re-keys the saved alt groups by owner.
  • The GRM import reads a group's time through Alts:HeldAt and saves through Alts:Apply, with no record ids and no stamping of its own. Nicknames.MayAuthor asks Alts:SameAccount, as alts.md says. alts.md's nickname storage text and NK-01 to NK-11 now match the members-set nick part.
  • Spec changed with a reason: Tests/alts_spec.lua:503 registered a second UGR_ALTS_CHANGED handler writing the same variable, so every message counted twice and no implementation could pass. The second registration is removed, with that reason in the spec.
  • Red first (run 20261016-163757.982): alts_spec's 61 examples failed on the missing ns.Alts.SameAccount, AL-19's 7 on the missing version 6, and grm_import_spec's 2 alt-group examples. Green: run 20261016-182152.779 (203 passed, the alts, migrations, nicknames, import, Alts tab, nickname display, chat tags and saved-variables specs), and run 20261016-182406.403 (259 passed, every other spec that reads ns.Alts). The whole suite and coverage ran later (run 20261016-224930.539: 1468 passed, 0 failed, 3 pending; Modules/Alts.lua 448/448). Nothing has been tried in game, including the order addons feed the library at login, a known cost in alts.md.

Changed: the Log tab's filter chips fit in two rows (EL-26, EL-19)

The operator, 2026-10-15: "the toggle tabs take up too much space, we need to condense it and make the buttons smaller with less words, so we can get it down to 1, 2 rows at most". Each chip now shows a one-word label from the locale (chip_<category>: Joins, Leaves, Rejoins, Promos, Ranks, Levels, Public, Officer, Custom, Names, Returns, Bans, Banned, Alts, Guild, Deaths, GRM) with the full name in its tooltip, is padded Log.CHIP_PAD (4) a side instead of the library's 10, and the search box is Log.SEARCH_W (140) wide instead of 200. Offline, the search box and all 17 chips take 2 rows at 560 pixels, down from 6; the list hangs from the bottom of the filter row, so it starts that much higher. EL-19 now expects the short labels (reason in its comment). Peer Review F5 (thread 00173eb7): a new category with no short label would silently fall back to its long name and break the two-row fit, and EL-19/EL-26 do not catch that (a mutation run with death removed left both green, run 20261016-164700.798). The list is now public as Log.ShortLabels(), and a new EL-26 example checks it has an entry for every SavedVariables.CATEGORIES category and none other, each matching its chip_<category> locale string. It ran red first (run 20261016-164024.604) and green with locale_strings_spec (run 20261016-164746.524). That run also restored the imported (GRM) label, which a red-first mutation from another session had left out. GUI/Tabs/Log.lua is at 100% line coverage (run 20261016-161034.390, which also ran the whole suite: 1444 passed, 0 failed, 3 pending). Not verified: the harness's fixed 7-pixel character width is not the game font, and 560 is a guess at the pane's width, so a narrower pane in game could still wrap to a third row.

Tests: harness pin moved to dd620e5 (the game's guild event log, whispers, sender-name filters)

  • Tests/wowapi now pins WoWAPITesting dd620e5, which includes be92d12's model of the guild event log. Tests/roster_tracking_spec.lua drops its hand-made installLog and drives RT-38 to RT-42 through guild.setEventLog, guild.eventLogQueries and guild.deliverEventLog; RT-40 removes QueryGuildEventLog for its one example, since the harness now always has it.
  • The stand-ins named in the nickname and right-click menu entries below are gone: the menu spec reads env.wow.tells, env.wow.friends and env.wow.ignores, and Tests/env_ugr.lua's sender-name filter stand-in is replaced by the harness's own (env.wow.senderNameFilters, env.wow.hearChat). env.unload removes UGR's filter, because the harness keeps those registrations across a reset.
  • MD-29 had gone red on the new pin because its whisper stand-in recorded ChatFrame_SendTell and LibGuildRoster now calls ChatFrameUtil.SendTell; it reads env.wow.tells now and is green.
  • Whole suite 1504 passed, 9 failed, 2 pending (run 20261016-131323.268); the nine are the MD-34 and MD-35 pane specs, written red and not built yet. Coverage was not re-run: no addon code changed.

Added: an Alts icon tab on the guild window's top edge (MD-35, a preview)

The operator, 2026-10-15: "lets put the alts tab in there now, and we can see what it looks like and whether we need to do anything with it". The direction (bank :4417): UGR's tools become icon tabs along the guild window's top edge, right of the portrait, styled like the game's side tabs, while the Members list becomes UGR's main view. Alts is the first, to see how it looks.

  • GUI/GuildTab.lua GuildTab:AddTopTabs(page, kind) puts one CheckButton per GuildTab.TOP_TABS entry on the LibGuildRoster page (so it hides with the page), anchored BOTTOMLEFT at the window's (page:GetParent()) TOPLEFT, TOP_TAB_X = 62 to clear the 60-wide portrait at -5 (classic_era CommunitiesFrame.xml:313-316). Registered from GuildTab:Enable's pane branch on OnGuildTabPageShown and for a page already on screen; a library without panes gets none.
  • The art is the game's CommunitiesFrameTabTemplate when C_XMLUtil.GetTemplateInfo reports it (all five Blizzard trees have the template; Forever's inherits RightSideTabTemplate), as LibGuildRoster builds its own side tab, else drawn by hand with classic_era CommunitiesTabs.xml's textures. The template's scripts are replaced: its click calls the window's SetDisplayMode.
  • GuildTab:OnTopTabClick selects UGR's pane and the tab, or, when the pane already shows that tab, the member list. GuildTab:PaintTopTabs checks the tab exactly while the pane shows it, called from MainWindow:ShowTab and MainWindow:Unembed.
  • GameTooltip added to .luarc.json's globals (the tab's hover).
  • Peer Review (thread cf5f8601): F1, the first click built the last-used tab and only then Alts; now OnTopTabClick sets MainWindow.pendingTab while it selects the pane and MainWindow:Embed opens on it. F2, a spec that every TOP_TABS key is in ns.Constants.TAB_ORDER, run on a renamed copy too so it is known to bite. Before a second tab: the art's border overhangs 29 px right, so TOP_TAB_GAP 4 would put the next tab under it (Peer Review's figures).
  • Tests/guild_tab_spec.lua MD-35 (13 specs) red first (run 20261015-203138.530, AddTopTabs missing; F1's spec red in run 20261015-211133.511, building Log then Alts), green whole suite 1510 passed, 0 failed, 2 pending (run 20261015-212513.525); GUI/GuildTab.lua 157/157, GUI/MainWindow.lua 202/202 (coverage run 20261015-213017.202). Not seen in game: the placement, the side-tab art drawn on a top edge (the art's flap is drawn for a right-hand edge), and the icon INV_Misc_GroupNeedMore are all unverified on every client.
  • Peer Review edge note (thread cf5f8601, reply 7b56556e): OnTopTabClick now calls SelectGuildTabPane inside pcall, so MainWindow.pendingTab is cleared even when selecting the pane raises, and then re-raises the error. Its spec ran red first (run 20261015-214026.487). The stub now takes (_, owner, kind) and the example checks the call, because the zero-argument stub made the language server flag every call in the file. GUI/GuildTab.lua is at 100% with the error branch covered (coverage run 20261016-160945.323).

Changed: the guild-window pane has no tab strip and no Roster tab; Log, Alts, Bans, Sync are top-edge tabs (MD-34, MD-35)

The operator, 2026-10-15, on seeing the pane: "this doesn't look good. i spent a LOT of time with LGR getting the borders and stuff right, you need to use that", "you can get rid of the roster tab, it's the main tab", "i don't want the tabs IN the window, I want them across the top like where the funky button is", and "rotate the tab on the top 90 degrees counter clockwise".

  • GUI/MainWindow.lua Embed no longer builds an AceGUI TabGroup: MW.tabs is a plain host whose content is LibGuildRoster's pane parent, so a tab's content sits inside the page's own borders. paneTab(key) turns Roster (or anything that is not a tab) into Log, and Open("roster") is ignored while the pane shows. ShowTab attaches raw frames only for the free-standing window, which keeps its TabGroup and Roster tab. Unembed hides the content frames; with no TabGroup in the pane there is nothing to release. The OnSizeChanged fit hook is gone, since content anchored to the whole pane follows its size. MainWindow.Offered is the one rule for which tabs a player is offered.
  • GUI/GuildTab.lua TOP_TABS is Log, Alts, Bans, Sync, TOP_TAB_GAP 20 (the game's side tabs are 20 apart, CommunitiesFrame.xml:390), TAB_SIZE exposed. The tab art is always drawn by hand (the template path is gone), with the border turned a quarter counter-clockwise (SetTexCoord(1,0,0,0,1,1,0,1), TOPLEFT -11, 29, size 64). PaintTopTabs lays the tabs out, hides Bans for a player who may not see it and closes up the tabs after it; UGR_BAN_ACCESS_CHANGED repaints them, so a rank change shows at once.
  • The 9 MD-34 and MD-35 examples written red on 2026-10-15 pass, and a new example covers the ban-access repaint. guild_tab_spec and window_spec 88 passed, 0 failed (run 20261016-161306.043); GUI/GuildTab.lua 169/169 and GUI/MainWindow.lua 195/195 (coverage run 20261016-160945.323, which also ran the whole suite: 1444 passed, 0 failed, 3 pending). docs/design/member-details.md MD-34 and MD-35 match. Not seen in game: the turned art, its offset, the tabs' spacing on the top edge, the content filling the pane, and the new icons (INV_Misc_Book_09, Ability_Warrior_Disarm, Spell_Nature_Reincarnation) on every client.

Added: nicknames, shown with the main's name (ALT-09, ALT-10; Discord thread c59e3510)

The operator, 2026-10-15: "yes, we need to do nicknames now, it's a good thing to do, and that's in addition to showing the main like GRM does now. we need to show both". New Modules/Nicknames.lua (ns.Nicknames, in all five TOCs); design in docs/design/alts.md, "Nicknames and the main's name".

  • One nickname per character, saved as a synced nick:<lower-cased name> record ({ kind = "nick", name, text, share }) through Sync:SaveRecord; clearing saves a removal. Not on the alt-group record as alts.md first sketched: group records are admin-only, and a player sets their own nickname. At most 20 characters (strlenutf8), trimmed; | (the chat escape) and control characters are refused; empty text clears. Shared with the alt group by default: a character with no nickname of its own shows the group's newest shared one.
  • Set by the character, any character in its alt group, or an officer or approved player (ns.Admin). Received only from those senders: new Sync.AUTHORS / Sync.MayAuthor, and Sync:Accepts(sender, kind, record) now takes the record. The sender is judged, never by. Sync.KEYS.nick refuses a malformed record before it is stored.
  • Drawn WITH the character's name and the main's, never instead of either ("Bobs, alt of Bob"): the member panel's name row plus a Nickname row whose editor has a share box; a Nickname line in the guild window row tooltip; a "Known as" column on the Roster tab; the Alts tab's headings and rows; both tabs' search reads nicknames.
  • Chat: the sender's name of guild, officer, whisper (both ways), party, raid, raid warning and instance lines reads [Bobby (Bobs, alt of Bob)]: hi, through ns.Compat.AddSenderNameFilter (ChatFrameUtil.AddSenderNameFilter, in all five Blizzard trees); the message text is untouched. On a client without it, the fallback ns.Compat.AddChatFilter puts "(Bobs, alt of Bob) " before the text instead. System online/offline lines are not tagged, because a whisper window matches those lines unchanged after the filters (ChatFrameOverrides.lua :247-270, Classic Era tree).
  • Settings names.nicknames and names.chatMain (per guild, both on); /ugr nick <text> sets your own, /ugr nick clears it. No migration: the records live in the existing dataset.
  • Specs: new Tests/nicknames_spec.lua (NK-01 to NK-10), Tests/chat_tags_spec.lua (CT-01 to CT-06, CT-06 on all five clients), Tests/nickname_display_spec.lua (ND-01 to ND-08). Changed, with the reason in each: roster_tab_spec (the new column; floor checks on larger screens), window_spec (1365x768 screen), settings_spec, slash_spec, toc_spec.
  • Red run 20261015-194037.818 (31 failed, every one "ns.Nicknames is missing"); green whole suite 1489 passed, 0 failed, 2 pending (run 20261015-200638.556); 100% line coverage on all 13 touched code files (run 20261015-201928.043).
  • Known costs: catch-up takes a nickname record only from its owner, the owner's alt group or an admin, so a new client can wait until one of them is online (not verified with two clients). GRM nicknames are not imported. Not seen in game: the chat filter, the column, the editor. (The "Known as" column's raise of the width floor is undone below.)

Fixed: Peer Review on nicknames and the admin gate (thread d395c58c, reply fb7ac64b)

  • F1: Nicknames.SenderNameFilter tags the name the client built, kept whole with its colour, for a current member (arg2) on a CHAT_EVENTS line; nil otherwise. The client runs it last in GetDecoratedSenderName (ChatFrameUtil.lua:977-1019) and draws it only as the link text, so arg1 is unchanged for other addons and the log. On WHISPER_INFORM the tag is the recipient's. Enable registers it once through ns.Compat.AddSenderNameFilter and falls back to ChatFilter per event only without it. The harness has no sender-name filters, so Tests/env_ugr.lua stages a stand-in (env.senderNameFilters, env.decoratedSender), requested from WoWAPITesting. Not seen in game.
  • F2: Sync.lua refuseReply sends replies refused for an unknown kind, a foreign key or the wrong lane through RefuseReply ("kind"/"key"/"lane"), as the rank refusal does, instead of leaving the asker to wait out the 180 s timeout. KNOWN COST: a v0.2.0 client has no KEYS for the new admin:/nick: kinds and refuses such a reply the old way, so it may wait up to 180 s per key and ask again later. Verified in v0.2.0 Sync.lua:498-499 and DeltaSyncP2PNumbered.lua:152, :539, :1221-1226; repeat rate and in-game behaviour not verified. See docs/design/sync.md.
  • F3: Nicknames:Set returns Sync:SaveRecord's refusal message; /ugr nick prints it.
  • Q4: RosterTab.FixedWidth leaves Known as out, so MainWindow.MIN_WIDTH is back to 965 px at scale 1 (1099 put a 16:9 screen under the floor above UI scale ~1.24). The column shows only while RosterTab.Fits (floor list part plus 134, times the scale, plus the 16 px gutter), re-checked on refresh and OnSizeChanged, through RowList:SetColumns. New MainWindow.DEFAULT_WIDTH (1099) is the never-saved size. In-game OnSizeChanged on the content frame is not verified.
  • Specs: red run 20261015-204241.006 (20 failed), green 1508/0/2 (run 20261015-211023.275), 100% coverage on the five files (run 20261015-210336.254); chat_tags_spec rewritten; env_ugr.lua sender-name stand-in; sync_spec "refused for its shape"; NK-11; roster_tab_spec, window_spec, ND-06 screen and width changes with reasons.

Changed: one admin gate for every admin action, officers plus an approved list

The operator, 2026-10-15, on a Discord request about clearing the log (thread 5185513d): "it should be officers plus a list of people allowed to do stuff. that should be for everything, not one thing. if you're an officer, or on the approved list, you can do all the 'admin' level stuff." New Modules/Admin.lua (ns.Admin, in all five TOCs); specs in Tests/admin_gate_spec.lua.

  • One gate, ns.Admin, now decides every admin action: editing join and promotion dates and custom notes, linking alts, the ban list (edit, see, the read-only API, who receives bans, running the ban host), clearing the log, editing macro rules, the GRM import, and saving any shared record (Sync:SaveRecord / SaveRecords). A received record's sender is judged by the same gate (Sync:Accepts); the record's by is still not trusted.
  • Removed Sync:Threshold, CanAuthor, WhyNotAuthor, RanksLoading and the settings sync.generalRank, sync.customNoteRank, sync.banRank. Despite their "Lowest rank to share ... with" labels, no code picked recipients by them; they decided who may write and whose records are accepted, so all three were admin gates. The sharing gates (IsValidPeer with limitSendToMyRank, IsMember, Push) are unchanged.
  • An officer is anyone at or above sync.officerRank (default rank 1, or the lowest rank in a smaller guild). It has no AceDB default, because AceDB writes a default back over an empty value at every login (AceDB-3.0.lua:126-128). A saved rank the guild no longer has reads as the default, never as everyone.
  • The approved list is synced: one record per character, admin:<lower-cased name> holding { kind = "admin", name, removed }, so every member agrees on it. Only officers by rank may change it, when saving and when receiving, so a listed alt cannot add others. Removing a name saves a newer record marked removed.
  • Guild info lock: UGR{o=<n>} sets the officer rank. An older g/c/b block still works and reads as its strictest value.
  • Settings page: a "Lowest officer rank" dropdown (no "Allow all ranks" entry: picking the lowest rank does that), an "Approve a player" box and a "Remove <name>" button per approved player, greyed for a non-officer with "Only officers can change this."
  • Refusals read "Only officers and approved players can change this." (was "Your rank cannot change this."); the ban, import and guild info wordings were changed to match.
  • Macro actions (remove, promote, demote) stay on the game's own permissions; only editing the rules is gated.
  • Migration 4 (SCHEMA_VERSION 4) carries the strictest of the three old ranks into officerRank and deletes them; a missing banRank counts as its old default, 1, so nobody gains access by upgrading.
  • Fixed on the way: choosing "Allow all ranks" for the ban rank was undone at the next login, by the AceDB default above.
  • Known costs: members below the carried officer rank who could edit dates, alts or custom notes, or clear the log, by default can no longer do so until an officer lowers the rank or approves them. A receiver that does not yet hold an approval refuses the newly approved character's record until the next sync offers it again (superseded by the data-set rewrite above: a receiver now stores what any guild peer relays, and the admin gate applies only at save). The GRM import no longer reads the game's officer flag, so an approved alt can import.
  • Specs changed with a reason in each comment: sync_records, sync_guild_info (rewritten), settings, settings_panel, guild, alts, alts_auto, alts_tab (its after_each also lacked a rank reset that the old default hid), members, member_panel, event_log, log_tab, bans, bans_tab, ban_api, sync_bans, macro_rules, macro_rules_sync, grm_import (IM-12, IM-12b, IM-18 rewritten), roster_menu, guild_tab, sync_two_peers, toc.
  • Red run 20261015-183958.310 (200 failed); green whole suite 1458 passed, 0 failed, 2 pending (run 20261015-190555.721); 100% line coverage on all 18 touched production files (coverage run 20261015-191302.953). Not seen in game.

Added: a right-click menu on the Roster tab's member rows (Discord thread 47403b72)

A player asked on Discord for a menu on UGR's own roster rows. GUI/Tabs/Roster.lua menuFor builds it, opened with W:OpenMenu on a right-click of a member row (not a heading), the Bans tab's idiom. Specs in Tests/roster_menu_spec.lua.

  • Entries: Whisper, Invite to group (online only), Kick from guild, Link alt, Add friend, Ignore, Copy name, Member details. The player's own row has no Whisper, Invite, Add friend or Ignore; a former member's row has no Invite, Kick or Link alt.
  • An entry the player can never use is left out; one blocked only for now is greyed with the reason in its tooltip. Kick appears only when ns.MacroQueue:WhyNot("remove", name) allows it (the game's own rank rules) and is greyed in combat; Link alt is greyed with the admin gate's reason (it was Sync:WhyNotAuthor("general")'s until the admin gate above replaced it).
  • Kick never calls C_GuildInfo.Uninvite, which is protected. It opens a confirmation (RosterTab:KickDialog, built once, frame names ns.Constants.KICK_DIALOG_NAME / KICK_BUTTON_NAME) whose Remove button is LibAceGUIWidgets' secure action button running the client's own guild-remove command (ns.Compat.GuildRemoveMacro, the localized SLASH_GUILD_UNINVITE1, /gremove when the global is missing). The dialog hides on PLAYER_REGEN_DISABLED, because a frame holding a secure button cannot be hidden in combat.
  • Link alt asks for the alt's name and links it through ns.Alts:Link; a typed short name is matched to a current member in any case. A refusal is shown over the tab.
  • New ns.Compat wrappers, whose comments cite each call's source in all five trees of F:\Blizzard API Docs: Whisper (ChatFrameUtil.SendTell), InviteToGroup (C_PartyInfo.InviteUnit), AddFriend / AddIgnore (C_FriendList), GuildRemoveMacro.
  • The harness lacks three game functions the menu calls, so the spec installs stand-ins for them until WoWAPITesting adds them (requested on its inbox).
  • Not seen in game, for the release checklist: the secure Remove button, whether the PLAYER_REGEN_DISABLED handler runs before the lockdown, and /gremove with a Name-Realm name.

Added: the player's own characters are linked as alts (ALT-08, Discord thread 0a1fd728)

A player asked on Discord for their own characters to be tagged as main and alts without doing it by hand. WoW has no API that lists an account's characters, so Alts:SeeOwnCharacter (run on UGR_ROSTER_CHANGES) learns them one login at a time.

  • Each character seen as a current member is recorded in the guild's myCharacters table (account-wide, as UGR_DB.global is), with the realm time it was first seen.
  • With alts.autoLinkOwn on (the default), the character is linked to the account's best partner through Link, the manual path, so the permission check, the sync record and the log entry are the same as a hand link. The partner is one whose group has a main, then one in a group, then the first seen, then by name. A group with no main gets the account's first-seen character as its main; a main the group already has is kept.
  • Linked characters are marked done, so one the player unlinked by hand is never linked again. A character that left the guild is never a partner. One try per login per guild, held back while the ranks load and retried once; a refusal for rank saves nothing, and the next login tries again.
  • Settings page: "Link my characters in this guild as alts" in a new Alts section.
  • Tests/alts_auto_spec.lua (16 specs) red first (run 20261015-133431.622), green in run 20261015-140537.395.
  • The default is on. The operator has not yet said whether it should be; it is one line in SavedVariables.lua to change. Not seen in game.

Added: alt notes written from the Alts tab (Discord thread b4b13b22)

A player asked on Discord for alts' guild notes to be filled in from the alt links. They clarified that the name in "Name Alt" is the main's, so an alt of Bob gets "Bob Alt".

  • Alts:NotePlan lists the notes a write would change: every alt of every group with a main who is a current member, getting the template with each <main> replaced by the main's short name. A note the player may not edit, one that already reads the text (a write still waiting on the game included) and one past the game's 31-character limit are skipped and counted. The note's text is replaced, not added to.
  • A "Write alt notes" button on the Alts tab asks once, with the count and what it skips, then writes exactly that plan through Members:SetGameNote, unless the guild changed in between. The button is greyed with NotePlan's reason when it cannot write.
  • Settings (Alts section): "Write alt notes from the Alts tab" (alts.notes, off by default), the note to write (public or officer, public by default), and the template (alts.noteTemplate, default L["<main> Alt"]; refused when its text without the markers is already over the limit). The note settings stay usable while the switch is off, so the template can be set up first.
  • Tests/alt_notes_spec.lua green in run 20261015-140537.395; its red run was not recorded.
  • Not verified: whether the server throttles many note writes from one click. Not seen in game.

Added: an Export button in the UGR window (Discord thread 4deddeec)

A player asked on Discord for the export to open from the window, not only from /ugr export. An Export icon now sits left of the Settings gear in the window's bottom row (GUI/MainWindow.lua, the dressed row's export entry) and calls ns.ExportWindow:Open(), with a tooltip from the locale. Tests/export_window_spec.lua EX-21, green in run 20261015-135747.636. Not verified: that the INV_Misc_Note_01 icon exists on all five clients. Not seen in game.

Tests: the whole suite after these four features

Whole suite 1430 passed, 0 failed, 2 pending (run 20261015-152334.565). GUI/Tabs/Roster.lua, Constants.lua, Modules/Alts.lua, GUI/Tabs/Alts.lua, GUI/MainWindow.lua and APICompat.lua at 100% line coverage (coverage run 20261015-152903.989).

Changed: the Roster tab hides offline members until the Offline chip is on

A player asked on Discord (2026-10-14) for the roster to open without offline members. The char.roster.offline default in SavedVariables.lua is now false. AceDB strips a value equal to its default when it saves (removeDefaults, AceDB-3.0.lua:173), and true was the default until now, so no character has it saved: every character, including one that had the chip on, opens with offline members hidden once and has to turn the chip back on. A character that had turned it off keeps false. Tests/roster_tab_spec.lua UI-11 asserts the default and red first (run 20261015-130112.878); the other Roster tab specs are about the whole list, so their load turns the chip on as a player would (reason in the spec), and Tests/settings_spec.lua ST-02's expected defaults follow. Green in run 20261015-130228.124. docs/design/ui.md updated.

Changed: the rank lists' no-limit choice reads "Allow all ranks"

A player asked on Discord (2026-10-14) for the settings page's rank lists ("Lowest rank to share dates and alts with", custom notes, bans) to call their no-limit choice "Allow all ranks" instead of "Lowest rank", which read as a rank of its own. Only the label changed: the locale key is now L["Allow all ranks"] (Locale/enUS.lua, GUI/SettingsPanel.lua rankList), the stored value is still nil. The ST-12 specs in Tests/settings_panel_spec.lua assert the new word; red first (run 20261015-125908.243), green in run 20261015-125953.559.

Superseded the same day by the admin gate above: those three rank lists are gone, replaced by one "Lowest officer rank" list with no "Allow all ranks" entry (choosing the lowest rank allows all).

Fixed: a member's race is saved in one spelling (Peer Review F3, thread 767f1f41)

The roster stores LibGuildRoster's race token (NightElf, Scourge), but the GRM import stored whatever GRM had saved, and GRM saves more than one spelling: mostly the token, sometimes a race id number, the localized race name, an empty string, or a Human placeholder when its lookup failed (research written up in docs/research/grm-core-roster-sync-alts.md, "Race spellings"). The export printed the stored value raw, so one column mixed NightElf, Night Elf and 4.

  • New ns.Compat.RaceToken(value) turns a token, a race id or the client's race name into the token (nil for anything else, "" included), and ns.Compat.RaceName(token) gives the client's name for it. Both read C_CreatureInfo.GetRaceInfo, which every flavour documents, over ids 1 to 200, once per session; 200 being above every race id is not verified against the newest client.
  • The GRM import stores RaceToken(r.race) for a member UGR does not hold. An unrecognised value is not stored, so the roster's own race read fills it later for a current member.
  • The export's Race column writes the race in the client's language, or the stored value when no race matches it.
  • Migration 3 (SCHEMA_VERSION 3) converts races 0.2.0 saved, in current and former members of every guild; "" is dropped and an unrecognised value is left as it was. It runs before AceDB, at load; that C_CreatureInfo answers that early in game is not verified.
  • Known cost: GRM's Human placeholder cannot be told from a real Human and imports as Human.
  • Specs red first (run 20261015-123647.759: the import, export and both migration specs), green in run 20261015-123803.141. Tests/locale_strings_spec.lua allows the new migration name, as it does the other two, because it is a developer-facing step name. Whole suite 1324 passed, 0 failed, 4 pending (run 20261015-124436.031); APICompat.lua, Migrations.lua, Modules/GRMImport.lua, Modules/Export.lua and Constants.lua at 100% (coverage run 20261015-124146.217). Not seen in game.

Changed: which clients may write guild notes comes from LibGuildRoster (GuildRoster contract f4cd150d)

  • LibGuildRoster 1.3.0 (unreleased) adds note editing to its Members tab and publishes its client list as CAN_WRITE_NOTES, now the same as UGR's: Classic Era, TBC and Mists, never Retail, Forever or an unknown client. Compat.CanWriteNotes (APICompat.lua) reads it when the library has it, so the two cannot drift, and keeps its own list only for an older library. apicompat_spec pins it under all five clients: the library's answer is used, the fallback list is right, and the loaded library agrees with UGR's list. Red first (run 20261016-193727.453, the library's answer not used on all five), green with members_spec (run 20261016-193759.564, 134 passed). Whether a note write lands in game is still not verified, here or in the library.

Added: roster tracking reads LibGuildRoster 1.3.0's officers, event log, GUIDs and race (RT-38 to RT-43)

GuildRoster delivered five of UGR's six roster-data requests in its unreleased 1.3.0 (MINOR 34; inbox thread 2f8b405d, reply 229cb15d) and declined live note changes. Every use is feature-detected, so an older library tracks as before. Specs first, in Tests/roster_tracking_spec.lua: run 20261015-115520.731 had RT-38, 39, 41, 42 and 43 red and RT-40 green, as written; RT-43 turned out to be red for the wrong reason (below).

  • Roster:CompareAtLogin asks for the guild event log once per guild (GR:RequestGuildEventLog) and waits for the answer, so a kick, a leave or an invite while nobody was online names the officer. The answer is taken from OnGuildEventLogUpdated alone: the library fires it for the first answer after a request even when the log is unchanged (guildEventLogAsked), so a new guild's log that reads the same as the old one's still arrives (RT-42). A first version also took GUILD_EVENT_LOG_UPDATE itself; that fallback and Roster.askedLog are removed (Peer Review, thread 767f1f41), and RT-38, 39 and 42 pass through the callback alone (run 20261015-122943.982). If the server never answers, the comparison waits until the next login. A request that could not be sent compares at once. The UGR_GUILD_CHANGED handler resets the request for the new guild.
  • The library's log rows (type, player, actor, rank, ago) are mapped to Roster:Diff's shape; the newest matching row wins, as before. A join's inviter comes from its invite row.
  • A live kick's kickedBy and a live rank change's changedBy become the change's by, queued calls included (RT-41).
  • Roster:Refresh reads the race with GR:GetMemberRace(name) only while the record has none (RT-43). The live entry no longer carries a race, because the library's member has none.
  • Rank list and guild identity stay on ns.Compat: GR:GetGuildRanks can return a partial list while rank names load, and GR:GetHomeGuildInfo's realm spelling would change every saved guild key. Roster:Ranks says why in its comment. Reasons in docs/design/roster-tracking.md, "Library requests", which also records the declined request: a note someone else edits mid-session is seen at that player's next login.
  • Specs changed, with reasons in their comments: the Refresh spec no longer passes a race on the live entry; BN-17 and the GRM import's "member UGR holds" spec now clear the GUID they assume is missing, because the login comparison stores one since 1.3.0; RT-43 set the race table before env.load, which wipes it, so it was red because the race was gone, not because the code was missing. Now set in afterLibs; with the race read removed it goes red on the race (run 20261015-121934.369).
  • Whole suite 1320 passed, 0 failed, 4 pending (run 20261015-121416.457); Modules/Roster.lua 447/447 (coverage run 20261015-121901.206). Not seen in game, and GuildRoster 1.3.0 is not released, so none of this runs for players yet.

Added: UGR inside the guild window's Members tab (MD-33, MD-34)

A player asked on Discord (thread 82d3cfa0) for UGR's whole window inside the guild window's Members tab. LibGuildRoster had no slot for a consumer's content there, so UGR asked for one (GuildRoster inbox cf43f522) and GuildRoster built it into its unreleased 1.3.0 (MINOR 34): RegisterGuildTabPane.

  • GUI/GuildTab.lua GuildTab:RegisterPane registers a pane under ns.addon, labelled L["Ultimate Guild Roster"], when the library has RegisterGuildTabPane; RefreshLocale registers it again, which relabels the library's switcher in place.
  • New MainWindow:Embed(parent) (the pane's shown) hosts UGR's TabGroup in the parent the library lays over the member list; MainWindow:Unembed(parent) (its hidden) hands the TabGroup back to AceGUI's pool, W:AttachRawFrames taking the tab content out as it does for the window. The window's tab setup moved into two local helpers (newTabs, startTabs) that both paths share.
  • One set of tab frames serves both: Embed closes a free-standing window, and MainWindow:Open while the tabs are embedded selects the tab in the pane instead of opening a window. RefreshActiveTab, RefreshTabs and RefreshLocale now act on whichever TabGroup is on screen (MainWindow.tabs), not only the window's.
  • MemberPanel:Open(name) with no page docks beside the pane while the tabs are embedded, and closes with it.
  • Fallback for a library without panes (MD-33, built earlier this cycle): a "Ultimate Guild Roster" button under the page opens UGR's window docked beside the page (MainWindow:Open(tab, page), MainWindow.dockedTo), and MainWindow:CloseDocked closes it when the page is put away.
  • Tests/guild_tab_spec.lua MD-34 (8 specs) red first (run 20261015-143233.003, no pane registered), green in run 20261015-143411.838. MD-33's specs now remove RegisterGuildTabPane before enabling, because the button is only the fallback on a library that has panes (reason in the spec's comment). GUI/GuildTab.lua 94/94, GUI/MainWindow.lua 184/184, GUI/MemberPanel.lua 352/352 (coverage run 20261015-143908.196). Not seen in game, and GuildRoster 1.3.0 is not released: the pane's placement is the library's, unseen, and whether UGR's lists fit a pane smaller than the window's width floor is not verified.
  • Peer Review (thread d0f49e1b):
    • U1, sizing: AceGUI's TabGroup lays out its strip and sizes its content only from SetWidth / SetHeight (AceGUIContainer-TabGroup.lua:359, :453-473), and anchoring alone called neither. Embed now sizes the TabGroup from the pane before the strip is built, and hooks the pane's OnSizeChanged (once per pane; the library fires no resize event, GuildRoster reply dae91f7c) to size it again.
    • U5, dialogs when the pane goes: Unembed now closes the open question (ns.UI.ClosePrompt) and the Log tab's "Clear a range" dialog (ns.Tabs.Log:CloseRange). Before this, the range dialog stayed on screen with no UGR behind it, and its Clear still deleted log entries. Every dialog, and what happens to it when the pane goes: the question and the range dialog close; a member panel docked to the pane closes; the ban dialog, the export window and the GRM import dialog stay open. None of the last three is opened by a tab, and closing the window leaves them open too. The pane has no Escape layer (its proxy would hide the tabs), so one Escape on the range dialog closes the dialog and the guild window together: the client's CloseWindows hides the open left panel and every shown UISpecialFrames entry in one call (Blizzard classic_era UIParentPanelManager.lua:1041-1092, UIPanelWindows.lua:25, :31). Accepted: nothing is cleared, since the clear runs only from the dialog's accept.
    • U4: moving the tabs from one window kind's pane to the other is specced.
    • Toggle (minimap button, key binding) while the tabs are in the pane now puts the member list back (SelectGuildTabPane(nil, kind), the kind remembered by Embed). It used to do nothing there.
    • Six specs: sizing, the two window kinds, the range dialog and question, the ban dialog, the export window and import dialog, and Toggle. Three were run red before the code (which three was not recorded). The export/import spec passed as soon as it was written; it is a regression guard. The resize spec resizes the pane itself, not the page, because the harness fires OnSizeChanged only on the frame whose size was set and not on frames anchored to it (Tests/wowapi/env/frames.lua applySize; requested from WoWAPITesting, inbox df32a280). Removing the hook turns that spec red (run 20261015-145421.185). Tests/guild_tab_spec.lua 41/41 (run 20261015-150028.887); GUI/MainWindow.lua 199/199, GUI/GuildTab.lua, GUI/MemberPanel.lua, GUI/Tabs/Log.lua and GUI/UI.lua 100% (coverage run 20261015-145916.707).
    • In game only, for the release checklist: whether the lists fit the pane (U2), and whether UGR draws above the pane's background (U3). GuildRoster's advice for U3, if it does not: set the TabGroup's frame level to the pane's plus one after re-parenting.

Added: member tools on the guild window's Members tab (MD-29 to MD-32)

Players coming from GRM reach member tools from the game's guild window (operator, 2026-10-15, for a guild that asked for it). The Members tab there is LibGuildRoster's, so UGR uses the extension points GuildRoster 1.3.0 (MINOR 34) added at UGR's request (inbox thread fa4de08e) and never hooks the library's frames. Spec first: Tests/guild_tab_spec.lua, written before the code. Its red run was reported but its run id was not recorded; the first green run is 20261015-111104.636. The spec added for the "Alt" tooltip line went red with that line removed (run 20261015-113143.558).

  • New GUI/GuildTab.lua (ns.GuildTab, enabled from OnEnable after the roster) registers a row click handler, menu items and tooltip lines under ns.addon, and only when the library has RegisterGuildTabClickHandler; an older library leaves the tab as it was.
  • A left-click on a member UGR holds (current or former) opens the member panel docked beside the Members tab page and parented to it, without opening UGR's window. The click is taken from the library, so it does not whisper; Whisper stays first in the right-click menu. A sister guild's row and an unknown name are left to the library.
  • The right-click menu adds "Member details", and for a player who may see the ban list "Ban player" (the ban dialog with the name, class and realm filled in) or "Edit ban".
  • The row tooltip adds the ban warning, the main or alt line and the join date, drawn with the panel's own date wording (MemberPanel.DateWords).
  • MemberPanel:Open(name, page) docks to the page when given one and re-parents the panel, keeping its strata; MemberPanel:Close(from) leaves a panel docked elsewhere open, so closing UGR's window no longer closes a panel on the guild window, and the page being put away (OnGuildTabPageHidden) closes it. A panel on the guild window stays out of UGR's Escape layer.
  • BanDialog:Open(name) for a player who is not banned now opens a new ban with that name picked.
  • Tests/toc_spec.lua lists GUI/GuildTab.lua after GUI/MemberPanel.lua in all five TOCs.
  • Whole suite 1313 passed, 0 failed, 4 pending (desk coverage run 20261015-112411.756), with GUI/GuildTab.lua at 100%; MemberPanel, BanDialog, MainWindow, Core and enUS at 100% (coverage run 20261015-112135.202). Switches on only once GuildRoster 1.3.0 is released. Not seen in game; least sure of the panel's draw order over the library's page, which it lifts to HIGH strata.

A read-only ban API for other addons

Asked for by FastGuildInvite to import UGR's ban list into its blacklist and to leave the "banned player is in your guild" warning to one addon (inbox thread a552781e). Spec first: Tests/ban_api_spec.lua, red before the code (runs 20261014-155130.697 and 20261014-160409.877).

  • Three methods on the AceAddon object UltimateGuildRoster (Modules/Bans.lua, end of file), all reading through ns.Bans, so another addon sees exactly what the Bans tab shows: GetActiveBans() returns a new array of { name, reason, bannedAt } in the tab's order, {} below the ban rank or guildless, and nil while the guild's ranks are still loading (FGI drops its imported bans on {} and keeps them on nil; its reply 1e585326, spec red first in run 20261014-164312.472); IsBanned(name) takes a bare name or Name-Realm; WarnsAboutBans() is true when the player may see bans and the guild's "Banned player present" chat switch is on. No method writes.
  • Bans:CheckReadable() sends UGR_BANS_CHANGED with no id once each time the ban list becomes readable or stops being (the ranks arriving after the roster, a promotion, a demotion), from the existing GUILD_RANKS_UPDATE / GUILD_ROSTER_UPDATE handlers, from Settings:Changed (a changed ban rank or guild-info lock), and on UGR_GUILD_CHANGED. The last announced state is one value for the session, guildless counting as not readable, so leaving or switching guilds is announced too; a switch between two guilds whose lists are both readable is announced as well, since the list is another guild's. Peer Review P1 and P2 (thread 8f0023a2) found the guild-leave and setting cases missing from the first build, which kept the state per guild table and was not called from Settings:Changed; specs red first (run 20261014-162652.865). A reader that imports on the message no longer waits for the next ban to see the list. The Bans tab's own listener ignores the id, so it simply redraws. The readable-to-readable guild switch is spec'd too, driving a second guild the way the event log's specs do (Peer Review reply 7e537804); red with the guild-change clause removed (run 20261015-103250.786).
  • The list is officer data; the method's comment says a caller must not pass it on below the ban rank. Nothing can enforce that, and the saved variable UGR_DB is readable by any addon anyway, so the API gives out nothing that was not already on disk.
  • Whole suite 1295 passed, 0 failed, 4 pending (run 20261014-165000.455); Modules/Bans.lua and Modules/Settings.lua at 100% (coverage run 20261014-165358.387). Not seen in game.

0.2.0 and 0.1.0 are in CHANGELOG_ARCHIVE.md.

This mod has no additional files