Dateien nach "Core" hochladen

This commit is contained in:
2026-10-27 14:05:11 +00:00
parent 25b2abcbef
commit 5bdd2904f8
5 changed files with 2525 additions and 0 deletions
+1324
View File
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
-- Freewood-OctoWoW-Core: Export und Import als kurzer Code
--
-- Format: FW1:<art>:<pruefsumme>:<koerper>
-- art kleingeschriebene Datenart, z. B. "pets" oder "beasts"
-- pruefsumme 8 Hex-Zeichen ueber den Koerper (erkennt Tippfehler und abgeschnittene Codes)
-- koerper Base64( Komprimiert( CBOR( { v, k, t, a, d } ) ) )
-- Das ist Komprimierung und Kodierung, keine Verschluesselung.
--
-- Sicherheit beim Import:
-- * Es wird nie Code ausgefuehrt, nur Daten gelesen (CBOR).
-- * Groessenlimit, Tiefenlimit, Knotenlimit, nur Text/Zahl/Wahrheitswert/Tabelle.
-- * Das zustaendige Modul prueft die Daten zusaetzlich (validate) und fuegt sie
-- selbst zusammen (import). Regel fuer Module: importierte Eintraege mit
-- src = "import" markieren und eigene Daten nie ueberschreiben.
--
-- Ein Modul meldet sich so an:
-- Freewood.Export.RegisterHandler("pets", {
-- label = "Jaeger-Begleiter" (Text oder Funktion),
-- export = function() return <Tabelle> end,
-- validate = function(data, payload) return true end, -- optional
-- import = function(data, payload) return "3 neu, 1 ergaenzt" end,
-- })
local ADDON, FW = ...
local Export = {}
FW.Export = Export
local Util = FW.Util
local FORMAT_VERSION = 1
local MAX_ENCODED = 150000
local HARD_MAX = 400000 -- groesstes erlaubtes Limit eines Handlers (handler.maxEncoded)
local LIMITS = {
maxDepth = 12,
maxNodes = 200000,
maxString = 4096,
}
Export.handlers = {}
Export.handlerList = {}
function Export.RegisterHandler(kind, handler)
assert(type(kind) == "string" and kind:match("^[a-z0-9]+$"),
"Freewood.Export: Datenart muss aus a-z und 0-9 bestehen")
assert(type(handler) == "table", "Freewood.Export: Handler fehlt")
handler.kind = kind
if not Export.handlers[kind] then
Export.handlerList[#Export.handlerList + 1] = handler
else
for i, h in ipairs(Export.handlerList) do
if h.kind == kind then
Export.handlerList[i] = handler
end
end
end
Export.handlers[kind] = handler
end
function Export.GetLabel(handler)
local label = handler.label
if type(label) == "function" then
label = label()
end
return label or handler.kind
end
local function GetEncoder()
local E = C_EncodingUtil
if E and E.SerializeCBOR and E.DeserializeCBOR and E.CompressString
and E.DecompressString and E.EncodeBase64 and E.DecodeBase64 then
return E
end
return nil
end
-- Prueft eine entpackte Datenstruktur auf harmlose Form.
function Export.CheckStructure(data)
if type(data) ~= "table" then
return false
end
local nodes = 0
local seen = {}
local stack = { { data, 1 } }
while #stack > 0 do
local entry = table.remove(stack)
local tbl, depth = entry[1], entry[2]
if depth > LIMITS.maxDepth or seen[tbl] then
return false
end
seen[tbl] = true
for k, v in pairs(tbl) do
nodes = nodes + 1
if nodes > LIMITS.maxNodes then
return false
end
local kt = type(k)
if kt ~= "string" and kt ~= "number" then
return false
end
if kt == "string" and #k > LIMITS.maxString then
return false
end
local vt = type(v)
if vt == "string" then
if #v > LIMITS.maxString then
return false
end
elseif vt == "number" then
if v ~= v or v == math.huge or v == -math.huge then
return false
end
elseif vt == "table" then
stack[#stack + 1] = { v, depth + 1 }
elseif vt ~= "boolean" then
return false
end
end
end
return true
end
-- Erzeugt den Code. Rueckgabe: text oder nil, fehlerSchluessel
function Export.Encode(kind, data)
local E = GetEncoder()
if not E then
return nil, "ERR_NOAPI"
end
if type(kind) ~= "string" or not kind:match("^[a-z0-9]+$") or type(data) ~= "table" then
return nil, "ERR_INVALID"
end
local payload = { v = FORMAT_VERSION, k = kind, t = time(), a = FW.version, d = data }
local ok, cbor = pcall(E.SerializeCBOR, payload)
if not ok or type(cbor) ~= "string" then
return nil, "ERR_INVALID"
end
local ok2, packed = pcall(E.CompressString, cbor)
if not ok2 or type(packed) ~= "string" then
return nil, "ERR_INVALID"
end
local ok3, body = pcall(E.EncodeBase64, packed)
if not ok3 or type(body) ~= "string" then
return nil, "ERR_INVALID"
end
local hLimit = Export.handlers[kind] and tonumber(Export.handlers[kind].maxEncoded) or MAX_ENCODED
if #body > math.min(hLimit, HARD_MAX) then
return nil, "ERR_TOO_LARGE"
end
return string.format("FW%d:%s:%s:%s", FORMAT_VERSION, kind, Util.Hash(body), body)
end
-- Liest einen Code. Rueckgabe: payload oder nil, fehlerSchluessel
function Export.Decode(text)
if type(text) ~= "string" then
return nil, "ERR_FORMAT"
end
text = text:gsub("%s+", "")
if text == "" then
return nil, "ERR_FORMAT"
end
if #text > HARD_MAX + 64 then
return nil, "ERR_TOO_LARGE"
end
local version = text:match("^FW(%d+):")
if not version then
return nil, "ERR_FORMAT"
end
if tonumber(version) > FORMAT_VERSION then
return nil, "ERR_VERSION"
end
local kind, crc, body = text:match("^FW%d+:([a-z0-9]+):(%x%x%x%x%x%x%x%x):(.+)$")
if not kind then
return nil, "ERR_FORMAT"
end
if body:find("[^%w%+/=_%-]") then
return nil, "ERR_FORMAT"
end
local hl = Export.handlers[kind] and tonumber(Export.handlers[kind].maxEncoded) or MAX_ENCODED
if #body > math.min(hl, HARD_MAX) then
return nil, "ERR_TOO_LARGE"
end
if Util.Hash(body) ~= crc:lower() then
return nil, "ERR_CHECKSUM"
end
local E = GetEncoder()
if not E then
return nil, "ERR_NOAPI"
end
local ok, packed = pcall(E.DecodeBase64, body)
if not ok or type(packed) ~= "string" then
return nil, "ERR_DECODE"
end
local ok2, cbor = pcall(E.DecompressString, packed)
if not ok2 or type(cbor) ~= "string" then
return nil, "ERR_DECODE"
end
local ok3, payload = pcall(E.DeserializeCBOR, cbor)
if not ok3 or type(payload) ~= "table" then
return nil, "ERR_DECODE"
end
if payload.v ~= FORMAT_VERSION then
if type(payload.v) == "number" and payload.v > FORMAT_VERSION then
return nil, "ERR_VERSION"
end
return nil, "ERR_INVALID"
end
if payload.k ~= kind or type(payload.d) ~= "table" then
return nil, "ERR_INVALID"
end
if not Export.CheckStructure(payload.d) then
return nil, "ERR_INVALID"
end
return payload
end
-- Exportiert die Daten eines Moduls. Rueckgabe: text oder nil, fehlerSchluessel, extra
function Export.ExportKind(kind)
local handler = Export.handlers[kind]
if not handler or not handler.export then
return nil, "ERR_NO_HANDLER", kind
end
local ok, data = pcall(handler.export)
if not ok or type(data) ~= "table" then
return nil, "ERR_INVALID"
end
if next(data) == nil then
return nil, "EXCHANGE_NOTHING_EXPORT"
end
-- encodeAs: ein Handler darf unter der Art eines anderen Handlers exportieren (dann importiert dieser)
return Export.Encode(handler.encodeAs or kind, data)
end
-- Importiert einen Code ueber das zustaendige Modul.
-- Rueckgabe: true, statistik, art oder false, fehlerSchluessel, extra
function Export.Import(text)
local payload, err = Export.Decode(text)
if not payload then
return false, err
end
local handler = Export.handlers[payload.k]
if not handler or not handler.import then
return false, "ERR_NO_HANDLER", payload.k
end
if handler.validate then
local ok, valid, message = pcall(handler.validate, payload.d, payload)
if not ok or not valid then
return false, "ERR_INVALID", type(message) == "string" and message or nil
end
end
local ok, result = pcall(handler.import, payload.d, payload)
if not ok then
FW:Debug("Import fehlgeschlagen:", result)
return false, "ERR_INVALID"
end
return true, result, payload.k
end
+493
View File
@@ -0,0 +1,493 @@
-- Freewood-OctoWoW-Core: Grundgeruest
-- Namensraum, Ereignissystem, Modulverwaltung, Datenspeicher.
-- Andere Addons der Familie greifen ueber die globale Variable "Freewood" darauf zu.
local ADDON, FW = ...
_G.Freewood = FW
FW.ADDON = ADDON
-- OctoWoW-Fassung (Port von Freewood 0.56.0, das fuer "Forever" geschrieben wurde).
-- testBypass: NUR fuer die Testfassungen. Umgeht die Berechtigungsliste (Dev-Sperre), damit wir alles pruefen koennen.
-- Zum Abschalten: testBypass = false (eine Stelle, gilt fuer alle Module).
FW.OCTO = { name = "OctoWoW", based = "Freewood 0.56.0", testBypass = false }
-- Fehler, die schon beim Laden der Dateien auftreten (bevor Freewood-OctoWoW-Dev da ist), werden hier gesammelt.
-- Freewood-OctoWoW-Dev holt sie spaeter ins Log (Dev.Probe).
FW.earlyErrors = {}
do
local getH, setH = _G.geterrorhandler, _G.seterrorhandler
if getH and setH then
local old = getH()
FW.earlyHandler = function(err)
local s = tostring(err)
if s:find("Freewood", 1, true) then
local list = FW.earlyErrors
if #list < 200 then
list[#list + 1] = { t = _G.GetTime and _G.GetTime() or 0, m = s }
end
end
if old then
return old(err)
end
end
setH(FW.earlyHandler)
end
end
FW.API_VERSION = 1 -- wird erhoeht, wenn sich die Modul-Schnittstelle inkompatibel aendert
FW.DB_VERSION = 1 -- Version des gespeicherten Datenformats
local function GetMeta(field)
local fn = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata
if fn then
local ok, value = pcall(fn, ADDON, field)
if ok then
return value
end
end
return nil
end
FW.version = GetMeta("Version") or "dev"
------------------------------------------------------------------------
-- Ausgabe und Fehlerbehandlung
------------------------------------------------------------------------
local PREFIX = "|cffc89b3cFreewood|r"
local function Join(...)
local parts = {}
for i = 1, select("#", ...) do
parts[i] = tostring((select(i, ...)))
end
return table.concat(parts, " ")
end
function FW:Print(...)
print(PREFIX .. ": " .. Join(...))
end
function FW:Debug(...)
if self.db and self.db.settings and self.db.settings.debug then
print(PREFIX .. " |cff8c8a84[debug]|r " .. Join(...))
end
end
-- Ruft fn geschuetzt auf. Ein Fehler wird an den Standard-Fehlerhandler
-- weitergereicht und bricht den Aufrufer nicht ab.
local function SafeCall(fn, ...)
local ok, err = pcall(fn, ...)
if not ok then
local handler = geterrorhandler and geterrorhandler()
if handler then
handler(err)
else
print(err)
end
end
return ok
end
FW.SafeCall = SafeCall
------------------------------------------------------------------------
-- Interne Nachrichten (Ereignissystem des Cores)
------------------------------------------------------------------------
local callbacks = {}
function FW:On(event, fn, owner)
local list = callbacks[event]
if not list then
list = {}
callbacks[event] = list
end
list[#list + 1] = { fn = fn, owner = owner }
end
function FW:Off(event, owner)
local list = callbacks[event]
if not list then
return
end
for i = #list, 1, -1 do
if list[i].owner == owner then
table.remove(list, i)
end
end
end
function FW:Fire(event, ...)
local list = callbacks[event]
if not list then
return
end
local snapshot = {}
for i = 1, #list do
snapshot[i] = list[i]
end
for i = 1, #snapshot do
local cb = snapshot[i]
if cb.owner then
SafeCall(cb.fn, cb.owner, ...)
else
SafeCall(cb.fn, ...)
end
end
end
------------------------------------------------------------------------
-- Spielereignisse (ein gemeinsamer Frame fuer alle Module)
------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame")
local gameHandlers = {}
-- OctoWoW (1.12): Ereignis -> echte Ereignisse, die es dort gibt
local EVENT_ALIAS = {
GROUP_ROSTER_UPDATE = { "PARTY_MEMBERS_CHANGED", "RAID_ROSTER_UPDATE" },
PET_STATS_UPDATE = { "UNIT_HAPPINESS", "UNIT_PET_EXPERIENCE" },
}
local aliasTarget = {}
function FW:RegisterEvent(event, fn, owner)
local list = gameHandlers[event]
if not list then
local ok = pcall(eventFrame.RegisterEvent, eventFrame, event)
if not ok then
self:Debug("Ereignis nicht verfuegbar:", event)
return false
end
list = {}
gameHandlers[event] = list
-- OctoWoW: moderne Ereignisse, die der 1.12-Client nicht kennt, ueber die echten Ereignisse abbilden
local alias = EVENT_ALIAS[event]
if alias then
for ai = 1, #alias do
local real = alias[ai]
if pcall(eventFrame.RegisterEvent, eventFrame, real) then
aliasTarget[real] = event
end
end
end
end
list[#list + 1] = { fn = fn, owner = owner }
return true
end
function FW:UnregisterEvent(event, owner)
local list = gameHandlers[event]
if not list then
return
end
-- OctoWoW: while-Schleife statt "for i = #list, 1, -1" (die Rueckwaerts-Schleife lieferte hier nil-Eintraege)
local i = #list
while i >= 1 do
local h = list[i]
if h and h.owner == owner then
table.remove(list, i)
end
i = i - 1
end
if #list == 0 then
gameHandlers[event] = nil
eventFrame:UnregisterEvent(event)
local alias = EVENT_ALIAS[event]
if alias then
for ai = 1, #alias do
if aliasTarget[alias[ai]] == event then
aliasTarget[alias[ai]] = nil
eventFrame:UnregisterEvent(alias[ai])
end
end
end
end
end
local curEvent
local function Dispatch(list, ...)
local snapshot = {}
for i = 1, #list do
snapshot[i] = list[i]
end
-- Zeitmesser (Dev-Reiter): FW.HandlerTimer(event, ms, owner), nur wenn gesetzt
local hook, prof = FW.HandlerTimer, _G.debugprofilestop
for i = 1, #snapshot do
local h = snapshot[i]
local t0 = hook and prof and prof()
if h.owner then
SafeCall(h.fn, h.owner, ...)
else
SafeCall(h.fn, ...)
end
if t0 then hook(curEvent, prof() - t0, h.owner) end
end
end
-- Von der Kanal-Schicht (Compat) genutzt: ein Ereignis wie vom Spiel an die angemeldeten Module geben
function FW:InjectEvent(event, ...)
local list = gameHandlers[event]
if list then
curEvent = event
Dispatch(list, ...)
end
end
eventFrame:SetScript("OnEvent", function(_, event, ...)
local list = gameHandlers[event]
if list then
curEvent = event
Dispatch(list, ...)
end
local target = aliasTarget[event]
if target and gameHandlers[target] then
curEvent = event
Dispatch(gameHandlers[target], ...)
end
end)
------------------------------------------------------------------------
-- Datenspeicher
-- FreewoodDB : gilt fuer den ganzen Account
-- FreewoodCharDB : gilt nur fuer den aktuellen Charakter
-- Module legen ihre Daten unter db.modules[<Modulname>] ab (siehe NewModule).
------------------------------------------------------------------------
FW.defaults = {
global = {
dbVersion = FW.DB_VERSION,
installed = 0,
settings = {
minimap = { hide = false, angle = 215 },
scale = 1,
locale = "auto",
debug = false,
windowPos = false,
windowSize = false,
},
disabledModules = {},
modules = {},
},
char = {
dbVersion = FW.DB_VERSION,
modules = {},
},
}
-- Migrationen: FW.migrations[n] hebt Daten von Version n-1 auf n.
FW.migrations = {}
local function Migrate(db, kind)
local from = tonumber(db.dbVersion) or 0
for version = from + 1, FW.DB_VERSION do
local migration = FW.migrations[version]
if migration then
SafeCall(migration, db, kind)
end
end
db.dbVersion = FW.DB_VERSION
end
function FW:InitDB()
if type(FreewoodDB) ~= "table" then
FreewoodDB = {}
end
if type(FreewoodCharDB) ~= "table" then
FreewoodCharDB = {}
end
self.dbNewer = false
local sets = {
{ db = FreewoodDB, defaults = self.defaults.global, kind = "global" },
{ db = FreewoodCharDB, defaults = self.defaults.char, kind = "char" },
}
for _, set in ipairs(sets) do
local db = set.db
local fresh = next(db) == nil
local stored = tonumber(db.dbVersion)
if fresh then
db.dbVersion = self.DB_VERSION
elseif stored and stored > self.DB_VERSION then
-- Daten einer neueren Version: nichts migrieren, nichts abwerten.
self.dbNewer = true
else
Migrate(db, set.kind)
end
self.Util.MergeDefaults(db, set.defaults)
end
if FreewoodDB.installed == 0 then
FreewoodDB.installed = time()
end
self.db = FreewoodDB
self.settings = FreewoodDB.settings
self.chardb = FreewoodCharDB
end
function FW:ApplyLocale()
local setting = self.settings and self.settings.locale or "auto"
local code
if setting == "deDE" or setting == "enUS" then
code = setting
else
code = (GetLocale() == "deDE") and "deDE" or "enUS"
end
self:SetLocale(code)
end
------------------------------------------------------------------------
-- Module
-- Ablauf eines Moduls: NewModule -> OnInitialize (einmal, Daten bereit)
-- -> OnEnable (nach PLAYER_LOGIN) -> optional OnDisable
------------------------------------------------------------------------
FW.modules = {}
FW.moduleList = {}
function FW:RequireAPI(minVersion, who)
if (self.API_VERSION or 0) >= minVersion then
return true
end
self:Print(string.format(self.L.API_TOO_OLD, tostring(who or "?"), minVersion, self.API_VERSION))
return false
end
function FW:NewModule(name, opts)
assert(type(name) == "string" and name ~= "", "Freewood:NewModule - Name fehlt")
if self.modules[name] then
error("Freewood: Modul '" .. name .. "' ist bereits registriert", 2)
end
local mod = opts or {}
mod.name = name
mod.state = "registered"
mod.core = self
function mod:IsEnabled()
return self.state == "enabled"
end
self.modules[name] = mod
self.moduleList[#self.moduleList + 1] = mod
if self.ready then
self:InitModule(mod)
if self.loggedIn then
self:EnableModule(mod)
end
end
self:Fire("FW_MODULE_REGISTERED", mod)
return mod
end
function FW:GetModule(name)
return self.modules[name]
end
function FW:InitModule(mod)
if mod.state ~= "registered" then
return
end
local globalData = self.db.modules
local charData = self.chardb.modules
globalData[mod.name] = globalData[mod.name] or {}
charData[mod.name] = charData[mod.name] or {}
if mod.defaults then
self.Util.MergeDefaults(globalData[mod.name], mod.defaults)
end
if mod.charDefaults then
self.Util.MergeDefaults(charData[mod.name], mod.charDefaults)
end
mod.db = globalData[mod.name]
mod.chardb = charData[mod.name]
mod.state = "initialized"
if mod.OnInitialize then
SafeCall(mod.OnInitialize, mod)
end
end
function FW:EnableModule(mod)
if mod.state ~= "initialized" and mod.state ~= "disabled" then
return
end
if self.db.disabledModules[mod.name] then
mod.state = "disabled"
return
end
mod.state = "enabled"
if mod.OnEnable then
SafeCall(mod.OnEnable, mod)
end
self:Fire("FW_MODULE_ENABLED", mod)
end
function FW:DisableModule(mod)
if mod.state ~= "enabled" then
return
end
mod.state = "disabled"
if mod.OnDisable then
SafeCall(mod.OnDisable, mod)
end
end
function FW:SetModuleEnabled(name, enabled)
local mod = self.modules[name]
self.db.disabledModules[name] = (not enabled) or nil
if mod then
if enabled then
self:EnableModule(mod)
else
self:DisableModule(mod)
end
end
end
------------------------------------------------------------------------
-- Start
------------------------------------------------------------------------
function FW:OnAddonLoaded(name)
if name ~= ADDON then
return
end
self:UnregisterEvent("ADDON_LOADED", self)
self:InitDB()
self:ApplyLocale()
self.ready = true
if self.dbNewer then
self:Print(self.L.DB_NEWER)
end
self:Fire("FW_DB_READY")
-- Module, die vor der Datenbank registriert wurden (sollte nicht vorkommen)
for _, mod in ipairs(self.moduleList) do
self:InitModule(mod)
end
end
function FW:OnLogin()
self.loggedIn = true
self:Fire("FW_LOGIN")
for _, mod in ipairs(self.moduleList) do
self:InitModule(mod)
self:EnableModule(mod)
end
end
FW:RegisterEvent("ADDON_LOADED", FW.OnAddonLoaded, FW)
FW:RegisterEvent("PLAYER_LOGIN", FW.OnLogin, FW)
+51
View File
@@ -0,0 +1,51 @@
-- Freewood-OctoWoW-Core: Slash-Befehle (/freewood und /fw)
local ADDON, FW = ...
SLASH_FREEWOOD1 = "/freewood"
SLASH_FREEWOOD2 = "/fw"
local function PrintHelp()
local L = FW.L
FW:Print(L.SLASH_HELP_TITLE)
for i = 1, 7 do
print(" " .. L["SLASH_HELP_" .. i])
end
end
local function Handle(msg)
local L = FW.L
msg = strtrim(msg or ""):lower()
local cmd = msg:match("^(%S*)")
if cmd == "" then
FW.UI:Toggle()
elseif cmd == "settings" or cmd == "config" or cmd == "options" then
FW.UI:Show("settings")
elseif cmd == "exchange" or cmd == "export" or cmd == "import" then
FW.UI:Show("exchange")
elseif cmd == "minimap" then
FW.Minimap:SetHidden(not FW.settings.minimap.hide)
elseif cmd == "modules" then
if #FW.moduleList == 0 then
FW:Print(L.SLASH_NO_MODULES)
else
for _, mod in ipairs(FW.moduleList) do
FW:Print(mod.name, mod.version and ("v" .. mod.version) or "", "(" .. mod.state .. ")")
end
end
elseif cmd == "debug" then
FW.settings.debug = not FW.settings.debug
FW:Print(FW.settings.debug and L.SLASH_DEBUG_ON or L.SLASH_DEBUG_OFF)
elseif cmd == "reset" then
FW.UI:ResetPosition()
FW:Print(L.SLASH_RESET_DONE)
elseif cmd == "version" then
local _, _, _, toc = GetBuildInfo()
FW:Print(string.format(L.SLASH_VERSION, FW.version, FW.API_VERSION, tostring(toc or "?")))
else
PrintHelp()
end
end
SlashCmdList["FREEWOOD"] = Handle
+384
View File
@@ -0,0 +1,384 @@
-- 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