From 106539a7f8802f2c36783ff814e0714616f0c2d8 Mon Sep 17 00:00:00 2001 From: Ionut Adrian Ciolan Date: Mon, 10 Aug 2026 22:07:03 +0300 Subject: [PATCH 1/2] Fix roster synchronization and empty-row handling --- PBM/PBM_ClassTabs.lua | 20 +++++-- PBM/PBM_RosterSync.lua | 120 ++++++++++++++++++++++++++++++++++++++++ PBM/PBM_TrackerCore.lua | 15 ++++- PlayerBotManager.toc | 1 + 4 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 PBM/PBM_RosterSync.lua diff --git a/PBM/PBM_ClassTabs.lua b/PBM/PBM_ClassTabs.lua index 759e033..e0dab88 100644 --- a/PBM/PBM_ClassTabs.lua +++ b/PBM/PBM_ClassTabs.lua @@ -1,4 +1,4 @@ -PBM = PBM or {} +PBM = PBM or {} PBM.State = PBM.State or {} -- ── Class-tab drag-to-reorder state (mirrors LichborneTracker) ──── @@ -344,6 +344,12 @@ function PBM.OpenNameMenu(row) -- class (unscanned/empty) simply have no menu. local _rd = row.dbIndex and LichborneTrackerDB.rows[row.dbIndex] if not _rd then return end + if not _rd.name or _rd.name == "" then + if LichborneAddStatus then + LichborneAddStatus:SetText("|cffffaa44Empty slot. Sync the roster or add a target/group first.|r") + end + return + end local opener = CLASS_MENU_OPEN[_rd.cls] if not (opener and PBM[opener]) then return end @@ -915,11 +921,17 @@ function PBM.RefreshRows() end) end - -- Delete + -- Delete. Empty-name rows can still contain stale spec/gear data in + -- SavedVariables, so remove those directly by DB index instead of + -- making them impossible to clear. row.delBtn:SetScript("OnClick", function() local srcData = LichborneTrackerDB.rows[di] - if not srcData or not srcData.name or srcData.name == "" then return end - PBM.RemoveCharacterReferences(srcData.name) + if not srcData then return end + if srcData.name and srcData.name ~= "" then + PBM.RemoveCharacterReferences(srcData.name) + else + table.remove(LichborneTrackerDB.rows, di) + end PBM.RefreshRows() if PBM.State.overviewRowFrames and #PBM.State.overviewRowFrames > 0 then PBM.RefreshOverviewRows() end if PBM.State.raidRowFrames and #PBM.State.raidRowFrames > 0 then PBM.RefreshRaidRows() end diff --git a/PBM/PBM_RosterSync.lua b/PBM/PBM_RosterSync.lua new file mode 100644 index 0000000..32e5844 --- /dev/null +++ b/PBM/PBM_RosterSync.lua @@ -0,0 +1,120 @@ +PBM = PBM or {} +PBM.State = PBM.State or {} + +local CLASS_NAMES = { + ["DeathKnight"] = "Death Knight", + ["Death Knight"] = "Death Knight", + ["Druid"] = "Druid", + ["Hunter"] = "Hunter", + ["Mage"] = "Mage", + ["Paladin"] = "Paladin", + ["Priest"] = "Priest", + ["Rogue"] = "Rogue", + ["Shaman"] = "Shaman", + ["Warlock"] = "Warlock", + ["Warrior"] = "Warrior", +} + +local function Trim(value) + return (value:gsub("^%s+", ""):gsub("%s+$", "")) +end + +local function FindTrackedBot(name) + local wanted = name:lower() + for _, row in ipairs(LichborneTrackerDB.rows or {}) do + if row.name and row.name ~= "" and row.name:lower() == wanted then + return row + end + end +end + +local function AddTrackedBot(name, cls) + local existing = FindTrackedBot(name) + if existing then return false end + + PBM.EnsureClass(cls) + local slot + for _, row in ipairs(LichborneTrackerDB.rows) do + if row.cls == cls and (not row.name or row.name == "") then + slot = row + break + end + end + if not slot then + slot = PBM.DefaultRow(cls) + table.insert(LichborneTrackerDB.rows, slot) + end + + slot.name = name + return true +end + +-- Parse the exact response emitted by mod-playerbots ListBots(): +-- Bot roster: +OnlineName Class, -OfflineName Class +-- Returns recognized, numberAdded, numberListed. +function PBM.ImportBotRosterMessage(message) + if type(message) ~= "string" then return false end + + local clean = message:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "") + local payload = clean:match("^Bot roster:%s*(.*)$") + if payload == nil then return false end + + local added, total = 0, 0 + for rawEntry in payload:gmatch("([^,]+)") do + local entry = Trim(rawEntry) + local _, name, rawClass = entry:match("^([+-])(%S+)%s+(.+)$") + local cls = rawClass and CLASS_NAMES[Trim(rawClass)] + if name and cls then + total = total + 1 + if AddTrackedBot(name, cls) then added = added + 1 end + end + end + + if PBM.RefreshRows then PBM.RefreshRows() end + if PBM.State.overviewRowFrames and #PBM.State.overviewRowFrames > 0 and PBM.RefreshOverviewRows then + PBM.RefreshOverviewRows() + end + + return true, added, total +end + +function PBM.RequestBotRoster() + PBM.State.rosterSyncPending = true + SendChatMessage(".playerbots bot list", "SAY") +end + +local rosterTimerFrame + +function PBM.ScheduleBotRosterRequest(delay) + local callback = function() PBM.RequestBotRoster() end + if C_Timer and C_Timer.After then + C_Timer.After(delay, callback) + return + end + + if not rosterTimerFrame then rosterTimerFrame = CreateFrame("Frame") end + local elapsed = 0 + rosterTimerFrame:SetScript("OnUpdate", function(self, delta) + elapsed = elapsed + delta + if elapsed >= delay then + self:SetScript("OnUpdate", nil) + callback() + end + end) +end + +local rosterEventFrame = CreateFrame and CreateFrame("Frame", "PBMRosterSyncEventFrame") +if rosterEventFrame then + rosterEventFrame:RegisterEvent("CHAT_MSG_SYSTEM") + rosterEventFrame:SetScript("OnEvent", function(_, _, message) + local matched, added, total = PBM.ImportBotRosterMessage(message) + if not matched then return end + + PBM.State.rosterSyncPending = false + local status = "|cff44ff44Roster synced: " .. total .. " found, " .. added .. " added.|r" + if LichborneAddStatus then LichborneAddStatus:SetText(status) end + if LichborneOutput then + LichborneOutput("|cffC69B3APBM:|r Roster synced: " .. total .. " found, " .. added .. " added.", 1, 0.85, 0) + end + end) +end diff --git a/PBM/PBM_TrackerCore.lua b/PBM/PBM_TrackerCore.lua index 3a276d3..a888cb2 100644 --- a/PBM/PBM_TrackerCore.lua +++ b/PBM/PBM_TrackerCore.lua @@ -21,6 +21,9 @@ local function OnFirstShow() if PBM.State.setupDone then return end PBM.State.setupDone = true local f = LichborneTrackerFrame + -- Allow normal desktop-style click-to-front behavior when this window + -- overlaps other DIALOG-strata addon windows. + f:SetToplevel(true) local fl = f:GetFrameLevel() -- Tabs (centered in frame) @@ -1122,13 +1125,19 @@ local function OnFirstShow() local loginBtn = MakeSimpleBtn("LichborneLoginBtn", "|cffd4af37Log in All Bots|r", 0.1, 0.6, 0.2, 335, 110, - 155, {{"Log in All Bots",0.78,0.61,0.23},{".playerbots bot add *",0.8,0.8,0.8}}) - loginBtn:SetScript("OnClick", function() SendChatMessage(".playerbots bot add *", "PARTY") end) + 155, {{"Log in All Bots",0.78,0.61,0.23},{"Logs in bots, then syncs the server roster.",0.8,0.8,0.8}}) + loginBtn:SetScript("OnClick", function() + if LichborneAddStatus then LichborneAddStatus:SetText("|cffffff88Logging in bots and requesting roster...|r") end + -- SAY reaches the command handler even when the player is not grouped; + -- PARTY was rejected client-side outside a party. + SendChatMessage(".playerbots bot add *", "SAY") + PBM.ScheduleBotRosterRequest(0.75) + end) local logoutBtn = MakeSimpleBtn("LichborneLogoutBtn", "|cffd4af37Log Out All Bots|r", 0.90, 0.20, 0.20, 335, 76, 155, {{"Log Out All Bots",0.78,0.61,0.23},{".playerbots bot remove *",0.8,0.8,0.8}}) - logoutBtn:SetScript("OnClick", function() SendChatMessage(".playerbots bot remove *", "PARTY") end) + logoutBtn:SetScript("OnClick", function() SendChatMessage(".playerbots bot remove *", "SAY") end) -- ── Remove Orphaned Bots button ──────────────────────────── -- Sends .playerbots bot remove for every character in the Overview tab roster diff --git a/PlayerBotManager.toc b/PlayerBotManager.toc index 0e5170d..7cd4f88 100644 --- a/PlayerBotManager.toc +++ b/PlayerBotManager.toc @@ -18,6 +18,7 @@ LichborneTracker.xml PBM\PBM_Constants.lua PBM\IPTiersColor.lua PBM\PBM_Database.lua +PBM\PBM_RosterSync.lua PBM\PBM_GearScore.lua PBM\PBM_Needs.lua PBM\PBM_Sort.lua From 76f97fa6856ce5288189e8b1a0ba03129a502ba4 Mon Sep 17 00:00:00 2001 From: Ionut Adrian Ciolan Date: Mon, 10 Aug 2026 22:23:21 +0300 Subject: [PATCH 2/2] Refine roster sync scheduling --- PBM/PBM_ClassTabs.lua | 4 +--- PBM/PBM_RosterSync.lua | 39 ++++++++++++++++----------------------- PBM/PBM_TrackerCore.lua | 5 +---- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/PBM/PBM_ClassTabs.lua b/PBM/PBM_ClassTabs.lua index e0dab88..7629485 100644 --- a/PBM/PBM_ClassTabs.lua +++ b/PBM/PBM_ClassTabs.lua @@ -921,9 +921,7 @@ function PBM.RefreshRows() end) end - -- Delete. Empty-name rows can still contain stale spec/gear data in - -- SavedVariables, so remove those directly by DB index instead of - -- making them impossible to clear. + -- Delete row.delBtn:SetScript("OnClick", function() local srcData = LichborneTrackerDB.rows[di] if not srcData then return end diff --git a/PBM/PBM_RosterSync.lua b/PBM/PBM_RosterSync.lua index 32e5844..b86fa73 100644 --- a/PBM/PBM_RosterSync.lua +++ b/PBM/PBM_RosterSync.lua @@ -1,24 +1,13 @@ -PBM = PBM or {} -PBM.State = PBM.State or {} - -local CLASS_NAMES = { - ["DeathKnight"] = "Death Knight", - ["Death Knight"] = "Death Knight", - ["Druid"] = "Druid", - ["Hunter"] = "Hunter", - ["Mage"] = "Mage", - ["Paladin"] = "Paladin", - ["Priest"] = "Priest", - ["Rogue"] = "Rogue", - ["Shaman"] = "Shaman", - ["Warlock"] = "Warlock", - ["Warrior"] = "Warrior", -} - local function Trim(value) return (value:gsub("^%s+", ""):gsub("%s+$", "")) end +local function NormalizeClass(name) + name = Trim(name) + if name == "DeathKnight" then name = "Death Knight" end + return PBM.CLASS_COLORS[name] and name or nil +end + local function FindTrackedBot(name) local wanted = name:lower() for _, row in ipairs(LichborneTrackerDB.rows or {}) do @@ -63,7 +52,7 @@ function PBM.ImportBotRosterMessage(message) for rawEntry in payload:gmatch("([^,]+)") do local entry = Trim(rawEntry) local _, name, rawClass = entry:match("^([+-])(%S+)%s+(.+)$") - local cls = rawClass and CLASS_NAMES[Trim(rawClass)] + local cls = rawClass and NormalizeClass(rawClass) if name and cls then total = total + 1 if AddTrackedBot(name, cls) then added = added + 1 end @@ -79,16 +68,21 @@ function PBM.ImportBotRosterMessage(message) end function PBM.RequestBotRoster() - PBM.State.rosterSyncPending = true SendChatMessage(".playerbots bot list", "SAY") end local rosterTimerFrame +local rosterScheduleGeneration = 0 function PBM.ScheduleBotRosterRequest(delay) - local callback = function() PBM.RequestBotRoster() end + rosterScheduleGeneration = rosterScheduleGeneration + 1 + local generation = rosterScheduleGeneration + local function requestRoster() + if generation == rosterScheduleGeneration then PBM.RequestBotRoster() end + end + if C_Timer and C_Timer.After then - C_Timer.After(delay, callback) + C_Timer.After(delay, requestRoster) return end @@ -98,7 +92,7 @@ function PBM.ScheduleBotRosterRequest(delay) elapsed = elapsed + delta if elapsed >= delay then self:SetScript("OnUpdate", nil) - callback() + requestRoster() end end) end @@ -110,7 +104,6 @@ if rosterEventFrame then local matched, added, total = PBM.ImportBotRosterMessage(message) if not matched then return end - PBM.State.rosterSyncPending = false local status = "|cff44ff44Roster synced: " .. total .. " found, " .. added .. " added.|r" if LichborneAddStatus then LichborneAddStatus:SetText(status) end if LichborneOutput then diff --git a/PBM/PBM_TrackerCore.lua b/PBM/PBM_TrackerCore.lua index a888cb2..2a6e102 100644 --- a/PBM/PBM_TrackerCore.lua +++ b/PBM/PBM_TrackerCore.lua @@ -21,8 +21,6 @@ local function OnFirstShow() if PBM.State.setupDone then return end PBM.State.setupDone = true local f = LichborneTrackerFrame - -- Allow normal desktop-style click-to-front behavior when this window - -- overlaps other DIALOG-strata addon windows. f:SetToplevel(true) local fl = f:GetFrameLevel() @@ -1128,8 +1126,7 @@ local function OnFirstShow() 155, {{"Log in All Bots",0.78,0.61,0.23},{"Logs in bots, then syncs the server roster.",0.8,0.8,0.8}}) loginBtn:SetScript("OnClick", function() if LichborneAddStatus then LichborneAddStatus:SetText("|cffffff88Logging in bots and requesting roster...|r") end - -- SAY reaches the command handler even when the player is not grouped; - -- PARTY was rejected client-side outside a party. + -- Use SAY to reach handler outside groups SendChatMessage(".playerbots bot add *", "SAY") PBM.ScheduleBotRosterRequest(0.75) end)