changeset 76:124da015c4a2

- Some more debugging aids (logging error/assert, auto-enable of testing panel, reminder of GOP history mode) - Move (finally!) hypertext handling code out to each call site. - Fix some bugs in previous alpha code. - Initial-but-mostly-tested code to handle items that have a "unique" field which are in fact always the same (for example, elementium gem cluster). Still need to test the case in which a remote tracker sees them first. - The rest of the variable-cutoff history cleanup.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Fri, 08 Jun 2012 08:05:37 +0000
parents 676fb79a4ae2
children a07c9dd79f3a
files core.lua gui.lua sfrr.ogg verbage.lua
diffstat 4 files changed, 492 insertions(+), 216 deletions(-) [+]
line wrap: on
line diff
--- a/core.lua	Fri Jun 01 02:17:57 2012 +0000
+++ b/core.lua	Fri Jun 08 08:05:37 2012 +0000
@@ -93,7 +93,7 @@
 rewrite.
 
 Some variables are needlessly initialized to nil just to look uniform and
-serve as a reminder.
+serve as a spelling reminder.
 
 ]==]
 
@@ -143,8 +143,8 @@
 	.." 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 "
+local unique_collision = "|cffff1010%s:|r  Item '%s' was carrying unique tag "
+	.."<%s>, 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"
@@ -159,7 +159,7 @@
 	--['heirloom'] = 7,
 }
 local my_name				= UnitName('player')
-local comm_cleanup_ttl		= 4   -- seconds in the cache
+local comm_cleanup_ttl		= 4   -- seconds in the communications cache
 local revision_large		= nil -- defaults to 1, possibly changed by revision
 local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss
 
@@ -188,12 +188,12 @@
 
 	DEBUG_PRINT		= false
 	debug = {
-		comm = false,
-		loot = false,
-		flow = false,
-		notraid = false,
-		cache = false,
-		alsolog = false,
+		comm		= false,
+		loot		= false,
+		flow		= false,
+		notraid		= false,
+		cache		= false,
+		alsolog		= false,
 	}
 	-- This looks ugly, but it factors out the load-time decisions from
 	-- the run-time ones.  Args to [dp]print are concatenated with spaces.
@@ -228,25 +228,41 @@
 		pprint = flib.nullfunc
 	end
 
+	-- The same observable behavior as the Lua builtins, but with slightly
+	-- different hardcoded strings and, more importantly, implicit logging.
+	function error(txt,lvl)
+		pprint('ERROR()', txt)
+		pprint('DEBUGSTACK()', _G.debugstack())
+		_G.error(txt,lvl)
+	end
+	function assert(cond,msg,...)
+		if cond then
+			return cond,msg,...
+		else
+			error('ASSERT() FAILED:  '..tostring(msg or 'nil'))
+		end
+	end
+
 	enabled			= false
 	rebroadcast		= false
-	display			= nil   -- display frame, when visible
+	display			= nil   -- reference to display frame iff visible
 	loot_clean		= nil   -- index of last GUI entry with known-current visual data
-	sender_list		= {active={},names={}}   -- this should be reworked
 	threshold		= debug.loot and 0 or 3     -- rare by default
 	sharder			= nil   -- name of person whose loot is marked as shards
 
 	-- The rest is also used in the GUI:
 
+	sender_list		= {active={},names={}}   -- this should be reworked
 	popped			= nil   -- non-nil when reminder has been shown, actual value unimportant
 
 	bossmod_registered = nil
-	bossmods = {}
+	bossmods		= {}
 
-	requesting		= nil   -- for prompting for additional rebroadcasters
+	requesting		= nil   -- prompting for additional rebroadcasters
 
-	-- don't use NUM_ITEM_QUALITIES as the upper bound unless we expect heirlooms to show up
-	thresholds = {}
+	-- don't use NUM_ITEM_QUALITIES as the upper loop bound unless we expect
+	-- heirlooms to show up
+	thresholds		= {}
 	for i = 0,6 do
 		thresholds[i] = _G.ITEM_QUALITY_COLORS[i].hex .. _G["ITEM_QUALITY"..i.."_DESC"] .. "|r"
 	end
@@ -267,7 +283,7 @@
 	self:Printf([[|cffff1010ERROR:|r  <|cff00ff00%s|r>  Ouro Loot cannot finish loading.  You will need to type |cff30adff%s|r once these problems are resolved, and try again.]], msg, _G.SLASH_RELOAD1)
 	SLASH_ACECONSOLE_OUROLOOT1 = nil
 	SLASH_ACECONSOLE_OUROLOOT2 = nil
-	_G.error (msg, --[[level=]]2)
+	self.error (msg, --[[level=]]2)
 end
 
 -- Seriously?  ORLY?
@@ -293,13 +309,17 @@
 ------ Globals
 local g_loot			= nil
 local g_restore_p		= nil
-local g_wafer_thin		= nil   -- for prompting for additional rebroadcasters
+local g_wafer_thin		= nil   -- 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 g_unique_replace	= nil
 local opts				= nil
 
+local error				= addon.error
+local assert			= addon.assert
+
 -- for speeding up local loads, not because I think _G will change
 local _G = _G
 local type = _G.type
@@ -316,7 +336,7 @@
 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
+local _history_by_loot_id, _notify_about_remote, _setup_unique_replace
 
 -- 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
