view core.lua @ 9:4fba9c6b5d3d

Sanity check on history realm list dropdown.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Fri, 17 Jun 2011 20:30:46 +0000
parents 30ba1f35e164
children 67b8537e8432
line wrap: on
line source
local addon = select(2,...)

--[==[
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 index formatted into text window FOO, default 0
- saved:		table of copies of saved texts, default nil; keys are numeric
				indices of tables, subkeys of those are name/forum/attend/date
- autoshard:	optional name of disenchanting player, default nil
- threshold:	optional loot threshold, default nil

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

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

]==]

------ Saved variables
OuroLootSV		= nil   -- possible copy of g_loot
OuroLootSV_opts	= nil   -- same as option_defaults until changed
OuroLootSV_hist	= nil


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


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

	revision		= 15
	ident			= "OuroLoot2"
	identTg			= "OuroLoot2Tg"
	status_text		= nil

	DEBUG_PRINT		= false
	debug = {
		comm = false,
		loot = false,
		flow = false,
		notraid = false,
		cache = false,
	}
	function dprint (t,...)
		if DEBUG_PRINT and debug[t] then return _G.print("<"..t.."> ",...) end
	end

	if author_debug then
		function pprint(t,...)
			return _G.print("<<"..t..">> ",...)
		end
	else
		pprint = flib.nullfunc
	end

	enabled			= false
	rebroadcast		= false
	display			= nil   -- display frame, when visible
	loot_clean		= nil   -- index of last GUI entry with known-current visual data
	sender_list		= {active={},names={}}   -- this should be reworked
	threshold		= debug.loot and 0 or 3     -- rare by default
	sharder			= nil   -- name of person whose loot is marked as shards

	-- The rest is also used in the GUI:

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

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

	bossmod_registered = nil
	bossmods = {}

	requesting		= nil   -- for prompting for additional rebroadcasters

	thresholds, quality_hexes = {}, {}
	for i = 0,6 do
		local hex = _G.select(4,_G.GetItemQualityColor(i))
		local desc = _G["ITEM_QUALITY"..i.."_DESC"]
		quality_hexes[i] = hex
		thresholds[i] = hex .. desc .. "|r"
	end

	_G.setfenv (1, _G)
end

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


------ Globals
local g_loot			= nil
local g_restore_p		= nil
local g_saved_tmp		= nil   -- restoring across a clear
local g_wafer_thin		= nil   -- for prompting for additional rebroadcasters
local g_today			= nil   -- "today" entry in g_loot
local opts				= nil

local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber

local pprint, tabledump = addon.pprint, flib.tabledump

-- En masse forward decls of symbols defined inside local blocks
local _register_bossmod
local makedate, create_new_cache, _init

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

	function addon.format_hypertext (code, text, color)
		return hypertext_format_str:format (code,
			type(color)=='number' and addon.quality_hexes[color] or color,
			text)
	end

	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton)
		local ltype, arg = strsplit(":",link)
		if ltype ~= "OuroRaid" then return end
		if arg == 'openloot' then
			addon:BuildMainDisplay()
		elseif arg == 'help' then
			addon:BuildMainDisplay('help')
		elseif arg == 'bcaston' then
			if not addon.rebroadcast then
				addon:Activate(nil,true)
			end
			addon:broadcast('bcast_responder')
		elseif arg == 'waferthin' then   -- mint? it's wafer thin!
			g_wafer_thin = true          -- fuck off, I'm full
			addon:broadcast('bcast_denied')   -- remove once tested
		end
	end)

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

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

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


