comparison Core.lua @ 57:01b63b8ed811 v21

total rewrite to version 21
author yellowfive
date Fri, 05 Jun 2015 11:05:15 -0700
parents
children ee701ce45354
comparison
equal deleted inserted replaced
56:75431c084aa0 57:01b63b8ed811
1 -- AskMrRobot
2 -- Does cool stuff associated with askmrrobot.com:
3 -- Import/Export gear and optimization solutions from/to the website
4 -- Improve the combat logging experience and augment it with extra data not available directly in the log file
5 -- Team Optimizer convenience functionality
6
7 AskMrRobot = LibStub("AceAddon-3.0"):NewAddon("AskMrRobot", "AceEvent-3.0", "AceComm-3.0", "AceConsole-3.0", "AceSerializer-3.0")
8 local Amr = AskMrRobot
9 Amr.Serializer = LibStub("AskMrRobot-Serializer")
10
11 Amr.ADDON_NAME = "AskMrRobot"
12
13 -- types of inter-addon messages that we receive, used to parcel them out to the proper handlers
14 Amr.MessageTypes = {
15 Version = "_V",
16 VersionRequest = "_VR",
17 Team = "_T"
18 }
19
20 local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true)
21 local AceGUI = LibStub("AceGUI-3.0")
22
23 -- minimap icon and LDB support
24 local _amrLDB = LibStub("LibDataBroker-1.1"):NewDataObject(Amr.ADDON_NAME, {
25 type = "launcher",
26 text = "Ask Mr. Robot",
27 icon = "Interface\\AddOns\\" .. Amr.ADDON_NAME .. "\\Media\\icon",
28 OnClick = function(self, button, down)
29 if button == "LeftButton" then
30 if IsControlKeyDown() then
31 Amr:Wipe()
32 else
33 Amr:Toggle()
34 end
35 elseif button == "RightButton" then
36 Amr:EquipGearSet()
37 end
38 end,
39 OnTooltipShow = function(tt)
40 tt:AddLine("Ask Mr. Robot", 1, 1, 1);
41 tt:AddLine(" ");
42 tt:AddLine(L.MinimapTooltip)
43 end
44 })
45 local _icon = LibStub("LibDBIcon-1.0")
46
47
48 -- initialize the database
49 local function initializeDb()
50
51 local defaults = {
52 char = {
53 FirstUse = true, -- true if this is first time use, gets cleared after seeing the export help splash window
54 SubSpecs = {}, -- last seen subspecs for this character, used to deal with some ambiguous specs
55 Equipped = {}, -- for each spec group (1 or 2), slot id to item link
56 BagItems = {}, -- list of item links for bag
57 BankItems = {}, -- list of item links for bank
58 VoidItems = {}, -- list of item links for void storage
59 BagItemsAndCounts = {}, -- used mainly for the shopping list
60 BankItemsAndCounts = {}, -- used mainly for the shopping list
61 GearSets = {}, -- imported gear sets, key by spec group (1 or 2), slot id to item object
62 ExtraItemData = {}, -- for each spec group (1 or 2): mainly for legacy support, item id to object with socketColor and duplicateId information
63 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)
64 ExtraEnchantData = {}, -- for each spec group (1 or 2): enchant id to enchant display information and material information
65 Logging = { -- character logging settings
66 Enabled = false, -- whether logging is currently on or not
67 LastZone = nil, -- last zone the player was in
68 LastDiff = nil, -- last difficulty for the last zone the player was in
69 LastWipe = nil -- last time a wipe was called by this player
70 },
71 TeamOpt = {
72 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
73 History = {}, -- history of drops since joining the current group
74 Rolls = {}, -- current loot choices for a loot distribution in progress
75 Role = nil, -- Leader or Member, changes UI to the mode most appropriate for this user
76 Loot = {}, -- the last loot seen by the master looter
77 LootGuid = nil, -- guid of the last unit looted by the master looter, will be "container" if there is no target
78 LootInProgress = false -- true if looting is currently in progress
79 }
80 },
81 profile = {
82 minimap = { -- minimap hide/show and position settings
83 hide = false
84 },
85 window = {}, -- main window position settings
86 lootWindow = {}, -- loot window position settings
87 shopWindow = {}, -- shopping list window position settings
88 options = {
89 autoGear = false, -- auto-equip saved gear sets when changing specs
90 shopAh = false -- auto-show shopping list at AH
91 },
92 Logging = { -- global logging settings
93 Auto = {} -- for each instanceId, for each difficultyId, true if auto-logging enabled
94 }
95 },
96 global = {
97 Region = nil, -- region that this user is in, all characters on the same account should be the same region
98 Shopping = {}, -- shopping list data stored globally for access on any character
99 Logging = { -- a lot of log data is stored globally for simplicity, can only be raiding with one character at a time
100 Wipes = {}, -- times that a wipe was called
101 PlayerData = {}, -- player data gathered at fight start
102 PlayerExtras = {} -- player extra data like auras, gathered at fight start
103 },
104 TeamOpt = { -- this stuff is stored globally in case a player e.g. switches to an alt in a raid group
105 LootGear = {}, -- gear info that needs to be transmitted with the next loot
106 Rankings = {}, -- last rankings imported by the loot ranker
107 RankingString = nil -- last ranking string imported, kept around for efficient serialization
108 }
109 }
110 }
111
112 -- set defaults for auto-logging
113 for i, instanceId in ipairs(Amr.InstanceIdsOrdered) do
114 local byDiff = defaults.profile.Logging.Auto[instanceId]
115 if not byDiff then
116 byDiff = {}
117 defaults.profile.Logging.Auto[instanceId] = byDiff
118 end
119
120 for k, difficultyId in pairs(Amr.Difficulties) do
121 if byDiff[difficultyId] == nil then
122 byDiff[difficultyId] = false
123 end
124 end
125 end
126
127 Amr.db = LibStub("AceDB-3.0"):New("AskMrRobotDb2", defaults)
128
129 Amr.db.RegisterCallback(Amr, "OnProfileChanged", "RefreshConfig")
130 Amr.db.RegisterCallback(Amr, "OnProfileCopied", "RefreshConfig")
131 Amr.db.RegisterCallback(Amr, "OnProfileReset", "RefreshConfig")
132 end
133
134 function Amr:OnInitialize()
135
136 initializeDb()
137
138 Amr:RegisterChatCommand("amr", "SlashCommand")
139
140 _icon:Register(Amr.ADDON_NAME, _amrLDB, self.db.profile.minimap)
141
142 -- listen for inter-addon communication
143 self:RegisterComm(Amr.ChatPrefix, "OnCommReceived")
144 end
145
146 local _enteredWorld = false
147 local _pendingInit = false
148
149 function finishInitialize()
150
151 -- record region, the only thing that we still can't get from the log file
152 Amr.db.global.Region = Amr.RegionNames[GetCurrentRegion()]
153
154 -- make sure that some initialization is deferred until after PLAYER_ENTERING_WORLD event so that data we need is available;
155 -- 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
156 Amr.Wait(5, function()
157 Amr:InitializeVersions()
158 Amr:InitializeGear()
159 Amr:InitializeExport()
160 Amr:InitializeCombatLog()
161 Amr:InitializeTeamOpt()
162 end)
163 end
164
165 function onPlayerEnteringWorld()
166
167 _enteredWorld = true
168
169 if _pendingInit then
170 finishInitialize()
171 _pendingInit = false
172 end
173 end
174
175 function Amr:OnEnable()
176
177 -- listen for changes to the snapshot enable state, and always make sure it is enabled if using the core AskMrRobot addon
178 self:RegisterMessage("AMR_SNAPSHOT_STATE_CHANGED", function(eventName, isEnabled)
179 if not isEnabled then
180 -- immediately re-enable on any attempt to disable
181 Amr.Serializer:EnableSnapshots()
182 end
183 end)
184 self.Serializer:EnableSnapshots()
185
186 -- update based on current configuration whenever enabled
187 self:RefreshConfig()
188
189 -- if we have fully entered the world, do initialization; otherwise wait for PLAYER_ENTERING_WORLD to continue
190 if not _enteredWorld then
191 _pendingInit = true
192 else
193 _pendingInit = false
194 finishInitialize()
195 end
196 end
197
198 function Amr:OnDisable()
199 -- disabling is not supported
200 end
201
202
203 ----------------------------------------------------------------------------------------
204 -- Slash Commands
205 ----------------------------------------------------------------------------------------
206 local _slashMethods = {
207 hide = "Hide",
208 show = "Show",
209 toggle = "Toggle",
210 equip = "EquipGearSet", -- parameter is "primary" or "secondary", or no parameter to toggle
211 version = "PrintVersions",
212 wipe = "Wipe",
213 undowipe = "UndoWipe",
214 test = "Test"
215 }
216
217 function Amr:SlashCommand(input)
218 input = string.lower(input)
219 local parts = {}
220 for w in input:gmatch("%S+") do
221 table.insert(parts, w)
222 end
223
224 if #parts == 0 then return end
225
226 local func = _slashMethods[parts[1]]
227 if not func then return end
228
229 local funcArgs = {}
230 for i = 2, #parts do
231 table.insert(funcArgs, parts[i])
232 end
233
234 Amr[func](Amr, unpack(funcArgs))
235 end
236
237
238 ----------------------------------------------------------------------------------------
239 -- Configuration
240 ----------------------------------------------------------------------------------------
241
242 -- refresh all state based on the current values of configuration options
243 function Amr:RefreshConfig()
244
245 self:UpdateMinimap()
246 self:RefreshOptionsUi()
247 self:RefreshLogUi()
248 end
249
250 function Amr:UpdateMinimap()
251
252 if self.db.profile.minimap.hide or not Amr:IsEnabled() then
253 _icon:Hide(Amr.ADDON_NAME)
254 else
255 -- change icon color if logging
256 if Amr:IsLogging() then
257 _amrLDB.icon = 'Interface\\AddOns\\AskMrRobot\\Media\\icon_green'
258 else
259 _amrLDB.icon = 'Interface\\AddOns\\AskMrRobot\\Media\\icon'
260 end
261
262 _icon:Show(Amr.ADDON_NAME)
263 end
264 end
265
266
267 ----------------------------------------------------------------------------------------
268 -- Version Checking
269 ----------------------------------------------------------------------------------------
270
271 -- version of addon being run by each person in the player's raid or group
272 Amr.GroupVersions = {}
273
274 local function toGroupVersionKey(realm, name)
275 realm = string.gsub(realm, "%s+", "")
276 return name .. "-" .. realm
277 end
278
279 -- prune out version information for players no longer in the current raid group
280 local function pruneVersionInfo()
281
282 local newVersions = {}
283 local units = Amr:GetGroupUnitIdentifiers()
284
285 for i, unitId in ipairs(units) do
286 local realm, name = Amr:GetRealmAndName(unitId)
287 if realm then
288 local key = toGroupVersionKey(realm, name)
289 newVersions[key] = Amr.GroupVersions[key]
290 end
291 end
292
293 Amr.GroupVersions = newVersions
294 end
295
296 -- send version information to other people in the same raid group
297 local function sendVersionInfo()
298
299 local realm = GetRealmName()
300 local name = UnitName("player")
301 local ver = GetAddOnMetadata(Amr.ADDON_NAME, "Version")
302
303 local msg = string.format("%s\n%s\n%s\n%s", Amr.MessageTypes.Version, realm, name, ver)
304 Amr:SendAmrCommMessage(msg)
305 end
306
307 local function onVersionInfoReceived(message)
308
309 -- message will be of format: realm\nname\nversion
310 local parts = {}
311 for part in string.gmatch(message, "([^\n]+)") do
312 table.insert(parts, part)
313 end
314
315 local key = toGroupVersionKey(parts[2], parts[3])
316 local ver = parts[4]
317
318 Amr.GroupVersions[key] = tonumber(ver)
319
320 -- make sure that versions are properly pruned in case this message arrived late and the player has since been removed from the group
321 pruneVersionInfo()
322 end
323
324 -- get the addon version another person in the player's raid/group is running, or 0 if they are not running the addon
325 function Amr:GetAddonVersion(realm, name)
326 local ver = Amr.GroupVersions[toGroupVersionKey(realm, name)]
327 return ver or 0
328 end
329
330 function Amr:PrintVersions()
331
332 if not IsInGroup() and not IsInRaid() then
333 self:Print(L.VersionChatNotGrouped)
334 return
335 end
336
337 local units = self:GetGroupUnitIdentifiers()
338
339 local msg = {}
340 table.insert(msg, L.VersionChatTitle)
341
342 for i, unitId in ipairs(units) do
343 local realm, name = self:GetRealmAndName(unitId)
344 if realm then
345 local key = toGroupVersionKey(realm, name)
346 local ver = Amr.GroupVersions[key]
347 if not ver then
348 table.insert(msg, key .. " |cFFFF0000" .. L.VersionChatNotInstalled .. "|r")
349 else
350 table.insert(msg, key .. " v" .. ver)
351 end
352 end
353 end
354
355 msg = table.concat(msg, "\n")
356 print(msg)
357 end
358
359 function Amr:InitializeVersions()
360 Amr:AddEventHandler("GROUP_ROSTER_UPDATE", pruneVersionInfo)
361 Amr:AddEventHandler("GROUP_ROSTER_UPDATE", sendVersionInfo)
362
363 -- request version information from anyone in my group upon initialization
364 if IsInGroup() or IsInRaid() then
365 Amr:SendAmrCommMessage(Amr.MessageTypes.VersionRequest)
366 end
367 end
368
369
370 ----------------------------------------------------------------------------------------
371 -- Generic Helpers
372 ----------------------------------------------------------------------------------------
373
374 local _waitTable = {}
375 local _waitFrame = nil
376
377 -- execute the specified function after the specified delay (in seconds)
378 function Amr.Wait(delay, func, ...)
379 if not _waitFrame then
380 _waitFrame = CreateFrame("Frame", "AmrWaitFrame", UIParent)
381 _waitFrame:SetScript("OnUpdate", function (self, elapse)
382 local count = #_waitTable
383 local i = 1
384 while(i <= count) do
385 local waitRecord = table.remove(_waitTable, i)
386 local d = table.remove(waitRecord, 1)
387 local f = table.remove(waitRecord, 1)
388 local p = table.remove(waitRecord, 1)
389 if d > elapse then
390 table.insert(_waitTable, i, { d-elapse, f, p })
391 i = i + 1
392 else
393 count = count - 1
394 f(unpack(p))
395 end
396 end
397 end)
398 end
399 table.insert(_waitTable, { delay, func, {...} })
400 return true
401 end
402
403 -- helper to iterate over a table in order by its keys
404 function Amr.spairs(t, order)
405 -- collect the keys
406 local keys = {}
407 for k in pairs(t) do keys[#keys+1] = k end
408
409 -- if order function given, sort by it by passing the table and keys a, b,
410 -- otherwise just sort the keys
411 if order then
412 table.sort(keys, function(a,b) return order(t, a, b) end)
413 else
414 table.sort(keys)
415 end
416
417 -- return the iterator function
418 local i = 0
419 return function()
420 i = i + 1
421 if keys[i] then
422 return keys[i], t[keys[i]]
423 end
424 end
425 end
426
427 function Amr.StartsWith(str, prefix)
428 if string.len(str) < string.len(prefix) then return false end
429 return string.sub(str, 1, string.len(prefix)) == prefix
430 end
431
432 -- helper to get the unit identifiers (e.g. to pass to GetUnitName) for all members of the player's current group/raid
433 function Amr:GetGroupUnitIdentifiers()
434
435 local units = {}
436 if IsInRaid() then
437 for i = 1,40 do
438 table.insert(units, "raid" .. i)
439 end
440 elseif IsInGroup() then
441 table.insert(units, "player")
442 for i = 1,4 do
443 table.insert(units, "party" .. i)
444 end
445 else
446 table.insert(units, "player")
447 end
448
449 return units
450 end
451
452 -- helper to get the realm and name from a unitId (e.g. "player" or "raid1")
453 function Amr:GetRealmAndName(unitId)
454
455 local name = GetUnitName(unitId, true)
456 if not name then return end
457
458 local realm = GetRealmName()
459 local splitPos = string.find(name, "-")
460 if splitPos ~= nil then
461 realm = string.sub(name, splitPos + 1)
462 name = string.sub(name, 1, splitPos - 1)
463 end
464
465 return realm, name
466 end
467
468 -- find the unitid of a player given the name and realm... this comes from the server so the realm will be in english...
469 -- TODO: more robust handling of players with same name but different realms in the same group on non-english clients
470 function Amr:GetUnitId(unitRealm, unitName)
471
472 local nameMatches = {}
473
474 local units = Amr:GetGroupUnitIdentifiers()
475 for i, unitId in ipairs(units) do
476 local realm, name = Amr:GetRealmAndName(unitId)
477 if realm then
478 -- remove spaces to ensure proper matches
479 realm = string.gsub(realm, "%s+", "")
480 unitRealm = string.gsub(unitRealm, "%s+", "")
481
482 if unitRealm == realm and unitName == name then return unitId end
483 if unitName == name then
484 table.insert(nameMatches, unitId)
485 end
486 end
487 end
488
489 -- only one player with same name, must be the player of interest
490 if #nameMatches == 1 then return nameMatches[1] end
491
492 -- could not find or ambiguous
493 return nil
494 end
495
496
497 -- scanning tooltip b/c for some odd reason the api has no way to get basic item properties...
498 -- so you have to generate a fake item tooltip and search for pre-defined strings in the display text
499 local _scanTt
500 function Amr:GetScanningTooltip()
501 if not _scanTt then
502 _scanTt = CreateFrame("GameTooltip", "AmrUiScanTooltip", nil, "GameTooltipTemplate")
503 _scanTt:SetOwner(UIParent, "ANCHOR_NONE")
504 end
505 return _scanTt
506 end
507
508 local function scanTooltipHelper(txt, ...)
509 for i = 1, select("#", ...) do
510 local region = select(i, ...)
511 if region and region:GetObjectType() == "FontString" then
512 local text = region:GetText() -- string or nil
513 print(text)
514 end
515 end
516 end
517
518 -- search the tooltip for txt, returns true if it is encountered on any line
519 function Amr:IsTextInTooltip(tt, txt)
520 local regions = { tt:GetRegions() }
521 for i, region in ipairs(regions) do
522 if region and region:GetObjectType() == "FontString" then
523 if region:GetText() == txt then
524 return true
525 end
526 end
527 end
528 return false
529 end
530
531 -- helper to determine if an item in the player's bag is soulbound
532 function Amr:IsSoulbound(bagId, slotId)
533 local tt = self:GetScanningTooltip()
534 tt:ClearLines()
535 if bagId then
536 tt:SetBagItem(bagId, slotId)
537 else
538 tt:SetInventoryItem("player", slotId)
539 end
540 return self:IsTextInTooltip(tt, ITEM_SOULBOUND)
541 end
542
543 -- helper to determine if an item has a unique constraint
544 function Amr:IsUnique(bagId, slotId)
545 local tt = self:GetScanningTooltip()
546 tt:ClearLines()
547 if bagId then
548 tt:SetBagItem(bagId, slotId)
549 else
550 tt:SetInventoryItem("player", slotId)
551 end
552 if self:IsTextInTooltip(tt, ITEM_UNIQUE_EQUIPPABLE) then return true end
553 if self:IsTextInTooltip(tt, ITEM_UNIQUE) then return true end
554 return false
555 end
556
557
558 ----------------------------------------------------------------------------------------
559 -- Inter-Addon Communication
560 ----------------------------------------------------------------------------------------
561 function Amr:SendAmrCommMessage(message, channel)
562 -- prepend version to all messages
563 local v = GetAddOnMetadata(Amr.ADDON_NAME, "Version")
564 message = v .. "\r" .. message
565
566 Amr:SendCommMessage(Amr.ChatPrefix, message, channel or "RAID")
567 end
568
569 function Amr:OnCommReceived(prefix, message, distribution, sender)
570
571 local parts = {}
572 for part in string.gmatch(message, "([^\r]+)") do
573 table.insert(parts, part)
574 end
575
576 local ver = parts[1]
577 if ver then ver = tonumber(ver) end
578 if ver then
579 -- newest versions of the addon start all messages with a version number
580 message = parts[2]
581 end
582
583 -- we always allow version checks, even from old versions of the addon that aren't otherwise compatible
584 if Amr.StartsWith(message, Amr.MessageTypes.Version) then
585 -- version checking between group members
586 if Amr.StartsWith(message, Amr.MessageTypes.VersionRequest) then
587 sendVersionInfo()
588 else
589 onVersionInfoReceived(message)
590 end
591
592 return
593 end
594
595 -- any other kind of message is ignored if the version is too old
596 if not ver or ver < Amr.MIN_ADDON_VERSION then return end
597
598 if Amr.StartsWith(message, Amr.MessageTypes.Team) then
599 -- if fully initialized, process team optimizer messages
600 if Amr["ProcessTeamMessage"] then
601 Amr:ProcessTeamMessage(message)
602 end
603 else
604 -- if we are fully loaded, process a player snapshot when it is received (combat logging)
605 if Amr["ProcessPlayerSnapshot"] then
606 self:ProcessPlayerSnapshot(message)
607 end
608 end
609 end
610
611
612 ----------------------------------------------------------------------------------------
613 -- Events
614 ----------------------------------------------------------------------------------------
615 local _eventHandlers = {}
616
617 local function handleEvent(eventName, ...)
618 local list = _eventHandlers[eventName]
619 if list then
620 --print(eventName .. " handled")
621 for i, handler in ipairs(list) do
622 if type(handler) == "function" then
623 handler(select(1, ...))
624 else
625 Amr[handler](Amr, select(1, ...))
626 end
627 end
628 end
629 end
630
631 -- 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).
632 -- This is a simple wrapper to allow adding multiple handlers to the same event, thus allowing better encapsulation of code from file to file.
633 function Amr:AddEventHandler(eventName, methodOrName)
634 local list = _eventHandlers[eventName]
635 if not list then
636 list = {}
637 _eventHandlers[eventName] = list
638 Amr:RegisterEvent(eventName, handleEvent)
639 end
640 table.insert(list, methodOrName)
641 end
642
643 Amr:AddEventHandler("PLAYER_ENTERING_WORLD", onPlayerEnteringWorld)
644
645
646 ----------------------------------------------------------------------------------------
647 -- Debugging
648 ----------------------------------------------------------------------------------------
649 --[[
650 function Amr:Test(val1, val2, val3)
651
652 local link = GetLootSlotLink(tonumber(val1))
653 local index = Amr:TestLootIndex(link)
654 print("loot index: " .. index)
655
656 if val2 then
657 local candidate = Amr:TestLootCandidate(link, val2, val3)
658 print("loot candidate: " .. candidate)
659
660 GiveMasterLoot(index, candidate)
661 end
662 end
663 ]]