@@ -342,48 +362,63 @@
 
 -- Hypertext support, inspired by DBM broadcast pizza timers
 do
-	local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h"
-	local strsplit = _G.strsplit
+	local hypertext_format_str = "|HOuroLoot:%d|h%s[%s]|r|h"
+	local func_map = {} --_G.setmetatable({}, {__mode = 'k'})
+	local text_map = {} --_G.setmetatable({}, {__mode = 'kv'})
+	local base = _G.newproxy(true)
+	_G.getmetatable(base).__tostring = function(ud) return text_map[ud] end
+	--@debug@
+	-- collecting these tokens is an interesting micro-optimization but not yet
+	_G.getmetatable(base).__gc = function(ud)
+		print("Collecting hyperlink object <",tostring(ud),">")
+	end
+	--@end-debug@
 
 	-- TEXT will automatically be surrounded by brackets
-	-- COLOR can be item quality code or a hex string
-	function addon.format_hypertext (code, text, color)
-		return hypertext_format_str:format (code,
-			type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color,
-			text)
+	-- COLOR can be ITEM_QUALITY_* or a formatting string ("|cff...")
+	-- FUNC can be "MethodName", "tab_title", or a function
+	--
+	-- Returns an obaque token.  Calling tostring() on the token will yield a
+	-- formatted clickable string that can be displayed in chat.  This is
+	-- largely an excuse to fool around with Lua data constructs.
+	function addon.format_hypertext (text, color, func)
+		local ret = _G.newproxy(base)
+		local num = #text_map + 1
+		text_map[ret] = hypertext_format_str:format (num,
+				type(color)=='number' and ITEM_QUALITY_COLORS[color].hex or color,
+				text)
+		text_map[num] = ret
+		func_map[ret] = func
+		return ret
 	end
 
-	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton)
+	--[[
+	link:         OuroLoot:n
+	fullstring:   |HOuroLoot:n|h|cff.....[foo]|r|h
+	mousebutton:  "LeftButton", "MiddleButton", "RightButton"
+
+	amusingly, print()'ing the fullstring below as a debugging aid yields
+	another clickable link, yay data reproducability
+	]]
+	local strsplit = _G.strsplit
+	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, fullstring, mousebutton)
 		local ltype, arg = strsplit(":",link)
-		if ltype ~= "OuroRaid" then return end
-		-- XXX this is crap, redo this as a dispatch table with code at the call site
-		if arg == 'openloot' then
-			addon:BuildMainDisplay()
-		elseif arg == 'popupurl' then
-			-- Sadly, this is not generated by the packager, so hardcode it for now.
-			-- The 'data' field is handled differently for onshow than for other callbacks.
-			StaticPopup_Show("OUROL_URL", --[[text_arg1=]]nil, --[[text_arg2=]]nil,
-				--[[data=]][[http://www.curse.com/addons/wow/ouroloot]])
-		elseif arg == 'doping' then
-			addon:DoPing()
-		elseif arg == 'help' then
-			addon:BuildMainDisplay('help')
-		elseif arg == 'bcaston' then
-			if not addon.rebroadcast then
-				addon:Activate(nil,true)
+		if ltype ~= "OuroLoot" then return end
+		local f = func_map[text_map[tonumber(arg)]]
+		if type(f) == 'function' then
+			f()
+		elseif type(f) == 'string' then
+			if type(addon[f]) == 'function' then
+				addon[f](addon)             -- method name
+			else
+				addon:BuildMainDisplay(f)   -- tab title fragment
 			end
-			addon:broadcast('bcast_responder')
-		elseif arg == 'waferthin' then   -- mint? it's wafer thin!
-			g_wafer_thin = true          -- fuck off, I'm full
-			addon:broadcast('bcast_denied')   -- remove once tested
-		elseif arg == 'reload' then
-			addon:BuildMainDisplay('opt')
 		end
 	end)
 
 	local old = ItemRefTooltip.SetHyperlink
 	function ItemRefTooltip:SetHyperlink (link, ...)
-		if link:match("^OuroRaid") then return end
+		if link:match("^OuroLoot") then return end
 		return old (self, link, ...)
 	end
 end
@@ -416,9 +451,17 @@
 	if (GetLFGMode()) and (GetLFGModeType() == 'raid') then
 		t,r = 'LFR', 25
 	elseif diffcode == 1 then
-		t,r = (GetNumRaidMembers()>0) and "10",10 or "5",5
+		if GetNumRaidMembers() > 0 then
+			t,r = "10",10
+		else
+			t,r = "5",5
+		end
 	elseif diffcode == 2 then
-		t,r = (GetNumRaidMembers()>0) and "25",25 or "5h",5
+		if GetNumRaidMembers() > 0 then
+			t,r = "25",25
+		else
+			t,r = "5h",5
+		end
 	elseif diffcode == 3 then
 		t,r = "10h", 10
 	elseif diffcode == 4 then
@@ -572,13 +615,14 @@
 	local function _test (cache, x)
 		-- FIXME This can return false positives, if called after the onloop
 		-- fifo has been removed but before the GC has removed the weak entry.
-		-- What to do, what to do...
+		-- What to do, what to do...  try forcing a GC during alldone.
 		return cache.hash[x] ~= nil
 	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.
+		-- FWIW, on_alldone is passed this table as its sole argument:
 		local c = {
 			ttl = ttl,
 			name = name,
@@ -612,8 +656,9 @@
 
 	if _G.OuroLootSV_opts == nil then
 		_G.OuroLootSV_opts = {}
+		local vclick = self.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'help')
 		self:ScheduleTimer(function(s)
-			s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
+			s:Print(virgin, tostring(vclick))
 			virgin = nil
 		end,10,self)
 	else
@@ -695,7 +740,7 @@
 					end
 				end
 				-- format 3 to format 4 was a major revamp of per-player data
-				self:_uplift_history_format(player,rname)
+				self:_uplift_history_format(player)
 			end
 		end
 	end
@@ -803,29 +848,34 @@
 	-- There is no ITEM_QUALITY_LEGENDARY constant.  Sigh.
 	do
 		local AC = LibStub("AceConsole-3.0")
-		local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
+		local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5,
+			--[[empty -> nil -> main tab]]'')
+		local chat_prefix_s = tostring(chat_prefix)
 		function addon:Print (str, ...)
 			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
-				return AC:Print (chat_prefix, str:format(...))
+				return AC:Print (chat_prefix_s, str:format(...))
 			else
-				return AC:Print (chat_prefix, str, ...)
+				return AC:Print (chat_prefix_s, 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(...))
+				return AC:Print (frame, chat_prefix_s, str:format(...))
 			else
-				return AC:Print (frame, chat_prefix, str, ...)
+				return AC:Print (frame, chat_prefix_s, str, ...)
 			end
 		end
 	end
 
 	while opts.keybinding do
 		if InCombatLockdown() then
+			local reload = self.format_hypertext ([[the options tab]],
+				ITEM_QUALITY_UNCOMMON, 'opt')
 			self:Print("Cannot create '%s' as a keybinding while in combat!",
 				opts.keybinding_text)
-			self:Print("The rest of the addon will continue to work, but you will need to reload out of combat to get the keybinding.  Either type /reload or use the button on %s in the lower right.", self.format_hypertext('reload',"the options tab",ITEM_QUALITY_UNCOMMON))
+			self:Print("The rest of the addon will continue to work, but you will need to reload out of combat to get the keybinding.  Either type /reload or use the button on %s in the lower right.",
+				tostring(reload))
 			break
 		end
 
@@ -858,6 +908,8 @@
 	trigger on 'receive item' instead, which would detect extracting stuff
 	from mail, or s/PUSHED/CREATED/ for things like healthstones and guild
 	cauldron flasks.
+
+	??? do something with LOOT_ITEM_WHILE_PLAYER_INELIGIBLE for locked LFRs?
 	]]
 
 	-- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%.
