-- Freewood-OctoWoW-Codex: lernt Quests, Kreaturen und Beute beim Spielen. -- -- Gespeichert (Account-weit) in Codex.db: -- quests[questID] = { -- t = Titel, lvl = Stufe, pl = niedrigste Spielerstufe bei Annahme, zone = Karte der Annahme, -- give = Ref, turn = Ref, -- Ref = { k="npc"|"object"|"other", id, n, m, x, y } -- xp, money, rep = { [Fraktion] = Wert }, -- fixed = { { id, c } }, choice = { { id, c } }, chosen = { [itemID] = Anzahl }, -- obj = { { ty, name, txt, req, ids = { [npcID] = Treffer } } }, -- qi = { [itemID] = { ["u"|"o"] = Anzahl } }, -- Quest-Items und ihre Quellen -- seen, done, first, last, partial } -- units[npcID] = { n, l1, l2, c, cells = { [m*GRID^2+zelle] = n }, -- k = gelootete Leichen, coin, coinN, d = { [itemID] = { n, q, qi } }, -- alt = { k, d } } -- alt: zweites Beutefenster (Haeuten usw.) -- objects[objID] = wie units (ohne Stufen) -- names[itemID] = Itemname (fuer die Suche) -- xp[spielerstufe][gegnerstufe][klasse] = { n, sum, min, max } Kill-Erfahrung ohne Zusaetze -- xpb[...] gleiche Form, aber mit Ruhe-/Gruppen-/Raidzusatz -- units[npcID].xp[spielerstufe] = { n, sum, min, max } -- units[npcID].xm[spielerstufe][gegnerstufe] = { n, sum, min, max } -- quests[id].obj[i].item = Item-ID, gelernt aus dem Beutefenster, wenn der Zaehler steigt -- objEmpty = Ziele ohne lesbaren Namen (max. 5, fuer /fwcodex test) -- objects[id].n = Name, vom Objekt-Tooltip gelernt -- units[npcID].f = Tierfamilie (Text) -- remote[absender] = { seen, n, cursor, hash, rec = { ["u123"] = bereinigter Datensatz } } empfangene Daten (CodexShare.lua) -- sync = { stamps = { ["u123"] = { sig, upd } }, sent, received } Aenderungsstaende eigener Daten -- settings.share = ein Schalter fuer das gesamte Teilen -- -- Geheime Werte (secret values) werden mit Util.Clean verworfen, jede Erfassung laeuft geschuetzt. -- Kein Kampflog: Beute, Quest-Fenster und Ziele werden ausserhalb des Kampfes gelesen. local ADDON, NS = ... local FW = _G.Freewood if not FW or not FW:RequireAPI(1, ADDON) then return end local Util = FW.Util local L = FW.L local GRID = 200 local MAX_CELLS = 600 local MAX_DROPS = 80 local MAX_OBJ = 12 local MAX_REWARDS = 12 local MAX_IDS = 12 local MAX_NAMES = 6000 local MAX_LOOTED_GUIDS = 300 local RECENT_SECS = 60 -- so lange passt ein gelootetes Quest-Item zu einem steigenden Zaehler 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 local Codex = FW:NewModule("Codex", { version = GetMeta("Version") or "dev", title = L.CODEX_TITLE, description = L.CODEX_DESC, defaults = { quests = {}, units = {}, objects = {}, names = {}, ttk = {}, -- Kampfdauer { [Klasse] = { [Spielerstufe] = { [Stufenabstand] = { n, sum, min, max } } } }, Zehntelsekunden qtimer = {}, -- laufende Quest-Zeitnehmer (siehe CodexTime.lua) istats = {}, -- Itemwerte { [itemID] = { [Schluessel] = Zahl } }, vom Spiel gelesen (nur lokal) disp = {}, -- Creature-Display-IDs fuer 2D-Bilder { [npcID] = displayID } (nur lokal) cnames = {}, -- von Hand eingetragene Itemnamen { [itemID] = { de, en } } xp = {}, xpb = {}, xr = {}, -- Erfahrung je Kill MIT Ruhebonus (Gesamtwert), gleiche Form wie xp (nur lokal) xpUnparsed = {}, objEmpty = {}, remote = {}, sync = { stamps = {}, sent = 0, received = 0 }, settings = { enabled = true, share = true, shareDefaultOn = true }, recipes = {}, -- Rezepte der Berufe { [Beruf] = { [Name] = { it, rg, ob } } } (siehe CodexRecipes.lua, nur lokal) wrq = {}, -- Ruf-Anforderungen der Waren { [haendlerID] = { v = 1, [itemID] = { f = Fraktion, s = Stufe 1-8, ok = erfuellt } } } (nur lokal) vfac = {}, -- Fraktion von Haendlern { [haendlerID] = "A" | "H" } (nur lokal) ied = {}, -- Eigenschaften von Items { [itemID] = { ed = { z, dg } } }, wird geteilt inames = {}, -- vom Spiel gelernte Instanznamen { [instanceID] = Name } (nur lokal) path = {}, -- Wegenetz { [welt] = { [zellKey] = Maske } }, siehe CodexPath.lua (nur lokal) fav = {}, -- Favoriten und Notizen { ["q123"] = { note, t } }, nur lokal dnotes = {}, -- Dev-Notizen { ["q123"] = { n = Text, t } }, nur lokal, nur mit Freewood-OctoWoW-Dev und Bearbeitungsrecht }, }) NS.Codex = Codex FW.Codex = Codex Codex.GRID = GRID local state = { pool = nil, -- Ruhe-Vorrat (GetXPExhaustion) vor dem naechsten Kill xpOpen = 0, -- Kills, deren Ruhebonus noch bestimmt wird xpJobs = {}, detail = nil, -- zuletzt geoeffnetes Annahme-Fenster turn = nil, -- zuletzt geoeffnetes Abgabe-Fenster objState = {}, -- [questID] = { [i] = Anzahl } recentItems = {}, -- [itemID] = { t, quest, qid }: zuletzt gelootete Items requested = {}, -- [itemID] = true: Itemdaten schon angefordert pendingInfo = 0, -- noch nicht angekommene Itemdaten objNames = {}, -- [objectID] = Name, vom Tooltip gelernt, bevor das Objekt gelootet wurde repBuf = {}, -- { t, name, amount } lootedGuids = {}, -- Leichen, die in dieser Sitzung schon ein Beutefenster hatten lootedItems = {}, -- [guid] = { [itemID] = true }: dort schon gezaehlte Items lootedCount = 0, noteAt = {}, -- Drosselung fuer NoteUnit stats = { quests = 0, loot = 0, errors = 0 }, } Codex.runtime = state ------------------------------------------------------------------------ -- Kleine Pruefer ------------------------------------------------------------------------ local function Num(v) if Util.IsSecret(v) then return nil end if type(v) == "number" and v == v and v ~= math.huge and v ~= -math.huge then return v end return nil end local function Str(v, max) if Util.IsSecret(v) then return nil end if type(v) == "string" and v ~= "" then return v:sub(1, max or 80) end return nil end local function Strip(text) text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|T.-|t", "") return (text:gsub("^%s+", ""):gsub("%s+$", "")) end local function Now() return time() end -- Fuehrt fn geschuetzt aus; Fehler (zum Beispiel durch geheime Werte) werden still gezaehlt local function Safe(fn, ...) local ok, a, b, c = pcall(fn, ...) if not ok then state.stats.errors = state.stats.errors + 1 if FW.settings and FW.settings.debug then FW:Debug("Codex: " .. tostring(a)) end return nil end return a, b, c end ------------------------------------------------------------------------ -- Orte und Raster ------------------------------------------------------------------------ local function PlayerPoint() local loc = Util.GetPlayerLocation() if loc and loc.mapID and loc.x and loc.y then return loc.mapID, loc.x, loc.y end return nil end local function CellKey(m, x, y) local cx = math.min(GRID - 1, math.max(0, math.floor(x * GRID))) local cy = math.min(GRID - 1, math.max(0, math.floor(y * GRID))) return m * GRID * GRID + cy * GRID + cx end local function NoteInstance(ent) local id = Codex.CurrentInstance and Codex.CurrentInstance() if not id then return end local iz = ent.iz if not iz then iz = {} ent.iz = iz end if iz[id] then iz[id] = iz[id] + 1 return end local n = 0 for _ in pairs(iz) do n = n + 1 end if n < 3 then iz[id] = 1 end end local function AddCell(ent) NoteInstance(ent) local m, x, y = PlayerPoint() if not m then return end ent.cells = ent.cells or {} local key = CellKey(m, x, y) if ent.cells[key] then ent.cells[key] = ent.cells[key] + 1 return end local n = 0 for _ in pairs(ent.cells) do n = n + 1 if n >= MAX_CELLS then return end end ent.cells[key] = 1 end -- Rueckgabe: Karten-ID und Zelle als Liste { { m, cx, cy, n } } function Codex.DecodeCells(cells) local list = {} for key, n in pairs(cells or {}) do local m = math.floor(key / (GRID * GRID)) local idx = key % (GRID * GRID) list[#list + 1] = { m = m, cx = idx % GRID, cy = math.floor(idx / GRID), n = n } end return list end ------------------------------------------------------------------------ -- Instanzen: das Spiel nennt in Dungeons oft keinen Namen, aber die Instanz-ID (GetInstanceInfo, 8. Rueckgabe). -- Die ID ist sprachunabhaengig. Als "Zone" wird sie negativ gespeichert (-389 = Flammenschlund). ------------------------------------------------------------------------ Codex.INSTANCES = { [30] = { "Alteractal", "Alterac Valley" }, [33] = { "Burg Schattenfang", "Shadowfang Keep" }, [34] = { "Das Verlies", "The Stockade" }, [36] = { "Die Todesminen", "The Deadmines" }, [43] = { "Die Höhlen des Wehklagens", "Wailing Caverns" }, [47] = { "Kral der Klingenhauer", "Razorfen Kraul" }, [48] = { "Die Tiefschwarze Grotte", "Blackfathom Deeps" }, [70] = { "Uldaman", "Uldaman" }, [90] = { "Gnomeregan", "Gnomeregan" }, [109] = { "Der Tempel von Atal'Hakkar", "The Temple of Atal'Hakkar" }, [129] = { "Hügel der Klingenhauer", "Razorfen Downs" }, [189] = { "Das Scharlachrote Kloster", "Scarlet Monastery" }, [209] = { "Zul'Farrak", "Zul'Farrak" }, [229] = { "Schwarzfelsspitze", "Blackrock Spire" }, [230] = { "Die Schwarzfels-Tiefen", "Blackrock Depths" }, [249] = { "Onyxias Hort", "Onyxia's Lair" }, [289] = { "Scholomance", "Scholomance" }, [309] = { "Zul'Gurub", "Zul'Gurub" }, [329] = { "Stratholme", "Stratholme" }, [349] = { "Maraudon", "Maraudon" }, [369] = { "Die Tiefenbahn", "The Deeprun Tram" }, [389] = { "Flammenschlund", "Ragefire Chasm" }, [409] = { "Geschmolzener Kern", "Molten Core" }, [429] = { "Düsterbruch", "Dire Maul" }, [469] = { "Pechschwingenhort", "Blackwing Lair" }, [489] = { "Kriegshymnenschlucht", "Warsong Gulch" }, [509] = { "Ruinen von Ahn'Qiraj", "Ruins of Ahn'Qiraj" }, [529] = { "Arathibecken", "Arathi Basin" }, [531] = { "Tempel von Ahn'Qiraj", "Temple of Ahn'Qiraj" }, [533] = { "Naxxramas", "Naxxramas" }, } -- Name einer Instanz: eingebaute Liste (Deutsch / Englisch), sonst vom Spiel gelernt, sonst "Instanz #ID" function Codex.InstanceName(id) local en = Codex.Lang() == "en" local b = Codex.INSTANCES[id] if b then return en and b[2] or b[1] end local learned = Codex.db and Codex.db.inames and Codex.db.inames[id] if learned then return learned end return string.format(FW.L.CODEX_INSTANCE_UNKNOWN or "Instance #%d", id) end -- Instanz-ID, in der wir gerade stehen (nil in der offenen Welt) function Codex.CurrentInstance() if not (IsInInstance and GetInstanceInfo) then return nil end local ok, inside, itype = pcall(IsInInstance) if not ok or Util.IsSecret(inside) or not inside or Util.IsSecret(itype) or itype == "none" then return nil end local ok2, name, _, _, _, _, _, _, id = pcall(GetInstanceInfo) if not ok2 or Util.IsSecret(id) or type(id) ~= "number" or id < 1 or id > 100000 then return nil end -- lesbaren Namen fuer unbekannte Instanzen merken (nur lokal, in der aktuellen Sprache) if type(name) == "string" and name ~= "" and not Util.IsSecret(name) and not Codex.INSTANCES[id] and Codex.db then Codex.db.inames = Codex.db.inames or {} Codex.db.inames[id] = name:sub(1, 60) end return id end -- Kreaturtyp (UnitCreatureType, je nach Client englisch oder deutsch) -> fester Kurzname Codex.CT_NAMES = { ["beast"] = "beast", ["wildtier"] = "beast", ["bestie"] = "beast", ["humanoid"] = "humanoid", ["humanoide"] = "humanoid", ["undead"] = "undead", ["untoter"] = "undead", ["untote"] = "undead", ["demon"] = "demon", ["d\195\164mon"] = "demon", ["daemon"] = "demon", ["dämon"] = "demon", ["dragonkin"] = "dragonkin", ["drachkin"] = "dragonkin", ["elemental"] = "elemental", ["elementar"] = "elemental", ["giant"] = "giant", ["riese"] = "giant", ["mechanical"] = "mechanical", ["mechanisch"] = "mechanical", ["critter"] = "critter", ["kleintier"] = "critter", ["totem"] = "totem", ["gas cloud"] = "gascloud", ["gaswolke"] = "gascloud", } Codex.CT_CODES = { beast = true, humanoid = true, undead = true, demon = true, dragonkin = true, elemental = true, giant = true, mechanical = true, critter = true, totem = true, gascloud = true, other = true, pet = true, } function Codex.MapName(mapID) if type(mapID) == "number" and mapID < 0 then return Codex.InstanceName(-mapID) end if C_Map and C_Map.GetMapInfo and mapID then local ok, info = pcall(C_Map.GetMapInfo, mapID) if ok and type(info) == "table" and type(info.name) == "string" then return info.name end end return mapID and ("#" .. mapID) or "?" end -- Hauptkarte einer Zellenmenge: Karte mit den meisten Treffern function Codex.MainMap(cells) local per, best, bestN = {}, nil, 0 for key, n in pairs(cells or {}) do local m = math.floor(key / (GRID * GRID)) per[m] = (per[m] or 0) + n if per[m] > bestN then best, bestN = m, per[m] end end return best end ------------------------------------------------------------------------ -- GUIDs, Einheiten, Objekte ------------------------------------------------------------------------ -- "Creature-0-4621-1-27730-4107-0000477262" -> "npc", 4107; GameObject -> "object", id local function ParseGUID(guid) if Util.IsSecret(guid) or type(guid) ~= "string" then return nil end local kind, _, _, _, _, id = strsplit("-", guid) id = tonumber(id) if not id then local hk, hid = Util.ParseHexGUID(guid) if not hk then return nil end kind, id = hk, hid end if kind == "Creature" or kind == "Vehicle" then return "npc", id end if kind == "GameObject" then return "object", id end return nil end Codex.ParseGUID = ParseGUID function Codex:Unit(id, create) local u = self.db.units[id] if not u and create then u = {} self.db.units[id] = u end return u end function Codex:Object(id, create) local o = self.db.objects[id] if not o and create then o = {} self.db.objects[id] = o end return o end function Codex:Quest(id, create) local q = self.db.quests[id] if not q and create then q = { seen = 0, done = 0, first = Now() } self.db.quests[id] = q state.stats.quests = state.stats.quests + 1 end return q end local function NoteName(itemID, name) name = Str(name, 80) if not itemID or not name then return end local names = Codex.db.names if names[itemID] == name then return end if names[itemID] == nil then local n = 0 for _ in pairs(names) do n = n + 1 if n >= MAX_NAMES then return end end end names[itemID] = name end -- Qualitaet (Farbe) eines Items merken, damit Namen auch farbig sind, wenn das Spiel das Item gerade nicht kennt function Codex.RememberQuality(itemID, quality) if type(itemID) ~= "number" or type(quality) ~= "number" or quality < 0 or quality > 8 then return end local iq = Codex.db.iq if not iq then iq = {} Codex.db.iq = iq end if iq[itemID] == quality then return end if iq[itemID] == nil then local n = 0 for _ in pairs(iq) do n = n + 1 if n >= 8000 then return end end end iq[itemID] = quality end function Codex.QualityOf(itemID) local info = Util.GetItem(itemID) if info and info.quality then Codex.RememberQuality(itemID, info.quality) return info.quality end return Codex.db.iq and Codex.db.iq[itemID] or nil end Codex.AddCell = AddCell Codex.NoteName = NoteName Codex.Str = Str Codex.Safe = Safe -- Einheit am Unit-Token beobachten (Name, Stufe, Fundort). Nur angreifbare Kreaturen. function Codex:NoteUnit(unit) if Util.IsSecret(unit) or not UnitExists(unit) then return end local isPlayer, controlled = UnitIsPlayer(unit), UnitPlayerControlled(unit) if Util.IsSecret(isPlayer) or Util.IsSecret(controlled) or isPlayer or controlled then return end local canAttack = UnitCanAttack and UnitCanAttack("player", unit) if Util.IsSecret(canAttack) or not canAttack then return end local kind, id = ParseGUID(UnitGUID(unit)) if kind ~= "npc" then return end local now = GetTime and GetTime() or 0 if state.noteAt[id] and now - state.noteAt[id] < 1.5 then return end state.noteAt[id] = now local u = self:Unit(id, true) local name = Str(UnitName(unit), 60) if name then u.n = name end local lvl = Num(UnitLevel and UnitLevel(unit)) if lvl and lvl > 0 then u.l1 = u.l1 and math.min(u.l1, lvl) or lvl u.l2 = u.l2 and math.max(u.l2, lvl) or lvl end local class = UnitClassification and Str(UnitClassification(unit), 16) if class and class ~= "normal" and class ~= "trivial" and class ~= "minus" then u.c = class end local family = UnitCreatureFamily and Str(UnitCreatureFamily(unit), 40) if family then u.f = family end local ctype = UnitCreatureType and Str(UnitCreatureType(unit), 40) local ctcode = ctype and Codex.CT_NAMES[ctype:lower()] if ctcode then u.ct = ctcode end AddCell(u) end -- Objektnamen kommen vom Tooltip (Mouseover auf Truhe, Kraut usw.); data.id ist die Objekt-ID function Codex:OnObjectTooltip(data) Safe(function() if type(data) ~= "table" or Util.IsSecret(data) or not self.db.settings.enabled then return end if TooltipUtil and TooltipUtil.SurfaceArgs then pcall(TooltipUtil.SurfaceArgs, data) end local id = Num(data.id) local line = type(data.lines) == "table" and data.lines[1] or nil local name = type(line) == "table" and Str(line.leftText, 60) or nil if not id or not name then return end Codex.lastObject = { id = id, name = name, t = (GetTime and GetTime()) or 0 } local o = self.db.objects[id] if o then o.n = name else local count = 0 for _ in pairs(state.objNames) do count = count + 1 end if count >= 200 then state.objNames = {} end state.objNames[id] = name end end) end function Codex:OnTargetChanged() Safe(self.NoteUnit, self, "target") if self.Names then Safe(self.Names.Read, self.Names, "target", "target") end end function Codex:OnMouseover() Safe(self.NoteUnit, self, "mouseover") if self.Names then Safe(self.Names.Read, self.Names, "mouseover", "mouse") end end ------------------------------------------------------------------------ -- Quest-Fenster lesen ------------------------------------------------------------------------ -- NPC oder Objekt, mit dem gerade gesprochen wird. nil bei Item-Quests. local function ReadInteraction() local ref = { k = "other" } local guid = UnitGUID and UnitGUID("npc") local kind, id = ParseGUID(guid) if kind then ref.k = kind ref.id = id ref.n = Str(UnitName("npc"), 60) if kind == "npc" then local u = Codex:Unit(id, true) if ref.n then u.n = ref.n end else local o = Codex:Object(id, true) if ref.n then o.n = ref.n end end end local m, x, y = PlayerPoint() if m then ref.m, ref.x, ref.y = m, Util.Round(x, 3), Util.Round(y, 3) end return ref end local function ItemIDFromLink(link) link = Util.Clean(link) if type(link) ~= "string" then return nil end return tonumber(link:match("item:(%d+)")) end local function ReadRewardList(kind, count) local list = {} for i = 1, math.min(count or 0, MAX_REWARDS) do local link = GetQuestItemLink and GetQuestItemLink(kind, i) local id = ItemIDFromLink(link) local name, _, c if GetQuestItemInfo then name, _, c = GetQuestItemInfo(kind, i) end if id then list[#list + 1] = { id = id, c = Num(c) or 1 } NoteName(id, name) end end return list end local function ReadRewards() local r = {} r.xp = Num(GetRewardXP and GetRewardXP()) r.money = Num(GetRewardMoney and GetRewardMoney()) r.fixed = ReadRewardList("reward", Num(GetNumQuestRewards and GetNumQuestRewards())) r.choice = ReadRewardList("choice", Num(GetNumQuestChoices and GetNumQuestChoices())) return r end local function ApplyRewards(q, r, final) if not r then return end if r.xp and r.xp > 0 and (final or not q.xp) then q.xp = r.xp end if r.money and (final or not q.money) then q.money = r.money end if #r.fixed > 0 or final then q.fixed = #r.fixed > 0 and r.fixed or nil end if #r.choice > 0 or final then q.choice = #r.choice > 0 and r.choice or nil end end ------------------------------------------------------------------------ -- Quest-Ziele ------------------------------------------------------------------------ -- Nur das Verb ("getötet") ist kein Name: so entstanden Ziele ohne Namen, wenn der Server den Kreaturnamen noch nicht kannte local VERBS = { "getötet", "besiegt", "erschlagen", "beseitigt", "vernichtet", "zerstört", "erledigt", "bezwungen", "slain", "killed", "defeated", "destroyed", "vanquished", "eliminated" } local VERB_ONLY = {} for _, v in ipairs(VERBS) do VERB_ONLY[v] = true end VERB_ONLY["zur strecke gebracht"] = true function Codex.IsBadObjName(name) return type(name) == "string" and VERB_ONLY[name:lower()] == true end -- Verb am Ende entfernen ("Wolf getötet" -> "Wolf"); nur das Verb ergibt nil function Codex.StripKillVerb(name) if type(name) ~= "string" then return nil end name = name:gsub("^[%s:]+", ""):gsub("[%s:]+$", "") for _ = 1, 2 do local low = name:lower() local cut for _, v in ipairs(VERBS) do local suffix = " " .. v if #low > #suffix and low:sub(-#suffix) == suffix then cut = #suffix break end end if not cut then local sfx = " zur strecke gebracht" if #low > #sfx and low:sub(-#sfx) == sfx then cut = #sfx end end if not cut then break end name = name:sub(1, #name - cut) end name = name:gsub("^[%s:]+", ""):gsub("[%s:]+$", "") if name == "" or Codex.IsBadObjName(name) then return nil end return name end local function ObjectiveName(text) text = Strip(text) local name = text:match("^(.-):%s*%d+%s*/%s*%d+") or text:match("^%d+%s*/%s*%d+%s+(.+)$") or text -- Kill-Ziele heissen "Wolf getoetet" / "Wolf slain": das Verb gehoert nicht zum Namen name = Codex.StripKillVerb(name) or "" -- Nur Zaehler ohne Namen ("0/3"): kein Name if name == "" or name:match("^%d+%s*/%s*%d+$") or Codex.IsBadObjName(name) then return nil end return name:sub(1, 60) end -- Ziele ohne lesbaren Namen merken (fuer /fwcodex test) local function NoteEmptyObjective(ty, text) local list = Codex.db and Codex.db.objEmpty if not list or #list >= 5 then return end local entry = tostring(ty) .. " | " .. tostring(text):sub(1, 100) for _, e in ipairs(list) do if e == entry then return end end list[#list + 1] = entry end -- Liest die Ziele einer Quest: Liste { ty, text, name, have, req, done } local function ReadObjectives(questID) local list = {} if C_QuestLog and C_QuestLog.GetQuestObjectives then local objs = C_QuestLog.GetQuestObjectives(questID) if type(objs) == "table" then for _, o in ipairs(objs) do if type(o) == "table" then local text = Str(o.text, 120) list[#list + 1] = { ty = Str(o.type, 16) or "event", text = text and Strip(text) or "", have = Num(o.numFulfilled) or 0, req = Num(o.numRequired) or 0, done = o.finished and true or false, } end end end return list end local index = GetQuestLogIndexByID and GetQuestLogIndexByID(questID) if index and GetNumQuestLeaderBoards then for i = 1, GetNumQuestLeaderBoards(index) do local text, ty, done = GetQuestLogLeaderBoard(i, index) text = Str(text, 120) local have, req = (text or ""):match("(%d+)%s*/%s*(%d+)") list[#list + 1] = { ty = Str(ty, 16) or "event", text = text and Strip(text) or "", have = tonumber(have) or 0, req = tonumber(req) or 0, done = done and true or false, } end end return list end local function StoreObjectives(q, list) local old = q.obj or {} local new = {} for i, o in ipairs(list) do if i > MAX_OBJ then break end local prev = old[i] local nm = o.text ~= "" and ObjectiveName(o.text) or nil if not nm and prev and prev.name and not Codex.IsBadObjName(prev.name) then nm = prev.name -- ein schon bekannter Name geht nicht wieder verloren end new[i] = { ty = o.ty, txt = o.text ~= "" and o.text:sub(1, 100) or nil, name = nm, req = o.req > 0 and o.req or nil, ids = prev and prev.ids or nil, item = prev and prev.item or nil, } if not new[i].name and not new[i].item then NoteEmptyObjective(o.ty, o.text) end end if #new > 0 then q.obj = new end end local function QuestLogInfo(questID) if C_QuestLog and C_QuestLog.GetLogIndexForQuestID then local index = C_QuestLog.GetLogIndexForQuestID(questID) if index and C_QuestLog.GetInfo then local info = C_QuestLog.GetInfo(index) if type(info) == "table" then return Str(info.title, 100), Num(info.level) end end end if GetQuestLogIndexByID and GetQuestLogTitle then local index = GetQuestLogIndexByID(questID) if index and index > 0 then local title, level = GetQuestLogTitle(index) return Str(title, 100), Num(level) end end return nil end local function TrackObjectives(questID, list) local counts = {} for i, o in ipairs(list) do counts[i] = o.have end state.objState[questID] = counts end -- Ziele aller Quests im Log pruefen; steigt ein Kill-Zaehler, wird das anvisierte Ziel gelernt function Codex:CheckObjectives() for questID, counts in pairs(state.objState) do local list = ReadObjectives(questID) local q = self:Quest(questID, false) if #list == 0 then state.objState[questID] = nil else for i, o in ipairs(list) do local before = counts[i] local entry = q and q.obj and q.obj[i] if entry then -- fehlender Name: aus dem aktuellen Text nachtragen (Itemnamen sind oft erst spaeter geladen) if (not entry.name or Codex.IsBadObjName(entry.name)) and o.text ~= "" then local nm = ObjectiveName(o.text) if nm then entry.name = nm entry.txt = o.text:sub(1, 100) if self.LinkObjectives then self.LinkObjectives(q, questID) end end end if before and o.have > before then if o.ty == "monster" then self:LearnKillTarget(entry, o) elseif o.ty == "item" then self:LearnObjectiveItem(questID, entry, o.have - before, i) end end end counts[i] = o.have end if #list ~= #(q and q.obj or {}) and q then StoreObjectives(q, list) end end end end -- Steigt der Zaehler eines Sammel-Ziels, war es meist das Quest-Item, das gerade gelootet wurde. -- Genau ein Kandidat (oder ein Namenstreffer) wird dem Ziel zugeordnet. function Codex:LearnObjectiveItem(questID, entry, delta, idx) if entry.item then return end local now = Now() local want = entry.name and entry.name:lower() -- Kandidaten: kuerzlich gelootete Items dieser Quest (oder ohne Quest-Angabe); mit passender Menge zuerst local strict, loose = {}, {} local nStrict, nLoose, exact = 0, 0, nil local key = tostring(questID) .. ":" .. tostring(idx or 0) state.objRise = state.objRise or {} local since = state.objRise[key] -- nur Beute nach dem letzten Anstieg dieses Zaehlers zaehlt state.objRise[key] = now for itemID, r in pairs(state.recentItems) do if now - r.t <= RECENT_SECS and (not since or r.t > since) and (not r.qid or r.qid == 0 or r.qid == questID) then loose[itemID] = true nLoose = nLoose + 1 if not delta or not r.qty or r.qty == delta then strict[itemID] = true nStrict = nStrict + 1 end local nm = self.db.names[itemID] if want and nm and nm:lower() == want then exact = itemID end end end local cands, n = strict, nStrict if n == 0 then cands, n = loose, nLoose end local found if exact then found = exact else -- ueber mehrere Zaehler-Anstiege schneiden sich die Kandidaten auf genau ein Item zusammen state.objCand = state.objCand or {} local prior = state.objCand[key] if prior then local cut, cn = {}, 0 for itemID in pairs(cands) do if prior[itemID] then cut[itemID] = true cn = cn + 1 end end if cn > 0 then cands, n = cut, cn end end state.objCand[key] = cands if n == 1 then found = next(cands) end if found then state.objCand[key] = nil end end if found then entry.item = found -- Quelle(n) des ersten Fundes gleich eintragen (spaetere Funde laufen ueber die Item-ID) local r = state.recentItems[found] local q = self:Quest(questID, false) if r and q and r.srcs then q.qi = q.qi or {} q.qi[found] = q.qi[found] or {} for _, sk in ipairs(r.srcs) do q.qi[found][sk] = (q.qi[found][sk] or 0) + 1 end end FW:Fire("CODEX_CHANGED", "quest", questID) end end -- Quest-Zuordnungen von Hand: Index { kill[questID] = { ["u38"]=true }, item[questID][itemID] = { ["u38"]=true } } local qsrc = { rev = -1 } local function QuestSrcIndex() local V = Codex.V() if qsrc.rev == Codex.chainRev and qsrc.v == V then return qsrc end qsrc = { rev = Codex.chainRev, v = V, kill = {}, item = {}, rel = {}, relOf = {} } for _, def in ipairs({ { "units", "u" }, { "objects", "o" } }) do for id, e in pairs(V[def[1]] or {}) do local ed = type(e) == "table" and e.ed or nil if type(ed) == "table" then for _, qid in ipairs(ed.qk or {}) do local t = qsrc.kill[qid] if not t then t = {} qsrc.kill[qid] = t end t[def[2] .. id] = true end for _, s in ipairs(ed.qd or {}) do local q, it = tostring(s):match("^(%d+):(%d+)$") q, it = tonumber(q), tonumber(it) if q and it then local byItem = qsrc.item[q] if not byItem then byItem = {} qsrc.item[q] = byItem end local t = byItem[it] if not t then t = {} byItem[it] = t end t[def[2] .. id] = true end end end end end -- gehoert zur Quest: von der Kreatur/dem Objekt/dem Item aus (qb) und von der Quest aus (qr) local function Rel(qid, letter, id) local r = qsrc.rel[qid] if not r then r = { i = {}, u = {}, o = {} } qsrc.rel[qid] = r end r[letter][id] = true local key = letter .. id local of = qsrc.relOf[key] if not of then of = {} qsrc.relOf[key] = of end of[qid] = true end for _, def in ipairs({ { "units", "u" }, { "objects", "o" }, { "ied", "i" } }) do for id, e in pairs(V[def[1]] or {}) do local ed = type(e) == "table" and e.ed or nil if type(ed) == "table" then for _, qid in ipairs(ed.qb or {}) do Rel(qid, def[2], id) end end end end for qid, q in pairs(V.quests or {}) do local ed = type(q) == "table" and q.ed or nil if type(ed) == "table" then for _, tok in ipairs(ed.qr or {}) do local k, n = tostring(tok):match("^([iuo]):(%d+)$") if k and n then Rel(qid, k, tonumber(n)) end end end end return qsrc end -- Zugeordnet zu einer Quest: { i = {Item-IDs}, u = {Kreatur-IDs}, o = {Objekt-IDs} } (sortiert) function Codex.QuestRelated(qid) local r = QuestSrcIndex().rel[qid] local out = { i = {}, u = {}, o = {} } if r then for letter in pairs(out) do for id in pairs(r[letter]) do out[letter][#out[letter] + 1] = id end table.sort(out[letter]) end end return out end -- Quests, zu denen ein Item (letter "i"), eine Kreatur ("u") oder ein Objekt ("o") gehoert (sortiert) function Codex.EntityQuests(letter, id) local set = QuestSrcIndex().relOf[letter .. id] local out = {} for qid in pairs(set or {}) do out[#out + 1] = qid end table.sort(out) return out end function Codex.QuestKillKeys(qid) return QuestSrcIndex().kill[qid] or {} end function Codex.QuestItemKeys(qid, itemID) local byItem = QuestSrcIndex().item[qid] return byItem and byItem[itemID] or {} end function Codex.QuestManualItems(qid) local out = {} for itemID in pairs(QuestSrcIndex().item[qid] or {}) do out[#out + 1] = itemID end table.sort(out) return out end -- Was ein Eintrag von Hand zugeordnet hat: Ziel-Quests (Liste) und Quest-Items (Liste "Quest:Item") function Codex.ManualQuestLinks(kind, id) local V = Codex.V() local e = (kind == "units" and V.units or V.objects)[id] local ed = type(e) == "table" and e.ed or nil return ed and ed.qk or {}, ed and ed.qd or {} end -- Items eines Ziels: gelernte Item-ID, Namenstreffer oder (bei genau einem Item-Ziel) die Quest-Items der Quest function Codex.ObjectiveItems(q, o, qid) local ids = {} if o.item then ids[1] = o.item return ids end local lname = o.name and o.name:lower() if lname then for itemID, name in pairs(Codex.db.names) do if name:lower() == lname then ids[#ids + 1] = itemID end end end if #ids == 0 and o.ty == "item" and q.qi then local itemObjs = 0 for _, ob in ipairs(q.obj or {}) do if ob.ty == "item" then itemObjs = itemObjs + 1 end end if itemObjs == 1 then for itemID in pairs(q.qi) do ids[#ids + 1] = itemID end end end -- von Hand eingetragene Quest-Items (bei genau einem Item-Ziel) if #ids == 0 and o.ty == "item" and qid then local itemObjs = 0 for _, ob in ipairs(q.obj or {}) do if ob.ty == "item" then itemObjs = itemObjs + 1 end end if itemObjs == 1 then for _, itemID in ipairs(Codex.QuestManualItems(qid)) do ids[#ids + 1] = itemID end end end table.sort(ids) return ids end -- Kill-Ziele einer Quest mit den Kreaturen der Vorlage verknuepfen (IDs eintragen, fehlende Namen auffuellen). -- Erst nach Namen; Ziele ohne Namen nur der Reihe nach und nur, wenn genau so viele Kreaturen uebrig sind wie Ziele. function Codex.LinkObjectives(q, qid) local Seed = Codex.Seed if not (q and q.obj and Seed and Seed.Ready and Seed.Ready()) then return false end local sq = Seed.Quest(qid) if not sq then return false end local units = {} for _, r in ipairs(sq.obj or {}) do if r.kind == "units" then units[#units + 1] = r.id end end if #units == 0 then return false end local mons = {} for _, o in ipairs(q.obj) do if o.ty == "monster" then mons[#mons + 1] = o end end if #mons == 0 then return false end local changed, used = false, {} for _, o in ipairs(mons) do if o.name and not Codex.IsBadObjName(o.name) then local l = o.name:lower() for _, uid in ipairs(units) do local nm = Seed.Name("units", uid) if nm and nm:lower() == l then used[uid] = true if not (o.ids and o.ids[uid]) then o.ids = o.ids or {} o.ids[uid] = 1 changed = true end end end end end local open, free = {}, {} for _, o in ipairs(mons) do if (not o.name or Codex.IsBadObjName(o.name)) and not (o.ids and next(o.ids)) then open[#open + 1] = o end end for _, uid in ipairs(units) do if not used[uid] then free[#free + 1] = uid end end if #open > 0 and #open == #free then for i, o in ipairs(open) do local nm = Seed.Name("units", free[i]) if nm then o.name = nm o.ids = { [free[i]] = 1 } changed = true end end end return changed end -- einmal je Sitzung: alle bekannten Quests nachtragen (sobald die Vorlage geladen ist) function Codex.RepairObjectives() if Codex.objRepaired or not (Codex.db and Codex.db.quests) then return 0 end local Seed = Codex.Seed if not (Seed and Seed.Ready and Seed.Ready()) then return 0 end Codex.objRepaired = true local n = 0 for qid, q in pairs(Codex.db.quests) do if type(q) == "table" and q.obj and Codex.LinkObjectives(q, qid) then n = n + 1 end end return n end function Codex:LearnKillTarget(objEntry, o) local guid = UnitGUID and UnitGUID("target") local kind, id = ParseGUID(guid) if kind ~= "npc" then return end local name = Str(UnitName("target"), 60) if not name then return end local want = (objEntry.name or o.text or ""):lower() local have = name:lower() if want == "" or not (want == have or want:find(have, 1, true) or have:find(want, 1, true)) then return end objEntry.ids = objEntry.ids or {} if objEntry.ids[id] == nil then local n = 0 for _ in pairs(objEntry.ids) do n = n + 1 end if n >= MAX_IDS then return end end objEntry.ids[id] = (objEntry.ids[id] or 0) + 1 local u = self:Unit(id, true) u.n = u.n or name end ------------------------------------------------------------------------ -- Ruf ------------------------------------------------------------------------ local function FormatToPattern(fmt) if type(fmt) ~= "string" then return nil end local order = {} local marked = fmt:gsub("%%(%d?)%$?([sd])", function(_, kind) order[#order + 1] = kind return "\1" .. kind .. "\2" end) marked = marked:gsub("([%^%$%(%)%.%[%]%*%+%-%?%%])", "%%%1") marked = marked:gsub("\1s\2", "(.-)"):gsub("\1d\2", "(%%d+)") return "^" .. marked .. "$", order end local repPatterns = {} local function BuildRepPatterns() repPatterns = {} for _, def in ipairs({ { "FACTION_STANDING_INCREASED", 1 }, { "FACTION_STANDING_DECREASED", -1 } }) do local fmt = _G[def[1]] local pattern, order = FormatToPattern(fmt) if pattern then local nameIdx, amountIdx for i, kind in ipairs(order) do if kind == "s" and not nameIdx then nameIdx = i end if kind == "d" and not amountIdx then amountIdx = i end end repPatterns[#repPatterns + 1] = { pattern = pattern, name = nameIdx or 1, amount = amountIdx or 2, sign = def[2] } end end end function Codex:OnFactionMessage(text) Safe(function() text = Util.Clean(text) if type(text) ~= "string" then return end if #repPatterns == 0 then BuildRepPatterns() end for _, p in ipairs(repPatterns) do local caps = { text:match(p.pattern) } if #caps >= 2 then local amount = tonumber(caps[p.amount]) local name = Str(caps[p.name], 60) if amount and name then state.repBuf[#state.repBuf + 1] = { t = GetTime and GetTime() or 0, name = name, amount = amount * p.sign } while #state.repBuf > 20 do table.remove(state.repBuf, 1) end end return end end end) end ------------------------------------------------------------------------ -- Quest-Ereignisse ------------------------------------------------------------------------ -- Item aus der Tasche benutzt (Rechtsklick): merken, damit eine Quest ohne NPC dem Item zugeordnet werden kann function Codex.NoteItemUse(bag, slot) Safe(function() local id local ci = _G.C_Container if ci and ci.GetContainerItemID then id = ci.GetContainerItemID(bag, slot) elseif _G.GetContainerItemID then id = _G.GetContainerItemID(bag, slot) end id = Num(id) if id then state.itemUse = { id = id, t = GetTime and GetTime() or 0 } end end) end -- Quest-Texte: bereinigt (keine Farbcodes), Absatz als {br}, der eigene Charaktername als $N local function ReplaceWord(s, word, repl) local low, lw, out, pos = s:lower(), word:lower(), {}, 1 local n = 0 while true do local a, b = low:find(lw, pos, true) if not a then break end local before = a > 1 and s:sub(a - 1, a - 1) or "" local after = s:sub(b + 1, b + 1) n = n + 1 if not before:find("[%a\128-\255]") and not after:find("[%a\128-\255]") then out[n] = s:sub(pos, a - 1) .. repl else out[n] = s:sub(pos, b) end pos = b + 1 end out[n + 1] = s:sub(pos) return table.concat(out) end local function CleanQText(s, cap) if type(s) ~= "string" or s == "" then return nil end s = s:gsub("|[cC]%x%x%x%x%x%x%x%x", ""):gsub("|[rR]", "") s = s:gsub("\r", ""):gsub("\n%s*\n+", "{br}"):gsub("\n", " ") s = s:gsub("|", ""):gsub("%c", " "):gsub("%s+", " ") s = s:gsub("^%s+", ""):gsub("%s+$", "") local name = UnitName and UnitName("player") if type(name) == "string" and #name >= 2 then s = ReplaceWord(s, name, "$N") end if #s < 3 then return nil end return s:sub(1, cap) end Codex.CleanQText = CleanQText local QTEXT_CAP = { d = 900, o = 400, p = 400, r = 600 } -- Merkt Texte in der Quest (nur was noch fehlt). Rueckgabe: true, wenn etwas Neues dazukam function Codex.StoreQText(q, tx) if type(q) ~= "table" or type(tx) ~= "table" then return false end local added = false for f in pairs(QTEXT_CAP) do local v = tx[f] if type(v) == "string" and v ~= "" then q.tx = q.tx or {} if not q.tx[f] then q.tx[f] = v added = true end end end return added end -- Quest zu einem offenen Quest-Fenster (Fortschritt/Abgabe): ueber die Quest-ID, sonst ueber den Titel local function QuestForWindow() local qid = Num(GetQuestID and GetQuestID()) local quests = Codex.db and Codex.db.quests if type(quests) ~= "table" then return nil end if qid and quests[qid] then return qid, quests[qid] end local title = Str(GetTitleText and GetTitleText(), 100) if not title then return nil end local found, foundQ for id, q in pairs(quests) do if type(q) == "table" and q.t == title then if found then return nil end -- nicht eindeutig found, foundQ = id, q end end return found, foundQ end function Codex:OnQuestProgress() Safe(function() local id, q = QuestForWindow() if not q then return end if Codex.StoreQText(q, { p = CleanQText(GetProgressText and GetProgressText(), QTEXT_CAP.p) }) then FW:Fire("CODEX_CHANGED", "quest", id) end end) end function Codex:OnQuestDetail(startItem) Safe(function() local si = Num(startItem) and Num(startItem) > 0 and Num(startItem) or nil local nowT = GetTime and GetTime() or 0 local giver0 = ReadInteraction() local learned if not si and giver0.k == "other" and state.itemUse and nowT - state.itemUse.t <= 20 then si, learned = state.itemUse.id, true -- Quest ohne NPC kurz nach Item-Benutzung: das Item startet sie end Codex.lastStartItem = { id = si, learned = learned, giver = giver0.k, at = nowT, used = state.itemUse and state.itemUse.id } state.detail = { si = si, t = GetTime and GetTime() or 0, title = Str(GetTitleText and GetTitleText(), 100), qid = Num(GetQuestID and GetQuestID()), tx = { d = CleanQText(GetQuestText and GetQuestText(), QTEXT_CAP.d), o = CleanQText(GetObjectiveText and GetObjectiveText(), QTEXT_CAP.o), }, giver = giver0, rew = ReadRewards(), } Codex.LearnChainOffer(state.detail) end) end -- Folgequest lernen: Das Spiel bietet die naechste Quest einer Kette gleich nach der Abgabe beim selben NPC an. -- Erkannt wird sie am Angebot (QUEST_DETAIL) in den ersten Sekunden nach der Abgabe; eine Quest, die man spaeter -- von Hand beim selben NPC waehlt, zaehlt nicht. Es zaehlt der Zeitpunkt des Angebots, nicht der der Annahme, und -- die Reihenfolge von Abgabe und Angebot ist egal (das Spiel meldet beides fast gleichzeitig). Codex.CHAIN_WINDOW = 4 local function LinkChain(q, questID, t, giver) local last = state.lastTurnIn if not (last and q) or last.qid == questID then return false end if math.abs((t or 0) - (last.t or 0)) > Codex.CHAIN_WINDOW then return false end -- derselbe NPC (oder dasselbe Objekt) local g, tn = giver, last.npc if g and tn and g.id and tn.id and (g.id ~= tn.id or g.k ~= tn.k) then return false end q.pre = q.pre or {} for _, p in ipairs(q.pre) do if p == last.qid then return false end end if #q.pre >= 8 then return false end q.pre[#q.pre + 1] = last.qid Codex.chainRev = (Codex.chainRev or 0) + 1 return true end function Codex.LearnChain(q, questID, detail) if not detail then return false end return LinkChain(q, questID, detail.t, detail.giver) end -- Wird diese Quest gerade als unmittelbare Folgequest der letzten Abgabe angeboten (selber NPC, wenige Sekunden)? Rueckgabe: Quest-ID der Abgabe local function FollowUpOf(detail) local last = state.lastTurnIn if not (detail and detail.qid and last) or last.qid == detail.qid then return nil end if math.abs((detail.t or 0) - (last.t or 0)) > Codex.CHAIN_WINDOW then return nil end local g, tn = detail.giver, last.npc if g and tn and g.id and tn.id and (g.id ~= tn.id or g.k ~= tn.k) then return nil end return last.qid end -- Schon beim Angebot (ohne Annahme): die Quest wird nach der Abgabe vom selben NPC angeboten local ChainOfferCore function Codex.LearnChainOffer(detail) local from = FollowUpOf(detail) local learned = ChainOfferCore(detail) -- der Levelplaner nimmt spontane Folgequests in seinen Guide auf if from then FW:Fire("CODEX_FOLLOWUP_OFFER", detail.qid, from, detail) end return learned end ChainOfferCore = function(detail) if not (detail and detail.qid and state.lastTurnIn) then return false end local V = Codex.db.quests local q = V[detail.qid] if not q then if not detail.title then return false end -- Probelauf, ohne den Datensatz anzulegen, wenn es keine Kette waere local last = state.lastTurnIn if last.qid == detail.qid or math.abs((detail.t or 0) - (last.t or 0)) > Codex.CHAIN_WINDOW then return false end local g, tn = detail.giver, last.npc if g and tn and g.id and tn.id and (g.id ~= tn.id or g.k ~= tn.k) then return false end q = Codex:Quest(detail.qid, true) q.t = detail.title if q.gif then q.give = detail.giver or q.give q.gif = nil else q.give = q.give or detail.giver end end if LinkChain(q, detail.qid, detail.t, detail.giver) then FW:Fire("CODEX_CHANGED", "quest", detail.qid) return true end return false end -- Vorquests einer Quest: aus den Daten (gelernt oder mitgeliefert) und der Classic-Vorlage -- Quest, die ein Gegenstand startet: Item-ID oder nil function Codex.QuestStartItem(id) local q = Codex.V().quests[id] return q and q.si or nil end -- Ketten-Index: Folgequests je Quest (aus den Vorquests der Daten und den "Folgequests" der Devs) und -- Vorquests, die Devs ueber die Folgequest-Liste der Elternquest nachtragen local nextCache = { view = nil, rev = -1, idx = nil, devPre = nil } Codex.chainRev = 0 local function ChainCache() local V = Codex.V() if nextCache.view ~= V or nextCache.rev ~= Codex.chainRev or not nextCache.idx then local idx, devPre = {}, {} local function Link(parent, child) idx[parent] = idx[parent] or {} for _, x in ipairs(idx[parent]) do if x == child then return end end table.insert(idx[parent], child) end for qid, q in pairs(V.quests) do for _, p in ipairs(q.pre or {}) do if p ~= qid then Link(p, qid) end end for _, n in ipairs(q.ed and q.ed.nx or {}) do if n ~= qid then Link(qid, n) devPre[n] = devPre[n] or {} table.insert(devPre[n], qid) end end end -- Folgequests aus dem Client-Cache des Servers (CodexWdbChain.lua) local wdbPre = {} for parent, child in pairs(Codex.WdbNext or {}) do if parent ~= child then Link(parent, child) wdbPre[child] = wdbPre[child] or {} table.insert(wdbPre[child], parent) end end for _, l in pairs(idx) do table.sort(l) end for _, l in pairs(devPre) do table.sort(l) end for _, l in pairs(wdbPre) do table.sort(l) end nextCache = { view = V, rev = Codex.chainRev, idx = idx, devPre = devPre, wdbPre = wdbPre } end return nextCache end -- Vorquests einer Quest: Dev-Eintrag ersetzt alles (Daten, Vorlage); sonst Daten, Vorlage und Beobachtungen function Codex.QuestPre(id) local out, have = {}, {} local q = Codex.V().quests[id] local dev = q and q.ed and q.ed.pre if dev then for _, p in ipairs(dev) do if p ~= id and not have[p] then have[p] = true out[#out + 1] = p end end else for _, p in ipairs(q and q.pre or {}) do if p ~= id and not have[p] then have[p] = true out[#out + 1] = p end end local sq = Codex.Seed and Codex.Seed.Ready() and Codex.Seed.Quest(id) or nil for _, p in ipairs(sq and sq.pre or {}) do if p ~= id and not have[p] then have[p] = true out[#out + 1] = p end end -- Aus Beobachtungen abgeleitet (nur wenn nichts Sicheres bekannt ist) if #out == 0 and Codex.Avail and q then local own = Codex.db.quests[id] for _, p in ipairs(Codex.Avail.InferredPre(q, own ~= nil and (own.po or 0) >= 1)) do if p ~= id then out[#out + 1] = p end end end end -- Elternquests, die diese Quest als Folgequest eingetragen haben for _, p in ipairs(ChainCache().devPre[id] or {}) do if p ~= id and not have[p] then have[p] = true out[#out + 1] = p end end if not dev then for _, p in ipairs(ChainCache().wdbPre[id] or {}) do if p ~= id and not have[p] then have[p] = true out[#out + 1] = p end end end return out end -- Folgequests: Quests, die diese als Vorquest haben (Daten, Vorlage, Dev-Eintraege) function Codex.QuestNext(id) local V = Codex.V() local out, have = {}, {} for _, n in ipairs(ChainCache().idx[id] or {}) do have[n] = true out[#out + 1] = n end if Codex.Seed and Codex.Seed.Ready() then for _, n in ipairs(Codex.Seed.NextQuests(id)) do local qn = V.quests[n] local devPre = qn and qn.ed and qn.ed.pre local overridden = false if devPre then overridden = true for _, p in ipairs(devPre) do if p == id then overridden = false end end end if not have[n] and n ~= id and not overridden then have[n] = true out[#out + 1] = n end end end table.sort(out) return out end -- Stellung in einer Kette: "start", "mid", "end" oder nil (keine Kette) function Codex.ChainRole(id) local hasPre, hasNext = #Codex.QuestPre(id) > 0, #Codex.QuestNext(id) > 0 if hasPre and hasNext then return "mid" end if hasNext then return "start" end if hasPre then return "end" end return nil end function Codex:OnQuestAccepted(a, b) Safe(function() local questID = Num(b) or Num(a) if not questID then return end if Codex.InferDone then Codex.InferDone({ questID }) end local q = self:Quest(questID, true) q.seen = (q.seen or 0) + 1 q.last = Now() q.partial = nil local d = state.detail local now = GetTime and GetTime() or 0 if d and now - d.t < 300 and (not d.qid or d.qid == questID) then q.t = d.title or q.t if d.si then q.si = d.si else q.give = d.giver or q.give end ApplyRewards(q, d.rew, false) Codex.StoreQText(q, d.tx) if d.giver and d.giver.m then q.zone = d.giver.m end end state.detail = nil Codex.LearnChain(q, questID, d) local title, level = QuestLogInfo(questID) q.t = q.t or title q.lvl = level or q.lvl local pl = Num(UnitLevel and UnitLevel("player")) if pl then q.pl = q.pl and math.min(q.pl, pl) or pl end local list = ReadObjectives(questID) StoreObjectives(q, list) TrackObjectives(questID, list) FW:Fire("CODEX_CHANGED", "quest", questID) end) end function Codex:OnQuestComplete() Safe(function() local qid0, q0 = QuestForWindow() if q0 and Codex.StoreQText(q0, { r = CleanQText(GetRewardText and GetRewardText(), QTEXT_CAP.r) }) then FW:Fire("CODEX_CHANGED", "quest", qid0) end state.turn = { t = GetTime and GetTime() or 0, qid = Num(GetQuestID and GetQuestID()), npc = ReadInteraction(), rew = ReadRewards(), } end) end -- Merkt sich die gewaehlte Belohnung (Aufruf von GetQuestReward(index)) function Codex:OnChoice(index) Safe(function() local turn = state.turn index = Num(index) if turn and index and turn.rew and turn.rew.choice[index] then turn.chosen = turn.rew.choice[index].id end end) end -- Luecken fuellen: Zone aus Geber/Abgabe-NPC, Geber aus der Abgabe der (einzigen) Vorquest. Echte Werte ersetzen die abgeleiteten (q.gif). function Codex.FillGaps() local quests = Codex.db and Codex.db.quests if type(quests) ~= "table" then return 0 end local n = 0 for id, q in pairs(quests) do if type(q) == "table" then if not q.zone then local sp = q.turn or q.give if type(sp) == "table" and type(sp.m) == "number" then q.zone = sp.m n = n + 1 end end if not q.give and not q.si and type(q.pre) == "table" and #q.pre == 1 then local p = quests[q.pre[1]] local tn = type(p) == "table" and p.turn or nil if type(tn) == "table" and tn.id then q.give = { n = tn.n, m = tn.m, x = tn.x, y = tn.y, id = tn.id, k = tn.k } q.gif = true n = n + 1 end end end end if n > 0 then Codex.chainRev = (Codex.chainRev or 0) + 1 end return n end function Codex:OnQuestTurnedIn(questID, xp, money) Safe(function() questID = Num(questID) if not questID then return end if Codex.MarkTurned then Codex.MarkTurned(questID, true) end local q = self:Quest(questID, true) q.done = (q.done or 0) + 1 q.last = Now() q.partial = nil xp, money = Num(xp), Num(money) if xp and xp > 0 then q.xp = xp end if money then q.money = money end local turn = state.turn local tnow = GetTime and GetTime() or 0 if turn and tnow - turn.t < 300 and (not turn.qid or turn.qid == questID) then q.turn = turn.npc or q.turn ApplyRewards(q, turn.rew, true) if xp and xp > 0 then q.xp = xp end if money then q.money = money end if turn.chosen then q.chosen = q.chosen or {} q.chosen[turn.chosen] = (q.chosen[turn.chosen] or 0) + 1 end end state.turn = nil state.objState[questID] = nil q.t = q.t or QuestLogInfo(questID) -- fuer das Lernen von Folgequests: diese Abgabe samt NPC und Zeit merken state.lastTurnIn = { qid = questID, t = tnow, npc = q.turn } -- Ruf: Meldungen kurz vor und nach der Abgabe einsammeln local t0 = tnow C_Timer.After(2, function() Safe(Codex.FillGaps) end) C_Timer.After(1.5, function() Safe(function() local rep for _, e in ipairs(state.repBuf) do if e.t >= t0 - 1 and e.t <= t0 + 2 then rep = rep or {} rep[e.name] = (rep[e.name] or 0) + e.amount end end if rep then q.rep = rep FW:Fire("CODEX_CHANGED", "quest", questID) end end) end) FW:Fire("CODEX_CHANGED", "quest", questID) end) end function Codex:OnQuestRemoved(questID) questID = Num(questID) if questID then state.objState[questID] = nil end end function Codex:OnQuestLogUpdate() if self.checkTimer then return end self.checkTimer = true C_Timer.After(0.4, function() Codex.checkTimer = nil Safe(Codex.CheckObjectives, Codex) end) end -- Bereits angenommene Quests im Log erfassen (ohne Geber) function Codex:ScanLog() Safe(function() local count if C_QuestLog and C_QuestLog.GetNumQuestLogEntries then count = C_QuestLog.GetNumQuestLogEntries() elseif GetNumQuestLogEntries then count = GetNumQuestLogEntries() end local activeIDs = {} for i = 1, Num(count) or 0 do local title, level, questID, isHeader if C_QuestLog and C_QuestLog.GetInfo then local info = C_QuestLog.GetInfo(i) if type(info) == "table" then title, level, questID, isHeader = info.title, info.level, info.questID, info.isHeader end elseif GetQuestLogTitle then local t, l, _, h, _, _, _, id = GetQuestLogTitle(i) title, level, questID, isHeader = t, l, id, h end questID = Num(questID) if questID and questID > 0 and not isHeader then activeIDs[#activeIDs + 1] = questID local q = self:Quest(questID, false) if not q then q = self:Quest(questID, true) q.seen = 0 q.partial = true q.t = Str(title, 100) q.lvl = Num(level) local list = ReadObjectives(questID) StoreObjectives(q, list) end TrackObjectives(questID, ReadObjectives(questID)) end end if Codex.InferDone then Codex.InferDone(activeIDs) end end) end ------------------------------------------------------------------------ -- Beute ------------------------------------------------------------------------ local function EntityFor(kind, id) if kind == "npc" then return Codex:Unit(id, true) elseif kind == "object" then return Codex:Object(id, true) end return nil end local function AddDrop(bucket, itemID, qty, isQuest) bucket.d = bucket.d or {} local d = bucket.d[itemID] if not d then local n = 0 for _ in pairs(bucket.d) do n = n + 1 if n >= MAX_DROPS then return end end d = { n = 0, q = 0 } bucket.d[itemID] = d end d.n = d.n + 1 d.q = d.q + (qty or 1) if isQuest then d.qi = 1 end end -- Octo/1.12: GetLootSlotInfo meldet nicht, ob ein Item ein Quest-Item ist. Ersatz: Name oder ID passt zu einem Item-Ziel -- einer Quest im Log. IDs der Quests im Log: einmal je Beutefenster gelesen. local function LogQuestIDs() local out = {} local count if C_QuestLog and C_QuestLog.GetNumQuestLogEntries then count = C_QuestLog.GetNumQuestLogEntries() elseif GetNumQuestLogEntries then count = GetNumQuestLogEntries() end for i = 1, Num(count) or 0 do local questID, isHeader if C_QuestLog and C_QuestLog.GetInfo then local info = C_QuestLog.GetInfo(i) if type(info) == "table" then questID, isHeader = info.questID, info.isHeader end elseif GetQuestLogTitle then local _, _, _, h, _, _, _, id = GetQuestLogTitle(i) questID, isHeader = id, h end questID = Num(questID) if questID and questID > 0 and not isHeader then out[#out + 1] = questID end end return out end function Codex:QuestForLootItem(ids, itemID, lootName) local want = type(lootName) == "string" and lootName:lower() or nil for _, qid in ipairs(ids) do local q = self:Quest(qid, false) for _, ob in ipairs(q and q.obj or {}) do if ob.ty == "item" and ((ob.item and ob.item == itemID) or (want and ob.name and ob.name:lower() == want)) then return qid end end end return nil end -- Wird dieses Item gerade fuer eine Quest im Log gebraucht? (Item-Ziel passt nach ID oder Name und ist noch nicht erfuellt) local function NeedsItem(self, logIDs, live, itemID, lootName) local want = type(lootName) == "string" and lootName:lower() or nil for _, qid in ipairs(logIDs) do local q = self:Quest(qid, false) if q and q.obj then for i, ob in ipairs(q.obj) do if ob.ty == "item" and ((ob.item and ob.item == itemID) or (want and ob.name and ob.name:lower() == want)) then local objs = live[qid] if not objs then objs = ReadObjectives(qid) live[qid] = objs end local lv = objs[i] if not (lv and lv.done) then return true end end end end end return false end function Codex:OnLootOpened() Safe(function() if not self.db.settings.enabled or not GetNumLootItems then return end local windows, order = {}, {} local logIDs for slot = 1, GetNumLootItems() do local sources = { GetLootSourceInfo(slot) } local stype = GetLootSlotType and Num(GetLootSlotType(slot)) local link = GetLootSlotLink and Util.Clean(GetLootSlotLink(slot)) local itemID = ItemIDFromLink(link) local _, lootName, lootQty, _, _, _, isQuest, questID if GetLootSlotInfo then _, lootName, lootQty, _, _, _, isQuest, questID = GetLootSlotInfo(slot) end logIDs = logIDs or LogQuestIDs() if itemID and not isQuest then local qid = self:QuestForLootItem(logIDs, itemID, lootName) if qid then isQuest, questID = true, qid end end if itemID then NoteName(itemID, lootName) -- Alle gelooteten Items merken (nicht nur gemeldete Quest-Items): Der Client sagt auf Octo nicht, -- ob ein Item zu einer Quest gehoert, und Item- und Zielnamen stehen in verschiedenen Sprachen. -- Steigt spaeter ein Sammel-Zaehler, wird das Item aus diesen Kandidaten abgeleitet. local recent = state.recentItems local srcs = {} for si = 1, #sources, 2 do local g = sources[si] if type(g) == "string" and not Util.IsSecret(g) then local sk, sid = ParseGUID(g) if sk and sid then srcs[#srcs + 1] = (sk == "npc" and "u" or "o") .. sid end end end recent[itemID] = { t = Now(), quest = isQuest and true or false, qid = Num(questID), qty = Num(lootQty) or 1, srcs = srcs } local n = 0 for _ in pairs(recent) do n = n + 1 end if n > 30 then local cutoff = Now() - RECENT_SECS for id, r in pairs(recent) do if r.t < cutoff then recent[id] = nil end end end end for i = 1, #sources, 2 do local guid, qty = sources[i], Num(sources[i + 1]) or 1 if type(guid) == "string" and not Util.IsSecret(guid) then local w = windows[guid] if not w then w = { items = {}, coin = 0, hasCoin = false } windows[guid] = w order[#order + 1] = guid end if itemID then local it = w.items[itemID] if not it then it = { q = 0, quest = (isQuest and true or false), qid = Num(questID), name = lootName } w.items[itemID] = it end it.q = it.q + qty elseif stype == 2 then local cq = Num(lootQty) w.coin = w.coin + ((cq and cq > 0 and #sources == 2) and cq or qty) w.hasCoin = true end end end end local targetGuid = UnitGUID and Util.Clean(UnitGUID("target")) for _, guid in ipairs(order) do local kind, id = ParseGUID(guid) local ent = kind and EntityFor(kind, id) if ent and kind == "object" and not ent.n then ent.n = state.objNames[id] if not ent.n and UnitGUID and UnitName and Util.Clean(UnitGUID("softinteract")) == guid then ent.n = Str(UnitName("softinteract"), 60) end end if ent then local w = windows[guid] local again = state.lootedGuids[guid] if not again then if state.lootedCount >= MAX_LOOTED_GUIDS then state.lootedGuids, state.lootedItems, state.lootedCount = {}, {}, 0 end state.lootedGuids[guid] = true state.lootedCount = state.lootedCount + 1 end -- zweites Beutefenster derselben Leiche: was schon gezaehlt ist (liegengelassen), nicht nochmal local seen = state.lootedItems[guid] if not seen then seen = {} state.lootedItems[guid] = seen end local fresh, freshCount = {}, 0 for itemID, it in pairs(w.items) do if not (again and seen[itemID]) then fresh[itemID] = it freshCount = freshCount + 1 end end local skip = again and freshCount == 0 and not (w.hasCoin and w.coin > 0) local bucket = ent if again and not skip then ent.alt = ent.alt or { k = 0 } bucket = ent.alt end if not skip then bucket.k = (bucket.k or 0) + 1 end if w.hasCoin and w.coin > 0 and not again then ent.coin = (ent.coin or 0) + w.coin ent.coinN = (ent.coinN or 0) + 1 end local srcKey = (kind == "npc" and "u" or "o") .. id -- Quest-Items zaehlen nur, solange die Quest laeuft und das Item noch gebraucht wird local live, neededNow = {}, {} for itemID, it in pairs(fresh) do seen[itemID] = true local old = bucket.d and bucket.d[itemID] local needed = true if it.quest or (old and old.qi) then needed = NeedsItem(self, logIDs or {}, live, itemID, it.name) neededNow[itemID] = needed end if needed then AddDrop(bucket, itemID, it.q, it.quest) end if it.quest and it.qid and it.qid > 0 then local q = self:Quest(it.qid, false) if q then q.qi = q.qi or {} q.qi[itemID] = q.qi[itemID] or {} q.qi[itemID][srcKey] = (q.qi[itemID][srcKey] or 0) + 1 end end end if not skip and not again and bucket.d then -- Nenner der Drop-Chance fuer Quest-Items: nur Leichen, bei denen die Quest lief for itemID, d in pairs(bucket.d) do local need = neededNow[itemID] if need == nil then need = NeedsItem(self, logIDs or {}, live, itemID, nil) end if d.qi and need then d.qk = (d.qk or 0) + 1 end end end if not again then ent.lt = Now() AddCell(ent) if kind == "npc" and targetGuid == guid then state.noteAt[id] = nil self:NoteUnit("target") end if kind == "npc" and self.Names then if ent.n then self.Names:Consume(ent.n) else self.Names:Resolve(id, ent, guid) end end end state.stats.loot = state.stats.loot + 1 FW:Fire("CODEX_CHANGED", kind == "npc" and "unit" or "object", id) end end end) end ------------------------------------------------------------------------ -- Erfahrung pro Kill ------------------------------------------------------------------------ -- Klassifizierung auf wenige Schluessel vereinfachen local function ClassKey(class) if class == "elite" or class == "rare" or class == "rareelite" or class == "worldboss" then return class end return "normal" end function Codex.GrayLevel(pl) if pl <= 5 then return 0 elseif pl <= 39 then return pl - 5 - math.floor(pl / 10) elseif pl <= 59 then return pl - 1 - math.floor(pl / 5) end return pl - 9 end function Codex.ZeroDiff(pl) if pl < 8 then return 5 end if pl <= 9 then return 6 end if pl <= 11 then return 7 end if pl <= 15 then return 8 end if pl <= 19 then return 9 end if pl <= 29 then return 11 end if pl <= 39 then return 12 end if pl <= 44 then return 13 end if pl <= 49 then return 14 end if pl <= 54 then return 15 end if pl <= 59 then return 16 end return 17 end -- Farbe des Gegners (red, orange, yellow, green, gray) function Codex.XPColor(pl, ml) if ml >= pl + 5 then return "red" end if ml >= pl + 3 then return "orange" end if ml >= pl - 2 then return "yellow" end if ml > Codex.GrayLevel(pl) then return "green" end return "gray" end -- Sicht fuer die Anzeige: eigene und empfangene Daten zusammen (ohne Fremddaten einfach die Datenbank) function Codex.V() local share = Codex.Share if share then return share.View() end return Codex.db end -- Kill-Erfahrung nach der klassischen Formel (Classic 1-60), nur als Ersatz fuer fehlende Messwerte -- Gegner gleich oder hoeher: Basis aus der Spielerstufe. Gegner niedriger: Basis aus der Gegnerstufe -- (45 + 5 x Gegnerstufe), so gemessen in Forever (Stufe 3 gegen 2 = 44, Stufe 2 gegen 1 = 40). function Codex.FormulaXP(pl, ml, class) local gain if ml >= pl then local base = pl * 5 + 45 local d = math.min(ml - pl, 4) gain = math.floor((base * (20 + d) / 10 + 1) / 2) elseif ml > Codex.GrayLevel(pl) then local zd = Codex.ZeroDiff(pl) gain = math.floor((ml * 5 + 45) * (zd + ml - pl) / zd) else gain = 0 end if class == "elite" or class == "rareelite" then gain = gain * 2 end return gain, Codex.XPColor(pl, ml) end -- Erfahrung fuer einen Kill: gemessener Wert (eigene und empfangene Messreihen), sonst die Formel. -- Rueckgabe: XP, Farbe, gemessen (true/false), Anzahl der Messwerte function Codex.PredictXP(pl, ml, class) local gain, color = Codex.FormulaXP(pl, ml, class) local share = Codex.Share if share and gain > 0 then local measured, n = share.XPMeasured(pl, ml, ClassKey(class)) if measured and measured > 0 then return measured, color, true, n end end return gain, color, false, 0 end local function AddSample(s, xp) s.n = (s.n or 0) + 1 s.sum = (s.sum or 0) + xp s.min = s.min and math.min(s.min, xp) or xp s.max = s.max and math.max(s.max, xp) or xp end local function Bucket(tbl, pl, ml, cls) tbl[pl] = tbl[pl] or {} tbl[pl][ml] = tbl[pl][ml] or {} tbl[pl][ml][cls] = tbl[pl][ml][cls] or {} return tbl[pl][ml][cls] end -- Alle Messreihen als Liste { pl, ml, cls, n, avg, min, max, pred, diff, bonus } function Codex:XPRows(includeBonus) local rows = {} for _, def in ipairs({ { Codex.V().xp, false }, { self.db.xpb, true }, { self.db.xr or {}, true, true } }) do if includeBonus or not def[2] then for pl, byMob in pairs(def[1]) do for ml, byClass in pairs(byMob) do for cls, s in pairs(byClass) do if s.n and s.n > 0 then local pred, color = Codex.FormulaXP(pl, ml, cls) local avg = s.sum / s.n rows[#rows + 1] = { pl = pl, ml = ml, cls = cls, n = s.n, avg = avg, min = s.min, max = s.max, pred = pred, color = color, diff = avg - pred, bonus = def[2], rested = def[3], } end end end end end end table.sort(rows, function(a, b) if a.pl ~= b.pl then return a.pl > b.pl end if a.ml ~= b.ml then return a.ml < b.ml end return a.cls < b.cls end) return rows end -- Muster aus den Spiel-Texten (COMBATLOG_XPGAIN_FIRSTPERSON...) bauen local xpPatterns, xpIgnore local function BuildXPPatterns() xpPatterns, xpIgnore = {}, {} for key, fmt in pairs(_G) do if type(key) == "string" and key:find("^COMBATLOG_XPGAIN_FIRSTPERSON") and type(fmt) == "string" then local pattern, order = FormatToPattern(fmt) if pattern then if key:find("UNNAMED") then xpIgnore[#xpIgnore + 1] = pattern else local nameIdx, xpIdx for i, kind in ipairs(order) do if kind == "s" and not nameIdx then nameIdx = i end if kind == "d" and not xpIdx then xpIdx = i end end local bonusKind if #order > 2 then bonusKind = key:find("RAID") and "raid" or key:find("GROUP") and "group" or "bonus" end xpPatterns[#xpPatterns + 1] = { pattern = pattern, name = nameIdx or 1, xp = xpIdx or 2, bonus = bonusKind, len = #fmt, } end end end end table.sort(xpPatterns, function(a, b) return a.len > b.len end) end function Codex:OnXPMessage(text) Safe(function() if self.Names then self.Names.xpSeen = self.Names.xpSeen + 1 if Util.IsSecret(text) then self.Names.fail.xpSecret = self.Names.fail.xpSecret + 1 end end text = Util.Clean(text) if type(text) ~= "string" or not self.db.settings.enabled then return end if not xpPatterns then BuildXPPatterns() end local matched for _, p in ipairs(xpPatterns) do local caps = { text:match(p.pattern) } if #caps >= 2 then matched = { name = Str(caps[p.name], 60), xp = tonumber(caps[p.xp]), bonus = p.bonus } break end end if not matched then -- Octo: Meldung mit Ruhebonus ("X dies, you gain 38 experience. (+16 exp Rested bonus)"): die Spiel-Texte passen nicht. -- Die genannten XP enthalten den Bonus, die Grund-XP sind die Differenz. local nm, gx, rb = text:match("^(.+) dies, you gain (%d+) experience%.%s*%(%+(%d+) exp [^%)]*bonus%)") if not nm then nm, gx = text:match("^(.+) dies, you gain (%d+) experience%.?$") end if nm and gx then local base = tonumber(gx) - (tonumber(rb) or 0) if base > 0 then matched = { name = Str(nm, 60), xp = base } end end end if not matched then for _, pattern in ipairs(xpIgnore) do if text:match(pattern) then return end end local list = self.db.xpUnparsed if #list < 5 then list[#list + 1] = text:sub(1, 200) end return end if not matched.name or not matched.xp or matched.xp <= 0 then return end if matched.name and not matched.bonus then local tk0, tid0 = ParseGUID(UnitGUID and UnitGUID("target")) FW:Fire("CODEX_KILL", matched.name, tk0 == "npc" and tid0 or nil) end -- Name der getoeteten Kreatur lernen (Ziel passt) oder fuer das naechste Beutefenster merken if self.Names then local tk, tid = ParseGUID(UnitGUID and UnitGUID("target")) local tn = Str(UnitName and UnitName("target"), 60) if tk == "npc" and tn == matched.name then self.Names:Learn(tid, tn, "xp") else self.Names:NoteKill(matched.name) end end -- Gegner: anvisiert und gleicher Name, sonst eindeutiger Name in der Datenbank local ml, cls, id local tkind, tid = ParseGUID(UnitGUID and UnitGUID("target")) if tkind == "npc" and Str(UnitName("target"), 60) == matched.name then ml = Num(UnitLevel("target")) cls = Str(UnitClassification and UnitClassification("target"), 16) id = tid else for uid, u in pairs(self.db.units) do if u.n == matched.name and u.l1 and u.l1 == u.l2 then if id then id = nil break end id, ml, cls = uid, u.l1, u.c end end end local pl = state.pl or Num(UnitLevel and UnitLevel("player")) -- Kill fuer die Kampfdauer melden, auch wenn die Gegnerstufe hier unbekannt ist -- (die Zeitmessung kennt die Stufe des anvisierten Gegners aus dem Kampf) if self.Time and not matched.bonus then self.Time:OnKill(pl, ml, id, ml and ClassKey(cls) or nil) end if not (pl and ml and ml > 0 and pl > 0) then return end cls = ClassKey(cls) if matched.bonus then AddSample(Bucket(self.db.xpb, pl, ml, cls), matched.xp) FW:Fire("CODEX_CHANGED", "xp", pl) return end -- Ruhebonus: steht in der Meldung nicht getrennt, also am Ruhe-Vorrat erkennen (er sinkt um den Bonus) local base = state.pool if base and base > 0 and C_Timer and C_Timer.After then local job = { pl = pl, ml = ml, cls = cls, id = id, xp = matched.xp, base = base } if state.xpOpen > 0 then -- mehrere Kills dicht hintereinander: der Bonus laesst sich nicht zuordnen job.unsure = true for _, j in ipairs(state.xpJobs) do j.unsure = true end end state.xpOpen = state.xpOpen + 1 state.xpJobs[#state.xpJobs + 1] = job C_Timer.After(0.4, function() Safe(function() Codex:FinishXP(job) end) end) return end self:RecordXP(pl, ml, cls, id, matched.xp, nil) end) end -- Ruhe-Vorrat lesen (0 ohne Ruhebonus, nil wenn die Abfrage fehlt) local function ReadPool() if type(_G.GetXPExhaustion) ~= "function" then return nil end local ok, v = pcall(_G.GetXPExhaustion) if not ok then return nil end return tonumber(v) or 0 end -- normal: Erfahrung ohne Ruhebonus; total: mit Ruhebonus (nil, wenn keiner dabei war) function Codex:RecordXP(pl, ml, cls, id, normal, total) self.db.xr = self.db.xr or {} AddSample(Bucket(self.db.xp, pl, ml, cls), normal) if total then AddSample(Bucket(self.db.xr, pl, ml, cls), total) end if id then local u = self:Unit(id, true) u.xp = u.xp or {} u.xp[pl] = u.xp[pl] or {} AddSample(u.xp[pl], normal) -- zusaetzlich nach Gegnerstufe getrennt (Kreaturen mit Stufe 1-2 usw.) u.xm = u.xm or {} u.xm[pl] = u.xm[pl] or {} u.xm[pl][ml] = u.xm[pl][ml] or {} AddSample(u.xm[pl][ml], normal) if total then u.xr = u.xr or {} u.xr[pl] = u.xr[pl] or {} u.xr[pl][ml] = u.xr[pl][ml] or {} AddSample(u.xr[pl][ml], total) end end FW:Fire("CODEX_CHANGED", "xp", pl) if id then FW:Fire("CODEX_CHANGED", "unit", id) end end function Codex:FinishXP(job) state.xpOpen = math.max(0, state.xpOpen - 1) for i, j in ipairs(state.xpJobs) do if j == job then table.remove(state.xpJobs, i) break end end local after = ReadPool() if after then state.pool = after end local bonus = after and (job.base - after) or 0 if bonus <= 0 then self:RecordXP(job.pl, job.ml, job.cls, job.id, job.xp, nil) elseif job.unsure or bonus >= job.xp then self.xpDropped = (self.xpDropped or 0) + 1 else self:RecordXP(job.pl, job.ml, job.cls, job.id, job.xp - bonus, job.xp) end end -- Ruhe-Vorrat merken: beim Kampfbeginn und ausserhalb des Kampfes (Ausruhen fuellt ihn) function Codex:OnPoolEvent(event) if event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_ENTERING_WORLD" or not (UnitAffectingCombat and UnitAffectingCombat("player")) then if state.xpOpen == 0 then state.pool = ReadPool() end end end function Codex:OnLevelUp(newLevel) newLevel = Num(newLevel) if newLevel then state.pl = newLevel end end ------------------------------------------------------------------------ -- Abfragen fuer Oberflaeche und andere Addons ------------------------------------------------------------------------ -- Quest-Status des eingeloggten Charakters: "active" (im Questlog), "done" (abgegeben) oder nil. -- Das Spiel kennt beides lokal, es wird nichts gespeichert. -- Simulation (Guide-Berechnung im Levelplaner): Codex.sim = { done = {}, active = {}, memo = {}, level = n }. -- Waehrend eine Teilstrecke gerechnet wird, gelten die simulierten Quests als erledigt oder angenommen; alles andere -- liefert das Spiel (einmal je Quest gemerkt, damit die Rechnung keine Schnittstellenaufrufe wiederholt). local realQuestStatus function Codex.QuestStatus(id) local sim = Codex.sim if sim then if sim.done[id] then return "done" end if sim.active[id] then return "active" end local m = sim.memo[id] if m == nil then m = realQuestStatus(id) or false sim.memo[id] = m end return m or nil end return realQuestStatus(id) end -- Eigene Quests (Freewood-OctoWoW-Creator): IDs ab LOCAL_BASE, Stand liefert das Creator-Modul (nie das Questlog des Spiels) Codex.LOCAL_BASE = 9000000 function Codex.IsLocalQuest(id) return type(id) == "number" and id >= Codex.LOCAL_BASE and id < Codex.LOCAL_BASE + 1000000 end -- Octo kennt keine Abfrage "Quest erledigt": Abgaben selbst merken, je Charakter (db.turned[Schluessel][questID]) local function TurnedSet(create) local db = Codex.db local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil if not db or not key then return nil end db.turned = db.turned or {} local t = db.turned[key] if not t and create ~= false then t = {} db.turned[key] = t -- einmalig und nur fuer den allerersten Charakter (Umstieg auf Speicherung je Charakter): bisher aufgezeichnete Abgaben -- diesem Charakter zuschreiben. Jeder weitere Charakter beginnt leer (sonst gaelten neue Charaktere als "alles erledigt"). local others = false for k, v in pairs(db.turned) do if k ~= key and type(v) == "table" then others = true break end end if not others then local V = Codex.V and Codex.V() for qid, q in pairs(V and V.quests or {}) do if type(q) == "table" and (q.done or 0) > 0 then t[qid] = true end end end end return t end function Codex.MarkTurned(id, on) local t = TurnedSet(true) if t and id then t[id] = on ~= false and true or nil end end -- Wer eine Folgequest im Log hat, hat die Vorquest erledigt (nur bei eindeutiger, einzelner Vorquest) function Codex.InferDone(ids) local t = TurnedSet(true) if not t then return end local n = 0 for _, id in ipairs(ids or {}) do local cur, depth = id, 0 while cur and depth < 15 do local pre = Codex.QuestPre and Codex.QuestPre(cur) or {} if #pre ~= 1 then break end cur = pre[1] if not t[cur] then t[cur] = true n = n + 1 end depth = depth + 1 end end if n > 0 then Codex.chainRev = (Codex.chainRev or 0) + 1 end return n end _G.SLASH_FWDONE1 = "/fwdone" _G.SlashCmdList = _G.SlashCmdList or {} _G.SlashCmdList["FWDONE"] = function(msg) if string.match(msg or "", "^%s*reset") then local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil local say0 = function(x) if _G.DEFAULT_CHAT_FRAME then _G.DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Freewood|r " .. x) end end if not (Codex.db and key) then say0("Charakter unbekannt.") return end if not string.match(msg, "reset%s+ja") then say0("/fwdone reset ja loescht die gemerkten Abgaben dieses Charakters (" .. tostring(key) .. "). Danach gilt er als ohne erledigte Quests; angenommene Quests bleiben. Dann im Planer NEU druecken.") return end Codex.db.turned = Codex.db.turned or {} Codex.db.turned[key] = {} Codex.chainRev = (Codex.chainRev or 0) + 1 say0("Abgaben dieses Charakters geloescht. Jetzt im Planer NEU druecken.") if _G.FW and _G.FW.Fire then _G.FW:Fire("QUEST_TURNED_IN_LOCAL", 0) end return end local id, undo = string.match(msg or "", "^(%d+)%s*(%a*)") id = tonumber(id) local say = function(x) if _G.DEFAULT_CHAT_FRAME then _G.DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Freewood|r " .. x) end end if not id then say("/fwdone [weg] markiert eine Quest als erledigt (oder wieder offen)") return end Codex.MarkTurned(id, undo ~= "weg") Codex.chainRev = (Codex.chainRev or 0) + 1 say(string.format("Quest %d: %s", id, undo == "weg" and "wieder offen" or "erledigt")) if _G.FW and _G.FW.Fire then _G.FW:Fire("QUEST_TURNED_IN_LOCAL", id) end end realQuestStatus = function(id) if Codex.LocalStatus and Codex.IsLocalQuest(id) then return Codex.LocalStatus(id) end local QL = _G.C_QuestLog if QL and QL.GetLogIndexForQuestID then local ok, idx = pcall(QL.GetLogIndexForQuestID, id) if ok and type(idx) == "number" and idx > 0 then return "active" end end do local t = TurnedSet(true) if t and t[id] then return "done" end end local fn = (QL and QL.IsQuestFlaggedCompleted) or _G.IsQuestFlaggedCompleted if type(fn) == "function" then local ok, done = pcall(fn, id) if ok and done then return "done" end end -- Ersatz: Liste aller erledigten Quests local list if QL and QL.GetAllCompletedQuestIDs then local ok, r = pcall(QL.GetAllCompletedQuestIDs) if ok and type(r) == "table" then for _, v in pairs(r) do if v == id then return "done" end end end elseif type(_G.GetQuestsCompleted) == "function" then local ok, r = pcall(_G.GetQuestsCompleted) if ok and type(r) == "table" and r[id] then return "done" end end return nil end function Codex:GetQuest(id) return Codex.V().quests[id] end -- Display-ID einer Kreatur (fuer das 2D-Bild in der Liste) Codex.dispStats = { read = 0, fail = 0, shown = 0 } function Codex:GetDisplayID(npcID) local d = self.db.disp and self.db.disp[npcID] return type(d) == "number" and d > 0 and d or nil end -- Octo: PlayerModel:SetCreature zeigt nur Kreaturen aus dem Client-Cache. Unbekannte -- werden zuerst angefragt (CREATURE_DATA_LOAD_RESULT), dann laeuft fn. local creatureWait = {} local function RunWaiting(id) local cbs = creatureWait[id] creatureWait[id] = nil if cbs then for _, fn in ipairs(cbs) do pcall(fn) end end end function Codex.EnsureCreature(id, fn) local C = _G.C_CreatureInfo if type(id) ~= "number" or not (C and C.RequestLoadCreatureByID) then fn() return end if creatureWait[id] then table.insert(creatureWait[id], fn) return end creatureWait[id] = { fn } if not Codex.creatureWatch then local w = CreateFrame("Frame") w:RegisterEvent("CREATURE_DATA_LOAD_RESULT") w:SetScript("OnEvent", function(_, _, a1) local got = tonumber(a1) or tonumber(_G.arg1) if got then RunWaiting(got) end end) Codex.creatureWatch = w end local ok = pcall(C.RequestLoadCreatureByID, id) if not ok then RunWaiting(id) return end if creatureWait[id] and C_Timer and C_Timer.After then C_Timer.After(4, function() RunWaiting(id) end) end end -- Display-ID direkt aus dem Kreatur-Cache (ClassicAPI), ohne Modell function Codex.CacheDisplayID(id) local C = _G.C_CreatureInfo if not (C and C.GetCreatureInfoByID) then return nil end local ok, info = pcall(C.GetCreatureInfoByID, id) if ok and type(info) == "table" then local d = tonumber(info.displayID) if d and d > 0 then return d end end return nil end function Codex:SetDisplayID(npcID, displayID) if type(npcID) ~= "number" or type(displayID) ~= "number" or displayID <= 0 then return false end self.db.disp = self.db.disp or {} if self.db.disp[npcID] ~= displayID then self.db.disp[npcID] = displayID return true end return false end ------------------------------------------------------------------------ -- Itemwerte (Staerke, Beweglichkeit, Ausdauer, Ruestung, Schaden pro Sekunde ...) ------------------------------------------------------------------------ -- Waffen: Schadensspanne und Tempo stehen nur im Tooltip (die Wertetabelle liefert nur den Schaden pro Sekunde). -- Gespeichert als _dmin, _dmax, _spd und _s (= gelesen) neben den anderen Werten. local scanTip local scanTried = {} local function ScanTooltip() if scanTip == nil then scanTip = false if CreateFrame and UIParent then local ok, tip = pcall(CreateFrame, "GameTooltip", "FreewoodCodexScanTip", nil, "GameTooltipTemplate") if ok and tip then scanTip = tip end end end return scanTip or nil end local function TemplatePattern(template) if type(template) ~= "string" or template == "" then return nil end local p = template:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1") p = p:gsub("%%%%[sd]", "(.-)") return "^" .. p .. "$" end local function LineText(name, i) local fs = _G[name .. i] local ok, text = pcall(function() return fs and fs.GetText and fs:GetText() end) if ok and type(text) == "string" and not Util.IsSecret(text) then return text end return nil end local function CleanLine(text) if type(text) ~= "string" or Util.IsSecret(text) then return nil end return (text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")) end -- Zeilen des Item-Tooltips { { links, rechts } }: erst die Tooltip-Daten (neue Clients), sonst ein versteckter Tooltip. -- Zweiter Rueckgabewert: woher sie stammen (fuer /fwcodex weapon) -- Tooltip noch nicht fertig? (nur "Frage Gegenstandsinformationen ab ..." oder gar keine Zeilen) local function LinesReady(lines) if #lines < 2 then return false end local wait = _G.RETRIEVING_ITEM_INFO if type(wait) == "string" and lines[1][1] == wait then return false end return true end -- Rueckgabe: Zeilen, Quelle, fertig (false = Itemdaten noch nicht geladen, spaeter nochmal) local function TooltipLines(itemID) local firstLines, firstSource if C_TooltipInfo and C_TooltipInfo.GetItemByID then local ok, data = pcall(C_TooltipInfo.GetItemByID, itemID) if ok and type(data) == "table" and type(data.lines) == "table" then local lines = {} for _, ln in ipairs(data.lines) do if type(ln) == "table" then lines[#lines + 1] = { CleanLine(ln.leftText), CleanLine(ln.rightText) } end end if LinesReady(lines) then return lines, "C_TooltipInfo", true end if #lines > 0 then firstLines, firstSource = lines, "C_TooltipInfo" end end end local tip = ScanTooltip() if not tip then return firstLines or {}, firstSource or "kein Tooltip", false end local ok = pcall(function() tip:SetOwner(UIParent, "ANCHOR_NONE") tip:ClearLines() tip:SetHyperlink("item:" .. itemID) end) if not ok then return firstLines or {}, firstSource or "Tooltip-Fehler", false end local lines = {} for i = 1, (tip.NumLines and tip:NumLines() or 0) do lines[#lines + 1] = { CleanLine(LineText("FreewoodCodexScanTipTextLeft", i)), CleanLine(LineText("FreewoodCodexScanTipTextRight", i)) } end pcall(tip.Hide, tip) if not LinesReady(lines) and firstLines then return firstLines, firstSource, false end return lines, "versteckter Tooltip", LinesReady(lines) end local function Number(text) return tonumber((tostring(text):gsub(",", "."))) end -- Schaden und Tempo aus den Zeilen. Fehlt das Tempo im Text, wird es aus Schaden und DPS errechnet. local function ParseWeapon(lines, dps) local patterns = { TemplatePattern(_G.DAMAGE_TEMPLATE_WITH_SCHOOL), TemplatePattern(_G.DAMAGE_TEMPLATE), } local speedWord = type(_G.WEAPON_SPEED) == "string" and _G.WEAPON_SPEED or "Speed" speedWord = speedWord:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1") local dmin, dmax, spd for _, ln in ipairs(lines) do local left, right = ln[1], ln[2] if left and not dmin then for _, pat in ipairs(patterns) do local a, b = left:match(pat) a, b = a and Number(a), b and Number(b) if a and b then dmin, dmax = a, b break end end if not dmin then -- allgemein: Zeile beginnt mit "min - max" local a, b = left:match("^(%d+[%.,]?%d*)%s*[%-\226\128\147\226\128\148]+%s*(%d+[%.,]?%d*)%s*%a") a, b = a and Number(a), b and Number(b) if a and b then dmin, dmax = a, b end end end if not spd then for _, text in ipairs({ left or "", right or "" }) do local v = text:match(speedWord .. "%s+([%d%.,]+)") v = v and Number(v) if v and v > 0 then spd = v break end end end end if dmin and dmax and not spd and dps and dps > 0 then spd = math.floor(((dmin + dmax) / 2 / dps) * 100 + 0.5) / 100 end if dmin and dmax and spd then return dmin, dmax, spd end return nil end local function DPSOf(stats) for key, value in pairs(stats) do if key:find("DAMAGE_PER_SECOND", 1, true) and type(value) == "number" then return value end end return nil end -- Rueckgabe: min, max, Tempo (oder nil, wenn der Tooltip noch leer ist oder keine Waffe zeigt) local function ReadWeapon(itemID, dps) local lines, _, ready = TooltipLines(itemID) local dmin, dmax, spd = ParseWeapon(lines, dps) return dmin, dmax, spd, ready end -- Ausgabe fuer /fwcodex weapon : zeigt, was der Tooltip liefert function Codex.WeaponDebug(itemID) if Util.RequestItem then Util.RequestItem(itemID, function() end) end -- Itemdaten anfordern local lines, source, ready = TooltipLines(itemID) FW:Print(string.format("Waffe %d: Quelle %s, %d Zeilen%s", itemID, source, #lines, ready and "" or " (Itemdaten noch nicht geladen, in ein paar Sekunden noch einmal eingeben)")) for i, ln in ipairs(lines) do if i <= 14 then FW:Print(string.format(" %d: [%s] [%s]", i, tostring(ln[1]), tostring(ln[2]))) end end local st = Codex.db.istats and Codex.db.istats[itemID] local dmin, dmax, spd = ParseWeapon(lines, st and DPSOf(st)) FW:Print(string.format("Ergebnis: Schaden %s - %s, Tempo %s, DPS laut Werten %s", tostring(dmin), tostring(dmax), tostring(spd), tostring(st and DPSOf(st)))) end -- Tooltip-Scanner (/fwcodex tipscan [stop]): liest die Tooltips aller bekannten Items und sammelt die Textzeilen -- mit Effekten ("Equip:", "Chance on hit:", "Use:" und alles, was nicht zu den Standardzeilen gehoert). -- Ergebnis: je Textmuster (Zahlen als # ersetzt) Anzahl und ein Beispiel-Item, im Log (Reiter "Log") und in db.tipscan. local tipScan local STD_PATTERNS = { "^Binds when", "^Requires ", "^Durability", "^Classes:", "^Races:", "^Unique", "^Sell", "^Soulbound", "^Quest Item", "^Two%-Hand", "^One%-Hand", "^Main Hand", "^Off Hand", "^Ranged", "^Held In Off", "^Thrown", "^Projectile", "^Relic", "^[%d%.,]+ %- [%d%.,]+ ", "^%(?[%d%.,]+ damage per second", "^[%d%.,]+ Armor", "^[%d%.,]+ Block", "^%+[%d%.,]+ [%a ]+$", "^%-[%d%.,]+ [%a ]+$", "^%+?[%d%.,]+ [%a ]+ Resistance", "^%d+ Slot ", "^Item Level", "^\"", "^Cloth$", "^Leather$", "^Mail$", "^Plate$", "^Shield$", "^Sword$", "^Axe$", "^Mace$", "^Dagger$", "^Staff$", "^Bow$", "^Gun$", "^Crossbow$", "^Wand$", "^Fist Weapon$", "^Polearm$", "^Idol$", "^Libram$", "^Totem$", "^Back$", "^Head$", "^Neck$", "^Shoulder$", "^Chest$", "^Waist$", "^Legs$", "^Feet$", "^Wrist$", "^Hands$", "^Finger$", "^Trinket$", "^Shirt$", "^Held", "^Bag$", "^Quiver$", "^Ammo Pouch$", "^%d+ Slot", } local function IsStd(text) for _, pat in ipairs(STD_PATTERNS) do if text:find(pat) then return true end end return false end local function Norm(text) return (text:gsub("%d+[%.,]?%d*", "#")) end function Codex.TipScan(ids) if tipScan then FW:Print("Tooltip-Scan laeuft schon (/fwcodex tipscan stop zum Abbrechen).") return end local list, seen = {}, {} for _, id in ipairs(ids or {}) do if type(id) == "number" and not seen[id] then seen[id] = true list[#list + 1] = id end end for id in pairs(Codex.db.istats or {}) do if type(id) == "number" and not seen[id] then seen[id] = true list[#list + 1] = id end end table.sort(list) local st = { list = list, pos = 1, pat = {}, done = 0, wait = 0, skipped = 0, t0 = _G.GetTime and _G.GetTime() or 0 } tipScan = st FW:Print(string.format("Tooltip-Scan: %d Items, laeuft im Hintergrund ...", #list)) local function Finish(aborted) tipScan = nil local rows = {} for key, v in pairs(st.pat) do rows[#rows + 1] = { key = key, n = v.n, id = v.id, ex = v.ex, kind = v.kind } end table.sort(rows, function(a, b) if a.n ~= b.n then return a.n > b.n end return a.key < b.key end) Codex.db.tipscan = { when = _G.date and _G.date("%Y-%m-%d %H:%M:%S") or "", items = st.done, skipped = st.skipped, rows = rows } local Log = FW.Dev and FW.Dev.Diag local function Out(text) if Log and Log.Add then Log.Add("scan", text) else FW:Print(text) end end Out(string.format("TIPSCAN %s: %d Items gelesen, %d uebersprungen (nicht geladen), %d Textmuster", aborted and "abgebrochen" or "fertig", st.done, st.skipped, #rows)) for i, r in ipairs(rows) do Out(string.format("TIPSCAN %d x %s | %s | Beispiel %d: %s", r.n, r.kind, r.key, r.id, r.ex)) if i >= 700 then break end end FW:Print(string.format("Tooltip-Scan %s: %d Textmuster im Log (Reiter Log, Art \"scan\") und in den gespeicherten Daten.", aborted and "abgebrochen" or "fertig", #rows)) end local function Step() if tipScan ~= st then return Finish(true) end local n = 0 while st.pos <= #list and n < 40 do local id = list[st.pos] if Util.RequestItem and not (Util.GetItem and Util.GetItem(id)) then pcall(Util.RequestItem, id, function() end) end local lines, _, ready = TooltipLines(id) if not ready and st.wait < 6 then st.wait = st.wait + 1 break -- spaeter nochmal versuchen end st.wait = 0 if ready then st.done = st.done + 1 for i = 2, #lines do local left = lines[i][1] if type(left) == "string" and left ~= "" and not IsStd(left) then local kind = "sonst" if left:find("^Equip:") then kind = "equip" elseif left:find("^Chance on hit:") then kind = "proc" elseif left:find("^Use:") then kind = "use" end local key = Norm(left) local v = st.pat[key] if not v then st.pat[key] = { n = 1, id = id, ex = left, kind = kind } else v.n = v.n + 1 end end end else st.skipped = st.skipped + 1 end st.pos = st.pos + 1 n = n + 1 end if st.pos > #list then return Finish(false) end if C_Timer and C_Timer.After then C_Timer.After(0.05, Step) end end Step() end function Codex.TipScanStop() tipScan = nil end local function HasDPS(stats) for key in pairs(stats) do if key:find("DAMAGE_PER_SECOND", 1, true) then return true end end return false end function Codex.ResetWeaponScan(itemID) scanTried[itemID] = nil end local function AddWeapon(itemID, out) if out._s or (scanTried[itemID] or 0) >= 6 or not HasDPS(out) then return end local dmin, dmax, spd, ready = ReadWeapon(itemID, DPSOf(out)) if dmin then out._dmin, out._dmax, out._spd, out._s = dmin, dmax, spd, 1 scanTried[itemID] = 6 elseif ready then scanTried[itemID] = 6 -- Tooltip war fertig und enthaelt keine Waffenwerte else scanTried[itemID] = (scanTried[itemID] or 0) + 1 -- noch nicht geladen: spaeter erneut versuchen end end -- Gemeinsamer Zaubermacht-Wert ("Schaden und Heilung um bis zu 18") gilt fuer beide: -- Zauberschaden und Zauberheilung bekommen ihn, damit Anzeige und Filter einheitlich sind. -- Rueckgabe: true, wenn etwas geaendert wurde. local SP_DAMAGE, SP_HEALING = "ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", "ITEM_MOD_SPELL_HEALING_DONE_SHORT" local function SplitSpellPower(t) local changed = false for _, key in ipairs({ "ITEM_MOD_SPELL_POWER_SHORT", "ITEM_MOD_SPELL_POWER" }) do local v = t[key] if type(v) == "number" and v ~= 0 then t[SP_DAMAGE] = math.max(t[SP_DAMAGE] or 0, v) t[SP_HEALING] = math.max(t[SP_HEALING] or 0, v) end if v ~= nil then t[key] = nil changed = true end end -- Schluessel ohne "_SHORT" (ITEM_MOD_BLOCK_VALUE) und mit sind derselbe Wert: auf die Form mit "_SHORT" vereinheitlichen local alias for key, v in pairs(t) do if type(key) == "string" and key:sub(1, 9) == "ITEM_MOD_" and key:sub(-6) ~= "_SHORT" then alias = alias or {} alias[#alias + 1] = key end end if alias then for _, key in ipairs(alias) do local v, target = t[key], key .. "_SHORT" if type(v) == "number" and (t[target] or 0) < v then t[target] = v end t[key] = nil changed = true end end return changed end Codex.SplitSpellPower = SplitSpellPower -- Effekte aus dem Tooltip ("Equip:", "Chance on hit:", "Use:", Set-Boni): bekannte Texte werden Zahlenwerte (FW_*), alles -- andere nur Markierungen (Wert 1). Die Zeilen selbst stehen in _fx, _e = 1 heisst "Effekte gelesen" (Stand EFFECT_VER). local EFFECT_VER = 3 local SCHOOLS = { ARCANE = true, FIRE = true, FROST = true, HOLY = true, NATURE = true, SHADOW = true } local EQUIP_RULES = { { "^Improves your chance to get a critical strike by ([%d%.]+)%%", "FW_CRIT" }, { "^Improves your chance to get a critical strike with spells by ([%d%.]+)%%", "FW_SPELLCRIT" }, { "^Improves your chance to get a critical strike with missile weapons by ([%d%.]+)%%", "FW_RANGEDCRIT" }, { "^Improves your chance to hit by ([%d%.]+)%%", "FW_HIT" }, { "^Improves your chance to hit with spells by ([%d%.]+)%%", "FW_SPELLHIT" }, { "^Increases your chance to dodge an attack by ([%d%.]+)%%", "FW_DODGE" }, { "^Increases your chance to parry an attack by ([%d%.]+)%%", "FW_PARRY" }, { "^Increases your chance to block attacks with %a+ shield by ([%d%.]+)%%", "FW_BLOCK" }, { "^Increases the block value of your shield by ([%d%.]+)", "FW_BLOCKVAL" }, { "^Restores ([%d%.]+) mana per 5 sec", "FW_MP5" }, { "^Restores ([%d%.]+) health per 5 sec", "FW_HP5" }, { "^Restores ([%d%.]+) health every 5 sec", "FW_HP5" }, { "^Increases your attack and casting speed by ([%d%.]+)%%", "FW_HASTE" }, { "^Increases ranged attack speed by ([%d%.]+)%%", "FW_RANGEDSPEED" }, { "^Your attacks ignore ([%d%.]+) of the target's armor", "FW_ARMORPEN" }, { "^Decreases the magical resistances of your spell targets by ([%d%.]+)", "FW_SPELLPEN" }, { "^([%d%.]+)%% of damage dealt is returned as healing", "FW_LIFESTEAL" }, { "^%+([%d%.]+) Attack Power%.?$", "ITEM_MOD_ATTACK_POWER_SHORT" }, { "^Increases attack power by ([%d%.]+)%.?$", "ITEM_MOD_ATTACK_POWER_SHORT" }, { "^%+([%d%.]+) Ranged Attack Power%.?$", "ITEM_MOD_RANGED_ATTACK_POWER_SHORT" }, { "^Increases ranged attack power by ([%d%.]+)%.?$", "ITEM_MOD_RANGED_ATTACK_POWER_SHORT" }, } local function SetMax(out, key, v) if type(v) == "number" and v ~= 0 and (out[key] or 0) < v then out[key] = v end end local function ParseEffects(lines, out) local fx = {} local function Flag(key) out[key] = 1 end for i = 2, #lines do local left = lines[i][1] if type(left) == "string" and left ~= "" and left:sub(1, 1) ~= "\n" then local txt = left:gsub("%s+$", "") if txt:find("^Equip:") then local t = txt:gsub("^Equip:%s*", "") local done = false for _, r in ipairs(EQUIP_RULES) do local v = t:match(r[1]) if v then SetMax(out, r[2], tonumber(v)) done = true break end end if not done then local v = t:match("^Increases damage and healing done by magical spells and effects by up to (%d+)") if v then SetMax(out, "ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", tonumber(v)) SetMax(out, "ITEM_MOD_SPELL_HEALING_DONE_SHORT", tonumber(v)) done = true end end if not done then local v = t:match("^Increases healing done by spells and effects by up to (%d+)") if v then SetMax(out, "ITEM_MOD_SPELL_HEALING_DONE_SHORT", tonumber(v)) done = true end end if not done then local sch, v = t:match("^Increases damage done by (%a+) spells and effects by up to (%d+)") if sch and SCHOOLS[sch:upper()] then SetMax(out, "FW_SD_" .. sch:upper(), tonumber(v)) done = true end end if not done then local name, v = t:match("^Increased ([%a ]-) %+(%d+)") if name then local k = name:upper():gsub(" ", "_") SetMax(out, (k == "DEFENSE" or k == "FISHING") and ("FW_" .. k) or ("FW_SK_" .. k), tonumber(v)) done = true end end if not done then -- Bewegungstempo: Schwimmen, Laufen, Reittier (ohne Prozentangabe zaehlt der Wert 1) local low = t:lower() local key = low:find("swim speed", 1, true) and "FW_SWIM" or ((low:find("run speed", 1, true) or low:find("movement speed", 1, true)) and "FW_RUNSPEED") or (low:find("mount speed", 1, true) and "FW_MOUNTSPEED") or nil if key then SetMax(out, key, tonumber(low:match("([%d%.]+)%%")) or 1) done = true end end if not done then Flag("FW_EQUIPFX") end fx[#fx + 1] = txt elseif txt:find("^Chance on hit:") then Flag("FW_PROC") fx[#fx + 1] = txt elseif txt:find("^Use:") then Flag("FW_USE") fx[#fx + 1] = txt elseif txt:find("^%(%d+%) Set:") then Flag("FW_SET") fx[#fx + 1] = txt elseif txt:find("^%+ ?[%d%.]+ %- [%d%.]+ %a+ Damage$") then Flag("FW_EXTRADMG") fx[#fx + 1] = txt else local v = txt:match("^Adds ([%d%.]+) damage per second$") if v then SetMax(out, "FW_AMMODPS", tonumber(v)) fx[#fx + 1] = txt end end end end while #fx > 8 do fx[#fx] = nil end for k, t in ipairs(fx) do if #t > 200 then fx[k] = t:sub(1, 200) end end out._fx = (#fx > 0) and fx or nil out._e = EFFECT_VER end Codex.FXFLAGS = { FW_EQUIPFX = true, FW_PROC = true, FW_USE = true, FW_SET = true, FW_EXTRADMG = true } local effectTries = {} -- Effekte eines gespeicherten Eintrags nachtragen (nur wenn der Tooltip fertig ist) local function AddEffects(itemID, out) if out._e == EFFECT_VER or (effectTries[itemID] or 0) >= 3 then return false end effectTries[itemID] = (effectTries[itemID] or 0) + 1 local lines, _, ready = TooltipLines(itemID) if not ready then return false end ParseEffects(lines, out) return true end Codex.AddEffects = AddEffects local function ReadStats(itemID) local link = "item:" .. itemID local fn = (C_Item and C_Item.GetItemStats) or GetItemStats if type(fn) ~= "function" then return nil end local ok, t = pcall(fn, link) if not ok or type(t) ~= "table" then return nil end local out, any = {}, false for key, value in pairs(t) do if type(key) == "string" and type(value) == "number" and value ~= 0 and not key:find("^EMPTY_SOCKET") then out[key] = value any = true end end SplitSpellPower(out) if any then AddWeapon(itemID, out) end AddEffects(itemID, out) if not any then for k, v in pairs(out) do if type(k) == "string" and k:sub(1, 1) ~= "_" and type(v) == "number" and v ~= 0 then any = true break end end end return out, any end local function HasAnyStat(st) for k, v in pairs(st) do if type(k) == "string" and k:sub(1, 1) ~= "_" and type(v) == "number" and v ~= 0 then return true end end return false end -- Rüstung und Waffen haben immer Werte: eine leere Antwort heisst dort "noch nicht geladen" local function ShouldHaveStats(itemID) local info = Util.GetItem and Util.GetItem(itemID) if type(info) ~= "table" then return false end local loc = info.equipLoc if type(loc) == "string" and loc ~= "" and loc ~= "INVTYPE_BAG" and loc ~= "INVTYPE_TABARD" and loc ~= "INVTYPE_BODY" then return true end return info.classID == 2 or info.classID == 4 end local emptyTries = {} -- itemID -> Versuche in dieser Sitzung (leer gelesene Ruestung/Waffen werden hoechstens dreimal neu gelesen) -- Werte eines Items (aus der Datenbank, sonst vom Spiel lesen und merken) function Codex:GetStats(itemID) if type(itemID) ~= "number" then return nil end local store = self.db.istats if not store then store = {} self.db.istats = store end local st = store[itemID] if st and not HasAnyStat(st) and (emptyTries[itemID] or 0) < 3 and ShouldHaveStats(itemID) then -- aelterer leerer Eintrag zu Ruestung/Waffe: neu lesen emptyTries[itemID] = (emptyTries[itemID] or 0) + 1 local again, any = ReadStats(itemID) if any then store[itemID] = again st = again end end if st then SplitSpellPower(st) -- aeltere Eintraege mit gemeinsamem Zaubermacht-Wert if st._e ~= EFFECT_VER and Util.GetItem and Util.GetItem(itemID) then AddEffects(itemID, st) -- aeltere Eintraege: Effekte nachtragen end if not st._s and Util.GetItem and Util.GetItem(itemID) then AddWeapon(itemID, st) -- aeltere Eintraege: Schaden und Tempo nachtragen end return st end if not (Util.GetItem and Util.GetItem(itemID)) then return nil -- Item noch nicht geladen: spaeter noch einmal versuchen end local out, any = ReadStats(itemID) if out then if not any and ShouldHaveStats(itemID) and (emptyTries[itemID] or 0) < 3 then -- Ruestung/Waffe ohne Werte gelesen: nicht merken, spaeter noch einmal versuchen emptyTries[itemID] = (emptyTries[itemID] or 0) + 1 return out end store[itemID] = out -- auch leer (Item ohne Werte), damit nicht erneut gelesen wird return out end return nil end -- Im Hintergrund: gespeicherte Eintraege ohne Effekte neu lesen (kleine Schritte, keine Ruckler) local fxUpgrading -- Entfernt Items, die der Server nicht kennt (Item-Lader: fehlgeschlagen), aus der eigenen Datenbank function Codex.PurgeMissing() local db = Codex.db local fail = db and db.itemload and db.itemload.fail if not fail then return 0 end local n = 0 local cat = db.cat and db.cat.i for id in pairs(fail) do if cat and cat[id] and not (Util and Util.GetItem and Util.GetItem(id)) then cat[id] = nil n = n + 1 end if db.istats and db.istats[id] and not (Util and Util.GetItem and Util.GetItem(id)) then db.istats[id] = nil n = n + 1 end end if n > 0 and FW.Dev and FW.Dev.Diag then FW.Dev.Diag.Add("scan", "Aufgeraeumt: " .. n .. " unbekannte Items entfernt") end return n end function Codex.UpgradeEffects() if fxUpgrading or not (C_Timer and C_Timer.After) then return end local store = Codex.db and Codex.db.istats if not store then return end local list = {} for id, st in pairs(store) do if type(id) == "number" and type(st) == "table" and st._e ~= EFFECT_VER then list[#list + 1] = id end end if #list == 0 then return end table.sort(list) fxUpgrading = true local pos = 1 local function Step() local n = 0 while pos <= #list and n < 15 do local id = list[pos] local st = store[id] if type(st) == "table" and st._e ~= EFFECT_VER and Util.GetItem and Util.GetItem(id) then AddEffects(id, st) end pos = pos + 1 n = n + 1 end if pos <= #list then C_Timer.After(0.1, Step) else fxUpgrading = false end end Step() end -- Item-Lader (/fwcodex itemload [stop|status]): fordert die Itemdaten der Datenbank langsam an und liest dann Werte und Effekte. -- Fortsetzen nach Neustart: erledigte Items haben Werte (_e), fehlgeschlagene stehen in db.itemload.fail. local loader local LOAD_RATE = 0.25 -- Sekunden zwischen zwei Schritten local LOAD_MAX_OUT = 40 -- so viele Anfragen gleichzeitig offen local LOAD_TIMEOUT = 5 -- Sekunden, danach gilt eine Anfrage als fehlgeschlagen local function LoaderState() local db = Codex.db db.itemload = db.itemload or {} db.itemload.fail = db.itemload.fail or {} return db.itemload end function Codex.ItemLoadStatus() local st = loader if not st then FW:Print("Item-Lader: aus. Start mit /fwcodex itemload") return end FW:Print(string.format("Item-Lader: %d von %d bearbeitet, %d geladen, %d fehlgeschlagen, %d offen", st.pos - 1, #st.list, st.loaded, st.failed, st.out)) end function Codex.ItemLoadStop(silent) if loader then loader.stop = true loader = nil if not silent then FW:Print("Item-Lader gestoppt (Fortsetzen mit /fwcodex itemload).") end end if Codex.db then LoaderState().on = nil end end function Codex.ItemLoadStart(ids, resume) if loader then Codex.ItemLoadStatus() return end if not (C_Timer and C_Timer.After and Util.RequestItem) then FW:Print("Item-Lader: nicht verfuegbar (keine Timer/Item-Abfrage).") return end local ls = LoaderState() local store = Codex.db.istats or {} local list, seen = {}, {} for _, id in ipairs(ids or {}) do if type(id) == "number" and not seen[id] and not ls.fail[id] then seen[id] = true local st = store[id] if not (st and st._e == EFFECT_VER) then list[#list + 1] = id end end end -- zuerst Items, die schon im Cache sind (kosten keine Abfrage) local cached, rest = {}, {} for _, id in ipairs(list) do if Util.GetItem and Util.GetItem(id) then cached[#cached + 1] = id else rest[#rest + 1] = id end end table.sort(cached) table.sort(rest) for _, id in ipairs(rest) do cached[#cached + 1] = id end local state = { list = cached, pos = 1, loaded = 0, failed = 0, out = 0, pending = {}, t0 = _G.GetTime and _G.GetTime() or 0 } loader = state ls.on = true if #cached == 0 then FW:Print("Item-Lader: nichts zu tun, alle bekannten Items sind eingelesen.") loader = nil ls.on = nil return end FW:Print(string.format("Item-Lader: %d Items, im Hintergrund (Stand mit /fwcodex itemload status, Stopp mit /fwcodex itemload stop).", #cached)) local nextReport = 500 local function Now() return _G.GetTime and _G.GetTime() or 0 end local function Finish() if loader == state then loader = nil end LoaderState().on = nil if Codex.MarkItemsDirty then Codex.MarkItemsDirty() end FW:Print(string.format("Item-Lader fertig: %d geladen, %d fehlgeschlagen. Bitte /reload, damit alles gespeichert wird.", state.loaded, state.failed)) end local function Done(id, info) if state.pending[id] then state.pending[id] = nil state.out = state.out - 1 end if info then state.loaded = state.loaded + 1 LoaderState().fail[id] = nil Safe(function() Codex:GetStats(id) end) else state.failed = state.failed + 1 LoaderState().fail[id] = 1 end end local function Step() if loader ~= state then return end -- Zeitueberschreitungen local now = Now() for id, t in pairs(state.pending) do if now - t > LOAD_TIMEOUT then Done(id, nil) end end local n = 0 while state.pos <= #state.list and state.out < LOAD_MAX_OUT and n < 10 do local id = state.list[state.pos] state.pos = state.pos + 1 n = n + 1 if Util.GetItem and Util.GetItem(id) then state.loaded = state.loaded + 1 Safe(function() Codex:GetStats(id) end) else state.pending[id] = now state.out = state.out + 1 Util.RequestItem(id, function(info) Done(id, info) end) end end if state.pos > nextReport then nextReport = nextReport + 500 if Codex.MarkItemsDirty then Codex.MarkItemsDirty() end FW:Print(string.format("Item-Lader: %d von %d, %d geladen, %d fehlgeschlagen", state.pos - 1, #state.list, state.loaded, state.failed)) end if state.pos > #state.list and state.out <= 0 then return Finish() end C_Timer.After(LOAD_RATE, Step) end Step() end function Codex:CaptureStats(ids) local n = 0 for _, id in ipairs(ids or {}) do if self.db.istats == nil or self.db.istats[id] == nil then if self:GetStats(id) then n = n + 1 end end end return n end -- Anzeigename eines Wertes ("ITEM_MOD_AGILITY_SHORT" -> "Beweglichkeit") function Codex.StatName(key) if key:sub(1, 3) == "FW_" then local txt = FW.L["CODEX_FX_" .. key] -- FW.L liefert bei fehlendem Schluessel den Schluessel selbst zurueck if type(txt) == "string" and txt ~= "CODEX_FX_" .. key then return txt end if key:sub(1, 6) == "FW_SK_" then local nm = key:sub(7):gsub("_", " "):lower():gsub("(%a)([%w]*)", function(a, b) return a:upper() .. b end) return string.format(FW.L.CODEX_FX_SKILL, nm) end end local g = _G[key] if type(g) == "string" and g ~= "" then return g end local nm = key:gsub("^ITEM_MOD_", ""):gsub("_SHORT$", ""):gsub("_", " "):lower() return (nm:gsub("(%a)([%w]*)", function(a, b) return a:upper() .. b end)) end function Codex.FormatStat(value) if math.abs(value - math.floor(value)) < 0.05 then return string.format("%d", math.floor(value + 0.5)) end return string.format("%.1f", value) end -- Sortierte Liste der Werte-Schluessel, die in der Datenbank vorkommen Codex.FXKEYS = { "FW_CRIT", "FW_SPELLCRIT", "FW_RANGEDCRIT", "FW_HIT", "FW_SPELLHIT", "FW_DODGE", "FW_PARRY", "FW_BLOCK", "FW_BLOCKVAL", "FW_MP5", "FW_HP5", "FW_DEFENSE", "FW_FISHING", "FW_HASTE", "FW_RANGEDSPEED", "FW_ARMORPEN", "FW_SPELLPEN", "FW_LIFESTEAL", "FW_AMMODPS", "FW_SD_ARCANE", "FW_SD_FIRE", "FW_SD_FROST", "FW_SD_HOLY", "FW_SD_NATURE", "FW_SD_SHADOW", "FW_SWIM", "FW_RUNSPEED", "FW_MOUNTSPEED", "FW_PROC", "FW_USE", "FW_SET", "FW_EQUIPFX", "FW_EXTRADMG" } function Codex:StatKeys(ids) local seen, list = {}, {} for _, key in ipairs(Codex.FXKEYS) do seen[key] = true list[#list + 1] = key end for _, id in ipairs(ids or {}) do local st = self.db.istats and self.db.istats[id] if st then for rawKey in pairs(st) do local key = rawKey if key:sub(1, 9) == "ITEM_MOD_" and key:sub(-6) ~= "_SHORT" then key = key .. "_SHORT" end if not seen[key] and key:sub(1, 1) ~= "_" then seen[key] = true list[#list + 1] = key end end end end table.sort(list, function(a, b) return Codex.StatName(a) < Codex.StatName(b) end) return list end function Codex:GetUnit(id) return Codex.V().units[id] end function Codex:GetObject(id) return Codex.V().objects[id] end function Codex:ItemName(itemID) local c = self.db.cnames and self.db.cnames[itemID] local lang = Codex.Lang() if c and c[lang] then return c[lang] end local info = Util.GetItem and Util.GetItem(itemID) if info and info.name then return info.name end local seen = self.db.names[itemID] or (c and (c.de or c.en)) if seen then return seen end -- zuletzt die Vorlage (Vanilla-Daten), nur fuer noch nie gesehene Items local seed = Codex.Seed and Codex.Seed.Ready() and Codex.Seed.Name("items", itemID) if seed then return seed end return Codex.ExtraItemName and Codex.ExtraItemName(itemID) or nil end ------------------------------------------------------------------------ -- Namen von Hand eintragen (Deutsch / Englisch) -- Kreaturen, Objekte und Quests: Felder cde / cen am Datensatz (werden geteilt, eigener Eintrag geht vor). -- Items: db.cnames[itemID] = { de, en } (nur lokal, Itemnamen liefert das Spiel). ------------------------------------------------------------------------ function Codex.Lang() return (FW.activeLocale == "deDE") and "de" or "en" end local CUSTOM_MAX = 60 local function CleanCustom(v, max) if type(v) ~= "string" then return nil end v = v:gsub("[%c|]", ""):gsub("^%s+", ""):gsub("%s+$", "") if v == "" then return nil end return v:sub(1, max or CUSTOM_MAX) end Codex.CleanCustom = CleanCustom -- Anzeigename einer Kreatur / eines Objekts: gewaehlte Sprache, sonst vom Spiel gelernt, sonst die andere Sprache function Codex.NameOf(ent) if type(ent) ~= "table" then return nil end local mine, other = ent.cde, ent.cen if Codex.Lang() == "en" then mine, other = ent.cen, ent.cde end return mine or ent.n or other or ent.ne end function Codex.QuestTitle(q, id) -- Vorlage (Vanilla) nur als letzter Ausweg local function SeedTitle() return Codex.Seed and Codex.Seed.Ready() and Codex.Seed.QuestTitle(id) or nil end if type(q) ~= "table" then return SeedTitle() or ("Quest #" .. tostring(id)) end local mine, other = q.cde, q.cen if Codex.Lang() == "en" then mine, other = q.cen, q.cde end return mine or q.t or other or SeedTitle() or ("Quest #" .. tostring(id)) end local CUSTOM_TABLE = { units = "units", objects = "objects", quests = "quests" } local CUSTOM_EVENT = { units = "unit", objects = "object", quests = "quest", items = "item", zones = "item" } -- Eigene Namen (de, en) zu einer Kreatur, einem Objekt, einer Quest oder einem Item function Codex:GetCustom(kind, id) -- vorgemerkte (noch nicht gesendete) Aenderungen eines Bearbeiters zuerst local hook = Codex.DevHook local staged = hook and hook.Staged and hook.Staged(kind, id) if staged then return staged.de, staged.en end if kind == "items" then local c = self.db.cnames and self.db.cnames[id] return c and c.de, c and c.en end local t = CUSTOM_TABLE[kind] and self.db[CUSTOM_TABLE[kind]] local e = t and t[id] return e and e.cde, e and e.cen end -- de / en setzen (leer oder nil loescht). Rueckgabe: true, wenn gespeichert -- Namen aendern darf nur, wer berechtigt ist und das Modul Freewood-OctoWoW-Dev hat; die Aenderung wird dort erst -- vorgemerkt (Vorschau) und mit "Senden" uebernommen (direct = true). function Codex:SetCustom(kind, id, de, en, direct) if type(id) ~= "number" or not CUSTOM_EVENT[kind] then return false end local max = kind == "quests" and 100 or CUSTOM_MAX de, en = CleanCustom(de, max), CleanCustom(en, max) if not direct then local hook = Codex.DevHook if not (Codex.Trust and Codex.Trust.CanEdit() and hook and hook.Stage) then return false, "denied" end return hook.Stage(kind, id, de, en) end if kind == "items" then self.db.cnames = self.db.cnames or {} if de or en then self.db.cnames[id] = { de = de, en = en } else self.db.cnames[id] = nil end else local t = self.db[CUSTOM_TABLE[kind]] local e = t[id] if not e then if not (de or en) then return true end if kind == "units" then e = self:Unit(id, true) elseif kind == "objects" then e = self:Object(id, true) else e = self:Quest(id, true) end end e.cde, e.cen = de, en end FW:Fire("CODEX_CHANGED", CUSTOM_EVENT[kind], id) return true end -- "fehlerhaft"-Markierung (true / false = aufgehoben / nil = keine Angabe); gleiche Regeln wie bei Namen function Codex:SetBad(kind, id, value, direct) if type(id) ~= "number" or not CUSTOM_TABLE[kind] then return false end if not direct then local hook = Codex.DevHook local action = value == true and "bad" or "unbad" if not (Codex.Trust and Codex.Trust.Can(action) and hook and hook.StageBad) then return false, "denied" end return hook.StageBad(kind, id, value) end local t = self.db[CUSTOM_TABLE[kind]] local e = t[id] if not e then if value == nil then return true end if kind == "units" then e = self:Unit(id, true) elseif kind == "objects" then e = self:Object(id, true) else e = self:Quest(id, true) end end e.bad = value FW:Fire("CODEX_CHANGED", CUSTOM_EVENT[kind], id) return true end function Codex:IsBad(kind, id) local t = CUSTOM_TABLE[kind] and self.db[CUSTOM_TABLE[kind]] local e = t and t[id] local view = Codex.V()[CUSTOM_TABLE[kind] or ""] local v = view and view[id] return (v and v.bad == true) or (e and e.bad == true) or false end ------------------------------------------------------------------------ -- Eigenschaften von Hand (Dev): Zone(n), Dungeon ja/nein, Klasse, Stufen -- Kreaturen, Objekte, Quests: Feld ed am Datensatz (wird geteilt, eigener Eintrag geht vor). -- Items: db.ied[itemID] = { ed } (eigener Datensatztyp "i"). Zonen: negative Zahl = Instanz-ID. -- ed = { z = { mapID, ... }, dg = true/false, cl = "normal"|"elite"|"rare"|"rareelite"|"worldboss", l1, l2 } ------------------------------------------------------------------------ -- Gebiete: die von Hand gesetzte Stufenspanne liegt im Item-Datensatz "i" unter einer hohen ID -- (50000000 + Karten-ID, Instanzen 50200000 + Instanz-ID), damit Teilen, Vormerken und Protokoll gleich laufen. Codex.ZONE_BASE = 50000000 function Codex.ZoneEdID(key) if type(key) ~= "number" then return nil end if key < 0 then return Codex.ZONE_BASE + 200000 + (-key) end return Codex.ZONE_BASE + key end function Codex.ZoneFromEdID(id) if type(id) ~= "number" or id < Codex.ZONE_BASE then return nil end local r = id - Codex.ZONE_BASE if r >= 200000 then return -(r - 200000) end return r end local ED_CLASS = { normal = true, elite = true, rare = true, rareelite = true, worldboss = true } local MAX_ED_ZONES = 6 Codex.ED_CLASSES = { "normal", "elite", "rare", "rareelite", "worldboss" } Codex.MAX_ED_ZONES = MAX_ED_ZONES local function EdInt(v, lo, hi) v = tonumber(v) if v and v == math.floor(v) and v >= lo and v <= hi then return v end return nil end -- Bereinigt eine Eigenschaften-Tabelle; Rueckgabe nil, wenn nichts uebrig bleibt -- Quest-Zuordnung von Hand (Kreaturen und Objekte): qk = Quests, deren Ziel dies ist; qd = "Quest:Item" (laesst das Quest-Item fallen) local function EdIdList(list, max) if type(list) ~= "table" then return nil end local ids, seen = {}, {} for i, p in ipairs(list) do if i > 20 then break end p = EdInt(p, 1, 100000000) if p and not seen[p] and #ids < (max or 8) then ids[#ids + 1] = p seen[p] = true end end return #ids > 0 and ids or nil end local function EdPairList(list, max) if type(list) ~= "table" then return nil end local out, seen = {}, {} for i, v in ipairs(list) do if i > 20 then break end local q, it if type(v) == "string" then q, it = v:match("^(%d+):(%d+)$") end q, it = EdInt(q, 1, 100000000), EdInt(it, 1, 100000000) if q and it then local key = q .. ":" .. it if not seen[key] and #out < (max or 8) then out[#out + 1] = key seen[key] = true end end end return #out > 0 and out or nil end -- Zuordnung "gehoert zur Quest": qb (Kreatur, Objekt, Item: Quest-IDs), qr (Quest: Liste "i:Item", "u:Kreatur", "o:Objekt") local function EdTokenList(list, max) if type(list) ~= "table" then return nil end local out, seen = {}, {} for i, v in ipairs(list) do if i > 40 then break end local k, n if type(v) == "string" then k, n = v:match("^([iuo]):(%d+)$") end n = EdInt(n, 1, 100000000) if k and n then local key = k .. ":" .. n if not seen[key] and #out < (max or 24) then out[#out + 1] = key seen[key] = true end end end return #out > 0 and out or nil end function Codex.CleanEd(ed, kind) if type(ed) ~= "table" then return nil end local out = {} if kind == "zones" then -- Gebiete: nur die Stufenspanne von Hand local l1, l2 = EdInt(ed.l1, 1, 130), EdInt(ed.l2, 1, 130) if l1 or l2 then l1, l2 = l1 or l2, l2 or l1 if l1 > l2 then l1, l2 = l2, l1 end return { l1 = l1, l2 = l2 } end return nil end if type(ed.z) == "table" then local z, seen = {}, {} for i, m in ipairs(ed.z) do if i > 20 then break end m = EdInt(m, -100000, 100000) if m and m ~= 0 and not seen[m] and #z < MAX_ED_ZONES then z[#z + 1] = m seen[m] = true end end if #z > 0 then out.z = z end end if ed.dg == true or ed.dg == false then out.dg = ed.dg end if kind == "units" then if ED_CLASS[ed.cl] then out.cl = ed.cl end local l1, l2 = EdInt(ed.l1, 1, 130), EdInt(ed.l2, 1, 130) if l1 or l2 then l1, l2 = l1 or l2, l2 or l1 if l1 > l2 then l1, l2 = l2, l1 end out.l1, out.l2 = l1, l2 end end if kind == "units" or kind == "objects" then out.qk = EdIdList(ed.qk) out.qd = EdPairList(ed.qd) end if kind == "units" or kind == "objects" or kind == "items" then out.qb = EdIdList(ed.qb, 12) end if kind == "quests" then out.qr = EdTokenList(ed.qr) end if kind == "items" then -- Schluessel von Hand: Item, das zum Oeffnen (Truhe) noetig ist, um dieses Item zu bekommen local key = EdInt(ed.key, 1, 100000000) if key then out.key = key end end if kind == "quests" then -- Vorquests von Hand (jede genuegt), Folgequests, davor/danach (Reihenfolge im Plan), Stufe, Questtext local function Ids(list, max) if type(list) ~= "table" then return nil end local ids, seen = {}, {} for i, p in ipairs(list) do if i > 20 then break end p = EdInt(p, 1, 100000000) if p and not seen[p] and #ids < (max or 8) then ids[#ids + 1] = p seen[p] = true end end return #ids > 0 and ids or nil end out.pre = Ids(ed.pre) out.nx = Ids(ed.nx) out.bf = Ids(ed.bf) out.af = Ids(ed.af) local ql = EdInt(ed.ql, 1, 130) if ql then out.ql = ql end local si = EdInt(ed.si, 1, 100000000) -- Item, durch das die Quest startet if si then out.si = si end if type(ed.tx) == "string" then local t = ed.tx:gsub("[%c]", " "):gsub("^%s+", ""):gsub("%s+$", "") if t ~= "" then out.tx = t:sub(1, 600) end end end if next(out) == nil then return nil end return out end local function CopyEd(ed) if not ed then return nil end local out = { dg = ed.dg, cl = ed.cl, l1 = ed.l1, l2 = ed.l2, key = ed.key, ql = ed.ql, tx = ed.tx, si = ed.si } if ed.z then out.z = {} for i, m in ipairs(ed.z) do out.z[i] = m end end for _, f in ipairs({ "pre", "nx", "bf", "af", "qk", "qd", "qb", "qr" }) do if ed[f] then out[f] = {} for i, p in ipairs(ed[f]) do out[f][i] = p end end end return out end Codex.CopyEd = CopyEd -- Gespeicherter Stand (ohne Vormerkung) function Codex:GetEdStored(kind, id) local t = (kind == "items" or kind == "zones") and self.db.ied or (CUSTOM_TABLE[kind] and self.db[CUSTOM_TABLE[kind]]) local e = t and t[id] return e and e.ed or nil end -- Eigenschaften zu einer Kreatur, einem Objekt, einer Quest oder einem Item (vorgemerkte Aenderungen zuerst) function Codex:GetEd(kind, id) local hook = Codex.DevHook local st = hook and hook.StagedEd and hook.StagedEd(kind, id) if st then return CopyEd(st.ed) end return CopyEd(self:GetEdStored(kind, id)) end -- ed = nil loescht. Wie bei Namen: erst vormerken (Freewood-OctoWoW-Dev), mit "Senden" uebernehmen (direct = true) function Codex:SetEd(kind, id, ed, direct) if type(id) ~= "number" or not CUSTOM_EVENT[kind] then return false end ed = Codex.CleanEd(ed, kind) if not direct then local hook = Codex.DevHook if not (Codex.Trust and Codex.Trust.CanEdit() and hook and hook.StageEd) then return false, "denied" end return hook.StageEd(kind, id, ed) end if kind == "items" or kind == "zones" then self.db.ied = self.db.ied or {} self.db.ied[id] = ed and { ed = CopyEd(ed) } or nil if Codex.Share and Codex.Share.NoteEd then Codex.Share.NoteEd(ed ~= nil) end else local t = self.db[CUSTOM_TABLE[kind]] local e = t[id] if not e then if not ed then return true end if kind == "units" then e = self:Unit(id, true) elseif kind == "objects" then e = self:Object(id, true) else e = self:Quest(id, true) end end e.ed = CopyEd(ed) if Codex.Share and Codex.Share.NoteEd then Codex.Share.NoteEd(ed ~= nil) end end Codex.chainRev = (Codex.chainRev or 0) + 1 FW:Fire("CODEX_CHANGED", CUSTOM_EVENT[kind], id) return true end -- Verschlossene Truhen: Item, das ein anderes Item (aus einer Truhe) erst zugaenglich macht { [Item] = Schluessel-Item }. -- Eingebaut: Vergilbter Umschlag (Truhe auf dem Gebaeude bei Lieutenant Benedict) braucht Benedicts Schluessel. Codex.KEY_DEFAULT = { [4881] = 4882 } function Codex.KeyOf(itemID) if not itemID then return nil end local ed = Codex:GetEd("items", itemID) if ed and ed.key then return ed.key end return Codex.KEY_DEFAULT[itemID] end -- Datensatz mit Eigenschaften: ueberschreibt Klasse (c), Stufen (l1, l2), Dungeon (dg) und Zonen (ez, bei Quests zone). -- inplace = true: direkt in der (frisch gebauten) Tabelle e, sonst eine Ueberlagerung ueber dem Original. function Codex.ApplyEd(e, kind, inplace) local ed = type(e) == "table" and e.ed if not ed then return e end local o = inplace and e or {} if ed.cl then if ed.cl == "normal" then o.c = false else o.c = ed.cl end end if ed.l1 then o.l1, o.l2 = ed.l1, ed.l2 or ed.l1 end if ed.dg ~= nil then o.dg = ed.dg end if ed.z then o.ez = ed.z if kind == "quests" then o.zone = ed.z[1] end end if ed.pre then o.pre = ed.pre end if ed.key then o.key = ed.key end if kind == "quests" then if ed.ql then o.lvl = ed.ql end if ed.si then o.si = ed.si end if ed.tx then o.dtx = ed.tx end end if inplace then return e end return setmetatable(o, { __index = e }) end -- Karten eines Datensatzes: von Hand gesetzte zuerst, sonst die Hauptkarte der Fundorte function Codex.ZonesOf(e) if type(e) ~= "table" then return {} end if e.ez and #e.ez > 0 then return e.ez end if type(e.iz) == "table" then local best, bestN for id, n in pairs(e.iz) do if not bestN or n > bestN then best, bestN = id, n end end if best then return { -best } end end local m = e.cells and Codex.MainMap(e.cells) return m and { m } or {} end function Codex:SetCustomLang(kind, id, lang, name) local de, en = self:GetCustom(kind, id) if lang == "de" then de = name else en = name end return self:SetCustom(kind, id, de, en) end -- Text mit Zeilen "ID Name" (auch "ID;Name", "ID=Name", "IDName") einlesen. -- lang: "de" oder "en". Rueckgabe: gespeichert, uebersprungen function Codex:ImportNames(kind, lang, text) local done, skipped = 0, 0 if type(text) ~= "string" or not CUSTOM_EVENT[kind] or (lang ~= "de" and lang ~= "en") then return 0, 0 end local lines = 0 for line in (text .. "\n"):gmatch("([^\r\n]*)[\r\n]") do local id, name = line:match("^%s*#?(%d+)%s*[=;:,\t ]%s*(.-)%s*$") if id or line:find("%S") then lines = lines + 1 if lines > 1000 then skipped = skipped + 1 elseif id and CleanCustom(name) and tonumber(id) <= 100000000 then self:SetCustomLang(kind, tonumber(id), lang, name) done = done + 1 else skipped = skipped + 1 end end end return done, skipped end -- Drop-Chance: Anteil der gelooteten Leichen, in denen das Item lag. Rueckgabe: chance (0..1), n, k function Codex.DropChance(ent, itemID, alt) local bucket = alt and ent.alt or ent if not bucket or not bucket.d or not bucket.d[itemID] or not bucket.k or bucket.k == 0 then return nil, 0, bucket and bucket.k or 0 end local d = bucket.d[itemID] local n = d.n if d.qi and d.qk and d.qk >= n and d.qk > 0 then return n / d.qk, n, d.qk -- Quest-Item: nur Leichen mit laufender Quest end return n / bucket.k, n, bucket.k end -- Sortierte Beuteliste { { id, chance, n, k, avg, qi } } function Codex.DropList(ent, alt) local bucket = alt and ent.alt or ent local list = {} if bucket and bucket.d and bucket.k and bucket.k > 0 then for id, d in pairs(bucket.d) do local den = (d.qi and d.qk and d.qk >= d.n and d.qk > 0) and d.qk or bucket.k list[#list + 1] = { id = id, chance = d.n / den, n = d.n, k = den, avg = d.n > 0 and d.q / d.n or 1, qi = d.qi and true or false, } end end table.sort(list, function(a, b) if a.chance ~= b.chance then return a.chance > b.chance end return a.id < b.id end) return list end -- Durchschnittliches Gold pro Leiche mit Gold (Kupfer) function Codex.AverageCoin(ent) if ent.coin and ent.coinN and ent.coinN > 0 then return ent.coin / ent.coinN end return nil end -- Orte: Zellen zu Fundstellen zusammenfassen (je BIN x BIN Zellen eine Stelle). -- Rueckgabe: { { m, x, y, n } } sortiert nach Treffern, x/y als Anteil 0..1 local SPOT_BIN = 5 function Codex.TopSpots(cells, limit) local bins = {} for key, n in pairs(cells or {}) do local m = math.floor(key / (GRID * GRID)) local idx = key % (GRID * GRID) local cx, cy = idx % GRID, math.floor(idx / GRID) local bk = m * 1000000 + math.floor(cy / SPOT_BIN) * 1000 + math.floor(cx / SPOT_BIN) local b = bins[bk] if not b then b = { m = m, sx = 0, sy = 0, n = 0 } bins[bk] = b end b.sx, b.sy, b.n = b.sx + (cx + 0.5) * n, b.sy + (cy + 0.5) * n, b.n + n end local list = {} for _, b in pairs(bins) do list[#list + 1] = { m = b.m, x = b.sx / b.n / GRID, y = b.sy / b.n / GRID, n = b.n } end table.sort(list, function(a, b) if a.n ~= b.n then return a.n > b.n end if a.m ~= b.m then return a.m < b.m end if a.y ~= b.y then return a.y < b.y end return a.x < b.x end) if limit and #list > limit then for i = #list, limit + 1, -1 do list[i] = nil end end return list end function Codex.AddCells(dst, cells) for key, n in pairs(cells or {}) do dst[key] = (dst[key] or 0) + n end return dst end local MAX_POINTS = 4000 local function EntityName(ent, id, kind) return (ent and ent.n) or ((kind == "objects" and "#" or "#") .. tostring(id)) end local function AddEntityPoints(points, ent, id, kind, label) if not ent or not ent.cells then return end for key, n in pairs(ent.cells) do if #points >= MAX_POINTS then return end local m = math.floor(key / (GRID * GRID)) local idx = key % (GRID * GRID) points[#points + 1] = { m = m, x = ((idx % GRID) + 0.5) / GRID, y = (math.floor(idx / GRID) + 0.5) / GRID, n = n, name = label or EntityName(ent, id, kind), k = kind == "objects" and "o" or "u", } end end -- Fundstellen der Vorlage (Vanilla) als Punkte local function AddSeedPoints(points, kind, id, label) local Seed = Codex.Seed if not (Seed and Seed.Ready()) then return end local cells = Seed.Cells(kind, id) if next(cells) then AddEntityPoints(points, { cells = cells, n = Seed.Name(kind, id) }, id, kind, label) end end local function HasCells(ent) return ent and ent.cells and next(ent.cells) ~= nil end local function AddSourceKey(points, key, label) local tk = key:sub(1, 1) == "u" and "units" or "objects" local id = tonumber(key:sub(2)) local ent = id and (tk == "units" and Codex.V().units[id] or Codex.V().objects[id]) if HasCells(ent) then AddEntityPoints(points, ent, id, tk, label) elseif id then AddSeedPoints(points, tk, id, label) end end -- Punkte fuer die Weltkarte. spec = { kind = "units"|"objects"|"items"|"quests", id = ... } -- Punkt: { m, x, y, n, name, pin } (pin: feste Stelle wie Quest-Geber) function Codex.CollectPoints(spec) local points = {} local kind, id = spec.kind, spec.id if kind == "units" or kind == "objects" then local ent = kind == "units" and Codex.V().units[id] or Codex.V().objects[id] if HasCells(ent) then AddEntityPoints(points, ent, id, kind) else AddSeedPoints(points, kind, id) end elseif kind == "items" then for _, pair in ipairs({ { "units", Codex.V().units }, { "objects", Codex.V().objects } }) do for eid, ent in pairs(pair[2]) do local chance = Codex.DropChance(ent, id, false) if chance then AddEntityPoints(points, ent, eid, pair[1], string.format("%s (%d%%)", EntityName(ent, eid, pair[1]), math.floor(chance * 100 + 0.5))) end end end -- Vorlage: nur wenn es noch keine eigenen Fundstellen gibt (die besten Quellen) local Seed = Codex.Seed if #points == 0 and Seed and Seed.Ready() then local it = Seed.Item(id) for _, def in ipairs({ { "units", it and it.u or {}, 8 }, { "objects", it and it.o or {}, 3 } }) do for i, src in ipairs(def[2]) do if i > def[3] then break end AddSeedPoints(points, def[1], src.id, string.format("%s (%d%%)", Seed.Name(def[1], src.id) or ("#" .. src.id), math.floor(src.chance + 0.5))) end end end elseif kind == "quests" then local q = Codex.V().quests[id] if q then for _, def in ipairs({ { q.give, L.CODEX_TAG_GIVE, "give" }, { q.turn, L.CODEX_TAG_TURN, "turn" } }) do local ref = def[1] if ref and ref.m and ref.x and ref.y then points[#points + 1] = { m = ref.m, x = ref.x, y = ref.y, n = 4, pin = true, tag = def[3], name = string.format(def[2], ref.n or "?") } end end for _, o in ipairs(q.obj or {}) do if o.ty == "monster" then for uid in pairs(o.ids or {}) do AddEntityPoints(points, Codex.V().units[uid], uid, "units") end for key in pairs(Codex.QuestKillKeys(id)) do AddSourceKey(points, key) end elseif o.ty == "item" then for _, itemID in ipairs(Codex.ObjectiveItems(q, o, id)) do local before = #points for key in pairs(Codex.QuestItemKeys(id, itemID)) do AddSourceKey(points, key) end for key in pairs(q.qi and q.qi[itemID] or {}) do AddSourceKey(points, key) end -- eigene Quests (Creator) kennen keine Quest-Gegenstaende: dann die bekannten Quellen des Gegenstands zeigen if q.loc and #points == before then for _, p in ipairs(Codex.CollectPoints({ kind = "items", id = itemID })) do points[#points + 1] = p end end end end end end -- Vorlage: Geber, Abgabe und Ziele, soweit eigene Daten fehlen local Seed = Codex.Seed local sq = Seed and Seed.Ready() and Seed.Quest(id) if sq then local function Pin(refs, tag, have, tagName) if have then return end for _, r in ipairs(refs) do if r.kind ~= "items" then local m, x, y = Seed.FirstSpot(r.kind, r.id) if m then points[#points + 1] = { m = m, x = x, y = y, n = 4, pin = true, tag = tagName, name = string.format(tag, Seed.Name(r.kind, r.id) or ("#" .. r.id)) } end end end end Pin(sq.start, L.CODEX_TAG_GIVE, q and q.give and q.give.m, "give") Pin(sq.stop, L.CODEX_TAG_TURN, q and q.turn and q.turn.m, "turn") if not (q and q.obj and #q.obj > 0) then for _, r in ipairs(sq.obj) do if r.kind ~= "items" then AddSeedPoints(points, r.kind, r.id) end end end end end return points end -- Karte und uebergeordnete Karten (Stadt -> Gebiet -> Kontinent) function Codex.MapChain(mapID) local list, seen = { mapID }, { [mapID] = true } local CM = _G.C_Map local id = mapID for _ = 1, 6 do local ok, info = pcall(function() return CM and CM.GetMapInfo and CM.GetMapInfo(id) end) local parent = ok and info and info.parentMapID or nil if not parent or parent == 0 or seen[parent] then break end list[#list + 1] = parent seen[parent] = true id = parent end return list end -- Position des Spielers auf der Karte mapID (auch auf einer uebergeordneten Karte) function Codex.PlayerPosOn(mapID, loc) if not loc then return nil end if loc.mapID == mapID and loc.x and loc.y then return loc.x, loc.y end local CM = _G.C_Map if CM and CM.GetPlayerMapPosition then local ok, pos = pcall(CM.GetPlayerMapPosition, mapID, "player") if ok and pos and pos.GetXY then local x, y = pos:GetXY() if type(x) == "number" and type(y) == "number" and (x ~= 0 or y ~= 0) then return x, y end end end return nil end -- Kann der Spieler die Quest jetzt annehmen? Nicht angenommen, nicht abgeschlossen, Mindeststufe erreicht, -- Volk und Klasse passen, und (laut Vorlage) wurde eine Vorquest abgeschlossen. local function HasBit(mask, index) return math.floor(mask / 2 ^ (index - 1)) % 2 == 1 end -- Klasse und Volk des Spielers als Nummer (Bit-Index fuer die Masken der Quest-Daten). -- Der Client liefert UnitClass/UnitRace nur mit Name und englischem Kennwort (kein dritter Wert): daraus die Nummer ableiten. local CLASS_ID = { WARRIOR = 1, PALADIN = 2, HUNTER = 3, ROGUE = 4, PRIEST = 5, SHAMAN = 7, MAGE = 8, WARLOCK = 9, DRUID = 11 } local RACE_ID = { HUMAN = 1, ORC = 2, DWARF = 3, NIGHTELF = 4, SCOURGE = 5, UNDEAD = 5, TAUREN = 6, GNOME = 7, TROLL = 8, GOBLIN = 9, BLOODELF = 10, HIGHELF = 10 } local function PlayerClassID() if not UnitClass then return nil end local _, token, id = UnitClass("player") if type(id) == "number" and id > 0 then return id end return type(token) == "string" and CLASS_ID[token:upper()] or nil end local function PlayerRaceID() if not UnitRace then return nil end local name, token, id = UnitRace("player") if type(id) == "number" and id > 0 then return id end local key = type(token) == "string" and token:upper():gsub("[%s_]", "") or nil if key and RACE_ID[key] then return RACE_ID[key] end key = type(name) == "string" and name:upper():gsub("[%s_]", "") or nil return key and RACE_ID[key] or nil end Codex.PlayerClassID, Codex.PlayerRaceID = PlayerClassID, PlayerRaceID -- assume: Menge von Quest-IDs, die als abgegeben gelten (Plan: Folgequest nach der Abgabe) function Codex.QuestAvailable(id, assume) if Codex.QuestStatus(id) ~= nil then return false end local sq = Codex.Seed and Codex.Seed.Ready() and Codex.Seed.Quest(id) or nil local live = Codex.V().quests[id] if not sq and not live then return true end -- Voraussetzungen: Vorlage, sonst die Angaben in den eigenen Daten local min = (sq and sq.min) or (live and live.mn) if (not min or min == 0) and live and Codex.Avail then local own = Codex.db.quests[id] min = Codex.Avail.InferredMin(live, own ~= nil and own.mi ~= nil) or min end local race = (sq and sq.race) or (live and live.rc) local class = (sq and sq.class) or (live and live.cl) local pl = Codex.sim and Codex.sim.level or (UnitLevel and UnitLevel("player")) or nil if min and min > 0 and pl and pl < min then return false end if race and race > 0 then local raceID = PlayerRaceID() if raceID and not HasBit(race, raceID) then return false end end if class and class > 0 then local classID = PlayerClassID() if classID and not HasBit(class, classID) then return false end end local pre = Codex.QuestPre(id) if #pre > 0 then local any = false for _, p in ipairs(pre) do if Codex.QuestStatus(p) == "done" or (assume and assume[p]) then any = true break end end if not any then return false end end return true end -- Quest-Route: eine Station (der Questgeber) je annehmbarer Quest. -- Schritt 1 (aufwendig, einmal je Route): Kandidaten mit Position sammeln -- opts (optional): { below = n, above = m } = nur Quests von Spielerstufe - n bis Spielerstufe + m local function QuestLevel(id) local q = Codex.V().quests[id] local lv = q and tonumber(q.lvl) if not lv or lv <= 0 then local sq = Codex.Seed and Codex.Seed.Ready() and Codex.Seed.Quest(id) or nil lv = sq and tonumber(sq.lvl) end return lv and lv > 0 and lv or nil end function Codex.QuestRouteCandidates(ids, opts) local cands = {} local pl = UnitLevel and UnitLevel("player") or nil local lo, hi if opts and pl then if opts.below then lo = pl - opts.below end if opts.above then hi = pl + opts.above end end for _, id in ipairs(ids) do local inRange = true if lo or hi then local lv = QuestLevel(id) inRange = lv ~= nil and (not lo or lv >= lo) and (not hi or lv <= hi) end if inRange and Codex.QuestAvailable(id) then local pick, any for _, p in ipairs(Codex.CollectPoints({ kind = "quests", id = id })) do if p.pin then any = any or p if p.tag == "give" and not pick then pick = p end end end pick = pick or any if pick then local q = Codex.V().quests[id] cands[#cands + 1] = { id = id, m = pick.m, x = pick.x, y = pick.y, title = Codex.QuestTitle(q, id) } end end end return cands end -- Schritt 2 (billig, bei jeder Bewegung): Karte waehlen und nach naechstem Nachbarn ordnen. -- Rueckgabe: Punkte (mit Nummer im Namen), Karten-ID, Weg (Spielerposition + Stationen) local MAX_ROUTE = 40 function Codex.QuestRouteOrder(cands, loc) if #cands == 0 then return {}, nil, {} end -- Karte: die des Spielers, sonst (Stadt, Hoehle ...) die naechste uebergeordnete Karte mit Stationen. -- Ohne Spielerposition die Karte mit den meisten Stationen. Nie ein fernes Gebiet. local perMap, bestMap, bestN = {}, nil, 0 for _, c in ipairs(cands) do perMap[c.m] = (perMap[c.m] or 0) + 1 if perMap[c.m] > bestN or (perMap[c.m] == bestN and c.m < bestMap) then bestMap, bestN = c.m, perMap[c.m] end end local mapID if loc and loc.mapID then for _, m in ipairs(Codex.MapChain(loc.mapID)) do if perMap[m] then mapID = m break end end if not mapID then return {}, nil, {} end else mapID = bestMap end local pool = {} for _, c in ipairs(cands) do if c.m == mapID then pool[#pool + 1] = c end end table.sort(pool, function(a, b) return a.id < b.id end) local cx, cy = Codex.PlayerPosOn(mapID, loc) local path = {} if cx then path[1] = { m = mapID, x = cx, y = cy } end if not cx then cx, cy = pool[1].x, pool[1].y end local points = {} while #pool > 0 and #points < MAX_ROUTE do local bi, bd for i, c in ipairs(pool) do local d = (c.x - cx) ^ 2 + (c.y - cy) ^ 2 if not bd or d < bd then bi, bd = i, d end end local c = table.remove(pool, bi) points[#points + 1] = { m = c.m, x = c.x, y = c.y, n = 4, pin = true, id = c.id, name = string.format("%d. %s", #points + 1, c.title) } cx, cy = c.x, c.y path[#path + 1] = { m = c.m, x = c.x, y = c.y } end return points, mapID, path end function Codex.QuestRoute(ids, loc) return Codex.QuestRouteOrder(Codex.QuestRouteCandidates(ids), loc) end -- Itemdaten (Qualitaet, Typ, Preis) nachladen; feuert CODEX_ITEMS_LOADED, wenn etwas angekommen ist. -- Rueckgabe: Zahl neu angeforderter Items function Codex:LoadItemInfo(ids) local started = 0 for _, id in ipairs(ids) do if not state.requested[id] and not Util.GetItem(id) then state.requested[id] = true state.pendingInfo = state.pendingInfo + 1 started = started + 1 Util.RequestItem(id, function() state.pendingInfo = math.max(0, state.pendingInfo - 1) if not state.infoTimer then state.infoTimer = true C_Timer.After(0.5, function() state.infoTimer = nil FW:Fire("CODEX_ITEMS_LOADED") end) end end) end end return started end -- Durchschnittlicher Wert einer gelooteten Leiche in Kupfer: tatsaechliches Gold plus Haendlerwert -- der Items nach Drop-Chance und Durchschnittsmenge. Rueckgabe: gesamt, gold, items, Items ohne Preisdaten function Codex.KillValue(ent, useAH) local k = ent.k or 0 if k <= 0 then return nil end local coin = (ent.coin or 0) / k local items, missing = 0, 0 for id, d in pairs(ent.d or {}) do if not d.qi then local info = Util.GetItem(id) if info then local v = info.sellPrice or 0 if useAH then local ah = Codex.AuctionPrice(id) if ah and ah > v then v = ah end end items = items + (d.q / k) * v else local ah = useAH and Codex.AuctionPrice(id) if ah then items = items + (d.q / k) * ah else missing = missing + 1 end end end end return coin + items, coin, items, missing end -- Auktionshauspreis in Kupfer. Ein spaeteres Auktionsmodul setzt Codex.AuctionProvider = function(itemID) return kupfer end; -- ohne Anbieter bleibt der Wert leer (und die Sortierung "Auktionspreis" wird nicht angeboten). function Codex.AuctionPrice(itemID) local fn = Codex.AuctionProvider if type(fn) ~= "function" then return nil end local ok, v = pcall(fn, itemID) if ok and type(v) == "number" and v == v and v >= 0 then return v end return nil end function Codex:Counts() local q, u, o, n = 0, 0, 0, 0 for _ in pairs(Codex.V().quests) do q = q + 1 end for _ in pairs(Codex.V().units) do u = u + 1 end for _ in pairs(Codex.V().objects) do o = o + 1 end for _ in pairs(self.db.names) do n = n + 1 end return q, u, o, n end ------------------------------------------------------------------------ -- Start ------------------------------------------------------------------------ function Codex:OnEnable() -- Teilen ist von Anfang an an: einmalig auch bei vorhandenen Daten einschalten (danach gilt, was der Spieler waehlt) if self.db and self.db.settings and not self.db.settings.shareDefaultOn then self.db.settings.shareDefaultOn = true self.db.settings.share = true end FW:RegisterEvent("QUEST_DETAIL", self.OnQuestDetail, self) FW:RegisterEvent("QUEST_ACCEPTED", self.OnQuestAccepted, self) FW:RegisterEvent("QUEST_PROGRESS", self.OnQuestProgress, self) FW:RegisterEvent("QUEST_COMPLETE", self.OnQuestComplete, self) FW:RegisterEvent("QUEST_TURNED_IN", self.OnQuestTurnedIn, self) FW:RegisterEvent("QUEST_REMOVED", self.OnQuestRemoved, self) FW:RegisterEvent("QUEST_LOG_UPDATE", self.OnQuestLogUpdate, self) FW:RegisterEvent("UNIT_QUEST_LOG_CHANGED", self.OnQuestLogUpdate, self) FW:RegisterEvent("LOOT_OPENED", self.OnLootOpened, self) FW:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE", self.OnFactionMessage, self) FW:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN", self.OnXPMessage, self) FW:RegisterEvent("PLAYER_LEVEL_UP", self.OnLevelUp, self) for _, event in ipairs({ "PLAYER_REGEN_DISABLED", "PLAYER_ENTERING_WORLD", "UPDATE_EXHAUSTION" }) do local event = event FW:RegisterEvent(event, function() Codex:OnPoolEvent(event) end, self) end C_Timer.After(2, function() Codex:OnPoolEvent("PLAYER_ENTERING_WORLD") end) state.pl = Num(UnitLevel and UnitLevel("player")) FW:RegisterEvent("PLAYER_TARGET_CHANGED", self.OnTargetChanged, self) FW:RegisterEvent("UPDATE_MOUSEOVER_UNIT", self.OnMouseover, self) if not self.hooked and hooksecurefunc and type(_G.GetQuestReward) == "function" then hooksecurefunc("GetQuestReward", function(index) Codex:OnChoice(index) end) self.hooked = true end if not self.useHooked and hooksecurefunc then local ci = _G.C_Container if ci and type(ci.UseContainerItem) == "function" then hooksecurefunc(ci, "UseContainerItem", function(bag, slot) Codex.NoteItemUse(bag, slot) end) self.useHooked = true elseif type(_G.UseContainerItem) == "function" then hooksecurefunc("UseContainerItem", function(bag, slot) Codex.NoteItemUse(bag, slot) end) self.useHooked = true end end if not self.tipHooked and TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum and Enum.TooltipDataType and Enum.TooltipDataType.Object then local ok = pcall(TooltipDataProcessor.AddTooltipPostCall, Enum.TooltipDataType.Object, function(_, data) Codex:OnObjectTooltip(data) end) self.tipHooked = ok end C_Timer.After(3, function() Codex:ScanLog() end) C_Timer.After(6, function() Safe(Codex.FillGaps) end) C_Timer.After(40, function() Safe(Codex.UpgradeEffects) end) C_Timer.After(10, function() Safe(Codex.PurgeMissing) end) C_Timer.After(90, function() local ls = Codex.db and Codex.db.itemload if ls and ls.on and Codex.AllItemIds then Safe(function() Codex.ItemLoadStart(Codex.AllItemIds(), true) end) end end) C_Timer.After(4, function() if Codex.Forever then Codex.Forever.Load() end end) if self.Avail then self.Avail:Start() end if self.Names then self.Names:Start() end if self.Time then self.Time:Start() end if self.Roles then self.Roles:Start() end if self.Recipes then self.Recipes:Start() end if self.Sync then self.Sync:Init() end if self.Lookup then self.Lookup:Start() end end function Codex:OnDisable() for _, event in ipairs({ "QUEST_DETAIL", "QUEST_ACCEPTED", "QUEST_PROGRESS", "QUEST_COMPLETE", "QUEST_TURNED_IN", "QUEST_REMOVED", "QUEST_LOG_UPDATE", "UNIT_QUEST_LOG_CHANGED", "LOOT_OPENED", "CHAT_MSG_COMBAT_FACTION_CHANGE", "CHAT_MSG_COMBAT_XP_GAIN", "PLAYER_LEVEL_UP", "PLAYER_TARGET_CHANGED", "UPDATE_MOUSEOVER_UNIT", "PLAYER_REGEN_DISABLED", "PLAYER_ENTERING_WORLD", "UPDATE_EXHAUSTION" }) do FW:UnregisterEvent(event, self) end if self.Names then self.Names:Stop() end if self.Time then self.Time:Stop() end if self.Roles then self.Roles:Stop() end if self.Recipes then self.Recipes:Stop() end if self.Sync then self.Sync:Stop() end if self.Lookup then self.Lookup:Stop() end end -- Test im Spiel: /fwcodex test function Codex:RunTest() local Lc = FW.L local function yn(v) return v and Lc.CODEX_YES or Lc.CODEX_NO end FW:Print(string.format(Lc.CODEX_TEST_API, yn(GetLootSourceInfo), yn(C_QuestLog and C_QuestLog.GetQuestObjectives), yn(GetQuestReward), yn(self.hooked))) local q, u, o, n = self:Counts() FW:Print(string.format(Lc.CODEX_TEST_COUNTS, q, u, o, n)) FW:Print(string.format(Lc.CODEX_TEST_STATS, state.stats.loot, state.stats.errors)) local rows = self:XPRows(true) FW:Print(string.format(Lc.CODEX_TEST_XP, #rows, #self.db.xpUnparsed)) for _, text in ipairs(self.db.xpUnparsed) do FW:Print(" ? " .. text) end if self.Share then local own = self.Share.Summary() local senders, records = self.Share.RemoteStats() FW:Print(string.format(Lc.CODEX_TEST_SHARE, self.db.settings.share and Lc.CODEX_ON or Lc.CODEX_OFF, own or 0, senders, records)) end local empty = self.db.objEmpty or {} FW:Print(string.format(Lc.CODEX_TEST_OBJ, #empty)) for _, text in ipairs(empty) do FW:Print(" ? " .. text) end end