-- Freewood-OctoWoW-Codex: Datenschicht fuers Teilen -- -- * Eigene Datensaetze (Kreatur, Objekt, Quest, Erfahrungstabelle) werden bereinigt und mit einem -- Aenderungsstand (upd) versehen. Der Stand liegt in db.sync.stamps und wird aus dem Inhalt -- berechnet, so muss die Erfassung selbst nichts davon wissen. -- * Empfangene Datensaetze liegen getrennt je Absender in db.remote und werden nie in die -- eigenen Daten geschrieben. Ein neuer Stand desselben Absenders ersetzt den alten (idempotent). -- * Share.View() liefert die Sicht fuer die Anzeige: eigene und fremde Daten zusammengerechnet. -- Ohne fremde Daten ist das einfach Codex.db. -- -- Schluessel eines Datensatzes: "u123" Kreatur, "o45" Objekt, "q678" Quest, "x0" Erfahrung, "t0" Kampfdauer. local ADDON, NS = ... local FW = _G.Freewood local Codex = NS and NS.Codex if not FW or not Codex then return end local Share = {} Codex.Share = Share NS.Share = Share local GRID = Codex.GRID or 200 local MAX_SENDERS = 12 local MAX_RECORDS = 2500 -- je Absender local MAX_PATH_RECORDS = 60000 -- Wegenetz-Bloecke je Absender (nur der Stand wird gemerkt) local MAX_CELLS_SHARED = 120 local MAX_DROPS = 80 local MAX_NAMES = 6000 local SELF_IMPORT = "~import" local KIND_OF = { u = "units", o = "objects", q = "quests", i = "ied" } local LETTER_OF = { unit = "u", object = "o", quest = "q", item = "i" } local CLASSES = { elite = true, rare = true, rareelite = true, worldboss = true } local XP_CLASSES = { normal = true, elite = true, rare = true, rareelite = true, worldboss = true } ------------------------------------------------------------------------ -- Pruefer (alles, was von aussen kommt, geht hier durch) ------------------------------------------------------------------------ local function Text(v, max) if type(v) ~= "string" then return nil end v = v:gsub("|[cC]%x%x%x%x%x%x%x%x", ""):gsub("|[rR]", ""):gsub("|T.-|t", ""):gsub("|H.-|h", "") v = v:gsub("|", ""):gsub("%c", " ") v = v:gsub("^%s+", ""):gsub("%s+$", "") if v == "" then return nil end if #v > max then v = v:sub(1, max) end return v end local function Int(v, lo, hi) if type(v) ~= "number" or v ~= v or v == math.huge or v == -math.huge then return nil end v = math.floor(v) if v < lo or v > hi then return nil end return v end local function Frac(v) if type(v) ~= "number" or v ~= v or v < 0 or v > 1 then return nil end return math.floor(v * 10000 + 0.5) / 10000 end local function Sample(s) if type(s) ~= "table" then return nil end local n, sum = Int(s.n, 1, 1000000), Int(s.sum, 0, 10000000000) local lo, hi = Int(s.min, 0, 10000000), Int(s.max, 0, 10000000) if not (n and sum and lo and hi) or lo > hi then return nil end if sum < lo * n - 1 or sum > hi * n + 1 then return nil end return { n = n, sum = sum, min = lo, max = hi } end -- Tabelle { [schluessel] = zahl } mit Obergrenze; die groessten Werte bleiben local function TopCounts(src, keyFn, valLo, valHi, limit) local list = {} local seen = 0 for k, v in pairs(src) do seen = seen + 1 if seen > 20000 then break end local key, val = keyFn(k), Int(v, valLo, valHi) if key and val then list[#list + 1] = { key, val } end end table.sort(list, function(a, b) if a[2] ~= b[2] then return a[2] > b[2] end return a[1] < b[1] end) local out, count = {}, 0 for _, e in ipairs(list) do if count >= limit then break end out[e[1]] = e[2] count = count + 1 end return out, count end local function DropTable(src, k) if type(src) ~= "table" or not k then return nil end local list = {} local seen = 0 for id, d in pairs(src) do seen = seen + 1 if seen > 2000 then break end local iid = Int(id, 1, 10000000) if iid and type(d) == "table" then local n = Int(d.n, 1, 1000000) if n and n <= k then local q = Int(d.q, n, 100000000) or n local qk = d.qi and Int(d.qk, n, k) or nil list[#list + 1] = { id = iid, n = n, q = q, qi = d.qi and 1 or nil, qk = qk } end end end table.sort(list, function(a, b) if a.n ~= b.n then return a.n > b.n end return a.id < b.id end) local out, count = {}, 0 for _, e in ipairs(list) do if count >= MAX_DROPS then break end out[e.id] = { n = e.n, q = e.q, qi = e.qi, qk = e.qk } count = count + 1 end if count == 0 then return nil end return out end local function Ref(r) if type(r) ~= "table" then return nil end local k = r.k if k ~= "npc" and k ~= "object" and k ~= "other" then return nil end return { k = k, id = Int(r.id, 0, 100000000), n = Text(r.n, 60), m = Int(r.m, 0, 100000), x = Frac(r.x), y = Frac(r.y), } end local function RewardList(list) if type(list) ~= "table" then return nil end local out = {} for i = 1, math.min(#list, 12) do local e = list[i] local id = type(e) == "table" and Int(e.id, 1, 10000000) if id then out[#out + 1] = { id = id, c = Int(e.c, 1, 1000) or 1 } end end return #out > 0 and out or nil end local function SrcKey(k) if type(k) == "string" and k:match("^[uo]%d+$") and #k <= 12 then return k end return nil end local function ObjectiveList(list) if type(list) ~= "table" then return nil end local out = {} for i = 1, math.min(#list, 12) do local o = list[i] if type(o) == "table" then local ty = Text(o.ty, 16) local e = { ty = ty and ty:lower():gsub("[^a-z]", "") or nil, name = Text(o.name, 80), txt = Text(o.txt, 100), req = Int(o.req, 0, 100000), item = Int(o.item, 1, 10000000), } if e.ty == "" then e.ty = nil end if type(o.ids) == "table" then e.ids = TopCounts(o.ids, function(k) return Int(k, 1, 100000000) end, 1, 1000000, 12) if next(e.ids) == nil then e.ids = nil end end out[i] = e else out[i] = {} end end return #out > 0 and out or nil end local function QuestItems(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for item, sources in pairs(src) do local iid = Int(item, 1, 10000000) if iid and type(sources) == "table" and count < 12 then local s = TopCounts(sources, SrcKey, 1, 1000000, 12) if next(s) ~= nil then out[iid] = s count = count + 1 end end end return count > 0 and out or nil end local function XPTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for pl, byMl in pairs(src) do local p = Int(pl, 1, 80) if p and type(byMl) == "table" then for ml, byCls in pairs(byMl) do local m = Int(ml, 1, 130) if m and type(byCls) == "table" then for cls, s in pairs(byCls) do if XP_CLASSES[cls] and count < 400 then local sm = Sample(s) if sm then out[p] = out[p] or {} out[p][m] = out[p][m] or {} out[p][m][cls] = sm count = count + 1 end end end end end end end return count > 0 and out or nil end -- Kampfdauer: ttk[Klasse][Spielerstufe][Gegnerstufe - Spielerstufe] = { n, sum, min, max } (Zehntelsekunden) local function TTKTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for cls, byPl in pairs(src) do if type(cls) == "string" and cls:match("^%u+$") and #cls <= 16 and type(byPl) == "table" then for pl, byDelta in pairs(byPl) do local p = Int(pl, 1, 80) if p and type(byDelta) == "table" then for delta, s in pairs(byDelta) do local d = Int(delta, -5, 5) local sm = d and count < 1200 and Sample(s) if sm then out[cls] = out[cls] or {} out[cls][p] = out[cls][p] or {} out[cls][p][d] = sm count = count + 1 end end end end end end return count > 0 and out or nil end -- Kampfdauer einer Kreatur: tt[Klasse][Spielerstufe] = { n, sum, min, max } (Zehntelsekunden) local function UnitTTK(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for cls, byPl in pairs(src) do if type(cls) == "string" and cls:match("^%u+$") and #cls <= 16 and type(byPl) == "table" then for pl, s in pairs(byPl) do local p = Int(pl, 1, 80) local sm = p and count < 60 and Sample(s) if sm then out[cls] = out[cls] or {} out[cls][p] = sm count = count + 1 end end end end return count > 0 and out or nil end -- Rollen: nur bekannte Namen, Zaehler begrenzt local ROLE_KEYS = { vendor = true, repair = true, flight = true, innkeeper = true, stablemaster = true, spirithealer = true, auctioneer = true, battlemaster = true, banker = true, classtrainer = true, proftrainer = true, mailbox = true, } local MAX_WARES_SHARED = 90 local MAX_TRAIN_SHARED = 100 local MAX_DEST_SHARED = 40 local MAX_PRICES_SHARED = 4 local function RoleTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for role, n in pairs(src) do local v = type(role) == "string" and ROLE_KEYS[role] and Int(n, 1, 100000) if v then out[role] = v count = count + 1 end end return count > 0 and out or nil end -- { [preis in Kupfer] = Anzahl }, hoechstens MAX_PRICES_SHARED (haeufigste) local function PriceMap(src) if type(src) ~= "table" then return nil end local out, count = TopCounts(src, function(k) return Int(k, 0, 2000000000) end, 1, 100000, MAX_PRICES_SHARED) return count > 0 and out or nil end local function SortedKeys(src, keyFn) local list = {} local seen = 0 for k, v in pairs(src) do seen = seen + 1 if seen > 5000 then break end local key = keyFn(k) if key ~= nil and type(v) == "table" then list[#list + 1] = { key, v } end end table.sort(list, function(a, b) return a[1] < b[1] end) return list end local function WareTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for _, e in ipairs(SortedKeys(src, function(k) return Int(k, 1, 10000000) end)) do if count >= MAX_WARES_SHARED then break end local pr = PriceMap(e[2].pr) if pr then out[e[1]] = { pr = pr, st = Int(e[2].st, 2, 1000), lim = e[2].lim and 1 or nil, ec = e[2].ec and 1 or nil } count = count + 1 end end return count > 0 and out or nil end local function TrainTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for _, e in ipairs(SortedKeys(src, function(k) return Text(k, 100) end)) do if count >= MAX_TRAIN_SHARED then break end local r = e[2] local nm = Text(r.nm, 60) local pr = PriceMap(r.pr) if nm and pr then out[e[1]] = { nm = nm, sp = Int(r.sp, 1, 10000000), rk = Text(r.rk, 30), lv = Int(r.lv, 1, 130), sn = Text(r.sn, 40), sr = Int(r.sr, 1, 1000), pr = pr } count = count + 1 end end return count > 0 and out or nil end local function FlightTable(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for _, e in ipairs(SortedKeys(src, function(k) return Text(k, 60) end)) do if count >= MAX_DEST_SHARED then break end local pr = PriceMap(e[2]) if pr then out[e[1]] = pr count = count + 1 end end return count > 0 and out or nil end local function PerLevel(src, depth) -- xm[spielerstufe][gegnerstufe] bzw. xp[spielerstufe] if type(src) ~= "table" then return nil end local out, count = {}, 0 for pl, v in pairs(src) do local p = Int(pl, 1, 80) if p and type(v) == "table" then if depth == 1 then local sm = Sample(v) if sm and count < 40 then out[p] = sm count = count + 1 end else for ml, s in pairs(v) do local m = Int(ml, 1, 130) local sm = m and Sample(s) if sm and count < 40 then out[p] = out[p] or {} out[p][m] = sm count = count + 1 end end end end end return count > 0 and out or nil end -- DEV-Funktionen: Namen und "fehlerhaft"-Markierung (bad: 1 = fehlerhaft, 0 = Markierung aufgehoben) local function BadFlag(v) if v == true or v == 1 then return 1 end if v == false or v == 0 then return 0 end return nil end local function CanEditNames() return Codex.Trust ~= nil and Codex.Trust.CanEdit() end local BUILTIN = "~forever" -- die mitgelieferten Startdaten (Freewood-OctoWoW-ForeverData): wie ein Berechtigter, aber ganz unten local function IsDevSender(name) if name == BUILTIN then return true end return Codex.Trust ~= nil and Codex.Trust.IsDev(name) end -- Eigenschaften (Zonen, Dungeon, Klasse, Stufen) von Berechtigten; auf der Leitung ist dg 1 / 0 local function WireBool(v) if v == true or v == 1 then return true end if v == false or v == 0 then return false end return nil end local function SanitizeEd(src, kind) if type(src) ~= "table" then return nil end local probe = { z = src.z, dg = WireBool(src.dg), cl = src.cl, l1 = src.l1, l2 = src.l2, pre = src.pre, key = src.key, nx = src.nx, bf = src.bf, af = src.af, ql = src.ql, tx = src.tx, si = src.si, qk = src.qk, qd = src.qd, qb = src.qb, qr = src.qr } local ed = Codex.CleanEd(probe, kind) if not ed then return nil end if ed.dg ~= nil then ed.dg = ed.dg and 1 or 0 end return ed end -- Aus dem Datensatz eines anderen Spielers in die eigene Form (dg als Wahrheitswert) local function EdFromWire(ed) if type(ed) ~= "table" then return nil end local out = Codex.CopyEd(ed) out.dg = WireBool(ed.dg) return out end -- Texte je Teilfeld fuer das Protokoll local function EdFields(ed) local f = {} if type(ed) ~= "table" then return f end if ed.z then f.ez = table.concat(ed.z, ",") end local dg = WireBool(ed.dg) if dg ~= nil then f.edg = dg and "1" or "0" end f.ecl = ed.cl if ed.l1 then f.el = ed.l1 .. "-" .. (ed.l2 or ed.l1) end if ed.pre then f.epre = table.concat(ed.pre, ",") end if ed.key then f.ekey = tostring(ed.key) end if ed.nx then f.enx = table.concat(ed.nx, ",") end if ed.bf then f.ebf = table.concat(ed.bf, ",") end if ed.af then f.eaf = table.concat(ed.af, ",") end if ed.ql then f.eql = tostring(ed.ql) end if ed.tx then f.etx = ed.tx end if ed.si then f.esi = tostring(ed.si) end if ed.qk then f.eqk = table.concat(ed.qk, ",") end if ed.qd then f.eqd = table.concat(ed.qd, ",") end if ed.qb then f.eqb = table.concat(ed.qb, ",") end if ed.qr then f.eqr = table.concat(ed.qr, ",") end return f end Share.EdFields = EdFields -- alle Teilfelder der Eigenschaften (fuer Vorschau und Protokoll) Share.ED_FIELDS = { "ez", "edg", "ecl", "el", "epre", "ekey", "enx", "ebf", "eaf", "eql", "etx", "esi", "eqk", "eqd", "eqb", "eqr" } -- Instanzen, in denen eine Kreatur / ein Objekt gesehen wurde: { [instanceID] = Anzahl }, hoechstens 3 local function SanitizeIz(src) if type(src) ~= "table" then return nil end local out, count = {}, 0 for id, n in pairs(src) do local iid, cnt = Int(id, 1, 100000), Int(n, 1, 1000000) if iid and cnt and count < 3 then out[iid] = cnt count = count + 1 end end return count > 0 and out or nil end local function SanitizeEntity(rec, isObject) local out = {} out.n = Text(rec.n, 60) out.cde, out.cen = Text(rec.cde, 60), Text(rec.cen, 60) out.bad = BadFlag(rec.bad) out.ed = SanitizeEd(rec.ed, isObject and "objects" or "units") out.iz = SanitizeIz(rec.iz) if not isObject then out.l1, out.l2 = Int(rec.l1, 1, 130), Int(rec.l2, 1, 130) if out.l1 and out.l2 and out.l1 > out.l2 then out.l1, out.l2 = out.l2, out.l1 end out.c = CLASSES[rec.c] and rec.c or nil out.f = Text(rec.f, 40) end out.k = Int(rec.k, 0, 1000000) local k = out.k or 0 if k > 0 then out.coinN = Int(rec.coinN, 0, k) out.coin = out.coinN and out.coinN > 0 and Int(rec.coin, 0, 1000000000000) or nil if not out.coin then out.coinN = nil end end if type(rec.cells) == "table" then local cells, count = TopCounts(rec.cells, function(key) return Int(key, 0, 1000000000) end, 1, 1000000, MAX_CELLS_SHARED) if count > 0 then out.cells = cells end end out.d = DropTable(rec.d, k) if type(rec.alt) == "table" then local ak = Int(rec.alt.k, 1, 1000000) if ak then out.alt = { k = ak, d = DropTable(rec.alt.d, ak) } end end out.ro = RoleTable(rec.ro) if not isObject then out.xm = PerLevel(rec.xm, 2) out.tt = UnitTTK(rec.tt) out.ti = Text(rec.ti, 40) out.wa = WareTable(rec.wa) out.tr = TrainTable(rec.tr) if type(rec.trc) == "string" and Codex.CLASS_TOKENS and Codex.CLASS_TOKENS[rec.trc] then out.trc = rec.trc end out.fl = FlightTable(rec.fl) out.fn = Text(rec.fn, 60) out.ct = (type(rec.ct) == "string" and Codex.CT_CODES and Codex.CT_CODES[rec.ct]) and rec.ct or nil out.ne = Text(rec.ne, 60) end if type(rec.nm) == "table" then local nm, count = {}, 0 local function Wanted(id) return (out.d and out.d[id]) or (out.alt and out.alt.d and out.alt.d[id]) end for id, name in pairs(rec.nm) do local iid = Int(id, 1, 10000000) local t = iid and Wanted(iid) and Text(name, 80) if t and count < MAX_DROPS then nm[iid] = t count = count + 1 end end if count > 0 then out.nm = nm end end if not (out.n or (out.k and out.k > 0) or out.cells or out.iz or out.cde or out.cen or out.bad or out.ed or out.tt or out.xm or out.ro or out.wa or out.tr or out.fl) then return nil end return out end -- Items: nur die Eigenschaften von Berechtigten local function SanitizeItem(rec, id) local out = { ed = SanitizeEd(rec.ed, (id and id >= Codex.ZONE_BASE) and "zones" or "items") } if not out.ed then return nil end return out end local QTEXT_CAP = { d = 900, o = 400, p = 400, r = 600 } local function QuestTexts(tx) if type(tx) ~= "table" then return nil end local out, any = {}, false for f, cap in pairs(QTEXT_CAP) do local v = Text(tx[f], cap) if v and #v >= 3 then out[f] = v any = true end end return any and out or nil end local function SanitizeQuest(rec) local out = {} out.t = Text(rec.t, 100) if not out.t then return nil end out.cde, out.cen = Text(rec.cde, 100), Text(rec.cen, 100) out.bad = BadFlag(rec.bad) out.ed = SanitizeEd(rec.ed, "quests") out.lvl, out.pl = Int(rec.lvl, 0, 130), Int(rec.pl, 0, 130) out.zone = Int(rec.zone, 0, 100000) out.give, out.turn = Ref(rec.give), Ref(rec.turn) out.si = Int(rec.si, 1, 100000000) -- Kette und Voraussetzungen: Vorquests (jede genuegt), Mindeststufe, Volks- und Klassenmaske if type(rec.pre) == "table" then local pre = {} for i = 1, math.min(#rec.pre, 8) do local v = Int(rec.pre[i], 1, 100000000) if v then pre[#pre + 1] = v end end if #pre > 0 then out.pre = pre end end out.mn, out.rc, out.cl = Int(rec.mn, 1, 130), Int(rec.rc, 1, 4294967295), Int(rec.cl, 1, 4294967295) out.ev = Int(rec.ev, 1, 255) -- Event-Nummer (Weltevent): nur Event-Quests plant der Levelplaner nur auf Wunsch -- Beobachtungen zur Verfuegbarkeit: pi (Kandidat -> Anzahl), po (Beobachtungen), mi (Stufenbereich) if type(rec.pi) == "table" then local pi, count = {}, 0 for k, v in pairs(rec.pi) do local id, n = Int(k, 1, 100000000), Int(v, 1, 100000) if id and n and count < 8 then pi[id] = n count = count + 1 end end local po = Int(rec.po, 1, 100000) if count > 0 and po then out.pi, out.po = pi, po end end if type(rec.mi) == "table" then local lo, hi, n = Int(rec.mi.lo, 0, 130), Int(rec.mi.hi, 1, 130), Int(rec.mi.n, 1, 100000) if lo and hi and n and hi > lo then out.mi = { lo = lo, hi = hi, n = n } end end out.xp, out.money = Int(rec.xp, 0, 10000000), Int(rec.money, 0, 1000000000) if type(rec.rep) == "table" then local rep, count = {}, 0 for name, v in pairs(rec.rep) do local nm, val = Text(name, 60), Int(v, -100000, 100000) if nm and val and count < 8 then rep[nm] = val count = count + 1 end end if count > 0 then out.rep = rep end end out.fixed, out.choice = RewardList(rec.fixed), RewardList(rec.choice) out.tm, out.tr, out.ta = Sample(rec.tm), Sample(rec.tr), Sample(rec.ta) out.obj = ObjectiveList(rec.obj) out.qi = QuestItems(rec.qi) out.tx = QuestTexts(rec.tx) return out end local function SanitizeXPRecord(rec) local xp = XPTable(rec.xp) if not xp then return nil end return { xp = xp } end local function SanitizeTTKRecord(rec) local tk = TTKTable(rec.tk) if not tk then return nil end return { tk = tk } end -- Verkehrsverbindung (Schiff, Zeppelin, ...): von Berechtigten erfasst local function SanitizeTransportRecord(rec) local T = Codex.Transport local kindOk = false for _, k in ipairs(T and T.KINDS or {}) do if rec.k == k then kindOk = true end end if not kindOk then return nil end local function Spot(p) if type(p) ~= "table" then return nil end local m, x, y = Int(p.m, -1000000, 1000000), Frac(p.x), Frac(p.y) if not (m and x and y) then return nil end return { m = m, x = x, y = y } end local a, b = Spot(rec.a), Spot(rec.b) local t = Int(rec.t, 1, 7200) if not (a and b and t) then return nil end local out = { k = rec.k, t = t, a = a, b = b, two = rec.two ~= false and true or false } if rec.f == "A" or rec.f == "H" then out.f = rec.f end out.n = Text(rec.n, 40) if rec.del == true then out.del = true end return out end -- Ortspunkt (Instanz-Eingang, Berg-Aufstieg, ...): von Berechtigten erfasst local function SanitizeSpotRecord(rec) local S = Codex.Spots local kindOk = false for _, k in ipairs(S and S.KINDS or {}) do if rec.k == k then kindOk = true end end if not kindOk or type(rec.p) ~= "table" then return nil end local m, x, y = Int(rec.p.m, -1000000, 1000000), Frac(rec.p.x), Frac(rec.p.y) if not (m and x and y) then return nil end local out = { k = rec.k, p = { m = m, x = x, y = y }, q = {} } if type(rec.q) == "table" then local seen = {} for _, v in ipairs(rec.q) do local id = Int(v, 1, 1000000) if id and not seen[id] and #out.q < (S and S.MAX_QUESTS or 20) then seen[id] = true out.q[#out.q + 1] = id end end table.sort(out.q) end out.am = Int(rec.am, -1000000, 1000000) if rec.f == "A" or rec.f == "H" then out.f = rec.f end out.n = Text(rec.n, 40) if rec.del == true then out.del = true end if not out.am and #out.q == 0 and not out.del then return nil end return out end -- Flugroute: Dauer, Preis, Strecke (Weltpunkte) und die Namen der beiden Flugpunkte local function SanitizeTaxiNode(n) if type(n) ~= "table" then return nil end local out = { n = Text(n.n, 60), a = Int(n.a, -1000000, 1000000), b = Int(n.b, -1000000, 1000000) } local m, x, y = Int(n.m, 1, 1000000), Frac(n.x), Frac(n.y) if m and x and y then out.m, out.x, out.y = m, x, y end return out end local function SanitizeTaxiRecord(rec) local t, c = Int(rec.t, 5, 7200), Int(rec.c, 0, 100000000) if not t or not c or type(rec.p) ~= "table" then return nil end local cnt = #rec.p if cnt < 4 or cnt > 80 or cnt % 2 ~= 0 then return nil end local p = {} for i = 1, cnt do local v = Int(rec.p[i], -1000000, 1000000) if not v then return nil end p[i] = v end local A, B = SanitizeTaxiNode(rec.A), SanitizeTaxiNode(rec.B) if not A or not B then return nil end return { t = t, c = c, n = Int(rec.n or 1, 1, 100000) or 1, p = p, A = A, B = B } end -- Wegenetz-Block: Text "Zelle.Maske,..." wird geprueft und in Normalform gebracht local function SanitizePathRecord(rec) local P = Codex.Path local cells = P and P.ParseChunk(rec.c) if not cells then return nil end return { c = P.ChunkText(cells) } end local function SanitizeLinkRecord(rec) local P = Codex.Path local list = P and P.ParseLinks(rec.l) if not list then return nil end return { l = list } end -- key: "u123"; rec: unbekannte Eingabe. Rueckgabe: bereinigter Datensatz { y, id, upd, ... } oder nil function Share.Sanitize(key, rec, upd) if type(key) ~= "string" or type(rec) ~= "table" then return nil end local letter, num = key:match("^([uoqixtpzghf])(%d+)$") local id = num and Int(tonumber(num), 0, (letter == "f") and 100000000000000 or (letter == "p") and 65536000000 or 100000000) if not letter or not id then return nil end upd = Int(upd, 1, 4000000000) if not upd then return nil end local out if letter == "u" then out = SanitizeEntity(rec, false) elseif letter == "o" then out = SanitizeEntity(rec, true) elseif letter == "q" then out = SanitizeQuest(rec) elseif letter == "i" then out = SanitizeItem(rec, id) elseif letter == "t" then out = SanitizeTTKRecord(rec) elseif letter == "p" then out = SanitizePathRecord(rec) elseif letter == "z" then out = SanitizeLinkRecord(rec) elseif letter == "g" then out = SanitizeTransportRecord(rec) elseif letter == "h" then out = SanitizeSpotRecord(rec) elseif letter == "f" then out = SanitizeTaxiRecord(rec) else out = SanitizeXPRecord(rec) end if not out then return nil end out.y, out.id, out.upd = letter, id, upd return out end ------------------------------------------------------------------------ -- Eigene Datensaetze und ihr Stand ------------------------------------------------------------------------ local function Store() local db = Codex.db db.sync = db.sync or {} db.sync.stamps = db.sync.stamps or {} db.sync.sent = db.sync.sent or 0 db.sync.received = db.sync.received or 0 return db.sync end Share.Store = Store local function Sum(t) local s = 0 for _, v in pairs(t) do s = s + (type(v) == "table" and (v.n or 0) or v) end return s end local function BucketSig(b) local s = (b.k or 0) * 17 for _, d in pairs(b.d or {}) do s = s + (d.n or 0) * 13 + 1 end return s end local function StrSig(str) if type(str) ~= "string" then return 0 end local h = #str for i = 1, math.min(#str, 100) do h = (h * 31 + str:byte(i)) % 1000003 end return h end local function EdSig(ed) if type(ed) ~= "table" then return 0 end local h = 0 for k, v in pairs(EdFields(ed)) do h = h + StrSig(k) * 3 + StrSig(tostring(v)) * 59 end return h end local function Signature(letter, e) local s if letter == "i" then return (1 + EdSig(e.ed)) % 2147483647 elseif letter == "q" then s = (e.seen or 0) * 3 + (e.done or 0) * 5 + #(e.t or "") * 7 + (e.xp or 0) + (e.money or 0) s = s + #(e.fixed or {}) * 11 + #(e.choice or {}) * 13 + (e.partial and 1 or 0) s = s + (e.tm and e.tm.n or 0) * 23 + (e.tr and e.tr.n or 0) * 29 + (e.ta and e.ta.n or 0) * 31 for i, o in ipairs(e.obj or {}) do s = s + #(o.name or "") * (i + 1) + (o.item or 0) + Sum(o.ids or {}) * 3 + 1 end for item, srcs in pairs(e.qi or {}) do s = s + item % 997 + Sum(srcs) end if e.give then s = s + (e.give.id or 0) end if e.turn then s = s + (e.turn.id or 0) end s = s + (e.si or 0) % 1019 for _, p in ipairs(e.pre or {}) do s = s + p % 997 * 3 + 1 end s = s + (e.mn or 0) * 5 + (e.rc or 0) % 1009 + (e.cl or 0) % 1013 if type(e.tx) == "table" then for _, v in pairs(e.tx) do s = s + #tostring(v) * 3 + 1 end end for p2, n2 in pairs(e.pi or {}) do s = s + p2 % 991 * n2 end s = s + (e.po or 0) * 3 + (e.mi and (e.mi.lo * 7 + e.mi.hi * 11 + (e.mi.n or 0)) or 0) if CanEditNames() then s = s + StrSig(e.cde) * 17 + StrSig(e.cen) * 19 + (BadFlag(e.bad) or 7) * 53 + EdSig(e.ed) end else s = BucketSig(e) + Sum(e.coinN and { e.coinN } or {}) * 5 + #(e.n or "") * 7 + (e.l1 or 0) + (e.l2 or 0) * 11 + (e.ct and #e.ct * 37 or 0) + (e.ne and #e.ne * 41 or 0) if CanEditNames() then s = s + StrSig(e.cde) * 17 + StrSig(e.cen) * 19 + (BadFlag(e.bad) or 7) * 53 + EdSig(e.ed) end s = s + Sum(e.iz or {}) * 61 for _ in pairs(e.cells or {}) do s = s + 1 end s = s + Sum(e.cells or {}) if e.alt then s = s + BucketSig(e.alt) * 3 end for _, byMl in pairs(e.xm or {}) do s = s + Sum(byMl) * 19 end for _, byPl in pairs(e.tt or {}) do for _, st in pairs(byPl) do s = s + (st.n or 0) * 37 + (st.sum or 0) % 101 end end s = s + Sum(e.ro or {}) * 41 + StrSig(e.ti) * 43 + StrSig(e.fn) * 47 + StrSig(e.trc) * 53 for id, w in pairs(e.wa or {}) do s = s + id % 991 + Sum(w.pr or {}) * 3 + (w.st or 0) for price in pairs(w.pr or {}) do s = s + price % 997 end end for key, r in pairs(e.tr or {}) do s = s + #key + (r.sp or 0) % 997 + (r.lv or 0) * 5 + (r.sr or 0) + Sum(r.pr or {}) * 3 for price in pairs(r.pr or {}) do s = s + price % 997 end end for dest, pr in pairs(e.fl or {}) do s = s + #dest * 7 + Sum(pr) * 3 for price in pairs(pr) do s = s + price % 997 end end end return s % 2147483647 end local function IsShareable(letter, e) if type(e) ~= "table" then return false end if letter == "q" then return type(e.t) == "string" and not e.loc -- eigene Quests des Creators bleiben bei dir end if letter == "i" then return CanEditNames() and e.ed ~= nil end -- auch ohne Namen (zum Beispiel in Instanzen gelootete Kreaturen); Namen kommen von anderen Spielern -- auch mit nur einem von Hand eingetragenen Namen -- (auch Kreaturen, bei denen nur Kampfdauer oder Erfahrung gemessen wurde) local named = CanEditNames() and (e.cde ~= nil or e.cen ~= nil or e.bad ~= nil or e.ed ~= nil) return ((e.k or 0) > 0) or e.cells ~= nil or e.iz ~= nil or named or e.tt ~= nil or e.xm ~= nil or e.ro ~= nil or e.wa ~= nil or e.tr ~= nil or e.fl ~= nil end Share.IsShareable = IsShareable local function XPSignature() local s, any = 0, false for _, byMl in pairs(Codex.db.xp or {}) do for _, byCls in pairs(byMl) do for _, sm in pairs(byCls) do s = s + (sm.n or 0) * 7 + 1 any = true end end end return any and s % 2147483647 or nil end local function TTKSignature() local s, any = 0, false for _, byPl in pairs(Codex.db.ttk or {}) do for _, byDelta in pairs(byPl) do for _, sm in pairs(byDelta) do s = s + (sm.n or 0) * 7 + 1 any = true end end end return any and s % 2147483647 or nil end local scanCache, scanAt = nil, 0 local firstScanDone = false local function Touch(stamps, key, sig, now) local s = stamps[key] if not s then stamps[key] = { sig = sig, upd = now } return true elseif s.sig ~= sig then s.sig = sig s.upd = math.max(now, (s.upd or 0) + 1) return true end return false end -- Alle eigenen Datensaetze durchgehen. Rueckgabe: Liste { key, upd }, Anzahl, Pruefwert, geaenderte Schluessel function Share.Scan(force) local now = time() if scanCache and not force and now - scanAt < 30 then return scanCache.list, scanCache.count, scanCache.hash, {} end local store = Store() local stamps = store.stamps local list, changed, hash = {}, {}, 0 for letter, field in pairs(KIND_OF) do for id, e in pairs(Codex.db[field]) do if type(id) == "number" and IsShareable(letter, e) then local key = letter .. id if Touch(stamps, key, Signature(letter, e), now) then changed[#changed + 1] = key end local upd = stamps[key].upd list[#list + 1] = { key = key, upd = upd } hash = (hash + (id % 100003) * 31 + upd % 1000003 + (letter:byte() * 7)) % 2147483647 end end end local xs = XPSignature() if xs then if Touch(stamps, "x0", xs, now) then changed[#changed + 1] = "x0" end list[#list + 1] = { key = "x0", upd = stamps.x0.upd } hash = (hash + stamps.x0.upd % 1000003 + 5) % 2147483647 end local ts = TTKSignature() if ts then if Touch(stamps, "t0", ts, now) then changed[#changed + 1] = "t0" end list[#list + 1] = { key = "t0", upd = stamps.t0.upd } hash = (hash + stamps.t0.upd % 1000003 + 11) % 2147483647 end for id, rec in pairs(Codex.db.transport or {}) do if type(id) == "number" and type(rec) == "table" and rec.upd then list[#list + 1] = { key = "g" .. id, upd = rec.upd } hash = (hash + id % 100003 * 17 + rec.upd % 1000003 + 103) % 2147483647 end end for id, rec in pairs(Codex.db.spots or {}) do if type(id) == "number" and type(rec) == "table" and rec.upd then list[#list + 1] = { key = "h" .. id, upd = rec.upd } hash = (hash + id % 100003 * 19 + rec.upd % 1000003 + 107) % 2147483647 end end if Codex.Taxi and Codex.Taxi.SyncList then for _, e in ipairs(Codex.Taxi.SyncList()) do list[#list + 1] = e hash = (hash + (tonumber(e.key:sub(2)) or 0) % 100003 * 23 + e.upd % 1000003 + 109) % 2147483647 end end if Codex.Path and Codex.Path.SyncList then for _, e in ipairs(Codex.Path.SyncList()) do list[#list + 1] = e hash = (hash + (tonumber(e.key:sub(2)) or 0) % 100003 * 13 + e.upd % 1000003 + e.key:byte() * 3) % 2147483647 end end scanCache = { list = list, count = #list, hash = hash } scanAt = now if not firstScanDone then firstScanDone = true changed = {} end return list, #list, hash, changed end function Share.Summary() local _, count, hash = Share.Scan(false) return count, hash end function Share.InvalidateScan() scanCache = nil end -- Stand eines einzelnen eigenen Datensatzes neu berechnen. Rueckgabe: key, upd, geaendert function Share.StampOne(kind, id) local letter = LETTER_OF[kind] if not letter or type(id) ~= "number" then return nil end local e = Codex.db[KIND_OF[letter]][id] if not IsShareable(letter, e) then return nil end local key = letter .. id local stamps = Store().stamps local changed = Touch(stamps, key, Signature(letter, e), time()) scanCache = nil return key, stamps[key].upd, changed end function Share.OwnUpd(key) local s = Store().stamps[key] return s and s.upd or nil end -- Verzeichnis: eigene Datensaetze mit Stand > since, aelteste zuerst function Share.Directory(since, limit) local list = Share.Scan(false) local out = {} for _, e in ipairs(list) do if e.upd > (since or 0) then out[#out + 1] = e end end table.sort(out, function(a, b) if a.upd ~= b.upd then return a.upd < b.upd end return a.key < b.key end) while #out > (limit or 400) do out[#out] = nil end return out end -- Eigener Datensatz zum Senden (bereinigt). Rueckgabe: record, upd function Share.OwnRecord(key) local letter, num = tostring(key):match("^([uoqixtpzghf])(%d+)$") if not letter then return nil end local id = tonumber(num) if letter == "g" then local rec = Codex.db.transport and Codex.db.transport[id] if type(rec) ~= "table" or not rec.upd then return nil end local out = Share.Sanitize(key, rec, rec.upd) return out, rec.upd end if letter == "h" then local rec = Codex.db.spots and Codex.db.spots[id] if type(rec) ~= "table" or not rec.upd then return nil end local out = Share.Sanitize(key, rec, rec.upd) return out, rec.upd end if letter == "f" then local T = Codex.Taxi local raw, upd = T and T.RouteRecord(id) if not raw then return nil end return Share.Sanitize(key, raw, upd), upd end if letter == "p" or letter == "z" then local P = Codex.Path if not P then return nil end local list = Share.Scan(false) local upd for _, e in ipairs(list) do if e.key == key then upd = e.upd break end end if not upd then return nil end local raw = letter == "p" and P.ChunkRecord(id) or P.LinkRecord() if not raw then return nil end local rec = Share.Sanitize(key, raw, upd) return rec, upd end local upd = Share.OwnUpd(key) if not upd then Share.Scan(true) upd = Share.OwnUpd(key) end if not upd then return nil end local raw if letter == "x" then raw = { xp = Codex.db.xp } elseif letter == "t" then raw = { tk = Codex.db.ttk } else local e = Codex.db[KIND_OF[letter]][id] if not IsShareable(letter, e) then return nil end raw = e if letter == "u" or letter == "o" then local nm = {} for _, bucket in ipairs({ e, e.alt or {} }) do for itemID in pairs(bucket.d or {}) do local name = Codex.db.names[itemID] if name then nm[itemID] = name end end end raw = setmetatable({ nm = nm }, { __index = e }) end end local rec = Share.Sanitize(key, raw, upd) -- Namen und Markierungen verschicken nur Berechtigte (mit Modul Freewood-OctoWoW-Dev) if rec and (letter == "u" or letter == "o" or letter == "q" or letter == "i") and not CanEditNames() then rec.cde, rec.cen, rec.bad, rec.ed = nil, nil, nil, nil end return rec, upd end -- Zu grosse Datensaetze verkleinern (Waren, Lehrstoff, Flugziele halbieren, kleinste Schluessel bleiben) function Share.Shrink(rec) if type(rec) ~= "table" then return nil end local out, cut = {}, false for k, v in pairs(rec) do out[k] = v end if rec.tx then -- zu gross: zuerst die Quest-Texte weglassen out.tx = nil cut = true end for _, field in ipairs({ "wa", "tr", "fl" }) do local src = rec[field] if type(src) == "table" then local keys = {} for key in pairs(src) do keys[#keys + 1] = key end table.sort(keys, function(a, b) if type(a) == type(b) then return a < b end return type(a) == "number" end) if #keys > 8 then local kept = {} for i = 1, math.floor(#keys / 2) do kept[keys[i]] = src[keys[i]] end out[field] = kept cut = true end end end if not cut then return nil end return out end ------------------------------------------------------------------------ -- Fremde Daten ------------------------------------------------------------------------ local view, index, allDirty, dirtyIds, xpRecs, xpDirty = nil, nil, true, {}, {}, false local lastXPBuild = -1000 -- Zeit des letzten Neubaus der XP- und Kampfzeit-Sicht (hoechstens alle 20 s) local ttkRecs = {} local nameCount local function Remote() local db = Codex.db db.remote = db.remote or {} return db.remote end function Share.RemoteStats() local senders, records = 0, 0 for _, s in pairs(Remote()) do senders = senders + 1 records = records + (s.n or 0) end return senders, records end -- Diagnose (/fwcodex share): empfangene Datensaetze je Absender und Art (u o q i x t p z g h f), Dev-Status, eigene Datensaetze je Art function Share.RemoteInfo() local out = {} for name, s in pairs(Remote()) do local by = {} for _, rec in pairs(s.rec) do by[rec.y or "?"] = (by[rec.y or "?"] or 0) + 1 end out[#out + 1] = { name = name, dev = IsDevSender(name) and true or false, n = s.n or 0, np = s.np or 0, by = by } end return out end function Share.OwnInfo() local by = {} for _, e in ipairs(Share.Scan(false) or {}) do local l = tostring(e.key):sub(1, 1) by[l] = (by[l] or 0) + 1 end return by end -- Mitgelieferte Startdaten (nur im Speicher, nie gespeichert und nie gesendet): Schluessel -> Datensatz local builtin = { rec = {}, n = 0 } function Share.HasBuiltin() return builtin.n > 0 end function Share.BuiltinCount() return builtin.n end -- Woher stammt ein Datensatz? "own" = eigene Beobachtung, "remote" = von anderen Spielern, "start" = nur aus den Startdaten function Share.Origin(letter, id) local field = KIND_OF[letter] if not field or type(id) ~= "number" then return nil end local own = Codex.db and Codex.db[field] and Codex.db[field][id] if type(own) == "table" then return "own" end local key = letter .. id for _, s in pairs(Remote()) do if s.rec and s.rec[key] then return "remote" end end if builtin.rec[key] then return "start" end return nil end function Share.HasRemote() return next(Remote()) ~= nil or builtin.n > 0 end function Share.Sender(name, create) local R = Remote() local s = R[name] if not s and create then local count, oldest, oldestT = 0, nil, math.huge for key, v in pairs(R) do count = count + 1 if key ~= SELF_IMPORT and (v.seen or 0) < oldestT then oldest, oldestT = key, v.seen or 0 end end if count >= MAX_SENDERS and oldest then R[oldest] = nil allDirty = true end s = { seen = time(), rec = {}, n = 0, cursor = 0 } R[name] = s end return s end local function NoteItemNames(nm) if type(nm) ~= "table" then return end local names = Codex.db.names if not nameCount then nameCount = 0 for _ in pairs(names) do nameCount = nameCount + 1 end end for id, name in pairs(nm) do if names[id] == nil and nameCount < MAX_NAMES then names[id] = name nameCount = nameCount + 1 end end end -- Bereinigten Datensatz eines Absenders speichern. Rueckgabe: true, wenn neu oder neuer function Share.StoreRecord(name, rec) if type(name) ~= "string" or type(rec) ~= "table" or not rec.y then return false end local s = Share.Sender(name, true) local key = rec.y .. rec.id local old = s.rec[key] if old and old.upd >= rec.upd then return false end if rec.y == "g" then -- Verkehrsverbindungen nur von Berechtigten (oder aus dem eigenen Import) if name ~= SELF_IMPORT and not IsDevSender(name) then return false end if not old then if (s.np or 0) >= MAX_PATH_RECORDS then return false end s.np = (s.np or 0) + 1 end s.rec[key] = rec s.seen = time() if Codex.Transport then Codex.Transport.rev = (Codex.Transport.rev or 0) + 1 end FW:Fire("CODEX_TRANSPORT_CHANGED", rec.id) return true end if rec.y == "h" then -- Ortspunkte nur von Berechtigten (oder aus dem eigenen Import) if name ~= SELF_IMPORT and not IsDevSender(name) then return false end if not old then if (s.np or 0) >= MAX_PATH_RECORDS then return false end s.np = (s.np or 0) + 1 end s.rec[key] = rec s.seen = time() if Codex.Spots then Codex.Spots.Bump(rec.id) end return true end if rec.y == "f" then -- Flugrouten: in die gemeinsamen Flugdaten einmischen, vom Absender bleibt nur der Stand local T = Codex.Taxi if not T or not T.StoreRemote(rec) then return false end if not old then if (s.np or 0) >= MAX_PATH_RECORDS then return false end s.np = (s.np or 0) + 1 end s.rec[key] = { y = rec.y, id = rec.id, upd = rec.upd } s.seen = time() return true end if rec.y == "p" or rec.y == "z" then -- Wegenetz: sofort in die gemeinsamen Daten einmischen, vom Absender bleibt nur der Stand local P = Codex.Path if not P then return false end if not old then if (s.np or 0) >= MAX_PATH_RECORDS then return false end s.np = (s.np or 0) + 1 end if rec.y == "p" then local cells = P.ParseChunk(rec.c) if not cells then return false end P.MergeChunk(rec.id, cells) else P.MergeLinks(rec.l) end s.rec[key] = { y = rec.y, id = rec.id, upd = rec.upd } s.seen = time() return true end if not old then if (s.n or 0) >= MAX_RECORDS then return false end s.n = (s.n or 0) + 1 end -- Aenderungen von Berechtigten melden (das Modul Freewood-OctoWoW-Dev schreibt sie ins Protokoll) if Codex.DevLog and (rec.cde or rec.cen or rec.bad ~= nil or rec.ed or (old and (old.cde or old.cen or old.bad ~= nil or old.ed))) and IsDevSender(name) then local o = old or {} for _, f in ipairs({ "cde", "cen", "bad" }) do if rec[f] ~= o[f] then pcall(Codex.DevLog, { who = name, kind = rec.y, id = rec.id, field = f, old = o[f], new = rec[f], remote = true }) end end local nf, of = EdFields(rec.ed), EdFields(o.ed) for _, f in ipairs(Share.ED_FIELDS) do if nf[f] ~= of[f] then pcall(Codex.DevLog, { who = name, kind = rec.y, id = rec.id, field = f, old = of[f], new = nf[f], remote = true }) end end end NoteItemNames(rec.nm) rec.nm = nil s.rec[key] = rec s.seen = time() if rec.y == "x" or rec.y == "t" then allDirty = true else dirtyIds[key] = true end return true end -- Startdaten laden: list = Liste roher Datensaetze { y, id, upd, ... }; jeder geht durch Sanitize -- (Wegenetz fliesst in die gemeinsamen Daten, alles andere liegt als unterste Schicht im Speicher). -- Rueckgabe: Anzahl angenommener Datensaetze -- Startdaten in Scheiben laden (Begin, Add je Scheibe, Done), damit das Spiel beim Anmelden nicht stehen bleibt. -- SetBuiltin(list) macht alles auf einmal. function Share.BuiltinBegin() builtin = { rec = {}, n = 0 } end function Share.BuiltinAdd(list, from, to) local accepted = 0 local P = Codex.Path for i = from or 1, to or #list do local raw = list[i] local key = type(raw) == "table" and type(raw.y) == "string" and type(raw.id) == "number" and (raw.y .. math.floor(raw.id)) or nil local rec = key and Share.Sanitize(key, raw, raw.upd) if rec then if rec.y == "p" then local cells = P and P.ParseChunk(rec.c) if cells then P.MergeChunk(rec.id, cells) accepted = accepted + 1 end elseif rec.y == "z" then if P and P.MergeLinks(rec.l) then accepted = accepted + 1 end else builtin.rec[key] = rec builtin.n = builtin.n + 1 accepted = accepted + 1 if rec.y == "u" or rec.y == "o" then NoteItemNames(rec.nm) end end end end return accepted end function Share.BuiltinDone() view, allDirty = nil, true if Codex.Transport then Codex.Transport.rev = (Codex.Transport.rev or 0) + 1 end if Codex.Spots then Codex.Spots.Bump() end end function Share.SetBuiltin(list) Share.BuiltinBegin() local accepted = Share.BuiltinAdd(list or {}, 1, #(list or {})) Share.BuiltinDone() return accepted end -- Empfangene Datensaetze einer Art (nur von aktuell berechtigten Absendern, fuer "g") function Share.RemoteRecords(letter) local out = {} for _, rec in pairs(builtin.rec) do if rec.y == letter and (letter == "g" or letter == "h") then out[#out + 1] = rec end end for name, s in pairs(Remote()) do if (letter ~= "g" and letter ~= "h") or name == SELF_IMPORT or IsDevSender(name) then for _, rec in pairs(s.rec) do if rec.y == letter then out[#out + 1] = rec end end end end return out end function Share.HaveRemote(name, key) local s = Remote()[name] local r = s and s.rec[key] return r and r.upd or nil end function Share.ClearRemote() local R = Remote() local count = 0 for name, s in pairs(R) do count = count + (s.n or 0) R[name] = nil end allDirty = true view = nil if Codex.Path and Codex.Path.ClearRemote then Codex.Path.ClearRemote() end if Codex.Taxi and Codex.Taxi.ClearRemote then Codex.Taxi.ClearRemote() end if Codex.Transport then Codex.Transport.rev = (Codex.Transport.rev or 0) + 1 end if Codex.Spots then Codex.Spots.Bump() end return count end ------------------------------------------------------------------------ -- Zusammenfuehren ------------------------------------------------------------------------ local function AddSample(dst, s) dst.n = (dst.n or 0) + s.n dst.sum = (dst.sum or 0) + s.sum dst.min = dst.min and math.min(dst.min, s.min) or s.min dst.max = dst.max and math.max(dst.max, s.max) or s.max end local function CopySample(s) return { n = s.n, sum = s.sum, min = s.min, max = s.max } end local function MergeLevels(dst, src, depth) for pl, v in pairs(src or {}) do if depth == 1 then dst[pl] = dst[pl] or {} AddSample(dst[pl], v) else dst[pl] = dst[pl] or {} for ml, s in pairs(v) do dst[pl][ml] = dst[pl][ml] or {} AddSample(dst[pl][ml], s) end end end end local function MergeBucket(dst, src) dst.k = (dst.k or 0) + (src.k or 0) if src.d then dst.d = dst.d or {} for id, d in pairs(src.d) do local t = dst.d[id] if not t then t = { n = 0, q = 0 } dst.d[id] = t end t.n = t.n + (d.n or 0) t.q = t.q + (d.q or 0) if d.qi then t.qi = 1 t.qk = (t.qk or 0) + (d.qk or 0) end end end end local function MergeUnitTTK(dst, src) for cls, byPl in pairs(src or {}) do dst[cls] = dst[cls] or {} for pl, st in pairs(byPl) do dst[cls][pl] = dst[cls][pl] or {} AddSample(dst[cls][pl], st) end end end local function MergePrices(dst, src) for price, n in pairs(src or {}) do dst[price] = (dst[price] or 0) + n end end local function MergeRoles(dst, src) for role, n in pairs(src or {}) do dst[role] = (dst[role] or 0) + n end end local function MergeWares(dst, src) for id, w in pairs(src or {}) do local d = dst[id] if not d then d = { pr = {} } dst[id] = d end MergePrices(d.pr, w.pr) d.st = d.st or w.st d.lim = d.lim or w.lim d.ec = d.ec or w.ec end end local function MergeTrain(dst, src) for key, r in pairs(src or {}) do local d = dst[key] if not d then d = { pr = {} } dst[key] = d end MergePrices(d.pr, r.pr) d.nm = d.nm or r.nm d.sp = d.sp or r.sp d.rk = d.rk or r.rk d.lv = d.lv or r.lv d.sn = d.sn or r.sn d.sr = d.sr or r.sr end end local function MergeFlights(dst, src) for dest, pr in pairs(src or {}) do dst[dest] = dst[dest] or {} MergePrices(dst[dest], pr) end end -- Namen und Markierung kommen nur von berechtigten Absendern; bei mehreren gilt der neueste Stand, -- eigene Werte gehen vor. Die Absender-Datensaetze bleiben unveraendert (ein gesperrter Absender wird so wieder wirksam). local function MergeEditorFields(m, e) if m._init == nil then m._init = true local editor = CanEditNames() m._ownName = editor and (m.cde ~= nil or m.cen ~= nil) if not editor then m.bad = nil end m._ownBad = editor and m.bad ~= nil if not editor then m.ed = nil end m._ownEd = editor and m.ed ~= nil end local r = e.rec if not IsDevSender(e.s) then return end if r.ed and not m._ownEd and (not m._edUpd or r.upd > m._edUpd) then m.ed, m._edUpd = EdFromWire(r.ed), r.upd end if (r.cde or r.cen) and not m._ownName and (not m._nameUpd or r.upd > m._nameUpd) then m.cde, m.cen, m._nameUpd = r.cde, r.cen, r.upd end if r.bad ~= nil and not m._ownBad and (not m._badUpd or r.upd > m._badUpd) then m.bad, m._badUpd = r.bad == 1, r.upd end end local function MergeEntity(own, list) local m = {} local remoteK = 0 if own then m.n, m.l1, m.l2, m.c, m.f = own.n, own.l1, own.l2, own.c, own.f m.ct, m.ne = own.ct, own.ne m.cde, m.cen = own.cde, own.cen m.bad = own.bad m.ed = own.ed if own.iz then m.iz = {} for id, n in pairs(own.iz) do m.iz[id] = n end end m.coin, m.coinN = own.coin, own.coinN m.cells = {} for key, n in pairs(own.cells or {}) do m.cells[key] = n end MergeBucket(m, own) if own.alt then m.alt = {} MergeBucket(m.alt, own.alt) end m.xm, m.xp = {}, {} MergeLevels(m.xm, own.xm, 2) MergeLevels(m.xp, own.xp, 1) m.tt = {} MergeUnitTTK(m.tt, own.tt) m.ro, m.wa, m.tr, m.fl = {}, {}, {}, {} MergeRoles(m.ro, own.ro) MergeWares(m.wa, own.wa) MergeTrain(m.tr, own.tr) MergeFlights(m.fl, own.fl) m.ti, m.fn, m.trc = own.ti, own.fn, own.trc else m.cells, m.xm, m.xp, m.tt = {}, {}, {}, {} m.ro, m.wa, m.tr, m.fl = {}, {}, {}, {} end for _, e in ipairs(list) do local r = e.rec m.n = m.n or r.n MergeEditorFields(m, e) m.f = m.f or r.f m.ct = m.ct or r.ct m.ne = m.ne or r.ne m.c = m.c or r.c if r.iz then m.iz = m.iz or {} local cnt = 0 for _ in pairs(m.iz) do cnt = cnt + 1 end for id, n in pairs(r.iz) do if m.iz[id] or cnt < 3 then if not m.iz[id] then cnt = cnt + 1 end m.iz[id] = (m.iz[id] or 0) + n end end end if r.l1 then m.l1 = m.l1 and math.min(m.l1, r.l1) or r.l1 end if r.l2 then m.l2 = m.l2 and math.max(m.l2, r.l2) or r.l2 end if r.coinN and r.coinN > 0 then m.coin = (m.coin or 0) + (r.coin or 0) m.coinN = (m.coinN or 0) + r.coinN end for key, n in pairs(r.cells or {}) do m.cells[key] = (m.cells[key] or 0) + n end MergeBucket(m, r) remoteK = remoteK + (r.k or 0) if r.alt then m.alt = m.alt or {} MergeBucket(m.alt, r.alt) end MergeLevels(m.xm, r.xm, 2) MergeUnitTTK(m.tt, r.tt) MergeRoles(m.ro, r.ro) MergeWares(m.wa, r.wa) MergeTrain(m.tr, r.tr) MergeFlights(m.fl, r.fl) m.ti = m.ti or r.ti m.trc = m.trc or r.trc m.fn = m.fn or r.fn end if next(m.ro) == nil then m.ro = nil end if next(m.wa) == nil then m.wa = nil end if next(m.tr) == nil then m.tr = nil end if Codex.LearnPlanReset then Codex.LearnPlanReset() end if next(m.fl) == nil then m.fl = nil end if next(m.tt) == nil then m.tt = nil end if next(m.cells) == nil then m.cells = nil end if next(m.xm) == nil then m.xm = nil end if next(m.xp) == nil then m.xp = nil end m.rk = remoteK > 0 and remoteK or nil m.remote = #list m.k = m.k or nil return m end local function PlusSample(a, b) if not b then return a end if not a then return CopySample(b) end local out = CopySample(a) AddSample(out, b) return out end local function MergeQuest(own, list) local m = {} if own then for k, v in pairs(own) do m[k] = v end end local objCopied = false for _, e in ipairs(list) do local r = e.rec MergeEditorFields(m, e) for _, f in ipairs({ "t", "lvl", "pl", "zone", "give", "turn", "si", "xp", "money", "rep", "fixed", "choice", "mn", "rc", "cl", "ev" }) do if m[f] == nil then m[f] = r[f] end end if r.tx then -- Quest-Texte: fehlende Teile aus fremden Daten (neue Tabelle, die eigene bleibt unveraendert) local tx = {} for k, v in pairs(m.tx or {}) do tx[k] = v end for k, v in pairs(r.tx) do if not tx[k] then tx[k] = v end end m.tx = tx end if r.pi and r.po then -- Beobachtungen zusammenzaehlen (neue Tabelle, die eigene bleibt unveraendert) local pi = {} for k, v in pairs(m.pi or {}) do pi[k] = v end local count = 0 for _ in pairs(pi) do count = count + 1 end for k, v in pairs(r.pi) do if pi[k] or count < 8 then if not pi[k] then count = count + 1 end pi[k] = (pi[k] or 0) + v end end m.pi, m.po = pi, (m.po or 0) + r.po end if r.mi then local mi = m.mi if mi then local lo, hi = math.max(mi.lo, r.mi.lo), math.min(mi.hi, r.mi.hi) if hi > lo then m.mi = { lo = lo, hi = hi, n = (mi.n or 1) + (r.mi.n or 1) } end else m.mi = r.mi end end if r.pre then -- Vorquests vereinen local have, merged = {}, {} for _, p in ipairs(m.pre or {}) do have[p] = true merged[#merged + 1] = p end for _, p in ipairs(r.pre) do if not have[p] and #merged < 8 then have[p] = true merged[#merged + 1] = p end end m.pre = merged end for _, f in ipairs({ "tm", "tr", "ta" }) do if r[f] then m[f] = PlusSample(m[f], r[f]) end end if r.obj then if not m.obj then m.obj = r.obj objCopied = false else for i, ro in ipairs(r.obj) do local mo = m.obj[i] if mo and (not mo.ty or not ro.ty or mo.ty == ro.ty) and ((not mo.name and ro.name) or (not mo.item and ro.item) or (not mo.ids and ro.ids)) then if not objCopied then local copy = {} for j, o in ipairs(m.obj) do local c = {} for k, v in pairs(o) do c[k] = v end copy[j] = c end m.obj = copy objCopied = true end mo = m.obj[i] mo.ty = mo.ty or ro.ty mo.name = mo.name or ro.name mo.txt = mo.txt or ro.txt mo.req = mo.req or ro.req mo.item = mo.item or ro.item mo.ids = mo.ids or ro.ids end end end end if r.qi then local copy = {} for item, srcs in pairs(m.qi or {}) do copy[item] = {} for k, v in pairs(srcs) do copy[item][k] = v end end for item, srcs in pairs(r.qi) do copy[item] = copy[item] or {} for k, v in pairs(srcs) do copy[item][k] = math.max(copy[item][k] or 0, v) end end m.qi = copy end end if not own then m.seen, m.done, m.first, m.last = 0, 0, nil, nil m.rem = true end return m end local function BuildXP() local db = Codex.db local xp = {} if #xpRecs > 0 then for pl, byMl in pairs(db.xp or {}) do for ml, byCls in pairs(byMl) do for cls, s in pairs(byCls) do xp[pl] = xp[pl] or {} xp[pl][ml] = xp[pl][ml] or {} xp[pl][ml][cls] = CopySample(s) end end end for _, rec in ipairs(xpRecs) do for pl, byMl in pairs(rec.xp or {}) do for ml, byCls in pairs(byMl) do for cls, s in pairs(byCls) do xp[pl] = xp[pl] or {} xp[pl][ml] = xp[pl][ml] or {} xp[pl][ml][cls] = xp[pl][ml][cls] or {} AddSample(xp[pl][ml][cls], s) end end end end view.xp = xp else view.xp = db.xp end xpDirty = false end local function BuildTTK() local db = Codex.db if #ttkRecs == 0 then view.ttk = db.ttk return end local tk = {} local function Add(src) for cls, byPl in pairs(src or {}) do for pl, byDelta in pairs(byPl) do for d, sm in pairs(byDelta) do tk[cls] = tk[cls] or {} tk[cls][pl] = tk[cls][pl] or {} tk[cls][pl][d] = PlusSample(tk[cls][pl][d], sm) end end end end Add(db.ttk) for _, rec in ipairs(ttkRecs) do Add(rec.tk) end view.ttk = tk end local function Rebuild() local db = Codex.db index = { u = {}, o = {}, q = {}, i = {} } xpRecs = {} ttkRecs = {} local names = {} for name in pairs(Remote()) do names[#names + 1] = name end table.sort(names) for _, name in ipairs(names) do local s = Remote()[name] for _, rec in pairs(s.rec) do if rec.y == "x" then xpRecs[#xpRecs + 1] = rec elseif rec.y == "t" then ttkRecs[#ttkRecs + 1] = rec elseif rec.y == "p" or rec.y == "z" or rec.y == "g" or rec.y == "h" or rec.y == "f" then -- Wegenetz liegt schon in den gemeinsamen Daten (Codex.Path), Verkehrsverbindungen liest Codex.Transport else local bucket = index[rec.y] bucket[rec.id] = bucket[rec.id] or {} local list = bucket[rec.id] list[#list + 1] = { s = name, rec = rec } end end end -- Startdaten ganz unten (nach allen Absendern) local bkeys = {} for key in pairs(builtin.rec) do bkeys[#bkeys + 1] = key end table.sort(bkeys) for _, key in ipairs(bkeys) do local rec = builtin.rec[key] if rec.y == "x" then xpRecs[#xpRecs + 1] = rec elseif rec.y == "t" then ttkRecs[#ttkRecs + 1] = rec elseif index[rec.y] then local bucket = index[rec.y] bucket[rec.id] = bucket[rec.id] or {} table.insert(bucket[rec.id], { s = BUILTIN, rec = rec }) end end view = { quests = {}, units = {}, objects = {}, ied = {}, xpb = db.xpb, names = db.names } for letter, field in pairs(KIND_OF) do local out = view[field] for id, e in pairs(db[field]) do out[id] = e.ed and Codex.ApplyEd(e, field) or e end for id, list in pairs(index[letter]) do local merged if letter == "q" then merged = MergeQuest(db[field][id], list) else merged = MergeEntity(db[field][id], list) end if merged.bad == true and not CanEditNames() then merged = nil end if merged then merged = Codex.ApplyEd(merged, field, true) end out[id] = merged end end BuildXP() BuildTTK() allDirty = false dirtyIds = {} end local function Refresh(key) local letter, num = key:match("^([uoqi])(%d+)$") if not letter then return end local id = tonumber(num) local field = KIND_OF[letter] local list = index[letter][id] -- neue Fremd-Eintraege in den Index aufnehmen list = nil for name, s in pairs(Remote()) do local rec = s.rec[key] if rec then list = list or {} list[#list + 1] = { s = name, rec = rec } end end if list then table.sort(list, function(a, b) return a.s < b.s end) end if builtin.rec[key] then list = list or {} list[#list + 1] = { s = BUILTIN, rec = builtin.rec[key] } end index[letter][id] = list local own = Codex.db[field][id] if list then local merged = letter == "q" and MergeQuest(own, list) or MergeEntity(own, list) if merged.bad == true and not CanEditNames() then merged = nil end if merged then merged = Codex.ApplyEd(merged, field, true) end view[field][id] = merged else view[field][id] = own and own.ed and Codex.ApplyEd(own, field) or own end end -- Sicht fuer die Anzeige: Felder quests, units, objects, xp, xpb, names -- Berechtigungen haben sich geaendert: Sicht neu aufbauen function Share.TrustChanged() allDirty = true end -- Eigene Eigenschaften (ed) muessen auch ohne fremde Daten in die Sicht local hasEd function Share.HasEd() if hasEd == nil then hasEd = false -- aeltere Form der Item-Eigenschaften (ohne Huelle ed) umwandeln for id, e in pairs(Codex.db.ied or {}) do if e.ed == nil and (e.z or e.dg ~= nil) then Codex.db.ied[id] = { ed = e } end end for _, field in ipairs({ "units", "objects", "quests", "ied" }) do for _, e in pairs(Codex.db[field] or {}) do if e.ed then hasEd = true break end end if hasEd then break end end end return hasEd end function Share.NoteEd(present) if present then hasEd = true else hasEd = nil end -- nil: beim naechsten Mal neu pruefen view, allDirty = nil, true end function Share.View() if not Share.HasRemote() and not Share.HasEd() then view, allDirty = nil, true return Codex.db end if not view or allDirty then Rebuild() elseif xpDirty then -- Erfahrungs- und Kampfzeit-Sicht nicht nach jedem Kill neu bauen (kann mit fremden Daten mehrere 100 ms dauern) local t = _G.GetTime and _G.GetTime() or 0 if t - lastXPBuild >= 20 then lastXPBuild = t BuildXP() BuildTTK() end end if next(dirtyIds) then local ids = dirtyIds dirtyIds = {} for key in pairs(ids) do Refresh(key) end end return view end -- Eigene Aenderung: nur noetig, wenn es zu diesem Datensatz fremde Daten gibt oder er in der Sicht fehlt function Share.OnChanged(kind, id) if not view then return end if kind == "xp" or kind == "ttk" then if #xpRecs > 0 or #ttkRecs > 0 then xpDirty = true end return end local letter = LETTER_OF[kind] if letter and type(id) == "number" then local field = KIND_OF[letter] if view[field][id] == nil or index[letter][id] then dirtyIds[letter .. id] = true end end end FW:On("CODEX_CHANGED", function(_, kind, id) Share.OnChanged(kind, id) end, Share) -- Gemessene Kampfdauer in Sekunden und Anzahl fuer (Klasse, Spielerstufe, Stufenabstand) function Share.TTKMeasured(cls, pl, delta) local tk = Share.View().ttk local s = tk and tk[cls] and tk[cls][pl] and tk[cls][pl][delta] if not s or not s.n or s.n < 1 then return nil end return s.sum / s.n / 10, s.n end -- Kampfdauer aller Klassen bei (Spielerstufe, Stufenabstand): Liste { cls, secs, n } nach Dauer sortiert function Share.TTKClasses(pl, delta) local list = {} local tk = Share.View().ttk for cls, byPl in pairs(tk or {}) do local s = byPl[pl] and byPl[pl][delta] if s and s.n and s.n > 0 then list[#list + 1] = { cls = cls, secs = s.sum / s.n / 10, n = s.n } end end table.sort(list, function(a, b) if a.secs ~= b.secs then return a.secs < b.secs end return a.cls < b.cls end) return list end -- Gemessene Erfahrung fuer (Spielerstufe, Gegnerstufe, Klasse): Mittelwert und Anzahl, -- nur wenn die Messwerte zusammenpassen function Share.XPMeasured(pl, ml, cls) local xp = Share.View().xp local s = xp and xp[pl] and xp[pl][ml] and xp[pl][ml][cls] if not s or not s.n or s.n < 1 then return nil end local avg = s.sum / s.n if (s.max - s.min) > math.max(2, avg * 0.1) then return nil end return math.floor(avg + 0.5), s.n end ------------------------------------------------------------------------ -- Export und Import als Code ------------------------------------------------------------------------ -- Empfangene Datensaetze aller Absender (je Schluessel der neueste), fuer berechtigte Bearbeiter local function ReceivedRecords(limit, skip) local best = {} for _, s in pairs(Remote()) do for key, rec in pairs(s.rec) do if rec.y ~= "p" and rec.y ~= "z" and rec.y ~= "f" and not skip[key] and (not best[key] or rec.upd > best[key].upd) then best[key] = rec end end end local list = {} for _, rec in pairs(best) do list[#list + 1] = rec end table.sort(list, function(a, b) if a.upd ~= b.upd then return a.upd > b.upd end return (a.y .. a.id) < (b.y .. b.id) end) local out = {} for i = 1, math.min(#list, limit) do out[i] = list[i] end return out end local function ExportList(limit, withReceived) local list = Share.Scan(true) table.sort(list, function(a, b) if a.upd ~= b.upd then return a.upd > b.upd end return a.key < b.key end) local out, have = {}, {} for i = 1, math.min(#list, limit) do local rec = Share.OwnRecord(list[i].key) if rec then out[#out + 1] = rec have[list[i].key] = true end end if withReceived and #out < limit then for _, rec in ipairs(ReceivedRecords(limit - #out, have)) do out[#out + 1] = rec end end return out end -- withReceived: auch empfangene Daten anderer Spieler (nur fuer Berechtigte mit Modul Freewood-OctoWoW-Dev) function Share.ExportData(withReceived) if withReceived and not (Codex.Trust and Codex.Trust.Can("export")) then return {} end local limit = 1500 local data repeat data = { list = ExportList(limit, withReceived) } local ok = true if FW.Export and FW.Export.Encode then local text = FW.Export.Encode("codex", data) ok = text ~= nil end if ok or limit <= 25 or #data.list == 0 then break end limit = math.floor(limit / 2) until false if #data.list == 0 then return {} end return data end function Share.ValidateImport(data) return type(data) == "table" and type(data.list) == "table" and #data.list > 0 and #data.list <= 5000 end function Share.ImportData(data) local L = FW.L local added, skipped = 0, 0 for _, raw in ipairs(data.list or {}) do local key = type(raw) == "table" and type(raw.y) == "string" and type(raw.id) == "number" and (raw.y .. math.floor(raw.id)) or nil local rec = key and Share.Sanitize(key, raw, raw.upd) if rec and Share.StoreRecord(SELF_IMPORT, rec) then added = added + 1 else skipped = skipped + 1 end end FW:Fire("CODEX_CHANGED", "import") return string.format(L.CODEX_IMPORT_STATS, added, skipped) end if FW.Export and FW.Export.RegisterHandler then FW.Export.RegisterHandler("codex", { label = function() return FW.L.CODEX_EXPORT_LABEL end, export = function() return Share.ExportData() end, validate = function(data) return Share.ValidateImport(data) end, import = function(data) return Share.ImportData(data) end, }) end