comparison core.lua @ 73:32eb24fb2ebf

- This code is not quite ready for prime time. Do not run it yet. - Loot events have associated unique IDs, enabling some new actions over the network. These IDs are preserved as part of realm history. As a result, the stored history format has completely changed (and requires less memory as a bonus). - "Prescan for faster handling" option, default off. - "Mark as <x>" now broadcast to other trackers. Older versions can't receive the message, of course. Future: Broadcast reassigning loot. - New options controlling whether (and where) to print a message when another player broadcasts those kinds of changes to existing loot. - Names colored by class when that data is available; CUSTOM_CLASS_COLORS supported. - Metric boatloads of minor tweaks and optimizations throughout.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Tue, 29 May 2012 22:50:09 +0000
parents fb330a1fb6e9
children 124da015c4a2
comparison
equal deleted inserted replaced
72:bb19899c65a7 73:32eb24fb2ebf
90 After he retired, I began modifying the code. Eventually I set aside the 90 After he retired, I began modifying the code. Eventually I set aside the
91 entire package and rewrote the loot tracker module from scratch. Many of the 91 entire package and rewrote the loot tracker module from scratch. Many of the
92 variable/function naming conventions (sv_*, g_*, and family) stayed across the 92 variable/function naming conventions (sv_*, g_*, and family) stayed across the
93 rewrite. 93 rewrite.
94 94
95 Some variables are needlessly initialized to nil just to look uniform. 95 Some variables are needlessly initialized to nil just to look uniform and
96 serve as a reminder.
96 97
97 ]==] 98 ]==]
98 99
99 ------ Saved variables 100 ------ Saved variables
100 OuroLootSV = nil -- possible copy of g_loot 101 OuroLootSV = nil -- possible copy of g_loot
128 ['Custom...'] = '', 129 ['Custom...'] = '',
129 }, 130 },
130 ['forum_current'] = '[item] by name', 131 ['forum_current'] = '[item] by name',
131 ['display_disabled_LODs'] = false, 132 ['display_disabled_LODs'] = false,
132 ['display_bcast_from'] = true, 133 ['display_bcast_from'] = true,
134 ['precache_history_uniques'] = false,
135 ['chatty_on_remote_changes'] = false,
136 ['chatty_on_remote_changes_frame'] = 1,
133 } 137 }
134 local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command" 138 local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command"
135 .." to show the main display. You should probably browse the instructions" 139 .." to show the main display. You should probably browse the instructions"
136 .." if you've never used this before; %s to display the help window. This" 140 .." if you've never used this before; %s to display the help window. This"
137 .." welcome message will not intrude again." 141 .." welcome message will not intrude again."
138 local newer_warning = "A newer version has been released. You can %s to display" 142 local newer_warning = "A newer version has been released. You can %s to display"
139 .." a download URL for copy-and-pasting. You can %s to ping other raiders" 143 .." a download URL for copy-and-pasting. You can %s to ping other raiders"
140 .." for their installed versions (same as '/ouroloot ping' or clicking the" 144 .." for their installed versions (same as '/ouroloot ping' or clicking the"
141 .." 'Ping!' button on the options panel)." 145 .." 'Ping!' button on the options panel)."
146 local unique_collision = "|cffff1010%s:|r Item '%s' was carrying unique tag <"
147 ..">, but that was already in use! (New sender was '%s', previous cache "
148 .."entry was <%s/%s>.) This may require a live human to figure out; the "
149 .."loot in question has not been stored."
150 local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s%s|r to %s%s|r"
142 local qualnames = { 151 local qualnames = {
143 ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0, 152 ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0,
144 ['white'] = 1, ['common'] = 1, 153 ['white'] = 1, ['common'] = 1,
145 ['green'] = 2, ['uncommon'] = 2, 154 ['green'] = 2, ['uncommon'] = 2,
146 ['blue'] = 3, ['rare'] = 3, 155 ['blue'] = 3, ['rare'] = 3,
185 notraid = false, 194 notraid = false,
186 cache = false, 195 cache = false,
187 alsolog = false, 196 alsolog = false,
188 } 197 }
189 -- This looks ugly, but it factors out the load-time decisions from 198 -- This looks ugly, but it factors out the load-time decisions from
190 -- the run-time ones. 199 -- the run-time ones. Args to [dp]print are concatenated with spaces.
191 if tekdebug then 200 if tekdebug then
192 function dprint (t,...) 201 function dprint (t,...)
193 if DEBUG_PRINT and debug[t] then 202 if DEBUG_PRINT and debug[t] then
194 local text = flib.safefprint(tekdebug,"<"..t.."> ",...) 203 local text = flib.safefprint(tekdebug,"<"..t.."> ",...)
195 if debug.alsolog then 204 if debug.alsolog then
285 local g_loot = nil 294 local g_loot = nil
286 local g_restore_p = nil 295 local g_restore_p = nil
287 local g_wafer_thin = nil -- for prompting for additional rebroadcasters 296 local g_wafer_thin = nil -- for prompting for additional rebroadcasters
288 local g_today = nil -- "today" entry in g_loot 297 local g_today = nil -- "today" entry in g_loot
289 local g_boss_signpost = nil 298 local g_boss_signpost = nil
299 local g_seeing_oldsigs = nil
300 local g_uniques = nil -- memoization of unique loot events
290 local opts = nil 301 local opts = nil
291 302
292 local pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe = 303 -- for speeding up local loads, not because I think _G will change
293 pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe 304 local _G = _G
305 local type = _G.type
306 local select = _G.select
307 local pairs = _G.pairs
308 local ipairs = _G.ipairs
309 local tinsert = _G.table.insert
310 local tremove = _G.table.remove
311 local tostring = _G.tostring
312 local tonumber = _G.tonumber
313 local wipe = _G.table.wipe
314
294 local pprint, tabledump = addon.pprint, flib.tabledump 315 local pprint, tabledump = addon.pprint, flib.tabledump
295 local CopyTable, GetNumRaidMembers = CopyTable, GetNumRaidMembers 316 local CopyTable, GetNumRaidMembers = _G.CopyTable, _G.GetNumRaidMembers
296 -- En masse forward decls of symbols defined inside local blocks 317 -- En masse forward decls of symbols defined inside local blocks
297 local _register_bossmod, makedate, create_new_cache, _init, _log 318 local _register_bossmod, makedate, create_new_cache, _init, _log
319 local _history_by_loot_id, _notify_about_remote
298 320
299 -- Try to extract numbers from the .toc "Version" and munge them into an 321 -- Try to extract numbers from the .toc "Version" and munge them into an
300 -- integral form for comparison. The result doesn't need to be meaningful as 322 -- integral form for comparison. The result doesn't need to be meaningful as
301 -- long as we can reliably feed two of them to "<" and get useful answers. 323 -- long as we can reliably feed two of them to "<" and get useful answers.
302 -- 324 --
319 end 341 end
320 342
321 -- Hypertext support, inspired by DBM broadcast pizza timers 343 -- Hypertext support, inspired by DBM broadcast pizza timers
322 do 344 do
323 local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h" 345 local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h"
346 local strsplit = _G.strsplit
324 347
325 -- TEXT will automatically be surrounded by brackets 348 -- TEXT will automatically be surrounded by brackets
326 -- COLOR can be item quality code or a hex string 349 -- COLOR can be item quality code or a hex string
327 function addon.format_hypertext (code, text, color) 350 function addon.format_hypertext (code, text, color)
328 return hypertext_format_str:format (code, 351 return hypertext_format_str:format (code,
403 end 426 end
404 -- dynamic difficulties always return normal "codes" 427 -- dynamic difficulties always return normal "codes"
405 if isdynamic and perbossheroic == 1 then 428 if isdynamic and perbossheroic == 1 then
406 t = t .. "h" 429 t = t .. "h"
407 end 430 end
408 pprint("instance_tag final", t, r)
409 return name .. "(" .. t .. ")", r 431 return name .. "(" .. t .. ")", r
410 end 432 end
411 addon.instance_tag = instance_tag -- grumble 433 addon.instance_tag = instance_tag -- grumble
412 addon.latest_instance = nil -- spelling reminder, assigned elsewhere 434 addon.latest_instance = nil -- spelling reminder, assigned elsewhere
413 435
436 -- Memoizing cache of unique IDs as we generate or search for them. Keys are
437 -- the uniques, values are the following:
438 -- 'history' active index into self.history
439 -- 'history_may' index into player's uniques list, CAN QUICKLY BE OUTDATED
440 -- and will instantly be wrong after manual insertion
441 -- 'loot' active index into g_loot
442 -- with all but the history entry optional. Values of g_uniqes.NOTFOUND
443 -- indicate a known missing status. Use g_uniques:RESET() to wipe the cache
444 -- and return to searching mode.
445 do
446 local notfound = -1
447 local notfound_ret = { history = notfound }
448 local mt
449
450 -- This can either be its own function or a slightly redundant __index.
451 local function m_probe_only (t, k)
452 return rawget(t,k) or notfound_ret
453 end
454
455 -- Expensive search.
456 local function m_full_search (t, k)
457 local L, H, HU, loot
458 -- Try active loot entries first
459 for i,e in addon:filtered_loot_iter('loot') do
460 if k == e.unique then
461 L,loot = i,e
462 break
463 end
464 end
465 -- If it's active, try looking through that player's history first.
466 if L then
467 local hi,h = addon:get_loot_history (loot.person)
468 for ui,u in ipairs(h.unique) do
469 if k == u then
470 H, HU = hi, ui
471 break
472 end
473 end
474 else
475 -- No luck? Ugh, may have been reassigned and we're probing from
476 -- older data. Search the rest of current realm's history.
477 for hi,h in ipairs(addon.history) do
478 for ui,u in ipairs(h.unique) do
479 if k == u then
480 H, HU = hi, ui
481 break
482 end
483 end
484 end
485 end
486 local ret = { loot = L, history = H or notfound, history_may = HU }
487 t[k] = ret
488 return ret
489 end
490
491 local function m_setmode (self, mode)
492 mt.__index = (mode == 'probe') and m_probe_only or
493 (mode == 'search') and m_full_search or
494 nil -- maybe error() here?
495 end
496
497 local function m_reset (self)
498 wipe(self)
499 self[''] = notfound_ret -- special case for receiving older broadcast
500 self.NOTFOUND = notfound
501 self.RESET = m_reset
502 self.SEARCH = m_full_search
503 self.TEST = m_probe_only
504 self.SETMODE = m_setmode
505 mt.__index = m_full_search
506 return self
507 end
508
509 -- If unique keys ever change into objects instead of strings, change
510 -- this into a weakly-keyed table.
511 mt = { __metatable = 'Should be using setmode.' }
512
513 g_uniques = setmetatable (m_reset{}, mt)
514 end
515
414 516
415 ------ Expiring caches 517 ------ Expiring caches
416 --[[ 518 --[[
417 foo = create_new_cache("myfoo",15[,cleanup]) -- ttl 519 cache = create_new_cache ("mycache", 15 [,cleanup])
418 foo:add("blah") 520 cache:add(foo)
419 foo:test("blah") -- returns true 521 cache:test(foo) -- returns true
522 ....5 seconds pass
523 cache:add(bar)
524 ....10 seconds pass
525 cache:test(foo) -- returns false
526 cache:test(bar) -- returns true
527 ....5 seconds pass
528 ....bar also gone, cleanup() called
420 ]] 529 ]]
421 do 530 do
422 local caches = {} 531 local caches = {}
423 local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup() 532 local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup()
424 local time = _G.time 533 local time = _G.time
429 local alldone = true 538 local alldone = true
430 -- this is ass-ugly 539 -- this is ass-ugly
431 for name,c in pairs(caches) do 540 for name,c in pairs(caches) do
432 local fifo = c.fifo 541 local fifo = c.fifo
433 local active = #fifo > 0 542 local active = #fifo > 0
434 while (#fifo > 0) and (now - fifo[1].t > c.ttl) do 543 while (#fifo > 0) and (now > fifo[1].t) do
435 addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">") 544 addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">")
436 tremove(fifo,1) 545 tremove(fifo,1)
437 end 546 end
438 if active and #fifo == 0 and c.func then 547 if active and #fifo == 0 and c.func then
439 addon.dprint('cache', name, "empty, firing cleanup") 548 addon.dprint('cache', name, "empty, firing cleanup")
442 alldone = alldone and (#fifo == 0) 551 alldone = alldone and (#fifo == 0)
443 end 552 end
444 if alldone then 553 if alldone then
445 addon.dprint('cache',"OnLoop FINISHING animation group") 554 addon.dprint('cache',"OnLoop FINISHING animation group")
446 cleanup_group:Finish() 555 cleanup_group:Finish()
556 _G.collectgarbage()
447 else 557 else
448 addon.dprint('cache',"OnLoop done, not yet finished") 558 addon.dprint('cache',"OnLoop done, not yet finished")
449 end 559 end
450 end) 560 end)
451 561
452 local function _add (cache, x) 562 local function _add (cache, x)
453 local datum = { t=time(), m=x } 563 local datum = { t=time()+cache.ttl, m=x }
454 cache.hash[x] = datum 564 cache.hash[x] = datum
455 tinsert (cache.fifo, datum) 565 tinsert (cache.fifo, datum)
456 if not cleanup_group:IsPlaying() then 566 if not cleanup_group:IsPlaying() then
457 addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group") 567 addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group")
458 cache.cleanup:SetDuration(1) -- hmmm 568 cache.cleanup:SetDuration(1) -- hmmm
488 598
489 ------ Ace3 framework stuff 599 ------ Ace3 framework stuff
490 function addon:OnInitialize() 600 function addon:OnInitialize()
491 if self.author_debug then 601 if self.author_debug then
492 _G.OL = self 602 _G.OL = self
493 end 603 _G.g_uniques = g_uniques
494 _log = OuroLootSV_log 604 end
605 _log = _G.OuroLootSV_log
495 606
496 -- VARIABLES_LOADED has fired by this point; test if we're doing something like 607 -- VARIABLES_LOADED has fired by this point; test if we're doing something like
497 -- relogging during a raid and already have collected loot data 608 -- relogging during a raid and already have collected loot data
609 local OuroLootSV = _G.OuroLootSV
498 g_restore_p = OuroLootSV ~= nil 610 g_restore_p = OuroLootSV ~= nil
499 self.dprint('flow', "oninit sets restore as", g_restore_p) 611 self.dprint('flow', "oninit sets restore as", g_restore_p)
500 612
501 if OuroLootSV_opts == nil then 613 if _G.OuroLootSV_opts == nil then
502 OuroLootSV_opts = {} 614 _G.OuroLootSV_opts = {}
503 self:ScheduleTimer(function(s) 615 self:ScheduleTimer(function(s)
504 s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON)) 616 s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
505 virgin = nil 617 virgin = nil
506 end,10,self) 618 end,10,self)
507 else 619 else
508 virgin = nil 620 virgin = nil
509 end 621 end
510 opts = OuroLootSV_opts 622 opts = _G.OuroLootSV_opts
511 local stored_datarev = opts.datarev or 14 623 local stored_datarev = opts.datarev or 14
512 for opt,default in pairs(option_defaults) do 624 for opt,default in pairs(option_defaults) do
513 if opts[opt] == nil then 625 if opts[opt] == nil then
514 opts[opt] = default 626 opts[opt] = default
515 end 627 end
555 667
556 self:RegisterChatCommand("ouroloot", "OnSlash") 668 self:RegisterChatCommand("ouroloot", "OnSlash")
557 if opts.register_slashloot then 669 if opts.register_slashloot then
558 -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused 670 -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused
559 -- maybe try to detect if this command is already in use... 671 -- maybe try to detect if this command is already in use...
560 SLASH_ACECONSOLE_OUROLOOT2 = "/loot" 672 _G.SLASH_ACECONSOLE_OUROLOOT2 = "/loot"
561 end 673 end
562 674
563 self.history_all = self.history_all or OuroLootSV_hist or {} 675 self.history_all = self.history_all or _G.OuroLootSV_hist or {}
564 local r = self:load_assert (GetRealmName(), "how the freak does GetRealmName() fail?") 676 local r = self:load_assert (_G.GetRealmName(), "how the freak does GetRealmName() fail?")
565 self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) 677 self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
566 self.history = self.history_all[r] 678 self.history = self.history_all[r]
567 679
568 local histformat = self.history_all.HISTFORMAT 680 local histformat = self.history_all.HISTFORMAT
569 self.history_all.HISTFORMAT = nil -- don't keep this in live data 681 self.history_all.HISTFORMAT = nil -- don't keep this in live data
570 if (not InCombatLockdown()) and OuroLootSV_hist and 682 if _G.OuroLootSV_hist
571 (histformat == nil or histformat < 3) -- restored data but it's older 683 and (histformat == nil or histformat < 4)
572 then 684 then -- some big honkin' loops
573 -- Big honkin' loop
574 for rname,realm in pairs(self.history_all) do 685 for rname,realm in pairs(self.history_all) do
575 for pk,player in ipairs(realm) do 686 for pk,player in ipairs(realm) do
576 for lk,loot in ipairs(player) do 687 if histformat == nil or histformat < 3 then
577 if loot.count == "" then 688 for lk,loot in ipairs(player) do
578 loot.count = nil 689 if loot.count == "" then
579 end 690 loot.count = nil
580 if not loot.unique then 691 end
581 loot.unique = loot.id .. ' ' .. loot.when 692 if not loot.unique then
693 loot.unique = loot.id .. ' ' .. loot.when
694 end
582 end 695 end
583 end 696 end
584 end 697 -- format 3 to format 4 was a major revamp of per-player data
585 end 698 self:_uplift_history_format(player,rname)
586 end 699 end
700 end
701 end
702 self._uplift_history_format = nil
587 --OuroLootSV_hist = nil 703 --OuroLootSV_hist = nil
588 704
589 -- Handle changes to the stored data format in stages from oldest to newest. 705 -- Handle changes to the stored data format in stages from oldest to newest.
706 -- bumpers[X] is responsible for updating from X to X+1.
707 -- (This is turning into a lot of loops over the same table. Consolidate?)
590 if OuroLootSV then 708 if OuroLootSV then
591 local dirty = false 709 local dirty = false
592 local bumpers = {} 710 local bumpers = {}
593 bumpers[14] = function() 711 bumpers[14] = function()
594 for i,e in ipairs(OuroLootSV) do 712 for i,e in ipairs(OuroLootSV) do
632 -- In the not-very-many days between 16 and 19, I managed to break 750 -- In the not-very-many days between 16 and 19, I managed to break
633 -- the exact same data in the exact same way. At least they're 751 -- the exact same data in the exact same way. At least they're
634 -- not actually running the same loop twice... probably... sigh. 752 -- not actually running the same loop twice... probably... sigh.
635 753
636 bumpers[19] = function() 754 bumpers[19] = function()
755 local date = _G.date
637 for i,e in ipairs(OuroLootSV) do 756 for i,e in ipairs(OuroLootSV) do
638 if e.kind == 'loot' and e.history_unique then 757 if e.kind == 'loot' then
639 e.unique, e.history_unique = e.history_unique, nil 758 if e.history_unique then
759 e.unique, e.history_unique = e.history_unique, nil
760 end
761 if e.unique == nil or #e.unique == 0 then
762 e.unique = e.id .. ' ' .. date("%Y/%m/%d %H:%M",e.stamp)
763 end
640 end 764 end
641 end 765 end
642 end 766 end
643 767
644 --[===[ 768 --[===[
665 789
666 function addon:OnEnable() 790 function addon:OnEnable()
667 self:RegisterEvent("PLAYER_LOGOUT") 791 self:RegisterEvent("PLAYER_LOGOUT")
668 self:RegisterEvent("RAID_ROSTER_UPDATE") 792 self:RegisterEvent("RAID_ROSTER_UPDATE")
669 793
670 -- Cribbed from Talented. I like the way jerry thinks: the first argument 794 -- Cribbed from Talented. I like the way jerry thinks: the first string
671 -- can be a format spec for the remainder of the arguments. (The new 795 -- argument can be a format spec for the remainder of the arguments.
672 -- AceConsole:Printf isn't used because we can't specify a prefix without 796 -- AceConsole:Printf isn't used because we can't specify a prefix without
673 -- jumping through ridonkulous hoops.) The part about overriding :Print 797 -- jumping through ridonkulous hoops. The part about overriding :Print
674 -- with a version using prefix hyperlinks is my fault. 798 -- with a version using prefix hyperlinks is my fault.
799 --
800 -- CFPrint added instead of the usual Print testing of the first arg for
801 -- frame-ness, which would slow down all printing and only rarely be useful.
675 -- 802 --
676 -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh. 803 -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh.
677 do 804 do
678 local AC = LibStub("AceConsole-3.0") 805 local AC = LibStub("AceConsole-3.0")
679 local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5) 806 local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
682 return AC:Print (chat_prefix, str:format(...)) 809 return AC:Print (chat_prefix, str:format(...))
683 else 810 else
684 return AC:Print (chat_prefix, str, ...) 811 return AC:Print (chat_prefix, str, ...)
685 end 812 end
686 end 813 end
814 function addon:CFPrint (frame, str, ...)
815 assert(type(frame)=='table' and frame.AddMessage)
816 if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
817 return AC:Print (frame, chat_prefix, str:format(...))
818 else
819 return AC:Print (frame, chat_prefix, str, ...)
820 end
821 end
687 end 822 end
688 823
689 while opts.keybinding do 824 while opts.keybinding do
690 if InCombatLockdown() then 825 if InCombatLockdown() then
691 self:Print("Cannot create '%s' as a keybinding while in combat!", 826 self:Print("Cannot create '%s' as a keybinding while in combat!",
697 KeyBindingFrame_LoadUI() 832 KeyBindingFrame_LoadUI()
698 local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate") 833 local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
699 btn:SetAttribute("type", "macro") 834 btn:SetAttribute("type", "macro")
700 btn:SetAttribute("macrotext", "/ouroloot toggle") 835 btn:SetAttribute("macrotext", "/ouroloot toggle")
701 if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then 836 if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then
702 -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when GCBS 837 -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when
703 -- decides to return neither 1 nor 2 during load, for reasons nobody has ever learned 838 -- GCBS() decides to return neither 1 nor 2 during load, for reasons nobody
839 -- has ever learned
704 local c = GetCurrentBindingSet() 840 local c = GetCurrentBindingSet()
705 if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then 841 if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then
706 SaveBindings(c) 842 SaveBindings(c)
707 end 843 end
708 else 844 else
756 button:SetPoint("TOPLEFT",20,-20) 892 button:SetPoint("TOPLEFT",20,-20)
757 _b:SetScript("OnShow",nil) 893 _b:SetScript("OnShow",nil)
758 end) 894 end)
759 _G.InterfaceOptions_AddCategory(bliz) 895 _G.InterfaceOptions_AddCategory(bliz)
760 896
897 -- Maybe load up g_uniques now?
898 if opts.precache_history_uniques then
899 self:_cache_history_uniques()
900 end
901
761 self:_scan_LOD_modules() 902 self:_scan_LOD_modules()
903
904 self:_set_remote_change_chatframe (opts.chatty_on_remote_changes_frame, --[[silent_p=]]true)
762 905
763 if self.debug.flow then self:Print"is in control-flow debug mode." end 906 if self.debug.flow then self:Print"is in control-flow debug mode." end
764 end 907 end
765 --function addon:OnDisable() end 908 --function addon:OnDisable() end
766 909
835 978
836 979
837 ------ Event handlers 980 ------ Event handlers
838 function addon:_clear_SVs() 981 function addon:_clear_SVs()
839 g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests 982 g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests
840 OuroLootSV = nil 983 _G.OuroLootSV = nil
841 OuroLootSV_saved = nil 984 _G.OuroLootSV_saved = nil
842 OuroLootSV_opts = nil 985 _G.OuroLootSV_opts = nil
843 OuroLootSV_hist = nil 986 _G.OuroLootSV_hist = nil
844 OuroLootSV_log = nil 987 _G.OuroLootSV_log = nil
845 ReloadUI() 988 _G.ReloadUI()
846 end 989 end
847 function addon:PLAYER_LOGOUT() 990 function addon:PLAYER_LOGOUT()
848 self:UnregisterEvent("RAID_ROSTER_UPDATE") 991 self:UnregisterEvent("RAID_ROSTER_UPDATE")
849 self:UnregisterEvent("PLAYER_ENTERING_WORLD") 992 self:UnregisterEvent("PLAYER_ENTERING_WORLD")
850 993
851 local worth_saving = #g_loot > 0 or next(g_loot.raiders) 994 local worth_saving = #g_loot > 0 or _G.next(g_loot.raiders)
852 if not worth_saving then for text in self:registered_textgen_iter() do 995 if not worth_saving then for text in self:registered_textgen_iter() do
853 worth_saving = worth_saving or g_loot.printed[text] > 0 996 worth_saving = worth_saving or g_loot.printed[text] > 0
854 end end 997 end end
855 if worth_saving then 998 if worth_saving then
856 opts.autoshard = self.sharder 999 opts.autoshard = self.sharder
857 opts.threshold = self.threshold 1000 opts.threshold = self.threshold
858 for i,e in ipairs(g_loot) do 1001 for i,e in ipairs(g_loot) do
859 e.cols = nil 1002 e.cols = nil
860 end 1003 end
861 OuroLootSV = g_loot 1004 _G.OuroLootSV = g_loot
862 else 1005 else
863 OuroLootSV = nil 1006 _G.OuroLootSV = nil
864 end 1007 end
865 1008
866 worth_saving = false 1009 worth_saving = false
867 for r,t in pairs(self.history_all) do if type(t) == 'table' then 1010 for r,t in pairs(self.history_all) do if type(t) == 'table' then
868 if #t == 0 then 1011 if #t == 0 then
873 t.st = nil 1016 t.st = nil
874 t.byname = nil 1017 t.byname = nil
875 end 1018 end
876 end end 1019 end end
877 if worth_saving then 1020 if worth_saving then
878 OuroLootSV_hist = self.history_all 1021 _G.OuroLootSV_hist = self.history_all
879 OuroLootSV_hist.HISTFORMAT = 3 1022 _G.OuroLootSV_hist.HISTFORMAT = 4
880 else 1023 else
881 OuroLootSV_hist = nil 1024 _G.OuroLootSV_hist = nil
882 end 1025 end
883 OuroLootSV_log = #OuroLootSV_log > 0 and OuroLootSV_log or nil 1026 _G.OuroLootSV_log = #_G.OuroLootSV_log > 0 and _G.OuroLootSV_log or nil
884 end 1027 end
885 1028
886 do 1029 do
887 local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex, 1030 local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex,
888 UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo = 1031 UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo =
1014 end 1157 end
1015 end 1158 end
1016 end 1159 end
1017 end 1160 end
1018 1161
1019 -- helper for CHAT_MSG_LOOT handler 1162 --[=[ CHAT_MSG_LOOT handler and its helpers.
1163 Situations for "unique tag" generation, given N people seeing local loot
1164 events, M people seeing remote rebroadcasts, and player Z adding manually:
1165
1166 + Local tracking: All LOCALs should see the same itemstring, thus the same
1167 unique ID stripped out of field #9. LOCALn includes this in the broadcast
1168 to REMOTEm. Tag is a large number, meaningless for clients and players.
1169
1170 + Local broadcasting, remote tracking: same as local tracking. Possibly
1171 some weirdness if all local versions are significantly older than the remote
1172 versions; in this case each REMOTEn will generate their own tags of the form
1173 itemID+formatted_date, which will not be "unique" for the next 60 seconds.
1174 As long as at least one LOCALn is recent enough to strip and broadcast a
1175 proper ID, multiple items of the same visible name will not be "lost".
1176
1177 + Z manually inserts a loot entry: Z generates a tag, preserved locally.
1178 If Z rebrodcasts that entry, all REMOTEs will see it. Tag is of the form
1179 "n" followed by a random number.
1180 ]=]
1020 do 1181 do
1182 local counter, _do_loot
1183 do
1184 local count = 0
1185 function counter() count = count + 1; return count; end
1186 end
1187
1021 local function maybe_trash_kill_entry() 1188 local function maybe_trash_kill_entry()
1022 -- this is set on various boss interactions, so we've got a kill/wipe 1189 -- this is set on various boss interactions, so we've got a kill/wipe
1023 -- entry already 1190 -- entry already -- XXX maybe clear it after a delay, so that loot
1191 -- from trash after a boss isn't grouped with that boss?
1024 if addon.latest_instance then return end 1192 if addon.latest_instance then return end
1025 --addon.latest_instance = instance_tag() 1193 --addon.latest_instance = instance_tag()
1026 local ss, max, inst = addon:snapshot_raid() 1194 local ss, max, inst = addon:snapshot_raid()
1027 addon.latest_instance = inst 1195 addon.latest_instance = inst
1028 addon:_mark_boss_kill (addon._addBossEntry{ 1196 addon:_mark_boss_kill (addon._addBossEntry{
1030 instance=addon.latest_instance, duration=0, 1198 instance=addon.latest_instance, duration=0,
1031 raidersnap=ss, maxsize=max 1199 raidersnap=ss, maxsize=max
1032 }) 1200 })
1033 end 1201 end
1034 1202
1203 local random = _G.math.random
1204 local function many_uniques_handle_it (u, check_p)
1205 if u and check_p then
1206 -- Check and alert for an existing value.
1207 u = tostring(u)
1208 if g_uniques[u].history ~= g_uniques.NOTFOUND then
1209 return nil, u
1210 end
1211 addon.dprint('loot',"verified unique tag ("..u..")")
1212 else
1213 -- Need to *find* an unused value. For now use a range of
1214 -- J*10^4 where J is Jenny's Constant. Thank you, xkcd.com/1047.
1215 repeat
1216 u = 'n' .. random(8675309)
1217 until g_uniques:TEST(u).history ~= g_uniques.NOTFOUND
1218 addon.dprint('loot',"created unique tag", u)
1219 end
1220 return true, u
1221 end
1222
1035 -- Recent loot cache 1223 -- Recent loot cache
1036 local candidates = {} 1224 local candidates = {}
1225 local sigmap = {}
1226 _G.sigmap = sigmap
1227 local function preempt_older_signature (oldersig, newersig)
1228 local origin = candidates[oldersig] and candidates[oldersig].from
1229 if origin and g_seeing_oldsigs[origin] then
1230 -- replace entry from older client with this newer one
1231 candidates[oldersig] = nil
1232 addon.dprint('cache', "preempting signature <", oldersig, "> from", origin)
1233 end
1234 return false
1235 end
1236
1037 local function prefer_local_loots (cache) 1237 local function prefer_local_loots (cache)
1038 -- The function name is a bit of a misnomer, as local entries overwrite 1238 -- The function name is a bit of a misnomer, as local entries overwrite
1039 -- remote entries as the candidate table is populated. This routine is 1239 -- remote entries as the candidate table is populated. This routine is
1040 -- here to extract the final results once the cache timers have expired. 1240 -- here to extract the final results once the cache timers have expired.
1041 -- 1241 --
1042 -- Keep this sync'd with the local_override branch below. 1242 -- Keep this sync'd with the local_override branch below.
1043 for i,sig in ipairs(candidates) do 1243 for i,sig in ipairs(candidates) do
1044 addon.dprint('loot', "processing candidate entry", i, sig) 1244 addon.dprint('loot', "processing candidate entry", i, sig)
1045 local loot = candidates[sig] 1245 local loot = candidates[sig]
1046 if loot then 1246 if loot then
1047 addon.dprint('loot', i, "was found")
1048 maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry 1247 maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry
1049 candidates[sig] = nil 1248 candidates[sig] = nil
1050 local looti = addon._addLootEntry(loot) 1249 local looti = addon._addLootEntry(loot)
1250 addon.dprint('loot', "entry", i, "was found, added at index", looti)
1051 if (loot.disposition ~= 'shard') 1251 if (loot.disposition ~= 'shard')
1052 and (loot.disposition ~= 'gvault') 1252 and (loot.disposition ~= 'gvault')
1053 and (not addon.history_suppress) 1253 and (not addon.history_suppress)
1054 then 1254 then
1055 addon:_addHistoryEntry(looti) 1255 addon:_addHistoryEntry(looti)
1059 1259
1060 if addon.display then 1260 if addon.display then
1061 addon:redisplay() 1261 addon:redisplay()
1062 end 1262 end
1063 wipe(candidates) 1263 wipe(candidates)
1064 end 1264 wipe(sigmap)
1065 addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) 1265 end
1066 1266 local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots)
1067 local GetItemInfo, GetItemIcon, random = GetItemInfo, GetItemIcon, math.random 1267
1268 local strsplit, GetItemInfo, GetItemIcon, UnitClass =
1269 _G.strsplit, _G.GetItemInfo, _G.GetItemIcon, _G.UnitClass
1068 1270
1069 -- 'from' only present if this is triggered by a broadcast 1271 -- 'from' only present if this is triggered by a broadcast
1070 function addon:_do_loot (local_override, recipient, unique, itemid, count, from, extratext) 1272 function _do_loot (self, local_override, recipient, unique, itemid, count, from, extratext)
1273 local prefix = "_do_loot[" .. counter() .. "]"
1071 local itexture = GetItemIcon(itemid) 1274 local itexture = GetItemIcon(itemid)
1072 local iname, ilink, iquality = GetItemInfo(itemid) 1275 local iname, ilink, iquality = GetItemInfo(itemid)
1073 local i 1276 local cache_miss
1074 if (not iname) or (not itexture) then 1277 if (not iname) or (not itexture) then
1075 i = true 1278 cache_miss = true
1076 iname, ilink, iquality, itexture = 1279 iname, ilink, iquality, itexture =
1077 UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] 1280 UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]]
1078 end 1281 end
1079 self.dprint('loot',">>_do_loot, R:", recipient, "U:", unique, "I:", 1282 self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:",
1080 itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) 1283 itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality)
1081 1284
1082 itemid = tonumber(ilink:match("item:(%d+)") or 0) 1285 itemid = tonumber(ilink:match("item:(%d+)") or 0)
1083 unique = tostring(unique or random(8675309)) -- also, xkcd.com/1047 1286
1084 -- This is only a 'while' to make jumping out of it easy and still do cleanup below. 1287 -- This is only a 'while' to make jumping out of it easy.
1085 while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do 1288 local i, unique_okay, ret1, ret2
1289 while local_override
1290 or ((iquality >= self.threshold) and not opts.itemfilter[itemid])
1291 do
1292 unique_okay, unique = many_uniques_handle_it (unique, not local_override)
1293 if not unique_okay then
1294 i = g_uniques[unique]
1295 local err = unique_collision:format (ERROR_CAPS, iname, unique,
1296 tostring(from), tostring(i.loot), tostring(i.history))
1297 self:Print(err)
1298 _G.PlaySound("igQuestFailed", "master")
1299 -- Make sure this is logged one way or another
1300 ;(self.debug.loot and self.dprint or pprint)('loot', "COLLISION", prefix, err);
1301 ret1, ret2 = nil, err
1302 break
1303 end
1304
1086 if (self.rebroadcast and (not from)) and not local_override then 1305 if (self.rebroadcast and (not from)) and not local_override then
1087 self:vbroadcast('loot', recipient, unique, itemid, count) 1306 self:vbroadcast('loot', recipient, unique, itemid, count)
1088 end 1307 end
1089 if (not self.enabled) and (not local_override) then break end 1308 if (not self.enabled) and (not local_override) then break end
1090 local signature = unique .. recipient .. iname .. (count or "") 1309
1091 if from and self.recent_loot:test(signature) then 1310 local oldersig = recipient .. iname .. (count or "")
1092 self.dprint('cache', "remote loot <",signature,"> already in cache, skipping") 1311 local signature, seenit
1312 if #unique > 0 then
1313 -- newer case
1314 signature = unique .. oldersig
1315 sigmap[oldersig] = signature
1316 seenit = from and (recent_loot:test(signature)
1317 -- The following clause is what handles older 'casts arriving
1318 -- earlier. All this is tested inside-out to maximize short
1319 -- circuit avaluation.
1320 or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature)))
1093 else 1321 else
1094 -- There is some redundancy in all this, in the interests of ease-of-coding 1322 -- older case, only remote
1095 i = { 1323 assert(from)
1096 kind = 'loot', 1324 signature = sigmap[oldersig] or oldersig
1097 person = recipient, 1325 seenit = recent_loot:test(signature)
1098 person_class= select(2,UnitClass(recipient)), 1326 end
1099 cache_miss = i and true or nil, 1327
1100 quality = iquality, 1328 if seenit then
1101 itemname = iname, 1329 self.dprint('cache', "remote", prefix, "<", signature,
1102 id = itemid, 1330 "> already in cache, skipping from", from)
1103 itemlink = ilink, 1331 break
1104 itexture = itexture, 1332 end
1105 unique = unique, 1333
1106 count = (count and count ~= "") and count or nil, 1334 -- There is some redundancy in all this, in the interests of ease-of-coding
1107 bcast_from = from, 1335 i = {
1108 extratext = extratext, 1336 kind = 'loot',
1109 variant = self:is_variant_item(ilink), 1337 person = recipient,
1110 } 1338 person_class= select(2,UnitClass(recipient)),
1111 if opts.itemvault[itemid] then 1339 cache_miss = cache_miss,
1112 i.disposition = 'gvault' 1340 quality = iquality,
1113 elseif recipient == self.sharder then 1341 itemname = iname,
1114 i.disposition = 'shard' 1342 id = itemid,
1343 itemlink = ilink,
1344 itexture = itexture,
1345 unique = unique,
1346 count = (count and count ~= "") and count or nil,
1347 bcast_from = from,
1348 extratext = extratext,
1349 variant = self:is_variant_item(ilink),
1350 }
1351 if opts.itemvault[itemid] then
1352 i.disposition = 'gvault'
1353 elseif recipient == self.sharder then
1354 i.disposition = 'shard'
1355 end
1356 if local_override then
1357 -- player is adding loot by hand, don't wait for network cache timeouts
1358 -- keep this sync'd with prefer_local_loots above
1359 if i.extratext == 'shard'
1360 or i.extratext == 'gvault'
1361 or i.extratext == 'offspec'
1362 then
1363 i.disposition = i.extratext
1115 end 1364 end
1116 if local_override then 1365 local looti = self._addLootEntry(i)
1117 -- player is adding loot by hand, don't wait for network cache timeouts 1366 if (i.disposition ~= 'shard')
1118 -- keep this sync'd with prefer_local_loots above 1367 and (i.disposition ~= 'gvault')
1119 if i.extratext == 'shard' 1368 and (not self.history_suppress)
1120 or i.extratext == 'gvault' 1369 then
1121 or i.extratext == 'offspec' 1370 self:_addHistoryEntry(looti)
1122 then
1123 i.disposition = i.extratext
1124 end
1125 local looti = self._addLootEntry(i)
1126 if (i.disposition ~= 'shard')
1127 and (i.disposition ~= 'gvault')
1128 and (not self.history_suppress)
1129 then
1130 self:_addHistoryEntry(looti)
1131 end
1132 i = looti -- return value mostly for gui's manual entry
1133 else
1134 self.recent_loot:add(signature)
1135 candidates[signature] = i
1136 tinsert (candidates, signature)
1137 self.dprint('cache', "loot <",signature,"> added to cache as candidate", #candidates)
1138 end 1371 end
1372 ret1 = looti -- return value mostly for gui's manual entry
1373 else
1374 recent_loot:add(signature)
1375 candidates[signature] = i
1376 tinsert (candidates, signature)
1377 self.dprint('cache', prefix, "<", signature,
1378 "> added to cache as candidate", #candidates)
1139 end 1379 end
1140 break 1380 break
1141 end 1381 end
1142 self.dprint('loot',"<<_do_loot out") 1382 self.dprint('loot',"<<"..prefix, "out")
1143 return i 1383 return ret1, ret2
1144 end 1384 end
1145 1385
1386 -- Returns the index of the resulting new loot entry, or nil after
1387 -- displaying any errors.
1146 function addon:CHAT_MSG_LOOT (event, ...) 1388 function addon:CHAT_MSG_LOOT (event, ...)
1147 if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end 1389 if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end
1148 1390
1149 --[[ 1391 --[[
1150 iname: Hearthstone 1392 iname: Hearthstone
1171 end 1413 end
1172 end 1414 end
1173 1415
1174 self.dprint('loot', "CHAT_MSG_LOOT, person is", person, 1416 self.dprint('loot', "CHAT_MSG_LOOT, person is", person,
1175 ", itemstring is", itemstring, ", count is", count) 1417 ", itemstring is", itemstring, ", count is", count)
1176 if not itemstring then return end -- "So-and-So selected Greed", etc, not actual looting 1418 if not itemstring then return end -- "PlayerX selected Greed", etc, not looting
1177 1419
1178 -- Name might be colorized, remove the highlighting 1420 -- Name might be colorized, remove the highlighting
1179 if person then 1421 if person then
1180 person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person 1422 person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person
1181 else 1423 else
1185 --local id = tonumber(itemstring:match('|Hitem:(%d+):')) 1427 --local id = tonumber(itemstring:match('|Hitem:(%d+):'))
1186 local id,unique,_ 1428 local id,unique,_
1187 _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring) 1429 _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring)
1188 if unique == 0 then unique = nil end 1430 if unique == 0 then unique = nil end
1189 1431
1190 return self:_do_loot (false, person, unique, id, count) 1432 return _do_loot (self, false, person, unique, id, count)
1191 1433
1192 elseif event == "broadcast" then 1434 elseif event == "broadcast" then
1193 return self:_do_loot(false, ...) 1435 return _do_loot(self, false, ...)
1194 1436
1195 elseif event == "manual" then 1437 elseif event == "manual" then
1196 local r,i,n = ... 1438 local r,i,n = ...
1197 return self:_do_loot(true, r, --[[unique=]]nil, i, 1439 return _do_loot(self, true, r, --[[unique=]]nil, i,
1198 --[[count=]]nil, --[[from=]]nil, n) 1440 --[[count=]]nil, --[[from=]]nil, n)
1199 end 1441 end
1200 end 1442 end
1201 end 1443 end
1202 1444
1330 "previous data, or 'help' to read about saved-texts commands.") 1572 "previous data, or 'help' to read about saved-texts commands.")
1331 return 1573 return
1332 end 1574 end
1333 self.rebroadcast = true -- hardcode to true; this used to be more complicated 1575 self.rebroadcast = true -- hardcode to true; this used to be more complicated
1334 self.enabled = not opt_bcast_only 1576 self.enabled = not opt_bcast_only
1577 g_seeing_oldsigs = nil
1335 if opt_threshold then 1578 if opt_threshold then
1336 self:SetThreshold (opt_threshold, --[[quiet_p=]]true) 1579 self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
1337 end 1580 end
1338 self:Print("Ouro Raid Loot is %s. Threshold currently %s.", 1581 self:Print("Ouro Raid Loot is %s. Threshold currently %s.",
1339 self.enabled and "tracking" or "only broadcasting", 1582 self.enabled and "tracking" or "only broadcasting",
1362 self.dprint('flow', "Clear: display visible but eoiST not set??") 1605 self.dprint('flow', "Clear: display visible but eoiST not set??")
1363 end 1606 end
1364 self.display:Hide() 1607 self.display:Hide()
1365 end 1608 end
1366 g_restore_p = nil 1609 g_restore_p = nil
1367 OuroLootSV = nil 1610 _G.OuroLootSV = nil
1368 self:_reset_timestamps() 1611 self:_reset_timestamps()
1369 if verbose_p then 1612 if verbose_p then
1370 if (OuroLootSV_saved and #OuroLootSV_saved>0) then 1613 if (_G.OuroLootSV_saved and #_G.OuroLootSV_saved>0) then
1371 self:Print("Current loot data cleared, %d saved sets remaining.", #OuroLootSV_saved) 1614 self:Print("Current loot data cleared, %d saved sets remaining.", #_G.OuroLootSV_saved)
1372 else 1615 else
1373 self:Print("Current loot data cleared.") 1616 self:Print("Current loot data cleared.")
1374 end 1617 end
1375 end 1618 end
1376 _init(self,st) 1619 _init(self,st)
1389 -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version 1632 -- 3) VARIABLES_LOADED replaces SV_log pointer with restored version
1390 -- 4) logging happens to _log table (now with no other references) 1633 -- 4) logging happens to _log table (now with no other references)
1391 -- 5) at logout, nothing new has been entered in the table being saved 1634 -- 5) at logout, nothing new has been entered in the table being saved
1392 local date = _G.date 1635 local date = _G.date
1393 function addon:log_with_timestamp (msg) 1636 function addon:log_with_timestamp (msg)
1394 tinsert (_log, date('%m:%d %H:%M:%S ')..msg) 1637 tinsert (_log, date('%m/%d %H:%M:%S ')..msg)
1395 end 1638 end
1396 end 1639 end
1397 1640
1398 -- Check for plugins which haven't already been loaded, and add hooks for 1641 -- Check for plugins which haven't already been loaded, and add hooks for
1399 -- them. Credit to DBM for the approach here. 1642 -- them. Credit to DBM for the approach here.
1410 end 1653 end
1411 end 1654 end
1412 end 1655 end
1413 end 1656 end
1414 1657
1658 -- Routines for printing changes made by remote users.
1659 do
1660 local remote_change_chatframe
1661
1662 function addon:_set_remote_change_chatframe (arg, silent_p)
1663 local frame
1664 if type(arg) == 'number' then
1665 arg = _G.math.min (arg, _G.NUM_CHAT_WINDOWS)
1666 frame = _G['ChatFrame'..arg]
1667 elseif type(arg) == 'string' then
1668 frame = _G[arg]
1669 end
1670 if type(frame) == 'table' and type(frame.AddMessage) == 'function' then
1671 remote_change_chatframe = frame
1672 if not silent_p then
1673 local msg = "Now printing to chat frame " .. arg
1674 if frame.GetName then
1675 msg = msg .. " (" .. tostring(frame:GetName()) .. ")"
1676 end
1677 self:Print(msg)
1678 if frame ~= _G.DEFAULT_CHAT_FRAME then
1679 self:CFPrint (frame, msg)
1680 end
1681 end
1682 return frame
1683 else
1684 self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg)
1685 end
1686 end
1687
1688 function _notify_about_remote (sender, index, from_whom, olddisp)
1689 local e = g_loot[index]
1690 local from_color, from_text, to_color, to_text
1691 if from_whom then
1692 -- FIXME need to return previous name/class from reassign_loot
1693
1694 else
1695 if olddisp then
1696 from_text = addon.disposition_colors[olddisp].text
1697 else
1698 olddisp = "normal"
1699 from_text = "normal"
1700 end
1701 from_color = addon.disposition_colors[olddisp].hex
1702 if e.disposition then
1703 to_text = addon.disposition_colors[e.disposition].text
1704 else
1705 to_text = "normal"
1706 end
1707 to_color = addon.disposition_colors[e.disposition or "normal"].hex
1708 end
1709
1710 addon:CFPrint (remote_change_chatframe, remote_chatty, sender, index,
1711 e.itemlink, from_color, from_text, to_color, to_text)
1712 end
1713 end
1714
1415 -- Adds indices to traverse the tables in a nice sorted order. 1715 -- Adds indices to traverse the tables in a nice sorted order.
1416 do 1716 do
1417 local byindex, temp = {}, {} 1717 local byindex, temp = {}, {}
1418 local function sort (src, dest) 1718 local function sort (src, dest)
1419 for k in pairs(src) do 1719 for k in pairs(src) do
1420 temp[#temp+1] = k 1720 temp[#temp+1] = k
1421 end 1721 end
1422 table.sort(temp) 1722 _G.table.sort(temp)
1423 wipe(dest) 1723 wipe(dest)
1424 for i = 1, #temp do 1724 for i = 1, #temp do
1425 dest[i] = src[temp[i]] 1725 dest[i] = src[temp[i]]
1426 end 1726 end
1427 end 1727 end
1489 if not closest_boss then 1789 if not closest_boss then
1490 fencepost = closest_time 1790 fencepost = closest_time
1491 elseif not closest_time then 1791 elseif not closest_time then
1492 fencepost = closest_boss 1792 fencepost = closest_boss
1493 else 1793 else
1494 fencepost = math.min(closest_time,closest_boss) 1794 fencepost = _G.math.min(closest_time,closest_boss)
1495 end 1795 end
1496 end 1796 end
1497 return fencepost 1797 return fencepost
1498 end 1798 end
1499 1799
1536 function _init (self, possible_st) 1836 function _init (self, possible_st)
1537 self.dprint('flow',"_init running") 1837 self.dprint('flow',"_init running")
1538 self.loot_clean = nil 1838 self.loot_clean = nil
1539 self.hist_clean = nil 1839 self.hist_clean = nil
1540 if g_restore_p then 1840 if g_restore_p then
1541 g_loot = OuroLootSV 1841 g_loot = _G.OuroLootSV
1542 self.popped = #g_loot > 0 1842 self.popped = #g_loot > 0
1543 self.dprint('flow', "restoring", #g_loot, "entries") 1843 self.dprint('flow', "restoring", #g_loot, "entries")
1544 self:ScheduleTimer("Activate", 12, opts.threshold) 1844 self:ScheduleTimer("Activate", 12, opts.threshold)
1545 -- FIXME printed could be too large if entries were deleted, how much do we care? 1845 -- FIXME printed could be too large if entries were deleted, how much do we care?
1546 self.sharder = opts.autoshard 1846 self.sharder = opts.autoshard
1552 self:gui_init(g_loot) 1852 self:gui_init(g_loot)
1553 opts.autoshard = nil 1853 opts.autoshard = nil
1554 opts.threshold = nil 1854 opts.threshold = nil
1555 1855
1556 if g_restore_p then 1856 if g_restore_p then
1557 self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values 1857 self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values
1558 else 1858 else
1559 self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 1859 self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0
1560 end 1860 end
1561 if possible_st then 1861 if possible_st then
1562 possible_st:SetData(g_loot) 1862 possible_st:SetData(g_loot)
1563 end 1863 end
1564 1864
1565 self.status_text = ("%s communicating as ident %s commrev %s"):format(self.revision,self.ident,self.commrev) 1865 self.status_text = ("%s communicating as ident %s commrev %s"):
1866 format (self.revision, self.ident, self.commrev)
1566 self:RegisterComm(self.ident) 1867 self:RegisterComm(self.ident)
1567 self:RegisterComm(self.identTg, "OnCommReceivedNocache") 1868 self:RegisterComm(self.identTg, "OnCommReceivedNocache")
1568 1869
1569 if self.author_debug then 1870 if self.author_debug then
1570 _G.Oloot = g_loot 1871 _G.Oloot = g_loot
1575 do 1876 do
1576 function addon:snapshot_raid (only_inraid_p) 1877 function addon:snapshot_raid (only_inraid_p)
1577 local ss = CopyTable(g_loot.raiders) 1878 local ss = CopyTable(g_loot.raiders)
1578 local instance,maxsize = instance_tag() 1879 local instance,maxsize = instance_tag()
1579 if only_inraid_p then 1880 if only_inraid_p then
1580 for name,info in next, ss do 1881 for name,info in _G.next, ss do
1581 if info.online == 3 then 1882 if info.online == 3 then
1582 ss[name] = nil 1883 ss[name] = nil
1583 end 1884 end
1584 end 1885 end
1585 end 1886 end
1586 return ss, maxsize, instance, time() 1887 return ss, maxsize, instance, _G.time()
1587 end 1888 end
1588 end 1889 end
1589 1890
1590 -- Tie-in with Deadly Boss Mods (or other such addons) 1891 -- Tie-in with Deadly Boss Mods (or other such addons)
1591 do 1892 do
1629 addon:Print("Registered kill for '%s' in %s!", boss.bossname, boss.instance) 1930 addon:Print("Registered kill for '%s' in %s!", boss.bossname, boss.instance)
1630 end 1931 end
1631 end 1932 end
1632 wipe(candidates) 1933 wipe(candidates)
1633 end 1934 end
1634 addon.recent_boss = create_new_cache ('boss', 10, fixup_durations) 1935 local recent_boss = create_new_cache ('boss', 10, fixup_durations)
1635 1936
1636 -- Similar to _do_loot, but duration+ parms only present when locally generated. 1937 -- Similar to _do_loot, but duration+ parms only present when locally generated.
1637 local function _do_boss (self, reason, bossname, intag, maxsize, duration) 1938 local function _do_boss (self, reason, bossname, intag, maxsize, duration)
1638 self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, 1939 self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname,
1639 "T:", intag, "MS:", maxsize, "D:", duration) 1940 "T:", intag, "MS:", maxsize, "D:", duration)
1644 while self.enabled do 1945 while self.enabled do
1645 if reason == 'wipe' and opts.no_tracking_wipes then break end 1946 if reason == 'wipe' and opts.no_tracking_wipes then break end
1646 bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname 1947 bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname
1647 local not_from_local = duration == nil 1948 local not_from_local = duration == nil
1648 local signature = bossname .. reason 1949 local signature = bossname .. reason
1649 if not_from_local and self.recent_boss:test(signature) then 1950 if not_from_local and recent_boss:test(signature) then
1650 self.dprint('cache', "remote boss <",signature,"> already in cache, skipping") 1951 self.dprint('cache', "remote boss <",signature,"> already in cache, skipping")
1651 else 1952 else
1652 self.recent_boss:add(signature) 1953 recent_boss:add(signature)
1653 g_boss_signpost = #g_loot + 1 1954 g_boss_signpost = #g_loot + 1
1654 self.dprint('loot', "added boss signpost", g_boss_signpost) 1955 self.dprint('loot', "added boss signpost", g_boss_signpost)
1655 -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're 1956 -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're
1656 -- outside the instance) and so this only happens once as a non-local event, 1957 -- outside the instance) and so this only happens once as a non-local event,
1657 -- (2) we see a local event first and all non-local events are filtered 1958 -- (2) we see a local event first and all non-local events are filtered
1761 end 2062 end
1762 end 2063 end
1763 2064
1764 -- Adding entries to the loot record, and tracking the corresponding timestamp. 2065 -- Adding entries to the loot record, and tracking the corresponding timestamp.
1765 do 2066 do
2067 local rawget, setmetatable = _G.rawget, _G.setmetatable
2068
1766 -- This shouldn't be required. /sadface 2069 -- This shouldn't be required. /sadface
1767 local loot_entry_mt = { 2070 local loot_entry_mt = {
1768 __index = function (e,key) 2071 __index = function (e,key)
1769 if key == 'cols' then 2072 if key == 'cols' then
1770 pprint('mt', e.kind, "key is", key) 2073 pprint('mt', e.kind, "key is", key)
1844 function addon._addLootEntry (e) 2147 function addon._addLootEntry (e)
1845 setmetatable(e,loot_entry_mt) 2148 setmetatable(e,loot_entry_mt)
1846 2149
1847 if not done_todays_date then do_todays_date() end 2150 if not done_todays_date then do_todays_date() end
1848 2151
1849 local h, m = GetGameTime() 2152 local h, m = _G.GetGameTime()
1850 --local localuptime = math.floor(GetTime()) 2153 --local localuptime = math.floor(GetTime())
1851 local time_t = time() 2154 local time_t = _G.time()
1852 e.hour = h 2155 e.hour = h
1853 e.minute = m 2156 e.minute = m
1854 e.stamp = time_t --localuptime 2157 e.stamp = time_t --localuptime
1855 local index = #g_loot + 1 2158 local index = #g_loot + 1
1856 g_loot[index] = e 2159 g_loot[index] = e
1909 self:Print("Fixing up missing item cache data...") 2212 self:Print("Fixing up missing item cache data...")
1910 2213
1911 local numfound = 0 2214 local numfound = 0
1912 local borkedpat = '^'..UNKNOWN..': (%S+)' 2215 local borkedpat = '^'..UNKNOWN..': (%S+)'
1913 2216
1914 for i,e in self:filtered_loot_iter('loot') do 2217 -- 'while true' so that we can use (inner) break as (outer) continue
1915 if e.cache_miss then 2218 for i,e in self:filtered_loot_iter('loot') do while true do
1916 local borked_id = e.itemname:match(borkedpat) 2219 if not e.cache_miss then break end
1917 if borked_id then 2220 local borked_id = e.itemname:match(borkedpat)
1918 numfound = numfound + 1 2221 if not borked_id then break end
1919 -- Best to use the safest and most flexible API here, which is GII and 2222 numfound = numfound + 1
1920 -- its assload of return values. 2223 -- Best to use the safest and most flexible API here, which is GII and
1921 local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) 2224 -- its assload of return values.
1922 if iname then 2225 local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id)
1923 self:Print(" Entry %d patched up with %s.", i, ilink) 2226 if iname then
1924 e.quality = iquality 2227 local msg = [[ Entry %d patched up with %s.]]
1925 e.itemname = iname 2228 e.quality = iquality
1926 e.id = tonumber(ilink:match("item:(%d+)")) 2229 e.itemname = iname
1927 e.itemlink = ilink 2230 e.id = tonumber(ilink:match("item:(%d+)"))
1928 e.itexture = itexture 2231 e.itemlink = ilink
1929 e.cache_miss = nil 2232 e.itexture = itexture
2233 e.cache_miss = nil
2234 if e.unique then
2235 local gu = g_uniques[e.unique]
2236 local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache")
2237 if gu.loot ~= i then -- is this an actual problem?
2238 pprint('loot', ("Unique value '%s' had iterator value %d but g_uniques index %s."):format(e.unique,i,tostring(gu.loot)))
1930 end 2239 end
1931 end 2240 if player_i then
1932 end 2241 player_h.id[e.unique] = e.id
1933 end 2242 msg = [[ Entry %d (and history) patched up with %s.]]
2243 end
2244 end
2245 self:Print(msg, i, ilink)
2246 end
2247 break
2248 end end
1934 2249
1935 self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) 2250 self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound)
1936 end 2251 end
1937 2252
1938 2253
1939 ------ Saved texts 2254 ------ Saved texts
1940 function addon:check_saved_table(silent_p) 2255 function addon:check_saved_table(silent_p)
1941 local s = OuroLootSV_saved 2256 local s = _G.OuroLootSV_saved
1942 if s and (#s > 0) then return s end 2257 if s and (#s > 0) then return s end
1943 OuroLootSV_saved = nil 2258 _G.OuroLootSV_saved = nil
1944 if not silent_p then self:Print("There are no saved loot texts.") end 2259 if not silent_p then self:Print("There are no saved loot texts.") end
1945 end 2260 end
1946 2261
1947 function addon:save_list() 2262 function addon:save_list()
1948 local s = self:check_saved_table(); if not s then return end; 2263 local s = self:check_saved_table(); if not s then return end;
1950 self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name) 2265 self:Print("#%d %s %d entries %s", i, t.date, t.count, t.name)
1951 end 2266 end
1952 end 2267 end
1953 2268
1954 function addon:save_saveas(name) 2269 function addon:save_saveas(name)
1955 OuroLootSV_saved = OuroLootSV_saved or {} 2270 _G.OuroLootSV_saved = _G.OuroLootSV_saved or {}
1956 local SV = OuroLootSV_saved 2271 local SV = _G.OuroLootSV_saved
1957 local n = #SV + 1 2272 local n = #SV + 1
1958 local save = { 2273 local save = {
1959 name = name, 2274 name = name,
1960 date = makedate(), 2275 date = makedate(),
1961 count = #g_loot, 2276 count = #g_loot,
2003 -- ["OtherPlayer"] = 2, 2318 -- ["OtherPlayer"] = 2,
2004 -- ["Farmbuyer"] = 1, 2319 -- ["Farmbuyer"] = 1,
2005 -- } 2320 -- }
2006 -- [1] = { 2321 -- [1] = {
2007 -- ["name"] = "Farmbuyer", 2322 -- ["name"] = "Farmbuyer",
2008 -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot 2323 -- ["person_class"] = "PRIEST", -- may be missing, used in display only
2009 -- [2] = { ......., [count = "x3"] } -- previous loot 2324 -- -- sorted array:
2325 -- ["unique"] = { most_recent_tag, previous_tag, .... },
2326 -- -- these are indexed by unique tags, and 'count' may be missing:
2327 -- ["when"] = { ["tag"] = "formatted timestamp for displaying loot" },
2328 -- ["id"] = { ["tag"] = 11111 },
2329 -- ["count"] = { ["tag"] = "x3", .... },
2010 -- }, 2330 -- },
2011 -- [2] = { 2331 -- [2] = {
2012 -- ["name"] = "OtherPlayer", 2332 -- ["name"] = "OtherPlayer",
2013 -- ...... 2333 -- ......
2014 -- }, ...... 2334 -- }, ......
2015 -- }, 2335 -- },
2016 -- ["OtherRealm"] = ...... 2336 -- ["OtherRealm"] = ......
2017 -- } 2337 -- }
2338 --
2339 -- Up through 2.81.4 (specifically through rev 95), an individual player's
2340 -- table looked like this:
2341 -- ["name"] = "Farmbuyer",
2342 -- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot
2343 -- [2] = { ......., [count = "x3"] } -- previous loot
2344 -- which was much easier to manipulate, but had a ton of memory overhead.
2018 do 2345 do
2019 local tsort = table.sort 2346 -- Sorts a player's history from newest to oldest, according to the
2020 local comp = function(L,R) return L.when > R.when end 2347 -- formatted timestamp. This is expensive, and destructive for P.unique.
2348 local function sort_player (p)
2349 local new_uniques, uniques_bywhen, when_array = {}, {}, {}
2350 for u,tstamp in pairs(p.when) do
2351 uniques_bywhen[tstamp] = u
2352 when_array[#when_array+1] = tstamp
2353 end
2354 _G.table.sort(when_array)
2355 for i,tstamp in ipairs(when_array) do
2356 new_uniques[i] = uniques_bywhen[tstamp]
2357 end
2358 p.unique = new_uniques
2359 end
2360
2361 -- Possibly called during login. Cleared when no longer needed.
2362 -- Rewrites a PLAYER table from format 3 to format 4.
2363 function addon:_uplift_history_format (player, realmname)
2364 local unique, when, id, count = {}, {}, {}, {}
2365 local name = player.name
2366
2367 for i,h in ipairs(player) do
2368 local U = h.unique
2369 unique[i] = U
2370 when[U] = h.when
2371 id[U] = h.id
2372 count[U] = h.count
2373 end
2374
2375 wipe(player)
2376 player.name = name
2377 player.id, player.when, player.unique, player.count =
2378 id, when, unique, count
2379 end
2380 function addon:_cache_history_uniques()
2381 UpdateAddOnMemoryUsage()
2382 local before = GetAddOnMemoryUsage(nametag)
2383 local trouble
2384 local count = 0
2385 for hi,player in ipairs(self.history) do
2386 for ui,u in ipairs(player.unique) do
2387 g_uniques[u] = { history = hi, history_may = ui }
2388 count = count + 1
2389 end
2390 end
2391 for i,e in self:filtered_loot_iter('loot') do
2392 if e.unique and e.unique ~= "" then
2393 local hmmm = _G.rawget(g_uniques,e.unique)
2394 if hmmm then
2395 hmmm.loot = i --;print("Active loot", i, "found with tag", e.unique)
2396 elseif e.disposition == 'shard' or e.disposition == 'gvault' then
2397 g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
2398 count = count + 1
2399 --print("Active loot", i, "INSERTED with tag", e.unique, "as", e.disposition)
2400 else
2401 hmmm = "wonked data ("..i.."/"..tostring(e.unique)..") in precache loop!"
2402 pprint(hmmm)
2403 -- try to simply fix up errors as we go
2404 g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
2405 trouble = true
2406 end
2407 else
2408 trouble = true
2409 pprint('loot', "ERROR precache loop found bad unique tag!",
2410 i, "tag", tostring(e.unique), "from?", tostring(e.bcast_from))
2411 end
2412 end
2413 UpdateAddOnMemoryUsage()
2414 local after = GetAddOnMemoryUsage(nametag)
2415 self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.",
2416 self.history.realm, (after-before)/1024, count)
2417 if trouble then
2418 self:Print("Note that there were inconsistencies in the data;",
2419 "you should consider submitting a bug report (including your",
2420 "SavedVariables file), and regenerating or preening this",
2421 "realm's loot history.")
2422 end
2423 g_uniques:SETMODE('probe')
2424 self._cache_history_uniques = nil
2425 end
2021 2426
2022 -- Builds the map of names to array indices, using passed table or 2427 -- Builds the map of names to array indices, using passed table or
2023 -- self.history, and stores the result into its 'byname' field. Also 2428 -- self.history, and stores the result into its 'byname' field. Also
2024 -- called from the GUI code at least once. 2429 -- called from the GUI code at least once.
2025 function addon:_build_history_names (opt_hist) 2430 function addon:_build_history_names (opt_hist)
2026 local hist = opt_hist or self.history 2431 local hist = opt_hist or self.history
2027 local m = {} 2432 local m = {}
2028 for i = 1, #hist do 2433 for i = 1, #hist do
2029 m[hist[i].name] = i 2434 m[hist[i].name] = i
2030 end 2435 end
2436 -- why yes, I *did* spend many years as a YP/NIS admin, how did you know?
2031 hist.byname = m 2437 hist.byname = m
2032 end 2438 end
2033 2439
2034 -- Prepares and returns table to be used as self.history. 2440 -- Prepares and returns table to be used as self.history.
2035 function addon:_prep_new_history_category (prev_table, realmname) 2441 function addon:_prep_new_history_category (prev_table, realmname)
2057 function addon:get_loot_history (name) 2463 function addon:get_loot_history (name)
2058 local i 2464 local i
2059 i = self.history.byname[name] 2465 i = self.history.byname[name]
2060 if not i then 2466 if not i then
2061 i = #self.history + 1 2467 i = #self.history + 1
2062 self.history[i] = { name=name } 2468 self.history[i] = { name=name, id={}, when={}, unique={}, count={} }
2063 self.history.byname[name] = i 2469 self.history.byname[name] = i
2064 end 2470 end
2065 return i, self.history[i] 2471 return i, self.history[i]
2066 end 2472 end
2067 2473
2474 -- Prepends data from the loot entry at LOOTINDEX to be the new most
2475 -- recent history entry for that player.
2068 function addon:_addHistoryEntry (lootindex) 2476 function addon:_addHistoryEntry (lootindex)
2069 local e = g_loot[lootindex] 2477 local e = g_loot[lootindex]
2070 if e.kind ~= 'loot' then return end 2478 if e.kind ~= 'loot' then return end
2071 2479
2072 local i,h = self:get_loot_history(e.person) 2480 local i,h = self:get_loot_history(e.person)
2481 local when = self:format_timestamp (g_today, e)
2482
2073 -- If any of these change, update the end of history_handle_disposition. 2483 -- If any of these change, update the end of history_handle_disposition.
2074 local n = {
2075 id = e.id,
2076 when = self:format_timestamp (g_today, e),
2077 count = e.count,
2078 }
2079 tinsert (h, 1, n)
2080 if (not e.unique) or (#e.unique==0) then 2484 if (not e.unique) or (#e.unique==0) then
2081 e.unique = n.id .. ' ' .. n.when 2485 e.unique = e.id .. ' ' .. when
2082 end 2486 end
2083 n.unique = e.unique 2487 local U = e.unique
2488 tinsert (h.unique, 1, U)
2489 h.when[U] = when
2490 h.id[U] = e.id
2491 h.count[U] = e.count
2492
2493 g_uniques[U] = { loot = lootindex, history = i }
2084 end 2494 end
2085 2495
2086 -- Create new history table based on current loot. 2496 -- Create new history table based on current loot.
2087 function addon:rewrite_history (realmname) 2497 function addon:rewrite_history (realmname)
2088 local r = assert(realmname) 2498 local r = assert(realmname)
2089 self.history_all[r] = self:_prep_new_history_category (nil, r) 2499 self.history_all[r] = self:_prep_new_history_category (nil, r)
2090 self.history = self.history_all[r] 2500 self.history = self.history_all[r]
2501 g_uniques:RESET()
2091 2502
2092 local g_today_real = g_today 2503 local g_today_real = g_today
2093 for i,e in ipairs(g_loot) do 2504 for i,e in ipairs(g_loot) do
2094 if e.kind == 'time' then 2505 if e.kind == 'time' then
2095 g_today = e 2506 g_today = e
2100 g_today = g_today_real 2511 g_today = g_today_real
2101 self.hist_clean = nil 2512 self.hist_clean = nil
2102 2513
2103 -- safety measure: resort players' tables based on formatted timestamp 2514 -- safety measure: resort players' tables based on formatted timestamp
2104 for i,h in ipairs(self.history) do 2515 for i,h in ipairs(self.history) do
2105 tsort (h, comp) 2516 sort_player(h)
2106 end 2517 end
2107 end 2518 end
2108 2519
2109 -- Clears all but latest entry for each player. 2520 -- Clears all but latest entry for each player.
2110 function addon:preen_history (realmname) 2521 function addon:preen_history (realmname)
2111 local r = assert(realmname) 2522 local r = assert(realmname)
2523 g_uniques:RESET()
2112 for i,h in ipairs(self.history) do 2524 for i,h in ipairs(self.history) do
2113 tsort (h, comp) 2525 -- This is going to do horrible things to memory. The subtables
2114 while #h > 1 do 2526 -- after this step would be large and sparse, with no good way
2115 tremove (h) 2527 -- of shrinking the allocation...
2116 end 2528 sort_player(h)
2117 end 2529 -- ...so it's better in the long run to discard them.
2118 end 2530 local U = h.unique[1]
2119 2531 h.unique = { U }
2120 -- Given an entry in a g_loot table, looks up the corresponding history 2532 h.id = { [U] = h.id[U] }
2121 -- entry. Returns the player's index and history table (as in get_loot_history) 2533 h.when = { [U] = h.when[U] }
2122 -- and the index into that table of the loot entry. On failure, returns nil 2534 h.count = { [U] = h.count[U] }
2123 -- and an error message ready to be formatted with the loot's name/itemlink. 2535 end
2124 function addon:_history_by_loot_id (loot, operation_text) 2536 end
2125 -- Using assert() here would be concatenating error strings that probably 2537
2126 -- wouldn't be used. Do more verbose testing instead. 2538 -- Given a unique tag OR an entry in a g_loot table, looks up the
2127 if type(loot) ~= 'table' then 2539 -- corresponding history entry. Returns the player's index and history
2128 error("trying to "..operation_text.." nonexistant entry") 2540 -- table (as in get_loot_history) and the index into that table of the
2129 end 2541 -- loot entry. On failure, returns nil and an error message ready to be
2130 if loot.kind ~= 'loot' then 2542 -- formatted with the loot's name/itemlink.
2131 error("trying to "..operation_text.." something that isn't loot") 2543 function _history_by_loot_id (needle, operation_text)
2132 end
2133
2134 local player = loot.person
2135 local tag = loot.unique
2136 local errtxt 2544 local errtxt
2137 local player_i, player_h, hist_i 2545 if type(needle) == 'string' then
2138 2546 -- unique tag
2139 if not tag then 2547 elseif type(needle) == 'table' then
2140 errtxt = "Entry for %s is missing a history tag!" 2548 if needle.kind ~= 'loot' then
2549 error("trying to "..operation_text.." something that isn't loot")
2550 end
2551 needle = needle.unique
2552 if not needle then
2553 return nil, --[[errtxt=]]"Entry for %s is missing a history tag!"
2554 end
2141 else 2555 else
2142 player_i,player_h = self:get_loot_history(player) 2556 error("'"..tostring(needle).."' is neither unique string nor loot entry!")
2143 for i,h in ipairs(player_h) do 2557 end
2144 if h.unique == tag then 2558
2145 hist_i = i 2559 local player_i, player_h
2146 break 2560 local cache = g_uniques[needle]
2561
2562 if cache.history == g_uniques.NOTFOUND then
2563 -- 1) loot an item, 2) clear old history, 3) reassign from current loot
2564 -- Bah. Anybody that tricky is already recoding the tables directly anyhow.
2565 errtxt = "There is no record of %s ever having been assigned!"
2566 else
2567 player_i = cache.history
2568 player_h = addon.history[player_i]
2569 if cache.history_may
2570 and needle == player_h.unique[cache.history_may]
2571 then
2572 return player_i, player_h, cache.history_may
2573 end
2574 for i,u in ipairs(player_h.unique) do
2575 if needle == u then
2576 cache.history_may = i -- might help, might not
2577 return player_i, player_h, i
2147 end 2578 end
2148 end 2579 end
2149 if not hist_i then 2580 end
2150 -- 1) loot an item, 2) clear old history, 3) reassign from current loot 2581
2151 -- Bah. Anybody that tricky is already recoding the tables directly anyhow. 2582 if not errtxt then
2152 errtxt = "There is no record of %s ever having been assigned!" 2583 -- The cache finder got a hit, but now it's gone? WTF?
2153 end 2584 errtxt = "ZOMG! %s was in history but now is gone. Possibly your history tables have been corrupted and should be recreated. This is likely a bug. Tell Farmbuyer what steps you took to cause this."
2154 end 2585 end
2155 2586 return nil, errtxt
2156 if errtxt then
2157 return nil, errtxt
2158 end
2159 return player_i, player_h, hist_i
2160 end 2587 end
2161 2588
2162 function addon:reassign_loot (index, to_name) 2589 function addon:reassign_loot (index, to_name)
2163 assert(type(to_name)=='string' and to_name:len()>0) 2590 assert(type(to_name)=='string' and to_name:len()>0)
2164 local e = g_loot[index] 2591 local e = g_loot[index]
2165 local from_i, from_h, hist_i = self:_history_by_loot_id (e, "reassign") 2592 local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign")
2166 local from_name = e.person 2593 local from_name = e.person
2167 local to_i,to_h = self:get_loot_history(to_name) 2594 local to_i,to_h = self:get_loot_history(to_name)
2168 2595
2169 if not from_i then 2596 if not from_i then -- from_h here is the formatted error text
2170 -- from_h is the formatted error text
2171 self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) 2597 self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink)
2172 else 2598 else
2173 local hist_h = tremove (from_h, hist_i) 2599 local U = tremove (from_h.unique, hist_i)
2174 tinsert (to_h, 1, hist_h) 2600 -- The loot master giveth...
2175 tsort (from_h, comp) 2601 to_h.unique[#to_h.unique+1] = U
2176 tsort (to_h, comp) 2602 to_h.when[U] = from_h.when[U]
2603 to_h.id[U] = from_h.id[U]
2604 to_h.count[U] = from_h.count[U]
2605 sort_player(to_h)
2606 -- ...and the loot master taketh away.
2607 from_h.when[U] = nil
2608 from_h.id[U] = nil
2609 from_h.count[U] = nil
2610 -- Blessed be the lookup cache of the loot master.
2611 g_uniques[U] = { loot = index, history = to_i }
2177 end 2612 end
2178 e.person = to_name 2613 e.person = to_name
2179 e.person_class = select(2,UnitClass(to_name)) 2614 e.person_class = select(2,_G.UnitClass(to_name))
2180 self.hist_clean = nil 2615 self.hist_clean = nil
2181 2616
2182 self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name) 2617 self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name)
2183 end 2618 end
2184 2619
2187 -- be pulled from LOOTINDEX instead). 2622 -- be pulled from LOOTINDEX instead).
2188 function addon:_delHistoryEntry (lootindex, opt_e) 2623 function addon:_delHistoryEntry (lootindex, opt_e)
2189 local e = opt_e or g_loot[lootindex] 2624 local e = opt_e or g_loot[lootindex]
2190 if e.kind ~= 'loot' then return end 2625 if e.kind ~= 'loot' then return end
2191 2626
2192 local from_i, from_h, hist_i = self:_history_by_loot_id (e, "delete") 2627 local from_i, from_h, hist_i = _history_by_loot_id (e, "delete")
2193 if not from_i then 2628 if not from_i then
2194 -- from_h is the formatted error text 2629 -- from_h is the formatted error text
2195 self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink) 2630 self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink)
2196 return 2631 return
2197 end 2632 end
2198 2633
2199 --[[local hist_h = ]]tremove (from_h, hist_i) 2634 local hist_u = tremove (from_h.unique, hist_i)
2200 tsort (from_h, comp) 2635 from_h.when[hist_u] = nil
2636 from_h.id[hist_u] = nil
2637 from_h.count[hist_u] = nil
2638 g_uniques[hist_u] = nil
2201 self.hist_clean = nil 2639 self.hist_clean = nil
2202 2640
2203 self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person) 2641 self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person)
2204 end 2642 end
2205 2643
2206 -- Any extra work for the "Mark as <x>" dropdown actions. The 2644 -- Any extra work for the "Mark as <x>" dropdown actions. The
2207 -- corresponding <x> will already have been assigned in the loot entry. 2645 -- corresponding <x> will already have been assigned in the loot entry.
2208 local deleted_cache = {} --setmetatable({}, {__mode='k'}) 2646 local deleted_cache = {}
2209 function addon:history_handle_disposition (index, olddisp) 2647 function addon:history_handle_disposition (index, olddisp)
2210 local e = g_loot[index] 2648 local e = g_loot[index]
2211 -- Standard disposition has a nil entry, but that's tedious in debug 2649 -- Standard disposition has a nil entry, but that's tedious in debug
2212 -- output, so force to a string instead. 2650 -- output, so force to a string instead.
2213 olddisp = olddisp or 'normal' 2651 olddisp = olddisp or 'normal'
2216 if olddisp == newdisp then return end 2654 if olddisp == newdisp then return end
2217 2655
2218 local name = e.person 2656 local name = e.person
2219 2657
2220 if (newdisp == 'shard' or newdisp == 'gvault') then 2658 if (newdisp == 'shard' or newdisp == 'gvault') then
2221 local name_i, name_h, hist_i = self:_history_by_loot_id (e, "mark") 2659 local name_i, name_h, hist_i = _history_by_loot_id (e, "mark")
2222 -- remove history entry 2660 -- remove history entry if it exists
2223 if hist_i then 2661 if hist_i then
2224 local hist_h = tremove (name_h, hist_i) 2662 local c = flib.new()
2225 deleted_cache[e.unique] = hist_h 2663 local hist_u = tremove (name_h.unique, hist_i)
2664 c.when = name_h.when[hist_u]
2665 c.id = name_h.id[hist_u]
2666 c.count = name_h.count[hist_u]
2667 deleted_cache[hist_u] = c
2668 name_h.when[hist_u] = nil
2669 name_h.id[hist_u] = nil
2670 name_h.count[hist_u] = nil
2226 self.hist_clean = nil 2671 self.hist_clean = nil
2227 elseif (olddisp == 'shard' or olddisp == 'gvault') then 2672 elseif (olddisp == 'shard' or olddisp == 'gvault') then
2228 -- Sharding a vault item, or giving the auto-sharder something to bank, 2673 -- Sharding a vault item, or giving the auto-sharder something to bank,
2229 -- etc, wouldn't necessarily have had a history entry to begin with. 2674 -- etc, wouldn't necessarily have had a history entry to begin with.
2675 -- So this isn't treated as an error.
2230 else 2676 else
2231 self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink) 2677 self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink)
2232 end 2678 end
2233 return 2679 return
2234 end 2680 end
2240 2686
2241 -- Must create a new history entry. Could call '_addHistoryEntry(index)' 2687 -- Must create a new history entry. Could call '_addHistoryEntry(index)'
2242 -- but that would duplicate a lot of effort. To start with, check the 2688 -- but that would duplicate a lot of effort. To start with, check the
2243 -- cache of stuff we've already deleted; if it's not there then just do 2689 -- cache of stuff we've already deleted; if it's not there then just do
2244 -- the same steps as _addHistoryEntry. 2690 -- the same steps as _addHistoryEntry.
2245 local entry 2691 -- FIXME The deleted cache isn't nearly as useful now with the new data structures.
2246 if e.unique and deleted_cache[e.unique] then 2692 local when
2247 entry = deleted_cache[e.unique]
2248 deleted_cache[e.unique] = nil
2249 end
2250 local when = g_today and self:format_timestamp (g_today, e) or tostring(e.stamp)
2251 entry = entry or {
2252 id = e.id,
2253 when = when,
2254 count = e.count,
2255 }
2256 tinsert (name_h, 1, entry)
2257 if (not e.unique) or (#e.unique==0) then 2693 if (not e.unique) or (#e.unique==0) then
2258 e.unique = entry.id .. ' ' .. entry.when 2694 when = g_today and self:format_timestamp (g_today, e) or date("%Y/%m/%d %H:%M",e.stamp)
2259 end 2695 e.unique = e.id .. ' ' .. when
2260 entry.unique = e.unique 2696 end
2697 local U = e.unique
2698 local c = deleted_cache[U]
2699 deleted_cache[U] = nil
2700 name_h.unique[#name_h.unique+1] = U
2701 name_h.when[U] = c and c.when or when or date("%Y/%m/%d %H:%M",e.stamp)
2702 name_h.id[U] = e.id -- c.id
2703 name_h.count[U] = c and c.count or e.count
2704 sort_player(name_h)
2705 g_uniques[U] = { loot = index, history = name_i }
2261 self.hist_clean = nil 2706 self.hist_clean = nil
2707
2708 if c then flib.del(c) end
2709
2262 return 2710 return
2263 end 2711 end
2712 end
2713
2714 -- This is not entirely "history" but not completely anything else either.
2715 -- Handles the primary "Mark as <x>" action. Arguments depend on who's
2716 -- calling it:
2717 -- "local", row_index, new_disposition
2718 -- "remote", sender, unique_id, item_id, old_disposition, new_disposition
2719 -- In the local case, must also broadcast a trigger. In the remote case,
2720 -- must figure out the corresponding loot entry (if it exists). In both
2721 -- cases, must update history appropriately. Returns nil if anything odd
2722 -- happens (not necessarily an error!); returns the affected loot index
2723 -- on success.
2724 function addon:loot_mark_disposition (how, ...)
2725 -- This must all be filled out in all cases:
2726 local e, index, olddisp, newdisp, unique, id
2727 -- Only set in remote case:
2728 local sender
2729
2730 if how == "local" then
2731 index, newdisp = ...
2732 index = assert(tonumber(index))
2733 e = g_loot[index]
2734 id = e.id
2735 unique = e.unique -- can potentially still be nil at this step
2736 olddisp = e.disposition
2737
2738 elseif how == "remote" then
2739 sender, unique, id, olddisp, newdisp = ...
2740 local cache = g_uniques[unique]
2741 if cache.loot then
2742 index = tonumber(cache.loot)
2743 e = g_loot[index]
2744 end
2745
2746 else
2747 return -- silently ignore newer cases from newer clients
2748 end
2749
2750 if self.debug.loot then
2751 local m = ("Re-mark index %d(pre-unique %s) with id %d from '%s' to '%s'."):
2752 format(index, unique, id, tostring(olddisp), tostring(newdisp))
2753 self.dprint('loot', m)
2754 if sender == my_name then
2755 self.dprint('loot',"(Returning early from double self-mark.)")
2756 return index
2757 end
2758 end
2759
2760 if not e then
2761 -- say something?
2762 return
2763 end
2764
2765 e.disposition = newdisp
2766 e.bcast_from = nil -- I actually don't remember now why this gets cleared...
2767 e.extratext = nil
2768 self:history_handle_disposition (index, olddisp)
2769 -- A unique tag has been set by this point.
2770 if how == "local" then
2771 unique = assert(e.unique)
2772 self:vbroadcast('mark', unique, id, olddisp, newdisp)
2773 end
2774 return index
2264 end 2775 end
2265 end 2776 end
2266 2777
2267 2778
2268 ------ Player communication 2779 ------ Player communication
2269 do 2780 do
2270 local select, tconcat, strsplit = select, table.concat, strsplit 2781 local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack
2271 --[[ old way: repeated string concatenations, BAD 2782 --[[ old way: repeated string concatenations, BAD
2272 new way: new table on every call, BAD 2783 new way: new table on every call, BAD
2273 local msg = ... 2784 local msg = ...
2274 for i = 2, select('#',...) do 2785 for i = 2, select('#',...) do
2275 msg = msg .. '\a' .. (select(i,...) or "") 2786 msg = msg .. '\a' .. (select(i,...) or "")
2276 end 2787 end
2277 return msg 2788 return msg
2278 ]] 2789 ]]
2279 local function assemble(t,...) 2790 local function assemble(t,...)
2280 if select('#',...) > 0 then 2791 local n = select('#',...)
2792 if n > 0 then
2281 local msg = {t,...} 2793 local msg = {t,...}
2282 -- tconcat requires strings, but T is known to be one already 2794 -- tconcat requires strings, but T is known to be one already
2283 for i = 2, #msg do 2795 -- can't use #msg since there might be holes
2796 for i = 2, n+1 do
2284 msg[i] = tostring(msg[i] or "") 2797 msg[i] = tostring(msg[i] or "")
2285 end 2798 end
2286 return tconcat (msg, '\a') 2799 return tconcat (msg, '\a')
2287 end 2800 end
2288 return t 2801 return t
2329 OCR_funcs.revcheck = function (sender, _, revlarge) 2842 OCR_funcs.revcheck = function (sender, _, revlarge)
2330 addon.dprint('comm', "revcheck, sender", sender) 2843 addon.dprint('comm', "revcheck, sender", sender)
2331 addon:_check_revision (revlarge) 2844 addon:_check_revision (revlarge)
2332 end 2845 end
2333 2846
2847 OCR_funcs['17mark'] = function (sender, _, unique, item, old, new)
2848 addon.dprint('comm', "DOTmark/17, sender", sender, "unique", unique,
2849 "item", item, "from old", old, "to new", new)
2850 local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new)
2851 --if not addon.enabled then return end -- hmm
2852 if index and opts.chatty_on_remote_changes then
2853 _notify_about_remote (sender, index, --[[from_whom=]]nil, old)
2854 end
2855 end
2856
2334 OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext) 2857 OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext)
2335 addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count) 2858 addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count)
2336 if not addon.enabled then return end 2859 if not addon.enabled then return end
2337 adduser (sender, nil, true) 2860 adduser (sender, nil, true)
2861 -- Empty unique string will pass through all of the loot handling,
2862 -- and then be rewritten by the history routine (into older string
2863 -- of ID+date).
2864 g_seeing_oldsigs = g_seeing_oldsigs or {}
2865 g_seeing_oldsigs[sender] = true
2338 addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext) 2866 addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext)
2339 end 2867 end
2340 OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16 2868 OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16
2341 OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext) 2869 OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext)
2342 addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, 2870 addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip,
2343 "unique", unique, "item", item, "count", count) 2871 "unique", unique, "item", item, "count", count, "extratext", extratext)
2344 if not addon.enabled then return end 2872 if not addon.enabled then return end
2345 adduser (sender, nil, true) 2873 adduser (sender, nil, true)
2346 addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext) 2874 addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext)
2347 end 2875 end
2348 2876
2352 if not addon.enabled then return end 2880 if not addon.enabled then return end
2353 adduser (sender, nil, true) 2881 adduser (sender, nil, true)
2354 addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0) 2882 addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0)
2355 end 2883 end
2356 OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize) 2884 OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize)
2357 addon.dprint('comm', "DOTboss16, sender", sender, "reason", reason, 2885 addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason,
2358 "name", bossname, "it", instancetag, "size", maxsize) 2886 "name", bossname, "it", instancetag, "size", maxsize)
2359 if not addon.enabled then return end 2887 if not addon.enabled then return end
2360 adduser (sender, nil, true) 2888 adduser (sender, nil, true)
2361 addon:on_boss_broadcast (reason, bossname, instancetag, maxsize) 2889 addon:on_boss_broadcast (reason, bossname, instancetag, maxsize)
2362 end 2890 end
2383 -- remove this tag once it's all tested 2911 -- remove this tag once it's all tested
2384 OCR_funcs.bcast_denied = function (sender) 2912 OCR_funcs.bcast_denied = function (sender)
2385 if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end 2913 if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end
2386 end 2914 end
2387 2915
2388 -- Incoming message dispatcher 2916 -- Incoming message disassembler and dispatcher. The static weak table
2917 -- is not my favorite approach to handling ellipses, but it lets me loop
2918 -- through potential nils easily without creating a ton of garbage.
2919 local OCR_data = setmetatable({}, {__mode='v'})
2389 local function dotdotdot (sender, tag, ...) 2920 local function dotdotdot (sender, tag, ...)
2390 local f = OCR_funcs[tag] 2921 local f = OCR_funcs[tag]
2391 addon.dprint('comm', ":... processing",tag,"from",sender) 2922 if f then
2392 if f then return f(sender,tag,...) end 2923 --wipe(OCR_data) costs more than its worth here
2393 addon.dprint('comm', "unknown comm message",tag",from", sender) 2924 local n = select('#',...)
2394 end 2925 for i = 1, n do
2395 -- Recent message cache 2926 local d = select(i,...)
2927 OCR_data[i] = (d ~= "") and d or nil
2928 end
2929 addon.dprint('comm', ":... processing", tag, "from", sender, "with arg count", n)
2930 return f(sender,tag,unpack(OCR_data,1,n))
2931 end
2932 addon.dprint('comm', "unknown comm message", tag, "from", sender)
2933 end
2934 -- Recent message cache (this can be accessed via advanced options panel)
2396 addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl) 2935 addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)
2397 2936
2398 function addon:OnCommReceived (prefix, msg, distribution, sender) 2937 function addon:OnCommReceived (prefix, msg, distribution, sender)
2399 if prefix ~= self.ident then return end 2938 if prefix ~= self.ident then return end
2400 if not self.debug.comm then 2939 if not self.debug.comm then