view mleqdkp.lua @ 47:1070a14cfee4

Updated lib-st widget from WRDW. Properly notice new generating tabs. Updated help text.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 28 Jan 2012 18:24:22 +0000
parents 40ff9c63badc
children
line wrap: on
line source
-- This file is one gigantic exercise in abusing the garbage collector via
-- string manipulation.  A real XML-handling library would be cleaner, alas,
-- we can't load third-party .so/.dll inside the WoW client.  Life is hard.

local addon = select(2,...)
local pairs, ipairs, tinsert, tremove, tconcat = pairs, ipairs, table.insert, table.remove, table.concat
local tostring, tonumber = tostring, tonumber

local banner_formatted = "Formatted version (scroll down for unformatted):"
local banner_unformatted = "Unformatted version:"
local banner_sep = "==========================="

-- We keep some local state bundled up rather than trying to pass it around
-- as paramters (which would have entailed creating a ton of closures).
local state = {}
local tag_lookup_handlers = {}
local do_tag_lookup_handler


--[[
This is based on CT_RaidTracker 1.7.32, reconstructing the output from
code inspection.  No official format documents are available on the web
without downloading and installing the EQDKP webserver package.  Bah.

Case of tag names shouldn't matter, but is preserved here from CT_RaidTracker
code for comparison of generated output.

There is some waste adding newlines between elements here only to strip them
out later, but it's worth the extra cycles for debugging and verification.

$TIMESTAMP,$ENDTIME MM/DD/YY HH:mm:ss except we don't have seconds available
$REALM              GetRealmName()
$ZONE               raw name, not snarky
$RAIDNOTE           arbitrary text for the raid event
$PHAT_LEWTS         all accumulated loot entries
]]
local XML = ([====[
<RaidInfo>
<Version>1.4</Version>  {In live output, this is missing due to scoping bug in ct_raidtracker.lua:3471}
<key>$TIMESTAMP</key>
<realm>$REALM</realm>
<start>$TIMESTAMP</start>   {Same as the key, apparently?}
<end>$ENDTIME</end>    {Set by the "end the raid" command in CTRT, here it is just the final entry time}
<zone>$ZONE</zone>   {may be optional.  first one on the list in case of multiple zones?}
{<difficulty>$DIFFICULTY</difficulty>   {this scales badly in places like ICC.  may be optional?}}

<PlayerInfos>
  $PLAYER_INFOS
</PlayerInfos>

<BossKills>
  $BOSS_KILLS
</BossKills>

<Wipes>
  $WIPES
</Wipes>
{<NextBoss>Baron Steamroller</NextBoss>  {only one "next boss" for the whole
                          raid event? presumably where to pick up next time?}}

<note><![CDATA[$RAIDNOTE - Zone: $ZONE]]></note>

<Join>
  $JOINS
</Join>
<Leave>
  $LEAVES
</Leave>

<Loot>
  $PHAT_LEWTS
</Loot>
</RaidInfo>]====]):gsub('%b{}', "")

--[[
See the loot markup below.
]]
local boss_kills_xml = ([====[
  <key$N>
    <name>$BOSS_NAME</name>
    <time>$BOSS_TIME</time>
    <attendees></attendees>   {this is actually empty in the working example...}
    <difficulty>$DIFFICULTY</difficulty>
  </key$N>
]====]):gsub('%b{}', "")

local function boss_kills_tag_lookup (tag)
	if tag == 'N' then
		return tostring(state.key)
	elseif tag == 'BOSS_NAME' then
		return state.entry.bosskill
	elseif tag == 'BOSS_TIME' then
		return do_tag_lookup_handler (state.index, state.entry, 'TIME')
	else
		return do_tag_lookup_handler (state.index, state.entry, tag) or 'NYI'
	end
end

--[[
Handles the PlayerInfo, Join, and Leave tags.
]]
local joinleave_xml
local player_info_xml = ([====[
  <key$N>
$PLAYER_GRUNTWORK
  </key$N>
]====]):gsub('%b{}', "")

local function player_info_tag_lookup (tag)
	if tag == 'N' then
		return tostring(state.key)
	elseif tag == 'NAME' then
		return state.index
	elseif tag == 'TIME' then
		return state.time
	end
	local ltag = tag:lower()
	if state.entry[ltag] then
		-- handles race, guild, sex, class, level
		return state.entry[ltag]
	end
	return "?"
