Quantcast
-- Broker_RaidMakeup.lua
-- Written by KyrosKrane Sylvanblade (kyros@kyros.info)
-- Copyright (c) 2018 KyrosKrane Sylvanblade
-- Licensed under the MIT License, as per the included file.


--#########################################
--# Description
--#########################################

-- This add-on creates a LibDataBroker object that shows you the makeup of your raid (tanks, healers, and dps).
-- Requires an LDB display to show the info.
-- No configuration or setup.


--#########################################
--# Globals and utilities
--#########################################

-- Get a local reference to speed up execution.
local _G = _G
local string = string
local print = print
local setmetatable = setmetatable
local select = select
local type = type
local pairs = pairs

-- Define a global for our namespace
local BRM = {}


--#########################################
--# Frame for event handling
--#########################################

-- Create the frame to hold our event catcher, and the list of events.
BRM.Frame, BRM.Events = CreateFrame("Frame"), {}


--#########################################
--# Constants
--#########################################

-- The strings that define the addon
BRM.ADDON_NAME="Broker_RaidMakeup" -- the internal addon name for LibStub and other addons
BRM.USER_ADDON_NAME="Broker_RaidMakeup" -- the name displayed to the user

-- The strings used by the game to represent the roles. I don't think these are localized in the game.
BRM.ROLE_HEALER = "HEALER"
BRM.ROLE_TANK = "TANK"
BRM.ROLE_DPS = "DAMAGER"
BRM.ROLE_NONE = "NONE"

-- The faction strings. Again, probably not localized
BRM.FACTION_ALLIANCE = "Alliance"
BRM.FACTION_HORDE = "Horde"

-- The version of this add-on
BRM.Version = "@project-version@"


--#########################################
--# Slash command options and settings
--#########################################

BRM.OptionsTable = {
	type = "group",
	args = {
		showcounts = {
			name = "Show counts in tooltip",
			desc = "Show the role counts in the tooltip as well as in the broker display",
			type = "toggle",
			set = function(info,val) BRM.DB.ShowCountInTooltip = val end,
			get = function(info) return BRM.DB.ShowCountInTooltip end,
			descStyle = "inline",
			width = "full",
		},
		minimapicon = {
			name = "Show minimap icon",
			desc = "Show a minimap icon for " .. BRM.USER_ADDON_NAME,
			type = "toggle",
			set = function(info,val) BRM:SetMinimapButton(val) end,
			get = function(info) return (not BRM.DB.MinimapSettings.hide) end,
			descStyle = "inline",
			width = "full",
		}, -- minimapicon
		debug = {
			name = "Enable debug output",
			desc = "Prints extensive debugging output about everything " .. BRM.USER_ADDON_NAME .. " does",
			type = "toggle",
			set = function(info,val) BRM:SetDebugMode(val) end,
			get = function(info) return BRM.DebugMode end,
			descStyle = "inline",
			width = "full",
			hidden = true,
		}, -- debug
	} -- args
} -- BRM.OptionsTable


-- Process the options and create the AceConfig options table
BRM.AceConfig = LibStub("AceConfig-3.0")
BRM.AceConfig:RegisterOptionsTable(BRM.ADDON_NAME, BRM.OptionsTable, {"brm"})

-- Create the frame to set the options and add it to the Blizzard settings
BRM.ConfigFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions(BRM.ADDON_NAME, BRM.USER_ADDON_NAME)


--#########################################
--# Debugging setup
--#########################################

-- Debug settings
-- This is needed to debug stuff before the addon loads. After the addon loads, the permanent value is stored in BRM.DB.DebugMode
BRM.DebugMode = false

--@alpha@
BRM.DebugMode = true
--@end-alpha@


-- Print debug output to the chat frame.
function BRM:DebugPrint(...)
	if not BRM.DebugMode then return end

	print ("|cff" .. "a00000" .. "BRM Debug:|r", ...)
end -- BRM:DebugPrint


-- Print regular output to the chat frame.
function BRM:ChatPrint(...)
	print ("|cff" .. "0066ff" .. "BRM:|r", ...)
