-- Freewood-OctoWoW-Codex: Flugrouten (Taxi) -- -- Erfasst beim Flug: Start- und Zielpunkt, die Strecke (Punkte alle paar Sekunden), die Flugdauer und den bezahlten Preis. -- Je Charakter: welche Flugpunkte er kennt, wie viel Gold er fuer Fluege ausgegeben hat und wie oft er geflogen ist. -- -- db.taxi.nodes[id] = { n = Name, w = Welt, tx, ty = Position auf der Flugkarte, a, b = Weltposition (erster, zweiter Wert von UnitPosition) } -- db.taxi.routes[id] = { a = Start-ID, b = Ziel-ID, t = Sekunden, c = Preis in Kupfer, n = Anzahl, p = { a1, b1, a2, b2, ... }, upd = Stand } -- db.taxiChar[Charakter] = { known = { [id] = true }, spent = Kupfer, trips = Anzahl, log = { { t, a, b, c, d }, ... } } -- Die Flugpunkt-ID kommt aus der Position auf der Flugkarte (sprachunabhaengig), sonst aus dem Namen. -- Zwischenlandungen: aufgezeichnet wird die Gesamtstrecke bis zum gewaehlten Ziel. local ADDON, NS = ... local FW = _G.Freewood local Codex = NS and NS.Codex if not FW or not Codex then return end local Taxi = { state = {} } Codex.Taxi = Taxi NS.Taxi = Taxi local state = Taxi.state local SAMPLE = 3 -- Sekunden zwischen zwei Streckenpunkten local MAX_POINTS = 120 -- Punkte je Route (je 2 Zahlen) local MAX_ROUTES = 600 local MAX_NODES = 300 local MAX_LOG = 30 local PRICE_WINDOW = 6 -- Sekunden nach dem Start, in denen der abgezogene Betrag nachgemessen wird local function Clock() return _G.GetTime and _G.GetTime() or 0 end local function Now() return _G.time and _G.time() or 0 end local function Round(v) return math.floor((v or 0) + 0.5) end local function DB() local db = Codex.db if not db then return nil end local t = db.taxi if type(t) ~= "table" then t = {}; db.taxi = t end t.nodes = t.nodes or {} t.routes = t.routes or {} return t end local function CharRec(create) local db = Codex.db local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or nil if not db or not key then return nil end db.taxiChar = db.taxiChar or {} local r = db.taxiChar[key] if not r and create ~= false then r = { known = {}, spent = 0, trips = 0, log = {} } db.taxiChar[key] = r end return r end function Taxi.Money(c) c = math.floor(tonumber(c) or 0) local g, s, k = math.floor(c / 10000), math.floor(c / 100) % 100, c % 100 local out = {} if g > 0 then out[#out + 1] = g .. "g" end if s > 0 then out[#out + 1] = s .. "s" end if k > 0 or #out == 0 then out[#out + 1] = k .. "c" end return table.concat(out, " ") end function Taxi.Duration(sec) sec = math.floor(tonumber(sec) or 0) return string.format("%d:%02d", math.floor(sec / 60), sec % 60) end ------------------------------------------------------------------------ -- Abfragen (fuer Karte und Planer) ------------------------------------------------------------------------ Taxi.rev = 0 local merged -- Eigene und empfangene Routen zusammen (eigene haben Vorrang) function Taxi.Routes() local t = DB() if not t then return {} end local rem = Codex.db.taxir if not rem or next(rem) == nil then return t.routes end if merged and merged.rev == Taxi.rev then return merged.routes end local out = {} for k, r in pairs(rem) do out[k] = r end for k, r in pairs(t.routes) do out[k] = r end merged = { rev = Taxi.rev, routes = out } return out end function Taxi.Node(id) local t = DB() if not t then return nil end local n = t.nodes[id] if n then return n end local rn = Codex.db.taxin return rn and rn[id] or nil end -- Kennt dieser Charakter den Flugpunkt? function Taxi.Known(id) local c = CharRec(false) return c and c.known and c.known[id] and true or false end function Taxi.CharStats() local c = CharRec(false) if not c then return nil end local n = 0 for _ in pairs(c.known or {}) do n = n + 1 end return { known = n, spent = c.spent or 0, trips = c.trips or 0, secs = c.secs or 0, log = c.log, routes = c.rt } end -- Wie oft dieser Charakter die Route geflogen ist und wie lange insgesamt: n, Sekunden function Taxi.RouteStat(a, b) local c = CharRec(false) local r = c and c.rt and c.rt[a .. ">" .. b] if not r then return 0, 0 end return r.n or 0, r.sec or 0 end ------------------------------------------------------------------------ -- Flugkarte lesen ------------------------------------------------------------------------ local function NodeId(i, world) if _G.TaxiNodePosition then local ok, x, y = pcall(_G.TaxiNodePosition, i) if ok and type(x) == "number" and type(y) == "number" then return string.format("%d:%d:%d", world or 0, Round(x * 1000), Round(y * 1000)), x, y end end local name = _G.TaxiNodeName and _G.TaxiNodeName(i) if type(name) == "string" and name ~= "" then return "n:" .. name, nil, nil end return nil end -- Kartenposition des Flugpunkts (Karte, x, y), solange der Spieler dort steht; fuer den Levelplaner local function SetMapPos(n) local loc = FW.Util and FW.Util.GetPlayerLocation and FW.Util.GetPlayerLocation() if loc and loc.mapID and loc.x and loc.y and loc.x > 0 and loc.y > 0 then if n.m ~= loc.mapID or not n.x then n.m, n.x, n.y = loc.mapID, loc.x, loc.y return true end end return false end function Taxi.Scan() local count = _G.NumTaxiNodes and _G.NumTaxiNodes() or 0 if type(count) ~= "number" or count <= 0 then return end local T, C = DB(), CharRec(true) if not T or not C then return end local P = Codex.Path and Codex.Path.Read and Codex.Path.Read() local world = P and P.world or 0 local nodes = 0 for _ in pairs(T.nodes) do nodes = nodes + 1 end state.slot, state.cur = {}, nil local changed = false for i = 1, math.min(count, 120) do local kind = _G.TaxiNodeGetType and _G.TaxiNodeGetType(i) local name = _G.TaxiNodeName and _G.TaxiNodeName(i) local id, tx, ty = NodeId(i, world) if id then local n = T.nodes[id] if not n and nodes < MAX_NODES then n = {} T.nodes[id] = n nodes = nodes + 1 end if n then if type(name) == "string" and name ~= "" then n.n = name:sub(1, 60) end n.w, n.tx, n.ty = world, tx, ty end if kind ~= "NONE" and not C.known[id] then C.known[id] = true; changed = true end if kind == "CURRENT" then state.cur = id if n and P then n.a, n.b = Round(P.y), Round(P.x) end if n and SetMapPos(n) then changed = true end end local cost = _G.TaxiNodeCost and tonumber(_G.TaxiNodeCost(i)) or nil state.slot[i] = { id = id, cost = cost and math.floor(cost) or nil, kind = kind } end end if changed then Taxi.rev = Taxi.rev + 1 FW:Fire("CODEX_TAXI_CHANGED") end if Taxi.OnOpen then pcall(Taxi.OnOpen) end end ------------------------------------------------------------------------ -- Teilen (Share/Sync, Datensatzart "f", je Route ein Datensatz) ------------------------------------------------------------------------ -- Zahl zur Route: Flugpunkt-Zahl (Welt, x, y) * 10^7 + Flugpunkt-Zahl des Ziels. Nur Flugpunkte mit Positions-ID sind teilbar. local NODE_W = 1002001 local SHARE_POINTS = 40 local function NodeNum(id) local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$") w, x, y = tonumber(w), tonumber(x), tonumber(y) if not w or w > 8 or x > 1000 or y > 1000 then return nil end return w * NODE_W + x * 1001 + y end local function NodeFromNum(n) if type(n) ~= "number" or n < 0 or n >= 9 * NODE_W then return nil end local w, r = math.floor(n / NODE_W), n % NODE_W return string.format("%d:%d:%d", w, math.floor(r / 1001), r % 1001) end local function RouteNum(a, b) local na, nb = NodeNum(a), NodeNum(b) if not na or not nb then return nil end return na * 10000000 + nb end local function RouteIds(num) local a, b = NodeFromNum(math.floor(num / 10000000)), NodeFromNum(num % 10000000) if not a or not b then return nil end return a, b end function Taxi.SyncList() local out = {} local t = DB() if not t then return out end for _, r in pairs(t.routes) do local num = r.upd and RouteNum(r.a, r.b) if num then out[#out + 1] = { key = "f" .. string.format("%.0f", num), upd = r.upd } end end return out end local function NodeShare(id) local n = Taxi.Node(id) or {} return { n = n.n, a = n.a, b = n.b, m = n.m, x = n.x, y = n.y } end -- Eigene Route als Datensatz (Strecke auf hoechstens SHARE_POINTS Punkte verkleinert) function Taxi.RouteRecord(num) local a, b = RouteIds(num) local t = DB() local r = a and t and t.routes[a .. ">" .. b] if not r or not r.upd or type(r.p) ~= "table" then return nil end local pts, m = {}, math.floor(#r.p / 2) if m < 2 then return nil end local take = math.min(m, SHARE_POINTS) for i = 1, take do local j = (take == 1) and 1 or (1 + math.floor((i - 1) * (m - 1) / (take - 1) + 0.5)) pts[#pts + 1] = r.p[j * 2 - 1] pts[#pts + 1] = r.p[j * 2] end return { t = r.t, c = r.c, n = r.n or 1, p = pts, A = NodeShare(a), B = NodeShare(b) }, r.upd end local function NodeFromRecord(id, rn) local w, x, y = tostring(id):match("^(%d+):(%d+):(%d+)$") return { n = rn.n, w = tonumber(w), tx = tonumber(x) / 1000, ty = tonumber(y) / 1000, a = rn.a, b = rn.b, m = rn.m, x = rn.x, y = rn.y } end -- Empfangene Route einmischen. Rueckgabe: true, wenn gespeichert function Taxi.StoreRemote(rec) local db = Codex.db if not db or type(rec) ~= "table" then return false end local a, b = RouteIds(rec.id) if not a then return false end db.taxir = db.taxir or {} db.taxin = db.taxin or {} local rid = a .. ">" .. b local old = db.taxir[rid] if old and (old.upd or 0) >= rec.upd then return false end if not old then local count = 0 for _ in pairs(db.taxir) do count = count + 1 end if count >= MAX_ROUTES then return false end end db.taxir[rid] = { a = a, b = b, t = rec.t, c = rec.c, n = rec.n, p = rec.p, upd = rec.upd } local own = DB() for id, rn in pairs({ [a] = rec.A, [b] = rec.B }) do if not (own and own.nodes[id]) and not db.taxin[id] then local nodes = 0 for _ in pairs(db.taxin) do nodes = nodes + 1 end if nodes < MAX_NODES then db.taxin[id] = NodeFromRecord(id, rn) end end end Taxi.rev = Taxi.rev + 1 return true end -- Levelplaner: Fluege als Kanten { id, k, t, wait, from = { m, x, y }, to = { m, x, y } }, nur zwischen Flugpunkten, -- die dieser Charakter kennt und deren Kartenposition bekannt ist function Taxi.PlannerOn() local db = Codex.db return not (db and db.settings and db.settings.taxiPlan == false) end local edgeCache function Taxi.Edges() if not Taxi.PlannerOn() then return {} end local key = Codex.Trust and Codex.Trust.SelfKey and Codex.Trust.SelfKey() or "?" if edgeCache and edgeCache.rev == Taxi.rev and edgeCache.key == key then return edgeCache.list end local list = {} for rid, r in pairs(Taxi.Routes()) do local na, nb = Taxi.Node(r.a), Taxi.Node(r.b) if na and nb and na.m and nb.m and r.t and Taxi.Known(r.a) and Taxi.Known(r.b) then list[#list + 1] = { id = "f" .. rid, k = "flight", t = r.t, wait = 20, name = nb.n, fa = r.a, fb = r.b, from = { m = na.m, x = na.x, y = na.y }, to = { m = nb.m, x = nb.x, y = nb.y } } end end edgeCache = { rev = Taxi.rev, key = key, list = list } return list end function Taxi.ClearRemote() local db = Codex.db if db then db.taxir, db.taxin = nil, nil end Taxi.rev = Taxi.rev + 1 end ------------------------------------------------------------------------ -- Flug erfassen ------------------------------------------------------------------------ local function OnTake(slot) local s = state.slot and state.slot[slot] if not s or not state.cur then return end state.pending = { from = state.cur, to = s.id, quote = s.cost or 0, money0 = _G.GetMoney and _G.GetMoney() or 0, asked = Clock(), started = false } end local function Finish(p, now) state.pending = nil Taxi.flight = nil local T, C = DB(), CharRec(true) if not T or not C then return end local dur = now - p.t0 if dur < 5 then return end -- Endpunkt: Position der Landung local P = Codex.Path and Codex.Path.Read and Codex.Path.Read() if P then p.pts[#p.pts + 1] = Round(P.y) p.pts[#p.pts + 1] = Round(P.x) local n = T.nodes[p.to] if n and not n.a then n.a, n.b = Round(P.y), Round(P.x) end if n and not n.m then SetMapPos(n) end end local paid = p.paid or 0 if paid <= 0 or paid > p.quote * 3 + 100 then paid = p.quote or 0 end local rid = p.from .. ">" .. p.to local r = T.routes[rid] local isNew = false if not r then local count = 0 for _ in pairs(T.routes) do count = count + 1 end if count >= MAX_ROUTES then return end r = { a = p.from, b = p.to, t = Round(dur), c = paid, n = 1, p = p.pts, upd = Now() } T.routes[rid] = r isNew = true else r.n = (r.n or 1) + 1 r.t = Round((r.t * (r.n - 1) + dur) / r.n) r.c = paid if #p.pts > #(r.p or {}) then r.p = p.pts end r.upd = Now() end C.known[p.to] = true C.spent = (C.spent or 0) + paid C.trips = (C.trips or 0) + 1 C.secs = (C.secs or 0) + Round(dur) C.rt = C.rt or {} local rs = C.rt[rid] if not rs then rs = { n = 0, sec = 0, gold = 0 }; C.rt[rid] = rs end rs.n, rs.sec, rs.gold = rs.n + 1, rs.sec + Round(dur), (rs.gold or 0) + paid C.log = C.log or {} C.log[#C.log + 1] = { t = Now(), a = p.from, b = p.to, c = paid, d = Round(dur) } while #C.log > MAX_LOG do table.remove(C.log, 1) end if isNew then local na, nb = T.nodes[p.from], T.nodes[p.to] FW:Print(string.format("Codex: Flugroute %s -> %s gespeichert (%s, %s, %d Punkte)", na and na.n or p.from, nb and nb.n or p.to, Taxi.Duration(dur), Taxi.Money(paid), math.floor(#p.pts / 2))) end Taxi.rev = Taxi.rev + 1 FW:Fire("CODEX_TAXI_CHANGED") FW:Fire("CODEX_CHANGED", "taxi") end local function Tick() local p = state.pending if not p then return end local now = Clock() local on = _G.UnitOnTaxi and _G.UnitOnTaxi("player") and true or false if on and not p.started then p.started, p.t0, p.nextAt, p.pts = true, now, now, {} local r = Taxi.Routes()[p.from .. ">" .. p.to] local nd = Taxi.Node(p.to) Taxi.flight = { t0 = now, exp = r and r.t or nil, name = nd and nd.n or "?" } end if p.started then if on then if now - p.t0 <= PRICE_WINDOW then local d = p.money0 - (_G.GetMoney and _G.GetMoney() or 0) if d > (p.paid or 0) then p.paid = d end end if now >= p.nextAt and #p.pts < MAX_POINTS * 2 then local P = Codex.Path and Codex.Path.Read and Codex.Path.Read() if P then p.pts[#p.pts + 1] = Round(P.y) p.pts[#p.pts + 1] = Round(P.x) end p.nextAt = now + SAMPLE end else Finish(p, now) end elseif now - p.asked > 15 then state.pending = nil -- Flug nicht angetreten Taxi.flight = nil end end local acc = 0 local frame = _G.CreateFrame and _G.CreateFrame("Frame") if frame then frame:SetScript("OnUpdate", function(_, elapsed) acc = acc + (elapsed or 0) if acc < 1 then return end acc = 0 pcall(Tick) end) end -- Tooltip am Flugziel auf der Flugkarte: Reisedauer (und wie oft schon geflogen) function Taxi.ButtonTip(button) if not (button and button.GetID and state.cur and state.slot) then return end local s = state.slot[button:GetID()] local tt = _G.GameTooltip if not s or not tt or s.id == state.cur then return end local r = Taxi.Routes()[state.cur .. ">" .. s.id] local L = FW.L if r and r.t then tt:AddLine(string.format(L.CODEX_TAXI_TIP_TIME, Taxi.Duration(r.t)), 0.94, 0.75, 0.36) else tt:AddLine(L.CODEX_TAXI_TIP_NONE, 0.6, 0.59, 0.55) end local n, sec = Taxi.RouteStat(state.cur, s.id) if n > 0 then tt:AddLine(string.format(L.CODEX_TAXI_TIP_COUNT, n, Taxi.Duration(sec)), 0.6, 0.59, 0.55) end tt:Show() end local function Install() if Taxi.hooked then return end Taxi.hooked = true if _G.hooksecurefunc and _G.TaxiNodeOnButtonEnter then pcall(_G.hooksecurefunc, "TaxiNodeOnButtonEnter", function(button) pcall(Taxi.ButtonTip, button) end) end if _G.hooksecurefunc and _G.TakeTaxiNode then pcall(_G.hooksecurefunc, "TakeTaxiNode", function(slot) pcall(OnTake, slot) end) end FW:RegisterEvent("TAXIMAP_OPENED", function() pcall(Taxi.Scan) end, Taxi) end Install() ------------------------------------------------------------------------ -- /fwtaxi ------------------------------------------------------------------------ SLASH_FREEWOODTAXI1 = "/fwtaxi" SlashCmdList["FREEWOODTAXI"] = function(arg) local T = DB() if not T then return end local nodes, routes = 0, 0 for _ in pairs(T.nodes) do nodes = nodes + 1 end for _ in pairs(T.routes) do routes = routes + 1 end if arg == "diag" then FW:Print(string.format("Taxi: TakeTaxiNode=%s TaxiNodePosition=%s TaxiNodeCost=%s TaxiNodeGetType=%s UnitOnTaxi=%s Hook=%s", tostring(_G.TakeTaxiNode ~= nil), tostring(_G.TaxiNodePosition ~= nil), tostring(_G.TaxiNodeCost ~= nil), tostring(_G.TaxiNodeGetType ~= nil), tostring(_G.UnitOnTaxi ~= nil), tostring(Taxi.hooked))) FW:Print(string.format("Taxi: aktueller Punkt=%s, Plaetze=%d, Flug laeuft=%s", tostring(state.cur), state.slot and #state.slot or 0, tostring(state.pending and state.pending.started or false))) return end local c = Taxi.CharStats() FW:Print(string.format("Taxi: %d Flugpunkte, %d Routen gespeichert.", nodes, routes)) if c then FW:Print(string.format("Taxi: dieser Charakter kennt %d Flugpunkte, %d Fluege, %s ausgegeben, %s in der Luft.", c.known, c.trips, Taxi.Money(c.spent), Taxi.Duration(c.secs))) end local list = {} for _, r in pairs(T.routes) do list[#list + 1] = r end table.sort(list, function(x, y) return (x.upd or 0) > (y.upd or 0) end) for i = 1, math.min(#list, 5) do local r = list[i] local na, nb = T.nodes[r.a], T.nodes[r.b] FW:Print(string.format(" %s -> %s: %s, %s, %d Punkte, %dx", na and na.n or r.a, nb and nb.n or r.b, Taxi.Duration(r.t), Taxi.Money(r.c), math.floor(#(r.p or {}) / 2), r.n or 1)) end end