------ Expiring caches
--[[
foo = create_new_cache("myfoo",15[,cleanup]) -- ttl
foo:add("blah")
foo:test("blah")   -- returns true
]]
do
	local caches = {}
	local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
	cleanup_group:SetLooping("REPEAT")
	cleanup_group:SetScript("OnLoop", function(cg)
		addon.dprint('cache',"OnLoop firing")
		local now = GetTime()
		local alldone = true
		-- this is ass-ugly
		for _,c in ipairs(caches) do
			while (#c > 0) and (now - c[1].t > c.ttl) do
				addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m)
				tremove(c,1)
			end
			alldone = alldone and (#c == 0)
		end
		if alldone then
			addon.dprint('cache',"OnLoop finishing animation group")
			cleanup_group:Finish()
			for _,c in ipairs(caches) do
				if c.func then c:func() end
			end
		end
		addon.dprint('cache',"OnLoop done")
	end)

	local function _add (cache, x)
		tinsert(cache, {t=GetTime(),m=x})
		if not cleanup_group:IsPlaying() then
			addon.dprint('cache', cache.name, "STARTING animation group")
			cache.cleanup:SetDuration(2)  -- hmmm
			cleanup_group:Play()
		end
	end
	local function _test (cache, x)
		for _,v in ipairs(cache) do
			if v.m == x then return true end
		end
	end
	function create_new_cache (name, ttl, on_alldone)
		local c = {
			ttl = ttl,
			name = name,
			add = _add,
			test = _test,
			cleanup = cleanup_group:CreateAnimation("Animation"),
			func = on_alldone,
		}
		c.cleanup:SetOrder(1)
		-- setting OnFinished for cleanup fires at the end of each inner loop,
		-- with no 'requested' argument to distinguish cases.  thus, on_alldone.
		tinsert (caches, c)
		return c
	end
end


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

	if OuroLootSV_opts == nil then
		OuroLootSV_opts = {}
		self:ScheduleTimer(function(s)
			s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
			virgin = nil
		end,10,self)
	end
	opts = OuroLootSV_opts
	for opt,default in pairs(option_defaults) do
		if opts[opt] == nil then
			opts[opt] = default
		end
	end
	option_defaults = nil
	-- transition&remove old options
	opts["forum_use_itemid"] = nil
	if opts["forum_format"] then
		opts.forum["Custom..."] = opts["forum_format"]
		opts["forum_format"] = nil
	end
	-- get item filter table if needed
	if opts.itemfilter == nil then
		opts.itemfilter = addon.default_itemfilter
	end
	addon.default_itemfilter = nil

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

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

	_init(self)
	self.OnInitialize = nil
end

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

	-- Cribbed from Talented.  I like the way jerry thinks: the first argument
	-- can be a format spec for the remainder of the arguments.  (The new
	-- AceConsole:Printf isn't used because we can't specify a prefix without
	-- jumping through ridonkulous hoops.)  The part about overriding :Print
	-- with a version using prefix hyperlinks is my fault.
	do
		local AC = LibStub("AceConsole-3.0")
		local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
		function addon:Print (str, ...)
			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
				return AC:Print (chat_prefix, str:format(...))
			else
				return AC:Print (chat_prefix, str, ...)
			end
		end
	end

	if opts.keybinding then
		local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
		btn:SetAttribute("type", "macro")
		btn:SetAttribute("macrotext", "/ouroloot toggle")
		if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then
			SaveBindings(GetCurrentBindingSet())
		else
			self:Print("Error registering '%s' as a keybinding, check spelling!", opts.keybinding_text)
		end
	end

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


------ Event handlers
function addon:_clear_SVs()
	g_loot = {}  -- not saved, just fooling PLAYER_LOGOUT tests
	OuroLootSV = nil
	OuroLootSV_opts = nil
	OuroLootSV_hist = nil
	ReloadUI()
end
function addon:PLAYER_LOGOUT()
	if (#g_loot > 0) or g_loot.saved
	   or (g_loot.forum and g_loot.forum ~= "")
	   or (g_loot.attend and g_loot.attend ~= "")
	then
		g_loot.autoshard = self.sharder
		g_loot.threshold = self.threshold
		for i,e in ipairs(g_loot) do
			e.cols = nil
		end
		OuroLootSV = g_loot
	end
	for r,t in pairs(self.history_all) do if type(t) == 'table' then
		if #t == 0 then
			self.history_all[r] = nil
		else
			t.realm = nil
			t.st = nil
			t.byname = nil
		end
	end end
	OuroLootSV_hist = self.history_all
end

function addon:RAID_ROSTER_UPDATE (event)
	if GetNumRaidMembers() > 0 then
		local inside,whatkind = IsInInstance()
		if inside and (whatkind == "pvp" or whatkind == "arena") then
			return self.dprint('flow', "got RRU event but in pvp zone, bailing")
		end
		if event == "Activate" then
			-- dispatched manually from Activate
			self:RegisterEvent "CHAT_MSG_LOOT"
			_register_bossmod(self)
		elseif event == "RAID_ROSTER_UPDATE" then
			-- event registration from onload, joined a raid, maybe show popup
			if opts.popup_on_join and not self.popped then
				self.popped = StaticPopup_Show "OUROL_REMIND"
				self.popped.data = self
			end
		end
	else
		self:UnregisterEvent "CHAT_MSG_LOOT"
		self.popped = nil
	end
end

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

	local GetItemInfo, GetItemIcon = GetItemInfo, GetItemIcon

	-- 'from' and onwards only present if this is triggered by a broadcast
	function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
		local itexture = GetItemIcon(itemid)
		--local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(itemid)
		local iname, ilink, iquality = GetItemInfo(itemid)
		if (not iname) or (not itexture) then
			iname, ilink, iquality, itexture = 
				UNKNOWN..': '..itemid, 'item:6948', ITEM_QUALITY_COMMON, [[ICONS\INV_Misc_QuestionMark]]
		end
		self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext)

		local i
		itemid = tonumber(ilink:match("item:(%d+)"))
		if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then
			if (self.rebroadcast and (not from)) and not local_override then
				self:broadcast('loot', recipient, itemid, count)
			end
			if self.enabled or local_override then
				local signature = recipient .. iname .. (count or "")
				if self.recent_loot:test(signature) then
					self.dprint('cache', "loot <",signature,"> already in cache, skipping")
				else
					self.recent_loot:add(signature)
					i = self._addLootEntry{   -- There is some redundancy here...
						kind		= 'loot',
						person		= recipient,
						person_class= select(2,UnitClass(recipient)),
						quality		= iquality,
						itemname	= iname,
						id			= itemid,
						itemlink	= ilink,
						itexture	= itexture,
						disposition	= (recipient == self.sharder) and 'shard' or nil,
						count		= count,
						bcast_from	= from,
						extratext	= extratext,
						is_heroic	= self:is_heroic_item(ilink),
					}
					self.dprint('loot', "added entry", i)
					self:_addHistoryEntry(i)
					if self.display then
						self:redisplay()
						--[[
						local st = self.display:GetUserData("eoiST")
						if st and st.frame:IsVisible() then
							st:OuroLoot_Refresh()
						end
						]]
					end
				end
			end
		end
		self.dprint('loot',"<<_do_loot out")
		return i
	end

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

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

		if event == "CHAT_MSG_LOOT" then
			local msg = ...
			--ChatFrame2:AddMessage("original string:  >"..(msg:gsub("\124","\124\124")).."<")
			local person, itemstring, remainder = msg:match(self.loot_pattern)
			self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder)
			if not person then return end    -- "So-and-So selected Greed", etc, not actual looting
			local count = remainder and remainder:match(".*(x%d+)$")

			-- Name might be colorized, remove the highlighting
			local p = person:match("|c%x%x%x%x%x%x%x%x(%S+)")
			person = p or person
			person = (person == UNIT_YOU) and my_name or person

			local id = tonumber((select(2, strsplit(":", itemstring))))

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

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

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