end -- BRM:DebugPrint


-- Debugging code to see what the hell is being passed in...
function BRM:PrintVarArgs(...)
	if not BRM.DebugMode then return end

	local n = select('#', ...)
	BRM:DebugPrint ("There are ", n, " items in varargs.")
	local msg
	for i = 1, n do
		msg = select(i, ...)
		BRM:DebugPrint ("Item ", i, " is ", msg)
	end
end -- BRM:PrintVarArgs()


-- Dumps a table into chat. Not intended for production use.
function BRM:DumpTable(tab, indent)
	if not BRM.DebugMode then return end

	if not indent then indent = 0 end
	if indent > 10 then
		BRM:DebugPrint("Recursion is at 11 already; aborting.")
		return
	end
	for k, v in pairs(tab) do
		local s = ""
		if indent > 0 then
			for i = 0, indent do
				s = s .. "    "
			end
		end
		if "table" == type(v) then
			s = s .. "Item " .. k .. " is sub-table."
			BRM:DebugPrint(s)
			indent = indent + 1
			BRM:DumpTable(v, indent)
			indent = indent - 1
		else
			s = s .. "Item " .. k .. " is " .. tostring(v)
			BRM:DebugPrint(s)
		end
	end
end -- BRM:DumpTable()


-- Sets the debug mode and writes the setting to the DB
function BRM:SetDebugMode(setting)
	BRM.DebugMode = setting
	BRM.DB.DebugMode = setting
end


--#########################################
--# Select the actual icons used
--#########################################

-- The icons to use when displaying in the broker display
BRM.MainIcon = IconClass("Interface\\Icons\\Inv_helm_robe_raidpriest_k_01") -- Placeholder icon to use until we determine the faction later.
BRM.AllianceIcon = IconClass("Interface\\Calendar\\UI-Calendar-Event-PVP02")
BRM.HordeIcon = IconClass("Interface\\Calendar\\UI-Calendar-Event-PVP01")

-- Role icons
BRM.TankIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-PORTRAITROLES.blp", 64, 64, 0, 0+19, 22, 22+19)
BRM.HealerIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-PORTRAITROLES.blp", 64, 64, 19, 19+19, 1, 1+19)
BRM.DPSIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-PORTRAITROLES.blp", 64, 64, 19, 19+19, 22, 22+19)
BRM.UnknownIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-ROLES.blp", 256, 256, 135, 135+64, 67, 67+64)

-- Icons I considered but didn't like
--BRM.AllianceIcon = "Interface\\Icons\\Inv_misc_head_human_02"
--BRM.HordeIcon = "Interface\\Icons\\Achievement_femalegoblinhead"
--BRM.HealerIcon = "Interface\\Icons\\Spell_holy_flashheal.blp"
--BRM.AllianceIcon = IconClass("Interface\\Icons\\Inv_tabard_a_78wrynnvanguard")
--BRM.HordeIcon = IconClass("Interface\\Icons\\Inv_tabard_a_77voljinsspear")
--BRM.TankIcon = IconClass("Interface\\Icons\\Inv_shield_06.blp")
--BRM.HealerIcon = IconClass("Interface\\Icons\\spell_chargepositive.blp")
--BRM.DPSIcon = IconClass("Interface\\Icons\\Inv_sword_27.blp")
--BRM.UnknownIcon = IconClass("Interface\\Icons\\Inv_misc_questionmark.blp")

-- These high res icons don't look very good when squished down to a broker display. The low-res ones above are better.
--BRM.TankIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-ROLES.blp", 256, 256, 0, 0+64, 68, 68+64)
--BRM.HealerIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-ROLES.blp", 256, 256, 68, 68+64, 0, 0+64)
--BRM.DPSIcon = IconClass("Interface\\LFGFRAME\\UI-LFG-ICON-ROLES.blp", 256, 256, 68, 68+64, 68, 68+64)


--#########################################
--# Variables for tracking raid members
--#########################################

BRM.TankCount = 0
BRM.HealerCount = 0
BRM.DPSCount = 0
BRM.UnknownCount = 0
BRM.TotalCount = 0

