-- 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