feat: expand QuestieLearner and QuestieLearnerComms
QuestieLearner: - Expose QuestieLearner.data as direct ref to Questie.db.global.learnedData - Add mc (match-count) field tracking to LearnNPC/Quest/Item/Object - Add _Learner:BroadcastIfCommsAvailable() hook called after every write - Add QuestieLearner:HandleNetworkData() — validates, merges, and deduplicates incoming network data for NPC/QUEST/ITEM/OBJECT types, including coordinate and item-drop list merging QuestieLearnerComms: - Move DebugLog definition to top of file (was called before definition) - Fix IsDuplicateMessage fallback: table.getn(serializedData) -> string.len(), mod() -> math.mod() for Lua 5.0 compatibility - Add mutedUntil table and implement 5-minute timed mute on 3 strikes (previously the 3-strike branch was an empty if body with no effect) - Remove duplicated nil-check + DebugLog block in ProcessRawMessage
This commit is contained in:
@@ -13,9 +13,21 @@ local addonPrefix = "QuestieLearner"
|
|||||||
local hiddenChannelName = "questiecomm"
|
local hiddenChannelName = "questiecomm"
|
||||||
local ProtocolVersion = 1
|
local ProtocolVersion = 1
|
||||||
|
|
||||||
|
-- Dev Logging Flags — defined first so all functions below can call DebugLog
|
||||||
|
local LOG_CRITICAL = true
|
||||||
|
local LOG_DEVELOP = false
|
||||||
|
|
||||||
|
local function DebugLog(tier, msg)
|
||||||
|
if tier == "CRITICAL" and LOG_CRITICAL then
|
||||||
|
Questie:Print("|cFF00FF00[QL-CRITICAL]|r " .. msg)
|
||||||
|
elseif tier == "DEVELOP" and LOG_DEVELOP then
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "|cFF00FFFF[QL-DEV]|r " .. msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Throttling (Token Bucket)
|
-- Throttling (Token Bucket)
|
||||||
local bucketCapacity = 9
|
local bucketCapacity = 9
|
||||||
local bucketWindow = 60 -- seconds
|
local bucketWindow = 60
|
||||||
local tokenRefillRate = bucketCapacity / bucketWindow
|
local tokenRefillRate = bucketCapacity / bucketWindow
|
||||||
local currentTokens = bucketCapacity
|
local currentTokens = bucketCapacity
|
||||||
local lastTokenUpdate = GetTime()
|
local lastTokenUpdate = GetTime()
|
||||||
@@ -24,50 +36,50 @@ local lastChatMessageTime = 0
|
|||||||
local rateLimitQueue = {}
|
local rateLimitQueue = {}
|
||||||
|
|
||||||
-- Deduplication & Quarantine
|
-- Deduplication & Quarantine
|
||||||
local messageCache = {} -- [hash] = true
|
local messageCache = {}
|
||||||
local incomingMessageQueue = {}
|
local incomingMessageQueue = {}
|
||||||
|
|
||||||
-- Sender Trust System
|
-- Sender Trust System
|
||||||
local senderTrust = {} -- [sender] = { strikes = 0, lastMsg = 0, count = 0 }
|
local senderTrust = {}
|
||||||
local bannedSenders = {}
|
local bannedSenders = {}
|
||||||
|
local mutedUntil = {}
|
||||||
local XXH = LibStub("XXH_Lua_Lib", true)
|
local XXH = LibStub("XXH_Lua_Lib", true)
|
||||||
|
|
||||||
local function RecordStrike(sender, reason)
|
local function RecordStrike(sender, reason)
|
||||||
if not senderTrust[sender] then senderTrust[sender] = { strikes = 0, lastMsg = 0, count = 0 } end
|
if not senderTrust[sender] then senderTrust[sender] = { strikes = 0, lastMsg = 0, count = 0 } end
|
||||||
senderTrust[sender].strikes = senderTrust[sender].strikes + 1
|
senderTrust[sender].strikes = senderTrust[sender].strikes + 1
|
||||||
DebugLog("DEVELOP", sender .. " gained a strike (" .. reason .. "). Total: " .. senderTrust[sender].strikes)
|
DebugLog("DEVELOP", sender .. " gained a strike (" .. reason .. "). Total: " .. senderTrust[sender].strikes)
|
||||||
|
|
||||||
if senderTrust[sender].strikes >= 7 then
|
if senderTrust[sender].strikes >= 7 then
|
||||||
bannedSenders[sender] = true
|
bannedSenders[sender] = true
|
||||||
DebugLog("CRITICAL", "Sender " .. sender .. " permanently banned (7 strikes).")
|
DebugLog("CRITICAL", "Sender " .. sender .. " permanently banned (7 strikes).")
|
||||||
elseif senderTrust[sender].strikes >= 3 then
|
elseif senderTrust[sender].strikes >= 3 then
|
||||||
DebugLog("CRITICAL", "Sender " .. sender .. " temporarily muted (3 strikes).")
|
mutedUntil[sender] = GetTime() + 300 -- 5-minute mute
|
||||||
|
DebugLog("CRITICAL", "Sender " .. sender .. " muted for 5 minutes (3 strikes).")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function IsSenderTrusted(sender)
|
local function IsSenderTrusted(sender)
|
||||||
if bannedSenders[sender] then return false end
|
if bannedSenders[sender] then return false end
|
||||||
if not senderTrust[sender] then senderTrust[sender] = { strikes = 0, lastMsg = 0, count = 0 } end
|
if mutedUntil[sender] then
|
||||||
|
if GetTime() < mutedUntil[sender] then return false end
|
||||||
-- Transient 3-strike mute (e.g. timeout for 5 minutes). Simplified for now: just relies on the strike count limit logic above/below or explicit ban.
|
mutedUntil[sender] = nil -- mute expired
|
||||||
if senderTrust[sender].strikes >= 3 then
|
|
||||||
-- Could add timeout decay here, but static 7 ban is enforced
|
|
||||||
end
|
end
|
||||||
|
if not senderTrust[sender] then senderTrust[sender] = { strikes = 0, lastMsg = 0, count = 0 } end
|
||||||
-- Basic Spam Check
|
|
||||||
local now = GetTime()
|
local now = GetTime()
|
||||||
if now - senderTrust[sender].lastMsg < 1.0 then
|
if now - senderTrust[sender].lastMsg < 1.0 then
|
||||||
senderTrust[sender].count = senderTrust[sender].count + 1
|
senderTrust[sender].count = senderTrust[sender].count + 1
|
||||||
if senderTrust[sender].count > 10 then
|
if senderTrust[sender].count > 10 then
|
||||||
RecordStrike(sender, "Spamming")
|
RecordStrike(sender, "Spamming")
|
||||||
senderTrust[sender].count = 0 -- Reset to avoid immediate cascade, rely on strike
|
senderTrust[sender].count = 0
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
senderTrust[sender].count = 1
|
senderTrust[sender].count = 1
|
||||||
end
|
end
|
||||||
senderTrust[sender].lastMsg = now
|
senderTrust[sender].lastMsg = now
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -76,38 +88,24 @@ local function IsDuplicateMessage(serializedData)
|
|||||||
if XXH then
|
if XXH then
|
||||||
hash = XXH.xxh32(serializedData, 0)
|
hash = XXH.xxh32(serializedData, 0)
|
||||||
else
|
else
|
||||||
-- Fallback simple sum
|
|
||||||
hash = 0
|
hash = 0
|
||||||
for i = 1, table.getn(serializedData) do
|
for i = 1, string.len(serializedData) do
|
||||||
hash = mod((hash + string.byte(serializedData, i)), 4294967296)
|
hash = math.mod(hash + string.byte(serializedData, i), 4294967296)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if messageCache[hash] then return true end
|
if messageCache[hash] then return true end
|
||||||
|
|
||||||
-- Limit cache size
|
|
||||||
local count = 0
|
local count = 0
|
||||||
for _ in pairs(messageCache) do count = count + 1 end
|
for _ in pairs(messageCache) do count = count + 1 end
|
||||||
if count > 500 then
|
if count > 500 then
|
||||||
messageCache = {} -- Simple clear for now
|
messageCache = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
messageCache[hash] = true
|
messageCache[hash] = true
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Dev Logging Flags
|
|
||||||
local LOG_CRITICAL = true
|
|
||||||
local LOG_DEVELOP = false -- Set to true for deep debugging
|
|
||||||
|
|
||||||
local function DebugLog(tier, msg)
|
|
||||||
if tier == "CRITICAL" and LOG_CRITICAL then
|
|
||||||
Questie:Print("|cFF00FF00[QL-CRITICAL]|r " .. msg)
|
|
||||||
elseif tier == "DEVELOP" and LOG_DEVELOP then
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "|cFF00FFFF[QL-DEV]|r " .. msg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function QuestieLearnerComms:Initialize()
|
function QuestieLearnerComms:Initialize()
|
||||||
DebugLog("DEVELOP", "Initializing QuestieLearnerComms")
|
DebugLog("DEVELOP", "Initializing QuestieLearnerComms")
|
||||||
|
|
||||||
@@ -272,9 +270,5 @@ function _QuestieLearnerComms:ProcessRawMessage(encodedMsg, sender)
|
|||||||
|
|
||||||
DebugLog("DEVELOP", "Received " .. tostring(op) .. " " .. tostring(typ) .. " " .. tostring(id) .. " from " .. tostring(sender))
|
DebugLog("DEVELOP", "Received " .. tostring(op) .. " " .. tostring(typ) .. " " .. tostring(id) .. " from " .. tostring(sender))
|
||||||
|
|
||||||
if not typ or not id or not d then return end
|
|
||||||
|
|
||||||
DebugLog("DEVELOP", "Received " .. tostring(op) .. " " .. tostring(typ) .. " " .. tostring(id) .. " from " .. tostring(sender))
|
|
||||||
|
|
||||||
QuestieLearner:HandleNetworkData(typ, id, d)
|
QuestieLearner:HandleNetworkData(typ, id, d)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ _Learner.pendingQuests = {}
|
|||||||
_Learner.pendingItems = {}
|
_Learner.pendingItems = {}
|
||||||
_Learner.pendingObjects = {}
|
_Learner.pendingObjects = {}
|
||||||
|
|
||||||
|
-- Direct reference to learnedData, set on Initialize
|
||||||
|
QuestieLearner.data = nil
|
||||||
|
|
||||||
local function GetZoneId()
|
local function GetZoneId()
|
||||||
local mapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
|
local mapId = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
|
||||||
if mapId then return mapId end
|
if mapId then return mapId end
|
||||||
@@ -95,7 +98,10 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
existing.mc = (existing.mc or 0) + 1
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned NPC:", npcId, name or "?")
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned NPC:", npcId, name or "?")
|
||||||
|
_Learner:BroadcastIfCommsAvailable("NPC", npcId, existing)
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieLearner:LearnQuest(questId, name, questLevel, requiredLevel, zoneOrSort, objectives)
|
function QuestieLearner:LearnQuest(questId, name, questLevel, requiredLevel, zoneOrSort, objectives)
|
||||||
@@ -121,7 +127,10 @@ function QuestieLearner:LearnQuest(questId, name, questLevel, requiredLevel, zon
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
existing.mc = (existing.mc or 0) + 1
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Quest:", questId, name or "?")
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Quest:", questId, name or "?")
|
||||||
|
_Learner:BroadcastIfCommsAvailable("QUEST", questId, existing)
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieLearner:LearnQuestGiver(questId, npcId, isStart)
|
function QuestieLearner:LearnQuestGiver(questId, npcId, isStart)
|
||||||
@@ -175,7 +184,10 @@ function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemCl
|
|||||||
if itemClass and not existing[12] then existing[12] = itemClass end
|
if itemClass and not existing[12] then existing[12] = itemClass end
|
||||||
if itemSubClass and not existing[13] then existing[13] = itemSubClass end
|
if itemSubClass and not existing[13] then existing[13] = itemSubClass end
|
||||||
|
|
||||||
|
existing.mc = (existing.mc or 0) + 1
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Item:", itemId, name or "?")
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Item:", itemId, name or "?")
|
||||||
|
_Learner:BroadcastIfCommsAvailable("ITEM", itemId, existing)
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieLearner:LearnItemDrop(itemId, npcId)
|
function QuestieLearner:LearnItemDrop(itemId, npcId)
|
||||||
@@ -231,7 +243,10 @@ function QuestieLearner:LearnObject(objectId, name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
existing.mc = (existing.mc or 0) + 1
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Object:", objectId, name or "?")
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Learned Object:", objectId, name or "?")
|
||||||
|
_Learner:BroadcastIfCommsAvailable("OBJECT", objectId, existing)
|
||||||
end
|
end
|
||||||
|
|
||||||
function QuestieLearner:InjectLearnedData()
|
function QuestieLearner:InjectLearnedData()
|
||||||
@@ -653,9 +668,97 @@ end
|
|||||||
|
|
||||||
function QuestieLearner:Initialize()
|
function QuestieLearner:Initialize()
|
||||||
EnsureLearnedData()
|
EnsureLearnedData()
|
||||||
|
QuestieLearner.data = Questie.db.global.learnedData
|
||||||
self:RegisterEvents()
|
self:RegisterEvents()
|
||||||
self:InjectLearnedData()
|
self:InjectLearnedData()
|
||||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Initialized")
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Initialized")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Called by LearnNPC/Quest/Item/Object after every write. Broadcasts only if comms module is loaded.
|
||||||
|
function _Learner:BroadcastIfCommsAvailable(typ, id, data)
|
||||||
|
local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms")
|
||||||
|
if QuestieLearnerComms and QuestieLearnerComms.BroadcastLearnedData then
|
||||||
|
local op = (data.mc and data.mc > 1) and "UPDATE" or "NEW"
|
||||||
|
QuestieLearnerComms:BroadcastLearnedData(op, typ, id, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Receives validated, decoded data from QuestieLearnerComms and merges it into local learnedData.
|
||||||
|
function QuestieLearner:HandleNetworkData(typ, id, d)
|
||||||
|
if not self:IsEnabled() then return end
|
||||||
|
if not EnsureLearnedData() then return end
|
||||||
|
if not typ or not id or not d then return end
|
||||||
|
|
||||||
|
local store
|
||||||
|
if typ == "NPC" then
|
||||||
|
if not Questie.db.global.learnedData.settings.learnNpcs then return end
|
||||||
|
store = Questie.db.global.learnedData.npcs
|
||||||
|
elseif typ == "QUEST" then
|
||||||
|
if not Questie.db.global.learnedData.settings.learnQuests then return end
|
||||||
|
store = Questie.db.global.learnedData.quests
|
||||||
|
elseif typ == "ITEM" then
|
||||||
|
if not Questie.db.global.learnedData.settings.learnItems then return end
|
||||||
|
store = Questie.db.global.learnedData.items
|
||||||
|
elseif typ == "OBJECT" then
|
||||||
|
if not Questie.db.global.learnedData.settings.learnObjects then return end
|
||||||
|
store = Questie.db.global.learnedData.objects
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local existing = store[id]
|
||||||
|
if not existing then
|
||||||
|
store[id] = d
|
||||||
|
store[id].mc = 1
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Network NEW", typ, id)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Merge: adopt non-nil fields from remote that we don't have locally
|
||||||
|
for k, v in pairs(d) do
|
||||||
|
if k ~= "mc" and existing[k] == nil then
|
||||||
|
existing[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Merge coordinate tables (index [7] for NPCs, [4] for Objects)
|
||||||
|
local coordKey = (typ == "NPC") and 7 or (typ == "OBJECT" and 4 or nil)
|
||||||
|
if coordKey and type(d[coordKey]) == "table" then
|
||||||
|
existing[coordKey] = existing[coordKey] or {}
|
||||||
|
for zoneId, coords in pairs(d[coordKey]) do
|
||||||
|
existing[coordKey][zoneId] = existing[coordKey][zoneId] or {}
|
||||||
|
for _, coord in ipairs(coords) do
|
||||||
|
local found = false
|
||||||
|
for _, existCoord in ipairs(existing[coordKey][zoneId]) do
|
||||||
|
if math.abs(existCoord[1] - coord[1]) < 1 and math.abs(existCoord[2] - coord[2]) < 1 then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found then
|
||||||
|
table.insert(existing[coordKey][zoneId], coord)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Merge item drop list (index [2] for Items)
|
||||||
|
if typ == "ITEM" and type(d[2]) == "table" then
|
||||||
|
existing[2] = existing[2] or {}
|
||||||
|
for _, npcId in ipairs(d[2]) do
|
||||||
|
local found = false
|
||||||
|
for _, existId in ipairs(existing[2]) do
|
||||||
|
if existId == npcId then found = true; break end
|
||||||
|
end
|
||||||
|
if not found then table.insert(existing[2], npcId) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
existing.mc = (existing.mc or 0) + 1
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] Network MERGE", typ, id, "mc:", existing.mc)
|
||||||
|
|
||||||
|
-- Keep .data reference in sync
|
||||||
|
QuestieLearner.data = Questie.db.global.learnedData
|
||||||
|
end
|
||||||
|
|
||||||
return QuestieLearner
|
return QuestieLearner
|
||||||
|
|||||||
Reference in New Issue
Block a user