1263 lines
41 KiB
Lua
1263 lines
41 KiB
Lua
-- Freewood-OctoWoW-Codex: Daten mit anderen Spielern teilen (Addon-Nachrichten)
|
|
--
|
|
-- EIN Schalter (db.settings.share). Eingeschaltet wird ueber Gilde, Gruppe, Schlachtzug und einen
|
|
-- geheimen Kanal ("FreewoodCodex", aus allen Chatfenstern ausgeblendet) gerufen und getauscht.
|
|
-- Gesendet werden nur eigene Beobachtungen, keine Charakternamen, keine persoenlichen Angaben.
|
|
--
|
|
-- Ablauf (alles "ziehen", nie ungefragt grosse Datenmengen schieben):
|
|
-- H Hallo mit Anzahl und Pruefwert der eigenen Datensaetze (Kanal)
|
|
-- A Ankuendigung geaenderter Datensaetze schluessel:stand,... (Kanal)
|
|
-- Q Anfrage nach dem Verzeichnis ab einem Stand (Fluestern)
|
|
-- V Verzeichnis schluessel:stand,... in Stuecken (Fluestern)
|
|
-- R Anfrage nach bestimmten Datensaetzen (Fluestern)
|
|
-- B Datensatz in Stuecken (Fluestern)
|
|
-- Empfangenes wird streng geprueft (Share.Sanitize) und getrennt je Absender gespeichert.
|
|
|
|
local ADDON, NS = ...
|
|
|
|
local FW = _G.Freewood
|
|
local Codex = NS and NS.Codex
|
|
local Share = NS and NS.Share
|
|
if not FW or not Codex or not Share then
|
|
return
|
|
end
|
|
|
|
local Sync = { started = false }
|
|
NS.Sync = Sync
|
|
Codex.Sync = Sync
|
|
|
|
local PREFIX = "FWCodex1"
|
|
local GLOBAL_CHANNEL = "FreewoodCodex"
|
|
local MAXLEN = 230
|
|
local SEND_INTERVAL = 0.25 -- Takt des Zeitgebers
|
|
local MIN_INTERVAL = 3 -- kleinster Abstand zwischen zwei Nachrichten (Sekunden)
|
|
local MAX_INTERVAL = 6
|
|
local GLOBAL_INTERVAL = 1.5
|
|
local MAX_QUEUE = 500
|
|
local MAX_CHUNKS = 40
|
|
local MAX_BUFFERS = 6
|
|
local SESSION_TTL = 600
|
|
local DIGEST_COOLDOWN = 60 -- ein Partner darf unser Verzeichnis jede Minute holen
|
|
local SERVE_WINDOW = 300
|
|
local MAX_SERVE = 150 -- Datensaetze je Partner und Zeitraum
|
|
local MAX_REQUEST = 50 -- Datensaetze je Abgleich
|
|
local MAX_DIGEST = 400
|
|
local MAX_SESSIONS_HOUR = 12
|
|
local MAX_RECEIVED_HOUR = 400
|
|
local ANNOUNCE_EVERY = 60
|
|
local ANNOUNCE_MIN = 300
|
|
local SCAN_EVERY = 300
|
|
local KINDS = { "guild", "party", "raid", "global" }
|
|
|
|
local state = {
|
|
queue = {},
|
|
peers = {}, -- [name] = { kind, t, hash, count, queried }
|
|
sessions = {}, -- [name] = { t, digest, got }
|
|
served = {}, -- [name] = { t, count, dirT }
|
|
buffers = {}, -- [name:key] = { t, n, parts, got }
|
|
received = {}, -- [name] = { t, count }
|
|
pending = {}, -- [key] = true: zu verkuendende Datensaetze
|
|
announced = {}, -- [key] = zeitpunkt
|
|
sessionLog = {},
|
|
lastGlobalSend = 0,
|
|
rawNames = {},
|
|
routes = {}, -- [name] = Art des Kanals, ueber den wir ihn zuletzt gehoert haben
|
|
served_partners = {}, -- [name] = zeitpunkt der ersten Bedienung in der Stunde
|
|
lastReply = 0,
|
|
interval = MIN_INTERVAL,
|
|
nextSend = 0,
|
|
log = false,
|
|
status = { global = "off" },
|
|
stats = { txType = {}, rxType = {}, throttled = 0, fail = 0, lastResult = "-", rx = 0, tx = 0, hello = 0, helloIn = 0, queries = 0, served = 0, rejected = 0, byChannel = {} },
|
|
}
|
|
Sync.state = state
|
|
|
|
local function Now()
|
|
return (GetTime and GetTime()) or time()
|
|
end
|
|
|
|
local function Store()
|
|
return Share.Store()
|
|
end
|
|
|
|
function Sync:IsEnabled()
|
|
return Codex.db ~= nil and Codex.db.settings ~= nil and Codex.db.settings.share == true
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Kleine Helfer
|
|
------------------------------------------------------------------------
|
|
|
|
local B36 = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
|
|
local function ToB36(n)
|
|
n = math.floor(n or 0)
|
|
if n <= 0 then
|
|
return "0"
|
|
end
|
|
local out = ""
|
|
while n > 0 do
|
|
local r = n % 36
|
|
out = B36:sub(r + 1, r + 1) .. out
|
|
n = (n - r) / 36
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function FromB36(s)
|
|
if type(s) ~= "string" or s == "" or #s > 8 or s:find("[^0-9a-z]") then
|
|
return nil
|
|
end
|
|
local n = 0
|
|
for i = 1, #s do
|
|
n = n * 36 + (B36:find(s:sub(i, i), 1, true) - 1)
|
|
end
|
|
return n
|
|
end
|
|
|
|
local function Clean(sender)
|
|
if type(sender) ~= "string" then
|
|
return nil
|
|
end
|
|
if Ambiguate then
|
|
local ok, v = pcall(Ambiguate, sender, "none")
|
|
if ok and v then
|
|
return v
|
|
end
|
|
end
|
|
return sender
|
|
end
|
|
|
|
local function KeyOf(sender)
|
|
local c = Clean(sender)
|
|
return c and c:lower() or nil
|
|
end
|
|
|
|
-- Name ohne Realm, ohne Leerzeichen (auch geschuetzte), kleingeschrieben; Namen koennen Leerzeichen enthalten
|
|
local function NameCore(s)
|
|
if type(s) ~= "string" then
|
|
return ""
|
|
end
|
|
s = s:gsub("\194\160", " "):match("^([^%-]*)") or s
|
|
s = s:gsub("%s", ""):lower()
|
|
return s
|
|
end
|
|
|
|
-- Eigene Namen: alle Schreibweisen, die das Spiel liefert, plus Namen, die wir als Echo unserer eigenen Nachrichten erkannt haben
|
|
local learnedSelf = {}
|
|
local SESSION_NONCE = string.format("%06d", math.random(0, 999999)) .. string.format("%03d", math.random(0, 999))
|
|
local function SelfCores()
|
|
local out = {}
|
|
local function add(v)
|
|
local c = NameCore(v)
|
|
if c ~= "" then
|
|
out[c] = true
|
|
end
|
|
end
|
|
if UnitName then
|
|
add((UnitName("player")))
|
|
end
|
|
if UnitFullName then
|
|
local ok, n = pcall(UnitFullName, "player")
|
|
if ok then add(n) end
|
|
end
|
|
if GetUnitName then
|
|
local ok, n = pcall(GetUnitName, "player", false)
|
|
if ok then add(n) end
|
|
end
|
|
for c in pairs(learnedSelf) do
|
|
out[c] = true
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function IsSelf(sender)
|
|
if type(sender) ~= "string" then
|
|
return false
|
|
end
|
|
local c = NameCore(sender)
|
|
return c ~= "" and SelfCores()[c] == true
|
|
end
|
|
|
|
local function GetEncoder()
|
|
local E = _G.C_EncodingUtil
|
|
if E and E.SerializeCBOR and E.DeserializeCBOR and E.CompressString
|
|
and E.DecompressString and E.EncodeBase64 and E.DecodeBase64 then
|
|
return E
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function EncodeRecord(rec)
|
|
local E = GetEncoder()
|
|
if not E then
|
|
return nil
|
|
end
|
|
local ok, cbor = pcall(E.SerializeCBOR, rec)
|
|
if not ok or type(cbor) ~= "string" then
|
|
return nil
|
|
end
|
|
local ok2, packed = pcall(E.CompressString, cbor)
|
|
if not ok2 or type(packed) ~= "string" then
|
|
return nil
|
|
end
|
|
local ok3, body = pcall(E.EncodeBase64, packed)
|
|
if not ok3 or type(body) ~= "string" then
|
|
return nil
|
|
end
|
|
return body
|
|
end
|
|
|
|
-- Eigener, nachsichtiger Base64-Leser: jede Vierergruppe fuer sich (auch wenn mitten im Text Auffuellzeichen stehen)
|
|
local B64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
local B64IDX
|
|
local function LenientB64(text)
|
|
if not B64IDX then
|
|
B64IDX = {}
|
|
for i = 1, #B64CHARS do B64IDX[B64CHARS:sub(i, i)] = i - 1 end
|
|
end
|
|
local out = {}
|
|
for i = 1, #text, 4 do
|
|
local g = text:sub(i, i + 3)
|
|
local v, n = 0, 0
|
|
for j = 1, 4 do
|
|
local c = g:sub(j, j)
|
|
local x = B64IDX[c]
|
|
if x then v = v * 64 + x n = n + 1 elseif c == "=" or c == "" then v = v * 64 else return nil end
|
|
end
|
|
if n < 2 then return nil end
|
|
local b1, b2, b3 = math.floor(v / 65536) % 256, math.floor(v / 256) % 256, v % 256
|
|
out[#out + 1] = string.char(b1)
|
|
if n >= 3 then out[#out + 1] = string.char(b2) end
|
|
if n >= 4 then out[#out + 1] = string.char(b3) end
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
-- Diagnose (wird gespeichert, damit man nachsehen kann, woran ein Empfang scheitert)
|
|
local function Diag(stage, detail)
|
|
local st = Store()
|
|
st.diag = st.diag or {}
|
|
st.diag[stage] = (st.diag[stage] or 0) + 1
|
|
if detail then st.diag.last = stage .. ": " .. tostring(detail):sub(1, 120) end
|
|
end
|
|
|
|
-- Rueckgabe: Datensatz | nil, Stufe
|
|
local function DecodeRecord(body)
|
|
local E = GetEncoder()
|
|
if not E then return nil, "noapi" end
|
|
if type(body) ~= "string" or body:find("[^%w%+/=]") then return nil, "chars" end
|
|
local function Try(packed)
|
|
if type(packed) ~= "string" then return nil, "base64" end
|
|
local ok2, cbor = pcall(E.DecompressString, packed)
|
|
if not ok2 or type(cbor) ~= "string" then return nil, "decompress", cbor end
|
|
local ok3, rec = pcall(E.DeserializeCBOR, cbor)
|
|
if not ok3 or type(rec) ~= "table" then return nil, "cbor", rec end
|
|
if FW.Export and FW.Export.CheckStructure and not FW.Export.CheckStructure(rec) then return nil, "structure" end
|
|
return rec
|
|
end
|
|
local ok, packed = pcall(E.DecodeBase64, body)
|
|
local rec, stage, err = Try(ok and packed or nil)
|
|
if rec then return rec end
|
|
-- zweiter Versuch mit dem nachsichtigen Leser
|
|
local rec2, stage2, err2 = Try(LenientB64(body))
|
|
if rec2 then Diag("lenient") return rec2 end
|
|
return nil, stage2 or stage, err2 or err
|
|
end
|
|
|
|
-- Roundtrip-Test der Kodierung (Befehl /fwcodex enc): zeigt jede Stufe, Ergebnis wird gespeichert
|
|
function Sync:EncTest()
|
|
local E = GetEncoder()
|
|
local st = Store()
|
|
local out = {}
|
|
local function Add(t) out[#out + 1] = t FW:Print("Codex-Test: " .. t) end
|
|
if not E then Add("C_EncodingUtil nicht vollstaendig vorhanden") st.enctest = out return end
|
|
local sample = { y = "u", id = 80117, ne = "Haywire Battlechicken", k = 3, l1 = 2, l2 = 2, d = { [80159] = { q = 1, n = 1 } } }
|
|
local function Head(v) return type(v) == "string" and (#v .. " Zeichen, Anfang " .. v:sub(1, 24):gsub("[^%w%+/=]", "?")) or type(v) end
|
|
local ok1, cbor = pcall(E.SerializeCBOR, sample)
|
|
Add("CBOR: " .. tostring(ok1) .. " " .. Head(cbor))
|
|
local ok2, packed = pcall(E.CompressString, cbor)
|
|
Add("Komprimiert: " .. tostring(ok2) .. " " .. Head(packed))
|
|
local ok3, body = pcall(E.EncodeBase64, packed)
|
|
Add("Base64: " .. tostring(ok3) .. " " .. Head(body))
|
|
local rec, stage, err = DecodeRecord(body)
|
|
Add("Zurueck: " .. (rec and ("ok, ne=" .. tostring(rec.ne) .. ", id=" .. tostring(rec.id)) or ("FEHLER bei " .. tostring(stage) .. " " .. tostring(err))))
|
|
-- Teilstuecke wie beim Senden zusammensetzen
|
|
local parts, size = {}, 40
|
|
if type(body) == "string" then
|
|
for i = 1, #body, size do parts[#parts + 1] = body:sub(i, i + size - 1) end
|
|
local rec2, stage2 = DecodeRecord(table.concat(parts))
|
|
Add("Aus " .. #parts .. " Stuecken: " .. (rec2 and "ok" or ("FEHLER bei " .. tostring(stage2))))
|
|
end
|
|
local direct = pcall(E.DecodeBase64, body)
|
|
Add("DecodeBase64 allein: " .. tostring(direct))
|
|
st.enctest = out
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Senden mit Warteschlange
|
|
------------------------------------------------------------------------
|
|
|
|
local function Log(arrow, where, who, text)
|
|
if state.log then
|
|
FW:Print(string.format("|cff88ccffCodex %s|r %s%s %s", arrow, where or "?", who and (" " .. tostring(who):gsub("|", "||")) or "", ((text or ""):sub(1, 110):gsub("|", "||"))))
|
|
end
|
|
end
|
|
|
|
-- Rueckgabe: gesendet, gebremst (das Spiel hat die Nachricht wegen zu hoher Rate abgelehnt)
|
|
local function Raw(dist, target, text)
|
|
local api = _G.C_ChatInfo
|
|
if api and api.SendAddonMessage then
|
|
local ok, res = pcall(api.SendAddonMessage, PREFIX, text, dist, target)
|
|
state.stats.lastResult = tostring(ok and res or "Fehler")
|
|
if ok and res ~= false and (type(res) ~= "number" or res == 0) then
|
|
state.stats.tx = state.stats.tx + 1
|
|
local t = text:sub(1, 1)
|
|
state.stats.txType[t] = (state.stats.txType[t] or 0) + 1
|
|
Log("->", dist .. (target and ("#" .. tostring(target)) or ""), nil, text)
|
|
return true, false
|
|
end
|
|
if ok and (res == 3 or res == 8) then
|
|
state.stats.throttled = state.stats.throttled + 1
|
|
Log("!!", "gebremst", nil, text)
|
|
return false, true
|
|
end
|
|
state.stats.fail = state.stats.fail + 1
|
|
return false, false
|
|
end
|
|
return false, false
|
|
end
|
|
|
|
local function Enqueue(item)
|
|
local q = state.queue
|
|
if #q >= MAX_QUEUE then
|
|
for i = 1, #q do
|
|
local t = q[i].text:sub(1, 1)
|
|
if t == "H" or t == "A" then
|
|
table.remove(q, i)
|
|
break
|
|
end
|
|
end
|
|
if #q >= MAX_QUEUE then
|
|
return false
|
|
end
|
|
end
|
|
q[#q + 1] = item
|
|
return true
|
|
end
|
|
|
|
function Sync:Pump()
|
|
local q = state.queue
|
|
if #q == 0 then
|
|
return false
|
|
end
|
|
local now = Now()
|
|
if now < state.nextSend then
|
|
return false
|
|
end
|
|
for i = 1, #q do
|
|
local item = q[i]
|
|
if not (item.dist == "CHANNEL" and now - state.lastGlobalSend < GLOBAL_INTERVAL) then
|
|
table.remove(q, i)
|
|
if item.dist == "CHANNEL" then
|
|
state.lastGlobalSend = now
|
|
end
|
|
local sent, throttled = Raw(item.dist, item.target, item.text)
|
|
if throttled then
|
|
-- zu schnell: Nachricht behalten, Abstand vergroessern
|
|
item.tries = (item.tries or 0) + 1
|
|
if item.tries <= 8 then
|
|
table.insert(q, 1, item)
|
|
end
|
|
state.interval = math.min(MAX_INTERVAL, state.interval * 1.6)
|
|
else
|
|
state.interval = math.max(MIN_INTERVAL, state.interval * 0.95)
|
|
end
|
|
state.nextSend = now + state.interval
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Antwort an einen Spieler: ueber denselben Kanal, auf dem wir ihn gehoert haben, mit seinem Namen
|
|
-- als Empfaenger in der Nachricht (andere ignorieren sie). Fluestern gibt es bewusst nicht.
|
|
local function Reply(key, text)
|
|
local kind = state.routes[key] or (state.peers[key] and state.peers[key].kind)
|
|
if not kind then
|
|
return false
|
|
end
|
|
local dist, target = Sync:Distribution(kind)
|
|
if not dist then
|
|
return false
|
|
end
|
|
local who = state.rawNames[key] or key
|
|
return Enqueue({ dist = dist, target = target, text = text:sub(1, 1) .. "|" .. who .. text:sub(2) })
|
|
end
|
|
|
|
local function InstanceGroup()
|
|
return _G.LE_PARTY_CATEGORY_INSTANCE and IsInGroup and IsInGroup(_G.LE_PARTY_CATEGORY_INSTANCE)
|
|
end
|
|
|
|
function Sync:Distribution(kind)
|
|
-- OctoWoW: nur der eigene Kanal (Gilde/Gruppe/Schlachtzug werden nicht benutzt)
|
|
if kind ~= "global" then return nil end
|
|
if kind == "guild" then
|
|
if IsInGuild and IsInGuild() then
|
|
return "GUILD"
|
|
end
|
|
elseif kind == "party" then
|
|
if InstanceGroup() then
|
|
return "INSTANCE_CHAT"
|
|
end
|
|
if IsInGroup and IsInGroup() and not (IsInRaid and IsInRaid()) then
|
|
return "PARTY"
|
|
end
|
|
elseif kind == "raid" then
|
|
if IsInRaid and IsInRaid() then
|
|
if InstanceGroup() then
|
|
return "INSTANCE_CHAT"
|
|
end
|
|
return "RAID"
|
|
end
|
|
elseif kind == "global" then
|
|
if GetChannelName then
|
|
local id = GetChannelName(GLOBAL_CHANNEL)
|
|
if type(id) == "number" and id > 0 then
|
|
return "CHANNEL", id
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function Broadcast(text)
|
|
local used, sent = {}, 0
|
|
for _, kind in ipairs(KINDS) do
|
|
local dist, target = Sync:Distribution(kind)
|
|
if dist then
|
|
local key = dist .. tostring(target or "")
|
|
if not used[key] then
|
|
used[key] = true
|
|
Enqueue({ dist = dist, target = target, text = text })
|
|
sent = sent + 1
|
|
end
|
|
end
|
|
end
|
|
return sent
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Hallo und Ankuendigungen
|
|
------------------------------------------------------------------------
|
|
|
|
-- Eigentuemer: Berechtigungsliste an alle schicken (hoechstens alle 5 Minuten, force = sofort)
|
|
local lastTrustSend = 0
|
|
function Sync:SendTrust(force)
|
|
if not self:IsEnabled() or not Codex.Trust or not Codex.Trust.IsSelfOwner() then
|
|
return false
|
|
end
|
|
if not force and Now() - lastTrustSend < 300 then
|
|
return false
|
|
end
|
|
lastTrustSend = Now()
|
|
local sent = 0
|
|
for _, line in ipairs(Codex.Trust.Chunks()) do
|
|
sent = sent + Broadcast(line)
|
|
end
|
|
return sent > 0
|
|
end
|
|
|
|
function Sync:SendHello()
|
|
if not self:IsEnabled() then
|
|
return false
|
|
end
|
|
local n, h = Share.Summary()
|
|
state.stats.hello = state.stats.hello + 1
|
|
if next(learnedSelf) == nil then
|
|
Broadcast("E|" .. SESSION_NONCE)
|
|
end
|
|
return Broadcast(string.format("H|%d|%d", h, n)) > 0
|
|
end
|
|
|
|
local function SessionAllowed()
|
|
local now = Now()
|
|
local log = state.sessionLog
|
|
for i = #log, 1, -1 do
|
|
if now - log[i] > 3600 then
|
|
table.remove(log, i)
|
|
end
|
|
end
|
|
return #log < MAX_SESSIONS_HOUR
|
|
end
|
|
|
|
function Sync:QueueAnnounce(key)
|
|
if self:IsEnabled() and type(key) == "string" then
|
|
state.pending[key] = true
|
|
end
|
|
end
|
|
|
|
function Sync:OnChanged(kind, id)
|
|
if not self:IsEnabled() then
|
|
return
|
|
end
|
|
local key, _, changed = Share.StampOne(kind, id)
|
|
if key and changed then
|
|
self:QueueAnnounce(key)
|
|
end
|
|
end
|
|
|
|
function Sync:FlushAnnounce()
|
|
if not self:IsEnabled() then
|
|
state.pending = {}
|
|
return 0
|
|
end
|
|
local now = Now()
|
|
local keys = {}
|
|
for key in pairs(state.pending) do
|
|
if now - (state.announced[key] or 0) >= ANNOUNCE_MIN then
|
|
keys[#keys + 1] = key
|
|
end
|
|
end
|
|
table.sort(keys)
|
|
local parts, count = {}, 0
|
|
local function flush()
|
|
if #parts > 0 then
|
|
Broadcast("A|" .. table.concat(parts, ","))
|
|
parts = {}
|
|
end
|
|
end
|
|
for _, key in ipairs(keys) do
|
|
state.pending[key] = nil
|
|
local upd = Share.OwnUpd(key)
|
|
if upd then
|
|
state.announced[key] = now
|
|
parts[#parts + 1] = string.format("%s:%s", key, ToB36(upd))
|
|
count = count + 1
|
|
if #parts >= 12 then
|
|
flush()
|
|
end
|
|
end
|
|
end
|
|
flush()
|
|
return count
|
|
end
|
|
|
|
-- Eigene Daten in Abstaenden durchsehen (erfasst auch Aenderungen ohne Ereignis)
|
|
function Sync:ScanOwn()
|
|
if not self:IsEnabled() then
|
|
return
|
|
end
|
|
local _, _, _, changed = Share.Scan(true)
|
|
for _, key in ipairs(changed) do
|
|
state.pending[key] = true
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Verzeichnis und Datensaetze
|
|
------------------------------------------------------------------------
|
|
|
|
local function PackList(prefix, entries)
|
|
local msgs, current, len = {}, {}, 0
|
|
for _, e in ipairs(entries) do
|
|
local add = #e + 1
|
|
if len + add > MAXLEN - 12 and #current > 0 then
|
|
msgs[#msgs + 1] = table.concat(current, ",")
|
|
current, len = {}, 0
|
|
end
|
|
current[#current + 1] = e
|
|
len = len + add
|
|
end
|
|
if #current > 0 then
|
|
msgs[#msgs + 1] = table.concat(current, ",")
|
|
end
|
|
local out = {}
|
|
for i, body in ipairs(msgs) do
|
|
out[i] = string.format("%s|%d/%d|%s", prefix, i, #msgs, body)
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function ParseEntries(body)
|
|
local out = {}
|
|
if type(body) ~= "string" then
|
|
return out
|
|
end
|
|
for key, upd in body:gmatch("([uoqixtpzghf]%d+):([0-9a-z]+)") do
|
|
local nupd = FromB36(upd)
|
|
if nupd and nupd > 0 and #key <= 16 and #out < MAX_DIGEST then
|
|
out[#out + 1] = { key = key, upd = nupd }
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Jeder darf Daten holen (es sind nur oeffentliche Spieldaten), aber nur wenige Partner je Stunde
|
|
function Sync:MayServe(name)
|
|
local now = Now()
|
|
local list = state.served_partners
|
|
if list[name] and now - list[name] < 3600 then
|
|
return true
|
|
end
|
|
local count = 0
|
|
for key, t in pairs(list) do
|
|
if now - t >= 3600 then
|
|
list[key] = nil
|
|
else
|
|
count = count + 1
|
|
end
|
|
end
|
|
if count >= 30 then
|
|
state.stats.rejected = state.stats.rejected + 1
|
|
return false
|
|
end
|
|
list[name] = now
|
|
return true
|
|
end
|
|
|
|
function Sync:SendDigest(name, since)
|
|
local dir = Share.Directory(since, MAX_DIGEST)
|
|
local flat = {}
|
|
for _, e in ipairs(dir) do
|
|
flat[#flat + 1] = string.format("%s:%s", e.key, ToB36(e.upd))
|
|
end
|
|
for _, msg in ipairs(PackList("V", flat)) do
|
|
Reply(name, msg)
|
|
end
|
|
if #flat == 0 then
|
|
Reply(name, "V|1/1|")
|
|
end
|
|
return #flat
|
|
end
|
|
|
|
-- Welche Datensaetze aus einer Liste fehlen uns (neuer Stand)? Aelteste zuerst.
|
|
local function Missing(name, entries)
|
|
local need = {}
|
|
for _, e in ipairs(entries) do
|
|
local have = Share.HaveRemote(name, e.key)
|
|
if not have or have < e.upd then
|
|
need[#need + 1] = e
|
|
end
|
|
end
|
|
table.sort(need, function(a, b)
|
|
if a.upd ~= b.upd then
|
|
return a.upd < b.upd
|
|
end
|
|
return a.key < b.key
|
|
end)
|
|
return need
|
|
end
|
|
|
|
function Sync:SendRequest(name, need)
|
|
local ids = {}
|
|
for i, e in ipairs(need) do
|
|
if i > MAX_REQUEST then
|
|
break
|
|
end
|
|
ids[#ids + 1] = string.format("%s:%s", e.key, ToB36(e.upd))
|
|
end
|
|
if #ids == 0 then
|
|
return 0
|
|
end
|
|
for _, msg in ipairs(PackList("R", ids)) do
|
|
Reply(name, msg)
|
|
end
|
|
state.sessions[name] = state.sessions[name] or { t = Now() }
|
|
state.sessions[name].t = Now()
|
|
return #ids
|
|
end
|
|
|
|
function Sync:ServeRecords(name, entries)
|
|
local now = Now()
|
|
local s = state.served[name]
|
|
if not s then
|
|
s = { t = now, count = 0 }
|
|
state.served[name] = s
|
|
elseif now - s.t > SERVE_WINDOW then
|
|
s.t, s.count = now, 0
|
|
end
|
|
local sent = 0
|
|
for _, e in ipairs(entries) do
|
|
if s.count >= MAX_SERVE then
|
|
break
|
|
end
|
|
local rec, upd = Share.OwnRecord(e.key)
|
|
if rec then
|
|
local body = EncodeRecord(rec)
|
|
local who = state.rawNames[name] or name
|
|
local size = MAXLEN - (2 + #who + 1 + #e.key + 1 + 8 + 1 + 8)
|
|
local n = body and math.ceil(#body / size) or 0
|
|
-- zu grosse Datensaetze (viele Waren/Lehrstoff) schrittweise verkleinern, statt sie zu verlieren
|
|
local tries = 0
|
|
while body and n > MAX_CHUNKS and tries < 5 do
|
|
tries = tries + 1
|
|
rec = Share.Shrink(rec)
|
|
body = rec and EncodeRecord(rec) or nil
|
|
n = body and math.ceil(#body / size) or 0
|
|
end
|
|
if body and n >= 1 and n <= MAX_CHUNKS then
|
|
for i = 1, n do
|
|
local piece = body:sub((i - 1) * size + 1, i * size)
|
|
Reply(name, string.format("B|%s|%s|%d/%d|%s", e.key, ToB36(upd), i, n, piece))
|
|
end
|
|
s.count = s.count + 1
|
|
sent = sent + 1
|
|
Store().sent = (Store().sent or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
return sent
|
|
end
|
|
|
|
local function ReceivedAllowed(name)
|
|
local now = Now()
|
|
local r = state.received[name]
|
|
if not r or now - r.t > 3600 then
|
|
r = { t = now, count = 0 }
|
|
state.received[name] = r
|
|
end
|
|
return r.count < MAX_RECEIVED_HOUR, r
|
|
end
|
|
|
|
function Sync:ReceiveChunk(name, key, upd, i, n, piece)
|
|
local sess = state.sessions[name]
|
|
if not sess or Now() - sess.t > SESSION_TTL then
|
|
Diag("nosession", name)
|
|
return false
|
|
end
|
|
if not key or not upd or not i or not n or n < 1 or n > MAX_CHUNKS or i < 1 or i > n or #piece > MAXLEN then
|
|
return false
|
|
end
|
|
local bkey = name .. ":" .. key .. ":" .. tostring(upd)
|
|
local buf = state.buffers[bkey]
|
|
if not buf then
|
|
local count = 0
|
|
for _ in pairs(state.buffers) do
|
|
count = count + 1
|
|
end
|
|
if count >= MAX_BUFFERS then
|
|
return false
|
|
end
|
|
buf = { t = Now(), n = n, parts = {}, got = 0 }
|
|
state.buffers[bkey] = buf
|
|
end
|
|
if buf.n ~= n then
|
|
state.buffers[bkey] = nil
|
|
return false
|
|
end
|
|
if not buf.parts[i] then
|
|
buf.parts[i] = piece
|
|
buf.got = buf.got + 1
|
|
end
|
|
if buf.got < n then
|
|
return true
|
|
end
|
|
state.buffers[bkey] = nil
|
|
local ok, r = ReceivedAllowed(name)
|
|
if not ok then
|
|
Diag("limit", name)
|
|
return false
|
|
end
|
|
r.count = r.count + 1
|
|
local raw, stage, err = DecodeRecord(table.concat(buf.parts))
|
|
if not raw then
|
|
Diag("decode_" .. tostring(stage), err)
|
|
state.stats.rejected = state.stats.rejected + 1
|
|
return false
|
|
end
|
|
local rec = Share.Sanitize(key, raw, upd)
|
|
if not rec then
|
|
Diag("sanitize", key)
|
|
state.stats.rejected = state.stats.rejected + 1
|
|
return false
|
|
end
|
|
if Share.StoreRecord(name, rec) then
|
|
local store = Store()
|
|
store.received = (store.received or 0) + 1
|
|
store.lastReceived = time()
|
|
FW:Fire("CODEX_SYNC_CHANGED")
|
|
else
|
|
Diag("notstored", key)
|
|
end
|
|
return true
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Empfang
|
|
------------------------------------------------------------------------
|
|
|
|
local function KindOf(channel)
|
|
if channel == "GUILD" then
|
|
return "guild"
|
|
elseif channel == "PARTY" then
|
|
return "party"
|
|
elseif channel == "RAID" then
|
|
return "raid"
|
|
elseif channel == "INSTANCE_CHAT" then
|
|
return "party"
|
|
elseif channel == "CHANNEL" then
|
|
return "global"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Sync:HandleMessage(channel, sender, text)
|
|
if not self:IsEnabled() then
|
|
return false
|
|
end
|
|
if type(text) ~= "string" or #text < 3 or #text > 255 then
|
|
return false
|
|
end
|
|
if type(sender) ~= "string" or sender == "" or IsSelf(sender) then
|
|
return false
|
|
end
|
|
local name = KeyOf(sender)
|
|
if not name or name == "" or name:sub(1, 1) == "~" then
|
|
return false
|
|
end
|
|
state.stats.rx = state.stats.rx + 1
|
|
state.stats.byChannel[channel] = (state.stats.byChannel[channel] or 0) + 1
|
|
state.rawNames[name] = sender
|
|
local t = text:sub(1, 1)
|
|
local body = text:sub(3)
|
|
local kind = KindOf(channel)
|
|
if kind and (t == "H" or t == "A") then
|
|
state.routes[name] = kind
|
|
end
|
|
|
|
-- Berechtigungsliste vom Eigentuemer (wird nur von Eigentuemern uebernommen)
|
|
if t == "T" and kind then
|
|
if Codex.Trust then Codex.Trust.Receive(sender, body) end
|
|
return true
|
|
end
|
|
if kind then
|
|
if t == "H" then
|
|
local h, n = body:match("^(%d+)|(%d+)$")
|
|
h, n = tonumber(h), tonumber(n)
|
|
if not h or not n then
|
|
return false
|
|
end
|
|
state.stats.helloIn = state.stats.helloIn + 1
|
|
local peer = state.peers[name] or {}
|
|
local newcomer = not state.peers[name] or Now() - state.peers[name].t > 600
|
|
local known = Share.Sender(name, false)
|
|
state.peers[name] = { kind = kind, t = Now(), hash = h, count = n, queried = peer.queried }
|
|
-- Neuling gruesst zurueck: einmal antworten, damit er unsere Daten auch bekommt
|
|
if newcomer and Codex.Trust and Codex.Trust.IsSelfOwner() then
|
|
C_Timer.After(5 + math.random() * 10, function() Sync:SendTrust() end)
|
|
end
|
|
if newcomer and Now() - state.lastReply > 60 then
|
|
state.lastReply = Now()
|
|
C_Timer.After(3 + math.random() * 5, function()
|
|
Sync:SendHello()
|
|
end)
|
|
end
|
|
-- eine Anfrage gilt 10 Minuten, ohne Antwort schon nach 90 Sekunden nicht mehr
|
|
local sess = state.sessions[name]
|
|
local window = (sess and (sess.got or 0) == 0) and 90 or 600
|
|
if n > 0 and not (known and known.hash == h) and not (peer.queried and Now() - peer.queried < window) and SessionAllowed() then
|
|
local active = 0
|
|
for _, s in pairs(state.sessions) do
|
|
if Now() - s.t < 120 and s.digest then
|
|
active = active + 1
|
|
end
|
|
end
|
|
if active < 2 then
|
|
state.peers[name].queried = Now()
|
|
state.sessionLog[#state.sessionLog + 1] = Now()
|
|
state.sessions[name] = { t = Now(), digest = {}, got = 0 }
|
|
local since = known and known.cursor or 0
|
|
Reply(name, "Q|" .. ToB36(since))
|
|
end
|
|
end
|
|
return true
|
|
elseif t == "A" then
|
|
state.peers[name] = {
|
|
kind = kind, t = Now(), hash = (state.peers[name] or {}).hash, queried = (state.peers[name] or {}).queried,
|
|
}
|
|
local need = Missing(name, ParseEntries(body))
|
|
if #need > 0 and #need <= 12 then
|
|
self:SendRequest(name, need)
|
|
end
|
|
return true
|
|
elseif t == "Q" or t == "V" or t == "R" or t == "B" then
|
|
local target, rest = body:match("^([^|]+)|(.*)$")
|
|
if not target or not IsSelf(target) then
|
|
return false
|
|
end
|
|
state.routes[name] = kind
|
|
return self:HandleDirect(name, t, rest)
|
|
end
|
|
return false
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Nachrichten an uns persoenlich (Q, V, R, B): name = Absender, t = Art, body = Inhalt
|
|
function Sync:HandleDirect(name, t, body)
|
|
if t == "Q" then
|
|
-- nur Partner bedienen, die uns ueber einen Kanal gerufen haben
|
|
if not self:MayServe(name) then
|
|
return false
|
|
end
|
|
state.stats.queries = state.stats.queries + 1
|
|
local s = state.served[name]
|
|
if s and s.dirT and Now() - s.dirT < DIGEST_COOLDOWN then
|
|
return false
|
|
end
|
|
if not s then
|
|
s = { t = Now(), count = 0 }
|
|
state.served[name] = s
|
|
end
|
|
s.dirT = Now()
|
|
self:SendDigest(name, FromB36(body) or 0)
|
|
return true
|
|
elseif t == "V" then
|
|
local sess = state.sessions[name]
|
|
if not sess or not sess.digest or Now() - sess.t > SESSION_TTL then
|
|
return false
|
|
end
|
|
local i, n, list = body:match("^(%d+)/(%d+)|(.*)$")
|
|
i, n = tonumber(i), tonumber(n)
|
|
if not i or not n or n < 1 or n > 30 or i < 1 or i > n then
|
|
return false
|
|
end
|
|
for _, e in ipairs(ParseEntries(list)) do
|
|
if #sess.digest < MAX_DIGEST then
|
|
sess.digest[#sess.digest + 1] = e
|
|
end
|
|
end
|
|
sess.got = (sess.got or 0) + 1
|
|
if sess.got >= n then
|
|
local digest = sess.digest
|
|
sess.digest = nil
|
|
table.sort(digest, function(a, b)
|
|
if a.upd ~= b.upd then return a.upd < b.upd end
|
|
return a.key < b.key
|
|
end)
|
|
local need = Missing(name, digest)
|
|
local first = {}
|
|
for idx = 1, math.min(#need, MAX_REQUEST) do
|
|
first[idx] = need[idx]
|
|
end
|
|
self:SendRequest(name, first)
|
|
-- Fortschritt merken: alles bis zum letzten angefragten Stand gilt als abgeholt
|
|
local sender = Share.Sender(name, true)
|
|
local mark
|
|
if #need > MAX_REQUEST then
|
|
mark = first[#first].upd
|
|
elseif #digest >= MAX_DIGEST then
|
|
mark = digest[#digest].upd
|
|
else
|
|
mark = nil
|
|
end
|
|
if mark then
|
|
sender.cursor = math.max(sender.cursor or 0, mark)
|
|
-- der Rest wird beim naechsten Hallo geholt
|
|
if state.peers[name] then
|
|
state.peers[name].queried = nil
|
|
end
|
|
else
|
|
if #digest > 0 then
|
|
sender.cursor = math.max(sender.cursor or 0, digest[#digest].upd)
|
|
end
|
|
sender.hash = state.peers[name] and state.peers[name].hash or sender.hash
|
|
end
|
|
end
|
|
return true
|
|
elseif t == "R" then
|
|
if not self:MayServe(name) then
|
|
return false
|
|
end
|
|
local _, _, list = body:match("^(%d+)/(%d+)|(.*)$")
|
|
self:ServeRecords(name, ParseEntries(list))
|
|
return true
|
|
elseif t == "B" then
|
|
local key, upd, i, n, piece = body:match("^([uoqixtpzghf]%d+)|([0-9a-z]+)|(%d+)/(%d+)|(.+)$")
|
|
return self:ReceiveChunk(name, key, FromB36(upd), tonumber(i), tonumber(n), piece)
|
|
end
|
|
return false
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Geheimer Kanal
|
|
------------------------------------------------------------------------
|
|
|
|
function Sync:JoinGlobal()
|
|
if not self:IsEnabled() then
|
|
return
|
|
end
|
|
local id = GetChannelName and GetChannelName(GLOBAL_CHANNEL)
|
|
if type(id) == "number" and id > 0 then
|
|
state.status.global = "ok"
|
|
return
|
|
end
|
|
if type(JoinChannelByName) ~= "function" then
|
|
state.status.global = "unavailable"
|
|
return
|
|
end
|
|
local ok = pcall(JoinChannelByName, GLOBAL_CHANNEL)
|
|
if not ok then
|
|
state.status.global = "failed"
|
|
return
|
|
end
|
|
if type(ChatFrame_RemoveChannel) == "function" then
|
|
for i = 1, (_G.NUM_CHAT_WINDOWS or 10) do
|
|
local frame = _G["ChatFrame" .. i]
|
|
if frame then
|
|
pcall(ChatFrame_RemoveChannel, frame, GLOBAL_CHANNEL)
|
|
end
|
|
end
|
|
end
|
|
state.status.global = "joining"
|
|
C_Timer.After(3, function()
|
|
local cid = GetChannelName and GetChannelName(GLOBAL_CHANNEL)
|
|
state.status.global = (type(cid) == "number" and cid > 0) and "ok" or "failed"
|
|
if state.status.global == "ok" and Sync:IsEnabled() then
|
|
Sync:SendHello()
|
|
end
|
|
FW:Fire("CODEX_SYNC_CHANGED")
|
|
end)
|
|
end
|
|
|
|
function Sync:LeaveGlobal()
|
|
if type(LeaveChannelByName) == "function" then
|
|
pcall(LeaveChannelByName, GLOBAL_CHANNEL)
|
|
end
|
|
state.status.global = "off"
|
|
end
|
|
|
|
function Sync:SetEnabled(on)
|
|
Codex.db.settings.share = on and true or false
|
|
if on then
|
|
self:Start()
|
|
self:JoinGlobal()
|
|
C_Timer.After(2 + math.random() * 3, function()
|
|
Sync:ScanOwn()
|
|
Sync:SendHello()
|
|
end)
|
|
else
|
|
self:LeaveGlobal()
|
|
self:Stop()
|
|
end
|
|
FW:Fire("CODEX_SYNC_CHANGED")
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Start / Stopp und Zeitgeber
|
|
------------------------------------------------------------------------
|
|
|
|
local generation = 0
|
|
|
|
local function Loop(gen, delay, fn)
|
|
C_Timer.After(delay, function()
|
|
if not Sync.started or gen ~= generation then
|
|
return
|
|
end
|
|
fn()
|
|
Loop(gen, delay, fn)
|
|
end)
|
|
end
|
|
|
|
function Sync:OnAddonMessage(prefix, text, channel, sender)
|
|
if prefix ~= PREFIX then
|
|
return
|
|
end
|
|
if type(text) == "string" then
|
|
local t = text:sub(1, 1)
|
|
state.stats.rxType[t] = (state.stats.rxType[t] or 0) + 1
|
|
end
|
|
Log("<-", tostring(channel), tostring(sender), tostring(text))
|
|
-- Probe: "E|<Nummer>" mit unserer Sitzungsnummer. Kommt sie zurueck, ist der Absender wir selbst
|
|
-- (so erfahren wir, unter welchem Namen uns die anderen sehen, auch mit Nachnamen)
|
|
if type(text) == "string" and text:sub(1, 2) == "E|" then
|
|
if text == "E|" .. SESSION_NONCE and type(sender) == "string" then
|
|
local c = NameCore(sender)
|
|
if c ~= "" and not learnedSelf[c] then
|
|
learnedSelf[c] = true
|
|
Log("ii", "eigener Name erkannt", sender, "")
|
|
end
|
|
end
|
|
return
|
|
end
|
|
self:HandleMessage(channel, sender, text)
|
|
end
|
|
|
|
local groupHelloAt = 0
|
|
function Sync:OnGroupChanged()
|
|
if not self:IsEnabled() or Now() - groupHelloAt < 120 then
|
|
return
|
|
end
|
|
groupHelloAt = Now()
|
|
C_Timer.After(8, function()
|
|
Sync:SendHello()
|
|
end)
|
|
end
|
|
|
|
function Sync:Start()
|
|
if self.started or not self:IsEnabled() then
|
|
return
|
|
end
|
|
self.started = true
|
|
generation = generation + 1
|
|
local gen = generation
|
|
local api = _G.C_ChatInfo
|
|
if api and api.RegisterAddonMessagePrefix then
|
|
pcall(api.RegisterAddonMessagePrefix, PREFIX)
|
|
end
|
|
FW:RegisterEvent("CHAT_MSG_ADDON", self.OnAddonMessage, self)
|
|
FW:RegisterEvent("GROUP_ROSTER_UPDATE", self.OnGroupChanged, self)
|
|
FW:On("CODEX_CHANGED", function(_, kind, id)
|
|
Sync:OnChanged(kind, id)
|
|
end, self)
|
|
Loop(gen, SEND_INTERVAL, function()
|
|
Sync:Pump()
|
|
local now = Now()
|
|
for key, buf in pairs(state.buffers) do
|
|
if now - buf.t > 60 then
|
|
state.buffers[key] = nil
|
|
end
|
|
end
|
|
end)
|
|
Loop(gen, ANNOUNCE_EVERY, function()
|
|
Sync:FlushAnnounce()
|
|
end)
|
|
Loop(gen, SCAN_EVERY, function()
|
|
Sync:ScanOwn()
|
|
end)
|
|
-- erstes Hallo nach dem Einloggen, verteilt, damit nicht alle gleichzeitig rufen
|
|
C_Timer.After(5, function()
|
|
if Sync.started and gen == generation then
|
|
Sync:JoinGlobal()
|
|
end
|
|
end)
|
|
C_Timer.After(10 + math.random() * 15, function()
|
|
if Sync.started and gen == generation then
|
|
Sync:JoinGlobal()
|
|
Share.Scan(true)
|
|
Sync:SendHello()
|
|
Loop(gen, 2700, function()
|
|
Sync:SendHello()
|
|
end)
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Sync:Stop()
|
|
self.started = false
|
|
generation = generation + 1
|
|
FW:UnregisterEvent("CHAT_MSG_ADDON", self)
|
|
FW:UnregisterEvent("GROUP_ROSTER_UPDATE", self)
|
|
FW:Off("CODEX_CHANGED", self)
|
|
state.queue = {}
|
|
state.pending = {}
|
|
end
|
|
|
|
function Sync:SetLog(on)
|
|
state.log = on and true or false
|
|
FW:Print("Codex-Protokoll: " .. (state.log and "an (-> gesendet, <- empfangen)" or "aus"))
|
|
FW:Fire("CODEX_SYNC_CHANGED")
|
|
end
|
|
|
|
function Sync:HeardCount()
|
|
local n, now = 0, Now()
|
|
for _, p in pairs(state.peers) do
|
|
if now - p.t < 3600 then
|
|
n = n + 1
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
function Sync:Debug()
|
|
local st = state.stats
|
|
local parts = {}
|
|
for ch, n in pairs(st.byChannel) do
|
|
parts[#parts + 1] = ch .. "=" .. n
|
|
end
|
|
table.sort(parts)
|
|
local dist = {}
|
|
for _, kind in ipairs(KINDS) do
|
|
dist[#dist + 1] = kind .. "=" .. tostring((self:Distribution(kind)) or "-")
|
|
end
|
|
local own = Share.Summary()
|
|
local ver = (C_AddOns and C_AddOns.GetAddOnMetadata and C_AddOns.GetAddOnMetadata("Freewood-OctoWoW-Codex", "Version")) or "?"
|
|
local me = {}
|
|
for c in pairs(SelfCores()) do me[#me + 1] = c end
|
|
table.sort(me)
|
|
FW:Print(string.format("Codex %s, ich bin: %s (erkannt als: %s)", tostring(ver), tostring(UnitName and UnitName("player")), table.concat(me, ", ")))
|
|
FW:Print(string.format("Codex-Teilen: %s, gestartet %s, Kanal %s, eigene Datensaetze %d",
|
|
self:IsEnabled() and "an" or "aus", tostring(self.started), tostring(state.status.global), own or 0))
|
|
FW:Print("Wege: " .. table.concat(dist, " "))
|
|
FW:Print(string.format("Gesendet %d (gebremst %d, fehlgeschlagen %d, Abstand %.1fs, letzte Antwort %s), empfangen %d (%s), Hallo aus %d, Hallo ein %d, Anfragen %d, abgelehnt %d, Warteschlange %d",
|
|
st.tx, st.throttled, st.fail, state.interval, st.lastResult, st.rx, table.concat(parts, " "), st.hello, st.helloIn, st.queries, st.rejected, #state.queue))
|
|
local function Types(t)
|
|
local out = {}
|
|
for _, k in ipairs({ "H", "A", "Q", "V", "R", "B" }) do
|
|
out[#out + 1] = k .. "=" .. (t[k] or 0)
|
|
end
|
|
return table.concat(out, " ")
|
|
end
|
|
FW:Print("Gesendet nach Art: " .. Types(st.txType))
|
|
FW:Print("Empfangen nach Art: " .. Types(st.rxType))
|
|
FW:Print(string.format("Bekannte Spieler (letzte Stunde): %d", self:HeardCount()))
|
|
local function Letters(by)
|
|
local o = {}
|
|
for _, l in ipairs({ "u", "o", "q", "i", "x", "t", "p", "z", "g", "h", "f" }) do
|
|
if by[l] then o[#o + 1] = l .. "=" .. by[l] end
|
|
end
|
|
return #o > 0 and table.concat(o, " ") or "-"
|
|
end
|
|
FW:Print("Eigene Datensaetze nach Art: " .. Letters(Share.OwnInfo()))
|
|
for _, r in ipairs(Share.RemoteInfo()) do
|
|
FW:Print(string.format("Von %s: Dev %s, normal %d, Wege/Punkte %d | %s", tostring(r.name), r.dev and "ja" or "NEIN", r.n, r.np, Letters(r.by)))
|
|
end
|
|
if Codex.Spots then
|
|
FW:Print(string.format("Ortspunkte sichtbar: %d (davon empfangen: %d), Verbindungen empfangen: %d",
|
|
Codex.Spots.Count(), #Share.RemoteRecords("h"), #Share.RemoteRecords("g")))
|
|
end
|
|
end
|
|
|
|
function Sync:Stats()
|
|
local store = Store()
|
|
local senders, records = Share.RemoteStats()
|
|
return {
|
|
sent = store.sent or 0,
|
|
received = store.received or 0,
|
|
lastReceived = store.lastReceived,
|
|
queue = #state.queue,
|
|
global = state.status.global,
|
|
senders = senders,
|
|
records = records,
|
|
heard = Sync:HeardCount(),
|
|
}
|
|
end
|
|
|
|
-- Wird vom Codex nach dem Laden der Daten aufgerufen. Ist der Schalter gesetzt, verbindet sich das
|
|
-- Teilen von selbst (auch nach /reload), ohne dass man den Haken neu setzen muss.
|
|
function Sync:Init()
|
|
if self:IsEnabled() then
|
|
self:Start()
|
|
end
|
|
end
|
|
|
|
function Sync:OnEnteringWorld()
|
|
if self:IsEnabled() and C_Timer and C_Timer.After then
|
|
C_Timer.After(30, function() Sync:SendTrust() end)
|
|
end
|
|
if self:IsEnabled() then
|
|
if not self.started then
|
|
self:Start()
|
|
elseif state.status.global ~= "ok" then
|
|
C_Timer.After(5, function()
|
|
if Sync.started then
|
|
Sync:JoinGlobal()
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
FW:RegisterEvent("PLAYER_ENTERING_WORLD", Sync.OnEnteringWorld, Sync)
|