diff core.lua @ 25:cb9635999171

- Reassigning loot, and marking loot as disenchanted/vault, will update the history entries also. - Tooltip help on dropdown menus appears at mouse now, instead of depending on Blizzard's "Beginner Tooltips" setting. - Forum BBcode output includes an option for MMO-Champion/Wowstead formatting. - Smarter cleanup functions for expiring caches. - Properly prefer locally-generated loot events, even when other users can see and rebroadcast the events back to you faster than you see them.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Wed, 05 Oct 2011 02:14:07 +0000
parents 61d932f0e8f2
children 68d7b903ee17
line wrap: on
line diff
--- a/core.lua	Wed Sep 21 06:21:48 2011 +0000
+++ b/core.lua	Wed Oct 05 02:14:07 2011 +0000
@@ -59,7 +59,8 @@
 	['bossmod'] = "DBM",
 	['keybinding_text'] = 'CTRL-SHIFT-O',
 	['forum'] = {
-		['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
+		['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
+		['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T',
 		['[item] by name'] = '[item]$N[/item]$X - $T',
 		['[item] by ID'] = '[item]$I[/item]$X - $T',
 		['Custom...'] = '',
@@ -261,7 +262,7 @@
 ]]
 do
 	local caches = {}
-	local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
+	local cleanup_group = _G.AnimTimerFrame:CreateAnimationGroup()
 	local time = _G.time
 	cleanup_group:SetLooping("REPEAT")
 	cleanup_group:SetScript("OnLoop", function(cg)
@@ -269,25 +270,30 @@
 		local now = time()
 		local alldone = true
 		-- this is ass-ugly
-		for _,c in ipairs(caches) do
-			while (#c > 0) and (now - c[1].t > c.ttl) do
-				addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m)
-				tremove(c,1)
+		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
+				addon.dprint('cache', name, "cache removing",fifo[1].t, fifo[1].m)
+				tremove(fifo,1)
 			end
-			alldone = alldone and (#c == 0)
+			if active and #fifo == 0 and c.func then
+				addon.dprint('cache', name, "empty, firing cleanup")
+				c:func()
+			end
+			alldone = alldone and (#fifo == 0)
 		end
 		if alldone then
 			addon.dprint('cache',"OnLoop finishing animation group")
 			cleanup_group:Finish()
-			for _,c in ipairs(caches) do
-				if c.func then c:func() end
-			end
 		end
 		addon.dprint('cache',"OnLoop done")
 	end)
 
 	local function _add (cache, x)
-		tinsert(cache, {t=time(),m=x})
+		local datum = { t=time(), m=x }
+		cache.hash[x] = datum
+		tinsert (cache.fifo, datum)
 		if not cleanup_group:IsPlaying() then
 			addon.dprint('cache', cache.name, "STARTING animation group")
 			cache.cleanup:SetDuration(2)  -- hmmm
@@ -295,11 +301,15 @@
 		end
 	end
 	local function _test (cache, x)
-		for _,v in ipairs(cache) do
+		return cache.hash[x] ~= nil
+		--[[for _,v in ipairs(cache) do
 			if v.m == x then return true end
-		end
+		end]]
 	end
+
 	function create_new_cache (name, ttl, on_alldone)
+		-- setting OnFinished for cleanup fires at the end of each inner loop,
+		-- with no 'requested' argument to distinguish cases.  thus, on_alldone.
 		local c = {
 			ttl = ttl,
 			name = name,
@@ -307,11 +317,11 @@
 			test = _test,
 			cleanup = cleanup_group:CreateAnimation("Animation"),
 			func = on_alldone,
+			fifo = {},
+			hash = setmetatable({}, {__mode='kv'}),
 		}
 		c.cleanup:SetOrder(1)
-		-- setting OnFinished for cleanup fires at the end of each inner loop,
-		-- with no 'requested' argument to distinguish cases.  thus, on_alldone.
-		tinsert (caches, c)
+		caches[name] = c
 		return c
 	end
 end
@@ -337,13 +347,21 @@
 			opts[opt] = default
 		end
 	end
+	-- transition&remove old options
+	opts['forum_use_itemid'] = nil
+	if opts['forum_format'] then
+		opts.forum['Custom...'] = opts['forum_format']
+		opts['forum_format'] = nil
+	end
+	if opts.forum['[url]'] then
+		opts.forum['[url] Wowhead'] = opts.forum['[url]']
+		opts.forum['[url]'] = nil
+		opts.forum['[url] MMO/Wowstead'] = option_defaults.forum['[url] MMO/Wowstead']
+		if opts['forum_current'] == '[url]' then
+			opts['forum_current'] = '[url] Wowhead'
+		end
+	end
 	option_defaults = nil
-	-- transition&remove old options
-	opts["forum_use_itemid"] = nil
-	if opts["forum_format"] then
-		opts.forum["Custom..."] = opts["forum_format"]
-		opts["forum_format"] = nil
-	end
 	if OuroLootSV then  -- may not be the same as testing g_restore_p soon
 		if OuroLootSV.saved then
 			OuroLootSV_saved = OuroLootSV.saved; OuroLootSV.saved = nil
@@ -617,7 +635,33 @@
 -- helper for CHAT_MSG_LOOT handler
 do
 	-- Recent loot cache
-	addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl)
+	local candidates = {}
+	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
+		-- to extract the results once the cache timers have expired.
+		for i,sig in ipairs(candidates) do
+			addon.dprint('loot', "processing candidate entry", i, sig)
+			local loot = candidates[sig]
+			if loot then
+				addon.dprint('loot', i, "was found")
+				candidates[sig] = nil
+				local looti = addon._addLootEntry(loot)
+				if (loot.disposition ~= 'shard')
+				   and (loot.disposition ~= 'gvault')
+				   and (not addon.history_suppress)
+				then
+					addon:_addHistoryEntry(looti)
+				end
+			end
+		end
+
+		if addon.display then
+			addon:redisplay()
+		end
+		table.wipe(candidates)
+	end
+	addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl, prefer_local_loots)
 
 	local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon
 
@@ -634,47 +678,39 @@
 		self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext, "q:", iquality)
 
 		itemid = tonumber(ilink:match("item:(%d+)") or 0)
-		if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then
+		-- This is only a loop 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
 			if (self.rebroadcast and (not from)) and not local_override then
 				self:broadcast('loot', recipient, itemid, count)
 			end
-			if self.enabled or local_override then
-				local signature = recipient .. iname .. (count or "")
-				if self.recent_loot:test(signature) then
-					self.dprint('cache', "loot <",signature,"> already in cache, skipping")
-				else
-					self.recent_loot:add(signature)
-					i = self._addLootEntry{   -- There is some redundancy here...
-						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,
-						disposition	= (recipient == self.sharder) and 'shard' or nil,
-						count		= count,
-						bcast_from	= from,
-						extratext	= extratext,
-						is_heroic	= self:is_heroic_item(ilink),
-					}
-					self.dprint('loot', "added loot entry", i)
-					if not self.history_suppress then
-						self:_addHistoryEntry(i)
-					end
-					if self.display then
-						self:redisplay()
-						--[[
-						local st = self.display:GetUserData("eoiST")
-						if st and st.frame:IsVisible() then
-							st:OuroLoot_Refresh()
-						end
-						]]
-					end
-				end
+			if (not self.enabled) and (not local_override) then break end
+			local signature = recipient .. iname .. (count or "")
+			if from and self.recent_loot:test(signature) then
+				self.dprint('cache', "loot <",signature,"> already in cache, skipping")
+			else
+				self.recent_loot:add(signature)
+				-- 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,
+					disposition	= (recipient == self.sharder) and 'shard' or nil,
+					count		= count,
+					bcast_from	= from,
+					extratext	= extratext,
+					is_heroic	= self:is_heroic_item(ilink),
+				}
+				candidates[signature] = i
+				tinsert (candidates, signature)
+				self.dprint('cache', "loot <",signature,"> added to cache, candidate", #candidates)
 			end
+			break
 		end
 		self.dprint('loot',"<<_do_loot out")
 		return i
@@ -1067,11 +1103,11 @@
 	end
 end
 
--- Tie-ins with Deadly Boss Mods
+-- Tie-in with Deadly Boss Mods (or other such addons)
 do
-	local candidates, location
+	local candidates = {}
+	local location
 	local function fixup_durations (cache)
-		if candidates == nil then return end  -- this is called for *all* cache expirations, including non-boss
 		local boss, bossi
 		boss = candidates[1]
 		if #candidates == 1 then
@@ -1108,7 +1144,7 @@
 				addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
 			end
 		end
-		candidates = nil
+		table.wipe(candidates)
 	end
 	addon.recent_boss = create_new_cache ('boss', 10, fixup_durations)
 
@@ -1145,7 +1181,6 @@
 					duration	= duration,      -- these two deliberately may be nil
 					raiderlist	= raiders and table.concat(raiders, ", ")
 				}
