Files
Questie-X/Modules/Libs/QuestieLib.lua
T
Xurkon 568cd44af8 fix: port Phase 1 perf/correctness audit fixes onto main
Brings the still-needed changes from questie-phase1perf that main lacked.
Main already had the Lua 5.0 shims (QuestieLoader bit/strsplit, QuestieStream
and QuestieSerializer math.mod sweep), so those are omitted.

- QuestieNameplate: skip missing entries instead of return-aborting the whole
  activeGUIDs loop (one bad unit no longer stalls every nameplate update)
- QuestieQuest.ClearAllNotes: skip DB-missing quests instead of aborting, so
  remaining quests' notes still get cleared
- QuestieOptionsTracker: fix fadeTickerValue:Cancel -> fadeTicker:Cancel (3x);
  fadeTickerValue is a number, the ticker handle is fadeTicker
- QuestieCommsData: nil-guard GetNPC/GetObject before reading .name
- QuestieAnnounce: bound the alreadySentBandaid dedup cache (reset at 1000)
- QuestieDB.IsComplete: hoist GetQuest into expectedQuest (one call, not two)
- QuestieFramePool/QuestieFrame: drop the dead BaseOnUpdate ticker branch
  (BaseOnUpdate was never defined; behavior was always OnUpdate=nil)
- QuestieLib: document unused questId arg on Ascension_IsScalingEnabled
- Database/Corrections x3: strip stray UTF-8 BOM
- Questie-X.toc: remove duplicate QuestieSlash.lua load line
2026-06-12 06:48:53 -05:00

807 lines
29 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = QuestieLoader.addonName
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(questId) -- questId accepted (unused) to match call sites and clear the arity lint
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 name == nil then
return nil
end
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) 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)
if not name then return tostring(questId) end
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") or 1,
QuestieDB.QueryQuestSingle(questId, "requiredLevel") or 1
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") or 0
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
local itemId = objectiveDB.Id
if not itemId then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLib:CacheItemNames] Skipping item objective without itemId for quest:", questId)
elseif not ((QuestieDB.ItemPointers or QuestieDB.itemData)[itemId]) then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLib:CacheItemNames] Requesting item information for missing itemId:", itemId)
local item = Item:CreateFromItemID(itemId)
item:ContinueOnItemLoad(
function()
local itemName = item:GetItemName()
if not itemId then
return
end
if not QuestieDB.itemDataOverrides[itemId] then
QuestieDB.itemDataOverrides[itemId] = { itemName, { questId }, {}, {} }
else
QuestieDB.itemDataOverrides[itemId][1] = itemName
end
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieLib:CacheItemNames] Created item information for item:", itemName, ":", itemId)
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") or "0.0.0"
end
local major, minor, patch = string.match(cachedVersion, "(%d+)%p(%d+)%p(%d+)")
return tonumber(major) or 0, tonumber(minor) or 0, tonumber(patch) or 0
end
function QuestieLib:GetAddonVersionString()
if (not cachedVersion) then
-- This brings up the ## Version from the TOC, falling back to various potential folder names on custom realms
cachedVersion = GetAddOnMetadata(addonName, "Version") or GetAddOnMetadata("Questie-X", "Version") or GetAddOnMetadata("Questie-Ebonhold", "Version") or "Unknown"
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