Dateien nach "/" hochladen
This commit is contained in:
+257
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user