Dateien nach "/" hochladen

This commit is contained in:
2026-10-27 14:08:44 +00:00
parent 5457dd095c
commit 05ba4e6191
4 changed files with 1860 additions and 0 deletions
+257
View File
@@ -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
+304
View File
@@ -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
+805
View File
@@ -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
+494
View File
@@ -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:<GUID>"). 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