@@ -1200,22 +1252,44 @@
 		})
 	end
 
+	-- Alert other trackers that unique tag EXISTING in subsequent 'casts
+	-- should be replaced by REPLACE instead.  If multiple players all saw
+	-- the same loot event, this will cause a flurry of cross-improvs.
+	local function _announce_unique_improvisation (existing, replace)
+		if not g_unique_replace then _setup_unique_replace() end
+		g_unique_replace.new_entry (g_unique_replace.me, existing, replace, 'improv')
+		addon:vbroadcast('improv', g_unique_replace.me, existing, replace)
+	end
+
 	local random = _G.math.random
-	local function many_uniques_handle_it (u, check_p)
-		if u and check_p then
+	local function _many_uniques_handle_it (u, prefix)
+		if u then
 			-- Check and alert for an existing value.
 			u = tostring(u)
 			if g_uniques[u].history ~= g_uniques.NOTFOUND then
-				return nil, u
+				if not g_unique_replace then _setup_unique_replace() end
+				local maybe = g_unique_replace.get_previous_replacement (u)
+				if maybe then
+					addon.dprint('loot',"previous replaced tag ("..u
+						..") with ("..maybe.."), using that instead")
+					return false, u, maybe
+				end
+				local can_replace_p,improv = _many_uniques_handle_it (nil, 'c')
+				if can_replace_p then
+					_announce_unique_improvisation (u, improv)
+					return false, u, improv
+				end
+				return false, 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.
+			prefix = prefix or 'n'
 			repeat
-				u = 'n' .. random(8675309)
-			until g_uniques:TEST(u).history ~= g_uniques.NOTFOUND
-			addon.dprint('loot',"created unique tag", u)
+				u = prefix .. random(8675309)
+			until g_uniques:TEST(u).history == g_uniques.NOTFOUND
+			addon.dprint('loot',"created unique tag ("..u..")")
 		end
 		return true, u
 	end
@@ -1223,13 +1297,14 @@
 	-- 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
+--pprint("preempt", oldersig, "::", newersig)
+		local origin = candidates[oldersig] and candidates[oldersig].bcast_from
+--pprint("preempt", "candidate", candidates[oldersig], "bcast:", origin)
 		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)
+			addon.dprint('loot', "preempting signature <", oldersig, "> from", origin)
 		end
 		return false
 	end
@@ -1253,6 +1328,9 @@
 				   and (not addon.history_suppress)
 				then
 					addon:_addHistoryEntry(looti)
+				elseif #loot.unique > 0 then
+					g_uniques[loot.unique] =   -- stub entry
+						{ loot = looti, history = g_uniques.NOTFOUND }
 				end
 			end
 		end
@@ -1285,21 +1363,28 @@
 		itemid = tonumber(ilink:match("item:(%d+)") or 0)
 
 		-- This is only a 'while' to make jumping out of it easy.
