changeset 56:fcc0d0ff5832

- instance_tag() also returns max instance size as a plain number, adjust call sites - clean up .raiders table, add some new fields, use copies of this instead of a single string - make sure datarev field is properly updated when it's already present - avoid multiple GetRaidRosterInfo loops scattered throughout the addon, instead just traverse the .raiders list, regularly updated - make the 'loot' and 'boss' broadcasts versioned. Handle receiving older. - new format for plaintext attendance output
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Fri, 13 Apr 2012 04:28:46 +0000
parents ac57a4342812
children 81d5449621f8
files LibFarmbuyer.lua bossmods.lua core.lua gui.lua text_tabs.lua verbage.lua
diffstat 6 files changed, 205 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- a/LibFarmbuyer.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/LibFarmbuyer.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -54,7 +54,7 @@
   Ditto for table recycling.
 ]]
 
-local MAJOR, MINOR = "LibFarmbuyer", 15
+local MAJOR, MINOR = "LibFarmbuyer", 16
 assert(LibStub,MAJOR.." requires LibStub")
 local lib = LibStub:NewLibrary(MAJOR, MINOR)
 if not lib then return end
@@ -102,7 +102,7 @@
 --[[
 	safeprint
 ]]
-local tconcat = table.concat
+local tconcat, tostring, tonumber = table.concat, tostring, tonumber
 local function undocontrol(c)
 	return ("\\%.3d"):format(c:byte())
 end
@@ -139,7 +139,7 @@
 		t[i] = tostring(t[i]):gsub('\124','\124\124')
 		                     :gsub('(%c)', undocontrol)
 	end