-- The game is firing the ACTIVE_TALENT_GROUP_CHANGED event before the PLAYER_ENTERING_WORLD event.
-- Since we're not fully in the world yet at that point, the group and raid query functions are returning unexpected results.
-- So, we use this variable to track whether the add-on is loaded and active.
-- We turn it on in the PLAYER_ENTERING_WORLD event.
BRM.IsActive = false


--#########################################
--# Count display and utility functions
--#########################################

function BRM:GetDisplayString()
	local OutputString = string.format("%d %s %d %s %d %s %d", BRM.TotalCount, BRM.TankIcon:GetIconString(), BRM.TankCount, BRM.HealerIcon:GetIconString(), BRM.HealerCount, BRM.DPSIcon:GetIconString(), BRM.DPSCount)
	if BRM.UnknownCount > 0 then
		OutputString = string.format("%s %s %d", OutputString, BRM.UnknownIcon:GetIconString(), BRM.UnknownCount)
	end
	return OutputString
end -- BRM:GetDisplayString()


function BRM:IncrementRole(role)
	-- Handle case of nil roles - can happen when the game has not fully loaded and we try to do a role check
	if not role then role = "unknown" end

	if BRM.ROLE_HEALER == role then
		BRM.HealerCount = BRM.HealerCount + 1
	elseif BRM.ROLE_TANK == role then
		BRM.TankCount = BRM.TankCount + 1
	elseif BRM.ROLE_DPS == role then
		BRM.DPSCount = BRM.DPSCount + 1
	else
		BRM.UnknownCount = BRM.UnknownCount + 1
	end

	BRM.TotalCount = BRM.TotalCount + 1
end -- BRM:IncrementRole()


function BRM:UpdateComposition()
	BRM:DebugPrint("in BRM:UpdateComposition")

	-- If the addon is not yet active, then just exit
	if not BRM.IsActive then
		BRM:DebugPrint("Addon is not active. Exiting without updating.")
		return
	end

	-- Zero out the counts so we can start fresh
	BRM.TankCount = 0
	BRM.HealerCount = 0
	BRM.DPSCount = 0
	BRM.UnknownCount = 0
	BRM.TotalCount = 0

	-- Figure out how many members in our group. Ungrouped returns zero.
	local members = GetNumGroupMembers()
	BRM:DebugPrint("members is " .. members)

	-- Variable for holding the role of each member we check, and a random iterator
	local Role, i

	if members and members > 0 then
		BRM:DebugPrint("I am in some kind of Group.")

		local CheckWord = IsInRaid() and "raid" or "party" -- this probably isn't localized in the game.
		BRM:DebugPrint("CheckWord is " .. CheckWord)

		-- OK, this is bloody screwy.
		-- If I'm in a party, then the addon has to check player and party1 to party4.
		-- But if I'm in a raid, the addon has to check raid1 to raid40, with no need to check player!

		if "raid" == CheckWord then
			-- Raid - iterate and count by role
			for i=1,members do
				Role = UnitGroupRolesAssigned(CheckWord .. i)
				if Role then
					BRM:DebugPrint("Group member " .. CheckWord .. i .. " has role " .. Role)
					BRM:IncrementRole(Role)
				else
					BRM:DebugPrint("Group member " .. CheckWord .. i .. " has no role")
					BRM:IncrementRole("unknown")
				end
			end -- for raid members
		else
			-- Party - iterate and count by role
			for i = 1, members - 1 do
				Role = UnitGroupRolesAssigned(CheckWord .. i)
				if Role then
					BRM:DebugPrint("Group member " .. CheckWord .. i .. " has role " .. Role)
					BRM:IncrementRole(Role)
				else
					BRM:DebugPrint("Group member " .. CheckWord .. i .. " has no role")
					BRM:IncrementRole("unknown")
				end
			end -- for party members

			-- Now repeat all that for the player.
			Role = UnitGroupRolesAssigned("player")
			if Role then
				BRM:DebugPrint("player has role " .. Role)
				BRM:IncrementRole(Role)
			else
				BRM:DebugPrint("player has no role")
				BRM:IncrementRole("unknown")
			end
		end -- if raid/party
	else
		BRM:DebugPrint("I am not in any kind of Group.")

		-- When not grouped, there is no role to check. So instead, we go off the player's specialization.

		-- get player role
		Role = select(5, GetSpecializationInfo(GetSpecialization()))
		-- GetSpecializationInfo returns: id, name, description, icon, background, role.

		if Role then
			BRM:DebugPrint("My role is " .. Role)
			BRM:IncrementRole(Role)
		else
			BRM:DebugPrint("Did not get role from specialization check")
			BRM:IncrementRole("unknown")
		end
	end

	BRM:DebugPrint("At end of role check, tanks = " .. BRM.TankCount .. ", healers = " .. BRM.HealerCount .. ", dps = " .. BRM.DPSCount .. ", other = " .. BRM.UnknownCount)

	BRM.LDO.text = BRM:GetDisplayString()
