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