-	local msg = tconcat(t,' ', i, tonumber(t.n) or #t)
+	local msg = tconcat(t,' ', 1, tonumber(t.n) or #t)
 	if type(f) == 'function' then
 		return msg,f(msg)
 	else
--- a/bossmods.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/bossmods.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -35,7 +35,7 @@
 local tonumber = tonumber
 
 -- WoW
-local GetRaidRosterInfo = GetRaidRosterInfo
+--local GetRaidRosterInfo = GetRaidRosterInfo
 
 -- OL
 local addon_do_boss 
@@ -44,7 +44,7 @@
 ------ Deadly Boss Mods
 do
 	local DBM
-	local location
+	local location, maxsize
 	local real_loadmod
 	-- When zoning into a raid instance not seen this session, make sure
 	-- we don't report a previous raid instance as current location.  DBM
@@ -66,7 +66,12 @@
 			name = "Unknown Boss"
 		end
 
-		local it = location or self.instance_tag()
+		local it
+		if location then
+			it = location
+		else
+			it, maxsize = self.instance_tag()
+		end
 		self.latest_instance = it
 		location = nil
 
@@ -75,18 +80,7 @@
 			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
-			local name = GetRaidRosterInfo(i)
-			if name then
-				tinsert (raiders, name)
-			end
-		end
-		table.sort(raiders)
-
-		return addon_do_boss (self, reason, name, it, duration, raiders)
+		return addon_do_boss (self, reason, name, it, maxsize, duration)
 	end
 
 	local function callback(...) DBMBossCallback(addon,...) end
@@ -102,7 +96,9 @@
 			addon_do_boss = OL_boss_worker
 			local r = DBM:RegisterCallback("kill", callback)
 					  DBM:RegisterCallback("wipe", callback)
-					  DBM:RegisterCallback("pull", function() location = self.instance_tag() end)
+					  DBM:RegisterCallback("pull", function()
+						  location, maxsize = self.instance_tag()
+					  end)
 			real_loadmod = DBM.LoadMod
 			DBM.LoadMod = resetting_loadmod
 			return r > 0
--- a/core.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/core.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -6,6 +6,18 @@
 - 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:
+-    class		capitalized English codename ("WARRIOR", "DEATHKNIGHT", etc)
+-    subgroup	1-8
+-    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
+-    online		1 = online, 2 = offline, 3 = no longer in raid
+	[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)
 
 Common g_loot entry indices:
 - kind			time/boss/loot
@@ -19,26 +31,29 @@
 
 Boss specific g_loot indices:
 - bossname		name of boss/encounter;
-- 				may be changed if "snarky boss names" option is enabled
+ 				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
-- duration		in seconds; may be missing
-- raiderlist	"Able, Baker, Charlie"; may be missing
+- maxsize		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
 
 Loot specific g_loot indices:
 - person		recipient
 - person_class	class of recipient if available; may be missing;
-- 				will be classID-style (e.g., DEATHKNIGHT)
+ 				will be classID-style (e.g., DEATHKNIGHT)
 - itemname		not including square brackets
 - id			itemID as number
 - itemlink		full clickable link
 - itexture		icon path (e.g., Interface\Icons\INV_Misc_Rune_01)
 - quality		ITEM_QUALITY_* number
 - disposition	offspec/gvault/shard; missing otherwise; can be set from
--				the extratext field
+				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"
+				extratext; triggers only for a stack of items, not "the boss
+				dropped double axes today"
 - is_heroic		true if item is heroic; missing otherwise
 - cache_miss	if GetItemInfo failed; SHOULD be missing (changes other fields)
 - bcast_from	if rebroadcast from another player; missing otherwise
@@ -88,7 +103,7 @@
 
 ------ Constants
 local option_defaults = {
-	['datarev'] = 15,    -- cheating, this isn't actually an option
+	['datarev'] = 16,    -- cheating, this isn't actually an option
 	['popup_on_join'] = true,
 	['register_slashloot'] = true,
 	['scroll_to_bottom'] = true,
@@ -138,7 +153,7 @@
 -- Play cute games with namespaces here just to save typing.  WTB Lua 5.2 PST.
 do local _G = _G setfenv (1, addon)
 
-	commrev			= 15  -- number
+	commrev			= '16'
 	revision		= _G.GetAddOnMetadata(nametag,"Version") or "?"  -- "x.yy.z", etc
 	ident			= "OuroLoot2"
 	identTg			= "OuroLoot2Tg"
@@ -233,10 +248,10 @@
 local g_boss_signpost	= nil
 local opts				= nil
 
-local pairs, ipairs, tinsert, tremove, tonumber, wipe =
-	pairs, ipairs, table.insert, table.remove, tonumber, table.wipe
+local pairs, ipairs, tinsert, tremove, tostring, tonumber, wipe =
+	pairs, ipairs, table.insert, table.remove, tostring, tonumber, table.wipe
 local pprint, tabledump = addon.pprint, flib.tabledump
-local GetNumRaidMembers = GetNumRaidMembers
+local CopyTable, GetNumRaidMembers = CopyTable, GetNumRaidMembers
 -- En masse forward decls of symbols defined inside local blocks
 local _register_bossmod, makedate, create_new_cache, _init, _log
 
@@ -323,29 +338,30 @@
 	end
 end
 
--- Returns an instance name or abbreviation
+-- Returns an instance name or abbreviation, followed by the raid size
 local function instance_tag()
 	local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo()
-	local t
+	local t, r
 	name = addon.instance_abbrev[name] or name
-	if typeof == "none" then return name end
+	if typeof == "none" then return name, MAX_RAID_MEMBERS end
 	-- diffstr is "5 Player", "10 Player (Heroic)", etc.  ugh.
 	if (GetLFGMode()) and (GetLFGModeType() == 'raid') then
-		t = 'LFR'
+		t,r = 'LFR', 25
 	elseif diffcode == 1 then
-		t = ((GetNumRaidMembers()>0) and "10" or "5")
+		t,r = (GetNumRaidMembers()>0) and "10",10 or "5",5
 	elseif diffcode == 2 then
-		t = ((GetNumRaidMembers()>0) and "25" or "5h")
+		t,r = (GetNumRaidMembers()>0) and "25",25 or "5h",5
 	elseif diffcode == 3 then
-		t = "10h"
+		t,r = "10h", 10
 	elseif diffcode == 4 then
-		t = "25h"
+		t,r = "25h", 25
 	end
 	-- dynamic difficulties always return normal "codes"
 	if isdynamic and perbossheroic == 1 then
 		t = t .. "h"
 	end
-	return name .. "(" .. t .. ")"
+	pprint("instance_tag final", t, r)
+	return name .. "(" .. t .. ")", r
 end
 addon.instance_tag = instance_tag   -- grumble
 addon.latest_instance = nil         -- spelling reminder, assigned elsewhere
@@ -448,6 +464,7 @@
 			opts[opt] = default
 		end
 	end
+	opts.datarev = option_defaults.datarev
 
 	-- transition&remove old options
 	opts['forum_use_itemid'] = nil
@@ -510,18 +527,33 @@
 	self.history_all.HISTFORMAT = nil   -- don't keep this in live data
 	--OuroLootSV_hist = nil
 
-	-- Handle changes to the stored data format, in stages from oldest to
-	-- newest.  This won't look coherent until multiple stages are happening.
-	if stored_datarev == nil then
-		self:Print("Transitioning saved data format to 15...")
-		for i,e in ipairs(OuroLootSV) do
-			if e.bosskill then
-				e.bossname, e.bosskill = e.bosskill, nil
+	-- Handle changes to the stored data format in stages from oldest to newest.
+	if OuroLootSV then
+		local dirty = false
+		if stored_datarev == nil then
+			self:Print("Transitioning saved data format to 15..."); dirty = true
+			for i,e in ipairs(OuroLootSV) do
+				if e.bosskill then
+					e.bossname, e.bosskill = e.bosskill, nil
+				end
 			end
+			stored_datarev = 15
 		end
-		stored_datarev = 15
+		if stored_datarev == 15 then
+			self:Print("Transitioning saved data format to 16..."); dirty = true
+			for i,e in ipairs(OuroLootSV) do
+				if e.kind == 'boss' then
+					e.maxsize, e.raiderlist, e.raidersnap = 0, nil, {}
+				end
+			end
+			OuroLootSV.raiders = OuroLootSV.raiders or {}
+			for name,r in pairs(OuroLootSV.raiders) do
+				r.subgroup = 0
+			end
+			stored_datarev = 16
+		end
+		if dirty then self:Print("Saved data has been massaged into shape.") end
 	end
-	--if stored_datarev == 15 then.... 
 
 	_init(self)
 	self.dprint('flow', "version strings:", revision_large, self.status_text)
@@ -681,9 +713,9 @@
 
 do
 	local IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex,
-				UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo =
+				UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo =
 	      IsInInstance, UnitName, UnitIsConnected, UnitClass, UnitRace, UnitSex,
-		  		UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo
+		  		UnitLevel, UnitInRaid, UnitIsVisible, GetGuildInfo, GetRaidRosterInfo
 	local time, difftime = time, difftime
 	local R_ACTIVE, R_OFFLINE, R_LEFT = 1, 2, 3
 
@@ -727,15 +759,21 @@
 					g_loot.raiders[name] = { needinfo=true }
 				end
 				local r = g_loot.raiders[name]
+				-- We grab a bunch of return values here, but only pay attention to
+				-- them under specific circumstances.
+				local grri_name, connected, subgroup, level, class, _
+				grri_name, _, subgroup, level, _, class, connected = GetRaidRosterInfo(i)
+				assert(name==grri_name, "UnitName =/= grri_name of same raidindex")
+				r.subgroup = subgroup
 				if r.needinfo and UnitIsVisible(unit) then
 					r.needinfo = nil
-					r.class    = select(2,UnitClass(unit))
+					r.class    = class    --select(2,UnitClass(unit))
 					r.race     = select(2,UnitRace(unit))
 					r.sex      = UnitSex(unit)
-					r.level    = UnitLevel(unit)
+					r.level    = level    --UnitLevel(unit)
 					r.guild    = GetGuildInfo(unit)
 				end
-				local connected = UnitIsConnected(unit)
+				--local connected = UnitIsConnected(unit)
 				if connected and r.online ~= R_ACTIVE then
 					r.join = r.join or now
 					r.online = R_ACTIVE
@@ -870,7 +908,7 @@
 		-- This is only a 'while' to make jumping out of it easy and still do cleanup below.
 		while local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) do
 			if (self.rebroadcast and (not from)) and not local_override then
-				self:broadcast('loot', recipient, itemid, count)
+				self:vbroadcast('loot', recipient, itemid, count)
 			end
 			if (not self.enabled) and (not local_override) then break end
 			local signature = recipient .. iname .. (count or "")
@@ -1211,32 +1249,6 @@
 	addon.sender_list.namesI = byindex
 end
 
--- Message sending.
--- See OCR_funcs.tag at the end of this file for incoming message treatment.
-do
-	local function assemble(...)
-		local msg = ...
-		for i = 2, select('#',...) do
-			msg = msg .. '\a' .. (select(i,...) or "")
-		end
-		return msg
-	end
-
-	-- broadcast('tag', <stuff>)
-	function addon:broadcast(...)
-		local msg = assemble(...)
-		self.dprint('comm', "<broadcast>:", msg)
-		-- the "GUILD" here is just so that we can also pick up on it
-		self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID")
-	end
-	-- whispercast(<to>, 'tag', <stuff>)
-	function addon:whispercast(to,...)
-		local msg = assemble(...)
-		self.dprint('comm', "<whispercast>@", to, ":", msg)
-		self:SendCommMessage(self.identTg, msg, "WHISPER", to)
-	end
-end
-
 function addon:DoPing()
 	self:Print("Give me a ping, Vasili. One ping only, please.")
 	self.sender_list.active = {}
@@ -1361,7 +1373,7 @@
 		possible_st:SetData(g_loot)
 	end
 
-	self.status_text = ("%s communicating as ident %s commrev %d"):format(self.revision,self.ident,self.commrev)
+	self.status_text = ("%s communicating as ident %s commrev %s"):format(self.revision,self.ident,self.commrev)
 	self:RegisterComm(self.ident)
 	self:RegisterComm(self.identTg, "OnCommReceivedNocache")
 
@@ -1418,11 +1430,11 @@
 	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'))
+	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:broadcast('boss', reason, bossname, intag)
+			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
@@ -1447,8 +1459,9 @@
 					bossname	= bossname,
 					reason		= reason,
 					instance	= intag,
-					duration	= duration,      -- these two deliberately may be nil
-					raiderlist	= raiders and table.concat(raiders, ", ")
+					duration	= duration,      -- deliberately may be nil
+					raidersnap	= CopyTable(g_loot.raiders),
+					maxsize		= maxsize,
 				}
 				tinsert(candidates,c)
 			end
@@ -1456,7 +1469,7 @@
 		end
 		self.dprint('loot',"<<_do_boss out")
 	end
-	-- No wrapping layer for now
+	-- 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)
@@ -2028,6 +2041,45 @@
 
 ------ Player communication
 do