------ Slash command handler
-- Thought about breaking this up into a table-driven dispatcher.  But
-- that would result in a pile of teensy functions, most of which would
-- never be called.  Too much overhead.  (2.0:  Most of these removed now
-- that GUI is in place.)
function addon:OnSlash (txt) --, editbox)
	txt = strtrim(txt:lower())
	local cmd, arg = ""
	do
		local s,e = txt:find("^%a+")
		if s then
			cmd = txt:sub(s,e)
			s = txt:find("%S", e+2)
			if s then arg = txt:sub(s,-1) end
		end
	end

	if cmd == "" then
		if InCombatLockdown() then
			return self:Print("Can't display window in combat.")
		else
			return self:BuildMainDisplay()
		end

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

	elseif cmd == "on" then                             self:Activate(arg)
	elseif cmd == "off" then                            self:Deactivate()
	elseif cmd == "broadcast" or cmd == "bcast" then    self:Activate(nil,true)

	elseif cmd == "fake" then  -- maybe comment this out for real users
		self:_mark_boss_kill (self._addLootEntry{
			kind='boss',reason='kill',bosskill="Baron Steamroller",instance=instance_tag(),duration=0
		})
		self:CHAT_MSG_LOOT ('manual', my_name, 54797)
		if self.display then
			self:redisplay()
		end
		self:Print "Baron Steamroller has been slain.  Congratulations on your rug."

	elseif cmd == "debug" then
		if arg then
			self.debug[arg] = not self.debug[arg]
			_G.print(arg,self.debug[arg])
			if self.debug[arg] then self.DEBUG_PRINT = true end
		else
			self.DEBUG_PRINT = not self.DEBUG_PRINT
		end

	elseif cmd == "save" and arg and arg:len() > 0 then
		self:save_saveas(arg)
	elseif cmd == "list" then
		self:save_list()
	elseif cmd == "restore" and arg and arg:len() > 0 then
		self:save_restore(tonumber(arg))
	elseif cmd == "delete" and arg and arg:len() > 0 then
		self:save_delete(tonumber(arg))

	elseif cmd == "help" then
		self:BuildMainDisplay('help')
	elseif cmd == "toggle" then
		if self.display then
			self.display:Hide()
		else
			return self:BuildMainDisplay()
		end

	else
		if self:OpenMainDisplayToTab(cmd) then
			return
		end
		self:Print("Unknown command '%s'. %s to see the help window.",
			cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON))
	end
