view core.lua @ 24:61d932f0e8f2

Reassigning loot in the main tab also updates entries in the history tab.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Wed, 21 Sep 2011 06:21:48 +0000
parents 8664134bba4f
children cb9635999171
line wrap: on
line source
local nametag, addon = ...

--[==[
g_loot's numeric indices are loot entries (including titles, separators,
etc); its named indices are:
- forum:		saved text from forum markup window, default nil
- attend:		saved text from raid attendence window, default nil
- printed.FOO:	last loot index formatted into text window FOO, default 0

Functions arranged like this, with these lables (for jumping to).  As a
rule, member functions with UpperCamelCase names are called directly by
user-facing code, ones with lowercase names are "one step removed", and
names with leading underscores are strictly internal helper functions.
------ Saved variables
------ Constants
------ Addon member data
------ Globals
------ Expiring caches
------ Ace3 framework stuff
------ Event handlers
------ Slash command handler
------ On/off
------ Behind the scenes routines
------ Saved texts
------ Loot histories
------ Player communication

This started off as part of a raid addon package written by somebody else.
After he retired, I began modifying the code.  Eventually I set aside the
entire package and rewrote the loot tracker module from scratch.  Many of the
variable/function naming conventions (sv_*, g_*, and family) stayed across the
rewrite.

Some variables are needlessly initialized to nil just to look uniform.

]==]

------ Saved variables
OuroLootSV       = nil   -- possible copy of g_loot
OuroLootSV_saved = nil   -- table of copies of saved texts, default nil; keys
                         -- are numeric indices of tables, subkeys of those
						 -- are name/forum/attend/date
OuroLootSV_opts  = nil   -- same as option_defaults until changed
                         -- autoshard:  optional name of disenchanting player, default nil
                         -- threshold:  optional loot threshold, default nil
OuroLootSV_hist  = nil
OuroLootSV_log   = {}


------ Constants
local option_defaults = {
	['popup_on_join'] = true,
	['register_slashloot'] = true,
	['scroll_to_bottom'] = true,
	['chatty_on_kill'] = false,
	['no_tracking_wipes'] = false,
	['snarky_boss'] = true,
	['keybinding'] = false,
	['bossmod'] = "DBM",
	['keybinding_text'] = 'CTRL-SHIFT-O',
	['forum'] = {
		['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
		['[item] by name'] = '[item]$N[/item]$X - $T',
		['[item] by ID'] = '[item]$I[/item]$X - $T',
		['Custom...'] = '',
	},
	['forum_current'] = '[item] by name',
}
local virgin = "First time loaded?  Hi!  Use the /ouroloot or /loot command"
	.." to show the main display.  You should probably browse the instructions"
	.." if you've never used this before; %s to display the help window.  This"
	.." welcome message will not intrude again."
local qualnames = {
	['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0,
	['white'] = 1, ['common'] = 1,
	['green'] = 2, ['uncommon'] = 2,
	['blue'] = 3, ['rare'] = 3,
	['epic'] = 4, ['purple'] = 4,
	['legendary'] = 5, ['orange'] = 5,
	['artifact'] = 6,
	--['heirloom'] = 7,
}
local my_name				= UnitName('player')
local comm_cleanup_ttl		= 5   -- seconds in the cache
local g_LOOT_ITEM_ss, g_LOOT_ITEM_MULTIPLE_sss, g_LOOT_ITEM_SELF_s, g_LOOT_ITEM_SELF_MULTIPLE_ss


------ Addon member data
local flib = LibStub("LibFarmbuyer")
addon.author_debug = flib.author_debug

-- Play cute games with namespaces here just to save typing.  WTB Lua 5.2 PST.
do local _G = _G setfenv (1, addon)

	commrev			= 15  -- number
	revision		= _G.GetAddOnMetadata(nametag,"Version") or "?"
	ident			= "OuroLoot2"
	identTg			= "OuroLoot2Tg"
	status_text		= nil

	DEBUG_PRINT		= false
	debug = {
		comm = false,
		loot = false,
		flow = false,
		notraid = false,
		cache = false,
		alsolog = false,
	}
	function dprint (t,...)
		if DEBUG_PRINT and debug[t] then
			local text = flib.safeprint("<"..t.."> ",...)
			if debug.alsolog then
				addon:log_with_timestamp(text)
			end
		end
	end

	if author_debug then
		function pprint(t,...)
			local text = flib.safeprint("<<"..t..">> ",...)
			if debug.alsolog then
				addon:log_with_timestamp(text)
			end
		end
	else
		pprint = flib.nullfunc
	end

	enabled			= false
	rebroadcast		= false
	display			= nil   -- display frame, when 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:

	popped			= nil   -- non-nil when reminder has been shown, actual value unimportant

	-- This is an amalgamation of all four LOOT_ITEM_* patterns.
	-- Captures:   1 person/You, 2 itemstring, 3 rest of string after final |r until '.'
	-- Can change 'loot' to 'item' to trigger on, e.g., extracting stuff from mail.
	--loot_pattern	= "(%S+) receives? loot:.*|cff%x+|H(.-)|h.*|r(.*)%.$"

	bossmod_registered = nil
	bossmods = {}

	requesting		= nil   -- for prompting for additional rebroadcasters

	thresholds = {}
	for i = 0,6 do
		thresholds[i] = _G.ITEM_QUALITY_COLORS[i].hex .. _G["ITEM_QUALITY"..i.."_DESC"] .. "|r"
	end

	_G.setfenv (1, _G)
end

addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot",
                "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0")


------ Globals
local g_loot			= nil
local g_restore_p		= nil
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 opts				= nil

local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber
local pprint, tabledump = addon.pprint, flib.tabledump
local GetNumRaidMembers = GetNumRaidMembers
-- En masse forward decls of symbols defined inside local blocks
local _register_bossmod
local makedate, create_new_cache, _init

-- Hypertext support, inspired by DBM broadcast pizza timers
do
	local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h"

	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)
	end

	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton)
		local ltype, arg = strsplit(":",link)
		if ltype ~= "OuroRaid" then return end
		if arg == 'openloot' then
			addon:BuildMainDisplay()
		elseif arg == 'help' then
			addon:BuildMainDisplay('help')
		elseif arg == 'bcaston' then
			if not addon.rebroadcast then
				addon:Activate(nil,true)
			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
		end
	end)

	local old = ItemRefTooltip.SetHyperlink
	function ItemRefTooltip:SetHyperlink (link, ...)
		if link:match("^OuroRaid") then return end
		return old (self, link, ...)
	end
