view core.lua @ 156:1e2ee3f52bc8 beta-l10n-2

Stub files to test server-side localization generator. These are NOT LOADED YET.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 21 Feb 2015 17:04:00 -0500
parents 42dd3076baaf
children
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
- raiders       accumulating raid roster data as we see raid members; indexed
                by player name with subtable fields:
-    fname      fully-qualified name; "PlayerName" or "PlayerName-RealmName"
-    class      capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc)
-    subgroup   1-8 (NUM_RAID_GROUPS), NRG+1 if something's wrong
-    race       English codename ("BloodElf", etc)
-    sex        1 = unknown/error, 2 = male, 3 = female
-    level      can be 0 if player was offline at the time
-    guild      guild name, or missing if unguilded
-    realm      realm name, or missing if same realm as player at the time
-    online     'online', 'offline', 'no_longer' [no longer in raid group]
    [both of these next two fields use time_t values:]
-    join       time player joined the raid (or first time we've seen them)
-    leave      time player left the raid (or time we've left the raid, if
                'online' is not 'no_longer')
-    needinfo   true if haven't yet gotten close enough to player to request
                unit data; SHOULD be missing

Common g_loot entry indices:
- kind          time/boss/loot
- hour          0-23, on the *physical instance server*, not the realm server
- minute        0-59, ditto
- stamp         time_t on the local computer, possibly tweaked for uniqueness

Time specific g_loot indices:
- startday      table with month/day/year/text fields from makedate()
                text is always "dd Month yyyy"

Boss specific g_loot indices:
- bossname      name of boss/encounter;
                may be changed if "snarky boss names" option is enabled
- reason        wipe/kill ("pull" does not generate an entry)
- instance      name of instance, including size and difficulty
- maxsize       max raid size: 5/10/25, presumably also 15 and 40 could show
                up; can be 0 if we're outside an instance and the player
                inside has an older version
- duration      in seconds; may be missing (only present if local)
- raidersnap    copy of g_loot.raiders at the time of the boss event; may be
                empty for manual snapshots the player didn't want included
                (not necessarily an "error" if this is missing entirely)

Loot specific g_loot indices:
- person        recipient
- person_class  class of recipient if available; may be missing;
                will be classID-style (e.g., DEATHKNIGHT)
- person_realm  recipient's realm if different from the player's; missing
                otherwise
- itemname      not including square brackets
- id            itemID as number
- itemstring    "item:nnnnn" string
- itemlink      full clickable link
- itexture      icon path (e.g., Interface\Icons\INV_Misc_Rune_01)
- quality       [LE_]ITEM_QUALITY_* number
- unique        an almost-certainly-unique string, content meaningless
- disposition   offspec/gvault/shard; missing otherwise; can be set from
                the extratext field
- count         e.g., "x3"; missing otherwise; can be set/removed from
                extratext; triggers only for a stack of items, not "the boss
                dropped double axes today"
- variant       1 = heroic item, 2 = LFR item; missing otherwise
                XXX CHANGED FOR WoD:  for now, if present at all, some kind
				of heroic/warforged/something-extra; missing otherwise
- cache_miss    if GetItemInfo failed; SHOULD be missing (changes other fields)
- bcast_from    player's name if received rebroadcast from that player;
                missing otherwise; can be deleted as a result of in-game
                fiddling of loot data
- extratext     text in Note column, including disposition and rebroadcasting;
                missing otherwise
- extratext_byhand    true if text edited by player directly; missing otherwise


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 (callback 'events', search for LCALLBACK)
------ 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 and
serve as a spelling reminder.

]==]

------ 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_hist  = nil
OuroLootSV_log   = {}


------ Constants
local option_defaults = { profile = {
	--['datarev'] = 20,    -- cheating, this isn't actually an option
	['popup_on_join'] = true,
	['register_slash_synonyms'] = false,
	['slash_synonyms'] = '/ol,/oloot',
	['scroll_to_bottom'] = true,
	['history_suppress_LFR'] = false,
	['history_ignore_xrealm'] = true,
	['gui_noob'] = true,
	['chatty_on_kill'] = false,
	['no_tracking_wipes'] = false,
	['snarky_boss'] = true,
	['keybinding'] = false,
	['bossmod'] = "DBM",
	['keybinding_text'] = '',  --'CTRL-SHIFT-O',
	['forum'] = {
		['[url] Wowhead'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
		['[url] MMO/Wowstead'] = '[http://db.mmo-champion.com/i/$I]$X - $T',
		['[item] by name'] = '[item]$N[/item]$X - $T',
		['[item] by ID'] = '[item]$I[/item]$X - $T',
		['Custom...'] = '',
	},
	['forum_current'] = '[item] by name',
	['display_disabled_LODs'] = false,
	['display_unusable_LODs'] = false,
	['display_bcast_from'] = true,
	['precache_history_uniques'] = false,
	['chatty_on_remote_changes'] = false,
	['chatty_on_local_changes'] = false,
	['chatty_on_changes_frame'] = 1,
	['itemfilter'] = {},
	['itemvault'] = {},
	['track_bonusrolls'] = true,
	['track_nonloot'] = false,
} }
local virgin = "First time loaded?  Hi!  Use the /ouroloot 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 newer_warning = "A newer version has been released. You can %s to display"
	.." 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 horrible_error_text = [[|cffff1010]] .. ERROR_CAPS
	..[[:|n|cffffff00Something unrecoverable has happened.  The error message]]
	..[[ which was provided follows in white:|r|n|n%s|n|n|cffffff00Ouro Loot]]
	..[[ will not display a window until this situation is corrected. ]]
	..[[ You can try typing|n|cff00ff40/ouroloot fix ?|n]]
	..[[|cffffff00to see what can be done by software alone.  You may still]]
	..[[ need to do a "/reload" afterwards, or even restart the game client.]]
local unique_collision = "Item '%s' was carrying unique tag <%s>, but that was already in use; tried to generate a new tag and failed!|n|nRemote sender was '%s', previous cache entry was <%s/%s>.|n|nThis may require a live human to figure out; the loot in question has not been stored."
local new_profile_warning = [[Be aware that profiles only store addon & plugin settings from the <Options> tab; loot and generated text is account-wide data, unrelated to your current profile.]]
local remote_chatty = "|cff00ff00%s|r changed %d/%s from %s to %s"
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		= 4   -- seconds in the communications cache
local version_large			= nil -- defaults to 1, possibly changed by version
local timestamp_fmt_unique	= '%Y/%m/%dT%H:%M:%S'
local timestamp_fmt_history	= '%Y/%m/%d %H:%M:%S'


------ 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			= '17'
	version			= _G.GetAddOnMetadata(nametag,"Version") or "?"  -- "x.yy.z", etc
	ident			= "OuroLoot2"
	identTg			= "OuroLoot2Tg"
	status_text		= nil
	revision		= "@project-revision@"
	--@debug@
	revision		= "DEVEL"
	--@end-debug@

	tekdebug		= nil
	if _G.tekDebug then
		local tdframe = _G.tekDebug:GetFrame("Ouro Loot")
		function tekdebug (txt)
			-- tekDebug notices "<name passed to getframe>|r:"
			tdframe:AddMessage('|cff17ff0dOuro Loot|r:'..txt,1,1,1)
		end
	end

	DEBUG_PRINT		= false
	debug = {
		comm		= false,
		loot		= false,
		flow		= false,
		notraid		= false,
		cache		= false,
		callback	= false,
		alsolog		= false,
	}
	--@debug@
	DEBUG_PRINT		= true
	debug.loot		= true
	debug.comm		= true
	is_guilded		= _G.IsInGuild()
	--@end-debug@

	-- This looks ugly, but it factors out the load-time decisions from
	-- the run-time ones.  Args to [dp]print are concatenated with spaces.
	if tekdebug then
		function dprint (t,...)
			if DEBUG_PRINT and debug[t] then
				local text = flib.safefprint(tekdebug,"<"..t.."> ",...)
				if debug.alsolog then
					addon:log_with_timestamp(text)
				end
			end
		end
	else
		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
	end

	if author_debug and tekdebug then
		function pprint (t,...)
			local text = flib.safefprint(tekdebug,"<<"..t..">> ",...)
			if debug.alsolog then
				addon:log_with_timestamp(text)
			end
		end
	else
		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:  '.._G.tostring(msg or 'nil'),3)
		end
	end

	enabled			= false
	rebroadcast		= false
	display			= nil   -- reference to display frame iff visible
	loot_clean		= nil   -- index of last GUI entry with known-current visual data
	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		= {}

	requesting		= nil   -- prompting for additional rebroadcasters

	lootjumps		= {}    -- maps hypertext idents to EOI line numbers

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

	_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")

-- if given, MSG should be a complete-ish sentence
function addon:load_assert (cond, msg, ...)
	if cond then
		return cond, msg, ...
	end
	msg = msg or "load-time assertion failed!"
	self.NOLOAD = msg
	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
	self.error (msg, --[[level=]]2)
end
addon.FILES_LOADED = 0

-- Seriously?  ORLY?
-- YARLY.  Go ahead and guess what was involved in tracking this down.  If
-- more such effects are added in the future, the "id==xxxxx" will need to
-- change into a probe of a table of known-problematic IDs.
for i = 1, 40 do   -- BUFF_MAX_DISPLAY==32, enh
	local id = select(11,UnitAura('player', i, 'HELPFUL'))
	if id == 88715 then
		-- What I really want to do is pause until the thing is clicked off,
		-- then continue with the rest of the file.  No can do.  Could also
		-- just set some hooks and then re-OnInit/OnEnable after the aura
		-- expires, but that's a hassle.  GAH.  Punt.
		local text = UnitAura('player', i, 'HELPFUL')
		text = ([[Cannot initialize while |cff71d5ff|Hspell:88715|h[%s]|h|cff00ff00 is active!]]):
			format(text)
		addon:load_assert(nil,text)
		return -- were this C code running through lint, I'd put NOTREACHED
	end
end

-- Class color support.  Do the expensive string.format calls up front, and
-- the cheap all-string-all-at-once single-op concatenation as needed.
do
	local cc = {}
	local function extract (color_info)
		local hex
		if color_info.colorStr then   -- MoP
			hex = "|c" .. color_info.colorStr
		else                          -- pre-MoP
			hex = ("|cff%.2x%.2x%.2x"):format(255*color_info.r,
				255*color_info.g, 255*color_info.b)
		end
		return { r=color_info.r, g=color_info.g, b=color_info.b, a=1, hex=hex }
	end
	local function fill_out_class_colors()
		for class,color in pairs(CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS) do
			cc[class] = extract(color)
		end
		cc.DEFAULT = extract(_G.NORMAL_FONT_COLOR)
	end
	if CUSTOM_CLASS_COLORS and CUSTOM_CLASS_COLORS.RegisterCallback then
		CUSTOM_CLASS_COLORS:RegisterCallback(fill_out_class_colors)
	end
	addon.class_colors = cc                               -- this stays around
	addon.fill_out_class_colors = fill_out_class_colors   -- this doesn't

	-- What I really want is to have the hooked :Print understand a special
	-- format specifier like "%Cs" and do the colorizing automatically.
	function addon:colorize (text, class)
		return ((class and cc[class]) or cc.DEFAULT).hex .. text .. "|r"
	end
end


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

local error				= addon.error
local assert			= addon.assert

local type, select, next, pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe =
	type, select, next, pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe

local pprint, tabledump = addon.pprint, flib.tabledump
local strsplit, CopyTable = strsplit, CopyTable
local GetNumGroupMembers = GetNumGroupMembers
local IsInRaid = IsInRaid
-- En masse forward decls of symbols defined inside local blocks
local _register_bossmod, makedate, create_new_cache, _init, _log, _do_loot_metas
local _history_by_loot_id, _setup_unique_replace, _unavoidable_collision
local _notify_about_change, _LFR_suppressing, _add_loot_disposition, _grok

-- 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
-- long as we can reliably feed two of them to "<" and get useful answers.
--
-- This makes/reinforces an assumption that version_large of release packages
-- (e.g., 2016001) will always be higher than those of development packages
-- (e.g., 87), due to the tagging system versus subversion file revs.  This
-- is good, as local dev code will never trigger a false positive update
-- warning for other users.
do
	local r = 0
	for d in addon.version:gmatch("%d+") do
		r = 1000*r + d
	end
	-- If it's a big enough number to obviously be a release, then make
	-- sure it's big enough to overcome many small previous point releases.
	while r > 2000 and r < 2000000 do
		r = 1000*r
	end
	version_large = math.max(r,1)
end

-- Hypertext support, inspired by DBM broadcast pizza timers
do
	local hypertext_format_str = "|HOuroLoot:%d|h%s[%s]|r|h"
	local func_map = {} --_G.setmetatable({}, {__mode = 'k'})
	local text_map = setmetatable({}, {__mode = 'v'})
	local base = newproxy(true)
	getmetatable(base).__tostring = function(ud) return text_map[ud] end
	--@debug@
	--[[ auto collecting these tokens is an interesting micro-optimization but not yet
	getmetatable(base).__index = {
		['done'] = function (ud)
			text_map[ud] = nil
			func_map[ud] = nil
		end,
	}
	getmetatable(base).__gc = function(ud)
		print("Collecting hyperlink object <",tostring(ud),">")
	end --]]
	--@end-debug@

	-- TEXT will automatically be surrounded by brackets
	-- COLOR can be LE_ITEM_QUALITY_* or a formatting string ("|cff...")
	-- FUNC can be "MethodName", "tab_title", or a function
	--
	-- Returns an opaque token and a matching number.  Calling tostring() on
	-- the token will yield a formatted clickable string that can be displayed
	-- in local chat.  Clicking a tab_title hyperlink opens the GUI to that
	-- tab; the MethodName and raw function callbacks will be passed the addon
	-- table, the same matching number, and the mouse button (ala OnClick) as
	-- arguments.
	--
	-- This is largely an excuse to fool around with Lua data constructs.
	function addon.format_hypertext (text, color, func)
		local ret = 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, num
	end

	--[[
	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
	]]
	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, fullstring, mousebutton)
		local ltype, arg = strsplit(":",link)
		if ltype ~= "OuroLoot" then return end
		arg = tonumber(arg)
		local f = func_map[text_map[arg]]
		local t = type(f)
		if t == 'string' then
			if type(addon[f]) == 'function' then
				addon[f](addon,arg,mousebutton)   -- method
			else
				addon:BuildMainDisplay(f)         -- tab title fragment
			end
		elseif t == 'function' then
			f (addon, arg, mousebutton)
		end
	end)

	local old = ItemRefTooltip.SetHyperlink
	function ItemRefTooltip:SetHyperlink (link, ...)
		if link:match("^OuroLoot") 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, followed by the raid size
