385 lines
11 KiB
Lua
385 lines
11 KiB
Lua
-- Freewood-OctoWoW-Core: Hilfsfunktionen
|
|
-- Kopieren/Zusammenfuehren von Tabellen, Geld, Items (asynchron), Pruefsumme,
|
|
-- NPC-ID aus GUID, Spielerposition.
|
|
|
|
local ADDON, FW = ...
|
|
|
|
local Util = {}
|
|
FW.Util = Util
|
|
|
|
------------------------------------------------------------------------
|
|
-- Tabellen
|
|
------------------------------------------------------------------------
|
|
|
|
function Util.CopyTable(src, seen)
|
|
if type(src) ~= "table" then
|
|
return src
|
|
end
|
|
seen = seen or {}
|
|
if seen[src] then
|
|
return seen[src]
|
|
end
|
|
local dst = {}
|
|
seen[src] = dst
|
|
for k, v in pairs(src) do
|
|
dst[k] = Util.CopyTable(v, seen)
|
|
end
|
|
return dst
|
|
end
|
|
|
|
-- Geheime Werte (secret values): das Spiel gibt manche Angaben, vor allem zu fremden Einheiten,
|
|
-- als "geheimen" Wert zurueck. Damit darf Addon-Code nichts rechnen, vergleichen oder indizieren.
|
|
function Util.IsSecret(v)
|
|
local fn = _G.issecretvalue
|
|
if type(fn) == "function" then
|
|
local ok, res = pcall(fn, v)
|
|
return ok and res and true or false
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Gibt v zurueck, aber nil, wenn der Wert geheim ist
|
|
function Util.Clean(v)
|
|
if Util.IsSecret(v) then
|
|
return nil
|
|
end
|
|
return v
|
|
end
|
|
|
|
-- Ergaenzt fehlende Schluessel in target aus defaults. Vorhandene Werte bleiben.
|
|
function Util.MergeDefaults(target, defaults)
|
|
for k, v in pairs(defaults) do
|
|
if type(v) == "table" then
|
|
if type(target[k]) ~= "table" then
|
|
target[k] = Util.CopyTable(v)
|
|
else
|
|
Util.MergeDefaults(target[k], v)
|
|
end
|
|
elseif target[k] == nil then
|
|
target[k] = v
|
|
end
|
|
end
|
|
return target
|
|
end
|
|
|
|
function Util.Count(t)
|
|
local n = 0
|
|
for _ in pairs(t) do
|
|
n = n + 1
|
|
end
|
|
return n
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Zahlen und Geld
|
|
------------------------------------------------------------------------
|
|
|
|
function Util.Round(n, decimals)
|
|
local mult = 10 ^ (decimals or 0)
|
|
return math.floor(n * mult + 0.5) / mult
|
|
end
|
|
|
|
function Util.Clamp(n, lo, hi)
|
|
if n < lo then
|
|
return lo
|
|
end
|
|
if n > hi then
|
|
return hi
|
|
end
|
|
return n
|
|
end
|
|
|
|
function Util.FormatNumber(n)
|
|
local text = string.format("%.0f", math.floor(math.abs(n) + 0.5))
|
|
local sep = (FW.activeLocale == "deDE") and "." or ","
|
|
local count
|
|
repeat
|
|
text, count = text:gsub("^(%d+)(%d%d%d)", "%1" .. sep .. "%2")
|
|
until count == 0
|
|
if n < 0 then
|
|
text = "-" .. text
|
|
end
|
|
return text
|
|
end
|
|
|
|
local MONEY_COLORS = { g = "f0bf5c", s = "c4c8d0", c = "c8793c" }
|
|
|
|
-- Kupfer -> "12G 34S 56K". opts.plain: ohne Farben, opts.noCopper: Kupfer weglassen
|
|
function Util.FormatMoney(copper, opts)
|
|
opts = opts or {}
|
|
copper = tonumber(copper) or 0
|
|
local negative = copper < 0
|
|
if negative then
|
|
copper = -copper
|
|
end
|
|
copper = math.floor(copper)
|
|
|
|
local gold = math.floor(copper / 10000)
|
|
local silver = math.floor((copper % 10000) / 100)
|
|
local rest = copper % 100
|
|
local L = FW.L
|
|
|
|
local function unit(key, symbol)
|
|
if opts.plain then
|
|
return symbol
|
|
end
|
|
return "|cff" .. MONEY_COLORS[key] .. symbol .. "|r"
|
|
end
|
|
|
|
local parts = {}
|
|
if gold > 0 then
|
|
parts[#parts + 1] = Util.FormatNumber(gold) .. unit("g", L.MONEY_G)
|
|
end
|
|
if silver > 0 then
|
|
parts[#parts + 1] = silver .. unit("s", L.MONEY_S)
|
|
end
|
|
if rest > 0 and not (opts.noCopper and gold > 0) then
|
|
parts[#parts + 1] = rest .. unit("c", L.MONEY_C)
|
|
end
|
|
if #parts == 0 then
|
|
parts[1] = "0" .. unit("c", L.MONEY_C)
|
|
end
|
|
|
|
local text = table.concat(parts, " ")
|
|
if negative then
|
|
text = "-" .. text
|
|
end
|
|
return text
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Pruefsumme (Adler-32-Variante, nur Rechnen mit kleinen Zahlen)
|
|
------------------------------------------------------------------------
|
|
|
|
function Util.Hash(text)
|
|
local a, b = 1, 0
|
|
for i = 1, #text do
|
|
a = (a + text:byte(i)) % 65521
|
|
b = (b + a) % 65521
|
|
end
|
|
return string.format("%04x%04x", b, a)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Items (mit asynchronem Nachladen)
|
|
------------------------------------------------------------------------
|
|
|
|
local function RawItemInfo(idOrLink)
|
|
if C_Item and C_Item.GetItemInfo then
|
|
return C_Item.GetItemInfo(idOrLink)
|
|
end
|
|
return GetItemInfo(idOrLink)
|
|
end
|
|
|
|
-- Liefert eine Tabelle oder nil, wenn die Daten noch nicht im Cache sind.
|
|
function Util.GetItem(idOrLink)
|
|
local name, link, quality, itemLevel, minLevel, itemType, itemSubType,
|
|
stackCount, equipLoc, icon, sellPrice, classID, subClassID = RawItemInfo(idOrLink)
|
|
if not name then
|
|
return nil
|
|
end
|
|
local id = tonumber(idOrLink)
|
|
if not id and type(link) == "string" then
|
|
id = tonumber(link:match("item:(%d+)"))
|
|
end
|
|
return {
|
|
id = id,
|
|
name = name,
|
|
link = link,
|
|
quality = quality or 1,
|
|
itemLevel = itemLevel,
|
|
minLevel = minLevel,
|
|
type = itemType,
|
|
subType = itemSubType,
|
|
stackCount = stackCount,
|
|
equipLoc = equipLoc,
|
|
icon = icon,
|
|
sellPrice = sellPrice or 0,
|
|
classID = classID,
|
|
subClassID = subClassID,
|
|
}
|
|
end
|
|
|
|
local pendingItems = {} -- itemID -> Liste der wartenden Callbacks
|
|
local itemQueue = {} -- noch nicht gesendete Anfragen (Reihenfolge)
|
|
local itemQueueHead = 1
|
|
local itemSent = {} -- itemID -> true, wenn gesendet und noch offen
|
|
local itemOut = 0 -- Zahl offener, gesendeter Anfragen
|
|
local itemFailed = {} -- itemID -> true, Zeitueberschreitung (wird am Ende nochmal versucht)
|
|
local itemFailedList = {}
|
|
local itemRetried = {}
|
|
local ITEM_TIMEOUT = 5 -- Sekunden ohne Antwort = nicht ladbar
|
|
local ITEM_MAX_OUT = 50 -- so viele Anfragen gleichzeitig offen
|
|
Util.ITEM_TIMEOUT = ITEM_TIMEOUT
|
|
|
|
local PumpItems
|
|
|
|
local function ItemFinish(itemID, info)
|
|
local list = pendingItems[itemID]
|
|
if itemSent[itemID] then
|
|
itemSent[itemID] = nil
|
|
itemOut = math.max(0, itemOut - 1)
|
|
end
|
|
pendingItems[itemID] = nil
|
|
if list then
|
|
for _, callback in ipairs(list) do
|
|
FW.SafeCall(callback, info)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Zweiter Versuch fuer alles, was keine Antwort bekam: erst wenn die Warteschlange leer ist.
|
|
local function RetryFailed()
|
|
if itemQueueHead <= #itemQueue or itemOut > 0 then
|
|
return
|
|
end
|
|
local todo = {}
|
|
for _, id in ipairs(itemFailedList) do
|
|
if itemFailed[id] and not itemRetried[id] then
|
|
itemRetried[id] = true
|
|
todo[#todo + 1] = id
|
|
end
|
|
end
|
|
itemFailedList = {}
|
|
for _, id in ipairs(todo) do
|
|
itemFailed[id] = nil
|
|
Util.RequestItem(id, function(info)
|
|
if info and FW.Fire then FW:Fire("CODEX_ITEMS_LOADED") end
|
|
end)
|
|
end
|
|
end
|
|
|
|
function PumpItems()
|
|
while itemOut < ITEM_MAX_OUT and itemQueueHead <= #itemQueue do
|
|
local id = itemQueue[itemQueueHead]
|
|
itemQueueHead = itemQueueHead + 1
|
|
if pendingItems[id] and not itemSent[id] then
|
|
if Util.GetItem(id) then
|
|
ItemFinish(id, Util.GetItem(id))
|
|
else
|
|
itemSent[id] = true
|
|
itemOut = itemOut + 1
|
|
if C_Item and C_Item.RequestLoadItemDataByID then
|
|
C_Item.RequestLoadItemDataByID(id)
|
|
end
|
|
if C_Timer and C_Timer.After then
|
|
C_Timer.After(ITEM_TIMEOUT, function()
|
|
if itemSent[id] then
|
|
if not itemFailed[id] then
|
|
itemFailed[id] = true
|
|
itemFailedList[#itemFailedList + 1] = id
|
|
end
|
|
ItemFinish(id, nil)
|
|
PumpItems()
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if itemQueueHead > #itemQueue then
|
|
itemQueue = {}
|
|
itemQueueHead = 1
|
|
if itemOut == 0 and #itemFailedList > 0 and C_Timer and C_Timer.After then
|
|
C_Timer.After(1, RetryFailed)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Ruft callback(info) auf, sobald die Item-Daten da sind (sofort, falls schon im Cache).
|
|
-- info ist nil, wenn das Item nicht geladen werden konnte (nach ITEM_TIMEOUT Sekunden ohne Antwort).
|
|
-- Anfragen laufen gestaffelt: schnelle Antworten zuerst, Ausfaelle werden zum Schluss einmal wiederholt.
|
|
function Util.RequestItem(itemID, callback)
|
|
local info = Util.GetItem(itemID)
|
|
if info then
|
|
FW.SafeCall(callback, info)
|
|
return
|
|
end
|
|
local list = pendingItems[itemID]
|
|
if not list then
|
|
list = {}
|
|
pendingItems[itemID] = list
|
|
itemQueue[#itemQueue + 1] = itemID
|
|
end
|
|
list[#list + 1] = callback
|
|
PumpItems()
|
|
end
|
|
|
|
FW:RegisterEvent("GET_ITEM_INFO_RECEIVED", function(itemID, success)
|
|
if not pendingItems[itemID] then
|
|
return
|
|
end
|
|
itemFailed[itemID] = nil
|
|
ItemFinish(itemID, success and Util.GetItem(itemID) or nil)
|
|
PumpItems()
|
|
end)
|
|
|
|
------------------------------------------------------------------------
|
|
-- Kreaturen und Orte
|
|
------------------------------------------------------------------------
|
|
|
|
-- GUID "Creature-0-4621-1-27730-4107-0000477262" -> 4107 (sechstes Feld)
|
|
-- Octo/1.12: GUID als Hex "0xF130003A5C0012AB" = Typ (F13 Kreatur, F14 Begleiter,
|
|
-- F11 Objekt, F15 Fahrzeug), Eintrags-ID (6 Hex), Zaehler (6 Hex).
|
|
-- Rueckgabe: "Creature" / "Pet" / "GameObject" / "Vehicle", ID; sonst nil.
|
|
local HEX_KIND = { F13 = "Creature", F14 = "Pet", F11 = "GameObject", F15 = "Vehicle" }
|
|
function Util.ParseHexGUID(guid)
|
|
if type(guid) ~= "string" then
|
|
return nil
|
|
end
|
|
local hex = guid:match("^0[xX](%x+)$")
|
|
if not hex or #hex ~= 16 then
|
|
return nil
|
|
end
|
|
local kind = HEX_KIND[hex:sub(1, 3):upper()]
|
|
if not kind then
|
|
return nil
|
|
end
|
|
return kind, tonumber(hex:sub(5, 10), 16)
|
|
end
|
|
|
|
function Util.GetNpcID(guid)
|
|
if type(guid) ~= "string" then
|
|
return nil
|
|
end
|
|
local hk, hid = Util.ParseHexGUID(guid)
|
|
if hk then
|
|
if hk == "Creature" or hk == "Vehicle" then
|
|
return hid
|
|
end
|
|
return nil
|
|
end
|
|
local kind, _, _, _, _, id = strsplit("-", guid)
|
|
if kind == "Creature" or kind == "Vehicle" then
|
|
return tonumber(id)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Aktuelle Position des Spielers. x und y sind 0..1 auf der Karte, koennen
|
|
-- in Instanzen fehlen. Die Koordinaten sind die des Spielers, nicht des Ziels.
|
|
function Util.GetPlayerLocation()
|
|
local mapID = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
|
|
local x, y
|
|
if mapID then
|
|
local pos = C_Map.GetPlayerMapPosition(mapID, "player")
|
|
if pos then
|
|
x, y = pos:GetXY()
|
|
end
|
|
end
|
|
return {
|
|
mapID = mapID,
|
|
x = x and Util.Round(x, 4) or nil,
|
|
y = y and Util.Round(y, 4) or nil,
|
|
zone = GetZoneText(),
|
|
subzone = GetSubZoneText(),
|
|
t = time(),
|
|
}
|
|
end
|
|
|
|
function Util.GetPlayerKey()
|
|
local name = UnitName("player")
|
|
local realm = GetRealmName() or ""
|
|
return name .. "-" .. (realm:gsub("%s", ""))
|
|
end
|