Files
2026-10-27 14:09:07 +00:00

2335 lines
74 KiB
Lua

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