UltimateGuildRoster-v0.1.0
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.1.0] - 2026-10-13
The first release: the framework, the shared guild sync connection, and the per-guild settings logic. Every guild-management feature is still to come.
Settings
Modules/Settings.lua(ns.Settings, all five TOCs): the one way to read and change a guild's settings, by dotted path (Get,Set,Guild,LogOn,ChatOn,Milestones).Setrefuses bad input with a message and keeps the old value: an empty or non-numeric number box, a number out of range (level 1 to the client's cap, inactive days 1 to 180, chat window 1 toNUM_CHAT_WINDOWS), or a rank the guild does not have. A wrong type on a switch, or a path that is no setting, raises. Milestones are every tenth level up to the cap, and each switch writes only its own level.Setreturns the message already read from the locale rather than the bare key settings.md named, because the locale spec requires every enUS key to be looked up throughL[...].ns.Compat.MaxLevel(GetMaxPlayerLevel) andns.Compat.GuildRanks(GuildControlGetNumRanks/GuildControlGetRankName), read throughns.Roster:Ranksandns.Roster:LowestRankuntil LibGuildRoster carries the rank list (request 2f8b405d).ns.Sync:Threshold(kind)reads a saved rank threshold; a missing or removed rank reads as the lowest rank, and the saved value is left alone. A saved value that is not a whole rank index (a hand-edited file, a later importer) also reads as the lowest rank; it used to raise comparing a string with a number (self-audit).- Before the client has the guild's ranks, the rank list reads as not loaded rather than partial: a
count of 0 or any missing or empty name gives no ranks,
Thresholdis nil in a guild (callers then share nothing gated by rank), and a rankSetis refused with "The guild's ranks have not loaded yet. Try again in a moment." instead of asking for a rank that cannot be chosen.GUILD_RANKS_UPDATE(documented in all five clients) repaints the settings page. What the client really answers before then is not verified, so both shapes are covered (Peer Review, thread 4477687f). char.logViewedAtis the first per-character default.- Tests:
Tests/settings_spec.lua(ST-01 to ST-09, and the ranks-not-loaded cases), written and run red first. ST-03 was green before the code existed because it pins a decision; it was seen red by adding a migration that moves the minimap setting, then put back.Tests/env_ugr.luagains aplayerNameoption (a second character on one account) and aranksoption over the harness'sguild.setRanks, which WoWAPITesting delivered for this as 6f9ae02 (inbox a7ad2c2c); the local stand-in used meanwhile is gone.env.unloadleaves the guild env alone, since its roster is the spec's own state across an unload, sosettings_specresets it after each example. The TOC spec lists the new file. 326 passed, 1 pending, green in default, reverse and shuffled order; line coverage 100% on every production file (992 lines).
Framework
- Five TOCs, one per live client:
Ultimate Guild Roster.toc(Classic Era 11509),_TBC(20506),_Mists(50504),_Mainlineand_Camelot(WoW Forever 16001). Wrath and Cata ship no TOC: the operator, 2026-10-13, "we need to support classic/tbc/mop/retail/forever at a minimum, the other versions aren't 'live' currently". A Wrath or Cata client is reported as an unknown client (ns.Compat.detected = false, every flavour flag false). - Every TOC carries
## X-Curse-Project-ID: 1744059, the CurseForge project the packager uploads to;Tests/toc_spec.luachecks it in all five. - Hard dependencies in every TOC: Ace3, LibAceGUIWidgets, VersionCheck-1.0, GuildRoster,
LibLocaleOverride, AceCommQueue-1.0, DeltaSync, LibGraph-2.0, and LibDBIcon-1.0 except on
_Camelot, which embeds LibDataBroker-1.1 and LibDBIcon-1.0 underLibs/and lists LibDBIcon only as an optional dependency..pkgmeta's required dependencies are checked against the TOCs by a spec. - Namespace: the addon's private table is published as the one global
UGR. The AceAddon object is "UltimateGuildRoster", with AceConsole, AceEvent and AceComm mixed in and AceCommQueue embedded last. - Version:
addon.Versionis the raw TOC version (VersionCheck reads it);ns.TidyVersionstrips theUltimateGuildRoster-vrelease-tag prefix for display and leaves an unpackagedUltimateGuildRoster-v0.1.0as it is. - Saved data:
UGR_DBthrough AceDB-3.0 with one shared profile (window size and position, last tab, minimap switch) andglobal(language override, and one table per guild). A versioned migration runner works on the raw table before AceDB wraps it: a malformed migration list raises before anything runs, a failing step keeps the last good version, and data from a newer build is left untouched with one chat line. - Per-guild store (schema 2): everything UGR saves about a guild lives under
UGR_DB.global.guilds["<GuildName>-<Realm>"], so characters in two guilds keep two separate sets of data.ns.Guild(Modules/Guild.lua) resolves the key at enable and onPLAYER_GUILD_UPDATEfromGetGuildInfoandC_Club.GetGuildClubId(ns.Compat.GuildIdentity), keeps the key it has while the client has not sent the guild's name yet, and firesUGR_GUILD_CHANGED. Each guild's table comes with the per-guild settings defaults fromdocs/design/settings.md. A guild renamed in game (same club id, new name) has its data moved to the new key and firesUGR_GUILD_RENAMED. Migration 2 parks a non-empty old account-wide dataset inglobal.pendingSync, and the first guild resolved adopts it, keeping the newer copy of any record it already holds. The sync dataset is now the current guild's; guildless, it is empty and a save stores nothing. Three older specs that pinned the account-wide dataset and the TOC file list changed with this reason. - Locale: every player-visible string is in
Locale/enUS.lua, registered with LibLocaleOverride; a translation is one file added toLocale/Locale.xml. The window-language override (UGR_DB.global.uiLanguageOverride) relabels the open window and the settings page without a reload; chat output and key-binding labels follow the game's language. /ugrand/ultimateguildroster:help(also on an empty line),toggle,show,hide,settings,lang,version.- Main window: AceGUI frame with six placeholder tabs (Roster, Log, Alts, Bans, Sync, Settings), each a LibAceGUIWidgets button strip over a striped, sortable list; a control that is not built yet is greyed with its reason in the tooltip. Minimap launcher (left-click toggles the window, right-click opens the settings), a key binding, and a settings page in the game's AddOns options (minimap switch, GuildRoster's Members-tab switch, language picker; refuses to open in combat).
- Roster: LibGuildRoster-1.0 is the roster source; UGR listens to its per-member callbacks and repaints the open window.
- Sync: a DeltaSync-1.0 host of UGR's own over AceComm and AceCommQueue, following DeltaSync's
canonical-hash rules (hash computed once, at save, with
setAtinside it; every other path forwards the stored hash; the newersetAtwins). Only guild members are sync peers. The dataset is empty in this build. - Sync tie-break: on equal
setAtthe copy with the greatercontentHashnow wins on every client, and an exact tie keeps ours. The incoming copy used to win any tie, so two officers holding different copies saved in the same second (or imported GRM records carrying GRM's own edit time) adopted each other's copy and swapped on every login forever. The spec that asserted the old rule changed with this reason (docs/design/sync.md, Edge cases). Hashes are numbers and compare as numbers (Sync.HashGreater); a first cut compared them as text, where 9 beats 10, which would have disagreed with numbered P2P's zero-padded canon (caught by Peer Review). A hash that arrives as a numeric string compares as the number it spells. - Sync catch-up moved from DeltaSync's hash P2P to its numbered P2P (
docs/design/sync.md, "The protocol"). The hash protocol never offered a key the asking client did not already hold, so a new officer with an empty dataset would have received nothing. Now every record key is numbered on the current guild'ssynctable (host:InitNumbers; any client in a guild may mint, since minting is deterministic), the catch-up broadcast goes out throughSync:Announce, and UGR supplies the numbered hooks:Sync:Canon(the 20-digitsetAt+contentHashcanon, formatted from the stored fields, never hashed, with one width rule inSync.CanonOK),servableCanon,canonImproves(compares the whole canon, so an equal-setAtpair settles on the greater hash; DeltaSync on its own compares the time only),onDeliver, and a data leg that claims the accepted slot withQueryArrivedbefore replying.isOwnKeyis false for every key. The hash-protocol specs were rewritten for the new protocol with this reason. - Sync waits for the guild's name: at login the client can report the player in a guild before it
sends the guild's name. Sync used to announce an empty dataset then, throw away anything it
received while marking it fetched, and never announce again. Now
ns.Guildalso resolves onGUILD_ROSTER_UPDATE, nothing is announced until the key is known, a record received without a guild is refused without completing its catch-up item, andUGR_GUILD_CHANGEDto a guild announces again (also on a move to another guild). Found by Peer Review. - Sync stopgap for DeltaSync's time-only offer-back: when a peer advertises our
setAtwith a lower hash, UGR announces again so the peer hears ours and pulls it. At most once per frame; no timer. A later broadcast that still advertises the lower copy is answered again: the first version remembered each peer's answered copy for the whole session, so a peer whose fetch failed was never answered again (Peer Review, Finding E). The spec that asserted "never twice" changed with this reason. Known cost, recorded indocs/design/sync.md: the extra announce can delay this player's own pending fetches by one collect window. A DeltaSync request for a symmetric offer-back hook replaces the stopgap later. - Sync datestamps use the realm's clock (
ns.Compat.ServerTime, overGetServerTime) instead of the player's owntime().setAtis the first half of every canon, so a player whose clock ran fast would have won every edit (SY-01; Peer Review flagged the divergence from the design). The spec that assertedtime()changed with this reason.Sync:Stampalso takes an optionalsetAt, for the GRM importer to keep GRM's own edit time (SY-02). - Sync's canon width is pinned to DeltaSync's own
NUM.CANON_WIDTHby a spec, so the two constants cannot drift apart. - Sync numbers a key as it saves. DeltaSync numbers keys only when it broadcasts or adopts a table,
and offers only numbered keys, so a record saved mid-session reached nobody, not even an officer
logging in with nothing, until its author logged in again.
Sync:SaveRecordnow mints after storing and takes an optionalsetAtfor the GRM importer. Found by the two-client spec (SY-21). Sync:SaveRecords(batch)saves a whole import and numbers it with one mint. Each mint scans every held key, raises the table version and replays the catch-up, so an import throughSaveRecordalone would have cost one of each per record (Peer Review, Finding G). An entry that is not a table, or has no string key and record table, is skipped rather than raised on, so one bad row cannot leave the rows before it stored but unnumbered; the return value counts the entries stored. The batch must be a list with no holes, sinceipairsstops at the first nil (Peer Review, Finding H).Sync:Stampignores a suppliedsetAtthat the canon could not carry (a fraction, a string, a negative or an eleven-digit number) and uses the realm's clock instead, so a record is never saved that nobody can be offered.Sync.WholeFieldis the one ruleStampandCanonshare.- Known cost, recorded in
docs/design/sync.md: two officers whose first saves alone land in the same server second hold two different number tables under one version, and DeltaSync never reconciles them until either saves another new key. Requested from DeltaSync. - Saved data:
SavedVariables.luahands AceDB the rawUGR_DBtable instead of its name. Identical in game; offline, the name form read the shared global, so two simulated players shared one database. - VersionCheck-1.0 is enabled on login.
Tests
- Offline suite on the WoWAPITesting harness (submodule
Tests/wowapi, pinned at 6f9ae02), written spec-first: 326 specs green and one pending on DeltaSync, with 100% line coverage of the 17 production files. - A static locale spec tokenises every production file and fails on any player-facing literal, on a key the code reads that enUS lacks, and on an enUS key nothing reads. A fixture translation proves a new language needs no code change.
Tests/sync_two_peers_spec.luaruns two whole copies of UGR as two players on the harness's wire (env.peer/env.peerLogininTests/env_ugr.lua), Classic Era only: an empty-handed player receives every record with the author's hash, still a number (SY-21, SY-26); the newer copy wins in both login orders (SY-22); an equal-time pair settles on the greater hash and a second catch-up moves nothing, which fails with UGR'scanonImprovesremoved (SY-25); two tables numbered apart merge (SY-28a). SY-28b, the same-version case, is pending on DeltaSync.
Docs
docs/research/: GRM behaviour described in our own words for feature planning (macro tool; core, roster, sync and alts; export, stats and the rest). No GRM code or text.docs/FEATURES.md: the feature catalogue, tagged by client and by first release or later, with the operator's 2026-10-13 decisions on import ordering, log retention, log and alts visibility, and the officers-only ban list.
This mod has no additional files