end

do
	local pi_xml_save = player_info_xml
	local gruntwork_tags = {
		"name", "race", "guild", "sex", "class", "level"
	}
	for i,tag in ipairs(gruntwork_tags) do
		gruntwork_tags[i] = ("    <%s>$%s</%s>"):format(tag,tag:upper(),tag)
	end
	player_info_xml = player_info_xml:gsub('$PLAYER_GRUNTWORK', table.concat(gruntwork_tags,'\n'))

	-- The join/leave blocks use "player" instead of "name".  They don't have a
	-- guild tag, but they do have a time tag.
	gruntwork_tags[1] = "    <player>$NAME</player>"
	gruntwork_tags[3] = "    <time>$TIME</time>"
	joinleave_xml = pi_xml_save:gsub('$PLAYER_GRUNTWORK', table.concat(gruntwork_tags,'\n'))
end

--[[
$N                  1-based loop variable for key element
$ITEMNAME           Without The Square Brackets of the Whale
$ITEMID             Not the numeric ID, actually a full itemstring without the leading "item:"
$ICON               Last component of texture path?
$CLASS,$SUBCLASS    ItemType
$COLOR              GetItemQualityColor, full 8-digit string
$COUNT,$BOSS,$ZONE,
  $PLAYER           all self-explanatory
$COSTS              in DKP points... hmmm
$ITEMNOTE           take the notes field for this one
$TIME               another formatted timestamp
]]
local phat_lewt_xml = ([====[
  <key$N>
$LEWT_GRUNTWORK
    <zone>$ZONE</zone>   {may be optional}
    <difficulty>$DIFFICULTY</difficulty>   {this scales badly in places like ICC.  may be optional?}
    <Note><![CDATA[$ITEMNOTE - Zone: $ZONE - Boss: $BOSS - $COSTS DKP]]></Note> {zone can be followed by difficulty}
  </key$N>
]====]):gsub('%b{}', "")

local function phat_lewt_tag_lookup (tag)
	if tag == 'N' then
		return tostring(state.key)
	elseif tag == 'COSTS'
		then return '1'
	else
		return do_tag_lookup_handler (state.index, state.entry, tag) or 'NYI'
	end
end

do
	local gruntwork_tags = {
		"ItemName", "ItemID", "Icon", "Class", "SubClass", "Color", "Count",
		"Player", "Costs", "Boss", "Time",
	}
	for i,tag in ipairs(gruntwork_tags) do
		gruntwork_tags[i] = ("    <%s>$%s</%s>"):format(tag,tag:upper(),tag)
	end
	phat_lewt_xml = phat_lewt_xml:gsub('$LEWT_GRUNTWORK', table.concat(gruntwork_tags,'\n'))
end


local function format_EQDKP_timestamp (day_entry, time_entry)
	--assert(day_entry.kind == 'time', day_entry.kind .. " passed to MLEQDKP timestamp")
	return addon:format_timestamp ("$M/$D/$Y $h:$m:00", day_entry, time_entry)
end


-- Look up tag strings for a particular item, given index and entry table.
tag_lookup_handlers.ITEMNAME =
	function (i, e)
		return e.itemname
	end

tag_lookup_handlers.ITEMID =
	function (i, e)
		return e.itemlink:match("^|c%x+|H(item[%d:]+)|h%[")
	end

tag_lookup_handlers.ICON =
	function (i, e)
		local str = e.itexture
		repeat
			local s = str:find('\\')
			if s then str = str:sub(s+1) end
		until not s
		return str
	end

tag_lookup_handlers.CLASS =
	function (i, e)
		return state.class
	end

tag_lookup_handlers.SUBCLASS =
	function (i, e)
		return state.subclass
	end

tag_lookup_handlers.COLOR =
	function (i, e)
		local q = select(4, GetItemQualityColor(e.quality))
		return q  -- skip leading |c (no longer returned by GIQC after 4.2)
	end

tag_lookup_handlers.COUNT =
	function (i, e)
		return e.count and e.count:sub(2) or "1"   -- skip the leading "x"
	end

