Mercurial > wow > askmrrobot
view Core.lua @ 58:3e395402ac52
Added tag v21 for changeset 01b63b8ed811
author | yellowfive |
---|---|
date | Fri, 05 Jun 2015 11:34:53 -0700 |
parents | 01b63b8ed811 |
children | ee701ce45354 |
line wrap: on
line source
-- 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 ]]