-				candidates = candidates or {}
 				tinsert(candidates,c)
 			end
 			break
@@ -1531,6 +1566,7 @@
 		if e.kind ~= 'loot' then return end
 
 		local i,h = self:get_loot_history(e.person)
+		-- If any of these change, update the end of history_handle_disposition.
 		local n = {
 			id = e.id,
 			when = self:format_timestamp (g_today, e),
@@ -1574,48 +1610,127 @@
 		end
 	end
 
-	function addon:reassign_loot (index, name_to)
-		local e = assert(g_loot[index], "trying to reassign nonexistant entry")
-		assert(e.kind=='loot', "trying to reassign something that isn't loot")
-		assert(type(name_to)=='string' and name_to:len()>0)
+	-- 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")
+		end
 
-		local name_from = e.person
-		local tag = e.history_unique
+		local player = loot.person
+		local tag = loot.history_unique
 		local errtxt
+		local player_i, player_h, hist_i
 
 		if not tag then
 			errtxt = "Entry for %s is missing a history tag!"
 		else
-			local from_i,from_h = self:get_loot_history(name_from)
-			local to_i,to_h = self:get_loot_history(name_to)
-
-			local hi
-			for i,h in ipairs(from_h) do
+			player_i,player_h = self:get_loot_history(player)
+			for i,h in ipairs(player_h) do
 				local unique = h.id .. ' ' .. h.when
 				if unique == tag then
-					hi = i
+					hist_i = i
 					break
 				end
 			end
-			if not hi then
+			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!"
-			else
-				hi = tremove (from_h, hi)
-				tinsert (to_h, 1, hi)
-				tsort (from_h, comp)
-				tsort (to_h, comp)
 			end
 		end
 
 		if errtxt then
-			self:Print(errtxt .. "  Loot will be reassigned but history will NOT be updated.", e.itemlink)
+			return nil, errtxt
 		end
-		e.person = name_to
-		e.person_class = select(2,UnitClass(name_to))
+		return player_i, player_h, hist_i
+	end
 
-		self:Print("Reassigned entry %d from '%s' to '%s'.", index, name_from, name_to)
+	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_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
+			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)
+		end
+		e.person = to_name
+		e.person_class = select(2,UnitClass(to_name))
+		self.hist_clean = nil
+
+		self:Print("Reassigned entry %d/%s from '%s' to '%s'.", index, e.itemlink, from_name, to_name)
+	end
+
+	-- 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'})
+	function addon:history_handle_disposition (index, olddisp)
+		local e = g_loot[index]
+		-- Standard disposition has a nil entry, but that's tedious in debug
+		-- output, so force to a string instead.
+		olddisp = olddisp or 'normal'
+		local newdisp = e.disposition or 'normal'
+		-- Ignore misclicks and the like
+		if olddisp == newdisp then return end
+
+		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
+			if hist_i then
+				local hist_h = tremove (name_h, hist_i)
+				deleted_cache[e.history_unique] = hist_h
+				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.
+			else
+				self:Print(name_h .. "  Loot has been marked, but history will NOT be updated.", e.itemlink)
+			end
+			return
+		end
+
+		if (olddisp == 'shard' or olddisp == 'gvault')
+		   and (newdisp == 'normal' or newdisp == 'offspec')
+		then
+			local name_i, name_h = self:get_loot_history(name)
+
+			-- Must create a new history entry.  Could call '_addHistoryEntry(index)'
+			-- 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.history_unique and deleted_cache[e.history_unique] then
+				entry = deleted_cache[e.history_unique]
+				deleted_cache[e.history_unique] = nil
+			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)
+			e.history_unique = e.history_unique or (entry.id .. ' ' .. entry.when)
+			self.hist_clean = nil
+			return
+		end
 	end
 end