-- should combine these next two
tag_lookup_handlers.BOSS =
	function (i, e)
		while i > 0 and state.loot[i].kind ~= 'boss' do
			i = i - 1
		end
		if i == 0 then return "No Boss Entry Found, Unknown Boss" end
		return state.loot[i].bosskill
	end

tag_lookup_handlers.ZONE =
	function (i, e)
		while i > 0 and state.loot[i].kind ~= 'boss' do
			i = i - 1
		end
		if i == 0 then return "No Boss Entry Found, Unknown Zone" end
		return state.loot[i].instance
	end

tag_lookup_handlers.DIFFICULTY =
	function (i, e)
		local tag = tag_lookup_handlers.ZONE(i,e)
		local N,h = tag:match("%((%d+)(h?)%)")
		if not N then return "1" end -- maybe signal an error instead?
		N = tonumber(N)
		N = ( (N==10) and 1 or 2 ) + ( (h=='h') and 2 or 0 )
		return tostring(N)
	end

tag_lookup_handlers.PLAYER =
	function (i, e)
		return state.player
	end

tag_lookup_handlers.ITEMNOTE =
	function (i, e)
		return state.itemnote
	end

tag_lookup_handlers.TIME =
	function (i, e)
		local ti,tl = addon:find_previous_time_entry(i)
		return format_EQDKP_timestamp(tl,e)
	end


function do_tag_lookup_handler (index, entry, tag)
	local h = tag_lookup_handlers[tag]
	if h then
		return h(index,entry)
	else
		error(("MLDKP tag lookup (index %d) on tag %s with no handler"):format(index,tag))
	end
end


