You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

458 lines
13 KiB

local myname, ns = ...
local HBD = LibStub("HereBeDragons-2.0")
local addon = LibStub("AceAddon-3.0"):NewAddon("SilverDragon", "AceEvent-3.0", "AceTimer-3.0", "AceConsole-3.0")
SilverDragon = addon
SilverDragon.NAMESPACE = ns -- for separate addons
addon.events = LibStub("CallbackHandler-1.0"):New(addon)
ns.CLASSIC = WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE
ns.CLASSICERA = WOW_PROJECT_ID == WOW_PROJECT_CLASSIC -- forever vanilla
local faction = UnitFactionGroup("player")
local Debug
do
local TextDump = LibStub("LibTextDump-1.0")
local debuggable = GetAddOnMetadata(myname, "Version") == 'v2022.24'
local _window
local function GetDebugWindow()
if not _window then
_window = TextDump:New(myname)
end
return _window
end
addon.GetDebugWindow = GetDebugWindow
addon.Debug = function(...)
if not debuggable then return end
-- if debugf then debugf:AddMessage(string.join(", ", tostringall(...))) end
GetDebugWindow():AddLine(string.join(', ', tostringall(...)))
end
addon.DebugF = function(...)
if not debuggable then return end
Debug(string.format(...))
end
function addon:ShowDebugWindow()
local window = self.GetDebugWindow()
if window:Lines() == 0 then
window:AddLine("Nothing has happened yet")
window:Display()
window:Clear()
return
end
window:Display()
end
addon.debuggable = debuggable
Debug = addon.Debug
end
BINDING_HEADER_SILVERDRAGON = "SilverDragon"
_G["BINDING_NAME_CLICK SilverDragonPopupButton:LeftButton"] = "Target last found mob"
_G["BINDING_NAME_CLICK SilverDragonMacroButton:LeftButton"] = "Scan for nearby mobs"
addon.escapes = {
-- |TTexturePath:size1:size2:xoffset:yoffset:dimx:dimy:coordx1:coordx2:coordy1:coordy2|t
-- |A:atlas:height:width[:offsetX:offsetY]|a
leftClick = CreateAtlasMarkup("newplayertutorial-icon-mouse-leftbutton", 12, 15),
rightClick = CreateAtlasMarkup("newplayertutorial-icon-mouse-rightbutton", 12, 15),
keyDown = [[|TInterface\TUTORIALFRAME\UI-TUTORIAL-FRAME:0:0:0:-1:512:512:9:66:437:490|t]],
green = _G.GREEN_FONT_COLOR_CODE,
red = _G.RED_FONT_COLOR_CODE,
}
if ns.CLASSIC then
addon.escapes.leftClick = [[|TInterface\TUTORIALFRAME\UI-TUTORIAL-FRAME:19:11:-1:0:512:512:9:67:227:306|t]]
addon.escapes.rightClick = [[|TInterface\TUTORIALFRAME\UI-TUTORIAL-FRAME:20:12:0:-1:512:512:9:66:332:411|t]]
end
addon.datasources = {
--[[
["source name"] = {
[54321] = {
name = "Bob",
vignette = "something that isn't the name",
quest = 12345,
tameable = isTameable,
notes = "notes",
mount = hasMount,
boss = isBoss,
locations = {[zoneid] = {coord,...}},
-- TODO, phase should really be per-zone in locations, but that's more of a data-model change than I want to make right now.
phase = artID,
hidden = isHidden,
},
...
}
--]]
}
addon.treasuresources = {}
local mobdb = setmetatable({}, {
__index = function(t, id)
for source, data in pairs(addon.datasources) do
if data[id] and addon.db.global.datasources[source] then
t[id] = data[id]
return data[id]
end
end
t[id] = false
return false
end,
})
ns.mobdb = mobdb
local mobsByZone = {
-- [zoneid] = { [mobid] = {coord, ...}
}
ns.mobsByZone = mobsByZone
local mobNamesByZone = {
-- [zoneid] = { [mobname] = mobid, ... }
}
ns.mobNamesByZone = mobNamesByZone
local questMobLookup = {
-- [questid] = { [mobid] = true, ... }
}
ns.questMobLookup = questMobLookup
local vignetteMobLookup = {
-- [name] = { [mobid] = true, ... }
}
ns.vignetteMobLookup = vignetteMobLookup
ns.vignetteTreasureLookup = {
-- [vignetteid] = { data },
}
function addon:RegisterMobData(source, data, updated)
if not updated then
if not self.HASWARNEDABOUTOLDDATA then
self.HASWARNEDABOUTOLDDATA = true
return self:Print("You have an old SilverDragon_[expansion] folder, which can be removed")
end
return
end
addon.datasources[source] = data
end
function addon:RegisterTreasureData(source, data, updated)
if not updated then return end
addon.treasuresources[source] = data
end
do
local function addQuestMobLookup(mobid, quest)
if type(quest) == "table" then
if quest.alliance then
return addQuestMobLookup(mobid, faction == "Alliance" and quest.alliance or quest.horde)
end
for _, questid in ipairs(quest) do
if not questMobLookup[quest] then
questMobLookup[quest] = {}
end
questMobLookup[quest][mobid] = true
end
else
if not questMobLookup[quest] then
questMobLookup[quest] = {}
end
questMobLookup[quest][mobid] = true
end
end
local function addMobToLookups(mobid, mobdata)
if mobdata.hidden then
return
end
if mobdata.locations then
for zoneid, coords in pairs(mobdata.locations) do
if not mobsByZone[zoneid] then
mobsByZone[zoneid] = {}
end
mobsByZone[zoneid][mobid] = coords
end
end
-- In the olden days, we had one mob per quest and/or vignette. Alas...
if mobdata.quest then
addQuestMobLookup(mobid, mobdata.quest)
end
if mobdata.vignette then
local vignetteMobs = vignetteMobLookup[mobdata.vignette]
if not vignetteMobs then
vignetteMobs = {}
vignetteMobLookup[mobdata.vignette] = vignetteMobs
end
vignetteMobs[mobid] = true
end
end
function addon:BuildLookupTables()
wipe(mobdb)
wipe(mobsByZone)
wipe(questMobLookup)
wipe(vignetteMobLookup)
for source, data in pairs(addon.datasources) do
if addon.db.global.datasources[source] then
for mobid, mobdata in pairs(data) do
self:NameForMob(mobid) -- prime cache
mobdata.id = mobid
mobdata.source = source
addMobToLookups(mobid, mobdata)
end
end
end
for source, data in pairs(addon.treasuresources) do
if addon.db.global.datasources[source] then
for vignetteid, vignettedata in pairs(data) do
ns.vignetteTreasureLookup[vignetteid] = vignettedata
end
end
end
self.events:Fire("Ready")
end
end
local globaldb
function addon:OnInitialize()
self.db = LibStub("AceDB-3.0"):New("SilverDragon3DB", {
global = {
mob_seen = {
-- 132132 = time()
},
mob_count = {
['*'] = 0,
},
datasources = {
['*'] = true,
},
always = {
},
ignore = {
[64403] = true, -- Alani
},
ignore_datasource = {
-- "BurningCrusade" = true,
},
},
locale = {
quest_name = {
-- store localized quest names
-- [id] = "name"
},
mob_name = {
-- store localized mob names
-- [id] = "name"
},
},
profile = {
scan = 1, -- scan interval, 0 for never
delay = 1200, -- number of seconds to wait between recording the same mob
instances = false,
taxi = true,
charloot = false,
lootappearances = true,
},
}, true)
globaldb = self.db.global
if SilverDragon2DB and SilverDragon2DB.global then
-- Migrating some data from v2
for mobid, when in pairs(SilverDragon2DB.global.mob_seen or {}) do
if when > 0 then
globaldb.mob_seen[mobid] = when
end
end
for mobid, count in pairs(SilverDragon2DB.global.mob_count or {}) do
globaldb.mob_count[mobid] = count
end
for mobid, watching in pairs(SilverDragon2DB.global.always or {}) do
globaldb.always[mobid] = watching
end
for mobid, ignored in pairs(SilverDragon2DB.global.ignore or {}) do
globaldb.ignore[mobid] = ignored
end
_G["SilverDragon2DB"] = nil
end
end
function addon:OnEnable()
self:BuildLookupTables()
if self.db.profile.scan > 0 then
self:ScheduleRepeatingTimer("CheckNearby", self.db.profile.scan)
end
end
-- returns true if the change had an effect
function addon:SetIgnore(id, ignore, quiet)
if not id then return false end
if (ignore and globaldb.ignore[id]) or (not ignore and not globaldb.ignore[id]) then
-- to avoid the nil/false issue
return false
end
globaldb.ignore[id] = ignore
if not quiet then
self.events:Fire("IgnoreChanged", id, globaldb.ignore[id])
end
return true
end
-- returns true if the change had an effect
function addon:SetCustom(id, watch, quiet)
if not id then return false end
if (watch and globaldb.always[id]) or (not watch and not globaldb.always[id]) then
-- to avoid the nil/false issue
return false
end
globaldb.always[id] = watch or nil
if not quiet then
self.events:Fire("CustomChanged", id, globaldb.always[id])
end
return true
end
-- returns name, vignette, tameable, last_seen, times_seen
function addon:GetMobInfo(id)
if mobdb[id] then
local m = mobdb[id]
local name = self:NameForMob(id)
return name, m.vignette or name, m.tameable, globaldb.mob_seen[id], globaldb.mob_count[id]
end
end
function addon:IsMobInZone(id, zone)
if mobsByZone[zone] then
return mobsByZone[zone][id]
end
end
do
local poi_expirations = {}
local poi_zone_expirations = {}
local pois_byzone = {}
local function refreshPois(zone)
local now = time()
if not poi_zone_expirations[zone] or now > poi_zone_expirations[zone] then
Debug("Refreshing zone POIs", zone)
pois_byzone[zone] = wipe(pois_byzone[zone] or {})
for _, poi in ipairs(C_AreaPoiInfo.GetAreaPOIForMap(zone)) do
pois_byzone[zone][poi] = true
poi_expirations[poi] = now + (C_AreaPoiInfo.GetAreaPOISecondsLeft(poi) or 60)
end
poi_zone_expirations[zone] = now + 1
end
end
local function checkPois(...)
for i=1, select("#", ...), 2 do
local zone, poi = select(i, ...)
local now = time()
if now > (poi_expirations[poi] or 0) then
refreshPois(zone)
poi_expirations[poi] = poi_expirations[poi] or (now + 60)
end
if pois_byzone[zone][poi] then
return true
end
end
end
function addon:IsMobInPhase(id, zone)
local phased, poi = true, true
if not mobdb[id] then return end
if mobdb[id].phase then
phased = mobdb[id].phase == C_Map.GetMapArtID(zone)
end
if mobdb[id].poi then
poi = checkPois(unpack(mobdb[id].poi))
end
return phased and poi
end
end
-- Returns id, addon:GetMobInfo(id)
function addon:GetMobByCoord(zone, coord, include_ignored)
if not mobsByZone[zone] then return end
for id, locations in pairs(mobsByZone[zone]) do
if self:IsMobInPhase(id, zone) and include_ignored or not self:ShouldIgnoreMob(id) then
for _, mob_coord in ipairs(locations) do
if coord == mob_coord then
return id, self:GetMobInfo(id)
end
end
end
end
end
function addon:GetMobLabel(id)
local name = self:NameForMob(id)
if not name then
return UNKNOWN
end
if not (mobdb[id] and mobdb[id].variant) then
return name
end
return name .. (" (" .. mobdb[id].variant .. ")")
end
do
local lastseen = {}
function addon:NotifyForMob(id, zone, x, y, is_dead, source, unit, silent, force, vignetteGUID)
self.events:Fire("Seen_Raw", id, zone, x, y, is_dead, source, unit)
if silent then
Debug("Skipping notification: silent call", id, source)
return
end
if self:ShouldIgnoreMob(id, zone) then
Debug("Skipping notification: ignored", id, source)
return
end
if not force and lastseen[id..zone] and time() < lastseen[id..zone] + self.db.profile.delay then
Debug("Skipping notification: seen", id, lastseen[id..zone], time() - self.db.profile.delay, source)
return
end
if (not self.db.profile.taxi) and UnitOnTaxi('player') then
Debug("Skipping notification: taxi", id, source)
return
end
globaldb.mob_count[id] = globaldb.mob_count[id] + 1
globaldb.mob_seen[id] = time()
lastseen[id..zone] = time()
self.events:Fire("Seen", id, zone, x or 0, y or 0, is_dead, source, unit, vignetteGUID)
return true
end
end
do
local zone_ignores = {
[550] = {
[32491] = true, -- Time-Lost
},
}
function addon:ShouldIgnoreMob(id, zone)
if globaldb.ignore[id] then
return true
end
if globaldb.always[id] then
-- If you've manually added a mob we should take that a signal that you always want it announced
-- (Unless you've also, weirdly, manually told it to be ignored as well.)
return false
end
if zone and zone_ignores[zone] and zone_ignores[zone][id] then
return true
end
if mobdb[id] then
if mobdb[id].hidden then
return true
end
if mobdb[id].faction == faction then
--This checks unit faction and ignores mobs your faction cannot do anything with.
--TODO: add an option for this?
return true
end
if mobdb[id].source and globaldb.ignore_datasource[mobdb[id].source] then
return true
end
end
end
end
-- Scanning:
function addon:CheckNearby()
if (not self.db.profile.instances) and IsInInstance() then return end
local zone = HBD:GetPlayerZone()
if not zone then return end
self.events:Fire("Scan", zone)
end