ONNReporter/ONNReporter.lua568 linhas
-- ONN Reporter: records game data for the Olympus News Network database.
--
-- What it records (and nothing else):
-- * NPCs you target, mouse over or talk to: name, level, type, and where they stand
-- * Loot from corpses, herbs, ore, chests and fishing (which items, how many)
-- * What vendors sell and for how much
-- * Quests: everything in the quest windows (description, objectives, the "do you have them yet"
-- and completion text, required items, rewards), who gives and ends them, and where objectives progress
-- * Books, letters and plaques you read: their pages
-- * Item details and tooltips for items you see
-- * Flight paths and map names
-- * What NPCs say: gossip windows and the lines they say, yell or emote nearby
-- It never records other players, player chat, your gold, your bags or anything about your account.
-- Your character's name is replaced with $N in any text NPCs say to you.
-- Everything is kept in ONNReporterDB (the SavedVariables file). ONN Companion reads that file
-- when WoW saves it (logout or /reload) and sends it to olympusnewsnetwork.com.
local ADDON = ...
local VERSION = "1.2.0"
local GRID = 200 -- positions are rounded to 0.5% of the map
local MAX_CELLS = 120 -- per NPC/object/quest per map, so the file stays small
local MAX_ITEMS = 40000
local db
local f = CreateFrame("Frame")
-- ---------- helpers ----------
local unpackf = table.unpack or unpack
local function pack(...) return { n = select("#", ...), ... } end
-- Call a game function without letting an error (or a missing function) break the addon.
local function safe(fn, ...)
if type(fn) ~= "function" then return end
local r = pack(pcall(fn, ...))
if r[1] then return unpackf(r, 2, r.n) end
end
local function now() return time() end
-- "Creature-0-1234-0-56-12345-0000ABCDEF" -> "Creature", 12345, "0000ABCDEF"
local function parseGUID(guid)
if type(guid) ~= "string" then return end
local kind, _, _, _, _, id, spawn = strsplit("-", guid)
id = tonumber(id)
if not id then return end
return kind, id, spawn
end
local function sourceKey(guid)
local kind, id = parseGUID(guid)
if kind == "Creature" or kind == "Vehicle" then return "c:" .. id, "c", id end
if kind == "GameObject" then return "o:" .. id, "o", id end
end
local function playerPos()
local map = safe(C_Map.GetBestMapForUnit, "player")
if not map then return end
local p = safe(C_Map.GetPlayerMapPosition, map, "player")
if not p then return end
local x, y
if p.GetXY then x, y = p:GetXY() else x, y = p.x, p.y end
if not x or not y or (x == 0 and y == 0) then return end
return map, math.floor(x * GRID + 0.5), math.floor(y * GRID + 0.5)
end
local function noteMap(map)
if not map or db.map[map] then return end
local info = safe(C_Map.GetMapInfo, map)
if info then db.map[map] = { n = info.name, p = info.parentMapID, t = info.mapType } end
end
local function addPos(store, id, map, x, y)
if not map then return end
noteMap(map)
store[id] = store[id] or {}
local m = store[id][map]
if not m then m = {}; store[id][map] = m end
local key = x .. "_" .. y
if m[key] then m[key] = m[key] + 1; return end
local n = 0
for _ in pairs(m) do n = n + 1; if n >= MAX_CELLS then return end end
m[key] = 1
end
-- Game text often includes your character's name ("Thanks, Aelric!"). Swap it for $N, the way
-- the game itself writes it, so no player names are ever kept.
local function scrub(text)
if type(text) ~= "string" or text == "" then return text end
local me = safe(UnitName, "player")
if type(me) == "string" and #me > 1 then text = text:gsub(me:gsub("(%p)", "%%%1"), "$N") end
return text:sub(1, 1500)
end
local function itemIdFromLink(link)
if type(link) ~= "string" then return end
return tonumber(link:match("item:(%d+)"))
end
-- ---------- items: details and tooltips, gathered in the background ----------
-- Every item you see counts: loot, vendors, quest rewards, and any item tooltip you look at
-- (bags, bank, chat links, the auction house, other players' gear when you hover it). Only the
-- item's own details are kept, never where you saw it or who had it.
local itemQueue, queued, itemCount = {}, {}, nil
local function wantItem(id)
id = tonumber(id)
if not id or id <= 0 or not db or queued[id] then return end
local it = db.item[id]
if it and it.b == db.build then return end
queued[id] = true
itemQueue[#itemQueue + 1] = id
end
local function tooltipLines(id)
local data = C_TooltipInfo and safe(C_TooltipInfo.GetItemByID, id)
if not data or not data.lines then return end
local out = {}
for i, line in ipairs(data.lines) do
local l, r = line.leftText, line.rightText
if type(l) == "string" and l:find(RETRIEVING_ITEM_INFO or "Retrieving item information", 1, true) then return false end
if i > 1 then
if l and l ~= "" then out[#out + 1] = (r and r ~= "") and (l .. "\t" .. r) or l end
end
if #out >= 30 then break end
end
return out
end
local function readItem(id)
local get = (C_Item and C_Item.GetItemInfo) or GetItemInfo
if not get then return false end
local name, _, quality, ilvl, req, typ, sub, stack, slot, _, sell = safe(get, id)
if not name then
if C_Item and C_Item.RequestLoadItemDataByID then safe(C_Item.RequestLoadItemDataByID, id) end
return false
end
if not itemCount then itemCount = 0; for _ in pairs(db.item) do itemCount = itemCount + 1 end end
if itemCount >= MAX_ITEMS and not db.item[id] then return true end
local tip = tooltipLines(id)
if tip == false then return false end -- the tooltip isn't loaded yet: try again shortly
if not db.item[id] then itemCount = itemCount + 1 end
db.item[id] = { n = name, q = quality, il = ilvl, rl = req, ty = typ, st = sub, sk = stack, eq = slot, sp = sell, tip = tip, b = db.build }
return true
end
local tries = {}
C_Timer.NewTicker(0.25, function()
if not db or InCombatLockdown() then return end
for _ = 1, 8 do
local id = table.remove(itemQueue, 1)
if not id then return end
if readItem(id) then queued[id] = nil
else
tries[id] = (tries[id] or 0) + 1
if tries[id] < 20 then itemQueue[#itemQueue + 1] = id else queued[id] = nil; tries[id] = nil end
end
end
end)
-- The game finished loading an item we asked about: read it right away.
local function itemLoaded(id, ok)
id = tonumber(id)
if id and queued[id] and ok ~= false and not InCombatLockdown() and readItem(id) then queued[id] = nil end
end
-- Any item tooltip shown anywhere.
local function hookTooltips()
if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum and Enum.TooltipDataType and Enum.TooltipDataType.Item then
TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, function(tt, data)
if data and data.id then wantItem(data.id) return end
if tt and tt.GetItem then local _, link = tt:GetItem(); wantItem(itemIdFromLink(link)) end
end)
return
end
for _, tt in ipairs({ GameTooltip, ItemRefTooltip, ShoppingTooltip1, ShoppingTooltip2 }) do
if tt and tt.HookScript then pcall(tt.HookScript, tt, "OnTooltipSetItem", function(self) local _, link = self:GetItem(); wantItem(itemIdFromLink(link)) end) end
end
end
-- ---------- NPCs ----------
local function noteUnit(unit, near)
if not UnitExists(unit) or UnitIsPlayer(unit) then return end
if safe(UnitPlayerControlled, unit) then return end -- pets and minions
local kind, id = parseGUID(UnitGUID(unit))
if kind ~= "Creature" or not id then return end
local e = db.npc[id] or {}
e.n = safe(UnitName, unit) or e.n
local lvl = safe(UnitLevel, unit)
if type(lvl) == "number" and lvl > 0 then
e.l = e.l and math.min(e.l, lvl) or lvl
e.L = e.L and math.max(e.L, lvl) or lvl
end
e.c = safe(UnitClassification, unit) or e.c
e.t = safe(UnitCreatureType, unit) or e.t
local r = safe(UnitReaction, unit, "player")
if type(r) == "number" then e.r = r end
e.s = now()
db.npc[id] = e
if near then
local map, x, y = playerPos()
if map then addPos(db.npos, id, map, x, y) end
end
end
-- Close enough that the player's position is the NPC's position (about 10 yards).
local function isNear(unit)
local ok = safe(CheckInteractDistance, unit, 3)
return ok and true or false
end
-- ---------- loot ----------
local function onLoot()
local n = GetNumLootItems and GetNumLootItems() or 0
if n == 0 then return end
local fishing = IsFishingLoot and IsFishingLoot()
local sources = {}
for slot = 1, n do
local link = GetLootSlotLink(slot)
local item = itemIdFromLink(link)
local info = { safe(GetLootSourceInfo, slot) } -- guid1, qty1, guid2, qty2, ...
if fishing then
local map = playerPos()
if map then info = { "fish:" .. map, select(3, GetLootSlotInfo(slot)) or 1 } end
end
for i = 1, #info, 2 do
local guid, qty = info[i], tonumber(info[i + 1]) or 1
local key, kind, id
if type(guid) == "string" and guid:sub(1, 5) == "fish:" then key = guid
else key, kind, id = sourceKey(guid) end
if key then
local _, _, spawn = parseGUID(guid)
sources[key] = sources[key] or { guid = guid, kind = kind, id = id, spawn = spawn, items = {} }
if item then
local s = sources[key].items
s[item] = (s[item] or 0) + qty
wantItem(item)
end
end
end
end
local t = now()
for key, s in pairs(sources) do
-- Each corpse or node counts once, even if you open its loot twice.
local seenKey = s.kind and s.guid or (key .. ":" .. t .. ":" .. math.random(1e6))
if not db.seen[seenKey] then
db.seen[seenKey] = t
local L = db.loot[key] or { o = 0, i = {} }
L.o = L.o + 1
for item, qty in pairs(s.items) do
local e = L.i[item] or { c = 0, q = 0 }
e.c, e.q = e.c + 1, e.q + qty
L.i[item] = e
end
db.loot[key] = L
if s.kind == "o" then
local map, x, y = playerPos()
if map then addPos(db.opos, s.id, map, x, y) end
end
end
end
end
-- ---------- vendors ----------
local function onMerchant()
local kind, npc = parseGUID(UnitGUID("npc"))
if kind ~= "Creature" then return end
noteUnit("npc", true)
local count = GetMerchantNumItems and GetMerchantNumItems() or 0
local out = {}
for i = 1, count do
local link = GetMerchantItemLink(i)
local id = itemIdFromLink(link)
if id then
local price, stack, avail, extended
if C_MerchantFrame and C_MerchantFrame.GetItemInfo then
local info = safe(C_MerchantFrame.GetItemInfo, i)
if info then price, stack, avail, extended = info.price, info.stackCount, info.numAvailable, info.hasExtendedCost end
elseif GetMerchantItemInfo then
local _, _, p, s, a, _, _, ext = GetMerchantItemInfo(i)
price, stack, avail, extended = p, s, a, ext
end
out[id] = { p = price, s = stack, a = avail, x = extended and 1 or nil }
wantItem(id)
end
end
db.vend[npc] = { t = now(), i = out }
end
-- ---------- quests ----------
local function giverKey(unit)
local key = sourceKey(UnitGUID(unit))
return key
end
-- Items in one part of the quest window: "choice", "reward" or "required", with how many.
local function questItems(kind, count)
local ids, qty = {}, {}
for i = 1, count or 0 do
local id = itemIdFromLink(safe(GetQuestItemLink, kind, i))
if id then
local _, _, n = safe(GetQuestItemInfo, kind, i)
ids[#ids + 1], qty[#qty + 1] = id, tonumber(n) or 1
wantItem(id)
end
end
return ids, qty
end
local function rewards(q)
local r = {}
r.c, r.cn = questItems("choice", safe(GetNumQuestChoices))
r.g, r.gn = questItems("reward", safe(GetNumQuestRewards))
-- Spells or abilities the quest teaches or casts on you.
local sp = {}
for i = 1, (safe(GetNumRewardSpells) or 0) do
local _, name = safe(GetRewardSpell, i)
if type(name) == "string" then sp[#sp + 1] = name end
end
if #sp > 0 then r.sp = sp end
q.m = safe(GetRewardMoney) or q.m
q.xp = safe(GetRewardXP) or q.xp
q.hn = safe(GetRewardHonor) or q.hn
q.r = r
end
local function questEntry(id)
local q = db.quest[id] or {}
db.quest[id] = q
return q
end
local function onQuestDetail()
local id = GetQuestID and GetQuestID()
if not id or id == 0 then return end
local q = questEntry(id)
q.t = GetTitleText() or q.t
q.d = scrub(GetQuestText()) or q.d
q.o = scrub(GetObjectiveText()) or q.o
local sg = safe(GetSuggestedGroupNum); if type(sg) == "number" and sg > 0 then q.sg = sg end
-- Started from an item in your bags rather than an NPC.
if not UnitExists("questnpc") and not UnitExists("npc") then q.si = 1 end
local giver = giverKey("questnpc") or giverKey("npc")
if giver then q.s = giver; if giver:sub(1, 1) == "c" then noteUnit("npc", true) end end
rewards(q)
end
-- "Do you have the items yet?": the text, plus the items and money the quest asks for.
local function onQuestProgress()
local id = GetQuestID and GetQuestID()
if not id or id == 0 then return end
local q = questEntry(id)
q.t = GetTitleText() or q.t
q.pt = scrub(safe(GetProgressText)) or q.pt
q.rq, q.rqn = questItems("required", safe(GetNumQuestItems))
local money = safe(GetQuestMoneyToGet); if type(money) == "number" and money > 0 then q.rm = money end
local ender = giverKey("questnpc") or giverKey("npc")
if ender then q.e = ender end
end
-- An NPC with several quests shows a greeting first.
local function onQuestGreeting()
noteUnit("npc", true)
local kind, id = parseGUID(UnitGUID("npc"))
if kind ~= "Creature" or not id then return end
local e = db.npc[id] or {}
db.npc[id] = e
e.gr = scrub(safe(GetGreetingText)) or e.gr
end
-- The quest log's own text, for quests picked up some other way (shared, auto-accepted...).
local function questLogText(index, q)
if q.d and q.o then return end
if QuestLogFrame and QuestLogFrame.IsShown and QuestLogFrame:IsShown() then return end
local desc, obj = safe(GetQuestLogQuestText, index)
if type(desc) == "string" and desc ~= "" then q.d = q.d or scrub(desc) end
if type(obj) == "string" and obj ~= "" then q.o = q.o or scrub(obj) end
end
local function onQuestComplete()
local id = GetQuestID and GetQuestID()
if not id or id == 0 then return end
local q = questEntry(id)
q.t = GetTitleText() or q.t
q.ct = GetRewardText and scrub(GetRewardText()) or q.ct
local ender = giverKey("questnpc") or giverKey("npc")
if ender then q.e = ender; if ender:sub(1, 1) == "c" then noteUnit("npc", true) end end
rewards(q)
end
-- Objective progress: remember where the counter went up.
local progress = {}
local function scanQuestLog(record)
if not C_QuestLog or not C_QuestLog.GetNumQuestLogEntries then return end
for i = 1, C_QuestLog.GetNumQuestLogEntries() do
local info = safe(C_QuestLog.GetInfo, i)
if info and not info.isHeader and info.questID and info.questID > 0 then
local id = info.questID
local q = questEntry(id)
q.t = info.title or q.t
q.l = info.level or q.l
if info.suggestedGroup and info.suggestedGroup > 0 then q.sg = info.suggestedGroup end
questLogText(i, q)
local objs = safe(C_QuestLog.GetQuestObjectives, id)
if objs then
local list, done = {}, 0
for _, o in ipairs(objs) do
list[#list + 1] = { o.text, o.type, o.numRequired }
done = done + (o.numFulfilled or 0)
end
q.ob = list
if record and progress[id] and done > progress[id] then
local map, x, y = playerPos()
if map then addPos(db.qpos, id, map, x, y) end
end
progress[id] = done
end
end
end
end
-- ---------- flight paths ----------
local function onTaxi()
noteUnit("npc", true)
local map = GetTaxiMapID and GetTaxiMapID()
if not map or not C_TaxiMap or not C_TaxiMap.GetAllTaxiNodes then return end
noteMap(map)
local nodes = safe(C_TaxiMap.GetAllTaxiNodes, map)
if not nodes then return end
db.taxi[map] = db.taxi[map] or {}
for _, n in ipairs(nodes) do
if n.name and n.position then
db.taxi[map][n.nodeID or n.name] = { n = n.name, x = math.floor((n.position.x or 0) * GRID + 0.5), y = math.floor((n.position.y or 0) * GRID + 0.5) }
end
end
end
-- ---------- books, letters and plaques ----------
local reading = nil
local function onTextBegin()
local title = safe(ItemTextGetItem)
local key
local kind, id = parseGUID(UnitGUID("npc"))
if kind == "GameObject" and id then key = "o:" .. id end
if not key then
-- A book or letter in your bags: match it by name to an item we know.
for itemId, it in pairs(db.item) do if it.n == title then key = "i:" .. itemId; break end end
end
if not key and type(title) == "string" then key = "n:" .. title:gsub("[^%w]", "_"):sub(1, 36) end
if not key then reading = nil; return end
local t = db.text[key] or { n = title, p = {} }
t.n = title or t.n
db.text[key] = t
reading = t
if kind == "GameObject" and id then local map, x, y = playerPos(); if map then addPos(db.opos, id, map, x, y) end end
end
local function onTextReady()
if not reading then onTextBegin() end
if not reading then return end
local page = safe(ItemTextGetPage) or 1
local text = scrub(safe(ItemTextGetText))
if type(text) == "string" and text ~= "" and page >= 1 and page <= 50 then reading.p[page] = text end
end
-- ---------- what NPCs say ----------
local MAX_LINES = 40
local function addLine(id, text, kind)
text = scrub(text)
if not id or type(text) ~= "string" or text == "" then return end
local e = db.npc[id] or {}
db.npc[id] = e
e.say = e.say or {}
for _, l in ipairs(e.say) do
if l[1] == text and l[2] == kind then l[3] = (l[3] or 1) + 1; return end
end
if #e.say < MAX_LINES then e.say[#e.say + 1] = { text, kind, 1 } end
end
-- The gossip window: the NPC's greeting and the choices it offers.
local function onGossip()
noteUnit("npc", true)
local kind, id = parseGUID(UnitGUID("npc"))
if kind ~= "Creature" or not id then return end
local e = db.npc[id] or {}
db.npc[id] = e
local text = C_GossipInfo and safe(C_GossipInfo.GetText) or (GetGossipText and safe(GetGossipText))
local opts = {}
local list = C_GossipInfo and safe(C_GossipInfo.GetOptions)
if type(list) == "table" then
for _, o in ipairs(list) do if o.name then opts[#opts + 1] = scrub(o.name) end; if #opts >= 15 then break end end
end
local qs = {}
for _, fn in ipairs({ C_GossipInfo and C_GossipInfo.GetAvailableQuests, C_GossipInfo and C_GossipInfo.GetActiveQuests }) do
local l = fn and safe(fn)
if type(l) == "table" then for _, q in ipairs(l) do if q.questID then qs[#qs + 1] = q.questID end end end
end
e.g = { t = scrub(text), o = opts, q = qs }
end
-- NPCs talking nearby (say, yell, emote, whisper to you, boss emotes). Player chat is never read:
-- these events only carry lines spoken by NPCs.
local SPEECH = { CHAT_MSG_MONSTER_SAY = "say", CHAT_MSG_MONSTER_YELL = "yell", CHAT_MSG_MONSTER_EMOTE = "emote", CHAT_MSG_MONSTER_WHISPER = "whisper", CHAT_MSG_RAID_BOSS_EMOTE = "boss", CHAT_MSG_RAID_BOSS_WHISPER = "boss" }
local function onSpeech(ev, ...)
local text, guid = (...), select(12, ...)
local kind, id = parseGUID(guid)
if kind ~= "Creature" or not id then return end
local e = db.npc[id]
if not e then db.npc[id] = { n = select(2, ...) }; e = db.npc[id] end
addLine(id, text, SPEECH[ev])
end
-- ---------- setup ----------
local function prune()
local cut = now() - 6 * 3600
for k, t in pairs(db.seen) do if t < cut then db.seen[k] = nil end end
end
local function onEvent(ev, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)
if ev == "ADDON_LOADED" then
if a1 ~= ADDON then return end
ONNReporterDB = ONNReporterDB or {}
db = ONNReporterDB
for _, k in ipairs({ "npc", "npos", "loot", "seen", "vend", "quest", "qpos", "item", "opos", "taxi", "map", "text" }) do db[k] = db[k] or {} end
local version, build = GetBuildInfo()
db.v, db.addon, db.build, db.locale = 1, VERSION, (version or "") .. "." .. (build or ""), GetLocale()
db.n = (db.n or 0) + 1
prune()
pcall(hookTooltips)
return
end
if not db then return end
if ev == "UPDATE_MOUSEOVER_UNIT" then noteUnit("mouseover", isNear("mouseover"))
elseif ev == "PLAYER_TARGET_CHANGED" then noteUnit("target", isNear("target"))
elseif ev == "GOSSIP_SHOW" then onGossip()
elseif SPEECH[ev] then onSpeech(ev, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)
elseif ev == "TRAINER_SHOW" or ev == "BANKFRAME_OPENED" then noteUnit("npc", true)
elseif ev == "LOOT_READY" or ev == "LOOT_OPENED" then onLoot()
elseif ev == "MERCHANT_SHOW" then onMerchant()
elseif ev == "TAXIMAP_OPENED" then onTaxi()
elseif ev == "QUEST_DETAIL" then onQuestDetail()
elseif ev == "QUEST_COMPLETE" then onQuestComplete()
elseif ev == "QUEST_PROGRESS" then onQuestProgress()
elseif ev == "QUEST_GREETING" then onQuestGreeting()
elseif ev == "ITEM_TEXT_BEGIN" then onTextBegin()
elseif ev == "ITEM_TEXT_READY" then onTextReady()
elseif ev == "ITEM_TEXT_CLOSED" then reading = nil
elseif ev == "QUEST_LOG_UPDATE" or ev == "QUEST_ACCEPTED" then scanQuestLog(ev == "QUEST_LOG_UPDATE")
elseif ev == "GET_ITEM_INFO_RECEIVED" or ev == "ITEM_DATA_LOAD_RESULT" then itemLoaded(a1, a2)
elseif ev == "PLAYER_LOGOUT" then prune()
end
end
-- One bad event (or a value the game keeps hidden in combat) never breaks the addon.
f:SetScript("OnEvent", function(_, ...) pcall(onEvent, ...) end)
for _, ev in ipairs({ "ADDON_LOADED", "UPDATE_MOUSEOVER_UNIT", "PLAYER_TARGET_CHANGED", "GOSSIP_SHOW", "TRAINER_SHOW", "BANKFRAME_OPENED", "LOOT_READY", "LOOT_OPENED", "MERCHANT_SHOW", "TAXIMAP_OPENED", "QUEST_DETAIL", "QUEST_COMPLETE", "QUEST_LOG_UPDATE", "QUEST_ACCEPTED", "GET_ITEM_INFO_RECEIVED", "ITEM_DATA_LOAD_RESULT", "PLAYER_LOGOUT", "CHAT_MSG_MONSTER_SAY", "CHAT_MSG_MONSTER_YELL", "CHAT_MSG_MONSTER_EMOTE", "CHAT_MSG_MONSTER_WHISPER", "CHAT_MSG_RAID_BOSS_EMOTE", "CHAT_MSG_RAID_BOSS_WHISPER", "QUEST_PROGRESS", "QUEST_GREETING", "ITEM_TEXT_BEGIN", "ITEM_TEXT_READY", "ITEM_TEXT_CLOSED" }) do
pcall(f.RegisterEvent, f, ev)
end
-- /onnr: what's been recorded so far.
SLASH_ONNREPORTER1 = "/onnr"
SlashCmdList.ONNREPORTER = function(msg)
if not db then return end
local function count(t) local n = 0; for _ in pairs(t) do n = n + 1 end; return n end
if msg == "reset" then
for _, k in ipairs({ "npc", "npos", "loot", "seen", "vend", "quest", "qpos", "item", "opos", "taxi", "map", "text" }) do db[k] = {} end
itemCount = nil; wipe(itemQueue); wipe(queued)
print("|cffeadfb8ONN Reporter|r: cleared. It starts recording again right away.")
return
end
print(("|cffeadfb8ONN Reporter %s|r: %d NPCs, %d loot sources, %d vendors, %d quests, %d items (%d waiting to load), %d flight maps. ONN Companion sends this when you log out or /reload."):format(
VERSION, count(db.npc), count(db.loot), count(db.vend), count(db.quest), count(db.item), #itemQueue, count(db.taxi)))
end