local instance_tag
do
	local codemap = {
		--[0] = "?",   -- should already be caught, avoid needless hash part
		[1] = "5",     -- normal or scenario
		[2] = "5h",
		[3] = "10",
		[4] = "25",
		[5] = "10h",
		[6] = "25h",
		[7] = "LFR",
		[8] = "C",     -- challenge mode
		[9] = "40",
	}
	function instance_tag()
		-- Return values of GetInstanceInfo changed considerably, now much
		-- more useful and consistent.
		local name, typeof, diffcode, _, raidsize, _, _, mapid, currsize = GetInstanceInfo()
		local t
		name = addon.instance_abbrev[mapid] or addon.instance_abbrev[name] or name
		if typeof == nil or typeof == "none" or diffcode == 0 or diffcode == nil then
			-- either outdoors or in a scenario (revisit those maybe)
			return name, MAX_RAID_MEMBERS
		end
		t = codemap[diffcode] or ("?"..diffcode.."?")
		return name .. "(" .. t .. ")", raidsize
	end
end
addon.instance_tag = instance_tag   -- grumble
addon.latest_instance = nil         -- spelling reminder, assigned elsewhere

-- Whether we're recording anything at all in the loot histories
local function _history_suppress()
	return _LFR_suppressing or addon.history_suppress
end

-- Memoizing cache of unique IDs as we generate or search for them.  Keys are
-- the uniques, values are the following:
--   'history'      active player name in self.history
--   'history_may'  index into player's uniques list, CAN QUICKLY BE OUTDATED
--                  and will instantly be wrong after manual insertion
--   'loot'         active index into g_loot
-- with all but the history entry optional.  Values of g_uniqes.NOTFOUND
-- indicate a known missing status.  Use g_uniques:RESET() to wipe the cache
-- and return to searching mode.
do
	local notfound = -1
	local notfound_ret = { history = notfound }
	local mt

	-- This can either be its own function or a slightly redundant __index.
	local function m_probe_only (t, k)
		return rawget(t,k) or notfound_ret
	end

	-- Expensive search.
	local function m_full_search (t, k)
		local L, H, HU, loot
		-- Try active loot entries first
		for i,e in addon:filtered_loot_iter('loot') do
			if k == e.unique then
				L,loot = i,e
				break
			end
		end
		-- If it's active, try looking through that player's history first.
		if L then
			local hi,h = addon:get_loot_history (loot.person)
			for ui,u in ipairs(h.unique) do
				if k == u then
					H, HU = h.name, ui
					break
				end
			end
		else
			-- No luck?  Ugh, may have been reassigned and we're probing from
			-- older data.  Search the rest of current realm's history.
			for hi,h in ipairs(addon.history) do
				for ui,u in ipairs(h.unique) do
					if k == u then
						H, HU = h.name, ui
						break
					end
				end
			end
		end
		local ret = { loot = L, history = H or notfound, history_may = HU }
		t[k] = ret
		return ret
	end

	local function m_setmode (self, mode)
		mt.__index = (mode == 'probe') and m_probe_only or
		             (mode == 'search') and m_full_search or
					 nil -- maybe error() here?
	end

	local function m_reset (self)
		wipe(self)
		self[''] = notfound_ret  -- special case for receiving older broadcast
		self.NOTFOUND = notfound
		self.RESET = m_reset
		self.SEARCH = m_full_search
		self.TEST = m_probe_only
		self.SETMODE = m_setmode
		mt.__index = m_full_search
		return self
	end

	-- If unique keys ever change into objects instead of strings, change
	-- this into a weakly-keyed table.
	mt = { __metatable = 'Should be using setmode.' }

	g_uniques = setmetatable (m_reset{}, mt)
end


