1174 lines
43 KiB
Lua
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
|