From 5bdd2904f88e46797da3670050b4b78a80bc728d Mon Sep 17 00:00:00 2001 From: Freewood <948+freewood@noreply.octowow.st> Date: Tue, 27 Oct 2026 14:05:11 +0000 Subject: [PATCH] Dateien nach "Core" hochladen --- Core/Compat.lua | 1324 +++++++++++++++++++++++++++++++++++++++++++++++ Core/Export.lua | 273 ++++++++++ Core/Init.lua | 493 ++++++++++++++++++ Core/Slash.lua | 51 ++ Core/Util.lua | 384 ++++++++++++++ 5 files changed, 2525 insertions(+) create mode 100644 Core/Compat.lua create mode 100644 Core/Export.lua create mode 100644 Core/Init.lua create mode 100644 Core/Slash.lua create mode 100644 Core/Util.lua diff --git a/Core/Compat.lua b/Core/Compat.lua new file mode 100644 index 0000000..650f0a9 --- /dev/null +++ b/Core/Compat.lua @@ -0,0 +1,1324 @@ +-- Freewood Compat (OctoWoW) +-- Ersatz fuer Funktionen, die im Forever-Client da sind, im OctoWoW-Client (1.12.1 + ClassicAPI) aber fehlen. +-- Gebaut aus dem Pruefergebnis von /fwprobe. Es wird nur ergaenzt, was fehlt - vorhandene Funktionen bleiben unberuehrt. +-- Wird als erste Datei des Core geladen; alles hier ist ohne FW lauffaehig. + +local G = _G + +local Compat = {} +G.FreewoodCompat = Compat +Compat.applied = {} +Compat.failed = {} + +local function Mark(name) + Compat.applied[#Compat.applied + 1] = name +end + +local function Try(name, fn) + local ok, err = pcall(fn) + if not ok then + Compat.failed[#Compat.failed + 1] = name .. ": " .. tostring(err) + end +end + +local function Table(name) + local t = G[name] + if type(t) ~= "table" then + t = {} + G[name] = t + end + return t +end + +-- Setzt G[name], wenn es fehlt +local function Global(name, value) + if G[name] == nil then + G[name] = value + Mark(name) + end +end + +-- Setzt t[key], wenn es fehlt +local function Field(t, label, key, value) + if t[key] == nil then + t[key] = value + Mark(label .. "." .. key) + end +end + +------------------------------------------------------------------------ +-- Textumbruch von Schriftzuegen: FontString:SetWordWrap +-- 1.12 bricht immer um, sobald eine Breite da ist. "Kein Umbruch" wird ueber eine Hoehe von einer Zeile nachgebildet. +------------------------------------------------------------------------ + +local function WordWrapImpl(self, wrap) + if wrap then + if self.fwSingleLine then + self.fwSingleLine = nil + self:SetHeight(0) + end + else + local _, size = self:GetFont() + if size and size > 0 and not self.fwSingleLine then + self.fwSingleLine = true + self:SetHeight(size + 2) + end + end +end + +local function GetWordWrapImpl(self) + return not self.fwSingleLine +end + +Compat.diag = {} + +Try("SetWordWrap", function() + local frame = CreateFrame("Frame") + local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + local meta = getmetatable(fs) + local diag = Compat.diag + diag.fsType = type(fs) + diag.fsMeta = type(meta) + diag.fsIndex = type(meta and meta.__index) + local methods = type(meta) == "table" and meta.__index or nil + if type(methods) == "table" and methods.SetWordWrap == nil then + pcall(function() + methods.SetWordWrap = WordWrapImpl + methods.GetWordWrap = GetWordWrapImpl + end) + end + if fs.SetWordWrap ~= nil then + diag.wrap = "metatable" + Mark("FontString:SetWordWrap (Metatabelle)") + return + end + + -- Zweiter Weg: die Huelle um CreateFrame (weiter unten) gibt jedem neuen FontString die Methoden direkt + diag.wrap = "CreateFrame-Huelle" + Compat.needFontStringPatch = true +end) + +------------------------------------------------------------------------ +-- Huelle um CreateFrame: +-- 1. neue FontStrings bekommen SetWordWrap/GetWordWrap, falls die Metatabelle nicht erweiterbar war +-- 2. Script-Handler bekommen self und ihre Argumente auch dann, wenn der Client sie nicht uebergibt +-- (1.12 setzt sonst nur die Globals this, event, arg1 ... arg9) +------------------------------------------------------------------------ + +local ARGS = { + OnSizeChanged = function() return G.arg1, G.arg2 end, + OnUpdate = function() return G.arg1 end, + OnClick = function() return G.arg1, G.arg2 end, + OnMouseDown = function() return G.arg1 end, + OnMouseUp = function() return G.arg1 end, + OnMouseWheel = function() return G.arg1 end, + OnTextChanged = function() return G.arg1 end, + OnValueChanged = function() return G.arg1 end, + OnEvent = function() + return G.event, G.arg1, G.arg2, G.arg3, G.arg4, G.arg5, G.arg6, G.arg7, G.arg8, G.arg9 + end, + OnDragStart = function() return G.arg1 end, + OnReceiveDrag = function() return G.arg1 end, + OnChar = function() return G.arg1 end, + OnKeyDown = function() return G.arg1 end, + OnKeyUp = function() return G.arg1 end, + OnHyperlinkClick = function() return G.arg1, G.arg2, G.arg3 end, + OnHyperlinkEnter = function() return G.arg1, G.arg2 end, + OnHyperlinkLeave = function() return G.arg1, G.arg2 end, +} + +local function Adapt(event, fn) + local getArgs = ARGS[event] + return function(self, ...) + if self == nil then + self = G.this + if getArgs then + return fn(self, getArgs()) + end + return fn(self) + end + if getArgs and select("#", ...) == 0 then + return fn(self, getArgs()) + end + return fn(self, ...) + end +end + +Try("CreateFrame-Huelle", function() + local origCreateFrame = G.CreateFrame + local function PatchFS(obj) + if Compat.needFontStringPatch and type(obj) == "table" and obj.SetWordWrap == nil then + obj.SetWordWrap = WordWrapImpl + obj.GetWordWrap = GetWordWrapImpl + end + return obj + end + local function Wrap(f) + if type(f) ~= "table" or f.fwWrapped then + return f + end + f.fwWrapped = true + if f.CreateFontString then + local origFS = f.CreateFontString + f.CreateFontString = function(self, ...) + return PatchFS(origFS(self, ...)) + end + end + if f.SetScript then + local origSet = f.SetScript + f.SetScript = function(self, name, fn) + if type(fn) == "function" then + fn = Adapt(name, fn) + end + return origSet(self, name, fn) + end + end + if f.HookScript then + local origHook = f.HookScript + f.HookScript = function(self, name, fn) + if type(fn) == "function" then + fn = Adapt(name, fn) + end + return origHook(self, name, fn) + end + end + return f + end + G.CreateFrame = function(...) + return Wrap(origCreateFrame(...)) + end + Wrap(G.UIParent) + Compat.diag.hull = "aktiv" + Mark("CreateFrame-Huelle (Script-Argumente, FontString)") +end) + +------------------------------------------------------------------------ +-- Gruppe: GetNumGroupMembers, IsInGroup, IsInRaid +------------------------------------------------------------------------ + +Global("IsInRaid", function() + return (G.GetNumRaidMembers and G.GetNumRaidMembers() or 0) > 0 +end) + +Global("IsInGroup", function() + return (G.GetNumRaidMembers and G.GetNumRaidMembers() or 0) > 0 + or (G.GetNumPartyMembers and G.GetNumPartyMembers() or 0) > 0 +end) + +Global("GetNumGroupMembers", function() + local raid = G.GetNumRaidMembers and G.GetNumRaidMembers() or 0 + if raid > 0 then + return raid + end + local party = G.GetNumPartyMembers and G.GetNumPartyMembers() or 0 + if party > 0 then + return party + 1 + end + return 0 +end) + +------------------------------------------------------------------------ +-- Namen: UnitFullName, GetNormalizedRealmName, Ambiguate +------------------------------------------------------------------------ + +Global("GetNormalizedRealmName", function() + local realm = G.GetRealmName and G.GetRealmName() + if type(realm) ~= "string" then + return nil + end + return (string.gsub(realm, "[%s%-']", "")) +end) + +Global("UnitFullName", function(unit) + local name = G.UnitName(unit or "player") + if not name then + return nil + end + local realm + if unit == nil or unit == "player" or G.UnitIsUnit(unit, "player") then + realm = G.GetRealmName and G.GetRealmName() or nil + end + return name, realm +end) + +Global("Ambiguate", function(name) + return name +end) + +------------------------------------------------------------------------ +-- Kleinkram, den der Code abfragt +------------------------------------------------------------------------ + +Global("issecretvalue", function() + return false +end) + +Global("UnitInVehicle", function() + return false +end) + +Global("IsFlying", function() + return false +end) + +Global("WEAPON_SPEED", "Speed") +Global("INVTYPE_THROWN", "Thrown") +Global("INVTYPE_RANGEDRIGHT", "Ranged") +Global("RETRIEVING_ITEM_INFO", "Retrieving item information") + +Global("GetContainerItemID", function(bag, slot) + local link = G.GetContainerItemLink and G.GetContainerItemLink(bag, slot) + if type(link) == "string" then + return tonumber(string.match(link, "item:(%d+)")) + end + return nil +end) + +Global("LootSlotHasItem", function(slot) + return G.LootSlotIsItem and G.LootSlotIsItem(slot) and true or false +end) + +-- 0 = leer, 1 = Gegenstand, 2 = Geld +Global("GetLootSlotType", function(slot) + if G.LootSlotIsCoin and G.LootSlotIsCoin(slot) then + return 2 + end + if G.LootSlotIsItem and G.LootSlotIsItem(slot) then + return 1 + end + return 0 +end) + +-- Beute-Quelle: 1.12 kennt GetLootSourceInfo nicht. Die Leiche ist das Ziel, das +-- Mouseover oder (wenn die Maus schon weiter ist) die zuletzt angeschaute tote Einheit. +Try("GetLootSourceInfo", function() + if G.GetLootSourceInfo ~= nil or not G.CreateFrame then return end + local last, lastT = nil, 0 + local function Dead(unit) + if not (G.UnitGUID and G.UnitExists and G.UnitExists(unit)) then return nil end + if G.UnitIsDead and G.UnitIsDead(unit) and not G.UnitIsPlayer(unit) then + local g = G.UnitGUID(unit) + if type(g) == "string" then return g end + end + return nil + end + local f = G.CreateFrame("Frame", "FreewoodCompatLootWatch") + f:RegisterEvent("UPDATE_MOUSEOVER_UNIT") + f:RegisterEvent("PLAYER_TARGET_CHANGED") + f:SetScript("OnEvent", function() + local g = Dead("mouseover") or Dead("target") + if g then + last, lastT = g, (G.GetTime and G.GetTime() or 0) + end + end) + Compat.lootSource = function() + local g = Dead("target") or Dead("mouseover") + if g then return g end + if last and (G.GetTime and G.GetTime() or 0) - lastT < 10 then return last end + return nil + end + G.GetLootSourceInfo = function(slot) + local guid = Compat.lootSource() + if not guid then return end + local qty = 1 + if G.LootSlotIsCoin and G.LootSlotIsCoin(slot) then + local _, text = G.GetLootSlotInfo(slot) + qty = 0 + if type(text) == "string" then + for n, unit in (string.gmatch or string.gfind)(text, "(%d+)%s*(%a+)") do + n = tonumber(n) or 0 + local u = string.lower(unit) + if u == string.lower(G.GOLD or "gold") then qty = qty + n * 10000 + elseif u == string.lower(G.SILVER or "silver") then qty = qty + n * 100 + else qty = qty + n end + end + end + else + local _, _, q = G.GetLootSlotInfo(slot) + qty = tonumber(q) or 1 + if qty < 1 then qty = 1 end + end + return guid, qty + end + Mark("GetLootSourceInfo") +end) + +Global("ChatEdit_InsertLink", function(text) + local box = G.ChatFrameEditBox + if text and box and box:IsVisible() then + box:Insert(text) + return true + end + return false +end) + +------------------------------------------------------------------------ +-- Questlog: C_QuestLog.GetNumQuestLogEntries / GetInfo / GetQuestObjectives / IsComplete +-- Vorhanden sind nur GetLogIndexForQuestID, GetQuestIDForLogIndex und GetNumQuestObjectives. +------------------------------------------------------------------------ + +Try("C_QuestLog", function() + local QL = Table("C_QuestLog") + + if QL.GetNumQuestLogEntries == nil and G.GetNumQuestLogEntries then + QL.GetNumQuestLogEntries = function() + return G.GetNumQuestLogEntries() + end + Mark("C_QuestLog.GetNumQuestLogEntries") + end + + if QL.GetInfo == nil and G.GetQuestLogTitle then + QL.GetInfo = function(index) + local title, level, tag, isHeader, isCollapsed, isComplete = G.GetQuestLogTitle(index) + if not title then + return nil + end + local header = isHeader and true or false + local id = 0 + if not header and QL.GetQuestIDForLogIndex then + id = QL.GetQuestIDForLogIndex(index) or 0 + end + return { + title = title, + level = level, + questLogIndex = index, + isHeader = header, + isCollapsed = isCollapsed and true or false, + isComplete = (isComplete == 1), + isFailed = (isComplete == -1), + questID = id, + suggestedGroup = tag, + } + end + Mark("C_QuestLog.GetInfo") + end + + if QL.GetQuestObjectives == nil and G.GetNumQuestLeaderBoards and G.GetQuestLogLeaderBoard then + QL.GetQuestObjectives = function(questID) + local list = {} + local index = QL.GetLogIndexForQuestID and QL.GetLogIndexForQuestID(questID) + if not index or index <= 0 then + return list + end + local count = G.GetNumQuestLeaderBoards(index) or 0 + for j = 1, count do + local text, kind, done = G.GetQuestLogLeaderBoard(j, index) + local have, need = string.match(text or "", "(%d+)%s*/%s*(%d+)") + list[j] = { + text = text, + type = kind, + finished = done and true or false, + numFulfilled = tonumber(have) or (done and 1 or 0), + numRequired = tonumber(need) or 1, + } + end + return list + end + Mark("C_QuestLog.GetQuestObjectives") + end + + if QL.IsComplete == nil and G.GetQuestLogTitle then + QL.IsComplete = function(questID) + local index = QL.GetLogIndexForQuestID and QL.GetLogIndexForQuestID(questID) + if not index or index <= 0 then + return false + end + local _, _, _, isHeader, _, isComplete = G.GetQuestLogTitle(index) + return (not isHeader) and isComplete == 1 + end + Mark("C_QuestLog.IsComplete") + end + + Global("GetQuestLogIndexByID", function(questID) + local index = QL.GetLogIndexForQuestID and QL.GetLogIndexForQuestID(questID) + return index or 0 + end) +end) + +------------------------------------------------------------------------ +-- Begleiter (Pet): C_PetInfo auf die eingebauten Funktionen abbilden +------------------------------------------------------------------------ + +Try("C_PetInfo", function() + local PI = Table("C_PetInfo") + if PI.GetPetHappiness == nil and G.GetPetHappiness then + PI.GetPetHappiness = function() + return G.GetPetHappiness() + end + Mark("C_PetInfo.GetPetHappiness") + end + if PI.GetPetFoodTypes == nil and G.GetPetFoodTypes then + PI.GetPetFoodTypes = function() + return G.GetPetFoodTypes() + end + Mark("C_PetInfo.GetPetFoodTypes") + end + if PI.GetPetTrainingPoints == nil and G.GetPetTrainingPoints then + PI.GetPetTrainingPoints = function() + return G.GetPetTrainingPoints() + end + Mark("C_PetInfo.GetPetTrainingPoints") + end +end) + +------------------------------------------------------------------------ +-- Kleine Luecken in vorhandenen C_-Tabellen +------------------------------------------------------------------------ + +Try("C_Container", function() + local C = Table("C_Container") + if G.GetContainerNumSlots then + Field(C, "C_Container", "GetContainerNumSlots", function(bag) + return G.GetContainerNumSlots(bag) + end) + end + if G.PickupContainerItem then + Field(C, "C_Container", "PickupContainerItem", function(bag, slot) + return G.PickupContainerItem(bag, slot) + end) + end +end) + +Try("C_AddOns", function() + local A = Table("C_AddOns") + if G.GetAddOnMetadata then + Field(A, "C_AddOns", "GetAddOnMetadata", function(addon, field) + return G.GetAddOnMetadata(addon, field) + end) + end +end) + +Try("C_MerchantFrame", function() + local M = Table("C_MerchantFrame") + if G.GetMerchantNumItems then + Field(M, "C_MerchantFrame", "GetNumItems", function() + return G.GetMerchantNumItems() + end) + end +end) + +------------------------------------------------------------------------ +-- Addon-Nachrichten: C_ChatInfo auf SendAddonMessage abbilden (RegisterAddonMessagePrefix gibt es in 1.12 nicht) +------------------------------------------------------------------------ + +Try("C_ChatInfo", function() + local CI = Table("C_ChatInfo") + if G.SendAddonMessage then + Field(CI, "C_ChatInfo", "SendAddonMessage", function(prefix, text, channel, target) + G.SendAddonMessage(prefix, text, channel or "PARTY", target) + return 0 + end) + end + Field(CI, "C_ChatInfo", "RegisterAddonMessagePrefix", function() + return true + end) + Field(CI, "C_ChatInfo", "IsAddonMessagePrefixRegistered", function() + return true + end) +end) + +------------------------------------------------------------------------ +-- Tooltips: C_TooltipInfo.* ueber einen versteckten Tooltip nachbauen. +-- Rueckgabe wie im Forever-Client: { lines = { { leftText, rightText }, ... } } oder nil. +-- Hinweis: "unit:"-Links kann 1.12 nicht anzeigen, GetHyperlink gibt dafuer nil zurueck. +------------------------------------------------------------------------ + +Try("C_TooltipInfo", function() + local TI = Table("C_TooltipInfo") + local TIP = "FreewoodCompatTip" + local tip + + local function Prepare() + if not tip then + tip = CreateFrame("GameTooltip", TIP, UIParent, "GameTooltipTemplate") + end + tip:SetOwner(UIParent, "ANCHOR_NONE") + tip:ClearLines() + return tip + end + + local function Collect() + local count = tip:NumLines() or 0 + if count < 1 then + return nil + end + local lines = {} + for i = 1, count do + local left = G[TIP .. "TextLeft" .. i] + local right = G[TIP .. "TextRight" .. i] + local lt = left and left:GetText() or nil + local rt = right and right:GetText() or nil + if rt == "" then + rt = nil + end + lines[i] = { type = 0, leftText = lt, rightText = rt } + end + return { lines = lines } + end + + local function Read(setter, ...) + local t = Prepare() + local ok = pcall(setter, t, ...) + if not ok then + return nil + end + return Collect() + end + + Field(TI, "C_TooltipInfo", "GetUnit", function(unit) + return Read(function(t, u) t:SetUnit(u) end, unit) + end) + + Field(TI, "C_TooltipInfo", "GetHyperlink", function(link) + if type(link) ~= "string" or string.find(link, "^unit:") then + return nil + end + return Read(function(t, l) t:SetHyperlink(l) end, link) + end) + + Field(TI, "C_TooltipInfo", "GetItemByID", function(id) + id = tonumber(id) + if not id then + return nil + end + return Read(function(t, i) t:SetHyperlink("item:" .. i .. ":0:0:0") end, id) + end) + + Field(TI, "C_TooltipInfo", "GetMerchantItem", function(index) + return Read(function(t, i) t:SetMerchantItem(i) end, index) + end) + + Field(TI, "C_TooltipInfo", "GetTrainerService", function(index) + return Read(function(t, i) t:SetTrainerService(i) end, index) + end) +end) + +-- /fwguid: zeigt die GUID des Ziels und wie Freewood sie versteht (Diagnose) +do + local G = _G + G.SLASH_FWGUID1 = "/fwguid" + G.SlashCmdList = G.SlashCmdList or {} + G.SlashCmdList["FWGUID"] = function() + local out = {} + for _, u in ipairs({ "player", "target", "pet", "mouseover" }) do + local g = G.UnitGUID and G.UnitGUID(u) + if g then + local FW = G.Freewood or G.FW + local k, id + local Util = FW and FW.Util + if Util and Util.ParseHexGUID then k, id = Util.ParseHexGUID(g) end + out[#out + 1] = u .. "=" .. tostring(g) .. " -> " .. tostring(k) .. " " .. tostring(id) + end + end + local FWx = G.Freewood + local R = FWx and FWx.Codex and FWx.Codex.Roles + local d = R and R.diag + if d then + out[#out + 1] = "Haendler: n=" .. tostring(d.count) .. " id=" .. tostring(d.id) .. " name=" .. tostring(d.name) .. + " preis=" .. tostring(d.price) .. " link=" .. tostring(d.link) + end + local msg = "Freewood GUID: " .. (#out > 0 and table.concat(out, " | ") or "keine Einheit") + if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage(msg) end + end +end + +-- /fwdisp : Diagnose fuer die Kreaturenbilder +do + local G = _G + G.SLASH_FWDISP1 = "/fwdisp" + G.SlashCmdList = G.SlashCmdList or {} + G.SlashCmdList["FWDISP"] = function(msg) + local id = tonumber(msg) or 823 + local C = G.C_CreatureInfo + local P = function(t) if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage("Freewood Bild: " .. t) end end + P("C_CreatureInfo=" .. tostring(C ~= nil) .. " Request=" .. tostring(C and C.RequestLoadCreatureByID ~= nil) .. + " Info=" .. tostring(C and C.GetCreatureInfoByID ~= nil) .. " Portrait=" .. tostring(G.SetPortraitTextureFromCreatureDisplayID ~= nil)) + local before = C and C.GetCreatureInfoByID and C.GetCreatureInfoByID(id) + P("ID " .. id .. " vorher im Cache: " .. tostring(before and before.displayID or "nein")) + local f = G.Freewood + local st = f and f.Codex and f.Codex.dispStats + if st then P("gelesen=" .. st.read .. " fehlgeschlagen=" .. st.fail .. " gezeigt=" .. st.shown) end + if C and C.RequestLoadCreatureByID then + local fr = G.CreateFrame("Frame") + fr:RegisterEvent("CREATURE_DATA_LOAD_RESULT") + fr:SetScript("OnEvent", function(_, _, a1, a2) + local info = C.GetCreatureInfoByID(id) + P("Antwort: id=" .. tostring(a1 or G.arg1) .. " ok=" .. tostring(a2 or G.arg2) .. " displayID=" .. tostring(info and info.displayID)) + fr:UnregisterAllEvents() + end) + P("Anfrage: " .. tostring(C.RequestLoadCreatureByID(id))) + end + end +end + +------------------------------------------------------------------------ +-- table.remove / table.insert: auf OctoWoW entfernte table.remove teils nichts. +-- Selbsttest beim Laden; ist etwas kaputt, ersetzen wir beide durch eigene Fassungen. +------------------------------------------------------------------------ + +Try("table-Ersatz", function() + -- Die eingebauten Fassungen versagen auf Octo in manchen Aufrufen (z.B. mit der Schleifenvariable als Position): + -- das Element bleibt stehen und wird immer wieder bearbeitet. Wir ersetzen beide immer durch eigene Fassungen. + local table = G.table + local function Len(t) + return #t + end + table.remove = function(t, pos) + local n = Len(t) + if n == 0 then return nil end + if pos == nil then pos = n end + pos = math.floor(tonumber(pos) or n) + if pos < 1 or pos > n then return nil end + local v = t[pos] + local i = pos + while i < n do + t[i] = t[i + 1] + i = i + 1 + end + t[n] = nil + return v + end + table.insert = function(t, a, b) + local n = Len(t) + if b == nil then + t[n + 1] = a + return + end + local pos = math.floor(tonumber(a) or (n + 1)) + if pos < 1 then pos = 1 end + if pos > n + 1 then pos = n + 1 end + local i = n + while i >= pos do + t[i + 1] = t[i] + i = i - 1 + end + t[pos] = b + end + Compat.tableOk = true + Mark("table.remove/insert (immer eigene Fassung)") +end) + +------------------------------------------------------------------------ +-- C_Timer.After, falls es fehlt +------------------------------------------------------------------------ + +Try("C_Timer.After", function() + local CT = Table("C_Timer") + if CT.After then return end + local frame = G.CreateFrame("Frame") + local list, count, now = {}, 0, 0 + frame:SetScript("OnUpdate", function() + local dt = G.arg1 or 0 + now = now + dt + if count == 0 then return end + local keep, kc = {}, 0 + local due, dc = {}, 0 + for i = 1, count do + local e = list[i] + if e.at <= now then + dc = dc + 1 + due[dc] = e + else + kc = kc + 1 + keep[kc] = e + end + end + list, count = keep, kc + for i = 1, dc do + pcall(due[i].fn) + end + end) + CT.After = function(delay, fn) + count = count + 1 + list[count] = { at = now + (tonumber(delay) or 0), fn = fn } + end + Mark("C_Timer.After") +end) + +------------------------------------------------------------------------ +-- C_EncodingUtil in reinem Lua (Octo hat es nicht): +-- SerializeCBOR/DeserializeCBOR eigenes, selbst beschreibendes Textformat (kein echtes CBOR) +-- CompressString/DecompressString nur ein Kopfzeichen (keine Kompression) +-- EncodeBase64/DecodeBase64 normales Base64 +-- Beide Seiten sind Freewood, daher reicht ein gemeinsames eigenes Format. +------------------------------------------------------------------------ + +Try("C_EncodingUtil", function() + local E = Table("C_EncodingUtil") + local sfind, ssub, sbyte, schar, sformat = string.find, string.sub, string.byte, string.char, string.format + local floor = math.floor + + local function Ser(v, out, st, depth) + local tv = type(v) + st.n = st.n + 1 + if st.n > 60000 or depth > 32 then error("zu gross") end + if tv == "boolean" then + st.o = st.o + 1; out[st.o] = v and "T" or "F" + elseif tv == "number" then + if v ~= v or v == math.huge or v == -math.huge then error("Zahl") end + st.o = st.o + 1; out[st.o] = "n" .. sformat("%.17g", v) .. ";" + elseif tv == "string" then + st.o = st.o + 1; out[st.o] = "s" .. string.len(v) .. ":" .. v + elseif tv == "table" then + st.o = st.o + 1; out[st.o] = "{" + for k, val in pairs(v) do + local tk, tvv = type(k), type(val) + if (tk == "string" or tk == "number") and (tvv == "string" or tvv == "number" or tvv == "boolean" or tvv == "table") then + Ser(k, out, st, depth + 1) + Ser(val, out, st, depth + 1) + end + end + st.o = st.o + 1; out[st.o] = "}" + else + error("Typ") + end + end + + local function Rd(s, st, depth) + st.n = st.n + 1 + if st.n > 60000 or depth > 32 then error("zu gross") end + local pos = st.pos + local c = ssub(s, pos, pos) + if c == "T" then st.pos = pos + 1 return true end + if c == "F" then st.pos = pos + 1 return false end + if c == "n" then + local e = sfind(s, ";", pos, true) + if not e then error("n") end + local num = tonumber(ssub(s, pos + 1, e - 1)) + if not num then error("n") end + st.pos = e + 1 + return num + end + if c == "s" then + local e = sfind(s, ":", pos, true) + if not e then error("s") end + local len = tonumber(ssub(s, pos + 1, e - 1)) + if not len or len < 0 or e + len > string.len(s) then error("s") end + st.pos = e + len + 1 + return ssub(s, e + 1, e + len) + end + if c == "{" then + local tbl = {} + st.pos = pos + 1 + while true do + local p = st.pos + local ch = ssub(s, p, p) + if ch == "}" then st.pos = p + 1 return tbl end + if ch == "" then error("Ende") end + local k = Rd(s, st, depth + 1) + if type(k) ~= "string" and type(k) ~= "number" then error("Schluessel") end + tbl[k] = Rd(s, st, depth + 1) + end + end + error("Zeichen") + end + + E.SerializeCBOR = function(value) + local out = {} + Ser(value, out, { n = 0, o = 0 }, 1) + return table.concat(out) + end + E.DeserializeCBOR = function(s) + if type(s) ~= "string" then error("kein Text") end + local st = { pos = 1, n = 0 } + local v = Rd(s, st, 1) + if st.pos ~= string.len(s) + 1 then error("Rest") end + return v + end + E.CompressString = function(s) return "0" .. s end + E.DecompressString = function(s) + if type(s) ~= "string" or ssub(s, 1, 1) ~= "0" then error("Kopf") end + return ssub(s, 2) + end + + local ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + local REV = {} + for i = 1, 64 do REV[sbyte(ALPHA, i)] = i - 1 end + + E.EncodeBase64 = function(s) + local out, oc = {}, 0 + local len = string.len(s) + local i = 1 + while i <= len do + -- einzeln lesen: string.byte liefert in diesem Lua (5.0) nur ein Zeichen + local b1, b2, b3 = sbyte(s, i), sbyte(s, i + 1), sbyte(s, i + 2) + local n = b1 * 65536 + (b2 or 0) * 256 + (b3 or 0) + local c1 = floor(n / 262144) + local r = n - c1 * 262144 + local c2 = floor(r / 4096) + r = r - c2 * 4096 + local c3 = floor(r / 64) + local c4 = r - c3 * 64 + oc = oc + 1 + out[oc] = ssub(ALPHA, c1 + 1, c1 + 1) .. ssub(ALPHA, c2 + 1, c2 + 1) + .. (b2 and ssub(ALPHA, c3 + 1, c3 + 1) or "=") .. (b3 and ssub(ALPHA, c4 + 1, c4 + 1) or "=") + i = i + 3 + end + return table.concat(out) + end + + E.DecodeBase64 = function(s) + if type(s) ~= "string" then error("kein Text") end + local len = string.len(s) + if len == 0 then return "" end + if floor(len / 4) * 4 ~= len then error("Laenge") end + local out, oc = {}, 0 + local i = 1 + while i <= len do + local a, b, c, d = sbyte(s, i), sbyte(s, i + 1), sbyte(s, i + 2), sbyte(s, i + 3) + local v1, v2 = REV[a], REV[b] + local v3, v4 = REV[c], REV[d] + if not v1 or not v2 then error("Zeichen") end + local pad3, pad4 = (c == 61), (d == 61) + if (not v3 and not pad3) or (not v4 and not pad4) or (pad3 and not pad4) then error("Zeichen") end + local n = v1 * 262144 + v2 * 4096 + (v3 or 0) * 64 + (v4 or 0) + local x1 = floor(n / 65536) + local r = n - x1 * 65536 + local x2 = floor(r / 256) + local x3 = r - x2 * 256 + oc = oc + 1 + if pad3 then + out[oc] = schar(x1) + elseif pad4 then + out[oc] = schar(x1, x2) + else + out[oc] = schar(x1, x2, x3) + end + i = i + 4 + end + return table.concat(out) + end + Mark("C_EncodingUtil (Eigenbau)") +end) + +------------------------------------------------------------------------ +-- Datenaustausch ueber den eigenen Kanal ("Freewood..."). +-- Octo kennt keine Addon-Nachrichten im Kanal; wir senden normalen Kanaltext +-- ^ +-- und geben Empfangenes wie ein CHAT_MSG_ADDON (prefix, text, "CHANNEL", sender) an Freewood weiter. +-- Maskierung: ~ -> ~0, | -> ~1, Steuerzeichen -> ~2 (verlustbehaftet, kommt im Protokoll nicht vor) +------------------------------------------------------------------------ + +Try("Kanal-Transport", function() + local Net = { sent = 0, got = 0, rejected = 0, deferred = 0, blocks = 0, minGap = 1.2, lastError = "" } + Compat.Net = Net + + local function Esc(s) + s = string.gsub(s, "~", "~0") + s = string.gsub(s, "|", "~1") + s = string.gsub(s, "[\1-\31]", "~2") + return s + end + local function Unesc(s) + return (string.gsub(s, "~(%d)", function(d) + if d == "0" then return "~" end + if d == "1" then return "|" end + return "" + end)) + end + Net.Esc, Net.Unesc = Esc, Unesc + + local CI = Table("C_ChatInfo") + local realSend = G.SendAddonMessage + CI.SendAddonMessage = function(prefix, text, dist, target) + if dist == "CHANNEL" then + local id = tonumber(target) + if not id and G.GetChannelName and type(target) == "string" then + id = G.GetChannelName(target) + end + if type(id) ~= "number" or id <= 0 then + Net.lastError = "kein Kanal" + return 1 + end + -- Der Server erlaubt nur begrenzt viele Zeilen und sperrt sonst ("You must wait N seconds before speaking again"). + -- Wir halten Abstand und senden waehrend einer Sperre nichts; 3 = gebremst, die Module versuchen es spaeter erneut. + local now = G.GetTime and G.GetTime() or 0 + if now < (Net.blockedUntil or 0) or now - (Net.lastSend or -100) < Net.minGap then + Net.deferred = Net.deferred + 1 + return 3 + end + local wire = tostring(prefix) .. "^" .. Esc(tostring(text)) + if string.len(wire) > 255 then + Net.rejected = Net.rejected + 1 + Net.lastError = "zu lang (" .. string.len(wire) .. ")" + return 2 + end + local ok, err = pcall(G.SendChatMessage, wire, "CHANNEL", nil, id) + if not ok then + Net.lastError = tostring(err) + return 1 + end + Net.sent = Net.sent + 1 + Net.lastSend = now + return 0 + end + if realSend then + local ok = pcall(realSend, prefix, text, dist or "PARTY", target) + return ok and 0 or 1 + end + return 1 + end + Mark("C_ChatInfo.SendAddonMessage (Kanal)") + + -- Sperrmeldung des Servers auswerten + local errFrame = G.CreateFrame("Frame") + errFrame:RegisterEvent("UI_ERROR_MESSAGE") + pcall(errFrame.RegisterEvent, errFrame, "CHAT_MSG_SYSTEM") + errFrame:SetScript("OnEvent", function() + local msg = G.arg1 + if type(msg) ~= "string" then return end + local a, b, secs = string.find(string.lower(msg), "must wait (%d+) seconds? before speaking") + if a then + local n = tonumber(secs) or 10 + Net.blockedUntil = (G.GetTime and G.GetTime() or 0) + n + 1 + Net.blocks = Net.blocks + 1 + Net.minGap = math.min(4, Net.minGap + 0.4) -- danach etwas langsamer senden + end + end) + + local function IsOurs(cname) + return type(cname) == "string" and string.find(cname, "Freewood", 1, true) ~= nil + end + + local frame = G.CreateFrame("Frame") + frame:RegisterEvent("CHAT_MSG_CHANNEL") + frame:SetScript("OnEvent", function() + local text, sender, cname = G.arg1, G.arg2, G.arg9 + if not IsOurs(cname) or type(text) ~= "string" or type(sender) ~= "string" then return end + local p = string.find(text, "^", 1, true) + if not p or p < 3 or p > 20 then return end + local prefix = string.sub(text, 1, p - 1) + if string.sub(prefix, 1, 2) ~= "FW" then return end + Net.got = Net.got + 1 + local FW = G.Freewood + if FW and FW.InjectEvent then + FW:InjectEvent("CHAT_MSG_ADDON", prefix, Unesc(string.sub(text, p + 1)), "CHANNEL", sender) + end + end) + + -- Datenzeilen und Beitrittsmeldungen unseres Kanals aus dem Chat heraushalten + if type(G.ChatFrame_AddMessageEventFilter) == "function" then + local function Hide(_, _, _, _, _, cn) + cn = cn or G.arg9 + if IsOurs(cn) then return true end + return false + end + pcall(G.ChatFrame_AddMessageEventFilter, "CHAT_MSG_CHANNEL", Hide) + pcall(G.ChatFrame_AddMessageEventFilter, "CHAT_MSG_CHANNEL_NOTICE", Hide) + pcall(G.ChatFrame_AddMessageEventFilter, "CHAT_MSG_CHANNEL_JOIN", Hide) + pcall(G.ChatFrame_AddMessageEventFilter, "CHAT_MSG_CHANNEL_LEAVE", Hide) + Net.filter = true + end +end) + +-- /fwchan: Zustand des Datenaustauschs +Try("fwchan", function() + G.SLASH_FWCHAN1 = "/fwchan" + G.SlashCmdList = G.SlashCmdList or {} + G.SlashCmdList["FWCHAN"] = function() + local P = function(t) if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffFWChan|r " .. t) end end + local N = Compat.Net or {} + P("table ok=" .. tostring(Compat.tableOk) .. " Filter=" .. tostring(N.filter) .. + " gesendet=" .. tostring(N.sent) .. " empfangen=" .. tostring(N.got) .. + " zu lang=" .. tostring(N.rejected) .. " gebremst=" .. tostring(N.deferred) .. " Sperren=" .. tostring(N.blocks) .. " Abstand=" .. tostring(N.minGap) .. " letzter Fehler=" .. tostring(N.lastError)) + local names = { "FreewoodCodex", "FreewoodBeasts", "FreewoodBrett" } + for i = 1, 3 do + local id = G.GetChannelName and G.GetChannelName(names[i]) + P(names[i] .. ": Kanal " .. tostring(id)) + end + local E = G.C_EncodingUtil + P("Verpackung: " .. tostring(E and E.SerializeCBOR ~= nil)) + end +end) + +------------------------------------------------------------------------ +-- /fwnpc : fragt eine Kreatur ueber ihre ID ab und zeigt ALLES, was der Client dazu liefert +-- (C_CreatureInfo.GetCreatureInfoByID, RequestLoadCreatureByID + Antwort-Ereignis, Typ- und Familien-Auskunft). +-- Die Ausgabe steht im Chat und wird in FreewoodDB.npcProbe[ID] abgelegt (SavedVariables, nach /reload lesbar). +------------------------------------------------------------------------ + +Try("fwnpc", function() + G.SLASH_FWNPC1 = "/fwnpc" + G.SlashCmdList = G.SlashCmdList or {} + + local function Dump(v, depth, seen) + local t = type(v) + if t ~= "table" then return tostring(v) end + if depth > 3 or seen[v] then return "{...}" end + seen[v] = true + local keys = {} + for k in pairs(v) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) + local out = {} + for i = 1, #keys do + out[i] = tostring(keys[i]) .. "=" .. Dump(v[keys[i]], depth + 1, seen) + end + return "{" .. table.concat(out, ", ") .. "}" + end + + G.SlashCmdList["FWNPC"] = function(msg) + local id = tonumber(msg) + local lines = {} + local function P(text) + lines[#lines + 1] = text + if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffFWNpc|r " .. text) end + end + if not id then P("Aufruf: /fwnpc zum Beispiel /fwnpc 51600") return end + local C = G.C_CreatureInfo + if not C then P("C_CreatureInfo fehlt") return end + + local function Info(label) + local ok, info = pcall(C.GetCreatureInfoByID, id) + P(label .. " GetCreatureInfoByID(" .. id .. "): " .. (ok and Dump(info, 1, {}) or ("Fehler: " .. tostring(info)))) + return ok and info or nil + end + local function Details(info) + if type(info) ~= "table" then return end + for k, v in pairs(info) do + local kl = string.lower(tostring(k)) + if type(v) == "number" and (string.find(kl, "type", 1, true) and C.GetCreatureTypeInfo) then + local ok, r = pcall(C.GetCreatureTypeInfo, v) + P(" GetCreatureTypeInfo(" .. tostring(v) .. ") [" .. tostring(k) .. "]: " .. (ok and Dump(r, 1, {}) or tostring(r))) + end + if type(v) == "number" and (string.find(kl, "family", 1, true) and C.GetCreatureFamilyInfo) then + local ok, r = pcall(C.GetCreatureFamilyInfo, v) + P(" GetCreatureFamilyInfo(" .. tostring(v) .. ") [" .. tostring(k) .. "]: " .. (ok and Dump(r, 1, {}) or tostring(r))) + end + end + end + + local before = Info("vorher:") + Details(before) + if C.GetCreatureTypeIDs then + local ok, r = pcall(C.GetCreatureTypeIDs) + P("GetCreatureTypeIDs: " .. (ok and Dump(r, 1, {}) or tostring(r))) + end + + local db = G.FreewoodDB + local function Save() + if type(db) == "table" then + db.npcProbe = db.npcProbe or {} + db.npcProbe[id] = lines + end + end + + if not C.RequestLoadCreatureByID then + P("RequestLoadCreatureByID fehlt") + Save() + return + end + local fr = G.CreateFrame("Frame") + local done = false + local function Finish(why) + if done then return end + done = true + fr:UnregisterAllEvents() + P("--- " .. why) + local after = Info("nachher:") + Details(after) + Save() + end + fr:RegisterEvent("CREATURE_DATA_LOAD_RESULT") + fr:SetScript("OnEvent", function() + P("Ereignis CREATURE_DATA_LOAD_RESULT: " .. tostring(G.arg1) .. ", " .. tostring(G.arg2) .. ", " .. tostring(G.arg3)) + Finish("Antwort erhalten") + end) + local ok, r = pcall(C.RequestLoadCreatureByID, id) + P("RequestLoadCreatureByID(" .. id .. "): " .. tostring(ok and r or ("Fehler: " .. tostring(r)))) + if G.C_Timer and G.C_Timer.After then + G.C_Timer.After(3, function() Finish("nach 3 Sekunden") end) + else + Finish("sofort (kein Zeitgeber)") + end + end +end) + +------------------------------------------------------------------------ +-- /fwmap: Kartenprobe. Zeigt, welche Karten-Funktionen/Frames diese Octo-Karte wirklich hat +-- und ob sich eine Zone per ID umschalten laesst. Aendert nur die angezeigte Karte, sonst nichts. +------------------------------------------------------------------------ + +Try("fwmap", function() + G.SLASH_FWMAP1 = "/fwmap" + G.SlashCmdList = G.SlashCmdList or {} + G.SlashCmdList["FWMAP"] = function(msg) + local lines = {} + local function P(text) + lines[#lines + 1] = text + if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffFWMap|r " .. text) end + end + local function Call(name, a, b) + local f = G[name] + if type(f) ~= "function" then return "fehlt" end + local r = { pcall(f, a, b) } + local out = {} + for i = 2, 6 do if r[i] ~= nil then out[table.getn(out) + 1] = tostring(r[i]) end end + if not r[1] then return "FEHLER " .. tostring(r[2]) end + return table.concat(out, ",") + end + local function Has(obj, names) + local out = {} + for i = 1, table.getn(names) do + local v = obj[names[i]] + out[i] = names[i] .. "=" .. (v ~= nil and (type(v) == "function" and "ja" or type(v)) or "nein") + end + return table.concat(out, " ") + end + + local globals = { "SetMapByID", "SetMapZoom", "SetMapToCurrentZone", "GetCurrentMapAreaID", "GetCurrentMapContinent", + "GetCurrentMapZone", "GetMapContinents", "GetMapZones", "ToggleWorldMap", "GetPlayerMapPosition", "GetMapInfo", + "GetNumMapLandmarks", "ProcessMapClick", "GetCursorPosition" } + for i = 1, table.getn(globals) do + P(globals[i] .. ": " .. (type(G[globals[i]]) == "function" and "ja" or "nein")) + end + P("Anfangszustand: Areal=" .. Call("GetCurrentMapAreaID") .. " Kontinent=" .. Call("GetCurrentMapContinent") + .. " Zone=" .. Call("GetCurrentMapZone") .. " MapInfo=" .. Call("GetMapInfo")) + + local wm = G.WorldMapFrame + if not wm then P("WorldMapFrame: FEHLT (Blizzard_WorldMap evtl. nicht geladen, Karte einmal oeffnen)") end + if wm then + P("WorldMapFrame sichtbar=" .. tostring(wm:IsShown()) .. " " .. Has(wm, { "SetMapID", "GetMapID", "GetCanvas", "ScrollContainer", "OnMapChanged", "HookScript" })) + end + local fr = { "WorldMapDetailFrame", "WorldMapButton", "WorldMapContinentDropDown", "WorldMapZoneDropDown", "WorldMapPlayer" } + for i = 1, table.getn(fr) do + local f = G[fr[i]] + if f then + local w, h = 0, 0 + if f.GetWidth then w, h = f:GetWidth() or 0, f:GetHeight() or 0 end + P(fr[i] .. ": da " .. math.floor(w) .. "x" .. math.floor(h) .. " sichtbar=" .. tostring(f.IsShown and f:IsShown()) + .. " " .. Has(f, { "SetClipsChildren", "CreateTexture", "CreateFontString" })) + else + P(fr[i] .. ": fehlt") + end + end + local tex = (G.WorldMapButton or G.WorldMapFrame) + if tex and tex.CreateTexture then + local ok, t = pcall(tex.CreateTexture, tex, nil, "OVERLAY") + if ok and t then + P("Textur: " .. Has(t, { "SetColorTexture", "SetTexture", "SetVertexColor", "SetRotation", "SetTexCoord", "AddMaskTexture", "SetRotation" })) + end + end + + local CM = G.C_Map + if type(CM) == "table" then + local keys = {} + for k in pairs(CM) do keys[table.getn(keys) + 1] = tostring(k) end + table.sort(keys) + P("C_Map: " .. table.concat(keys, ", ")) + else + P("C_Map: nein") + end + + -- Zone per ID umschalten testen (Durotar 14, Elwynn 12) + local tests = { 14, 12 } + for i = 1, table.getn(tests) do + local id = tests[i] + local before = Call("GetCurrentMapAreaID") + local r1 = Call("SetMapByID", id) + P("SetMapByID(" .. id .. "): " .. r1 .. " | Areal " .. before .. " -> " .. Call("GetCurrentMapAreaID") + .. " Kont=" .. Call("GetCurrentMapContinent") .. " Zone=" .. Call("GetCurrentMapZone")) + end + if type(CM) == "table" and type(CM.SetMapID) == "function" then + P("C_Map.SetMapID(14): " .. tostring(pcall(CM.SetMapID, 14)) .. " -> Areal " .. Call("GetCurrentMapAreaID")) + end + local r = Call("SetMapZoom", 1, 1) + P("SetMapZoom(1,1): " .. r .. " -> Areal " .. Call("GetCurrentMapAreaID") .. " Kont=" .. Call("GetCurrentMapContinent") .. " Zone=" .. Call("GetCurrentMapZone")) + P("Zurueck: SetMapToCurrentZone " .. Call("SetMapToCurrentZone") .. " -> Areal " .. Call("GetCurrentMapAreaID")) + + G.FreewoodDB = G.FreewoodDB or {} + G.FreewoodDB.mapProbe = lines + end +end) + +------------------------------------------------------------------------ +-- /fwah: Auktionshaus-Probe. Zeigt, welche Funktionen/Ereignisse es gibt und was eine Abfrage liefert. +-- /fwah Zustand (Funktionen, Fenster, aktuelle Liste) +-- /fwah query [Seite] sendet eine leere Abfrage und gibt das Ergebnis aus (Auktionshaus muss offen sein) +------------------------------------------------------------------------ + +Try("fwah", function() + G.SLASH_FWAH1 = "/fwah" + G.SlashCmdList = G.SlashCmdList or {} + local lines = {} + local function P(text) + lines[#lines + 1] = text + if G.DEFAULT_CHAT_FRAME then G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffFWAh|r " .. text) end + G.FreewoodDB = G.FreewoodDB or {} + G.FreewoodDB.ahProbe = lines + end + local function Res(r) + local out = {} + for i = 2, 16 do + local v = r[i] + if v ~= nil or i < 14 then out[#out + 1] = tostring(v) end + end + while #out > 0 and out[#out] == "nil" do out[#out] = nil end + return table.concat(out, " | ") + end + local counts, opened = {}, false + local watch = G.CreateFrame("Frame") + local events = { "AUCTION_HOUSE_SHOW", "AUCTION_HOUSE_CLOSED", "AUCTION_ITEM_LIST_UPDATE", "AUCTION_OWNED_LIST_UPDATE", "AUCTION_BIDDER_LIST_UPDATE" } + for i = 1, #events do pcall(watch.RegisterEvent, watch, events[i]) end + local wantDump = false + local function Dump(tag) + local f = G.GetNumAuctionItems + if type(f) ~= "function" then P("GetNumAuctionItems fehlt") return end + local r = { pcall(f, "list") } + P(tag .. " GetNumAuctionItems(list): " .. Res(r)) + local n = tonumber(r[2]) or 0 + for i = 1, math.min(3, n) do + local info = { pcall(G.GetAuctionItemInfo, "list", i) } + P(" [" .. i .. "] Info: " .. Res(info)) + if type(G.GetAuctionItemLink) == "function" then + local l = { pcall(G.GetAuctionItemLink, "list", i) } + P(" [" .. i .. "] Link: " .. tostring(l[2])) + end + end + end + watch:SetScript("OnEvent", function() + local e = G.event + counts[e] = (counts[e] or 0) + 1 + if e == "AUCTION_HOUSE_SHOW" then opened = true end + if e == "AUCTION_HOUSE_CLOSED" then opened = false end + if e == "AUCTION_ITEM_LIST_UPDATE" and wantDump then + wantDump = false + Dump("Ereignis AUCTION_ITEM_LIST_UPDATE (Anzahl " .. counts[e] .. ")") + end + end) + + G.SlashCmdList["FWAH"] = function(msg) + msg = msg or "" + local page = tonumber(string.match(msg, "query%s*(%d*)")) or 0 + if string.find(msg, "^query") then + if type(G.QueryAuctionItems) ~= "function" then P("QueryAuctionItems fehlt") return end + local can = type(G.CanSendAuctionQuery) == "function" and { pcall(G.CanSendAuctionQuery) } or nil + P("CanSendAuctionQuery: " .. (can and Res(can) or "fehlt")) + wantDump = true + local r = { pcall(G.QueryAuctionItems, "", nil, nil, 0, 0, 0, page, 0, 0) } + P("QueryAuctionItems(Seite " .. page .. "): " .. Res(r)) + return + end + local names = { "QueryAuctionItems", "GetNumAuctionItems", "GetAuctionItemInfo", "GetAuctionItemLink", "CanSendAuctionQuery", + "GetAuctionItemClasses", "GetAuctionItemSubClasses", "GetAuctionSort", "SetAuctionsTabShowing", "AuctionFrameBrowse_Update", + "GetSelectedAuctionItem", "PlaceAuctionBid", "GetItemInfo" } + local out = {} + for i = 1, #names do out[#out + 1] = names[i] .. "=" .. (type(G[names[i]]) == "function" and "ja" or "nein") end + P(table.concat(out, " ")) + local ah = G.C_AuctionHouse + if type(ah) == "table" then + local k = {} + for key in pairs(ah) do k[#k + 1] = tostring(key) end + table.sort(k) + P("C_AuctionHouse: " .. table.concat(k, ", ")) + else + P("C_AuctionHouse: nein") + end + P("AuctionFrame=" .. tostring(G.AuctionFrame ~= nil) .. (G.AuctionFrame and (" sichtbar=" .. tostring(G.AuctionFrame:IsShown())) or "") + .. " AuctionHouseFrame=" .. tostring(G.AuctionHouseFrame ~= nil) .. " Blizzard_AuctionUI geladen=" .. tostring(G.IsAddOnLoaded and G.IsAddOnLoaded("Blizzard_AuctionUI"))) + local c = {} + for i = 1, #events do c[#c + 1] = events[i] .. "=" .. (counts[events[i]] or 0) end + P("Ereignisse seit /reload: " .. table.concat(c, " ") .. " offen=" .. tostring(opened)) + Dump("Jetzt") + P("Weiter mit: /fwah query (und /fwah query 1 fuer die zweite Seite)") + end +end) diff --git a/Core/Export.lua b/Core/Export.lua new file mode 100644 index 0000000..974113f --- /dev/null +++ b/Core/Export.lua @@ -0,0 +1,273 @@ +-- Freewood-OctoWoW-Core: Export und Import als kurzer Code +-- +-- Format: FW1::: +-- 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 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 diff --git a/Core/Init.lua b/Core/Init.lua new file mode 100644 index 0000000..fd06b42 --- /dev/null +++ b/Core/Init.lua @@ -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[] 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) diff --git a/Core/Slash.lua b/Core/Slash.lua new file mode 100644 index 0000000..706e8d9 --- /dev/null +++ b/Core/Slash.lua @@ -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 diff --git a/Core/Util.lua b/Core/Util.lua new file mode 100644 index 0000000..4c07f19 --- /dev/null +++ b/Core/Util.lua @@ -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