end -- BRM:UpdateComposition()


-- This function handles refreshing the role counts and checking for count errors
function BRM:RefreshCounts()
	-- I'm trying to capture which situations don't result in an automatic update.
	-- That essentially indicates either an event I missed coding for, or some kind of bug that resulted in invalid role counts.
	local old_TankCount = BRM.TankCount
	local old_HealerCount = BRM.HealerCount
	local old_DPSCount = BRM.DPSCount
	local old_UnknownCount = BRM.UnknownCount
	local old_TotalCount = BRM.TotalCount

	-- Refresh the counts
	BRM:UpdateComposition()

	-- Check if the counts changed, indicating the error above.
	if old_TankCount ~= BRM.TankCount
		or old_HealerCount ~= BRM.HealerCount
		or old_DPSCount ~= BRM.DPSCount
		or old_UnknownCount ~= BRM.UnknownCount
		or old_TotalCount ~= BRM.TotalCount
		then
			BRM:DebugPrint("Counts are different after click.")
			BRM:DebugPrint("old_TankCount is "		.. (old_TankCount		or "nil") .. ", new TankCount is "		.. (BRM.TankCount		or "nil"))
			BRM:DebugPrint("old_HealerCount is "	.. (old_HealerCount		or "nil") .. ", new DPSCount is "		.. (BRM.DPSCount		or "nil"))
			BRM:DebugPrint("old_DPSCount is "		.. (old_DPSCount		or "nil") .. ", new DPSCount is "		.. (BRM.DPSCount		or "nil"))
			BRM:DebugPrint("old_UnknownCount is "	.. (old_UnknownCount	or "nil") .. ", new UnknownCount is "	.. (BRM.UnknownCount	or "nil"))
			BRM:DebugPrint("old_TotalCount is "		.. (old_TotalCount		or "nil") .. ", new TotalCount is "		.. (BRM.TotalCount		or "nil"))

			-- @TODO: Capture some info and give the player a way to report it.

			-- Also schedule an update in five seconds to ensure we capture any additional changes
			C_Timer.After(5, function() BRM:UpdateComposition() end)
	end
end -- BRM:RefreshCounts()


--#########################################
--# Actual LibDataBroker object
--#########################################

BRM.LDO = _G.LibStub("LibDataBroker-1.1"):NewDataObject(BRM.ADDON_NAME, {
	type = "data source",
	text = BRM:GetDisplayString(),
	value = "0",
	icon = BRM.MainIcon:GetIconString(),
	label = BRM.USER_ADDON_NAME,
	OnTooltipShow = function(tooltip)
		-- make sure we have a real tooltip
		if not tooltip or not tooltip.AddLine then
			BRM:DebugPrint("Got invalid tooltip, exiting OnTooltipShow")
			return
		end
		BRM:DebugPrint("Showing tooltip")

		-- delete existing lines
		tooltip:ClearLines()

		-- headline
		tooltip:AddLine(BRM.USER_ADDON_NAME)

		-- If the user wants the counts in the tooltip, add them.
		if BRM.DB.ShowCountInTooltip then
			BRM:DebugPrint("Preparing tooltip")

			local DisplayString = ""

			-- faction handling
			if BRM.FACTION_HORDE == BRM.Faction then
				DisplayString = BRM.HordeIcon:GetIconString()
			elseif BRM.FACTION_ALLIANCE == BRM.Faction then
				DisplayString = BRM.AllianceIcon:GetIconString()
			else
				-- What the hell?
				BRM:DebugPrint("Unknown faction detected - " .. BRM.Faction)
				DisplayString = BRM.MainIcon:GetIconString()
			end

			DisplayString = DisplayString .. BRM:GetDisplayString()

			tooltip:AddLine(DisplayString)
		end

		-- Add instructions
		tooltip:AddLine("Click to refresh")
		tooltip:AddLine("Right click for options")
	end,
}) -- BRM.LDO creation