end

do
	-- copied here because it's declared local to the calendar ui, thanks blizz ><
	local CALENDAR_FULLDATE_MONTH_NAMES = {
		FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH,
		FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE,
		FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER,
		FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER,
	}
	-- returns "dd Month yyyy", mm, dd, yyyy
	function makedate()
		Calendar_LoadUI()
		local _, M, D, Y = CalendarGetDate()
		local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y)
		return text, M, D, Y
	end
end

-- Returns an instance name or abbreviation
local function instance_tag()
	local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo()
	local t
	name = addon.instance_abbrev[name] or name
	if typeof == "none" then return name end
	-- diffstr is "5 Player", "10 Player (Heroic)", etc.  ugh.
	if diffcode == 1 then
		t = ((GetNumRaidMembers()>0) and "10" or "5")
	elseif diffcode == 2 then
		t = ((GetNumRaidMembers()>0) and "25" or "5h")
	elseif diffcode == 3 then
		t = "10h"
	elseif diffcode == 4 then
		t = "25h"
	end
	-- dynamic difficulties always return normal "codes"
	if isdynamic and perbossheroic == 1 then
		t = t .. "h"
	end
	return name .. "(" .. t .. ")"
end
addon.instance_tag = instance_tag   -- grumble


------ Expiring caches
--[[
foo = create_new_cache("myfoo",15[,cleanup]) -- ttl
foo:add("blah")
foo:test("blah")   -- returns true
]]
do
	local caches = {}
	local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
	local time = _G.time
	cleanup_group:SetLooping("REPEAT")
	cleanup_group:SetScript("OnLoop", function(cg)
		addon.dprint('cache',"OnLoop firing")
		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)
			end
			alldone = alldone and (#c == 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})
		if not cleanup_group:IsPlaying() then
			addon.dprint('cache', cache.name, "STARTING animation group")
			cache.cleanup:SetDuration(2)  -- hmmm
			cleanup_group:Play()
		end
	end
	local function _test (cache, x)
		for _,v in ipairs(cache) do
			if v.m == x then return true end
		end
	end
	function create_new_cache (name, ttl, on_alldone)
		local c = {
			ttl = ttl,
			name = name,
			add = _add,
			test = _test,
			cleanup = cleanup_group:CreateAnimation("Animation"),
			func = on_alldone,
		}
		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)
		return c
	end
end


------ Ace3 framework stuff
function addon:OnInitialize()
	-- VARIABLES_LOADED has fired by this point; test if we're doing something like
	-- relogging during a raid and already have collected loot data
	g_restore_p = OuroLootSV ~= nil
	self.dprint('flow', "oninit sets restore as", g_restore_p)

	if OuroLootSV_opts == nil then
		OuroLootSV_opts = {}
		self:ScheduleTimer(function(s)
			s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
			virgin = nil
		end,10,self)
	end
	opts = OuroLootSV_opts
	for opt,default in pairs(option_defaults) do
		if opts[opt] == nil then
			opts[opt] = default
		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
		end
		if OuroLootSV.threshold then
			opts.threshold = OuroLootSV.threshold; OuroLootSV.threshold = nil
		end
		if OuroLootSV.autoshard then
			opts.autoshard = OuroLootSV.autoshard; OuroLootSV.autoshard = nil
		end
	end
	-- get item filter table if needed
	if opts.itemfilter == nil then
		opts.itemfilter = addon.default_itemfilter
	end
	addon.default_itemfilter = nil

	self:RegisterChatCommand("ouroloot", "OnSlash")
	-- maybe try to detect if this command is already in use...
	if opts.register_slashloot then
		SLASH_ACECONSOLE_OUROLOOT2 = "/loot"
	end

	self.history_all = self.history_all or OuroLootSV_hist or {}
	local r = assert(GetRealmName())
	self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
	self.history = self.history_all[r]
	--OuroLootSV_hist = nil

	_init(self)
	self.OnInitialize = nil
end

