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.

666 lines
22 KiB

--[[
HandyNotes
]]
---------------------------------------------------------
-- Addon declaration
HandyNotes = LibStub("AceAddon-3.0"):NewAddon("HandyNotes", "AceConsole-3.0", "AceEvent-3.0")
local HandyNotes = HandyNotes
local L = LibStub("AceLocale-3.0"):GetLocale("HandyNotes", false)
local HBD = LibStub("HereBeDragons-2.0")
local HBDPins = LibStub("HereBeDragons-Pins-2.0")
local HBDMigrate = LibStub("HereBeDragons-Migrate")
local WoWClassic = (WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE)
---------------------------------------------------------
-- Our db upvalue and db defaults
local db
local options
local defaults = {
profile = {
enabled = true,
icon_scale = 1.0,
icon_alpha = 1.0,
icon_scale_minimap = 1.0,
icon_alpha_minimap = 1.0,
enabledPlugins = {
['*'] = true,
},
},
}
---------------------------------------------------------
-- Localize some globals
local floor, min, max = floor, math.min, math.max
local pairs, next, type = pairs, next, type
local CreateFrame = CreateFrame
local Minimap = Minimap
---------------------------------------------------------
-- xpcall safecall implementation
local xpcall = xpcall
local function errorhandler(err)
return geterrorhandler()(err)
end
local function safecall(func, ...)
-- we check to see if the func is passed is actually a function here and don't error when it isn't
-- this safecall is used for optional functions like OnEnter OnLeave etc. When they are not
-- present execution should continue without hinderance
if type(func) == "function" then
return xpcall(func, errorhandler, ...)
end
end
---------------------------------------------------------
-- Our frames recycling code
local pinCache = {}
local minimapPins = {}
local pinCount = 0
local function recyclePin(pin)
pin:Hide()
pinCache[pin] = true
end
local function clearAllPins(t)
for key, pin in pairs(t) do
recyclePin(pin)
t[key] = nil
end
end
local function getNewPin()
local pin = next(pinCache)
if pin then
pinCache[pin] = nil -- remove it from the cache
return pin
end
-- create a new pin
pinCount = pinCount + 1
pin = CreateFrame("Button", "HandyNotesPin"..pinCount, Minimap)
pin:SetFrameLevel(5)
pin:EnableMouse(true)
pin:SetWidth(12)
pin:SetHeight(12)
pin:SetPoint("CENTER", Minimap, "CENTER")
local texture = pin:CreateTexture(nil, "OVERLAY")
pin.texture = texture
texture:SetAllPoints(pin)
texture:SetTexelSnappingBias(0)
texture:SetSnapToPixelGrid(false)
pin:RegisterForClicks("AnyUp", "AnyDown")
pin:SetMovable(true)
pin:Hide()
return pin
end
---------------------------------------------------------
-- Plugin handling
HandyNotes.plugins = {}
local pluginsOptionsText = {}
--[[ Documentation:
HandyNotes.plugins table contains every plugin which we will use to iterate over.
In this table, the format is:
["Name of plugin"] = {table containing a set of standard functions, which we'll call pluginHandler}
Standard functions we require for every plugin:
iter, state, value = pluginHandler:GetNodes2(uiMapID, minimap)
Parameters
- uiMapID: The zone we want data for
- minimap: Boolean argument indicating that we want to get nodes to display for the minimap
Returns:
- iter: An iterator function that will loop over and return 5 values
(coord, uiMapID, iconpath, scale, alpha)
for every node in the requested zone. If the uiMapID return value is nil, we assume it is the
same uiMapID as the argument passed in. Mainly used for continent uiMapID where the map passed
in is a continent, and the return values are coords of subzone maps.
- state, value: First 2 args to pass into iter() on the initial iteration
Legacy Alternative - You're strongly urged to update to GetNodes2!
iter, state, value = pluginHandler:GetNodes(mapFile, minimap, dungeonLevel)
Parameters
- mapFile: The zone we want data for
- minimap: Boolean argument indicating that we want to get nodes to display for the minimap
- dungeonLevel: Level of the dungeon map. 0 indicates the zone has no dungeon levels
Returns:
- iter: An iterator function that will loop over and return 5 values
(coord, mapFile, iconpath, scale, alpha, dungeonLevel)
for every node in the requested zone. If the mapFile return value is nil, we assume it is the
same mapFile as the argument passed in. Mainly used for continent mapFile where the map passed
in is a continent, and the return values are coords of subzone maps. If the return dungeonLevel
is nil, we assume it is the same as the argument passed in.
- state, value: First 2 args to pass into iter() on the initial iteration
Standard functions you can provide optionally:
pluginHandler:OnEnter(uiMapID/mapFile, coord)
Function we will call when the mouse enters a HandyNote, you will generally produce a tooltip here.
pluginHandler:OnLeave(uiMapID/mapFile, coord)
Function we will call when the mouse leaves a HandyNote, you will generally hide the tooltip here.
pluginHandler:OnClick(button, down, uiMapID/mapFile, coord)
Function we will call when the user clicks on a HandyNote, you will generally produce a menu here on right-click.
]]
function HandyNotes:RegisterPluginDB(pluginName, pluginHandler, optionsTable)
if self.plugins[pluginName] ~= nil then
error(pluginName.." is already registered by another plugin.")
else
self.plugins[pluginName] = pluginHandler
end
minimapPins[pluginName] = {}
options.args.plugins.args[pluginName] = optionsTable
pluginsOptionsText[pluginName] = optionsTable and optionsTable.name or pluginName
end
local pinsHandler = {}
function pinsHandler:OnEnter(motion)
safecall(HandyNotes.plugins[self.pluginName].OnEnter, self, self.mapFile or self.uiMapID, self.coord)
end
function pinsHandler:OnLeave(motion)
safecall(HandyNotes.plugins[self.pluginName].OnLeave, self, self.mapFile or self.uiMapID, self.coord)
end
function pinsHandler:OnClick(button, down)
safecall(HandyNotes.plugins[self.pluginName].OnClick, self, button, down, self.mapFile or self.uiMapID, self.coord)
end
---------------------------------------------------------
-- Public functions
local continentZoneList = WoWClassic and {
[1414] = true, -- Kalimdor
[1415] = true, -- Eastern Kingdoms
[1945] = true, -- Outlands
[113] = true, -- Northrend
-- mapFile compat entries
["Kalimdor"] = 1414,
["Azeroth"] = 1415,
["Expansion01"] = 1945,
["Northrend"] = 113,
}
or {
[12] = true, -- Kalimdor
[13] = true, -- Azeroth
[101] = true, -- Outlands
[113] = true, -- Northrend
[424] = true, -- Pandaria
[572] = true, -- Draenor
[619] = true, -- Broken Isles
[875] = true, -- Zandalar
[876] = true, -- Kul Tiras
[1550] = true, -- Shadowlands
[1978] = true, -- Dragon Isles
-- mapFile compat entries
["Kalimdor"] = 12,
["Azeroth"] = 13,
["Expansion01"] = 101,
["Northrend"] = 113,
["TheMaelstromContinent"] = 948,
["Vashjir"] = 203,
["Pandaria"] = 424,
["Draenor"] = 572,
["BrokenIsles"] = 619,
}
local function fillContinentZoneList(continent)
continentZoneList[continent] = {}
local children = C_Map.GetMapChildrenInfo(continent)
if children then
for _, child in ipairs(children) do
if child.mapType == Enum.UIMapType.Zone then
table.insert(continentZoneList[continent], child.mapID)
end
end
end
end
-- Public function to get a list of zones in a continent
-- Can be called with a uiMapId, in which case it'll also return a list of uiMapIds,
-- or called with a legacy mapFile, in which case it'll return a list of legacy mapIDs
function HandyNotes:GetContinentZoneList(uiMapIdOrmapFile)
if not continentZoneList[uiMapIdOrmapFile] then return nil end
if type(continentZoneList[uiMapIdOrmapFile]) ~= "table" then
if type(uiMapIdOrmapFile) == "string" then
local uiMapIdContinent = continentZoneList[uiMapIdOrmapFile]
if type(continentZoneList[uiMapIdContinent]) ~= "table" then
fillContinentZoneList(uiMapIdContinent)
end
continentZoneList[uiMapIdOrmapFile] = {}
for _, uiMapId in ipairs(continentZoneList[uiMapIdContinent]) do
local mapId = HBDMigrate:GetLegacyMapInfo(uiMapId)
if mapId then
table.insert(continentZoneList[uiMapIdOrmapFile], mapId)
end
end
else
fillContinentZoneList(uiMapIdOrmapFile)
end
end
return continentZoneList[uiMapIdOrmapFile]
end
-- Public functions for plugins to convert between coords <--> x,y
function HandyNotes:getCoord(x, y)
return floor(x * 10000 + 0.5) * 10000 + floor(y * 10000 + 0.5)
end
function HandyNotes:getXY(id)
return floor(id / 10000) / 10000, (id % 10000) / 10000
end
-- Public functions for plugins to convert between legacy MapFile <-> Map ID
-- DEPRECATED! Update your plugins to use the new "uiMapId" everywhere
function HandyNotes:GetMapFiletoMapID(mapFile)
if not mapFile then return end
local uiMapId = HBDMigrate:GetUIMapIDFromMapFile(mapFile)
local mapID = HBDMigrate:GetLegacyMapInfo(uiMapId)
return mapID
end
function HandyNotes:GetMapIDtoMapFile(mapID)
if not mapID then return end
local uiMapId = HBDMigrate:GetUIMapIDFromMapAreaId(mapID)
local _, _, mapFile = HBDMigrate:GetLegacyMapInfo(uiMapId)
return mapFile
end
---------------------------------------------------------
-- Core functions
local function LegacyNodeIterator(t, state)
local coord, mapFile2, iconpath, scale, alpha, level2 = t.iter(t.data, state)
local uiMapID = HBDMigrate:GetUIMapIDFromMapFile(mapFile2 or t.mapFile, level2 or t.level)
return coord, uiMapID, iconpath, scale, alpha
end
local emptyTbl = {}
local function IterateNodes(pluginName, uiMapID, minimap)
local handler = HandyNotes.plugins[pluginName]
assert(handler)
if handler.GetNodes2 then
return handler:GetNodes2(uiMapID, minimap)
elseif handler.GetNodes then
local _mapID, level, mapFile = HBDMigrate:GetLegacyMapInfo(uiMapID)
if not mapFile then
return next, emptyTbl
end
local iter, data, state = handler:GetNodes(mapFile, minimap, level)
local t = { mapFile = mapFile, level = level, iter = iter, data = data }
return LegacyNodeIterator, t, state
else
error(("Plugin %s does not have GetNodes or GetNodes2"):format(pluginName))
end
end
---------------------------------------------------------
-- World Map Data Provider
HandyNotes.WorldMapDataProvider = CreateFromMixins(MapCanvasDataProviderMixin)
function HandyNotes.WorldMapDataProvider:RemoveAllData()
if self:GetMap() then
self:GetMap():RemoveAllPinsByTemplate("HandyNotesWorldMapPinTemplate")
end
end
function HandyNotes.WorldMapDataProvider:RefreshAllData(fromOnShow)
if not self:GetMap() then return end
self:RemoveAllData()
for pluginName in pairs(HandyNotes.plugins) do
safecall(self.RefreshPlugin, self, pluginName)
end
end
function HandyNotes.WorldMapDataProvider:RefreshPlugin(pluginName)
if not self:GetMap() then return end
for pin in self:GetMap():EnumeratePinsByTemplate("HandyNotesWorldMapPinTemplate") do
if pin.pluginName == pluginName then
self:GetMap():RemovePin(pin)
end
end
if not db.enabledPlugins[pluginName] then return end
local uiMapID = self:GetMap():GetMapID()
if not uiMapID then return end
for coord, uiMapID2, iconpath, scale, alpha in IterateNodes(pluginName, uiMapID, false) do
local x, y = floor(coord / 10000) / 10000, (coord % 10000) / 10000
if uiMapID2 and uiMapID ~= uiMapID2 then
x, y = HBD:TranslateZoneCoordinates(x, y, uiMapID2, uiMapID)
end
local mapFile
if not HandyNotes.plugins[pluginName].GetNodes2 then
mapFile = select(3, HBDMigrate:GetLegacyMapInfo(uiMapID2 or uiMapID))
end
if x and y then
self:GetMap():AcquirePin("HandyNotesWorldMapPinTemplate", pluginName, x, y, iconpath, scale or 1.0, alpha or 1.0, coord, uiMapID2 or uiMapID, mapFile)
end
end
end
--[[ Handy Notes WorldMap Pin ]]--
HandyNotesWorldMapPinMixin = CreateFromMixins(MapCanvasPinMixin)
function HandyNotesWorldMapPinMixin:OnLoad()
self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI")
self:SetMovable(true)
self:SetScalingLimits(1, 1.0, 1.2);
end
function HandyNotesWorldMapPinMixin:OnAcquired(pluginName, x, y, iconpath, scale, alpha, originalCoord, originalMapID, legacyMapFile)
self.pluginName = pluginName
self.coord = originalCoord
self.uiMapID = originalMapID
self.mapFile = legacyMapFile
self:SetPosition(x, y)
-- we need to handle right clicks for our nodes, so disable button pass-through
if self.SetPassThroughButtons then
self:SetPassThroughButtons("")
end
local size = 12 * db.icon_scale * scale
self:SetSize(size, size)
self:SetAlpha(min(max(db.icon_alpha * alpha, 0), 1))
local t = self.texture
if type(iconpath) == "table" then
if iconpath.tCoordLeft then
t:SetTexCoord(iconpath.tCoordLeft, iconpath.tCoordRight, iconpath.tCoordTop, iconpath.tCoordBottom)
else
t:SetTexCoord(0, 1, 0, 1)
end
if iconpath.r then
t:SetVertexColor(iconpath.r, iconpath.g, iconpath.b, iconpath.a)
else
t:SetVertexColor(1, 1, 1, 1)
end
t:SetTexture(iconpath.icon)
else
t:SetTexCoord(0, 1, 0, 1)
t:SetVertexColor(1, 1, 1, 1)
t:SetTexture(iconpath)
end
end
function HandyNotesWorldMapPinMixin:OnMouseEnter()
pinsHandler.OnEnter(self)
end
function HandyNotesWorldMapPinMixin:OnMouseLeave()
pinsHandler.OnLeave(self)
end
function HandyNotesWorldMapPinMixin:OnMouseDown(button)
pinsHandler.OnClick(self, button, true)
end
function HandyNotesWorldMapPinMixin:OnMouseUp(button)
pinsHandler.OnClick(self, button, false)
end
-- hack to avoid error in combat in 10.1.5
HandyNotesWorldMapPinMixin.SetPassThroughButtons = function() end
function HandyNotes:UpdateWorldMapPlugin(pluginName)
if not HandyNotes:IsEnabled() then return end
HandyNotes.WorldMapDataProvider:RefreshPlugin(pluginName)
end
-- This function updates all the icons on the world map for every plugin
function HandyNotes:UpdateWorldMap()
if not HandyNotes:IsEnabled() then return end
HandyNotes.WorldMapDataProvider:RefreshAllData()
end
---------------------------------------------------------
-- MiniMap Drawing
-- This function updates all the icons of one plugin on the world map
function HandyNotes:UpdateMinimapPlugin(pluginName)
--if not Minimap:IsVisible() then return end
HBDPins:RemoveAllMinimapIcons("HandyNotes" .. pluginName)
clearAllPins(minimapPins[pluginName])
if not db.enabledPlugins[pluginName] then return end
local uiMapID = HBD:GetPlayerZone()
if not uiMapID then return end
local ourScale, ourAlpha = 12 * db.icon_scale_minimap, db.icon_alpha_minimap
local frameLevel = Minimap:GetFrameLevel() + 5
local frameStrata = Minimap:GetFrameStrata()
for coord, uiMapID2, iconpath, scale, alpha in IterateNodes(pluginName, uiMapID, true) do
local icon = getNewPin()
icon:SetParent(Minimap)
icon:SetFrameStrata(frameStrata)
icon:SetFrameLevel(frameLevel)
scale = ourScale * (scale or 1.0)
icon:SetHeight(scale) -- Can't use :SetScale as that changes our positioning scaling as well
icon:SetWidth(scale)
icon:SetAlpha(min(max(ourAlpha * (alpha or 1.0), 0), 1))
local t = icon.texture
if type(iconpath) == "table" then
if iconpath.tCoordLeft then
t:SetTexCoord(iconpath.tCoordLeft, iconpath.tCoordRight, iconpath.tCoordTop, iconpath.tCoordBottom)
else
t:SetTexCoord(0, 1, 0, 1)
end
if iconpath.r then
t:SetVertexColor(iconpath.r, iconpath.g, iconpath.b, iconpath.a)
else
t:SetVertexColor(1, 1, 1, 1)
end
t:SetTexture(iconpath.icon)
else
t:SetTexCoord(0, 1, 0, 1)
t:SetVertexColor(1, 1, 1, 1)
t:SetTexture(iconpath)
end
icon:SetScript("OnClick", nil)
icon:SetScript("OnEnter", pinsHandler.OnEnter)
icon:SetScript("OnLeave", pinsHandler.OnLeave)
local x, y = floor(coord / 10000) / 10000, (coord % 10000) / 10000
HBDPins:AddMinimapIconMap("HandyNotes" .. pluginName, icon, uiMapID2 or uiMapID, x, y, true)
t:ClearAllPoints()
t:SetAllPoints(icon) -- Not sure why this is necessary, but people are reporting weirdly sized textures
minimapPins[pluginName][icon] = icon
icon.pluginName = pluginName
icon.coord = coord
if not HandyNotes.plugins[pluginName].GetNodes2 then
icon.mapFile = select(3, HBDMigrate:GetLegacyMapInfo(uiMapID2 or uiMapID))
else
icon.mapFile = nil
end
icon.uiMapID = uiMapID2 or uiMapID
end
end
-- This function updates all the icons on the minimap for every plugin
function HandyNotes:UpdateMinimap()
--if not Minimap:IsVisible() then return end
for pluginName in pairs(self.plugins) do
safecall(self.UpdateMinimapPlugin, self, pluginName)
end
end
-- This function runs when we receive a "HandyNotes_NotifyUpdate"
-- notification from a plugin that its icons needs to be updated
-- Syntax is plugin:SendMessage("HandyNotes_NotifyUpdate", "pluginName")
function HandyNotes:UpdatePluginMap(message, pluginName)
if self.plugins[pluginName] then
self:UpdateMinimapPlugin(pluginName)
self:UpdateWorldMapPlugin(pluginName)
end
end
---------------------------------------------------------
-- Our options table
options = {
type = "group",
name = L["HandyNotes"],
desc = L["HandyNotes"],
args = {
enabled = {
type = "toggle",
name = L["Enable HandyNotes"],
desc = L["Enable or disable HandyNotes"],
order = 1,
get = function(info) return db.enabled end,
set = function(info, v)
db.enabled = v
if v then HandyNotes:Enable() else HandyNotes:Disable() end
end,
disabled = false,
},
overall_settings = {
type = "group",
name = L["Overall settings"],
desc = L["Overall settings that affect every database"],
order = 10,
get = function(info) return db[info.arg] end,
set = function(info, v)
local arg = info.arg
db[arg] = v
if arg == "icon_scale" or arg == "icon_alpha" then
HandyNotes:UpdateWorldMap()
else
HandyNotes:UpdateMinimap()
end
end,
disabled = function() return not db.enabled end,
args = {
desc = {
name = L["These settings control the look and feel of HandyNotes globally. The icon's scale and alpha here are multiplied with the plugin's scale and alpha."],
type = "description",
order = 0,
},
icon_scale = {
type = "range",
name = L["World Map Icon Scale"],
desc = L["The overall scale of the icons on the World Map"],
min = 0.25, max = 2, step = 0.01,
arg = "icon_scale",
order = 10,
},
icon_alpha = {
type = "range",
name = L["World Map Icon Alpha"],
desc = L["The overall alpha transparency of the icons on the World Map"],
min = 0, max = 1, step = 0.01,
arg = "icon_alpha",
order = 20,
},
icon_scale_minimap = {
type = "range",
name = L["Minimap Icon Scale"],
desc = L["The overall scale of the icons on the Minimap"],
min = 0.25, max = 2, step = 0.01,
arg = "icon_scale_minimap",
order = 30,
},
icon_alpha_minimap = {
type = "range",
name = L["Minimap Icon Alpha"],
desc = L["The overall alpha transparency of the icons on the Minimap"],
min = 0, max = 1, step = 0.01,
arg = "icon_alpha_minimap",
order = 40,
},
},
},
plugins = {
type = "group",
name = L["Plugins"],
desc = L["Plugin databases"],
order = 20,
args = {
desc = {
name = L["Configuration for each individual plugin database."],
type = "description",
order = 0,
},
show_plugins = {
name = L["Show the following plugins on the map"], type = "multiselect",
order = 20,
values = pluginsOptionsText,
get = function(info, k)
return db.enabledPlugins[k]
end,
set = function(info, k, v)
db.enabledPlugins[k] = v
HandyNotes:UpdatePluginMap(nil, k)
end,
},
},
},
},
}
options.args.plugins.disabled = options.args.overall_settings.disabled
---------------------------------------------------------
-- Addon initialization, enabling and disabling
function HandyNotes:OnInitialize()
-- Set up our database
self.db = LibStub("AceDB-3.0"):New("HandyNotesDB", defaults)
self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileChanged")
self.db.RegisterCallback(self, "OnProfileCopied", "OnProfileChanged")
self.db.RegisterCallback(self, "OnProfileReset", "OnProfileChanged")
db = self.db.profile
-- Register options table and slash command
LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("HandyNotes", options)
self:RegisterChatCommand("handynotes", function() LibStub("AceConfigDialog-3.0"):Open("HandyNotes") end)
LibStub("AceConfigDialog-3.0"):AddToBlizOptions("HandyNotes", "HandyNotes")
-- Get the option table for profiles
options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
options.args.profiles.disabled = options.args.overall_settings.disabled
end
function HandyNotes:OnEnable()
if not db.enabled then
self:Disable()
return
end
self:RegisterMessage("HandyNotes_NotifyUpdate", "UpdatePluginMap")
self:UpdateMinimap()
WorldMapFrame:AddDataProvider(HandyNotes.WorldMapDataProvider)
self:UpdateWorldMap()
HBD.RegisterCallback(self, "PlayerZoneChanged", "UpdateMinimap")
end
function HandyNotes:OnDisable()
-- Remove all the pins
for pluginName in pairs(self.plugins) do
HBDPins:RemoveAllMinimapIcons("HandyNotes" .. pluginName)
clearAllPins(minimapPins[pluginName])
end
if WorldMapFrame.dataProviders[HandyNotes.WorldMapDataProvider] then
WorldMapFrame:RemoveDataProvider(HandyNotes.WorldMapDataProvider)
end
HBD.UnregisterCallback(self, "PlayerZoneChanged")
end
function HandyNotes:OnProfileChanged(event, database, newProfileKey)
db = database.profile
self:UpdateMinimap()
self:UpdateWorldMap()
end