diff Core.lua @ 57:01b63b8ed811 v21

total rewrite to version 21
author yellowfive
date Fri, 05 Jun 2015 11:05:15 -0700
parents
children ee701ce45354
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core.lua	Fri Jun 05 11:05:15 2015 -0700
@@ -0,0 +1,663 @@
+-- AskMrRobot
+-- Does cool stuff associated with askmrrobot.com:
+--   Import/Export gear and optimization solutions from/to the website
+--   Improve the combat logging experience and augment it with extra data not available directly in the log file
+--   Team Optimizer convenience functionality
+
+AskMrRobot = LibStub("AceAddon-3.0"):NewAddon("AskMrRobot", "AceEvent-3.0", "AceComm-3.0", "AceConsole-3.0", "AceSerializer-3.0")
+local Amr = AskMrRobot
+Amr.Serializer = LibStub("AskMrRobot-Serializer")
+
+Amr.ADDON_NAME = "AskMrRobot"
+
+-- types of inter-addon messages that we receive, used to parcel them out to the proper handlers
+Amr.MessageTypes = {
+	Version = "_V",
+	VersionRequest = "_VR",
+	Team = "_T"
+}
+
+local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true)
+local AceGUI = LibStub("AceGUI-3.0")
+
+-- minimap icon and LDB support
+local _amrLDB = LibStub("LibDataBroker-1.1"):NewDataObject(Amr.ADDON_NAME, {
+	type = "launcher",
+	text = "Ask Mr. Robot",
+	icon = "Interface\\AddOns\\" .. Amr.ADDON_NAME .. "\\Media\\icon",
+	OnClick = function(self, button, down)
+		if button == "LeftButton" then
+			if IsControlKeyDown() then
+				Amr:Wipe()
+			else
+				Amr:Toggle()
+			end
+		elseif button == "RightButton" then
+			Amr:EquipGearSet()
+		end
+	end,
+	OnTooltipShow = function(tt)
+		tt:AddLine("Ask Mr. Robot", 1, 1, 1);
+		tt:AddLine(" ");
+		tt:AddLine(L.MinimapTooltip)
+	end	
+})
+local _icon = LibStub("LibDBIcon-1.0")
+
+
+-- initialize the database
+local function initializeDb()
+
+	local defaults = {
+		char = {
+			FirstUse = true,           -- true if this is first time use, gets cleared after seeing the export help splash window
+			SubSpecs = {},             -- last seen subspecs for this character, used to deal with some ambiguous specs
+			Equipped = {},             -- for each spec group (1 or 2), slot id to item link
+			BagItems = {},             -- list of item links for bag
+			BankItems = {},            -- list of item links for bank
+			VoidItems = {},            -- list of item links for void storage
+			BagItemsAndCounts = {},    -- used mainly for the shopping list
+			BankItemsAndCounts = {},   -- used mainly for the shopping list			
+			GearSets = {},             -- imported gear sets, key by spec group (1 or 2), slot id to item object
+			ExtraItemData = {},        -- for each spec group (1 or 2): mainly for legacy support, item id to object with socketColor and duplicateId information
+			ExtraGemData = {},         -- for each spec group (1 or 2): gem enchant id to gem display information, and data used to detect identical gems (mainly for legacy support)
+			ExtraEnchantData = {},     -- for each spec group (1 or 2): enchant id to enchant display information and material information
+			Logging = {                -- character logging settings
+				Enabled = false,       -- whether logging is currently on or not
+				LastZone = nil,        -- last zone the player was in
+				LastDiff = nil,        -- last difficulty for the last zone the player was in
+				LastWipe = nil         -- last time a wipe was called by this player
+			},
+			TeamOpt = {
+				AllItems = {},         -- all equippable items no matter where it is, list of item unique ids, used to determine when a player gains a new equippable item
+				History = {},          -- history of drops since joining the current group
+				Rolls = {},            -- current loot choices for a loot distribution in progress
+				Role = nil,            -- Leader or Member, changes UI to the mode most appropriate for this user
+				Loot = {},             -- the last loot seen by the master looter
+				LootGuid = nil,        -- guid of the last unit looted by the master looter, will be "container" if there is no target
+				LootInProgress = false -- true if looting is currently in progress
+			}
+		},
+		profile = {
+			minimap = {                -- minimap hide/show and position settings
+				hide = false
+			},
+			window = {},               -- main window position settings
+			lootWindow = {},           -- loot window position settings
+			shopWindow = {},           -- shopping list window position settings
+			options = {
+				autoGear = false,      -- auto-equip saved gear sets when changing specs
+				shopAh = false         -- auto-show shopping list at AH
+			},
+			Logging = {                -- global logging settings
+				Auto = {}              -- for each instanceId, for each difficultyId, true if auto-logging enabled
+			}
+		},
+		global = {
+			Region = nil,              -- region that this user is in, all characters on the same account should be the same region
+			Shopping = {},             -- shopping list data stored globally for access on any character
+			Logging = {                -- a lot of log data is stored globally for simplicity, can only be raiding with one character at a time
+				Wipes = {},            -- times that a wipe was called
+				PlayerData = {},       -- player data gathered at fight start
+				PlayerExtras = {}      -- player extra data like auras, gathered at fight start
+			},
+			TeamOpt = {                -- this stuff is stored globally in case a player e.g. switches to an alt in a raid group
+				LootGear = {},         -- gear info that needs to be transmitted with the next loot
+				Rankings = {},         -- last rankings imported by the loot ranker
+				RankingString = nil    -- last ranking string imported, kept around for efficient serialization
+			}
+		}
+	}
+	
+	-- set defaults for auto-logging
+	for i, instanceId in ipairs(Amr.InstanceIdsOrdered) do
+		local byDiff = defaults.profile.Logging.Auto[instanceId]
+		if not byDiff then
+			byDiff = {}
+			defaults.profile.Logging.Auto[instanceId] = byDiff
+		end
+		
+		for k, difficultyId in pairs(Amr.Difficulties) do
+			if byDiff[difficultyId] == nil then
+				byDiff[difficultyId] = false
+			end
+		end
+	end
+	
+	Amr.db = LibStub("AceDB-3.0"):New("AskMrRobotDb2", defaults)
+	
+	Amr.db.RegisterCallback(Amr, "OnProfileChanged", "RefreshConfig")
+	Amr.db.RegisterCallback(Amr, "OnProfileCopied", "RefreshConfig")
+	Amr.db.RegisterCallback(Amr, "OnProfileReset", "RefreshConfig")
+end
+
+function Amr:OnInitialize()
+    
+	initializeDb()
+
+	Amr:RegisterChatCommand("amr", "SlashCommand")
+	
+	_icon:Register(Amr.ADDON_NAME, _amrLDB, self.db.profile.minimap)	
+
+	-- listen for inter-addon communication
+	self:RegisterComm(Amr.ChatPrefix, "OnCommReceived")	
+end
+
+local _enteredWorld = false
+local _pendingInit = false
+
+function finishInitialize()
+
+	-- record region, the only thing that we still can't get from the log file
+	Amr.db.global.Region = Amr.RegionNames[GetCurrentRegion()]
+	
+	-- make sure that some initialization is deferred until after PLAYER_ENTERING_WORLD event so that data we need is available;
+	-- also delay this initialization for a few extra seconds to deal with some event spam that is otherwise hard to identify and ignore when a player logs in
+	Amr.Wait(5, function()
+		Amr:InitializeVersions()
+		Amr:InitializeGear()
+		Amr:InitializeExport()
+		Amr:InitializeCombatLog()
+		Amr:InitializeTeamOpt()
+	end)
+end
+
+function onPlayerEnteringWorld()
+
+	_enteredWorld = true
+	
+	if _pendingInit then
+		finishInitialize()
+		_pendingInit = false
+	end
+end
+
+function Amr:OnEnable()
+    
+	-- listen for changes to the snapshot enable state, and always make sure it is enabled if using the core AskMrRobot addon
+	self:RegisterMessage("AMR_SNAPSHOT_STATE_CHANGED", function(eventName, isEnabled)
+		if not isEnabled then
+			-- immediately re-enable on any attempt to disable
+			Amr.Serializer:EnableSnapshots()
+		end	
+	end)
+	self.Serializer:EnableSnapshots()
+	
+	-- update based on current configuration whenever enabled
+	self:RefreshConfig()
+	
+	-- if we have fully entered the world, do initialization; otherwise wait for PLAYER_ENTERING_WORLD to continue
+	if not _enteredWorld then
+		_pendingInit = true
+	else
+		_pendingInit = false
+		finishInitialize()
+	end
+end
+
+function Amr:OnDisable()
+	-- disabling is not supported
+end
+
+
+----------------------------------------------------------------------------------------
+-- Slash Commands
+----------------------------------------------------------------------------------------
+local _slashMethods = {
+	hide      = "Hide",
+	show      = "Show",
+	toggle    = "Toggle",
+	equip     = "EquipGearSet",     -- parameter is "primary" or "secondary", or no parameter to toggle
+	version   = "PrintVersions",
+	wipe      = "Wipe",
+	undowipe  = "UndoWipe",
+	test      = "Test"
+}
+
+function Amr:SlashCommand(input)
+	input = string.lower(input)
+	local parts = {}
+	for w in input:gmatch("%S+") do 
+		table.insert(parts, w) 
+	end
+	
+	if #parts == 0 then return end
+	
+	local func = _slashMethods[parts[1]]
+	if not func then return end
+	
+	local funcArgs = {}
+	for i = 2, #parts do
+		table.insert(funcArgs, parts[i])
+	end
+	
+	Amr[func](Amr, unpack(funcArgs))
+end
+
+
+----------------------------------------------------------------------------------------
+-- Configuration
+----------------------------------------------------------------------------------------
+
+-- refresh all state based on the current values of configuration options
+function Amr:RefreshConfig()
+	
+	self:UpdateMinimap()	
+	self:RefreshOptionsUi()
+	self:RefreshLogUi()
+end
+
+function Amr:UpdateMinimap()
+
+	if self.db.profile.minimap.hide or not Amr:IsEnabled() then
+		_icon:Hide(Amr.ADDON_NAME)
+	else
+		-- change icon color if logging
+		if Amr:IsLogging() then
+			_amrLDB.icon = 'Interface\\AddOns\\AskMrRobot\\Media\\icon_green'
+		else
+			_amrLDB.icon = 'Interface\\AddOns\\AskMrRobot\\Media\\icon'
+		end
+		
+		_icon:Show(Amr.ADDON_NAME)
+	end
+end
+
+
+----------------------------------------------------------------------------------------
+-- Version Checking
+----------------------------------------------------------------------------------------
+
+-- version of addon being run by each person in the player's raid or group
+Amr.GroupVersions = {}
+
+local function toGroupVersionKey(realm, name)
+	realm = string.gsub(realm, "%s+", "")
+	return name .. "-" .. realm
+end
+
+-- prune out version information for players no longer in the current raid group
+local function pruneVersionInfo()
+
+	local newVersions = {}
+	local units = Amr:GetGroupUnitIdentifiers()
+	
+    for i, unitId in ipairs(units) do
+		local realm, name = Amr:GetRealmAndName(unitId)	
+		if realm then
+			local key = toGroupVersionKey(realm, name)
+			newVersions[key] = Amr.GroupVersions[key]
+		end
+    end
+	
+	Amr.GroupVersions = newVersions	
+end
+
+-- send version information to other people in the same raid group
+local function sendVersionInfo()
+	
+    local realm = GetRealmName()
+    local name = UnitName("player")
+	local ver = GetAddOnMetadata(Amr.ADDON_NAME, "Version")
+	
+	local msg = string.format("%s\n%s\n%s\n%s", Amr.MessageTypes.Version, realm, name, ver)
+	Amr:SendAmrCommMessage(msg)
+end
+
+local function onVersionInfoReceived(message)
+    
+	-- message will be of format: realm\nname\nversion
+    local parts = {}
+	for part in string.gmatch(message, "([^\n]+)") do
+		table.insert(parts, part)
+	end
+    
+    local key = toGroupVersionKey(parts[2], parts[3])
+    local ver = parts[4]
+	
+	Amr.GroupVersions[key] = tonumber(ver)
+	
+	-- make sure that versions are properly pruned in case this message arrived late and the player has since been removed from the group
+	pruneVersionInfo()
+end
+
+-- get the addon version another person in the player's raid/group is running, or 0 if they are not running the addon
+function Amr:GetAddonVersion(realm, name)
+	local ver = Amr.GroupVersions[toGroupVersionKey(realm, name)]
+	return ver or 0
+end
+
+function Amr:PrintVersions()
+
+	if not IsInGroup() and not IsInRaid() then
+		self:Print(L.VersionChatNotGrouped)
+		return
+	end
+	
+	local units = self:GetGroupUnitIdentifiers()
+	
+	local msg = {}
+	table.insert(msg, L.VersionChatTitle)
+	
+	for i, unitId in ipairs(units) do
+		local realm, name = self:GetRealmAndName(unitId)
+		if realm then
+			local key = toGroupVersionKey(realm, name)
+			local ver = Amr.GroupVersions[key]
+			if not ver then
+				table.insert(msg, key .. " |cFFFF0000" .. L.VersionChatNotInstalled .. "|r")
+			else
+				table.insert(msg, key .. " v" .. ver)
+			end
+		end
+	end
+	
+	msg = table.concat(msg, "\n")
+	print(msg)
+end
+
+function Amr:InitializeVersions()
+	Amr:AddEventHandler("GROUP_ROSTER_UPDATE", pruneVersionInfo)
+	Amr:AddEventHandler("GROUP_ROSTER_UPDATE", sendVersionInfo)
+
+	-- request version information from anyone in my group upon initialization
+	if IsInGroup() or IsInRaid() then
+		Amr:SendAmrCommMessage(Amr.MessageTypes.VersionRequest)
+	end
+end
+
+
+----------------------------------------------------------------------------------------
+-- Generic Helpers
+----------------------------------------------------------------------------------------
+
+local _waitTable = {}
+local _waitFrame = nil
+
+-- execute the specified function after the specified delay (in seconds)
+function Amr.Wait(delay, func, ...)
+	if not _waitFrame then
+		_waitFrame = CreateFrame("Frame", "AmrWaitFrame", UIParent)
+		_waitFrame:SetScript("OnUpdate", function (self, elapse)
+			local count = #_waitTable
+			local i = 1
+			while(i <= count) do
+				local waitRecord = table.remove(_waitTable, i)
+				local d = table.remove(waitRecord, 1)
+				local f = table.remove(waitRecord, 1)
+				local p = table.remove(waitRecord, 1)
+				if d > elapse then
+					table.insert(_waitTable, i, { d-elapse, f, p })
+					i = i + 1
+				else
+					count = count - 1
+					f(unpack(p))
+				end
+			end
+		end)
+	end
+	table.insert(_waitTable, { delay, func, {...} })
+	return true
+end
+
+-- helper to iterate over a table in order by its keys
+function Amr.spairs(t, order)
+    -- collect the keys
+    local keys = {}
+    for k in pairs(t) do keys[#keys+1] = k end
+
+    -- if order function given, sort by it by passing the table and keys a, b,
+    -- otherwise just sort the keys 
+    if order then
+        table.sort(keys, function(a,b) return order(t, a, b) end)
+    else
+        table.sort(keys)
+    end
+
+    -- return the iterator function
+    local i = 0
+    return function()
+        i = i + 1
+        if keys[i] then
+            return keys[i], t[keys[i]]
+        end
+    end
+end
+
+function Amr.StartsWith(str, prefix)
+	if string.len(str) < string.len(prefix) then return false end
+	return string.sub(str, 1, string.len(prefix)) == prefix
+end
+
+-- helper to get the unit identifiers (e.g. to pass to GetUnitName) for all members of the player's current group/raid
+function Amr:GetGroupUnitIdentifiers()
+
+	local units = {}    
+    if IsInRaid() then
+        for i = 1,40 do
+            table.insert(units, "raid" .. i)
+        end
+    elseif IsInGroup() then
+        table.insert(units, "player")
+        for i = 1,4 do
+            table.insert(units, "party" .. i)
+        end
+	else
+		table.insert(units, "player")
+    end
+	
+	return units
+end
+
+-- helper to get the realm and name from a unitId (e.g. "player" or "raid1")
+function Amr:GetRealmAndName(unitId)
+
+	local name = GetUnitName(unitId, true)
+	if not name then return end
+	
+	local realm = GetRealmName()
+	local splitPos = string.find(name, "-")
+	if splitPos ~= nil then
+		realm = string.sub(name, splitPos + 1)
+		name = string.sub(name, 1, splitPos - 1)
+	end
+	
+	return realm, name
+end
+
+-- find the unitid of a player given the name and realm... this comes from the server so the realm will be in english...
+-- TODO: more robust handling of players with same name but different realms in the same group on non-english clients
+function Amr:GetUnitId(unitRealm, unitName)
+
+	local nameMatches = {}
+	
+	local units = Amr:GetGroupUnitIdentifiers()
+	for i, unitId in ipairs(units) do
+		local realm, name = Amr:GetRealmAndName(unitId)	
+		if realm then
+			-- remove spaces to ensure proper matches
+			realm = string.gsub(realm, "%s+", "")
+			unitRealm = string.gsub(unitRealm, "%s+", "")
+			
+			if unitRealm == realm and unitName == name then return unitId end
+			if unitName == name then
+				table.insert(nameMatches, unitId)
+			end
+		end
+	end
+	
+	-- only one player with same name, must be the player of interest
+	if #nameMatches == 1 then return nameMatches[1] end
+
+	-- could not find or ambiguous
+	return nil
+end
+
+
+-- scanning tooltip b/c for some odd reason the api has no way to get basic item properties...
+-- so you have to generate a fake item tooltip and search for pre-defined strings in the display text
+local _scanTt
+function Amr:GetScanningTooltip()
+	if not _scanTt then
+		_scanTt = CreateFrame("GameTooltip", "AmrUiScanTooltip", nil, "GameTooltipTemplate")
+		_scanTt:SetOwner(UIParent, "ANCHOR_NONE")
+	end
+	return _scanTt
+end
+
+local function scanTooltipHelper(txt, ...)
+	for i = 1, select("#", ...) do
+        local region = select(i, ...)
+        if region and region:GetObjectType() == "FontString" then
+            local text = region:GetText() -- string or nil
+			print(text)
+        end
+    end
+end
+
+-- search the tooltip for txt, returns true if it is encountered on any line
+function Amr:IsTextInTooltip(tt, txt)
+	local regions = { tt:GetRegions() }
+	for i, region in ipairs(regions) do
+		if region and region:GetObjectType() == "FontString" then
+            if region:GetText() == txt then
+				return true
+			end
+        end	
+	end
+	return false
+end
+
+-- helper to determine if an item in the player's bag is soulbound
+function Amr:IsSoulbound(bagId, slotId)
+	local tt = self:GetScanningTooltip()
+	tt:ClearLines()
+	if bagId then
+		tt:SetBagItem(bagId, slotId)
+	else
+		tt:SetInventoryItem("player", slotId)
+	end
+	return self:IsTextInTooltip(tt, ITEM_SOULBOUND)
+end
+
+-- helper to determine if an item has a unique constraint
+function Amr:IsUnique(bagId, slotId)
+	local tt = self:GetScanningTooltip()
+	tt:ClearLines()
+	if bagId then
+		tt:SetBagItem(bagId, slotId)
+	else
+		tt:SetInventoryItem("player", slotId)
+	end
+	if self:IsTextInTooltip(tt, ITEM_UNIQUE_EQUIPPABLE)	then return true end
+	if self:IsTextInTooltip(tt, ITEM_UNIQUE) then return true end
+	return false
+end
+
+
+----------------------------------------------------------------------------------------
+-- Inter-Addon Communication
+----------------------------------------------------------------------------------------
+function Amr:SendAmrCommMessage(message, channel)
+	-- prepend version to all messages
+	local v = GetAddOnMetadata(Amr.ADDON_NAME, "Version")
+	message = v .. "\r" .. message
+	
+	Amr:SendCommMessage(Amr.ChatPrefix, message, channel or "RAID")
+end
+
+function Amr:OnCommReceived(prefix, message, distribution, sender)
+
+	local parts = {}
+	for part in string.gmatch(message, "([^\r]+)") do
+		table.insert(parts, part)
+	end
+	
+	local ver = parts[1]	
+	if ver then ver = tonumber(ver) end
+	if ver then
+		-- newest versions of the addon start all messages with a version number
+		message = parts[2]
+	end
+		
+	-- we always allow version checks, even from old versions of the addon that aren't otherwise compatible
+	if Amr.StartsWith(message, Amr.MessageTypes.Version) then
+		-- version checking between group members
+		if Amr.StartsWith(message, Amr.MessageTypes.VersionRequest) then
+			sendVersionInfo()
+		else
+			onVersionInfoReceived(message)
+		end
+		
+		return
+	end
+
+	-- any other kind of message is ignored if the version is too old
+	if not ver or ver < Amr.MIN_ADDON_VERSION then return end
+	
+	if Amr.StartsWith(message, Amr.MessageTypes.Team) then	
+		-- if fully initialized, process team optimizer messages
+		if Amr["ProcessTeamMessage"] then
+			Amr:ProcessTeamMessage(message)
+		end
+	else
+		-- if we are fully loaded, process a player snapshot when it is received (combat logging)
+		if Amr["ProcessPlayerSnapshot"] then
+			self:ProcessPlayerSnapshot(message)
+		end
+	end
+end
+
+
+----------------------------------------------------------------------------------------
+-- Events
+----------------------------------------------------------------------------------------
+local _eventHandlers = {}
+
+local function handleEvent(eventName, ...)
+	local list = _eventHandlers[eventName]
+	if list then
+		--print(eventName .. " handled")
+		for i, handler in ipairs(list) do
+			if type(handler) == "function" then
+				handler(select(1, ...))
+			else
+				Amr[handler](Amr, select(1, ...))
+			end
+		end
+	end
+end
+
+-- WoW and Ace seem to work on a "one handler" kind of approach to events (as far as I can tell from the sparse documentation of both).
+-- This is a simple wrapper to allow adding multiple handlers to the same event, thus allowing better encapsulation of code from file to file.
+function Amr:AddEventHandler(eventName, methodOrName)
+	local list = _eventHandlers[eventName]
+	if not list then
+		list = {}
+		_eventHandlers[eventName] = list
+		Amr:RegisterEvent(eventName, handleEvent)
+	end
+	table.insert(list, methodOrName)
+end
+
+Amr:AddEventHandler("PLAYER_ENTERING_WORLD", onPlayerEnteringWorld)
+
+
+----------------------------------------------------------------------------------------
+-- Debugging
+----------------------------------------------------------------------------------------
+--[[
+function Amr:Test(val1, val2, val3)
+
+	local link = GetLootSlotLink(tonumber(val1))
+	local index = Amr:TestLootIndex(link)
+	print("loot index: " .. index)
+	
+	if val2 then
+		local candidate = Amr:TestLootCandidate(link, val2, val3)
+		print("loot candidate: " .. candidate)
+		
+		GiveMasterLoot(index, candidate)
+	end
+end
+]]
\ No newline at end of file