diff core.lua @ 1:822b6ca3ef89

Import of 2.15, moving to wowace svn.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 16 Apr 2011 06:03:29 +0000
parents
children fe437e761ef8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,1328 @@
+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
+------ Locals
+------ 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,
+	['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.
+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(.*)%.$"
+
+	dbm_registered	= nil
+	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")
+
+
+------ Locals
+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 _registerDBM -- break out into separate file
+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 = GetRealmName()
+	self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
+	self.history = self.history_all[r]
+
+	_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
+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
+		--OuroLootSV = g_loot
+		--for i,e in ipairs(OuroLootSV) do
+		for i,e in ipairs(g_loot) do
+			e.cols = nil
+		end
+		OuroLootSV = g_loot
+	end
+	self.history.kind = nil
+	self.history.st = nil
+	self.history.byname = nil
+	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"
+			_registerDBM(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 = GetItemInfo
+
+	-- 'from' and onwards only present if this is triggered by a broadcast
+	function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
+		local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(itemid)
+		if not iname then return end   -- sigh
+		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"
+		_registerDBM(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
+
+	local GetRaidRosterInfo = GetRaidRosterInfo
+	function addon:DBMBossCallback (reason, mod, ...)
+		if (not self.rebroadcast) and (not self.enabled) then return end
+
+		local name
+		if mod.combatInfo and mod.combatInfo.name then
+			name = mod.combatInfo.name
+		elseif mod.id then
+			name = mod.id
+		else
+			name = "Unknown Boss"
+		end
+
+		local it = location or instance_tag()
+		location = nil
+
+		local duration = 0
+		if mod.combatInfo and mod.combatInfo.pull then
+			duration = math.floor (GetTime() - mod.combatInfo.pull)
+		end
+
+		-- attendance:  maybe put people in groups 6,7,8 into a "backup/standby"
+		-- list?  probably too specific to guild practices.
+		local raiders = {}
+		for i = 1, GetNumRaidMembers() do
+			tinsert(raiders, (GetRaidRosterInfo(i)))
+		end
+		table.sort(raiders)
+
+		return _do_boss (self, reason, name, it, duration, raiders)
+	end
+
+	local callback = function(...) addon:DBMBossCallback(...) end
+	function _registerDBM(self)
+		if DBM then
+			if not self.dbm_registered then
+				local rev = tonumber(DBM.Revision) or 0
+				if rev < 1503 then
+					self.status_text = "|cffff1010Deadly Boss Mods must be version 1.26 or newer to work with Ouro Loot.|r"
+					return
+				end
+				local r = DBM:RegisterCallback("kill", callback)
+						  DBM:RegisterCallback("wipe", callback)
+						  DBM:RegisterCallback("pull", function() location = instance_tag() end)
+				self.dbm_registered = r > 0
+			end
+		else
+			self.status_text = "|cffff1010Ouro Loot cannot find Deadly Boss Mods, loot will not be grouped by boss.|r"
+		end
+	end
+end  -- DBM tie-ins
+
+-- 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
+	-- Builds the map of names to array indices.
+	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
+
+	-- Maps a name to an array index, creating new tables if needed.  Returns
+	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 self.history[i]
+	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.cols = setmetatable({
+			{ value = realmname },
+		}, self.time_column1_used_mt)
+		]]
+
+		if not t.byname then
+			self:_build_history_names (t)
+		end
+
+		return t
+	end
+
+	function addon:_addHistoryEntry (lootindex)
+		local e = g_loot[lootindex]
+		local h = self:get_loot_history(e.person)
+		local n = {
+			id = e.id,
+			when = self:format_timestamp (g_today, e),
+			count = e.count,
+		}
+		h[#h+1] = n
+	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