Both sub-tables carried per-locale strings only so pfUI's own code could
match against localized text. ClassicAPI's numeric item APIs replace
both:
- GetBagFamily now reads classID/subClassID from C_Item.GetItemInfoInstant
and switches on the numbers (class 1 = Container, class 11 = Quiver).
- eqcompare pulls the itemID from GameTooltip:GetItem(), fetches the
numeric invType via C_Item.GetItemInventoryTypeByID, and looks up the
destination slot(s) in a numeric slotTable keyed by Enum.InventoryType.
Pair-slot invtypes (finger / trinket / one-hand weapon) list both
destinations directly, so the "_other" string-concat hack is gone.
Removes the setglobal INVTYPE_* injection, the tooltip text scan, and
the itemtypes + bagtypes locale sub-tables across all 7 files.
Audited every consumer of pfUI_locale[*][key] across the codebase. Four
sub-tables have no non-locale-file readers left:
- hunterpaging — old auto-page trigger removed
- interrupts — replaced by Nampower SPELL_INTERRUPTED events
- spells — replaced by ClassicAPI Spell.dbc lookups
- icons — replaced by ClassicAPI C_Spell.GetSpellName / icon path
Removed the entries from all 7 locale files. ~18k dead lines gone,
~70% shrink per file.
Seven ad-hoc OnUpdate handlers were only spinning long enough to reach
a known deadline or a next-frame defer, then unhooking themselves.
Convert them to their proper primitives:
- autovendor: 0.3s wait after junk sell → C_Timer.After(0.3, ...)
- innervatecall: cooldown-expiry ready ping → C_Timer.After(cd, ...)
- focus: re-arm UI_ERROR_MESSAGE next tick → RunNextFrame
- macrotweak: conflict scan after addons load → RunNextFrame
- ui-widgets (CreateQuestionDialog): font-measure resize → RunNextFrame
- libdebuff: post-PEW Nampower init → RunNextFrame
- bubbles: WorldFrame scan after chat event → RunNextFrame
Net -18 lines and no more throwaway frames sitting on the OnUpdate list.
Introduce a central pfUI.events registry (ClassicAPI's
CallbackRegistryMixin, undefined events allowed) initialized in pfUI.lua
before any module body runs, so publishers/subscribers don't depend on
module load order.
firstrun sets `pfUI.firstrun.completed` and fires `firstrun:complete` at
the point NextStep detects no pending steps. PLAYER_ENTERING_WORLD re-
enters this path on every zone, so the flag is a one-shot guard.
addoncompat drops its 0.1s OnUpdate poll and either RunQueues immediately
(returning user, all steps already done) or subscribes to the event.
Buff and debuff slots only called CooldownFrame_SetTimer on the
`expirationTime > 0` (or `duration > 0` for buffs) paths. When the new
target's aura at the same slot index had neither — permanent / passive
auras like Retribution Aura — neither branch fired and the slot kept
displaying the previous target's swirl/timer.
Add an explicit 0/0/0 clear on every path that doesn't set a real
timer, so the button always starts from a known state.
Fixes#13.
`GetPlayerBuff(PLAYER_BUFF_START_ID + this.id, filter)` assumes the
visual index pfUI shows matches the engine's slot order. When that
mismapping happens — most easily reproduced by stacking buffs that
share a slot family — right-clicking one buff cancels another.
`C_Spell.CancelSpellByID(spellID)` ships CMSG_CANCEL_AURA keyed to the
spell, not a slot, so it's immune to whatever order the slot table is
in. Cache `spellId` on the button at refresh time in buff.lua; in the
unitframes/buffwatch handlers fetch the aura fresh via
`C_UnitAuras.GetAuraDataByIndex` at click time.
Fixes#10.
New tooltip.movespeed config knob (default off, checkbox in the GUI's
tooltip page). When on, the unit tooltip gains a "Speed: N%" line where
N is the unit's run speed normalized to vanilla's 7.0 yd/s base — 100
unmounted, 160 on a 60% mount, 200 on epic, less under snares.
Uses runSpeed (return 2 of GetUnitSpeed), not currentSpeed, so the
number reflects what the unit *would* be running at — visible even
while they're standing still. runSpeed is 0 for out-of-range units, so
the line is skipped in that case.
The player frame's "Effective Haste" mode was hardcoded talent-position
scrapes: GetTalentInfo(1, 16) for the Mage "Accelerated Arcana"
(flat 5%) and GetTalentInfo(1, 14) for the Warlock "Rapid Deterioration"
(3% per rank), folded into the displayed haste % via
`(1 / (modCastSpeed * modCastingTime) - 1) * 100`.
That's two problems in one:
- Hardcoded talent indices and effect percentages — brittle to any
Turtle tree reshuffle or retune.
- Conceptually muddled: it folds gear-haste and talent-cast-reduction
into one number that's hard to read as anything specific. The actual
effective cast time is already shown on the cast bar via
C_Spell.UnitCastingInfo (engine helper accounts for SpellMod op 10).
Drop modCastingTime, the LEARNED_SPELL_IN_TAB watcher frame that
maintained it, the per-class talent scrape, and the hasteMode == "2"
display branch. Collapse the now-binary "display_haste" config from a
3-option dropdown to a checkbox. Users on legacy "2" will see the
checkbox unchecked once and can re-enable with a single click.
Four more sites: GetUnbuffedRoster + SendChatMessageWide in api.lua,
the loot menu's inRaid local, and the raid module's early-return
guard. Same intent, named helper.
Three more `if GetNumRaidMembers() > 0 then` raid-vs-party branches
switched to `if IsInRaid()`. Same intent, named helper. Repo is now
clean of the legacy idiom (verified with a final grep).
Loops that actually need the count (`for i = 1, GetNumRaidMembers() do
GetRaidRosterInfo(i)`) keep the call — only the boolean form changes.
ClassicAPI ships modern IsInGroup() / IsInRaid() backports — drop the
GetNumPartyMembers() > 0 and GetNumRaidMembers() > 0 idioms (and the
GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 conflation) for the
named-intent variants. UnitInRaid("player") → IsInRaid() at the same
sites.
With Nampower as a hard dep, /pfcast for spell names always takes the
early CastSpellByName(msg, unit) path. The fallback branch that did the
SpellTargetUnit dance (resolve a friendly unit token, disable AutoSelf
Cast, call SpellTargetUnit) hasn't been reachable in a while, and it
dragged a pile of supporting infrastructure with it.
modules/mouseover.lua:
- Drop the st_units token list, GetUnitString helper, and the
UnitTokenFromGUID rewrite of GetUnitString — all only used by the
dead fallback.
- Drop the NoSelfCast helper (only the dead fallback called it).
- Drop the pfMouseOver frame; its only purpose was to hold a .unit
field the dead fallback wrote and libpredict's hook read.
- The macro path collapses to: if not the current target, swap target,
run the loadstring'd func, restore the previous target.
- 99 lines → 34.
libs/libpredict.lua:
- Drop the dead `local mouseover = pfUI.uf.mouseover.unit` plumbing in
the CastSpellByName hook — pfUI.uf.mouseover is gone and the field
was permanently nil anyway. The three `target or mouseover or default`
fallback chains collapse to `target or default`.
Modern mouseover/click-to-cast detection in libpredict goes through
pfUI.libpredict_pending_cast (populated by libdebuff from Nampower's
SPELL_CAST_EVENT) — that path is GUID-based, server-authoritative, and
untouched.
Replace per-locale name tables and per-rank ID lists with single
canonical-rank lookups through C_Spell. Spell.dbc bits hoisted to
named constants at module top.
libpredict:
- Four 7-locale tables (PRAYER_OF_HEALING / REJUVENATION / RENEW /
REGROWTH) collapsed to one C_Spell.GetSpellName(rank1id) call each.
- 25-entry SPELL_IDS (all ranks of Rejuv + Renew) for SPELL_GO_SELF
HoT detection replaced by name comparison against REJUVENATION /
RENEW. No per-rank ID maintenance.
libdebuff:
- GetSpellRecField(id, "name") → C_Spell.GetSpellName(id) at all call
sites; the presence-guard pattern is gone (ClassicAPI is a hard
dep, per memory).
- GetSpellRecField(id, "rank") → C_Spell.GetSpellSubtext(id).
swingtimer:
- Hoist FLAG_AUTOATTACK / ATTR_KEEP_SWINGS / ATTR_ON_NEXT_SWING to
module-top constants via tonumber("0xNN", 16) so the SPELL_GO_SELF
hot path stops re-parsing them on every call. Lua 5.0 has no hex
number literals; strtoul-backed tonumber handles the "0x" prefix.
Drop four hardcoded spell-ID tables (swingDelaySpells, hsSpellIDs,
cleaveSpellIDs, maulSpellIDs) and the broad interruptFlags > 0 reset
heuristic in favor of server-parity checks against Spell.dbc bits via
nampower's GetSpellRecField:
- "Resets the swing on cast complete" gated on InterruptFlags's
SPELL_INTERRUPT_FLAG_AUTOATTACK (0x08) and AttributesEx2's lack of
SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS (0x20000) — mirrors the server's
Spell::IsMeleeAttackResetSpell. Reset OH alongside MH to match.
- "Slam-style delay" derived from absent 0x08 + a cast time existing
(implicit since SPELL_START_SELF only fires for cast-time spells).
Freezes the swing timer at SPELL_START and adds cast duration on
SPELL_GO instead of resetting.
- HS / Cleave / Maul classification via IsOnSwingSpell (ATTR_ON_NEXT_
SWING bit 0x04) + name comparison against rank-1 canonical names,
centralized into ClassifyOnSwingSpell + SetQueuedKind helpers.
Switch the lone GetSpellRec table-allocator call to GetSpellRecField for
consistency (single-field hash lookup, no shared-table reuse caveat).
Wand Shoot (spellID 5019) wasn't recognized as ranged anywhere, so it
fell through every branch of the SPELL_GO_SELF dispatch and hit the
catch-all interruptFlags > 0 reset, wiping the mainhand swing on every
shot. Casters melee-weaving between MH swings and wand fires lost their
swing visualization.
Replace the hardcoded RANGED_SPELLIDS / WAND_SHOOT_SPELLIDS tables with
C_Spell.IsRangedAutoAttackSpell (Spell.dbc AUTO_REPEAT bit) — catches
both Auto Shot and Shoot today, plus any future auto-repeat ranged
spell. ResetRanged now takes a replaceMH flag: true for Auto Shot /
Throw (Hunter ranged replaces melee), false for wand (independent
timers, both tick concurrently).
Closes#5.
Dev / git-cloned installs carry "@project-version@" in the toc, which
pfUI normalizes to pfUI.version.string = "dev". MigrateConfig was
writing that literal back into pfUI_config.version; on the next /reload
checkversion() parsed "dev" through tonumber() → nil → 0, evaluated
curversion as 0/0/0, and re-fired every version-gated migration block.
The >3.6.1 buff migration unconditionally rewrites buffs.{buffs,debuffs,
weapons} from the legacy global.hide{,w}buff knobs, so user toggles for
those checkboxes were getting wiped on every reload.
Stamp "999.999.999" on dev builds so subsequent comparisons turn false.
Affected users will need to re-toggle the buffs/debuffs/weapon-buffs
checkboxes once after this update.
Refs #7.
libhealth ships with pfUI and Nampower's GetUnitField (hard-dep) covers
the real-HP read; the MobHealth integration was a chained last-resort
fallback that could never actually win, and on the nameplate path it
also mixed percentage and real-value scales into the bar's SetMinMaxValues
since it overwrote hp/hpmax without hpmin.
GUID is the unique identity; the name check was leftover from the
name-only era and became dead weight once cachedGuid (NAME_PLATE_UNIT_
ADDED) was added alongside it.
The plate-reuse reset only cleared name/guid/player/cdCache, leaving
hp/hpmax/rgb/namecolor/levelcolor/target/mouseover/inCombat behind. The
PERF gates downstream ("only update X when X changed") then skipped bar
fill / color / text updates when the new occupant happened to share a
cached value with the previous one — most easily reproduced on
plate-pool reuse in starting zones (mobs churn fast, percentage values
collide). Nuke the whole cache via table.wipe.
Refs #8.
Drop CHAT_MSG_COMBAT_FACTION_CHANGE + SanitizePattern(FACTION_STANDING_
INCREASED) string parsing in favor of FACTION_STANDING_CHANGED, which
ships (factionID, newStanding, repGained) directly — locale-independent,
no chat-string scrape. Track factionID instead of faction name.
Collapse the two for i=1, 99 GetFactionInfo loops (tooltip + bar fill)
into a single GetRepDisplay helper: GetFactionInfoByID for the remembered
faction, C_Reputation.GetWatchedFactionData for the watched one.
Watched-faction-changed detection now compares factionIDs.
Hook DoTradeSkill to capture the requested count, then on the first
SPELLCAST_START of an isTradeskill cast stretch endTime to span all
crafts so the player bar fills continuously across the chain. Mid-chain
SPELLCAST_START / SPELL_START_SELF events refresh the "(N)" remaining
label and reset a per-craft spark that crosses the bar once per craft.
SPELL_GO_SELF counts completions; SPELLCAST_STOP no-ops while merged.
Gated by a new C.castbar.player.mergetradeskill knob (default on).
Drive the cast bar from cast lifecycle events instead of polling C_Spell
every frame. OnUpdate now only animates a stamped start/end and fades out;
all state transitions come from events:
- player: vanilla SPELLCAST_START / _STOP / _FAILED / _INTERRUPTED /
_CHANNEL_START / _CHANNEL_STOP, plus nampower SPELL_START_SELF (the only
signal for a chained same-spell recast, which never runs the client cast
path) and SPELL_DELAYED_SELF for pushback (applied from its delayMs arg).
- non-player (target/focus): nampower SPELL_START_OTHER / SPELL_FAILED_OTHER
+ PLAYER_TARGET_CHANGED / PLAYER_FOCUS_CHANGED.
Data comes from ClassicAPI's C_Spell.UnitCastingInfo / UnitChannelInfo
(player exact; other units from the SMSG_SPELL_START cache). SPELL_START_*
re-polls are deferred one frame so ClassicAPI's packet co-hook has stamped
before the read. SPELLCAST_CHANNEL_STOP only clears when a channel is shown,
so a lagged channel-stop doesn't wipe a following cast's bar.
Remove the now-unused CASTBAR_EVENT_* constants from compat/vanilla.lua.
ClassicAPI's recent C_Spell additions cover remote-unit casts natively
(SMSG_SPELL_START co-hook caching per caster GUID), so the two parallel
cast trackers pfUI was running — libcast.lua and the libdebuff_casts
table inside libdebuff.lua — can both retire.
Migrations:
- modules/castbar.lua: focus/player cast-info gathering reads
C_Spell.UnitCastingInfo / UnitChannelInfo directly. Fallback ladder
(libdebuff_casts → pfGetCastInfo → pfGetChannelInfo) collapses into a
single call. Pushback handlers stop writing back into a non-existent
cache; the local this.endTime is the source of truth.
- modules/nameplates.lua: GetCastInfo(guid) now resolves to a unit token
via UnitTokenFromGUID and queries C_Spell, returning the same compact
struct shape downstream code expected. UpdateCastbar collapses from a
three-branch hierarchy (dead IterDebuffs / libdebuff_casts / libcast)
to one C_Spell read.
- modules/afkcam.lua: pfGetCastInfo+pfGetChannelInfo round-trip becomes
a single C_Spell.UnitCastingInfo("player") or UnitChannelInfo fallback.
- libs/libpredict.lua: HealComm timing uses C_Spell on the sender's unit
token after a small group-roster walk to resolve the sender's name.
Deletions:
- libs/libcast.lua entirely (-571 lines) plus its init/libs.xml entry.
- libdebuff_casts / libdebuff_item_icons tables and their write sites
in libs/libdebuff.lua (the SPELL_START_*, SPELL_GO_*, SPELL_FAILED_*
event handlers stop maintaining them but keep firing the
libdebuff_*_hooks broadcast surface for actionbar / swingtimer /
libtotem). SPELLCAST_CHANNEL_STOP now reads the active channel from
C_Spell.ChannelInfo.
- modules/superwow.lua's supercast block — UNIT_CASTEVENT writes into
libcast.db are redundant now that C_Spell co-hooks the same packet.
- The cast-bar item-icon override that swapped in a potion/trinket
icon for item-triggered casts. Spell icon stays; the item-icon
metadata path (libdebuff_item_icons) went with libdebuff_casts.
Steady Shot synthetic cast bar — Turtle WoW-specific:
- castbar.lua gains a pfUI.synthetic_casts[unit] fallback that fires
only when C_Spell returns nil, so abilities the engine treats as
instant but which have a meaningful wait window can still render a
cast bar.
- modules/turtle-wow.lua replaces the old libcast.customcast block with
a Nampower SPELL_QUEUE_EVENT subscriber. ON_SWING_QUEUED matching
the localized "Steady Shot" name writes a 1.4s synthetic entry;
ON_SWING_QUEUE_POPPED clears it; castbar's endMs guard self-expires
the entry as a safety net. Note: haste scaling (libcast.ApplyShotHaste)
is gone — bar may finish slightly early under +ranged haste buffs.
Net: 152 insertions, 972 deletions.
ClassicAPI's UnitIsFeignDeath reads UNIT_FIELD_FLAGS bit 29 directly —
the authoritative server flag, no detection guesswork needed. Combined
with Nampower's GetUnitField("health"), we read live HP off the
descriptor instead of caching the moment-of-death healthbar value via
libtipscan.
Drops the name-keyed cache, the UNIT_HEALTH / PLAYER_TARGET_CHANGED
event handlers, and the tooltip scanner. Also fixes the staleness bug
where a feigning hunter taking further damage kept showing the cached
snapshot from when feign first triggered — live read updates with
every UnitHealth call now.
With every external caller of libdebuff:UnitDebuff / :UnitOwnDebuff
now on C_UnitAuras, the two public per-aura readers and the
nameplate-side cache they were feeding have no consumers.
- libs/libdebuff.lua: removes libdebuff:UnitDebuff (~120 lines),
libdebuff:UnitOwnDebuff (~75 lines), the _ownDebuffSortFunc helper,
and the local cache table. The slotOwnership / ownDebuffs /
allAuraCasts / pendingCasts bookkeeping stays — GetBestAuraCast
(libpredict) and GetEnhancedDebuffs (CleveRoids) still read it, and
the event handlers maintain it. GetSlotCaster / GetDebuffSlotMap
stay too; the DEBUFF_ADDED_OTHER handler and the debug printer use
them. File goes 2010 → 1870 lines.
- modules/nameplates.lua: deletes PlateCacheDebuffs (was already
rewritten on C_UnitAuras and unused once the display loop bypassed
the cache), PlateUnitDebuff, the cachedVerify scaffolding, and the
nameplate.UnitDebuff / nameplate.CacheDebuffs registrations.
- api/config.lua + modules/gui.lua: drops the now-defunct
"guessdebuffs" knob — its only effect was gating the dead cache.
ClassicAPI's recent C_UnitAuras additions (sourceUnit / sourceGUID /
non-player expirationTime / the PLAYER filter token) finally cover
everything libdebuff:UnitDebuff and :UnitOwnDebuff were doing — caster
attribution, accurate timing for non-player units, and the own-debuffs
filter. Migrating the remaining callers off the libdebuff readers.
- api/unitframes.lua: collapses the debuff render path's three-branch
if/else into a single C_UnitAuras.GetAuraDataByIndex with a HARMFUL
or HARMFUL|PLAYER filter selected by the selfdebuff config; tooltip
slot-finders match by sourceGUID instead of libdebuff's caster flag;
custom-debuff indicator scan unifies the same way.
- modules/buffwatch.lua: GetBuffData drops the libdebuff fallback;
tooltip slot-finder mirrors the unitframes pattern.
- libs/libpredict.lua: drops the orphaned UnitHasBuff slot-loop (no
callers left).
- api/api.lua: pfUI.api.UnitHasBuff tightens from a HELPFUL iteration
to a single GetAuraDataBySpellName lookup.
Each site that builds a cooldown ring from expirationTime carries the
talent-extension guard — when expirationTime exceeds the dbc base
duration (e.g. Shadow Affinity → SW:P), clamp start to now and use the
remaining time as the effective duration so CooldownFrame_SetTimer
doesn't get a future start it treats as "not yet begun".
Replaces the 99999999 sentinel with `LATEST = MIN`. The elseif
`< LATEST` then reduces to `< MIN` on source builds, which the
preceding `if` branch always catches first — so the soft-nudge never
fires for unpinned installs. No explicit guard needed.
A source/cloned install previously showed "ClassicAPI v9999.99.99 is
available" because the sentinel leaked into FormatVersion before the
release workflow could patch it. With this layout, source installs are
silent and only properly-pinned release zips trigger the nudge.
Workflow sed broadens to "= .*" so it overwrites the line regardless
of whether the RHS is a digit string or a Lua identifier.
Wrapping the version-check block in a do…end indented every constant
by two spaces, so the sed pattern's ^local anchor stopped matching and
the followup grep returned exit 1, killing the step under bash -e.
Captures the leading whitespace into \1 so the indent is preserved
through the substitution, and matches the two-space alignment between
LATEST and = so we don't accidentally hit LATEST_URL on the line below.
The earlier change pulled the ClassicAPI URL via
GetAddOnMetadata("!!!ClassicAPI", "X-Website"), but that's only
readable when ClassicAPI is loaded — and the popup that needs the URL
is exactly the case where the dep is absent or out of date. Hardcode
PFUI_CLASSIC_API_WEBSITE as a constant in pfUI.lua instead.
Also points the editbox and chat messages at /releases/latest rather
than /releases/tag/<MIN>. GitHub redirects to the actual latest
release, so users land on something current instead of the version
floor (which may be an ancient tag).
Splits the ClassicAPI version check into a hard floor (MIN, manual)
and a soft target (LATEST, pinned by the release workflow). Below MIN
pfUI disables itself entirely as before; between MIN and LATEST it
runs normally but fires a delayed chat nudge after PLAYER_LOGIN with
the available update. PLAYER_LOGIN handlers swap to
EventUtil.ContinueOnPlayerLogin so we don't have to spin a frame for
each branch.
URLs centralize on toc X-Website fields. pfUI.lua factors out
ClassicAPIReleaseUrl(version) sourcing from !!!ClassicAPI's metadata;
modules/gui.lua and libs/libdebuff.lua replace hardcoded
me0wg4ming/pfUI links with GetAddOnMetadata(pfUI.name, "X-Website")
lookups.
release.yml gains a pre-package step that queries ClassicAPI's latest
release tag, packs it (X*10000 + Y*100 + Z), and seds only the LATEST
line in pfUI.lua so the published zip ships with the correct soft
target.
Bare DisenchantAll() errored — the DLL requires an arg. Forward the
slash message through and default to "greens" when empty. Per
nampower's player_scripts.cpp, quality is a STRING keyword ("greens",
"blues", "purples", pipe-combined); numbers are interpreted as item
IDs, not quality levels. tonumber() lets users pass numeric item IDs
unquoted.
pfUI module bodies run under pfUI.env (setfenv in LoadModule), which
proxies reads to _G via __index but has no __newindex. Bare
"SLASH_FOO = ..." assignments therefore land on pfUI.env, not on _G,
and WoW's slash dispatcher reads _G.SLASH_* directly — so the slash
command never registered. SlashCmdList[KEY] assignments happened to
work because the table is read first (falls through), then the key
write hits the real global table.
/pfunitxp, /clickthrough, /ct, /disenchantall, /dea — all silently
broken until now. Fixed by prefixing SLASH_* writes with _G.
Packager auto-generates the GitHub release body from git log since the
last tag. The first release has no prior tag, so it walks all history
and overshoots GitHub's 125k-char body limit. Ship a curated
CHANGELOG.md with the highlights and point packager at it via
.pkgmeta's manual-changelog directive. Subsequent releases diff
tag-to-tag and stay small — either delete both files after this
release lands to restore auto-gen, or keep maintaining CHANGELOG.md
manually.
Switches the addon-message prefix to "pfUI-brues" so we don't share an
inbound queue with upstream pfUI installs at all — they don't register
a listener for this prefix, so our broadcasts get dropped at the engine
level instead of needing branch-string filtering on their end. The
"main" branch tag stays as a tiebreaker for any future fork variant
sharing this prefix.
Source/dev installs leave the toc Version as "@project-version@" until
release tooling substitutes it. pfUI.lua now stamps those as "dev" with
major/minor/fix=0 instead of silently falling back to 1.2.0, and
updatenotify short-circuits on pfUI.version.string == "dev" before
broadcasting so dev installs don't ship a stale low version to everyone
in guild/raid. Single source of truth — the dev marker lives in pfUI.lua.
URLs centralize via the toc's X-Website. updatenotify and firstrun read
it via GetAddOnMetadata + string.format("%s"), and all eight translation
files swap their hardcoded shagu.org references for the same %s slot.
Drive-bys: groupsize math uses ClassicAPI's IsInRaid/IsInGroup; toc
Author typo "modfied" → "modified".