565 lines
16 KiB
Lua
565 lines
16 KiB
Lua
-- Freewood-OctoWoW-Pet: erfasst den aktiven Jaeger-Begleiter und speichert ihn.
|
|
--
|
|
-- Gespeichert wird im Core-Datenspeicher (Account-weit): Pet.db.pets[<Besitzer>:<Name>].
|
|
-- Alle Abfragen laufen geschuetzt (pcall) und nur ausserhalb des Kampfes, damit
|
|
-- ein veraenderter oder gesperrter Rueckgabewert nie einen Fehler auslost.
|
|
--
|
|
-- Eintrag (Felder, alle optional ausser name/level):
|
|
-- name, family, creatureType, level, npcID,
|
|
-- happiness (1..3), damagePct, loyaltyRate, loyaltyText, loyaltyLevel,
|
|
-- trainingTotal, trainingSpent, speed, dmgMin, dmgMax, armor, armorBase, hpMax,
|
|
-- foods = { "..." }, abilities = { { name, rank, spellID, passive } }, others = { "..." },
|
|
-- updated, firstSeen, owner (nur eigene), src ("own" | "import")
|
|
|
|
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 MAX_ABILITIES = 60
|
|
local MAX_OTHERS = 30
|
|
local MAX_FOODS = 12
|
|
local MAX_IMPORT = 300
|
|
|
|
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 Pet = FW:NewModule("Pet", {
|
|
version = GetMeta("Version") or "dev",
|
|
title = L.PET_TITLE,
|
|
description = L.PET_DESC,
|
|
defaults = { pets = {} },
|
|
})
|
|
NS.Pet = Pet
|
|
FW.Pet = Pet -- fuer andere Freewood-Addons
|
|
|
|
------------------------------------------------------------------------
|
|
-- Kleine Pruefer
|
|
------------------------------------------------------------------------
|
|
|
|
local function Num(v)
|
|
if Util.IsSecret(v) then
|
|
return nil
|
|
end
|
|
if type(v) == "number" and v == v and v ~= math.huge and v ~= -math.huge then
|
|
return v
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function NumIn(v, lo, hi)
|
|
v = Num(v)
|
|
if v and v >= lo and v <= hi then
|
|
return v
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function Str(v, max)
|
|
if Util.IsSecret(v) then
|
|
return nil
|
|
end
|
|
if type(v) == "string" and v ~= "" then
|
|
return v:sub(1, max or 64)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function Round(v, decimals)
|
|
v = Num(v)
|
|
if not v then
|
|
return nil
|
|
end
|
|
return Util.Round(v, decimals or 0)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Auslesen
|
|
------------------------------------------------------------------------
|
|
|
|
local function ReadAbilities()
|
|
local abilities, others = {}, {}
|
|
|
|
if not (C_SpellBook and C_SpellBook.HasPetSpells and C_SpellBook.GetSpellBookItemName
|
|
and Enum and Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Pet) then
|
|
return abilities, others
|
|
end
|
|
|
|
local bank = Enum.SpellBookSpellBank.Pet
|
|
local count = C_SpellBook.HasPetSpells()
|
|
if type(count) ~= "number" then
|
|
return abilities, others
|
|
end
|
|
|
|
for i = 1, count do
|
|
local ok, spellName, subName = pcall(C_SpellBook.GetSpellBookItemName, i, bank)
|
|
if ok and type(spellName) == "string" and spellName ~= "" then
|
|
local rank
|
|
if type(subName) == "string" then
|
|
rank = tonumber(subName:match("(%d+)"))
|
|
end
|
|
|
|
local spellID, passive
|
|
if C_SpellBook.GetSpellBookItemInfo then
|
|
local okInfo, info = pcall(C_SpellBook.GetSpellBookItemInfo, i, bank)
|
|
if okInfo and type(info) == "table" then
|
|
spellID = Num(info.spellID) or Num(info.actionID)
|
|
if info.isPassive == true then
|
|
passive = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if rank then
|
|
if #abilities < MAX_ABILITIES then
|
|
abilities[#abilities + 1] = {
|
|
name = spellName:sub(1, 48),
|
|
rank = rank,
|
|
spellID = spellID,
|
|
passive = passive,
|
|
}
|
|
end
|
|
elseif #others < MAX_OTHERS then
|
|
others[#others + 1] = spellName:sub(1, 48)
|
|
end
|
|
end
|
|
end
|
|
|
|
return abilities, others
|
|
end
|
|
|
|
local function ReadFoods()
|
|
if not (C_PetInfo and C_PetInfo.GetPetFoodTypes) then
|
|
return nil
|
|
end
|
|
local ok, a, b, c, d, e, f, g, h = pcall(C_PetInfo.GetPetFoodTypes)
|
|
if not ok then
|
|
return nil
|
|
end
|
|
local raw = { a, b, c, d, e, f, g, h }
|
|
if type(a) == "table" then
|
|
raw = a
|
|
end
|
|
local foods = {}
|
|
for _, value in ipairs(raw) do
|
|
if type(value) == "string" and #foods < MAX_FOODS then
|
|
foods[#foods + 1] = value:sub(1, 32)
|
|
end
|
|
end
|
|
if #foods == 0 then
|
|
return nil
|
|
end
|
|
return foods
|
|
end
|
|
|
|
local function ReadNpcID()
|
|
local guid = Util.Clean(UnitGUID("pet"))
|
|
if type(guid) ~= "string" then
|
|
return nil
|
|
end
|
|
local hk, hid = Util.ParseHexGUID(guid)
|
|
if hk then
|
|
if hk == "Pet" or hk == "Creature" then return hid end
|
|
return nil
|
|
end
|
|
local kind, _, _, _, _, id = strsplit("-", guid)
|
|
if kind == "Pet" or kind == "Creature" then
|
|
return tonumber(id)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Pet:BuildRecord()
|
|
local name = Util.Clean(UnitName("pet"))
|
|
if type(name) ~= "string" or name == "" or (UNKNOWN and name == UNKNOWN) then
|
|
return nil
|
|
end
|
|
|
|
local r = {
|
|
name = name,
|
|
family = Str(UnitCreatureFamily("pet")),
|
|
creatureType = Str(UnitCreatureType("pet")),
|
|
level = NumIn(UnitLevel("pet"), 1, 100),
|
|
npcID = ReadNpcID(),
|
|
updated = time(),
|
|
owner = Util.GetPlayerKey(),
|
|
src = "own",
|
|
}
|
|
if not r.level then
|
|
return nil
|
|
end
|
|
|
|
if C_PetInfo then
|
|
if C_PetInfo.GetPetHappiness then
|
|
local happiness, damagePct, loyaltyRate = C_PetInfo.GetPetHappiness()
|
|
r.happiness = NumIn(happiness, 1, 3)
|
|
r.damagePct = Round(damagePct)
|
|
r.loyaltyRate = Round(loyaltyRate)
|
|
end
|
|
if C_PetInfo.GetPetLoyalty then
|
|
local text = C_PetInfo.GetPetLoyalty()
|
|
if type(text) == "string" then
|
|
r.loyaltyText = text:sub(1, 80)
|
|
r.loyaltyLevel = NumIn(tonumber(text:match("(%d+)")), 1, 20)
|
|
end
|
|
end
|
|
if C_PetInfo.GetPetTrainingPoints then
|
|
local total, spent = C_PetInfo.GetPetTrainingPoints()
|
|
r.trainingTotal = Round(total)
|
|
r.trainingSpent = Round(spent)
|
|
end
|
|
end
|
|
|
|
r.speed = Round((UnitAttackSpeed("pet")), 2)
|
|
|
|
local dmgMin, dmgMax = UnitDamage("pet")
|
|
r.dmgMin = Round(dmgMin)
|
|
r.dmgMax = Round(dmgMax)
|
|
|
|
local armorBase, armorEffective = UnitArmor("pet")
|
|
r.armorBase = Round(armorBase)
|
|
r.armor = Round(armorEffective) or r.armorBase
|
|
|
|
r.hpMax = Round((UnitHealthMax("pet")))
|
|
|
|
-- Zusatzwerte (alle optional, jeweils geschuetzt gelesen)
|
|
local function Try(fn, ...)
|
|
if type(fn) ~= "function" then
|
|
return nil
|
|
end
|
|
local ok, a, b, c, d = pcall(fn, ...)
|
|
if ok then
|
|
return a, b, c, d
|
|
end
|
|
return nil
|
|
end
|
|
local apBase, apPos, apNeg = Try(UnitAttackPower, "pet")
|
|
if type(apBase) == "number" then
|
|
r.apBase = Round(apBase)
|
|
r.apBonus = Round((tonumber(apPos) or 0) + (tonumber(apNeg) or 0))
|
|
end
|
|
local _, runSpeed = Try(GetUnitSpeed, "pet")
|
|
if type(runSpeed) == "number" then
|
|
r.runSpeed = Round(runSpeed, 2)
|
|
end
|
|
r.resist = {}
|
|
for school = 1, 6 do
|
|
local _, total = Try(UnitResistance, "pet", school)
|
|
r.resist[school] = type(total) == "number" and Round(total) or nil
|
|
end
|
|
if next(r.resist) == nil then
|
|
r.resist = nil
|
|
end
|
|
local crit = Try(GetCritChance)
|
|
if type(crit) == "number" then
|
|
r.critMelee = Round(crit, 2)
|
|
end
|
|
local sCrit = Try(GetSpellCritChance, 2)
|
|
if type(sCrit) == "number" then
|
|
r.critSpell = Round(sCrit, 2)
|
|
end
|
|
local rCrit = Try(GetRangedCritChance)
|
|
if type(rCrit) == "number" then
|
|
r.critRanged = Round(rCrit, 2)
|
|
end
|
|
|
|
r.foods = ReadFoods()
|
|
r.abilities, r.others = ReadAbilities()
|
|
|
|
return r
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Speichern
|
|
------------------------------------------------------------------------
|
|
|
|
function Pet:Store(record)
|
|
local key = record.owner .. ":" .. record.name
|
|
local old = self.db.pets[key]
|
|
record.firstSeen = (old and old.firstSeen) or record.updated
|
|
self.db.pets[key] = record
|
|
self.activeKey = key
|
|
if not self.selectedKey then
|
|
self.selectedKey = key
|
|
end
|
|
FW:Fire("PET_UPDATED", key, record)
|
|
return key
|
|
end
|
|
|
|
-- Liest den aktiven Begleiter. Rueckgabe: true bei Erfolg, sonst false, Grund
|
|
function Pet:Scan()
|
|
if not UnitExists("pet") then
|
|
-- Begleiter entlassen oder noch nicht da: "aktiv" zuruecknehmen
|
|
if self.activeKey then
|
|
self.activeKey = nil
|
|
FW:Fire("PET_UPDATED", nil, nil)
|
|
end
|
|
return false, "nopet"
|
|
end
|
|
if InCombatLockdown and InCombatLockdown() then
|
|
self.pending = true
|
|
return false, "combat"
|
|
end
|
|
local ok, record = pcall(self.BuildRecord, self)
|
|
if not ok then
|
|
FW:Debug("Freewood-OctoWoW-Pet: Auslesen fehlgeschlagen:", record)
|
|
return false, "error"
|
|
end
|
|
if not record then
|
|
return false, "nodata"
|
|
end
|
|
self:Store(record)
|
|
return true
|
|
end
|
|
|
|
function Pet:RequestScan(delay)
|
|
if self.scanQueued then
|
|
return
|
|
end
|
|
self.scanQueued = true
|
|
C_Timer.After(delay or 0.6, function()
|
|
self.scanQueued = false
|
|
self:Scan()
|
|
end)
|
|
end
|
|
|
|
function Pet:Delete(key)
|
|
if self.db.pets[key] then
|
|
self.db.pets[key] = nil
|
|
if self.selectedKey == key then
|
|
self.selectedKey = nil
|
|
end
|
|
if self.activeKey == key then
|
|
self.activeKey = nil
|
|
end
|
|
FW:Fire("PET_UPDATED", key, nil)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Ereignisse
|
|
------------------------------------------------------------------------
|
|
|
|
function Pet:OnUnitPet(unit)
|
|
if unit == "player" then
|
|
self:RequestScan(1.0)
|
|
end
|
|
end
|
|
|
|
function Pet:OnRegenEnabled()
|
|
if self.pending then
|
|
self.pending = false
|
|
self:RequestScan(0.5)
|
|
end
|
|
end
|
|
|
|
function Pet:OnStatsChanged()
|
|
self:RequestScan(1.5)
|
|
end
|
|
|
|
function Pet:OnEnable()
|
|
FW:RegisterEvent("UNIT_PET", self.OnUnitPet, self)
|
|
FW:RegisterEvent("PET_STATS_UPDATE", self.OnStatsChanged, self)
|
|
FW:RegisterEvent("PET_BAR_UPDATE", self.OnStatsChanged, self)
|
|
FW:RegisterEvent("SPELLS_CHANGED", self.OnStatsChanged, self)
|
|
FW:RegisterEvent("PLAYER_REGEN_ENABLED", self.OnRegenEnabled, self)
|
|
FW:RegisterEvent("PLAYER_ENTERING_WORLD", self.OnStatsChanged, self)
|
|
self:RequestScan(2.0)
|
|
end
|
|
|
|
function Pet:OnDisable()
|
|
for _, event in ipairs({ "UNIT_PET", "PET_STATS_UPDATE", "PET_BAR_UPDATE", "SPELLS_CHANGED",
|
|
"PLAYER_REGEN_ENABLED", "PLAYER_ENTERING_WORLD" }) do
|
|
FW:UnregisterEvent(event, self)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Export / Import
|
|
-- Geteilt wird nur, was kein Persoenliches enthaelt: kein Besitzer, keine Notizen.
|
|
------------------------------------------------------------------------
|
|
|
|
local function SanitizeList(src, max, itemFn)
|
|
local list = {}
|
|
if type(src) ~= "table" then
|
|
return list
|
|
end
|
|
for i, item in ipairs(src) do
|
|
if i > max then
|
|
break
|
|
end
|
|
local clean = itemFn(item)
|
|
if clean then
|
|
list[#list + 1] = clean
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
-- Uebernimmt nur bekannte Felder mit gepruefter Form. Liefert nil, wenn der Eintrag unbrauchbar ist.
|
|
local function Sanitize(src)
|
|
if type(src) ~= "table" then
|
|
return nil
|
|
end
|
|
local name = Str(src.name, 48)
|
|
local level = NumIn(src.level, 1, 100)
|
|
if not name or not level then
|
|
return nil
|
|
end
|
|
|
|
local r = {
|
|
name = name,
|
|
level = level,
|
|
family = Str(src.family, 48),
|
|
creatureType = Str(src.creatureType, 48),
|
|
npcID = NumIn(src.npcID, 1, 10000000),
|
|
happiness = NumIn(src.happiness, 1, 3),
|
|
damagePct = NumIn(src.damagePct, 0, 1000),
|
|
loyaltyRate = NumIn(src.loyaltyRate, -1000, 1000),
|
|
loyaltyText = Str(src.loyaltyText, 80),
|
|
loyaltyLevel = NumIn(src.loyaltyLevel, 1, 20),
|
|
trainingTotal = NumIn(src.trainingTotal, 0, 100000),
|
|
trainingSpent = NumIn(src.trainingSpent, 0, 100000),
|
|
speed = NumIn(src.speed, 0, 100),
|
|
dmgMin = NumIn(src.dmgMin, 0, 10000000),
|
|
dmgMax = NumIn(src.dmgMax, 0, 10000000),
|
|
armor = NumIn(src.armor, 0, 10000000),
|
|
armorBase = NumIn(src.armorBase, 0, 10000000),
|
|
hpMax = NumIn(src.hpMax, 0, 100000000),
|
|
apBase = NumIn(src.apBase, 0, 10000000),
|
|
apBonus = NumIn(src.apBonus, -10000000, 10000000),
|
|
runSpeed = NumIn(src.runSpeed, 0, 100),
|
|
critMelee = NumIn(src.critMelee, 0, 100),
|
|
critSpell = NumIn(src.critSpell, 0, 100),
|
|
critRanged = NumIn(src.critRanged, 0, 100),
|
|
updated = NumIn(src.updated, 0, 4102444800) or 0,
|
|
}
|
|
|
|
if type(src.resist) == "table" then
|
|
local res = {}
|
|
for school = 1, 6 do
|
|
res[school] = NumIn(src.resist[school], 0, 100000)
|
|
end
|
|
if next(res) ~= nil then
|
|
r.resist = res
|
|
end
|
|
end
|
|
|
|
r.foods = SanitizeList(src.foods, MAX_FOODS, function(v)
|
|
return Str(v, 32)
|
|
end)
|
|
if #r.foods == 0 then
|
|
r.foods = nil
|
|
end
|
|
|
|
r.abilities = SanitizeList(src.abilities, MAX_ABILITIES, function(a)
|
|
if type(a) ~= "table" then
|
|
return nil
|
|
end
|
|
local abilityName = Str(a.name, 48)
|
|
if not abilityName then
|
|
return nil
|
|
end
|
|
return {
|
|
name = abilityName,
|
|
rank = NumIn(a.rank, 0, 20),
|
|
spellID = NumIn(a.spellID, 1, 10000000),
|
|
passive = (a.passive == true) or nil,
|
|
}
|
|
end)
|
|
|
|
r.others = SanitizeList(src.others, MAX_OTHERS, function(v)
|
|
return Str(v, 48)
|
|
end)
|
|
|
|
return r
|
|
end
|
|
|
|
function Pet:ExportData()
|
|
local list = {}
|
|
for _, record in pairs(self.db.pets) do
|
|
if record.src ~= "import" then
|
|
local clean = Sanitize(record)
|
|
if clean then
|
|
list[#list + 1] = clean
|
|
end
|
|
end
|
|
end
|
|
table.sort(list, function(a, b)
|
|
return (a.updated or 0) > (b.updated or 0)
|
|
end)
|
|
if #list == 0 then
|
|
return {}
|
|
end
|
|
return { list = list }
|
|
end
|
|
|
|
function Pet:ValidateImport(data)
|
|
if type(data) ~= "table" or type(data.list) ~= "table" then
|
|
return false, L.PET_IMPORT_EMPTY
|
|
end
|
|
if #data.list > MAX_IMPORT then
|
|
return false, L.PET_IMPORT_TOO_MANY
|
|
end
|
|
if #data.list == 0 then
|
|
return false, L.PET_IMPORT_EMPTY
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Pet:ImportData(data)
|
|
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 key = "import:" .. clean.name .. ":" .. (clean.family or "?") .. ":" .. clean.level
|
|
local existing = self.db.pets[key]
|
|
if existing and (existing.updated or 0) >= clean.updated then
|
|
skipped = skipped + 1
|
|
else
|
|
clean.src = "import"
|
|
clean.importedAt = time()
|
|
clean.firstSeen = (existing and existing.firstSeen) or clean.importedAt
|
|
self.db.pets[key] = clean
|
|
if existing then
|
|
updated = updated + 1
|
|
else
|
|
added = added + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
FW:Fire("PET_UPDATED", nil, nil)
|
|
return string.format(FW.L.PET_IMPORT_STATS, added, updated, skipped)
|
|
end
|
|
|
|
FW.Export.RegisterHandler("pets", {
|
|
label = function()
|
|
return FW.L.PET_EXPORT_LABEL
|
|
end,
|
|
export = function()
|
|
return Pet:ExportData()
|
|
end,
|
|
validate = function(data)
|
|
return Pet:ValidateImport(data)
|
|
end,
|
|
import = function(data)
|
|
return Pet:ImportData(data)
|
|
end,
|
|
})
|