end

function addon:SetThreshold (arg, quiet_p)
	local q = tonumber(arg)
	if q then
		q = math.floor(q+0.001)
		if q<0 or q>6 then
			return self:Print("Threshold must be 0-6.")
		end
	else
		q = qualnames[arg]
		if not q then
			return self:Print("Unrecognized item quality argument.")
		end
	end
	self.threshold = q
	if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end
end


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

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

function addon:Clear(verbose_p)
	local repopup, st
	if self.display then
		-- in the new version, this is likely to always be the case
		repopup = true
		st = self.display:GetUserData("eoiST")
		if not st then
			self.dprint('flow', "Clear: display visible but eoiST not set??")
		end
		self.display:Hide()
	end
	g_restore_p = nil
	OuroLootSV = nil
	self:_reset_timestamps()
	g_saved_tmp = g_loot.saved
	if verbose_p then
		if (g_saved_tmp and #g_saved_tmp>0) then
			self:Print("Current loot data cleared, %d saved sets remaining.", #g_saved_tmp)
		else
			self:Print("Current loot data cleared.")
		end
	end
	_init(self,st)
	if repopup then
		addon:BuildMainDisplay()
	end
end


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

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

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

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

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

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

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

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

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

	if g_restore_p then
		self:zero_printed_fenceposts()                  -- g_loot.printed.* = previous/safe values
	else
		self:zero_printed_fenceposts(0)                 -- g_loot.printed.* = 0
	end
	if possible_st then
		possible_st:SetData(g_loot)
	end

	self.status_text = ("v2r%d communicating as ident %s"):format(self.revision,self.ident)
	self:RegisterComm(self.ident)
	self:RegisterComm(self.identTg, "OnCommReceivedNocache")

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

-- Tie-ins with Deadly Boss Mods
do
	local candidates, location
	local function fixup_durations (cache)
		if candidates == nil then return end  -- this is called for *all* cache expirations, including non-boss
		local boss, bossi
		boss = candidates[1]
		if #candidates == 1 then
			-- (1) or (2)
			boss.duration = boss.duration or 0
			addon.dprint('loot', "only one 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 candidate", i, "duration", c.duration)
					break
				end
			end
		end
		bossi = addon._addLootEntry(boss)
		addon.dprint('loot', "added entry", bossi)
		if boss.reason == 'kill' then
			addon:_mark_boss_kill (bossi)
			if opts.chatty_on_kill then
				addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
			end
		end
		candidates = nil
	end
	addon.recent_boss = create_new_cache ('boss', 10, fixup_durations)

	-- Similar to _do_loot, but duration+ parms only present when locally generated.
	local function _do_boss (self, reason, bossname, intag, duration, raiders)
		self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag,
		            "D:", duration, "RL:", (raiders and #raiders or 'nil'))
		if self.rebroadcast and duration then
			self:broadcast('boss', reason, bossname, intag)
		end
		-- This is only a loop to make jumping out of it easy, and still do cleanup below.
		while self.enabled do
			if reason == 'wipe' and opts.no_tracking_wipes then break end
			bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname
			local not_from_local = duration == nil
			local signature = bossname .. reason
			if not_from_local and self.recent_boss:test(signature) then
				self.dprint('cache', "boss <",signature,"> already in cache, skipping")
			else
				self.recent_boss:add(signature)
				-- Possible scenarios:  (1) we don't see a boss event at all (e.g., we're
				-- outside the instance) and so this only happens once as a non-local event,
				-- (2) we see a local event first and all non-local events are filtered
				-- by the cache, (3) we happen to get some non-local events before doing
				-- our local event (not because of network weirdness but because our local
				-- DBM might not trigger for a while).
				local c = {
					kind		= 'boss',
					bosskill	= bossname,      -- minor misnomer, might not actually be a kill
					reason		= reason,
					instance	= intag,
					duration	= duration,      -- these two deliberately may be nil
					raiderlist	= raiders and table.concat(raiders, ", ")
				}
				candidates = candidates or {}
				tinsert(candidates,c)
				break
			end
		end
		self.dprint('loot',"<<_do_boss out")
	end
	-- No wrapping layer for now
	addon.on_boss_broadcast = _do_boss

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

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

	function addon:register_boss_mod (name, registration_func, deregistration_func)
		assert(type(name)=='string')
		assert(type(registration_func)=='function')
		if deregistration_func ~= nil then assert(type(deregistration_func)=='function') end
		self.bossmods[#self.bossmods+1] = {
			n = name,
			r = registration_func,
			d = deregistration_func,
		}
		self.bossmods[name] = self.bossmods[#self.bossmods]
		assert(self.bossmods[name].n == self.bossmods[#self.bossmods].n)
	end

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

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

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

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

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

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

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

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

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

		if not done_todays_date then do_todays_date() end

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


------ Saved texts
function addon:check_saved_table(silent_p)
	local s = g_loot.saved
	if s and (#s > 0) then return s end
	g_loot.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)
	g_loot.saved = g_loot.saved or {}
	local n = #(g_loot.saved) + 1
	local save = {
		name = name,
		date = makedate(),
		count = #g_loot,
		forum = g_loot.forum,
		attend = g_loot.attend,
	}
	self:Print("Saving current loot texts to #%d '%s'", n, name)
	g_loot.saved[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.
	g_loot.forum = save.forum
	g_loot.attend = save.attend
end

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


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

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

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

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

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

		return t
	end

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

	function addon:_addHistoryEntry (lootindex)
		local e = g_loot[lootindex]
		if e.kind ~= 'loot' then return end

		local i,h = self:get_loot_history(e.person)
		local n = {
			id = e.id,
			when = self:format_timestamp (g_today, e),
			count = e.count,
		}
		tinsert (h, 1, n)
	end

	function addon:rewrite_history (realmname)
		local r = assert(realmname)
		self.history_all[r] = self:_prep_new_history_category (nil, r)
		self.history = self.history_all[r]

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

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

	function addon:preen_history (realmname)
		local r = assert(realmname)
		for i,h in ipairs(self.history) do
			tsort (h, comp)
			while #h > 1 do
				tremove (h)
			end
		end
	end
end


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

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

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

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

	OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
		addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", instancetag)
		if not addon.enabled then return end
		adduser (sender, nil, true)
		addon:on_boss_broadcast (reason, bossname, instancetag)
	end

	OCR_funcs.bcast_req = function (sender)
		if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast))
		then
			addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in. Or do nothing for now to see if other requests arrive.",
				sender,
				addon.format_hypertext('bcaston',"the red pill",'|cffff4040'),
				addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd'))
		end
		self.popped = true
	end

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

	-- Incoming message dispatcher
	local function dotdotdot (sender, tag, ...)
		local f = OCR_funcs[tag]
		addon.dprint('comm', ":... processing",tag,"from",sender)
		if f then return f(sender,tag,...) end
		addon.dprint('comm', "unknown comm message",tag",from", sender)
	end
	-- Recent message cache
	addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)

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

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

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

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

-- vim:noet