fix: v1.4.4 - AceGUI pool fixes, event handling nil checks, QuestieLearner serialization, l10n format fixes

This commit is contained in:
Xurkon
2026-03-20 21:16:58 -05:00
parent f32520078c
commit 2df8a7b96d
26 changed files with 569 additions and 83 deletions
+24
View File
@@ -1,5 +1,29 @@
# Changelog # 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 ## v1.4.2 — Quest Route Optimization
- **[Feature]** Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation. - **[Feature]** Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.
+16
View File
@@ -173,6 +173,8 @@ function AceGUI:Create(type)
if WidgetRegistry[type] then if WidgetRegistry[type] then
local widget = newWidget(type) local widget = newWidget(type)
if not widget then return end
if rawget(widget, "Acquire") then if rawget(widget, "Acquire") then
widget.OnAcquire = widget.Acquire widget.OnAcquire = widget.Acquire
widget.Acquire = nil 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. -- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
-- @param widget The widget to release -- @param widget The widget to release
function AceGUI:Release(widget) function AceGUI:Release(widget)
if not widget then return end
safecall(widget.PauseLayout, widget) safecall(widget.PauseLayout, widget)
widget:Fire("OnRelease") widget:Fire("OnRelease")
safecall(widget.ReleaseChildren, widget) safecall(widget.ReleaseChildren, widget)
@@ -310,6 +313,7 @@ do
end end
WidgetBase.Fire = function(self, name, ...) WidgetBase.Fire = function(self, name, ...)
if not self or not self.type or not self.events then return end
if self.events[name] then if self.events[name] then
local success, ret = safecall(self.events[name], self, name, ...) local success, ret = safecall(self.events[name], self, name, ...)
if success then if success then
@@ -422,14 +426,19 @@ do
local WidgetContainerBase = AceGUI.WidgetContainerBase local WidgetContainerBase = AceGUI.WidgetContainerBase
WidgetContainerBase.PauseLayout = function(self) WidgetContainerBase.PauseLayout = function(self)
if self then
self.LayoutPaused = true self.LayoutPaused = true
end end
end
WidgetContainerBase.ResumeLayout = function(self) WidgetContainerBase.ResumeLayout = function(self)
if self then
self.LayoutPaused = nil self.LayoutPaused = nil
end end
end
WidgetContainerBase.PerformLayout = function(self) WidgetContainerBase.PerformLayout = function(self)
if not self then return end
if self.LayoutPaused then if self.LayoutPaused then
return return
end end
@@ -438,7 +447,9 @@ do
--call this function to layout, makes sure layed out objects get a frame to get sizes etc --call this function to layout, makes sure layed out objects get a frame to get sizes etc
WidgetContainerBase.DoLayout = function(self) WidgetContainerBase.DoLayout = function(self)
if self then
self:PerformLayout() self:PerformLayout()
end
-- if not self.parent then -- if not self.parent then
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate) -- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
-- end -- end
@@ -481,8 +492,10 @@ do
end end
WidgetContainerBase.SetLayout = function(self, Layout) WidgetContainerBase.SetLayout = function(self, Layout)
if self then
self.LayoutFunc = AceGUI:GetLayout(Layout) self.LayoutFunc = AceGUI:GetLayout(Layout)
end end
end
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust) WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
if adjust then if adjust then
@@ -627,6 +640,7 @@ end
-- Very simple Layout, Children are stacked on top of each other down the left side -- Very simple Layout, Children are stacked on top of each other down the left side
AceGUI:RegisterLayout("List", AceGUI:RegisterLayout("List",
function(content, children) function(content, children)
if not content then return end
local height = 0 local height = 0
local width = content.width or content:GetWidth() or 0 local width = content.width or content:GetWidth() or 0
for i = 1, #children do for i = 1, #children do
@@ -664,6 +678,7 @@ AceGUI:RegisterLayout("List",
-- A single control fills the whole content area -- A single control fills the whole content area
AceGUI:RegisterLayout("Fill", AceGUI:RegisterLayout("Fill",
function(content, children) function(content, children)
if not content then return end
if children[1] then if children[1] then
children[1]:SetWidth(content:GetWidth() or 0) children[1]:SetWidth(content:GetWidth() or 0)
children[1]:SetHeight(content:GetHeight() or 0) children[1]:SetHeight(content:GetHeight() or 0)
@@ -683,6 +698,7 @@ end
AceGUI:RegisterLayout("Flow", AceGUI:RegisterLayout("Flow",
function(content, children) function(content, children)
if layoutrecursionblock then return end if layoutrecursionblock then return end
if not content then return end
--used height so far --used height so far
local height = 0 local height = 0
--width used in the current row --width used in the current row
+12 -11
View File
@@ -1,29 +1,30 @@
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> <Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="..\Libs\LibStub\LibStub.lua"/> <Script file="..\Libs\LibStub\LibStub.lua"/>
<Include file="..\Libs\CallbackHandler-1.0\CallbackHandler-1.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\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="..\Libs\AceEvent-3.0\AceEvent-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\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="..\Libs\AceBucket-3.0\AceBucket-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\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\AceDBOptions-3.0\AceDBOptions-3.0.xml"/>
<!--<Include file="Libs\AceLocale-3.0\AceLocale-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\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\AceConfig-3.0.xml"/-->
<Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-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\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
<Include file="..\Libs\AceConfig-3.0\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/> <Include file="Libs\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
<Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/> <Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/>
<Include file="..\Libs\AceComm-3.0\AceComm-3.0.xml"/> <Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/>
<!--Include file="AceTab-3.0\AceTab-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 --> <!-- 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\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\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"/--> <!--Script file="Libs\Krowi_WorldMapButtons\Krowi_WorldMapButtons-1.4.lua"/-->
@@ -558,10 +558,13 @@ do
end end
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) border:SetAllPoints(frame)
if frame.SetFixedFrameStrata then
frame:SetFixedFrameStrata(true) frame:SetFixedFrameStrata(true)
frame:SetFixedFrameLevel(true) frame:SetFixedFrameLevel(true)
end
local text = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight") local text = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
text:SetSize(290, 0) text:SetSize(290, 0)
@@ -574,11 +577,14 @@ do
button:SetNormalFontObject(GameFontNormal) button:SetNormalFontObject(GameFontNormal)
button:SetHighlightFontObject(GameFontHighlight) button:SetHighlightFontObject(GameFontHighlight)
button:SetNormalTexture(130763) -- "Interface\\Buttons\\UI-DialogBox-Button-Up" 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: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: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) button:SetText(newText)
return button return button
end end
+17
View File
@@ -139,6 +139,8 @@ function AceGUI:Create(widgetType)
if WidgetRegistry[widgetType] then if WidgetRegistry[widgetType] then
local widget = newWidget(widgetType) local widget = newWidget(widgetType)
if not widget then return end
if rawget(widget, "Acquire") then if rawget(widget, "Acquire") then
widget.OnAcquire = widget.Acquire widget.OnAcquire = widget.Acquire
widget.Acquire = nil 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. -- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
-- @param widget The widget to release -- @param widget The widget to release
function AceGUI:Release(widget) function AceGUI:Release(widget)
if not widget then return end
if widget.isQueuedForRelease then return end if widget.isQueuedForRelease then return end
widget.isQueuedForRelease = true widget.isQueuedForRelease = true
safecall(widget.PauseLayout, widget) safecall(widget.PauseLayout, widget)
@@ -296,6 +299,7 @@ do
end end
WidgetBase.Fire = function(self, name, ...) WidgetBase.Fire = function(self, name, ...)
if not self or not self.type or not self.events then return end
if self.events[name] then if self.events[name] then
local success, ret = safecall(self.events[name], self, name, ...) local success, ret = safecall(self.events[name], self, name, ...)
if success then if success then
@@ -412,14 +416,19 @@ do
local WidgetContainerBase = AceGUI.WidgetContainerBase local WidgetContainerBase = AceGUI.WidgetContainerBase
WidgetContainerBase.PauseLayout = function(self) WidgetContainerBase.PauseLayout = function(self)
if self then
self.LayoutPaused = true self.LayoutPaused = true
end end
end
WidgetContainerBase.ResumeLayout = function(self) WidgetContainerBase.ResumeLayout = function(self)
if self then
self.LayoutPaused = nil self.LayoutPaused = nil
end end
end
WidgetContainerBase.PerformLayout = function(self) WidgetContainerBase.PerformLayout = function(self)
if not self then return end
if self.LayoutPaused then if self.LayoutPaused then
return return
end end
@@ -428,7 +437,9 @@ do
--call this function to layout, makes sure layed out objects get a frame to get sizes etc --call this function to layout, makes sure layed out objects get a frame to get sizes etc
WidgetContainerBase.DoLayout = function(self) WidgetContainerBase.DoLayout = function(self)
if self then
self:PerformLayout() self:PerformLayout()
end
-- if not self.parent then -- if not self.parent then
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate) -- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
-- end -- end
@@ -471,8 +482,10 @@ do
end end
WidgetContainerBase.SetLayout = function(self, Layout) WidgetContainerBase.SetLayout = function(self, Layout)
if self then
self.LayoutFunc = AceGUI:GetLayout(Layout) self.LayoutFunc = AceGUI:GetLayout(Layout)
end end
end
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust) WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
if adjust then if adjust then
@@ -617,6 +630,7 @@ end
-- Very simple Layout, Children are stacked on top of each other down the left side -- Very simple Layout, Children are stacked on top of each other down the left side
AceGUI:RegisterLayout("List", AceGUI:RegisterLayout("List",
function(content, children) function(content, children)
if not content then return end
local height = 0 local height = 0
local width = content.width or content:GetWidth() or 0 local width = content.width or content:GetWidth() or 0
for i = 1, #children do for i = 1, #children do
@@ -654,6 +668,7 @@ AceGUI:RegisterLayout("List",
-- A single control fills the whole content area -- A single control fills the whole content area
AceGUI:RegisterLayout("Fill", AceGUI:RegisterLayout("Fill",
function(content, children) function(content, children)
if not content then return end
if children[1] then if children[1] then
children[1]:SetWidth(content:GetWidth() or 0) children[1]:SetWidth(content:GetWidth() or 0)
children[1]:SetHeight(content:GetHeight() or 0) children[1]:SetHeight(content:GetHeight() or 0)
@@ -674,6 +689,7 @@ end
AceGUI:RegisterLayout("Flow", AceGUI:RegisterLayout("Flow",
function(content, children) function(content, children)
if layoutrecursionblock then return end if layoutrecursionblock then return end
if not content then return end
--used height so far --used height so far
local height = 0 local height = 0
--width used in the current row --width used in the current row
@@ -852,6 +868,7 @@ Cell:
]] ]]
AceGUI:RegisterLayout("Table", AceGUI:RegisterLayout("Table",
function (content, children) function (content, children)
if not content then return end
local obj = content.obj local obj = content.obj
obj:PauseLayout() obj:PauseLayout()
@@ -187,7 +187,12 @@ local function Constructor()
local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND") local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
scrollbg:SetAllPoints(scrollbar) scrollbg:SetAllPoints(scrollbar)
if scrollbg.SetColorTexture then
scrollbg:SetColorTexture(0, 0, 0, 0.4) scrollbg:SetColorTexture(0, 0, 0, 0.4)
else
scrollbg:SetTexture(0, 0, 0)
scrollbg:SetVertexColor(0, 0, 0, 0.4)
end
--Container Support --Container Support
local content = CreateFrame("Frame", nil, scrollframe) local content = CreateFrame("Frame", nil, scrollframe)
@@ -679,7 +679,12 @@ local function Constructor()
local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND") local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
scrollbg:SetAllPoints(scrollbar) scrollbg:SetAllPoints(scrollbar)
if scrollbg.SetColorTexture then
scrollbg:SetColorTexture(0,0,0,0.4) 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) local border = CreateFrame("Frame", nil, frame, BackdropTemplateMixin and "BackdropTemplate" or nil)
border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT") border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT")
@@ -143,7 +143,12 @@ local function Constructor()
colorSwatch.background = texture colorSwatch.background = texture
texture:SetWidth(16) texture:SetWidth(16)
texture:SetHeight(16) texture:SetHeight(16)
if texture.SetColorTexture then
texture:SetColorTexture(1, 1, 1) texture:SetColorTexture(1, 1, 1)
else
texture:SetTexture(1, 1, 1)
texture:SetVertexColor(1, 1, 1, 1)
end
texture:SetPoint("CENTER", colorSwatch) texture:SetPoint("CENTER", colorSwatch)
texture:Show() texture:Show()
@@ -455,7 +455,12 @@ do
local line = self.frame:CreateTexture(nil, "OVERLAY") local line = self.frame:CreateTexture(nil, "OVERLAY")
line:SetHeight(1) 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("LEFT", self.frame, "LEFT", 10, 0)
line:SetPoint("RIGHT", self.frame, "RIGHT", -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>
+24 -24
View File
@@ -20,7 +20,7 @@ local myJourneyLocales = {
["deDE"] = "Meine Reise", ["deDE"] = "Meine Reise",
["koKR"] = "나의 여정", ["koKR"] = "나의 여정",
["esMX"] = "Mi viaje", ["esMX"] = "Mi viaje",
["enUS"] = true, ["enUS"] = "My Journey",
["zhCN"] = "我的任务历程", ["zhCN"] = "我的任务历程",
["zhTW"] = "我的冒險日記", ["zhTW"] = "我的冒險日記",
["esES"] = "Mi viaje", ["esES"] = "Mi viaje",
@@ -32,7 +32,7 @@ local myJourneyLocales = {
["deDE"] = "%s's Reise", ["deDE"] = "%s's Reise",
["koKR"] = "%s의 여정", ["koKR"] = "%s의 여정",
["esMX"] = "Viaje de %s", ["esMX"] = "Viaje de %s",
["enUS"] = true, ["enUS"] = "%s's Journey",
["zhCN"] = "%s的任务历程", ["zhCN"] = "%s的任务历程",
["zhTW"] = "%s的冒險日記", ["zhTW"] = "%s的冒險日記",
["esES"] = "Viaje de %s", ["esES"] = "Viaje de %s",
@@ -44,7 +44,7 @@ local myJourneyLocales = {
["deDE"] = "Deine aktuelle Historie", ["deDE"] = "Deine aktuelle Historie",
["koKR"] = "최근 기록", ["koKR"] = "최근 기록",
["esMX"] = "Tu historial reciente", ["esMX"] = "Tu historial reciente",
["enUS"] = true, ["enUS"] = "Your Recent History",
["zhCN"] = "近期纪录", ["zhCN"] = "近期纪录",
["zhTW"] = "最近的歷史記錄", ["zhTW"] = "最近的歷史記錄",
["esES"] = "Tu historial reciente", ["esES"] = "Tu historial reciente",
@@ -56,7 +56,7 @@ local myJourneyLocales = {
["deDE"] = "Es ist an der Zeit, dass du dich auf deine erste Reise begibst!", ["deDE"] = "Es ist an der Zeit, dass du dich auf deine erste Reise begibst!",
["koKR"] = "당신은 이제 막 첫번째 여정을 시작했습니다!", ["koKR"] = "당신은 이제 막 첫번째 여정을 시작했습니다!",
["esMX"] = "¡Es hora de que te embarques en tu primera aventura!", ["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"] = "是时候踏上旅程了!", ["zhCN"] = "是时候踏上旅程了!",
["zhTW"] = "該是你踏上第一次旅程的時候了!", ["zhTW"] = "該是你踏上第一次旅程的時候了!",
["esES"] = "¡Es hora de que te embarques en tu primera aventura!", ["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!", ["deDE"] = "Herzlichen Glückwunsch! Du hast Level %s erreicht!",
["koKR"] = "축하합니다! %s 레벨을 달성했습니다!", ["koKR"] = "축하합니다! %s 레벨을 달성했습니다!",
["esMX"] = "¡Felicitaciones, has alcanzado el %s!", ["esMX"] = "¡Felicitaciones, has alcanzado el %s!",
["enUS"] = true, ["enUS"] = "Congratulations! You reached %s !",
["zhCN"] = "恭喜你达到了%s", ["zhCN"] = "恭喜你达到了%s",
["zhTW"] = "恭喜! 已達到 %s !", ["zhTW"] = "恭喜! 已達到 %s !",
["esES"] = "¡Enhorabuena! ¡Has alcanzado el %s!", ["esES"] = "¡Enhorabuena! ¡Has alcanzado el %s!",
@@ -80,7 +80,7 @@ local myJourneyLocales = {
["deDE"] = "Du hast die Quest '%s' angenommen", ["deDE"] = "Du hast die Quest '%s' angenommen",
["koKR"] = "%s 퀘스트를 수락했습니다", ["koKR"] = "%s 퀘스트를 수락했습니다",
["esMX"] = "Aceptaste la misión %s", ["esMX"] = "Aceptaste la misión %s",
["enUS"] = true, ["enUS"] = "You Accepted the quest %s",
["zhCN"] = "你接受了任务:%s", ["zhCN"] = "你接受了任务:%s",
["zhTW"] = "已接受任務 %s", ["zhTW"] = "已接受任務 %s",
["esES"] = "Aceptaste la misión %s", ["esES"] = "Aceptaste la misión %s",
@@ -92,7 +92,7 @@ local myJourneyLocales = {
["deDE"] = "Quest %s: %s", ["deDE"] = "Quest %s: %s",
["koKR"] = "퀘스트 %s: %s", ["koKR"] = "퀘스트 %s: %s",
["esMX"] = "Misión %s: %s", ["esMX"] = "Misión %s: %s",
["enUS"] = true, ["enUS"] = "Quest %s: %s",
["zhCN"] = "任务%s%s", ["zhCN"] = "任务%s%s",
["zhTW"] = "任務 %s: %s", ["zhTW"] = "任務 %s: %s",
["esES"] = "Misión %s: %s", ["esES"] = "Misión %s: %s",
@@ -104,7 +104,7 @@ local myJourneyLocales = {
["deDE"] = "Level %s", ["deDE"] = "Level %s",
["koKR"] = "%s 레벨", ["koKR"] = "%s 레벨",
["esMX"] = "nivel %s", ["esMX"] = "nivel %s",
["enUS"] = true, ["enUS"] = "Level %s",
["zhCN"] = "等级%s", ["zhCN"] = "等级%s",
["zhTW"] = "等級 %s", ["zhTW"] = "等級 %s",
["esES"] = "nivel %s", ["esES"] = "nivel %s",
@@ -116,7 +116,7 @@ local myJourneyLocales = {
["deDE"] = "Du hast Level %s erreicht", ["deDE"] = "Du hast Level %s erreicht",
["koKR"] = "%s 레벨을 달성했습니다", ["koKR"] = "%s 레벨을 달성했습니다",
["esMX"] = "Alcanzaste el nivel %s", ["esMX"] = "Alcanzaste el nivel %s",
["enUS"] = true, ["enUS"] = "You Reached Level %s",
["zhCN"] = "你达到了等级 %s", ["zhCN"] = "你达到了等级 %s",
["zhTW"] = "你已達到 %s 級", ["zhTW"] = "你已達到 %s 級",
["esES"] = "Alcanzaste el nivel %s", ["esES"] = "Alcanzaste el nivel %s",
@@ -128,7 +128,7 @@ local myJourneyLocales = {
["deDE"] = "Jahr %s", ["deDE"] = "Jahr %s",
["koKR"] = "%s년", ["koKR"] = "%s년",
["esMX"] = "Año %s", ["esMX"] = "Año %s",
["enUS"] = true, ["enUS"] = "Year %s",
["zhCN"] = "%s年", ["zhCN"] = "%s年",
["zhTW"] = "%s 年", ["zhTW"] = "%s 年",
["esES"] = "Año %s", ["esES"] = "Año %s",
@@ -140,7 +140,7 @@ local myJourneyLocales = {
["deDE"] = "Angenommen", ["deDE"] = "Angenommen",
["koKR"] = "수락", ["koKR"] = "수락",
["esMX"] = "Aceptada", ["esMX"] = "Aceptada",
["enUS"] = true, ["enUS"] = "Accepted",
["zhCN"] = "接受", ["zhCN"] = "接受",
["zhTW"] = "接受", ["zhTW"] = "接受",
["esES"] = "Aceptada", ["esES"] = "Aceptada",
@@ -152,7 +152,7 @@ local myJourneyLocales = {
["deDE"] = "Du hast die Quest '%s' abgebrochen", ["deDE"] = "Du hast die Quest '%s' abgebrochen",
["koKR"] = "%s 퀘스트를 포기했습니다", ["koKR"] = "%s 퀘스트를 포기했습니다",
["esMX"] = "Abandonaste la misión %s", ["esMX"] = "Abandonaste la misión %s",
["enUS"] = true, ["enUS"] = "You Abandoned the quest %s",
["zhCN"] = "你放弃了任务:%s", ["zhCN"] = "你放弃了任务:%s",
["zhTW"] = "已放棄任務 %s", ["zhTW"] = "已放棄任務 %s",
["esES"] = "Abandonaste la misión %s", ["esES"] = "Abandonaste la misión %s",
@@ -164,7 +164,7 @@ local myJourneyLocales = {
["deDE"] = "Abgebrochen", ["deDE"] = "Abgebrochen",
["koKR"] = "포기", ["koKR"] = "포기",
["esMX"] = "Abandonada", ["esMX"] = "Abandonada",
["enUS"] = true, ["enUS"] = "Abandoned",
["zhCN"] = "放弃", ["zhCN"] = "放弃",
["zhTW"] = "放棄", ["zhTW"] = "放棄",
["esES"] = "Abandonada", ["esES"] = "Abandonada",
@@ -176,7 +176,7 @@ local myJourneyLocales = {
["deDE"] = "Titel des Eintrags", ["deDE"] = "Titel des Eintrags",
["koKR"] = "제목", ["koKR"] = "제목",
["esMX"] = "Título", ["esMX"] = "Título",
["enUS"] = true, ["enUS"] = "Entry Title",
["zhCN"] = "输入标题", ["zhCN"] = "输入标题",
["zhTW"] = "輸入標題", ["zhTW"] = "輸入標題",
["esES"] = "Título", ["esES"] = "Título",
@@ -188,7 +188,7 @@ local myJourneyLocales = {
["deDE"] = "Tagebucheintrag", ["deDE"] = "Tagebucheintrag",
["koKR"] = "내용", ["koKR"] = "내용",
["esMX"] = "Entrada de viaje", ["esMX"] = "Entrada de viaje",
["enUS"] = true, ["enUS"] = "Journal Entry",
["zhCN"] = "输入游记", ["zhCN"] = "输入游记",
["zhTW"] = "冒險筆記內容", ["zhTW"] = "冒險筆記內容",
["esES"] = "Entrada de viaje", ["esES"] = "Entrada de viaje",
@@ -200,7 +200,7 @@ local myJourneyLocales = {
["deDE"] = "Du hast die Quest '%s' abgeschlossen", ["deDE"] = "Du hast die Quest '%s' abgeschlossen",
["koKR"] = "%s 퀘스트를 완료했습니다", ["koKR"] = "%s 퀘스트를 완료했습니다",
["esMX"] = "Completaste la misión %s", ["esMX"] = "Completaste la misión %s",
["enUS"] = true, ["enUS"] = "You Completed the quest %s",
["zhCN"] = "你完成了任务:%s", ["zhCN"] = "你完成了任务:%s",
["zhTW"] = "已完成任務 %s", ["zhTW"] = "已完成任務 %s",
["esES"] = "Completaste la misión %s", ["esES"] = "Completaste la misión %s",
@@ -212,7 +212,7 @@ local myJourneyLocales = {
["deDE"] = "Eintrag hinzufügen", ["deDE"] = "Eintrag hinzufügen",
["koKR"] = "메모 추가", ["koKR"] = "메모 추가",
["esMX"] = "Añadir entrada", ["esMX"] = "Añadir entrada",
["enUS"] = true, ["enUS"] = "Add Entry",
["zhCN"] = "添加条目", ["zhCN"] = "添加条目",
["zhTW"] = "新增內容", ["zhTW"] = "新增內容",
["esES"] = "Añadir entrada", ["esES"] = "Añadir entrada",
@@ -224,7 +224,7 @@ local myJourneyLocales = {
["deDE"] = "Notiz erstellt: %s", ["deDE"] = "Notiz erstellt: %s",
["koKR"] = "메모 추가: %s", ["koKR"] = "메모 추가: %s",
["esMX"] = "Nota creada: %s", ["esMX"] = "Nota creada: %s",
["enUS"] = true, ["enUS"] = "Note Created: %s",
["zhCN"] = "创建:%s", ["zhCN"] = "创建:%s",
["zhTW"] = "筆記建立於: %s", ["zhTW"] = "筆記建立於: %s",
["esES"] = "Nota creada: %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.", ["deDE"] = "Keine Notiz angegeben. Du musst eine Notiz angeben, bevor ein Eintrag angelegt werden kann.",
["koKR"] = "내용이 입력되지 않았습니다. 메모를 추가하려면 내용을 입력해주세요.", ["koKR"] = "내용이 입력되지 않았습니다. 메모를 추가하려면 내용을 입력해주세요.",
["esMX"] = "No has introducido una nota. Tienes que introducir una antes de crear tu nota.", ["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"] = "内容不可为空", ["zhCN"] = "内容不可为空",
["zhTW"] = "沒有輸入筆記,送出前必須先輸入筆記內容。", ["zhTW"] = "沒有輸入筆記,送出前必須先輸入筆記內容。",
["esES"] = "No has introducido una nota. Tienes que introducir uno antes de crear tu nota.", ["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.", ["deDE"] = "Kein Titel angegeben. Du musst einen Titel angeben, bevor ein Eintrag angelegt werden kann.",
["koKR"] = "제목이 입력되지 않았습니다. 메모를 추가하려면 제목을 입력해주세요.", ["koKR"] = "제목이 입력되지 않았습니다. 메모를 추가하려면 제목을 입력해주세요.",
["esMX"] = "No has introducido un título. Tienes que introducir uno antes de crear tu nota.", ["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"] = "标题不可为空", ["zhCN"] = "标题不可为空",
["zhTW"] = "沒有輸入標題,送出筆記前必須先輸入標題。", ["zhTW"] = "沒有輸入標題,送出筆記前必須先輸入標題。",
["esES"] = "No has introducido un título. Tienes que introducir uno antes de crear tu nota.", ["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", ["deDE"] = "Neue Notiz für: %s",
["koKR"] = "새로운 메모: %s", ["koKR"] = "새로운 메모: %s",
["esMX"] = "Nueva Nota para: %s", ["esMX"] = "Nueva Nota para: %s",
["enUS"] = true, ["enUS"] = "New Note For: %s",
["zhCN"] = "新笔记:%s", ["zhCN"] = "新笔记:%s",
["zhTW"] = "新筆記: %s", ["zhTW"] = "新筆記: %s",
["esES"] = "Nueva nota para: %s", ["esES"] = "Nueva nota para: %s",
@@ -272,7 +272,7 @@ local myJourneyLocales = {
["deDE"] = "Notiz: %s", ["deDE"] = "Notiz: %s",
["koKR"] = "메모: %s", ["koKR"] = "메모: %s",
["esMX"] = "Nota: %s", ["esMX"] = "Nota: %s",
["enUS"] = true, ["enUS"] = "Note: %s",
["zhCN"] = "笔记:%s", ["zhCN"] = "笔记:%s",
["zhTW"] = "筆記: %s", ["zhTW"] = "筆記: %s",
["esES"] = "Nota: %s", ["esES"] = "Nota: %s",
@@ -284,7 +284,7 @@ local myJourneyLocales = {
["deDE"] = "Neue Abenteuer-Notiz hinzufügen", ["deDE"] = "Neue Abenteuer-Notiz hinzufügen",
["koKR"] = "새 여행 메모 추가", ["koKR"] = "새 여행 메모 추가",
["esMX"] = "Añadir nueva nota de aventura", ["esMX"] = "Añadir nueva nota de aventura",
["enUS"] = true, ["enUS"] = "Add New Adventure Note",
["zhCN"] = "添加冒险笔记", ["zhCN"] = "添加冒险笔记",
["zhTW"] = "新增冒險筆記", ["zhTW"] = "新增冒險筆記",
["esES"] = "Añadir nueva nota de aventura", ["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!", ["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가 당신을 위해 기억해드립니다!", ["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", ["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就会替你保存下来!", ["zhCN"] = "替你的魔兽升级之旅创建一个条目,纪录特别的时刻。只要输入标题和内容,Questie就会替你保存下来!",
["zhTW"] = "在你的冒險日記中建立新內容來記錄特別的時刻,只要簡單的輸入標題和內容描述,任務位置提示插件就會幫你保存起來!", ["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", ["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",
+9
View File
@@ -147,6 +147,15 @@ function _l10n:translate(key, ...)
return format(key, unpack(args)) return format(key, unpack(args))
end 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)) return format(translationValue, unpack(args))
end end
@@ -57,6 +57,7 @@ function _QuestieJourney:CreateObjectiveText(desc)
end end
function _QuestieJourney:HandleTabChange(container, group) function _QuestieJourney:HandleTabChange(container, group)
if not container then return end
if not _QuestieJourney.containerCache then if not _QuestieJourney.containerCache then
_QuestieJourney.containerCache = container _QuestieJourney.containerCache = container
end end
+35 -5
View File
@@ -28,12 +28,31 @@ local LOG_DEVELOP = false
local function DebugLog(tier, msg) local function DebugLog(tier, msg)
if tier == "CRITICAL" and LOG_CRITICAL then if tier == "CRITICAL" and LOG_CRITICAL then
Questie:Print("|cFF00FF00[QL-CRITICAL]|r " .. msg) -- print("[QuestieLearnerComms] " .. msg)
elseif tier == "DEVELOP" and LOG_DEVELOP then elseif tier == "DEVELOP" and LOG_DEVELOP then
Questie:Debug(Questie.DEBUG_DEVELOP, "|cFF00FFFF[QL-DEV]|r " .. msg) -- print("[QuestieLearnerComms] " .. msg)
end end
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) -- Throttling (Token Bucket)
local bucketCapacity = 9 local bucketCapacity = 9
local bucketWindow = 60 local bucketWindow = 60
@@ -167,18 +186,29 @@ function _QuestieLearnerComms:ProcessReinforcement()
end end
function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data) 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 = { local payload = {
_ver = ProtocolVersion, _ver = ProtocolVersion,
op = op, -- "NEW", "UPDATE", "CONFIRM" op = op, -- "NEW", "UPDATE", "CONFIRM"
typ = entityType, typ = entityType,
id = entityId, id = entityId,
d = data, d = sanitizedData,
ts = time() ts = time()
} }
-- 2. Serialize and Compress -- 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 compressed = LibDeflate:CompressDeflate(serialized, {level = 9})
local encoded = LibDeflate:EncodeForPrint(compressed) local encoded = LibDeflate:EncodeForPrint(compressed)
+49 -1
View File
@@ -17,6 +17,8 @@ end
-- Polyfill for xpcall variadic arguments (missing in standard Lua 5.0/5.1 WoW clients). -- 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, -- Modern Ace3 uses xpcall(func, err, ...) which drops arguments on legacy clients,
-- leading to 'self' being nil in addon callbacks. -- 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 = xpcall
local xpcall_supported = false local xpcall_supported = false
pcall(function() pcall(function()
@@ -24,7 +26,7 @@ pcall(function()
end) end)
if not xpcall_supported then 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. -- 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. -- 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 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 -- No extra args provided
return _xpcall(func, err) return _xpcall(func, err)
end 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 end
-- addon is running on 3.3.5 WotLK client -- addon is running on 3.3.5 WotLK client
@@ -91,6 +120,25 @@ if not TooltipBackdropTemplateMixin then
TooltipBackdropTemplateMixin = BackdropTemplateMixin TooltipBackdropTemplateMixin = BackdropTemplateMixin
end 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) -- API difference compatibility (Era/Wotlk)
------------------------------------------- -------------------------------------------
+13 -4
View File
@@ -16,6 +16,12 @@ local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
---@type l10n ---@type l10n
local l10n = QuestieLoader:ImportModule("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 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 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 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(); local LINK_LENGTHS = LINK_CODE:len();
-- handles clicking on link -- handles clicking on link
-- FIX: Added InCombatLockdown guard and pcall to prevent tainting secure execution paths. -- FIX: Added InCombatLockdown guard, deferred execution, and pcall to prevent tainting
-- SetItemRef can be called during action button clicks (e.g., quest item tooltips) which -- secure execution paths. SetItemRef can be called during action button clicks (e.g., quest
-- run in a protected execution context. If the hook runs insecure code, it can taint -- item tooltips) which run in a protected execution context. If the hook runs insecure code,
-- the call chain and cause "ADDON_ACTION_BLOCKED: tried to call UseAction()" errors. -- 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) hooksecurefunc("SetItemRef", function(link)
if InCombatLockdown() then return end if InCombatLockdown() then return end
local linkType = link:sub(1, LINK_LENGTHS); local linkType = link:sub(1, LINK_LENGTHS);
if linkType == LINK_CODE then if linkType == LINK_CODE then
C_Timer.After(0, function()
pcall(QuestieDebugOffer.ShowOffer, link) pcall(QuestieDebugOffer.ShowOffer, link)
end)
end end
end); end);
+1
View File
@@ -288,6 +288,7 @@ end
--- Fires when a System Message (yellow text) is output to the main chat window --- 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 ---@param message string The message value from the CHAT_MSG_SYSTEM event
function _EventHandler:ChatMsgSystem(message) 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 -- 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 if string.find(message, questCompletedMessage) == 1 or string.find(message, questAcceptedMessage) == 1 then
MinimapIcon:UpdateText(message) MinimapIcon:UpdateText(message)
+1
View File
@@ -55,6 +55,7 @@ end
--Always compare to the UnitLevel parameter, returning the highest. --Always compare to the UnitLevel parameter, returning the highest.
---@param level Level ---@param level Level
function QuestiePlayer:SetPlayerLevel(level) function QuestiePlayer:SetPlayerLevel(level)
if level == nil then return end
local localLevel = UnitLevel("player"); local localLevel = UnitLevel("player");
_QuestiePlayer.playerLevel = math_max(localLevel, level); _QuestiePlayer.playerLevel = math_max(localLevel, level);
end end
+8 -8
View File
@@ -190,18 +190,18 @@ function MapIconTooltip:Show()
end end
elseif iconData.Type == "available" or iconData.Type == "complete" then elseif iconData.Type == "available" or iconData.Type == "complete" then
local tip = _MapIconTooltip:GetAvailableOrCompleteTooltip(icon) local tip = _MapIconTooltip:GetAvailableOrCompleteTooltip(icon)
if not npcAndObjectOrder[tip.title] then if not npcAndObjectOrder["default"] then
npcAndObjectOrder[tip.title] = {npcNames = {}, quests = {}}; npcAndObjectOrder["default"] = {npcNames = {}, quests = {}};
end end
npcAndObjectOrder[tip.title].npcNames[iconData.Name] = true npcAndObjectOrder["default"].npcNames[iconData.Name] = true
npcAndObjectOrder[tip.title].quests[tip.title] = tip 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 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) local tip = _MapIconTooltip:GetAvailableOrCompleteTooltip(icon)
if not npcAndObjectOrder[tip.title] then if not npcAndObjectOrder["default"] then
npcAndObjectOrder[tip.title] = {npcNames = {}, quests = {}}; npcAndObjectOrder["default"] = {npcNames = {}, quests = {}};
end end
npcAndObjectOrder[tip.title].npcNames[iconData.Name] = true npcAndObjectOrder["default"].npcNames[iconData.Name] = true
npcAndObjectOrder[tip.title].quests[tip.title] = tip npcAndObjectOrder["default"].quests[tip.title] = tip
elseif iconData.CustomTooltipData then elseif iconData.CustomTooltipData then
manualOrder[iconData.CustomTooltipData.Title] = { Body = { iconData.CustomTooltipData.Body or "" } } manualOrder[iconData.CustomTooltipData.Title] = { Body = { iconData.CustomTooltipData.Body or "" } }
elseif iconData.ManualTooltipData then elseif iconData.ManualTooltipData then
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 30300 ## 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: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión ## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.4.2 ## Version: 1.4.4
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 30300 ## 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: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misión ## Notes-esMX: Ayundante de misión
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.4.2 ## Version: 1.4.4
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+2 -2
View File
@@ -1,11 +1,11 @@
## Interface: 11200 ## 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: A standalone Classic QuestHelper
## Notes-esMX: Ayundante de misiones ## Notes-esMX: Ayundante de misiones
## Notes-esES: Ayundante de misiones ## Notes-esES: Ayundante de misiones
## Notes-ptBR: Ajudante de misiones ## Notes-ptBR: Ajudante de misiones
## Notes-frFR: Assistant de quêtes ## Notes-frFR: Assistant de quêtes
## Version: 1.4.2 ## Version: 1.4.4
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+1 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.4.2 ## Version: 1.4.4
## RequiredDeps: ## 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 ## 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 ## SavedVariables: QuestieConfig, QuestieLearnerDB
+1 -1
View File
@@ -2,7 +2,7 @@
<img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" /> <img src="docs/QuestieXlogo.png" alt="Questie-X Logo" width="320" />
![Version](https://img.shields.io/badge/Questie--X-v1.4.2-blue.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/Questie--X-v1.4.4-blue.svg?style=for-the-badge)
[![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases) [![Downloads](https://img.shields.io/github/downloads/Xurkon/Questie-X/total?style=for-the-badge&color=e67e22)](https://github.com/Xurkon/Questie-X/releases)
[![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/) [![Documentation](https://img.shields.io/badge/Documentation-View%20Docs-58a6ff?style=for-the-badge)](https://xurkon.github.io/Questie-X/)
[![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon) [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/Xurkon)
+30
View File
@@ -176,6 +176,36 @@
</div> </div>
<div class="container"> <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> <h2 id="v142">v1.4.2 — Quest Route Optimization</h2>
<ul> <ul>
<li><strong>[Feature]</strong> Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.</li> <li><strong>[Feature]</strong> Added Quest Route Optimization with three modes: Single Quest, All Tracked Quests, and TSP Approximation.</li>