function addon:OnEnable()
	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
	-- AceConsole:Printf isn't used because we can't specify a prefix without
	-- jumping through ridonkulous hoops.)  The part about overriding :Print
	-- with a version using prefix hyperlinks is my fault.
	do
		local AC = LibStub("AceConsole-3.0")
		local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
		function addon:Print (str, ...)
			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
				return AC:Print (chat_prefix, str:format(...))
			else
				return AC:Print (chat_prefix, str, ...)
			end
		end
	end

	if opts.keybinding then
		KeyBindingFrame_LoadUI()
		local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
		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
			local c = GetCurrentBindingSet()
			if c == ACCOUNT_BINDINGS or c == CHARACTER_BINDINGS then
				SaveBindings(c)
			end
		else
			self:Print("Error registering '%s' as a keybinding, check spelling!", opts.keybinding_text)
		end
	end

	--[[
	The four loot format patterns of interest, changed into relatively tight
	string match patterns.  Done at enable-time rather than load-time against
	the slim chance that one of the non-US "delocalizers" needs to mess with
	the global patterns before we transform them.
	
	The SELF variants can be replaced with LOOT_ITEM_PUSHED_SELF[_MULTIPLE] to
	trigger on 'receive item' instead, which would detect extracting stuff
	from mail, or s/PUSHED/CREATED/ for things like healthstones and guild
	cauldron flasks.
	]]

	-- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%.
	g_LOOT_ITEM_ss = _G.LOOT_ITEM:gsub('%.$','%%.'):gsub('%%s','(.+)')

	-- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%.
	g_LOOT_ITEM_MULTIPLE_sss = _G.LOOT_ITEM_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)')

	-- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%.
	g_LOOT_ITEM_SELF_s = _G.LOOT_ITEM_SELF:gsub('%.$','%%.'):gsub('%%s','(.+)')

	-- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%.
	g_LOOT_ITEM_SELF_MULTIPLE_ss = _G.LOOT_ITEM_SELF_MULTIPLE:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)')

	if self.debug.flow then self:Print"is in control-flow debug mode." end
end
--function addon:OnDisable() end


------ 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()
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)
	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
	if worth_saving then
		opts.autoshard = self.sharder
		opts.threshold = self.threshold
		for i,e in ipairs(g_loot) do
			e.cols = nil
		end
		OuroLootSV = g_loot
	else
		OuroLootSV = nil
	end

	for r,t in pairs(self.history_all) do if type(t) == 'table' then
		if #t == 0 then
			self.history_all[r] = nil
		else
			t.realm = nil
			t.st = nil
			t.byname = nil
		end
	end end
	OuroLootSV_hist = self.history_all
	OuroLootSV_log = #OuroLootSV_log > 0 and OuroLootSV_log or nil
end

do
	local IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex,
				UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo =
	      IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex,
		  		UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo
	local time, difftime = time, difftime
	local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3

	local lastevent, now = 0, 0
	local redo_count = 0
	local redo, timer_handle

	function addon:CheckRoster (leaving_p, now_a)
		if not g_loot.raiders then return end -- bad transition

		now = now_a or time()

		if leaving_p then
			if timer_handle then
				self:CancelTimer(timer_handle)
				timer_handle = nil
			end
			for name,r in pairs(g_loot.raiders) do
				r.leave = r.leave or now
			end
			return
		end

		for name,r in pairs(g_loot.raiders) do
			if r.online ~= R_LEFT and not UnitInRaid(name) then
				r.online = R_LEFT
				r.leave = now
			end
		end

		if redo then
			redo_count = redo_count + 1
		end
		redo = false
		for i = 1, GetNumRaidMembers() do
			local unit = 'raid'..i
			local name = UnitName(unit)
			-- No, that's not my typo, it really is "uknownbeing" in Blizzard's code.
			if name and name ~= UNKNOWN and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then
				if not g_loot.raiders[name] then
					g_loot.raiders[name] = { needinfo=true }
				end
				local r = g_loot.raiders[name]
				if r.needinfo and UnitIsVisible(unit) then
					r.needinfo = nil
					r.class    = select(2,UnitClass(unit))
					r.race     = select(2,UnitRace(unit))
					r.sex      = UnitSex(unit)
					r.level    = UnitLevel(unit)
					r.guild    = GetGuildInfo(unit)
				end
				local connected = UnitIsConnected(unit)
				if connected and r.online ~= R_ACTIVE then
					r.join = r.join or now
					r.online = R_ACTIVE
				elseif (not connected) and r.online ~= R_OFFLINE then
					r.leave = now
					r.online = R_OFFLINE
				end
				redo = redo or r.needinfo
			end
		end
		if redo then  -- XXX test redo_count here and eventually give up?
			if not timer_handle then
				timer_handle = self:ScheduleRepeatingTimer("RAID_ROSTER_UPDATE", 60)
			end
		else
			redo_count = 0
			if timer_handle then
				self:CancelTimer(timer_handle)
				timer_handle = nil
			end
		end
	end

	function addon:RAID_ROSTER_UPDATE (event)
		if GetNumRaidMembers() == 0 then
			-- Leaving a raid group.
			-- Because of PLAYER_ENTERING_WORLD, this code also executes on load
			-- screens while soloing and in regular groups.  Take care.
			self.dprint('flow', "GetNumRaidMembers == 0")
			if self.enabled and not self.debug.notraid then
				self.dprint('flow', "enabled, leaving raid")
				self.popped = nil
				self:Deactivate()  -- self:UnregisterEvent("CHAT_MSG_LOOT")
				self:CheckRoster(--[[leaving raid]]true)
			end
			return
		end

		local inside,whatkind = IsInInstance()
		if inside and (whatkind == "pvp" or whatkind == "arena") then
			return self.dprint('flow', "got RRU event but in pvp zone, bailing")
		end

		local docheck = self.enabled
		if event == "Activate" then
			-- dispatched manually from Activate
			self:RegisterEvent("CHAT_MSG_LOOT")
			_register_bossmod(self)
			docheck = true
		elseif event == "RAID_ROSTER_UPDATE" then
			-- hot code path, be careful

			-- event registration from onload, joined a raid, maybe show popup
			self.dprint('flow', "RRU check:", self.popped, opts.popup_on_join)
			if (not self.popped) and opts.popup_on_join then
				self.popped = StaticPopup_Show "OUROL_REMIND"
				self.popped.data = self
				return
			end
		end
		-- Throttle the checks fired by common events.
		if docheck and not InCombatLockdown() then
			now = time()
			if difftime(now,lastevent) > 45 then
				lastevent = now
				self:CheckRoster(false,now)
			end
		end
	end
