-- 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[] 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)