-		local i, unique_okay, ret1, ret2
+		local i, unique_okay, replacement, 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)
+			unique_okay, unique, replacement =
+				_many_uniques_handle_it ((not local_override) and unique)
 			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
+				if replacement then
+					-- collision, but we've generated a placeholder for now
+					-- and broadcast the fact
+					self.dprint('loot', "substituting", unique, "with", replacement)
+				else
+					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.PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "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
 			end
 
 			if (self.rebroadcast and (not from)) and not local_override then
@@ -1312,16 +1397,20 @@
 			if #unique > 0 then
 				-- newer case
 				signature = unique .. oldersig
+--pprint("newer", "mapping older <", oldersig, "> to newer <", signature, ">")
 				sigmap[oldersig] = signature
-				seenit = from and (recent_loot:test(signature)
+--pprint("newer", "testing recent for", signature, "yields", recent_loot:test(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)))
+					-- circuit evaluation; the preempt function always returns
+					-- false to force seenit off.
+					or (g_seeing_oldsigs and preempt_older_signature(oldersig,signature))
 			else
 				-- older case, only remote
 				assert(from)
 				signature = sigmap[oldersig] or oldersig
+--pprint("older", "testing signature will be", signature)
 				seenit = recent_loot:test(signature)
 			end
 
@@ -1342,7 +1431,7 @@
 				id			= itemid,
 				itemlink	= ilink,
 				itexture	= itexture,
-				unique		= unique,
+				unique		= replacement or unique,
 				count		= (count and count ~= "") and count or nil,
 				bcast_from	= from,
 				extratext	= extratext,
@@ -1368,8 +1457,12 @@
 				   and (not self.history_suppress)
 				then
 					self:_addHistoryEntry(looti)
+				else
+					g_uniques[i.unique] =   -- stub entry
+						{ loot = looti, history = g_uniques.NOTFOUND }
 				end
 				ret1 = looti  -- return value mostly for gui's manual entry
+				self.dprint('loot', "manual", looti)
 			else
 				recent_loot:add(signature)
 				candidates[signature] = i
@@ -1448,88 +1541,92 @@
 -- that would result in a pile of teensy functions, most of which would
 -- never be called.  Too much overhead.  (2.0:  Most of these removed now
 -- that GUI is in place.)
-function addon:OnSlash (txt) --, editbox)
-	txt = strtrim(txt:lower())
-	local cmd, arg = ""
-	do
-		local s,e = txt:find("^%a+")
-		if s then
-			cmd = txt:sub(s,e)
-			s = txt:find("%S", e+2)
-			if s then arg = txt:sub(s,-1) end
-		end
-	end
-
-	if cmd == "" then
-		if InCombatLockdown() then
-			return self:Print("Shouldn't display window in combat.")
-		else
-			return self:BuildMainDisplay()
+do
+	local green_help_link = addon.format_hypertext ([[Click here]],
+		ITEM_QUALITY_UNCOMMON, 'help')
+	function addon:OnSlash (txt) --, editbox)
+		txt = strtrim(txt:lower())
+		local cmd, arg = ""
+		do
+			local s,e = txt:find("^%a+")
+			if s then
+				cmd = txt:sub(s,e)
+				s = txt:find("%S", e+2)
+				if s then arg = txt:sub(s,-1) end
+			end
 		end
 
-	elseif cmd:find("^thre") then
-		self:SetThreshold(arg)
+		if cmd == "" then
+			if InCombatLockdown() then
+				return self:Print("Shouldn't display window in combat.")
+			else
+				return self:BuildMainDisplay()
+			end
 
-	elseif cmd == "on" then                             self:Activate(arg)
-	elseif cmd == "off" then                            self:Deactivate()
-	elseif cmd == "broadcast" or cmd == "bcast" then    self:Activate(nil,true)
+		elseif cmd:find("^thre") then
+			self:SetThreshold(arg)
 
-	elseif cmd == "toggle" then
-		if self.display then
-			self.display:Hide()
+		elseif cmd == "on" then                             self:Activate(arg)
+		elseif cmd == "off" then                            self:Deactivate()
+		elseif cmd == "broadcast" or cmd == "bcast" then    self:Activate(nil,true)
+
+		elseif cmd == "toggle" then
+			if self.display then
+				self.display:Hide()
+			else
+				return self:BuildMainDisplay()
+			end
+
+		elseif cmd == "fake" then  -- maybe comment this out for real users
+			self:_mark_boss_kill (self._addBossEntry{
+				kind='boss',reason='kill',bossname="Baron Steamroller",duration=0
+			})
+			self:CHAT_MSG_LOOT ('manual', my_name, 54797)
+			if self.display then
+				self:redisplay()
+			end
+			self:Print "Baron Steamroller has been slain.  Congratulations on your rug."
+
+		elseif cmd == "debug" then
+			if arg then
+				self.debug[arg] = not self.debug[arg]
+				_G.print(arg,self.debug[arg])
+				if self.debug[arg] then self.DEBUG_PRINT = true end
+			else
+				self.DEBUG_PRINT = not self.DEBUG_PRINT
+			end
+
+		elseif cmd == "save" and arg and arg:len() > 0 then
+			self:save_saveas(arg)
+		elseif cmd == "list" then
+			self:save_list()
+		elseif cmd == "restore" and arg and arg:len() > 0 then
+			self:save_restore(tonumber(arg))
+		elseif cmd == "delete" and arg and arg:len() > 0 then
+			self:save_delete(tonumber(arg))
+
+		elseif cmd == "help" then
+			self:BuildMainDisplay('help')
+		elseif cmd == "ping" then
+			self:DoPing()
+
+		elseif cmd == "fixcache" then
+			self:do_item_cache_fixup()
+
 		else