------ Expiring caches
--[[
cache = create_new_cache ("mycache", 15 [,cleanup])
cache:add(foo)
cache:test(foo)   -- returns true
   ....5 seconds pass
cache:add(bar)
   ....10 seconds pass
cache:test(foo)   -- returns false
cache:test(bar)   -- returns true
   ....5 seconds pass
   ....bar also gone, cleanup() called:
function cleanup (expired_entries)
  for i = 1, #expired_entries do  -- this table is in strict FIFO order
    print(i, expired_entries[i])
	-- 1  foo
	-- 2  bar
  end
end
]]
do
	local AnimTimerFrame = CreateFrame('Frame',nil,nil) -- FIXME transition hack
	local caches = {}
	local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
	local time, next = time, next
	local new, del = flib.new, flib.del
	cleanup_group:SetLooping("REPEAT")
	cleanup_group:SetScript("OnLoop", function(cg)
		addon.dprint('cache',"OnLoop firing")
		local now = time()
		local actives = 0
		local expired = new()
		-- this is ass-ugly
		for name,c in next, caches do
			local fifo = c.fifo
			local active = #fifo > 0
			actives = actives + (active and 1 or 0)
			while (#fifo > 0) and (now > fifo[1].t) do
				local datum = tremove(fifo,1)
				addon.dprint('cache', name, "cache removing", datum.t, "<", datum.m, ">")
				c.hash[datum.m] = nil
				tinsert(expired,datum.m)
				del(datum)
			end
			if active and #fifo == 0 and c.func then
				addon.dprint('cache', name, "empty, firing cleanup")
				c.func(expired)
			end
			wipe(expired)
		end
		del(expired)
		if actives == 0 then
			addon.dprint('cache',"OnLoop FINISHING animation group")
			cleanup_group:Finish()
		else
			addon.dprint('cache',"OnLoop done, not yet finished")
		end
	end)

	local function _add (cache, x)
		assert(type(x)~='number')
		local datum = new()
		datum.m = x
		datum.t = time() + cache.ttl
		cache.hash[x] = datum
		tinsert (cache.fifo, datum)
		if not cleanup_group:IsPlaying() then
			addon.dprint('cache', cache.name, "with entry", datum.t, "<", datum.m, "> STARTING animation group")
			cleanup_group:Play()
		end
	end
	local function _test (cache, x)
		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.
		local c = {
			ttl = ttl,
			name = name,
			add = _add,
			test = _test,
			cleanup = cleanup_group:CreateAnimation("Animation"),
			func = on_alldone,
			-- Testing merging these two (_add's 'x' must not be numeric)
			fifo = {},
			--hash = {},
		}
		c.hash = c.fifo
		c.cleanup:SetOrder(1)  -- [1,100] range within parent animation
		c.cleanup:SetDuration(0.8)  -- hmmm
		caches[name] = c
		return c
	end
end


------ Ace3 framework stuff
function addon:DBProfileRefresh()
	opts = self.db.profile
end

function addon:OnInitialize()
	if self.author_debug then
		_G.OL = self
		_G.g_uniques = g_uniques
	end
	-- This kludgy-looking thing is because if there are serious errors in
	-- loading, some of the actions during PLAYER_LOGOUT can destroy data.
	if self.FILES_LOADED ~= 7 then
		print('|cffaee3ffouroloot reports load count of',self.FILES_LOADED,'fml|r')
		self:SetEnabledState(false)
		return
	end
	self.FILES_LOADED = nil

	_log = OuroLootSV_log

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

	-- Primarily for plugins, but can be of use to me also...  LCALLBACK
	self.callbacks = LibStub("CallbackHandler-1.0"):New(self)
	--function self.callbacks:OnUsed (target_aka_self, eventname)  end
	--function self.callbacks:OnUnused (target_aka_self, eventname)  end

	if _G.OuroLootOptsDB == nil then
		local vclick = self.format_hypertext ([[click here]], LE_ITEM_QUALITY_UNCOMMON, 'help')
		self:ScheduleTimer(function(s)
			for id in pairs(self.default_itemfilter) do
				opts.itemfilter[id] = true
			end
			for id in pairs(self.default_itemvault) do
				opts.itemvault[id] = true
			end
			s:Print(virgin, tostring(vclick))
			virgin = nil
		end,10,self)
	else
		virgin = nil
	end
	self.db = LibStub("AceDB-3.0"):New("OuroLootOptsDB", option_defaults , --[[Default=]]true)
	self.db.RegisterCallback (self, "OnNewProfile", function()
		self:Print(new_profile_warning)
	end)
	self.db.RegisterCallback (self, "OnProfileChanged", "DBProfileRefresh")
	self.db.RegisterCallback (self, "OnProfileCopied", "DBProfileRefresh")
	self.db.RegisterCallback (self, "OnProfileReset", "DBProfileRefresh")
	self:DBProfileRefresh()

	--[[
	local stored_datarev = opts.datarev or 14
	for opt,default in pairs(option_defaults) do
		if opts[opt] == nil then
			opts[opt] = default
		end
	end
	opts.datarev = option_defaults.datarev]]

	self:RegisterChatCommand("ouroloot", "OnSlash")
	if opts.register_slash_synonyms then
		-- Maybe use %w here for non-English locales?
		local n = 2
		for s in opts.slash_synonyms:gmatch("/%a+") do
			_G['SLASH_ACECONSOLE_OUROLOOT'..n] = s
			n = n + 1
		end
	end

	self.history_all = self.history_all or _G.OuroLootSV_hist or {}
	local r = self:load_assert (GetRealmName(), "how the freak does GetRealmName() fail?")
	self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
	self.history = self.history_all[r]

	local histformat = self.history_all.HISTFORMAT
	self.history_all.HISTFORMAT = nil   -- don't keep this in live data
	if _G.OuroLootSV_hist
	   and (histformat == nil or histformat < 4)
	then   -- some big honkin' loops
		for rname,realm in pairs(self.history_all) do
			for pk,player in ipairs(realm) do
				if histformat == nil or histformat < 3 then
					for lk,loot in ipairs(player) do
						if loot.count == "" then
							loot.count = nil
						end
						if not loot.unique then
							loot.unique = loot.id .. ' ' .. loot.when
						end
					end
				end
				-- format 3 to format 4 was a major revamp of per-player data
				self:_uplift_history_format(player)
			end
		end
	end
	self._uplift_history_format = nil
	--OuroLootSV_hist = nil

	-- Handle changes to the stored data format in stages from oldest to newest.
	-- bumpers[X] is responsible for updating from X to X+1.
	-- (This is turning into a lot of loops over the same table.  Consolidate?)
	if false and OuroLootSV then
		local dirty = false
		local bumpers = {}
		--bumpers[14] = function()   start
		--bumpers[19] = function()   latest

		--[===[
		local real = bumpers
		bumpers = newproxy(true)
		local mt = getmetatable(bumpers)
		mt.__index = real
		mt.__gc = function() print"whadda ya know, garbage collection works" end ]===]

		while stored_datarev < opts.datarev do
			self:Printf("Transitioning saved data format to %d...", stored_datarev+1)
			dirty = true
			bumpers[stored_datarev]()
			stored_datarev = stored_datarev + 1
		end
		if dirty then self:Print("Saved data has been massaged into shape.") end
	end

	self:FINISH_SPECIAL_TABS()
	_init(self)
	self.dprint('flow', "version strings:", version_large, self.revision, self.status_text)
	g_gui = self.gui_state_pointer
	self.gui_state_pointer = nil
	self.load_assert = nil

	--[[
	The loot format patterns of interest, changed into relatively tight string
	match patterns.  Done during initialization rather than at load-time
	against the slim chance that one of the non-US "delocalizers" needs to
	adjust the global patterns before we transform them.
	]]
	local function ss (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)') end
	local function ssd (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') end
	local function s (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)') end
	local function sd (m) return m:gsub('%.$','%%.'):gsub('%%s','(.+)'):gsub('x%%d','(x%%d+)') end

	local loot_patterns = {
		-- LOOT_ITEM = "%s receives loot: %s." --> (.+) receives loot: (.+)%.
		-- LOOT_ITEM_BONUS_ROLL = "%s receives bonus loot: %s." --> (.+) receives bonus loot: (.+)%.
		-- LOOT_ITEM_PUSHED = "%s receives item: %s." --> (.+) receives item: (.+)%.
		['LOOT_ss'] = ss(_G.LOOT_ITEM),
		['BONUS_ss'] = ss(_G.LOOT_ITEM_BONUS_ROLL),
		['ITEM_ss'] = ss(_G.LOOT_ITEM_PUSHED),

		-- LOOT_ITEM_MULTIPLE = "%s receives loot: %sx%d." --> (.+) receives loot: (.+)(x%d+)%.
		-- LOOT_ITEM_BONUS_ROLL_MULTIPLE = "%s receives bonus loot: %sx%d." --> (.+) receives bonus loot: (.+)(x%d+)%.
		-- LOOT_ITEM_PUSHED_MULTIPLE = "%s receives item: %sx%d." --> (.+) receives item: (.+)(x%d+)%.
		['LOOT_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_MULTIPLE),
		['BONUS_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_BONUS_ROLL_MULTIPLE),
		['ITEM_MULTIPLE_ssd'] = ssd(_G.LOOT_ITEM_PUSHED_MULTIPLE),

		-- LOOT_ITEM_SELF = "You receive loot: %s." --> You receive loot: (.+)%.
		-- LOOT_ITEM_BONUS_ROLL_SELF = "You receive bonus loot: %s." --> You receive bonus loot: (.+)%.
		-- LOOT_ITEM_PUSHED_SELF = "You receive item: %s." --> You receive item: (.+)%.
		['LOOT_SELF_s'] = s(_G.LOOT_ITEM_SELF),
		['BONUS_SELF_s'] = s(_G.LOOT_ITEM_BONUS_ROLL_SELF),
		['ITEM_SELF_s'] = s(_G.LOOT_ITEM_PUSHED_SELF),

		-- LOOT_ITEM_SELF_MULTIPLE = "You receive loot: %sx%d." --> You receive loot: (.+)(x%d+)%.
		-- LOOT_ITEM_BONUS_ROLL_SELF_MULTIPLE = "You receive bonus loot: %sx%d." --> You receive bonus loot: (.+)(x%d+)%.
		-- LOOT_ITEM_PUSHED_SELF_MULTIPLE = "You receive item: %sx%d." --> You receive item: (.+)(x%d+)%.
		['LOOT_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_SELF_MULTIPLE),
		['BONUS_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_BONUS_ROLL_SELF_MULTIPLE),
		['ITEM_SELF_MULTIPLE_sd'] = sd(_G.LOOT_ITEM_PUSHED_SELF_MULTIPLE),

		-- LOOT_ITEM_WHILE_PLAYER_INELIGIBLE is mostly the same as LOOT_ITEM with
		-- an inline texture and no full stop.  The punctuation in the texture
		-- path causes fits while matching, so just make that a wildcard rather
		-- than trying to escape it all.
		['LOOT_WHILE_PLAYER_INELIGIBLE_ss'] =
			_G.LOOT_ITEM_WHILE_PLAYER_INELIGIBLE:gsub('\124T%S*\124t','\124T%%S*\124t'):gsub('%%s','(.+)'),
	}
	_grok = setfenv (_grok, loot_patterns)
	for k,v in pairs(loot_patterns) do
		self.dprint('loot', "Loot pattern", k, "set to", v)
	end

	self.OnInitialize = nil   -- free up ALL the things!
end

function addon:OnEnable()
	self:RegisterEvent("PLAYER_LOGOUT")
	self:RegisterEvent("GROUP_ROSTER_UPDATE")

	-- This Print cribbed from Talented.  I like the way jerry thinks: the
	-- first string argument can be a format spec for the remainder of the
	-- arguments.  AceConsole:Printf isn't used because we can't specify a
	-- prefix without jumping through ridonkulous hoops.
	--
	-- Everything dealing with a prefix hyperlink is my fault.
	--
	-- CFPrint added instead of the usual Print testing of the first arg for
	-- frame-ness, which would slow down all printing and only rarely be useful.
	do
		local AC = LibStub("AceConsole-3.0")
		function addon.chatprefix (code, arg)
			local f = ''   -- empty -> BuildMainDisplay(empty) -> main tab
			if code == "GoToLootLine" then
				f = code
			--elseif .....
			end
			local ret, num = self.format_hypertext ("Ouro Loot",
				LE_ITEM_QUALITY_LEGENDARY, f)
			if code == "GoToLootLine" then
				self.lootjumps[num] = arg
			end
			return ret, num
		end
		--local chat_prefix = self.format_hypertext ("Ouro Loot", --[[legendary]]5, '')
		--local chat_prefix_s = tostring(chat_prefix)
		local chat_prefix_s = tostring((addon.chatprefix()))
		function addon:Print (str, ...)
			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
				return AC:Print (chat_prefix_s, str:format(...))
			else
				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_s, str:format(...))
			else
				return AC:Print (frame, chat_prefix_s, str, ...)
			end
		end
		function addon:PCFPrint (frame, prefix, str, ...)
			assert(type(frame)=='table' and frame.AddMessage)
			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
				return AC:Print (frame, tostring(prefix), str:format(...))
			else
				return AC:Print (frame, tostring(prefix), str, ...)
			end
		end
	end

	-- Copy these over once, now that other addons have mostly loaded.  Any
	-- future tweaks via CUSTOM_CLASS_COLORS will trigger the same callback.
	if addon.fill_out_class_colors then
		addon.fill_out_class_colors()
		addon.fill_out_class_colors = nil
	end

	while opts.keybinding do
		if InCombatLockdown() then
			local reload = self.format_hypertext ([[the options tab]],
				LE_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.",
				tostring(reload))
			break
		end

		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, for reasons nobody
			-- has ever learned
			local c = GetCurrentBindingSet()
			if c == _G.ACCOUNT_BINDINGS or c == _G.CHARACTER_BINDINGS then
				SaveBindings(c)
			end
		else
			self:Print("Error registering '%s' as a keybinding, check spelling!",
				opts.keybinding_text)
		end
		break
	end

	--[[
	Throw in the default disposition types.  This could be called during load
	were it not for the need to talk to the GUI data (not set up yet).

	Args:  code, rhex, ghex, bhex,
		text_notes, opt_text_menu (uses notes if nil), opt_tooltip_txt,
		can_reassign_p, do_history_p, from_notes_text_p
	]]
	local norm = _add_loot_disposition (self, 'normal', "ff","ff","ff", "", "normal/mainspec",
		[[This is the default.  Selecting any 'Mark as <x>' action blanks out extra notes about who broadcast this entry, etc.]],
		true, true, false)
	_add_loot_disposition (self, 'offspec', "c6","9b","6d", "offspec", nil, nil,
		true, true, true)
	_add_loot_disposition (self, 'shard',   "a3","35","ee", "shard",
		"disenchanted", nil, false, false, true)
	_add_loot_disposition (self, 'gvault',  "33","ff","99", _G.GUILD_BANK:lower(),
		nil, nil, false, false, true)
	-- fix up the odd(!) standard case of having a nil disposition field
	norm.arg2 = nil

	--[[
	Stick something in the Blizzard addons options list, where most users
	will probably look these days.  Try to be conservative about needless
	frame creation.
	]]
	local bliz = CreateFrame("Frame")
	bliz.name = "Ouro Loot"  -- must match X-LoadOn-InterfaceOptions
	if AddonLoader then
		AddonLoader:RemoveInterfaceOptions(bliz.name)
	end
	bliz:SetScript("OnShow", function(_b)
		local button = CreateFrame("Button",nil,_b,"UIPanelButtonTemplate")
		button:SetWidth(150)
		button:SetHeight(22)
		button:SetScript("OnClick", function()
			InterfaceOptionsFrameCancel:Click()
			HideUIPanel(GameMenuFrame)
			addon:BuildMainDisplay('opt')
		end)
		button:SetText('"/ouroloot options"')
		button:SetPoint("TOPLEFT",20,-20)
		_b:SetScript("OnShow",nil)
	end)
	InterfaceOptions_AddCategory(bliz)

	-- Maybe load up g_uniques now?
	-- Calling the memory-querying APIs while in combat instantly triggers
	-- the "script ran too long" error, see
	-- http://www.wowinterface.com/forums/showthread.php?t=44812
	if opts.precache_history_uniques and not InCombatLockdown() then
		self:_cache_history_uniques()
	end
	self._cache_history_uniques = nil

	-- This will be nil if there are no such modules.  Used by the GUI
	-- in _gui_add_disabled_LOD_tabs
	self._disabled_LOD_modules = self:_scan_LOD_modules()

	self:_set_chatty_change_chatframe (opts.chatty_on_changes_frame, --[[silent_p=]]true)

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

do
	--[[
	Module support (aka plugins).  Field names with special meanings:
	- option_defaults:  (IN) Standard AceDB-style table.  Use a profiles key!
	- db:  (OUT) AceDB object, set during init.
	- opts:  (OUT) Pointer to plugin's "db.profile" subtable.

	Inherited unchanged:
	- _add_loot_disposition

	Inherited module variations:
	- OnInitialize, OnEnable
	- register_text_generator, register_tab_control:  also flag plugin as
		a text-generating module in main addon
	]]
	local prototype = {}
	local textgen_registry, chat_prefixes, chat_codes

	-- By default, no plugins.  First one in sets up code for any after.
	addon.get_textgen_plugin = flib.nullfunc

	-- Called as part of NewModule, after embedding and metas are done.
	-- Mostly this is one-time setup stuff that we don't need until a plugin
	-- is actually built.
	function addon:OnModuleCreated (plugin)
		textgen_registry, chat_prefixes, chat_codes = {}, {}, {}
		addon.get_textgen_plugin = function(a,t)
			return textgen_registry[t]
		end
		prototype.register_text_generator = function(p,t,...)
			textgen_registry[t] = p
			return addon:register_text_generator(t,...)
		end
		prototype.register_tab_control = function(p,t,...)
			textgen_registry[t] = p
			return addon:register_tab_control(t,...)
		end

		function addon:ModulePrefixClick (codenum, mousebutton)
			local plugin = assert(chat_codes[codenum])
			if not plugin:IsEnabled() then return end
			if mousebutton == 'LeftButton' then
				if plugin.PrefixLeftClick then
					plugin:PrefixLeftClick(codenum)
				else
					self:BuildMainDisplay()
				end
			elseif mousebutton == 'RightButton' then
				local uniqueval = plugin.name
				if plugin.PrefixRightClick then
					uniqueval = plugin:PrefixRightClick(codenum)
				end
				self:BuildMainDisplay('opt',uniqueval)
			end
		end
		function addon:OnModuleCreated (plugin)
			local token, code = self.format_hypertext (plugin.moduleName,
				LE_ITEM_QUALITY_LEGENDARY, "ModulePrefixClick")
			chat_prefixes[plugin] = token
			chat_codes[code] = plugin
			-- remove the libraries' embedded pointers so that the prototype
			-- can be inherited
			plugin.Print = nil
		end

		return self:OnModuleCreated(plugin)
	end

	function prototype.OnInitialize (plugin)
		if plugin.option_defaults then
			plugin.db = addon.db:RegisterNamespace (plugin.moduleName, plugin.option_defaults)
			plugin.opts = plugin.db.profile
			--plugin:SetEnabledState(plugin.db.profile.enabled)  if that flag is needed later
		end
	end

	--function prototype.OnEnable (plugin)
	--end

	function prototype.GetOption (plugin, info)
		local name = info[#info]
		return plugin.db.profile[name]
	end
	function prototype.SetOption (plugin, info, value)
		local name = info[#info]
		plugin.db.profile[name] = value
		local arg = info.arg
		if type(arg) == 'function' then
			plugin[arg](plugin,info)
		end
	end

	-- may eventually just rework the main print routines and inherit all 3
	function prototype.Print (plugin, str, ...)
		local AC = LibStub("AceConsole-3.0")
		local ps = tostring(chat_prefixes[plugin])
		if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
			return AC:Print (ps, str:format(...))
		else
			return AC:Print (ps, str, ...)
		end
	end

	-- For references that would be nil at load time, and I don't feel like
	-- rearranging the entire file.
	function addon:MODULE_PROTOTYPE_POINTERS()
		prototype.add_loot_disposition = _add_loot_disposition
		self.MODULE_PROTOTYPE_POINTERS = nil
	end

	addon:SetDefaultModuleLibraries("AceConsole-3.0")
	addon:SetDefaultModulePrototype(prototype)

	local err = [[Module '%s' cannot register itself because it failed a required condition: '%s']]
	function addon:ConstrainedNewModule (modname, minrev, mincomm, mindata, ...)
		if not addon.author_debug then
			if minrev and tonumber(minrev) > (tonumber(self.revision) or math.huge) then
				self:Print(err,modname,
					"revision "..self.revision.." older than minimum "..minrev)
				return false
			end
			if mincomm and tonumber(mincomm) > tonumber(self.commrev) then
				self:Print(err,modname,
					"commrev "..self.commrev.." older than minimum "..mincomm)
				return false
			end
			--[[if mindata and tonumber(mindata) > opts.datarev then
				self:Print(err,modname,
					"datarev "..opts.datarev.." older than minimum "..mindata)
				return false
			end]]
		end
		return self:NewModule(modname,...)
	end
end

--[[ LCALLBACK
Standard ace3-style callback registration and dispatching.  All player names
are simple (uncolored) strings.  The "uniqueID" always refers to the unique
tag string stored as 'unique' in loot entries and used as keys in history.
Item IDs are always of numeric type.

'Activate', enabled_p, rebroadcast_p, threshold
    The two boolean predicates are self-explanatory.  The threshold is an
    LE_ITEM_QUALITY_* constant integer.

'Deactivate', raiderdata
    After all system events have been unregistered.  Argument is a holder of
	the current g_loot.raiders table (in 'raidersnap').

'Reset'
    Clicking "Clear Loot", after all data manipulation is finished.

'NewBoss', boss
    Boss event triggered by a local bossmod (DBM, etc) or a remote OL tracker.
    Argument is a g_loot table entry of kind=='boss'.

'NewBossEntry', boss
    New row in primary EOI table of kind=='boss'.  Includes all 'NewBoss'
    occasions, plus manual boss additions, testing, etc.  Arg same as NewBoss.

'NewLootEntry', loot, row_index
'DelLootEntry', loot
    New or removed row in primary EOI table of kind=='loot'.  Argument is a
    g_loot table entry of kind=='loot', and the index into g_loot of where
	the entry "is" (read: "will be" by the time the event fires).

'NewEOIEntry', entry, row_index
'DelEOIEntry', entry
    New or removed row in primary EOI table, of any kind.  Argument is the
    g_loot entry, already inserted into or removed from g_loot.  Note that
	boss entries may shift around after this event (if loot has happened and
	needs to be re-sorted).

'NewHistory', player_name, uniqueID
'DelHistory', player_name, uniqueID
    New or removed entry in player history.  Name argument self-explanatory.
    ID is the corresponding loot, already inserted into or removed from the
    history structures.

'Reassign', uniqueID, itemID, loot, from_player_name, to_player_name
    Loot reassigned from one player to another.  Loot represented by the
    unique & item IDs, and the g_loot entry of kind=='loot'.  The player
    names are self-explanatory.

'MarkAs', uniqueID, itemID, loot, old_disposition, new_disposition
    The "Mark as <x>" action (as if from the item right-click menu, possibly
    from a remote tracker) has finished.  ID & loot arguments are as in
    'Reassign'.  The old/new dispositions are those of the g_loot index
    "disposition" (described at the top of core.lua), with the added possible
    value of "normal" meaning exactly that.
]]
do
	-- We don't want to trigger plugins or other addons as soon as something
	-- interesting happens, because a nontrivial amount of work happens "soon"
	-- after the interesting event:  cleanups/fixups, improvs from network,
	-- etc.  So firing a callback is delayed ever so briefly by human scales.
	--
	-- For data safety, we replace any table arguments with read-only proxies
	-- before passing them to the callbacks.  It's not supposed to be securing
	-- the data against tampering; the goal is to prevent accidents, not fraud.
	local unpack, setmetatable = unpack, setmetatable
	local mtnewindex = function() --[[local]]error("This table is read-only", 3) end
	local function make_readonly (t)
		return setmetatable({}, {
			__newindex = mtnewindex,
			__index = t,
			__metatable = false,
			__tostring = getmetatable(t) and getmetatable(t).__tostring,
		})
	end

	local queue = create_new_cache ('callbacks', 1.2, function (allcbs)
		for _,c in ipairs(allcbs) do
			addon.callbacks:Fire (unpack(c))
			flib.del(c)
		end
	end)
	function addon:Fire (...)
		self.dprint('callback', ...)
		local capture = flib.new(...)
		for k,v in ipairs(capture) do if type(v) == 'table' then
			capture[k] = make_readonly(v)
		end end
		queue:add(capture)
	end
end


------ Event handlers
function addon:_clear_SVs()
	g_loot = {}  -- not saved, just fooling PLAYER_LOGOUT tests
	_G.OuroLootSV = nil
	_G.OuroLootSV_saved = nil
	_G.OuroLootOptsDB = nil
	_G.OuroLootSV_hist = nil
	_G.OuroLootSV_log = nil
	ReloadUI()
end
function addon:PLAYER_LOGOUT()
	-- Can these still fire at the very last instant?
	--self:UnregisterEvent("GROUP_ROSTER_UPDATE")
	--self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self.LOGGING_OUT = true  -- kludge this instead

	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
		_G.OuroLootSV = g_loot
	else
		_G.OuroLootSV = nil
	end

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

do
	local IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex,
				UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo =
	      IsInInstance, UnitIsConnected, UnitClass, UnitRace, UnitSex,
		  		UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo
	local time, difftime = time, difftime
	local R_ACTIVE, R_OFFLINE, R_LEFT = 'online', 'offline', 'no_longer'
	local was_in_raid

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

	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

		-- XXX somewhere in here, we could fire a useful callback event

		if redo then
			redo_count = redo_count + 1
		end
		redo = false
		for i = 1, GetNumGroupMembers() do
			local unit = 'raid'..i
			-- We grab a bunch of return values here, but only pay attention to
			-- them under specific circumstances.  Try to use as many of these
			-- values as possible rather than multiple Unit* calls.
			local fname, connected, subgroup, level, class, _
			fname, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i)
			-- No, that's not my typo, it really is "uknownbeing" in Blizzard's code.
			if fname and fname ~= UNKNOWN and fname ~= UNKNOWNOBJECT and fname ~= UKNOWNBEING then
				local name,realm = fname:match("(%S+)-(%S+)")
				if realm and realm == my_realm then   -- shouldn't happen
					realm = nil
				end
				if not name then
					assert(realm == nil)
					name = fname
				end
				if not g_loot.raiders[name] then
					g_loot.raiders[name] = { needinfo=true }
				end
				local r = g_loot.raiders[name]
				r.subgroup = subgroup or (NUM_RAID_GROUPS+1)
				if r.needinfo and UnitIsVisible(unit) then
					r.needinfo = nil
					r.fname    = fname
					r.class    = class    --select(2,UnitClass(unit))
					r.race     = select(2,UnitRace(unit))
					r.sex      = UnitSex(unit)
					r.level    = level    --UnitLevel(unit)
					r.guild    = GetGuildInfo(unit)
					r.realm    = realm
				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("GROUP_ROSTER_UPDATE", 60)
			end
		else
			redo_count = 0
			if timer_handle then
				self:CancelTimer(timer_handle)
				timer_handle = nil
			end
		end
	end

	function addon:GROUP_ROSTER_UPDATE (event)
		if self.LOGGING_OUT then return end
		if not IsInRaid() then
			if was_in_raid then
				-- Leaving a raid group.
				self.dprint('flow', "no longer in raid group")
				was_in_raid = false
				if self.enabled and not self.debug.notraid then
					self.dprint('flow', "enabled, leaving raid")
					self.popped = nil
					self:Deactivate()
					self:CheckRoster(--[[leaving raid]]true)
				end
				_LFR_suppressing = nil
			end
			-- Flow for 5-player groups goes right to here.
			return
		end

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

		local docheck = self.enabled
		if event == "GROUP_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",nil,nil,self)
				return
			end
		elseif event == "Activate" then
			-- dispatched from Activate
			if opts.history_suppress_LFR
			   and GetLFGMode(LE_LFG_CATEGORY_RF)
			then
				_LFR_suppressing = true
			end
			my_realm = self.history.realm
			_register_bossmod(self)
			self:RegisterEvent("CHAT_MSG_LOOT")
			was_in_raid = true
			docheck = true
		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

--[=[ CHAT_MSG_LOOT handler and its helpers.
Situations for "unique tag" generation, given N people seeing local loot
events, M people seeing remote rebroadcasts, and player Z adding manually:

+ Local tracking:  All LOCALs should see the same itemstring, thus the same
  unique ID stripped out of field #9.  LOCALn includes this in the broadcast
  to REMOTEm.  Tag is a large number, meaningless for clients and players.

+ Local broadcasting, remote tracking:  same as local tracking.  Possibly
  some weirdness if all local versions are significantly older than the remote
  versions; in this case each REMOTEn will generate their own tags of the form
  itemID+formatted_date, which will not be "unique" for the next 60 seconds.
  As long as at least one LOCALn is recent enough to strip and broadcast a
  proper ID, multiple items of the same visible name will not be "lost".

+ Z manually inserts a loot entry:  Z generates a tag, preserved locally.
  If Z rebrodcasts that entry, all REMOTEs will see it.  Tag is of the form
  "n" followed by a random number.
]=]
do
	local counter, _do_loot
	do
		local count = 0
		function counter() count = count + 1; return count; end
	end

	local function maybe_trash_kill_entry()
		-- This field is set on various boss interactions, so we've got a
		-- kill/wipe entry already.
		if addon.latest_instance then return end
		local ss, max, inst = addon:snapshot_raid()
		addon.latest_instance = inst
		addon:_mark_boss_kill (addon._addBossEntry{
			kind='boss', reason='kill', bossname=[[trash]],
			instance=addon.latest_instance, duration=0,
			raidersnap=ss, maxsize=max
		})
	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 = math.random
	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
				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 = prefix .. random(8675309)
			until g_uniques:TEST(u).history == g_uniques.NOTFOUND
			addon.dprint('loot',"created unique tag ("..u..")")
		end
		return true, u
	end

	-- Recent loot cache
	local candidates = {}
	local sigmap = {}
	local function preempt_older_signature (oldersig, newersig)
		local origin = candidates[oldersig] and candidates[oldersig].bcast_from
		if origin and g_seeing_oldsigs[origin] then
			-- replace entry from older client with this newer one
			candidates[oldersig] = nil
			addon.dprint('loot', "preempting signature <", oldersig, "> from", origin)
		end
		return false
	end

	local function prefer_local_loots (cache)
		-- The function name is a bit of a misnomer, as local entries overwrite
		-- remote entries as the candidate table is populated.  This routine is
		-- here to extract the final results once the cache timers have expired.
		--
		-- Keep this sync'd with the local_override branch below.
		for i,sig in ipairs(candidates) do
			addon.dprint('loot', "processing candidate entry", i, sig)
			local loot = candidates[sig]
			if loot then
				maybe_trash_kill_entry() -- Generate *some* kind of boss/location entry
				candidates[sig] = nil
				local looti = addon._addLootEntry(loot)
				addon.dprint('loot', "entry", i, "was found, added at index", looti)
				if addon:_test_disposition (loot.disposition, 'affects_history')
				   and not _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

		if addon.display then
			addon:redisplay()
		end
		wipe(candidates)
		wipe(sigmap)
	end
	local recent_loot = create_new_cache ('loot', comm_cleanup_ttl+3, prefer_local_loots)

	local GetItemInfo, GetItemIcon, UnitClass = GetItemInfo, GetItemIcon, UnitClass

	-- 'item' can be any of things passable to GetItemInfo, so we make no
	--     assumptions but rather base all information off of returns from
	--     GetItemInfo, including re-extracting the itemstring
	--     (in the case of local messages, 'item' is an itemstring)
	-- 'from' only present if this is triggered by a broadcast
	function _do_loot (self, local_override, recipient, unique, item, count, from, extratext)
		if unique == 0 then unique = nil end
		local prefix = "_do_loot[" .. counter() .. "]"

		--[[
			iname:		Hearthstone
			iquality:	integer
			ilink:		clickable formatted link
			itemstring:	item:6948:....
		]]
		local cache_miss, itemid
		local iname, ilink, iquality,
			-- ilvl, minlvl, type, subtype, stacksize, equiploc, texture, sellprice
			_, _, _, _, _, _, itexture = GetItemInfo(item)
		if not itexture then
			-- Fun fact, GetItemIcon behaves oddly on a nil argument, and may
			-- or may not always work on a non-numerical-itemID argument
			itemid = tonumber(item)
			if (itemid) then
				itexture = GetItemIcon(itemid)
			end
		end
		if (not iname) or (not itexture) then
			cache_miss = true
			iname, ilink, iquality, itexture = 
				UNKNOWN..': '..item, 'item:6948', LE_ITEM_QUALITY_COMMON,
				[[Interface\ICONS\INV_Misc_QuestionMark]]
		end
		self.dprint('loot',">>"..prefix, "R:", recipient, "U:", unique, "I:",
			item, "C:", count, "frm:", from, "ex:", extratext,
			"q:", iquality, "tex:", itexture)

		-- Get a known-good string and integer out of the returned link.
		local itemstring = ilink:match("item[%-?%d:]+")
		itemid = tonumber(itemstring:match("^item:(%d+)") or 0)

		-- This is only a 'while' to make jumping out of it easy.
		local i, unique_okay, replacement, ret1, ret2
		while local_override
		      or ((iquality >= self.threshold) and not opts.itemfilter[itemid])
		do
			unique_okay, unique, replacement =
				_many_uniques_handle_it ((not local_override) and unique)
			if not unique_okay then
				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 (iname, unique,
						tostring(from), tostring(i.loot), tostring(i.history))
					_unavoidable_collision (err)
					-- 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
				self:vbroadcast('loot', recipient, unique, itemstring, count, extratext)
			end
			if (not self.enabled) and (not local_override) then break end

			local oldersig = recipient .. iname .. (count or "")
			local signature, seenit
			if #unique > 0 then
				-- newer case
				signature = unique .. oldersig
				sigmap[oldersig] = signature
				seenit = (from and recent_loot:test(signature))
					-- The following clause is what handles older 'casts arriving
					-- earlier.  All this is tested inside-out to maximize short
					-- circuit 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
				seenit = recent_loot:test(signature)
			end

			if seenit then
				self.dprint('loot', "remote", prefix, "<", signature,
					"> already in cache, skipping from", from)
				break
			end

			-- Most of the time this will already be in place and filled out,
			-- but there are situations where not.  Even if we can't get data
			-- back, at least avoid errors (or the need for existence checks).
			if not g_loot.raiders[recipient] then
				g_loot.raiders[recipient] = { needinfo=true }
			end

			-- There is some redundancy in all this, in the interests of ease-of-coding
			i = {
				kind		= 'loot',
				person		= recipient,
				person_class= g_loot.raiders[recipient].class,
				person_realm= g_loot.raiders[recipient].realm,
				cache_miss	= cache_miss,
				quality		= iquality,
				itemname	= iname,
				id			= itemid,
				itemstring	= itemstring,
				itemlink	= ilink,  -- determines the GUI tooltip
				itexture	= itexture,
				unique		= replacement or unique,
				count		= (count and count ~= "") and count or nil,
				bcast_from	= from,
				extratext	= extratext,
				variant		= self:is_variant_item(itemstring),
			}
			if opts.itemvault[itemid] then
				i.disposition = 'gvault'
			elseif recipient == self.sharder then
				i.disposition = 'shard'
			end
			if local_override then
				-- player is adding loot by hand, don't wait for network cache timeouts
				-- keep this sync'd with prefer_local_loots above
				if i.extratext then
					for disp,text in self:_iter_dispositions('from_notes_text') do
						if text == i.extratext then
							i.disposition = disp
							break
						end
					end
				end
				local looti = self._addLootEntry(i)
				if self:_test_disposition (i.disposition, 'affects_history')
				   and not _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
				tinsert (candidates, signature)
				self.dprint('loot', prefix, "<", signature,
					"> added to cache as candidate", #candidates)
			end
			break
		end
		self.dprint('loot',"<<"..prefix, "out")
		return ret1, ret2    -- FIXME mostly garbage
	end

	local match = string.match
	_grok = function (msg, bonus_p, item_p)
		local person, itemlink, count, extra
		while true do
			extra = "loot"
			-- test in most likely order:  other people get more loot than you do
			person, itemlink, count = match (msg, LOOT_MULTIPLE_ssd); if person then break end
			person, itemlink = match (msg, LOOT_ss); if person then break end
			-- Could only do this if in an LFR... but the restriction
			-- might apply elsewhere soon enough.
			person, itemlink = match (msg, LOOT_WHILE_PLAYER_INELIGIBLE_ss); if person then break end

			-- other people get more bonus loot than you do, too
			if bonus_p then
				extra = "bonus"
				person, itemlink, count = match (msg, BONUS_MULTIPLE_ssd); if person then break end
				person, itemlink = match (msg, BONUS_ss); if person then break end
			end

			-- i can haz cheezburgr?
			extra = "loot"
			itemlink, count = match (msg, LOOT_SELF_MULTIPLE_sd); if itemlink then break end
			itemlink = match (msg, LOOT_SELF_s); if itemlink then break end

			-- getting stuff out of the mailbox, the salvage boxes, etc
			if item_p then
				extra = "item"
				person, itemlink, count = match (msg, ITEM_MULTIPLE_ssd); if person then break end
				person, itemlink = match (msg, ITEM_ss); if person then break end
				itemlink, count = match (msg, ITEM_SELF_MULTIPLE_sd); if itemlink then break end
				itemlink = match (msg, ITEM_SELF_s); if itemlink then break end
			end
			break
		end
		return person, itemlink, count, extra
	end

	-- Returns the index of the resulting new loot entry, or nil after
	-- displaying any errors.
	function addon:CHAT_MSG_LOOT (event, ...)
		if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end

		if event == "CHAT_MSG_LOOT" then
			local msg = ...
			--ChatFrame2:AddMessage("original string:  >"..(msg:gsub("\124","\124\124")).."<")
			self.dprint('loot', "CHAT_MSG_LOOT, message is >"..(msg:gsub("\124","\124\124")).."<")

			local person, itemlink, count, extra = _grok (msg, opts.track_bonusrolls, opts.track_nonloot)

			if not itemlink then return end    -- "PlayerX selected Greed", etc, not looting
			self.dprint('loot', "CHAT_MSG_LOOT, person is", person,
				", itemlink is", itemlink, ", count is", count, "extra is", extra)
			extra = (extra == "bonus") and [[Bonus roll!]] or
				((extra == "item") and [[Non-loot]] or nil) or
				nil

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

			return _do_loot (self, --[[override=]]false, person, --[[unique=]]nil,
				match(itemlink,"item[%-?%d:]+"), count, --[[from=]]nil, extra)

		elseif event == "broadcast" then
			return _do_loot (self, --[[override=]]false, ...)

		elseif event == "manual" then
			local recip,item,text = ...
			return _do_loot (self, --[[override=]]true, recip,
				--[[unique=]]nil, item, --[[count=]]nil, --[[from=]]nil, text)
		end
	end
end

-- This only triggers on entering combat after a registered boss kill.
-- Clearing this field forces subsequent trash kills to generate an entry
-- via maybe_trash_kill_entry.
-- (Possibly what is wanted is to start a 3 or 5 minute timer, and *then*
-- look for the next combat?)
function addon:PLAYER_REGEN_DISABLED()
	self:UnregisterEvent ("PLAYER_REGEN_DISABLED")
	self.latest_instance = nil
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.)
do
	local green_help_link = addon.format_hypertext ([[Click here]],
		LE_ITEM_QUALITY_UNCOMMON, 'help')
	function addon:OnSlash (txt) --, editbox)
		txt = strtrim(txt:lower())
		local cmd, arg = ""
		do
			local s,e = txt:find("^%w+")
			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()
			end

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

		elseif cmd == "on" then                             self:Activate("cmdon",arg)
		elseif cmd == "off" then                            self:Deactivate()
		elseif cmd == "broadcast" or cmd == "bcast" then    self:Activate("cmdbcast",nil,true)
		elseif cmd == "on5" then  self.debug.notraid = true self:Activate("cmdon5",arg)

		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)
			self:CHAT_MSG_LOOT ('manual', my_name, 'item:109948:0:0:0:0:0:0:0:100:0:2:2:499:524')
			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.is_guilded = IsInGuild()
				self.debug[arg] = not self.debug[arg]
				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:find("^ver") then
			self:Print(self.status_text)
		elseif cmd == "help" then
			self:BuildMainDisplay('help')
		elseif cmd == "ping" then
			self:DoPing()

		elseif cmd == "fix" then
			if arg == "?" then
				self:Print[['/ouroloot fix cache' updates loot that wasn't in the cache]]
				self:Print[['/ouroloot fix history' repairs inconsistent data on the History tab]]
				self:Print[['/ouroloot fix' changes no stored data, only allows the window to be displayed again (this is built into all fixes above)]]
				return
			elseif arg == "cache" then
				self:do_item_cache_fixup(--[[force_silent=]]false)
			elseif arg == "history" then
				self:repair_history_integrity()
			end
			self.NOLOAD = nil
			self:Print("Window unlocked, best of luck.")

		else
			if self:OpenMainDisplayToTab(cmd,arg) then
				return
			end
			self:Print("Unknown command '%s'. %s to see the help window.",
				cmd, tostring(green_help_link))
		end
	end
end

function addon:SetThreshold (arg, quiet_p)
	local q = tonumber(arg)
	if q then
		q = math.floor(q+0.1)
		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
-- Both of these need to be (effectively) idempotent.
function addon:Activate (whence, opt_threshold, opt_bcast_only)
	self.dprint('flow', ":Activate called from", whence)
	self:RegisterEvent("GROUP_ROSTER_UPDATE")
	self:RegisterEvent("PLAYER_ENTERING_WORLD",
		function() self:ScheduleTimer("GROUP_ROSTER_UPDATE", 5, "PLAYER_ENTERING_WORLD") end)
	self.popped = true
	if self.DO_ITEMID_FIX then
		self.DO_ITEMID_FIX = nil
		self:do_item_cache_fixup(--[[force_silent=]]not self.author_debug)
		self.loot_clean = nil
	end
	if IsInRaid() then
		self.dprint('flow', ">:Activate calling RRU")
		self:GROUP_ROSTER_UPDATE("Activate")
	elseif self.debug.notraid then
		self.dprint('flow', ">:(notraid) 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("Restored previous data, but not in a raid",
				"and 5-player mode is 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.")
		if #g_loot > 200 then
			self:Print("|cffff0505Crikey!|r  You are carrying around a lot of",
				"stored loot data.  You should seriously consider clearing it",
				"out, as some of the text generation routines can choke the",
				"game client if they run for too long.")
		end
		return
	end
	self.rebroadcast = true  -- hardcode to true; this used to be more complicated
	self.enabled = not opt_bcast_only
	g_seeing_oldsigs = nil
	if opt_threshold then
		self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
	end
	self:Fire ('Activate', self.enabled, self.rebroadcast, self.threshold)
	self:Print("Now %s; threshold currently %s.",
		self.enabled and "tracking" or "only broadcasting",
		self.thresholds[self.threshold])
	self:broadcast('revcheck',version_large)
	self.dprint('flow', ":Activate from", whence, "finished")
end

-- Note:  running '/ouroloot 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("GROUP_ROSTER_UPDATE")
	self:UnregisterEvent("PLAYER_ENTERING_WORLD")
	self:UnregisterEvent("CHAT_MSG_LOOT")
	_LFR_suppressing = nil
	-- Passing .raiders directly doesn't work with a proxy (again, WTB Lua
	-- 5.2 and its __pairs iterators).  Give it the same structure as a boss
	-- entry instead.
	self:Fire ('Deactivate', { raidersnap = g_loot.raiders })
	self:Print("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("GUI state").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)
	self:Fire ('Reset')
	if repopup then
		addon:BuildMainDisplay()
	end
end


------ Behind the scenes routines
-- Semi-experimental debugging aid.
do
	-- 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
	-- 4)  logging happens to _log table (now with no other references)
	-- 5)  at logout, nothing new has been entered in the table being saved
	local date = date
	function addon:log_with_timestamp (msg)
		tinsert (_log, date('%m/%d %H:%M:%S  ')..msg)
	end
end

-- Check for plugins which haven't already been loaded, and add hooks for
-- them.  Credit to DBM for the approach here.
function addon:_scan_LOD_modules()
	local disabled
	for i = 1, GetNumAddOns() do
		if GetAddOnMetadata (i, "X-OuroLoot-Plugin")
		   and IsAddOnLoadOnDemand(i)
		   and not IsAddOnLoaded(i)
		then
			-- 'loadflag' will be false for LOD addons
			local folder, title, notes, loadflag, reason = GetAddOnInfo(i)
			local enabled = GetAddOnEnableState (my_name, i) > 0
			local tabtitle = GetAddOnMetadata (i, "X-OuroLoot-Plugin")
			local decision = 0
			self.dprint('flow', "scanning", folder, "loadflag is", loadflag,
				"enabled is", enabled, "reason-why-not is", reason)
			if enabled or opts.display_disabled_LODs then
				self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason)
				decision = 1
			elseif (not enabled) and reason == 'DISABLED' then
				disabled = disabled or flib.new()
				local t = flib.new()
				t.tabtitle = tabtitle
				t.folder = folder
				t.addon_index = i
				disabled[#disabled+1] = t
				decision = 2
			elseif opts.display_unusable_LODs then
				self:_gui_add_LOD_tab (tabtitle, folder, i, enabled, reason)
				decision = 3
			end
			self.dprint('flow', "...decision was", decision)
		end
	end
	return disabled
end

-- Routines for printing changes made by remote users.
do
	local change_chatframe

	function addon:_set_chatty_change_chatframe (arg, silent_p)
		local frame
		if type(arg) == 'number' then
			arg = math.min (arg, _G.NUM_CHAT_WINDOWS)
			frame = _G['ChatFrame'..arg]
		elseif type(arg) == 'string' then
			frame = _G[arg]
		end
		if type(frame) == 'table' and type(frame.AddMessage) == 'function' then
			change_chatframe = frame
			if not silent_p then
				local msg = "Now printing to chat frame " .. arg
				if frame.GetName then
					msg = msg .. " (" .. tostring(frame:GetName()) .. ")"
				end
				self:Print(msg)
				if frame ~= _G.DEFAULT_CHAT_FRAME then
					self:CFPrint (frame, msg)
				end
			end
			return frame
		else
			self:Print("'%s' was not a valid chat frame number/name, no change has been made.", arg)
		end
	end

	local function _notify (chatframe, source, index, olddisp, from_whom, from_class)
		local e = g_loot[index]
		if not e then
			-- how did this happen?
			return
		end
		if source == my_name then
			source = _G.UNIT_YOU
		end
		local from_text, to_text
		if from_whom then
			if from_whom == my_name then
				from_whom = _G.UNIT_YOU
			end
			from_text = addon:colorize (from_whom, from_class)
			to_text = e.person == my_name and _G.UNIT_YOU or e.person
			to_text = addon:colorize (to_text, e.person_class)
		else
			if olddisp then
				from_text = addon.disposition_colors[olddisp].text
			else
				olddisp = "normal"
				from_text = "normal"
			end
			from_text = addon.disposition_colors[olddisp].hex
				.. from_text .. "|r"

			if e.disposition then
				to_text = addon.disposition_colors[e.disposition].text
			else
				to_text = "normal"
			end
			to_text = addon.disposition_colors[e.disposition].hex
				.. to_text .. "|r"
		end

		addon.dprint ('loot', "notification:", source, index,
			e.itemlink, from_text, to_text)
		local jumpprefix = addon.chatprefix ("GoToLootLine", index)
		addon:PCFPrint (chatframe, jumpprefix, remote_chatty, source, index,
			e.itemlink, from_text, to_text)
	end

	function _notify_about_change (sender, index, olddisp, from_whom, from_class)
		_notify (change_chatframe, sender, index, olddisp, from_whom, from_class)
	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)
		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)
		wipe(temp)
		addon.sender_list.activeI = #byindex
		sort (addon.sender_list.names, byindex)
		wipe(temp)
	end
	addon.sender_list.namesI = byindex
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')
	self:broadcast('revcheck',version_large)
end

function addon:_check_version (otherrev)
	self.dprint('comm', version_large, "revchecking against", otherrev)
	otherrev = tonumber(otherrev)
	if otherrev == version_large then
		-- normal case

	elseif otherrev < version_large then
		self.dprint('comm', "ours is newer, notifying")
		self:broadcast('revcheck',version_large)

	else
		self.dprint('comm', "ours is older, (possibly) yammering")
		if newer_warning then
			local pop = addon.format_hypertext ([[click here]], LE_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]], LE_ITEM_QUALITY_UNCOMMON, 'DoPing')
			self:Print(newer_warning, tostring(pop), tostring(ping))
			newer_warning = nil
		end
	end
