Files
OctoQuestRewards/Core.lua
T

195 lines
7.9 KiB
Lua

local A = OctoQuestRewards
local G = getfenv(0)
A.predictions = {}
A.frame = CreateFrame("Frame", "OctoQuestRewardsEvents")
function A.Print(text)
if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff88ccffOQR:|r " .. text) end
end
function A.DebugRecord(r, force)
local text = "ID=" .. tostring(r.id or "?") .. " title=" .. r.title
.. " questLevel=" .. tostring(r.level or "?") .. " playerLevel=" .. r.playerLevel
.. " IDsource=" .. r.idSource .. " XPsource=" .. r.xpSource
.. " predictedXP=" .. tostring(r.xp or "unknown")
.. " predictedMoney=" .. tostring(r.money or "unknown") .. "c"
.. " moneySource=" .. tostring(r.moneySource or "unavailable")
if force or text ~= A.lastDebug then A.Print(text); A.lastDebug = text end
end
-- One deferred update per event burst, removed immediately. No idle polling.
function A.Schedule()
A.frame:SetScript("OnUpdate", function()
A.frame:SetScript("OnUpdate", nil)
A.RenderAll()
end)
end
local function Difference(actual, predicted)
if actual == nil or predicted == nil then return "unknown" end
return tostring(actual - predicted)
end
function A.Report(r, xp, money, source)
if not A.db.debug then return end
if r then A.DebugRecord(r, true) end
A.Print("actualXP=" .. tostring(xp or "unknown") .. " actualMoney=" .. tostring(money or "unknown")
.. "c differenceXP(actual-predicted)=" .. Difference(xp, r and r.xp)
.. " differenceMoney=" .. Difference(money, r and r.money) .. "c source=" .. source)
A.db.lastTurnIn = { id = r and r.id, title = r and r.title,
playerLevel = r and r.playerLevel, predictedXP = r and r.xp,
predictedMoney = r and r.money, xp = xp, money = money, source = source }
end
function A.BeginTurnIn()
if not A.db.debug then return end
local r = A.GetRecord("reward")
if r then
A.pending = { record = r, expires = GetTime() + 8 }
A.DebugRecord(r, true)
end
end
local function EscapePattern(text)
return (string.gsub(text, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
end
local function CaptureFormat(message, template, marker, capture)
if not template then return nil end
local pattern = EscapePattern(template)
pattern = string.gsub(pattern, EscapePattern(EscapePattern(marker)), function() return capture end)
local _, _, value = string.find(message, "^" .. pattern .. "$")
return value
end
local function ReadChatMoney(text)
local value, found = 0, false
for _, unit in ipairs({{G.GOLD, 10000}, {G.SILVER, 100}, {G.COPPER, 1}}) do
if type(unit[1]) == "string" then
local _, _, amount = string.find(string.lower(text), "(%d+)%s*" .. EscapePattern(string.lower(unit[1])))
if amount then value, found = value + tonumber(amount) * unit[2], true end
end
end
if found then return value end
end
function A.SystemMessage(message)
local p = A.pending
if not A.db.debug or not p or type(message) ~= "string" then return end
if GetTime() > p.expires then A.pending = nil; return end
local title = CaptureFormat(message, G.ERR_QUEST_COMPLETE_S, "%s", "(.-)")
if title then
if title ~= p.record.title then A.pending = nil; return end
p.confirmed = true
end
local xp = CaptureFormat(message, G.ERR_QUEST_REWARD_EXP_I, "%d", "(%d+)")
if xp then p.xp = tonumber(xp) end
local moneyText = CaptureFormat(message, G.ERR_QUEST_REWARD_MONEY_S, "%s", "(.-)")
if moneyText then p.money = ReadChatMoney(moneyText) end
if p.confirmed and (title or xp or moneyText) then
A.Report(p.record, p.xp, p.money, "server quest system messages; missing values remain unknown")
end
end
-- This Vanilla function has one argument and no return payload to preserve.
-- Wrapping is necessary for correlating server chat with an actual submission.
local function SetupHooks()
if type(G.GetQuestReward) == "function" then
local old = G.GetQuestReward
G.GetQuestReward = function(choice)
A.BeginTurnIn()
return old(choice)
end
end
if type(G.QuestLog_UpdateQuestDetails) == "function" then
if type(G.hooksecurefunc) == "function" then
G.hooksecurefunc("QuestLog_UpdateQuestDetails", A.Schedule)
else
local old = G.QuestLog_UpdateQuestDetails
-- Inspected Vanilla FrameXML: single doNotScroll argument, no returns.
G.QuestLog_UpdateQuestDetails = function(doNotScroll)
old(doNotScroll)
A.Schedule()
end
end
end
A.SetupUI()
end
local function Has(fn) return type(fn) == "function" and "yes" or "no" end
function A.Status()
local version, build = GetBuildInfo()
A.Print("1.0.0 loaded; client=" .. tostring(version) .. " build=" .. tostring(build)
.. " content=" .. tostring(G.TURTLE_WOW_VERSION or "unknown") .. "; debug=" .. tostring(A.db.debug)
.. "; XP scaling=" .. A.db.scaling .. " (unverified server match, assumed 1x)")
A.Print("NPC money API=" .. Has(G.GetRewardMoney) .. "; log money API=" .. Has(G.GetQuestLogRewardMoney)
.. "; ClassicAPI quest cache=" .. Has(C_QuestLog and C_QuestLog.GetQuestDetails)
.. "; pfQuest=" .. tostring(pfDB ~= nil) .. "; pfExtend XP=" .. tostring(PfExtend_QuestRewards ~= nil)
.. "; QUEST_TURNED_IN=" .. (A.turnedInSeen and "observed" or "not observed")
.. "; event registration=" .. tostring(A.turnInRegistered))
if A.current then A.DebugRecord(A.current, true) end
local last = A.db.lastTurnIn
if last then
A.Print("last receipt: ID=" .. tostring(last.id or "?") .. " title=" .. tostring(last.title or "?")
.. " actualXP=" .. tostring(last.xp or "unknown") .. " actualMoney=" .. tostring(last.money or "unknown")
.. "c source=" .. tostring(last.source))
end
end
local function Command(message)
local cmd = string.lower(message or "")
if cmd == "debug" then
A.db.debug = not A.db.debug
A.pending, A.lastDebug = nil, nil
A.Print("Debug " .. (A.db.debug and "enabled" or "disabled") .. ".")
A.Schedule()
elseif cmd == "status" then A.Status()
elseif cmd == "scaling vanilla" or cmd == "scaling full25" then
A.db.scaling = cmd == "scaling vanilla" and "vanilla" or "full25"
A.Print("XP scaling assumption: " .. A.db.scaling .. "; estimates remain unverified.")
A.Schedule()
else
A.Print("/oqr debug toggles turn-in comparisons; /oqr status shows sources and last receipt.")
A.Print("Optional: /oqr scaling vanilla or /oqr scaling full25. Default: Vanilla scaling, 1x rate.")
end
end
local initialized
A.frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
if initialized then return end
initialized = true
OctoQuestRewardsDB = OctoQuestRewardsDB or {}
A.db = OctoQuestRewardsDB
if A.db.scaling ~= "full25" then A.db.scaling = "vanilla" end
SLASH_OCTOQUESTREWARDS1 = "/oqr"
SlashCmdList.OCTOQUESTREWARDS = Command
SetupHooks()
A.turnInRegistered = pcall(A.frame.RegisterEvent, A.frame, "QUEST_TURNED_IN")
if C_QuestLog then pcall(A.frame.RegisterEvent, A.frame, "QUEST_DATA_LOAD_RESULT") end
A.Schedule()
elseif not initialized then return
elseif event == "QUEST_TURNED_IN" then
A.turnedInSeen = true
local id, xp, money = tonumber(arg1), tonumber(arg2), tonumber(arg3)
local r = id and A.predictions[id]
if A.pending and (not r or r.id == A.pending.record.id) then r = A.pending.record end
if not r then
r = { id = id, title = "unknown", playerLevel = UnitLevel("player"),
idSource = "QUEST_TURNED_IN", xpSource = "unavailable" }
end
A.Report(r, xp, money, "QUEST_TURNED_IN server completion packet")
A.pending = nil
elseif event == "CHAT_MSG_SYSTEM" then A.SystemMessage(arg1)
elseif event == "QUEST_FINISHED" or event == "QUEST_GREETING" then
A.current = nil
A.Schedule()
else A.Schedule() end
end)
for _, name in ipairs({"PLAYER_LOGIN", "QUEST_DETAIL", "QUEST_PROGRESS", "QUEST_COMPLETE",
"QUEST_FINISHED", "QUEST_GREETING", "QUEST_ITEM_UPDATE", "QUEST_LOG_UPDATE", "PLAYER_LEVEL_UP",
"CHAT_MSG_SYSTEM", "UI_SCALE_CHANGED"}) do
pcall(A.frame.RegisterEvent, A.frame, name)
end