-			return self:BuildMainDisplay()
+			if self:OpenMainDisplayToTab(cmd) then
+				return
+			end
+			self:Print("Unknown command '%s'. %s to see the help window.",
+				cmd, tostring(green_help_link))
 		end
-
-	elseif cmd == "fake" then  -- maybe comment this out for real users
-		self:_mark_boss_kill (self._addBossEntry{
-			kind='boss',reason='kill',bossname="Baron Steamroller",duration=0
-		})
-		self:CHAT_MSG_LOOT ('manual', my_name, 54797)
-		if self.display then
-			self:redisplay()
-		end
-		self:Print "Baron Steamroller has been slain.  Congratulations on your rug."
-
-	elseif cmd == "debug" then
-		if arg then
-			self.debug[arg] = not self.debug[arg]
-			_G.print(arg,self.debug[arg])
-			if self.debug[arg] then self.DEBUG_PRINT = true end
-		else
-			self.DEBUG_PRINT = not self.DEBUG_PRINT
-		end
-
-	elseif cmd == "save" and arg and arg:len() > 0 then
-		self:save_saveas(arg)
-	elseif cmd == "list" then
-		self:save_list()
-	elseif cmd == "restore" and arg and arg:len() > 0 then
-		self:save_restore(tonumber(arg))
-	elseif cmd == "delete" and arg and arg:len() > 0 then
-		self:save_delete(tonumber(arg))
-
-	elseif cmd == "help" then
-		self:BuildMainDisplay('help')
-	elseif cmd == "ping" then
-		self:DoPing()
-
-	elseif cmd == "fixcache" then
-		self:do_item_cache_fixup()
-
-	else
-		if self:OpenMainDisplayToTab(cmd) then
-			return
-		end
-		self:Print("Unknown command '%s'. %s to see the help window.",
-			cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON))
 	end
 end
 
 function addon:SetThreshold (arg, quiet_p)
 	local q = tonumber(arg)
 	if q then
-		q = math.floor(q+0.001)
+		q = math.floor(q+0.1)
 		if q<0 or q>6 then
 			return self:Print("Threshold must be 0-6.")
 		end
@@ -1555,7 +1652,7 @@
 		self.dprint('flow', ">:Activate calling RRU")
 		self:RAID_ROSTER_UPDATE("Activate")
 	elseif self.debug.notraid then
-		self.dprint('flow', ">:Activate registering loot and bossmods")
+		self.dprint('flow', ">:(notraid) Activate registering loot and bossmods")
 		self:RegisterEvent("CHAT_MSG_LOOT")
 		_register_bossmod(self)
 	elseif g_restore_p then
@@ -1626,7 +1723,7 @@
 ------ Behind the scenes routines
 -- Semi-experimental debugging aid.
 do
-	-- Putting _log local to here can result in this sequence:
+	-- Declaring _log as local to here can result in this sequence:
 	-- 1)  logging happens, followed by reload or logout/login
 	-- 2)  _log points to SV_log
 	-- 3)  VARIABLES_LOADED replaces SV_log pointer with restored version
@@ -1707,6 +1804,8 @@
 			to_color = addon.disposition_colors[e.disposition or "normal"].hex
 		end
 
+		addon.dprint ('loot', "notifying:", sender, index,
+			e.itemlink, from_color, from_text, to_color, to_text)
 		addon:CFPrint (remote_change_chatframe, remote_chatty, sender, index,
 			e.itemlink, from_color, from_text, to_color, to_text)
 	end
@@ -1755,11 +1854,17 @@
 		self:broadcast('revcheck',revision_large)
 
 	else
-		self.dprint('comm', "ours is older, yammering")
+		self.dprint('comm', "ours is older, (possibly) yammering")
 		if newer_warning then
-			self:Print(newer_warning,
-				self.format_hypertext('popupurl',"click here",ITEM_QUALITY_UNCOMMON),
-				self.format_hypertext('doping',"click here",ITEM_QUALITY_UNCOMMON))
+			local pop = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON,
+				function()
+					-- Sadly, this is not generated by the packager, so hardcode it for now.
+					-- The 'data' field is handled differently for onshow than for other callbacks.
+					StaticPopup_Show("OUROL_URL", --[[text_arg1=]]nil, --[[text_arg2=]]nil,
+						--[[data=]][[http://www.curse.com/addons/wow/ouroloot]])
+				end)
+			local ping = addon.format_hypertext ([[click here]], ITEM_QUALITY_UNCOMMON, 'DoPing')
+			self:Print(newer_warning, tostring(pop), tostring(ping))
 			newer_warning = nil
 		end
 	end
@@ -2018,7 +2123,9 @@
 	function addon:register_boss_mod (name, registration_func, deregistration_func)
 		assert(type(name)=='string')
 		assert(type(registration_func)=='function')
-		if deregistration_func ~= nil then assert(type(deregistration_func)=='function') end
+		if deregistration_func ~= nil then
+			assert(type(deregistration_func)=='function')
+		end
 		self.bossmods[#self.bossmods+1] = {
 			n = name,
 			r = registration_func,
@@ -2235,7 +2342,9 @@
 				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)))
