FastGuildInvite-v2.14.13
What's new
<FGI> FastGuildInvite
[v2.14.13] (2026-10-04) — saved templates show again, the baton can be handed on, policy pushes dated by the server clock, and Cataclysm races
Fixed — saved templates had no rows until a new one was saved (FGI_Core.lua)
Player report (Discord, 2026-10-02): "the Your Templates section only shows up after saving a new
template, even if you already have saved ones." GUI/TemplatesPage.lua builds one row per template
at file load, and at file load SettingsProfile:Library() is still its empty stand-in because the DB
is not bound yet. The only rebuild was OnLibraryChanged, which fires on a Store or Forget, so
templates from an earlier session had no Apply, Forget or Send until the player saved another.
OnInitialize now calls TemplatesPage:Refresh() straight after the login template is applied.
zz_templates_page_spec pins the call's position after addon.DB = DB, and that a template already
in the library gets a row on refresh. Not yet seen in game.
Fixed — the holder could not hand a role to anyone when the guild has no list for it (Modules/FGI_Recruiter.lua)
Player report: "FGI is not letting me assign the designated welcomer status to someone else ...
However, he was able to use it to take it from me." Recruiter:CandidatesFor returned nobody when a
role had no policy list, while MayHold (behind Take) falls open, so anyone could take the role and
the holder had no Assign menu. That contradicted the file's own rule that a guild with no policy can
pass the baton around freely. With no list it now offers every online guildmate who passes MayHold,
sorted, leaving yourself out. recruiter_spec pins it.
Not confirmed as this player's cause. The welcomer list inherits the announcer list when unset, and the same player pushes policy lists, so their guild may well have one. For that case:
Changed — a refused Take or Assign now says why (GUI/Tabs/GuildRoster.lua)
Both menu clicks discarded AssignBaton's refusal, so a refused assign did nothing and said nothing.
GuildRosterTab.ReportBatonRefusal prints one chat line naming the role and the reason (not on the
list, or cannot take duty right now). AssignBaton returns a third value, "assigner", when the
refusal is about the person assigning, so the line does not blame the target. Four new enUS keys
(batonRefused*), client locale through fn.chatLoc. The next report from this player should name
the gate that refuses them.
Fixed — names dropped out of the policy lists after Push policy to guild (Modules/FGI_DeltaSync.lua, GUI/SettingsPanel.lua)
Player report, WoW Forever (Discord, 2026-10-04, with before/after screenshots): after Push policy to guild, the first name ("Vishi Swaz", the pusher) was gone from the announcer, inviter and welcomer boxes, leaving "Sienara Drakos" in all three. The cause is not confirmed, and two changes ship for the two explanations the screenshots allow:
- The lists really changed: a save was dated by the player's own PC clock.
StampPolicySavestampedsetAtwithtime(), and every leaf canon is ordered by that number on every client (canonBeats). An officer whose PC ran fast therefore won every leaf against any later push, and the next exchange put their older lists back. Reading the code found no other writer that drops one entry: the only production writers of the three lists are the boxes' Accept (parsePolicyNamesresolves and de-dupes, never drops), the Clear buttons, and the merge inApplyPolicy; our own echoed push is refused as identical (ApplyPolicy, the equal-hash return). It is now stamped withGetServerTime(), falling back totime()where that is absent.policy_sync_specsteers both clocks and pins a save stamped by the server while the PC clock is an hour slow. Clients still on an older version stamp with their PC clock until they update. - Only the drawing was wrong. AceConfigDialog rebuilds the page from inside the button's own click
(
ActivateControl, the full-refresh branch). The button now asks for one more redraw after its click has returned.zz_settings_panel_specpins one redraw after the click and none inside it; its push examples now drain that redraw, which otherwise fired inside the Blizzard-panel example's clock and failed its re-feed count.
An earlier draft of this entry said the names came back once the tab was reopened. Nothing the player posted says that; the claim was mine and unsourced, and it is withdrawn. Neither change is verified in game.
Fixed — Cataclysm Classic had no Worgen or Goblin, and Wrath's race/class pairs (functions.lua, Locale/summary.lua)
fn.raceClassBranch mapped isCata to the wrath table (TBC plus Death Knight), and
Locale/summary.lua gave Cata the TBC race list. So a Cata scan never searched for Worgen or Goblin,
and never for Cata's new pairs (Human Hunter, Orc Mage, Dwarf Shaman, Tauren Paladin, Gnome Priest and
the rest). A new cata table is generated the same way as the others, from CharBaseInfo joined to
ChrRaces and ChrClasses for wow_classic build 4.4.2.60895: 12 races, 91 rows, every row
accounted for. summary.lua gains a Cata race list with Goblin (9) and Worgen (22). The README and
CurseForge caveats about Cata are replaced. Found on the way: wago.tools ignores build= when
product= is also passed and serves the product's current build (Mists, with Monk and Pandaren);
the functions.lua header now says so. Not changed: Cata still reads MoP Classic's instance list
(fn.instanceAreaFlavour).
zz_raceclass_branches_spec.lua:46 now expects raceClassBranch({ isCata = true }) == "cata"
(it asserted the defect, "wrath"). New examples pin the cata table (12 races, 91 pairs, Goblin,
Worgen, no Monk) and add cata to the branch list.
Still open from the same reports
- Templates not sending on WoW Forever. The reporter (2026-10-03): the sender's chat showed
nothing, and the receiver got nothing. Every path past
Wire:Sendprints a verdict (AceCommQueue reports a refusal asdelivered == false, ChatThrottleLib calls back on every non-throttle result) and a refusal to try prints its own line, so total silence points at the send never starting -- most likely the Send to box, which AceConfig commits only on Enter or its Okay button. Not confirmed.GUI/TemplatesPage.luanow prints "Sending the template X to Y..." the moment a send is handed over (new enUS key; two examples inzz_templates_page_spec), so the next report separates "never sent" from "lost in the queue". - Whispers to players already in a guild (crimsonmane, 2026-10-02). A full audit of the /who
library and the queue (2026-10-03) found the scan-time guild gate sound, and the leak AFTER it: a
stranger's guild is read once, from the /who that queued them, and never again.
fn:queueEntryStalere-checks only our own guild and the anti-spam stores;invitePlayerdrains the OLDEST row first (index or 1), so a deep queue always contacts the stalest candidate; and invite types 2 and 3 send the whisper before any server answer can exist. Only rows restored from a previous session are held for a fresh /who. That gap is a reading of the code, not a cause seen in the field: the operator's/fgi debugscan on WoW Forever (2026-10-03) rejected all 43 guilded players and queued 7 with an empty guild, and a hand-typed/whoof one of them confirmed no guild in the game's own Who list (which shows guilds, checked against a<Nameless>member). No change made for it; the next evidence needed is a real whisper to a guilded player captured with/fgi debugon. The two defects below were found on the way and are fixed.
Fixed — "already in a guild" lost when the invite and the reply spelled the name differently (functions.lua, Modules/Scan.lua)
The field screenshot behind the guilded-whisper reports: "You have invited Teksuo-Myrzael to join your
guild." then "Teksuo is already in a guild." The handler looked the reply up in pendingInvites by
raw key, and off retail normalizePlayerName keeps both spellings as written, so the invite was never
found. The player was not anti-spammed, the Type 4 decline whisper stayed queued under the qualified
name, and the next guildless sighting offered them again. The decline branch had the same lookup.
New fn.pendingKeyFor resolves the reply: exact key, then fn.playerKeyed's own-realm cases, then a
bare reply against realm-qualified keys when exactly one matches (two in flight is refused, not
guessed). promotePendingToAntiSpam, clearPending and the decline branch use it, and the
auto-decline and in-guild branches clear msgQueue under the queued spelling as well. Five examples
in zz_scan_system_handler_spec. Classic family only; retail and Forever normalize both sides alike.
Fixed — /who: retail and Forever read a missing guild as "no guild"; Classic lost rows after a gap (Libs/LibWho)
NormalizeWhoInfo_Retail wrote info.fullGuildName or info.Guild or "", so a row with no guild field
passed the scan's p.Guild ~= "" gate as guildless. The Classic path keeps nil, which the gate drops.
The or "" is gone from Guild and fullGuildName; pinned in zzzz_libwho_retail_timeout_spec. The
client documents fullGuildName as never nil, so this is a fail-closed guard, not this report's cause.
The Classic reader stored result[i] = info after skipping a nil row, leaving a hole that
for i = 1, #results could stop at or index past into addNewPlayer(nil), which raised and lost the
rest of the answer. It appends now, as the retail reader already did. No spec drives a nil row.
Tests — harness pinned at 09a0a8c (Tests/support/roster.lua, Tests/recruiter_spec.lua)
The WoWAPITesting pin moved from f6fd3d4 to 09a0a8c. Mainline is Interface 120100 there, so
recruiter_spec expects that. Harness 349b7c3 moved GetNormalizedRealmName onto the player's
realm (wow.realmName), and roster.setRealm only wrote guild.realm, which turned 41 connected-realm
specs red; it now writes both and roster.reset puts the player's realm back. Two env gaps found on the
way (STANDARD_TEXT_FONT, and UIDropDownMenuTemplate's 40x32 size) were staged here, delivered by the
harness as 6767dae, and the stand-ins deleted the same day.
[v2.14.12] (2026-10-01) — WoW Forever is recognised again, raid markers on Forever, and the blacklist reason on the kick prompt
New — the "blacklisted player is in your guild" prompt shows the reason (functions.lua)
Player request (Discord, 2026-10-01), against a screenshot of the prompt: "This screen should show
the blacklist reason". showNextKickPrompt now looks the entry up with IsInBlackList(name, "exact") at prompt time and adds a Reason: <text> line under the name. It reads both shapes the
store holds, { reason, time, blacklister } tables and the bare reason strings older saves carry, the
same way GUI/Tabs/Blacklist.lua does. An empty reason adds no line. It reuses the existing Reason
locale key, so no locale file changed. zzzz_blacklist_kick_popup_spec pins all three cases. Not yet
seen in game, including how the dialog's height takes the extra line.
Fixed — FGI never recognised WoW Forever; it ran as Classic Era there (FGI_Compatibility.lua)
v2.14.0 detected Forever as WOW_PROJECT_ID == WOW_PROJECT_MAINLINE plus an interface number below
100000. The Forever client reports project id 18. Measured in-client 2026-10-01 on build
1.60.1.70124 with /run FGI.gameVersion:PrintInfo(): Name: Classic (Fallback), Build: 16001,
Project ID: 18, Identified: false (guessed), Is Retail: false, Is Forever: false,
Is Classic: true. So from v2.14.0 to v2.14.11, every gv.isRetail and gv.isForever branch was
dead on Forever, and it ran Classic Era's code: the class, race and zone lists, the retail API paths,
the unit-menu combat and instance gates, and every Forever-specific fix shipped in v2.14.7 to
v2.14.11. Forever's Blizzard_ProjectConstants/ProjectConstants.lua defines only 1 and 2, so there
is no named constant. gv.isForever is now WOW_PROJECT_ID == 18, and gv.isRetail is true on
Forever as the design always intended. zz_version_detect_spec drove Forever as MAINLINE, the same
wrong assumption, which is why it stayed green. It now drives 18 and pins the pasted output.
Known cost: every retail branch now runs on Forever for the first time. Those branches were written and tested against live retail, not Forever. A Forever problem with any of them would be new in this version.
Not changed, and also checks for MAINLINE: the LibDBIcon-1.0 copy bundled only in the _Camelot TOC
(Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua:537, :576, :611), so it takes its non-retail path on
Forever. It is that library's code; left as is.
Fixed — ADDON_ACTION_FORBIDDEN SetRaidTarget() from a unit-frame menu on WoW Forever (Modules/FGI_ChatMenu.lua)
Field report on v2.14.11: picking a raid marker from a right-clicked portrait was blocked and blamed
on FGI. The stack ends in TargetFrame.lua:698, the line in the Forever tree (live's is 692).
The player's observation: open world, out of combat, on v2.14.11. Because of the detection defect
above, none of the retail menu gates (combat, instance) ran on Forever, so FGI added its entry to
every unit-frame menu there. The generator also now adds nothing to the seven marker-hosting
unit-frame menus on Forever, in or out of an instance (/fgi menulog says "unit-frame menu on WoW
Forever"). Whether retail's instance-only rule alone would be enough on Forever is untested. This
skip is the safer setting until it is. The chat, friends, Battle.net friend and guild menus are
unchanged. Known cost: no FGI entry when right-clicking a target, focus, party or raid frame on
Forever. zz_chat_menu_spec pins it. In-game, 2026-10-01, Forever, open world, solo: the
detection printed WoW Forever / Is Forever: true; /fgi menulog showed FGI adding nothing to
MENU_UNIT_PLAYER and MENU_UNIT_TARGET; setting raid markers from those menus raised no Lua error.
With the skip turned off (/run FGI.gameVersion.isForever=false, retail's gates still active),
/fgi menulog showed "FGI submenu added" on MENU_UNIT_TARGET once and MENU_UNIT_PLAYER three
times, and setting raid markers raised no Lua error either. So the error did not reproduce with FGI's
entry present on this build. The reporter was on v2.14.11, where the whole addon ran Classic Era code
on Forever; whether the error came from the entry, from that, or is intermittent is not known. The
skip stays as the safer setting.
[v2.14.11] (2026-09-30) — Hold warnings off by default, announcement fixes, and whisper tabs that wait for a reply
Fixed — wording that overstated what WoW Forever blocks
v2.14.9 and v2.14.10 said Forever "never lets" / "does not let" a welcome go out by itself. The evidence
is one player's BugSack report of ADDON_ACTION_BLOCKED on the timer-driven welcome send. The
CurseForge notes for both versions and the Forever note in the "Send welcomes automatically" tooltip
now say Forever blocked FGI's automatic welcome, and the v2.14.10 heading below is reworded the same
way. Behaviour is unchanged.
New — "Show recruiting hold warnings in chat"
Player report (Discord, 2026-09-29): the queueHeldNotice line read as spam on every press. Operator,
2026-09-30: "have a setting to hide these types of warning messages, so the users don't see them. have
it set so they don't see them by default." New DB.global.showHoldWarnings (default false, Settings
→ General → Scanning, order 9, catalogued under notify in FGI_SettingsProfile). fn.showHoldWarnings()
gates the four chat notices that explain a hold: queueHeldNotice, queueDroppedNotice,
protectedPauseNotice and inviterHeldNotice. Only the chat line is gated; every hold still happens
and the Scan tab's held count is unchanged. Recruitment-goal notices are not included -- they go
through RecruitGoals:Notice, which also raises the goal prompt.
Known cost: with the default, a player whose invites are held gets no explanation in chat. That is what was asked for.
The kept-queue setting (persistQueue) was already false in the shipped defaults, so a new install
has it off; a spec now pins both defaults. Existing players who switched it on keep their choice.
Specs: zz_invite_holds_spec (the notice examples switch the setting on; a new block pins the default
holding silently), zz_invite_spec, zz_settings_panel_spec.
Fixed — an announcement that did not go out still started its cooldown
Player report (Vishiswaz, Forever, 2026-09-30): "the announcement did not go out / the timer happened
/ and I was definitely in guild chat". Announce:Send stamps the profile's cooldown and the
rotation's timer and turn on the click that sends; the echo sweep reported the missing echo but never
undid the stamps. The pending record now carries what the stamps overwrote, and sweepConfirms
restores them via rollbackStamps, each only if it still holds our stamp, so a later post or a
guildmate's sync is left alone. The next click retries the post.
Known cost: a guild announcement's cooldown push to peers (announceSync:PushPost) is not recalled, so
guildmates keep that cooldown. And if a post really did go out but its echo was not recognised, the
retry posts it again.
announceMsgDidNotPost no longer says "check you're in that channel", which made no sense for Guild; it
now says the post never appeared in chat and the next click tries again. Why this post was dropped on
Forever is not known.
Specs in announce_channels_spec: rollback of a profile, rollback of a rotation's timer and turn, an
echoed post keeping its stamp, and a re-stamp surviving the rollback.
Fixed — the login hold on announcements could run out behind the loading screen
Vishiswaz confirmed the failed post came straight after a login. The 15 s LOGIN_ANNOUNCE_GRACE was
armed at PLAYER_LOGIN / PLAYER_ENTERING_WORLD, and both fire while the loading screen is still up, so
a slow load spent the window before the player could move; Wingman's first click then posted into a
guild chat the server had not joined us to. The session's first LOADING_SCREEN_DISABLED (present in
the Classic Era and Forever API trees) now restarts the window (Announce:LoadingScreenDone); later
loading screens do not. Wingman already passes a held click on to scan or invite (tryAnnounce only
consumes a click that posted). 15 s is unchanged; whether it is long enough on Forever after the load
is not verified. Spec in announce_channels_spec.
Announcements also hold while the chat API reports its server down: CHAT_SERVER_DISCONNECTED sets the
hold and CHAT_SERVER_RECONNECTED clears it (both events are in every flavour's ChatInfoDocumentation).
Nothing is stamped while held. A normal Classic Era login fired neither (observed in-game 2026-09-30), so
this covers a real outage, not the login window. Spec in announce_channels_spec.
Changed — a whisper tab opens when the player answers, not on every whisper sent
A player on Discord (rocky, 2026-09-30): "could it only spawn a whisper window when someone responds
instead of on every outgoing text?" The operator asked for it. WT:OpenKeys (the tabs) now lists only
conversations with replied set; the new WT:CapturedKeys lists every captured one, answered or not.
An unanswered conversation is still recorded from its first line and still kept out of the game's chat,
so the tab that appears on the reply already holds what was sent. The whisperMode hold now follows
CapturedKeys, not the tabs, so the game still opens no window of its own for an outgoing recruiting
whisper that has no tab yet. zz_whisper_tabs_spec updated to the new rule.
Fixed (unverified) — Guild Invite from the whisper tab's right-click menu was blocked
BugSack, 2026-09-30: ADDON_ACTION_BLOCKED on Invite() from FGI_WhisperTabs.lua run, called from
the menu row's OnClick, through ChatMenu.GuildInvite -> API.GuildInvite -> C_GuildInfo.Invite. The
call is inside a click, as the tray's own invite buttons are, and those go through. The one difference
found is that the row hid the menu before running the action. The action now runs first and the menu
hides after it. That the Hide is what cost the click its hardware event is not verified; it needs
an in-game test on the client that raised it.
Changed — functions.lua comments trimmed
At 517 KB it was over the Lua language server's 500 KB preload limit, so the file went unscanned. The
change-history narration in its comments was removed (257 KB, 5,947 lines). Checked by compiling it
and the previous version, with debug info stripped, and diffing the listings: the instructions and
constants are identical apart from this release's showHoldWarnings code.
[v2.14.10] (2026-09-29) — Wingman no longer stuck on a kept queue, and WoW Forever name fixes
Fixed — Wingman stuck on a queue kept from a previous session
Player report (Vishiswaz, WoW Forever, 2026-09-29): "wingman won't run because of this ... It doesn't
tell me HOW to run a scan to re-confirm them". Every queued row was HELD (restored by persistQueue,
guild not re-confirmed) and invite was first in the step order. tryInvite claimed the click whenever
the queue was non-empty, fn:invitePlayer could contact nobody and printed the held notice, and the
scan step -- the only thing that lifts a hold, via fn.releaseQueueHold in fn:addNewPlayer -- never
ran. The invite step now passes the click on when #list <= fn.queueHeldCount(). Not Forever-specific:
any flavour with a fully held restored queue hit it. queueHeldNotice now names the remedy (the >>
scan button or Wingman, and Decline to remove one by hand). Pinned in zz_wingman_session_spec, run
red first.
Fixed — WoW Forever: "Send welcomes automatically" showed ticked for a behaviour FGI does not use there
Peer Review, thread a08ac06a, finding 2. Since v2.14.9 Welcome:AutoSend() is always false on
Forever, but the Settings box still read ticked. On Forever it is now shown unticked and disabled,
and its tooltip says welcomes there always wait for a click. Live retail keeps auto-send; that it
works there is untested -- no block has been reported, which is not the same thing.
Fixed — WoW Forever: another officer's first-name policy stamp could read as ours
Peer Review, thread a08ac06a, finding 1. policyIsOurs (GUI/Tabs/Filters.lua) accepted
UnitName's first return as a match for the legacy setBy stamp. On Forever names are unique only as
"First Last", so a policy stamped "Vishi" by another officer read as ours, and that answer decides
whose guild policy we may overwrite. On Forever only the full name matches now; every other client is
unchanged.
Both pinned in zz_forever_names_spec, each run red by reverting its fix line in place and restored.
The check now fails closed (Peer Review reply 3): the Forever flavour is asked as well as the client's
RegionalUniqueNamesEnabled switch, in policyIsOurs and in fn.myName, which also joins the
surname itself when LibGuildRoster's UnitKeyName hands back the first name alone. Spec run red first.
Fixed — WoW Forever: the By officer list showed one officer twice
Peer Review, thread a08ac06a, finding 3. Before v2.14.9 an officer's stats row was keyed by the first
name alone; from v2.14.9 by the full name, so both rows were held and synced. At login
(MemberStats:onLogin, after the own-row refresh) a first-name row is dropped once a full-name row
with that first name is held; acceptRemote refuses such a row from a peer still on an older copy,
and a full-name row arriving retires the first-name one. Forever only. Names are split with the
client's own Constants.CharacterNameSeparatorConsts.CHARACTERNAME_SURNAME_SEPARATOR (a space only when
the constant is absent), mirroring Blizzard's NameUtil.SplitPlayerNameIntoParts, through the new
fn.surnameSeparator / fn.splitFullName, which fn.myName now joins with too. Two officers sharing a first
name lose the older one's pre-v2.14.9 row -- accepted, as the list is display-only. History's "Mine
only" keeps counting a first-name by stamp as ours, with the same accepted collision risk. Spec run
red first.
Changed — one answer to "is this me"
Peer Review, thread a08ac06a, finding 5. The UnitName("player") fallbacks beside fn.isCommSelf /
fn.myName are deleted: Announce's isSelf, Welcome:OnGuildChat, canEditGuildPolicy and the
policy push's setBy in GUI/SettingsPanel.lua, and the last line of policyIsOurs. None was ever
taken in production. announce_channels_spec and welcome_spec supply fn.isCommSelf in their
fixtures rather than loading functions.lua, whose standalone load needs the whole bootstrap chain;
the real one is pinned in zz_forever_names_spec.
[v2.14.9] (2026-09-29) — skip names in other alphabets, and WoW Forever announce and welcome fixes
New — skip names not written in the Latin alphabet
Feature request (Discord, 2026-09-29): "Auto skip characters with non-english characters in their
name", with a screenshot of two Chinese names in the compact tray. Settings > General > Scanning now has
Skip names not written in the Latin alphabet (DB.global.latinNamesOnly), off by default at the
operator's word. The game gives no language for a /who result (Forever's WhoInfo carries name,
guild, level, race, class, area, gender and season only), so fn.nameIsLatin judges the character
name's script: ASCII, Latin-1, Latin Extended-A/B and Latin Extended Additional pass; any other
non-ASCII byte (Han, Hangul, kana, Cyrillic, Thai, ...) does not. A realm suffix is dropped first. It
is checked at discovery in fn:addNewPlayer (counted as filtered, after unique) and again at invite
time in fn:queueRowFiltered, so a queue built before the switch does not keep inviting those names.
It cannot tell Spanish from English, and does not try. Pinned in zz_latin_names_spec.
Fixed — WoW Forever: ADDON_ACTION_BLOCKED on the auto-sent guild welcome
Player report, Forever, 2026-09-29: ADDON_ACTION_BLOCKED on C_ChatInfo.SendChatMessage, raised from
flush() inside Welcome:Pump(true) -- the C_Timer auto-send path added in v2.14.6. The Forever
client refuses that timer send even for GUILD. autoSend's block watch did catch it and fall back to
clicks, but only after the client had raised the error. Welcome:AutoSend() now answers false on
gv.isForever, so Forever never arms the timer and every welcome goes out on the next click. Live
retail is not known to block it and keeps the setting; that is not verified in-client. Pinned in
welcome_spec ("never auto-sends on WoW Forever").
Fixed — "Whisper tabs on the compact tray" could not be unticked
Player report (Vishiswaz, Discord, 2026-09-29): "The button to disable whispers on the compact tray
does not allow you to un-check it." The v2.14.8 setter in GUI/SettingsPanel.lua wrote
(not v) and false or nil, and x and false or y is always y in Lua, so unticking stored nil,
which get reads as on. It is now a plain if. The v2.14.8 spec wrote DB.global.whisperTabs
directly and never ran the setter, which is how it shipped; zz_whisper_tabs_spec now drives the
Settings checkbox's own get/set both ways.
Fixed — WoW Forever: "Announce to Guild did NOT go out" after the announcement went out
Player report (Vishiswaz, Discord, 2026-09-29): "announce to guild went out but it thinks it
didn't", with the post visible in guild chat and FGI's failure line three seconds later. Delivery is
confirmed by the post's own echo, and isSelf in Modules/Announce.lua compared the echo's sender
with UnitName("player"). On Forever that returns the first name and the surname separately, so
"Vishi Swaz" never equalled "Vishi" and every guild post timed out as undelivered.
isSelfnow asksfn.isCommSelf, the Forever-aware check v2.14.8 added for addon messages.Welcome:OnGuildChathad the same comparison for its own welcome echo, which on Forever read the player's welcome as a guildmate's; it asksfn.isCommSelftoo.fn.isCommSelfdepended on LibGuildRoster 1.1.1'sUnitKeyNameto join the two names and fell back to the first name alone without it. It now joins them itself in that case, with the client's surname separator, the way the library does.Constantsadded to.luarc.json.- Pinned in
announce_channels_spec(the matcher asksfn.isCommSelf) andzz_forever_names_spec(the full name is recognised withoutUnitKeyName). The Forever example inannounce_channels_specfirst failed with the realUnitNamestub because that file loads Announce withoutfunctions.lua; it now pins the delegation, and the name rule is pinned where it lives.
Fixed — WoW Forever: the rest of FGI knowing the player by their first name only
Swept after the announce fix: every other place that took UnitName("player")'s first return as
the player's name. New fn.myName() is the one answer -- the full "First Last" on Forever (through
LibGuildRoster's UnitKeyName, or the same join done here without it), UnitName's first return
everywhere else -- and fn.isCommSelf now uses it. Moved onto it:
- the policy-editor check in
GUI/SettingsPanel.lua(canEditGuildPolicy), now compared without case, and the Add member box Title-cases each word so "vishi swaz" becomes "Vishi Swaz"; - the policy's
setBystamp when pushing, andpolicyIsOursinGUI/Tabs/Filters.lua, which accepts either spelling so a policy stamped before this release is still recognised; GuildEvents:OnLeave's "is this our own departure" test, throughfn.isCommSelf;MemberStats:myKeyand its row name, so the By officer line keys a Forever officer by full name;- recruit attribution (
rec.by) andRecruits:isMine, which still counts a first-name-only stamp as ours; - the anti-spam list's Inviter and the blacklist's Blacklister columns.
On every other client each of these reads exactly what it read before. A Forever officer's By
officer row stamped before this release stays under the first name; the new row carries the full one.
Pinned in zz_forever_names_spec; guild_events_spec's stand-in fn gained the isCommSelf the
real one has. Found by a literal search for UnitName("player"), so a site spelled differently was
not covered.
[v2.14.8] (2026-09-29) — WoW Forever fixes, and a switch for the whisper tabs
Fixed — WoW Forever: "Hide outgoing whisper echoes" did nothing
Player report, Forever, 2026-09-29. fn.refreshWhisperHideDeadline keys the recipient of each FGI
whisper through fn:fullPlayerName, and fn.hideWhisper looks the echo's name up the same way. On
Forever one side carried an appended realm and the other did not, so the two keys never met and every
echo showed. With fn:fullPlayerName following the regional-names rule (below), both sides are the
full name. Pinned in zz_forever_names_spec by driving the real send-then-echo pair.
Fixed — "No player named X is currently playing." repeated in chat, every game version
Player report on Forever, 2026-09-28, three identical lines in four seconds; the operator, 2026-09-29: "it should be fixed in all version to be behind a debug flag" and "it's not only forever, it's all game versions that have this issue".
It is the server's reply to a whisper that could not be delivered, and FGI sends one recruit message as several chunks, so one unreachable recruit printed it once per chunk -- on every flavour.
- All versions:
fn.hideNotFound, aCHAT_MSG_SYSTEMfilter registered at load throughaddon.API.AddMessageEventFilter, hides that line when the name is one FGI itself just whispered (addon._outgoingWhispers), unless debug mode is on. The pattern is built from the client's own localizedERR_CHAT_PLAYER_NOT_FOUND_S. A whisper the player typed to anybody else still shows its error, and FGI's own not-found handling (Modules/Scan.lua) is unaffected -- a chat filter changes only what the chat window draws. - Forever only, the cause of every whisper failing there:
fn:formatNameForRetailAPIsent "First Last-SomeRealm" whenever the realm was not the player's own internal realm, and the server rejects that. With regional names on it now always sends the full name with no realm. (That the server accepts "First Last" follows Blizzard's own whisper UI; not verified on a Forever client.) - Forever, addressed addon messages too: a follow-up from the reporter -- "You were online / When
it happened" -- ruled out an offline recruit. FGI's baton and announce-cooldown catch-up replies
(
Modules/FGI_Baton.lua,Modules/FGI_AnnounceSync.lua) whisper the asker by the name the addon message arrived with, realm included. Bothsendhelpers andaddon.API.SendChatMessagenow address WHISPER sends throughfn.whisperTarget, which drops the realm only on a client with regional names and returns the name untouched everywhere else.fn.whisperTargetdelegates to LibGuildRoster'slib:WhisperTargetwhen the library carries it (1.1.2, thread 54277063 -- the library's ownherereply and sister-pull sends had the same fault and now use it), and keeps its own fallback for an older library. DeltaSync fixed its own whisper sends the same way (thread dd611b26). Both library fixes reach players only when those libraries release. Which of the sends produced the reported lines was not established. AceCommQueue and VersionCheck also send whisper-addressed messages and were not looked at. - Pinned in
Tests/zz_forever_names_spec.lua: hidden for FGI's own target, shown for a bystander, shown with debug on, other system lines untouched, and the Forever send name.
New — a setting to turn the compact tray's whisper tabs off
A player asked (Discord, 2026-09-28): "Is there a way to turn this off in the settings? I prefer the
whispers in my normal/general chat feed." There was none. Settings > General > Appearance now has
Whisper tabs on the compact tray, on by default (DB.global.whisperTabs, stored as false only when switched
off). Off, WT:Enabled() stops everything in Modules/FGI_WhisperTabs.lua: no whisper is recorded, the
chat filter keeps nothing out of the game's chat, the open-tab list is empty so the strip hides, and
WT:Refresh releases the whisperMode hold. Switching it off takes effect at once, not on the next
whisper. Pinned in zz_whisper_tabs_spec.
Fixed — WoW Forever: FGI did not recognise the player as themselves
Player report, WoW Forever, 2026-09-28: the Settings page read "Current designated inviter: Vishi Swaz" to Vishi Swaz, where TBC correctly says "You are the designated inviter right now." The reporter's own conclusion: "since realm names are gone, and the character names cannot be reused in different rulesets for Forever, the appended realm name should probably be abandoned/disabled for Forever".
On Forever a character is "First Last", unique across the region, and UnitName returns the first
name and the SURNAME. Two things keyed one character two ways:
- LibGuildRoster built the player's own key from the first name plus a realm, while the roster
had the full name. Its 1.1.1 fixes that (full name, no realm, when the client's
RegionalUniqueNamesEnabled()says so); the designated-inviter readout and the election's "is it me?" read the library's key, so that half needs LibGuildRoster 1.1.1 released. - FGI's own keys did the same:
fn:normalizePlayerNameandfn:fullPlayerNameappended a realm, andfn.isCommSelfcompared againstUnitName's first return only. On a client with regional names they now take the library's rule -- the full name, no realm appended, an API-added-Realmdropped -- delegated toLibGuildRoster:CanonName/UnitKeyName, with the same client switch as the fallback. Every other client is untouched: the switch is absent there. - Saved keys are repaired at login (Peer Review a04ec605, release blocker). Everything FGI stored
on Forever before this release carries the realm it used to append, so a player blacklisted as
"First Last-Realm" read as clean under "First Last" and would be invited again. New
addon.MigrateForeverKeys, run fromaddon.MigrateRealmStoreson every Forever login, rewrites the hyphenated keys ofblackList,blackListRemoved,alreadySended,leaveandfactionrealm.recruitsthroughfn:fullPlayerName, keeping the newer entry when both spellings exist (and fixing a recruit'sname). Every login rather than once, because guildmates on v2.14.7 keep syncing realm keys in;DB.global.migratedToForeverKeysrecords that it ran. It also stops that function's Classic-family pass, which APPENDS a realm to every bare blacklist key, from running on Forever -- it would have undone the repair on the next login. That pass is barred by thegv.isForeverflavour as well as the runtime switch, and the repair runs again fromOnEnable(PLAYER_LOGIN), because whetherRegionalUniqueNamesEnabled()already answers duringOnInitializeis not verified. - FGI's fallback copy of LibGuildRoster's Forever rule had drifted (Peer Review a04ec605 F5). A
new spec runs both on one list of names: the fallback kept the space in "Name - Realm" and a stray
colon.
regionalKeynow cleans the name step for step asCanonNamedoes. - Names cut at the space, swept for Peer Review a04ec605.
/fgi template send <n> <player>matched the player with%S+, so a Forever "First Last" was refused as a usage error; it now takes the rest of the line.TemplateWire:Sendwas the one whisper-channel send not addressed throughfn.whisperTarget, and now is.fn:parseName([^%s-]+) had no caller and is deleted. The realm-strippers^([^%-]+)inFGI_Welcome,FGI_WhisperTabsandFGI_RealmResolver, and the/fgiblparser, keep a space and need nothing. RegionalUniqueNamesEnabledadded to.luarc.json.Tests/zz_forever_names_spec.luapins the Forever keys and the control (no switch, realm still appended). The name examples were not run red against the old code; the four login-repair examples were, and all four failed.
Fixed — a Lua error on WoW Forever when hovering the world after an FGI tooltip
Player report, WoW Forever, 2026-09-28: LibLocaleOverride-1.0.lua:757: attempt to compare local 'text' (a secret string value, while execution tainted by 'LibLocaleOverride'), from FGI's GameTooltip:Show
hook (functions.lua, refontTooltip) under Blizzard's world-cursor unit tooltip. addon.Tooltip.Owner
flags the shared GameTooltip as FGI's, and only OnHide cleared the flag, but Blizzard re-owns the
tooltip for a world-cursor unit without hiding it. So the flag outlived FGI's tooltip, FGI re-fonted
Blizzard's lines, and on Forever/retail those lines can hold secret strings.
addon.Tooltip.Ownerand the minimap broker tooltip now record the owning frame (addon._ttOwner).- The Show hook re-fonts only while
GameTooltip:GetOwner()is still that frame; otherwise it puts the line fonts back, drops the flag, and touches nothing else. zz_tooltip_font_restore_spec"never walks a tooltip another frame took over without hiding it first" pins it, and was run red with the owner check disabled.
Fixed — CurseForge listed LibDBIcon as embedded, so the app never installed it
v2.14.7's Forever-only copy of LibDBIcon kept upstream's first line,
--@curseforge-project-slug: libdbicon-1-0@. The BigWigs packager reads that keyword in any shipped
Lua file and records the library as an Embedded Library on the upload, which contradicts
.pkgmeta's required-dependencies. The project page showed LibDBIcon-1.0 as "Embedded Library"
(operator's screenshot, 2026-09-28), so the CurseForge app did not install it for the six flavours that
need the standalone copy. The keyword line is removed from Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua and a
comment says not to restore it; the Forever embed itself still loads. A CurseForge relation belongs to
the uploaded file and one zip serves every flavour, so the relation cannot differ per flavour: Required
is the one that works for all seven.
After this release uploads, check the CurseForge project's Related Projects: LibDBIcon-1.0 must read
"Required Dependency". A harness contract asks for a .pkgmeta checker that would have caught this
(WoWAPITesting thread efb04252).
Changed — chat lines nobody asked for are debug-only
At the operator's request (GuildRoster thread cbda57eb: "people are going to whine about this, these types of prints need to be behind a debug on/off flag and a setting in the control panel"):
- The two "Scan timer restarted" lines in
fn.watchForeignWhoprint only withDB.global.debugon (Settings "Debug mode", or/fgi debug). LibGuildRoster's sister-guild /who triggered them at every login. The button countdown is unchanged. - The three keybind "OnClick fired" traces in
FGI_Core.lua(F5/Invite, F6/NextSearch, Announce) are debug-only too; they printed on every hotkey press. - Every other chat line was read and kept: each answers a press, is already opt-in, reports queued work that was dropped, or answers a slash command.
Changed — packaging and titles
.pkgmetacarries no comments at all (operator, 2026-09-28: "can you just take all the damn comments out of the .pkgmeta? they do nothing but cause issue").- Every TOC's title is the fleet brand colour:
## Title: |cffFF8000FastGuildInvite|r(DeltaSync thread 8de14b7d). wow-version-replication.ps1treats a "Deleted" whose source file still exists as a change, so a save-by-replace can never remove a live file from the other installs (TOGProfessionMaster thread eb9d3b3f).
Tests
zz_queue_seen_spec: the recheck-driver sweep matched onlyrefreshqueue, while its comment claimed to back every deletedfnname; it now coversqueuerefresh,queueseendaysandseenpassas well.altgroups_spec: the persistence check is a paired positive (A.Feedpresent, nothing named store/accept) rather than the module's complete key set, which went red on any harmless new function.zz_recruit_goals_speckeeps its whole-surface check and now says the surface is the contract there.zz_scan_rate_floor_specandzzzz_core_specpin the debug-only lines: silent by default, printed with debug on.
[v2.14.7] (2026-09-27) — the minimap button on WoW Forever
Changed — WoW Forever embeds LibDBIcon again, temporarily
LibDBIcon-1.0 is not yet published for WoW Forever (its standalone TOC lists no 16001, and CurseForge does not offer it there), so FGI's hard dependency on it left Forever players unable to load FGI. The operator, 2026-09-27: "i need to temorarily embed libdbicon in the Forever version of FGI ... i don't want it in the other versions just the Forever version".
FastGuildInvite_Camelot.tocloadsLibs\LibDataBroker-1.1\LibDataBroker-1.1.lua(MINOR 4) andLibs\LibDBIcon-1.0\LibDBIcon-1.0.lua(MINOR 56) first, copied from the standalone LibDBIcon-1.0 v12.0.3. LibStub and CallbackHandler come from Ace3, still a hard dependency there.LibDBIcon-1.0moves from_Camelot's## Dependenciesto## OptionalDeps, so a standalone copy still loads first when present and LibStub keeps the higher MINOR.- The other six TOCs are unchanged: they embed nothing and still hard-depend on the standalone. The
files ship in every zip (one package serves all flavours) but only
_Camelotloads them. Tests/zz_toc_manifest_spec.luapins it:_Camelotloads both files, in order, before any FGI file, and lists LibDBIcon only as optional; no other TOC loads them; both files exist.- To undo when LibDBIcon ships for Forever: delete the two TOC lines and the two
Libsfolders, putLibDBIcon-1.0back in_Camelot's## Dependencies, and emptyFOREVER_EMBEDSin the spec.
Tests — 74 assertions that pinned nothing now pin their key
writ's spec-shape check (inbox 3e7ade77) found 74 is_nil assertions on a literal key across 35
spec files: a misspelled or renamed key would have passed them forever. Each now derives its key the
way the code does, pairs the nil with a positive assertion on the same key before the removing
action, or compares the table's complete key set. No new assertion failed, so no production defect
was found.
[v2.14.6] (2026-09-26) — welcomes send by themselves again, whisper tabs that keep your place, and recruitment goals that hold invitations only
Changed — a recruitment goal holds invitations only, says so once, and can exempt ranks
Reported on Discord by Vishiswaz, who asked for the goal, with a screenshot of the "Recruiting stopped" line every 30-60 seconds: "It should either be one message every hour or X long amount of time, or it should be just one message one time when it hits the recruitment goal, and not message again unless the recruitment resumes and the goal gets hit again. Also, it should be tied to the Invite Candidates function of Wingman. I have that disabled right now, so I should not be getting this. When I recommended the function, I did not mean for it to affect announcements or anything else, but as a function of wingman." And: "Is it possible we could have a guild policy that allows people from X rank and above to override the guild recruitment goal policy with their personal policy? So I as an officer can set limits on non-officers but not myself?"
- Invitations only.
Modules/Wingman.luano longer stops the whole drain at a met goal; the check moved intotryInvite, which returns false so the click falls through to announce or scan. With the Invite step off, the goal is never consulted and nothing prints. The scan gate infn:nextSearch(functions.lua) and the announce gate inAnnounce:Sendare removed, along with the now-unusedannounceMsgGoalReached/announceMsgPersonalGoalReachedstrings. The Invite button's gate infn:invitePlayerstays and still answers every press. - Said once.
Modules/FGI_RecruitGoals.luareplaces the 30-secondNOTICE_EVERYthrottle with anoticedflag: an unasked notice (Wingman's) prints once, andGoals:Blocking()clears the flag the first time it finds the goal no longer met. A deliberate press (explicit) is still answered every time, and also sets the flag. - Ranks exempt from the guild's goal. New policy field
gmPolicy.goals.exemptRank(0-based rank index, likeeditors.minRank), authored in Settings, Guild Policy, "Ranks that follow only their own goals".Goals:Exempt()readsGetGuildInfo("player")'s third return; for an exempt rankGoals:Targets()ignores the guild's targets, so only the member's own goals apply. It is policy, set by whoever may edit policy and pushed to everyone, so the 2026-09-09 no-bypass rule stands. - Settings text for the goals section now says invitations stop and scanning and announcements carry on. The personal-goals text no longer says a personal goal can never let you recruit later than the guild's, which is false for an exempt rank (Peer Review, thread bcd42b14).
- Known cost: a guild at its goal keeps advertising and scanning, so players who answer an ad whisper and get no invite. That is what was asked for, and the player notes say it.
Specs: Tests/zz_recruit_goals_spec.lua gains a rank-exemption block and "once per time the goal is
reached" examples. The scan and announcement examples now assert they are NOT held, and the Wingman
examples assert the invite step is held while the click scans, and that nothing prints with Invite
off. FGI_RecruitGoals.lua is at 100% line coverage. Not verified in a client.
Fixed — an FGI /who could switch an open Social window to its Who tab
GuildRoster, thread c5a1a43e. Libs/LibWho/LibWho.lua's BorrowWhoList left Blizzard's
WHO_LIST_UPDATE registration in place whenever a Who-list owner was VISIBLE. With FriendsFrame
open on its Guild tab (or GuildRoster's Sisters page inside it) that handed the handler back while
our /who was out, and the answer ran WhoList_Update, which ends
PanelTemplates_SetTab(FriendsFrame, 2); ShowUIPanel(FriendsFrame) (Classic Era
FriendsFrame.lua:364-366, :1003-1004). New whoPaneOf(owner): for FriendsFrame the Who tab's
sub-frame WhoFrame (FriendsFrame_ShowSubFrame("WhoFrame"), Classic :479, retail Mainline
:489), any other owner itself. The borrow is skipped only while that is visible, and the
debt-settling OnShow hook sits on it and re-checks it when it fires, so opening Social on another
tab no longer settles the debt; turning to the Who tab does. Three examples in
Tests/zz_who_libraries_invariant_spec.lua. Not verified in a client.
Changed — Wingman asks GuildRoster whether a /who is owed
fn.foreignWhoOwed calls GuildRoster 0.10.0's lib:IsWhoOwed() when it exists, and keeps the
whoSync.queue read for an older library. IsWhoOwed leaves out the library's new location
queries, which alternate with other senders on their own, so they do not take Wingman's scan clicks.
Fixed — /fgi resetSettings emptied the History tab
fn:resetSettings (functions.lua) promises to keep the records, and its keep-list carried the
history event log but not DB.factionrealm.recruits, which is what the History tab reads
(Modules/FGI_Recruits.lua), nor DB.factionrealm.memberStats, the Statistics tab's By-officer line
(Modules/FGI_MemberStats.lua). Both are on the keep-list now, and the completion line names them.
Found by the v2.14.6 docs audit. Tests/zz_reset_settings_spec.lua gains an example for the two.
Changed — docs brought in line with the addon, and five stale in-game texts
A code-against-docs audit of README.md and docs/Curseforge_Description.html corrected, among
others: the designated duty lists need Enable guild policy; Wingman's locks follow the steps it
runs; its auto-stop is "may neither announce nor invite"; /fgi version prints the game version
only; /fgi queue lists what would be dropped; the blacklisted-member check is off by default;
multi-select invites go one per click; Wrath and Cataclysm Classic borrow another version's
race/class lists. /fgids is now documented. The Mute announce skip notices tooltip no longer
lists "chat queue busy", a message removed with its gate (Locale/enUS.lua,
GUI/SettingsPanel.lua).
Four more in-game texts were wrong and are corrected in Locale/enUS.lua: the Hotkeys header said
the keys never touch WoW's binding system (they are set through SetBindingClick); the
member-history header said both tooltip toggles default on (the chat-name one is off); the Wingman
minimum-interval tooltip named the old "Scan interval"; and the blacklist-check description offered
a Skip button that was removed. Where the MEANING changed (the hotkey and member-history headers),
the key changed too, so the other locales' now-wrong translations are orphaned and those players see
the correct English until a translator updates it (Peer Review, thread aa0a1e73). The Wingman
tooltip only renamed a setting, so its key stands and existing translations keep working; the
blacklist-check text is a token key found only in enUS.
Tests — four more file-order dependencies, found by shuffled runs
Tests/support/addon.luaM.initDBbuilds a database whenFGI.DBis not a real AceDB object (it used to accept any table with aglobalfield, soTests/guild_events_spec.lua's stub could survive). On an existing database it puts back any DEFAULT an earlier file deleted, without replacing a table that exists. Seed 1 ran every status-bar example withoutfactionrealm.history.Tests/zz_tabs_render_spec.luaandTests/zz_history_counters_spec.luacreate the history series they read.Tests/zz_scan_groups_data_spec.luasets the level range its standalone bucket needs.Tests/zz_whisper_tabs_spec.lua'sfireraises an existing zero-attempt recruit record to one attempt, and puts it back afterwards (seed 12345).- The full suite passes in forward, reverse and shuffled order (seeds 1, 42 and 12345).
Changed — whisper tabs: only players FGI invited, oldest on the left, and clicks on them are not Wingman clicks
Reported on Discord by crimsonmane: "I have to stop wingman if i want to scroll through the whispers, even closing any whisper tabs is futile because it's a mouse click which triggers another whisper ... Currently this area captures any whisper, not just ones triggered by FGI." The operator's answer, on 2026-09-26: "can we make it so we're only capturing whispers of the people that we've invited, not all whispers? can we also make it so the 'oldest' whispers are on the left, so new whispers don't 'push' the whispers you're using off the screen? would it be possible to exclude clicks here from the action of wingman?"
Modules/FGI_WhisperTabs.luaWT:Recordnow records a whisper only whenWT:IsRecruitfinds a record infn.recruitswithattempts > 0, meaning this character sent the player an invite or whisper through FGI. A bare record is not enough (Peer Review, thread cae6b6fb):onJoinedwrites one for every guild join andacceptRemotefor every record a guildmate syncs, both withattempts = 0, andonAttemptis the only writer ofattempts. A player invited outside FGI (right-click Guild Invite,/ginvite) gets no tab. The gold "recruit" colour and tooltip line no longer mark anything, so the tooltip line is gone. This replaces v2.14.3's "every whisper gets a tab".WT.orderis now the order conversations opened, oldest first.touch()becameenter(), which appends a new conversation and never moves an existing one. The cap drops the oldest conversation that is neither open nor unread. A response no longer jumps to the first slot or resets the scroll (the 2026-09-24 rule it replaces). A hidden tab with unread lines still flashes its arrow.A conversation's FIRST reply now moves its tab left, to just after the conversations already answered (a new
repliedflag on the conversation). Answered conversations lead in the order they were answered, and unanswered ones follow, oldest first. The operator's words: "it shouldn't go to the absolut left, it should go left, but be to the right of any other active conversations". Later replies and the player's own lines move nothing.No limits on conversations or lines. The operator asked, 2026-09-26: "why is there a tab limit? is that some arbitrary number?" It was:
MAX_CONVOS = 12andMAX_LINES = 50, both picked, and both justified by a SavedVariables risk this session-only module never had. Their follow-up: "you need to remove those ... this is OUR ADDON ... we have our OWN window". Both are gone.enter()only appends, andpaintThreadsizes the thread box (ScrollingMessageFrame:SetMaxLines, a Lua history buffer per Blizzard'sScrollingMessageFrame.lua:240) to the whole conversation. Extra tabs scroll behind the arrows.Lines read like the game's chat, with class-coloured names.
paintThreadformats each line throughCHAT_WHISPER_GET/CHAT_WHISPER_INFORM_GET(asBlizzard_GMChatUI.lua:98does), so it shows[Name] whispers:/To [Name]:as a|Hplayer:link. The name is coloured fromCUSTOM_CLASS_COLORSorRAID_CLASS_COLORSonce the conversation's class is known.WT:Recordlearns it once from the event's GUID viaGetPlayerInfoByGUID(second return, the English class token), so a conversation with only outgoing lines shows the name uncoloured.Each arrow shows how many tabs are hidden on its side (
< 3,2 >).ARROW_Wwent from 16 to 30 to fit the count.The strip, the conversation pane and the player menu sit outside the compact tray's rectangle, so
mouseOverOwnUIinModules/Wingman.luanever saw them, and every click there drained a Wingman step. The newWT:IsMouseOver()covers all three, andmouseOverOwnUIasks it.The game's own whisper window still opened after a few exchanges (operator, 2026-09-26: "i still got a popup in the old chat box after a few back and forth", then "it has the conversation in both places now. i was hoping to capture all of one conversation into the FGI window UNTIL the FGI tab is closed"). Three changes, all keyed on the new
WT:Captures(name): a player FGI invited whose tray tab has not been closed, and only while the compact tray is shown, since with it hidden the conversation would appear nowhere.WT.chatFilter, registered forCHAT_MSG_WHISPERand_INFORMthroughaddon.API.AddMessageEventFilter, keeps every captured line out of every game chat window.- While any conversation is captured,
WT:Refreshholds whisperMode atinlinethrough the newfn.holdInlineWhisperMode("whisperTabs"), on every flavour. It gives the player's value back throughfn.releaseInlineWhisperModewhen the last tab closes, the tray hides, or atPLAYER_LOGOUT. This was the actual cause. On the Classic family theFCF_OpenTemporaryWindowhook closes the window for our own send, and thenFloatingChatFrameManager_OnEventcallsFCF_SelectDockFrameandFCF_FadeInChatFrameon it, which show it again (ClassicFloatingChatFrame.lua:2354-2361). Only prevention works there. There is no timer and no echo counting: a first version counted echoes back through the send path'sC_Timerrestore, and Peer Review (thread 0aa18e31) ruled that out under the operator's rule against timers that call the WoW API. The send path's own restore defers to the hold. Known cost: while a conversation is captured, a whisper from anyone else shows in the chat window instead of popping out. - The hook also closes a window opened by an incoming line of a captured conversation, where nothing re-shows it. I first blamed a window opened before the change loaded; the operator confirmed it was not open.
- Closing a tray tab now hands the conversation back to the game's chat for the session: the next whisper no longer reopens it, which replaces the 2026-09-24 rule.
- Retail: the filter and the whisperMode hold apply there too, so no game window opens for a captured conversation. The only exception is the very first incoming line from a recruit who has no tab yet, which arrives before the hold is taken. Not verified in a client.
WT:IsRecruitnow tries the name as given,Ambiguate(key, "none")andfn:fullPlayerName. The recruit store keys byfn:normalizePlayerName, which leaves a Classic name bare, while the tabs and the hook passName-Realm, so a single lookup missed same-realm recruits.
Specs: Tests/zz_whisper_tabs_spec.lua (a stranger gets no tab, oldest-first order, a response
moves nothing, a new conversation joins on the right, IsMouseOver per frame) and
Tests/zz_wingman_session_spec.lua (a click over the tabs does not pump or drain). Not verified
in a client.
Changed — no timer restores whisperMode any more
Peer Review (thread 0aa18e31, finding 2), under the operator's standing rule against a timer that
calls the WoW API. The retail send path's fn.suppressPopoutForSend / fn.notifyEchoSuppressed
restored whisperMode through C_Timer: a one-frame C_Timer.After(0) after the last echo, and the
checkWhisperModeFallback checker at 60 s for echoes that never came. Both ended in SetCVar. Both
are gone. The last echo now only marks the restore due, and the new fn.settleWhisperMode, called
first in fn.pumpClick, performs it on the player's next click, or once WHISPER_MODE_FALLBACK has
passed. The old one-frame deferral protected against the manager seeing the final echo after our
filter, and a later click is a later frame by construction. Cost: whisperMode stays inline
from the last echo to the next click, so a reply in that gap shows inline. PLAYER_LOGOUT still
restores regardless. Tests/zz_whisper_popout_spec.lua now drives the restore by click and asserts
no timer is armed.
The whisper tabs' WT:Refresh no longer takes its whisperMode hold from the tray's own OnShow or
OnSizeChanged. The settings gear re-shows the tray from C_Timer.After(0)
(Modules/compactFrame.lua, cf.gearBtn), which would have put SetCVar in a timer. The hold is
retaken on the next whisper or click on the tabs.
Changed — welcomes send by themselves again once the delay is up (DB.global.welcomeAuto, on)
DADDY on Discord, 2026-09-26: welcomes were "still working on clicks"; the operator: "they don't want a click, they want it to go out after a short delay like it did before, why can't we do that again as an option?"
v2.13.0 was wrong about why it removed the timer send, and I wrote it. It read
HasRestrictions = true on SendChatMessage (Era ChatInfoDocumentation.lua:403-406) as "no chat
send without a click, on any flavour", and moved every welcome onto the next click. The client's
rule is narrower: only SAY, YELL and CHANNEL need a hardware event; GUILD and WHISPER do not
(warcraft.wiki.gg API_SendChatMessage; the harness's enforceChat models the same). What raised the
ten ADDON_ACTION_BLOCKED in the v2.13.0 field report was never established; retail's
chat-messaging lockdown is the likeliest candidate, not verified. Earlier this session a timer
send was built and reverted under the operator's no-timers rule (Peer Review, thread 3eefec1c); the
operator's words above are the explicit exception for the welcome.
Modules/FGI_Welcome.lua: newWelcome:AutoSend()/NextDueAt()/ScheduleAuto(). Every queue change arms ONEC_Timer.Afterfor the earliest due welcome (a token discards an outdated one, so a late joiner pushing the grouping window out is honoured); it callsPump(true). Whispers go one per run on both paths, and the timer spaces auto sendsAUTO_SPACING(1 s) apart, so a big intake is not whispered all at once (Peer Review, thread f3981466). In chat-messaging lockdown nothing is sent and the timer looks again every 10 s (a poll: no lockdown-ended event was looked for).ClickDelivery()now answersnot AutoSend(). The click path is unchanged and still delivers early.- If the client blocks an auto send anyway (Peer Review, thread f3981466):
ADDON_ACTION_BLOCKEDisSynchronousEvent = truewith the addon name first (EraRestrictedActionsDocumentation.lua:71), so ablockWatchframe sees it inside the send. The welcome is put back in the queue, the session falls back to click delivery, and one chat line (welcomeAutoBlocked, enUS) says so. It lasts until/reload. FGI_Core.luadefaultwelcomeAuto = true; Settings > Guild's v2.13.0 description ("Welcomes go out on your next click") is a toggle again, "Send welcomes automatically". Added to the settings template catalogue. The pre-v2.13.0welcomeOnClickkey is still not read.- A click-anywhere "catcher" frame was written first in this session for recruiters without Wingman, and removed before it ran: the operator does not want a click at all.
Specs: Tests/welcome_spec.lua gains an auto-send block (sends with no click, every due whisper in
one run, a late joiner re-arms and the outdated timer sends nothing, held in lockdown then sent);
the click-delivery block runs with welcomeAuto = false. Tests/zz_protected_welcome_spec.lua adds
two examples under the harness's chat rule proving the timer's GUILD and WHISPER sends are delivered
with no click, and its blanket-guard pair now says that guard is stricter than the client.
FGI_Welcome.lua 100% line coverage. Not verified in a client.
Fixed — a welcome waiting to be sent is no longer dropped after ten minutes
Reported on Discord (DADDY / Vishiswaz): "I got a message that the welcome message timed out since my
last button push ... i do not want it to miss people just because i do not hit a button."
Modules/FGI_Welcome.lua queues every guild welcome and welcome whisper for the click pump, and
Pump dropped any entry older than WELCOME_QUEUE_TTL (600 s) with a chat notice. A player who
stepped away lost every greeting owed while they were gone.
Now nothing is dropped for age alone. WELCOME_QUEUE_TTL is replaced by WELCOME_STALE (600 s),
which only makes the click ask LibGuildRoster whether a stale welcome can still land: a stale guild
line leaves out a joiner the roster no longer has (a leaver seen live was already removed through
CancelFor), and a stale whisper to a member the roster shows offline is dropped instead of sent,
without spending the click's one message. With no ready roster the welcome is kept. The notice
(welcomeQueueExpired, enUS) now says what it means and no longer names the "Send welcomes on a
click" setting v2.13.0 removed. The key exists only in enUS.
Tests/welcome_spec.lua: the example that asserted the ten-minute drop is replaced by the
regression guard (queued, twenty minutes with no click, still pending, one click sends it once), plus
stale-whisper-offline, stale-joiner-left and roster-not-ready examples. FGI_Welcome.lua coverage
264/264.
Not verified in a client.
Fixed — FGI's /who could stay "quiet" after the player opened the Social pane
Found by the shuffled-order suite (--order shuffle:1), not in the field. Libs/LibWho/LibWho.lua's
BorrowWhoList hooked the OnShow of only the Blizzard panels still registered for WHO_LIST_UPDATE.
When GuildRoster's sister-guild /who had already borrowed FriendsFrame's registration, FGI's query saw no
panel, borrowed nothing and hooked nothing -- so the player opening the Social pane settled
GuildRoster's debt and left FGI's Quiet standing. It had passed in file order only because an earlier
spec hooked the same frame. FriendsFrame is now hooked on every borrow; on the Classic family it is the
only Who host. LibWho_Retail.lua was not examined for the same gap.
Fixed — Announce's secret-string guard failed open when its helper was missing
Modules/Announce.lua checked chat payloads with if fn and fn.isSafeString and not fn.isSafeString(x),
which skipped the check whenever the helper was absent and let a non-string reach :match. A local
isSafe now falls back to type(s) == "string". Found by the reverse-order suite, where another spec's
copy of the module (on a stub FGI with no isSafeString) received a table as a channel name.
Tests — the suite passes in any file order
Reverse order and shuffled seeds 12345, 1 and 42 are all 3664/0, from 13 failures (reverse) and 7 / 11 / 1
(the seeds). Every fix is in the spec that leaked state or failed to declare what it reads: the quiet-zone
and instance-map caches, the Fairbanks realm two roster specs left behind, Wingman's global overlay name,
the scan tab's module-level widgets, a leftover scan cooldown, an enforced guild policy, the main window's
one-shot reopen frame, and a dialog example that broke its anchor before a release hook could restore it.
The _G restore audit (Peer Review 13c69367) converted 16 skippable inline restores to finally() and
fixed one teardown that nilled harness-owned chat-filter globals. FGI_Core.lua coverage is back to 100%
with an example for the full-reset reload (:971).
[v2.14.5] (2026-09-25) — personal recruitment goals, a scan list that keeps its place, and the Oxford comma
New — personal recruitment goals
Requested on Discord by Vishiswaz: "Recruitment goals, but on a personal setting also. Whichever is
stricter, personal or guild-enforced, is the one FGI enforces." Settings > General > My recruitment
goals sets a per-character (DB.char.goals) total and online target. Modules/FGI_RecruitGoals.lua
gains Goals:Personal() and Goals:Targets(), which takes the LOWER positive target for each kind
(guild on a tie), and Goals:Met() now judges each kind against that and reports source
("guild" / "personal"). A personal goal can only stop a recruiter sooner, never later, so the
guild policy's no-bypass rule is unchanged, and it still applies when the guild policy is off. A personal
stop is always the chat line (the guild's pop-up setting governs only the guild's goal), worded "your
own goal" (recruitGoalPersonalTotal / recruitGoalPersonalOnline), and a held announcement says the
same (announceMsgPersonalGoalReached). The panel shows which target is in force and whose it is. Pinned
in Tests/zz_recruit_goals_spec.lua ("a recruiter's own goal beside the guild's"). char.goals has a
default ({}) in FGI_Core.lua and is in the settings-template catalogue (Modules/FGI_SettingsProfile.lua,
group "duty"), because every setting is offered to the template author; it is not synced, so the sync
exclusion does not apply.
Changed — FGI's own RowList is gone; every list is LibAceGUIWidgets'
GUI/RowList.lua carried FGI's original list (~900 lines) as a fallback for a player whose
LibAceGUIWidgets was older than MINOR 34, the first to carry every field FGI's tabs pass. It stayed
through v2.14.4, one release after the library shipped MINOR 34, and is deleted here. What remains is
the translation layer (hidden -> show, the class columns as format, the per-list refontHook
and tooltipOwner). A library older than MINOR 34 is now an error naming the fix ("Update
LibAceGUIWidgets") rather than a quiet fallback. The same text is also printed once in chat from
OnEnable (RowList.WarnIfLibraryTooOld), because a Lua error reaches only players who show them
and everyone else would see an empty tab (Peer Review 61cd8c48). Tests/zz_rowlist_scroll_spec.lua
runs once, against the library, and pins the error and the chat line.
Fixed — the Full Window scan list jumped back to the top on every scan tick
Reported on Discord by Vishiswaz. ScanTab.Refresh(preserveScroll) treated a missing flag as "reset the
scroll to row 1", and the engine's live-refresh hook (addon.MainWindow.refreshScanTab, fired from
functions.lua on every /who batch and counter change) calls it with no arguments -- so a player
scrolled down a filling list was thrown back to the top every few seconds. The flag is now read as
preserveScroll ~= false: the list keeps its offset (the library clamps it to the new length) unless a
caller explicitly passes false. No caller on the tab wanted the reset. Pinned in
Tests/zz_scan_strip_actions_spec.lua ("the live refresh while a scan fills the list").
Fixed — combined welcomes now use the serial (Oxford) comma
Reported on Discord by Vishiswaz. Modules/FGI_Welcome.lua's joinNames joined three or more names as
"A, B and C". A new client-locale key, welcomeNameSerialJoin (", and "), joins the last of three
or more; two names still use welcomeNameFinalJoin (" and "). The serial comma is English: because
fn.chatLoc flattens the client locale over enUS, the enUS serial joiner is used only while the final
joiner is still the English " and " -- a locale that translates the final joiner and not the serial one
gets "A, B und C" (Peer Review, thread fa0cb971). NamesFromPeerWelcome splits on the
serial joiner first -- splitting on " and " alone would read "B," as a name and miss the peer
suppression -- and falls back to " and ", so a peer still on v2.14.4 is read correctly. Pinned in
Tests/welcome_spec.lua.
[v2.14.4] (2026-09-25) — the scan waits the full timer you set
New — DB.global.scanRespectTimer, on by default
Reported on Discord (Vishiswaz, with a video): "Cooldown timer for scans set to 10 seconds, but it
skips down to like 4 seconds." It was working as designed and the design was wrong for the player.
Since v2.13.0 the button's countdown starts at the slider value (fn.getGiveUpAfter, the give-up
deadline) on the send, and when the answer lands searchWhoResultCallback replaced it with what was
left of the server's /who limit (fn.scanFloorRemaining, 5 s Classic family / 8 s retail). An answer
in under a second turned 10 into ~4. The operator: "a setting that is turned on by default to not do
the 'shorten' thing, it just respects the timer folks set."
- On (default): the slider value is also the least time between two of FGI's scans, measured
from FGI's own send.
fn.scanFloorRemainingnow returns the larger of the server's remainder and the user timer's remainder, so the answer path, the press gate and the foreign-/who countdown all hold to it without a second code path. Wingman's scan step reads the same countdown flag, so it paces to the timer as well. - Off: the v2.13.0 behaviour, server limit only.
- Two clocks. The server limit is measured from the later of FGI's send (
lastSentAt) and a /who sent by the player or another addon (lastForeignSentAt, new;fn.watchForeignWhoused to stamplastSentAt). The user's timer is measured fromlastSentAtonly, so a sister-guild lookup does not restart the player's pace. - The write-off still re-issues on the press that wrote a query off: the timer is the give-up
threshold, so at
waited >= giveUpAfterits remainder is exactly 0. fn.scanRespectsTimer()answers ON for an unset value.fn.scanFloorReason()says which limit is holding the press, and the refusal line uses it:Scan held: waiting out your scan timer (%ds).for the user's timer, the existing server line otherwise. The debug line names which one too.- Settings > General > Scanning: new toggle Wait the full timer between scans under the slider;
the slider is renamed Scan timer (seconds) (was "Give up on a /who after (seconds)") with a
tooltip that describes both jobs. The setting is in the settings-profile catalogue. The new and
changed strings exist only in
enUS.lua, as the ones they replace did, so other locales fall back to English for them.
Fixed — /fgi resetDB now does what the Anti-Spam tab's Clear All does
The slash command reassigned DB.realm.alreadySended = {} and left
addon.search.tempSendedInvites alone, so a cleared name stayed filtered out of scans until
/reload; Clear All emptied the table in place and flushed that cache. Both now call the new
fn.clearAntiSpam (functions.lua), which also redraws the Anti-Spam tab if it is open. The README
and CurseForge page said the command made "everyone invitable again"; they now say it empties your
own list and that guildmates can sync entries back. Bridge:ApplyEntries writes any missing
guild:alreadySended name and this list has no removal record, unlike the blacklist's tombstones.
The Anti-Spam tab's Clear All help line says the same (new enUS key; the old hiIN translation is
removed, so Hindi shows it in English).
Peer Review follow-ups (thread f60a4dd2)
- Wingman paces at the user timer. Its scan step waits on the tray's countdown, which now includes the timer, so a player who never touched the slider (default 10) scans about every 10 s on Classic instead of about every 5 s. That is the operator's instruction applied as written.
- A refused Wingman scan no longer spends the click.
fn:nextSearchnow returnstrueon the one line that sends; Wingman'stryScanreturns that, so a press the timer or the server limit refuses falls through to invite or announce. - The foreign-/who "Scan timer restarted" line prints only when
fn.scanFloorReason()is"server". With the user's timer longer, that /who changed nothing the player waits for. zz_scan_rate_floor_specrestoressubdivideLvl, which it had left off for later files.
Tests
zz_scan_rate_floor_spec: a new block for the timer (default on, full countdown after an instant answer, held past the server's limit, released at the timer, never faster than the server, the refusal names the timer, a foreign /who does not restart it, the write-off retry is not blocked). The file's existing examples pin the server limit and now run with the setting off.zz_settings_panel_spec: the toggle reads on when unset and writes through to the gate.zz_scan_inflight_gate_spec,zz_scan_passes_spec,zz_compact_scan_press_specrun with the setting off, because they step over the server's limit on purpose and are not about pacing.zz_libwho_restore_specandzz_who_libraries_invariant_specclearlastForeignSentAtalong withlastSentAt, since they are the files that drive a player's /who.- 3647 passed, 0 failed, in file order and in shuffled order (seed 12345).
Removed — the embedded ChatThrottleLib
Libs/ChatThrottleLib (v29) never ran: Ace3, a hard dependency, loads v32 first. Dropped from all
seven TOCs; the one spec that loaded it now loads Ace3's copy.
Changed — LibDBIcon is a dependency on WoW Forever too
The operator: "lets also make libdbicon an external dep". _Camelot now lists LibDBIcon-1.0 in
## Dependencies like the other six, and Libs/LibDBIcon-1.0 and Libs/LibDataBroker-1.1 are
deleted. It had kept the embedded copies because the standalone's TOC does not list Interface 16001,
but Ace3 (already required there) does not list it either, so the exception protected nothing.
zz_toc_manifest_spec now pins that no TOC embeds them; the test loader reads the installed
standalone LibDBIcon-1.0 copies. Not checked on a Forever client.
Removed — /fgi seedbl
A dev helper that wrote thirty invented "Name-Realm" bans through the real fn:blackList. Any
player could type it, and the blacklist is shared with the guild, so it put made-up bans (names
that may belong to real players on the realm) into every guildmate's list. Removed rather than
gated behind debug mode, because debug mode would not have stopped the sync.
Tests/zz_slash_seedbl_spec.lua now pins that the command adds nothing.
Docs — release prep
docs/Curseforge_Description.html and Modules/intro.lua carry the v2.14.4 notes (New / Changed /
Fixed, /fgi seedbl as a Removed bullet). README gains a How fast it scans paragraph (the scan
timer and its toggle) and no longer lists ChatThrottleLib among the copies FGI carries. Parity was
checked against today's changes only: the full docs-versus-code audit ran earlier on 2026-09-25.
A second full audit (README and the HTML feature sections against the code) then found and fixed:
HTML still saying Forever carries its own LibDBIcon and that the addon compartment is retail-only;
README and HTML saying Ask at login when I'm the designated recruiter "turns the login prompt
off" (it is on by default; unticking it stops the prompt); the README naming Seen again after
without "(hours)"; and /fgi kbstate and /fgi escstate missing from the README table. In-game
text fixed on the way: Scan tab help (the scan timer, the counter list, Level range source under
General → Scanning), Filters help ("every active filter" contradicted its own OR rule), Custom Scan's
Strict help (splits follow the subdivision settings, not "class then race"), and the Settings
tooltips for Level range (splits where the players are, not always in half) and Mute announce
skip notices (a horn press always explains itself). Their old keys stay orphaned in hiIN and the
other locales, which show English for these lines until retranslated. Not verified: whether
/fgi resetDB's "everyone becomes invitable again" holds when guildmates' synced lists refill it.
Docs — the v2.14.3 Peer Review wording fixes
Peer Review found three README/CurseForge overclaims in the v2.14.3 release prep, and I shipped v2.14.3 without applying them. Applied now:
- Copy names covers the Blacklist, Anti-Spam, History and Scan tabs only (
GUI/UI.lua), and it copies the whole list, not what a search is showing. - Addon compartment is "retail and WoW Forever", not retail only:
Blizzard_Minimap/Mainline/ AddonCompartment.luais inwow-ui-source-forever, and FGI's toggle only checks that the compartment exists. The Settings tooltip still says "Retail only"; that is a translated string and is on the todo list with the other stale help text. - The welcomer fallback to the announcer list applies only when an announcer list is set.
Fixed — in-game help text that described settings which have moved or gone
Found by the 2026-09-25 docs audit; each claim re-read in code before rewording.
- Anti-Spam tab help: the expiry lives in Settings → Advanced → Anti-spam memory, not "Settings →
Main" (
GUI/SettingsPanel.lua,antiSpamExpiryDays). - Custom Scan help: the default sweep's level range is the Scan tab's readout or the filters'
range (Level range source), not "Settings → Main → low/high level range" (
GUI/Tabs/Scan.luawriteslowLimit/highLimit). - Quiet Zones help said the list "pauses scanning". It does not: players found there are skipped at
ingestion, and the zone step leaves the zones out only with
quietZonesSkipWhoon. - Race/Class chart help and the tab's header comment said the table is "maintained by hand". It is
generated from the game's CharBaseInfo data per product branch (
functions.lua, the header aboveRaceClassCombo). - Wingman "Post announcements" and the Scanning header tooltips named an "Announcement spacing"
setting that no longer exists, and described staggered bursts. A Wingman click that posts sends
one announcement to one channel and otherwise falls through to the next step
(
Modules/Wingman.lua,tryAnnounce). The Scanning header also now describes the scan timer. - The addon-compartment toggle's tooltip says "Retail and WoW Forever only".
- The compact tray's "i" and gear tooltips were bare English strings; each paragraph is now a locale key (texture escapes kept outside the keys), so they can be translated (Peer Review cd121cc9 item 4). English only until then.
- That tooltip's height hint was an unmeasured 480 px; measured in the client at 514 px, so it is now 520 and both comments state the measurement (Peer Review cd121cc9 item 5). The hint only picks above-or-below placement, so 480 could put the tooltip above the tray without room for it.
- These are locale keys, so the new wording is English everywhere until translated: the Scanning header and Wingman announce tooltips had translations in every locale (now orphaned keys there), and the three MainWindow lines had Hindi ones, which were removed with the old keys.
[v2.14.3] (2026-09-25) — whisper tabs above the compact tray, LibAceGUIWidgets becomes a hard dependency, and the suite passes in a shuffled order
New — whisper tabs above the compact tray
Modules/FGI_WhisperTabs.lua is new, and it is in all seven TOCs straight after
Modules/compactFrame.lua. It puts a row of chat-style tabs along the top edge of the compact tray,
one per whisper conversation. Clicking a tab opens that conversation in a pane above the tabs, with
a reply box, and clicking it again folds the pane away. The operator asked for it so that their
chat and their recruiting are "all in the same place", instead of in new tabs on the general chat
box.
- Which whispers: every whisper conversation gets a tab, not only recruits. A player with a
recruit record (
fn.recruits) gets a gold tab.FGI_RecruitsgainedRecruits:get(name), a lookup that never creates a record, so the tabs can check any player without turning a stranger into a recruit. - Unread and closing: a tab shows its unread count. Its x closes it, and the next whisper from that player opens it again with the history intact.
- History: held for the session only and capped at 50 lines per conversation and 12 conversations; the least recently active is dropped. Nothing goes to SavedVariables.
- Listening: through
addon.API.RegisterChatEvents, so nothing is recorded during retail chat lockdown. - Replying: from the reply box's Enter, through
addon.API.SendChatMessage, after the lockdown check and the retail own-realm name trim. It deliberately does not go throughfn:sendWhisper, which would hide the player's own reply as a recruitment echo and, on retail, flipwhisperMode. The reply is recorded when the server echoes it back, so a whisper that fails to send never appears as sent. - Left alone: Blizzard's own whisper tabs are untouched. On retail they can only be changed
through the global
whisperModesetting, and FGI must never close or wrap Blizzard's chat tabs. Battle.net whispers are not included. - More tabs than fit, flashing, and reordering (operator, after the first in-game look: "i
need them to disappear and we have a < and > arrow on either side so we can scroll through
them ... the active conversations with response flash like in the main chat window and they
should reorder to the 'first' slot on the left"). Each tab is now as wide as its own name (48 to
140), not an equal share of the strip, so names are no longer cut to
Felon.... When they do not all fit,<and>appear at the ends and the tabs past the edge are hidden; each click moves the first shown tab by one, and each arrow greys out at its end. A tab with unread lines pulses the game's own chat-tab glow (Interface\ChatFrame\ChatFrameTab-NewMessage, the textureFloatingChatFrame.xmlgives a chat tab'sglow) through its own animation group, notUIFrameFlash, until it is opened. An incoming whisper moves its conversation to the first slot and scrolls the strip back to it. The player's own reply does not move the tab they are typing in. Two follow-ups from Peer Review (thread9ab1feae): an arrow pulses the same glow while a tab hidden on its side has unread lines, so an unread conversation scrolled out of view is never invisible; and over the 12-conversation cap, the one dropped is the least recently active that is neither open nor unread, falling back to the oldest only when every conversation is one of those. - Right-click a name for a player menu (operator: "i need to be able to right click on the name
in the box, and the name in the chat to get the same menu in the normal chat box, so i can
invite"). A right-click on a tab, or on a name in the conversation pane, opens FGI's own menu at
the cursor (
WT:ShowMenu): Whisper (focuses this conversation's reply box), Invite (C_PartyInfo.InviteUnit, throughfn:formatNameForRetailAPI), Guild Invite, Add Friend, Ignore (C_FriendList.AddFriend/AddIgnore), Blacklist, Unblacklist and Cancel. Guild Invite, Blacklist and Unblacklist are the same functions the chat menu's FGI submenu calls, now shared asaddon.ChatMenu.GuildInvite/Blacklist/UnblacklistinModules/FGI_ChatMenu.lua. The menu is on theTOOLTIPstrata, and closes on any row, on a mouse press off it (GLOBAL_MOUSE_DOWN, registered only while it is shown), and when the tray hides. Names in the pane are|Hplayer:...|hlinks with hyperlinks enabled; every other link in a whisper (an item, a quest) is handed toSetItemRef, so its tooltip opens. A plain left-click on a name focuses the pane's own reply box rather than starting a whisper in the main chat box.C_PartyInfo,GetCursorPosition,SetItemRefandIsModifiedClickare in.luarc.json. - Why it is FGI's menu and not the game's. The first build handed a right-click to
SetItemRefso the game's own "FRIEND" menu opened, FGI's submenu included. In the client that menu opened behind the tray: both sit onFULLSCREEN_DIALOG(the tray's default window layer), and the tray is toplevel. Two fixes to the tray's level and strata did not hold, and the operator settled it: "you can recreate your own with the same functionality ... and then you don't have to worry about taint or any of the other stuff". Nothing of Blizzard's menu system is opened or touched now, and the strata-dropping code is gone. Every entry runs from the row's own click handler.C_FriendList.AddFriendis markedHasRestrictionsin the Classic Era documentation (FriendListDocumentation.lua:13), a flag the docs do not define; whether it refuses a call from an addon's click has not been checked in a client. - At the top of the screen: when there is no room above the tray for the tabs and the pane, both
open below it instead (Peer Review, thread
5bda8599). A flip rather thanSetClampedToScreen, which would push the pane down over the tray. The decision is remade on every refresh and on every drag or resize of the tray, through ahooksecurefuncon the tray's ownStopMovingOrSizing. - The secret-value example now tests the guard. It asserted "nothing was recorded", which stayed
green with the guard bypassed: the harness's secret is a table, so
WT:Record's owntype()check dropped it, while in the client a secret string reports as a string. It now asserts the handler never reachesWT:Record. Both lockdown examples were driven red by bypassing the guard, then green with it restored.
Tests/zz_whisper_tabs_spec.lua covers the tabs and the menu; the example that expected a
right-clicked name to reach SetItemRef was removed with that behaviour. Confirmed in a client on
2026-09-24, arrows, flash and reordering included (operator: "the whisper tab is workign how i want
it to"). FGI's own right-click menu was confirmed in a client on 2026-09-25 (operator: "right click
works"). The compact tray's "i" help tooltip gained a Whisper tabs paragraph (tabs, arrows, x, and
the right-click menu's entries); its height hint went from 400 to 480.
Changed — LibAceGUIWidgets is a hard dependency, and Libs/GUI.lua is deleted
All seven TOCs list LibAceGUIWidgets in ## Dependencies, and .pkgmeta lists libaceguiwidgets
under required-dependencies. Nothing a player loads changes: GuildRoster already hard-depends on it,
so it always loaded ahead of FGI.
That load order is also why Libs/GUI.lua has not run in a client since the library moved its
widgets to version 30 (when that happened was not checked). The file registered ClearFrame,
GroupFrame and TLabel at version 26 and returned early if a newer copy existed. The library now
registers the same three names at version 30, so the legacy window, the dump and debug windows and
the intro labels have been the library's widgets. An August audit recorded GroupFrame and TLabel
TIED at 26, and a tie goes to whichever addon loaded first, so FGI's copies may have won before
then. Only the offline suite built FGI's copies,
because Tests/support/addon.lua never loaded the library. The suite now loads it first, as the
client does, and the file and its TOC line are gone. Six code comments that credited Libs/GUI.lua
with Ace3's stock Frame behaviour now name the real source.
UI.CreateMenuInfo now calls the library's identical copy. The 13 places that still built menu
rows with UIDropDownMenu_CreateInfo() directly now use it too (Peer Review L7). Eleven of them
set checked, so they pass true and keep the check-mark slot, which gives the same call as
before. The other two are the Yes/Cancel rows of the legacy window's clear-confirm menu, which are
actions, not choices. They now drop the unused slot, as every other FGI action menu does. That is
a few pixels of left padding on a two-row menu in the legacy window only, so it stays out of the
player notes. The legacy window's resize fix below does belong in them, under Fixed. The other
three helpers the library also has stay FGI's own, because each would change what players see:
AttachTooltip: the library's version replaces an earlier tooltip rather than adding to it, and it skips FGI's non-Latin re-font.ApplyMinResize: the library's follows a UI scale that any addon in the session can set.MakeLabel: the library's takes the shared accent colour instead of FGI's orange.
Every list in FGI now uses LibAceGUIWidgets' W.RowList when the installed library is MINOR 34
or later. W.RowList was extracted from FGI's list. The library added the four features FGI's
tabs needed, at FGI's request: external sort, expander cells, button cells and read-only rows.
addon.RowList:New hands the list to the library and translates FGI's options on the way, so no
tab changed:
action.hiddenbecomesaction.show.- The class colouring still reads
NoLocaleClassthroughaddon.ClassDisplay/addon.color, as aformatfunction. - The LibLocaleOverride re-font goes through the list's own
refontHook, never the library's session-global config.
Below MINOR 34, FGI's own implementation still runs, so a player with an older library loses nothing. Delete it once MINOR 34 has been released long enough. FGI's suite against the library found one library defect: a button cell whose tip answered nil still showed a header-only tooltip. The library fixed it the same hour.
Visible differences from FGI's own list: a slimmer scrollbar; blank cells sort LAST in both directions (FGI's own list sorted them first when ascending, against its own comment); number-like text sorts by value; and rows that tie keep their order. The main window's tabs were checked in a client on 2026-09-24 and look right.
Tooltips the list draws (headers, action icons, button cells) anchor through addon.Tooltip.Owner,
passed as the list's own tooltipOwner. The library added that option in MINOR 34 at FGI's request,
after Peer Review found the first cut anchoring them through the library. That skipped FGI's
non-Latin tooltip re-font. zz_rowlist_scroll_spec checks it on both builds, and fails on the library
build without the option.
Changed — LibDBIcon-1.0 is a required dependency, not an embedded copy (all but WoW Forever)
The operator's decision, delivered by TOGTools (inbox contract 08364b4b): an embedded LibDBIcon
only updates for players of the addon that ships it, so every addon with a minimap button would need
a re-release for each LibDBIcon update. The six non-Forever TOCs now list LibDBIcon-1.0 in
## Dependencies and no longer load Libs\LibDataBroker-1.1 or Libs\LibDBIcon-1.0; .pkgmeta
lists libdbicon-1-0 under required-dependencies. The standalone loads LibStub, CallbackHandler
and LibDataBroker-1.1 through its own embeds.xml, so the one dependency covers both libraries.
FastGuildInvite_Camelot.toc keeps the embedded copies, with a comment saying why: the standalone's
TOC lists 11508, 11509, 20505, 20506, 30405, 38001, 40402, 50503, 50504, 120100, 120005, 120007
and not WoW Forever's 16001 (read in LibDBIcon-1.0/LibDBIcon-1.0.toc:1). The library files stay
in the repo for that one TOC. No Lua changed: LibStub("LibDBIcon-1.0") and
LibStub("LibDataBroker-1.1") resolve the same either way.
Tests/zz_toc_manifest_spec.lua now allows the two embedded lines on _Camelot only, and checks
that the other six declare the dependency and _Camelot does not. The old spec went red on the
change ("FastGuildInvite_Camelot.toc and FastGuildInvite.toc list different files") before it was
updated. KNOWN COST: a player who installs by hand must now install LibDBIcon-1.0 too. Not verified
in a client.
Fixed — the legacy window resized only once per time it was opened
GUI/LegacyMainWindow.lua replaced the resize grips' OnMouseUp with its own position saver. On the
library's ClearFrame, that OnMouseUp is the only thing that ends a resize, so the resize state
stayed set after the first drag and every later drag was refused until the window was hidden. The
saver is now hooked instead (HookScript). The right-edge grip is also dropped through the library's
resize handle instead of being hidden directly, because the handle re-shows every grip it owns on a UI
scale change. That call runs before FGI's width lock, since it re-applies the handle's own bounds.
Tests/zzzz_legacy_window_spec.lua drives two drags in a row. It fails against the old code with
"the latch is stuck" and passes with the fix.
The same handle also re-applied its own bounds on any library UI-scale change: scaled, and with no maximum. That unlocked the fixed width, and at scale 2.0 it widened the window to an 800 floor (Peer Review M1). Giving the handle the 635 width would not help, because it scales whatever it holds and this window's contents do not scale. So the handle's scale listener is removed for this one window. ClearFrame's own chrome re-layout is a separate listener and still runs. The spec now changes the scale to 2.0 and checks that the width lock and the width survive.
Fixed — the Dump window's text box hung out of the bottom, and the pop-out windows were see-through
Seen in a client on 2026-09-24. Modules/dump.lua sized its text box by line count
(SetNumLines(39)) with only a top anchor. That is taller than the window, so the box ran past the
bottom edge and covered the status bar and Close button that LibAceGUIWidgets' ClearFrame draws
there. It is now held by two opposite corners: top-left against the button column, and bottom-right
45 pixels above the window's bottom edge. That also replaces its fixed width. The window can be
resized and login restores a saved width, so a narrower window used to push the box into the
buttons (Peer Review). The anchors are set after every call that re-runs the box's own sizing.
zzzz_dump_debug_windows_spec checks both corners.
The dump, debug and legacy windows also never received the window-opacity fill that makes the main
window solid, so their backdrop showed the world through it. fn:applyWindowOpacity now reaches all
three, and each applies the fill from its own OnShow. None of them is ever released, so, as with
the intro pop-up, no shared-pool cleanup is owed. Both fixes were confirmed on the Dump window in a
client the same day.
Fixed — the TOC author was misspelled
All seven TOCs said ## Author: Pmptasty. They now say Pimptasty, the account these addons
publish under. Reported by the BijouRR peer review.
Changed — FastGuildInvite_Mainline.toc drops the stale 110207
Retail is on 12.0.5 (120005); 11.2.7 is a number no live client runs. The new TOC spec below flagged it because Ace3 no longer lists it.
Changed — GUI/Tabs/GuildRoster.lua and GUI/Tabs/Scan.lua read addon.LocNum, not the global FGI.LocNum
Each file takes local addon = FGI (GuildRoster :18, Scan :32) and then used the global -- four
sites and twelve respectively. Identical in the
client; offline, a spec that swaps _G.FGI for a fixture left the live tab reading the fixture, and
the roster callback died on LocNum nil.
Tests — --order shuffle:12345 went from 1687 failures to 0, then a new permutation found 24 more
The harness's
securecallfunctionpropagated errors (the client hands them to the error handler and returns). One raising CallbackHandler handler therefore left LibGuildRoster'sregistry.recurseat 1 for the rest of the run, and every laterRegisterCallbackwas parked ininsertQueueforever -- measured through a new failure message inzz_roster_rank_refresh_spec(recurse=1, parked=2). Filed to WoWAPITesting and fixed there as8e2e404; the pin moved to it. That one root was behind the designate-election, policy-names, guild-leave, blacklist-sweep and rank-refresh clusters.The trigger was ours: seven non-
zz_specs replaced_G.C_Timerand ten replaced_G.timeat file scope and never put them back.policy_sync_spec's recorder had noNewTimer, so the real bridge's roster-ready handler raised in DeltaSync'sP2PSession.lua:308. Every one now saves the global and restores it in a file-scopeteardown; that recorder gainedNewTimer. A stubtimealso ignores the table formtime({...}), which is how the Statistics tab came to bucket against a constant.Tests/support/addon.lua's load snapshot now also restorestime,date,GetServerTimeandprint, and the fields ofFGI,FGI.API,FGI.functionsandC_Timeras they were at load (deleting nothing, and neverFGI.DB, whichinitDBowns). It puts FGI's ownSlashCmdListentries back into whichever table is live, sincewow.reset()installs a fresh one. Measured: without the clock names, 171 failures; withoutprint, 167.announce_soundboard_specnow declares LibGuildRoster ready itself; its profiles post to guild chat, which Announce holds until the roster is built.2026-09-24, from 240 failures to 21. Two causes:
zz_reset_settings_specwrotehistory.invites[1]into the live list and then set it to nil. When an earlier file had already logged invites, that left a hole at the front of a longer list, and every laterhistory.trimdied oninvites[1].time. About 230 examples in later files failed as a result. The example now uses its own table and puts the original back. Peer Review then found the put-back was inline, after seven assertions, so any failure skipped it (M2). Every example in the file now registers its cleanup before writing, andafter_eachruns it.totals.acceptis restored to its earlier value, not reset to 0. The "Kept-Testrealm" blacklist entry another example wrote, and never removed, is cleaned up the same way.Modules/compactFrame.luaread the globalFGI.LocNumat six sites, the same slip fixed above in the Guild Roster and Scan tabs. It now readsaddon.LocNum. Peer Review then pointed at the rest of the class, and the other nine files that still read the global were converted too:GUI/MainWindow.luaand the Announce, Anti-Spam, Blacklist, Eligibility, Filters, History, Messages and Statistics tabs. Every one opens withlocal addon = FGI, so nothing changes in the client. No shipped call site readsFGI.LocNumany more.
The 21 that remain do not come from either fix.
zzzz_perf_spec's one bareassert.has_errorpins its message (Peer Review, 2026-09-23): a bare one accepts any error.zz_scan_engine_specdeclares the default subdivision axes per example and puts them back: a dozen specs writesubdivideLvl, several leave itfalse, and every range then cost 1.zz_scan_batch_speccloses the main window before each example as well as after (a window an earlier file left open madeMW:Open("scan")a no-op, so the strip still readBlacklist (0)), and its failure message now lists every strip caption present.2026-09-25: seed 12345 is at 0 failures (3638 passed), and so is the default order. Removing one whisper-tab example moved the permutation again and surfaced four failures in three files, each an example relying on state no earlier file had touched.
zzzz_core_specre-arms the one-shot entering-world keybind frame (found by its handler'skeybindWorldFrameupvalue) instead of assuming nobody fired the event first.zzzz_feature_detect_fallbacks_spectakesFGI.RealmResolverout of the way, since an earlier file had taught itBob-Testrealm.zz_scan_guilded_column_specgives the zone gate an empty set, since another spec had left Orgrimmar on the Quiet Zones list; its failure message now names which gate refused the row. Two leftoverwrit-cannot:markers (in those last two files) were removed. Other seeds were not run. Peer Review's count of 60+ indented_G.<name> =replacements never checked for a restore, and ~20 inlineprintswaps, still stands; the loader snapshot masks that class rather than surfacing it.
Tests — harness pin b818c74 → 8e2e404 → de64643
8e2e404 is the securecallfunction fix above. de64643 adds tools/verify-toc-deps.lua; its
Adoption entry's own command fails from a consumer root (require("tools.toc") resolves against
the working directory), which was reported back. Run from the harness root it reports four gaps for
FastGuildInvite_Mainline.toc: 120005/120007/120100 missing from VersionCheck-1.0, GuildRoster,
AceCommQueue-1.0 and DeltaSync. Each of the four was sent a finding; none are FGI's files.
Tests — zz_toc_manifest_spec.lua (new): the seven TOCs load the same files
Only _Mainline and _Camelot may add LibWho_Retail.lua. For one afternoon it also checked our
hard dependencies' interface numbers (after Questbook found Ace3 missing 16001 for WoW Forever);
that half was removed in favour of the harness's verify-toc-deps, which reads every one of a
dependency's TOCs and is deliberately discovery rather than a gate. zz_presence_spec also gains
_Camelot, which it had missed.
No player-visible change in this section, so docs/Curseforge_Description.html and
Modules/intro.lua get no v2.14.3 entry for it.
Older releases have been moved to CHANGELOG_ARCHIVE.md — v2.14.2 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, v2.13.2, v2.13.3 and v2.13.4 whole on 2026-09-20, v2.13.5 whole on 2026-09-25, v2.14.0 and v2.14.1 whole the same day, v2.14.2 whole on 2026-10-03. The next candidate is v2.14.3, whole.
This mod has no additional files

