diff --git a/CodexMapAPI.lua b/CodexMapAPI.lua new file mode 100644 index 0000000..283a553 --- /dev/null +++ b/CodexMapAPI.lua @@ -0,0 +1,257 @@ +-- Freewood-OctoWoW-Codex: Kartenschnittstelle fuer die Weltkarte des 1.12-Clients (OctoWoW). +-- Die alte Karte kennt weder SetMapID noch GetMapID noch eine Kartenflaeche mit Zoom. Stattdessen: +-- Kartenflaeche = WorldMapDetailFrame (Anteile 0..1 von links oben), Zone umschalten per SetMapZoom(Kontinent, Zone). +-- Die Zuordnung Karten-ID (AreaTable) -> Kontinent/Zone laeuft ueber den Zonennamen (C_Map.GetMapInfo gegen GetMapZones) +-- und wird zusaetzlich beim Spielen gelernt (stehst du in einer Zone, merkt sich Codex ihre Nummer). +-- Auf Clients mit Retail-Karte (SetMapID/GetMapID) werden diese Funktionen direkt benutzt. + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex then + return +end + +local API = {} +Codex.MapAPI = API + +local resolved = {} -- [mapID] = "c:z" oder false (nicht gefunden) +local zoneToID = {} -- ["c:z"] = mapID + +local function Learned() + local st = Codex.db and Codex.db.settings + if not st then return {} end + st.mapZones = st.mapZones or {} + return st.mapZones +end + +local function Norm(s) + if type(s) ~= "string" then return nil end + s = string.gsub(string.lower(s), "[^%w]", "") + if s == "" then return nil end + return s +end + +function API.Classic() + local wm = _G.WorldMapFrame + return not (wm and wm.GetMapID and wm.SetMapID) +end + +function API.Canvas() + local wm = _G.WorldMapFrame + if not wm then return nil end + if not API.Classic() then + if wm.GetCanvas then + local ok, c = pcall(wm.GetCanvas, wm) + if ok and c then return c end + end + return wm.ScrollContainer and wm.ScrollContainer.Child or nil + end + return _G.WorldMapDetailFrame or _G.WorldMapButton +end + +-- Kontinent/Zone der angezeigten Karte +function API.Current() + if not (_G.GetCurrentMapContinent and _G.GetCurrentMapZone) then return nil end + local c, z = _G.GetCurrentMapContinent(), _G.GetCurrentMapZone() + if type(c) ~= "number" or type(z) ~= "number" then return nil end + return c, z +end + +function API.Signature() + local c, z = API.Current() + return c and (c .. ":" .. z) or "?" +end + +local function MapName(id) + local CM = _G.C_Map + if CM and CM.GetMapInfo then + local ok, info = pcall(CM.GetMapInfo, id) + if ok and type(info) == "table" and type(info.name) == "string" then return info.name end + end + if _G.C_Map and _G.C_Map.GetAreaInfo then + local ok, name = pcall(_G.C_Map.GetAreaInfo, id) + if ok and type(name) == "string" then return name end + end + return nil +end + +-- Karten-ID -> Kontinent, Zone (Zone 0 = Kontinentkarte) +function API.Resolve(id) + if type(id) ~= "number" then return nil end + local key = Learned()[id] or resolved[id] + if key == nil then + key = false + local want = Norm(MapName(id)) + if want and _G.GetMapContinents and _G.GetMapZones then + local conts = { _G.GetMapContinents() } + for c = 1, #conts do + if Norm(conts[c]) == want then key = c .. ":0" break end + local zones = { _G.GetMapZones(c) } + for z = 1, #zones do + if Norm(zones[z]) == want then key = c .. ":" .. z break end + end + if key then break end + end + end + resolved[id] = key + end + if not key then return nil end + local _, _, c, z = string.find(key, "^(%d+):(%d+)$") + return tonumber(c), tonumber(z) +end + +-- Karten-ID der angezeigten Karte. cands: Liste oder Funktion, die Karten-IDs liefert, unter denen gesucht wird. +function API.CurrentMapID(cands) + local wm = _G.WorldMapFrame + if not API.Classic() then + if wm and wm.GetMapID then + local ok, id = pcall(wm.GetMapID, wm) + if ok then return id end + end + return nil + end + local c, z = API.Current() + if not c then return nil end + local key = c .. ":" .. z + if zoneToID[key] then return zoneToID[key] end + for id, k in pairs(Learned()) do + if k == key then zoneToID[key] = id return id end + end + if type(cands) == "function" then cands = cands() end + local list = cands or {} + local bm = _G.C_Map and _G.C_Map.GetBestMapForUnit and select(2, pcall(_G.C_Map.GetBestMapForUnit, "player")) + if type(bm) == "number" then list[#list + 1] = bm end + for i = 1, #list do + local id = list[i] + local rc, rz = API.Resolve(id) + if rc == c and rz == z then + zoneToID[key] = id + return id + end + end + -- Zone ohne bekannte Daten (und noch nie betreten): Karten-ID ueber den Zonennamen suchen. + -- Quelle: zuerst der Karten-Scan (db.cat.m), dann alle Nummern 1..8000 durchprobieren. Ergebnis wird gemerkt (auch dauerhaft), + -- ein Fehlschlag fuer 60 Sekunden. + local now = _G.GetTime and _G.GetTime() or 0 + if API.noMatch and API.noMatch[key] and now - API.noMatch[key] < 60 then return nil end + local zname + if _G.GetMapContinents and _G.GetMapZones then + if z == 0 then zname = ({ _G.GetMapContinents() })[c] else zname = ({ _G.GetMapZones(c) })[z] end + end + local target = Norm(zname) + if target then + local ids = {} + local m = Codex.db and Codex.db.cat and Codex.db.cat.m + -- erst die Karten aus dem Scan (schnell), dann der ganze Bereich: der Scan kennt nicht jede Karte (z. B. Blackstone Island) + local seen = {} + if type(m) == "table" then + for id in pairs(m) do if type(id) == "number" then ids[#ids + 1] = id; seen[id] = true end end + table.sort(ids) + end + for id = 1, 8000 do if not seen[id] then ids[#ids + 1] = id end end + for i = 1, #ids do + local id = ids[i] + if Norm(MapName(id)) == target then + zoneToID[key] = id + resolved[id] = key + Learned()[id] = key + return id + end + end + end + API.noMatch = API.noMatch or {} + API.noMatch[key] = now + return nil +end + +-- Blizzards Kartenfenster (Titel, Auswahlfelder) nachziehen, falls es die Umschaltung nicht selbst tut +function API.RefreshWindow(c, z) + local names = { "WorldMapFrame_Update", "WorldMapContinentDropDown_Update", "WorldMapZoneDropDown_Update", "WorldMapZoomOutButton_Update" } + for i = 1, #names do + local f = _G[names[i]] + if type(f) == "function" then pcall(f) end + end + local title = _G.WorldMapFrameTitle or _G.WorldMapFrameTitleText + if title and title.SetText and _G.GetMapZones and _G.GetMapContinents then + local name + if z == 0 then + local conts = { _G.GetMapContinents() } + name = conts[c] + else + local zones = { _G.GetMapZones(c) } + name = zones[z] + end + if name then pcall(title.SetText, title, name) end + end +end + +function API.SetMap(id) + local wm = _G.WorldMapFrame + if not API.Classic() then + if wm and wm.SetMapID then pcall(wm.SetMapID, wm, id) return true end + return false + end + local c, z = API.Resolve(id) + if not c or not _G.SetMapZoom then return false end + if z == 0 then pcall(_G.SetMapZoom, c) else pcall(_G.SetMapZoom, c, z) end + API.RefreshWindow(c, z) + if _G.C_Timer and _G.C_Timer.After then + _G.C_Timer.After(0.2, function() API.RefreshWindow(c, z) end) + end + return true +end + +-- Mauszeiger auf der Kartenflaeche als Anteil 0..1 (links oben), sonst nil +function API.CursorNorm() + local canvas = API.Canvas() + if not (canvas and canvas.IsVisible and canvas:IsVisible()) then return nil end + local l, t, w, h = canvas:GetLeft(), canvas:GetTop(), canvas:GetWidth(), canvas:GetHeight() + if not (l and t and w and h and w > 1 and h > 1) then return nil end + local cx, cy = _G.GetCursorPosition() + local s = canvas:GetEffectiveScale() + if not (cx and s and s > 0) then return nil end + local x, y = (cx / s - l) / w, (t - cy / s) / h + if x < 0 or x > 1 or y < 0 or y > 1 then return nil end + return x, y +end + +-- Zonen-Nummern lernen: Karte kurz auf die aktuelle Zone stellen, solange das Kartenfenster zu ist +local function LearnHere() + local wm = _G.WorldMapFrame + if not API.Classic() or (wm and wm:IsShown()) then return end + if not (_G.SetMapToCurrentZone and _G.C_Map and _G.C_Map.GetBestMapForUnit) then return end + local ok, id = pcall(_G.C_Map.GetBestMapForUnit, "player") + if not ok or type(id) ~= "number" then return end + pcall(_G.SetMapToCurrentZone) + local c, z = API.Current() + if c and z and z > 0 then + local L = Learned() + L[id] = c .. ":" .. z + zoneToID[c .. ":" .. z] = id + end +end + +local function LearnLater() + if _G.C_Timer and _G.C_Timer.After then + _G.C_Timer.After(1.5, LearnHere) + else + LearnHere() + end +end + +local f = CreateFrame("Frame") +f:RegisterEvent("PLAYER_ENTERING_WORLD") +f:RegisterEvent("ZONE_CHANGED_NEW_AREA") +f:SetScript("OnEvent", LearnLater) + +-- /fwcodex mapzones: zeigt die Zuordnung fuer die angezeigte Karte +function API.Debug() + local c, z = API.Current() + FW:Print("Karte: Kontinent " .. tostring(c) .. ", Zone " .. tostring(z) .. ", Flaeche " + .. tostring(API.Canvas() and API.Canvas():GetName()) .. ", klassisch: " .. tostring(API.Classic())) + local n = 0 + for id, k in pairs(Learned()) do n = n + 1 end + FW:Print("Gelernte Zonen: " .. n) +end diff --git a/CodexMapScan.lua b/CodexMapScan.lua new file mode 100644 index 0000000..c03cd1f --- /dev/null +++ b/CodexMapScan.lua @@ -0,0 +1,304 @@ +-- Freewood-OctoWoW-Codex: Karten-Scan (ClassicAPI C_Map, C_TaxiMap) +-- Liest lokal (ohne Serveranfragen) alle Karten, Gebiete, Gebiets-Trigger und Flugpunkte aus dem Client. +-- Ablage in db.cat: m = Karten, a = Gebiete, ma = Gebiete je Karte, t = Trigger je Karte, x = Flugpunkte, shape = Aufbau der Antworten (zur Pruefung) +-- m[id] = "Feld=Wert;...|Name" a[id] = "Name" (oder "Feld=Wert;...|Name") t[mapID] = { "Feld=Wert;...", ... } x[nodeID] = "Feld=Wert;...|Name" + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex then + return +end + +local Scan = { } +Codex.MapScan = Scan +NS.MapScan = Scan + +local MAX_MAP = 2500 +local MAX_AREA = 6000 +local SLICE = 150 -- Eintraege je Schritt +local TICK = 0.05 + +local function Say(text) FW:Print("Karten-Scan: " .. text) end + +local function Store() + local db = Codex.db + if not db then return nil end + db.cat = db.cat or {} + local c = db.cat + c.m = c.m or {} + c.a = c.a or {} + c.ma = c.ma or {} + c.t = c.t or {} + c.x = c.x or {} + c.shape = c.shape or {} + return c +end + +local function Clean(t) + t = tostring(t):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|", "/"):gsub(";", ",") + return t +end + +-- Aufbau einer Antwort als Text (nur die erste Antwort je Funktion), damit man die Felder pruefen kann +local function Describe(v, depth) + depth = depth or 0 + local tv = type(v) + if tv ~= "table" then return tv .. (tv == "number" and ("=" .. tostring(v)) or (tv == "string" and ("=" .. v:sub(1, 20)) or "")) end + if depth >= 2 then return "table" end + local keys = {} + for k in pairs(v) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) + local parts = {} + for i, k in ipairs(keys) do + if i > 24 then parts[#parts + 1] = "..." break end + parts[#parts + 1] = tostring(k) .. ":" .. Describe(v[k], depth + 1) + end + return "{" .. table.concat(parts, ", ") .. "}" +end + +local function Shape(name, v) + local c = Store() + if c and c.shape[name] == nil then c.shape[name] = Describe(v):sub(1, 700) end +end + +-- einfache Felder als "Feld=Wert;..." (Zahlen, Wahrheitswerte, Zahlenlisten, x/y-Paare) +local function Pack(info, skip) + local keys = {} + for k, v in pairs(info) do + local tv = type(v) + if type(k) == "string" and k ~= skip and (tv == "number" or tv == "boolean" or tv == "table") then keys[#keys + 1] = k end + end + table.sort(keys) + local parts = {} + for _, k in ipairs(keys) do + local v = info[k] + if type(v) == "number" then + parts[#parts + 1] = k .. "=" .. (v == math.floor(v) and string.format("%d", v) or string.format("%.4f", v)) + elseif type(v) == "boolean" then + parts[#parts + 1] = k .. "=" .. (v and "1" or "0") + else + if type(v.x) == "number" and type(v.y) == "number" then + parts[#parts + 1] = k .. "=" .. string.format("%.4f:%.4f", v.x, v.y) + else + local list = {} + for i = 1, 40 do + if type(v[i]) == "number" then list[#list + 1] = tostring(v[i]) else break end + end + if #list > 0 then parts[#parts + 1] = k .. "=" .. table.concat(list, ",") end + end + end + end + return table.concat(parts, ";") +end + +local function Name(info) + if type(info) ~= "table" then return nil end + local n = info.name or info.areaName or info.mapName + return type(n) == "string" and n ~= "" and n or nil +end + +local function Count(t) local n = 0 for _ in pairs(t or {}) do n = n + 1 end return n end + +-- Phasen: jede holt Eintraege in Scheiben +local phases = {} + +phases[1] = { label = "Karten", total = function() return MAX_MAP end, step = function(c, id) + local CM = _G.C_Map + if not (CM and CM.GetMapInfo) then return end + local ok, info = pcall(CM.GetMapInfo, id) + if ok and type(info) == "table" then + local nm = Name(info) + if nm then + Shape("GetMapInfo", info) + c.m[id] = Pack(info, "name") .. "|" .. Clean(nm) + end + end +end } + +phases[2] = { label = "Gebiete", total = function() return MAX_AREA end, step = function(c, id) + local CM = _G.C_Map + if not (CM and CM.GetAreaInfo) then return end + local ok, r = pcall(CM.GetAreaInfo, id) + if not ok then return end + if type(r) == "string" and r ~= "" then + Shape("GetAreaInfo", r) + c.a[id] = Clean(r) + elseif type(r) == "table" and Name(r) then + Shape("GetAreaInfo", r) + c.a[id] = Pack(r, "name") .. "|" .. Clean(Name(r)) + end +end } + +-- Karten-IDs fuer die Phasen 3 und 4 +local mapIDs +local function MapIDs(c) + if not mapIDs then + -- Karten aus der Kartenliste plus die Welt-/Instanzkarten (0 bis 700): Trigger und Flugpunkte haengen an der Welt-Karten-ID + local seen = {} + mapIDs = {} + for id = 0, 700 do seen[id] = true mapIDs[#mapIDs + 1] = id end + for id in pairs(c.m) do + if type(id) == "number" and not seen[id] then seen[id] = true mapIDs[#mapIDs + 1] = id end + end + table.sort(mapIDs) + end + return mapIDs +end + +phases[3] = { label = "Trigger", total = function() local c = Store() return #MapIDs(c) end, step = function(c, i) + local CM = _G.C_Map + local id = MapIDs(c)[i] + if not (CM and id) then return end + if CM.GetAreaTriggers then + local ok, list = pcall(CM.GetAreaTriggers, id) + if ok and type(list) == "table" and next(list) then + Shape("GetAreaTriggers", list) + local rows = {} + for _, t in pairs(list) do + if type(t) == "table" then + rows[#rows + 1] = Pack(t) + elseif type(t) == "number" then + rows[#rows + 1] = "id=" .. t + end + end + if #rows > 0 then c.t[id] = rows end + end + end + -- Gebiete je Karte (zwei moegliche Funktionen) + for _, fname in ipairs({ "GetMapAreaIDs", "GetAreas" }) do + local f = CM[fname] + if f and not Scan.maDone then + local ok, list = pcall(f, id) + if ok and type(list) == "table" and next(list) then + Shape(fname, list) + local ids = {} + for k, v in pairs(list) do + if type(v) == "number" then ids[#ids + 1] = v + elseif type(v) == "table" then + local a = v.areaID or v.id + if type(a) == "number" then ids[#ids + 1] = a end + elseif type(k) == "number" and type(v) == "string" then ids[#ids + 1] = k end + end + table.sort(ids) + if #ids > 0 then c.ma[0] = table.concat(ids, ",") Scan.maDone = true end + end + end + end +end } + +local function TakeNodes(c, list) + if type(list) ~= "table" then return end + for _, n in pairs(list) do + if type(n) == "table" and type(n.nodeID) == "number" then + Shape("TaxiNode", n) + c.x[n.nodeID] = Pack(n, "name") .. "|" .. Clean(Name(n) or "") + end + end +end + +phases[4] = { label = "Flugpunkte", total = function() local c = Store() return #MapIDs(c) + 1 end, step = function(c, i) + local T = _G.C_TaxiMap + if not T then return end + local ids = MapIDs(c) + if i == 1 and T.GetAllTaxiNodes then + local ok, list = pcall(T.GetAllTaxiNodes) + if ok then TakeNodes(c, list) end + end + local id = ids[i - 1] + if id and T.GetAllTaxiNodes then + local ok, list = pcall(T.GetAllTaxiNodes, id) + if ok then TakeNodes(c, list) end + end + if id and T.GetTaxiNodesForMap then + local ok, list = pcall(T.GetTaxiNodesForMap, id) + if ok then TakeNodes(c, list) end + end +end } + +Scan.state = nil + +function Scan.Counts() + local c = Codex.db and Codex.db.cat + if not c then return 0, 0, 0, 0 end + return Count(c.m), Count(c.a), Count(c.t), Count(c.x) +end + +function Scan.Running() return Scan.state ~= nil and not Scan.state.stop end + +local function Tick(st) + if Scan.state ~= st then return end + if st.stop then Scan.state = nil return end + local c = Store() + local ph = phases[st.phase] + if not (c and ph) then Scan.state = nil return end + local total = ph.total() + local last = math.min(st.i + SLICE - 1, total) + for i = st.i, last do + local ok = pcall(ph.step, c, i) + if not ok then st.errors = (st.errors or 0) + 1 end + end + st.i = last + 1 + st.total = total + if st.i > total then + Say(string.format("%s fertig.", ph.label)) + if st.phase == 1 then mapIDs = nil end + st.phase = st.phase + 1 + st.i = 1 + if not phases[st.phase] then + local m, a, t, x = Scan.Counts() + Say(string.format("fertig: %d Karten, %d Gebiete, Trigger auf %d Karten, %d Flugpunkte%s.", m, a, t, x, + st.errors and (" (" .. st.errors .. " Fehler)") or "")) + Scan.state = nil + if FW.Fire then FW:Fire("CODEX_CHANGED", "mapscan") end + return + end + end + _G.C_Timer.After(TICK, function() Tick(st) end) +end + +-- Rueckgabe: true | false, Grund +function Scan.Start() + if Scan.state then return false, "busy" end + local CM = _G.C_Map + if not (CM and CM.GetMapInfo) or not _G.C_Timer then return false, "api" end + if not Store() then return false, "db" end + mapIDs = nil + Scan.maDone = false + local st = { phase = 1, i = 1, total = MAX_MAP } + Scan.state = st + Tick(st) + return true +end + +function Scan.Stop() + if Scan.state then Scan.state.stop = true end +end + +function Scan.Label() + local st = Scan.state + if not st then return nil end + return phases[st.phase] and phases[st.phase].label or "", math.min(st.i, st.total or 1), st.total or 1 +end + +SLASH_FREEWOODMAPSCAN1 = "/fwkarte" +SlashCmdList["FREEWOODMAPSCAN"] = function(msg) + msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "") + if msg == "stop" then + if Scan.Running() then Scan.Stop() Say("wird angehalten.") else Say("es laeuft nichts.") end + return + elseif msg == "status" then + local m, a, t, x = Scan.Counts() + Say(string.format("%d Karten, %d Gebiete, Trigger auf %d Karten, %d Flugpunkte.", m, a, t, x)) + return + end + local ok, why = Scan.Start() + if ok then + Say("gestartet (Karten, Gebiete, Trigger, Flugpunkte; lokal, ohne Serveranfragen). Anhalten: /fwkarte stop.") + elseif why == "busy" then + Say("es laeuft schon ein Scan.") + else + Say("nicht moeglich: " .. tostring(why)) + end +end diff --git a/CodexNameUI.lua b/CodexNameUI.lua new file mode 100644 index 0000000..b66b717 --- /dev/null +++ b/CodexNameUI.lua @@ -0,0 +1,805 @@ +-- Freewood-OctoWoW-Codex: Namen von Hand eintragen (Deutsch / Englisch) +-- Zwei Fenster im Tab "Datenbank": +-- "Name bearbeiten" fuer den gewaehlten Eintrag (Kreatur, Objekt, Quest, Item), mit Vorschlaegen aus dem Kampflog +-- "Namen einfuegen" mehrere Zeilen "ID Name" auf einmal fuer die aktuelle Liste + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex or not FW.UI then + return +end + +local UI = FW.UI +local Util = FW.Util + +local NameUI = {} +NS.NameUI = NameUI +Codex.NameUI = NameUI + +local EDITABLE = { units = true, objects = true, quests = true, items = true } +local KIND_LABEL = { units = "CODEX_KIND_UNITS", objects = "CODEX_KIND_OBJECTS", quests = "CODEX_KIND_QUESTS", items = "CODEX_KIND_ITEMS" } +local MAX_SUGGEST = 6 + +local function MakeInput(parent, width) + local box = UI.CreatePanel(parent, "bgDeep", "bronze") + box:SetSize(width, 26) + local edit = CreateFrame("EditBox", nil, box) + edit:SetAutoFocus(false) + edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2) + edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2) + edit:SetMaxLetters(100) + if not edit:SetFont(UI.GetFontPath("body"), 12, "") then + edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "") + end + edit:SetTextColor(UI.Color("text")) + edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) + box.edit = edit + return box +end + +-- Wowhead-Adresse fuer Nachschlagen (an einer Stelle, falls sich die Domain aendert) +local WOWHEAD_BASE = "https://www.wowhead.com/forever/" +local WOWHEAD_PATH = { units = "npc", objects = "object", quests = "quest", items = "item" } +function NameUI.LookupURL(kind, id, lang) + local path = WOWHEAD_PATH[kind] + if not path or not id then return nil end + return WOWHEAD_BASE .. (lang == "de" and "de/" or "") .. path .. "=" .. tostring(id) +end + +-- Schreibgeschuetztes Feld: Klick markiert alles, dann Strg+C +local function MakeLink(parent, width) + local box = MakeInput(parent, width) + box:SetHeight(22) + local edit = box.edit + edit:SetMaxLetters(0) + edit:SetTextColor(UI.Color("goldLight")) + local function Select(self) self:HighlightText() end + edit:SetScript("OnEditFocusGained", Select) + edit:SetScript("OnMouseUp", function(self) self:SetFocus(); self:HighlightText() end) + edit:SetScript("OnTextChanged", function(self, user) + if user and box.fwUrl and self:GetText() ~= box.fwUrl then + self:SetText(box.fwUrl) + self:HighlightText() + end + end) + function box.SetURL(url) + box.fwUrl = url or "" + edit:SetText(url or "") + edit:SetCursorPosition(0) + end + return box +end + +local function MakeWindow(parent, width, height) + local w = UI.CreatePanel(parent, "bg", "gold") + w:SetSize(width, height) + w:SetPoint("CENTER", parent, "CENTER", 0, 0) + w:SetFrameStrata("DIALOG") + w:EnableMouse(true) + w:Hide() + return w +end + +local function GameName(kind, id) + local v = Codex.V() + if kind == "units" then + local e = v.units[id] + return e and e.n + elseif kind == "objects" then + local e = v.objects[id] + return e and e.n + elseif kind == "quests" then + local q = v.quests[id] + return q and q.t + end + local info = Util.GetItem and Util.GetItem(id) + return (info and info.name) or Codex.db.names[id] +end + +------------------------------------------------------------------------ +-- Fenster "Name bearbeiten" +------------------------------------------------------------------------ + +local function BuildEdit(parent, onChanged) + local L = FW.L + local win = MakeWindow(parent, 480, 520) + local state = {} + + local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" }) + title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14) + local game = UI.CreateLabel(win, "", { size = 11, color = "textDim" }) + game:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40) + game:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40) + game:SetWordWrap(false) + + local deLabel = UI.CreateLabel(win, L.CODEX_NAME_DE, { size = 12 }) + deLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -68) + local deBox = MakeInput(win, 448) + deBox:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -86) + local deLink = MakeLink(win, 448) + deLink:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -114) + local enLabel = UI.CreateLabel(win, L.CODEX_NAME_EN, { size = 12 }) + enLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -146) + local enBox = MakeInput(win, 448) + enBox:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -164) + local enLink = MakeLink(win, 448) + enLink:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -192) + + local hint = UI.CreateLabel(win, "", { size = 11, color = "textDim", justifyV = "TOP" }) + hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -222) + hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -222) + hint:SetHeight(32) + hint:SetWordWrap(true) + + local sugTitle = UI.CreateLabel(win, L.CODEX_NAME_KILLS, { size = 12, color = "goldLight" }) + sugTitle:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -260) + local sugNone = UI.CreateLabel(win, L.CODEX_NAME_NOKILLS, { size = 11, color = "textDim", justifyV = "TOP" }) + sugNone:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -284) + sugNone:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -284) + sugNone:SetHeight(32) + sugNone:SetWordWrap(true) + + local function Fill(text) + local box = Codex.Lang() == "en" and enBox or deBox + box.edit:SetText(text or "") + box.edit:SetFocus() + end + local sugButtons = {} + for i = 1, MAX_SUGGEST do + local b = UI.CreateButton(win, "", 448, 24, function(self) + if self.fwText then Fill(self.fwText) end + end, "default") + b:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -284 - (i - 1) * 28) + b:Hide() + sugButtons[i] = b + end + + local function Save() + if state.kind and state.id then + Codex:SetCustom(state.kind, state.id, deBox.edit:GetText(), enBox.edit:GetText()) + if onChanged then onChanged() end + end + win:Hide() + end + local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 140, 28, Save, "primary") + save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14) + local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 140, 28, function() + deBox.edit:SetText("") + enBox.edit:SetText("") + Save() + end, "default") + clear:SetPoint("LEFT", save, "RIGHT", 8, 0) + local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 140, 28, function() win:Hide() end, "default") + cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0) + + function win.Open(kind, id) + state.kind, state.id = kind, id + local label = L[KIND_LABEL[kind]] or kind + title:SetText(string.format("%s #%s", label, tostring(id))) + local gn = GameName(kind, id) + game:SetText(gn and string.format(L.CODEX_NAME_GAME, gn) or "") + local de, en = Codex:GetCustom(kind, id) + deBox.edit:SetText(de or "") + enBox.edit:SetText(en or "") + hint:SetText((kind == "items" and L.CODEX_NAME_HINT_LOCAL or L.CODEX_NAME_HINT) .. "\n" .. (L.CODEX_NAME_LINK or "")) + deLink.SetURL(NameUI.LookupURL(kind, id, "de")) + enLink.SetURL(NameUI.LookupURL(kind, id, "en")) + local names = {} + if kind == "units" and Codex.Names and Codex.Names.RecentKills then + for _, k in ipairs(Codex.Names:RecentKills(900, MAX_SUGGEST)) do + names[#names + 1] = k.name + if k.alt then names[#names + 1] = k.alt end + end + end + local showSug = kind == "units" + if showSug then sugTitle:Show() else sugTitle:Hide() end + if showSug and #names == 0 then sugNone:Show() else sugNone:Hide() end + for i, b in ipairs(sugButtons) do + local text = names[i] + if text then + b.fwText = text + b:SetLabel(text) + b:Show() + else + b.fwText = nil + b:Hide() + end + end + win.fwKey = kind .. ":" .. tostring(id) + win:Show() + win:Raise() + end + return win +end + +------------------------------------------------------------------------ +-- Fenster "Eigenschaften bearbeiten": Zone(n), Dungeon, Klasse, Stufen +-- (Blizzard liefert aus Instanzen keine Orte und oft keine Klasse; hier koennen Berechtigte sie nachtragen) +------------------------------------------------------------------------ + +local mapIndex + +-- Alle Karten des Spiels (Name -> ID), einmal gelesen +local function MapIndex() + if mapIndex then return mapIndex end + mapIndex = {} + if C_Map and C_Map.GetMapInfo then + for id = 1, 3000 do + local ok, info = pcall(C_Map.GetMapInfo, id) + if ok and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then + mapIndex[#mapIndex + 1] = { id = id, name = info.name, lower = info.name:lower() } + end + end + end + return mapIndex +end + +-- Karten, deren Name den Text enthaelt (oder deren ID passt). Rueckgabe: Liste { id, name } +function NameUI.FindMaps(text, limit) + local out = {} + text = type(text) == "string" and text:lower():gsub("^%s+", ""):gsub("%s+$", "") or "" + if text == "" then return out end + local num = tonumber(text) + if num and num >= 1 and num == math.floor(num) then + out[1] = { id = num, name = Codex.MapName(num) } + end + -- eingebaute Instanzen (Dungeons, Schlachtfelder): negative ID + local ids = {} + for id in pairs(Codex.INSTANCES) do ids[#ids + 1] = id end + table.sort(ids) + for _, id in ipairs(ids) do + if #out >= (limit or 6) then break end + local b = Codex.INSTANCES[id] + if b[1]:lower():find(text, 1, true) or b[2]:lower():find(text, 1, true) then + out[#out + 1] = { id = -id, name = Codex.InstanceName(id) } + end + end + for _, m in ipairs(MapIndex()) do + if #out >= (limit or 6) then break end + if m.lower:find(text, 1, true) and m.id ~= num then + out[#out + 1] = { id = m.id, name = m.name } + end + end + return out +end + +local CLASS_CYCLE = { false, "normal", "elite", "rare", "rareelite", "worldboss" } + +local function ClassLabel(cl) + local L = FW.L + if not cl then return L.CODEX_PROPS_AUTO end + return L["CODEX_PROPS_CLASS_" .. cl] or cl +end + +local function BuildProps(parent, onChanged) + local L = FW.L + local win = MakeWindow(parent, 520, 650) + local state = { z = {} } + local MAXZ = Codex.MAX_ED_ZONES + + local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" }) + title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14) + local game = UI.CreateLabel(win, "", { size = 11, color = "textDim" }) + game:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40) + game:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40) + game:SetWordWrap(false) + + -- Zonen + local zoneTitle = UI.CreateLabel(win, L.CODEX_PROPS_ZONES, { size = 12, color = "goldLight" }) + zoneTitle:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -68) + local zoneRows = {} + for i = 1, MAXZ do local i = i + local lbl = UI.CreateLabel(win, "", { size = 12 }) + lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 24, -92 - (i - 1) * 24) + lbl:SetWidth(400) + lbl:SetWordWrap(false) + local rm = UI.CreateButton(win, "x", 24, 20, function() + table.remove(state.z, i) + win.Refresh() + end, "default") + rm:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -90 - (i - 1) * 24) + zoneRows[i] = { lbl = lbl, rm = rm } + end + local zoneNone = UI.CreateLabel(win, L.CODEX_PROPS_ZONES_NONE, { size = 11, color = "textDim" }) + zoneNone:SetPoint("TOPLEFT", win, "TOPLEFT", 24, -92) + + local zoneInput = MakeInput(win, 300) + zoneInput:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 8) + local sugBtns = {} + local function AddZone(id) + if id and #state.z < MAXZ then + for _, z in ipairs(state.z) do if z == id then return end end + state.z[#state.z + 1] = id + end + zoneInput.edit:SetText("") + win.Refresh() + end + local function Suggest() + local found = NameUI.FindMaps(zoneInput.edit:GetText(), #sugBtns) + for i, b in ipairs(sugBtns) do + local m = found[i] + if m then + b.fwId = m.id + b:SetLabel(string.format("%s (#%d)", m.name, m.id < 0 and -m.id or m.id)) + b:Show() + else + b.fwId = nil + b:Hide() + end + end + end + zoneInput.edit:SetScript("OnTextChanged", function(_, user) if user then Suggest() end end) + local here = UI.CreateButton(win, L.CODEX_PROPS_HERE, 170, 26, function() + local inst = Codex.CurrentInstance and Codex.CurrentInstance() + if inst then + AddZone(-inst) + return + end + local m = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player") + if m then AddZone(m) end + end, "default") + here:SetPoint("LEFT", zoneInput, "RIGHT", 8, 0) + for i = 1, 5 do + local b = UI.CreateButton(win, "", 488, 22, function(self) + if self.fwId then AddZone(self.fwId) end + end, "default") + b:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 40 - (i - 1) * 24) + b:Hide() + sugBtns[i] = b + end + local zoneHint = UI.CreateLabel(win, L.CODEX_PROPS_ZONE_HINT, { size = 11, color = "textDim", justifyV = "TOP" }) + zoneHint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -92 - MAXZ * 24 - 40 - 5 * 24 - 2) + zoneHint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -92 - MAXZ * 24 - 40 - 5 * 24 - 2) + zoneHint:SetHeight(30) + zoneHint:SetWordWrap(true) + + -- Dungeon / Klasse / Stufen + local baseY = 92 + MAXZ * 24 + 40 + 5 * 24 + 36 + local dgLabel = UI.CreateLabel(win, L.CODEX_PROPS_DUNGEON, { size = 12 }) + dgLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 4) + local dgBtn = UI.CreateButton(win, "", 200, 24, function() + if state.dg == nil then state.dg = true elseif state.dg == true then state.dg = false else state.dg = nil end + win.Refresh() + end, "default") + dgBtn:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY) + local clLabel = UI.CreateLabel(win, L.CODEX_PROPS_CLASS, { size = 12 }) + clLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 36) + local clBtn = UI.CreateButton(win, "", 200, 24, function() + local idx = 1 + for i, c in ipairs(CLASS_CYCLE) do if c == (state.cl or false) then idx = i end end + state.cl = CLASS_CYCLE[idx % #CLASS_CYCLE + 1] or nil + if state.cl == false then state.cl = nil end + win.Refresh() + end, "default") + clBtn:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 32) + local lvLabel = UI.CreateLabel(win, L.CODEX_PROPS_LEVELS, { size = 12 }) + lvLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 72) + local l1Box = MakeInput(win, 70) + l1Box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 68) + l1Box.edit:SetMaxLetters(3) + local lvDash = UI.CreateLabel(win, "-", { size = 12 }) + lvDash:SetPoint("LEFT", l1Box, "RIGHT", 6, 0) + local l2Box = MakeInput(win, 70) + l2Box:SetPoint("LEFT", l1Box, "RIGHT", 20, 0) + l2Box.edit:SetMaxLetters(3) + -- Quests: Vorquests (an der Stelle der Stufen) + local preLabel = UI.CreateLabel(win, L.CODEX_PROPS_PRE, { size = 12 }) + preLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 72) + local preBox = MakeInput(win, 300) + preBox:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - 68) + preBox.edit:SetMaxLetters(60) + + -- Quests: Folgequests, davor, danach, Stufe, Questtext (Dev) + local QROW = { nx = 36, bf = 108, af = 140, ql = 172, tx = 204, si = 236 } + local qLabels, qBoxes = {}, {} + local function QField(key, label, width, letters, y) + local lbl = UI.CreateLabel(win, label, { size = 12 }) + lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - y - 4) + local box = MakeInput(win, width) + box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - y) + box.edit:SetMaxLetters(letters) + qLabels[key], qBoxes[key] = lbl, box + end + QField("nx", L.CODEX_PROPS_NEXT, 300, 60, QROW.nx) + QField("bf", L.CODEX_PROPS_BEFORE, 300, 60, QROW.bf) + QField("af", L.CODEX_PROPS_AFTER, 300, 60, QROW.af) + QField("ql", L.CODEX_PROPS_QLEVEL, 70, 3, QROW.ql) + QField("tx", L.CODEX_PROPS_TEXT, 300, 600, QROW.tx) + QField("si", L.CODEX_PROPS_STARTITEM, 120, 9, QROW.si) + + -- Kreaturen und Objekte: Quest-Zuordnung von Hand (Ziel von Quests, laesst Quest-Items fallen) + local uLabels, uBoxes = {}, {} + local function UField(key, label, width, letters, y) + local lbl = UI.CreateLabel(win, label, { size = 12 }) + lbl:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - y - 4) + local box = MakeInput(win, width) + box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - y) + box.edit:SetMaxLetters(letters) + uLabels[key], uBoxes[key] = lbl, box + end + UField("qk", L.CODEX_PROPS_QK, 300, 80, 108) + UField("qd", L.CODEX_PROPS_QD, 300, 120, 140) + UField("qb", L.CODEX_PROPS_QB, 300, 80, 172) + QField("qr", L.CODEX_PROPS_QR, 300, 200, 268) + + local hint = UI.CreateLabel(win, L.CODEX_PROPS_HINT, { size = 11, color = "textDim", justifyV = "TOP" }) + hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - 104) + hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -baseY - 104) + hint:SetHeight(58) + hint:SetWordWrap(true) + + function win.Refresh() + for i = 1, MAXZ do + local row = zoneRows[i] + local id = state.z[i] + if id then + row.lbl:SetText(string.format("%s |cff9a978c#%d|r", Codex.MapName(id), id < 0 and -id or id)) + row.lbl:Show() + row.rm:Show() + else + row.lbl:Hide() + row.rm:Hide() + end + end + if #state.z == 0 then zoneNone:Show() else zoneNone:Hide() end + dgBtn:SetLabel(state.dg == nil and L.CODEX_PROPS_UNKNOWN or (state.dg and L.CODEX_PROPS_YES or L.CODEX_PROPS_NO)) + local isUnit = state.kind == "units" + local hasDungeon = state.kind ~= nil + if hasDungeon then dgLabel:Show(); dgBtn:Show() end + if isUnit then + clLabel:Show(); clBtn:Show(); lvLabel:Show(); l1Box:Show(); l2Box:Show(); lvDash:Show() + clBtn:SetLabel(ClassLabel(state.cl)) + else + clLabel:Hide(); clBtn:Hide(); lvLabel:Hide(); l1Box:Hide(); l2Box:Hide(); lvDash:Hide() + end + if state.kind == "quests" or state.kind == "items" then + preLabel:SetText(state.kind == "items" and L.CODEX_PROPS_KEY or L.CODEX_PROPS_PRE) + preLabel:Show(); preBox:Show() + else + preLabel:Hide(); preBox:Hide() + end + local isQuest = state.kind == "quests" + for key, lbl in pairs(qLabels) do + if isQuest then lbl:Show(); qBoxes[key]:Show() else lbl:Hide(); qBoxes[key]:Hide() end + end + local isEnt = state.kind == "units" or state.kind == "objects" + local isItem = state.kind == "items" + for key, lbl in pairs(uLabels) do + local show = isEnt or (isItem and key == "qb") + if show then lbl:Show(); uBoxes[key]:Show() else lbl:Hide(); uBoxes[key]:Hide() end + end + -- bei Items steht "Gehoert zu Quest" direkt unter dem Schluessel-Feld + local qbY = isItem and 108 or 172 + uLabels.qb:ClearAllPoints() + uLabels.qb:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - qbY - 4) + uBoxes.qb:ClearAllPoints() + uBoxes.qb:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -baseY - qbY) + -- fuer Quests (und Kreaturen/Objekte mit Quest-Zeilen) ist das Fenster hoeher, der Hinweis rutscht nach unten + win:SetHeight(isQuest and (baseY + 430) or (isEnt and (baseY + 340) or (isItem and 700 or 650))) + hint:ClearAllPoints() + local hy = isQuest and (268 + 40) or (isEnt and 212 or (isItem and 148 or 104)) + hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -baseY - hy) + hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -baseY - hy) + end + + local function Collect() + local ed = { z = {}, dg = state.dg, cl = state.cl } + for i, z in ipairs(state.z) do ed.z[i] = z end + if state.kind == "units" then + ed.l1, ed.l2 = tonumber(l1Box.edit:GetText()), tonumber(l2Box.edit:GetText()) + elseif state.kind == "quests" then + local pre = {} + for n in (preBox.edit:GetText() or ""):gmatch("%d+") do pre[#pre + 1] = tonumber(n) end + if #pre > 0 then ed.pre = pre end + for _, key in ipairs({ "nx", "bf", "af" }) do + local list = {} + for n in (qBoxes[key].edit:GetText() or ""):gmatch("%d+") do list[#list + 1] = tonumber(n) end + if #list > 0 then ed[key] = list end + end + ed.ql = tonumber(qBoxes.ql.edit:GetText()) + local tx = qBoxes.tx.edit:GetText() + if tx and tx:find("%S") then ed.tx = tx end + ed.si = tonumber((qBoxes.si.edit:GetText() or ""):match("%d+")) + elseif state.kind == "items" then + ed.key = tonumber((preBox.edit:GetText() or ""):match("%d+")) + end + if state.kind == "units" or state.kind == "objects" then + local qk, qd = {}, {} + for n in (uBoxes.qk.edit:GetText() or ""):gmatch("%d+") do qk[#qk + 1] = tonumber(n) end + for q, it in (uBoxes.qd.edit:GetText() or ""):gmatch("(%d+)%s*[:/]%s*(%d+)") do qd[#qd + 1] = q .. ":" .. it end + if #qk > 0 then ed.qk = qk end + if #qd > 0 then ed.qd = qd end + end + if state.kind == "units" or state.kind == "objects" or state.kind == "items" then + local qb = {} + for n in (uBoxes.qb.edit:GetText() or ""):gmatch("%d+") do qb[#qb + 1] = tonumber(n) end + if #qb > 0 then ed.qb = qb end + end + if state.kind == "quests" then + local qr = {} + for k, n in (qBoxes.qr.edit:GetText() or ""):gmatch("([iuoIUO])%s*[:#]?%s*(%d+)") do qr[#qr + 1] = k:lower() .. ":" .. n end + if #qr > 0 then ed.qr = qr end + end + return ed + end + local function Save() + if state.kind and state.id then + Codex:SetEd(state.kind, state.id, Collect()) + if onChanged then onChanged() end + end + win:Hide() + end + local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 140, 28, Save, "primary") + save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14) + local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 140, 28, function() + state.z, state.dg, state.cl = {}, nil, nil + l1Box.edit:SetText("") + l2Box.edit:SetText("") + preBox.edit:SetText("") + for _, b in pairs(qBoxes) do b.edit:SetText("") end + for _, b in pairs(uBoxes) do b.edit:SetText("") end + Save() + end, "default") + clear:SetPoint("LEFT", save, "RIGHT", 8, 0) + local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 140, 28, function() win:Hide() end, "default") + cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0) + + function win.Open(kind, id) + state.kind, state.id = kind, id + local ed = Codex:GetEd(kind, id) or {} + state.z = {} + for i, z in ipairs(ed.z or {}) do state.z[i] = z end + state.dg, state.cl = ed.dg, ed.cl + l1Box.edit:SetText(ed.l1 and tostring(ed.l1) or "") + l2Box.edit:SetText(ed.l2 and tostring(ed.l2) or "") + preBox.edit:SetText(kind == "items" and (ed.key and tostring(ed.key) or "") or (ed.pre and table.concat(ed.pre, ", ") or "")) + hint:SetText(kind == "quests" and (L.CODEX_PROPS_PRE_HINT .. "\n" .. L.CODEX_PROPS_HINT) + or (kind == "items" and (L.CODEX_PROPS_KEY_HINT .. "\n" .. L.CODEX_PROPS_HINT)) + or ((kind == "units" or kind == "objects") and (L.CODEX_PROPS_Q_HINT .. "\n" .. L.CODEX_PROPS_HINT)) or L.CODEX_PROPS_HINT) + if kind == "quests" then hint:SetText(L.CODEX_PROPS_QR_HINT .. "\n" .. L.CODEX_PROPS_PRE_HINT .. "\n" .. L.CODEX_PROPS_HINT) end + for _, key in ipairs({ "nx", "bf", "af" }) do + qBoxes[key].edit:SetText(ed[key] and table.concat(ed[key], ", ") or "") + end + qBoxes.ql.edit:SetText(ed.ql and tostring(ed.ql) or "") + qBoxes.tx.edit:SetText(ed.tx or "") + qBoxes.si.edit:SetText(ed.si and tostring(ed.si) or "") + uBoxes.qk.edit:SetText(ed.qk and table.concat(ed.qk, ", ") or "") + uBoxes.qd.edit:SetText(ed.qd and table.concat(ed.qd, ", ") or "") + uBoxes.qb.edit:SetText(ed.qb and table.concat(ed.qb, ", ") or "") + qBoxes.qr.edit:SetText(ed.qr and table.concat(ed.qr, ", ") or "") + zoneInput.edit:SetText("") + Suggest() + title:SetText(string.format("%s #%s", L[KIND_LABEL[kind]] or kind, tostring(id))) + local gn = GameName(kind, id) + game:SetText(gn and string.format(L.CODEX_NAME_GAME, gn) or "") + win.fwKey = kind .. ":" .. tostring(id) + win.Refresh() + win:Show() + win:Raise() + end + return win +end + +-- Stufenspanne eines Gebiets von Hand setzen (nur Berechtigte): geht wie alle Eigenschaften ueber die Vorschau im Dev-Reiter +local function BuildZoneProps(parent, onChanged) + local L = FW.L + local win = MakeWindow(parent, 420, 210) + local state = {} + local title = UI.CreateLabel(win, "", { font = "heading", size = 14, color = "goldLight" }) + title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14) + local lvLabel = UI.CreateLabel(win, L.CODEX_PROPS_LEVELS, { size = 12 }) + lvLabel:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -52) + local l1Box = MakeInput(win, 70) + l1Box:SetPoint("TOPLEFT", win, "TOPLEFT", 190, -48) + l1Box.edit:SetMaxLetters(3) + local dash = UI.CreateLabel(win, "-", { size = 12 }) + dash:SetPoint("LEFT", l1Box, "RIGHT", 6, 0) + local l2Box = MakeInput(win, 70) + l2Box:SetPoint("LEFT", l1Box, "RIGHT", 20, 0) + l2Box.edit:SetMaxLetters(3) + local hint = UI.CreateLabel(win, L.CODEX_ZONE_EDIT_HINT, { size = 11, color = "textDim", justifyV = "TOP" }) + hint:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -86) + hint:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -86) + hint:SetHeight(60) + hint:SetWordWrap(true) + local function Save(clear) + if state.key then + local ed + if not clear then ed = { l1 = tonumber(l1Box.edit:GetText()), l2 = tonumber(l2Box.edit:GetText()) } end + Codex:SetEd("zones", Codex.ZoneEdID(state.key), ed) + if onChanged then onChanged() end + end + win:Hide() + end + local save = UI.CreateButton(win, L.CODEX_NAME_SAVE, 120, 28, function() Save(false) end, "primary") + save:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14) + local clear = UI.CreateButton(win, L.CODEX_NAME_CLEAR, 120, 28, function() Save(true) end, "default") + clear:SetPoint("LEFT", save, "RIGHT", 8, 0) + local cancel = UI.CreateButton(win, L.CODEX_NAME_CANCEL, 120, 28, function() win:Hide() end, "default") + cancel:SetPoint("LEFT", clear, "RIGHT", 8, 0) + function win.Open(key) + state.key = key + local ed = Codex:GetEd("zones", Codex.ZoneEdID(key)) or {} + l1Box.edit:SetText(ed.l1 and tostring(ed.l1) or "") + l2Box.edit:SetText(ed.l2 and tostring(ed.l2) or "") + title:SetText(string.format("%s: %s", L.CODEX_KIND_ZONES, Codex.MapName(key))) + win.fwKey = "zones:" .. tostring(key) + win:Show() + win:Raise() + end + return win +end + +------------------------------------------------------------------------ +-- Fenster "Namen einfuegen" +------------------------------------------------------------------------ + +local function BuildImport(parent, onChanged) + local L = FW.L + local win = MakeWindow(parent, 560, 440) + local state = { lang = Codex.Lang() } + + local title = UI.CreateLabel(win, L.CODEX_NAME_IMPORT, { font = "heading", size = 14, color = "goldLight" }) + title:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -14) + local help = UI.CreateLabel(win, "", { size = 11, color = "textDim", justifyV = "TOP" }) + help:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -40) + help:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -40) + help:SetHeight(32) + help:SetWordWrap(true) + + local deButton, enButton + local function Sync() + deButton:SetStyle(state.lang == "de" and "primary" or "default") + enButton:SetStyle(state.lang == "en" and "primary" or "default") + end + deButton = UI.CreateButton(win, L.CODEX_NAME_DE, 140, 26, function() state.lang = "de"; Sync() end, "default") + deButton:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -78) + enButton = UI.CreateButton(win, L.CODEX_NAME_EN, 140, 26, function() state.lang = "en"; Sync() end, "default") + enButton:SetPoint("LEFT", deButton, "RIGHT", 8, 0) + + local area = UI.CreateTextArea(win, 528, 220) + area:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -112) + + local result = UI.CreateLabel(win, "", { size = 12, color = "goldLight" }) + result:SetPoint("TOPLEFT", win, "TOPLEFT", 16, -344) + result:SetPoint("TOPRIGHT", win, "TOPRIGHT", -16, -344) + + local go = UI.CreateButton(win, L.CODEX_NAME_IMPORT_GO, 140, 28, function() + local done, skipped = Codex:ImportNames(state.kind, state.lang, area:GetText()) + result:SetText(string.format(L.CODEX_NAME_IMPORT_RESULT, done, skipped)) + if onChanged then onChanged() end + end, "primary") + go:SetPoint("BOTTOMLEFT", win, "BOTTOMLEFT", 16, 14) + local close = UI.CreateButton(win, L.CODEX_NAME_CLOSE, 140, 28, function() win:Hide() end, "default") + close:SetPoint("LEFT", go, "RIGHT", 8, 0) + + function win.Open(kind) + state.kind = kind + state.lang = Codex.Lang() + Sync() + help:SetText(string.format(L.CODEX_NAME_IMPORT_HELP, L[KIND_LABEL[kind]] or kind)) + result:SetText("") + area:SetText("") + win:Show() + win:Raise() + end + return win +end + +------------------------------------------------------------------------ +-- Knoepfe im Tab +-- getSel() gibt art, auswahl zurueck; onChanged() aktualisiert Liste und Detail +------------------------------------------------------------------------ + +function NameUI.Attach(f, getSel, onChanged, origin) + local L = FW.L + local ctl = { height = 0 } + origin = origin or { x = 0, y = 0 } + local editWin, importWin, propsWin, zoneWin + function NameUI.OpenZone(key) + if not (Codex.Trust and Codex.Trust.CanEdit()) then return end + zoneWin = zoneWin or BuildZoneProps(f, onChanged) + zoneWin.Open(key) + end + + local editButton = UI.CreateButton(f, L.CODEX_NAME_EDIT, 150, 26, function() + local kind, id = getSel() + if EDITABLE[kind] and id then + editWin = editWin or BuildEdit(f, onChanged) + editWin.Open(kind, id) + end + end, "default") + editButton:Hide() + + local importButton = UI.CreateButton(f, L.CODEX_NAME_IMPORT, 150, 26, function() + local kind = getSel() + if EDITABLE[kind] then + importWin = importWin or BuildImport(f, onChanged) + importWin.Open(kind) + end + end, "default") + importButton:Hide() + + local propsButton = UI.CreateButton(f, L.CODEX_PROPS_BTN, 150, 26, function() + local kind, id = getSel() + if EDITABLE[kind] and id then + propsWin = propsWin or BuildProps(f, onChanged) + propsWin.Open(kind, id) + end + end, "default") + propsButton:Hide() + + -- "Fehlerhaft markieren" (nur Berechtigte): blendet den Datensatz bei allen Spielern aus + local function BadNow(kind, id) + local hook = Codex.DevHook + local staged = hook and hook.StagedBad and hook.StagedBad(kind, id) + if staged ~= nil then return staged end + return Codex:IsBad(kind, id) + end + local badButton = UI.CreateButton(f, L.CODEX_BAD_MARK, 170, 26, function() + local kind, id = getSel() + if kind and id and kind ~= "items" then + Codex:SetBad(kind, id, not BadNow(kind, id)) + if onChanged then onChanged() end + ctl.Update() + end + end, "default") + badButton:Hide() + + -- Die Knoepfe stehen in einer eigenen Reihe unter "Zurueck/Favorit", von links nach rechts; + -- ist der Platz zu knapp, laufen sie in die naechste Reihe. ctl.height ist die belegte Hoehe. + local strip = { propsButton, badButton, importButton, editButton } + function ctl.Layout() + local avail = (f:GetWidth() or 0) - origin.x + if avail < 240 then avail = 640 end + local x, y, any = 0, 0, false + for _, b in ipairs(strip) do + if b:IsShown() then + local w = b:GetWidth() or 150 + if x > 0 and x + w > avail then + x = 0 + y = y + 32 + end + b:ClearAllPoints() + b:SetPoint("TOPLEFT", f, "TOPLEFT", origin.x + x, -(origin.y + y)) + x = x + w + 8 + any = true + end + end + local h = any and (y + 32) or 0 + if h ~= ctl.height then + ctl.height = h + if origin.relayout then origin.relayout() end + end + end + ctl.strip = strip + + function ctl.Update() + local kind, id = getSel() + if EDITABLE[kind] and kind ~= "items" and id and Codex.Trust then + local bad = BadNow(kind, id) + if Codex.Trust.Can(bad and "unbad" or "bad") then + badButton:SetLabel(bad and L.CODEX_BAD_UNMARK or L.CODEX_BAD_MARK) + badButton:SetStyle(bad and "primary" or "default") + badButton:Show() + else + badButton:Hide() + end + else + badButton:Hide() + end + -- Namen bearbeiten: nur berechtigte Charaktere mit dem Modul Freewood-OctoWoW-Dev + local can = Codex.Trust ~= nil and Codex.Trust.CanEdit() + if can and EDITABLE[kind] and id then editButton:Show() else editButton:Hide() end + if can and EDITABLE[kind] then importButton:Show() else importButton:Hide() end + if can and EDITABLE[kind] and id then propsButton:Show() else propsButton:Hide() end + if propsWin and propsWin:IsShown() and propsWin.fwKey ~= (tostring(kind) .. ":" .. tostring(id)) then propsWin:Hide() end + if editWin and editWin:IsShown() and editWin.fwKey ~= (tostring(kind) .. ":" .. tostring(id)) then editWin:Hide() end + if importWin and importWin:IsShown() and not EDITABLE[kind] then importWin:Hide() end + ctl.Layout() + end + ctl.editButton, ctl.importButton, ctl.propsButton = editButton, importButton, propsButton + return ctl +end diff --git a/CodexNames.lua b/CodexNames.lua new file mode 100644 index 0000000..cd87589 --- /dev/null +++ b/CodexNames.lua @@ -0,0 +1,494 @@ +-- Freewood-OctoWoW-Codex: Namen von Kreaturen lernen +-- +-- Forever gibt Kreaturennamen im Kampf als "geheime Werte" zurueck. In Instanzen ist fast alles Kampf, +-- darum kannte der Codex dort nur die IDs. Hier werden Namen auf mehreren Wegen gesammelt: +-- target / mouse Ziel und Mouseover (auch Leichen und noch nicht bekannte Kreaturen) +-- scan ausserhalb des Kampfes alle paar Sekunden: Ziel, Fokus, Gruppenziele, Namensplaketten +-- plate Ereignis "Namensplakette erscheint" (falls das Spiel es erlaubt) +-- xp XP-Meldung "X stirbt ..." mit passendem Ziel +-- pend XP-Meldung ohne lesbares Ziel: Name wird dem naechsten frisch gelooteten Unbekannten zugeordnet, +-- aber nur, wenn eindeutig (genau ein Name in den letzten 90 Sekunden) +-- quest Kill-Ziele von Quests (eigene und fremde), die genau eine Kreatur-ID kennen +-- cache Name war schon gesehen, bevor die Kreatur einen Datensatz hatte + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex then + return +end +local Util = FW.Util + +local Names = { + stats = { target = 0, mouse = 0, scan = 0, plate = 0, xp = 0, pend = 0, quest = 0, cache = 0, tip = 0 }, + tipFail = { na = 0, err = 0, none = 0, secret = 0 }, + xpSeen = 0, + kills = {}, evCount = {}, evSecret = 0, samples = {}, killEvents = {}, + reads = 0, secret = 0, sweeps = 0, plateEvents = 0, + fail = { existSecret = 0, playerSecret = 0, guidSecret = 0, notNpc = 0, xpSecret = 0 }, + idName = {}, idCount = 0, pending = {}, running = false, plateOk = nil, +} +NS.Names = Names +Codex.Names = Names + +local MAX_CACHE = 800 +local PENDING_SECS = 90 +local SCAN_SECS = 2.5 +local QUEST_EVERY = 24 -- Durchlaeufe zwischen zwei Quest-Auswertungen (ca. eine Minute) +local TOKENS = { "target", "focus", "mouseover", "targettarget", "focustarget", "softenemy" } + +local function ParseGUID(guid) + local parse = Codex.ParseGUID + if parse then + return parse(guid) + end +end + +local function CleanName(v) + if Util.IsSecret(v) or 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, 60) +end + +-- Kreatur kennt der Codex schon (eigene oder fremde Daten)? +local function Known(id) + if Codex.db.units[id] then + return true + end + local v = Codex.V and Codex.V() + return v and v.units and v.units[id] ~= nil +end + +-- Name zu einer Kreatur-ID eintragen. Gibt true zurueck, wenn ein neuer Name gespeichert wurde. +function Names:Learn(id, name, src) + name = CleanName(name) + if name and Codex.StripKillVerb then name = Codex.StripKillVerb(name) end + if type(id) ~= "number" or not name then + return false + end + local u = Codex.db.units[id] + if u and u.n then + return false + end + if not u and self.idName[id] then + return false + end + if u or Known(id) then + u = u or Codex:Unit(id, true) + u.n = name + self.idName[id] = nil + if src and self.stats[src] then + self.stats[src] = self.stats[src] + 1 + end + FW:Fire("CODEX_CHANGED", "unit", id) + return true + end + -- noch kein Datensatz: Name merken, bis die Kreatur gelootet wird + if not self.idName[id] and self.idCount < MAX_CACHE then + self.idName[id] = name + self.idCount = self.idCount + 1 + end + return false +end + +-- Eine Einheit lesen: nur Kreaturen, nur wenn der Name lesbar ist. +-- fail[...] zaehlt, woran es scheitert (nur zur Diagnose): exist, player, guid, npc, name +function Names:Read(unit, src) + local f = self.fail + local existsRaw = UnitExists and UnitExists(unit) + if Util.IsSecret(existsRaw) then + f.existSecret = f.existSecret + 1 + return + end + if not existsRaw then + return + end + local playerRaw = UnitIsPlayer and UnitIsPlayer(unit) + if Util.IsSecret(playerRaw) then + f.playerSecret = f.playerSecret + 1 + return + end + if playerRaw then + return + end + local guidRaw = UnitGUID and UnitGUID(unit) + if Util.IsSecret(guidRaw) then + f.guidSecret = f.guidSecret + 1 + return + end + local kind, id = ParseGUID(guidRaw) + if kind ~= "npc" then + f.notNpc = f.notNpc + 1 + return + end + self.reads = self.reads + 1 + local raw = UnitName and UnitName(unit) + if Util.IsSecret(raw) then + self.secret = self.secret + 1 + return + end + self:Learn(id, raw, src) +end + +local function GroupTokens() + local list = {} + local n = GetNumGroupMembers and GetNumGroupMembers() or 0 + if n > 1 then + local raid = IsInRaid and IsInRaid() + local prefix, count = raid and "raid" or "party", raid and math.min(n, 40) or math.min(n - 1, 4) + for i = 1, count do + list[#list + 1] = prefix .. i .. "target" + end + end + return list +end + +-- Alle bekannten Quellen einmal lesen. Im Kampf sind die Namen geheim, dann lohnt es nicht. +function Names:Scan(src) + src = src or "scan" + if UnitAffectingCombat and Util.Clean(UnitAffectingCombat("player")) then + return + end + self.sweeps = self.sweeps + 1 + for _, unit in ipairs(TOKENS) do + self:Read(unit, src) + end + for _, unit in ipairs(GroupTokens()) do + self:Read(unit, src) + end + for i = 1, 40 do + local unit = "nameplate" .. i + if UnitExists and Util.Clean(UnitExists(unit)) then + self:Read(unit, "plate") + end + end +end + +-- Kill-Ziele von Quests mit genau einer bekannten Kreatur-ID +function Names:FromQuests() + local v = Codex.V and Codex.V() + if not v or not v.quests then + return 0 + end + local learned = 0 + for _, q in pairs(v.quests) do + for _, ob in ipairs(q.obj or {}) do + if ob.ty == "monster" and ob.name and type(ob.ids) == "table" then + local only, count + count = 0 + for id in pairs(ob.ids) do + only = id + count = count + 1 + if count > 1 then + break + end + end + if count == 1 and self:Learn(only, ob.name, "quest") then + learned = learned + 1 + end + end + end + end + return learned +end + +-- XP-Meldung: Name einer getoeteten Kreatur ohne lesbares Ziel merken +function Names:NoteKill(name) + name = CleanName(name) + if not name then + return + end + local list = self.pending + list[#list + 1] = { name = name, t = time() } + while #list > 20 do + table.remove(list, 1) + end +end + +-- Versuch: Name ueber den Tooltip-Link der Beute-GUID ("unit:"). Die GUID aus dem Beutefenster ist lesbar. +function Names:FromTooltip(guid) + local api = _G.C_TooltipInfo and _G.C_TooltipInfo.GetHyperlink + if type(api) ~= "function" or type(guid) ~= "string" then + self.tipFail.na = self.tipFail.na + 1 + return nil + end + local ok, data = pcall(api, "unit:" .. guid) + if not ok then + self.tipFail.err = self.tipFail.err + 1 + return nil + end + if Util.IsSecret(data) then + self.tipFail.secret = self.tipFail.secret + 1 + return nil + end + local line = type(data) == "table" and type(data.lines) == "table" and data.lines[1] + local text = type(line) == "table" and line.leftText + if Util.IsSecret(text) then + self.tipFail.secret = self.tipFail.secret + 1 + return nil + end + local name = CleanName(text) + if not name then + self.tipFail.none = self.tipFail.none + 1 + end + return name +end + +-- Eine gelootete Kreatur mit bekanntem Namen hat ihre XP-Meldung "verbraucht" +function Names:Consume(name) + local list = self.pending + for i = #list, 1, -1 do + if list[i].name == name then + table.remove(list, i) + return + end + end +end + +-- Frisch gelootete Kreatur ohne Namen: aus Merkzettel oder XP-Meldungen auffuellen +function Names:Resolve(id, ent, guid) + if type(id) ~= "number" or type(ent) ~= "table" or ent.n then + return + end + local cached = self.idName[id] + if cached then + ent.n = cached + self.idName[id] = nil + self.stats.cache = self.stats.cache + 1 + return + end + local tip = guid and self:FromTooltip(guid) + if tip then + ent.n = tip + self.stats.tip = self.stats.tip + 1 + return + end + if (ent.k or 0) > 1 then + return + end + local now, list = time(), self.pending + for i = #list, 1, -1 do + if now - list[i].t > PENDING_SECS then + table.remove(list, i) + end + end + -- nur zuordnen, wenn in den letzten 90 Sekunden genau ein Name gemeldet wurde + local only + for _, p in ipairs(list) do + if only and only ~= p.name then + return + end + only = p.name + end + if not only then + return + end + for i = #list, 1, -1 do + if list[i].name == only then + table.remove(list, i) + break + end + end + ent.n = only + self.stats.pend = self.stats.pend + 1 +end + +-- Kampflog-Zeilen "Euer Charakter besiegt X." / "X stirbt." als Vorschlaege fuer Namen (nie automatisch zugeordnet) +local KILL_EVENTS = { "CHAT_MSG_COMBAT_HOSTILE_DEATH", "CHAT_MSG_COMBAT_SELF_HITS", "CHAT_MSG_COMBAT_PARTY_HITS", + "CHAT_MSG_COMBAT_MISC_INFO", "CHAT_MSG_COMBAT_PET_HITS" } +local KILL_PATTERNS = { + "^Euer Charakter besiegt (.+)%.$", "besiegt (.+)%.$", "^Ihr habt (.+) get\195\182tet%.$", "^(.+) stirbt%.$", + "^You have slain (.+)!$", "^You defeat (.+)%.$", "defeats (.+)%.$", "^(.+) dies%.$", +} + +-- Zeile pruefen. Rueckgabe: Name oder nil +function Names.ParseKill(text) + if type(text) ~= "string" then + return nil + end + text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|H.-|h(.-)|h", "%1") + for _, pattern in ipairs(KILL_PATTERNS) do + local name = text:match(pattern) + if name then + name = name:gsub("^%s+", ""):gsub("%s+$", "") + if name ~= "" and #name <= 80 then + return name + end + end + end + return nil +end + +function Names:OnKillLine(event, text) + self.evCount[event] = (self.evCount[event] or 0) + 1 + if Util.IsSecret(text) then + self.evSecret = self.evSecret + 1 + return + end + if type(text) ~= "string" then + return + end + local sample = self.samples[event] + if not sample then + sample = {} + self.samples[event] = sample + end + if #sample < 2 then + sample[#sample + 1] = text:sub(1, 90) + end + local name = Names.ParseKill(text) + if not name then + return + end + local list, now = self.kills, time() + local last = list[#list] + if last and last.name == name and now - last.t < 2 then + return + end + list[#list + 1] = { name = name, t = now } + while #list > 40 do + table.remove(list, 1) + end +end + +-- Zuletzt besiegte Gegner (neueste zuerst, ohne Doppelte). Pro Eintrag: name, alt (ohne letztes grossgeschriebenes Wort) +function Names:RecentKills(maxAge, limit) + maxAge, limit = maxAge or 900, limit or 8 + local now, seen, out = time(), {}, {} + for i = #self.kills, 1, -1 do + local k = self.kills[i] + if now - k.t <= maxAge and not seen[k.name] then + seen[k.name] = true + local alt + local head, tail = k.name:match("^(.+)%s+(%S+)$") + if head and tail:match("^%u") and #head >= 3 then + alt = head + end + out[#out + 1] = { name = k.name, alt = alt, t = k.t } + if #out >= limit then + break + end + end + end + return out +end + +local function Tick() + if not Names.running then + return + end + pcall(Names.Scan, Names, "scan") + Names.tickCount = (Names.tickCount or 0) + 1 + if Names.tickCount % QUEST_EVERY == 0 then + pcall(Names.FromQuests, Names) + end + C_Timer.After(SCAN_SECS, Tick) +end + +function Names:OnRegen() + C_Timer.After(0.5, function() pcall(Names.Scan, Names, "scan") end) + C_Timer.After(2.5, function() pcall(Names.Scan, Names, "scan") end) +end + +function Names:OnPlate(unit) + self.plateEvents = self.plateEvents + 1 + if type(unit) == "string" then + self:Read(unit, "plate") + end +end + +-- Alte Eintraege mit anhaengendem Kill-Verb ("... beseitigt", "getötet") bereinigen (einmal beim Start) +function Names.Cleanup() + local strip = Codex.StripKillVerb + if not strip or not Codex.db then return end + local n = 0 + for _, u in pairs(Codex.db.units or {}) do + if type(u) == "table" and type(u.n) == "string" then + local c = strip(u.n) + if c ~= u.n then u.n = c n = n + 1 end + end + end + for _, q in pairs(Codex.db.quests or {}) do + for _, ob in ipairs(type(q) == "table" and q.obj or {}) do + if type(ob.name) == "string" then + local c = strip(ob.name) + if c ~= ob.name then ob.name = c n = n + 1 end + end + end + end + Names.cleaned = n +end + +function Names:Start() + if self.running then + return + end + pcall(Names.Cleanup) + self.running = true + FW:RegisterEvent("PLAYER_REGEN_ENABLED", self.OnRegen, self) + -- das Ereignis kann in gesperrten Situationen verboten sein: Fehler abfangen + self.plateOk = pcall(FW.RegisterEvent, FW, "NAME_PLATE_UNIT_ADDED", self.OnPlate, self) + for _, loopEvent in ipairs(KILL_EVENTS) do + local event = loopEvent -- eigene Variable je Durchlauf: Lua 5.0 teilt die Schleifenvariable zwischen Closures + self.killEvents[event] = pcall(FW.RegisterEvent, FW, event, function(owner, text) + Names:OnKillLine(event, text) + end, Names) + end + C_Timer.After(12, function() + pcall(Names.FromQuests, Names) + end) + C_Timer.After(SCAN_SECS, Tick) +end + +function Names:Stop() + self.running = false + FW:UnregisterEvent("PLAYER_REGEN_ENABLED", self) + if self.plateOk then + FW:UnregisterEvent("NAME_PLATE_UNIT_ADDED", self) + end + for event, ok in pairs(self.killEvents) do + if ok then + FW:UnregisterEvent(event, self) + end + end +end + +function Names:Debug() + local total, named = 0, 0 + local v = Codex.V and Codex.V() or Codex.db + for _, u in pairs(v.units or {}) do + total = total + 1 + if u.n then + named = named + 1 + end + end + local st = self.stats + FW:Print(string.format("Codex-Namen: %d Kreaturen, %d mit Namen, %d ohne", total, named, total - named)) + FW:Print(string.format("Neu gelernt ueber: Ziel %d, Mouseover %d, Suche %d, Namensplakette %d, XP-Meldung %d, XP-Zuordnung %d, Quest %d, Merkzettel %d, Tooltip %d", + st.target, st.mouse, st.scan, st.plate, st.xp, st.pend, st.quest, st.cache, st.tip)) + local tf = self.tipFail + FW:Print(string.format("XP-Meldungen gesehen %d (davon geheim %d); Tooltip-Weg: nicht vorhanden %d, Fehler %d, geheim %d, ohne Namen %d", + self.xpSeen, self.fail.xpSecret, tf.na, tf.err, tf.secret, tf.none)) + local evs = {} + for event, n in pairs(self.evCount) do + evs[#evs + 1] = event:gsub("^CHAT_MSG_COMBAT_", "") .. "=" .. n + end + table.sort(evs) + FW:Print(string.format("Kampflog-Zeilen: %s (geheim %d); gemerkte Kills %d", #evs > 0 and table.concat(evs, " ") or "keine", self.evSecret, #self.kills)) + for event, list in pairs(self.samples) do + for _, text in ipairs(list) do + FW:Print(" Beispiel " .. event:gsub("^CHAT_MSG_COMBAT_", "") .. ": " .. text:gsub("|", "||")) + end + end + local f = self.fail + FW:Print(string.format("Abbrueche vor dem Namen: Einheit geheim %d, Spieler-Angabe geheim %d, GUID geheim %d, keine Kreatur %d; Instanz: %s", + f.existSecret, f.playerSecret, f.guidSecret, f.notNpc, tostring(IsInInstance and select(1, IsInInstance())))) + FW:Print(string.format("Gelesen %d, davon im Kampf geheim %d; Suchdurchlaeufe %d; Namensplaketten-Ereignis %s (%d); Merkzettel %d, offene XP-Namen %d", + self.reads, self.secret, self.sweeps, self.plateOk and "ok" or "nicht verfuegbar", self.plateEvents, self.idCount, #self.pending)) +end