+					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
@@ -2250,6 +2359,122 @@
 	self:Print("...finished.  Found %d |4entry:entries; with weird data.", numfound)
 end
 
+do
+	local gur
+
+	-- Strictly speaking, we'd want to handle individual 'exist' entries
+	-- as they expire, rather then waiting for all of them to expire and then
+	-- doing them as a group.  But, if there're more than one of these per
+	-- boss, something else is funky anyhow and certainly not a hurry.
+	local function fixup_unique_replacements()
+--print("replacements fixup happening!")
+--tabledump(g_unique_replace.replacements)
+--_G.GRR = g_unique_replace.replacements
+		for exist,info in pairs(gur.replacements) do
+			local winning_index = 1
+			local winner = info[1]
+			info[1] = nil
+			pprint('improv', "fixup for", exist, "starting with", winner[1],
+				"with", winner[2], "out of", #info, "total entries")
+			-- Lowest player GUID wins.  Seniority gotta count for something.
+			for i = 2, #info do
+				if winner[1] <= info[i][1] then
+					pprint('improv', "champ wins against", i, info[i][1])
+					flib.del(info[i])
+				else
+					pprint('improv', "challenger wins with", i, info[i][1])
+					flib.del(winner)
+					winner = info[i]
+					winning_index = i
+				end
+				info[i] = nil
+			end
+			pprint('improv', "final:", winner[1], winner[2])
+			--[[
+			A:  winner was generated locally
+			   >g_loot and history already has the replacement value
+			   >winning_index == 1
+			B:  winner was generated remotely
+			   >need to scan and replace
+			]]
+			if winning_index ~= 1 then
+--XXX still needs to be debugged:
+				local cache = g_uniques[exist]
+				local looti = assert(cache.loot)  -- can't possibly be missing...
+				if g_loot[looti].unique ~= exist then
+					pprint('improv', "WTF. entry", looti,
+						"does not match original unique tag! instead",
+						g_loot[looti].unique)
+				else
+					pprint('improv', "found and replaced loot entry", looti)
+					g_loot[looti].unique = winner[2]
+				end
+				local hi,ui = cache.history, cache.history_may
+				if hi ~= g_uniques.NOTFOUND then
+					local hist = addon.history[hi]
+					if ui and hist.unique[ui] == exist then
+						-- ui is valid
+					else
+						ui = nil
+						for i,ui2 in ipairs(hist.unique) do
+							if ui2 == exist then
+								ui = i
+								break
+							end
+						end
+					end
+					if ui then
+						pprint('improv', "found and replacing history entry", hi,
+							ui, hist.name)
+						hist.when[winner[2]] = hist.when[exist]
+						hist.id[winner[2]] = hist.id[exist]
+						hist.count[winner[2]] = hist.count[exist]
+						hist.unique[ui] = winner[2]
+						hist.when[exist] = nil
+						hist.id[exist] = nil
+						hist.count[exist] = nil
+					end
+				end
+			end
+			pprint('improv', "finished with", exist, "into", winner[2])
+			flib.del(winner)
+			flib.del(info)
+			gur.replacements[exist] = nil
+		end
+	end
+
+	local function new_entry (id, exist, repl, is_local)
+		pprint('improv', "new_entry", id, exist, repl, is_local)
+		gur.replacements[exist] = gur.replacements[exist] or flib.new()
+		tinsert (gur.replacements[exist], flib.new (tonumber(id), repl))
+		if is_local then
+			gur.replacements[exist].LOCAL = repl
+		end
+		gur.cache:add (exist)
+	end
+
+	local function get_previous_replacement (exist)
+		local l = gur.replacements[exist]
+		if l and l.LOCAL then
+			pprint('improv', "check for previous", exist, "returns valid",
+				l.LOCAL)
+			return l.LOCAL
+		end
+		pprint('improv', "check for previous", exist, "returns nil")
+	end
+
+	function _setup_unique_replace ()
+		gur = {}
+		gur.cache = create_new_cache ('improv', 10, fixup_unique_replacements)
+		gur.me = tonumber(_G.UnitGUID('player'):sub(-7),16)
+		gur.replacements = {}
+		gur.new_entry = new_entry
+		gur.get_previous_replacement = get_previous_replacement
+		g_unique_replace = gur
+		_setup_unique_replace  = nil
+	end
+end
+
 
 ------ Saved texts
 function addon:check_saved_table(silent_p)
@@ -2345,13 +2570,16 @@
 do
 	-- Sorts a player's history from newest to oldest, according to the
 	-- formatted timestamp.  This is expensive, and destructive for P.unique.
+	local function compare_timestamps (L, R)
+		return L > R    -- reverse of normal order, newest first
+	end
 	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)
+		_G.table.sort (when_array, compare_timestamps)
 		for i,tstamp in ipairs(when_array) do
 			new_uniques[i] = uniques_bywhen[tstamp]
 		end
@@ -2360,7 +2588,7 @@
 
 	-- 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)
+	function addon:_uplift_history_format (player)
 		local unique, when, id, count = {}, {}, {}, {}
 		local name = player.name
 
@@ -2377,6 +2605,7 @@
 		player.id, player.when, player.unique, player.count =
 			id, when, unique, count
 	end