-- Handler for if user clicks on the display
function BRM.LDO:OnClick(button)
	BRM:DebugPrint("Got click on LDB object")

	if button == "LeftButton" then
		BRM:DebugPrint("Got left button")
		BRM:RefreshCounts()
	elseif button == "RightButton" then
		BRM:DebugPrint("Got right button")
		-- toggle showing the count
		InterfaceOptionsFrame_OpenToCategory(BRM.ConfigFrame)
		InterfaceOptionsFrame_OpenToCategory(BRM.ConfigFrame)
			-- Yes, this should be here twice. Workaround for a Blizzard bug.
			-- When you first open the options panel, it opens to the Game control tab, not the Addons control tab.
			-- Calling this twice bypasses that.
	else
		BRM:DebugPrint("Got some other button")
	end

end -- BRM.LDO:OnClick()


--#########################################
--# Minimap icon handling
--#########################################

function BRM:CreateMinimapButton()
	if not BRM.MinimapIcon then
		BRM:DebugPrint("Creating minimap icon")
		BRM.MinimapIcon = LibStub("LibDBIcon-1.0")
		BRM.MinimapIcon:Register(BRM.ADDON_NAME, BRM.LDO, BRM.DB.MinimapSettings)
	end
end -- BRM:CreateMinimapButton()


function BRM:ShowMinimapButton()
	BRM:DebugPrint("Showing minimap icon")
	BRM.DB.MinimapSettings.hide = false
	BRM.MinimapIcon:Show(BRM.ADDON_NAME)
end -- BRM:ShowMinimapButton()


function BRM:HideMinimapButton()
	BRM:DebugPrint("Hiding minimap icon")
	BRM.DB.MinimapSettings.hide = true
	BRM.MinimapIcon:Hide(BRM.ADDON_NAME)
end -- BRM:HideMinimapButton()

-- This function is for calling from the options panel. Pass in true to show the icon, false to hide it (which matches the values of the checkbox in the config panel)
function BRM:SetMinimapButton(state)
	if true == state then
		BRM:ShowMinimapButton()
	else
		BRM:HideMinimapButton()
	end
end -- BRM:SetMinimapButton()


--#########################################
--# Load saved settings
--#########################################

-- Get existing settings from the DB, or create default settings.
function BRM.LoadSettings()
	BRM:DebugPrint("Loading or creating DB")
	if BRM_DB then
		-- Load the settings saved by the game.
		BRM:DebugPrint ("Restoring existing BRM DB")
		BRM.DB = BRM_DB

		-- These situations should only occur during development or upgrade situations
		if not BRM.DB.MinimapSettings then BRM.DB.MinimapSettings = {} end
		if not BRM.DB.ShowCountInTooltip then BRM.DB.ShowCountInTooltip = false end
		if not BRM.DB.DebugMode then BRM.DB.DebugMode = false end
	else
		-- Initialize settings on first use
		BRM:DebugPrint ("Creating new BRM DB")
		BRM.DB = {}
		BRM.DB.Version = 1
		BRM.DB.MinimapSettings = {}
		BRM.DB.ShowCountInTooltip = false
		BRM.DB.DebugMode = false
	end

	BRM.DebugMode = BRM.DB.DebugMode

	BRM:DebugPrint ("DB contents follow")
	BRM:DumpTable(BRM.DB)
	BRM:DebugPrint ("End DB contents")