end

-- helper for CHAT_MSG_LOOT handler
do
	-- Recent loot cache
	addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl)

	local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon

	-- 'from' and onwards only present if this is triggered by a broadcast
	function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
		local itexture = GetItemIcon(itemid)
		local iname, ilink, iquality = GetItemInfo(itemid)
		local i
		if (not iname) or (not itexture) then
			i = true
			iname, ilink, iquality, itexture = 
				UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]]
		end
		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
			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
			end
		end
		self.dprint('loot',"<<_do_loot out")
		return i
	end

	function addon:CHAT_MSG_LOOT (event, ...)
		if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end

		--[[
			iname:		Hearthstone
			iquality:	integer
			ilink:		clickable formatted link
			itemstring:	item:6948:....
			itexture:	inventory icon texture
		]]

		if event == "CHAT_MSG_LOOT" then
			local msg = ...
			local person, itemstring, count
			--ChatFrame2:AddMessage("original string:  >"..(msg:gsub("\124","\124\124")).."<")
			--local person, itemstring, remainder = msg:match(self.loot_pattern)

			-- test in most likely order:  other people get more loot than "you" do
			person, itemstring, count = msg:match(g_LOOT_ITEM_MULTIPLE_sss)
			if not person then
				person, itemstring = msg:match(g_LOOT_ITEM_ss)
			end
			if not person then
				itemstring, count = msg:match(g_LOOT_ITEM_SELF_MULTIPLE_ss)
				if not itemstring then
					itemstring = msg:match(g_LOOT_ITEM_SELF_s)
				end
			end

			--self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder)
			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
			--local count = remainder and remainder:match(".*(x%d+)$")

			-- Name might be colorized, remove the highlighting
			if person then
				person = person:match("|c%x%x%x%x%x%x%x%x(%S+)") or person
			else
				-- UNIT_YOU / You
				person = my_name
			end

			--local id = tonumber((select(2, strsplit(":", itemstring))))
			local id = tonumber(itemstring:match('|Hitem:(%d+):'))

			return self:_do_loot (false, person, id, count)

		elseif event == "broadcast" then
			return self:_do_loot(false, ...)

		elseif event == "manual" then
			local r,i,n = ...
			return self:_do_loot(true, r,i,nil,nil,n)
		end
	end
end


------ Slash command handler
-- Thought about breaking this up into a table-driven dispatcher.  But
-- 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("Can't display window in combat.")
		else
			return self:BuildMainDisplay()
		end

	elseif cmd:find("^thre") then
		self:SetThreshold(arg)

	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._addLootEntry{
			kind='boss',reason='kill',bosskill="Baron Steamroller",instance=instance_tag(),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)
		if q<0 or q>6 then
			return self:Print("Threshold must be 0-6.")
		end
	else
		q = qualnames[arg]
		if not q then
			return self:Print("Unrecognized item quality argument.")
		end
	end
	self.threshold = q
	if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end
end


