FastGuildInvite-v2.13.5
What's new
<FGI> FastGuildInvite
[v2.13.5] (2026-09-17) — Recheck is deleted, and Seen counts sightings on its own with a user-set gap
New — "Seen again after (hours)", Settings → General → Scanning, default 4
DB.global.seenAgainHours, a range slider (0.5 to 48 in half-hour steps) directly under "Keep the
invite queue between sessions", since it only means anything for a queue that lasts. The operator's
number: "change it from a day to an exposed user setting? set it at default to 4h". Read through
fn.seenAgainAfter(), which returns seconds and applies the same default when the key is absent,
because AceDB fills a default only into a fresh table and a saved-variables file from before this
release has no such key. Listed in Modules/FGI_SettingsProfile.lua's catalogue under Scanning, so
a template can carry it.
Changed — Seen is a count of sightings, folded inside the gap, written only by the ordinary scan
Spacedoc, via the operator: "wasn't idea to do recheck just doing normal search and if you see him next day or few hours later that he get +1 there, and that i can still do invite without recheck." The operator: "rip out recheck and do what he wants. i didn't want recheck, he did."
Seen used to be a count of DAYS: fn.recordQueueSeen folded every hit on one calendar day into
one entry, so a candidate re-found a few hours later showed nothing new, and the only way to move
the column inside a day was the Recheck button. It is now a count of SIGHTINGS. A sighting is
recorded when a /who result re-finds a queued row and at least fn.seenAgainAfter() has passed
since the row's last entry; a repeat inside the gap is folded, so the overlapping queries a
subdivided sweep issues still count once, and the gap is measured from the last COUNTED sighting so
a player found every hour still counts again every four. The row's seen list now holds epoch
seconds (fn.now(), a new seam beside fn.today() because time is a file-local in
functions.lua), one entry per sighting, capped at 30. Rows saved by earlier builds hold day
strings: those carry no moment to measure from, so the first re-sighting of such a row counts, the
record continues in seconds, and the column, which reads #seen, counts both kinds.
A first draft of this release counted once per pass of the level range and again on a new day,
keyed by a passKey advanced at every wrap and on Reset. The operator replaced that with the gap
setting the same day; fn.beginSeenPass and the key are gone, and its absence is pinned beside the
recheck's.
fn.queueSeenDays is deleted: it had no production caller since the Scan tab started reading the
row's own list.
Changed — the Recheck button, /fgi requeue, /fgi requeuestop and the presence probe are deleted
Three generations of Recheck went before this one: a /who per queued name (v2.11.2), a
pool-derived level query whose result filter starved every other caller (v2.12.0), and the silent
addon-message presence sweep (v2.12.1 to v2.13.4). With the column filling in from ordinary
scanning, none of them had anything left to do. Gone:
Modules/FGI_Presence.lua(the probe, the sweep,presenceProgress, itsCHAT_MSG_SYSTEMhandler) and its line in all six TOCs and inTests/support/addon.lua.fn.refreshQueueRun,fn.refreshQueueStop,fn.refreshQueueStatus(functions.lua).- The
requeue/requeuestoparms of the slash dispatch and thehelpRequeuehelp line (FGI_Core.lua,Locale/enUS.lua). - The Recheck strip button,
RECHECK_BTN_W, its label repaint, andL["Recheck"]/L["Recheck the queue"]with the button's tooltip body (GUI/Tabs/Scan.lua,Locale/enUS.lua). Row 2 of the strip ends at Reset, soROW2_FIXED_Wfalls from 598 to 522: the all-hidden floor drops from 668 to 592 and the row-1/row-2 crossover moves from 5/4 counters to 3/2. The all-shown floor stays 840, since Row 1 binds there and did not change. MainWindow:SetPresenceProgressand the green "Checking who's online" status-bar branch (GUI/MainWindow.lua).Modules/Scan.lua'saddon.nameFromFormatexport stays: the comment naming Presence as its consumer is corrected, the parser is still the one place system messages are read.
The Seen header tooltip no longer points at a button; README.md and the CurseForge page lose the
Recheck paragraphs and the two command rows.
Tests/zz_presence_spec.lua is rewritten to assert that no TOC and not the offline load list names
the deleted module, the one thing a stale TOC line or a half-done merge could silently break.
Tests. Tests/zz_queue_seen_spec.lua is rewritten for sightings, driven through the fn.now
seam: the fold inside the gap, the count once the gap has passed measured from the last counted
sighting, the next-day count, the setting honoured at one hour, the four-hour default with the key
absent, a legacy day-string row continuing in seconds, the 30-entry cap, and an end-to-end example
through the real /who callback (two overlapping queries then a sweep five hours later: two
sightings). Its absence block now names every deleted driver plus fn.queueSeenDays,
fn.beginSeenPass and FGI.Presence. The Recheck blocks
in zz_scan_strip_actions_spec.lua and zz_slash_misc_spec.lua, helpRequeue in
zz_slash_help_spec.lua, and the presence module's own spec are removed with the feature, each with
the reason written in place. zz_scan_minwidth_spec.lua is re-baselined to the new Row 2 (624 at
three counters, 592 at two and at none, floor >= 592). zz_add_new_player_spec.lua's seed
examples are reworded for sightings.
[v2.13.4] (2026-09-16) — every pass starts from the configured range, rotation arrows and Assign to work, guild filter edits reach the guild, and a leaver is not welcomed
Fixed — a completed pass replayed the last pass's split pieces instead of rebuilding from the range
Field report (Spacedoc via the operator, retail screenshot): range 1-90, Scan complete, and
Next scan: 1-5. Every level split removes the capped query and leaves its halves in
whoQueryList, so a finished pass is a list of LEAVES, and fn:nextSearch's wrap branch fired that
list again. That froze every cut at the first pass. Since v2.13.3 a cut comes from where that
query's 50 rows were standing (fn.levelSplitPoint), so replaying it replays a sample the realm no
longer matches, and the pile cut only ever ran once. The same freeze meant a range or custom-scan
edit made mid-pass never reached a later pass until Reset.
The wrap now clears the queue and rebuilds it from the configured sources, as the Scan Groups path
already did per pass. The queue construction moved out of fn:nextSearch into fn.freshPassQueue()
(pure: selected custom scans with their flags, then the effective level-range sweep) and a local
populatePassQueue() (anti-spam expiry, then Scan Groups or freshPassQueue), shared by the
empty-queue start and the wrap. The Scan tab's Next scan fallback at the end of a pass now names
fn.freshPassQueue()[1] rather than the old list's first leaf; with a bucket queue it keeps list[1].
KNOWN COST: each pass re-pays the capped parents on the way down. On a piled realm that is one
extra /who (1-90, then 1-89 / 90-90); on an evenly spread realm it is the halvings back down to
the leaves.
Tests/zz_scan_queue_population_spec.lua gains five examples: the wrap rebuilds from the range, picks
up a mid-pass range edit, re-queues custom scans with fresh flags (no stale flag survives), prices
the new pass from the rebuilt queue, and freshPassQueue touches no scan state.
Fixed — rotation member rows were re-sorted by the active column, and the arrows never reached a tagged rotation's guild snapshot
Field report (Vishiswaz): "it looks like they DO work but they don't appear in their new order
until a reload/relog". Two defects in GUI/Tabs/Announce.lua:
- The sort. A header click collapsed every rotation (
onSortChanged), but RowList kept the sort, and expanding a rotation while it was active let_getSortedDatare-sort the spliced member rows by that column. Measured in the new spec with the old wiring and Name sorted descending:1. Beta | Rot | Beta | Alpha | 2. Alpha, members scattered away from their rotation. A reload cleared RowList's sort, which is why the order then looked right. The tab now usesexternalSort, the same arrangement Eligibility adopted in v2.13.0 for the same reason:buildRows(sortKey, sortDesc)orders the top-level rows (sortTopRows, RowList's comparator, Name on_sn) and splices members after, and a sort no longer collapses expanded rotations. - The guild snapshot. While guild announcements are enforced the expanded rows draw the policy
snapshot, and the arrows only swapped the editor's own profile, so a tagged rotation neither
moved on screen nor changed the order the guild posts until the next Save. The arrows now call
reordered(rotId), which re-snapshots throughrefreshPolicySnapshotsFor, gated on the tag and not on permission as Save is (finding 9).
Tests/zz_announce_rotation_reorder_spec.lua (3 examples) drives the rendered tab: expand, click
the real up arrow, and read the rows in pool (draw) order, for a personal rotation, under a Name
sort, and for an enforced tagged rotation.
Fixed — an officer's Save stopped reaching the guild's filter once they had adopted another officer's policy, and the Filters row hid the difference
Field report (Vishiswaz, TBC): a guild filter wanting Paladin, Priest, Shaman and Warlock, and the
chat log still showing 70-70 c-"Mage", 70-70 r-"Tauren" c-"Hunter" and a Rogue search.
Tests/zz_class_filter_queries_spec.lua drives the real /who callback with those four classes, as
the member's own filter and as the enforced guild copy, race-first and class-first: no unwanted class
is ever queued. So the engine was sound and the filter it scanned with was not the one on screen.
Two defects in GUI/Tabs/Filters.lua, found by reading:
- Save.
FiltersTab.RefreshGuildMarkForrefused unlesspolicyIsOurs(). With two officers, the first policy one adopts from the other setsgmPolicyReceived, and every later Save of a marked filter updated only the officer's own copy. The gate is nowpolicyIsOurs() or canEditPolicy(): an officer who may edit policy could already untick and re-tick the Guild box to the same effect, while an ordinary member is still refused. The login window where the rank index is nil still falls back topolicyIsOurs(). - The row.
buildRowshid the enforced guild copy behind a personal row of the same name and drew the personal criteria. While guild filters are enforced the row now draws the copy the scan uses.
Tests/zz_filters_guildmark_spec.lua: the examples written for an ordinary member now set a
non-editor rank explicitly. The harness's default rank may edit policy, so they had been running as
an officer without saying so. An officer example is added. NOT VERIFIED that this was the state of
Vishiswaz's guild.
Fixed — a welcome still went out to a member who had already left
Field report (Vishiswaz): "Goonbrain has left the guild" at 04:30:47, the auto-blacklist line at
04:30:50, "Welcome to the guild, Goonbrain!" at 04:30:52. The greeting waited in the click queue and
went out on the next click regardless. Welcome:CancelFor(name) drops the leaver from the pending
guild line (bare name) and the whisper queue (normalized full name), closing the window if nobody is
left; GuildEvents:OnLeave calls it before the isNew gate, since the leave list may already hold
the name from an earlier stint. Others in a combined line are still greeted.
Tests/zz_protected_welcome_spec.lua gains both cases, through OnLeave.
Two gaps in that fix, confirmed by Peer Review (thread 1dd7a8fe, reply ec939e4d) and fixed with
the remedy it gave:
- The event-log route never cancelled.
GuildEvents:ProcessEventLogconcludes "they are gone" insideif not fn:isPlayerInMyGuild(name)and now callsWelcome:CancelFor(handoff or name)there too.handoff, notname: the log gives a bare name, the welcome was queued under LibGuildRoster'sName-Realm, andnormalizePlayerNameleaves a bare name bare on the Classic family, so my first version matched nothing there while its spec, which queued a bare name, stayed green (Peer Review re-check, replyb46cde32). The spec now queues the library's spelling. The queue is session-only, so this only reaches a departure from this session. KNOWN COST: a member who left while we were offline and rejoins now, before the roster cache lists them, can lose their fresh welcome when the log is read. - The guild line matched the bare name, so Name-RealmA leaving dropped Name-RealmB's welcome.
PostGuildWelcome(chatName, fullName)now keeps the normalized full name beside the display name (pendingFull, every removal throughremovePending),FGI_Corepasses it, andCancelFormatches on it; the bare name is used only for an entry queued without one.
Tests/zz_protected_welcome_spec.lua adds a same-name-other-realm example and a departure seen only
in the event log.
Fixed — a member who received the guild policy could rewrite a guild announcement on their own client
Self-audit finding 2 (2026-09-16), unreviewed. AnnounceTab.RefreshPolicySnapshotsFor was gated on
the tag alone (finding 9), but gmPolicy is one record every member receives, so the tag is on a
plain member's client too. A member whose personal profile shares an id with a guild snapshot (a
template carries announcement profiles as they are) rewrote their copy of the guild announcement on
Save, or on a rotation arrow since this release, and then posted it in their own wording. It now takes
the Filters tab's gate, PolicyIsOurs() or canEditPolicy(), with policyIsOurs exported from
GUI/Tabs/Filters.lua as addon.PolicyIsOurs rather than copied. Finding 9's cases still refresh:
the author during the login window, and a revoked delegate's own record.
Tests/zz_announce_policy_spec.lua adds a receiving non-editor (refused) and a receiving officer
(refreshed).
Fixed — the Filters row showed the guild's criteria with nothing saying so, and Scan Groups' next scan
Self-audit findings 4 and 1 (2026-09-16), unreviewed.
- Filters row. A row showing the enforced guild copy loads the member's own copy when clicked, and
a member who cannot edit policy only ever saves their own copy, so the row did not change and read as
"Save did nothing". For that member the row is now named
<name> (guild), like the read-only guild rows; an officer, whose Save refreshes the guild copy, sees it unmarked. The row click loadsrawName.Tests/zz_class_filter_queries_spec.luarenders both cases. - One builder for a pass.
ScanGroups.PopulateQueue's standalone bucket was a second copy offn.freshPassQueue. That function now takesexclude(the scan names a group already runs) and builds the bucket;PopulateQueueis split into a side-effect-freeScanGroups.BuildBucketsplus activation;freshPassQueuegains the nil-range guard the copy had. The Scan tab's Next scan past the end of a bucket now names the next bucket's first query, or the first query of a rebuilt grouped pass, instead of the ungrouped queue.Tests/zz_scan_queue_population_spec.luaasserts the standalone bucket equalsfreshPassQueueminus the grouped names.
Fixed — "Assign to" in the Wingman right-click menu could not be opened
Field report (Vishiswaz): "doesn't seem to do anything on mouseover, isn't interactable". The row in
GuildRosterTab.AddBatonEntries carried notClickable. Blizzard's UIDropDownMenu_AddButton turns
that into info.disabled: the button is disabled, an invisible button covers it, and the expand arrow
is disabled, so UIDropDownMenuButton_OnEnter, the only thing that opens a hasArrow submenu, never
runs. Identical in the classic_era, classic, anniversary and live trees. The row now uses
keepShownOnClick. The same row is on the Guild Roster tab's baton menu, which had the same fault.
Tests/zz_wingman_step_menu_spec.lua asserts the row's flags; the harness does not model the
dispatch itself.
Changed — "Wingman paused" says so once per instance visit, not every 30 seconds
Operator: "this is firing too often, make it less often". The overlay drains on every click, so the
30 s throttle still printed the line every half-minute of clicking for a whole dungeon. It now prints
once when a paused drain first happens, and is re-armed by a drain outside instanced content or by
Start. Tests/zz_wingman_instance_pause_spec.lua gains once-per-visit and next-visit examples.
Changed — the zone step searches every open-world zone, not only the zones the first 50 results stood in
Vishiswaz (TBC): "The subdivision by zone should be searching every single eligible zone, not just the ones that came up in Search 3." The operator reopened the 2026-09-14 sample-only decision: "change it the way he wants it". A capped "70 Troll Hunter" returns 50 players from a handful of zones, so a Troll Hunter anywhere else was never asked about.
fn.zoneFanOut now lists the zones the capped query's rows stood in first, in row order, then every
other zone from fn.getWorldZones(), minus IsInQuietZone (the built-in instance list always, the
user's own list while their toggle is on). The zone list is data, like the instance list:
tools/instance-areas.lua now also writes FGI_WORLD_ZONE_AREAS into FGI_InstanceAreas.lua, every
zone of every world map per flavour, and fn.getWorldZones resolves the ids through
C_Map.GetAreaInfo so the names are the client's own in every locale. Phased copies of one zone
collapse to one name. An empty resolve is not cached.
Three corrections to the generator's zone rule, found by reading the list before shipping it:
- Capitals. A zone was a top-level area with sub-areas, and Classic Era's Orgrimmar, Ironforge and Undercity have none, so the busiest places in the game were missing on Era and TBC (and The Exodar on TBC and MoP). A top-level area carrying AreaTable flags 0x100 and 0x10 together now counts too, which every faction capital has in all four tables (312 on era/tbc; 2113848, 2097464, 69222712 later). The first cut tested 0x100 alone, and retail's Legion Dalaran is two dozen top-level rows with 0x100 but not 0x10 (Flags_0 18728): 23 fragments such as "Sewer Exit Pipe" reached the list, and the outdoor "The Violet Hold" (7895) un-silenced the dungeon of that name. A capital row with no sub-areas is also refused when a dungeon or raid of that flavour has its name. Silenced counts are unchanged (48/64/170/481).
- The development map. Open maps with
Directory"development" are skipped, so Programmer Isle is no longer a zone on Era and TBC. - Retail runs and story scenes. Single-zone open maps that are a run or a scripted scene cannot be
told from Deepholm or Exile's Reach by any Map or AreaTable field (InstanceType, flags, loading
screen and expansion all overlap), so
NOT_SEARCHED_MAPS/NOT_SEARCHED_AREASin the generator name them with a reason each: the two Horrific Visions, Torghast, Font of Fealty, Grand Time Adventure, Hall of Awakening, the old "Kul Tiras" row on Eastern Kingdoms and Faralohn, which never shipped. They only keep an id off the zone step; the instance list is untouched. Val and Naigtal look like these and are not: they are Midnight's weekly Showdown zones, so they stay.
Zones per flavour: Era 46, TBC 61, MoP 102, retail 216. KNOWN COST: one capped leaf now asks about every zone. On TBC that is up to 61 queries at the 5-second floor, about five minutes, and retail's 216 at 8 seconds is about half an hour. The progress bar steps back further when a zone split lands, because the split is still charged when it happens.
Not verified: that C_Map.GetAreaInfo returns exactly the string /who prints for these ids. The
instance list rests on the same assumption. TrinityCore's WhoEntry carries an area id for the client
to name, which supports it, but nobody has compared the two in a client.
Tests/zz_who_subdivision_spec.lua's zone examples were written for the sample and passed only because
the offline client could name no world zone. They now name three Classic Era zones and assert the exact
fan-out: sample first, then the rest, instances and the user's zones left out, another flavour's zone
left out, and an empty answer not remembered. Tests/zz_instance_areas_spec.lua pins the capitals, the
Outland zones on TBC only, Hillsbrad everywhere, no Programmer Isle, no Dalaran fragments, the retail
exclusions with Val and Naigtal kept, and that no area is both a zone to search and an instance in the
same flavour. FGI_WORLD_ZONE_AREAS is on the accepted-globals list.
Fixed — Hillsbrad Foothills was treated as an instance on TBC, MoP and retail
Follow-up to the same report ("it is doing SOME vanilla zones ... But not all of the time"). Reading
the generated instance list turned up [267] = { tbc = 1, mop = 1 } -- Hillsbrad Foothills (dungeon). An instance map on
those flavours names 267 as its own area, and tools/instance-areas.lua counted a top-level area as
a zone only in an elseif after the instance check, so a row an instance claimed never reached the
overworld refusal. Hillsbrad Foothills, with Tarren Mill and Southshore under it, was silenced:
everyone standing there was dropped at ingestion and the zone step never asked about it. The zone test
now runs regardless of the instance claim. Regenerated from the same 2026-09-14 builds: the only
changes are new refusals, 267 on tbc/mop/retail and 616 "Mount Hyjal" on mop (a zone there). A
doorstep with no sub-areas (Era's Onyxia's Lair) still fails the zone test and stays silenced.
Tests/zz_instance_areas_spec.lua asserts Hillsbrad is silenced nowhere.
A full read of the regenerated table (the operator: "look at the zone tables again, to ensure they
are accurate") found one more: 207 "The Great Sea" silenced on era/tbc/mop. It is a top-level row
inside the Deadmines map and also the open ocean off both continents (332 on Kalimdor), so every
player at sea was dropped; retail's 8276 "The Veiled Sea" scenario is the same shape. It is not a zone
with sub-areas, so the rule above cannot see it. The generator's sea rule: a name that is any
top-level area of a world map is refused when the row is a secondary region of its instance map (the
map also carries a row that collides with nothing, "The Deadmines") or a scenario.
Map.AreaTableID cannot identify the main row, because it is 0 on the Deadmines. The first cut
refused every colliding non-own row, and that un-silenced Azjol-Nerub, The Violet Hold, Auchindoun,
Tol Dagor and Tazavesh on retail, each the only row of its map with an outdoor doorstep of the same
name; it was replaced before anything shipped, and the spec now pins those five. Net change from the
sea rule: 207 refused on era/tbc/mop; retail refuses 8276, and scenario copies whose dungeon keeps the
name silenced (7777 The Violet Hold, 10272 Tol Dagor, 8635 The Frozen Throne), 8538 Twisting Nether,
14770 Azmerloth, and 4896 "The Frozen Throne" wing of Icecrown Citadel. KNOWN COST: a player on
the Lich King's platform in retail Icecrown Citadel is no longer silenced if /who reports that wing
by name. The rest of the raid still is.
The other outdoor-sounding names in the table (Starfall Village, Shadow Hold, Alcaz Island,
Stromgarde Keep, Greenstone Village, Coldridge Valley) are sub-areas of a zone outdoors, so /who
reports the zone and they cannot catch an outdoor player.
Fixed — the Dump window said filters were off, and its Filters button raised on a raid requirement
Found by reading lines the coverage sweep reported unrun. Both Filters readouts in Modules/dump.lua
gated on DB.realm.enableFilters, a legacy master switch the scan no longer reads
(soleAllowedClass: "no master enableFilters gate") and which defaults to false. So General said
Filters: Off and Filters listed nothing while the scan filtered normally, which is the wrong answer
in the window a player pastes when asked what their addon is doing. The one filter line it could print
formatted the raid thresholds as rioRaid[0], [1], [2] with %d; they are saved at [1]..[3], so
the nil [0] raised for any filter with a raid requirement.
Both now read fn.effectiveFilters(), the set the scan applies, and say (guild policy) when that set
is the guild's. The filter lines print the v2 wantedClasses / wantedRaces and label a legacy
filter's classFilter / raceFilter as exclusions. Tests/zzzz_dump_debug_windows_spec.lua (17
examples) covers these, the Areas list's own-zone marker, and both windows' drag-to-save, close button
and login restore, with the login handlers reached through fireEventFiltered rather than a real
PLAYER_LOGIN at every frame. Modules/debug.lua and Modules/dump.lua are now at 100%.
Fixed — every login cleared the invite and next-search hotkeys before binding them again
Found by reading FGI_Core.lua's unrun lines. On PLAYER_ENTERING_WORLD, purgeStaleClickBindings ran
ahead of fn.applyKeybinds and cleared any configured key whose action was CLICK FGI_CompactInviteBtn
or CLICK FGI_CompactScanBtn, then saved bindings.wtf. It was written in v2.2.1, when those were stale
pre-v2.1.8 bindings; since v2.2.4 they are exactly what applyKeybinds binds invite and next-search to.
So every login and /reload deleted the player's two working keys, saved that, and applyKeybinds
bound them again a moment later. Deleted as wholly redundant: the purge only looked at the three
configured keys, and applyKeybinds rebinds each with SetBindingClick straight after. (Whether
applyKeybinds' GetBinding(i) sweep sees user CLICK bindings is not established; the v2.2.1 note
said it does not, and nothing relies on it.) The same function's if GetNumBindings and GetBinding guard is gone
too: Blizzard's Keybindings settings page calls both bare in all four flavour trees.
Also from reading OnEnable: it cleared the debug window's anchors and pinned it TOPLEFT, while
Modules/debug.lua's PLAYER_LOGIN handler owns that position and centred the window without
clearing, so a player with no saved spot could be left with two anchors depending on which login handler
ran first. OnEnable no longer touches the window, and debug.lua clears before centring. Neither fix
has been seen in a client.
Tests — six examples for the parts of this release that had none
Asked by the operator ("did we write tests for all the changed things"), and the honest answer was
no. Added: the zone split's progress charge with the full zone list in play, at the default order and
zone-first (zz_who_subdivision_spec); the welcome queue's full names staying in step after a peer's
welcome removes the joiner before them (zz_protected_welcome_spec); fn.freshPassQueue with no level
range at all, and the Scan tab's Next scan at the end of a group and after the last group, rendered
and read (zz_scan_queue_population_spec); a click on a (guild)-marked Filters row loading the
member's own filter (zz_class_filter_queries_spec). Reasoned to fail on the old code, apart from the
zone charge, which covers a wider split than before rather than a change; not run against it.
Suite 3040/0. Review of the whole session requested from Peer Review (thread e35ca0ba), not yet
answered.
Re-measured the files this release touched that were at 100%: Modules/FGI_ScanGroups.lua,
Modules/FGI_Welcome.lua and Modules/FGI_GuildEvents.lua still are. functions.lua had dropped to
3107/3108 on one line not from this release: the presence guard in
fn.installConversationTabSuppressionHook on hooksecurefunc, FCF_OpenTemporaryWindow and
CHAT_FRAMES. All three exist before any addon on every flavour that reaches it
(Blizzard_ChatFrameBase_Classic.toc is LoadFirst: 1 in the classic_era, classic_anniversary and
classic trees; retail returns earlier), and only a spec deleting CHAT_FRAMES ever reached it. Deleted
with that citation; back to 3105/3105.
Tests — coverage: debug.lua, dump.lua, FGI_Placeholders.lua and FGI_Constants.lua to 100%
The four cheapest files left on the coverage list (61%, 75%, 70%, 91% on 2026-09-12). debug.lua and
dump.lua are covered by Tests/zzzz_dump_debug_windows_spec.lua, which also carries the Dump window
fix above. Tests/zz_placeholders_guildlink_spec.lua (13 examples) drives the club-finder link lookup
(no API, no club, no listing, a listing), the whisper GUILDLINK path (cached after one lookup, spaces
made unbreakable, refusal with a chat line, the G_LINK marker), and the estimator's whisper and split
modes (cached link, 120-byte allowance, GUILD, one message, N chunks against the real fn:messageSplit,
the unsplittable faildrop). Tests/zz_locale_derived_tables_spec.lua gains four examples for the
derived-tables net, reached through a specs-only FGI._guaranteeDerivedTables because it runs once at
registration: nothing missing, the empty-table repair, and each of its two messages.
Deleted rather than covered, each after reading the Blizzard source or the callers:
FGI_Placeholders.luaresolveGuildLink's plainfn.fetchGuildLink()fallback for a client withoutsecurecallfunction.Blizzard_SharedXMLBasecalls it at load in the classic_era, classic_anniversary and classic trees (SecureTypes.lua,MathUtil.lua), so no client lacks it.FGI_Constants.luaFGI:RebuildLocale(code), the back-compat shim for the pre-LibLocaleOverride builder: nothing called it.Locale/nlNL.lua's header named it as the fallback mechanism and now names LibLocaleOverride.
Suite 3070/0.
Tests — coverage: FGI_Core.lua to 100%
254 lines were unrun (74.9%). The slash specs had called them unreachable because FGI_Core.lua binds
its DB in OnInitialize, but in the whole suite an earlier file's PLAYER_LOGIN already runs OnInitialize
through AceAddon. Tests/zzzz_core_spec.lua (57 examples) delivers PLAYER_LOGIN to AceAddon's frame
alone, which is a no-op once the queue has drained, and reads the upvalue back off Console.FGIInput. It
covers the minimap launcher (main, compact show/hide/not built, settings, control-click group invite), the
chat menu's Black List entry (fast, prompt then advance, not the queue's head), the officer !fgi and
!blacklistGetList replies, the join handler's welcome and retail accept arms, OnEnable (no
SetItemRef hook, version check, auto-kick, slash registration, guild name), addon.MigrateRealmStores,
fn.applyKeybinds (bind, sweep, save, debug line, combat deferral) and its entering-world frame, the
three keybind buttons, and /fgi debug, blacklist, factorySettings, resetSettings,
resetWindowsPos, compact, kbstate and the rest of template.
OnInitialize runs once per Lua state (LibDBIcon refuses a second Register), so its two pieces with
several answers moved into addon.ReportLoginTemplate and addon.OpenDebugSession, like
addon.MigrateRealmStores before them. OnEnable had raised at its SetItemRef hook in every earlier
suite run, because the harness did not model that FrameXML global; requested and delivered the same
evening (harness d0f7343). The hook itself was then deleted (next section). Modules/debug.lua stays
at 100% after the fix above (its two redefined frame locals renamed).
Fixed — hovering a player from a connected realm showed a same-named local player's blacklist, invite and join lines
Modules/FGI_ChatTooltip.lua looked every store up by the link's name and then by the name with its realm
cut off, whatever that realm was. So hovering Bob-SisterRealm read a local Bob's blacklist reason, invite
stamp, race, join record and leave flag, and the roster fallback could show the local Bob's rank and
level. The stores are now read through fn.playerKeyed, which is keyedUnderEitherSpelling in
functions.lua returning the record instead of true: it drops a realm only when it is the client's
own, the rule the four invite gates already followed. The tooltip's roster fallback takes the same rule.
A blacklist record with no reason also printed Blacklisted: table: 0x...; it now says Blacklisted.
The same raw index was in two more places, both fixed through fn.playerKeyed. The race backfill
(Modules/FGI_RaceBackfill.lua) matched a /who result's bare name against join records keyed by the
roster's spelling, so it reported same-realm guildies as "not in FGI's stores"; and
MemberHistory:onJoin never carried the race of a recruit the scanner had filed bare. The backfill's
"already had race" count also compared the running fill totals, so once anyone earlier in the results
was filled nobody later was counted; it is per player now.
Fixed — /fgi help said /fgi requeue spends scan queries, and the README and CurseForge page described features that no longer exist
Locale/enUS.lua helpRequeue still read "Re-check queued candidates with a /who each (spends scan
queries)", untrue since the v2.12.1 presence sweep. It now says the check spends no scan queries.
A full pass of README.md and docs/Curseforge_Description.html against the code, each claim read in
its function. Removed as not in the code: a "Quiet List" of players, a priority "Custom List",
invite testing mode (no setting reaches testingMode), the filter-overwrite confirmation, the
"Remember All" setting, zone filters and a ChatThrottleLib claim for whispers (they call the chat API
directly). Corrected: the invite modes (Whisper + invite sends both on one click; Whisper only is a
private whisper, not a broadcast), race/class filters (include only), several filters on at once
(any one accepting keeps a player), the %player% placeholder (it is NAME; GUILD and retail
GUILDLINK were missing), the anti-spam window (days, 0 = forever), the Anti-Spam tab (X and
Re-check & invite, no right-click), "Auto-kick on blacklist" (the guild check asks Kick / Remove /
Skip), scanning "in the background" (every /who is one press), the scan engine's queue working
"automatically", the Race/Class table's source, the Eligibility tab's account rows, the minimap
clicks, settings paths (General → Scanning, Advanced → Sync, Guild Policy for duty messages), and
the README's claim that Recheck does not advance Seen (FGI_Presence.lua records a Seen day for an
online answer), the README Quickstart's "set ... zone" on the Filters tab, the README's zone step
(it still named only the zones the first 50 were in, before this release's every-open-world-zone
change), and the Statistics totals (they follow the chosen period, all-time only for All). The
README's CurseForge link is gone: Discord is its only external link. Pixel widths for hidden counters
were dropped rather than recomputed. Added to the
page: batch selection, the Last/Next scan line, level range source, held candidates and Guilded,
per-filter messages, Custom Scan Strict and Default Scan, Scan Groups, the soundboard, policy master
switch and extras, the blacklist search, the History / Statistics / Guild Roster tabs, member-history
and chat tooltips, race backfill, recruit-reply alerts, Only my invitees, the Invite Group window,
hotkeys, chat noise, notifications, reopen-after-reload, the Legacy window and Scan logs; and
/fgi roster list and /fgi help2 to both slash-command lists.
Found and left for after the release: in-game help text with the same drift (the Race/Class tab help says "maintained by hand", the History tab help describes the old per-invite log, the Quiet Zones help says scanning pauses while you are in a zone, a welcome message names the removed "Send welcomes on a click" setting, a subdivision tooltip says "halve the range", and the Anti-Spam tab points at a "Clear DB after" setting that no longer exists). Their keys are the English text, so each change touches all 31 locale files.
Changed — code no client could run, deleted after reading the flavour trees
- The chat-menu fallback.
FGI_Core.lua'shooksecurefunc("SetItemRef", ...), the follow-on dropdown it opened (ChatMenu.ShowFollowOn), the_registeredModernflag that decided it andChatMenu.PrintDiagnostic.Blizzard_Menu.tocisLoadFirstand loadsMenu.luafor every game type in all four trees, andMenu.ModifyMenuonly asserts its tag is a string, so registration always attached. The per-tagpcall, theMenupresence check andattachMenuTooltip'sSetTooltipcheck went too (MenuUtil.lua:84-93mixesSetTooltipinto every button), as did theSetText/AddLinefallbacks forGameTooltip_SetTitle/GameTooltip_AddNormalLine, which are SharedXML everywhere. Modules/FGI_UnitTooltip.lua's four retailpcallwrappers.enrichis registered only whennot gv.isRetail. Both registration branches stay:TooltipDataHandler.luais[ExcludeLoadGameType vanilla], so Classic Era takes theOnTooltipSetUnithook and TBC/Mists takeTooltipDataProcessor.GroupInvite:Close()(no caller),fn.backfillRacesFromCache(no caller), and presence checks in the group-invite panel, race backfill and chat tooltip on modules that load ahead of them in every TOC.- Four single arms:
history.lua'shistory.invitesmigration (the key is in the AceDB defaults, so it is never absent);FGI_Dialog.lua'sinsetWindowBackgroundfallback (functions.luadefines both);init.lua's bareGetAddOnMetadataarm (C_AddOns.GetAddOnMetadatais in all four trees);FGI_Compatibility.lua's "Unknown" name (the fallback always sets a flavour) and itsFGI_DEBUG.versionDetectionprint (nothing writes that key).
The keybind event frames moved to file-scope upvalues, and combat now reuses one re-registered frame. A frame is never freed in the client, so every keybind edit made in combat used to leave another frame that re-applied the bindings when combat ended; offline, a frame reachable only through its own script could be collected, which made the entering-world example pass or fail on collector timing.
Tests — coverage: the chat menu, both tooltips, the group-invite panel and the race backfill to 100%
FGI_ChatMenu.lua 94/94 was 36/184, FGI_ChatTooltip.lua 148/148 was 28/170, FGI_UnitTooltip.lua
50/50 was 17/72, FGI_GroupInvite.lua 113/113 was 16/125, FGI_RaceBackfill.lua 84/84 was 11/93;
history.lua, FGI_Dialog.lua, init.lua and FGI_Compatibility.lua reach 100% by the deletions above;
functions.lua, FGI_Core.lua and FGI_MemberHistory.lua stay at 100%. Every file in the first measured
chunk of twenty is now at 100%. New specs:
zz_chat_menu_spec (11), zz_chat_tooltip_spec (12, including the connected-realm examples),
zz_unit_tooltip_spec (9, reloading the module against each hook API), zz_group_invite_spec (6, the
real panel and RowList rows), zz_race_backfill_spec (9, including the per-player count), and one
example in zz_member_history_spec. Two of them pin RealmResolver.Resolve to unknown: its name cache
outlives spec files, and an earlier file's Bob decided which realm a bare Bob got.
Harness requests: Menu.ModifyMenu with GameTooltip_SetTitle/GameTooltip_AddNormalLine (delivered
as ae94ed6), after a local stand-in in Tests/support/addon.lua that is now deleted. Whole suite
3175/0.
Tests — harness pin moved to 7d63d11
From 77cba8f, through 1af5f84. Carries SetItemRef, the Menu model and the tooltip helpers above, the
raw CHAT_MSG_ADDON sender as Name-Realm for same-realm senders, GetNormalizedRealmName keeping
apostrophes, Ambiguate following its documented contexts, and frames.reset() raising on a handler
added mid-dispatch. Every Adoption entry since 77cba8f asks for nothing or a suite run; the suite is
green with no spec changes beyond deleting the two stand-ins.
Tests — harness pin moved to 77cba8f
34107ac (mouse propagation on both frame models) and d09f6df (FriendsFrame starts hidden) among
them. Full suite green with no spec changes; the Wingman specs' SetPropagateMouseClicks
stand-ins only install when the method is absent, so they are now inert.
Tests — the /who harvest count now counts only LibWho's reads
Tests/zz_who_result_count_spec.lua counted every C_FriendList.GetWhoInfo call, and
LibGuildRoster-1.0 (a real sibling library the suite loads) now also reads each row on
WHO_LIST_UPDATE for its sister-guild sightings, so every count doubled (100 for 50, 14 for 7). The
counter now counts a call only when its caller's source is LibWho.
Tests — the "client without click pass-through" Wingman example never modelled that client
Tests/wingman_prompt_spec.lua removed the propagation methods with = nil to stand in for a
client that lacks them. Frame methods sit on a shared prototype reached through __index, so nil
only drops an instance override and the read falls through: to the hollow layer's truthy no-op at
the current harness pin, and to the real method from harness 34107ac on. The example passed only
because the no-op getter returned nil. The methods are now set to false, which shadows the
prototype and reaches Wingman.lua's not overlay.SetPropagateMouseClicks branch as intended.
Raised by WoWAPITesting before the pin moves.
[v2.13.3] (2026-09-14) — a capped level band is cut where the players are, not in the middle
Changed — a level band that comes back full is cut at the level the sample is piled on, so a max-level realm reaches 90-90 in one split instead of seven
Field report (Spacedoc, Discord, 2026-09-14), retail: "if you have 15 people on levels 1-89 and
50+ on level 90 ... it is around 6-8 [splits]". Traced through the arithmetic halving it was
exactly seven: 1-90 → 46-90 → 68-90 → 79-90 → 85-90 → 88-90 → 89-90 → 90-90, every one a capped
/who returning the same level-90s again, plus the seven low halves each of those left — fifteen
level-only queries before the race/class fan-out that actually finds anyone could begin. The
reporter's count was right and the answer given on Discord ("three, maybe five") was not.
The cut now comes from the rows. The 50 rows a capped query returned are a sample of who is
standing in the band, and fn.levelSplitPoint(a, b, rows) cuts at the sample's upper median level
when that level holds at least half the sample — a pile — and halves the band as before when it
does not. For the reporter's realm the median is 90 and the first cut is 1-89 / 90-90: the
1-89 half returns the levellers in one uncapped /who, and the seven splits are one. The upper
median rather than the lower so a sample split evenly between 89 and 90 cuts between them; a pile
sitting on the band's floor is isolated the mirror way (1-1 / 2-90). Rows outside the band are
ignored and no usable row at all means the old middle. No setting, no new tag, nothing for a user
to configure — the same algorithm, cutting in a better place.
The pile guard is measured, not cautious, and it is the part worth reading. Nobody documents
which 50 a live /who returns when more match, so Tests/zz_level_split_point_spec.lua (29
examples) does what the Discord thread could not: a fake /who server over four synthetic realms,
the whole scan drained through the real registered callback under both cut rules, under three
server orderings (shuffled, lowest-50-first, highest-50-first), and every /who counted. An
unguarded median was worse than halving on an evenly spread realm whenever the server hands
back the lowest 50 — 37 /who against 23 — because the sample then describes only the bottom slice
of the band and its median peels the band off four levels at a time. With the guard the spread
realm is the old halving query for query under every ordering (asserted), and the piled realms
come in at or under it everywhere:
| realm | server returns | halving | this |
|---|---|---|---|
retail-shaped 1-90, 40 levellers + 1,500 at cap |
shuffled | 32 | 20 |
| lowest 50 first | 31 | 21 | |
| highest 50 first | 31 | 19 | |
classic-shaped 1-60, 120 levellers + 600 at cap |
shuffled | 29 | 23 |
| lowest / highest first | 30 | 24 | |
evenly spread 1-90, 400 players |
any | 23 | 23 |
starter pile 1-90, 300 at 1-10 + 40 above |
shuffled / lowest / highest | 70 | 70 / 64 / 68 |
The progress denominator is untouched: a band of n levels is priced n·leaf + (n−1) and two halves
of n levels price to the same number wherever the boundary falls, so the conservation the
subdivision spec asserts holds by identity — pinned at an off-centre cut. The descending-sweep
setting is applied after the cut as before.
Docs — the scan's "how a full search narrows" is now described where the features are
README.md never said how a search gets past the 50-result cap at all; the HTML said it in one
bullet. Both now carry the four-step narrowing (level, race, class, zone), that its order is a
setting, and that a full level band is cut where its players are. The HTML's guild-integration
line also lists MoP Classic, which it had left out.
[v2.13.2] (2026-09-14) — every officer online announced the same leaver, the scan still reached players inside instances, a grouped welcome short-changed the last joiner, and on Classic a recruit's reply never reached their record
Fixed — with several officers running FGI, a member leaving was announced to officer chat once per officer
Field report (Vishiswaz, TBC): "more than 1 person online with fgi in officer chat causes multiple
o chat messages for people leaving the guild" — two identical <FGI> Player Koje-Dreamscythe has been blacklisted. Reason - Left the guild lines, one from each officer's client, a second apart.
Reported as a regression, and it is one from the reporter's side: v2.13.0 made the auto-blacklist
route work on Classic for the first time, so the duplication it always had became visible.
Every client running FGI is in the same OnMemberLeft callback for the same leaver, and every one
of them called fn:blackList, which posts the officer line unconditionally. The entry landing
on every client is right — it is the same entry, and DeltaSync reconciles it — but the line has
to have one sender. fn:blackList takes a fourth argument, auto; both departure routes in
Modules/FGI_GuildEvents.lua pass it, and an automatic add posts the officer line only from the
elected announcer (Recruiter:CanPostAutoNotice, the announcer election under a new name — the
same list Q4 gave the welcomer, and for the same reason: it is who speaks for the guild, and every
client computes it from the same policy and roster). A person's own /fgibl or Blacklist-tab add
is unchanged — one person, one line. KNOWN COST, and it is the same shape as the welcomer's: a
guild running no policy at all still falls open, so several officers with FGI and no announcer list
still each post. Falling closed there would silence the line for every guild that never configured
a policy, and in the commonest case — one officer running FGI — falling open is one line and correct.
Found beside it: the officer line was worded from the UI-language override (L) while the chat
print next to it used the client locale. Both the add and the removal line now go through
fn.chatLoc(), the rule every other line into somebody else's chat frame follows.
Tests/guild_events_spec.lua pins that both routes pass the flag; Tests/zz_functions_spec.lua
(new block, six examples) drives the real fn:blackList: a manual add posts whatever the election
says, an automatic add posts only when elected, the entry lands either way, no election module
means post, the setting off means silence, and the wording comes from the client locale.
Fixed — the list of places that count as an instance is generated from the client's own data, for every flavour, and the zone step no longer runs a /who into one
Field report (Vishiswaz, TBC): "fgi still searching in instances" with z-"The Botanica" on
screen. The operator's reading of it, verbatim, which is the whole of the fix: "filter out anyone
in a dungeon/instance/raid/etc from the who results so you don't invite someone in a protected
state" — and on where the data comes from: "you have to get that data for EVERY flavor of the
game and maintain it for EVERY flavor of the game. it's a pain, but we have to do it as the API
isn't working well."
The quiet-zone set was never complete, on any flavour. It was 45 hand-typed area ids (fewer
instances than vanilla alone has) unioned with a walk of the client's Dungeon-typed maps — a walk
that finds nothing on Classic Era, which files its dungeons as ordinary Zone maps, and that on
every flavour depends on a map-type flag meaning something it does not quite mean. So a /who row
standing in an instance the set did not name passed the gate, was queued, and was invited.
The set is now generated. FGI_InstanceAreas.lua is every top-level area of every instanced
map in the four flavours' own DB2 tables (Map.InstanceType ~= 0 joined to AreaTable, plus the
area each instanced map names as its own — Classic Era's Onyxia's Lair row says it is on Kalimdor
and only the raid map's own pointer catches it), produced by tools/instance-areas.lua from
wago.tools exports and regenerated per patch; the directions are in the tool. 511 ids across
Classic Era (49), TBC (66), MoP (173) and retail (489). fn.getStaticAreas resolves this client's
ids through C_Map.GetAreaInfo, which is what makes it right in every locale, and counts what
resolved; the Dump window's Areas button prints the count. The Dungeon-map walk stays as a second
source, unioned as before.
Each id names the flavours that silence it, and that is not decoration. A name is not the same place in every game: Zul'Aman is the raid on TBC and MoP and, since Midnight, a whole levelling zone on retail; area 206 is a "Westfall" inside the Deadmines on Classic Era and Utgarde Keep on MoP and retail. The generator refuses to silence any name that is also an overworld ZONE in that flavour — a top-level area with sub-areas, on a WORLD map — and prints every refusal so the decision is auditable. What a world map is took four cuts, each learned from a wrong one: any top-level overworld row (dropped the outdoor doorstep rows of Blackrock Depths, Onyxia's Lair, The Violet Hold, so the very dungeons the list exists for fell out); any open map (retail's Death Knight campaign phase is one open map holding one "Scarlet Monastery" with sub-areas, which made the real one scannable); two or more zones (made Deepholm, one zone on its own map, a non-world, so a scenario copy silenced the real zone); a loading screen (retail's story phases of Icecrown Citadel, Firelands and Ny'alotha have one, so the raids fell out). A world is an open map holding two or more zones, or one zone plus a loading screen with no dungeon or raid of that name — a single-zone map named after a raid is that raid's story phase; a single-zone map only a scenario copies is the real place. KNOWN COST: a recruit standing on an instance's outdoor doorstep is silenced with the instance (they are about to be inside anyway); on retail Eye of Azshara, Tol Barad, Ashran and Wintergrasp share their name with a real zone and are therefore not silenced there; and Torghast is refused because its lobby is an open map of that name, so its runs are scannable.
And the zone step reads the results through the same gate. The fan-out over a capped query's
zones is still the sample — the zones the 50 rows were standing in, as it has always been — but the
sample is now filtered by IsInQuietZone, the predicate the rows themselves go through, so an
instance a row stood in never gets a query of its own. That is the z-"The Botanica" line.
The instance list is baked in; the Quiet Zones toggle is the user's list only. The operator,
verbatim: "the 'built in instance list' is NOT part of the quiet zone configuration, it's baked
in." Until now IsInQuietZone read DB.global.quietZones over the whole union, so switching the
toggle off let players inside dungeons through and searched their instances. fn.getAreas now
tags each name "instance" or "custom"; an instance is skipped unconditionally, a custom zone
while the toggle is on, and a name on both lists is an instance (the user cannot opt a dungeon back
in by adding it to their own list). The Quiet Zones tab's master-switch tooltip is rewritten to say
so (enUS; the 30 other locales carry the old key and fall back to English for that one tooltip), the
Dump window's Areas readout marks the user's entries (your list), and fn.isInstanceArea is the
public form of the question. Pinned in zz_add_new_player_spec (an instance is skipped with the
toggle off, and stays one when added to the user's list), zz_who_subdivision_spec (no query into
an instance with the toggle off; a custom zone is searched again once the toggle is off) and the
end-to-end block below.
Reverted the same day, before release: a draft of this fix had redesigned the fan-out to
union every Zone-typed map in the client's tree behind the sample (~55 queries per capped leaf on
TBC). The operator: "i didn't need you to change the design." fn.getZoneMaps and the tooltip
that described it are gone; the tooltip says what the step does.
Tests/zz_instance_areas_spec.lua (new, 23 examples) pins the generated table's shape, the eleven
instances the two TBC reports named silenced on TBC — including the seven Vishiswaz marked with
question marks, driven end to end as /who rows through the real registered callback on a TBC
client: none is queued, the Nagrand control is, the toggle changes nothing, and a client that cannot
name them queues all twelve — the seven vanilla dungeons and the Season of Discovery instances on
Classic Era, Zul'Aman silenced on TBC and MoP but not retail, Blackrock Depths everywhere, Deepholm
and Westfall nowhere, Icecrown Citadel despite its story phase, the flavour a client reads (Wrath
and Cata read MoP's column, the nearest data), fn.getStaticAreas per flavour and its count, and
the Quiet Zones tab's PvP group derived from the same table and filtered to this flavour.
Tests/zz_who_subdivision_spec.lua (block rewritten, seven examples) drives the real callback: a
sampled instance gets no query whatever the toggle says, only the sampled zones are asked about, a
sampled zone the map does not name is kept, the custom list is honoured while the toggle is on and
searched again when it is off, and a batch standing entirely in quiet zones answers no axis.
Fixed — a grouped welcome went out on the first joiner's clock, so a late joiner could be greeted before they could read it
Field report (Vishiswaz, TBC): "the welcome delay is to make sure that people can see their
welcome message … when grouping up multiple joiners together, the timer goes based off the first
person invited in the group, not the last, which can result in the last person not seeing their
message." Exactly right: PostGuildWelcome set the window's due time once, from the first joiner,
and a rider joining in its last second was greeted one second later. A late joiner now pushes the
due time out to at least the welcome delay from their join; the window still closes on time
when nobody joins late, and the ten-minute life is still counted from the first joiner, so a stream
of joiners cannot hold a welcome open forever. welcome_spec pins both.
Fixed — on Classic, a recruit's first reply opened a second History record instead of marking the attempt "replied"
Found by the coverage pass on Modules/WhisperAlert.lua, which had never had its alert path driven
offline (17 of 71 lines). The reply was recorded as fn.recruits:onReplied(key) with key the
realm-qualified fn:fullPlayerName(sender) ("Zugar-Realm"), while the attempt is counted in
fn:invitePlayer as onAttempt(playerName) with the raw /who name ("Zugar"). The roll-up keys
every record through fn:normalizePlayerName, which is the identity on every flavour but
retail — so on Classic the two spellings were two records: "Zugar: 4 attempts, tried" beside
"Zugar-Realm: replied", and the one line the History tab exists for was never written. Retail
resolves both spellings to one qualified key, which is why no field report ever named it.
The alert now hands onReplied the raw sender, exactly as the attempt does.
Tests/zz_whisper_alert_spec.lua (new, 19 examples) pins it with an attempt followed by a reply
and one record at the end; the rest of the file drives the three cues, their three settings, the
deferred chat-tab flash (matched case-folded because FCFManager_GetChatTarget upper-cases a
whisper target, FloatingChatFrame.lua:2286), the login-burst window and the lockdown gate.
Fixed — a click during an encounter dropped a joiner's welcome whisper for good
Modules/FGI_Welcome.lua's chat-messaging-lockdown check sat inside sendWhisper, which the click
pump called after removing the entry from the queue — so the whisper a recruit was owed went
nowhere if the recruiter's next click landed inside a Mythic+, and the comment beside it still
described the timer era. The pump now asks API.InChatLockdown() once per click and holds the
entry, unsent and still queued, for the first click after the encounter, inside its ten-minute life
like any other. welcome_spec pins it: nothing goes out during lockdown, the count stays at one,
the next click sends.
Tests — thirteen more modules at 100%, and the branches that were deleted instead
The method that closed functions.lua — read each uncovered branch against the Blizzard trees
before driving it — over the logic modules. At 100% now: WhisperAlert, RecruitGoals,
AnnounceSync, MemberHistory, RealmResolver, Welcome, GuildEvents, AltGroups, WidgetPool, Baton,
SettingsProfile, Presence, intro; Recruiter, DeltaSync and Wingman driven to their last lines
(re-measure pending — the coverage tool timed out twice on the box while the harness ran its own
gate). Suite 2873/0.
DeltaSync (Tests/zz_deltasync_bridge_spec.lua, new): the host callbacks driven at the
bridge with a recording host — OnSyncAccepted requests the delta with our baseline, a refused
send and a version broadcast reach the debug log, an untrusted query is dropped, a merged
anti-spam entry leaves the live scan queue and frees the P2P session slot; /fgids and
/fgids sync; the MINOR-15 floor refusing an older DeltaSync in chat; the single-revision and
stored-canon arms for a DeltaSync without MakeHashEntry / ComputeHashV2; the recruits
item stamped by statusAt. IsTrustedSender is one line now: LibGuildRoster's
IsInGuild answers a boolean on every path (its own pre-build arm scans the live roster), so the
hand-rolled roster scan and the "can't verify, allow" fallback behind ~= nil could not run — and
the second would have opened the gate.
Wingman (Tests/zz_wingman_session_spec.lua, new, plus the prompt and step-menu specs): the
configured step order and its fallback, the announce step (posts, falls through when nothing
posts, skipped in lockdown), the protected-state pause, auto-stop, the scope line's name caps,
the lock tooltip, the overlay's own click (pump first, and nothing over FGI's own windows); a
start inside combat that arms and finishes on PLAYER_REGEN_ENABLED, keeping its duty
provenance; cancelling while armed and the toggle debounce; the session restore and its
retries, and the first duty evaluation waiting out the warmup — all through the load-time
timers the prompt spec already recorded; the step menu's tick writing the setting and
repainting, and its outside-click close; the auto-stop under a policy that bars both roles.
Deleted as unreachable on every flavour, not covered, each read in F:\Blizzard API Docs
per flavour tree: the UIFrameFlash / FCF_StartAlertFlash / CHAT_FRAMES feature-detects in
WhisperAlert (Blizzard_SharedXMLBase is AllowLoad: Both, Blizzard_ChatFrameBase is
LoadFirst: 1); five no-C_Timer fallback arms across WhisperAlert and FGI_Presence (every
UITimerDocumentation.lua declares it); Presence's "client does not expose
C_ChatInfo.SendAddonMessage" refusal; WhisperAlert's inner canaccessvalue(sender) retest and
pcall(fn.fullPlayerName), and RealmResolver's inline lockdown return — all three sit behind
API.RegisterChatEvents' wrapper, which has already asked about every argument and returned
during lockdown before the body can run; MemberHistory's addon.API feature-detect;
SettingsProfile's locale() pcall (FGI:GetLocale is in the first file of every TOC);
Recruiter's listForRole "unknown role" arm (every caller passes isRole first); AltGroups'
two identical return false arms. Deleted as code with no production entry point:
RealmResolver.NoteFullName and .Count, Recruiter:AssignedMap (superseded by
AssignedWire at S48).
WhisperAlert reads addon.DB live rather than a snapshot taken at PLAYER_LOGIN:
/fgi resetDB replaces the table, and the snapshot would have kept gating the alert's three
settings on the discarded one.
A suite leak, fixed: Tests/zz_functions_spec.lua's conversation-tab block set
CHAT_FRAMES, ChatFrame7 and ChatFrame8 to nil in its teardown, written when the harness
modelled none of them. The harness owns all three now, so every later spec file ran with no
chat-frame list and two dead frames — invisible until the first file walked the array, which
failed in the full suite and passed alone. The block now drives the harness's real
FCF_OpenTemporaryWindow / FCF_Close and stands in nothing.
The harness pin moves to 2c7279f. UIFrameFlash, FCF_StartAlertFlash,
GetNumGroupMembers, IsInRaid and UnitFullName were raised (inbox thread 02b2da6bd297)
and delivered the same evening; the stand-ins are gone, and the whisper-alert spec asserts the
overlay's visibility and alpha on the clock and the chat tab's alerting flag rather than the
call. One specs-only entry point came with it: fn.forgetConversationTabSuppressionHook,
because the addon hooks FCF_OpenTemporaryWindow once per session behind a guard and the
harness reinstalls that global per spec file, which drops the wrapper while the guard stays set.
Nothing in the addon calls it.
Fixed — closing the Settings panel could take the compact tray with it, because the visibility fallback read the wrong SettingsPanel
GUI/SettingsPanel.lua's ShouldBlockEscClose — the check both ESC proxies ask before hiding
their window — has a flag for the panel being open and a fallback that reads the panel's own
visibility "in case the hook didn't work". The fallback was spelled
(Settings and SettingsPanel) or InterfaceOptionsFrame, and inside that file SettingsPanel is
our module table (line 595), which has no IsShown, so the read fell through; and
InterfaceOptionsFrame is declared in none of the four Blizzard trees, so the second half never
resolved either. The fallback answered false on every flavour since it was written. It reads
_G.SettingsPanel now — Blizzard_SettingsPanel.xml declares name="SettingsPanel" in
classic_era, classic, anniversary and live alike. Not run in a client: whether the flag path
already covered every close on Classic, so the fallback never decided anything, is exactly what
this could not tell. Found by reading the function to drive the compact tray's ESC proxy.
Tests — the compact tray at 100%, its handlers driven for the first time
Modules/compactFrame.lua measured 590/764 on 2026-09-13: three geometry specs had pinned its
layout to the pixel and never pressed a button. Tests/zz_compact_frame_spec.lua (new, 48
examples) drives every handler the way the client would — GetScript and call — the eleven
tooltips (including the gate that silences all of them, Wingman taking over the >> and invite
tooltips, the help icon's 500 px width hint, the broadcast horn's Ready / countdown / activity
rows), the three drag handles ending in one TOPLEFT save, the corner resize saving its width and
refusing the button row's floor, expand / close / gear (with the next-frame re-show) / Wingman
(right-click menu, warmup hold, toggle) / both horns, the warmup countdown overlay, the picker's
own drag, resize and wheel and its name-tie order, the queue rows filled class-coloured and sized,
the wheel clamped to the list, the four row icons including blacklist's fast-vs-confirm split,
the ESC proxy's two refusals, and the login restore of a saved position and width, a junk save,
and the reopen opt-in. 753/753 after.
The login restore is cf.restoreSavedLayout() now, called by the event handler, so a spec
can drive it with a chosen save: the event frame is an anonymous file-local that unregisters
itself on first delivery, and frames.fireEvent reaches every registered frame in the suite.
Deleted as unreachable on every flavour: the three elseif SetMinResize fallbacks (tray
bounds, picker build, picker floor) — SetResizeBounds is in every flavour's
SimpleFrameAPIDocumentation.lua and SetMinResize in none; makeRowIcon's
Interface\Icons\ TexCoord crop — all four row icons are Buttons/ and RaidFrame/ art.
GUI/UI.lua:71 and GUI/LegacyMainWindow.lua:318 carry the same SetMinResize fallback and
are left for the GUI pass.
One harness gap, raised and delivered the same hour: _G.SettingsPanel was built shown
where Blizzard_SettingsPanel.xml says hidden="true", so the fixed fallback blocked every
ESC-close example offline (inbox thread 861ddf9e). The harness pin moves to 82eb99e
(contains 557390a, the panel hidden after every reset); the before_each that hid it by hand
is gone. Whole suite 2922/0 at the new pin.
Older releases have been moved to CHANGELOG_ARCHIVE.md — v2.13.1 and earlier, in full. v2.13.0 went section by section while it was the only version in this file (ten on 2026-09-09, twelve on 2026-09-10, eight on 2026-09-11, four on 2026-09-13, four on 2026-09-14, ten on 2026-09-16), and its heading and last section followed on 2026-09-16. v2.13.1 moved whole on 2026-09-16. The next candidate is v2.13.2, whole.
This mod has no additional files