end

-- Generic helpers
-- Returns index and entry at that index, or nil if not found.
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
-- Essentially a _find_next_after('time'-or-'boss'), but if KIND is
-- 'boss', will also stop upon finding a timestamp.  Returns nil if
-- appropriate fencepost is not found.
function addon._find_timeboss_fencepost (kind, index)
	local fencepost
	local closest_time = addon._find_next_after('time',index)
	if kind == 'time' then
		fencepost = closest_time
	elseif kind == 'boss' then
		local closest_boss = addon._find_next_after('boss',index)
		if not closest_boss then
			fencepost = closest_time
		elseif not closest_time then
			fencepost = closest_boss
		else
			fencepost = math.min(closest_time,closest_boss)
		end
	end
	return fencepost
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

-- Iterate through g_loot.raiders in sorted (alphabetical/collated) order.  If
-- USE_FULLNAME then the realmname is appended.  If ONLINE_FILTER is present,
-- then only raider entries with a matching 'online' key are included.  Loop
-- variables are a running count, the raider name, and the corresponding entry
-- from g_loot.raiders.
do
	local function nextr (list, index)
		index = index + 1
		local name = list[index]
		if not name then
			flib.del(list)
			return nil
		end
		return index, name, list.__safety[name]
	end

	function addon:sorted_raiders_iter (use_fullname_p, opt_online_filter)
		local t = flib.new()
		for name,info in next, g_loot.raiders do
			if (not opt_online_filter) or (info.online == opt_online_filter) then
				-- this is not exactly "A?B:C" semantics, but it is exactly
				-- the behavior we want when fname is not present
				tinsert (t, use_fullname_p and info.fname or name)
			end
		end
		table.sort(t)
		t.__safety = g_loot.raiders
		return nextr, t, 0
	end
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_variant_item(item)   -- returns number 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 and 1)
		       or (t == RAID_FINDER and 2)  -- no ITEM_ for this, apparently
		       or nil
	end
	]]
	local bcodes = {
		[448] = "Warforged",  -- and 499?
		[449] = "Heroic",   -- 524?
		[450] = "Mythic",
		[451] = "LFR",
		[15] = "Epic",
		[171] = "Rare",
	}
	function addon:is_variant_item(istr)