+
 	function addon:_cache_history_uniques()
 		UpdateAddOnMemoryUsage()
 		local before = GetAddOnMemoryUsage(nametag)
@@ -2398,11 +2627,12 @@
 					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)
+					hmmm = "active data not found in history ("..i.."/"..tostring(e.unique)
+						..") in precache loop!  trying to fixup for this session"
+					pprint(hmmm)  -- more?
 					-- try to simply fix up errors as we go
 					g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
-					trouble = true
+					--trouble = true
 				end
 			else
 				trouble = true
@@ -2517,21 +2747,34 @@
 		end
 	end
 
-	-- Clears all but latest entry for each player.
-	function addon:preen_history (realmname)
+	-- Clears all but the most recent HOWMANY (optional, default 1) entries
+	-- for each player on REALMNAME.
+	-- This function's name is the legacy of the orignal fsck(8) "-p" option,
+	-- which has a similar feel.
+	function addon:preen_history (realmname, howmany)
 		local r = assert(realmname)
+		howmany = tonumber(howmany) or 1
+		if type(self.history_all[r]) ~= 'table' then
+			return
+		end
 		g_uniques:RESET()
-		for i,h in ipairs(self.history) do
+		for i,h in ipairs(self.history_all[r]) do
 			-- 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] }
+			local new_unique, new_id, new_when, new_count = {}, {}, {}, {}
+			for ui = 1, howmany do
+				local U = h.unique[ui]
+				if not U then break end
+				new_unique[ui] = U
+				new_id[U] = h.id[U]
+				new_when[U] = h.when[U]
+				new_count[U] = h.count[U]
+			end
+			h.unique, h.id, h.when, h.count =
+				new_unique, new_id, new_when, new_count
 		end
 	end
 
@@ -2744,7 +2987,7 @@
 			end
 
 		else
-			return  -- silently ignore newer cases from newer clients
+			return  -- silently ignore future cases from future clients
 		end
 
 		if self.debug.loot then
@@ -2752,7 +2995,7 @@
 				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.)")
+				self.dprint('loot',"(Returning early from debug mode's double self-mark.)")
 				return index
 			end
 		end
@@ -2780,7 +3023,7 @@
 do
 	local select, tconcat, strsplit, unpack = select, table.concat, strsplit, unpack
 	--[[ old way:  repeated string concatenations, BAD
-		 new way:  new table on every call, BAD
+		 new way:  new table on every broadcast, BAD
 	local msg = ...
 	for i = 2, select('#',...) do
 		msg = msg .. '\a' .. (select(i,...) or "")
@@ -2844,6 +3087,13 @@
 		addon:_check_revision (revlarge)
 	end
 
+	OCR_funcs['17improv'] = function (sender, _, senderid, existing, replace)
+		addon.dprint('comm', "DOTimprov/17, sender", sender, "id", senderid,
+			"existing", existing, "replace", replace)
+		if not g_unique_replace then _setup_unique_replace() end
+		g_unique_replace.new_entry (senderid, existing, replace)
+	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)
@@ -2890,13 +3140,25 @@
 	end
 	OCR_funcs['17boss'] = OCR_funcs['16boss']
 
+	local bcast_on = addon.format_hypertext ([[the red pill]], '|cffff4040',
+		function()
+			if not addon.rebroadcast then
+				addon:Activate(nil,true)
+			end
+			addon:broadcast('bcast_responder')
+		end)
+	local waferthin = addon.format_hypertext ([[the blue pill]], '|cff0070dd',
+		function()
+			g_wafer_thin = true               -- mint? it's wafer thin!
+			addon:broadcast('bcast_denied')   -- fuck off, I'm full
+		end)
 	OCR_funcs.bcast_req = function (sender)
 		if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast))
 		then
 			addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.",
 				sender,
-				addon.format_hypertext('bcaston',"the red pill",'|cffff4040'),
-				addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd'))
+				tostring(bcast_on),
+				tostring(waferthin))
 		end
 		addon.popped = true
 	end
--- a/gui.lua	Fri Jun 01 02:17:57 2012 +0000
+++ b/gui.lua	Fri Jun 08 08:05:37 2012 +0000
@@ -58,6 +58,9 @@
 local window_title		= "Ouro Loot"
 local dirty_tabs		= nil
 
+local error				= addon.error
+local assert			= addon.assert
+
 local pairs, ipairs, tinsert, tremove, tostring, tonumber =
 	pairs, ipairs, table.insert, table.remove, tostring, tonumber
 
@@ -164,13 +167,14 @@
 		g_loot[text_type] = g_loot[text_type] or ""
 
 		if g_loot.printed[text_type] >= #g_loot then return false end
