From f38395a95c973fdbdd97a25f5f8980a49326d4ca Mon Sep 17 00:00:00 2001 From: Horyoshi <42314202+Cinecom@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:52:02 +0200 Subject: [PATCH] Fix profsync guild-wide lag, price MC trash, bid on 0 GP items (10.20.5) profsync: stop expanding skill headers on every read. Expand/CollapseSkillHeader fire SKILL_LINES_CHANGED, which this addon also listens to, so each read re-armed its own debounced check -- a permanent 15s cycle that dragged pfQuest's quest-giver rebuild and Cartographer's addon walk along with it on every guild member, whether or not they ever opened a profession. Reads are now plain scans that report whether a collapsed header may have censored them; an expand happens only where the difference actually matters (a known profession vanishing, the open window's own rank, /status), and is bracketed by a grace window so our own echo is ignored. A drop is never announced off an incomplete read. loot: only skip items with NO price entry. A price of 0 is a known item that just hasn't been priced yet, and it was silently skipping the bid/trade window. prices: Molten Core trash belts and bracers 0 -> 67 GP, Boots of Blistering Flames and Core Forged Helmet 100 -> 67, add Core Forged Greaves. minep default 300. Co-Authored-By: Claude Opus 5 --- prices.lua | 41 +++++++------- profsync.lua | 144 +++++++++++++++++++++++++++++++++++++------------ shootyepgp.lua | 12 +++-- shootyepgp.toc | 2 +- 4 files changed, 140 insertions(+), 59 deletions(-) diff --git a/prices.lua b/prices.lua index 235c211..880d292 100644 --- a/prices.lua +++ b/prices.lua @@ -147,6 +147,7 @@ local prices = { [18808] = {125,"T1"}; --Gloves of the Hypnotic Flame [18809] = {125,"T1"}; --Sash of Whispered Secrets [18812] = {125,"T1"}; --Wristguards of True Flight +[18806] = {9,"T1"}; --Core Forged Greaves [19140] = {150,"T1"}; --Cauterizing Band [18805] = {150,"T1"}; --Core Hound Tooth [18803] = {300,"T1"}; --Finkle's Lava Dredger @@ -180,27 +181,27 @@ local prices = { [92080] = {9999,"T1"}; --Molten Corehound --Trash Mobs -[16817] = {0,"T1"}; --Girdle of Prophecy -[16802] = {0,"T1"}; --Arcanist Belt -[16806] = {0,"T1"}; --Felheart Belt -[16827] = {0,"T1"}; --Nightslayer Belt -[16828] = {0,"T1"}; --Cenarion Belt -[16851] = {0,"T1"}; --Giantstalker's Belt -[16838] = {0,"T1"}; --Earthfury Belt -[16858] = {0,"T1"}; --Lawbringer Belt -[16864] = {0,"T1"}; --Belt of Might +[16817] = {67,"T1"}; --Girdle of Prophecy +[16802] = {67,"T1"}; --Arcanist Belt +[16806] = {67,"T1"}; --Felheart Belt +[16827] = {67,"T1"}; --Nightslayer Belt +[16828] = {67,"T1"}; --Cenarion Belt +[16851] = {67,"T1"}; --Giantstalker's Belt +[16838] = {67,"T1"}; --Earthfury Belt +[16858] = {67,"T1"}; --Lawbringer Belt +[16864] = {67,"T1"}; --Belt of Might [81260] = {150,"T1"}; --Lavashard Axe -[16819] = {0,"T1"}; --Vambraces of Prophecy -[16799] = {0,"T1"}; --Arcanist Bindings -[16804] = {0,"T1"}; --Felheart Bracers -[16825] = {0,"T1"}; --Nightslayer Bracelets -[16830] = {0,"T1"}; --Cenarion Bracers -[16850] = {0,"T1"}; --Giantstalker's Bracers -[16840] = {0,"T1"}; --Earthfury Bracers -[16857] = {0,"T1"}; --Lawbringer Bracers -[16861] = {0,"T1"}; --Bracers of Might -[81261] = {100,"T1"}; --Boots of Blistering Flames -[81262] = {100,"T1"}; --Core Forged Helmet +[16819] = {67,"T1"}; --Vambraces of Prophecy +[16799] = {67,"T1"}; --Arcanist Bindings +[16804] = {67,"T1"}; --Felheart Bracers +[16825] = {67,"T1"}; --Nightslayer Bracelets +[16830] = {67,"T1"}; --Cenarion Bracers +[16850] = {67,"T1"}; --Giantstalker's Bracers +[16840] = {67,"T1"}; --Earthfury Bracers +[16857] = {67,"T1"}; --Lawbringer Bracers +[16861] = {67,"T1"}; --Bracers of Might +[81261] = {67,"T1"}; --Boots of Blistering Flames +[81262] = {67,"T1"}; --Core Forged Helmet [81263] = {125,"T1"}; --Lost Dark Iron Chain [81264] = {125,"T1"}; --Shoulderpads of True Flight [81265] = {125,"T1"}; --Ashskin Belt diff --git a/profsync.lua b/profsync.lua index 75a2879..1b9c377 100644 --- a/profsync.lua +++ b/profsync.lua @@ -109,6 +109,11 @@ local LEARN_SETTLE = 12 -- quiet time after the last "you have learned" local LEARN_MAX_WAIT = 90 -- ...but never postpone a learn batch longer than this local RANK_MIN_GAP = 300 -- min seconds between two skill-level-only messages local SKILL_DEBOUNCE = 15 -- settle time after SKILL_LINES_CHANGED +-- ⚠ Expanding or collapsing a skill header FIRES SKILL_LINES_CHANGED — see the +-- warning above ReadSkills. This is how long the addon disbelieves that event +-- after having caused it itself. A genuine skill-up landing inside the window is +-- simply missed, which costs a cosmetic rank line until the next idle sweep. +local SKILL_ECHO_GRACE = 1 local IDLE_CHECK = 300 -- background skill re-check cadence local ACK_TIMEOUT = 60 -- how long a batch may sit unacknowledged local RETRY_DELAY = 600 -- pause everything after finding the relay offline @@ -186,6 +191,7 @@ local pending = {} -- [profId] = batch awaiting acknowledgement local metaCache = {} -- [profId][recipeId] = { n = name, i = icon } local metaRounds = 0 local skillsCheckAt = nil -- debounced SKILL_LINES_CHANGED handler +local skillEchoUntil = nil -- ignore SKILL_LINES_CHANGED we caused ourselves local nextIdleCheck = nil local learnSettleAt = nil -- debounced "you have learned" burst local learnFirstAt = nil -- start of the current burst, bounds the debounce @@ -523,32 +529,26 @@ local function RestoreSkillHeaders(collapsed) end end --- Returns [profId] = { r = rank, m = max } for every tracked profession this --- character has, or nil when the pane could not be read cleanly right now. -local function ReadSkills() - local anyCollapsed = false +local function AnyCollapsedHeader() for i = 1, GetNumSkillLines() do local _, isHeader, isExpanded = GetSkillLineInfo(i) - if isHeader and not isExpanded then anyCollapsed = true break end + if isHeader and not isExpanded then return true end end + return false +end - local restore = nil - if anyCollapsed then - restore = ExpandSkillHeaders() - if not restore then return nil end - end - - -- ⚠ A NAME CAN APPEAR TWICE. "Survival" is both a Turtle profession and a - -- hunter's class skill line, and a hunter sees both in this pane, so the last - -- one read must not win by accident — the higher cap does, which is always the - -- profession. A hunter who does NOT have the profession is caught later and - -- more cheaply: nothing in their spellbook opens it, so it is never scanned - -- and never reported. - -- - -- Deliberately no minimum-cap gate here. It would be a second way to tell the - -- two apart, but it is also a single condition that could silently blank every - -- profession on a client whose returns differ, and the spellbook check already - -- covers the case. +-- ⚠ A NAME CAN APPEAR TWICE. "Survival" is both a Turtle profession and a +-- hunter's class skill line, and a hunter sees both in this pane, so the last +-- one read must not win by accident — the higher cap does, which is always the +-- profession. A hunter who does NOT have the profession is caught later and +-- more cheaply: nothing in their spellbook opens it, so it is never scanned +-- and never reported. +-- +-- Deliberately no minimum-cap gate here. It would be a second way to tell the +-- two apart, but it is also a single condition that could silently blank every +-- profession on a client whose returns differ, and the spellbook check already +-- covers the case. +local function ScanSkillLines() local out = {} for i = 1, GetNumSkillLines() do local name, isHeader, _, rank, _, _, maxRank = GetSkillLineInfo(i) @@ -562,11 +562,48 @@ local function ReadSkills() end end end - - RestoreSkillHeaders(restore) return out end +--[[ ⚠ EXPANDING A SKILL HEADER IS EXPENSIVE FOR THE WHOLE UI, NOT JUST FOR US. + + ExpandSkillHeader/CollapseSkillHeader fire SKILL_LINES_CHANGED — FrameXML's own + expand button calls nothing else and relies on that event to redraw the pane — + and that event goes to EVERY addon. On a normal OctoWoW install it lands on + pfQuest, whose handler compares the visible skill-line names and, because + collapsing a header hides its children from GetSkillLineInfo, sees a change + every single time and runs a full pfDatabase:SearchQuests() quest-giver rebuild; + and on Cartographer's Professions module, which walks GetNumAddOns() calling + GetAddOnMetadata on every installed addon. + + Worse, it used to feed itself: this addon listens to SKILL_LINES_CHANGED too, + so its own expand/collapse re-armed the debounced check, which read the skills + again, which expanded again — a permanent 15-second cycle of quest-giver + rebuilds on every guild member, whether or not they ever opened a profession. + That is what made the whole guild lag (2026-08-09). + + So: the plain read NEVER touches a header, and callers that can live with a + partial answer ask for one. `mayExpand` is for the rare case where the + difference actually matters — telling "profession abandoned" apart from + "profession hidden under a collapsed header" — and it brackets the mutation in + skillEchoUntil so our own echo is not mistaken for a real skill change. + + Returns skills, complete. `complete` false means a collapsed header may be + hiding professions, so a MISSING one proves nothing. ]] +local function ReadSkills(mayExpand) + local skills = ScanSkillLines() + if not AnyCollapsedHeader() then return skills, true end + if not mayExpand then return skills, false end + + skillEchoUntil = GetTime() + SKILL_ECHO_GRACE + local restore = ExpandSkillHeaders() -- nil when the Skills pane is open + if not restore then return skills, false end + skills = ScanSkillLines() + RestoreSkillHeaders(restore) + skillEchoUntil = GetTime() + SKILL_ECHO_GRACE + return skills, true +end + -- --------------------------------------------------------------------------- -- Reading an open profession window -- --------------------------------------------------------------------------- @@ -790,13 +827,31 @@ end local function CheckSkills() if not CanSend() then return end - local skills = ReadSkills() - if not skills then - skillsCheckAt = GetTime() + SKILL_DEBOUNCE -- Skills pane open, come back - return - end + local skills, complete = ReadSkills(false) local db = DB() + -- ⚠ Only pay for a header expand when a profession we ALREADY KNOW ABOUT has + -- vanished from the read, because that is the single decision a partial read + -- cannot make (see ReadSkills for what an expand costs the rest of the UI). A + -- rank read off a visible line is trustworthy on its own, and a BRAND NEW + -- profession hidden under a collapsed header is not worth an expand either: + -- opening its window is the only way to use it, and that reports the rank + -- alongside the recipes. + if not complete then + local suspect = false + for id in pairs(db.profs) do + if PROF_BY_ID[id] and not skills[id] then suspect = true break end + end + if suspect then + local full, ok = ReadSkills(true) + if not ok then + skillsCheckAt = GetTime() + SKILL_DEBOUNCE -- Skills pane open, come back + return + end + skills, complete = full, ok + end + end + -- Gone from the pane means abandoned. The row is kept, flagged, and only -- deleted once the website confirms — otherwise a lost "dropped" message -- would leave the site advertising recipes the player no longer has. @@ -809,7 +864,10 @@ local function CheckSkills() db.profs[id] = nil pending[id] = nil elseif not skills[id] then - if not state.gone or not pending[id] then + -- Belt and braces: the suspect check above should have upgraded the read + -- to a complete one before we get here. Never announce a drop off a read + -- that a collapsed header could have censored. + if complete and (not state.gone or not pending[id]) then state.gone = true pending[id] = { drop = true, at = GetTime() } Send("X", tostring(id)) @@ -1043,8 +1101,16 @@ local function OnWindowVisible(isCraft) if prof then local state = ProfState(prof.id) - local skills = ReadSkills() + -- The cheap read first; only if THIS profession is the one hidden under a + -- collapsed header is an expand worth it — and here it is, because the + -- alternative is publishing a rank of 0/0. This path already only runs when + -- the recipe list actually changed, so it is rare. + local skills = ReadSkills(false) local info = skills and skills[prof.id] + if not info then + skills = ReadSkills(true) + info = skills and skills[prof.id] + end -- The stored rank is -1 until the website has confirmed one, which is not -- a number to put on the wire; fall back to 0 when the pane is unreadable. local rank = (info and info.r) or state.r @@ -1107,6 +1173,12 @@ f:SetScript("OnEvent", function() end if event == "SKILL_LINES_CHANGED" then + -- ⚠ IGNORE OUR OWN ECHO. Expanding or collapsing a header to read the pane + -- fires this event, so without the grace window the check that did the + -- expanding re-arms itself and runs again forever, every SKILL_DEBOUNCE + -- seconds, dragging pfQuest's quest-giver rebuild along with it. See + -- ReadSkills. + if skillEchoUntil and GetTime() < skillEchoUntil then return end -- Fires on every point of skill-up, so it only arms a debounced check; that -- check sends nothing unless a profession appeared, vanished, or moved. skillsCheckAt = GetTime() + SKILL_DEBOUNCE @@ -1198,10 +1270,12 @@ local function PrintStatus() or (online and " online" or " OFFLINE")) Say("guild " .. guild .. " | relay " .. relay) - local skills = ReadSkills() - if not skills then - Say("could not read the Skills pane (close the character window)") - else + -- A human typed this, so the expand is worth it here whatever it costs. + local skills, complete = ReadSkills(true) + if not complete then + Say("partial reading — close the character window for the full picture") + end + do local parts, any = {}, false for id, info in pairs(skills) do local prof = PROF_BY_ID[id] diff --git a/shootyepgp.lua b/shootyepgp.lua index d6a55c1..fa5eb6d 100644 --- a/shootyepgp.lua +++ b/shootyepgp.lua @@ -10,7 +10,7 @@ local T = AceLibrary("Tablet-2.0") local L = AceLibrary("AceLocale-2.2"):new("shootyepgp") sepgp.VARS = { basegp = 300, - minep = 0, + minep = 300, baseaward_ep = 100, decay = 0.9, max = 1000, @@ -2250,7 +2250,10 @@ function sepgp:tradeLoot(playerState,targetState) local link_found, _, itemColor, itemString, itemName = string.find(itemLink, "^(|c%x+)|H(.+)|h(%[.+%])") if (link_found) then local price = sepgp_prices:GetPrice(itemString,sepgp_progress) - if not (price) or price == 0 then + -- Only skip items with NO price entry (untracked trash). A price of 0 + -- means a known item that just hasn't been priced yet (Gear Points seeds + -- new items at 0) -- still open the window so it can be handed out/logged. + if not (price) then return end local bind = self:itemBinding(itemString) @@ -2380,7 +2383,10 @@ function sepgp:processLoot(player,itemLink,source) local bind = self:itemBinding(itemString) if not (bind) then return end local price = sepgp_prices:GetPrice(itemString,sepgp_progress) - if (not (price)) or (price == 0) then + -- Only skip items with NO price entry (untracked trash). A price of 0 means + -- a known item that just hasn't been priced yet (Gear Points seeds new items + -- at 0) -- still open the MS/OS window so it can be handed out/logged. + if (not (price)) then return end local class,_ diff --git a/shootyepgp.toc b/shootyepgp.toc index 23f4be2..831a7c6 100644 --- a/shootyepgp.toc +++ b/shootyepgp.toc @@ -7,7 +7,7 @@ ## X-Website: https://github.com/cinecom/shootyepgp/ ## X-Category: Guild ## X-Credits: shooty, Qcat(zhCN) -## Version: 10.20.3 +## Version: 10.20.5 ## X-Alpha: ## OptionalDeps: Ace2 ## SavedVariables: sepgp_saychannel, sepgp_groupbyclass, sepgp_groupbyarmor, sepgp_groupbyrole, sepgp_raidonly, sepgp_dbver, sepgp_debug, sepgp_fubar, sepgp_sound, RaidStrikeDB, RaidStrikeResetTime