------ On/off
function addon:Activate (opt_threshold, opt_bcast_only)
	self.dprint('flow', ":Activate is running")
	self:RegisterEvent("RAID_ROSTER_UPDATE")
	self:RegisterEvent("PLAYER_ENTERING_WORLD",
		function() self:ScheduleTimer("RAID_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end)
	self.popped = true
	if GetNumRaidMembers() > 0 then
		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:RegisterEvent("CHAT_MSG_LOOT")
		_register_bossmod(self)
	elseif g_restore_p then
		g_restore_p = nil
		self.popped = nil  -- get the reminder if later joining a raid
		if #g_loot == 0 then
			-- only generated text and raider join/leave data, not worth verbage
			self.dprint('flow', ">:Activate restored generated texts, un-popping")
			return
		end
		self:Print("Ouro Raid Loot restored previous data, but not in a raid",
				"and 5-person mode not active.  |cffff0505NOT tracking loot|r;",
				"use 'enable' to activate loot tracking, or 'clear' to erase",
				"previous data, or 'help' to read about saved-texts commands.")
		return
	end
	self.rebroadcast = true  -- hardcode to true; this used to be more complicated
	self.enabled = not opt_bcast_only
	if opt_threshold then
		self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
	end
	self:Print("Ouro Raid Loot is %s.  Threshold currently %s.",
		self.enabled and "tracking" or "only broadcasting",
		self.thresholds[self.threshold])
end

-- Note:  running '/loot off' will also avoid the popup reminder when
-- joining a raid, but will not change the saved option setting.
function addon:Deactivate()
	self.enabled = false
	self.rebroadcast = false
	self:UnregisterEvent("RAID_ROSTER_UPDATE")
	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self:UnregisterEvent("CHAT_MSG_LOOT")
	self:Print("Ouro Raid Loot deactivated.")
end

function addon:Clear(verbose_p)
	local repopup, st
	if self.display then
		-- in the new version, this is likely to always be the case
		repopup = true
		st = self.display:GetUserData("eoiST")
		if not st then
			self.dprint('flow', "Clear: display visible but eoiST not set??")
		end
		self.display:Hide()
	end
	g_restore_p = nil
	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)
		else
			self:Print("Current loot data cleared.")
		end
	end
	_init(self,st)
	if repopup then
		addon:BuildMainDisplay()
	end
end


------ Behind the scenes routines
-- Semi-experimental debugging aid.
do
	local date = _G.date
	local log = OuroLootSV_log
	function addon:log_with_timestamp (msg)
		tinsert (log, date('%m:%d %H:%M:%S  ')..msg)
	end
end

-- Adds indices to traverse the tables in a nice sorted order.
do
	local byindex, temp = {}, {}
	local function sort (src, dest)
		for k in pairs(src) do
			temp[#temp+1] = k
		end
		table.sort(temp)
		table.wipe(dest)
		for i = 1, #temp do
			dest[i] = src[temp[i]]
		end
	end

	function addon.sender_list.sort()
		sort (addon.sender_list.active, byindex)
		table.wipe(temp)
		addon.sender_list.activeI = #byindex
		sort (addon.sender_list.names, byindex)
		table.wipe(temp)
	end
	addon.sender_list.namesI = byindex
end

-- Message sending.
-- See OCR_funcs.tag at the end of this file for incoming message treatment.
do
	local function assemble(...)
		local msg = ...
		for i = 2, select('#',...) do
			msg = msg .. '\a' .. (select(i,...) or "")
		end
		return msg
	end

	-- broadcast('tag', <stuff>)
	function addon:broadcast(...)
		local msg = assemble(...)
		self.dprint('comm', "<broadcast>:", msg)
		-- the "GUILD" here is just so that we can also pick up on it
		self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID")
	end
	-- whispercast(<to>, 'tag', <stuff>)
	function addon:whispercast(to,...)
		local msg = assemble(...)
		self.dprint('comm', "<whispercast>@", to, ":", msg)
		self:SendCommMessage(self.identTg, msg, "WHISPER", to)
	end
end

function addon:DoPing()
	self:Print("Give me a ping, Vasili. One ping only, please.")
	self.sender_list.active = {}
	self.sender_list.names = {}
	self:broadcast('ping')
end

-- Generic helpers
function addon._find_next_after (kind, index)
	index = index + 1
	while index <= #g_loot do
		if g_loot[index].kind == kind then
			return index, g_loot[index]
		end
		index = index + 1
	end
end

-- Iterate through g_loot entries according to the KIND field.  Loop variables
-- are g_loot indices and the corresponding entries (essentially ipairs + some
-- conditionals).
function addon:filtered_loot_iter (filter_kind)
	return self._find_next_after, filter_kind, 0
end

do
	local itt
	local function create()
		local tip, lefts = CreateFrame("GameTooltip"), {}
		for i = 1, 2 do -- scanning idea here also snagged from Talented
			local L,R = tip:CreateFontString(), tip:CreateFontString()
			L:SetFontObject(GameFontNormal)
			R:SetFontObject(GameFontNormal)
			tip:AddFontStrings(L,R)
			lefts[i] = L
		end
		tip.lefts = lefts
		return tip
	end
	function addon:is_heroic_item(item)   -- returns true or *nil*
		itt = itt or create()
		itt:SetOwner(UIParent,"ANCHOR_NONE")
		itt:ClearLines()
		itt:SetHyperlink(item)
		local t = itt.lefts[2]:GetText()
		itt:Hide()
		return (t == ITEM_HEROIC) or nil
	end
end

-- Called when first loading up, and then also when a 'clear' is being
-- performed.  If SV's are present then g_restore_p will be true.
function _init (self, possible_st)
	self.dprint('flow',"_init running")
	self.loot_clean = nil
	self.hist_clean = nil
	if g_restore_p then
		g_loot = OuroLootSV
		self.popped = #g_loot > 0
		self.dprint('flow', "restoring", #g_loot, "entries")
		self:ScheduleTimer("Activate", 12, opts.threshold)
		-- FIXME printed could be too large if entries were deleted, how much do we care?
		self.sharder = opts.autoshard
	else
		g_loot = { printed = {}, raiders = {} }
	end

	self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking
	self:gui_init(g_loot)
	opts.autoshard = nil
	opts.threshold = nil

	if g_restore_p then
		self:zero_printed_fenceposts()                  -- g_loot.printed.* = previous/safe values
	else
		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 %d"):format(self.revision,self.ident,self.commrev)
	self:RegisterComm(self.ident)
	self:RegisterComm(self.identTg, "OnCommReceivedNocache")

	if self.author_debug then
		_G.OL = self
		_G.Oloot = g_loot
	end
end

-- Tie-ins with Deadly Boss Mods
do
	local candidates, 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
			-- (1) or (2)
			boss.duration = boss.duration or 0
			addon.dprint('loot', "only one boss candidate")
		else
			-- (3), should only be one 'cast entry and our local entry
			if #candidates ~= 2 then
				-- could get a bunch of 'cast entries on the heels of one another
				-- before the local one ever fires, apparently... sigh
				--addon:Print("<warning> s3 cache has %d entries, does that seem right to you?", #candidates)
			end
			if candidates[2].duration == nil then
				--addon:Print("<warning> s3's second entry is not the local trigger, does that seem right to you?")
			end
			-- try and be generic anyhow
			for i,c in ipairs(candidates) do
				if c.duration then
					boss = c
					addon.dprint('loot', "fixup found boss candidate", i, "duration", c.duration)
					break
				end
			end
		end
		bossi = addon._addLootEntry(boss)
		-- addon.
		bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi
		g_boss_signpost = nil
		addon.dprint('loot', "added boss entry", bossi)
		if boss.reason == 'kill' then
			addon:_mark_boss_kill (bossi)
			if opts.chatty_on_kill then
				addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
			end
		end
		candidates = nil
	end
	addon.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, duration, raiders)
		self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag,
		            "D:", duration, "RL:", (raiders and #raiders or 'nil'))
		if self.rebroadcast and duration then
			self:broadcast('boss', reason, bossname, intag)
		end
		-- This is only a loop to make jumping out of it easy, and still do cleanup below.
		while self.enabled do
			if reason == 'wipe' and opts.no_tracking_wipes then break end
			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
				self.dprint('cache', "boss <",signature,"> already in cache, skipping")
			else
				self.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
				-- outside the instance) and so this only happens once as a non-local event,
				-- (2) we see a local event first and all non-local events are filtered
				-- by the cache, (3) we happen to get some non-local events before doing
				-- our local event (not because of network weirdness but because our local
				-- DBM might not trigger for a while).
				local c = {
					kind		= 'boss',
					bosskill	= bossname,      -- minor misnomer, might not actually be a kill
					reason		= reason,
					instance	= intag,
					duration	= duration,      -- these two deliberately may be nil
					raiderlist	= raiders and table.concat(raiders, ", ")
				}
				candidates = candidates or {}
				tinsert(candidates,c)
			end
			break
		end
		self.dprint('loot',"<<_do_boss out")
	end
	-- No wrapping layer for now
	addon.on_boss_broadcast = _do_boss

	function addon:_mark_boss_kill (index)
		local e = g_loot[index]
		if not e then
			self:Print("Something horribly wrong;", index, "is not a valid entry!")
			return
		end
		if not e.bosskill then
			self:Print("Something horribly wrong;", index, "is not a boss entry!")
			return
		end
		if e.reason ~= 'wipe' then
			-- enh, bail
			self.loot_clean = index-1
		end
		local attempts = 1
		local first

		local i,d = 1,g_loot[1]
		while d ~= e do
			if d.bosskill and
			   d.bosskill == e.bosskill and
			   d.reason == 'wipe'
			then
				first = first or i
				attempts = attempts + 1
				assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly")
			else
				i = i + 1
			end
			d = g_loot[i]
		end
		e.reason = 'kill'
		e.attempts = attempts
		self.loot_clean = first or index-1
	end

	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
		self.bossmods[#self.bossmods+1] = {
			n = name,
			r = registration_func,
			d = deregistration_func,
		}
		self.bossmods[name] = self.bossmods[#self.bossmods]
		assert(self.bossmods[name].n == self.bossmods[#self.bossmods].n)
	end

	function _register_bossmod (self, force_p)
		local x = self.bossmod_registered and self.bossmods[self.bossmod_registered]
		if x then
			if x.n == opts.bossmod and not force_p then
				-- trying to register with already-registered boss mod
				return
			else
				-- deregister
				if x.d then x.d(self) end
			end
		end

		x = nil
		for k,v in ipairs(self.bossmods) do
			if v.n == opts.bossmod then
				x = k
				break
			end
		end

		if not x then
			self.status_text = "|cffff1010No boss-mod found!|r"
			self:Print(self.status_text)
			return
		end

		if self.bossmods[x].r (self, _do_boss) then
			--self.bossmod_registered = x
			self.bossmod_registered = self.bossmods[x].n
		else
			self:Print("|cffff1010Boss mod registration failed|r")
		end
	end
end

-- Adding entries to the loot record, and tracking the corresponding timestamp.
do
	-- This shouldn't be required.  /sadface
	local loot_entry_mt = {
		__index = function (e,key)
			if key == 'cols' then
				pprint('mt', e.kind, "key is", key)
				--tabledump(e)  -- not actually that useful
				addon:_fill_out_eoi_data(1)
			end
			return rawget(e,key)
		end
	}

	-- Given a loot index, searches backwards for a timestamp.  Returns that
	-- index and the time entry, or nil if it falls off the beginning.  Pass an
	-- optional second index to search no earlier than it.
	-- May also be able to make good use of this in forum-generation routine.
	function addon:find_previous_time_entry(i,stop)
		stop = stop or 0
		while i > stop do
			if g_loot[i].kind == 'time' then
				return i, g_loot[i]
			end
			i = i - 1
		end
	end

	-- format_timestamp (["format_string"], Day, [Loot])
	-- DAY is a loot entry with kind=='time', and controls the date printed.
	-- LOOT may be any kind of entry in the g_loot table.  If present, it
	--    overrides the hour and minute printed; if absent, those values are
	--    taken from the DAY entry.
	-- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens.
	local format_timestamp_values, point2dee = {}, "%.2d"
	function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt)
		if not time_entry_opt then
			if type(fmt_opt) == 'table' then        -- Two entries, default format
				time_entry_opt, day_entry = day_entry, fmt_opt
				fmt_opt = "$Y/$M/$D $h:$m"
			--elseif type(fmt_opt) == "string" then   -- Day entry only, specified format
			end
		end
		--format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100)
		format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year)
		format_timestamp_values.M = point2dee:format (day_entry.startday.month)
		format_timestamp_values.D = point2dee:format (day_entry.startday.day)
		format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour)
		format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute)
		return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values)
	end

	local done_todays_date
	function addon:_reset_timestamps()
		done_todays_date = nil
	end
	local function do_todays_date()
		local text, M, D, Y = makedate()
		local found,ts = #g_loot+1
		repeat
			found,ts = addon:find_previous_time_entry(found-1)
			if found and ts.startday.text == text then
				done_todays_date = true
			end
		until done_todays_date or (not found)
		if done_todays_date then
			g_today = ts
		else
			done_todays_date = true
			g_today = g_loot[addon._addLootEntry{
				kind = 'time',
				startday = {
					text = text, month = M, day = D, year = Y
				}
			}]
		end
		addon:_fill_out_eoi_data(1)
	end

	-- Adding anything original to g_loot goes through this routine.
	function addon._addLootEntry (e)
		setmetatable(e,loot_entry_mt)

		if not done_todays_date then do_todays_date() end

		local h, m = GetGameTime()
		--local localuptime = math.floor(GetTime())
		local time_t = time()
		e.hour = h
		e.minute = m
		e.stamp = time_t --localuptime
		local index = #g_loot + 1
		g_loot[index] = e
		return index
	end

	-- Problem:  (1) boss kill happens, (2) fast looting happens, (3) boss
	-- cache cleanup fires.  Result:  loot shows up before boss kill entry.
	-- Solution:  We need to shuffle the boss entry above any of the loot
	-- from that boss.
	function addon._adjustBossOrder (is, should_be)
		--pprint('loot', is, should_be)
		if is == should_be then --pprint('loot', "equal, yay")
			return
		end
		if (type(is)~='number') or (type(should_be)~='number') or (is < should_be) then
			pprint('loot', is, should_be, "...the hell? bailing")
			return
		end
		if g_loot[should_be].kind == 'time' then
			should_be = should_be + 1
			if is == should_be then
				--pprint('loot', "needed to mark day entry, otherwise equal, yay")
				return
			end
		end

		assert(g_loot[is].kind == 'boss')
		local boss = tremove (g_loot, is)
		--pprint('loot', "MOVING", boss.bosskill)
		tinsert (g_loot, should_be, boss)
		return should_be
	end