--flib.safeiprint(strsplit (":", istr))
--flib.safeiprint(select (13, strsplit (":", istr)) )
		local bonuses = { select (13, strsplit (":", istr)) }
		if #bonuses < 1 then
			return -- nil for no bonus
		end
		local n = tonumber(bonuses[1]) or 0
		if n < 1 then
			return -- nil for no bonus
		end
		--@debug@
print("bonuses has", #bonuses, "entries, size entry is", bonuses[1], "for", istr)
		table.remove (bonuses, 1)
		assert (n == #bonuses, "test for variant item on " ..
			istr .. " extracted count " .. (n or "nil?") ..
			" but table size is " .. #bonuses)
		for i = 1, #bonuses do
			local b = tonumber(bonuses[i])
print("  bonus ", b, bcodes[b])
			bonuses[i] = b
		end
		--@end-debug@
		return 42  -- will figure out meaningful value later
	end
end

-- Called at the end of OnInit, 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 = _G.OuroLootSV
		self.popped = #g_loot > 0
		self.dprint('flow', "restoring", #g_loot, "entries")
		-- paranoia:  make sure the GUI isn't stumbling over these later
		local dofix, GetItemInfo = false, GetItemInfo
		for i,e in self:filtered_loot_iter('loot') do
			local missing_data = not GetItemInfo(e.id)
			e.cache_miss = (e.cache_miss or missing_data) or nil
			dofix = dofix or e.cache_miss
		end
		self.DO_ITEMID_FIX = dofix or nil
		_do_loot_metas()
		-- FIXME printed could be too large if entries were deleted, how much do we care?
		self.sharder = opts.autoshard
		if self.popped then
			-- Actual loot got restored, potentially re-activate.
			self:ScheduleTimer("Activate", 12, "_init-restore", opts.threshold)
		end
	else
		g_loot = {}
	end
	if type(g_loot.raiders) ~= 'table' then
		g_loot.raiders = {}
	end
	if type(g_loot.printed) ~= 'table' then
		g_loot.printed = {}
	end

	self.threshold = opts.threshold or self.threshold -- in the case of restoring but not tracking
	local g_loot_wrapper = self:gui_init (true, g_loot, g_uniques)
	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_wrapper)
	end
	-- Make sure we have a current .raiders array, since other dropdowns and
	-- whatnot depend on that information.
	self:CheckRoster()

	self.status_text = ("%s(r%s) communicating as ident %s commrev %s"):
		format (self.version, self.revision, self.ident, self.commrev)
	self:RegisterComm(self.ident)
	self:RegisterComm(self.identTg, "OnCommReceivedNocache")
end

-- Raid roster snapshots
do
	function addon:snapshot_raid (only_inraid_p)
		local ss = CopyTable(g_loot.raiders)
		local instance,maxsize = instance_tag()
		if only_inraid_p then
			for name,info in next, ss do
				if info.online == 3 then
					ss[name] = nil
				end
			end
		end
		return ss, maxsize, instance, time()
	end
end

-- Tie-in with Deadly Boss Mods (or other such addons)
do
	local candidates = {}
	local location
	local function fixup_durations (cache)
		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._addBossEntry(boss)
		addon:Fire ('NewBoss', boss)
		bossi = addon._adjustBossOrder (bossi, g_boss_signpost) or bossi
		g_boss_signpost = nil
		addon.latest_instance = boss.instance
		addon.dprint('loot', "added boss entry", bossi)
		if boss.reason == 'kill' then
			addon:RegisterEvent ("PLAYER_REGEN_DISABLED")
			addon:_mark_boss_kill (bossi)
			if opts.chatty_on_kill then
				local jumpprefix = addon.chatprefix ("GoToLootLine", bossi)
				addon:PCFPrint(_G.DEFAULT_CHAT_FRAME, jumpprefix,
					"Registered kill for '%s' in %s!", boss.bossname, boss.instance)
			end
		end
		wipe(candidates)
	end
	-- Ten seconds is a long time, but occasionally DBM takes for-EVAH to
	-- decide that a fight is over.
	local recent_boss = create_new_cache ('boss', 10, fixup_durations)

	-- Similar to _do_loot, but duration+ parms only present when locally generated.
	local function _do_boss (self, reason, bossname, intag, maxsize, duration)
		self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname,
		            "T:", intag, "MS:", maxsize, "D:", duration)
		if self.rebroadcast and duration then
			self:vbroadcast('boss', reason, bossname, intag, maxsize)
		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 recent_boss:test(signature) then
				self.dprint('cache', "remote boss <",signature,"> already in cache, skipping")
			else
				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',
					bossname	= bossname,
					reason		= reason,
					instance	= intag,
					duration	= duration,      -- deliberately may be nil
					raidersnap	= self:snapshot_raid(),
					maxsize		= maxsize,
				}
				tinsert(candidates,c)
			end
			break
		end
		self.dprint('loot',"<<_do_boss out")
	end
	-- This exposes the function to OCR, and can be a wrapper layer later.
	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.bossname 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

		-- Maybe redo this to only collapse *contiguous* wipes...?
		local i,d = 1,g_loot[1]
		while d ~= e do
			if d.bossname and
			   d.bossname == e.bossname and
			   d.instance == e.instance and
			   d.reason == 'wipe'
			then
				first = first or i
				attempts = attempts + 1
				assert(g_gui.g_dloot:remove(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 = 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
	local date, time, rawget, setmetatable = date, time, rawget, setmetatable

	--@debug@
	local tos = {}
	tos.time = function (e)
		return e.startday.text
	end
	tos.boss = function (e)
		return e.bossname .. '/' .. e.reason
	end
	tos.loot = function (e)
		return e.itemname .. '/' .. e.person .. '/' .. e.unique .. '/'
			.. tostring(e.disposition) .. (e.extratext and ('/'..e.extratext) or '')
	end
	--@end-debug@
	local loot_entry_mt = {
		__index = function (e,key)
			-- This shouldn't be required, as the refresh should be picking
			-- it up already.  Sigh.
			if key == 'cols' then
				pprint('mt', e.kind, "key is", key)
				addon:_fill_out_eoi_data(1)
			end
			return rawget(e,key)
		end,
		--@debug@
		__tostring = function (e)
			local k = e.kind
			if k then
				return ("<%s/%s>"):format(k, tos[k] and tos[k](e) or "?")
			end
			return "<unknown loot entry type>"
		end,
		--@end-debug@
	}
	function _do_loot_metas()
		for i,e in ipairs(g_loot) do
			setmetatable(e,loot_entry_mt)
		end
		_do_loot_metas = nil
	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 that.
	-- 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])
	-- FORMAT_STRING may contain $x (x in TmdHMS) tokens, with the same
	--    meanings as in Lua/strftime but restricted formatting:
	--      Y   year, 4 digit
	--      m   month, 2 digit
	--      d   day, 2 digit
	--      H   hour, 2 digit, 24-hour clock
	--      M   minute
	--      S   second
	-- 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 clock values printed; if absent, those values are
	--    taken from the DAY entry.
	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, caller-specified format
			end
		end
		format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year)
		format_timestamp_values.m = ("%.2d"):format (day_entry.startday.month)
		format_timestamp_values.d = ("%.2d"):format (day_entry.startday.day)
		format_timestamp_values.H = ("%.2d"):format ((time_entry_opt or day_entry).hour)
		format_timestamp_values.M = ("%.2d"):format ((time_entry_opt or day_entry).minute)
		format_timestamp_values.S = date ("%S", (time_entry_opt or day_entry).stamp)
		return fmt_opt:gsub ('%$([YmdHMS])', 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.
	-- More precisely, anything new on the EOI tab hits this; it does not
	-- necessarily need to be a looted item.
	function addon._addLootEntry (e)
		setmetatable(e,loot_entry_mt)

		if not done_todays_date then do_todays_date() end

		-- All kinds of things go awry (especially history preening) if two
		-- entries share the exact same timestamp, and we can't get any better
		-- than one second resolution.
		--
		-- Well, the only API in-game with finer precision is GetTime but the
		-- computer uptime doesn't help us.  Stripping the fractional part off
		-- and gluing it to time_t would be too risky as they don't cycle at
		-- the same time.  We could do something involving a count incrementing
		-- with the refresh rate, but... blech.
		--
		-- So we sort of cheat.  New entries are pushed into the future one
		-- second at a time, if needed, so that no two time_t's are equal.
		local h, m = GetGameTime()
		local time_t = time()
		local index = #g_loot  -- note, previous entry
		e.hour = h
		e.minute = m
		if index > 0 and g_loot[index].stamp >= time_t then
			time_t = g_loot[index].stamp + 1
			addon.dprint('flow', "bumping timestamp to", time_t)
		end
		e.stamp = time_t

		index = index + 1
		if e.kind == 'loot' then
			if (not e.unique) or (#e.unique==0) then
				e.unique = e.id .. (e.disposition or e.person) .. date (timestamp_fmt_unique, e.stamp)
			end
			addon:Fire ('NewLootEntry', e, index)
		end
		g_loot[index] = e
		g_gui.g_dloot[index] = nil
		addon:Fire ('NewEOIEntry', e, index)
		return index
	end

	-- Safety/convenience wrapper only.
	function addon._addBossEntry (e)
		local ret = addon._addLootEntry(e)
		assert(e.kind=='boss')
		local needSize = e.maxsize == nil
		local needSnap = e.raidersnap == nil
		local needInst = e.instance == nil
		if needSize or needSnap then
			local ss, max, inst = addon:snapshot_raid()
			if needSize then e.maxsize = max end
			if needSnap then e.raidersnap = ss end
			if needInst then e.instance = inst end
		end
		addon:Fire ('NewBossEntry', e, ret)
		return ret
	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)
		if is == should_be then
			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
				return
			end
		end

		assert(g_loot[is].kind == 'boss')
		local boss = g_gui.g_dloot:remove(is)
		g_gui.g_dloot:insert (should_be, boss)
		return should_be
	end
end

-- Disposition control; more precisely, how various dispositions affect flow
-- and displays elsewhere.
do
	local default = {
		text = "",
		hex = "|cffffffff",
		r = 1.0, g = 1.0, b = 1.0, a = 1,
		can_reassign = true,
		affects_history = true,
		from_notes_text = false,
	}
	local mt = {
		__index = function (t,k)
			-- don't point index directly at default, need to catch nils
			return default
		end,
	}
	local alldisps = setmetatable({}, mt)
	addon.disposition_colors = setmetatable({}, mt)

	-- These two are clunky wrappers, probably rework this once gain some data.
	function addon:_test_disposition (code, attrib)
		return alldisps[code][attrib]
	end
	function addon:_iter_dispositions (attrib)
		local r = {}
		for code,disp in next, alldisps do
			if disp[attrib] then
				r[code] = disp.text
			end
		end
		return next, r
	end

	function _add_loot_disposition (self, code, rhex, ghex, bhex, text_notes,
		text_menu, tooltip_txt, can_reassign_p, do_history_p, from_notes_text_p
	)
		assert(type(code)=='string' and #code>0)
		assert(type(text_notes)=='string')
		assert(type(rhex)=='string')
		assert(type(bhex)=='string')
		assert(type(ghex)=='string')

		addon.disposition_colors[code] = {
			text = text_notes,
			-- for chat output by core code
			hex = "|cff" .. rhex .. ghex .. bhex,
			-- for lib-st
			r = tonumber(rhex,16)/255,
			g = tonumber(ghex,16)/255,
			b = tonumber(bhex,16)/255,
			a = 1,
		}
		-- not not = poor man's "toboolean", don't potentially hold arg
		-- objects from garbage collection
		alldisps[code] = {
			text = text_notes,
			can_reassign = not not can_reassign_p,
			affects_history = not not do_history_p,
			from_notes_text = not not from_notes_text_p,
		}

		local dd = g_gui.add_dropdown_entry ('eoi_loot_mark', text_menu or text_notes,
			--[[function_table=]]nil, 'df_DISPOSITION', code, tooltip_txt)
		dd.colorCode = addon.disposition_colors[code].hex
		addon.dprint('flow', ("Source '%s' adds loot disposition '%s', flags"):
			format(self.name or tostring(self), code),
			can_reassign_p, do_history_p, from_notes_text_p)
		return dd
	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 (silent_p)
	if not silent_p then
		self:Print("Fixing up missing item cache data...")
	end

	local numfound = 0
	local earliest_fixed
	local borkedpat = '^'..UNKNOWN..': (%S+)'

	-- 'while true' so that we can use (inner) break as (outer) continue
	for i,e in self:filtered_loot_iter('loot') do while true do
		if not e.cache_miss then break end
		local borked_id = e.itemname:match(borkedpat) or e.id
		if not borked_id then break end
		numfound = numfound + 1
		-- Best to use the safest and most flexible API here, which is GII and
		-- its assload of return values.
		local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(borked_id)
		if iname then
			local msg = [[    Entry %d patched up with %s.]]
			e.quality = iquality
			e.itemname = iname
			e.itemstring = ilink:match("item[%-?%d:]+")
			e.id = tonumber(e.itemstring:match("^item:(%d+)") or 0)
			e.itemlink = ilink
			e.itexture = itexture
			e.cache_miss = nil
			if e.unique then
				local gu = g_uniques[e.unique]
				local player_i, player_h, hist_i = _history_by_loot_id (e.unique, "fixcache")
				if gu.loot ~= i then   -- is this an actual problem?
					pprint ('loot',
						("Unique value '%s' had iterator value %d but g_uniques index %s."):
						format(e.unique,i,tostring(gu.loot)))
				end
				if player_i then
					player_h.id[e.unique] = e.id
					msg = [[    Entry %d (and history) patched up with %s.]]
				end
			end
			earliest_fixed = earliest_fixed or i
			if not silent_p then self:Print(msg, i, ilink) end
		end
		break
	end end

	if earliest_fixed then
		self.loot_clean = earliest_fixed-1   -- this works out even at i == 1
	end
	if not silent_p then
		self:Print("...finished.  Found %d |4entry:entries; with weird data.", numfound)
	end
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()
		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 guid",
				winner[1], "with tag", 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
			   >winning_index == 1
			   >g_loot and history already has the replacement value
			B:  winner was generated remotely
			   >need to scan and replace
			Detecting A is strictly an optimization.  We should be able to do
			this code safely in all cases.  Important to note:  a local winner
			will always be at index 1, but a winner at index 1 does not
			necessarily mean it was locally generated (e.g., if the local
			itemfilter drops it but a remote player does an improv).  Just
			do the general case until/unless this becomes a problem.
			]]
			local cache = g_uniques:SEARCH(exist)
			local looti,hi,ui = cache.loot, cache.history, cache.history_may

			-- Active loot
			if looti and g_loot[looti].unique == exist then
				pprint('improv', "found and replaced loot entry", looti)
				g_loot[looti].unique = winner[2]
			else
				-- If sharded, filtered, or the improv was done by the local
				-- player, then the "previous" unique would not have made it
				-- into the tables to begin with.  So don't flag an error.
				pprint('improv', "No active loot found", looti,
					looti and g_loot[looti].unique, winning_index)
			end

			-- History
			if hi ~= g_uniques.NOTFOUND then
				hi = addon.history.byname[hi]
				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)
					assert(exist ~= winner[2])
					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

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

