comparison core.lua @ 25:cb9635999171

- Reassigning loot, and marking loot as disenchanted/vault, will update the history entries also. - Tooltip help on dropdown menus appears at mouse now, instead of depending on Blizzard's "Beginner Tooltips" setting. - Forum BBcode output includes an option for MMO-Champion/Wowstead formatting. - Smarter cleanup functions for expiring caches. - Properly prefer locally-generated loot events, even when other users can see and rebroadcast the events back to you faster than you see them.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Wed, 05 Oct 2011 02:14:07 +0000
parents 61d932f0e8f2
children 68d7b903ee17
comparison
equal deleted inserted replaced
24:61d932f0e8f2 25:cb9635999171
57 ['snarky_boss'] = true, 57 ['snarky_boss'] = true,
58 ['keybinding'] = false, 58 ['keybinding'] = false,
59 ['bossmod'] = "DBM", 59 ['bossmod'] = "DBM",
60 ['keybinding_text'] = 'CTRL-SHIFT-O', 60 ['keybinding_text'] = 'CTRL-SHIFT-O',
61 ['forum'] = { 61 ['forum'] = {
62 ['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T', 62 ['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
63 ['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T',
63 ['[item] by name'] = '[item]$N[/item]$X - $T', 64 ['[item] by name'] = '[item]$N[/item]$X - $T',
64 ['[item] by ID'] = '[item]$I[/item]$X - $T', 65 ['[item] by ID'] = '[item]$I[/item]$X - $T',
65 ['Custom...'] = '', 66 ['Custom...'] = '',
66 }, 67 },
67 ['forum_current'] = '[item] by name', 68 ['forum_current'] = '[item] by name',
259 foo:add("blah") 260 foo:add("blah")
260 foo:test("blah") -- returns true 261 foo:test("blah") -- returns true
261 ]] 262 ]]
262 do 263 do
263 local caches = {} 264 local caches = {}
264 local cleanup_group = AnimTimerFrame:CreateAnimationGroup() 265 local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup()
265 local time = _G.time 266 local time = _G.time
266 cleanup_group:SetLooping("REPEAT") 267 cleanup_group:SetLooping("REPEAT")
267 cleanup_group:SetScript("OnLoop", function(cg) 268 cleanup_group:SetScript("OnLoop", function(cg)
268 addon.dprint('cache',"OnLoop firing") 269 addon.dprint('cache',"OnLoop firing")
269 local now = time() 270 local now = time()
270 local alldone = true 271 local alldone = true
271 -- this is ass-ugly 272 -- this is ass-ugly
272 for _,c in ipairs(caches) do 273 for name,c in pairs(caches) do
273 while (#c > 0) and (now - c[1].t > c.ttl) do 274 local fifo = c.fifo
274 addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m) 275 local active = #fifo > 0
275 tremove(c,1) 276 while (#fifo > 0) and (now - fifo[1].t > c.ttl) do
276 end 277 addon.dprint('cache', name, "cache removing",fifo[1].t, fifo[1].m)
277 alldone = alldone and (#c == 0) 278 tremove(fifo,1)
279 end
280 if active and #fifo == 0 and c.func then
281 addon.dprint('cache', name, "empty, firing cleanup")
282 c:func()
283 end
284 alldone = alldone and (#fifo == 0)
278 end 285 end
279 if alldone then 286 if alldone then
280 addon.dprint('cache',"OnLoop finishing animation group") 287 addon.dprint('cache',"OnLoop finishing animation group")
281 cleanup_group:Finish() 288 cleanup_group:Finish()
282 for _,c in ipairs(caches) do
283 if c.func then c:func() end
284 end
285 end 289 end
286 addon.dprint('cache',"OnLoop done") 290 addon.dprint('cache',"OnLoop done")
287 end) 291 end)
288 292
289 local function _add (cache, x) 293 local function _add (cache, x)
290 tinsert(cache, {t=time(),m=x}) 294 local datum = { t=time(), m=x }
295 cache.hash[x] = datum
296 tinsert (cache.fifo, datum)
291 if not cleanup_group:IsPlaying() then 297 if not cleanup_group:IsPlaying() then
292 addon.dprint('cache', cache.name, "STARTING animation group") 298 addon.dprint('cache', cache.name, "STARTING animation group")
293 cache.cleanup:SetDuration(2) -- hmmm 299 cache.cleanup:SetDuration(2) -- hmmm
294 cleanup_group:Play() 300 cleanup_group:Play()
295 end 301 end
296 end 302 end
297 local function _test (cache, x) 303 local function _test (cache, x)
298 for _,v in ipairs(cache) do 304 return cache.hash[x] ~= nil
305 --[[for _,v in ipairs(cache) do
299 if v.m == x then return true end 306 if v.m == x then return true end
300 end 307 end]]
301 end 308 end
309
302 function create_new_cache (name, ttl, on_alldone) 310 function create_new_cache (name, ttl, on_alldone)
311 -- setting OnFinished for cleanup fires at the end of each inner loop,
312 -- with no 'requested' argument to distinguish cases. thus, on_alldone.
303 local c = { 313 local c = {
304 ttl = ttl, 314 ttl = ttl,
305 name = name, 315 name = name,
306 add = _add, 316 add = _add,
307 test = _test, 317 test = _test,
308 cleanup = cleanup_group:CreateAnimation("Animation"), 318 cleanup = cleanup_group:CreateAnimation("Animation"),
309 func = on_alldone, 319 func = on_alldone,
320 fifo = {},
321 hash = setmetatable({}, {__mode='kv'}),
310 } 322 }
311 c.cleanup:SetOrder(1) 323 c.cleanup:SetOrder(1)
312 -- setting OnFinished for cleanup fires at the end of each inner loop, 324 caches[name] = c
313 -- with no 'requested' argument to distinguish cases. thus, on_alldone.
314 tinsert (caches, c)
315 return c 325 return c
316 end 326 end
317 end 327 end
318 328
319 329
335 for opt,default in pairs(option_defaults) do 345 for opt,default in pairs(option_defaults) do
336 if opts[opt] == nil then 346 if opts[opt] == nil then
337 opts[opt] = default 347 opts[opt] = default
338 end 348 end
339 end 349 end
350 -- transition&remove old options
351 opts['forum_use_itemid'] = nil
352 if opts['forum_format'] then
353 opts.forum['Custom...'] = opts['forum_format']
354 opts['forum_format'] = nil
355 end
356 if opts.forum['[url]'] then
357 opts.forum['[url] Wowhead'] = opts.forum['[url]']
358 opts.forum['[url]'] = nil
359 opts.forum['[url] MMO/Wowstead'] = option_defaults.forum['[url] MMO/Wowstead']
360 if opts['forum_current'] == '[url]' then
361 opts['forum_current'] = '[url] Wowhead'
362 end
363 end
340 option_defaults = nil 364 option_defaults = nil
341 -- transition&remove old options
342 opts["forum_use_itemid"] = nil
343 if opts["forum_format"] then
344 opts.forum["Custom..."] = opts["forum_format"]
345 opts["forum_format"] = nil
346 end
347 if OuroLootSV then -- may not be the same as testing g_restore_p soon 365 if OuroLootSV then -- may not be the same as testing g_restore_p soon
348 if OuroLootSV.saved then 366 if OuroLootSV.saved then
349 OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil 367 OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil
350 end 368 end
351 if OuroLootSV.threshold then 369 if OuroLootSV.threshold then
615 end 633 end
616 634
617 -- helper for CHAT_MSG_LOOT handler 635 -- helper for CHAT_MSG_LOOT handler
618 do 636 do
619 -- Recent loot cache 637 -- Recent loot cache
620 addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl) 638 local candidates = {}
639 local function prefer_local_loots (cache)
640 -- The function name is a bit of a misnomer, as local entries overwrite
641 -- remote entries as the candidate table is populated. This routine is
642 -- to extract the results once the cache timers have expired.
643 for i,sig in ipairs(candidates) do
644 addon.dprint('loot', "processing candidate entry", i, sig)
645 local loot = candidates[sig]
646 if loot then
647 addon.dprint('loot', i, "was found")
648 candidates[sig] = nil
649 local looti = addon._addLootEntry(loot)
650 if (loot.disposition ~= 'shard')
651 and (loot.disposition ~= 'gvault')
652 and (not addon.history_suppress)
653 then
654 addon:_addHistoryEntry(looti)
655 end
656 end
657 end
658
659 if addon.display then
660 addon:redisplay()
661 end
662 table.wipe(candidates)
663 end
664 addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl, prefer_local_loots)
621 665
622 local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon 666 local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon
623 667
624 -- 'from' and onwards only present if this is triggered by a broadcast 668 -- 'from' and onwards only present if this is triggered by a broadcast
625 function addon:_do_loot (local_override, recipient, itemid, count, from, extratext) 669 function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
632 UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] 676 UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]]
633 end 677 end
634 self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) 678 self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality)
635 679
636 itemid = tonumber(ilink:match("item:(%d+)") or 0) 680 itemid = tonumber(ilink:match("item:(%d+)") or 0)
637 if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then 681 -- This is only a loop to make jumping out of it easy, and still do cleanup below.
682 while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do
638 if (self.rebroadcast and (not from)) and not local_override then 683 if (self.rebroadcast and (not from)) and not local_override then
639 self:broadcast('loot', recipient, itemid, count) 684 self:broadcast('loot', recipient, itemid, count)
640 end 685 end
641 if self.enabled or local_override then 686 if (not self.enabled) and (not local_override) then break end
642 local signature = recipient .. iname .. (count or "") 687 local signature = recipient .. iname .. (count or "")
643 if self.recent_loot:test(signature) then 688 if from and self.recent_loot:test(signature) then
644 self.dprint('cache', "loot <",signature,"> already in cache, skipping") 689 self.dprint('cache', "loot <",signature,"> already in cache, skipping")
645 else 690 else
646 self.recent_loot:add(signature) 691 self.recent_loot:add(signature)
647 i = self._addLootEntry{ -- There is some redundancy here... 692 -- There is some redundancy in all this, in the interests of ease-of-coding
648 kind = 'loot', 693 i = {
649 person = recipient, 694 kind = 'loot',
650 person_class= select(2,UnitClass(recipient)), 695 person = recipient,
651 cache_miss = i and true or nil, 696 person_class= select(2,UnitClass(recipient)),
652 quality = iquality, 697 cache_miss = i and true or nil,
653 itemname = iname, 698 quality = iquality,
654 id = itemid, 699 itemname = iname,
655 itemlink = ilink, 700 id = itemid,
656 itexture = itexture, 701 itemlink = ilink,
657 disposition = (recipient == self.sharder) and 'shard' or nil, 702 itexture = itexture,
658 count = count, 703 disposition = (recipient == self.sharder) and 'shard' or nil,
659 bcast_from = from, 704 count = count,
660 extratext = extratext, 705 bcast_from = from,
661 is_heroic = self:is_heroic_item(ilink), 706 extratext = extratext,
662 } 707 is_heroic = self:is_heroic_item(ilink),
663 self.dprint('loot', "added loot entry", i) 708 }
664 if not self.history_suppress then 709 candidates[signature] = i
665 self:_addHistoryEntry(i) 710 tinsert (candidates, signature)
666 end 711 self.dprint('cache', "loot <",signature,"> added to cache, candidate", #candidates)
667 if self.display then 712 end
668 self:redisplay() 713 break
669 --[[
670 local st = self.display:GetUserData("eoiST")
671 if st and st.frame:IsVisible() then
672 st:OuroLoot_Refresh()
673 end
674 ]]
675 end
676 end
677 end
678 end 714 end
679 self.dprint('loot',"<<_do_loot out") 715 self.dprint('loot',"<<_do_loot out")
680 return i 716 return i
681 end 717 end
682 718
1065 _G.OL = self 1101 _G.OL = self
1066 _G.Oloot = g_loot 1102 _G.Oloot = g_loot
1067 end 1103 end
1068 end 1104 end
1069 1105
1070 -- Tie-ins with Deadly Boss Mods 1106 -- Tie-in with Deadly Boss Mods (or other such addons)
1071 do 1107 do
1072 local candidates, location 1108 local candidates = {}
1109 local location
1073 local function fixup_durations (cache) 1110 local function fixup_durations (cache)
1074 if candidates == nil then return end -- this is called for *all* cache expirations, including non-boss
1075 local boss, bossi 1111 local boss, bossi
1076 boss = candidates[1] 1112 boss = candidates[1]
1077 if #candidates == 1 then 1113 if #candidates == 1 then
1078 -- (1) or (2) 1114 -- (1) or (2)
1079 boss.duration = boss.duration or 0 1115 boss.duration = boss.duration or 0
1106 addon:_mark_boss_kill (bossi) 1142 addon:_mark_boss_kill (bossi)
1107 if opts.chatty_on_kill then 1143 if opts.chatty_on_kill then
1108 addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance) 1144 addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
1109 end 1145 end
1110 end 1146 end
1111 candidates = nil 1147 table.wipe(candidates)
1112 end 1148 end
1113 addon.recent_boss = create_new_cache ('boss', 10, fixup_durations) 1149 addon.recent_boss = create_new_cache ('boss', 10, fixup_durations)
1114 1150
1115 -- Similar to _do_loot, but duration+ parms only present when locally generated. 1151 -- Similar to _do_loot, but duration+ parms only present when locally generated.
1116 local function _do_boss (self, reason, bossname, intag, duration, raiders) 1152 local function _do_boss (self, reason, bossname, intag, duration, raiders)
1143 reason = reason, 1179 reason = reason,
1144 instance = intag, 1180 instance = intag,
1145 duration = duration, -- these two deliberately may be nil 1181 duration = duration, -- these two deliberately may be nil
1146 raiderlist = raiders and table.concat(raiders, ", ") 1182 raiderlist = raiders and table.concat(raiders, ", ")
1147 } 1183 }
1148 candidates = candidates or {}
1149 tinsert(candidates,c) 1184 tinsert(candidates,c)
1150 end 1185 end
1151 break 1186 break
1152 end 1187 end
1153 self.dprint('loot',"<<_do_boss out") 1188 self.dprint('loot',"<<_do_boss out")
1529 function addon:_addHistoryEntry (lootindex) 1564 function addon:_addHistoryEntry (lootindex)
1530 local e = g_loot[lootindex] 1565 local e = g_loot[lootindex]
1531 if e.kind ~= 'loot' then return end 1566 if e.kind ~= 'loot' then return end
1532 1567
1533 local i,h = self:get_loot_history(e.person) 1568 local i,h = self:get_loot_history(e.person)
1569 -- If any of these change, update the end of history_handle_disposition.
1534 local n = { 1570 local n = {
1535 id = e.id, 1571 id = e.id,
1536 when = self:format_timestamp (g_today, e), 1572 when = self:format_timestamp (g_today, e),
1537 count = e.count, 1573 count = e.count,
1538 } 1574 }
1572 tremove (h) 1608 tremove (h)
1573 end 1609 end
1574 end 1610 end
1575 end 1611 end
1576 1612
1577 function addon:reassign_loot (index, name_to) 1613 -- Given an entry in a g_loot table, looks up the corresponding history
1578 local e = assert(g_loot[index], "trying to reassign nonexistant entry") 1614 -- entry. Returns the player's index and history table (as in get_loot_history)
1579 assert(e.kind=='loot', "trying to reassign something that isn't loot") 1615 -- and the index into that table of the loot entry. On failure, returns nil
1580 assert(type(name_to)=='string' and name_to:len()>0) 1616 -- and an error message ready to be formatted with the loot's name/itemlink.
1581 1617 function addon:_history_by_loot_id (loot, operation_text)
1582 local name_from = e.person 1618 -- Using assert() here would be concatenating error strings that probably
1583 local tag = e.history_unique 1619 -- wouldn't be used. Do more verbose testing instead.
1620 if type(loot) ~= 'table' then
1621 error("trying to "..operation_text.." nonexistant entry")
1622 end
1623 if loot.kind ~= 'loot' then
1624 error("trying to "..operation_text.." something that isn't loot")
1625 end
1626
1627 local player = loot.person
1628 local tag = loot.history_unique
1584 local errtxt 1629 local errtxt
1630 local player_i, player_h, hist_i
1585 1631
1586 if not tag then 1632 if not tag then
1587 errtxt = "Entry for %s is missing a history tag!" 1633 errtxt = "Entry for %s is missing a history tag!"
1588 else 1634 else
1589 local from_i,from_h = self:get_loot_history(name_from) 1635 player_i,player_h = self:get_loot_history(player)
1590 local to_i,to_h = self:get_loot_history(name_to) 1636 for i,h in ipairs(player_h) do
1591
1592 local hi
1593 for i,h in ipairs(from_h) do
1594 local unique = h.id .. ' ' .. h.when 1637 local unique = h.id .. ' ' .. h.when
1595 if unique == tag then 1638 if unique == tag then
1596 hi = i 1639 hist_i = i
1597 break 1640 break
1598 end 1641 end
1599 end 1642 end
1600 if not hi then 1643 if not hist_i then
1601 -- 1) loot an item, 2) clear old history, 3) reassign from current loot 1644 -- 1) loot an item, 2) clear old history, 3) reassign from current loot
1602 -- Bah. Anybody that tricky is already recoding the tables directly anyhow. 1645 -- Bah. Anybody that tricky is already recoding the tables directly anyhow.
1603 errtxt = "There is no record of %s ever having been assigned!" 1646 errtxt = "There is no record of %s ever having been assigned!"
1647 end
1648 end
1649
1650 if errtxt then
1651 return nil, errtxt
1652 end
1653 return player_i, player_h, hist_i
1654 end
1655
1656 function addon:reassign_loot (index, to_name)
1657 assert(type(to_name)=='string' and to_name:len()>0)
1658 local e = g_loot[index]
1659 local from_i, from_h, hist_i = self:_history_by_loot_id (e, "reassign")
1660 local from_name = e.person
1661 local to_i,to_h = self:get_loot_history(to_name)
1662
1663 if not from_i then
1664 -- from_h is the formatted error text
1665 self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink)
1666 else
1667 local hist_h = tremove (from_h, hist_i)
1668 tinsert (to_h, 1, hist_h)
1669 tsort (from_h, comp)
1670 tsort (to_h, comp)
1671 end
1672 e.person = to_name
1673 e.person_class = select(2,UnitClass(to_name))
1674 self.hist_clean = nil
1675
1676 self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name)
1677 end
1678
1679 -- Any extra work for the "Mark as <x>" dropdown actions. The
1680 -- corresponding <x> will already have been assigned in the loot entry.
1681 local deleted_cache = {} --setmetatable({}, {__mode='k'})
1682 function addon:history_handle_disposition (index, olddisp)
1683 local e = g_loot[index]
1684 -- Standard disposition has a nil entry, but that's tedious in debug
1685 -- output, so force to a string instead.
1686 olddisp = olddisp or 'normal'
1687 local newdisp = e.disposition or 'normal'
1688 -- Ignore misclicks and the like
1689 if olddisp == newdisp then return end
1690
1691 local name = e.person
1692
1693 if (newdisp == 'shard' or newdisp == 'gvault') then
1694 local name_i, name_h, hist_i = self:_history_by_loot_id (e, "mark")
1695 -- remove history entry
1696 if hist_i then
1697 local hist_h = tremove (name_h, hist_i)
1698 deleted_cache[e.history_unique] = hist_h
1699 self.hist_clean = nil
1700 elseif (olddisp == 'shard' or olddisp == 'gvault') then
1701 -- Sharding a vault item, or giving the auto-sharder something to bank,
1702 -- etc, wouldn't necessarily have had a history entry to begin with.
1604 else 1703 else
1605 hi = tremove (from_h, hi) 1704 self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink)
1606 tinsert (to_h, 1, hi) 1705 end
1607 tsort (from_h, comp) 1706 return
1608 tsort (to_h, comp) 1707 end
1609 end 1708
1610 end 1709 if (olddisp == 'shard' or olddisp == 'gvault')
1611 1710 and (newdisp == 'normal' or newdisp == 'offspec')
1612 if errtxt then 1711 then
1613 self:Print(errtxt .. " Loot will be reassigned but history will NOT be updated.", e.itemlink) 1712 local name_i, name_h = self:get_loot_history(name)
1614 end 1713
1615 e.person = name_to 1714 -- Must create a new history entry. Could call '_addHistoryEntry(index)'
1616 e.person_class = select(2,UnitClass(name_to)) 1715 -- but that would duplicate a lot of effort. To start with, check the
1617 1716 -- cache of stuff we've already deleted; if it's not there then just do
1618 self:Print("Reassigned entry %d from '%s' to '%s'.", index, name_from, name_to) 1717 -- the same steps as _addHistoryEntry.
1718 local entry
1719 if e.history_unique and deleted_cache[e.history_unique] then
1720 entry = deleted_cache[e.history_unique]
1721 deleted_cache[e.history_unique] = nil
1722 end
1723 local when = g_today and self:format_timestamp (g_today, e) or tostring(e.stamp)
1724 entry = entry or {
1725 id = e.id,
1726 when = when,
1727 count = e.count,
1728 }
1729 tinsert (name_h, 1, entry)
1730 e.history_unique = e.history_unique or (entry.id .. ' ' .. entry.when)
1731 self.hist_clean = nil
1732 return
1733 end
1619 end 1734 end
1620 end 1735 end
1621 1736
1622 1737
1623 ------ Player communication 1738 ------ Player communication