end

-- In the rare case of items getting put into the loot table without current
-- item cache data (which will have arrived by now).
function addon:do_item_cache_fixup()
	self:Print("Fixing up missing item cache data...")

	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
				end
			end
		end
	end

	self:Print("...finished.  Found %d |4entry:entries; with weird data.", numfound)
end


------ Saved texts
function addon:check_saved_table(silent_p)
	local s = OuroLootSV_saved
	if s and (#s > 0) then return s end
	OuroLootSV_saved = nil
	if not silent_p then self:Print("There are no saved loot texts.") end
end

function addon:save_list()
	local s = self:check_saved_table(); if not s then return end;
	for i,t in ipairs(s) do
		self:Print("#%d   %s    %d entries     %s", i, t.date, t.count, t.name)
	end
end

function addon:save_saveas(name)
	OuroLootSV_saved = OuroLootSV_saved or {}
	local SV = OuroLootSV_saved
	local n = #SV + 1
	local save = {
		name = name,
		date = makedate(),
		count = #g_loot,
	}
	for text in self:registered_textgen_iter() do
		save[text] = g_loot[text]
	end
	self:Print("Saving current loot texts to #%d '%s'", n, name)
	SV[n] = save
	return self:save_list()
end

function addon:save_restore(num)
	local s = self:check_saved_table(); if not s then return end;
	if (not num) or (num > #s) then
		return self:Print("Saved text number must be 1 - "..#s)
	end
	local save = s[num]
	self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name)
	self:Clear(--[[verbose_p=]]false)
	-- Clear will already have displayed the window, and re-selected the first
	-- tab.  Set these up for when the text tabs are clicked.
	for text in self:registered_textgen_iter() do
		g_loot[text] = save[text]
	end
end

function addon:save_delete(num)
	local s = self:check_saved_table(); if not s then return end;
	if (not num) or (num > #s) then
		return self:Print("Saved text number must be 1 - "..#s)
	end
	self:Print("Deleting saved text #"..num)
	tremove(s,num)
	return self:save_list()
end


------ Loot histories
-- history_all = {
--   ["Kilrogg"] = {
--     ["realm"] = "Kilrogg",                 -- not saved
--     ["st"] = { lib-st display table },     -- not saved
--     ["byname"] = {                         -- not saved
--       ["OtherPlayer"] = 2,
--       ["Farmbuyer"] = 1,
--     }
--     [1] = {
--       ["name"] = "Farmbuyer",
--       [1] = { id = nnnnn, when = "formatted timestamp for displaying" }  -- most recent loot
--       [2] = { ......., [count = "x3"]                                 }  -- previous loot
--     },
--     [2] = {
--       ["name"] = "OtherPlayer",
--       ......
--     }, ......
--   },
--   ["OtherRealm"] = ......
-- }
do
	local tsort = table.sort
	local comp = function(L,R) return L.when > R.when end

	-- Builds the map of names to array indices, using passed table or
	-- self.history, and stores the result into its 'byname' field.  Also
	-- called from the GUI code at least once.
	function addon:_build_history_names (opt_hist)
		local hist = opt_hist or self.history
		local m = {}
		for i = 1, #hist do
			m[hist[i].name] = i
		end
		hist.byname = m
	end

	-- Prepares and returns table to be used as self.history.
	function addon:_prep_new_history_category (prev_table, realmname)
		local t = prev_table or {
			--kind = 'realm',
			--realm = realmname,
		}
		t.realm = realmname

		--[[
		t.cols = setmetatable({
			{ value = realmname },
		}, self.time_column1_used_mt)
		]]

		if not t.byname then
			self:_build_history_names (t)
		end

		return t
	end

	-- Maps a name to an array index, creating new tables if needed.  Returns
	-- the index and the table at that index.
	function addon:get_loot_history (name)
		local i
		i = self.history.byname[name]
		if not i then
			i = #self.history + 1
			self.history[i] = { name=name }
			self.history.byname[name] = i
		end
		return i, self.history[i]
	end

	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 n = {
			id = e.id,
			when = self:format_timestamp (g_today, e),
			count = e.count,
		}
		tinsert (h, 1, n)
		e.history_unique = n.id .. ' ' .. n.when
	end

	-- Create new history table based on current loot.
	function addon:rewrite_history (realmname)
		local r = assert(realmname)
		self.history_all[r] = self:_prep_new_history_category (nil, r)
		self.history = self.history_all[r]

		local g_today_real = g_today
		for i,e in ipairs(g_loot) do
			if e.kind == 'time' then
				g_today = e
			elseif e.kind == 'loot' then
				self:_addHistoryEntry(i)
			end
		end
		g_today = g_today_real
		self.hist_clean = nil

		-- safety measure:  resort players' tables based on formatted timestamp
		for i,h in ipairs(self.history) do
			tsort (h, comp)
		end
	end

	-- Clears all but latest entry for each player.
	function addon:preen_history (realmname)
		local r = assert(realmname)
		for i,h in ipairs(self.history) do
			tsort (h, comp)
			while #h > 1 do
				tremove (h)
			end
		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)

		local name_from = e.person
		local tag = e.history_unique
		local errtxt

		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
				local unique = h.id .. ' ' .. h.when
				if unique == tag then
					hi = i
					break
				end
			end
			if not hi 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)
		end
		e.person = name_to
		e.person_class = select(2,UnitClass(name_to))

		self:Print("Reassigned entry %d from '%s' to '%s'.", index, name_from, name_to)
	end
end


------ Player communication
do
	local function adduser (name, status, active)
		if status then addon.sender_list.names[name] = status end
		if active then addon.sender_list.active[name] = active end
	end

	-- Incoming handler functions.  All take the sender name and the incoming
	-- tag as the first two arguments.  All of these are active even when the
	-- player is not tracking loot, so test for that when appropriate.
	local OCR_funcs = {}

	OCR_funcs.ping = function (sender)
		pprint('comm', "incoming ping from", sender)
		addon:whispercast (sender, 'pong', addon.revision, 
			addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled"))
	end
	OCR_funcs.pong = function (sender, _, rev, status)
		local s = ("|cff00ff00%s|r %s is |cff00ffff%s|r"):format(sender,rev,status)
		addon:Print("Echo: ", s)
		adduser (sender, s, status=="tracking" or status=="broadcasting" or nil)
	end

	OCR_funcs.loot = function (sender, _, recip, item, count, extratext)
		addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, "item", item, "count", count)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext)
	end

	OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
		addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", instancetag)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		addon:on_boss_broadcast (reason, bossname, instancetag)
	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. Or do nothing for now to see if other requests arrive.",
				sender,
				addon.format_hypertext('bcaston',"the red pill",'|cffff4040'),
				addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd'))
		end
		addon.popped = true
	end

	OCR_funcs.bcast_responder = function (sender)
		if addon.debug.comm or addon.requesting or
		   ((not g_wafer_thin) and (not addon.rebroadcast))
	   then
			addon:Print(sender, "has answered the call and is now broadcasting loot.")
		end
	end
	-- remove this tag once it's all tested
	OCR_funcs.bcast_denied = function (sender)
		if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end
	end

	-- Incoming message dispatcher
	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)
	end
	-- Recent message cache
	addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)

	function addon:OnCommReceived (prefix, msg, distribution, sender)
		if prefix ~= self.ident then return end
		if not self.debug.comm then
			if distribution ~= "RAID" and distribution ~= "WHISPER" then return end
			if sender == my_name then return end
		end
		self.dprint('comm', ":OCR from", sender, "message is", msg)

		if self.recent_messages:test(msg) then
			return self.dprint('cache', "message <",msg,"> already in cache, skipping")
		end
		self.recent_messages:add(msg)

		-- Nothing is actually returned, just (ab)using tail calls.
		return dotdotdot(sender,strsplit('\a',msg))
	end

	function addon:OnCommReceivedNocache (prefix, msg, distribution, sender)
		if prefix ~= self.identTg then return end
		if not self.debug.comm then
			if distribution ~= "WHISPER" then return end
			if sender == my_name then return end
		end
		self.dprint('comm', ":OCRN from", sender, "message is", msg)
		return dotdotdot(sender,strsplit('\a',msg))
	end
end

-- vim:noet