do
	local clicky
	function addon:horrible_horrible_error (err_msg)
		if self.display then
			local d = self.display
			if d then
				-- Take this down a piece at a time, on the assumption that
				-- the main window won't be able to do so.
				local gui = d:GetUserData("GUI state")
				local eoist = gui.eoiST
				if eoist then eoist:Hide() end
				local histst = gui.histST
				if histst then histst:Hide() end
				d:Hide()
			end
		end
		self.NOLOAD = err_msg
		-- This should happen so rarely that it's not worth moving into gui.lua
		if not StaticPopupDialogs["OUROL_ARGH"] then
			StaticPopupDialogs["OUROL_ARGH"] = flib.StaticPopup{
				button1 = OKAY,
			}
			clicky = addon.format_hypertext(
				[[ SYSTEM FAILURE -- RELEASE RINZLER ]], "|cffff0000",
				function() StaticPopup_Show "OUROL_ARGH" end)
		end
		StaticPopupDialogs["OUROL_ARGH"].text = horrible_error_text:format(err_msg)
		PlaySoundFile ([[Interface\AddOns\Ouro_Loot\sfrr.ogg]], "Master")
		addon:Print ("        ")
		addon:Print ("        ", clicky)
		addon:Print ("        ")
	end

	function _unavoidable_collision (err)
		addon:horrible_horrible_error (err)
		-- we don't actually need to kill the GUI in this case
		addon.NOLOAD = nil
	end
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",
--       ["person_class"] = "PRIEST", -- may be missing, used in display only
--       -- sorted array:
--       ["unique"] = { most_recent_tag, previous_tag, .... },
--       -- these are indexed by unique tags, and 'count' may be missing:
--       ["when"] = { ["tag"] = "formatted timestamp for displaying loot history" },
--       ["id"] = { ["tag"] = 11111 },
--       ["count"] = { ["tag"] = "x3", .... },
--     },
--     [2] = {
--       ["name"] = "OtherPlayer",
--       ......
--     }, ......
--   },
--   ["OtherRealm"] = ......
-- }
--
-- Up through 2.18.4 (specifically through rev 95), an individual player's
-- table looked like this:
--       ["name"] = "Farmbuyer",
--           -- most recent loot:
--       [1] = { id = nnnnn, when = "formatted timestamp for displaying" }
--           -- previous loot:
--       [2] = { ......., [count = "x3"]                                 }
-- which was much easier to manipulate, but had a ton of memory overhead.
do
	local new, del, date = flib.new, flib.del, date

	-- 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 = {}, new(), new()
		for u,tstamp in pairs(p.when) do
			-- XXX multiple identical tstamps
			uniques_bywhen[tstamp] = u
			when_array[#when_array+1] = tstamp
		end
		table.sort (when_array, compare_timestamps)
		for i,tstamp in ipairs(when_array) do
			new_uniques[i] = uniques_bywhen[tstamp]
		end
		p.unique = new_uniques
		del(uniques_bywhen)
		del(when_array)
	end

	function addon:repair_history_integrity()
		local rcount, pcount, hcount, errors = 0, 0, 0, 0
		local empties_to_delete = {}

		for rname,realm in pairs(self.history_all) do
			for pk,player in ipairs(realm) do
				local id, when, unique, count = {}, {}, {}, {}
				for i,h in ipairs(player.unique) do
					h = tostring(h)
					if player.when[h] and player.id[h] then
						unique[#unique+1] = h
						id[h] = player.id[h]
						when[h] = player.when[h]
						count[h] = player.count[h]
					else
						self:Print("> Realm %s, player %s, entry %d:  tag <%s>, id <%s>, time <%s>, count <%s>",
							rname, player.name, i, h, tostring(player.id[h]),
							tostring(player.when[h]), tostring(player.count[h]))
						errors = errors + 1
					end
					hcount = hcount + 1
				end
				player.id, player.when, player.unique, player.count =
					id, when, unique, count
				player.person_class = player.person_class or
					(g_loot.raiders[player.name] and g_loot.raiders[player.name].class)
				if #player.unique > 1 then
					sort_player(player)
				elseif #player.unique == 0 then
					tinsert (empties_to_delete, 1, pk)
				end
				pcount = pcount + 1
			end
			if #empties_to_delete > 0 then
				for _,pk in ipairs(empties_to_delete) do
					local player = tremove (realm, pk)
					self:Print("> Realm %s, player %s, is empty", rname, player.name)
					errors = errors + 1
				end
				wipe(empties_to_delete)
			end
			if #realm == 0 then
				self.history_all[rname] = nil
				self:Print("> Realm %s is empty", rname)
				errors = errors + 1
			end
			rcount = rcount + 1
		end
		self:_build_history_names()
		if errors > 0 then
			self:Print("The listed entries have been erased from history.")
		end
		self:Print("Finished examining %d realms, %d players, %d history entries.",
			rcount, pcount, hcount)
	end

	-- Possibly called during login.  Cleared when no longer needed.
	-- Rewrites a PLAYER table from format 3 to format 4.
	function addon:_uplift_history_format (player)
		local unique, when, id, count = {}, {}, {}, {}
		local name = player.name

		for i,h in ipairs(player) do
			local U = h.unique
			unique[i] = U
			when[U] = h.when
			id[U] = h.id
			count[U] = h.count
		end

		wipe(player)
		player.name = name
		player.id, player.when, player.unique, player.count =
			id, when, unique, count
	end

	function addon:_cache_history_uniques()
		UpdateAddOnMemoryUsage()
		local before = GetAddOnMemoryUsage(nametag)
		local trouble
		local count = 0
		for hi,player in ipairs(self.history) do
			for ui,u in ipairs(player.unique) do
				g_uniques[u] = { history = player.name, history_may = ui }
				count = count + 1
			end
		end
		for i,e in self:filtered_loot_iter('loot') do
			if e.unique and e.unique ~= "" then
				local hmmm = rawget(g_uniques,e.unique)
				if hmmm then
					hmmm.loot = i
				elseif not self:_test_disposition (e.disposition, 'affects_history') then
					g_uniques[e.unique] = { loot = i, history = g_uniques.NOTFOUND }
					count = count + 1
				else
					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 }
				end
			else
				-- The usual cause: when only source is from an older client
				-- and the disposition did not trigger addhistory, then not
				-- even a stub history entry happens.  Code has now been added
				-- to try harder to prevent this, but it's still best to not
				-- simple ignore it.
				trouble = true
				pprint('loot', "ERROR precache loop found missing/outdated unique tag!",
					i, "tag <"..tostring(e.unique).."> from?", tostring(e.bcast_from))
			end
		end
		UpdateAddOnMemoryUsage()
		local after = GetAddOnMemoryUsage(nametag)
		-- This displayed number is occasionally negative, if the GC happens
		-- to run during the loops.  How much should we care?
		self:Print("Pre-scanning history for faster loot handling on %s used %.2f MB of memory across %d entries.",
			self.history.realm, (after-before)/1024, count)
		if trouble then
			self:Print("Note that there were inconsistencies in the data;",
				"you should consider submitting a bug report (including your",
				"SavedVariables file), and regenerating or preening this",
				"realm's loot history.  If you keep seeing this message, type",
				"'/ouroloot fix ?' and try some of those actions.")
		end
		g_uniques:SETMODE('probe')
	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
		-- why yes, I *did* spend many years as a YP/NIS admin, how did you know?
		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

		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, id={}, when={}, unique={}, count={} }
			self.history.byname[name] = i
		end
		return i, self.history[i]
	end

	-- Prepends data from the loot entry at LOOTINDEX to the history for that
	-- player, making it the "most recent" entry regardless of actual data.
	-- In the usual case, builds a formatted timestamp string from g_today and
	-- the loot entry's recorded time (thus the formatted string really *will*
	-- be the most recent entry).  If g_today has not been set, then falls
	-- back on formatting LOOTINDEX's time_t 'stamp' field.
	--
	-- If RESORT_P is true-valued, then re-sorts the player's history based on
	-- formatted timestmps, instead of leaving the new entry as the latest.
	local tfmt_hist = timestamp_fmt_history:gsub('%%','$')
	function addon:_addHistoryEntry (lootindex, resort_p)
		local e = g_loot[lootindex]
		if e.kind ~= 'loot' then return end

		if e.person_realm and opts.history_ignore_xrealm then
			return
		end

		local i,h = self:get_loot_history(e.person)
		-- If we've added anything at all into g_loot this session, g_today
		-- will be set.  If we've logged on simply to manipulate history, then
		-- try and fake a timestamp (it'll be "close enough").
		-- (WoD:  This logic may be backwards now that loot entries track time_t.)
		local when = g_today
			         and self:format_timestamp (tfmt_hist, g_today, e)
			         or date (timestamp_fmt_history, e.stamp)
		assert(h.name==e.person)

		-- Should rarely happen anymore:
		if (not e.unique) or (#e.unique==0) then
			e.unique = e.id .. e.person .. when
		end
		local U = e.unique
		tinsert (h.unique, 1, U)
		h.when[U] = when
		h.id[U] = e.id
		h.count[U] = e.count
		if resort_p then
			sort_player(h)
		end
		g_uniques[U] = { loot = lootindex, history = e.person }
		self:Fire ('NewHistory', e.person, U)
	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]
		g_uniques:RESET()

		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
			sort_player(h)
		end
	end

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

	-- Given a unique tag OR an entry in a g_loot table, looks up the
	-- corresponding history entry.  Returns the player's index and history
	-- table (as in get_loot_history) and the index into that table of the
	-- loot entry.  On failure, returns nil and an error message ready to be
	-- formatted with the loot's name/itemlink.
	function _history_by_loot_id (needle, operation_text)
		local errtxt
		if type(needle) == 'string' then
			-- unique tag
		elseif type(needle) == 'table' then
			if needle.kind ~= 'loot' then
				error("trying to "..operation_text.." something that isn't loot")
			end
			needle = needle.unique
			if not needle then
				return nil, --[[errtxt=]]"Entry for %s is missing a history tag!"
			end
		else
			error("'"..tostring(needle).."' is neither unique string nor loot entry!")
		end

		local player_i, player_h
		local cache = g_uniques[needle]

		if cache.history == g_uniques.NOTFOUND then
			-- 1) loot an item, 2) clear old history, 3) reassign from current loot
			-- Bah.  Anybody that tricky is already recoding the tables directly anyhow.
			errtxt = "There is no record of %s ever having been assigned!"
		else
			player_i = addon.history.byname[cache.history]
			player_h = addon.history[player_i]
			if cache.history_may
			   and needle == player_h.unique[cache.history_may]
			then
				return player_i, player_h, cache.history_may
			end
			for i,u in ipairs(player_h.unique) do
				if needle == u then
					cache.history_may = i   -- might help, might not
					return player_i, player_h, i
				end
			end
		end

		if not errtxt then
			-- The cache finder got a hit, but now it's gone?  WTF?
			errtxt = "ZOMG!  %s was in history but now is gone.  Possibly your history tables have been corrupted and should be recreated.  This is likely a bug.  Tell Farmbuyer what steps you took to cause this, with as many details as possible."
		end
		return nil, errtxt
	end

	-- Handles reassigning loot between players.  Arguments depend on who's
	-- calling it:
	--     "local", row_index, new_recipient
	--     "remote", sender, unique_id, item_id, old_recipient, new_recipient
	-- In the local case, must also broadcast a trigger.  In the remote case,
	-- must figure out the corresponding loot entry (if it exists).  In both
	-- cases, must update history appropriately.  Returns nil if anything odd
	-- happens; returns the affected loot index on success.
	function addon:reassign_loot (how, ...)
		-- This must all be filled out in all cases:
		local e, index, from_name, to_name, unique, id
		-- Only set in remote case:
		local sender

		if how == "local" then
			-- GUI doesn't allow reassignment unless the item is not-shard,
			-- so we can assume the presence of a unique tag in this function.
			index, to_name = ...
			assert(type(to_name)=='string' and to_name:len()>0)
			index = assert(tonumber(index))
			e = g_loot[index]
			id = e.id
			unique = assert(e.unique)
			from_name = e.person

		elseif how == "remote" then
			sender, unique, id, from_name, to_name = ...
			id = tonumber(id)
			local cache
			local loop = 0
			repeat    -- wtb continue statement pst
				if loop > 1 then break end
				e = nil
				cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique]
				index = tonumber(cache.loot)
				if index then
					e = g_loot[index]
				else
				end
				loop = loop + 1
			until e and (e.id == id)

		else
			return  -- silently ignore future cases from future clients
		end

		if self.debug.loot then
			local m = ("Reassign index %d (pre-unique %s) with id %d from '%s' to '%s'."):
				format(index, unique, id, tostring(from_name), tostring(to_name))
			self.dprint('loot', m)
			if sender == my_name then
				self.dprint('loot',"(Returning early from debug mode's double self-reassign.)")
				return index
			end
		end

		if not e then
			-- say something?
			return
		end

		local from_i, from_h, hist_i = _history_by_loot_id (e, "reassign")
		local to_i,to_h = self:get_loot_history(to_name)

		if not from_i then
			if how == "local" then
				-- from_h here is the formatted error text
				self:Print(from_h .. "  Loot will be reassigned, but history will NOT be updated.", e.itemlink)
			end
		else
			-- XXX do some sanity checks here?  from_name == from_h.name, etc?
			-- If something were wrong at this point, what could we do?

			-- the Book of Job 1:21:  "Naked I came from my faction capital
			-- city, and naked I shall return thither."
			if from_h ~= to_h then
				local U = tremove (from_h.unique, hist_i)
				-- "The loot master giveth..."
				to_h.unique[#to_h.unique+1] = U
				to_h.when[U] = from_h.when[U]
				to_h.id[U] = from_h.id[U]
				to_h.count[U] = from_h.count[U]
				sort_player(to_h)
				-- "...and the loot master taketh away."
				from_h.when[U] = nil
				from_h.id[U] = nil
				from_h.count[U] = nil
			end
			-- "Blessed be the lookup cache of the loot master."
			g_uniques[unique] = { loot = index, history = to_name }
		end
		local from_person_class = e.person_class or from_h.person_class
			or (g_loot.raiders[from_name] and g_loot.raiders[from_name].class)
			or select(2,UnitClass(from_name))
		e.person = to_name
		e.person_class = to_h.person_class
			or (g_loot.raiders[to_name] and g_loot.raiders[to_name].class)
			or select(2,UnitClass(to_name))
		self.hist_clean = nil
		self.loot_clean = nil

		if how == "local" then
			if opts.chatty_on_local_changes then
				_notify_about_change (_G.UNIT_YOU, index, nil, from_name, from_person_class)
			end
			self:vbroadcast('reassign', unique, id, from_name, to_name)
		elseif opts.chatty_on_remote_changes then
			_notify_about_change (sender, index, nil, from_name, from_person_class)
		end
		if self.display then
			self.display:GetUserData("GUI state").eoiST:OuroLoot_Refresh(index)
			self:redisplay()
		end
		self:Fire ('Reassign', unique, id, e, from_name, to_name)
		return index
	end

	local function expunge (player, index_or_unique)
		local i,u
		if type(index_or_unique) == 'string' then
			for u = 1, #player.unique do
				if player.unique[u] == index_or_unique then
					i = u
					break
				end
			end
		elseif type(index_or_unique) == 'number' then
			i = index_or_unique
		end
		if not i then
			return -- error here?
		end
		u = player.unique[i]
		assert(#u>0)
		tremove (player.unique, i)
		player.when[u], player.id[u], player.count[u] = nil, nil, nil
		g_uniques[u] = nil
		addon.hist_clean = nil
		addon:Fire ('DelHistory', player.name, u)
		return #player.unique
	end

	-- Mirror of _addHistoryEntry.  Arguments are either:
	--   E          - loot entry
	--   U,ITEM     - unique tag, and a name/itemlink for errors
	-- If this entry was the only one for that player, will also remove that
	-- player's tables from the history array.
	--
	-- On success, returns the number of remaining history entries for that
	-- player (potentially zero).  On failure, returns nil+error.
	function addon:_delHistoryEntry (first, item)
		if type(first) == 'table' then
			item = first.itemlink or item
		--elseif type(first) == 'string' then
		end

		local from_i, from_h, hist_i = _history_by_loot_id (first, "delete")

		if not from_i then
			-- from_h is the formatted error text
			return nil, (from_h
			        .."  Loot will be deleted, but history will NOT be updated."
			       ):format(item)
		end

		local remaining = expunge (from_h, hist_i)
		if not remaining then
			return nil, "Something bizarre happening trying to delete "..item
		elseif remaining > 0 then
			return remaining
		end
		tremove (self.history, from_i)
		self:_build_history_names()
		return 0
	end

	-- Any extra work for the "Mark as <x>" dropdown actions.  The
	-- corresponding <x> will already have been assigned in the loot entry.
	function addon:history_handle_disposition (index, olddisp)
		local e = g_loot[index]
		-- Standard disposition has a nil entry, but that's tedious in debug
		-- output, so force to a string instead.
		olddisp = olddisp or 'normal'
		local newdisp = e.disposition or 'normal'
		-- Ignore misclicks and the like
		if olddisp == newdisp then return end

		local name = e.person

		if not self:_test_disposition (newdisp, 'affects_history') then
			-- remove history entry if it exists
			local name_i, name_h, hist_i = _history_by_loot_id (e, "mark")
			if hist_i then
				-- clears g_uniques and fires DelHistory
				expunge (name_h, hist_i)
			elseif not self:_test_disposition (olddisp, 'affects_history') then
				-- Sharding a vault item, or giving the auto-sharder something to bank,
				-- etc, wouldn't necessarily have had a history entry to begin with.
				-- So this isn't treated as an error.
			else
				self:Print(name_h .. "  Loot has been marked, but history will NOT be updated.", e.itemlink)
			end
			return
		end

		if (not self:_test_disposition (olddisp, 'affects_history'))
		   and self:_test_disposition (newdisp, 'affects_history')
		then
			-- Must create a new history entry.
			local name_i, name_h = self:get_loot_history(name)
			-- puts entry into g_uniques and fires NewHistory
			self:_addHistoryEntry (index, --[[re-sort entries=]]true)
			self.hist_clean = nil
			return
		end
	end

	-- This is not entirely "history" but not completely anything else either.
	-- Handles the primary "Mark as <x>" action.  Arguments depend on who's
	-- calling it:
	--     'local', row_index, new_disposition
	--     'remote', sender, unique_id, item_id, old_disposition, new_disposition
	-- In the local case, must also broadcast a trigger.  In the remote case,
	-- must figure out the corresponding loot entry (if it exists).  In both
	-- cases, must update history appropriately.  Returns nil if anything odd
	-- happens (not necessarily an error!); returns the affected loot index
	-- on success.
	function addon:loot_mark_disposition (how, ...)
		-- This must all be filled out in all cases:
		local e, index, olddisp, newdisp, unique, id
		-- Only set in remote case:
		local sender

		if how == 'local' then
			index, newdisp = ...
			index = assert(tonumber(index))
			e = g_loot[index]
			id = e.id
			unique = e.unique   -- can potentially still be nil at this step
			olddisp = e.disposition

		elseif how == 'remote' then
			sender, unique, id, olddisp, newdisp = ...
			id = tonumber(id)
			local cache
			local loop = 0
			repeat    -- wtb continue statement pst
				if loop > 1 then break end
				e = nil
				cache = cache and g_uniques:SEARCH(unique) or g_uniques[unique]
				index = tonumber(cache.loot)
				if index then
					e = g_loot[index]
				else
				end
				loop = loop + 1
			until e and (e.id == id)

		else
			return  -- silently ignore future cases from future clients
		end

		if self.debug.loot then
			local m = ("Re-mark index %d (pre-unique %s) with id %d from '%s' to '%s'."):
				format(index, unique, id, tostring(olddisp), tostring(newdisp))
			self.dprint('loot', m)
			if sender == my_name then
				self.dprint('loot',"(Returning early from debug mode's double self-mark.)")
				return index
			end
		end

		if not e then
			-- say something?
			return
		end

		e.disposition = newdisp
		e.bcast_from = nil  -- I actually don't remember now why this gets cleared...
		e.extratext = nil
		self:history_handle_disposition (index, olddisp)
		self.hist_clean = nil
		self.loot_clean = nil
		-- A unique tag has been set by this point.
		if how == 'local' then
			unique = assert(e.unique)
			if opts.chatty_on_local_changes then
				_notify_about_change (_G.UNIT_YOU, index, olddisp)
			end
			self:vbroadcast('mark', unique, id, olddisp, newdisp)
		end
		self:Fire ('MarkAs', unique, id, e, olddisp or 'normal', newdisp or 'normal')
		return index
	end
end


------ Player communication
do
	local select, tconcat, unpack = select, table.concat, unpack
	local function assemble(t,...)
		local n = select('#',...)
		if n > 0 then
			local msg = {t,...}
			-- tconcat requires strings, but T is known to be one already
			-- can't use #msg since there might be nil holes
			for i = 2, n+1 do
				msg[i] = tostring(msg[i] or "")
			end
			return tconcat (msg, '\a')
		end
		return t
	end

	-- broadcast('tag', <stuff>)
	-- vbroadcast('tag', <stuff>)
	function addon:vbroadcast(tag,...)
		return self:broadcast(self.commrev..tag,...)
	end
	function addon:broadcast(tag,...)
		local msg = assemble(tag,...)
		self.dprint('comm', "<broadcast>:", msg)
		self:SendCommMessage(self.ident, msg, "RAID")
		-- this is what lets us debug our own message traffic:
		if self.debug.comm and self.is_guilded then
			self:SendCommMessage(self.ident, msg, "GUILD")
		end
	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

	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)
		local what = addon.enabled and "tracking" or
			(addon.rebroadcast and "broadcasting" or "disabled")
		addon:whispercast (sender, 'pong', addon.version, what, addon.revision)
	end
	OCR_funcs.pong = function (sender, _, ver, status, opt_rev)
		local s = ("|cff00ff00%s|r %s(r%s) is |cff00ffff%s|r"):
			format (sender, ver, opt_rev or "?", status)
		addon:Print("Echo: ", s)
		adduser (sender, s, status=="tracking" or status=="broadcasting" or nil)
	end
	OCR_funcs.revcheck = function (sender, _, revlarge)
		addon.dprint('comm', "revcheck, sender", sender)
		addon:_check_version (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)
		local index = addon:loot_mark_disposition ("remote", sender, unique, item, old, new)
		--if not addon.enabled then return end   -- hmm
		if index and opts.chatty_on_remote_changes then
			_notify_about_change (sender, index, old)
		end
	end

	OCR_funcs['17reassign'] = function (sender, _, unique, item, from, to)
		addon.dprint('comm', "DOTreassign/17, sender", sender, "unique", unique,
			"item", item, "from", from, "to", to)
		--[[local index =]] addon:reassign_loot ("remote", sender, unique, item, from, to)
		-- Notification handled inside reassign_loot.
	end

	OCR_funcs['16loot'] = function (sender, _, recip, item, count, extratext)
		addon.dprint('comm', "DOTloot/16, sender", sender, "recip", recip, "item", item, "count", count)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		-- Empty unique string will pass through all of the loot handling,
		-- and then be rewritten by the history routine (into older string
		-- of ID+date).
		g_seeing_oldsigs = g_seeing_oldsigs or {}
		g_seeing_oldsigs[sender] = true
		addon:CHAT_MSG_LOOT ("broadcast", recip, --[[unique=]]"", item, count, sender, extratext)
	end
	OCR_funcs.loot = OCR_funcs['16loot']   -- old unversioned stuff goes to 16
	OCR_funcs['17loot'] = function (sender, _, recip, unique, item, count, extratext)
		addon.dprint('comm', "DOTloot/17, sender", sender, "recip", recip,
			"unique", unique, "item", item, "count", count, "extratext", extratext)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		addon:CHAT_MSG_LOOT ("broadcast", recip, unique, item, count, sender, extratext)
	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, --[[maxsize=]]0)
	end
	OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize)
		addon.dprint('comm', "DOTboss/16,17, sender", sender, "reason", reason,
			"name", bossname, "it", instancetag, "size", maxsize)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		addon:on_boss_broadcast (reason, bossname, instancetag, maxsize)
	end
	OCR_funcs['17boss'] = OCR_funcs['16boss']

	local bcast_on = addon.format_hypertext ([[the red pill]], '|cffff4040',
		function (self)
			if not self.rebroadcast then
				self:Activate("bcast-respond",nil,true)
			end
			self:broadcast('bcast_responder')
		end)
	local waferthin = addon.format_hypertext ([[the blue pill]], '|cff0070dd',
		function (self)
			g_wafer_thin = true              -- mint? it's wafer thin!
			self: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! Click %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in.",
				sender,
				tostring(bcast_on),
				tostring(waferthin))
		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 disassembler and dispatcher.  The static weak table
	-- is not my favorite approach to handling ellipses, but it lets me loop
	-- through potential nils easily without creating a ton of garbage.
	local OCR_data = setmetatable({}, {__mode='v'})
	local function dotdotdot (sender, tag, ...)
		local f = OCR_funcs[tag]
		if f then
			--wipe(OCR_data)   costs more than its worth here
			local n = select('#',...)
			for i = 1, n do
				local d = select(i,...)
				OCR_data[i] = (d ~= "") and d or nil
			end
			addon.dprint('comm', ":...processing", tag, "from", sender, "with arg count", n)
			return f(sender,tag,unpack(OCR_data,1,n))
		end
		addon.dprint('comm', "unknown comm message", tag, "from", sender)
	end
	-- Recent message cache (this can be accessed via advanced options panel)
	addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)

	function addon:OnCommReceived (prefix, msg, distribution, sender)
		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
			self.dprint('cache', "OCR message <",msg,"> already in cache, skipping")
			return
		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

addon:MODULE_PROTOTYPE_POINTERS()
addon.FILES_LOADED = 1
-- vim:noet