v9.7.2: Ebonhold Database integration and core logic refinements
This commit is contained in:
@@ -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
|
||||
-----------------
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 Ramer–Douglas–Peucker 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})
|
||||
@@ -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
|
||||
|
||||
]]--
|
||||
Reference in New Issue
Block a user