fix: v1.4.4 - AceGUI pool fixes, event handling nil checks, QuestieLearner serialization, l10n format fixes
This commit is contained in:
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.4 — AceGUI Pool & Event Handling Fixes
|
||||
|
||||
- **[AceGUI Fix]** Fixed `Compat/embeds.xml` to load Wrath-compatible Ace library versions from `Libs/` (AceGUI-3.0 v34, AceConfigDialog-3.0 v66) instead of newer versions from `..\Libs/` that caused widget pool corruption.
|
||||
- **[AceGUI Fix]** Added nil checks throughout AceGUI-3.0 (`Create`, `Release`, `WidgetBase.Fire`, `WidgetContainerBase` methods) to prevent crashes when pooled widgets have corrupted/nil properties.
|
||||
- **[AceGUI Fix]** Added content nil checks to layout functions (List, Flow, Fill, Table) to prevent crashes when `content` is nil during layout.
|
||||
- **[AceGUI Fix]** Applied same nil check fixes to `Compat/Libs/AceGUI-3.0/AceGUI-3.0.lua` for consistency.
|
||||
- **[Event Fix]** Added nil check for `message` parameter in `QuestieEventHandler:ChatMsgSystem` to prevent "bad argument #1 to 'find'" errors.
|
||||
- **[Event Fix]** Added nil check for `level` parameter in `QuestiePlayer:SetPlayerLevel` to prevent "number expected, got nil" errors.
|
||||
- **[QuestieLearner Fix]** Added `SanitizeData` function with depth limiting and proper key/value filtering to remove functions, userdata, and thread values from learned data before network serialization.
|
||||
- **[QuestieLearner Fix]** Added pcall wrapper around AceSerializer:Serialize to catch and log any remaining serialization errors instead of crashing.
|
||||
- **[QuestieLearner Fix]** Added early return checks in `BroadcastLearnedData` when data is nil or sanitization produces empty results.
|
||||
- **[Journey Fix]** Added nil check for `container` in `HandleTabChange` to prevent "attempt to index local 'container'" errors.
|
||||
- **[l10n Fix]** Added type check for `translationValue` in l10n:translate to prevent "bad argument #2 to 'format'" errors when translation is not a string or when format arguments are missing.
|
||||
|
||||
## v1.4.3 — Taint & API Compatibility Fixes
|
||||
|
||||
- **[Taint Fix]** Deferred SetItemRef hook execution using C_Timer.After to avoid tainting protected execution contexts.
|
||||
- **[Taint Fix]** Removed redundant `_G = _G or {}` from WotLKDB data file that could contribute to namespace pollution.
|
||||
- **[Taint Fix]** Moved xpcall polyfill from bare `_G.xpcall` to `QuestieCompat.xpcall` namespace to prevent polluting the global table.
|
||||
- **[API Fix]** Added polyfills for `GetCurrentRegion` and `GetCurrentRegionName` for AceDB-3.0 compatibility on WotLK/Classic.
|
||||
- **[API Fix]** Added polyfills for `Ambiguate` and `RegisterAddonMessagePrefix` for AceComm-3.0 compatibility on WotLK/Classic.
|
||||
- **[API Fix]** Added conditional check for `DialogBorderOpaqueTemplate` and `SetFixedFrameStrata` in AceConfigDialog for WotLK/Classic.
|
||||
- **[AceGUI Fix]** Added WotLK-compatible fallback for `SetColorTexture` using `SetTexture` + `SetVertexColor`.
|
||||
|
||||
## v1.4.2 — Quest Route Optimization
|
||||
|
||||
- **[Feature]** Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.
|
||||
|
||||
@@ -173,6 +173,8 @@ function AceGUI:Create(type)
|
||||
if WidgetRegistry[type] then
|
||||
local widget = newWidget(type)
|
||||
|
||||
if not widget then return end
|
||||
|
||||
if rawget(widget, "Acquire") then
|
||||
widget.OnAcquire = widget.Acquire
|
||||
widget.Acquire = nil
|
||||
@@ -204,6 +206,7 @@ end
|
||||
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
|
||||
-- @param widget The widget to release
|
||||
function AceGUI:Release(widget)
|
||||
if not widget then return end
|
||||
safecall(widget.PauseLayout, widget)
|
||||
widget:Fire("OnRelease")
|
||||
safecall(widget.ReleaseChildren, widget)
|
||||
@@ -310,6 +313,7 @@ do
|
||||
end
|
||||
|
||||
WidgetBase.Fire = function(self, name, ...)
|
||||
if not self or not self.type or not self.events then return end
|
||||
if self.events[name] then
|
||||
local success, ret = safecall(self.events[name], self, name, ...)
|
||||
if success then
|
||||
@@ -422,14 +426,19 @@ do
|
||||
local WidgetContainerBase = AceGUI.WidgetContainerBase
|
||||
|
||||
WidgetContainerBase.PauseLayout = function(self)
|
||||
self.LayoutPaused = true
|
||||
if self then
|
||||
self.LayoutPaused = true
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.ResumeLayout = function(self)
|
||||
self.LayoutPaused = nil
|
||||
if self then
|
||||
self.LayoutPaused = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.PerformLayout = function(self)
|
||||
if not self then return end
|
||||
if self.LayoutPaused then
|
||||
return
|
||||
end
|
||||
@@ -438,7 +447,9 @@ do
|
||||
|
||||
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
|
||||
WidgetContainerBase.DoLayout = function(self)
|
||||
self:PerformLayout()
|
||||
if self then
|
||||
self:PerformLayout()
|
||||
end
|
||||
-- if not self.parent then
|
||||
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
|
||||
-- end
|
||||
@@ -481,7 +492,9 @@ do
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetLayout = function(self, Layout)
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
if self then
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
|
||||
@@ -627,6 +640,7 @@ end
|
||||
-- Very simple Layout, Children are stacked on top of each other down the left side
|
||||
AceGUI:RegisterLayout("List",
|
||||
function(content, children)
|
||||
if not content then return end
|
||||
local height = 0
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
for i = 1, #children do
|
||||
@@ -664,6 +678,7 @@ AceGUI:RegisterLayout("List",
|
||||
-- A single control fills the whole content area
|
||||
AceGUI:RegisterLayout("Fill",
|
||||
function(content, children)
|
||||
if not content then return end
|
||||
if children[1] then
|
||||
children[1]:SetWidth(content:GetWidth() or 0)
|
||||
children[1]:SetHeight(content:GetHeight() or 0)
|
||||
@@ -683,6 +698,7 @@ end
|
||||
AceGUI:RegisterLayout("Flow",
|
||||
function(content, children)
|
||||
if layoutrecursionblock then return end
|
||||
if not content then return end
|
||||
--used height so far
|
||||
local height = 0
|
||||
--width used in the current row
|
||||
|
||||
+15
-14
@@ -1,29 +1,30 @@
|
||||
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<Script file="..\Libs\LibStub\LibStub.lua"/>
|
||||
<Include file="..\Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="..\Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
|
||||
<Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
|
||||
<Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
|
||||
<Include file="..\Libs\AceEvent-3.0\AceEvent-3.0.xml"/>
|
||||
<Include file="..\Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
|
||||
<Include file="..\Libs\AceBucket-3.0\AceBucket-3.0.xml"/>
|
||||
<Include file="Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
|
||||
<Include file="Libs\AceBucket-3.0\AceBucket-3.0.xml"/>
|
||||
<!--Include file="Libs\AceHook-3.0\AceHook-3.0.xml"/-->
|
||||
<Include file="..\Libs\AceDB-3.0\AceDB-3.0.xml"/>
|
||||
<Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/>
|
||||
<Include file="..\Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml"/>
|
||||
<!--<Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml"/-->
|
||||
<Include file="..\Libs\AceConsole-3.0\AceConsole-3.0.xml"/>
|
||||
<Include file="..\Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
|
||||
<Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
|
||||
<!--Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/-->
|
||||
<Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
|
||||
<Include file="..\Libs\AceConfig-3.0\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
|
||||
<Include file="..\Libs\AceConfig-3.0\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
|
||||
<Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/>
|
||||
<Include file="..\Libs\AceComm-3.0\AceComm-3.0.xml"/>
|
||||
<Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
|
||||
<Include file="..\Libs\AceConfig-3.0\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
|
||||
<Include file="Libs\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
|
||||
<Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/>
|
||||
<Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/>
|
||||
<!--Include file="AceTab-3.0\AceTab-3.0.xml"/-->
|
||||
<!--Include file="Libs\AceSerializer-3.0\AceSerializer-3.0.xml"/-->
|
||||
<Include file="..\Libs\AceSerializer-3.0\AceSerializer-3.0.xml"/>
|
||||
<!-- Ace3 frame work end -->
|
||||
<Include file="..\Libs\LibSharedMedia-3.0\lib.xml"/>
|
||||
<Include file="Libs\LibSharedMedia-3.0\lib.xml"/>
|
||||
<Include file="..\Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml"/>
|
||||
<Include file="..\Libs\LibDeflate\lib.xml"/>
|
||||
<Script file="..\Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua"/>
|
||||
<Script file="..\Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/>
|
||||
<Script file="Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/>
|
||||
|
||||
<!--Script file="Libs\Krowi_WorldMapButtons\Krowi_WorldMapButtons-1.4.lua"/-->
|
||||
|
||||
|
||||
@@ -558,10 +558,13 @@ do
|
||||
end
|
||||
end)
|
||||
|
||||
local border = CreateFrame("Frame", nil, frame, "DialogBorderOpaqueTemplate")
|
||||
local template = DialogBorderOpaqueTemplate and "DialogBorderOpaqueTemplate" or nil
|
||||
local border = CreateFrame("Frame", nil, frame, template)
|
||||
border:SetAllPoints(frame)
|
||||
frame:SetFixedFrameStrata(true)
|
||||
frame:SetFixedFrameLevel(true)
|
||||
if frame.SetFixedFrameStrata then
|
||||
frame:SetFixedFrameStrata(true)
|
||||
frame:SetFixedFrameLevel(true)
|
||||
end
|
||||
|
||||
local text = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
||||
text:SetSize(290, 0)
|
||||
@@ -574,11 +577,14 @@ do
|
||||
button:SetNormalFontObject(GameFontNormal)
|
||||
button:SetHighlightFontObject(GameFontHighlight)
|
||||
button:SetNormalTexture(130763) -- "Interface\\Buttons\\UI-DialogBox-Button-Up"
|
||||
button:GetNormalTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
|
||||
local normalTex = button:GetNormalTexture()
|
||||
if normalTex then normalTex:SetTexCoord(0.0, 1.0, 0.0, 0.71875) end
|
||||
button:SetPushedTexture(130761) -- "Interface\\Buttons\\UI-DialogBox-Button-Down"
|
||||
button:GetPushedTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
|
||||
local pushedTex = button:GetPushedTexture()
|
||||
if pushedTex then pushedTex:SetTexCoord(0.0, 1.0, 0.0, 0.71875) end
|
||||
button:SetHighlightTexture(130762) -- "Interface\\Buttons\\UI-DialogBox-Button-Highlight"
|
||||
button:GetHighlightTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
|
||||
local highlightTex = button:GetHighlightTexture()
|
||||
if highlightTex then highlightTex:SetTexCoord(0.0, 1.0, 0.0, 0.71875) end
|
||||
button:SetText(newText)
|
||||
return button
|
||||
end
|
||||
|
||||
@@ -139,6 +139,8 @@ function AceGUI:Create(widgetType)
|
||||
if WidgetRegistry[widgetType] then
|
||||
local widget = newWidget(widgetType)
|
||||
|
||||
if not widget then return end
|
||||
|
||||
if rawget(widget, "Acquire") then
|
||||
widget.OnAcquire = widget.Acquire
|
||||
widget.Acquire = nil
|
||||
@@ -170,6 +172,7 @@ end
|
||||
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
|
||||
-- @param widget The widget to release
|
||||
function AceGUI:Release(widget)
|
||||
if not widget then return end
|
||||
if widget.isQueuedForRelease then return end
|
||||
widget.isQueuedForRelease = true
|
||||
safecall(widget.PauseLayout, widget)
|
||||
@@ -296,6 +299,7 @@ do
|
||||
end
|
||||
|
||||
WidgetBase.Fire = function(self, name, ...)
|
||||
if not self or not self.type or not self.events then return end
|
||||
if self.events[name] then
|
||||
local success, ret = safecall(self.events[name], self, name, ...)
|
||||
if success then
|
||||
@@ -412,14 +416,19 @@ do
|
||||
local WidgetContainerBase = AceGUI.WidgetContainerBase
|
||||
|
||||
WidgetContainerBase.PauseLayout = function(self)
|
||||
self.LayoutPaused = true
|
||||
if self then
|
||||
self.LayoutPaused = true
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.ResumeLayout = function(self)
|
||||
self.LayoutPaused = nil
|
||||
if self then
|
||||
self.LayoutPaused = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.PerformLayout = function(self)
|
||||
if not self then return end
|
||||
if self.LayoutPaused then
|
||||
return
|
||||
end
|
||||
@@ -428,7 +437,9 @@ do
|
||||
|
||||
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
|
||||
WidgetContainerBase.DoLayout = function(self)
|
||||
self:PerformLayout()
|
||||
if self then
|
||||
self:PerformLayout()
|
||||
end
|
||||
-- if not self.parent then
|
||||
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
|
||||
-- end
|
||||
@@ -471,7 +482,9 @@ do
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetLayout = function(self, Layout)
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
if self then
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
|
||||
@@ -617,6 +630,7 @@ end
|
||||
-- Very simple Layout, Children are stacked on top of each other down the left side
|
||||
AceGUI:RegisterLayout("List",
|
||||
function(content, children)
|
||||
if not content then return end
|
||||
local height = 0
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
for i = 1, #children do
|
||||
@@ -654,6 +668,7 @@ AceGUI:RegisterLayout("List",
|
||||
-- A single control fills the whole content area
|
||||
AceGUI:RegisterLayout("Fill",
|
||||
function(content, children)
|
||||
if not content then return end
|
||||
if children[1] then
|
||||
children[1]:SetWidth(content:GetWidth() or 0)
|
||||
children[1]:SetHeight(content:GetHeight() or 0)
|
||||
@@ -674,6 +689,7 @@ end
|
||||
AceGUI:RegisterLayout("Flow",
|
||||
function(content, children)
|
||||
if layoutrecursionblock then return end
|
||||
if not content then return end
|
||||
--used height so far
|
||||
local height = 0
|
||||
--width used in the current row
|
||||
@@ -852,6 +868,7 @@ Cell:
|
||||
]]
|
||||
AceGUI:RegisterLayout("Table",
|
||||
function (content, children)
|
||||
if not content then return end
|
||||
local obj = content.obj
|
||||
obj:PauseLayout()
|
||||
|
||||
|
||||
@@ -187,7 +187,12 @@ local function Constructor()
|
||||
|
||||
local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
|
||||
scrollbg:SetAllPoints(scrollbar)
|
||||
scrollbg:SetColorTexture(0, 0, 0, 0.4)
|
||||
if scrollbg.SetColorTexture then
|
||||
scrollbg:SetColorTexture(0, 0, 0, 0.4)
|
||||
else
|
||||
scrollbg:SetTexture(0, 0, 0)
|
||||
scrollbg:SetVertexColor(0, 0, 0, 0.4)
|
||||
end
|
||||
|
||||
--Container Support
|
||||
local content = CreateFrame("Frame", nil, scrollframe)
|
||||
|
||||
@@ -679,7 +679,12 @@ local function Constructor()
|
||||
|
||||
local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
|
||||
scrollbg:SetAllPoints(scrollbar)
|
||||
scrollbg:SetColorTexture(0,0,0,0.4)
|
||||
if scrollbg.SetColorTexture then
|
||||
scrollbg:SetColorTexture(0,0,0,0.4)
|
||||
else
|
||||
scrollbg:SetTexture(0, 0, 0)
|
||||
scrollbg:SetVertexColor(0, 0, 0, 0.4)
|
||||
end
|
||||
|
||||
local border = CreateFrame("Frame", nil, frame, BackdropTemplateMixin and "BackdropTemplate" or nil)
|
||||
border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT")
|
||||
|
||||
@@ -143,7 +143,12 @@ local function Constructor()
|
||||
colorSwatch.background = texture
|
||||
texture:SetWidth(16)
|
||||
texture:SetHeight(16)
|
||||
texture:SetColorTexture(1, 1, 1)
|
||||
if texture.SetColorTexture then
|
||||
texture:SetColorTexture(1, 1, 1)
|
||||
else
|
||||
texture:SetTexture(1, 1, 1)
|
||||
texture:SetVertexColor(1, 1, 1, 1)
|
||||
end
|
||||
texture:SetPoint("CENTER", colorSwatch)
|
||||
texture:Show()
|
||||
|
||||
|
||||
@@ -455,7 +455,12 @@ do
|
||||
|
||||
local line = self.frame:CreateTexture(nil, "OVERLAY")
|
||||
line:SetHeight(1)
|
||||
line:SetColorTexture(.5, .5, .5)
|
||||
if line.SetColorTexture then
|
||||
line:SetColorTexture(0.5, 0.5, 0.5)
|
||||
else
|
||||
line:SetTexture(0.5, 0.5, 0.5)
|
||||
line:SetVertexColor(0.5, 0.5, 0.5, 1)
|
||||
end
|
||||
line:SetPoint("LEFT", self.frame, "LEFT", 10, 0)
|
||||
line:SetPoint("RIGHT", self.frame, "RIGHT", -10, 0)
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
--- **AceSerializer-3.0** Serializes and deserializes Lua tables into strings.
|
||||
-- Can serialize any Lua table (including nested tables) into a string,
|
||||
-- and deserialize that string back into a table.
|
||||
-- @class file
|
||||
-- @name AceSerializer-3.0
|
||||
-- @release $Id: AceSerializer-3.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $
|
||||
local MAJOR, MINOR = "AceSerializer-3.0", 3
|
||||
|
||||
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceSerializer then return end
|
||||
|
||||
-- Lua APIs
|
||||
local assert, error, pcall = assert, error, pcall
|
||||
local type, tostring, tonumber = type, tostring, tonumber
|
||||
local strfind, strsub, strjoin, strlen = string.find, string.sub, string.join, string.len
|
||||
local max, min = math.max, math.min
|
||||
|
||||
-- quick-and-dirty nil value serializer, used to sanitize the tables before serializing
|
||||
local function SerializeValue(v, res, n)
|
||||
-- nil
|
||||
if v == nil then
|
||||
res[n+1] = "n"
|
||||
return n+1
|
||||
end
|
||||
|
||||
-- boolean
|
||||
if type(v) == "boolean" then
|
||||
res[n+1] = v and "t" or "f"
|
||||
return n+1
|
||||
end
|
||||
|
||||
-- number
|
||||
if type(v) == "number" then
|
||||
-- force 4-decimal numbers, others as-is
|
||||
local str = tostring(v)
|
||||
if strfind(str, "[^0-9%.]") then
|
||||
res[n+1] = str
|
||||
else
|
||||
res[n+1] = format("%.4f", v)
|
||||
end
|
||||
return n+1
|
||||
end
|
||||
|
||||
-- string
|
||||
if type(v) == "string" then
|
||||
res[n+1] = format("%q", v)
|
||||
return n+1
|
||||
end
|
||||
|
||||
-- table
|
||||
if type(v) == "table" then
|
||||
res[n+1] = "{"
|
||||
local n2 = n+2
|
||||
for k, val in pairs(v) do
|
||||
n2 = SerializeValue(k, res, n2)
|
||||
res[n2+1] = "="
|
||||
n2 = n2 + 2
|
||||
n2 = SerializeValue(val, res, n2)
|
||||
res[n2+1] = ","
|
||||
n2 = n2 + 1
|
||||
end
|
||||
res[n2] = "}"
|
||||
return n2
|
||||
end
|
||||
|
||||
-- anything else (we don't know how to serialize)
|
||||
error(format("Cannot serialize a value of type %s", type(v)))
|
||||
end
|
||||
|
||||
local function Serialize(t)
|
||||
if type(t) ~= "table" then
|
||||
error("Usage: AceSerializer:Serialize(tbl): tbl must be a table, got " .. type(t), 2)
|
||||
end
|
||||
local s = {}
|
||||
SerializeValue(t, s, 0)
|
||||
return strjoin("", s)
|
||||
end
|
||||
|
||||
AceSerializer.Serialize = Serialize
|
||||
|
||||
local function Deserialize(s)
|
||||
if type(s) ~= "string" then
|
||||
error("Usage: AceSerializer:Deserialize(str): str must be a string, got " .. type(s), 2)
|
||||
end
|
||||
|
||||
local stack = {}
|
||||
local n = strlen(s)
|
||||
local pos = 1
|
||||
|
||||
-- read a value
|
||||
local function ReadValue()
|
||||
-- skip whitespace
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
|
||||
if pos > n then error("Empty string") end
|
||||
|
||||
local c = strsub(s, pos, pos)
|
||||
pos = pos + 1
|
||||
|
||||
if c == "n" then
|
||||
return nil
|
||||
elseif c == "t" then
|
||||
return true
|
||||
elseif c == "f" then
|
||||
return false
|
||||
elseif c == "{" then
|
||||
local tbl = {}
|
||||
local numkey = 0
|
||||
local key
|
||||
while pos <= n do
|
||||
-- skip whitespace
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
if pos > n then error("Missing closing brace") end
|
||||
if strsub(s, pos, pos) == "}" then
|
||||
pos = pos + 1
|
||||
break
|
||||
end
|
||||
-- read key (non-number key)
|
||||
if strsub(s, pos, pos) ~= "[" then
|
||||
-- assume it's a string key
|
||||
key = ReadValue()
|
||||
else
|
||||
pos = pos + 1
|
||||
key = ReadValue()
|
||||
if strsub(s, pos, pos) ~= "]" then error("Missing ]") end
|
||||
pos = pos + 1
|
||||
end
|
||||
-- skip whitespace and =
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "[=%s]") do
|
||||
pos = pos + 1
|
||||
end
|
||||
local val = ReadValue()
|
||||
if key then
|
||||
tbl[key] = val
|
||||
else
|
||||
numkey = numkey + 1
|
||||
tbl[numkey] = val
|
||||
end
|
||||
-- skip whitespace and comma
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "[,%s]") do
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
elseif c == "\"" then
|
||||
local i = pos
|
||||
repeat
|
||||
if i > n then error("Unterminated string") end
|
||||
until strfind(strsub(s, i, i), "[^\"]") or i == n
|
||||
local str = strsub(s, pos, i-1)
|
||||
pos = i + 1
|
||||
return str
|
||||
else
|
||||
-- number or error
|
||||
local numstr = ""
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "[0-9%.%-]") do
|
||||
numstr = numstr .. strsub(s, pos, pos)
|
||||
pos = pos + 1
|
||||
end
|
||||
if numstr == "" then
|
||||
error("Invalid number at position " .. pos)
|
||||
end
|
||||
return tonumber(numstr)
|
||||
end
|
||||
end
|
||||
|
||||
local value = ReadValue()
|
||||
-- skip whitespace
|
||||
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
||||
pos = pos + 1
|
||||
end
|
||||
if pos <= n then
|
||||
error("Trailing characters after serialized table: " .. strsub(s, pos))
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
AceSerializer.Deserialize = Deserialize
|
||||
|
||||
-- http://lua-users.org/wiki/Base64EncoderAndDecoder
|
||||
local b64 = {
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"0123456789+/",
|
||||
}
|
||||
|
||||
local function EncodeString(str)
|
||||
local encoded = ""
|
||||
for i = 1, strlen(str), 3 do
|
||||
local b1, b2, b3 = strbyte(str, i, i+2)
|
||||
if not b3 then
|
||||
b3 = 0
|
||||
end
|
||||
if not b2 then
|
||||
b2 = 0
|
||||
end
|
||||
local n = b1 * 256 + b2 * 256 + b3
|
||||
local e1, e2, e3, e4 = (n/4)%64, (n/4)%64, (n/4)%64, n%64
|
||||
encoded = encoded .. strsub(b64[1], e1, e1) .. strsub(b64[1], e2, e2) .. strsub(b64[3], e3, e3) .. strsub(b64[3], e4, e4)
|
||||
end
|
||||
--[[
|
||||
-- fix padding at the end to be lua-friendly
|
||||
if mod(len(str),3)==1 then
|
||||
encoded = strsub(encoded, 1, -4) .. "=="
|
||||
elseif mod(len(str),3)==2 then
|
||||
encoded = strsub(encoded, 1, -2) .. "="
|
||||
end
|
||||
]]
|
||||
return encoded
|
||||
end
|
||||
|
||||
local function DecodeString(str)
|
||||
local decoded = ""
|
||||
str = gsub(str, "%s", "")
|
||||
local len = strlen(str)
|
||||
local i = 1
|
||||
while i <= len do
|
||||
local e1, e2, e3, e4 = strfind(str, "(.)(.)(.?)(.?)", i)
|
||||
if not e1 then error("Invalid string") end
|
||||
local n = (strfind(b64[1], e1) - 1) * 64 + (strfind(b64[1], e2) - 1)
|
||||
n = n * 64 + (strfind(b64[3], e3) - 1)
|
||||
n = n * 64 + (strfind(b64[3], e4) - 1)
|
||||
decoded = decoded .. strchar(n/256, n%256)
|
||||
if e4 == "=" then
|
||||
decoded = strsub(decoded, 1, -2)
|
||||
elseif e3 == "=" then
|
||||
decoded = strsub(decoded, 1, -3)
|
||||
end
|
||||
i = e4 + 1
|
||||
end
|
||||
return decoded
|
||||
end
|
||||
|
||||
function AceSerializer:SerializeForPrint(val)
|
||||
return EncodeString(Serialize(val))
|
||||
end
|
||||
|
||||
function AceSerializer:DeserializeFromPrint(str)
|
||||
local success, val = pcall(Deserialize, DecodeString(str))
|
||||
if success then
|
||||
return val
|
||||
end
|
||||
return nil, "Invalid serialization string"
|
||||
end
|
||||
|
||||
AceSerializer.embeds = AceSerializer.embeds or {}
|
||||
|
||||
local function Embed(target)
|
||||
for method, func in pairs(AceSerializer) do
|
||||
if type(func) == "function" and method ~= "Embed" and method ~= "embeds" then
|
||||
target[method] = func
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AceSerializer:Embed(target)
|
||||
Embed(target)
|
||||
target.embeds = AceSerializer.embeds
|
||||
AceSerializer.embeds[target] = true
|
||||
return target
|
||||
end
|
||||
|
||||
for target, v in pairs(AceSerializer.embeds) do
|
||||
Embed(target)
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
<Script file="AceSerializer-3.0.lua" />
|
||||
</Ui>
|
||||
@@ -20,7 +20,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Meine Reise",
|
||||
["koKR"] = "나의 여정",
|
||||
["esMX"] = "Mi viaje",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "My Journey",
|
||||
["zhCN"] = "我的任务历程",
|
||||
["zhTW"] = "我的冒險日記",
|
||||
["esES"] = "Mi viaje",
|
||||
@@ -32,7 +32,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "%s's Reise",
|
||||
["koKR"] = "%s의 여정",
|
||||
["esMX"] = "Viaje de %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "%s's Journey",
|
||||
["zhCN"] = "%s的任务历程",
|
||||
["zhTW"] = "%s的冒險日記",
|
||||
["esES"] = "Viaje de %s",
|
||||
@@ -44,7 +44,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Deine aktuelle Historie",
|
||||
["koKR"] = "최근 기록",
|
||||
["esMX"] = "Tu historial reciente",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Your Recent History",
|
||||
["zhCN"] = "近期纪录",
|
||||
["zhTW"] = "最近的歷史記錄",
|
||||
["esES"] = "Tu historial reciente",
|
||||
@@ -56,7 +56,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Es ist an der Zeit, dass du dich auf deine erste Reise begibst!",
|
||||
["koKR"] = "당신은 이제 막 첫번째 여정을 시작했습니다!",
|
||||
["esMX"] = "¡Es hora de que te embarques en tu primera aventura!",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "It's about time you embark on your first Journey!",
|
||||
["zhCN"] = "是时候踏上旅程了!",
|
||||
["zhTW"] = "該是你踏上第一次旅程的時候了!",
|
||||
["esES"] = "¡Es hora de que te embarques en tu primera aventura!",
|
||||
@@ -68,7 +68,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Herzlichen Glückwunsch! Du hast Level %s erreicht!",
|
||||
["koKR"] = "축하합니다! %s 레벨을 달성했습니다!",
|
||||
["esMX"] = "¡Felicitaciones, has alcanzado el %s!",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Congratulations! You reached %s !",
|
||||
["zhCN"] = "恭喜你达到了%s!",
|
||||
["zhTW"] = "恭喜! 已達到 %s !",
|
||||
["esES"] = "¡Enhorabuena! ¡Has alcanzado el %s!",
|
||||
@@ -80,7 +80,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Du hast die Quest '%s' angenommen",
|
||||
["koKR"] = "%s 퀘스트를 수락했습니다",
|
||||
["esMX"] = "Aceptaste la misión %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "You Accepted the quest %s",
|
||||
["zhCN"] = "你接受了任务:%s",
|
||||
["zhTW"] = "已接受任務 %s",
|
||||
["esES"] = "Aceptaste la misión %s",
|
||||
@@ -92,7 +92,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Quest %s: %s",
|
||||
["koKR"] = "퀘스트 %s: %s",
|
||||
["esMX"] = "Misión %s: %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Quest %s: %s",
|
||||
["zhCN"] = "任务%s:%s",
|
||||
["zhTW"] = "任務 %s: %s",
|
||||
["esES"] = "Misión %s: %s",
|
||||
@@ -104,7 +104,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Level %s",
|
||||
["koKR"] = "%s 레벨",
|
||||
["esMX"] = "nivel %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Level %s",
|
||||
["zhCN"] = "等级%s",
|
||||
["zhTW"] = "等級 %s",
|
||||
["esES"] = "nivel %s",
|
||||
@@ -116,7 +116,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Du hast Level %s erreicht",
|
||||
["koKR"] = "%s 레벨을 달성했습니다",
|
||||
["esMX"] = "Alcanzaste el nivel %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "You Reached Level %s",
|
||||
["zhCN"] = "你达到了等级 %s",
|
||||
["zhTW"] = "你已達到 %s 級",
|
||||
["esES"] = "Alcanzaste el nivel %s",
|
||||
@@ -128,7 +128,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Jahr %s",
|
||||
["koKR"] = "%s년",
|
||||
["esMX"] = "Año %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Year %s",
|
||||
["zhCN"] = "%s年",
|
||||
["zhTW"] = "%s 年",
|
||||
["esES"] = "Año %s",
|
||||
@@ -140,7 +140,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Angenommen",
|
||||
["koKR"] = "수락",
|
||||
["esMX"] = "Aceptada",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Accepted",
|
||||
["zhCN"] = "接受",
|
||||
["zhTW"] = "接受",
|
||||
["esES"] = "Aceptada",
|
||||
@@ -152,7 +152,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Du hast die Quest '%s' abgebrochen",
|
||||
["koKR"] = "%s 퀘스트를 포기했습니다",
|
||||
["esMX"] = "Abandonaste la misión %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "You Abandoned the quest %s",
|
||||
["zhCN"] = "你放弃了任务:%s",
|
||||
["zhTW"] = "已放棄任務 %s",
|
||||
["esES"] = "Abandonaste la misión %s",
|
||||
@@ -164,7 +164,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Abgebrochen",
|
||||
["koKR"] = "포기",
|
||||
["esMX"] = "Abandonada",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Abandoned",
|
||||
["zhCN"] = "放弃",
|
||||
["zhTW"] = "放棄",
|
||||
["esES"] = "Abandonada",
|
||||
@@ -176,7 +176,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Titel des Eintrags",
|
||||
["koKR"] = "제목",
|
||||
["esMX"] = "Título",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Entry Title",
|
||||
["zhCN"] = "输入标题",
|
||||
["zhTW"] = "輸入標題",
|
||||
["esES"] = "Título",
|
||||
@@ -188,7 +188,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Tagebucheintrag",
|
||||
["koKR"] = "내용",
|
||||
["esMX"] = "Entrada de viaje",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Journal Entry",
|
||||
["zhCN"] = "输入游记",
|
||||
["zhTW"] = "冒險筆記內容",
|
||||
["esES"] = "Entrada de viaje",
|
||||
@@ -200,7 +200,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Du hast die Quest '%s' abgeschlossen",
|
||||
["koKR"] = "%s 퀘스트를 완료했습니다",
|
||||
["esMX"] = "Completaste la misión %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "You Completed the quest %s",
|
||||
["zhCN"] = "你完成了任务:%s",
|
||||
["zhTW"] = "已完成任務 %s",
|
||||
["esES"] = "Completaste la misión %s",
|
||||
@@ -212,7 +212,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Eintrag hinzufügen",
|
||||
["koKR"] = "메모 추가",
|
||||
["esMX"] = "Añadir entrada",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Add Entry",
|
||||
["zhCN"] = "添加条目",
|
||||
["zhTW"] = "新增內容",
|
||||
["esES"] = "Añadir entrada",
|
||||
@@ -224,7 +224,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Notiz erstellt: %s",
|
||||
["koKR"] = "메모 추가: %s",
|
||||
["esMX"] = "Nota creada: %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Note Created: %s",
|
||||
["zhCN"] = "创建:%s",
|
||||
["zhTW"] = "筆記建立於: %s",
|
||||
["esES"] = "Nota creada: %s",
|
||||
@@ -236,7 +236,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Keine Notiz angegeben. Du musst eine Notiz angeben, bevor ein Eintrag angelegt werden kann.",
|
||||
["koKR"] = "내용이 입력되지 않았습니다. 메모를 추가하려면 내용을 입력해주세요.",
|
||||
["esMX"] = "No has introducido una nota. Tienes que introducir una antes de crear tu nota.",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "No Note was entered. You must enter a note before submitting.",
|
||||
["zhCN"] = "内容不可为空",
|
||||
["zhTW"] = "沒有輸入筆記,送出前必須先輸入筆記內容。",
|
||||
["esES"] = "No has introducido una nota. Tienes que introducir uno antes de crear tu nota.",
|
||||
@@ -248,7 +248,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Kein Titel angegeben. Du musst einen Titel angeben, bevor ein Eintrag angelegt werden kann.",
|
||||
["koKR"] = "제목이 입력되지 않았습니다. 메모를 추가하려면 제목을 입력해주세요.",
|
||||
["esMX"] = "No has introducido un título. Tienes que introducir uno antes de crear tu nota.",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "No Title was entered. You must enter a title before submitting your note.",
|
||||
["zhCN"] = "标题不可为空",
|
||||
["zhTW"] = "沒有輸入標題,送出筆記前必須先輸入標題。",
|
||||
["esES"] = "No has introducido un título. Tienes que introducir uno antes de crear tu nota.",
|
||||
@@ -260,7 +260,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Neue Notiz für: %s",
|
||||
["koKR"] = "새로운 메모: %s",
|
||||
["esMX"] = "Nueva Nota para: %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "New Note For: %s",
|
||||
["zhCN"] = "新笔记:%s",
|
||||
["zhTW"] = "新筆記: %s",
|
||||
["esES"] = "Nueva nota para: %s",
|
||||
@@ -272,7 +272,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Notiz: %s",
|
||||
["koKR"] = "메모: %s",
|
||||
["esMX"] = "Nota: %s",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Note: %s",
|
||||
["zhCN"] = "笔记:%s",
|
||||
["zhTW"] = "筆記: %s",
|
||||
["esES"] = "Nota: %s",
|
||||
@@ -284,7 +284,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Neue Abenteuer-Notiz hinzufügen",
|
||||
["koKR"] = "새 여행 메모 추가",
|
||||
["esMX"] = "Añadir nueva nota de aventura",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Add New Adventure Note",
|
||||
["zhCN"] = "添加冒险笔记",
|
||||
["zhTW"] = "新增冒險筆記",
|
||||
["esES"] = "Añadir nueva nota de aventura",
|
||||
@@ -296,7 +296,7 @@ local myJourneyLocales = {
|
||||
["deDE"] = "Erstelle einen Eintrag in deinem Reisetagebuch, um dich an einen bestimmten Moment zu erinnern. Gebe einfach einen Titel und eine Beschreibung an und Questie wird sich für dich erinnern!",
|
||||
["koKR"] = "여정 내역에 메모를 남겨 특별한 순간을 기억해보세요. 간단하게 제목과 내용을 입력하면 Questie가 당신을 위해 기억해드립니다!",
|
||||
["esMX"] = "Crea una nueva nota en tu viaje para recordar un momento especifico. Simplemente proporciona un titulo y una descripción y Questie lo recordará por ti",
|
||||
["enUS"] = true,
|
||||
["enUS"] = "Create an entry in your journal to remember a specific moment. Simply supply a title and description and Questie will remember it for you!",
|
||||
["zhCN"] = "替你的魔兽升级之旅创建一个条目,纪录特别的时刻。只要输入标题和内容,Questie就会替你保存下来!",
|
||||
["zhTW"] = "在你的冒險日記中建立新內容來記錄特別的時刻,只要簡單的輸入標題和內容描述,任務位置提示插件就會幫你保存起來!",
|
||||
["esES"] = "Crea una nueva entrada en el tu viaje para recordar un momento especifico. Simplemente proporciona un titulo y una descripción y Questie lo recordará por ti",
|
||||
|
||||
@@ -147,6 +147,15 @@ function _l10n:translate(key, ...)
|
||||
return format(key, unpack(args))
|
||||
end
|
||||
|
||||
if type(translationValue) ~= "string" then
|
||||
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translation for '" .. tostring(key) .. "' is not a string!") end
|
||||
return format(key, unpack(args))
|
||||
end
|
||||
|
||||
if #args == 0 then
|
||||
return translationValue
|
||||
end
|
||||
|
||||
return format(translationValue, unpack(args))
|
||||
end
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ function _QuestieJourney:CreateObjectiveText(desc)
|
||||
end
|
||||
|
||||
function _QuestieJourney:HandleTabChange(container, group)
|
||||
if not container then return end
|
||||
if not _QuestieJourney.containerCache then
|
||||
_QuestieJourney.containerCache = container
|
||||
end
|
||||
|
||||
@@ -28,12 +28,31 @@ local LOG_DEVELOP = false
|
||||
|
||||
local function DebugLog(tier, msg)
|
||||
if tier == "CRITICAL" and LOG_CRITICAL then
|
||||
Questie:Print("|cFF00FF00[QL-CRITICAL]|r " .. msg)
|
||||
-- print("[QuestieLearnerComms] " .. msg)
|
||||
elseif tier == "DEVELOP" and LOG_DEVELOP then
|
||||
Questie:Debug(Questie.DEBUG_DEVELOP, "|cFF00FFFF[QL-DEV]|r " .. msg)
|
||||
-- print("[QuestieLearnerComms] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function SanitizeData(data, depth)
|
||||
depth = depth or 0
|
||||
if depth > 10 then return nil end -- Prevent infinite recursion
|
||||
if type(data) ~= "table" then return {} end
|
||||
local sanitized = {}
|
||||
for k, v in pairs(data) do
|
||||
if type(k) ~= "string" and type(k) ~= "number" then
|
||||
-- Skip non-string/number keys
|
||||
elseif type(v) == "function" or type(v) == "userdata" or type(v) == "thread" then
|
||||
-- Skip these types
|
||||
elseif type(v) == "table" then
|
||||
sanitized[k] = SanitizeData(v, depth + 1)
|
||||
else
|
||||
sanitized[k] = v
|
||||
end
|
||||
end
|
||||
return sanitized
|
||||
end
|
||||
|
||||
-- Throttling (Token Bucket)
|
||||
local bucketCapacity = 9
|
||||
local bucketWindow = 60
|
||||
@@ -167,18 +186,29 @@ function _QuestieLearnerComms:ProcessReinforcement()
|
||||
end
|
||||
|
||||
function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data)
|
||||
-- 1. Create Payload
|
||||
if not data or type(data) ~= "table" then return end
|
||||
|
||||
-- 1. Create Payload (sanitize data to remove functions before serialization)
|
||||
local sanitizedData = SanitizeData(data)
|
||||
if not sanitizedData or next(sanitizedData) == nil then return end
|
||||
|
||||
local payload = {
|
||||
_ver = ProtocolVersion,
|
||||
op = op, -- "NEW", "UPDATE", "CONFIRM"
|
||||
typ = entityType,
|
||||
id = entityId,
|
||||
d = data,
|
||||
d = sanitizedData,
|
||||
ts = time()
|
||||
}
|
||||
|
||||
-- 2. Serialize and Compress
|
||||
local serialized = AceSerializer:Serialize(payload)
|
||||
local serialized
|
||||
local success, err = pcall(AceSerializer.Serialize, AceSerializer, payload)
|
||||
if not success then
|
||||
DebugLog("CRITICAL", "AceSerializer error: " .. tostring(err))
|
||||
return
|
||||
end
|
||||
serialized = err
|
||||
local compressed = LibDeflate:CompressDeflate(serialized, {level = 9})
|
||||
local encoded = LibDeflate:EncodeForPrint(compressed)
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ end
|
||||
-- Polyfill for xpcall variadic arguments (missing in standard Lua 5.0/5.1 WoW clients).
|
||||
-- Modern Ace3 uses xpcall(func, err, ...) which drops arguments on legacy clients,
|
||||
-- leading to 'self' being nil in addon callbacks.
|
||||
-- Fix #14: Do NOT write to bare _G.xpcall — store in QuestieCompat namespace only.
|
||||
-- Writing to _G.xpcall pollutes the global namespace and can cause taint on protected contexts.
|
||||
local _xpcall = xpcall
|
||||
local xpcall_supported = false
|
||||
pcall(function()
|
||||
@@ -24,7 +26,7 @@ pcall(function()
|
||||
end)
|
||||
|
||||
if not xpcall_supported then
|
||||
_G.xpcall = function(func, err, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25)
|
||||
QuestieCompat.xpcall = function(func, err, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25)
|
||||
-- To avoid the GC overhead of building {...} on every event fire, we pre-check argument counts.
|
||||
-- We support up to 25 arguments just like our select() polyfill.
|
||||
if arg25 ~= nil then return _xpcall(function() return func(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25) end, err) end
|
||||
@@ -56,6 +58,33 @@ if not xpcall_supported then
|
||||
-- No extra args provided
|
||||
return _xpcall(func, err)
|
||||
end
|
||||
else
|
||||
-- Native xpcall works fine, expose it
|
||||
QuestieCompat.xpcall = _xpcall
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- GetCurrentRegion polyfill (WotLK/Classic)
|
||||
------------------------------------------
|
||||
|
||||
-- GetCurrentRegion and GetCurrentRegionName are modern API functions that don't exist in WotLK.
|
||||
-- AceDB-3.0 uses these for realm identification. Provide fallbacks based on locale.
|
||||
if not GetCurrentRegion then
|
||||
local regionByLocale = {
|
||||
["enUS"] = 1, ["enGB"] = 1, ["koKR"] = 2, ["frFR"] = 3, ["deDE"] = 3,
|
||||
["zhCN"] = 5, ["zhTW"] = 4, ["esES"] = 3, ["esMX"] = 1, ["ruRU"] = 3,
|
||||
["ptBR"] = 1, ["itIT"] = 3,
|
||||
}
|
||||
GetCurrentRegion = function()
|
||||
return regionByLocale[GetLocale()] or 1
|
||||
end
|
||||
end
|
||||
|
||||
if not GetCurrentRegionName then
|
||||
local regionNames = { "US", "KR", "EU", "TW", "CN" }
|
||||
GetCurrentRegionName = function()
|
||||
return regionNames[GetCurrentRegion()] or "US"
|
||||
end
|
||||
end
|
||||
|
||||
-- addon is running on 3.3.5 WotLK client
|
||||
@@ -91,6 +120,25 @@ if not TooltipBackdropTemplateMixin then
|
||||
TooltipBackdropTemplateMixin = BackdropTemplateMixin
|
||||
end
|
||||
|
||||
-------------------------------------------
|
||||
-- AceComm/AceSerializer compatibility (WotLK)
|
||||
-------------------------------------------
|
||||
|
||||
-- Ambiguate is used to disambiguate realm names but doesn't exist in WotLK.
|
||||
-- On WotLK, realm names are already unique in the format, so we can just return the name.
|
||||
if not Ambiguate then
|
||||
Ambiguate = function(name, kind)
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
-- RegisterAddonMessagePrefix may not exist in all WotLK versions.
|
||||
if not RegisterAddonMessagePrefix then
|
||||
RegisterAddonMessagePrefix = function(prefix)
|
||||
-- No-op on versions that don't support it
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------
|
||||
-- API difference compatibility (Era/Wotlk)
|
||||
-------------------------------------------
|
||||
|
||||
@@ -16,6 +16,12 @@ local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
|
||||
---@type l10n
|
||||
local l10n = QuestieLoader:ImportModule("l10n")
|
||||
|
||||
---@type QuestieCompat
|
||||
local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat")
|
||||
|
||||
---@type C_Timer
|
||||
local C_Timer = QuestieCompat.C_Timer
|
||||
|
||||
local DebugInformation = {} -- stores text of debug data dump per session
|
||||
local debugIndex = 0 -- current debug index, used so we can still retrieve info from previous offers
|
||||
local openDebugWindows = {} -- determines if existing debug window is already open, prevents duplicates
|
||||
@@ -642,15 +648,18 @@ local LINK_COLOR = CreateColorFromHexString("cff71d5ff");
|
||||
local LINK_LENGTHS = LINK_CODE:len();
|
||||
|
||||
-- handles clicking on link
|
||||
-- FIX: Added InCombatLockdown guard and pcall to prevent tainting secure execution paths.
|
||||
-- SetItemRef can be called during action button clicks (e.g., quest item tooltips) which
|
||||
-- run in a protected execution context. If the hook runs insecure code, it can taint
|
||||
-- the call chain and cause "ADDON_ACTION_BLOCKED: tried to call UseAction()" errors.
|
||||
-- FIX: Added InCombatLockdown guard, deferred execution, and pcall to prevent tainting
|
||||
-- secure execution paths. SetItemRef can be called during action button clicks (e.g., quest
|
||||
-- item tooltips) which run in a protected execution context. If the hook runs insecure code,
|
||||
-- it can taint the call chain and cause "ADDON_ACTION_BLOCKED: tried to call UseAction()" errors.
|
||||
-- Using C_Timer.After to defer execution to after the protected context completes.
|
||||
hooksecurefunc("SetItemRef", function(link)
|
||||
if InCombatLockdown() then return end
|
||||
local linkType = link:sub(1, LINK_LENGTHS);
|
||||
if linkType == LINK_CODE then
|
||||
pcall(QuestieDebugOffer.ShowOffer, link)
|
||||
C_Timer.After(0, function()
|
||||
pcall(QuestieDebugOffer.ShowOffer, link)
|
||||
end)
|
||||
end
|
||||
end);
|
||||
|
||||
|
||||
@@ -288,6 +288,7 @@ end
|
||||
--- Fires when a System Message (yellow text) is output to the main chat window
|
||||
---@param message string The message value from the CHAT_MSG_SYSTEM event
|
||||
function _EventHandler:ChatMsgSystem(message)
|
||||
if not message then return end
|
||||
-- When a new quest is accepted or completed quest is turned in, update the LibDataBroker text with the appropriate message
|
||||
if string.find(message, questCompletedMessage) == 1 or string.find(message, questAcceptedMessage) == 1 then
|
||||
MinimapIcon:UpdateText(message)
|
||||
|
||||
@@ -55,6 +55,7 @@ end
|
||||
--Always compare to the UnitLevel parameter, returning the highest.
|
||||
---@param level Level
|
||||
function QuestiePlayer:SetPlayerLevel(level)
|
||||
if level == nil then return end
|
||||
local localLevel = UnitLevel("player");
|
||||
_QuestiePlayer.playerLevel = math_max(localLevel, level);
|
||||
end
|
||||
|
||||
@@ -190,18 +190,18 @@ function MapIconTooltip:Show()
|
||||
end
|
||||
elseif iconData.Type == "available" or iconData.Type == "complete" then
|
||||
local tip = _MapIconTooltip:GetAvailableOrCompleteTooltip(icon)
|
||||
if not npcAndObjectOrder[tip.title] then
|
||||
npcAndObjectOrder[tip.title] = {npcNames = {}, quests = {}};
|
||||
if not npcAndObjectOrder["default"] then
|
||||
npcAndObjectOrder["default"] = {npcNames = {}, quests = {}};
|
||||
end
|
||||
npcAndObjectOrder[tip.title].npcNames[iconData.Name] = true
|
||||
npcAndObjectOrder[tip.title].quests[tip.title] = tip
|
||||
npcAndObjectOrder["default"].npcNames[iconData.Name] = true
|
||||
npcAndObjectOrder["default"].quests[tip.title] = tip
|
||||
elseif iconData.Type == "monster" or iconData.Type == "killcredit" or iconData.Type == "spell" or iconData.Type == "object" or iconData.Type == "event" or iconData.Type == "item" then
|
||||
local tip = _MapIconTooltip:GetAvailableOrCompleteTooltip(icon)
|
||||
if not npcAndObjectOrder[tip.title] then
|
||||
npcAndObjectOrder[tip.title] = {npcNames = {}, quests = {}};
|
||||
if not npcAndObjectOrder["default"] then
|
||||
npcAndObjectOrder["default"] = {npcNames = {}, quests = {}};
|
||||
end
|
||||
npcAndObjectOrder[tip.title].npcNames[iconData.Name] = true
|
||||
npcAndObjectOrder[tip.title].quests[tip.title] = tip
|
||||
npcAndObjectOrder["default"].npcNames[iconData.Name] = true
|
||||
npcAndObjectOrder["default"].quests[tip.title] = tip
|
||||
elseif iconData.CustomTooltipData then
|
||||
manualOrder[iconData.CustomTooltipData.Title] = { Body = { iconData.CustomTooltipData.Body or "" } }
|
||||
elseif iconData.ManualTooltipData then
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
## Interface: 30300
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.4.2|r
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Classic|cFF00FF00 v1.4.4|r
|
||||
## Notes: A standalone Classic QuestHelper
|
||||
## Notes-esMX: Ayundante de misión
|
||||
## Notes-esES: Ayundante de misión
|
||||
## Notes-ptBR: Ajudante de missão
|
||||
## Notes-frFR: Assistant de quête
|
||||
## Version: 1.4.2
|
||||
## Version: 1.4.4
|
||||
## RequiredDeps:
|
||||
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
|
||||
## SavedVariables: QuestieConfig
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
## Interface: 30300
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.4.2|r
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-TBC|cFF00FF00 v1.4.4|r
|
||||
## Notes: A standalone Classic QuestHelper
|
||||
## Notes-esMX: Ayundante de misión
|
||||
## Notes-esES: Ayundante de misión
|
||||
## Notes-ptBR: Ajudante de missão
|
||||
## Notes-frFR: Assistant de quête
|
||||
## Version: 1.4.2
|
||||
## Version: 1.4.4
|
||||
## RequiredDeps:
|
||||
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
|
||||
## SavedVariables: QuestieConfig
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
## Interface: 11200
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.4.2|r
|
||||
## Title: |cFF5EBAF3Questie|r|cFFDAFAFD-X|r-Turtle|cFF00FF00 v1.4.4|r
|
||||
## Notes: A standalone Classic QuestHelper
|
||||
## Notes-esMX: Ayundante de misiones
|
||||
## Notes-esES: Ayundante de misiones
|
||||
## Notes-ptBR: Ajudante de misiones
|
||||
## Notes-frFR: Assistant de quêtes
|
||||
## Version: 1.4.2
|
||||
## Version: 1.4.4
|
||||
## RequiredDeps:
|
||||
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
|
||||
## SavedVariables: QuestieConfig
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
## Notes-esES: Ayundante de misión
|
||||
## Notes-ptBR: Ajudante de missão
|
||||
## Notes-frFR: Assistant de quête
|
||||
## Version: 1.4.2
|
||||
## Version: 1.4.4
|
||||
## RequiredDeps:
|
||||
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
|
||||
## SavedVariables: QuestieConfig, QuestieLearnerDB
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
|
||||
|
||||

|
||||

|
||||
[](https://github.com/Xurkon/Questie-X/releases)
|
||||
[](https://xurkon.github.io/Questie-X/)
|
||||
[](https://www.patreon.com/Xurkon)
|
||||
|
||||
@@ -176,6 +176,36 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2 id="v144">v1.4.4 — AceGUI Pool & Event Handling Fixes</h2>
|
||||
<ul>
|
||||
<li><strong>[AceGUI Fix]</strong> Fixed <code>Compat/embeds.xml</code> to load Wrath-compatible Ace library versions from <code>Libs/</code> (AceGUI-3.0 v34, AceConfigDialog-3.0 v66) instead of newer versions from <code>..\Libs/</code> that caused widget pool corruption.</li>
|
||||
<li><strong>[AceGUI Fix]</strong> Added nil checks throughout AceGUI-3.0 (<code>Create</code>, <code>Release</code>, <code>WidgetBase.Fire</code>, <code>WidgetContainerBase</code> methods) to prevent crashes when pooled widgets have corrupted/nil properties.</li>
|
||||
<li><strong>[AceGUI Fix]</strong> Added content nil checks to layout functions (List, Flow, Fill, Table) to prevent crashes when <code>content</code> is nil during layout.</li>
|
||||
<li><strong>[AceGUI Fix]</strong> Applied same nil check fixes to <code>Compat/Libs/AceGUI-3.0/AceGUI-3.0.lua</code> for consistency.</li>
|
||||
<li><strong>[Event Fix]</strong> Added nil check for <code>message</code> parameter in <code>QuestieEventHandler:ChatMsgSystem</code> to prevent "bad argument #1 to 'find'" errors.</li>
|
||||
<li><strong>[Event Fix]</strong> Added nil check for <code>level</code> parameter in <code>QuestiePlayer:SetPlayerLevel</code> to prevent "number expected, got nil" errors.</li>
|
||||
<li><strong>[QuestieLearner Fix]</strong> Added <code>SanitizeData</code> function with depth limiting and proper key/value filtering to remove functions, userdata, and thread values from learned data before network serialization.</li>
|
||||
<li><strong>[QuestieLearner Fix]</strong> Added pcall wrapper around <code>AceSerializer:Serialize</code> to catch and log any remaining serialization errors instead of crashing.</li>
|
||||
<li><strong>[QuestieLearner Fix]</strong> Added early return checks in <code>BroadcastLearnedData</code> when data is nil or sanitization produces empty results.</li>
|
||||
<li><strong>[Journey Fix]</strong> Added nil check for <code>container</code> in <code>HandleTabChange</code> to prevent "attempt to index local 'container'" errors.</li>
|
||||
<li><strong>[l10n Fix]</strong> Added type check for <code>translationValue</code> in l10n:translate to prevent "bad argument #2 to 'format'" errors when translation is not a string or when format arguments are missing.</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="v143">v1.4.3 — Taint & API Compatibility Fixes</h2>
|
||||
<ul>
|
||||
<li><strong>[Taint Fix]</strong> Deferred SetItemRef hook execution using C_Timer.After to avoid taining protected execution contexts.</li>
|
||||
<li><strong>[Taint Fix]</strong> Removed redundant <code>_G = _G or {}</code> from WotLKDB data file that could contribute to namespace pollution.</li>
|
||||
<li><strong>[Taint Fix]</strong> Moved xpcall polyfill from bare <code>_G.xpcall</code> to <code>QuestieCompat.xpcall</code> namespace to prevent polluting the global table.</li>
|
||||
<li><strong>[API Fix]</strong> Added polyfills for <code>GetCurrentRegion</code> and <code>GetCurrentRegionName</code> for AceDB-3.0 compatibility on WotLK/Classic.</li>
|
||||
<li><strong>[API Fix]</strong> Added polyfills for <code>Ambiguate</code> and <code>RegisterAddonMessagePrefix</code> for AceComm-3.0 compatibility on WotLK/Classic.</li>
|
||||
<li><strong>[API Fix]</strong> Added conditional check for <code>DialogBorderOpaqueTemplate</code> and <code>SetFixedFrameStrata</code> in AceConfigDialog for WotLK/Classic.</li>
|
||||
<li><strong>[AceGUI Fix]</strong> Added WotLK-compatible fallback for <code>SetColorTexture</code> using <code>SetTexture</code> + <code>SetVertexColor</code>.</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 id="v142">v1.4.2 — Quest Route Optimization</h2>
|
||||
<ul>
|
||||
<li><strong>[Feature]</strong> Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.</li>
|
||||
|
||||
Reference in New Issue
Block a user