+	local select, tconcat, strsplit = select, table.concat, strsplit
+	--[[ old way:  repeated string concatenations, BAD
+		 new way:  new table on every call, BAD
+	local msg = ...
+	for i = 2, select('#',...) do
+		msg = msg .. '\a' .. (select(i,...) or "")
+	end
+	return msg
+	]]
+	local function assemble(t,...)
+		if select('#',...) > 0 then
+			local msg = {t,...}
+			-- tconcat requires strings, but T is known to be one already
+			for i = 2, #msg 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)
+		-- 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
+
 	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
@@ -2059,12 +2111,21 @@
 		adduser (sender, nil, true)
 		addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext)
 	end
+	OCR_funcs['16loot'] = OCR_funcs.loot
 
 	OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
-		addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", 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)
+		addon:on_boss_broadcast (reason, bossname, instancetag, --[[maxsize=]]0)
+	end
+	OCR_funcs['16boss'] = function (sender, _, reason, bossname, instancetag, maxsize)
+		addon.dprint('comm', "DOTboss16, 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.bcast_req = function (sender)
--- a/gui.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/gui.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -47,7 +47,8 @@
 local window_title		= "Ouro Loot"
 local dirty_tabs		= nil
 
-local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber
+local pairs, ipairs, tinsert, tremove, tostring, tonumber =
+	pairs, ipairs, table.insert, table.remove, tostring, tonumber
 
 local pprint, tabledump = addon.pprint, flib.tabledump
 local GetItemInfo, ITEM_QUALITY_COLORS = GetItemInfo, ITEM_QUALITY_COLORS
@@ -164,9 +165,11 @@
 			_tabtexts[tabtitle] = nil
 			tremove (_taborder, loaded_at)
 			next_insertion_position = loaded_at
+			local saved_next_insertion_position = loaded_at
 			local loaded, whynot = LoadAddOn(addon_index)
 			if loaded then
-				addon:Print(tabtitle, "loaded.")
+				addon:Print("%s loaded.  %d |4tab:tabs; added.", tabtitle,
+					next_insertion_position - saved_next_insertion_position)
 			else
 				what.disabled = true
 				_tabtexts[tabtitle] = what -- restore this for mouseovers
@@ -541,7 +544,7 @@
 	["Rebroadcast this loot entry"] = function(rowi)
 		local e = g_loot[rowi]
 		-- This only works because GetItemInfo accepts multiple argument formats
-		addon:broadcast('loot', e.person, e.itemlink, e.count, e.cols[3].value)
+		addon:vbroadcast('loot', e.person, e.itemlink, e.count, e.cols[3].value)
 		addon:Print("Rebroadcast entry", rowi, e.itemlink)
 	end,
 
@@ -553,10 +556,10 @@
 		repeat
 			local e = g_loot[rowi]
 			if e.kind == 'boss' then
-				addon:broadcast('boss', e.reason, e.bossname, e.instance)
+				addon:vbroadcast('boss', e.reason, e.bossname, e.instance)
 			elseif e.kind == 'loot' then
 				-- This only works because GetItemInfo accepts multiple argument formats
-				addon:broadcast('loot', e.person, e.itemlink, e.count, e.cols[3].value)
+				addon:vbroadcast('loot', e.person, e.itemlink, e.count, e.cols[3].value)
 			end
 			addon:Print("Rebroadcast entry", rowi, e.itemlink or e.bossname or UNKNOWN)
 			rowi = rowi + 1
@@ -2162,7 +2165,7 @@
 		data.name = text
 		local getinstance = StaticPopup_Show("OUROL_EOI_INSERT","instance")
 		getinstance.data = data
-		getinstance.editBox:SetText(addon.instance_tag())
+		getinstance.editBox:SetText((addon.instance_tag()))
 		-- This suppresses auto-hide (which would case the getinstance dialog
 		-- to go away), but only when mouse clicking.  OnEnter is on its own.
 		return true
--- a/text_tabs.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/text_tabs.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -137,12 +137,50 @@
 addon:register_text_generator ("forum", [[Forum Markup]], [[BBcode ready for Ouroboros forums]], forum, forum_specials)
 
 
+local ingroups, outgroups = {}, {}
+local function do_attendance (raidertable, max_group_number)
+	local tins, wipe, tsort, tconcat =
+		table.insert, table.wipe, table.sort, table.concat
+
+	-- Assumption:  everybody is packed into the first N groups.
+	if raidertable then for name,info in pairs(raidertable) do
+		if info.online ~= 3 then   -- 3 == left the raid
+			if info.subgroup <= max_group_number then
+				tins (ingroups, name)
+			else
+				tins (outgroups, name)
+			end
+		end
+	end end
+	if #ingroups > 0 then
+		tsort(ingroups)
+	else
+		ingroups[1] = [[Nobody recorded as inside the instance.]]
+	end
+	if #outgroups > 0 then
+		tsort(outgroups)
+	else
+		outgroups[1] = [[Nobody recorded as outside the instance.]]
+	end
+	local i,o = tconcat(ingroups,", "), tconcat(outgroups,", ")
+	wipe(ingroups)
+	wipe(outgroups)
+	return i,o
+end
+
+local saved_g_loot_pointer
 local function att (_, loot, last_printed, _, cache)
+	saved_g_loot_pointer = loot
 	for i = last_printed+1, #loot do
 		local e = loot[i]
 
 		if e.kind == 'boss' and e.reason == 'kill' then
-			cache[#cache+1] = ("\n%s -- %s\n%s"):format(e.instance, e.bossname, e.raiderlist or '<none recorded>')
+			-- This could, concievably, be different on a per-boss basis
+			-- (e.g., "we're dropping to 10-man for the PvP boss")
+			local i,o = do_attendance (e.raidersnap, e.maxsize / MEMBERS_PER_RAID_GROUP)
+
+			cache[#cache+1] = ("\n%s -- %s\n[+] %s\n[-] %s\n"):format(e.instance,
+				e.bossname, i, o)
 
 		elseif e.kind == 'time' then
 			cache[#cache+1] = e.startday.text
@@ -157,13 +195,11 @@
 		[[Take attendance now (will continue to take attendance on each boss kill).]])
 	w:SetFullWidth(true)
 	w:SetCallback("OnClick", function(_w)
-		local raiders = {}
-		for i = 1, GetNumRaidMembers() do
-			table.insert(raiders, (GetRaidRosterInfo(i)))
-		end
-		table.sort(raiders)
+		local instance, maxsize = addon.instance_tag()
+		local i,o = do_attendance (saved_g_loot_pointer.raiders, maxsize / MEMBERS_PER_RAID_GROUP)
 		local h, m = GetGameTime()
-		local additional = ("Attendance at %.2d:%.2d:\n%s"):format(h,m,table.concat(raiders, ", "))
+
+		local additional = ("Attendance for %s at %.2d:%.2d:\n[+] %s\n[-] %s"):format(instance, h, m, i, o)
 		editbox:SetText(editbox:GetText() .. '\n' .. additional)
 	end)
 	container:AddChild(w)
--- a/verbage.lua	Sat Apr 07 05:40:20 2012 +0000
+++ b/verbage.lua	Fri Apr 13 04:28:46 2012 +0000
@@ -329,7 +329,10 @@
 
 T.texts_other = [[
 So far the only other generated text is the <Attendance> tab, an alphabetized list
-on a per-boss basis.
+on a per-boss basis.  Players who are probably inside the raid are grouped
+under [:PLUS:], and the remaining players are under [-].  "Probably inside" is an
+assumption that groups 1 and 2 are raiding in a 10-player instance, groups 1-5
+are raiding in a 25-player instance, and so forth.
 
 Other addons can register their own text tabs and corresponding generation
 functions.  If you want to be able to feed text into an offline program (for
@@ -523,7 +526,7 @@
 	wrapped = wrapped:gsub ("([^\n])\n([^\n])", "%1 %2")
 	wrapped = wrapped:gsub ("|r\n\n", "|r\n")
 	wrapped = wrapped:gsub ("Ouroboros", "|cffa335ee<Ouroboros>|r")
-	wrapped = wrapped:gsub ("%*%(", "<") :gsub("%)%*", ">")
+	wrapped = wrapped:gsub ("%*%(", "<") :gsub("%)%*", ">") :gsub(":PLUS:", "+")
 	addon.helptext[funkykey] = wrapped
 end
 end -- do scope