From 1e5ba3048aa7826e104d688464eff9116428423d Mon Sep 17 00:00:00 2001 From: Freewood <948+freewood@noreply.octowow.st> Date: Tue, 27 Oct 2026 14:08:54 +0000 Subject: [PATCH] Dateien nach "/" hochladen --- CodexOwners.lua | 32 ++ CodexPath.lua | 1173 +++++++++++++++++++++++++++++++++++++++++++ CodexPathMap.lua | 604 ++++++++++++++++++++++ CodexQuestProbe.lua | 215 ++++++++ 4 files changed, 2024 insertions(+) create mode 100644 CodexOwners.lua create mode 100644 CodexPath.lua create mode 100644 CodexPathMap.lua create mode 100644 CodexQuestProbe.lua diff --git a/CodexOwners.lua b/CodexOwners.lua new file mode 100644 index 0000000..962e948 --- /dev/null +++ b/CodexOwners.lua @@ -0,0 +1,32 @@ +-- Freewood-OctoWoW-Codex: Eigentuemer der Datenbank +-- +-- Hier stehen die Charaktere (Name-Realm), die DEV-Rechte vergeben duerfen. Nur Listen, die von einem dieser +-- Charaktere kommen, werden von anderen Spielern uebernommen. Der Absender einer Addon-Nachricht wird vom +-- Spiel gesetzt und kann nicht gefaelscht werden. +-- +-- Eintrag zum Beispiel: "Lacustat-NZoth", +-- (Name-Realm, Realm ohne Apostroph und Leerzeichen: NZoth, CThun, YShaarj. Ohne Realm gilt der eigene Realm. +-- Auf OctoWoW haben Charaktere nur einen Namen.) +-- +-- Ist die Liste leer, gibt es niemanden mit DEV-Rechten: Namen koennen dann nicht bearbeitet werden. + +local ADDON, NS = ... +if not NS then + return +end + +NS.Owners = { + "Fwaroki-NZoth", -- PvE + "Freewood-CThun", -- Hardcore + "Fwfreewood-YShaarj", -- PvP +} + +-- Fest eingetragene Moderatoren (duerfen Namen aendern, Datensaetze markieren, Verbindungen und Ortspunkte weitergeben). +-- Gilt zusaetzlich zur Liste, die ein Eigentuemer ueber das Modul Rechte veroeffentlicht. Eine Sperre (blocked) dort geht vor. +NS.Moderators = { + "Pinkytwinky", + "Lacustat", + "Dipps", + "Arokisteel", + "Windsteel", +} diff --git a/CodexPath.lua b/CodexPath.lua new file mode 100644 index 0000000..5829b2e --- /dev/null +++ b/CodexPath.lua @@ -0,0 +1,1173 @@ +-- Freewood-OctoWoW-Codex: Wegenetz. Zeichnet auf, wo Spieler zu Fuss gegangen sind, als Raster aus Zellen mit Richtungen. +-- +-- Eine Zelle (CELL Yards Kantenlaenge) ist begehbar, sobald jemand zu Fuss darauf stand. Die Kanten (Schritte zu den +-- 8 Nachbarzellen) sagen, wie man von dort weitergekommen ist. Faelle werden als Einbahn-Kanten gespeichert. +-- Daraus kann der Plan spaeter Wege suchen (Hindernisse sind automatisch bekannt, weil niemand durch sie geht). +-- +-- Gespeichert wird nur bei Bewegung zu Fuss. Ausgeschlossen: Instanzen, Reittier, Flug, Taxi, freier Fall und +-- Auren, die Boden oder Wasser vortaeuschen (Levitieren, Leichter Fall, Wasserwandeln, Gleiter ...), ausserdem +-- unplausible Spruenge (Teleport, Ladebildschirm) und zu hohes Lauftempo. +-- +-- Datenform: db.path[welt][zellKey] = Maske +-- Bit 0-7 Richtungen, in die man aus der Zelle gegangen ist (O, NO, N, NW, W, SW, S, SO) +-- Bit 8-15 davon Faelle (nur in diese Richtung) +-- Bit 16 Zelle besucht +-- Zellkey = (cx + OFFSET) * 65536 + (cy + OFFSET). welt = Instanz-/Kontinent-ID aus UnitPosition. +-- db.pathz[welt][zellKey] = Hoehe in 4-Yard-Schritten, nur wenn das Spiel eine Hoehe liefert (sonst 0). +-- Gebietsuebergaenge: db.zlink[von][nach][art] = { n = Anzahl, p = { { fw, fk, tw, tk }, ... } } (bis 4 Punkte) +-- art "w" zu Fuss/Reittier ueber die Grenze (Nachbargebiete), "t" Sprung (Zeppelin, Schiff, Portal), "f" Flugpunkt, +-- "i" Instanz (nach = negative Instanz-ID; Eingang mit Startpunkt, Ausgang mit Zielpunkt). +-- von/nach sind Karten-IDs wie in den Gebieten des Codex. fw/fk = Welt und Zellkey des Startpunkts, tw/tk des Ziels. +-- Nur lokal (wird noch nicht geteilt). + +local ADDON, NS = ... + +local FW = _G.Freewood +if not FW or not FW:RequireAPI(1, ADDON) then + return +end + +local Codex = NS and NS.Codex +if not Codex then + return +end + +local Util = FW.Util +local Path = {} +Codex.Path = Path + +local CELL = 10 -- Yards je Zelle +local OFFSET = 30000 +local TICK = 1 -- Sekunden zwischen zwei Messungen +local MAX_STEP_CELLS = 3 -- weiter auseinander liegende Messungen gelten als Sprung (Teleport) +local MAX_SPEED = 18 -- Yards pro Sekunde (Reittier mit +150 % = 17,5); schneller = Effekt oder Teleport, nicht aufzeichnen +local MIN_FALL = 1.2 -- Sekunden Fall, ab denen es ein Fall ist (kein Sprung) +local MAX_CELLS = 300000 + +-- Richtungen als (dx, dy) in der Reihenfolge der Bits +local DIRS = { { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { 1, -1 } } +local DIR_OF = {} +for i, d in ipairs(DIRS) do DIR_OF[d[1] .. ":" .. d[2]] = i - 1 end +Path.DIRS = DIRS +Path.CELL = CELL + +-- Auren, bei denen keine Daten aufgezeichnet werden (Zauber-IDs, Klassik) ... +Path.SKIP_AURAS = { + [130] = true, -- Leichter Fall + [1706] = true, -- Levitieren + [546] = true, -- Wasserwandeln + [3714] = true, -- Pfad des Frosts + [13141] = true, -- Gnomen-Raketenstiefel + [11359] = true, -- Fallschirm +} +-- ... und nach Namen (fuer Aurenen, deren ID wir nicht kennen, zum Beispiel das Gleiten einer neuen Rasse) +local SKIP_WORDS = { "levit", "leichter fall", "slow fall", "wasserwand", "water walk", "gleit", "glide", "gliding", "fallschirm", "parachute", "schweb", "hover" } + +local LINK_POINTS = 4 +local WALK_CELLS = 12 -- Wechsel mit hoechstens so vielen Zellen Abstand (120 Yards) gelten als Grenzuebergang + +local state = { acc = 0, last = nil, fallT = 0, fallFrom = nil, reasons = {}, added = 0, samples = 0 } +Path.state = state + +local function DB() + local db = Codex.db + if not db then return nil end + db.path = db.path or {} + return db +end + +local function Reject(reason) + state.reasons[reason] = (state.reasons[reason] or 0) + 1 +end + +function Path.Enabled() + local db = Codex.db + return db and db.settings and db.settings.path ~= false +end + +function Path.SetEnabled(v) + local db = Codex.db + if db and db.settings then db.settings.path = v and true or false end +end + +local function CellOf(x, y) + return math.floor(x / CELL), math.floor(y / CELL) +end + +local function Key(cx, cy) + return (cx + OFFSET) * 65536 + (cy + OFFSET) +end + +function Path.KeyToCell(key) + return math.floor(key / 65536) - OFFSET, key % 65536 - OFFSET +end + +------------------------------------------------------------------------ +-- Teilen: Wegenetz in Bloecken von 16 x 16 Zellen (160 Yards), Gebietsuebergaenge als ein Datensatz +------------------------------------------------------------------------ + +local CHUNK = 16 +local MAX_REMOTE_CELLS = 400000 +local MAX_LINKS_SHARED = 600 +local mergedCells, linkCache = {}, nil + +local function ChunkId(world, cx, cy) + if type(world) ~= "number" or world < 0 or world >= 1000000 then return nil end + local gx, gy = math.floor(cx / CHUNK) + 128, math.floor(cy / CHUNK) + 128 + if gx < 0 or gx > 255 or gy < 0 or gy > 255 then return nil end + return world * 65536 + gx * 256 + gy +end + +local function ChunkParts(id) + local world = math.floor(id / 65536) + local r = id % 65536 + return world, math.floor(r / 256), r % 256 +end + +-- Bitweises Oder fuer 17-Bit-Masken (ohne bit-Bibliothek) +local function Or17(a, b) + if a == b or b == 0 then return a end + if a == 0 then return b end + local r, p = 0, 1 + for _ = 0, 16 do + local x, y = a % 2, b % 2 + if x == 1 or y == 1 then r = r + p end + a, b, p = (a - x) / 2, (b - y) / 2, p * 2 + end + return r +end + +local function Dirty(db, world, cx, cy) + local id = ChunkId(world, cx, cy) + if not id then return end + db.pathsync = db.pathsync or {} + local now = _G.time and _G.time() or 0 + db.pathsync[id] = math.max(now, (db.pathsync[id] or 0) + 1) + mergedCells[world] = nil + Path.rev = (Path.rev or 0) + 1 +end + +local function Count(db) + if db.pathCount then return db.pathCount end + local n = 0 + for _, w in pairs(db.path) do + for _ in pairs(w) do n = n + 1 end + end + db.pathCount = n + return n +end + +-- Zelle markieren; dirIndex = Richtung, in die es weiterging (optional), drop = Fall +local function Mark(world, cx, cy, dirIndex, drop, z) + local db = DB() + if not db then return end + local w = db.path[world] + if not w then w = {}; db.path[world] = w end + local k = Key(cx, cy) + local m = w[k] + if not m then + if Count(db) >= MAX_CELLS then Reject("voll") return end + m = 0 + db.pathCount = Count(db) + 1 + state.added = state.added + 1 + end + local newm = m + if newm < 65536 then newm = newm + 65536 end -- besucht + if dirIndex then + local bit = 2 ^ dirIndex + if math.floor(newm / bit) % 2 == 0 then newm = newm + bit end + if drop then + local dbit = 2 ^ (dirIndex + 8) + if math.floor(newm / dbit) % 2 == 0 then newm = newm + dbit end + end + end + w[k] = newm + if newm ~= m then Dirty(db, world, cx, cy) end + if z and z ~= 0 then + db.pathz = db.pathz or {} + db.pathz[world] = db.pathz[world] or {} + db.pathz[world][k] = math.floor(z / 4 + 0.5) + end +end + +-- Zellen entlang der Strecke von (ax,ay) nach (bx,by) (einschliesslich beider), je Schritt hoechstens ein Nachbar +local function Line(ax, ay, bx, by) + local cells = { { ax, ay } } + local x, y = ax, ay + local n = 0 + while (x ~= bx or y ~= by) and n < 10 do + x = x + (bx > x and 1 or (bx < x and -1 or 0)) + y = y + (by > y and 1 or (by < y and -1 or 0)) + cells[#cells + 1] = { x, y } + n = n + 1 + end + return cells +end + +-- Strecke zwischen zwei Messungen eintragen +local function Connect(world, a, b, drop, z) + local cells = Line(a.cx, a.cy, b.cx, b.cy) + for i = 1, #cells do + local c = cells[i] + local nxt = cells[i + 1] + local di + local isDrop = false + if nxt then + di = DIR_OF[(nxt[1] - c[1]) .. ":" .. (nxt[2] - c[2])] + isDrop = drop and i == 1 + end + Mark(world, c[1], c[2], di, isDrop, (i == #cells) and z or nil) + end +end + +-- Hat der Spieler eine Aura, die Boden oder Wasser vortaeuscht? +local function SkipAura() + local CU = _G.C_UnitAuras + local function Check(name, id) + if id and Path.SKIP_AURAS[id] then return true end + if type(name) == "string" and not (Util and Util.IsSecret and Util.IsSecret(name)) then + local low = name:lower() + for _, w in ipairs(SKIP_WORDS) do + if low:find(w, 1, true) then return true end + end + end + return false + end + if CU and CU.GetAuraDataByIndex then + for i = 1, 40 do + local ok, a = pcall(CU.GetAuraDataByIndex, "player", i, "HELPFUL") + if not ok or type(a) ~= "table" then break end + if Check(a.name, a.spellId) then return true end + end + return false + end + if _G.UnitBuff then + for i = 1, 40 do + local ok, name, _, _, _, _, _, _, _, _, id = pcall(_G.UnitBuff, "player", i) + if not ok or not name then break end + if Check(name, id) then return true end + end + end + return false +end + +------------------------------------------------------------------------ +-- Gebietsuebergaenge +------------------------------------------------------------------------ + +local function Flag(fn, ...) + if type(fn) ~= "function" then return false end + local ok, v = pcall(fn, ...) + return ok and v and true or false +end + + +local function Here(pos) + if not pos then return nil end + local cx, cy = CellOf(pos.x, pos.y) + return { world = pos.world, key = Key(cx, cy) } +end + +local function CurrentMap() + local CM = _G.C_Map + if not (CM and CM.GetBestMapForUnit) then return nil end + local ok, m = pcall(CM.GetBestMapForUnit, "player") + if not ok or type(m) ~= "number" or (Util and Util.IsSecret and Util.IsSecret(m)) then return nil end + return m +end + +local function Link(a, b, kind, from, to) + local db = DB() + if not db or a == b then return end + db.zlink = db.zlink or {} + local la = db.zlink[a] + if not la then la = {}; db.zlink[a] = la end + local lb = la[b] + if not lb then lb = {}; la[b] = lb end + local e = lb[kind] + if not e then e = { n = 0, p = {} }; lb[kind] = e end + linkCache = nil + e.n = e.n + 1 + local p = { fw = from and from.world, fk = from and from.key, tw = to and to.world, tk = to and to.key } + if #e.p < LINK_POINTS then + for _, q in ipairs(e.p) do + if q.fk == p.fk and q.tk == p.tk then return end + end + e.p[#e.p + 1] = p + end +end + +-- Benachbarte Gebiete: Liste { map, n, kinds = { w, t, f, i } } (beide Richtungen zusammen) +function Path.Neighbors(map) + local db = Codex.db + local out, idx = {}, {} + local function Add(other, kinds) + local e = idx[other] + if not e then e = { map = other, n = 0, kinds = {} }; idx[other] = e; out[#out + 1] = e end + for kind, v in pairs(kinds) do + e.kinds[kind] = (e.kinds[kind] or 0) + v.n + e.n = e.n + v.n + end + end + for a, row in pairs(Path.Links()) do + for b, kinds in pairs(row) do + if a == map then Add(b, kinds) elseif b == map then Add(a, kinds) end + end + end + table.sort(out, function(x, y) return x.n > y.n end) + return out +end + +-- Uebergangspunkte von a nach b: Liste { fw, fk, tw, tk } je Art, oder nil +function Path.Crossings(a, b, kind) + local db = Codex.db + local L = Path.Links() + local e = L[a] and L[a][b] and L[a][b][kind or "w"] + return e and e.p or nil +end + +function Path.ClearRemote() + local db = Codex.db + if not db then return end + db.pathr, db.pathrCount, db.zlinkr = nil, nil, nil + mergedCells, linkCache = {}, nil + Path.rev = (Path.rev or 0) + 1 +end + +-- Eigene und empfangene Zellen einer Welt (Maske = Oder beider). Ohne Fremddaten ist das die eigene Tabelle. +function Path.Cells(world) + local db = Codex.db + if not db then return nil end + local own = db.path and db.path[world] + local rem = db.pathr and db.pathr[world] + if not rem then return own end + if not own then return rem end + local m = mergedCells[world] + if m then return m end + m = {} + for k, v in pairs(rem) do m[k] = v end + for k, v in pairs(own) do m[k] = m[k] and Or17(m[k], v) or v end + mergedCells[world] = m + return m +end + +local function MergeLinkInto(out, a, b, kind, n, pts, cap) + out[a] = out[a] or {} + out[a][b] = out[a][b] or {} + local t = out[a][b][kind] + if not t then + t = { n = n, p = {} } + out[a][b][kind] = t + else + t.n = math.max(t.n, n) + end + for _, p in ipairs(pts or {}) do + local dup = false + for _, q in ipairs(t.p) do + if q.fk == p.fk and q.tk == p.tk then dup = true break end + end + if not dup and #t.p < cap then t.p[#t.p + 1] = p end + end +end + +-- Eigene und empfangene Gebietsuebergaenge zusammen +function Path.Links() + local db = Codex.db + if not db then return {} end + if not (db.zlinkr and next(db.zlinkr)) then return db.zlink or {} end + if linkCache then return linkCache end + local out = {} + for _, src in ipairs({ db.zlink or {}, db.zlinkr }) do + for a, row in pairs(src) do + for b, kinds in pairs(row) do + for kind, e in pairs(kinds) do MergeLinkInto(out, a, b, kind, e.n, e.p, 8) end + end + end + end + linkCache = out + return out +end + +------------------------------------------------------------------------ +-- Teilen: eigene Bloecke, Pruefen und Einmischen +------------------------------------------------------------------------ + +-- Staende der eigenen Bloecke einmalig anlegen (Daten, die vor dem Teilen aufgezeichnet wurden) +function Path.EnsureStamps() + local db = Codex.db + if not db or db.pathStamped then return end + db.pathsync = db.pathsync or {} + local now = _G.time and _G.time() or 0 + for world, w in pairs(db.path or {}) do + for key in pairs(w) do + local cx, cy = Path.KeyToCell(key) + local id = ChunkId(world, cx, cy) + if id and not db.pathsync[id] then db.pathsync[id] = now end + end + end + db.pathStamped = true +end + +local function OwnLinkSig() + local db = Codex.db + local s, any = 0, false + for a, row in pairs(db and db.zlink or {}) do + for b, kinds in pairs(row) do + for _, e in pairs(kinds) do + s = s + e.n * 7 + #(e.p or {}) + 1 + any = true + end + end + end + return any and s % 2147483647 or nil +end + +-- Verzeichnis der eigenen Bloecke: Liste { key, upd } +function Path.SyncList() + local db = Codex.db + local out = {} + if not db or not Path.Enabled() and not db.pathsync then return out end + Path.EnsureStamps() + for id, upd in pairs(db.pathsync or {}) do out[#out + 1] = { key = "p" .. id, upd = upd } end + local sig = OwnLinkSig() + if sig then + local zs = db.zsync + if not zs then zs = {}; db.zsync = zs end + if zs.sig ~= sig then + zs.sig = sig + zs.upd = math.max(_G.time and _G.time() or 0, (zs.upd or 0) + 1) + end + out[#out + 1] = { key = "z0", upd = zs.upd } + end + return out +end + +-- Eigener Block als Text "Zelle.Maske,..." (Zelle = 0..255 im Block, beides hexadezimal) +function Path.ChunkRecord(id) + local db = Codex.db + if not db or type(id) ~= "number" then return nil end + local world, gx, gy = ChunkParts(id) + local w = db.path and db.path[world] + if not w then return nil end + local bx, by = (gx - 128) * CHUNK, (gy - 128) * CHUNK + local parts = {} + for dx = 0, CHUNK - 1 do + for dy = 0, CHUNK - 1 do + local m = w[Key(bx + dx, by + dy)] + if m then parts[#parts + 1] = string.format("%x.%x", dx * CHUNK + dy, m) end + end + end + if #parts == 0 then return nil end + return { c = table.concat(parts, ",") } +end + +-- Text pruefen. Rueckgabe: Tabelle { [zelle] = maske } oder nil +function Path.ParseChunk(str) + if type(str) ~= "string" or #str > 6000 then return nil end + local out, n = {}, 0 + for idx, mask in str:gmatch("(%x+)%.(%x+)") do + local i, m = tonumber(idx, 16), tonumber(mask, 16) + if not i or not m or i > 255 or m > 131071 or m < 0 then return nil end + if not out[i] then n = n + 1 end + out[i] = m + if n > 256 then return nil end + end + if n == 0 then return nil end + return out +end + +function Path.ChunkText(cells) + local keys = {} + for i in pairs(cells) do keys[#keys + 1] = i end + table.sort(keys) + local parts = {} + for _, i in ipairs(keys) do parts[#parts + 1] = string.format("%x.%x", i, cells[i]) end + return table.concat(parts, ",") +end + +-- Empfangenen Block einmischen (Oder der Masken; mehrfach einspielen aendert nichts) +function Path.MergeChunk(id, cells) + local db = Codex.db + if not db or type(id) ~= "number" or type(cells) ~= "table" then return false end + local world, gx, gy = ChunkParts(id) + db.pathr = db.pathr or {} + local w = db.pathr[world] + if not w then w = {}; db.pathr[world] = w end + db.pathrCount = db.pathrCount or 0 + local bx, by = (gx - 128) * CHUNK, (gy - 128) * CHUNK + local changed = false + for idx, mask in pairs(cells) do + local k = Key(bx + math.floor(idx / CHUNK), by + idx % CHUNK) + local old = w[k] + if not old then + if db.pathrCount >= MAX_REMOTE_CELLS then return changed end + db.pathrCount = db.pathrCount + 1 + w[k] = mask + changed = true + else + local nm = Or17(old, mask) + if nm ~= old then w[k] = nm; changed = true end + end + end + if changed then mergedCells[world] = nil; Path.rev = (Path.rev or 0) + 1 end + return changed +end + +local KINDS = { w = true, t = true, f = true, i = true } + +local function OptInt(v, hi) + if v == nil then return nil, true end + if type(v) ~= "number" or v ~= v or v < 0 or v > hi then return nil, false end + return math.floor(v), true +end + +-- Eigene Gebietsuebergaenge als Liste { a, b, k, n, p = { { fw, fk, tw, tk } } } +function Path.LinkRecord() + local db = Codex.db + local list = {} + local keys = {} + for a, row in pairs(db and db.zlink or {}) do + for b, kinds in pairs(row) do + for kind, e in pairs(kinds) do keys[#keys + 1] = { a = a, b = b, k = kind, e = e } end + end + end + table.sort(keys, function(x, y) + if x.a ~= y.a then return x.a < y.a end + if x.b ~= y.b then return x.b < y.b end + return x.k < y.k + end) + for _, r in ipairs(keys) do + if #list >= MAX_LINKS_SHARED then break end + local pts = {} + for _, p in ipairs(r.e.p or {}) do + pts[#pts + 1] = { fw = p.fw, fk = p.fk, tw = p.tw, tk = p.tk } + end + list[#list + 1] = { a = r.a, b = r.b, k = r.k, n = r.e.n, p = pts } + end + if #list == 0 then return nil end + return { l = list } +end + +-- Liste pruefen. Rueckgabe: bereinigte Liste oder nil +function Path.ParseLinks(list) + if type(list) ~= "table" or #list == 0 or #list > MAX_LINKS_SHARED then return nil end + local out = {} + for _, r in ipairs(list) do + if type(r) ~= "table" then return nil end + local a, b, n = r.a, r.b, r.n + if type(a) ~= "number" or type(b) ~= "number" or a ~= a or b ~= b or math.abs(a) > 1000000 or math.abs(b) > 1000000 or a == b then return nil end + if type(r.k) ~= "string" or not KINDS[r.k] then return nil end + if type(n) ~= "number" or n ~= n or n < 1 or n > 1000000 then return nil end + local pts = {} + for _, p in ipairs(type(r.p) == "table" and r.p or {}) do + if #pts >= LINK_POINTS then break end + if type(p) ~= "table" then return nil end + local fw, ok1 = OptInt(p.fw, 1000000) + local tw, ok2 = OptInt(p.tw, 1000000) + local fk, ok3 = OptInt(p.fk, 2 ^ 40) + local tk, ok4 = OptInt(p.tk, 2 ^ 40) + if not (ok1 and ok2 and ok3 and ok4) then return nil end + pts[#pts + 1] = { fw = fw, fk = fk, tw = tw, tk = tk } + end + out[#out + 1] = { a = math.floor(a), b = math.floor(b), k = r.k, n = math.floor(n), p = pts } + end + return out +end + +function Path.MergeLinks(list) + local db = Codex.db + if not db or type(list) ~= "table" then return false end + db.zlinkr = db.zlinkr or {} + for _, r in ipairs(list) do MergeLinkInto(db.zlinkr, r.a, r.b, r.k, r.n, r.p, LINK_POINTS) end + linkCache = nil + return true +end + +------------------------------------------------------------------------ +-- Wegberechnung ueber das Wegenetz (fuer den Levelplan) +------------------------------------------------------------------------ +-- Das Netz wird zu Bloecken von 4 x 4 Zellen (40 Yards) vergroebert; Kanten gibt es dort, wo Zellen in +-- verschiedene Bloecke fuehren (Richtungen) oder aneinander grenzen. Faelle gelten nur in Fallrichtung. +-- Distance liefert die Strecke in Yards laengs des Netzes (inklusive der Strecke von und zu den naechsten +-- Netzpunkten), oder nil, wenn dort kein zusammenhaengender Weg bekannt ist (dann rechnet der Plan in Luftlinie). + +local BLOCK = 4 +local SNAP_BLOCKS = 3 -- so weit (in Bloecken) darf ein Ort vom Netz entfernt sein +local MAX_EXPAND = 20000 +local GRAPH_STALE = 120 -- Sekunden, die ein vorhandener Graph trotz neuer Daten weiterverwendet wird +local graphs = {} + +local function BKey(bx, by) + return (bx + 8000) * 65536 + (by + 8000) +end + +local function Now() + return _G.time and _G.time() or 0 +end + +-- Fuer jede Richtungsmaske (8 Bit) die Liste der gesetzten Richtungen (Index 1..8) +local DIRLIST = {} +for m = 0, 255 do + local l = {} + for i = 0, 7 do + if math.floor(m / 2 ^ i) % 2 == 1 then l[#l + 1] = i + 1 end + end + DIRLIST[m] = l +end + +local jobs = {} -- [welt] = laufender Aufbau +local GRAPH_PER_FRAME = 500 -- Zellen je Bild beim Aufbau im Hintergrund + +local function StartJob(world) + local cells = Path.Cells(world) + if not cells or not next(cells) then graphs[world] = false return nil end + local keys = {} + for k in pairs(cells) do keys[#keys + 1] = k end + local job = { world = world, cells = cells, keys = keys, i = 1, nodes = {}, built = Now(), rev = Path.rev or 0 } + jobs[world] = job + return job +end + +-- bis zu n Zellen verarbeiten; true, wenn alle durch sind +local function Step(job, n) + local cells, nodes, keys = job.cells, job.nodes, job.keys + local floor = math.floor + local function Node(bx, by) + local k = (bx + 8000) * 65536 + (by + 8000) + local nd = nodes[k] + if not nd then nd = { bx = bx, by = by, adj = {} }; nodes[k] = nd end + return nd, k + end + local function Edge(a, bk, cost) + local old = a.adj[bk] + if not old or cost < old then a.adj[bk] = cost end + end + local STRAIGHT, DIAG = BLOCK * CELL, BLOCK * CELL * 1.414 + local last = math.min(#keys, job.i + n - 1) + for idx = job.i, last do + local key = keys[idx] + local mask = cells[key] + if mask then + local cx, cy = floor(key / 65536) - OFFSET, key % 65536 - OFFSET + local bx, by = floor(cx / BLOCK), floor(cy / BLOCK) + local a, ak + local dirs = mask % 256 + local drops = floor(mask / 256) % 256 + if dirs > 0 then + for _, di in ipairs(DIRLIST[dirs]) do + local d = DIRS[di] + local nbx, nby = floor((cx + d[1]) / BLOCK), floor((cy + d[2]) / BLOCK) + if nbx ~= bx or nby ~= by then + if not a then a, ak = Node(bx, by) end + local b, bk = Node(nbx, nby) + local cost = (nbx ~= bx and nby ~= by) and DIAG or STRAIGHT + Edge(a, bk, cost) + if floor(drops / 2 ^ (di - 1)) % 2 == 0 then Edge(b, ak, cost) end + end + end + end + if drops == 0 then + -- Nachbarzelle aus einem anderen Weg: verbunden + for i = 1, 4 do + local d = DIRS[i] + local nm = cells[key + d[1] * 65536 + d[2]] + if nm and floor(nm / 256) % 256 == 0 then + local nbx, nby = floor((cx + d[1]) / BLOCK), floor((cy + d[2]) / BLOCK) + if nbx ~= bx or nby ~= by then + if not a then a, ak = Node(bx, by) end + local b, bk = Node(nbx, nby) + local cost = (nbx ~= bx and nby ~= by) and DIAG or STRAIGHT + Edge(a, bk, cost) + Edge(b, ak, cost) + end + end + end + end + end + end + job.i = last + 1 + return job.i > #keys +end + +-- Zusammenhangskomponenten (Kanten als ungerichtet), damit unerreichbare Ziele sofort auffallen +local function Finish(job) + local nodes = job.nodes + local parent = {} + local function Find(k) + local r = k + while parent[r] ~= r do r = parent[r] end + while parent[k] ~= r do local nx = parent[k]; parent[k] = r; k = nx end + return r + end + for k in pairs(nodes) do parent[k] = k end + for k, nd in pairs(nodes) do + for nk in pairs(nd.adj) do + local a, b = Find(k), Find(nk) + if a ~= b then parent[a] = b end + end + end + local comp = {} + for k in pairs(nodes) do comp[k] = Find(k) end + return { nodes = nodes, comp = comp, built = job.built, rev = job.rev, cache = {}, snaps = {} } +end + +local function BuildGraph(world) + local job = StartJob(world) + if not job then return nil end + Step(job, #job.keys + 1) + jobs[world] = nil + return Finish(job) +end + +local function Graph(world) + local g = graphs[world] + local rev = Path.rev or 0 + if g and (g.rev == rev or Now() - g.built < GRAPH_STALE) then return g end + if Path.syncGraph then + g = BuildGraph(world) + graphs[world] = g or false + return g + end + if not jobs[world] then StartJob(world) end + return g or nil +end + +-- Aufbau im Hintergrund voranbringen (je Bild); true, wenn ein Graph fertig wurde +function Path.StepJobs(n) + local world, job = next(jobs) + if not job then return false end + if Step(job, n or GRAPH_PER_FRAME) then + jobs[world] = nil + graphs[world] = Finish(job) + FW:Fire("CODEX_PATH_GRAPH", world) + return true + end + return false +end + +local graphFrame = _G.CreateFrame and _G.CreateFrame("Frame") +if graphFrame then + graphFrame:SetScript("OnUpdate", function() Path.StepJobs() end) +end + +-- Naechster Netzpunkt zu (x, y) in Yards. Rueckgabe: Knotenschluessel, Entfernung in Yards +local function Snap(g, x, y) + local sk = math.floor(x / 10) * 100000 + math.floor(y / 10) + local c = g.snaps[sk] + if c ~= nil then + if c == false then return nil end + return c[1], c[2] + end + local bx, by = math.floor(x / (BLOCK * CELL)), math.floor(y / (BLOCK * CELL)) + local best, bestD + for dx = -SNAP_BLOCKS, SNAP_BLOCKS do + for dy = -SNAP_BLOCKS, SNAP_BLOCKS do + local k = BKey(bx + dx, by + dy) + if g.nodes[k] then + local cxm, cym = (bx + dx + 0.5) * BLOCK * CELL, (by + dy + 0.5) * BLOCK * CELL + local d = math.sqrt((cxm - x) ^ 2 + (cym - y) ^ 2) + if not bestD or d < bestD then best, bestD = k, d end + end + end + end + if bestD and bestD > SNAP_BLOCKS * BLOCK * CELL then best = nil end + g.snaps[sk] = best and { best, bestD } or false + return best, bestD +end + +-- A* ueber die Bloecke. Rueckgabe: Strecke in Yards oder nil +local function Search(g, from, to) + local nodes = g.nodes + local goal = nodes[to] + if not goal then return nil end + local gx, gy = (goal.bx + 0.5) * BLOCK * CELL, (goal.by + 0.5) * BLOCK * CELL + local function H(n) + return 1.2 * math.sqrt(((n.bx + 0.5) * BLOCK * CELL - gx) ^ 2 + ((n.by + 0.5) * BLOCK * CELL - gy) ^ 2) + end + local dist, heap, size, done = { [from] = 0 }, {}, 0, {} + local function Push(k, pr) + size = size + 1 + local i = size + heap[i] = { k, pr } + while i > 1 do + local p = math.floor(i / 2) + if heap[p][2] <= heap[i][2] then break end + heap[p], heap[i] = heap[i], heap[p] + i = p + end + end + local function Pop() + local top = heap[1] + heap[1] = heap[size] + heap[size] = nil + size = size - 1 + local i = 1 + while true do + local l, r, m = i * 2, i * 2 + 1, i + if l <= size and heap[l][2] < heap[m][2] then m = l end + if r <= size and heap[r][2] < heap[m][2] then m = r end + if m == i then break end + heap[m], heap[i] = heap[i], heap[m] + i = m + end + return top[1] + end + Push(from, H(nodes[from])) + local expanded = 0 + while size > 0 do + local k = Pop() + if k == to then return dist[k] end + if not done[k] then + done[k] = true + expanded = expanded + 1 + if expanded > MAX_EXPAND then return nil end + for nk, cost in pairs(nodes[k].adj) do + local nd = dist[k] + cost + if not dist[nk] or nd < dist[nk] then + dist[nk] = nd + Push(nk, nd + H(nodes[nk])) + end + end + end + end + return nil +end + +-- Reihenfolge der Weltkoordinaten der Kartenschnittstelle: false = (erster, zweiter) wie UnitPosition, true = (zweiter, erster). +-- Gespeichert in settings.pathSwap (dieselbe Erkennung wie in der Kartenanzeige). +function Path.Swap() + local db = Codex.db + if not db or not db.settings then return nil end + if type(db.settings.pathSwap) == "boolean" then return db.settings.pathSwap end + local CM = _G.C_Map + if not (CM and CM.GetBestMapForUnit and CM.GetPlayerMapPosition and CM.GetWorldPosFromMapPos and _G.CreateVector2D) then return nil end + local pos = Path.Read() + if not pos then return nil end + local ok, mapID = pcall(CM.GetBestMapForUnit, "player") + if not ok or type(mapID) ~= "number" then return nil end + local ok2, mp = pcall(CM.GetPlayerMapPosition, mapID, "player") + if not ok2 or not mp or not mp.GetXY then return nil end + local px, py = mp:GetXY() + if type(px) ~= "number" or type(py) ~= "number" then return nil end + local ok3, _, wp = pcall(CM.GetWorldPosFromMapPos, mapID, _G.CreateVector2D(px, py)) + if not ok3 or not wp or not wp.GetXY then return nil end + local vx, vy = wp:GetXY() + if type(vx) ~= "number" or type(vy) ~= "number" then return nil end + -- pos.y = erster Wert von UnitPosition, pos.x = zweiter + local errNormal = math.abs(vx - pos.y) + math.abs(vy - pos.x) + local errSwap = math.abs(vx - pos.x) + math.abs(vy - pos.y) + if math.abs(errNormal - errSwap) < 50 then return nil end -- zu nah beieinander, nicht eindeutig + db.settings.pathSwap = errSwap < errNormal + return db.settings.pathSwap +end + +-- Weltkoordinaten der Kartenschnittstelle (Vektor x, y in Yards) -> Zellraum (x, y in Yards) +function Path.FromWorldVec(vx, vy) + local swap = Path.Swap() + if swap == nil then return nil end + if swap then return vx, vy end + return vy, vx +end + +-- Strecke in Yards laengs des Wegenetzes zwischen zwei Orten (Zellraum, Yards); nil = unbekannt, Luftlinie benutzen +function Path.Distance(world, ax, ay, bx, by) + if type(world) ~= "number" then return nil end + local straight = math.sqrt((ax - bx) ^ 2 + (ay - by) ^ 2) + if straight < 2 * BLOCK * CELL then return nil end + local g = Graph(world) + if not g then return nil end + local sa, da = Snap(g, ax, ay) + local sb, db_ = Snap(g, bx, by) + if not sa or not sb or sa == sb then return nil end + if g.comp[sa] ~= g.comp[sb] then return nil end + local ck = sa .. ":" .. sb + local cost = g.cache[ck] + if cost == nil then + cost = Search(g, sa, sb) or false + g.cache[ck] = cost + end + if not cost then return nil end + return da + cost + db_ +end + +function Path.ResetGraphs() + graphs = {} +end + +-- Wird bei jeder Messung aufgerufen, auch beim Reiten und Fliegen (Grenzen gelten fuer alle) +function Path.TrackZone(pos) + local z = state.zone + if Flag(_G.IsInInstance) then + if not state.inInst then + local id = Codex.CurrentInstance and Codex.CurrentInstance() + state.inInst = id and -id or true + if z and z.map and id then Link(z.map, -id, "i", z.here, nil) end + state.leftFrom = id and -id or nil + end + state.pend = nil + return + end + if state.inInst then + state.inInst = nil + state.zone, z = nil, nil + end + if Flag(_G.UnitOnTaxi, "player") then + if not state.taxi and z then state.taxi = { map = z.map, here = z.here } end + return + end + local m = CurrentMap() + if not m then return end + local here = Here(pos) + if state.leftFrom then + Link(state.leftFrom, m, "i", nil, here) + state.leftFrom = nil + end + if state.taxi then + local t = state.taxi + state.taxi = nil + if t.map and t.map ~= m then Link(t.map, m, "f", t.here, here) end + state.zone, state.pend = { map = m, here = here }, nil + return + end + if not z then + state.zone = { map = m, here = here } + return + end + if m == z.map then + state.pend = nil + z.here = here or z.here + return + end + -- anderes Gebiet: erst nach zwei Messungen in Folge werten (an der Grenze hin und her tritt oft doppelt) + if not state.pend or state.pend.map ~= m then + state.pend = { map = m, n = 1 } + return + end + state.pend.n = state.pend.n + 1 + if state.pend.n < 2 then return end + local kind = "t" + if z.here and here and z.here.world == here.world then + local fx, fy = Path.KeyToCell(z.here.key) + local tx, ty = Path.KeyToCell(here.key) + if math.max(math.abs(fx - tx), math.abs(fy - ty)) <= WALK_CELLS then kind = "w" end + end + Link(z.map, m, kind, z.here, here) + state.zone, state.pend = { map = m, here = here }, nil +end + +-- Aktuelle Messung: { x, y, z, world } oder nil, wenn nicht lesbar +function Path.Read() + if not _G.UnitPosition then return nil end + local ok, a, b, c, d = pcall(_G.UnitPosition, "player") + if not ok then return nil end + for _, v in ipairs({ a or 0, b or 0, c or 0, d or 0 }) do + if Util and Util.IsSecret and Util.IsSecret(v) then return nil end + end + if type(a) ~= "number" or type(b) ~= "number" then return nil end + -- UnitPosition liefert (Y, X, Z, Welt) + return { x = b, y = a, z = type(c) == "number" and c or 0, world = d } +end + +-- Eine Messung verarbeiten. now = Zeit in Sekunden. Rueckgabe: true, wenn aufgezeichnet. +function Path.Tick(now) + state.samples = state.samples + 1 + if not Path.Enabled() or not DB() then return false end + local reset = function(reason) + if reason then Reject(reason) end + state.last = nil + state.fallT, state.fallFrom = 0, nil + return false + end + local pos0 = Path.Read() + pcall(Path.TrackZone, pos0) + if Flag(_G.IsInInstance) then return reset("instanz") end + local pos = pos0 + if not pos or not pos.world then return reset("keine Position") end + local dead = _G.UnitIsDeadOrGhost and Flag(_G.UnitIsDeadOrGhost, "player") + if dead then return reset("tot") end + -- Reiten zaehlt mit (Bodenreittiere folgen dem Boden); Flug, Taxi und Fahrzeuge nicht + if Flag(_G.IsFlying) or Flag(_G.UnitOnTaxi, "player") or (_G.UnitInVehicle and Flag(_G.UnitInVehicle, "player")) then + return reset("Flug/Fahrzeug") + end + -- Fahrt wird als Verbindung erfasst (Start hier ... Ziel hier): Deck und Fahrweg gehoeren nicht ins Wegenetz + local T = Codex.Transport + if T and T.Recording and T.Recording() then return reset("Verbindung") end + local speed = _G.GetUnitSpeed and select(1, _G.GetUnitSpeed("player")) or 0 + if type(speed) == "number" and speed > MAX_SPEED then return reset("zu schnell") end + if SkipAura() then return reset("Aura") end + local cx, cy = CellOf(pos.x, pos.y) + local cur = { cx = cx, cy = cy, world = pos.world } + if Flag(_G.IsFalling) then + -- Fall: nichts eintragen, aber den Startpunkt merken + if not state.fallFrom and state.last and state.last.world == pos.world then state.fallFrom = state.last end + state.fallT = state.fallT + TICK + return false + end + if state.fallFrom then + -- gelandet: war es ein richtiger Fall (kein Sprung), Einbahn-Kante vom Start zur Landung + local from, long = state.fallFrom, state.fallT >= MIN_FALL + state.fallFrom, state.fallT = nil, 0 + if long and from.world == pos.world and math.max(math.abs(cx - from.cx), math.abs(cy - from.cy)) <= 8 then + local cells = Line(from.cx, from.cy, cx, cy) + local nxt = cells[2] + if nxt then + local di = DIR_OF[(nxt[1] - from.cx) .. ":" .. (nxt[2] - from.cy)] + Mark(pos.world, from.cx, from.cy, di, true) + end + Mark(pos.world, cx, cy, nil, false, pos.z) + state.last = cur + return true + end + end + local last = state.last + if last and last.world == pos.world then + local d = math.max(math.abs(cx - last.cx), math.abs(cy - last.cy)) + if d == 0 then return false end -- steht still: keine neuen Daten + if d > MAX_STEP_CELLS then + state.last = cur -- Teleport oder Ladebildschirm: neu ansetzen, nichts verbinden + Reject("Sprung") + Mark(pos.world, cx, cy, nil, false, pos.z) + return false + end + Connect(pos.world, last, cur, false, pos.z) + else + Mark(pos.world, cx, cy, nil, false, pos.z) + end + state.last = cur + return true +end + +-- Ist an dieser Stelle (Weltkoordinaten in Yards) schon etwas bekannt? Rueckgabe: Maske oder nil +function Path.At(world, x, y) + local db = Codex.db + if not db then return nil end + local w = Path.Cells(world) + if not w then return nil end + local cx, cy = CellOf(x, y) + return w[Key(cx, cy)] +end + +-- Kennzahlen fuer die Diagnose +function Path.Stats() + local db = Codex.db + local cells, drops, edges, worlds, links, crossings = 0, 0, 0, 0, 0, 0 + for _, w in pairs(db and db.path or {}) do + worlds = worlds + 1 + for _, m in pairs(w) do + cells = cells + 1 + for i = 0, 7 do + if math.floor(m / 2 ^ i) % 2 == 1 then edges = edges + 1 end + if math.floor(m / 2 ^ (i + 8)) % 2 == 1 then drops = drops + 1 end + end + end + end + for _, row in pairs(Path.Links()) do + for _, kinds in pairs(row) do + links = links + 1 + for _, e in pairs(kinds) do crossings = crossings + e.n end + end + end + local shared = 0 + for _, w in pairs(db and db.pathr or {}) do + for _ in pairs(w) do shared = shared + 1 end + end + return { cells = cells, edges = edges, drops = drops, worlds = worlds, links = links, crossings = crossings, shared = shared } +end + +------------------------------------------------------------------------ +-- Messen: alle TICK Sekunden +------------------------------------------------------------------------ + +local frame = _G.CreateFrame and _G.CreateFrame("Frame") +if frame then + frame:SetScript("OnUpdate", function(_, elapsed) + state.acc = state.acc + (elapsed or 0) + if state.acc < TICK then return end + state.acc = 0 + local ok = pcall(Path.Tick, _G.GetTime and _G.GetTime() or 0) + if not ok then Reject("Fehler") end + end) +end + +------------------------------------------------------------------------ +-- /fwpos: Diagnose (Position, Hoehe, Zustand, Auren) und Ein/Aus +------------------------------------------------------------------------ + +function Path.Diag() + local pos = Path.Read() + if not pos then + FW:Print("Pos: keine Position lesbar (UnitPosition fehlt, Instanz oder gesperrt).") + else + FW:Print(string.format("Pos: x=%.1f y=%.1f z=%s Welt=%s -> Zelle %d/%d%s", pos.x, pos.y, tostring(pos.z), tostring(pos.world), + (CellOf(pos.x, pos.y)), select(2, CellOf(pos.x, pos.y)), pos.z == 0 and " (Hoehe liefert das Spiel nicht: z ist 0)" or " (Hoehe wird gespeichert)")) + end + local speed = _G.GetUnitSpeed and select(1, _G.GetUnitSpeed("player")) or "?" + FW:Print(string.format("Pos: Tempo=%s | Reittier=%s Flug=%s Fall=%s Schwimmen=%s Instanz=%s", tostring(speed), tostring(Flag(_G.IsMounted)), + tostring(Flag(_G.IsFlying)), tostring(Flag(_G.IsFalling)), tostring(Flag(_G.IsSwimming)), tostring(Flag(_G.IsInInstance)))) + local names = {} + local CU = _G.C_UnitAuras + if CU and CU.GetAuraDataByIndex then + for i = 1, 40 do + local ok, a = pcall(CU.GetAuraDataByIndex, "player", i, "HELPFUL") + if not ok or type(a) ~= "table" then break end + names[#names + 1] = tostring(a.name) .. "(" .. tostring(a.spellId) .. ")" + end + elseif _G.UnitBuff then + for i = 1, 40 do + local ok, name, _, _, _, _, _, _, _, _, id = pcall(_G.UnitBuff, "player", i) + if not ok or not name then break end + names[#names + 1] = tostring(name) .. "(" .. tostring(id) .. ")" + end + end + FW:Print("Pos: Auren: " .. (#names > 0 and table.concat(names, ", ") or "keine") .. " | wird uebersprungen: " .. tostring(SkipAura())) + local st = Path.Stats() + FW:Print(string.format("Pos: Aufzeichnung %s | %d Zellen, %d Kanten, %d Faelle, %d Welten | neu in dieser Sitzung: %d", Path.Enabled() and "an" or "aus", + st.cells, st.edges, st.drops, st.worlds, state.added)) + FW:Print(string.format("Pos: %d Gebietsverbindungen (%d Uebergaenge)", st.links, st.crossings)) + local r = {} + for reason, n in pairs(state.reasons) do r[#r + 1] = reason .. " x" .. n end + table.sort(r) + if #r > 0 then FW:Print("Pos: verworfen: " .. table.concat(r, ", ")) end +end + +-- Einstellungen: Ein/Aus (standardmaessig an) und Kennzahlen +local UI = FW.UI +if UI and UI.RegisterSettingsSection then + UI:RegisterSettingsSection("codex-path", { + order = 107, + height = 200, + build = function(parent) + local L = FW.L + local f, stack = UI.CreateSection(parent, L.CODEX_PATH_HEADER) + local note = UI.CreateLabel(f, L.CODEX_PATH_NOTE, { size = 11, color = "textDim", justifyV = "TOP" }) + stack:Add(note, { fit = true, gap = 8 }) + local box = UI.CreateCheckbox(f, L.CODEX_PATH_SWITCH, function() return Path.Enabled() end, + function(on) Path.SetEnabled(on) end) + stack:Add(box, { fit = true, gap = 10 }) + local stats = UI.CreateLabel(f, "", { size = 11, color = "textDim", justifyV = "TOP" }) + stack:Add(stats, { fit = true, gap = 6, minHeight = 14 }) + f.box, f.stats = box, stats + return f + end, + refresh = function(frame) + frame.box:Refresh() + local st = Path.Stats() + frame.stats:SetText(string.format(FW.L.CODEX_PATH_STATS, st.cells, st.edges, st.drops, st.links)) + frame.stack:Layout() + end, + }) +end + +SLASH_FREEWOODPOS1 = "/fwpos" +SlashCmdList["FREEWOODPOS"] = function(msg) + msg = tostring(msg or ""):lower() + if msg == "aus" or msg == "off" then + Path.SetEnabled(false) + FW:Print("Pos: Aufzeichnung aus.") + elseif msg == "an" or msg == "on" then + Path.SetEnabled(true) + FW:Print("Pos: Aufzeichnung an.") + else + Path.Diag() + end +end diff --git a/CodexPathMap.lua b/CodexPathMap.lua new file mode 100644 index 0000000..e35e18f --- /dev/null +++ b/CodexPathMap.lua @@ -0,0 +1,604 @@ +-- Freewood-OctoWoW-Codex: Wegenetz auf der Weltkarte anzeigen (Dev-Reiter, /fwwege). +-- Ebenen: Wege (gruen), Faelle (rot), Gebietsuebergaenge (je Art eine Farbe), Luecken (gelb: bekannte Quest-Geber/-Abgaben und +-- Dienst-NPCs, bei denen noch kein Weg aufgezeichnet ist). Die Welt-Koordinaten des Wegenetzes werden mit drei Messpunkten +-- aus der Kartenschnittstelle in Kartenanteile umgerechnet (die Abbildung ist je Karte linear). + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex or not Codex.Path then + return +end + +local Path = Codex.Path +local PM = { on = false } +Codex.PathMap = PM + +local GRID = Codex.GRID or 200 +local CELL = Path.CELL or 10 +local BIN = 1 / 300 -- Karten-Anteil je Punkt (Raster der Anzeige) +local MAX_DOTS = 5000 +local GAP_RADIUS = 0.012 -- so nah muss ein Weg an einem bekannten Ort liegen, damit er nicht als Luecke zaehlt +local MAX_GAPS = 250 +local LAYER_LEVEL = 1400 +local PROBE = 100 -- Yards Abstand der Messpunkte fuer die Umrechnung + +local COLORS = { + w = { 0.30, 0.90, 0.40 }, -- Weg + f = { 0.75, 0.45, 1.00 }, -- Flugroute + r = { 0.10, 0.35, 1.00 }, -- fremder Weg (empfangen oder aus Startdaten): Blau, auf dem Pergament gut sichtbar + d = { 1.00, 0.25, 0.20 }, -- Fall + g = { 1.00, 0.85, 0.10 }, -- Luecke + lw = { 0.20, 0.80, 1.00 }, -- Grenzuebergang + lt = { 0.90, 0.30, 0.90 }, -- Sprung (Zeppelin, Portal) + lf = { 0.60, 0.60, 1.00 }, -- Flugpunkt + li = { 1.00, 0.60, 0.10 }, -- Instanz + s = { 1.00, 1.00, 1.00 }, -- Ortspunkt (nur fuer Berechtigte) +} + +local function View() + local db = Codex.db + db.settings.pathView = db.settings.pathView or {} + return db.settings.pathView +end + +function PM.LayerOn(k) + local v = View()[k] + return v ~= false +end + +function PM.SetLayer(k, on) + View()[k] = on and true or false + PM.Refresh() +end + +------------------------------------------------------------------------ +-- Umrechnung Welt -> Karte +------------------------------------------------------------------------ + +local function Vec(a, b) + return _G.CreateVector2D and _G.CreateVector2D(a, b) or { x = a, y = b, GetXY = function(self) return self.x, self.y end } +end + +-- Kartenanteil zu einer Weltposition; first/second wie UnitPosition (erster, zweiter Rueckgabewert) +local function MapPosOf(world, first, second, mapID, swap) + local CM = _G.C_Map + if not (CM and CM.GetMapPosFromWorldPos) then return nil end + local vec = swap and Vec(second, first) or Vec(first, second) + local ok, id, pos = pcall(CM.GetMapPosFromWorldPos, world, vec, mapID) + if not ok or not pos or not pos.GetXY then return nil end + local ok2, x, y = pcall(pos.GetXY, pos) + if not ok2 or type(x) ~= "number" or type(y) ~= "number" then return nil end + return x, y +end + +local function WorldOfMap(mapID) + local CM = _G.C_Map + if not (CM and CM.GetWorldPosFromMapPos) then return nil end + local ok, cont = pcall(CM.GetWorldPosFromMapPos, mapID, Vec(0.5, 0.5)) + if ok and type(cont) == "number" then return cont end + return nil +end + +local function CellCentre(cx, cy) + -- Zellen wurden aus x = zweiter, y = erster Rueckgabewert von UnitPosition gebildet + return (cy + 0.5) * CELL, (cx + 0.5) * CELL -- first, second +end + +local function InMap(x, y) + return x and y and x >= 0 and x <= 1 and y >= 0 and y <= 1 +end + +-- Erste Zellen einer Welt (fuer die Messpunkte) +local function SampleCells(cells, n) + local out = {} + for key in pairs(cells) do + out[#out + 1] = key + if #out >= n then break end + end + return out +end + +-- Orientierung bestimmen: erst am Spieler (falls er auf der Karte steht), sonst nach der Zahl der Zellen, die auf die Karte fallen +local function DetectSwap(world, cells, mapID) + local db = Codex.db + local saved = db.settings.pathSwap + if type(saved) == "boolean" then return saved end + local CM = _G.C_Map + local pos = Path.Read and Path.Read() + if pos and pos.world == world and CM and CM.GetPlayerMapPosition then + local ok, mp = pcall(CM.GetPlayerMapPosition, mapID, "player") + if ok and mp and mp.GetXY then + local px, py = mp:GetXY() + local best, bestErr + for _, swap in ipairs({ false, true }) do + local x, y = MapPosOf(world, pos.y, pos.x, mapID, swap) + if x then + local err = math.abs(x - px) + math.abs(y - py) + if not bestErr or err < bestErr then best, bestErr = swap, err end + end + end + if best ~= nil and bestErr < 0.05 then + db.settings.pathSwap = best + return best + end + end + end + local score = { [false] = 0, [true] = 0 } + for _, key in ipairs(SampleCells(cells, 40)) do + local cx, cy = Path.KeyToCell(key) + local f, s = CellCentre(cx, cy) + for _, swap in ipairs({ false, true }) do + local x, y = MapPosOf(world, f, s, mapID, swap) + if InMap(x, y) then score[swap] = score[swap] + 1 end + end + end + return score[true] > score[false] +end + +-- Lineare Abbildung (first, second) -> (x, y) der Karte, aus drei Messpunkten +local function Fit(world, cells, mapID, swap) + for _, key in ipairs(SampleCells(cells, 60)) do + local cx, cy = Path.KeyToCell(key) + local f0, s0 = CellCentre(cx, cy) + local x0, y0 = MapPosOf(world, f0, s0, mapID, swap) + if InMap(x0, y0) then + local x1, y1 = MapPosOf(world, f0 + PROBE, s0, mapID, swap) + local x2, y2 = MapPosOf(world, f0, s0 + PROBE, mapID, swap) + if x1 and x2 then + local a, b, c, d = (x1 - x0) / PROBE, (x2 - x0) / PROBE, (y1 - y0) / PROBE, (y2 - y0) / PROBE + if a ~= 0 or b ~= 0 or c ~= 0 or d ~= 0 then + return function(f, s) + return x0 + (f - f0) * a + (s - s0) * b, y0 + (f - f0) * c + (s - s0) * d + end + end + end + end + end + return nil +end + +-- Ersatz, wenn keine der Stichprobenzellen auf der Karte liegt (kleine Karten wie Staedte): Mittelpunkt der Karte +-- in Weltkoordinaten erfragen und die Abbildung dort messen +local function FitFromMap(world, mapID, swap) + local CM = _G.C_Map + if not (CM and CM.GetWorldPosFromMapPos) then return nil end + local ok, cont, wp = pcall(CM.GetWorldPosFromMapPos, mapID, Vec(0.5, 0.5)) + if not ok or cont ~= world or not wp or not wp.GetXY then return nil end + local ok2, vx, vy = pcall(wp.GetXY, wp) + if not ok2 or type(vx) ~= "number" or type(vy) ~= "number" then return nil end + local f0, s0 + if swap then f0, s0 = vy, vx else f0, s0 = vx, vy end + local x0, y0 = MapPosOf(world, f0, s0, mapID, swap) + local x1, y1 = MapPosOf(world, f0 + PROBE, s0, mapID, swap) + local x2, y2 = MapPosOf(world, f0, s0 + PROBE, mapID, swap) + if not (x0 and x1 and x2) then return nil end + local a, b, c, d = (x1 - x0) / PROBE, (x2 - x0) / PROBE, (y1 - y0) / PROBE, (y2 - y0) / PROBE + if a == 0 and b == 0 and c == 0 and d == 0 then return nil end + return function(f, s) + return x0 + (f - f0) * a + (s - s0) * b, y0 + (f - f0) * c + (s - s0) * d + end +end + +------------------------------------------------------------------------ +-- Daten einer Karte +------------------------------------------------------------------------ + +-- Rueckgabe: { dots = { { x, y, kind, w } }, counts = { w, d, x, g }, swap }, oder nil, wenn nichts umgerechnet werden kann +function PM.Compute(mapID) + local db = Codex.db + if not (db and mapID) then return nil end + local res = { dots = {}, counts = { w = 0, d = 0, x = 0, g = 0 }, mapID = mapID } + local world = WorldOfMap(mapID) + local cells = world and Path.Cells(world) + local map + res.world, res.fit, res.ncells = world, "none", 0 + if cells and next(cells) then + for _ in pairs(cells) do res.ncells = res.ncells + 1 end + res.swap = DetectSwap(world, cells, mapID) + map = Fit(world, cells, mapID, res.swap) + if map then + res.fit = "cells" + else + map = FitFromMap(world, mapID, res.swap) + if map then res.fit = "map" end + end + end + local bins = {} + local function Bin(x, y, kind, w) + local bx, by = math.floor(x / BIN), math.floor(y / BIN) + local key = by * 100000 + bx + local b = bins[key] + if not b then b = { x = x, y = y, kind = kind, w = w }; bins[key] = b + elseif kind == "d" then b.kind = "d" end + return b + end + local pathHash = {} + local own = (db.path and db.path[world]) or {} + if map then + for key, mask in pairs(cells) do + local cx, cy = Path.KeyToCell(key) + local f, s = CellCentre(cx, cy) + local x, y = map(f, s) + if InMap(x, y) then + local isDrop = math.floor(mask / 256) % 256 > 0 + local mine = own[key] ~= nil + local b = Bin(x, y, isDrop and "d" or (mine and "w" or "r"), 1) + if b.kind == "r" and mine then b.kind = "w" end -- eigener Weg geht vor + if isDrop then res.counts.d = res.counts.d + 1 + elseif mine then res.counts.w = res.counts.w + 1 + else res.counts.r = (res.counts.r or 0) + 1 end + local hx, hy = math.floor(x / GAP_RADIUS), math.floor(y / GAP_RADIUS) + pathHash[hy * 100000 + hx] = true + end + end + end + -- Flugrouten dieser Karte: Strecke als Punktreihe, Beschriftung in der Mitte + local flights, tags = {}, {} + local TX = Codex.Taxi + if map and TX and TX.Routes then + for _, r in pairs(TX.Routes()) do + local na, nb = TX.Node(r.a), TX.Node(r.b) + if na and nb and na.w == world and r.p and #r.p >= 4 then + local pts = {} + if na.a then pts[#pts + 1] = { na.a, na.b } end + for i = 1, #r.p - 1, 2 do pts[#pts + 1] = { r.p[i], r.p[i + 1] } end + if nb.a then pts[#pts + 1] = { nb.a, nb.b } end + local xs, ys = {}, {} + for i, q in ipairs(pts) do xs[i], ys[i] = map(q[1], q[2]) end + local any = false + for i = 1, #pts - 1 do + local x1, y1, x2, y2 = xs[i], ys[i], xs[i + 1], ys[i + 1] + if x1 and x2 and (InMap(x1, y1) or InMap(x2, y2)) then + any = true + local steps = math.max(1, math.min(40, math.ceil(math.max(math.abs(x2 - x1), math.abs(y2 - y1)) / 0.004))) + for k = 0, steps - 1 do + local fx, fy = x1 + (x2 - x1) * k / steps, y1 + (y2 - y1) * k / steps + if InMap(fx, fy) then flights[#flights + 1] = { x = fx, y = fy, kind = "f", w = 4 } end + end + end + end + if any then + local mid = math.max(1, math.floor(#pts / 2)) + if xs[mid] and InMap(xs[mid], ys[mid]) then + tags[#tags + 1] = { x = xs[mid], y = ys[mid], + text = string.format("%s > %s %s %s", na.n or "?", nb.n or "?", TX.Duration(r.t), TX.Money(r.c)) } + end + end + end + end + end + res.counts.f = #tags + res.tags = tags + -- Luecken: bekannte Orte ohne Weg in der Naehe + local function NearPath(x, y) + local hx, hy = math.floor(x / GAP_RADIUS), math.floor(y / GAP_RADIUS) + for dy = -1, 1 do + for dx = -1, 1 do + if pathHash[(hy + dy) * 100000 + hx + dx] then return true end + end + end + return false + end + local gaps, seen = {}, {} + local function Gap(x, y) + if not x or #gaps >= MAX_GAPS or not InMap(x, y) or NearPath(x, y) then return end + local k = math.floor(x / GAP_RADIUS) * 100000 + math.floor(y / GAP_RADIUS) + if seen[k] then return end + seen[k] = true + gaps[#gaps + 1] = { x = x, y = y, kind = "g", w = 2 } + end + local V = Codex.V and Codex.V() + for _, q in pairs(V and V.quests or {}) do + for _, p in ipairs({ q.give, q.turn }) do + if type(p) == "table" and p.m == mapID then Gap(p.x, p.y) end + end + end + for _, u in pairs(V and V.units or {}) do + if u.ro and next(u.ro) and u.cells then + for key in pairs(u.cells) do + local m = math.floor(key / (GRID * GRID)) + if m == mapID then + local idx = key % (GRID * GRID) + Gap(((idx % GRID) + 0.5) / GRID, (math.floor(idx / GRID) + 0.5) / GRID) + break + end + end + end + end + res.counts.g = #gaps + -- Gebietsuebergaenge dieser Karte + local crossings = {} + local function AddLink(a, b, kinds) + for kind, e in pairs(kinds) do + for _, p in ipairs(e.p or {}) do + local key, w + if a == mapID then key, w = p.fk, p.fw elseif b == mapID then key, w = p.tk, p.tw end + if key and w == world and map then + local cx, cy = Path.KeyToCell(key) + local f, s = CellCentre(cx, cy) + local x, y = map(f, s) + if InMap(x, y) then crossings[#crossings + 1] = { x = x, y = y, kind = "l" .. kind, w = 3, to = a == mapID and b or a } end + end + end + end + end + for a, row in pairs(Path.Links()) do + for b, kinds in pairs(row) do + if a == mapID or b == mapID then AddLink(a, b, kinds) end + end + end + res.counts.x = #crossings + -- Ortspunkte dieser Karte (Instanz-Eingaenge, Aufstiege): nur fuer Berechtigte + local spotDots = {} + local SP = Codex.Spots + if SP and Codex.Trust and Codex.Trust.CanEdit() then + for _, r in ipairs(SP.All()) do + if r.p.m == mapID and InMap(r.p.x, r.p.y) then + spotDots[#spotDots + 1] = { x = r.p.x, y = r.p.y, kind = "s", w = 3, name = r.n, sk = r.k } + end + end + end + res.counts.s = #spotDots + -- Reihenfolge: Wege unten, dann Faelle, Luecken, Uebergaenge oben + local list = {} + for _, b in pairs(bins) do list[#list + 1] = b end + table.sort(list, function(p, q) + if p.kind ~= q.kind then + local rank = { r = 0, w = 1 } + local a, b2 = rank[p.kind] or 2, rank[q.kind] or 2 + return a < b2 + end + if p.y ~= q.y then return p.y < q.y end + return p.x < q.x + end) + for _, b in ipairs(list) do res.dots[#res.dots + 1] = b end + for _, g in ipairs(flights) do res.dots[#res.dots + 1] = g end + for _, g in ipairs(gaps) do res.dots[#res.dots + 1] = g end + for _, c in ipairs(crossings) do res.dots[#res.dots + 1] = c end + for _, c in ipairs(spotDots) do res.dots[#res.dots + 1] = c end + return res +end + +-- Karten, die Daten haben (fuer die Auswahl): { { map, n } } nach Zahl der Uebergaenge +function PM.Maps() + local db = Codex.db + local count = {} + for a, row in pairs(Path.Links()) do + for b, kinds in pairs(row) do + local n = 0 + for _, e in pairs(kinds) do n = n + e.n end + if a > 0 then count[a] = (count[a] or 0) + n end + if b > 0 then count[b] = (count[b] or 0) + n end + end + end + local cur = _G.C_Map and _G.C_Map.GetBestMapForUnit and select(2, pcall(_G.C_Map.GetBestMapForUnit, "player")) + if type(cur) == "number" then count[cur] = count[cur] or 0 end + local out = {} + for m, n in pairs(count) do out[#out + 1] = { map = m, n = n } end + table.sort(out, function(x, y) + if x.n ~= y.n then return x.n > y.n end + return x.map < y.map + end) + return out +end + +------------------------------------------------------------------------ +-- Anzeige auf der Weltkarte +------------------------------------------------------------------------ + +local layer, pool, label + +local API = Codex.MapAPI + +local function Canvas() + return API.Canvas() +end + +local function CurrentMapID() + return API.CurrentMapID(function() + local list = {} + for _, e in ipairs(PM.Maps()) do list[#list + 1] = e.map end + return list + end) +end + +local function Scale(frame) + local s = 1 + if frame and frame.GetEffectiveScale and _G.UIParent then + local a, b = frame:GetEffectiveScale(), _G.UIParent:GetEffectiveScale() + if type(a) == "number" and type(b) == "number" and a > 0 and b > 0 then s = a / b end + end + return s +end + +local function EnsureLayer() + if layer then return layer end + local wm = _G.WorldMapFrame + if not (wm and Canvas()) then return nil end + if API.Classic() then + local canvas = Canvas() + layer = CreateFrame("Frame", nil, canvas) + layer:SetAllPoints(canvas) + layer:SetFrameLevel((canvas:GetFrameLevel() or 1) + 15) + else + layer = CreateFrame("Frame", nil, wm) + layer:SetAllPoints(wm.ScrollContainer or wm) + layer:SetFrameLevel((wm:GetFrameLevel() or 1) + LAYER_LEVEL) + end + if wm.GetFrameStrata then layer:SetFrameStrata(wm:GetFrameStrata()) end + if layer.SetClipsChildren then layer:SetClipsChildren(true) end + pool = {} + label = layer:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("TOPLEFT", layer, "TOPLEFT", 12, API.Classic() and -42 or -12) + label:SetJustifyH("LEFT") + local acc, lastScale, lastMap = 0, nil, nil + layer:SetScript("OnUpdate", function(_, elapsed) + acc = acc + (elapsed or 0) + if acc < 0.3 then return end + acc = 0 + if not PM.on then return end + local sc, m = Scale(Canvas()), API.Signature() + if sc ~= lastScale or m ~= lastMap then + lastScale, lastMap = sc, m + PM.Refresh() + end + end) + return layer +end + +local function Dot(i) + local t = pool[i] + if not t then + t = layer:CreateTexture(nil, "OVERLAY") + pool[i] = t + end + return t +end + +function PM.Refresh() + if not layer then + if not PM.on then return end + EnsureLayer() + end + if not layer then return end + for _, t in ipairs(pool) do t:Hide() end + for _, t in ipairs(PM.tagPool or {}) do t:Hide() end + if not PM.on then + label:SetText("") + layer:Hide() + return + end + layer:Show() + local mapID = CurrentMapID() + local canvas = Canvas() + if not (mapID and canvas) then + label:SetText(FW.L.CODEX_PATHMAP_NONE) -- nicht die Zeile der vorherigen Karte stehen lassen + return + end + local width, height = canvas:GetWidth() or 1, canvas:GetHeight() or 1 + if width < 2 or height < 2 then return end + local res = PM.Compute(mapID) + PM.last = res + local L = FW.L + if not res or (res.counts.w + (res.counts.f or 0) + (res.counts.r or 0) + res.counts.d + res.counts.x + res.counts.g + (res.counts.s or 0)) == 0 then + label:SetText(L.CODEX_PATHMAP_NONE) + return + end + label:SetText(string.format(L.CODEX_PATHMAP_LABEL, res.counts.w, res.counts.d, res.counts.x, res.counts.g)) + local scale, layerScale = Scale(canvas), Scale(layer) + local ratio = scale / layerScale + PM.drawInfo = { w = width, h = height, scale = scale, layerScale = layerScale, total = #res.dots, drawn = 0, shown = layer:IsShown() and true or false } + local n = 0 + for _, d in ipairs(res.dots) do + local show + if d.kind == "w" then show = PM.LayerOn("w") + elseif d.kind == "r" then show = PM.LayerOn("r") + elseif d.kind == "f" then show = PM.LayerOn("f") + elseif d.kind == "d" then show = PM.LayerOn("d") + elseif d.kind == "g" then show = PM.LayerOn("g") + elseif d.kind == "s" then show = PM.LayerOn("s") + else show = PM.LayerOn("x") end + if show then + n = n + 1 + if n > MAX_DOTS then break end + PM.drawInfo.drawn = n + local t = Dot(n) + local px = (d.w == 1 and 3 or (d.w == 4 and 4 or d.w == 2 and 8 or (d.kind == "s" and 14 or 10))) / layerScale + local c = COLORS[d.kind] or COLORS.w + t:ClearAllPoints() + t:SetSize(px, px) + t:SetPoint("CENTER", canvas, "TOPLEFT", d.x * width * ratio, -d.y * height * ratio) + t:SetColorTexture(c[1], c[2], c[3], (d.kind == "w" or d.kind == "r") and 0.85 or 1) + t:Show() + end + end + -- Beschriftung der Flugrouten + if PM.LayerOn("f") and res.tags then + PM.tagPool = PM.tagPool or {} + for i, tg in ipairs(res.tags) do + if i > 20 then break end + local fs = PM.tagPool[i] + if not fs then + fs = layer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + PM.tagPool[i] = fs + end + fs:ClearAllPoints() + fs:SetPoint("BOTTOM", canvas, "TOPLEFT", tg.x * width * ratio, -tg.y * height * ratio + 4) + fs:SetText(tg.text) + fs:SetTextColor(COLORS.f[1], COLORS.f[2], COLORS.f[3]) + fs:Show() + end + end +end + +-- Weltkarte oeffnen, auf eine Karte stellen (optional) und die Ebenen einblenden +function PM.Show(mapID) + PM.on = true + local wm = _G.WorldMapFrame + if wm then + if not wm:IsShown() then + if type(_G.ToggleWorldMap) == "function" then pcall(_G.ToggleWorldMap) elseif wm.Show then pcall(wm.Show, wm) end + end + if mapID then API.SetMap(mapID) end + if not PM.hooked then + PM.hooked = true + if wm.OnMapChanged then pcall(_G.hooksecurefunc, wm, "OnMapChanged", function() PM.Refresh() end) end + if wm.HookScript then wm:HookScript("OnShow", function() PM.Refresh() end) end + if API.Classic() and _G.hooksecurefunc and _G.SetMapZoom then + pcall(_G.hooksecurefunc, "SetMapZoom", function() PM.Refresh() end) + end + end + end + PM.Refresh() + FW:Fire("CODEX_PATHMAP_CHANGED") +end + +function PM.Hide() + PM.on = false + PM.Refresh() + FW:Fire("CODEX_PATHMAP_CHANGED") +end + +function PM.Toggle(mapID) + if PM.on then PM.Hide() else PM.Show(mapID) end +end + +-- /fwwege diag: Diagnose fuer die Karte, die gerade angezeigt wird (warum ist das Wegenetz leer?) +function PM.Diag() + local CM = _G.C_Map + local mapID = CurrentMapID() + FW:Print("Wegenetz-Diagnose: Karte " .. tostring(mapID)) + if not mapID then return end + local info = CM and CM.GetMapInfo and select(2, pcall(CM.GetMapInfo, mapID)) + FW:Print("Name: " .. tostring(type(info) == "table" and info.name or "?") .. ", Eltern-Karte: " .. tostring(type(info) == "table" and info.parentMapID or "?")) + local ok, cont, wp = false, nil, nil + if CM and CM.GetWorldPosFromMapPos then ok, cont, wp = pcall(CM.GetWorldPosFromMapPos, mapID, Vec(0.5, 0.5)) end + local vx, vy + if ok and wp and wp.GetXY then vx, vy = wp:GetXY() end + FW:Print(string.format("Kartenmitte in Weltkoordinaten: ok=%s, Welt=%s, x=%s, y=%s", tostring(ok), tostring(cont), tostring(vx), tostring(vy))) + local res = PM.Compute(mapID) + if not res then FW:Print("Compute: nil") return end + FW:Print(string.format("Welt %s, Zellen der Welt: %d, gespeicherte Achsen-Vertauschung: %s, benutzt: %s", + tostring(res.world), res.ncells or 0, tostring(Codex.db.settings.pathSwap), tostring(res.swap))) + FW:Print(string.format("Umrechnung: %s (cells = aus Zellen, map = aus Kartenmitte, none = keine)", tostring(res.fit))) + local di = PM.drawInfo + if di then + FW:Print(string.format("Zeichnen: Punkte %d, gezeichnet %d, Flaeche %.0fx%.0f, Skalierung %.2f / Ebene %.2f, Ebene sichtbar %s, an: %s", + di.total, di.drawn, di.w, di.h, di.scale, di.layerScale, tostring(di.shown), tostring(PM.on))) + else + FW:Print("Zeichnen: noch nicht gezeichnet (Wegenetz einschalten mit /fwwege)") + end + local flags = {} + for _, k in ipairs({ "w", "r", "f", "d", "g", "s", "x" }) do flags[#flags + 1] = k .. "=" .. (PM.LayerOn(k) and "an" or "AUS") end + FW:Print("Ebenen: " .. table.concat(flags, " ")) + local c = res.counts + FW:Print(string.format("Punkte auf dieser Karte: eigene %d, fremde %d, Beute %d, Uebergaenge %d, Luecken %d, Orte %d", + c.w or 0, c.r or 0, c.d or 0, c.x or 0, c.g or 0, c.s or 0)) +end + +SLASH_FREEWOODPATHMAP1 = "/fwwege" +SlashCmdList["FREEWOODPATHMAP"] = function(msg) + if type(msg) == "string" and msg:lower():match("^%s*diag") then PM.Diag() else PM.Toggle() end +end diff --git a/CodexQuestProbe.lua b/CodexQuestProbe.lua new file mode 100644 index 0000000..c5598bc --- /dev/null +++ b/CodexQuestProbe.lua @@ -0,0 +1,215 @@ +-- Freewood-OctoWoW-Codex: Probe /fwquest +-- Prueft auf Octo, was der Server zu einer Quest-ID liefert (C_QuestLog.GetQuestDetails) und ob sich die darin +-- genannten Kreaturen, Objekte und Items ueber ihre ID benennen lassen. Nur Diagnose: schreibt in den Chat und nach +-- Codex.db.questProbe (SavedVariables, erst bei /reload oder Abmelden), veraendert keine Daten der Datenbank. +-- +-- /fwquest prueft ein paar bekannte Quests (7, 6, 3904, 1467) +-- /fwquest [...] prueft diese Quest-IDs +-- /fwquest go prueft ein Spielobjekt (C_GameObjectInfo) +-- /fwquest npc prueft eine Kreatur (C_CreatureInfo) + +local ADDON, NS = ... + +local FW = _G.Freewood +local Codex = NS and NS.Codex +if not FW or not Codex then + return +end + +local Probe = {} +Codex.QuestProbe = Probe + +local WAIT = 3 + +local function Store() + local db = Codex.db + if not db then return nil end + db.questProbe = db.questProbe or {} + return db.questProbe +end + +local function Short(v) + local s = tostring(v) + s = s:gsub("|", "/"):gsub("\n", " ") + if #s > 120 then s = s:sub(1, 120) .. "..." end + return s +end + +-- Tabelle als Zeilen "pfad = wert" +local function Flatten(t, path, out, depth) + if depth > 3 then out[#out + 1] = path .. " = {...}" return end + local keys = {} + for k in pairs(t) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) return tostring(a) < tostring(b) end) + for _, k in ipairs(keys) do + local v = t[k] + local p = path == "" and tostring(k) or (path .. "." .. tostring(k)) + if type(v) == "table" then + Flatten(v, p, out, depth + 1) + else + out[#out + 1] = p .. " = " .. Short(v) + end + end +end + +local function Say(text) FW:Print("Quest-Probe: " .. text) end + +local function Log(rec, line) + rec[#rec + 1] = line + Say(line) +end + +local function Call(fn, a) + if type(fn) ~= "function" then return false, "fehlt" end + return pcall(fn, a) +end + +local function ItemName(id) + local CI = _G.C_Item + local get = (CI and CI.GetItemInfo) or _G.GetItemInfo + if not get then return nil end + local ok, name = pcall(get, id) + if ok and type(name) == "string" and name ~= "" then return name end + return nil +end + +local function CreatureName(id) + local CC = _G.C_CreatureInfo + if not (CC and CC.GetCreatureInfoByID) then return nil end + local ok, info = pcall(CC.GetCreatureInfoByID, id) + if ok and type(info) == "table" then + return tostring(info.name) .. " (Typ " .. tostring(info.type) .. ", Rang " .. tostring(info.rank) .. (info.subName and info.subName ~= "" and (", " .. info.subName) or "") .. ")" + end + return nil +end + +local function ObjectName(id) + local CG = _G.C_GameObjectInfo + if not (CG and CG.GetGameObjectInfoByID) then return nil end + local ok, info = pcall(CG.GetGameObjectInfoByID, id) + if ok and type(info) == "table" then + return tostring(info.name) .. " (Typ " .. tostring(info.type) .. ", Anzeige " .. tostring(info.displayID) .. ")" + end + return nil +end + +local function RequestNames(details) + local CC, CG, CI = _G.C_CreatureInfo, _G.C_GameObjectInfo, _G.C_Item + local function Req(fn, id) if fn then pcall(fn, id) end end + for _, r in ipairs(details.requirements or {}) do + if r.kind == "monster" then Req(CC and CC.RequestLoadCreatureByID, r.id) + elseif r.kind == "object" then Req(CG and CG.RequestLoadGameObjectByID, r.id) + elseif r.kind == "item" then Req(CI and CI.RequestLoadItemDataByID, r.id) end + end + for _, list in ipairs({ details.rewardItems or {}, details.choiceItems or {} }) do + for _, it in ipairs(list) do Req(CI and CI.RequestLoadItemDataByID, it.id) end + end + if (details.srcItemID or 0) > 0 then Req(CI and CI.RequestLoadItemDataByID, details.srcItemID) end +end + +local function ReportDetails(id, rec, d) + local lines = {} + Flatten(d, "", lines, 0) + Log(rec, "Quest " .. id .. ": " .. #lines .. " Felder") + for _, l in ipairs(lines) do rec[#rec + 1] = " " .. l end + Say(string.format("%s | Stufe %s | Typ %s | Geld %s | Ziele %d | feste Items %d | Wahl-Items %d | Ort %s", + Short(d.title), tostring(d.level), tostring(d.questType or "-"), tostring(d.rewardMoney), + #(d.requirements or {}), #(d.rewardItems or {}), #(d.choiceItems or {}), + d.poi and (tostring(d.poi.mapID) .. ":" .. tostring(d.poi.x) .. "," .. tostring(d.poi.y)) or "-")) +end + +local function ReportNames(id, rec, d) + for _, r in ipairs(d.requirements or {}) do + local name + if r.kind == "monster" then name = CreatureName(r.id) + elseif r.kind == "object" then name = ObjectName(r.id) + else name = ItemName(r.id) end + Log(rec, string.format(" Ziel %s %d x%s -> %s", r.kind, r.id, tostring(r.count), name or "kein Name (noch nicht geladen)")) + end + for _, def in ipairs({ { "feste Belohnung", d.rewardItems }, { "Wahl-Belohnung", d.choiceItems } }) do + for _, it in ipairs(def[2] or {}) do + Log(rec, string.format(" %s Item %d x%s -> %s", def[1], it.id, tostring(it.count), ItemName(it.id) or "kein Name")) + end + end + if (d.srcItemID or 0) > 0 then + Log(rec, string.format(" Start-Item %d -> %s", d.srcItemID, ItemName(d.srcItemID) or "kein Name")) + end +end + +function Probe.Quest(id) + local QL = _G.C_QuestLog + if not (QL and QL.GetQuestDetails and QL.RequestLoadQuestByID) then + Say("C_QuestLog.GetQuestDetails gibt es in dieser ClassicAPI-Version nicht.") + return + end + local st = Store() + if not st then return end + local rec = {} + st[id] = rec + local okC, cached = Call(QL.IsQuestDataCachedByID, id) + rec[#rec + 1] = "vorher im Zwischenspeicher: " .. tostring(okC and cached) + pcall(QL.RequestLoadQuestByID, id) + Say("Quest " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...") + C_Timer.After(WAIT, function() + local ok, d = pcall(QL.GetQuestDetails, id) + if not (ok and type(d) == "table") then + Log(rec, "Quest " .. id .. ": keine Antwort (unbekannt oder noch nicht geladen)" .. ((not ok) and (" - Fehler: " .. Short(d)) or "")) + return + end + ReportDetails(id, rec, d) + local okN, n = Call(QL.GetNumQuestObjectives, id) + rec[#rec + 1] = "GetNumQuestObjectives: " .. tostring(okN and n or "-") + RequestNames(d) + C_Timer.After(WAIT, function() ReportNames(id, rec, d) end) + end) +end + +function Probe.Object(id) + local CG = _G.C_GameObjectInfo + if not (CG and CG.GetGameObjectInfoByID) then Say("C_GameObjectInfo gibt es nicht.") return end + local st = Store() + if not st then return end + local rec = {} + st["go" .. id] = rec + rec[#rec + 1] = "vorher: " .. tostring(ObjectName(id)) + if CG.RequestLoadGameObjectByID then pcall(CG.RequestLoadGameObjectByID, id) end + Say("Objekt " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...") + C_Timer.After(WAIT, function() + Log(rec, "Objekt " .. id .. ": " .. tostring(ObjectName(id) or "keine Antwort")) + end) +end + +function Probe.Npc(id) + local CC = _G.C_CreatureInfo + if not (CC and CC.GetCreatureInfoByID) then Say("C_CreatureInfo gibt es nicht.") return end + local st = Store() + if not st then return end + local rec = {} + st["npc" .. id] = rec + rec[#rec + 1] = "vorher: " .. tostring(CreatureName(id)) + if CC.RequestLoadCreatureByID then pcall(CC.RequestLoadCreatureByID, id) end + Say("Kreatur " .. id .. ": Antwort in " .. WAIT .. " Sekunden ...") + C_Timer.After(WAIT, function() + Log(rec, "Kreatur " .. id .. ": " .. tostring(CreatureName(id) or "keine Antwort")) + end) +end + +function Probe.Command(msg) + msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "") + local go = msg:match("^go%s+(%d+)$") + if go then Probe.Object(tonumber(go)) return end + local npc = msg:match("^npc%s+(%d+)$") + if npc then Probe.Npc(tonumber(npc)) return end + local ids = {} + for n in msg:gmatch("%d+") do ids[#ids + 1] = tonumber(n) end + if #ids == 0 then ids = { 7, 6, 3904, 1467 } end + for i, id in ipairs(ids) do + if i > 8 then break end + local qid = id + C_Timer.After((i - 1) * (WAIT * 2 + 1), function() Probe.Quest(qid) end) + end + Say("Ergebnis steht auch in Codex.db.questProbe (SavedVariables nach /reload).") +end + +_G.SLASH_FWQUESTPROBE1 = "/fwquest" +SlashCmdList["FWQUESTPROBE"] = function(msg) Probe.Command(msg) end