local function generator (ttype, loot, last_printed, generated, cache)
	-- Because it's XML, generated text is "grown" by shoving more crap into
	-- the middle instead of appending to the end.  Only easy way of doing that
	-- here is regenerating it from scratch each time.
	generated[ttype] = nil

	local _
	local text = XML
	state.loot = loot

	-- TIMESTAMPs
	do
		local f,l  -- first and last timestamps in the table
		for i = 1, #loot do
			if loot[i].kind == 'time' then
				f = format_EQDKP_timestamp(loot[i])
				break
			end
		end
		_,l = addon:find_previous_time_entry(#loot)  -- latest timestamp
		l = format_EQDKP_timestamp(l,loot[#loot])
		text = text:gsub('$TIMESTAMP', f):gsub('$ENDTIME', l)
	end

	-- Loot
	do
		local all_lewts = {}
		local lewt_template = phat_lewt_xml

		state.key = 1
		for i,e in addon:filtered_loot_iter('loot') do
			state.index, state.entry = i, e
			-- no sense doing repeated getiteminfo calls
			state.class, state.subclass = select(6, GetItemInfo(e.id))

			-- similar logic as text_tabs.lua:
			-- assuming nobody names a toon "offspec" or "gvault"
			local P, N
			local disp = e.disposition or e.person
			if disp == 'offspec' then
				P,N = e.person, "offspec"
			elseif disp == 'gvault' then
				P,N = "guild vault", e.person
			else
				P,N = disp, ""
			end
			if e.extratext_byhand then
				N = N .. " -- " .. e.extratext
			end
			state.player, state.itemnote = P, N

			all_lewts[#all_lewts+1] = lewt_template:gsub('%$([%w_]+)',
				phat_lewt_tag_lookup)
			state.key = state.key + 1
		end

		text = text:gsub('$PHAT_LEWTS', tconcat(all_lewts, '\n'))
	end

	-- Player info, join times, leave times
	do
		local all_players, all_joins, all_leaves = {}, {}, {}
		local player_template, joinleave_template = player_info_xml, joinleave_xml
		local date = date

		state.key = 1
		if type(loot.raiders) == 'table' then for name,r in pairs(loot.raiders) do
			state.index, state.entry = name, r
			all_players[#all_players+1] = player_template:gsub('%$([%w_]+)', player_info_tag_lookup)
			state.time = date ("%m/%d/%y %H:%M:00", r.join)
			all_joins[#all_joins+1] = joinleave_template:gsub('%$([%w_]+)', player_info_tag_lookup)
			state.time = date ("%m/%d/%y %H:%M:00", r.leave)
			all_leaves[#all_leaves+1] = joinleave_template:gsub('%$([%w_]+)', player_info_tag_lookup)
			state.key = state.key + 1
		end end
		text = text:gsub('$PLAYER_INFOS', tconcat(all_players, '\n'))
		           :gsub('$JOINS', tconcat(all_joins, '\n'))
		           :gsub('$LEAVES', tconcat(all_leaves, '\n'))
	end

	-- Bosses and wipes (does anybody really use the latter?)
	do
		local all_bosses, all_wipes = {}, {}
		local boss_template = boss_kills_xml

		state.key = 1
		for i,e in addon:filtered_loot_iter('boss') do
			if e.reason == 'kill' then
				state.index, state.entry = i, e
				all_bosses[#all_bosses+1] = boss_template:gsub('%$([%w_]+)',
					boss_kills_tag_lookup)
				state.key = state.key + 1
			elseif e.reason == 'wipe' then
				all_wipes[#all_wipes+1] = ('<Wipe>%d</Wipe>'):format(e.stamp)
			end
		end

		text = text:gsub('$BOSS_KILLS', tconcat(all_bosses, '\n'))
		           :gsub('$WIPES', tconcat(all_wipes, '\n'))
	end

	-- In addition to doing the top-level zone, this will also catch any
	-- leftover $ZONE tags.  There could be multiple places in the raid, so
	-- we default to the first one we saw.
	do
		local iter = addon:filtered_loot_iter()  -- HACK
		local first_boss = iter('boss',0)
		local zone = first_boss and loot[first_boss].instance or "Unknown"
		text = text:gsub('$ZONE', zone)
	end

	-- Misc
	text = text:gsub('$REALM', (GetRealmName()))
	--text = text:gsub('$DIFFICULTY', )
	text = text:gsub('$RAIDNOTE', "")

	cache[#cache+1] = banner_formatted
	cache[#cache+1] = banner_sep
	cache[#cache+1] = text
	cache[#cache+1] = '\n'

	cache[#cache+1] = banner_unformatted
	cache[#cache+1] = banner_sep
	text = text:gsub('>%s+<', "><")
	cache[#cache+1] = text
	cache[#cache+1] = '\n'

	wipe(state)
	return true
end

local function specials (_, editbox, container, mkbutton)
	local b = mkbutton("Highlight",
		[[Highlight the unformatted copy for copy-and-pasting.]])
	b:SetFullWidth(true)
	b:SetCallback("OnClick", function(_b)
		local _,start,finish
		local txt = editbox:GetText()
		_,start = txt:find(banner_unformatted..'\n'..banner_sep..'\n')
		_,finish = txt:find("</RaidInfo>", start)
		editbox.editBox:HighlightText(start,finish)
		editbox.editBox:SetCursorPosition(start)
	end)
	container:AddChild(b)

	local b = mkbutton("Re-Unformat",
		[[Regenerate only the unformatted copy at the bottom <*from*> the formatted copy at the top.]])
	b:SetFullWidth(true)
	b:SetCallback("OnClick", function(_b)
		local _,start,finish
		local txt = editbox:GetText()
		_,start = txt:find(banner_formatted..'\n'..banner_sep..'\n', --[[init=]]1, --[[plain=]]true)
		_,finish = txt:find("</RaidInfo>", start, true)
		txt = txt:sub(start+1,finish)
		txt = banner_formatted .. '\n'
			.. banner_sep .. '\n'
			.. txt .. '\n\n\n'
			.. banner_unformatted .. '\n'
			.. banner_sep .. '\n'
			.. txt:gsub('>%s+<', "><") .. '\n'
		-- This would normally screw up the cached version, but we're regenerating
		-- everything on each new display for this tab anyhow.
		editbox.editBox:SetText(txt)
		_,start = txt:find(banner_unformatted..'\n'..banner_sep..'\n', --[[init=]]1, --[[plain=]]true)
		editbox.editBox:SetCursorPosition(start)
	end)
	container:AddChild(b)
end

addon:register_text_generator ("mleqdkp", [[ML/EQ-DKP]], [[MLdkp 1.1 EQDKP format]], generator, specials)

-- vim:noet