end -- BRM.LoadSettings()


--#########################################
--# Events to register and handle
--#########################################

-- This event is only for debugging.
-- Note that PLAYER_LOGIN is triggered after all ADDON_LOADED events
function BRM.Events:PLAYER_LOGIN(...)
	BRM:DebugPrint("Got PLAYER_LOGIN event")
end -- BRM.Events:PLAYER_LOGIN()


-- This event is for loading our saved settings.
function BRM.Events:ADDON_LOADED(addon)
	BRM:DebugPrint("Got ADDON_LOADED for " .. addon)
	if addon ~= BRM.ADDON_NAME then return end

	-- Load saved settings
	BRM.LoadSettings()

	-- Minimap button for LDB object
	BRM:CreateMinimapButton()
		-- Creating the minimap icon requires somewhere to save the data - namely, the addon DB.
		-- We don't load that until this event.
		-- So, this is the earliest point we can create the minimap icon.
		-- Note that initial state of whether to display the icon is handled auto-magically by the LDBIcon library, based on the variable storage you pass it.

end -- BRM.Events:ADDON_LOADED()


-- This triggers when someone joins or leaves a group, or changes their spec or role in the group.
function BRM.Events:GROUP_ROSTER_UPDATE(...)
	BRM:DebugPrint("Got GROUP_ROSTER_UPDATE")
	BRM:UpdateComposition()
end -- BRM.Events:GROUP_ROSTER_UPDATE()


-- This triggers when the player changes their talent spec.
function BRM.Events:ACTIVE_TALENT_GROUP_CHANGED(...)
	BRM:DebugPrint("Got ACTIVE_TALENT_GROUP_CHANGED")
	BRM:UpdateComposition()
end -- BRM.Events:ACTIVE_TALENT_GROUP_CHANGED()


-- On-load handler for addon initialization.
function BRM.Events:PLAYER_ENTERING_WORLD(...)
	-- Announce our load.
	BRM:DebugPrint("Got PLAYER_ENTERING_WORLD")

	-- It's now safe to turn on the addon and get counts.
	BRM:DebugPrint("Activating " .. BRM.USER_ADDON_NAME)
	BRM.IsActive = true
	BRM:UpdateComposition()

	-- Get the main app icon based on the player's faction
	BRM:DebugPrint("Determining faction")
	BRM.Faction, _ = UnitFactionGroup("player")

	if not BRM.Faction then
		BRM:DebugPrint("Faction is nil")
		return
	end

	if BRM.FACTION_HORDE == BRM.Faction then
		BRM:DebugPrint("Faction is Horde")
		BRM:DebugPrint("Inner string is " .. BRM.HordeIcon:GetIconStringInner())
		BRM.LDO.icon = BRM.HordeIcon.IconFile
	elseif BRM.FACTION_ALLIANCE == BRM.Faction then
		BRM:DebugPrint("Faction is Alliance")
		BRM:DebugPrint("Inner string is " .. BRM.AllianceIcon:GetIconStringInner())
		BRM.LDO.icon = BRM.AllianceIcon.IconFile
	else
		-- What the hell?
		BRM:DebugPrint("Unknown faction detected - " .. BRM.Faction)
	end

end -- BRM.Events:PLAYER_ENTERING_WORLD()


-- Save the db on logout.
function BRM.Events:PLAYER_LOGOUT(...)
	BRM:DebugPrint ("In PLAYER_LOGOUT, saving DB.")
	BRM_DB = BRM.DB
end -- BRM.Events:PLAYER_LOGOUT()


--#########################################
--# Implement the event handlers
--#########################################

-- Create the event handler function.
BRM.Frame:SetScript("OnEvent", function(self, event, ...)
	BRM.Events[event](self, ...) -- call one of the functions above
end)

-- Register all events for which handlers have been defined
for k, v in pairs(BRM.Events) do
	BRM:DebugPrint("Registering event ", k)
	BRM.Frame:RegisterEvent(k)
end