-		assert(addon.loot_clean == #g_loot, tostring(addon.loot_clean) .. " ~= " .. #g_loot)
+		assert (addon.loot_clean == #g_loot,
+			tostring(addon.loot_clean) .. " ~= " .. #g_loot)
 		-- if glc is nil, #==0 test already returned
 
 		local ok,ret = pcall (f, text_type, g_loot, g_loot.printed[text_type], g_generated, accumulator)
 		if not ok then
 			error(("ERROR:  text generator '%s' failed:  %s"):format(text_type, ret))
-			return false
+			return false  -- why is this here again?
 		end
 		if ret then
 			g_loot.printed[text_type] = #g_loot
@@ -1888,30 +1892,35 @@
 		container:SetScroll(1000)  -- scrollframe's max value
 	end
 
-	-- Initial lower panel function
-	local function adv_lower (container, specials)
-		local spacer = GUI:Create("Spacer")
-		spacer:SetFullWidth(true)
-		spacer:SetHeight(5)
-		container:AddChild(spacer)
-		local speedbump = GUI:Create("InteractiveLabel")
-		speedbump:SetFullWidth(true)
-		speedbump:SetFontObject(GameFontHighlightLarge)
-		speedbump:SetImage("Interface\\DialogFrame\\DialogAlertIcon")
-		speedbump:SetImageSize(50,50)
-		speedbump:SetText("The debugging/testing settings on the rest of this panel can"
-			.." seriously bork up the addon if you make a mistake.  If you're okay"
-			.." with the possibility of losing data, click this warning to load the panel.")
-		speedbump:SetCallback("OnClick", function (_sb)
-			adv_lower = adv_real
-			return addon:redisplay()
-			--return tabs_OnGroupSelected_func(container.parent,"OnGroupSelected","opt")
-		end)
-		container:AddChild(speedbump)
-		spacer = GUI:Create("Spacer")
-		spacer:SetFullWidth(true)
-		spacer:SetHeight(5)
-		container:AddChild(spacer)
+	-- Initial lower panel function (unless debug mode is on during load, which
+	-- means it was almost certainly hardcoded that way, which means it's
+	-- probably me testing).
+	local adv_lower
+	if addon.DEBUG_PRINT then
+		adv_lower = adv_real
+	else
+		function adv_lower (container, specials)
+			local spacer = GUI:Create("Spacer")
+			spacer:SetFullWidth(true)
+			spacer:SetHeight(5)
+			container:AddChild(spacer)
+			local speedbump = GUI:Create("InteractiveLabel")
+			speedbump:SetFullWidth(true)
+			speedbump:SetFontObject(GameFontHighlightLarge)
+			speedbump:SetImage("Interface\\DialogFrame\\DialogAlertIcon")
+			speedbump:SetImageSize(50,50)
+			speedbump:SetText("The debugging/testing settings on the rest of this panel can seriously bork up the addon if you make a mistake.  If you're okay with the possibility of losing data, click this warning to load the panel.")
+			speedbump:SetCallback("OnClick", function (_sb)
+				adv_lower = adv_real
+				return addon:redisplay()
+				--return tabs_OnGroupSelected_func(container.parent,"OnGroupSelected","opt")
+			end)
+			container:AddChild(speedbump)
+			spacer = GUI:Create("Spacer")
+			spacer:SetFullWidth(true)
+			spacer:SetHeight(5)
+			container:AddChild(spacer)
+		end
 	end
 
 	tabs_OnGroupSelected["opt"] = function(container,specials)
@@ -2233,6 +2242,15 @@
 	h:SetFullWidth(true)
 	h:SetText(_tabtexts[group].title)
 	spec:AddChild(h)
+	do
+		addon.sender_list.sort()
+		local fmt = "Received broadcast data from %d |4player:players;."
+		if addon.history_suppress then
+			-- this is the druid class color reworked into hex
+			fmt = fmt .. "  |cffff7d0aHistory recording suppressed.|r"
+		end
+		tabs.titletext:SetFormattedText (fmt, addon.sender_list.activeI)
+	end
 	return tabs_OnGroupSelected[group](tabs,spec,group)
 	--[====[
 	Unfortunately, :GetHeight() called on anything useful out of a TabGroup
@@ -2478,11 +2496,6 @@
 	tabs:SetCallback("OnRelease", function(_tabs)
 		tabs.titletext:SetFontObject(titletext_orig_fo)
 	end)
-	do
-		self.sender_list.sort()
-		tabs.titletext:SetFormattedText("Received broadcast data from %d |4player:players;.",
-			self.sender_list.activeI)
-	end
 	tabs:SetRelativeWidth(0.99-rhs_width)
 	tabs:SetFullHeight(true)
 	tabs:SetTabs(tabgroup_tabs)
@@ -2492,7 +2505,8 @@
 	end)
 	tabs:SetCallback("OnTabLeave", statusy_OnLeave)
 	tabs:SetUserData("special buttons group",tab_specials)
-	tabs:SelectTab(opt_tabselect or "eoi")
+	tabs:SelectTab((opt_tabselect and #opt_tabselect>0)
+		and opt_tabselect or "eoi")
 
 	display:AddChildren (tabs, control)
 	display:ApplyStatus()
Binary file sfrr.ogg has changed
--- a/verbage.lua	Fri Jun 01 02:17:57 2012 +0000
+++ b/verbage.lua	Fri Jun 08 08:05:37 2012 +0000
@@ -396,9 +396,9 @@
 +Clear Realm History> and +Clear ALL History> are used to periodically wipe the
 slate clean.  They do not generate any new entries from existing loot.
 
-+Clear Older> deletes history information for all items not shown in the "most recent
-loot" display.  It is another good periodic maintenance step, but does not discard
-as much data as the other actions.
++Clear Older> deletes history information older than a certain threshold (by
+default, 5 loot events).  It is another good periodic maintenance step, but
+does not discard as much data as the other actions.
 
 Using +Reassign to...> will also move the item between player histories.  The timestamp
 will not be changed; it will "always have been" received by the new recipient.