1340 lines
41 KiB
Lua
1340 lines
41 KiB
Lua
-- Freewood-OctoWoW-Beasts: Archiv wilder Tiere
|
|
-- Lernt beim Spielen (anvisieren, Wildtierkunde, Zaehmen), kennt optional Startdaten
|
|
-- aus Freewood-OctoWoW-BeastsData und teilt eigene Funde per Export/Import.
|
|
--
|
|
-- Eigene Eintraege (db.beasts[npcID]):
|
|
-- name, fid (Familien-ID), family (Text aus dem Spiel), lvl1, lvl2, c (Klassifizierung),
|
|
-- zone, tameable, diet (Kuerzel mfcbur), seen, lored, tamed, tamedAt, firstSeen, updated,
|
|
-- grid = { [Karte] = { [Zellen-Nr] = Anzahl } } (Raster 200 x 200, Zelle = 0,5 % der Karte),
|
|
-- petAbilities = { {name, rank, spellID, passive} },
|
|
-- extra = { "Zeile aus Wildtierkunde" }
|
|
-- Importierte Eintraege liegen getrennt in db.imported[npcID] und ueberschreiben nie eigene Daten.
|
|
|
|
local ADDON, NS = ...
|
|
|
|
local FW = _G.Freewood
|
|
if not FW or not FW:RequireAPI(1, ADDON) then
|
|
return
|
|
end
|
|
|
|
local Util = FW.Util
|
|
local L = FW.L
|
|
local Pet = FW.Pet
|
|
if not Pet then
|
|
return
|
|
end
|
|
|
|
local GRID = 200 -- Raster: 200 x 200 Zellen pro Karte (Zelle = 0,5 %)
|
|
local MAX_CELLS = 800 -- hoechstens so viele verschiedene Zellen pro Tier
|
|
local MAX_EXTRA = 8
|
|
local MAX_PETABIL = 60
|
|
local MAX_IMPORT = 500
|
|
local STAY_INTERVAL = 10 -- Sekunden: in derselben Zelle erst danach erneut zaehlen
|
|
local LORE_INTERVAL = 4 -- Sekunden zwischen zwei Tooltip-Auslesungen pro Tier
|
|
|
|
local DIET_ORDER = "mfcbur"
|
|
|
|
local FOOD_ALIASES = {
|
|
m = { "fleisch", "meat" },
|
|
f = { "fisch", "fish" },
|
|
c = { "käse", "kaese", "cheese" },
|
|
b = { "brot", "bread" },
|
|
u = { "fungus", "pilz", "pilze", "mushroom" },
|
|
r = { "obst", "frucht", "früchte", "fruit" },
|
|
}
|
|
local FOOD_BY_NAME = {}
|
|
for code, names in pairs(FOOD_ALIASES) do
|
|
for _, n in ipairs(names) do
|
|
FOOD_BY_NAME[n] = code
|
|
end
|
|
end
|
|
|
|
local function GetMeta(field)
|
|
local fn = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata
|
|
if fn then
|
|
local ok, value = pcall(fn, ADDON, field)
|
|
if ok then
|
|
return value
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local Beasts = FW:NewModule("Beasts", {
|
|
version = GetMeta("Version") or "dev",
|
|
title = L.BEAST_TITLE,
|
|
description = L.BEAST_DESC,
|
|
defaults = {
|
|
beasts = {}, imported = {}, relay = {}, familyNames = {},
|
|
settings = { seed = true, mapPower = 1.0, share = { guild = false, party = false, raid = false, global = false }, people = {} },
|
|
sync = { have = {}, sent = 0, received = 0 },
|
|
},
|
|
})
|
|
NS.Beasts = Beasts
|
|
FW.Beasts = Beasts
|
|
|
|
local function Now()
|
|
return (GetTime and GetTime()) or time()
|
|
end
|
|
|
|
local function Num(v, lo, hi)
|
|
if type(v) == "number" and v == v and v ~= math.huge and v ~= -math.huge then
|
|
if lo and v < lo then
|
|
return nil
|
|
end
|
|
if hi and v > hi then
|
|
return nil
|
|
end
|
|
return v
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function Str(v, max)
|
|
if type(v) ~= "string" then
|
|
return nil
|
|
end
|
|
v = v:gsub("[%c]", " ")
|
|
if v == "" then
|
|
return nil
|
|
end
|
|
return v:sub(1, max or 64)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Nahrung
|
|
------------------------------------------------------------------------
|
|
|
|
-- Liste von Texten ("Fleisch", "Fisch") in Kuerzel ("mf"), Reihenfolge fest
|
|
function Beasts.FoodsToCodes(list)
|
|
local have = {}
|
|
for _, text in ipairs(list or {}) do
|
|
if type(text) == "string" then
|
|
local code = FOOD_BY_NAME[string.lower((text:gsub("^%s+", ""):gsub("%s+$", "")))]
|
|
if code then
|
|
have[code] = true
|
|
end
|
|
end
|
|
end
|
|
local out = {}
|
|
for i = 1, #DIET_ORDER do
|
|
local c = DIET_ORDER:sub(i, i)
|
|
if have[c] then
|
|
out[#out + 1] = c
|
|
end
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
local function CleanDiet(v)
|
|
if type(v) ~= "string" then
|
|
return nil
|
|
end
|
|
local out = {}
|
|
for i = 1, #DIET_ORDER do
|
|
local c = DIET_ORDER:sub(i, i)
|
|
if v:find(c, 1, true) then
|
|
out[#out + 1] = c
|
|
end
|
|
end
|
|
if #out == 0 then
|
|
return nil
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Startdaten
|
|
------------------------------------------------------------------------
|
|
|
|
function Beasts:SeedNpcs()
|
|
local seed = _G.FreewoodBeastsSeed
|
|
return seed and seed.npcs or nil
|
|
end
|
|
|
|
function Beasts:HasSeed()
|
|
return self:SeedNpcs() ~= nil
|
|
end
|
|
|
|
function Beasts:SeedEnabled()
|
|
return self.db.settings.seed ~= false and self:HasSeed()
|
|
end
|
|
|
|
function Beasts:SeedEntry(id)
|
|
if not self:SeedEnabled() then
|
|
return nil
|
|
end
|
|
return self:SeedNpcs()[id]
|
|
end
|
|
|
|
function Beasts:SetSeedEnabled(on)
|
|
self.db.settings.seed = on and true or false
|
|
self.index = nil
|
|
FW:Fire("BEASTS_UPDATED")
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Karten- und Gebietsnamen
|
|
------------------------------------------------------------------------
|
|
|
|
local mapNames, areaNames = {}, {}
|
|
|
|
function Beasts.MapName(mapID)
|
|
if not mapID then
|
|
return nil
|
|
end
|
|
local cached = mapNames[mapID]
|
|
if cached ~= nil then
|
|
return cached or nil
|
|
end
|
|
local name
|
|
if C_Map and C_Map.GetMapInfo then
|
|
local ok, info = pcall(C_Map.GetMapInfo, mapID)
|
|
if ok and info then
|
|
name = info.name
|
|
end
|
|
end
|
|
mapNames[mapID] = name or false
|
|
return name
|
|
end
|
|
|
|
function Beasts.AreaName(areaID)
|
|
if not areaID then
|
|
return nil
|
|
end
|
|
local cached = areaNames[areaID]
|
|
if cached ~= nil then
|
|
return cached or nil
|
|
end
|
|
local name
|
|
if C_Map and C_Map.GetAreaInfo then
|
|
local ok, value = pcall(C_Map.GetAreaInfo, areaID)
|
|
if ok then
|
|
name = value
|
|
end
|
|
end
|
|
areaNames[areaID] = name or false
|
|
return name
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Eigene Eintraege
|
|
------------------------------------------------------------------------
|
|
|
|
Beasts.GRID = GRID
|
|
|
|
local function CellIndex(x, y)
|
|
local cx = math.min(GRID - 1, math.max(0, math.floor(x * GRID)))
|
|
local cy = math.min(GRID - 1, math.max(0, math.floor(y * GRID)))
|
|
return cy * GRID + cx
|
|
end
|
|
|
|
local function GridCount(grid)
|
|
local n = 0
|
|
if type(grid) == "table" then
|
|
for _, cells in pairs(grid) do
|
|
for _ in pairs(cells) do
|
|
n = n + 1
|
|
end
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
-- Eine Sichtung in die Zelle eintragen. Rueckgabe: true wenn die Zelle neu ist.
|
|
local function AddSighting(e, m, idx, amount)
|
|
e.grid = e.grid or {}
|
|
local cells = e.grid[m]
|
|
if not cells then
|
|
cells = {}
|
|
e.grid[m] = cells
|
|
end
|
|
local n = cells[idx]
|
|
if n then
|
|
cells[idx] = math.min(65535, n + (amount or 1))
|
|
return false
|
|
end
|
|
if GridCount(e.grid) >= MAX_CELLS then
|
|
return false
|
|
end
|
|
cells[idx] = math.min(65535, amount or 1)
|
|
return true
|
|
end
|
|
|
|
-- Raster als Liste { {m, x, y, n} } (x,y = Zellen 0..199), haeufigste zuerst
|
|
local function FlattenGrid(grid)
|
|
local list = {}
|
|
for m, cells in pairs(grid or {}) do
|
|
for idx, n in pairs(cells) do
|
|
list[#list + 1] = { m = m, x = idx % GRID, y = math.floor(idx / GRID), n = n }
|
|
end
|
|
end
|
|
table.sort(list, function(a, b)
|
|
if a.n ~= b.n then
|
|
return a.n > b.n
|
|
end
|
|
if a.m ~= b.m then
|
|
return a.m < b.m
|
|
end
|
|
if a.y ~= b.y then
|
|
return a.y < b.y
|
|
end
|
|
return a.x < b.x
|
|
end)
|
|
while #list > MAX_CELLS do
|
|
list[#list] = nil
|
|
end
|
|
return list
|
|
end
|
|
|
|
function Beasts:Touch(id)
|
|
local e = self.db.beasts[id]
|
|
if not e then
|
|
e = { firstSeen = time(), updated = time() }
|
|
self.db.beasts[id] = e
|
|
self.isNew = true
|
|
end
|
|
return e
|
|
end
|
|
|
|
-- Kurze Pruefsumme der wichtigen Felder: nur bei Aenderung wird neu gezeichnet und "updated" gesetzt
|
|
local function Signature(e)
|
|
local abil = 0
|
|
for _, a in ipairs(e.petAbilities or {}) do
|
|
abil = abil + (a.rank or 0) + 1
|
|
end
|
|
return table.concat({
|
|
tostring(e.name), tostring(e.fid), tostring(e.family), tostring(e.lvl1), tostring(e.lvl2), tostring(e.c),
|
|
tostring(e.zone), tostring(e.seen), tostring(e.lored), tostring(e.tameable), tostring(e.tamed),
|
|
tostring(e.diet), GridCount(e.grid), e.extra and #e.extra or 0, abil,
|
|
}, "|")
|
|
end
|
|
|
|
function Beasts:Commit(id, e, before)
|
|
if Signature(e) ~= before then
|
|
e.updated = time()
|
|
self.index = nil
|
|
FW:Fire("BEASTS_UPDATED", id)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function PlayerPoint()
|
|
local loc = Util.GetPlayerLocation()
|
|
if loc and loc.mapID and loc.x and loc.y then
|
|
return loc.mapID, loc.x, loc.y, loc.zone
|
|
end
|
|
return nil, nil, nil, loc and loc.zone
|
|
end
|
|
|
|
local function CleanLine(text)
|
|
text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|T.-|t", "")
|
|
return (text:gsub("^%s+", ""):gsub("%s+$", ""))
|
|
end
|
|
|
|
-- Liest den Tooltip einer Einheit. Rueckgabe: nil oder
|
|
-- { tameable=bool, diet="mf"|nil, extra={...} } (nur wenn Wildtierkunde-Zeilen da sind)
|
|
function Beasts:ReadLore(unit, name)
|
|
if not (C_TooltipInfo and C_TooltipInfo.GetUnit) then
|
|
return nil
|
|
end
|
|
local ok, data = pcall(C_TooltipInfo.GetUnit, unit)
|
|
if not ok or type(data) ~= "table" or type(data.lines) ~= "table" then
|
|
return nil
|
|
end
|
|
local info = { extra = {} }
|
|
local found = false
|
|
local level = UnitLevel and Util.Clean(UnitLevel(unit))
|
|
if type(level) ~= "number" then
|
|
level = nil
|
|
end
|
|
local typeLow = UnitCreatureType and Util.Clean(UnitCreatureType(unit))
|
|
typeLow = type(typeLow) == "string" and string.lower(typeLow) or nil
|
|
for i, line in ipairs(data.lines) do
|
|
local text = type(line) == "table" and Util.Clean(line.leftText)
|
|
if type(text) == "string" and i > 1 then
|
|
text = CleanLine(text)
|
|
local low = string.lower(text)
|
|
if text == "" or text == name then
|
|
-- nichts
|
|
elseif low == "wildtier" or low == "beast" or (typeLow and low == typeLow) then
|
|
-- Typzeile
|
|
elseif low == "zähmbar" or low == "zaehmbar" or low == "tameable" then
|
|
info.tameable = true
|
|
found = true
|
|
elseif low:find("^ernährung:") or low:find("^ernaehrung:") or low:find("^diet:") then
|
|
local rest = text:match(":%s*(.+)$") or ""
|
|
local foods = {}
|
|
for part in rest:gmatch("[^,]+") do
|
|
foods[#foods + 1] = part
|
|
end
|
|
local codes = Beasts.FoodsToCodes(foods)
|
|
if codes ~= "" then
|
|
info.diet = codes
|
|
end
|
|
found = true
|
|
elseif low:find("^spezialisierung:") or low:find("^specialization:") then
|
|
found = true
|
|
elseif low:find("^stufe") or low:find("^level") or (level and level > 0 and low:find(tostring(level), 1, true) and low:find("%d")) then
|
|
-- Stufenzeile
|
|
elseif #info.extra < MAX_EXTRA then
|
|
info.extra[#info.extra + 1] = text:sub(1, 120)
|
|
end
|
|
end
|
|
end
|
|
if not found then
|
|
return nil
|
|
end
|
|
return info
|
|
end
|
|
|
|
local function AddExtra(e, lines)
|
|
if not lines or #lines == 0 then
|
|
return
|
|
end
|
|
e.extra = e.extra or {}
|
|
for _, line in ipairs(lines) do
|
|
local dup = false
|
|
for _, have in ipairs(e.extra) do
|
|
if have == line then
|
|
dup = true
|
|
break
|
|
end
|
|
end
|
|
if not dup and #e.extra < MAX_EXTRA then
|
|
e.extra[#e.extra + 1] = line
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Einheit beobachten und ins Archiv uebernehmen
|
|
-- Rueckgabe: true, wenn die Einheit ein Wildtier fuers Archiv ist
|
|
-- Geheime Werte (secret values) fuehren zum Abbruch ohne Fehlermeldung.
|
|
function Beasts:Observe(unit)
|
|
local ok, result = pcall(self.ObserveImpl, self, unit)
|
|
if ok then
|
|
return result and true or false
|
|
end
|
|
return false
|
|
end
|
|
|
|
function Beasts:ObserveImpl(unit)
|
|
if Util.IsSecret(unit) or not UnitExists(unit) then
|
|
return false
|
|
end
|
|
local isPlayer, controlled = UnitIsPlayer(unit), UnitPlayerControlled(unit)
|
|
if Util.IsSecret(isPlayer) or Util.IsSecret(controlled) or isPlayer or controlled then
|
|
return false
|
|
end
|
|
local guid = Util.Clean(UnitGUID(unit))
|
|
if type(guid) ~= "string" then
|
|
return false
|
|
end
|
|
-- Octo/1.12: GUID ist ein Hex-String (0xF130...), nicht "Creature-..."
|
|
local hexKind = Util.ParseHexGUID(guid)
|
|
if hexKind then
|
|
if hexKind ~= "Creature" then
|
|
return false
|
|
end
|
|
elseif not guid:find("^Creature") then
|
|
return false
|
|
end
|
|
local id = Util.GetNpcID(guid)
|
|
if not id then
|
|
return false
|
|
end
|
|
|
|
local family = UnitCreatureFamily and Util.Clean(UnitCreatureFamily(unit))
|
|
if family == "" then
|
|
family = nil
|
|
end
|
|
local known = self.db.beasts[id] ~= nil
|
|
local inSeed = self:SeedNpcs() and self:SeedNpcs()[id] ~= nil
|
|
local name = Util.Clean(UnitName(unit))
|
|
|
|
local lore
|
|
local now = Now()
|
|
local last = self.loreAt[id]
|
|
if (family or known or inSeed) and (not last or now - last >= LORE_INTERVAL) then
|
|
self.loreAt[id] = now
|
|
lore = self:ReadLore(unit, name)
|
|
end
|
|
|
|
if not (family or inSeed or known or lore) then
|
|
return false
|
|
end
|
|
|
|
local e = self:Touch(id)
|
|
local wasNew = self.isNew
|
|
self.isNew = nil
|
|
local before = wasNew and "" or Signature(e)
|
|
|
|
e.name = Str(name, 64) or e.name
|
|
e.seen = true
|
|
e.lastSeen = time()
|
|
if family then
|
|
e.family = Str(family, 48)
|
|
local fid = Pet.GetFamilyID(family)
|
|
if fid then
|
|
e.fid = fid
|
|
self.db.familyNames[fid] = e.family
|
|
end
|
|
end
|
|
local lvl = UnitLevel and Util.Clean(UnitLevel(unit))
|
|
if type(lvl) == "number" and lvl > 0 then
|
|
e.lvl1 = (e.lvl1 and math.min(e.lvl1, lvl)) or lvl
|
|
e.lvl2 = (e.lvl2 and math.max(e.lvl2, lvl)) or lvl
|
|
end
|
|
local class = UnitClassification and Util.Clean(UnitClassification(unit))
|
|
if type(class) == "string" and class ~= "normal" and class ~= "trivial" and class ~= "minus" then
|
|
e.c = class
|
|
end
|
|
|
|
local m, x, y, zone = PlayerPoint()
|
|
if zone and zone ~= "" then
|
|
e.zone = Str(zone, 64)
|
|
end
|
|
if m and x and y then
|
|
local idx = CellIndex(x, y)
|
|
local key = m * GRID * GRID + idx
|
|
local lastAt = self.trackAt[id]
|
|
if self.lastCell[id] ~= key or not lastAt or now - lastAt >= STAY_INTERVAL then
|
|
self.lastCell[id] = key
|
|
self.trackAt[id] = now
|
|
AddSighting(e, m, idx)
|
|
end
|
|
end
|
|
|
|
if lore then
|
|
e.lored = true
|
|
if lore.tameable then
|
|
e.tameable = true
|
|
end
|
|
if lore.diet then
|
|
e.diet = lore.diet
|
|
end
|
|
AddExtra(e, lore.extra)
|
|
end
|
|
|
|
if self:Commit(id, e, before) and wasNew then
|
|
FW:Debug(string.format(FW.L.BEAST_LEARNED, e.name or tostring(id)))
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Solange ein Wildtier im Ziel ist, jede Sekunde die Position festhalten
|
|
function Beasts:StartTracking()
|
|
if self.tracking then
|
|
return
|
|
end
|
|
self.tracking = true
|
|
local function tick()
|
|
if not Beasts:IsEnabled() or not UnitExists("target") or not Beasts:Observe("target") then
|
|
Beasts.tracking = false
|
|
return
|
|
end
|
|
C_Timer.After(1, tick)
|
|
end
|
|
C_Timer.After(1, tick)
|
|
end
|
|
|
|
-- Begleiter wurde erfasst: das Tier gilt als gezaehmt, Zauberbuch wird mitgenommen
|
|
function Beasts:OnPetUpdated(key, rec)
|
|
if type(rec) ~= "table" or rec.src == "import" or not rec.npcID or key ~= Pet.activeKey then
|
|
return
|
|
end
|
|
local e = self:Touch(rec.npcID)
|
|
local before = self.isNew and "" or Signature(e)
|
|
self.isNew = nil
|
|
e.tamed = true
|
|
e.tameable = true
|
|
e.tamedAt = e.tamedAt or time()
|
|
if type(rec.family) == "string" and rec.family ~= "" then
|
|
e.family = Str(rec.family, 48)
|
|
local fid = Pet.GetFamilyID(rec.family)
|
|
if fid then
|
|
e.fid = fid
|
|
self.db.familyNames[fid] = e.family
|
|
end
|
|
end
|
|
if type(rec.foods) == "table" and #rec.foods > 0 then
|
|
local codes = Beasts.FoodsToCodes(rec.foods)
|
|
if codes ~= "" then
|
|
e.diet = codes
|
|
end
|
|
end
|
|
local list = {}
|
|
for _, a in ipairs(rec.abilities or {}) do
|
|
if #list >= MAX_PETABIL then
|
|
break
|
|
end
|
|
if type(a) == "table" and type(a.name) == "string" then
|
|
list[#list + 1] = { name = a.name, rank = a.rank, spellID = a.spellID, passive = a.passive and true or nil }
|
|
end
|
|
end
|
|
if #list > 0 then
|
|
e.petAbilities = list
|
|
end
|
|
self:Commit(rec.npcID, e, before)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Zusammengefuehrte Sicht (eigen, Import, Startdaten)
|
|
------------------------------------------------------------------------
|
|
|
|
function Beasts.StatusOf(own, imp)
|
|
local tamed = (own and own.tamed) or (imp and imp.tamed)
|
|
local lored = (own and own.lored) or (imp and imp.lored)
|
|
if tamed then
|
|
return "tamed"
|
|
elseif lored then
|
|
return "lored"
|
|
elseif own or imp then
|
|
return "seen"
|
|
end
|
|
return "unknown"
|
|
end
|
|
|
|
local function SeedZone(seed)
|
|
if not seed then
|
|
return nil
|
|
end
|
|
return Beasts.MapName(seed.m) or Beasts.AreaName(seed.a)
|
|
end
|
|
|
|
function Beasts:FamilyText(fid, fallback)
|
|
if not fid then
|
|
return fallback
|
|
end
|
|
return self.db.familyNames[fid] or Pet.FamilyDisplay(fid)
|
|
end
|
|
|
|
function Beasts:Summary(id)
|
|
local own, imp, seed = self.db.beasts[id], self.db.imported[id], self:SeedEntry(id)
|
|
local fid = (own and own.fid) or (imp and imp.fid) or (seed and seed.f ~= "" and seed.f) or nil
|
|
local familyText = (own and own.family) or (imp and imp.family)
|
|
local s = {
|
|
id = id,
|
|
own = own, imp = imp, seed = seed,
|
|
name = (own and own.name) or (imp and imp.name) or (seed and seed.n) or string.format(FW.L.BEAST_UNNAMED, id),
|
|
fid = fid,
|
|
status = Beasts.StatusOf(own, imp),
|
|
zone = (own and own.zone) or (imp and imp.zone) or SeedZone(seed),
|
|
lvl1 = (own and own.lvl1) or (imp and imp.lvl1) or (seed and seed.l and seed.l[1]),
|
|
lvl2 = (own and own.lvl2) or (imp and imp.lvl2) or (seed and seed.l and seed.l[2]),
|
|
c = (own and own.c) or (imp and imp.c) or (seed and seed.c),
|
|
diet = (own and own.diet) or (imp and imp.diet) or (seed and seed.d ~= "" and seed.d) or nil,
|
|
tameable = (own and own.tameable) or (imp and imp.tameable) or (seed and true) or nil,
|
|
}
|
|
s.family = (own and own.family) or self:FamilyText(fid, familyText)
|
|
s.sortName = string.lower(s.name)
|
|
return s
|
|
end
|
|
|
|
-- Alle Eintraege als Liste (ohne Filter), nach Namen sortiert
|
|
function Beasts:Index()
|
|
if self.index then
|
|
return self.index
|
|
end
|
|
local ids = {}
|
|
for id in pairs(self.db.beasts) do
|
|
ids[id] = true
|
|
end
|
|
for id in pairs(self.db.imported) do
|
|
ids[id] = true
|
|
end
|
|
local seed = self:SeedEnabled() and self:SeedNpcs()
|
|
if seed then
|
|
for id in pairs(seed) do
|
|
ids[id] = true
|
|
end
|
|
end
|
|
local list = {}
|
|
for id in pairs(ids) do
|
|
list[#list + 1] = self:Summary(id)
|
|
end
|
|
table.sort(list, function(a, b)
|
|
if a.sortName ~= b.sortName then
|
|
return a.sortName < b.sortName
|
|
end
|
|
return a.id < b.id
|
|
end)
|
|
self.index = list
|
|
return list
|
|
end
|
|
|
|
-- Filter: { text, fid, zone, status }
|
|
-- Name und Rang (Zahl) eines Zaubers aus den Spieldaten; nil, wenn noch nicht bekannt
|
|
function Beasts.SpellInfo(spellID)
|
|
local name, rank
|
|
if C_Spell and C_Spell.GetSpellInfo then
|
|
local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
|
|
if ok and type(info) == "table" then
|
|
name = info.name
|
|
end
|
|
end
|
|
if C_Spell and C_Spell.GetSpellSubtext then
|
|
local ok, text = pcall(C_Spell.GetSpellSubtext, spellID)
|
|
if ok and type(text) == "string" then
|
|
rank = tonumber(string.match(text, "(%d+)"))
|
|
end
|
|
end
|
|
return name, rank or 0
|
|
end
|
|
|
|
-- Faehigkeiten, die ein Tier beim Zaehmen lehrt (Startdaten) bzw. die sein Begleiter hat:
|
|
-- { [name] = { [rang] = true } } plus Hoechstrang; im Eintrag zwischengespeichert, sobald alle Namen bekannt sind
|
|
function Beasts:Abilities(s)
|
|
if s.abil then
|
|
return s.abil
|
|
end
|
|
local map, complete = {}, true
|
|
local function add(name, rank)
|
|
local e = map[name]
|
|
if not e then
|
|
e = { ranks = {}, max = 0 }
|
|
map[name] = e
|
|
end
|
|
e.ranks[rank] = true
|
|
if rank > e.max then
|
|
e.max = rank
|
|
end
|
|
end
|
|
if s.seed and s.seed.t then
|
|
for _, spellID in ipairs(s.seed.t) do
|
|
local name, rank = Beasts.SpellInfo(spellID)
|
|
if name then
|
|
add(name, rank)
|
|
else
|
|
complete = false
|
|
end
|
|
end
|
|
end
|
|
local pa = (s.own and s.own.petAbilities) or (s.imp and s.imp.petAbilities)
|
|
for _, a in ipairs(pa or {}) do
|
|
if a.name then
|
|
add(a.name, tonumber(a.rank) or 0)
|
|
end
|
|
end
|
|
if complete then
|
|
s.abil = map
|
|
end
|
|
return map
|
|
end
|
|
|
|
-- filter: text, fid, zone, status, ability (Name), rank (Zahl), rankMin (true = "ab diesem Rang")
|
|
-- Zweiter Rueckgabewert: Zahl der Tiere ohne Faehigkeitsdaten (nur bei Faehigkeitsfilter)
|
|
function Beasts:Filtered(filter)
|
|
local out = {}
|
|
local missing = 0
|
|
local text = filter.text and filter.text ~= "" and string.lower(filter.text) or nil
|
|
for _, s in ipairs(self:Index()) do
|
|
local ok = true
|
|
if filter.fid and s.fid ~= filter.fid then
|
|
ok = false
|
|
elseif filter.zone and s.zone ~= filter.zone then
|
|
ok = false
|
|
elseif filter.status and s.status ~= filter.status then
|
|
ok = false
|
|
elseif text then
|
|
local hay = s.sortName .. " " .. string.lower(s.family or "") .. " " .. string.lower(s.zone or "")
|
|
if not hay:find(text, 1, true) then
|
|
ok = false
|
|
end
|
|
end
|
|
if ok and filter.ability then
|
|
local abil = self:Abilities(s)
|
|
if not next(abil) then
|
|
missing = missing + 1
|
|
ok = false
|
|
else
|
|
local e = abil[filter.ability]
|
|
if not e then
|
|
ok = false
|
|
elseif filter.rank then
|
|
if filter.rankMin then
|
|
ok = e.max >= filter.rank
|
|
else
|
|
ok = e.ranks[filter.rank] == true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if ok then
|
|
out[#out + 1] = s
|
|
end
|
|
end
|
|
return out, missing
|
|
end
|
|
|
|
-- Alle Faehigkeiten (Namen, sortiert) und die Raenge einer Faehigkeit
|
|
function Beasts:AbilityChoices()
|
|
local names, seen = {}, {}
|
|
for _, s in ipairs(self:Index()) do
|
|
for name in pairs(self:Abilities(s)) do
|
|
if not seen[name] then
|
|
seen[name] = true
|
|
names[#names + 1] = name
|
|
end
|
|
end
|
|
end
|
|
table.sort(names)
|
|
return names
|
|
end
|
|
|
|
function Beasts:AbilityRanks(name)
|
|
local ranks, seen = {}, {}
|
|
for _, s in ipairs(self:Index()) do
|
|
local e = self:Abilities(s)[name]
|
|
if e then
|
|
for r in pairs(e.ranks) do
|
|
if r > 0 and not seen[r] then
|
|
seen[r] = true
|
|
ranks[#ranks + 1] = r
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.sort(ranks)
|
|
return ranks
|
|
end
|
|
|
|
-- Familien und Gebiete, die im Index vorkommen (fuer die Auswahllisten)
|
|
function Beasts:FilterChoices()
|
|
local fams, zones, famSeen, zoneSeen = {}, {}, {}, {}
|
|
for _, s in ipairs(self:Index()) do
|
|
if s.fid and not famSeen[s.fid] then
|
|
famSeen[s.fid] = true
|
|
fams[#fams + 1] = { id = s.fid, text = self:FamilyText(s.fid) }
|
|
end
|
|
if s.zone and not zoneSeen[s.zone] then
|
|
zoneSeen[s.zone] = true
|
|
zones[#zones + 1] = s.zone
|
|
end
|
|
end
|
|
table.sort(fams, function(a, b)
|
|
return a.text < b.text
|
|
end)
|
|
table.sort(zones)
|
|
return fams, zones
|
|
end
|
|
|
|
-- Fundorte gruppiert nach Karte: { {mapID, name, own={ {x,y,n} }, imp={...}, seed={ {x,y} }} }
|
|
-- x,y in Zehntel-Prozent (0..1000)
|
|
function Beasts:Locations(s)
|
|
local groups, order = {}, {}
|
|
local function group(m)
|
|
local g = groups[m]
|
|
if not g then
|
|
g = { mapID = m, name = Beasts.MapName(m) or ("Map " .. tostring(m)), own = {}, imp = {}, seed = {} }
|
|
groups[m] = g
|
|
order[#order + 1] = g
|
|
end
|
|
return g
|
|
end
|
|
local function addGrid(entry, key)
|
|
for m, cells in pairs(entry and entry.grid or {}) do
|
|
local g = group(m)
|
|
for idx, n in pairs(cells) do
|
|
g[key][#g[key] + 1] = { (idx % GRID) * 5 + 2.5, math.floor(idx / GRID) * 5 + 2.5, n }
|
|
end
|
|
table.sort(g[key], function(a, b)
|
|
if a[3] ~= b[3] then
|
|
return a[3] > b[3]
|
|
end
|
|
if a[2] ~= b[2] then
|
|
return a[2] < b[2]
|
|
end
|
|
return a[1] < b[1]
|
|
end)
|
|
end
|
|
end
|
|
addGrid(s.own, "own")
|
|
addGrid(s.imp, "imp")
|
|
if s.seed and s.seed.m and s.seed.p then
|
|
local g = group(s.seed.m)
|
|
for i = 1, #s.seed.p - 1, 2 do
|
|
g.seed[#g.seed + 1] = { s.seed.p[i], s.seed.p[i + 1] }
|
|
end
|
|
end
|
|
table.sort(order, function(a, b)
|
|
local ca = #a.own + #a.imp + #a.seed
|
|
local cb = #b.own + #b.imp + #b.seed
|
|
if ca ~= cb then
|
|
return ca > cb
|
|
end
|
|
return a.name < b.name
|
|
end)
|
|
return order
|
|
end
|
|
|
|
-- Gewichte pro Rasterzelle fuer eine Karte, ueber mehrere Tiere (ids = { [npcID] = true })
|
|
-- Rueckgabe: { [Zellen-Nr] = Gewicht }
|
|
function Beasts:MapWeights(ids, mapID)
|
|
local weights = {}
|
|
local function add(idx, w)
|
|
weights[idx] = (weights[idx] or 0) + w
|
|
end
|
|
for id in pairs(ids) do
|
|
local own, imp, seed = self.db.beasts[id], self.db.imported[id], self:SeedEntry(id)
|
|
local oc = own and own.grid and own.grid[mapID]
|
|
if oc then
|
|
for idx, n in pairs(oc) do
|
|
add(idx, 2 + math.min(n, 8) * 0.25)
|
|
end
|
|
end
|
|
local ic = imp and imp.grid and imp.grid[mapID]
|
|
if ic then
|
|
for idx, n in pairs(ic) do
|
|
add(idx, 1.5 + math.min(n, 8) * 0.15)
|
|
end
|
|
end
|
|
if seed and seed.m == mapID and seed.p then
|
|
for i = 1, #seed.p - 1, 2 do
|
|
local cx = math.min(GRID - 1, math.floor(seed.p[i] / 1000 * GRID))
|
|
local cy = math.min(GRID - 1, math.floor(seed.p[i + 1] / 1000 * GRID))
|
|
add(cy * GRID + cx, 1)
|
|
end
|
|
end
|
|
end
|
|
return weights
|
|
end
|
|
|
|
-- Punkte fuer die Codex-Karte (wie "Auf Karte zeigen" in der Datenbank):
|
|
-- { {m, x, y, n, name, k}, ... } mit x, y als Anteil 0..1; ids = { [npcID] = true }
|
|
local MAP_POINTS_MAX = 3000
|
|
function Beasts:MapPoints(ids)
|
|
local pts = {}
|
|
for id in pairs(ids) do
|
|
local sum = self:Summary(id)
|
|
local name = (sum and sum.name) or ("#" .. tostring(id))
|
|
local function addGrid(entry)
|
|
for m, cells in pairs(entry and entry.grid or {}) do
|
|
for idx, n in pairs(cells) do
|
|
pts[#pts + 1] = { m = m, x = ((idx % GRID) + 0.5) / GRID,
|
|
y = (math.floor(idx / GRID) + 0.5) / GRID, n = n, name = name, k = "u" }
|
|
end
|
|
end
|
|
end
|
|
addGrid(self.db.beasts[id])
|
|
addGrid(self.db.imported[id])
|
|
local seed = self:SeedEntry(id)
|
|
if seed and seed.m and seed.p then
|
|
for i = 1, #seed.p - 1, 2 do
|
|
pts[#pts + 1] = { m = seed.m, x = seed.p[i] / 1000, y = seed.p[i + 1] / 1000, n = 1, name = name, k = "u" }
|
|
end
|
|
end
|
|
end
|
|
if #pts > MAP_POINTS_MAX then
|
|
table.sort(pts, function(a, b) return a.n > b.n end)
|
|
for i = #pts, MAP_POINTS_MAX + 1, -1 do
|
|
pts[i] = nil
|
|
end
|
|
end
|
|
return pts
|
|
end
|
|
|
|
-- Tiere mit Funden in der Naehe einer Kartenzelle: { {id, own, imp, seed}, ... }, meiste Funde zuerst
|
|
function Beasts:BeastsAt(ids, mapID, cx, cy, radius)
|
|
local out = {}
|
|
local function near(idx)
|
|
local x = idx % GRID
|
|
local y = math.floor(idx / GRID)
|
|
return math.abs(x - cx) <= radius and math.abs(y - cy) <= radius
|
|
end
|
|
for id in pairs(ids) do
|
|
local own, imp, seed = self.db.beasts[id], self.db.imported[id], self:SeedEntry(id)
|
|
local h = { id = id, own = 0, imp = 0, seed = 0 }
|
|
local oc = own and own.grid and own.grid[mapID]
|
|
if oc then
|
|
for idx, n in pairs(oc) do
|
|
if near(idx) then h.own = h.own + n end
|
|
end
|
|
end
|
|
local ic = imp and imp.grid and imp.grid[mapID]
|
|
if ic then
|
|
for idx, n in pairs(ic) do
|
|
if near(idx) then h.imp = h.imp + n end
|
|
end
|
|
end
|
|
if seed and seed.m == mapID and seed.p then
|
|
for i = 1, #seed.p - 1, 2 do
|
|
local x = math.min(GRID - 1, math.floor(seed.p[i] / 1000 * GRID))
|
|
local y = math.min(GRID - 1, math.floor(seed.p[i + 1] / 1000 * GRID))
|
|
if math.abs(x - cx) <= radius and math.abs(y - cy) <= radius then h.seed = h.seed + 1 end
|
|
end
|
|
end
|
|
if h.own + h.imp + h.seed > 0 then
|
|
h.total = h.own * 3 + h.imp * 2 + h.seed
|
|
out[#out + 1] = h
|
|
end
|
|
end
|
|
table.sort(out, function(a, b)
|
|
if a.total ~= b.total then return a.total > b.total end
|
|
return a.id < b.id
|
|
end)
|
|
return out
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Ereignisse
|
|
------------------------------------------------------------------------
|
|
|
|
function Beasts:OnTargetChanged()
|
|
if self:Observe("target") then
|
|
self:StartTracking()
|
|
end
|
|
end
|
|
|
|
function Beasts:OnMouseover()
|
|
self:Observe("mouseover")
|
|
end
|
|
|
|
function Beasts:OnTargetAura(unit)
|
|
if unit ~= "target" then
|
|
return
|
|
end
|
|
-- Wildtierkunde zeigt sich als Aura: kurz warten, dann Tooltip erneut lesen
|
|
if self.auraTimer then
|
|
return
|
|
end
|
|
self.auraTimer = true
|
|
C_Timer.After(0.4, function()
|
|
Beasts.auraTimer = nil
|
|
local guid = Util.Clean(UnitGUID("target"))
|
|
local id = type(guid) == "string" and Util.GetNpcID(guid) or nil
|
|
if id then
|
|
Beasts.loreAt[id] = nil
|
|
end
|
|
Beasts:Observe("target")
|
|
end)
|
|
end
|
|
|
|
-- Alte Eintraege (Punktlisten) ins Raster uebertragen, Typzeile aus den Hinweisen entfernen
|
|
local function MigrateEntry(e)
|
|
if type(e) ~= "table" then
|
|
return
|
|
end
|
|
if type(e.locs) == "table" then
|
|
for _, p in ipairs(e.locs) do
|
|
if type(p) == "table" and type(p.m) == "number" and type(p.x) == "number" and type(p.y) == "number" then
|
|
AddSighting(e, p.m, CellIndex(p.x / 1000, p.y / 1000))
|
|
end
|
|
end
|
|
e.locs = nil
|
|
end
|
|
if type(e.extra) == "table" then
|
|
local keep = {}
|
|
for _, line in ipairs(e.extra) do
|
|
local low = type(line) == "string" and string.lower(line) or ""
|
|
if low ~= "wildtier" and low ~= "beast" and low ~= "" then
|
|
keep[#keep + 1] = line
|
|
end
|
|
end
|
|
e.extra = #keep > 0 and keep or nil
|
|
end
|
|
end
|
|
|
|
-- Alte Eintraege umwandeln. Laeuft bei OnEnable, denn OnInitialize wuerde bei schon
|
|
-- bereitem Core noch vor der Definition dieser Funktionen aufgerufen.
|
|
function Beasts:Migrate()
|
|
for _, e in pairs(self.db.beasts) do
|
|
MigrateEntry(e)
|
|
end
|
|
for _, e in pairs(self.db.imported) do
|
|
MigrateEntry(e)
|
|
end
|
|
self.index = nil
|
|
end
|
|
|
|
function Beasts:OnEnable()
|
|
self.loreAt = self.loreAt or {}
|
|
self.trackAt = self.trackAt or {}
|
|
self.lastCell = self.lastCell or {}
|
|
self:Migrate()
|
|
FW:RegisterEvent("PLAYER_TARGET_CHANGED", self.OnTargetChanged, self)
|
|
FW:RegisterEvent("UPDATE_MOUSEOVER_UNIT", self.OnMouseover, self)
|
|
FW:RegisterEvent("UNIT_AURA", self.OnTargetAura, self)
|
|
FW:On("PET_UPDATED", self.OnPetUpdated, self)
|
|
if self.Sync then
|
|
self.Sync:Start()
|
|
end
|
|
end
|
|
|
|
function Beasts:OnDisable()
|
|
if self.Sync then
|
|
self.Sync:Stop()
|
|
end
|
|
FW:UnregisterEvent("PLAYER_TARGET_CHANGED", self)
|
|
FW:UnregisterEvent("UPDATE_MOUSEOVER_UNIT", self)
|
|
FW:UnregisterEvent("UNIT_AURA", self)
|
|
FW:Off("PET_UPDATED", self)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Export / Import
|
|
-- Geteilt werden nur eigene Funde, nichts Persoenliches.
|
|
------------------------------------------------------------------------
|
|
|
|
local function SanitizeList(src, max, fn)
|
|
local list = {}
|
|
if type(src) ~= "table" then
|
|
return list
|
|
end
|
|
for _, v in ipairs(src) do
|
|
if #list >= max then
|
|
break
|
|
end
|
|
local clean = fn(v)
|
|
if clean ~= nil then
|
|
list[#list + 1] = clean
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
local function Sanitize(src)
|
|
if type(src) ~= "table" then
|
|
return nil
|
|
end
|
|
local id = Num(src.id, 1, 10000000)
|
|
if not id then
|
|
return nil
|
|
end
|
|
local r = {
|
|
id = math.floor(id),
|
|
name = Str(src.name, 64),
|
|
fid = Str(src.fid, 24),
|
|
family = Str(src.family, 48),
|
|
lvl1 = Num(src.lvl1, 1, 120),
|
|
lvl2 = Num(src.lvl2, 1, 120),
|
|
c = Str(src.c, 16),
|
|
zone = Str(src.zone, 64),
|
|
tameable = (src.tameable == true) or nil,
|
|
diet = CleanDiet(src.diet),
|
|
seen = (src.seen == true) or nil,
|
|
lored = (src.lored == true) or nil,
|
|
tamed = (src.tamed == true) or nil,
|
|
updated = Num(src.updated, 1, 4102444800) or time(),
|
|
}
|
|
-- Raster: flache Liste cells { {m,x,y,n} } (neu) oder Punkte locs (alt, Zehntel-Prozent)
|
|
local grid, total = {}, 0
|
|
local function put(m, cx, cy, n)
|
|
if total >= MAX_CELLS then
|
|
return
|
|
end
|
|
local cells = grid[m]
|
|
if not cells then
|
|
cells = {}
|
|
grid[m] = cells
|
|
end
|
|
local idx = cy * GRID + cx
|
|
if not cells[idx] then
|
|
total = total + 1
|
|
end
|
|
cells[idx] = math.max(cells[idx] or 0, n)
|
|
end
|
|
if type(src.cells) == "table" then
|
|
for _, p in ipairs(src.cells) do
|
|
if type(p) == "table" then
|
|
local m, x, y, n = Num(p.m, 1, 100000), Num(p.x, 0, GRID - 1), Num(p.y, 0, GRID - 1), Num(p.n, 1, 65535)
|
|
if m and x and y and n then
|
|
put(m, math.floor(x), math.floor(y), math.floor(n))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if type(src.locs) == "table" then
|
|
for _, p in ipairs(src.locs) do
|
|
if type(p) == "table" then
|
|
local m, x, y = Num(p.m, 1, 100000), Num(p.x, 0, 1000), Num(p.y, 0, 1000)
|
|
if m and x and y then
|
|
put(m, math.min(GRID - 1, math.floor(x / 1000 * GRID)), math.min(GRID - 1, math.floor(y / 1000 * GRID)), 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
r.grid = total > 0 and grid or nil
|
|
r.petAbilities = SanitizeList(src.petAbilities, MAX_PETABIL, function(a)
|
|
if type(a) ~= "table" then
|
|
return nil
|
|
end
|
|
local n = Str(a.name, 64)
|
|
if not n then
|
|
return nil
|
|
end
|
|
return { name = n, rank = Num(a.rank, 0, 20), spellID = Num(a.spellID, 1, 10000000), passive = (a.passive == true) or nil }
|
|
end)
|
|
r.extra = SanitizeList(src.extra, MAX_EXTRA, function(v)
|
|
return Str(v, 120)
|
|
end)
|
|
if #r.petAbilities == 0 then r.petAbilities = nil end
|
|
if #r.extra == 0 then r.extra = nil end
|
|
return r
|
|
end
|
|
|
|
function Beasts:ExportData()
|
|
local list = {}
|
|
for id, e in pairs(self.db.beasts) do
|
|
local copy = {}
|
|
for k, v in pairs(e) do
|
|
copy[k] = v
|
|
end
|
|
copy.id = id
|
|
copy.cells = FlattenGrid(e.grid)
|
|
local clean = Sanitize(copy)
|
|
if clean then
|
|
-- Ausgabe als flache Liste statt verschachtelter Tabelle
|
|
clean.cells = FlattenGrid(clean.grid)
|
|
clean.grid = nil
|
|
if #clean.cells == 0 then
|
|
clean.cells = nil
|
|
end
|
|
list[#list + 1] = clean
|
|
end
|
|
end
|
|
table.sort(list, function(a, b)
|
|
return a.id < b.id
|
|
end)
|
|
if #list == 0 then
|
|
return {}
|
|
end
|
|
return { list = list }
|
|
end
|
|
|
|
-- Was geteilt werden darf: der eigene Eintrag, sonst ein ueber das Brett erhaltener (db.relay) aus der Ebene "importiert"
|
|
function Beasts:ShareEntry(id)
|
|
local e = self.db.beasts[id]
|
|
if e then return e end
|
|
local relay = self.db.relay
|
|
if relay and relay[id] then return self.db.imported[id] end
|
|
return nil
|
|
end
|
|
|
|
-- Ids aller teilbaren Eintraege: eigene Funde und ueber das Brett erhaltene
|
|
function Beasts:ShareIds()
|
|
local out = {}
|
|
for id, e in pairs(self.db.beasts) do
|
|
if type(id) == "number" and (e.seen or e.lored or e.tamed) then out[id] = e end
|
|
end
|
|
for id in pairs(self.db.relay or {}) do
|
|
local e = self.db.imported[id]
|
|
if type(id) == "number" and e and not out[id] then out[id] = e end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Eigener Eintrag als bereinigter Datensatz zum Teilen (flache Zellenliste), nil wenn leer
|
|
function Beasts:ShareRecord(id)
|
|
local e = self:ShareEntry(id)
|
|
if not e then
|
|
return nil
|
|
end
|
|
local copy = {}
|
|
for k, v in pairs(e) do
|
|
copy[k] = v
|
|
end
|
|
copy.id = id
|
|
copy.cells = FlattenGrid(e.grid)
|
|
local clean = Sanitize(copy)
|
|
if not clean then
|
|
return nil
|
|
end
|
|
clean.cells = FlattenGrid(clean.grid)
|
|
clean.grid = nil
|
|
if #clean.cells == 0 then
|
|
clean.cells = nil
|
|
end
|
|
return clean
|
|
end
|
|
|
|
-- Zahl der eigenen teilbaren Eintraege und ihr Pruefwert (Reihenfolge egal)
|
|
function Beasts:ShareSummary()
|
|
local n, h = 0, 0
|
|
for id, e in pairs(self:ShareIds()) do
|
|
n = n + 1
|
|
h = (h + id * 131 + ((e.updated or 0) % 1000003)) % 2147483647
|
|
end
|
|
return n, h
|
|
end
|
|
|
|
function Beasts:ValidateImport(data)
|
|
if type(data) ~= "table" or type(data.list) ~= "table" or #data.list == 0 then
|
|
return false, L.BEAST_IMPORT_EMPTY
|
|
end
|
|
if #data.list > MAX_IMPORT then
|
|
return false, L.BEAST_IMPORT_TOO_MANY
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Beasts:ImportData(data, opts)
|
|
opts = opts or {}
|
|
local added, updated, skipped = 0, 0, 0
|
|
for _, item in ipairs(data.list) do
|
|
local clean = Sanitize(item)
|
|
if not clean then
|
|
skipped = skipped + 1
|
|
else
|
|
local id = clean.id
|
|
clean.id = nil
|
|
local old = self.db.imported[id]
|
|
if old and not opts.merge and (old.updated or 0) >= clean.updated then
|
|
skipped = skipped + 1
|
|
else
|
|
if old then
|
|
-- Fundorte beider Staende zusammenfuehren, Merkmale vereinen
|
|
local merged = { grid = {} }
|
|
for _, source in ipairs({ old.grid or {}, clean.grid or {} }) do
|
|
for m, cells in pairs(source) do
|
|
for idx, n in pairs(cells) do
|
|
local have = merged.grid[m] and merged.grid[m][idx]
|
|
if have then
|
|
merged.grid[m][idx] = math.max(have, n)
|
|
else
|
|
AddSighting(merged, m, idx, n)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
clean.grid = GridCount(merged.grid) > 0 and merged.grid or nil
|
|
clean.seen = clean.seen or old.seen
|
|
clean.lored = clean.lored or old.lored
|
|
clean.tamed = clean.tamed or old.tamed
|
|
clean.tameable = clean.tameable or old.tameable
|
|
clean.updated = math.max(old.updated or 0, clean.updated)
|
|
if not clean.petAbilities then clean.petAbilities = old.petAbilities end
|
|
if not clean.extra then clean.extra = old.extra end
|
|
updated = updated + 1
|
|
else
|
|
added = added + 1
|
|
end
|
|
clean.importedAt = time()
|
|
self.db.imported[id] = clean
|
|
if clean.fid and clean.family then
|
|
self.db.familyNames[clean.fid] = self.db.familyNames[clean.fid] or clean.family
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self.index = nil
|
|
FW:Fire("BEASTS_UPDATED")
|
|
return string.format(FW.L.BEAST_IMPORT_STATS, added, updated, skipped)
|
|
end
|
|
|
|
FW.Export.RegisterHandler("beasts", {
|
|
label = function()
|
|
return FW.L.BEAST_EXPORT_LABEL
|
|
end,
|
|
export = function()
|
|
return Beasts:ExportData()
|
|
end,
|
|
validate = function(data)
|
|
return Beasts:ValidateImport(data)
|
|
end,
|
|
import = function(data)
|
|
return Beasts:ImportData(data)
|
|
end,
|
|
})
|