Files
2026-10-27 14:08:54 +00:00

1174 lines
43 KiB
Lua

-- 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