v9.7.2: Ebonhold Database integration and core logic refinements

This commit is contained in:
Xurkon
2026-02-14 21:13:44 -06:00
parent 0a2cc15641
commit 6ef85d4e2d
573 changed files with 1751730 additions and 2 deletions
+311
View File
@@ -0,0 +1,311 @@
---@class MessageHandlerFactory
local MessageHandlerFactory = setmetatable(QuestieLoader:CreateModule("MessageHandlerFactory"),
{ __call = function(self) return self.New() end })
---@alias Event string
---@alias Callback fun(...:any):any
--- COMPATIBILITY ---
local C_Timer = QuestieCompat.C_Timer
--- Localize functions
local yield = coroutine.yield
local insert, remove = table.insert, table.remove
local wipe = wipe
--- Creates a new MessageHandler
---@return MessageHandler
function MessageHandlerFactory.New()
---@class MessageHandler
local handler = {}
--- Contains all the events that are fired repeatably
---@type table<Event,Callback[]>
handler.repeatEvents = {}
--- Contains all the events that are fired once
---@type table<Event,Callback[]>
handler.onceEvents = {}
-- Used when asyncronously calling events
---@type table<Event, boolean>
handler.executing = {}
--- The local function for both Async and Sync callbacks
---@param eventName Event
---@param asyncCount number? @How many events to fire per yield
---@param ... any @Input arguments for the callback
---@return table? @Returns a table of all the return values from the callbacks
local function fire(eventName, asyncCount, ...)
if handler.executing[eventName] then
error("Event '" .. eventName .. "' is already being executed!", 2)
end
handler.executing[eventName] = true
--* Fire once tables
if handler.onceEvents[eventName] then
local eventList = handler.onceEvents[eventName]
for callbackIndex = 1, #eventList do
-- Function call
eventList[callbackIndex](...)
end
wipe(eventList)
end
--* Fire repeat tables
local returnValues = nil
if handler.repeatEvents[eventName] then
local eventList = handler.repeatEvents[eventName]
for callbackIndex = 1, #eventList do
-- Function call
local retValue = eventList[callbackIndex](...)
-- If we have a return value we save it to the return table
if retValue then
if not returnValues then returnValues = {} end
returnValues[#returnValues + 1] = retValue
end
--If we are a async function we yield after each asyncCount
if asyncCount and callbackIndex % asyncCount == 0 then
yield()
end
end
end
handler.executing[eventName] = nil
return returnValues
end
--- Fire a callback event
---@param eventName Event
---@param ... any @Input arguments for the callback
---@return table? @A table containing all the return values
function handler:Fire(eventName, ...)
return fire(eventName, nil, ...)
end
--- Fire a async callback event which invokes coroutine yield
---@param eventName Event
---@param asyncCount number? @How many events to fire per yield
---@param ... any @Input arguments for the callback
---@return table? @A table containing all the return values
function handler:FireAsync(eventName, asyncCount, ...)
local returnValues = fire(eventName, asyncCount, ...)
--? We call yield here returning the value to the calling resume, makes it take one more resume to finish
yield(returnValues)
return returnValues
end
---Register a callback
---@param eventName Event
---@param callback Callback
function handler:RegisterRepeating(eventName, callback)
if not callback or type(callback) ~= "function" then
error("Usage: Register(eventName, callback): 'callback' - function expected.", 2)
elseif not eventName or type(eventName) ~= "string" then
error("Usage: Register(eventName, callback): 'eventName' - a string expected.", 2)
end
if not self.repeatEvents[eventName] then
self.repeatEvents[eventName] = {}
end
insert(self.repeatEvents[eventName], callback)
end
--- Register a callback that will only be called once
---@param eventName Event
---@param callback Callback
function handler:RegisterOnce(eventName, callback)
if not callback or type(callback) ~= "function" then
error("Usage: RegisterOnce(eventName, callback): 'callback' - function expected.", 2)
elseif not eventName or type(eventName) ~= "string" then
error("Usage: RegisterOnce(eventName, callback): 'eventName' - a string expected.", 2)
end
if not self.onceEvents[eventName] then
self.onceEvents[eventName] = {}
end
insert(self.onceEvents[eventName], callback)
end
---Unregister a callback by function
---@param eventName Event
---@param callback Callback
function handler:UnregisterRepeating(eventName, callback)
if not callback or type(callback) ~= "function" then
error("Usage: Unregister(eventName, callback): 'callback' - function expected.", 2)
elseif not eventName or type(eventName) ~= "string" then
error("Usage: Unregister(eventName, callback): 'eventName' - a string expected.", 2)
end
local eventList = self.repeatEvents[eventName]
if eventList then
for callbackIndex = 1, #eventList do
if eventList[callbackIndex] == callback then
remove(self.repeatEvents[eventName], callbackIndex)
return
end
end
end
end
---Unregisters all events for a given event name in repeat and once-lists
---@param eventName Event
function handler:UnregisterAll(eventName)
if not eventName or type(eventName) ~= "string" then
error("Usage: UnregisterAll(eventName): 'eventName' - a string expected.", 2)
end
if self.repeatEvents[eventName] then
wipe(self.repeatEvents[eventName])
end
if self.onceEvents[eventName] then
wipe(self.onceEvents[eventName])
end
end
return handler
end
----- Tests -----
do
--? This is the tests for MessageHandlerFactory
local function RunMessageHandlerTests()
Questie:Debug(Questie.DEBUG_CRITICAL, " -- Running " .. Questie:Colorize("MessageHandlerFactory", "yellow") .. " tests --")
local testEvent = "EVENT_TEST"
--- Test simple usage
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementFunction = function()
returnedCount = returnedCount + 1
end
-- Add and fire
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event was not fired", "red"))
-- Unregister and fire
MessageHandler:UnregisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event was fired after unregistering", "red"))
-- Register two events and fire
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event was not fired twice", "red"))
-- Unregister all events and fire
MessageHandler:UnregisterAll(testEvent)
MessageHandler:Fire(testEvent)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event was fired after unregistering all", "red"))
-- Register once and fire
MessageHandler:RegisterOnce(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
MessageHandler:Fire(testEvent)
assert(returnedCount == 4, Questie:Colorize(" -- FAILED: Event was not fired once", "red"))
end
--- Test multiple registered events
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementFunction = function()
returnedCount = returnedCount + 1
end
local incrementFunction2 = function()
returnedCount = returnedCount + 1
end
local testEvent2 = "EVENT_TEST2"
MessageHandler:RegisterRepeating(testEvent, incrementFunction)
MessageHandler:RegisterRepeating(testEvent2, incrementFunction2)
MessageHandler:Fire(testEvent)
assert(returnedCount == 1, Questie:Colorize(" -- FAILED: Event 1 was not fired", "red"))
MessageHandler:Fire(testEvent2)
assert(returnedCount == 2, Questie:Colorize(" -- FAILED: Event 2 was not fired", "red"))
-- Unregister and fire
MessageHandler:UnregisterRepeating(testEvent, incrementFunction)
MessageHandler:Fire(testEvent)
assert(returnedCount == 2, Questie:Colorize(" -- FAILED: Event 1 was fired after unregistering", "red"))
MessageHandler:Fire(testEvent2)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event 2 was not fired", "red"))
MessageHandler:UnregisterRepeating(testEvent2, incrementFunction2)
MessageHandler:Fire(testEvent2)
assert(returnedCount == 3, Questie:Colorize(" -- FAILED: Event 2 was fired after unregistering", "red"))
end
--- Test return
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementReturnFunction = function()
returnedCount = returnedCount + 1
return returnedCount
end
-- Register mutliple events and fire
for _ = 1, 5 do
MessageHandler:RegisterRepeating(testEvent, incrementReturnFunction)
end
local retVal = MessageHandler:Fire(testEvent)
assert(retVal, Questie:Colorize(" -- FAILED: Return value was nil", "red"))
assert(retVal[1] == 1, Questie:Colorize(" -- FAILED: 1 Function value was not returned", "red"))
assert(retVal[2] == 2, Questie:Colorize(" -- FAILED: 2 Function value was not returned", "red"))
assert(retVal[3] == 3, Questie:Colorize(" -- FAILED: 3 Function value was not returned", "red"))
assert(retVal[4] == 4, Questie:Colorize(" -- FAILED: 4 Function value was not returned", "red"))
assert(retVal[5] == 5, Questie:Colorize(" -- FAILED: 5 Function value was not returned", "red"))
end
--- Test async and async return
do
local MessageHandler = MessageHandlerFactory:New()
local returnedCount = 0
local incrementReturnFunction = function()
returnedCount = returnedCount + 1
return returnedCount
end
-- Register mutliple events and fire
for _ = 1, 5 do
MessageHandler:RegisterRepeating(testEvent, incrementReturnFunction)
end
local routine = coroutine.create(
function()
MessageHandler:FireAsync(testEvent, 2)
assert(returnedCount == 5, Questie:Colorize(" -- FAILED: Event was not fired the correct amount of times", "red"))
end
)
local timer
timer = C_Timer.NewTicker(0, function()
local success, retVal = coroutine.resume(routine)
if retVal then
assert(retVal[1] == 1, Questie:Colorize(" -- FAILED: 1 Function value was not returned", "red"))
assert(retVal[2] == 2, Questie:Colorize(" -- FAILED: 2 Function value was not returned", "red"))
assert(retVal[3] == 3, Questie:Colorize(" -- FAILED: 3 Function value was not returned", "red"))
assert(retVal[4] == 4, Questie:Colorize(" -- FAILED: 4 Function value was not returned", "red"))
assert(retVal[5] == 5, Questie:Colorize(" -- FAILED: 5 Function value was not returned", "red"))
end
assert(success, Questie:Colorize(" -- FAILED: Coroutine failed", "red"), retVal)
-- Kill the timer when the coroutine is dead.
if (coroutine.status(routine) == "dead") then
Questie:Debug(Questie.DEBUG_CRITICAL, "- MessageHandlerFactory - |cFF00FF00SUCCESS!|r")
timer:Cancel()
end
end)
end
end
-- Run it after all files has been loaded
C_Timer.After(2, RunMessageHandlerTests)
end
-----------------
+44
View File
@@ -0,0 +1,44 @@
---@class QuestieCombatQueue
local QuestieCombatQueue = QuestieLoader:CreateModule("QuestieCombatQueue")
---@type QuestieLib
local QuestieLib = QuestieLoader:CreateModule("QuestieLib")
--- COMPATIBILITY ---
local C_Timer = QuestieCompat.C_Timer
local tpack = QuestieLib.tpack
local tunpack = QuestieLib.tunpack
local _Queue = {}
local started = false
-- This will limit the amount of updates Questie does to the UI and will reduce the chance to lag the game
local maxUpdatesPerCircle = 5
function QuestieCombatQueue.Initialize()
C_Timer.NewTicker(0.1, function()
if InCombatLockdown() then
return
end
local entry = tremove(_Queue, 1)
local count = 0
while entry do
entry.func(tunpack(entry.args))
if InCombatLockdown() or count >= maxUpdatesPerCircle then
break
end
entry = tremove(_Queue, 1)
count = count + 1
end
end)
started = true
end
function QuestieCombatQueue:Queue(func, ...)
if started then
tinsert(_Queue, {func=func, args=tpack(...)})
end
end
+793
View File
@@ -0,0 +1,793 @@
local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata
---@class QuestieLib
local QuestieLib = QuestieLoader:CreateModule("QuestieLib")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
--- COMPATIBILITY ---
local addonName = QuestieCompat.Is335 and QuestieCompat.addonName or "Questie"
QuestieLib.AddonPath = "Interface\\Addons\\"..addonName.."\\"
local math_abs = math.abs
local math_sqrt = math.sqrt
local math_max = math.max
local math_random = math.random
local tinsert = table.insert
local stringSub = string.sub
local stringGsub = string.gsub
local strim = string.trim
local smatch = string.match
local tonumber = tonumber
-- =================================
-- Ascension Level Scaling (Core)
-- =================================
local function Ascension_IsScalingEnabled()
return Questie.db and Questie.db.profile and Questie.db.profile.enableAscensionScaling
end
local function Ascension_GetEffectiveQuestLevel(questId, baseQuestLevel, playerLevel)
if not Ascension_IsScalingEnabled(questId) then
return baseQuestLevel
end
local pl = playerLevel or QuestiePlayer.GetPlayerLevel()
local scaled = pl - 4
if scaled < 1 then scaled = 1 end
if baseQuestLevel and baseQuestLevel > scaled then
return baseQuestLevel
end
return scaled
end
function QuestieLib:IsQuestTrivialScaled(questId, questLevel)
-- If scaling is not enabled, fall back to the original behavior
if not Ascension_IsScalingEnabled(questId) then
return QuestieDB.IsTrivial(questLevel)
end
local playerLevel = QuestiePlayer.GetPlayerLevel()
local effectiveLevel = math.max(questLevel, playerLevel - 4)
local levelDiff = effectiveLevel - playerLevel
-- This is Blizzard's own logic
if levelDiff >= -GetQuestGreenRange("player") then
return false
end
return true
end
-- The original frame which we use to fetch the data required
-- Classic Wotlk Classic
local textWrapFrameObject = _G["QuestLogObjectivesText"] or _G["QuestInfoObjectivesText"]
--[[
Red: 5+ level above player
Orange: 3 - 4 level above player
Yellow: max 2 level below/above player
Green: 3 - GetQuestGreenRange() level below player (GetQuestGreenRange() changes on specific player levels)
Gray: More than GetQuestGreenRange() below player
--]]
function QuestieLib:PrintDifficultyColor(level, text, isDailyQuest, isEventQuest, isPvPQuest)
if isEventQuest == true then
return "|cFF6ce314" .. text .. "|r" -- Lime
end
if isPvPQuest == true then
return "|cFFE35639" .. text .. "|r" -- Maroon
end
if isDailyQuest == true then
return "|cFF21CCE7" .. text .. "|r" -- Blue
end
if level == -1 then
level = QuestiePlayer.GetPlayerLevel()
end
local levelDiff = level - QuestiePlayer.GetPlayerLevel()
if (levelDiff >= 5) then
return "|cFFFF1A1A" .. text .. "|r" -- Red
elseif (levelDiff >= 3) then
return "|cFFFF8040" .. text .. "|r" -- Orange
elseif (levelDiff >= -2) then
return "|cFFFFFF00" .. text .. "|r" -- Yellow
elseif (-levelDiff <= GetQuestGreenRange("player")) then
return "|cFF40C040" .. text .. "|r" -- Green
else
return "|cFFC0C0C0" .. text .. "|r" -- Grey
end
end
function QuestieLib:GetDifficultyColorPercent(level)
if level == -1 then level = QuestiePlayer.GetPlayerLevel() end
local levelDiff = level - QuestiePlayer.GetPlayerLevel()
if (levelDiff >= 5) then
-- return "|cFFFF1A1A"..text.."|r"; -- Red
return 1, 0.102, 0.102
elseif (levelDiff >= 3) then
-- return "|cFFFF8040"..text.."|r"; -- Orange
return 1, 0.502, 0.251
elseif (levelDiff >= -2) then
-- return "|cFFFFFF00"..text.."|r"; -- Yellow
return 1, 1, 0
elseif (-levelDiff <= GetQuestGreenRange("player")) then
-- return "|cFF40C040"..text.."|r"; -- Green
return 0.251, 0.753, 0.251
else
-- return "|cFFC0C0C0"..text.."|r"; -- Grey
return 0.753, 0.753, 0.753
end
end
-- 1.12 color logic
local function RGBToHex(r, g, b)
if r > 255 then r = 255 end
if g > 255 then g = 255 end
if b > 255 then b = 255 end
return string.format("|cFF%02x%02x%02x", r, g, b)
end
local function FloatRGBToHex(r, g, b) return RGBToHex(r * 254, g * 254, b * 254) end
function QuestieLib:GetRGBForObjective(objective)
if objective.fulfilled ~= nil and (not objective.Collected) then
objective.Collected = objective.fulfilled
objective.Needed = objective.required
end
if not objective.Collected or type(objective.Collected) ~= "number" then
return FloatRGBToHex(0.8, 0.8, 0.8)
end
local float = objective.Collected / objective.Needed
local trackerColor = Questie.db.profile.trackerColorObjectives
if not trackerColor or trackerColor == "white" or trackerColor == "minimal" then
-- White
return "|cFFEEEEEE"
elseif trackerColor == "whiteAndGreen" then
-- White and Green
return objective.Collected == objective.Needed and RGBToHex(76, 255, 76) or FloatRGBToHex(0.8, 0.8, 0.8)
elseif trackerColor == "whiteToGreen" then
-- White to Green
return FloatRGBToHex(0.8 - float / 2, 0.8 + float / 3, 0.8 - float / 2)
else
-- Red to Green
if float < .50 then return FloatRGBToHex(1, 0 + float / .5, 0) end
if float == .50 then return FloatRGBToHex(1, 1, 0) end
if float > .50 then return FloatRGBToHex(1 - float / 2, 1, 0) end
end
end
---@param questId number
---@param showLevel number @ Whether the quest level should be included
---@param showState boolean @ Whether to show (Complete/Failed)
---@param blizzLike boolean @True = [40+], false/nil = [40D/R]
function QuestieLib:GetColoredQuestName(questId, showLevel, showState, blizzLike)
local name = QuestieDB.QueryQuestSingle(questId, "name")
local level, _ = QuestieLib.GetTbcLevel(questId);
if showLevel then
name = QuestieLib:GetQuestString(questId, name, level, blizzLike)
end
if Questie.db.profile.enableTooltipsQuestID then
name = name .. " (" .. questId .. ")"
end
if showState then
local isComplete = QuestieDB.IsComplete(questId)
if isComplete == -1 then
name = name .. " " .. Questie:Colorize("(" .. l10n("Failed") .. ")", "red")
elseif isComplete == 1 then
name = name .. " " .. Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
-- Quests treated as complete - zero objectives or synthetic objectives
elseif isComplete == 0 and QuestieDB.GetQuest(questId).isComplete == true then
name = name .. " " .. Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
end
end
return QuestieLib:PrintDifficultyColor(level, name, QuestieDB.IsRepeatable(questId), QuestieDB.IsActiveEventQuest(questId), QuestieDB.IsPvPQuest(questId))
end
local colors = {
{ 0.3125, 0.44140625, 1 }, --Blizzard Polygon-blue --Alpha of 128
--{123, 146, 255}, --Blizzard Polygon-blue-2 --Alpha of 61
{ 0.5, 0.46875, 0.84765625 }, --Medium Purple
{ 0.58203125, 0.89453125, 0.0546875 }, --Inch Worm
{ 0.45703125, 0.8515625, 0.78125 }, --Downy
{ 1, 0.5625, 0.625 }, --Salmon Pink
--{149, 159, 112}, --Avocado, Bad? Fix it
{ 0, 0.6484375, 0.59375 }, --Persian Green
{ 0.70703125, 0.109375, 0.4765625 }, --Medium Violet Red --ORG Dark Purple {119, 18, 79}
{ 0.58203125, 0.2148375, 1 }, --Light Slate Blue
{ 0.72265625, 0.3671875, 0 }, --Alloy Orange
{ 0, 0.9765625, 0.546875 }, --Spring Green
{ 0.8515625, 0.2148375, 0.57421875 }, --Deep Cerise
{ 1, 0.65234375, 0 }, --Orange
{ 0.8125, 0.7109375, 1 }, --Mauve
{ 0, 0.25390625, 0.58984375 }, --Smalt
{ 1, 0.25, 1 }, --Pink Flamingo
{ 1, 1, 0 }, --Yellow
{ 0.16015625, 0.65234375, 0 }, --Slimy Green
{ 0, 0.66015625, 1 }, --Deep Sky Blue
{ 0.87109375, 0.87109375, 0.56640625 }, --Primrose
{ 0, 0.5859375, 0 }, --Vine Green --G67 default
{ 0, 0.3, 1 }, --Navy Blue
{ 0, 0.97265625, 0 }, --Lime
{ 0, 1, 1 }, --Aqua
{ 1, 0.1484375, 0 }, --Scarlet
}
local numColors = #colors
local lastColor = math_random(numColors)
---@return Color
function QuestieLib:ColorWheel()
lastColor = lastColor + 1
if lastColor > numColors then
lastColor = 1
end
return colors[lastColor]
end
---@return Color
function QuestieLib:GetRandomColor()
return colors[math_random(numColors)]
end
---@param questId number
---@param name string @The (localized) name of the quest
---@param level number @The quest level
---@param blizzLike boolean @True = [40+], false/nil = [40D/R]
function QuestieLib:GetQuestString(questId, name, level, blizzLike)
local questType, questTag = QuestieDB.GetQuestTagInfo(questId)
if questType and questTag then
local char = "+"
if (not blizzLike) then
char = stringSub(questTag, 1, 1)
end
-- The string.sub above doesn't work for multi byte characters in Chinese
local langCode = l10n:GetUILocale()
if questType == 1 then
-- Elite quest
name = "[" .. level .. "+" .. "] " .. name
elseif questType == 81 then
if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
char = "D"
end
-- Dungeon quest
name = "[" .. level .. char .. "] " .. name
elseif questType == 62 then
if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
char = "R"
end
-- Raid quest
name = "[" .. level .. char .. "] " .. name
elseif questType == 41 then
-- Which one? This is just default.
name = "[" .. level .. "] " .. name
-- PvP quest
-- name = "[" .. level .. questTag .. "] " .. name
elseif questType == 83 then
-- Legendary quest
name = "[" .. level .. "++" .. "] " .. name
else
-- Some other irrelevant type
name = "[" .. level .. "] " .. name
end
else
name = "[" .. level .. "] " .. name
end
return name
end
--- There are quests in TBC which have a quest level of -1. This indicates that the quest level is the
--- same as the player level. This function should be used whenever accessing the quest or required level.
---@param questId QuestId
---@param playerLevel Level? ---@ PlayerLevel, if nil we fetch current level
---@return Level questLevel
---@return Level requiredLevel
---@return Level requiredMaxLevel
function QuestieLib.GetTbcLevel(questId, playerLevel)
local questLevel, requiredLevel =
QuestieDB.QueryQuestSingle(questId, "questLevel"),
QuestieDB.QueryQuestSingle(questId, "requiredLevel")
if questLevel == -1 then
local level = playerLevel or QuestiePlayer.GetPlayerLevel()
if requiredLevel > level then
questLevel = requiredLevel
else
questLevel = level
requiredLevel = level
end
end
-- Ascension level scaling (effective level only)
questLevel = Ascension_GetEffectiveQuestLevel(questId, questLevel, playerLevel)
return questLevel, requiredLevel, QuestieDB.QueryQuestSingle(questId, "requiredMaxLevel")
end
---@param questId QuestId
---@param level Level @The quest level
---@param blizzLike boolean @True = [40+], false/nil = [40D/R]
---@return string levelString @String of format "[40+]"
function QuestieLib:GetLevelString(questId, _, level, blizzLike)
local questType, questTag = QuestieDB.GetQuestTagInfo(questId)
local retLevel = tostring(level)
if questType and questTag then
local char = "+"
if (not blizzLike) then
char = stringSub(questTag, 1, 1)
end
-- the string.sub above doesn't work for multi byte characters in Chinese
local langCode = l10n:GetUILocale()
if questType == 1 then
-- Elite quest
retLevel = "[" .. retLevel .. "+" .. "] "
elseif questType == 81 then
if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
char = "D"
end
-- Dungeon quest
retLevel = "[" .. retLevel .. char .. "] "
elseif questType == 62 then
if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
char = "R"
end
-- Raid quest
retLevel = "[" .. retLevel .. char .. "] "
elseif questType == 41 then
-- Which one? This is just default.
retLevel = "[" .. retLevel .. "] "
-- PvP quest
-- name = "[" .. level .. questTag .. "] " .. name
elseif questType == 83 then
-- Legendary quest
retLevel = "[" .. retLevel .. "++" .. "] "
else
-- Some other irrelevant type
retLevel = "[" .. retLevel .. "] "
end
else
retLevel = "[" .. retLevel .. "] "
end
return retLevel
end
function QuestieLib:GetRaceString(raceMask)
if not raceMask or raceMask == QuestieDB.raceKeys.NONE then
return ""
end
if raceMask == QuestieDB.raceKeys.ALL_ALLIANCE then
return l10n("Alliance")
elseif raceMask == QuestieDB.raceKeys.ALL_HORDE then
return l10n("Horde")
else
local raceString = ""
local raceTable = QuestieLib:UnpackBinary(raceMask)
local stringTable = {
l10n('Human'),
l10n('Orc'),
l10n('Dwarf'),
l10n('Nightelf'),
l10n('Undead'),
l10n('Tauren'),
l10n('Gnome'),
l10n('Troll'),
l10n('Goblin'),
l10n('Blood Elf'),
l10n('Draenei')
}
local firstRun = true
for k, v in pairs(raceTable) do
if v then
if firstRun then
firstRun = false
else
raceString = raceString .. ", "
end
raceString = raceString .. stringTable[k]
end
end
return raceString
end
end
function QuestieLib:CacheItemNames(questId)
local quest = QuestieDB.GetQuest(questId)
if (quest and quest.ObjectiveData) then
for _, objectiveDB in pairs(quest.ObjectiveData) do
if objectiveDB.Type == "item" then
if not ((QuestieDB.ItemPointers or QuestieDB.itemData)[objectiveDB.Id]) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLib:CacheItemNames] Requesting item information for missing itemId:", objectiveDB.Id)
local item = Item:CreateFromItemID(objectiveDB.Id)
item:ContinueOnItemLoad(
function()
local itemName = item:GetItemName()
if not QuestieDB.itemDataOverrides[objectiveDB.Id] then
QuestieDB.itemDataOverrides[objectiveDB.Id] = { itemName, { questId }, {}, {} }
else
QuestieDB.itemDataOverrides[objectiveDB.Id][1] = itemName
end
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieLib:CacheItemNames] Created item information for item:", itemName, ":", objectiveDB.Id)
end)
end
end
end
end
end
function QuestieLib:Euclid(x, y, i, e)
-- No need for absolute values as these are used only as squared
local xd = x - i
local yd = y - e
return math_sqrt(xd * xd + yd * yd)
end
function QuestieLib:Maxdist(x, y, i, e)
return math_max(math_abs(x - i), math_abs(y - e))
end
local cachedVersion
---@return number, number, number
function QuestieLib:GetAddonVersionInfo()
if (not cachedVersion) then
cachedVersion = GetAddOnMetadata(addonName, "Version")
end
local major, minor, patch = string.match(cachedVersion, "(%d+)%p(%d+)%p(%d+)")
return tonumber(major), tonumber(minor), tonumber(patch)
end
function QuestieLib:GetAddonVersionString()
if (not cachedVersion) then
-- This brings up the ## Version from the TOC
cachedVersion = GetAddOnMetadata(addonName, "Version")
end
return "v" .. cachedVersion
end
-- According to stack overflow, # and table.getn arent reliable (I've experienced this? not sure whats up)
function QuestieLib:Count(table)
local count = 0
for _, _ in pairs(table) do count = count + 1 end
return count
end
-- Credits to Shagu and pfQuest, why reinvent the wheel.
-- https://gitlab.com/shagu/pfQuest/blob/master/compat/pfUI.lua
local sanitize_cache = {}
function QuestieLib:SanitizePattern(pattern)
if not sanitize_cache[pattern] then
local ret = pattern
-- escape magic characters
ret = stringGsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
-- remove capture indexes
ret = stringGsub(ret, "%d%$", "")
-- catch all characters
ret = stringGsub(ret, "(%%%a)", "%(%1+%)")
-- convert all %s to .+
ret = stringGsub(ret, "%%s%+", ".+")
-- set priority to numbers over strings
ret = stringGsub(ret, "%(.%+%)%(%%d%+%)", "%(.-%)%(%%d%+%)")
-- cache it
sanitize_cache[pattern] = ret
end
return sanitize_cache[pattern]
end
function QuestieLib:SortQuestIDsByLevel(quests)
local sortedQuestsByLevel = {}
local function compareTablesByIndex(a, b)
return a[1] < b[1]
end
for q in pairs(quests) do
local questLevel, _ = QuestieLib.GetTbcLevel(q);
tinsert(sortedQuestsByLevel, { questLevel or 0, q })
end
table.sort(sortedQuestsByLevel, compareTablesByIndex)
return sortedQuestsByLevel
end
local randomSeed = 0
function QuestieLib:MathRandomSeed(seed)
randomSeed = seed
end
function QuestieLib:MathRandom(low_or_high_arg, high_arg)
local low
local high
if low_or_high_arg ~= nil then
if high_arg ~= nil then
low = low_or_high_arg
high = high_arg
else
low = 1
high = low_or_high_arg
end
end
randomSeed = (randomSeed * 214013 + 2531011) % 2 ^ 32
local rand = (math.floor(randomSeed / 2 ^ 16) % 2 ^ 15) / 0x7fff
if not high then
return rand
end
return low + math.floor(rand * high)
end
function QuestieLib:UnpackBinary(val)
local ret = {}
for q = 0, 16 do
if bit.band(bit.rshift(val, q), 1) == 1 then
tinsert(ret, true)
else
tinsert(ret, false)
end
end
return ret
end
-- Link contains test bench for regex in lua.
-- https://hastebin.com/anodilisuw.bash
-- QUEST_MONSTERS_KILLED etc. patterns are from WoW API
local L_QUEST_MONSTERS_KILLED = QuestieLib:SanitizePattern(QUEST_MONSTERS_KILLED)
local L_QUEST_ITEMS_NEEDED = QuestieLib:SanitizePattern(QUEST_ITEMS_NEEDED)
local L_QUEST_OBJECTS_FOUND = QuestieLib:SanitizePattern(QUEST_OBJECTS_FOUND)
--- 'FooBar slain: 0/3' --> 'FooBar'
--- 'EpicItem : 0/1' --> 'EpicItem'
---@param text string @requires nil check and first character ~= " " check before call
---@param objectiveType string
function QuestieLib.TrimObjectiveText(text, objectiveType)
local originalText = text
if objectiveType == "monster" then
local n, _, monsterName = smatch(text, L_QUEST_MONSTERS_KILLED)
if tonumber(monsterName) then -- SOME objectives are reversed in TBC, why blizzard?
monsterName = n
end
if (not monsterName) or (strlen(monsterName) == strlen(originalText)) then
--The above doesn't seem to work with the chinese, the row below tries to remove the extra numbers.
text = smatch(monsterName or text, "(.*)");
else
text = monsterName
end
elseif objectiveType == "item" then
local n, _, itemName = smatch(text, L_QUEST_ITEMS_NEEDED)
if tonumber(itemName) then -- SOME objectives are reversed in TBC, why blizzard?
itemName = n
end
text = itemName
elseif objectiveType == "object" then
local n, _, objectName = smatch(text, L_QUEST_OBJECTS_FOUND)
if tonumber(objectName) then -- SOME objectives are reversed in TBC, why blizzard?
objectName = n
end
text = objectName
end
-- If the functions above do not give a good answer fall back to older regex to get something.
if not text then
text = smatch(originalText, "^(.*):%s") or smatch(originalText, "%s(.*)$") or smatch(originalText, "^(.*)%s") or originalText
end
text = strim(text)
--Questie:Debug(Questie.DEBUG_DEVELOP, "[TrimObjectiveText] \""..originalText.."\" --> \""..text.."\"") -- Comment out this debug for speed when not used.
return text
end
---@return boolean
function QuestieLib.equals(a, b)
if a == nil and b == nil then return true end
if a == nil or b == nil then return false end
local ta = type(a)
local tb = type(b)
if ta ~= tb then return false end
if ta == "number" then
return math.abs(a - b) < 0.2
elseif ta == "table" then
for k, v in pairs(a) do
if (not QuestieLib.equals(b[k], v)) then
return false
end
end
for k, v in pairs(b) do
if (not QuestieLib.equals(a[k], v)) then
return false
end
end
return true
end
return a == b
end
---@return table A table of the handed parameters plus the 'n' field with the size of the table
function QuestieLib.tpack(...)
return { n = select("#", ...), ... }
end
--- Wow's own unpack stops at first nil. this version is not speed optimized.
--- Supports just above QuestieLib.tpack func as it requires the 'n' field.
---@param tbl table A table packed with QuestieLib.tpack
---@return table 'n' values of the tbl
function QuestieLib.tunpack(tbl)
if tbl.n == 0 then
return nil
end
local function recursion(i)
if i == tbl.n then
return tbl[i]
end
return tbl[i], recursion(i + 1)
end
return recursion(1)
end
---@alias TableWeakMode
---| '"v"' # Weak Value
---| '"k"' # Weak Key
---| '"kv"' # Weak Value and Weak Key
---| '""' # Regular table
---* Memoize a function with a cache
--! This does not support nil, never input nil into the table
---@param func function
---@param __mode TableWeakMode?
---@return table
function QuestieLib:TableMemoizeFunction(func, __mode)
return setmetatable({}, {
__index = function(self, k)
local v = func(k);
self[k] = v
return v;
end,
__mode = __mode or ""
});
end
--Part of the GameTooltipWrapDescription function
local textWrapObjectiveFontString
---Emulates the wrapping of a quest description
---@param line string @The line to wrap
---@param prefix string @The prefix to add to the line
---@param combineTrailing boolean @If the last line is only one word, combine it with previous? TRUE=COMBINE, FALSE=NOT COMBINE, default: true
---@param desiredWidth number @Set the desired width to wrap, default: 275
---@return table[] @A table of wrapped lines
function QuestieLib:TextWrap(line, prefix, combineTrailing, desiredWidth)
if not textWrapObjectiveFontString then
textWrapObjectiveFontString = UIParent:CreateFontString("questieObjectiveTextString", "ARTWORK", "QuestFont")
textWrapObjectiveFontString:SetWidth(textWrapFrameObject:GetWidth() or 275) --QuestLogObjectivesText default width = 275
textWrapObjectiveFontString:SetHeight(0);
textWrapObjectiveFontString:SetPoint("LEFT");
textWrapObjectiveFontString:SetJustifyH("LEFT");
---@diagnostic disable-next-line: redundant-parameter
textWrapObjectiveFontString:SetWordWrap(true)
textWrapObjectiveFontString:SetVertexColor(1, 1, 1, 1) --Set opacity to 0, even if it is shown it should be invisible
local font, size = textWrapFrameObject:GetFont()
--Chinese? "Fonts\\ARKai_T.ttf"
textWrapObjectiveFontString:SetFont(font, size);
textWrapObjectiveFontString:Hide()
end
if (textWrapObjectiveFontString:IsVisible()) then Questie:Error("TextWrap already running... Please report this on GitHub or Discord.") end
--Set Defaults
combineTrailing = combineTrailing or true
--We show the fontstring and set the text to start the process
--We have to show it or else the functions won't work... But we set the opacity to 0 on creation
textWrapObjectiveFontString:SetWidth(desiredWidth or textWrapFrameObject:GetWidth() or 275) --QuestLogObjectivesText default width = 275
textWrapObjectiveFontString:Show()
local useLine = line
textWrapObjectiveFontString:SetText(useLine)
--Is the line wrapped?
if (textWrapObjectiveFontString:GetUnboundedStringWidth() > textWrapObjectiveFontString:GetWrappedWidth()) then
local lines = {}
local startIndex = 1
local endIndex = 2 --We should be able to start at a later index...
--This function returns a list of size information per row, so we use this to calculate number of rows
local numberOfRows = #textWrapObjectiveFontString:CalculateScreenAreaFromCharacterSpan(startIndex, strlen(useLine))
for row = 1, numberOfRows do
local lastSpaceIndex
local indexes
--We use the previous way to get number of rows to loop through characterindex until we get 2 rows
repeat
indexes = textWrapObjectiveFontString:CalculateScreenAreaFromCharacterSpan(startIndex, endIndex)
--Last space of the line to be used to break a new row
if (string.sub(useLine, endIndex, endIndex) == " ") then
lastSpaceIndex = endIndex
end
endIndex = endIndex + 1
--If we are at the end of characters break and set endIndex to strlen
if (endIndex > strlen(useLine)) then
endIndex = strlen(useLine)
lastSpaceIndex = endIndex
break
end
until (#indexes > 1) --Until more than one row
--Get the line we calculated
--First to space then endIndex(chinese)
local newLine = string.sub(useLine, startIndex, lastSpaceIndex or endIndex)
--This combines a trailing word to the previous line if it is the only word of the line
--We check lastSpaceIndex here because the logic will be faulty (chinese client)
if (row == numberOfRows - 1 and combineTrailing and lastSpaceIndex) then
--Get the last line, in it's full
local lastLine = string.sub(useLine, endIndex - 2, strlen(useLine))
--Does the line not contain any space we combine it into the previous line
if (not string.find(lastLine, " ")) then
newLine = string.sub(useLine, startIndex, strlen(useLine))
--print("NL1", newLine)
table.insert(lines, prefix .. newLine)
--Break the for loop on last line, no more running required
break
end
end
--Change the startIndex to be the new line, and add the line to the lines list
startIndex = endIndex - 2
endIndex = endIndex
table.insert(lines, prefix .. newLine)
end
textWrapObjectiveFontString:Hide()
return lines
else
--Line was not wrapped, return the string as is.
textWrapObjectiveFontString:Hide()
useLine = prefix .. line
return { useLine }
end
end
function QuestieLib.GetSpawnDistance(spawnA, spawnB)
local x1, y1 = spawnA[1], spawnA[2]
local x2, y2 = spawnB[1], spawnB[2]
-- Adjust the x-coordinate to account the map scale
local distanceX = (x1 - x2) * 1.5
local distanceY = y1 - y2
return math_sqrt(distanceX * distanceX + distanceY * distanceY)
end
return QuestieLib
+39
View File
@@ -0,0 +1,39 @@
-- The only public class except for Questie
---@class QuestieLoader
QuestieLoader = {}
local modules = {}
QuestieLoader._modules = modules -- store reference so modules can be iterated for profiling
---@generic T
---@param name `T` @Module name
---@return T|{ private: table } @Module reference
function QuestieLoader:CreateModule(name)
if (not modules[name]) then
modules[name] = { private = {} }
return modules[name]
else
return modules[name]
end
end
---@generic T
---@param name `T` @Module name
---@return T|{ private: table } @Module reference
function QuestieLoader:ImportModule(name)
if (not modules[name]) then
modules[name] = { private = {} }
return modules[name]
else
return modules[name]
end
end
function QuestieLoader:PopulateGlobals() -- called when debugging is enabled
for name, module in pairs(modules) do
_G[name] = module
end
end
+334
View File
@@ -0,0 +1,334 @@
---@class QuestieSerializer
local QuestieSerializer = QuestieLoader:CreateModule("QuestieSerializer");
-------------------------
--Import modules.
-------------------------
---@type QuestieStreamLib
local QuestieStreamLib = QuestieLoader:ImportModule("QuestieStreamLib");
function QuestieSerializer:Hash(value)
if not value or type(value) ~= "string" or (string.len(value) <= 0) then
return 0
end
local h = 5381
for i=1, string.len(value) do
h = bit.band((31 * h + string.byte(value, i)), 4294967295)
end
return h
end
QuestieSerializer.SerializerHashDB = {
}
QuestieSerializer.SerializerHashDBReversed = {
}
local function addHash(str)
local hash = QuestieSerializer:Hash(str)
if QuestieSerializer.SerializerHashDBReversed[hash] then
-- dont add, also prevents collissions
return
end
QuestieSerializer.SerializerHashDB[str] = hash
QuestieSerializer.SerializerHashDBReversed[hash] = str
end
local function clearHashes()
QuestieSerializer.SerializerHashDB = {}
QuestieSerializer.SerializerHashDBReversed = {}
end
local function _pack(a, b, c, d)
return bit.lshift(a, 24) + bit.lshift(b, 16) + bit.lshift(c, 8) + d
end
local function _unpack(val)
return (mod(bit.rshift(val, 24), 256)),
(mod(bit.rshift(val, 16), 256)),
(mod(bit.rshift(val, 8), 256)),
(mod(val, 256));
end
-- code taken from lua-MessagePack (modified)
local function floatBitsToInt(n)
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local mant, expo = frexp(n)
if mant ~= mant then
return _pack(0xFF, 0x88, 0x00, 0x00) -- nan
elseif mant == math.huge or expo > 0x80 then
if sign == 0 then
return _pack(0x7F, 0x80, 0x00, 0x00) -- inf
else
return _pack(0xFF, 0x80, 0x00, 0x00) -- -inf
end
elseif (mant == 0.0 and expo == 0) or expo < -0x7E then
return _pack(sign, 0x00, 0x00, 0x00)-- zero
else
expo = expo + 0x7E
mant = floor((mant * 2.0 - 1.0) * ldexp(0.5, 24))
return _pack(sign + floor(expo / 0x2), (expo % 0x2) * 0x80 + floor(mant / 0x10000), floor(mant / 0x100) % 0x100, mant % 0x100)
end
end
local function intBitsToFloat(int)
local b1, b2, b3, b4 = _unpack(int)
local sign = b1 > 0x7F
local expo = (b1 % 0x80) * 0x2 + floor(b2 / 0x80)
local mant = ((b2 % 0x80) * 0x100 + b3) * 0x100 + b4
if sign then
sign = -1
else
sign = 1
end
local n
if mant == 0 and expo == 0 then
n = sign * 0.0
elseif expo == 0xFF then
if mant == 0 then
n = sign * math.huge
else
n = 0.0/0.0
end
else
n = sign * ldexp(1.0 + mant / 0x800000, expo - 0x7F)
end
return n
end
local function _ReadObject(self)
local typ = self.stream:ReadByte();
if typ > 31 then -- this isnt actually a type but a number value
return typ - 32
end
return QuestieSerializer.ReaderTable[typ](self);
end
local function _ReadTable(self, entryCount)
local ret = {}
for i=1, entryCount do
local key = _ReadObject(self)
if type(key) == "string" then
addHash(key)
end
local value = _ReadObject(self)
if type(value) == "string" then
addHash(value)
end
ret[key] = value
end
return ret
end
local function _ReadArray(self, entryCount)
local ret = {}
for i=1, entryCount do
local value = _ReadObject(self)
if type(value) == "string" then
addHash(value)
end
ret[i] = value
end
return ret
end
QuestieSerializer.ReaderTable = {
[1] = function(self) return nil end,
[2] = function(self) return self.stream:ReadInt() end,
[3] = function(self) return -self.stream:ReadInt() end,
[4] = function(self) return self.stream:ReadLong() end,
[5] = function(self) return -self.stream:ReadLong() end,
[6] = function(self) return intBitsToFloat(self.stream:ReadInt()) end,
[7] = function(self) return self.stream:ReadTinyString() end,
[8] = function(self) return self.stream:ReadShortString() end,
[9] = function(self) return QuestieSerializer.SerializerHashDBReversed[self.stream:ReadInt()] end,
[10] = function(self) return _ReadTable(self, self.stream:ReadByte()) end,
[11] = function(self) return _ReadTable(self, self.stream:ReadShort()) end,
[12] = function(self) return self.stream:ReadByte() end,
[13] = function(self) return -self.stream:ReadByte() end,
[14] = function(self) return self.stream:ReadShort() end,
[15] = function(self) return -self.stream:ReadShort() end,
[16] = function(self) return false end,
[17] = function(self) return true end,
[18] = function(self) return nil end,
[19] = function(self) return nil end,
[20] = function(self) return _ReadArray(self, self.stream:ReadByte()) end,
[21] = function(self) return _ReadArray(self, self.stream:ReadShort()) end,
[22] = function(self) return _ReadArray(self, self.stream:ReadInt()) end,
--up to 31
}
local function isArray(arr)
local i = 0
for e in pairs(arr) do
i = i + 1
if i ~= e then
return false
end
end
return true
end
QuestieSerializer.WriterTable = {
["number"] = function(self, value)
local _, fract = math.modf(value)
if fract > 0 then
QuestieSerializer.WriterTable["float"](self, value)
else
local sign = 0
if value < 0 then
value = math.abs(value)
sign = 1
end
if value > 2147483646 then
self.stream:WriteByte(4 + sign)
self.stream:WriteLong(value)
elseif value < 222 and sign == 0 then
self.stream:WriteByte(32 + value) -- encoded in type byte
elseif value < 255 then
self.stream:WriteByte(12 + sign)
self.stream:WriteByte(value)
elseif value < 65530 then
self.stream:WriteByte(14 + sign)
self.stream:WriteShort(value)
else
self.stream:WriteByte(2 + sign)
self.stream:WriteInt(value)
end
end
end,
["float"] = function(self, value)
self.stream:WriteByte(6)
self.stream:WriteInt(floatBitsToInt(value))
end,
["string"] = function(self, value)
if QuestieSerializer.SerializerHashDB[value] and string.len(value) > 4 then
self.stream:WriteByte(9)
self.stream:WriteInt(QuestieSerializer.SerializerHashDB[value])
elseif string.len(value) > 254 then
self.stream:WriteByte(8)
self.stream:WriteShortString(value)
else
self.stream:WriteByte(7)
self.stream:WriteTinyString(value)
end
end,
["table"] = function(self, value, depth)
local count = 0
if not depth then depth = 0; end
if depth > 100 then return; end
for key, v in pairs(value) do -- bad
if key and v then count = count + 1; end
end
if isArray(value) then
if count > 65530 then
self.stream:WriteByte(22) -- chungus array
self.stream:WriteInt(count)
elseif count > 254 then
self.stream:WriteByte(21) -- big array
self.stream:WriteShort(count)
else
self.stream:WriteByte(20) -- small array
self.stream:WriteByte(count)
end
for _, v in pairs(value) do
local t = type(v)
if not QuestieSerializer.WriterTable[t] then
print("QuestieSerializer Error: Unhandled type: " .. t)
else
QuestieSerializer.WriterTable[t](self, v, depth)
if t == "string" then
addHash(v)
end
end
end
else
if count > 254 then
self.stream:WriteByte(11) -- big table
self.stream:WriteShort(count)
else
self.stream:WriteByte(10) -- small table
self.stream:WriteByte(count)
end
for key, v in pairs(value) do
if key and v then
QuestieSerializer:WriteKeyValuePair(key, v, depth + 1)
end
end
end
end,
["boolean"] = function(self, value)
if value then
self.stream:WriteByte(17)
else
self.stream:WriteByte(16)
end
end,
["function"] = function(self, value)
self.stream:WriteByte(1) -- nil
end
}
function QuestieSerializer:WriteKeyValuePair(key, value, depth)
if not value or not key then return; end
if not depth then
depth = 0
end
if self.objectCount > 8192 and false then print("[QuestieSerializer] Too many objects in input table!") return end
self.objectCount = self.objectCount + 1
local keyType = type(key)
local valueType = type(value)
local writeKey = QuestieSerializer.WriterTable[keyType]
local writeValue = QuestieSerializer.WriterTable[valueType]
if not writeKey or not writeValue then
print("QuestieSerializer Error: Unhandled type: " .. keyType .. " " .. valueType)
else
writeKey(self, key, depth)
if keyType == "string" then
addHash(key)
end
writeValue(self, value, depth)
if valueType == "string" then
addHash(value)
end
end
end
function QuestieSerializer:SetupStream(encoding)
if not encoding then
encoding = "1short"
end
if self.stream and self.streamEncoding == encoding then
self.stream:reset()
else
self.stream = QuestieStreamLib:GetStream(encoding)
self.streamEncoding = encoding
end
clearHashes()
end
function QuestieSerializer:Serialize(tab, encoding)
QuestieSerializer:SetupStream(encoding)
self.objectCount = 0
--QuestieSerializer:WriteKeyValuePair("meta", {protocolVersion = 1, mode="1short"})
QuestieSerializer:WriteKeyValuePair(1, tab)
return self.stream:Save()
end
function QuestieSerializer:Deserialize(data, encoding)
QuestieSerializer:SetupStream(encoding)
self.stream:Load(data)
--local meta = _ReadTable(self, 1)
local retData = _ReadTable(self, 1)
return retData[1]
end
+134
View File
@@ -0,0 +1,134 @@
---@class RamerDouglasPeucker
local RamerDouglasPeucker = QuestieLoader:CreateModule("RamerDouglasPeucker")
-- code after this point is Ramer-Douglas-Peucker algorithm implemented by Eryn Lynn
--[[
MIT License
Copyright (c) 2018 Eryn Lynn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. ]]
-- Implementation of the RamerDouglasPeucker algorithm
-- readme https://github.com/evaera/RobloxLuaAlgorithms#ramerdouglaspeuckerlua
-- author: evaera
local function getSqDist(p1, p2)
local dx = p1[1] - p2[1]
local dy = p1[2] - p2[2]
return dx * dx + dy * dy
end
local function simplifyRadialDist(points, sqTolerance)
local prevPoint = points[1]
local newPoints = {prevPoint}
local point
for i=2, #points do
point = points[i]
if getSqDist(point, prevPoint) > sqTolerance then
table.insert(newPoints, point)
prevPoint = point
end
end
if prevPoint ~= point then
table.insert(newPoints, point)
end
return newPoints
end
local function getSqSegDist(p, p1, p2)
local x = p1[1]
local y = p1[2]
local dx = p2[1] - x
local dy = p2[2] - y
if dx ~= 0 or dy ~= 0 then
local t = ((p[1] - x) * dx + (p[2] - y) * dy) / (dx * dx + dy * dy)
if t > 1 then
x = p2[1]
y = p2[2]
elseif t > 0 then
x = x + dx * t
y = y + dy * t
end
end
dx = p[1] - x
dy = p[2] - y
return dx * dx + dy * dy
end
local function simplifyDPStep(points, first, last, sqTolerance, simplified)
local maxSqDist = sqTolerance
local index
for i=first+1, last do
local sqDist = getSqSegDist(points[i], points[first], points[last])
if sqDist > maxSqDist then
index = i
maxSqDist = sqDist
end
end
if maxSqDist > sqTolerance then
if index - first > 1 then
simplifyDPStep(points, first, index, sqTolerance, simplified)
end
table.insert(simplified, points[index])
if last - index > 1 then
simplifyDPStep(points, index, last, sqTolerance, simplified)
end
end
end
local function simplifyDouglasPeucker(points, sqTolerance)
local last = #points
local simplified={points[1]}
simplifyDPStep(points, 1, last, sqTolerance, simplified)
table.insert(simplified, points[last])
return simplified
end
local _RamerDouglasPeucker = function(points, tolerance, highestQuality)
if #points <= 2 then
return points
end
local sqTolerance = tolerance ~= nil and tolerance^2 or 1
points = highestQuality and points or simplifyRadialDist(points, sqTolerance)
points = simplifyDouglasPeucker(points, sqTolerance)
return points
end
setmetatable(RamerDouglasPeucker, { __call = function(_, ...) return _RamerDouglasPeucker(...) end})
+137
View File
@@ -0,0 +1,137 @@
---@class ThreadLib
local ThreadLib = QuestieLoader:CreateModule("ThreadLib")
--- COMPATIBILITY ---
local C_Timer = QuestieCompat.C_Timer
--Coroutine functions
local coStatus, coResume, coCreate = coroutine.status, coroutine.resume, coroutine.create
local lType = type
-- local cTimer = C_Timer
local newTicker = C_Timer.NewTicker
---Thread a function, callback function is called when the thread is done.
---@param threadFunction function @The function to thread
---@param delay integer @Anything below 0.05 is each frame
---@param errorMessage string? @What is the "Prepend" of the error message
---@param callbackFunction function? @Function to call when the thread is done
---@return Ticker Timer @The WoW timer, run Timer:Cancel() and let the handle of the thread become orphaned to cancel
---@return thread Thread @The coroutine thread
function ThreadLib.Thread(threadFunction, delay, errorMessage, callbackFunction)
if lType(threadFunction) ~= "function" then
error("ThreadLib:Thread: threadFunction is not a function")
end
if lType(delay) ~= "number" then
error("ThreadLib:Thread: delay is not a number")
end
if errorMessage and lType(errorMessage) ~= "string" then
error("ThreadLib:Thread: errorMessage is not a string")
end
if callbackFunction and lType(callbackFunction) ~= "function" then
error("ThreadLib:Thread: callbackFunction is not a function")
end
local thread = coCreate(threadFunction)
local timer
timer = newTicker(delay or 0, function()
if(coStatus(thread) == "suspended") then --It's faster not to lookup the value but instead have it here
local success, ret = coResume(thread)
-- Something in the coroutine went wrong, print the error and stop the timer
if not success then
Questie:Error(errorMessage or "Error in thread", ret)
timer:Cancel();
end
elseif (coStatus(thread) == "dead") then --It's faster not to lookup the value but instead have it here
timer:Cancel();
if(callbackFunction) then
callbackFunction()
end
--? Is this needed?
timer = nil
---@diagnostic disable-next-line: cast-local-type
thread = nil
end
end)
return timer, thread
end
---Thread a function, callback function is called when the thread is done.
---@param threadFunction function @The function to thread
---@param delay integer @Anything below 0.05 is each frame
---@param callbackFunction function @Function to call when the thread is done
---@return Ticker Timer @The WoW timer, run Timer:Cancel() and let the handle of the thread become orphaned to cancel
---@return thread Thread @The coroutine thread
function ThreadLib.ThreadCallback(threadFunction, delay, callbackFunction)
return ThreadLib.Thread(threadFunction, delay, nil, callbackFunction)
end
---Thread a function, using a specific error message.
---@param threadFunction function @The function to thread
---@param delay integer @Anything below 0.05 is each frame
---@param errorMessage string @What is the "Prepend" of the error message
---@return Ticker Timer @The WoW timer, run Timer:Cancel() and let the handle of the thread become orphaned to cancel
---@return thread Thread @The coroutine thread
function ThreadLib.ThreadError(threadFunction, delay, errorMessage)
return ThreadLib.Thread(threadFunction, delay, errorMessage)
end
---Thread a function
---@param threadFunction function @The function to thread
---@param delay integer @Anything below 0.05 is each frame
---@return Ticker Timer @The WoW timer, run Timer:Cancel() and let the handle of the thread become orphaned to cancel
---@return thread Thread @The coroutine thread
function ThreadLib.ThreadSimple(threadFunction, delay)
return ThreadLib.Thread(threadFunction, delay)
end
--? This was kind of a halv baked idea, that i questioned was even good, but i don't really want to delete it yet.
--[[
---@class Thread
---@field private _thread thread
---@field private _timer Ticker
---@field private _callback function?
---@field Kill fun()
local newThread = {
_thread = coCreate(threadFunction),
_callback = callbackFunction,
Continue = ThreadContinue,
---@param self Thread
Kill = function(self)
print(Questie.DEBUG_CRITICAL, "[ThreadLib] Thread cancelled")
self._timer:Cancel()
self._thread = nil
self._timer = nil
self.Kill = nil
self.Continue = nil
end
}
newThread._timer = newTicker(delay or 0, function()
if(coStatus(newThread._thread) == "suspended") then --It's faster not to lookup the value but instead have it here
local success, ret = coResume(newThread._thread)
-- Something in the coroutine went wrong, print the error and stop the timer
if not success then
Questie:Error(errorMessage or "Error in thread", ret)
newThread._timer:Cancel();
end
elseif (coStatus(newThread._thread) == "dead") then --It's faster not to lookup the value but instead have it here
newThread._timer:Cancel();
if(newThread._callback) then
callbackFunction()
end
newThread._thread = nil
newThread._timer = nil
wipe(newThread)
end
end)
return newThread
]]--