Mercurial > wow > ouroloot
diff 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 |
line wrap: on
line diff
--- a/core.lua Sat May 12 11:08:23 2012 +0000 +++ b/core.lua Tue May 29 22:50:09 2012 +0000 @@ -92,7 +92,8 @@ variable/function naming conventions (sv_*, g_*, and family) stayed across the rewrite. -Some variables are needlessly initialized to nil just to look uniform. +Some variables are needlessly initialized to nil just to look uniform and +serve as a reminder. ]==] @@ -130,6 +131,9 @@ ['forum_current'] = '[item] by name', ['display_disabled_LODs'] = false, ['display_bcast_from'] = true, + ['precache_history_uniques'] = false, + ['chatty_on_remote_changes'] = false, + ['chatty_on_remote_changes_frame'] = 1, } local virgin = "First time loaded? Hi! Use the /ouroloot or /loot command" .." to show the main display. You should probably browse the instructions" @@ -139,6 +143,11 @@ .." a download URL for copy-and-pasting. You can %s to ping other raiders" .." for their installed versions (same as '/ouroloot ping' or clicking the" .." 'Ping!' button on the options panel)." +local unique_collision = "|cffff1010%s:|r Item '%s' was carrying unique tag <" + ..">, but that was already in use! (New sender was '%s', previous cache " + .."entry was <%s/%s>.) This may require a live human to figure out; the " + .."loot in question has not been stored." +local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s%s|r to %s%s|r" local qualnames = { ['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0, ['white'] = 1, ['common'] = 1, @@ -187,7 +196,7 @@ alsolog = false, } -- This looks ugly, but it factors out the load-time decisions from - -- the run-time ones. + -- the run-time ones. Args to [dp]print are concatenated with spaces. if tekdebug then function dprint (t,...) if DEBUG_PRINT and debug[t] then @@ -287,14 +296,27 @@ local g_wafer_thin = nil -- for prompting for additional rebroadcasters local g_today = nil -- "today" entry in g_loot local g_boss_signpost = nil +local g_seeing_oldsigs = nil +local g_uniques = nil -- memoization of unique loot events local opts = nil -local pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe = - pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe +-- for speeding up local loads, not because I think _G will change +local _G = _G +local type = _G.type +local select = _G.select +local pairs = _G.pairs +local ipairs = _G.ipairs +local tinsert = _G.table.insert +local tremove = _G.table.remove +local tostring = _G.tostring +local tonumber = _G.tonumber +local wipe = _G.table.wipe + local pprint, tabledump = addon.pprint, flib.tabledump -local CopyTable, GetNumRaidMembers = CopyTable, GetNumRaidMembers +local CopyTable, GetNumRaidMembers = _G.CopyTable, _G.GetNumRaidMembers -- En masse forward decls of symbols defined inside local blocks local _register_bossmod, makedate, create_new_cache, _init, _log +local _history_by_loot_id, _notify_about_remote -- Try to extract numbers from the .toc "Version" and munge them into an -- integral form for comparison. The result doesn't need to be meaningful as @@ -321,6 +343,7 @@ -- Hypertext support, inspired by DBM broadcast pizza timers do local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h" + local strsplit = _G.strsplit -- TEXT will automatically be surrounded by brackets -- COLOR can be item quality code or a hex string @@ -405,18 +428,104 @@ if isdynamic and perbossheroic == 1 then t = t .. "h" end - pprint("instance_tag final", t, r) return name .. "(" .. t .. ")", r end addon.instance_tag = instance_tag -- grumble addon.latest_instance = nil -- spelling reminder, assigned elsewhere +-- Memoizing cache of unique IDs as we generate or search for them. Keys are +-- the uniques, values are the following: +-- 'history' active index into self.history +-- 'history_may' index into player's uniques list, CAN QUICKLY BE OUTDATED +-- and will instantly be wrong after manual insertion +-- 'loot' active index into g_loot +-- with all but the history entry optional. Values of g_uniqes.NOTFOUND +-- indicate a known missing status. Use g_uniques:RESET() to wipe the cache +-- and return to searching mode. +do + local notfound = -1 + local notfound_ret = { history = notfound } + local mt + + -- This can either be its own function or a slightly redundant __index. + local function m_probe_only (t, k) + return rawget(t,k) or notfound_ret + end + + -- Expensive search. + local function m_full_search (t, k) + local L, H, HU, loot + -- Try active loot entries first + for i,e in addon:filtered_loot_iter('loot') do + if k == e.unique then + L,loot = i,e + break + end + end + -- If it's active, try looking through that player's history first. + if L then + local hi,h = addon:get_loot_history (loot.person) + for ui,u in ipairs(h.unique) do + if k == u then + H, HU = hi, ui + break + end + end + else + -- No luck? Ugh, may have been reassigned and we're probing from + -- older data. Search the rest of current realm's history. + for hi,h in ipairs(addon.history) do + for ui,u in ipairs(h.unique) do + if k == u then + H, HU = hi, ui + break + end + end + end + end + local ret = { loot = L, history = H or notfound, history_may = HU } + t[k] = ret + return ret + end + + local function m_setmode (self, mode) + mt.__index = (mode == 'probe') and m_probe_only or + (mode == 'search') and m_full_search or + nil -- maybe error() here? + end + + local function m_reset (self) + wipe(self) + self[''] = notfound_ret -- special case for receiving older broadcast + self.NOTFOUND = notfound + self.RESET = m_reset + self.SEARCH = m_full_search + self.TEST = m_probe_only + self.SETMODE = m_setmode + mt.__index = m_full_search + return self + end + + -- If unique keys ever change into objects instead of strings, change + -- this into a weakly-keyed table. + mt = { __metatable = 'Should be using setmode.' } + + g_uniques = setmetatable (m_reset{}, mt) +end + ------ Expiring caches --[[ -foo = create_new_cache("myfoo",15[,cleanup]) -- ttl -foo:add("blah") -foo:test("blah") -- returns true +cache = create_new_cache ("mycache", 15 [,cleanup]) +cache:add(foo) +cache:test(foo) -- returns true + ....5 seconds pass +cache:add(bar) + ....10 seconds pass +cache:test(foo) -- returns false +cache:test(bar) -- returns true + ....5 seconds pass + ....bar also gone, cleanup() called ]] do local caches = {} @@ -431,7 +540,7 @@ for name,c in pairs(caches) do local fifo = c.fifo local active = #fifo > 0 - while (#fifo > 0) and (now - fifo[1].t > c.ttl) do + while (#fifo > 0) and (now > fifo[1].t) do addon.dprint('cache', name, "cache removing", fifo[1].t, "<", fifo[1].m, ">") tremove(fifo,1) end @@ -444,13 +553,14 @@ if alldone then addon.dprint('cache',"OnLoop FINISHING animation group") cleanup_group:Finish() + _G.collectgarbage() else addon.dprint('cache',"OnLoop done, not yet finished") end end) local function _add (cache, x) - local datum = { t=time(), m=x } + local datum = { t=time()+cache.ttl, m=x } cache.hash[x] = datum tinsert (cache.fifo, datum) if not cleanup_group:IsPlaying() then @@ -490,16 +600,18 @@ function addon:OnInitialize() if self.author_debug then _G.OL = self + _G.g_uniques = g_uniques end - _log = OuroLootSV_log + _log = _G.OuroLootSV_log -- VARIABLES_LOADED has fired by this point; test if we're doing something like -- relogging during a raid and already have collected loot data + local OuroLootSV = _G.OuroLootSV g_restore_p = OuroLootSV ~= nil self.dprint('flow', "oninit sets restore as", g_restore_p) - if OuroLootSV_opts == nil then - OuroLootSV_opts = {} + if _G.OuroLootSV_opts == nil then + _G.OuroLootSV_opts = {} self:ScheduleTimer(function(s) s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON)) virgin = nil @@ -507,7 +619,7 @@ else virgin = nil end - opts = OuroLootSV_opts + opts = _G.OuroLootSV_opts local stored_datarev = opts.datarev or 14 for opt,default in pairs(option_defaults) do if opts[opt] == nil then @@ -557,36 +669,42 @@ if opts.register_slashloot then -- NOTA BENE: do not use /loot in the LoadOn list, ChatTypeInfo gets confused -- maybe try to detect if this command is already in use... - SLASH_ACECONSOLE_OUROLOOT2 = "/loot" + _G.SLASH_ACECONSOLE_OUROLOOT2 = "/loot" end - self.history_all = self.history_all or OuroLootSV_hist or {} - local r = self:load_assert (GetRealmName(), "how the freak does GetRealmName() fail?") + self.history_all = self.history_all or _G.OuroLootSV_hist or {} + local r = self:load_assert (_G.GetRealmName(), "how the freak does GetRealmName() fail?") self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r) self.history = self.history_all[r] local histformat = self.history_all.HISTFORMAT self.history_all.HISTFORMAT = nil -- don't keep this in live data - if (not InCombatLockdown()) and OuroLootSV_hist and - (histformat == nil or histformat < 3) -- restored data but it's older - then - -- Big honkin' loop + if _G.OuroLootSV_hist + and (histformat == nil or histformat < 4) + then -- some big honkin' loops for rname,realm in pairs(self.history_all) do for pk,player in ipairs(realm) do - for lk,loot in ipairs(player) do - if loot.count == "" then - loot.count = nil - end - if not loot.unique then - loot.unique = loot.id .. ' ' .. loot.when + if histformat == nil or histformat < 3 then + for lk,loot in ipairs(player) do + if loot.count == "" then + loot.count = nil + end + if not loot.unique then + loot.unique = loot.id .. ' ' .. loot.when + end end end + -- format 3 to format 4 was a major revamp of per-player data + self:_uplift_history_format(player,rname) end end end + self._uplift_history_format = nil --OuroLootSV_hist = nil -- Handle changes to the stored data format in stages from oldest to newest. + -- bumpers[X] is responsible for updating from X to X+1. + -- (This is turning into a lot of loops over the same table. Consolidate?) if OuroLootSV then local dirty = false local bumpers = {} @@ -634,9 +752,15 @@ -- not actually running the same loop twice... probably... sigh. bumpers[19] = function() + local date = _G.date for i,e in ipairs(OuroLootSV) do - if e.kind == 'loot' and e.history_unique then - e.unique, e.history_unique = e.history_unique, nil + if e.kind == 'loot' then + if e.history_unique then + e.unique, e.history_unique = e.history_unique, nil + end + if e.unique == nil or #e.unique == 0 then + e.unique = e.id .. ' ' .. date("%Y/%m/%d %H:%M",e.stamp) + end end end end @@ -667,12 +791,15 @@ self:RegisterEvent("PLAYER_LOGOUT") self:RegisterEvent("RAID_ROSTER_UPDATE") - -- Cribbed from Talented. I like the way jerry thinks: the first argument - -- can be a format spec for the remainder of the arguments. (The new + -- Cribbed from Talented. I like the way jerry thinks: the first string + -- argument can be a format spec for the remainder of the arguments. -- AceConsole:Printf isn't used because we can't specify a prefix without - -- jumping through ridonkulous hoops.) The part about overriding :Print + -- jumping through ridonkulous hoops. The part about overriding :Print -- with a version using prefix hyperlinks is my fault. -- + -- CFPrint added instead of the usual Print testing of the first arg for + -- frame-ness, which would slow down all printing and only rarely be useful. + -- -- There is no ITEM_QUALITY_LEGENDARY constant. Sigh. do local AC = LibStub("AceConsole-3.0") @@ -684,6 +811,14 @@ return AC:Print (chat_prefix, str, ...) end end + function addon:CFPrint (frame, str, ...) + assert(type(frame)=='table' and frame.AddMessage) + if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then + return AC:Print (frame, chat_prefix, str:format(...)) + else + return AC:Print (frame, chat_prefix, str, ...) + end + end end while opts.keybinding do @@ -699,8 +834,9 @@ btn:SetAttribute("type", "macro") btn:SetAttribute("macrotext", "/ouroloot toggle") if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then - -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when GCBS - -- decides to return neither 1 nor 2 during load, for reasons nobody has ever learned + -- a simple SaveBindings(GetCurrentBindingSet()) occasionally fails when + -- GCBS() decides to return neither 1 nor 2 during load, for reasons nobody + -- has ever learned local c = GetCurrentBindingSet() if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then SaveBindings(c) @@ -758,8 +894,15 @@ end) _G.InterfaceOptions_AddCategory(bliz) + -- Maybe load up g_uniques now? + if opts.precache_history_uniques then + self:_cache_history_uniques() + end + self:_scan_LOD_modules() + self:_set_remote_change_chatframe (opts.chatty_on_remote_changes_frame, --[[silent_p=]]true) + if self.debug.flow then self:Print"is in control-flow debug mode." end end --function addon:OnDisable() end @@ -837,18 +980,18 @@ ------ Event handlers function addon:_clear_SVs() g_loot = {} -- not saved, just fooling PLAYER_LOGOUT tests - OuroLootSV = nil - OuroLootSV_saved = nil - OuroLootSV_opts = nil - OuroLootSV_hist = nil - OuroLootSV_log = nil - ReloadUI() + _G.OuroLootSV = nil + _G.OuroLootSV_saved = nil + _G.OuroLootSV_opts = nil + _G.OuroLootSV_hist = nil + _G.OuroLootSV_log = nil + _G.ReloadUI() end function addon:PLAYER_LOGOUT() self:UnregisterEvent("RAID_ROSTER_UPDATE") self:UnregisterEvent("PLAYER_ENTERING_WORLD") - local worth_saving = #g_loot > 0 or next(g_loot.raiders) + local worth_saving = #g_loot > 0 or _G.next(g_loot.raiders) if not worth_saving then for text in self:registered_textgen_iter() do worth_saving = worth_saving or g_loot.printed[text] > 0 end end @@ -858,9 +1001,9 @@ for i,e in ipairs(g_loot) do e.cols = nil end - OuroLootSV = g_loot + _G.OuroLootSV = g_loot else - OuroLootSV = nil + _G.OuroLootSV = nil end worth_saving = false @@ -875,12 +1018,12 @@ end end end if worth_saving then - OuroLootSV_hist = self.history_all - OuroLootSV_hist.HISTFORMAT = 3 + _G.OuroLootSV_hist = self.history_all + _G.OuroLootSV_hist.HISTFORMAT = 4 else - OuroLootSV_hist = nil + _G.OuroLootSV_hist = nil end - OuroLootSV_log = #OuroLootSV_log > 0 and OuroLootSV_log or nil + _G.OuroLootSV_log = #_G.OuroLootSV_log > 0 and _G.OuroLootSV_log or nil end do @@ -1016,11 +1159,36 @@ end end --- helper for CHAT_MSG_LOOT handler +--[=[ CHAT_MSG_LOOT handler and its helpers. +Situations for "unique tag" generation, given N people seeing local loot +events, M people seeing remote rebroadcasts, and player Z adding manually: + ++ Local tracking: All LOCALs should see the same itemstring, thus the same + unique ID stripped out of field #9. LOCALn includes this in the broadcast + to REMOTEm. Tag is a large number, meaningless for clients and players. + ++ Local broadcasting, remote tracking: same as local tracking. Possibly + some weirdness if all local versions are significantly older than the remote + versions; in this case each REMOTEn will generate their own tags of the form + itemID+formatted_date, which will not be "unique" for the next 60 seconds. + As long as at least one LOCALn is recent enough to strip and broadcast a + proper ID, multiple items of the same visible name will not be "lost". + ++ Z manually inserts a loot entry: Z generates a tag, preserved locally. + If Z rebrodcasts that entry, all REMOTEs will see it. Tag is of the form + "n" followed by a random number. +]=] do + local counter, _do_loot + do + local count = 0 + function counter() count = count + 1; return count; end + end + local function maybe_trash_kill_entry() -- this is set on various boss interactions, so we've got a kill/wipe - -- entry already + -- entry already -- XXX maybe clear it after a delay, so that loot + -- from trash after a boss isn't grouped with that boss? if addon.latest_instance then return end --addon.latest_instance = instance_tag() local ss, max, inst = addon:snapshot_raid() @@ -1032,8 +1200,40 @@ }) end + local random = _G.math.random + local function many_uniques_handle_it (u, check_p) + if u and check_p then + -- Check and alert for an existing value. + u = tostring(u) + if g_uniques[u].history ~= g_uniques.NOTFOUND then + return nil, u + end + addon.dprint('loot',"verified unique tag ("..u..")") + else + -- Need to *find* an unused value. For now use a range of + -- J*10^4 where J is Jenny's Constant. Thank you, xkcd.com/1047. + repeat + u = 'n' .. random(8675309) + until g_uniques:TEST(u).history ~= g_uniques.NOTFOUND + addon.dprint('loot',"created unique tag", u) + end + return true, u + end + -- Recent loot cache local candidates = {} + local sigmap = {} +_G.sigmap = sigmap + local function preempt_older_signature (oldersig, newersig) + local origin = candidates[oldersig] and candidates[oldersig].from + if origin and g_seeing_oldsigs[origin] then + -- replace entry from older client with this newer one + candidates[oldersig] = nil + addon.dprint('cache', "preempting signature <", oldersig, "> from", origin) + end + return false + end + local function prefer_local_loots (cache) -- The function name is a bit of a misnomer, as local entries overwrite -- remote entries as the candidate table is populated. This routine is @@ -1044,10 +1244,10 @@ addon.dprint('loot', "processing candidate entry", i, sig) local loot = candidates[sig] if loot then - addon.dprint('loot', i, "was found") maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry candidates[sig] = nil local looti = addon._addLootEntry(loot) + addon.dprint('loot', "entry", i, "was found, added at index", looti) if (loot.disposition ~= 'shard') and (loot.disposition ~= 'gvault') and (not addon.history_suppress) @@ -1061,88 +1261,130 @@ addon:redisplay() end wipe(candidates) + wipe(sigmap) end - addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) + local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots) - local GetItemInfo, GetItemIcon, random = GetItemInfo, GetItemIcon, math.random + local strsplit, GetItemInfo, GetItemIcon, UnitClass = + _G.strsplit, _G.GetItemInfo, _G.GetItemIcon, _G.UnitClass -- 'from' only present if this is triggered by a broadcast - function addon:_do_loot (local_override, recipient, unique, itemid, count, from, extratext) + function _do_loot (self, local_override, recipient, unique, itemid, count, from, extratext) + local prefix = "_do_loot[" .. counter() .. "]" local itexture = GetItemIcon(itemid) local iname, ilink, iquality = GetItemInfo(itemid) - local i + local cache_miss if (not iname) or (not itexture) then - i = true + cache_miss = true iname, ilink, iquality, itexture = UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]] end - self.dprint('loot',">>_do_loot, R:", recipient, "U:", unique, "I:", + self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality) itemid = tonumber(ilink:match("item:(%d+)") or 0) - unique = tostring(unique or random(8675309)) -- also, xkcd.com/1047 - -- This is only a 'while' to make jumping out of it easy and still do cleanup below. - while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do + + -- This is only a 'while' to make jumping out of it easy. + local i, unique_okay, ret1, ret2 + while local_override + or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) + do + unique_okay, unique = many_uniques_handle_it (unique, not local_override) + if not unique_okay then + i = g_uniques[unique] + local err = unique_collision:format (ERROR_CAPS, iname, unique, + tostring(from), tostring(i.loot), tostring(i.history)) + self:Print(err) + _G.PlaySound("igQuestFailed", "master") + -- Make sure this is logged one way or another + ;(self.debug.loot and self.dprint or pprint)('loot', "COLLISION", prefix, err); + ret1, ret2 = nil, err + break + end + if (self.rebroadcast and (not from)) and not local_override then self:vbroadcast('loot', recipient, unique, itemid, count) end if (not self.enabled) and (not local_override) then break end - local signature = unique .. recipient .. iname .. (count or "") - if from and self.recent_loot:test(signature) then - self.dprint('cache', "remote loot <",signature,"> already in cache, skipping") + + local oldersig = recipient .. iname .. (count or "") + local signature, seenit + if #unique > 0 then + -- newer case + signature = unique .. oldersig + sigmap[oldersig] = signature + seenit = from and (recent_loot:test(signature) + -- The following clause is what handles older 'casts arriving + -- earlier. All this is tested inside-out to maximize short + -- circuit avaluation. + or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature))) else - -- There is some redundancy in all this, in the interests of ease-of-coding - i = { - kind = 'loot', - person = recipient, - person_class= select(2,UnitClass(recipient)), - cache_miss = i and true or nil, - quality = iquality, - itemname = iname, - id = itemid, - itemlink = ilink, - itexture = itexture, - unique = unique, - count = (count and count ~= "") and count or nil, - bcast_from = from, - extratext = extratext, - variant = self:is_variant_item(ilink), - } - if opts.itemvault[itemid] then - i.disposition = 'gvault' - elseif recipient == self.sharder then - i.disposition = 'shard' + -- older case, only remote + assert(from) + signature = sigmap[oldersig] or oldersig + seenit = recent_loot:test(signature) + end + + if seenit then + self.dprint('cache', "remote", prefix, "<", signature, + "> already in cache, skipping from", from) + break + end + + -- There is some redundancy in all this, in the interests of ease-of-coding + i = { + kind = 'loot', + person = recipient, + person_class= select(2,UnitClass(recipient)), + cache_miss = cache_miss, + quality = iquality, + itemname = iname, + id = itemid, + itemlink = ilink, + itexture = itexture, + unique = unique, + count = (count and count ~= "") and count or nil, + bcast_from = from, + extratext = extratext, + variant = self:is_variant_item(ilink), + } + if opts.itemvault[itemid] then + i.disposition = 'gvault' + elseif recipient == self.sharder then + i.disposition = 'shard' + end + if local_override then + -- player is adding loot by hand, don't wait for network cache timeouts + -- keep this sync'd with prefer_local_loots above + if i.extratext == 'shard' + or i.extratext == 'gvault' + or i.extratext == 'offspec' + then + i.disposition = i.extratext end - if local_override then - -- player is adding loot by hand, don't wait for network cache timeouts - -- keep this sync'd with prefer_local_loots above - if i.extratext == 'shard' - or i.extratext == 'gvault' - or i.extratext == 'offspec' - then - i.disposition = i.extratext - end - local looti = self._addLootEntry(i) - if (i.disposition ~= 'shard') - and (i.disposition ~= 'gvault') - and (not self.history_suppress) - then - self:_addHistoryEntry(looti) - end - i = looti -- return value mostly for gui's manual entry - else - self.recent_loot:add(signature) - candidates[signature] = i - tinsert (candidates, signature) - self.dprint('cache', "loot <",signature,"> added to cache as candidate", #candidates) + local looti = self._addLootEntry(i) + if (i.disposition ~= 'shard') + and (i.disposition ~= 'gvault') + and (not self.history_suppress) + then + self:_addHistoryEntry(looti) end + ret1 = looti -- return value mostly for gui's manual entry + else + recent_loot:add(signature) + candidates[signature] = i + tinsert (candidates, signature) + self.dprint('cache', prefix, "<", signature, + "> added to cache as candidate", #candidates) end break end - self.dprint('loot',"<<_do_loot out") - return i + self.dprint('loot',"<<"..prefix, "out") + return ret1, ret2 end + -- Returns the index of the resulting new loot entry, or nil after + -- displaying any errors. function addon:CHAT_MSG_LOOT (event, ...) if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end @@ -1173,7 +1415,7 @@ self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", count is", count) - if not itemstring then return end -- "So-and-So selected Greed", etc, not actual looting + if not itemstring then return end -- "PlayerX selected Greed", etc, not looting -- Name might be colorized, remove the highlighting if person then @@ -1187,14 +1429,14 @@ _,id,_,_,_,_,_,_,unique = strsplit (":", itemstring) if unique == 0 then unique = nil end - return self:_do_loot (false, person, unique, id, count) + return _do_loot (self, false, person, unique, id, count) elseif event == "broadcast" then - return self:_do_loot(false, ...) + return _do_loot(self, false, ...) elseif event == "manual" then local r,i,n = ... - return self:_do_loot(true, r, --[[unique=]]nil, i, + return _do_loot(self, true, r, --[[unique=]]nil, i, --[[count=]]nil, --[[from=]]nil, n) end end @@ -1332,6 +1574,7 @@ end self.rebroadcast = true -- hardcode to true; this used to be more complicated self.enabled = not opt_bcast_only + g_seeing_oldsigs = nil if opt_threshold then self:SetThreshold (opt_threshold, --[[quiet_p=]]true) end @@ -1364,11 +1607,11 @@ self.display:Hide() end g_restore_p = nil - OuroLootSV = nil + _G.OuroLootSV = nil self:_reset_timestamps() if verbose_p then - if (OuroLootSV_saved and #OuroLootSV_saved>0) then - self:Print("Current loot data cleared, %d saved sets remaining.", #OuroLootSV_saved) + if (_G.OuroLootSV_saved and #_G.OuroLootSV_saved>0) then + self:Print("Current loot data cleared, %d saved sets remaining.", #_G.OuroLootSV_saved) else self:Print("Current loot data cleared.") end @@ -1391,7 +1634,7 @@ -- 5) at logout, nothing new has been entered in the table being saved local date = _G.date function addon:log_with_timestamp (msg) - tinsert (_log, date('%m:%d %H:%M:%S ')..msg) + tinsert (_log, date('%m/%d %H:%M:%S ')..msg) end end @@ -1412,6 +1655,63 @@ end end +-- Routines for printing changes made by remote users. +do + local remote_change_chatframe + + function addon:_set_remote_change_chatframe (arg, silent_p) + local frame + if type(arg) == 'number' then + arg = _G.math.min (arg, _G.NUM_CHAT_WINDOWS) + frame = _G['ChatFrame'..arg] + elseif type(arg) == 'string' then + frame = _G[arg] + end + if type(frame) == 'table' and type(frame.AddMessage) == 'function' then + remote_change_chatframe = frame + if not silent_p then + local msg = "Now printing to chat frame " .. arg + if frame.GetName then + msg = msg .. " (" .. tostring(frame:GetName()) .. ")" + end + self:Print(msg) + if frame ~= _G.DEFAULT_CHAT_FRAME then + self:CFPrint (frame, msg) + end + end + return frame + else + self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg) + end + end + + function _notify_about_remote (sender, index, from_whom, olddisp) + local e = g_loot[index] + local from_color, from_text, to_color, to_text + if from_whom then + -- FIXME need to return previous name/class from reassign_loot + + else + if olddisp then + from_text = addon.disposition_colors[olddisp].text + else + olddisp = "normal" + from_text = "normal" + end + from_color = addon.disposition_colors[olddisp].hex + if e.disposition then + to_text = addon.disposition_colors[e.disposition].text + else + to_text = "normal" + end + to_color = addon.disposition_colors[e.disposition or "normal"].hex + end + + addon:CFPrint (remote_change_chatframe, remote_chatty, sender, index, + e.itemlink, from_color, from_text, to_color, to_text) + end +end + -- Adds indices to traverse the tables in a nice sorted order. do local byindex, temp = {}, {} @@ -1419,7 +1719,7 @@ for k in pairs(src) do temp[#temp+1] = k end - table.sort(temp) + _G.table.sort(temp) wipe(dest) for i = 1, #temp do dest[i] = src[temp[i]] @@ -1491,7 +1791,7 @@ elseif not closest_time then fencepost = closest_boss else - fencepost = math.min(closest_time,closest_boss) + fencepost = _G.math.min(closest_time,closest_boss) end end return fencepost @@ -1538,7 +1838,7 @@ self.loot_clean = nil self.hist_clean = nil if g_restore_p then - g_loot = OuroLootSV + g_loot = _G.OuroLootSV self.popped = #g_loot > 0 self.dprint('flow', "restoring", #g_loot, "entries") self:ScheduleTimer("Activate", 12, opts.threshold) @@ -1554,15 +1854,16 @@ opts.threshold = nil if g_restore_p then - self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values + self:zero_printed_fenceposts() -- g_loot.printed.* = previous/safe values else - self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 + self:zero_printed_fenceposts(0) -- g_loot.printed.* = 0 end if possible_st then possible_st:SetData(g_loot) end - self.status_text = ("%s communicating as ident %s commrev %s"):format(self.revision,self.ident,self.commrev) + self.status_text = ("%s communicating as ident %s commrev %s"): + format (self.revision, self.ident, self.commrev) self:RegisterComm(self.ident) self:RegisterComm(self.identTg, "OnCommReceivedNocache") @@ -1577,13 +1878,13 @@ local ss = CopyTable(g_loot.raiders) local instance,maxsize = instance_tag() if only_inraid_p then - for name,info in next, ss do + for name,info in _G.next, ss do if info.online == 3 then ss[name] = nil end end end - return ss, maxsize, instance, time() + return ss, maxsize, instance, _G.time() end end @@ -1631,7 +1932,7 @@ end wipe(candidates) end - addon.recent_boss = create_new_cache ('boss', 10, fixup_durations) + local recent_boss = create_new_cache ('boss', 10, fixup_durations) -- Similar to _do_loot, but duration+ parms only present when locally generated. local function _do_boss (self, reason, bossname, intag, maxsize, duration) @@ -1646,10 +1947,10 @@ bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname local not_from_local = duration == nil local signature = bossname .. reason - if not_from_local and self.recent_boss:test(signature) then + if not_from_local and recent_boss:test(signature) then self.dprint('cache', "remote boss <",signature,"> already in cache, skipping") else - self.recent_boss:add(signature) + recent_boss:add(signature) g_boss_signpost = #g_loot + 1 self.dprint('loot', "added boss signpost", g_boss_signpost) -- Possible scenarios: (1) we don't see a boss event at all (e.g., we're @@ -1763,6 +2064,8 @@ -- Adding entries to the loot record, and tracking the corresponding timestamp. do + local rawget, setmetatable = _G.rawget, _G.setmetatable + -- This shouldn't be required. /sadface local loot_entry_mt = { __index = function (e,key) @@ -1846,9 +2149,9 @@ if not done_todays_date then do_todays_date() end - local h, m = GetGameTime() + local h, m = _G.GetGameTime() --local localuptime = math.floor(GetTime()) - local time_t = time() + local time_t = _G.time() e.hour = h e.minute = m e.stamp = time_t --localuptime @@ -1911,26 +2214,38 @@ local numfound = 0 local borkedpat = '^'..UNKNOWN..': (%S+)' - for i,e in self:filtered_loot_iter('loot') do - if e.cache_miss then - local borked_id = e.itemname:match(borkedpat) - if borked_id then - numfound = numfound + 1 - -- Best to use the safest and most flexible API here, which is GII and - -- its assload of return values. - local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) - if iname then - self:Print(" Entry %d patched up with %s.", i, ilink) - e.quality = iquality - e.itemname = iname - e.id = tonumber(ilink:match("item:(%d+)")) - e.itemlink = ilink - e.itexture = itexture - e.cache_miss = nil + -- 'while true' so that we can use (inner) break as (outer) continue + for i,e in self:filtered_loot_iter('loot') do while true do + if not e.cache_miss then break end + local borked_id = e.itemname:match(borkedpat) + if not borked_id then break end + numfound = numfound + 1 + -- Best to use the safest and most flexible API here, which is GII and + -- its assload of return values. + local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id) + if iname then + local msg = [[ Entry %d patched up with %s.]] + e.quality = iquality + e.itemname = iname + e.id = tonumber(ilink:match("item:(%d+)")) + e.itemlink = ilink + e.itexture = itexture + e.cache_miss = nil + if e.unique then + local gu = g_uniques[e.unique] + local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache") + if gu.loot ~= i then -- is this an actual problem? + pprint('loot', ("Unique value '%s' had iterator value %d but g_uniques index %s."):format(e.unique,i,tostring(gu.loot))) + end + if player_i then + player_h.id[e.unique] = e.id + msg = [[ Entry %d (and history) patched up with %s.]] end end + self:Print(msg, i, ilink) end - end + break + end end self:Print("...finished. Found %d |4entry:entries; with weird data.", numfound) end @@ -1938,9 +2253,9 @@ ------ Saved texts function addon:check_saved_table(silent_p) - local s = OuroLootSV_saved + local s = _G.OuroLootSV_saved if s and (#s > 0) then return s end - OuroLootSV_saved = nil + _G.OuroLootSV_saved = nil if not silent_p then self:Print("There are no saved loot texts.") end end @@ -1952,8 +2267,8 @@ end function addon:save_saveas(name) - OuroLootSV_saved = OuroLootSV_saved or {} - local SV = OuroLootSV_saved + _G.OuroLootSV_saved = _G.OuroLootSV_saved or {} + local SV = _G.OuroLootSV_saved local n = #SV + 1 local save = { name = name, @@ -2005,8 +2320,13 @@ -- } -- [1] = { -- ["name"] = "Farmbuyer", --- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot --- [2] = { ......., [count = "x3"] } -- previous loot +-- ["person_class"] = "PRIEST", -- may be missing, used in display only +-- -- sorted array: +-- ["unique"] = { most_recent_tag, previous_tag, .... }, +-- -- these are indexed by unique tags, and 'count' may be missing: +-- ["when"] = { ["tag"] = "formatted timestamp for displaying loot" }, +-- ["id"] = { ["tag"] = 11111 }, +-- ["count"] = { ["tag"] = "x3", .... }, -- }, -- [2] = { -- ["name"] = "OtherPlayer", @@ -2015,9 +2335,94 @@ -- }, -- ["OtherRealm"] = ...... -- } +-- +-- Up through 2.81.4 (specifically through rev 95), an individual player's +-- table looked like this: +-- ["name"] = "Farmbuyer", +-- [1] = { id = nnnnn, when = "formatted timestamp for displaying" } -- most recent loot +-- [2] = { ......., [count = "x3"] } -- previous loot +-- which was much easier to manipulate, but had a ton of memory overhead. do - local tsort = table.sort - local comp = function(L,R) return L.when > R.when end + -- Sorts a player's history from newest to oldest, according to the + -- formatted timestamp. This is expensive, and destructive for P.unique. + local function sort_player (p) + local new_uniques, uniques_bywhen, when_array = {}, {}, {} + for u,tstamp in pairs(p.when) do + uniques_bywhen[tstamp] = u + when_array[#when_array+1] = tstamp + end + _G.table.sort(when_array) + for i,tstamp in ipairs(when_array) do + new_uniques[i] = uniques_bywhen[tstamp] + end + p.unique = new_uniques + end + + -- Possibly called during login. Cleared when no longer needed. + -- Rewrites a PLAYER table from format 3 to format 4. + function addon:_uplift_history_format (player, realmname) + local unique, when, id, count = {}, {}, {}, {} + local name = player.name + + for i,h in ipairs(player) do + local U = h.unique + unique[i] = U + when[U] = h.when + id[U] = h.id + count[U] = h.count + end + + wipe(player) + player.name = name + player.id, player.when, player.unique, player.count = + id, when, unique, count + end + function addon:_cache_history_uniques() + UpdateAddOnMemoryUsage() + local before = GetAddOnMemoryUsage(nametag) + local trouble + local count = 0 + for hi,player in ipairs(self.history) do + for ui,u in ipairs(player.unique) do + g_uniques[u] = { history = hi, history_may = ui } + count = count + 1 + end + end + for i,e in self:filtered_loot_iter('loot') do + if e.unique and e.unique ~= "" then + local hmmm = _G.rawget(g_uniques,e.unique) + if hmmm then + hmmm.loot = i --;print("Active loot", i, "found with tag", e.unique) + elseif e.disposition == 'shard' or e.disposition == 'gvault' then + g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } + count = count + 1 + --print("Active loot", i, "INSERTED with tag", e.unique, "as", e.disposition) + else + hmmm = "wonked data ("..i.."/"..tostring(e.unique)..") in precache loop!" + pprint(hmmm) + -- try to simply fix up errors as we go + g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND } + trouble = true + end + else + trouble = true + pprint('loot', "ERROR precache loop found bad unique tag!", + i, "tag", tostring(e.unique), "from?", tostring(e.bcast_from)) + end + end + UpdateAddOnMemoryUsage() + local after = GetAddOnMemoryUsage(nametag) + self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.", + self.history.realm, (after-before)/1024, count) + if trouble then + self:Print("Note that there were inconsistencies in the data;", + "you should consider submitting a bug report (including your", + "SavedVariables file), and regenerating or preening this", + "realm's loot history.") + end + g_uniques:SETMODE('probe') + self._cache_history_uniques = nil + end -- Builds the map of names to array indices, using passed table or -- self.history, and stores the result into its 'byname' field. Also @@ -2028,6 +2433,7 @@ for i = 1, #hist do m[hist[i].name] = i end + -- why yes, I *did* spend many years as a YP/NIS admin, how did you know? hist.byname = m end @@ -2059,28 +2465,32 @@ i = self.history.byname[name] if not i then i = #self.history + 1 - self.history[i] = { name=name } + self.history[i] = { name=name, id={}, when={}, unique={}, count={} } self.history.byname[name] = i end return i, self.history[i] end + -- Prepends data from the loot entry at LOOTINDEX to be the new most + -- recent history entry for that player. function addon:_addHistoryEntry (lootindex) local e = g_loot[lootindex] if e.kind ~= 'loot' then return end local i,h = self:get_loot_history(e.person) + local when = self:format_timestamp (g_today, e) + -- If any of these change, update the end of history_handle_disposition. - local n = { - id = e.id, - when = self:format_timestamp (g_today, e), - count = e.count, - } - tinsert (h, 1, n) if (not e.unique) or (#e.unique==0) then - e.unique = n.id .. ' ' .. n.when + e.unique = e.id .. ' ' .. when end - n.unique = e.unique + local U = e.unique + tinsert (h.unique, 1, U) + h.when[U] = when + h.id[U] = e.id + h.count[U] = e.count + + g_uniques[U] = { loot = lootindex, history = i } end -- Create new history table based on current loot. @@ -2088,6 +2498,7 @@ local r = assert(realmname) self.history_all[r] = self:_prep_new_history_category (nil, r) self.history = self.history_all[r] + g_uniques:RESET() local g_today_real = g_today for i,e in ipairs(g_loot) do @@ -2102,81 +2513,105 @@ -- safety measure: resort players' tables based on formatted timestamp for i,h in ipairs(self.history) do - tsort (h, comp) + sort_player(h) end end -- Clears all but latest entry for each player. function addon:preen_history (realmname) local r = assert(realmname) + g_uniques:RESET() for i,h in ipairs(self.history) do - tsort (h, comp) - while #h > 1 do - tremove (h) - end + -- This is going to do horrible things to memory. The subtables + -- after this step would be large and sparse, with no good way + -- of shrinking the allocation... + sort_player(h) + -- ...so it's better in the long run to discard them. + local U = h.unique[1] + h.unique = { U } + h.id = { [U] = h.id[U] } + h.when = { [U] = h.when[U] } + h.count = { [U] = h.count[U] } end end - -- Given an entry in a g_loot table, looks up the corresponding history - -- entry. Returns the player's index and history table (as in get_loot_history) - -- and the index into that table of the loot entry. On failure, returns nil - -- and an error message ready to be formatted with the loot's name/itemlink. - function addon:_history_by_loot_id (loot, operation_text) - -- Using assert() here would be concatenating error strings that probably - -- wouldn't be used. Do more verbose testing instead. - if type(loot) ~= 'table' then - error("trying to "..operation_text.." nonexistant entry") - end - if loot.kind ~= 'loot' then - error("trying to "..operation_text.." something that isn't loot") + -- Given a unique tag OR an entry in a g_loot table, looks up the + -- corresponding history entry. Returns the player's index and history + -- table (as in get_loot_history) and the index into that table of the + -- loot entry. On failure, returns nil and an error message ready to be + -- formatted with the loot's name/itemlink. + function _history_by_loot_id (needle, operation_text) + local errtxt + if type(needle) == 'string' then + -- unique tag + elseif type(needle) == 'table' then + if needle.kind ~= 'loot' then + error("trying to "..operation_text.." something that isn't loot") + end + needle = needle.unique + if not needle then + return nil, --[[errtxt=]]"Entry for %s is missing a history tag!" + end + else + error("'"..tostring(needle).."' is neither unique string nor loot entry!") end - local player = loot.person - local tag = loot.unique - local errtxt - local player_i, player_h, hist_i + local player_i, player_h + local cache = g_uniques[needle] - if not tag then - errtxt = "Entry for %s is missing a history tag!" + if cache.history == g_uniques.NOTFOUND then + -- 1) loot an item, 2) clear old history, 3) reassign from current loot + -- Bah. Anybody that tricky is already recoding the tables directly anyhow. + errtxt = "There is no record of %s ever having been assigned!" else - player_i,player_h = self:get_loot_history(player) - for i,h in ipairs(player_h) do - if h.unique == tag then - hist_i = i - break + player_i = cache.history + player_h = addon.history[player_i] + if cache.history_may + and needle == player_h.unique[cache.history_may] + then + return player_i, player_h, cache.history_may + end + for i,u in ipairs(player_h.unique) do + if needle == u then + cache.history_may = i -- might help, might not + return player_i, player_h, i end end - if not hist_i then - -- 1) loot an item, 2) clear old history, 3) reassign from current loot - -- Bah. Anybody that tricky is already recoding the tables directly anyhow. - errtxt = "There is no record of %s ever having been assigned!" - end end - if errtxt then - return nil, errtxt + if not errtxt then + -- The cache finder got a hit, but now it's gone? WTF? + 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." end - return player_i, player_h, hist_i + return nil, errtxt end function addon:reassign_loot (index, to_name) assert(type(to_name)=='string' and to_name:len()>0) local e = g_loot[index] - local from_i, from_h, hist_i = self:_history_by_loot_id (e, "reassign") + local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign") local from_name = e.person local to_i,to_h = self:get_loot_history(to_name) - if not from_i then - -- from_h is the formatted error text + if not from_i then -- from_h here is the formatted error text self:Print(from_h .. " Loot will be reassigned, but history will NOT be updated.", e.itemlink) else - local hist_h = tremove (from_h, hist_i) - tinsert (to_h, 1, hist_h) - tsort (from_h, comp) - tsort (to_h, comp) + local U = tremove (from_h.unique, hist_i) + -- The loot master giveth... + to_h.unique[#to_h.unique+1] = U + to_h.when[U] = from_h.when[U] + to_h.id[U] = from_h.id[U] + to_h.count[U] = from_h.count[U] + sort_player(to_h) + -- ...and the loot master taketh away. + from_h.when[U] = nil + from_h.id[U] = nil + from_h.count[U] = nil + -- Blessed be the lookup cache of the loot master. + g_uniques[U] = { loot = index, history = to_i } end e.person = to_name - e.person_class = select(2,UnitClass(to_name)) + e.person_class = select(2,_G.UnitClass(to_name)) self.hist_clean = nil self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name) @@ -2189,15 +2624,18 @@ local e = opt_e or g_loot[lootindex] if e.kind ~= 'loot' then return end - local from_i, from_h, hist_i = self:_history_by_loot_id (e, "delete") + local from_i, from_h, hist_i = _history_by_loot_id (e, "delete") if not from_i then -- from_h is the formatted error text self:Print(from_h .. " Loot will be deleted, but history will NOT be updated.", e.itemlink) return end - --[[local hist_h = ]]tremove (from_h, hist_i) - tsort (from_h, comp) + local hist_u = tremove (from_h.unique, hist_i) + from_h.when[hist_u] = nil + from_h.id[hist_u] = nil + from_h.count[hist_u] = nil + g_uniques[hist_u] = nil self.hist_clean = nil self:Print("Removed history entry %d/%s from '%s'.", lootindex, e.itemlink, e.person) @@ -2205,7 +2643,7 @@ -- Any extra work for the "Mark as <x>" dropdown actions. The -- corresponding <x> will already have been assigned in the loot entry. - local deleted_cache = {} --setmetatable({}, {__mode='k'}) + local deleted_cache = {} function addon:history_handle_disposition (index, olddisp) local e = g_loot[index] -- Standard disposition has a nil entry, but that's tedious in debug @@ -2218,15 +2656,23 @@ local name = e.person if (newdisp == 'shard' or newdisp == 'gvault') then - local name_i, name_h, hist_i = self:_history_by_loot_id (e, "mark") - -- remove history entry + local name_i, name_h, hist_i = _history_by_loot_id (e, "mark") + -- remove history entry if it exists if hist_i then - local hist_h = tremove (name_h, hist_i) - deleted_cache[e.unique] = hist_h + local c = flib.new() + local hist_u = tremove (name_h.unique, hist_i) + c.when = name_h.when[hist_u] + c.id = name_h.id[hist_u] + c.count = name_h.count[hist_u] + deleted_cache[hist_u] = c + name_h.when[hist_u] = nil + name_h.id[hist_u] = nil + name_h.count[hist_u] = nil self.hist_clean = nil elseif (olddisp == 'shard' or olddisp == 'gvault') then -- Sharding a vault item, or giving the auto-sharder something to bank, -- etc, wouldn't necessarily have had a history entry to begin with. + -- So this isn't treated as an error. else self:Print(name_h .. " Loot has been marked, but history will NOT be updated.", e.itemlink) end @@ -2242,32 +2688,97 @@ -- but that would duplicate a lot of effort. To start with, check the -- cache of stuff we've already deleted; if it's not there then just do -- the same steps as _addHistoryEntry. - local entry - if e.unique and deleted_cache[e.unique] then - entry = deleted_cache[e.unique] - deleted_cache[e.unique] = nil + -- FIXME The deleted cache isn't nearly as useful now with the new data structures. + local when + if (not e.unique) or (#e.unique==0) then + when = g_today and self:format_timestamp (g_today, e) or date("%Y/%m/%d %H:%M",e.stamp) + e.unique = e.id .. ' ' .. when end - local when = g_today and self:format_timestamp (g_today, e) or tostring(e.stamp) - entry = entry or { - id = e.id, - when = when, - count = e.count, - } - tinsert (name_h, 1, entry) - if (not e.unique) or (#e.unique==0) then - e.unique = entry.id .. ' ' .. entry.when - end - entry.unique = e.unique + local U = e.unique + local c = deleted_cache[U] + deleted_cache[U] = nil + name_h.unique[#name_h.unique+1] = U + name_h.when[U] = c and c.when or when or date("%Y/%m/%d %H:%M",e.stamp) + name_h.id[U] = e.id -- c.id + name_h.count[U] = c and c.count or e.count + sort_player(name_h) + g_uniques[U] = { loot = index, history = name_i } self.hist_clean = nil + + if c then flib.del(c) end + return end end + + -- This is not entirely "history" but not completely anything else either. + -- Handles the primary "Mark as <x>" action. Arguments depend on who's + -- calling it: + -- "local", row_index, new_disposition + -- "remote", sender, unique_id, item_id, old_disposition, new_disposition + -- In the local case, must also broadcast a trigger. In the remote case, + -- must figure out the corresponding loot entry (if it exists). In both + -- cases, must update history appropriately. Returns nil if anything odd + -- happens (not necessarily an error!); returns the affected loot index + -- on success. + function addon:loot_mark_disposition (how, ...) + -- This must all be filled out in all cases: + local e, index, olddisp, newdisp, unique, id + -- Only set in remote case: + local sender + + if how == "local" then + index, newdisp = ... + index = assert(tonumber(index)) + e = g_loot[index] + id = e.id + unique = e.unique -- can potentially still be nil at this step + olddisp = e.disposition + + elseif how == "remote" then + sender, unique, id, olddisp, newdisp = ... + local cache = g_uniques[unique] + if cache.loot then + index = tonumber(cache.loot) + e = g_loot[index] + end + + else + return -- silently ignore newer cases from newer clients + end + + if self.debug.loot then + local m = ("Re-mark index %d(pre-unique %s) with id %d from '%s' to '%s'."): + format(index, unique, id, tostring(olddisp), tostring(newdisp)) + self.dprint('loot', m) + if sender == my_name then + self.dprint('loot',"(Returning early from double self-mark.)") + return index + end + end + + if not e then + -- say something? + return + end + + e.disposition = newdisp + e.bcast_from = nil -- I actually don't remember now why this gets cleared... + e.extratext = nil + self:history_handle_disposition (index, olddisp) + -- A unique tag has been set by this point. + if how == "local" then + unique = assert(e.unique) + self:vbroadcast('mark', unique, id, olddisp, newdisp) + end + return index + end end ------ Player communication do - local select, tconcat, strsplit = select, table.concat, strsplit + local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack --[[ old way: repeated string concatenations, BAD new way: new table on every call, BAD local msg = ... @@ -2277,10 +2788,12 @@ return msg ]] local function assemble(t,...) - if select('#',...) > 0 then + local n = select('#',...) + if n > 0 then local msg = {t,...} -- tconcat requires strings, but T is known to be one already - for i = 2, #msg do + -- can't use #msg since there might be holes + for i = 2, n+1 do msg[i] = tostring(msg[i] or "") end return tconcat (msg, '\a') @@ -2331,16 +2844,31 @@ addon:_check_revision (revlarge) end + OCR_funcs['17mark'] = function (sender, _, unique, item, old, new) + addon.dprint('comm', "DOTmark/17, sender", sender, "unique", unique, + "item", item, "from old", old, "to new", new) + local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new) + --if not addon.enabled then return end -- hmm + if index and opts.chatty_on_remote_changes then + _notify_about_remote (sender, index, --[[from_whom=]]nil, old) + end + end + OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext) addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count) if not addon.enabled then return end adduser (sender, nil, true) + -- Empty unique string will pass through all of the loot handling, + -- and then be rewritten by the history routine (into older string + -- of ID+date). + g_seeing_oldsigs = g_seeing_oldsigs or {} + g_seeing_oldsigs[sender] = true addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext) end OCR_funcs.loot = OCR_funcs['16loot'] -- old unversioned stuff goes to 16 OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext) - addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, - "unique", unique, "item", item, "count", count) + addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip, + "unique", unique, "item", item, "count", count, "extratext", extratext) if not addon.enabled then return end adduser (sender, nil, true) addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext) @@ -2354,7 +2882,7 @@ addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0) end OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize) - addon.dprint('comm', "DOTboss16, sender", sender, "reason", reason, + addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason, "name", bossname, "it", instancetag, "size", maxsize) if not addon.enabled then return end adduser (sender, nil, true) @@ -2385,14 +2913,25 @@ if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end end - -- Incoming message dispatcher + -- Incoming message disassembler and dispatcher. The static weak table + -- is not my favorite approach to handling ellipses, but it lets me loop + -- through potential nils easily without creating a ton of garbage. + local OCR_data = setmetatable({}, {__mode='v'}) local function dotdotdot (sender, tag, ...) local f = OCR_funcs[tag] - addon.dprint('comm', ":... processing",tag,"from",sender) - if f then return f(sender,tag,...) end - addon.dprint('comm', "unknown comm message",tag",from", sender) + if f then + --wipe(OCR_data) costs more than its worth here + local n = select('#',...) + for i = 1, n do + local d = select(i,...) + OCR_data[i] = (d ~= "") and d or nil + end + addon.dprint('comm', ":... processing", tag, "from", sender, "with arg count", n) + return f(sender,tag,unpack(OCR_data,1,n)) + end + addon.dprint('comm', "unknown comm message", tag, "from", sender) end - -- Recent message cache + -- Recent message cache (this can be accessed via advanced options panel) addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl) function addon:OnCommReceived (prefix, msg, distribution, sender)