Dateien nach "Core" hochladen
This commit is contained in:
+1324
File diff suppressed because it is too large
Load Diff
+273
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user