view gd.lua @ 20:5efaeb3ef4b1 6.0.4

Split out tracking of officer notes from player notes.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 08 Nov 2014 11:58:20 -0500
parents 3961b0cc4642
children
line wrap: on
line source
local nametag = ...
local addon = CreateFrame("Frame")
local L = LibStub("AceLocale-3.0"):GetLocale(nametag)
local SV, SVc

local DEFAULT_CHAT = 2   -- combat log (no constant predefined for that)

local guild_selection, log_ever_shown
addon.options = {
	--name = filled in during OnInit
	type = 'group',
	childGroups = 'tab',
	handler = addon,    -- functions listed as strings called as addon:func
	args = {
		version = {
			--name = filled in during OnEnable
			type = 'description',
			fontSize = "large",
			--image = "Interface\\AddOns\\GuildDelta\\test2",
			cmdHidden = true,
			order = 1,
		},
		note = {
			--name = filled in locals section
			type = 'description',
			cmdHidden = true,
			width = 'full',
			order = 2,
		},
		general = {
			name = GENERAL,
			desc = L["Tracking options"],
			type = 'group',
			order = 10,
			args = {
				fields = {
					name = L["Attributes"],
					desc = L["Track changes to these player attributes"],
					type = 'multiselect',
					order = 10,
					-- these need to be of function type rather than string keys of members
					values = function(info) return addon:MakeFieldList() end,
					get = function(info,x) return SV.fields[x] end,
					set = function(info,x,val) SV.fields[x] = val end,
				},
				nochange = {
					name = L["No Change"],
					desc = L["Print a message even when no changes are detected"],
					type = 'toggle',
					order = 15,
					get = function() return not not SV.notify_nochange end,
					set = function(info,val) SV.notify_nochange = val end,
				},
				color = {
					name = L["Color of output text"],
					type = 'color',
					order = 17,
					get = function() return SV.red, SV.green, SV.blue, 1 end,
					set = function(info,...) SV.red, SV.green, SV.blue = ... end,
				},
				spacer2 = {
					name = '',
					type = 'description',
					cmdHidden = true,
					width = 'full',
					order = 19,
				},
				guilds = {
					name = L["Guilds"],
					desc = L["Guilds for which a roster is known"],
					type = 'select',
					order = 20,
					width = 'double',
					values = function(info) return addon:MakeGuildList() end,
					get = function(info) return guild_selection end,
					set = function(info,val) guild_selection = val end,
				},
				clearguild = {
					name = L["Reset Guild"],
					desc = L["Erase stored data for selected guild; information will be scanned from scratch on next login."],
					type = 'execute',
					order = 22,
					disabled = function() return not guild_selection end,
					func = function()
						assert(type(guild_selection)=='string')
						local g,r = guild_selection:match("<([^>]+)> %- (.*)")
						local m = SV.members[r]
						if m then
							m[g] = nil
						else
							addon:Printf(L["Error:  %s cannot be matched as a realm name.  Please report this as a bug, including the name of the realm and guild."], r)
						end
					end,
				},
			},
		},
		output = {
			name = L["Output"],
			desc = L["What and where to print"],
			type = 'group',
			order = 20,
			args = {
				reset = {
					name = L["Reset Output"],
					desc = L["Restores default output settings"],
					type = 'execute',
					order = 10,
					func = "SetChat",
					arg = DEFAULT_CHAT,
				},
				spacer1 = {
					name = '',
					type = 'description',
					cmdHidden = true,
					width = 'full',
					order = 11,
				},
				print_chatframes = {
					name = L["Print Chatframe Numbers"],
					desc = L["Print each chat window number in its frame, for easy reference in the next slider option"],
					type = 'execute',
					--func = print_chatframes filled in below
					order = 20,
				},
				chatframe_num = {
					name = L["Output Chatframe"],
					desc = L["Which chat window to prefer for printing all the output during login"],
					type = 'range',
					min  = 1,
					max  = NUM_CHAT_WINDOWS,
					step = 1,
					get = function() return tonumber(SV.chatframe) or --[[in case of custom name]]DEFAULT_CHAT end,
					set = "SetChat",
					order = 25,
				},
				chatframe_name = {
					name = L["Chatframe Override"],
					desc = L["<Advanced>  If blank, uses the numerical slider.  If set, it is the Lua variable name of a frame with AddMessage capability to use for output."],
					type = 'input',
					get = function()
						return type(SV.chatframe) == 'string' and SV.chatframe or nil
					end,
					set = "SetChat",
					order = 26,
				},
				header = {
					name = '',
					type = 'header',
					order = 30,
				},
				snapshot = {
					name = L["Snapshot"],
					desc = L["Show a text window with the current guild roster."],
					type = 'execute',
					order = 31,
					func = "Snapshot",
				},
			},
		},
		persist = {
			name = L["Logging"],
			desc = L["Storing changes for later review"],
			type = 'group',
			order = 30,
			args = {
				note = {
					--name = filled in locals section
					type = 'description',
					cmdHidden = true,
					width = 'full',
					order = 10,
				},
				enable = {
					name = L["Enable Logging"],
					desc = L["Accumulate all deltas, including a timestamp.  |cffFF0000WARNING|r: if logging was enabled and you turn it off, the log itself will not be saved when exiting the game."],
					type = 'toggle',
					order = 15,
					get = function() return SV.logging end,
					set = function(i,v)
						SV.logging = v
						log_ever_shown = SV.logging or log_ever_shown
					end,
				},
				clearlog = {
					name = L["Reset Log"],
					desc = L["Erase accumulated deltas"],
					type = 'execute',
					order = 20,
					hidden = function() return not (SV.logging or log_ever_shown) end,
					disabled = function() return not SV.logging end,
					func = function()
						SV.logtext = nil
					end,
				},
				log = {
					name = L["Log"],
					desc = L["If you make changes, don't forget to click 'Accept' to save them.  Scroll down if needed."],
					type = 'input',
					order = 25,
					multiline = 15,
					width = 'full',
					hidden = function() return not (SV.logging or log_ever_shown) end,
					disabled = function() return not SV.logging end,
					get = function() return SV.logtext end,
					set = function(i,t) SV.logtext = t end,
				},
			},
		},
	}
}


-----------------------------------------------------------------------------
-- other locals
local tinsert, GetGuildRosterInfo = _G.table.insert, _G.GetGuildRosterInfo
local chatframe, fieldlist
local function prt (...)
	return chatframe:AddMessage(...)
end
local function cprt (txt)
	if SV.logging then addon:AddLogNote(txt) end
	return prt(txt, SV.red, SV.green, SV.blue)
end

function addon:current_guild_info (N)
	local ret = {}
	local swearing = GetCVar("profanityFilter")
	SetCVar ("profanityFilter", "0")
	for i = 1, N do
		local name,rank_as_string,_,level,_,_,publicnote,officernote = GetGuildRosterInfo(i)
		-- This technically does not work out to the same as A?B:C, combined
		-- with the logic below.  It does, however, still result in the entry
		-- not appearing in the returned table.
		publicnote = publicnote ~= "" and publicnote or nil
		officernote = officernote ~= "" and officernote or nil
		if name then -- redundant, but apparently happens on extreme lag
			tinsert(ret,
				{name   = name,
				 rank   = SV.fields.rank and rank_as_string or nil,
				 level  = SV.fields.level and level or nil,
				 pnote  = SV.fields.notes and publicnote or nil,
				 onote  = SV.fields.onotes and officernote or nil,
				})
		end
	end
	table.sort(ret, function (l,r) return l.name < r.name end)
	SetCVar ("profanityFilter", swearing)
	return ret
end

function addon.options.args.output.args.print_chatframes.func()
	for i = 1, NUM_CHAT_WINDOWS do
		local cf = _G['ChatFrame'..i]
		if not cf then break end
		addon:Printf(cf, L["This is frame number %d."], i)
	end
end

-- Localize these?
addon.options.args.note.name = 
"You can use the '/guilddelta' command to open the options window.\n\n"..
"The guild roster has already been scanned by the time you see this.  Therefore, "..
"if you make any changes to the Fields section below, you should probably relog "..
"immediately to begin tracking the changed fields.  Changes to the contents *OF* "..
"those fields will not be noticed until the first login after *that*.\n"

addon.options.args.persist.args.note.name = 
"Enabling logging will accumulate the text of the 'deltas' as you see them.  "..
"This can grow large over time, depending on the activity of your guilds, so you "..
"should use the Reset Log button below from time to time.\n\n"..
"If you click inside the text area below, you can use Control-A to select all "..
"the text, and Control-C to copy it to your computer's clipboard.  If you make "..
"any changes to the text, the changes will be preserved.  (You can remove uninteresting "..
"changes, add reminders to yourself, and so forth.)\n\n"


-----------------------------------------------------------------------------
addon = LibStub("AceAddon-3.0"):NewAddon(addon, nametag,
                "AceConsole-3.0")

function addon:OnInitialize()
	if _G.GuildDeltaSV == nil then
		-- Defaults need to transition from potential older savedvars
		_G.GuildDeltaSV = {
			chatframe = _G.GuildDelta_chatframe or DEFAULT_CHAT,
			fields = _G.GuildDelta_fields or
				{ rank = true, level = false, notes = true },
			members = _G.GuildDelta_memberdata or {},
			--notify_nochange = nil by default
			--logging = nil by default
		}
	end
	SV = _G.GuildDeltaSV
	-- upgrade path
	if SV.red == nil then
		SV.red =   255/255
		SV.green =  26/255
		SV.blue =  160/255
	end
	if SV.fields.onotes == nil then
		SV.fields.onotes = SV.fields.notes
	end
end

-- Remove anything that normal operation doesn't need after finishing its work.
function addon:cleanup()
	prt = nil; cprt = nil
	self.current_guild_info = nil
	self.cleanup = nil
	self.unload = nil
	self.logquay = nil
	self.AddLogNote = nil
	self.FinishLog = nil
end
-- Remove everything.
function addon:unload()
	self:cleanup()
	LibStub("AceAddon-3.0").addons[nametag] = nil
	addon = nil;
	-- put the userdata back so it counts as a Frame object again
	local ud = self[0]
	table.wipe(self)
	self[0] = ud
end

function addon:OnEnable()
	if not IsInGuild() then
		self:Print("You are not in a guild, not loading.")
		return self:unload()
	end

	self:RegisterEvent("GUILD_ROSTER_UPDATE")
	self:SetScript("OnEvent", self.GuildUpdate)

	self.options.args.version.name =
		"|cff30adffVersion " .. (GetAddOnMetadata(nametag, "Version") or "?") .. "|r"
	LibStub("AceConfig-3.0"):RegisterOptionsTable(nametag, self.options)
	self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions(nametag, "Guild Delta")
	self:RegisterChatCommand("guilddelta", "OnChatCommand")
	self:SetChat(self)
	log_ever_shown = SV.logging
	self.OnEnable = nil
end


function addon:OnChatCommand (input)
	if not input or input:trim() == "" then
		LibStub("AceConfigDialog-3.0"):Open(nametag)
	else
		LibStub("AceConfigCmd-3.0").HandleCommand(self, "guilddelta", nametag, input)
	end
end


-- 1 arg:  (self) called during startup to initialize
--         (other) resetting via menus to defaults (see arg field)
-- 2 args:  setting via menus to new value, number or custom name (see value)
function addon:SetChat (info, value)
	local n, nframe
	local isauto = info == self
	if isauto then
		n = SV.chatframe
	else -- coming via menu
		n = info.arg or value
	end
	if type(n) == 'number' then
		if isauto then
			-- start at the preferred number and find a non-minimized and
			-- potentially visible frame
			n = math.min(n,NUM_CHAT_WINDOWS)
			local cf,cft
			for i = n, 1, -1 do
				cf = _G['ChatFrame'..i]
				cft = _G[cf:GetName()..'Tab']
				if cft:IsVisible() then
					n = i
					break
				end
			end
		end
		nframe = _G['ChatFrame'..n]
	else
		nframe = _G[n]    -- advanced name
	end
	if type(nframe) == 'table' and type(nframe.AddMessage) == 'function' then
		if not isauto then
			SV.chatframe = n
			self:Print(L["Now printing to chat frame"], n,
				(type(nframe.name)=='string' and ("(".. nframe.name .. ")") or ""))
		end
		chatframe = nframe
	else
		self:Printf(L["'%s' was not a valid chat frame number/name, no change has been made."], n)
	end
end


function addon:Snapshot()
	if self.optionsFrame:IsVisible() then
		InterfaceOptionsFrameCancel:Click()
		HideUIPanel(GameMenuFrame)
	else
		LibStub("AceConfigDialog-3.0"):Close(nametag)
	end
	GameTooltip:Hide()

	local tinsert = table.insert
	local fmt = "%-20s%-16s%-20s%-20s"
	local results = {
		fmt:format(CALENDAR_PLAYER_NAME, L["FIELD_rank"], LABEL_NOTE, OFFICER_NOTE_COLON),
		('-'):rep(80),
	}

	for _,x in ipairs(SVc) do
		tinsert (results, fmt:format(x.name, x.rank or '--', x.pnote or '--', x.onote or ''))
	end

	results = table.concat (results, '\n')

	local GUI = LibStub("AceGUI-3.0")
	local f = GUI:Create("Frame")
	f:SetTitle(nametag)
	f:SetStatusText("Use Ctrl-A and Ctrl-C to copy this text to your system clipboard.")
	f:SetLayout("Fill")
	f:SetStatusTable{width=700}

	local eb = GUI:Create("MultiLineEditBox")
	eb:SetFullWidth(true)
	eb:SetFullHeight(true)
	eb:SetLabel("Pressing the Escape key while typing will return keystroke control to the usual chat window.")
	eb:DisableButton(true)
	eb:SetText(results)

	f:SetCallback("OnClose", function(_f)
		eb:SetText''   -- hmmmm
		GUI:Release(_f)
	end)
	f:AddChild(eb)
	eb:SetFocus()
	f:ApplyStatus()
	f:Show()
end


-- Not a "normal" PLAYER_LOGOUT handler; this only fires if the player is in
-- a guild and the update has already run.
function addon:PLAYER_LOGOUT()
	if not SV.logging then
		SV.logtext = nil
	end
end


function addon:GuildUpdate()
	local current_n = GetNumGuildMembers(true)
	if (current_n <= 0) or (not GetGuildInfo("player")) then
		-- catch the hell up, servers...
		return GuildRoster()
	end
	self:UnregisterEvent("GUILD_ROSTER_UPDATE")
	self:SetScript("OnEvent", self.PLAYER_LOGOUT) -- keepin' it real^H^H^H^Hsmall and kludgey
	self:RegisterEvent("PLAYER_LOGOUT")
	self.GuildUpdate = nil

	if SV.logtext and #SV.logtext > 20000 then
		self:Print(L["Your accumulated logfile has grown rather large; you should consider copying it out and clearing it."])
	end

	local guild = assert((GetGuildInfo("player")), "GetGuildInfo returns nil?")
	local realm = assert(GetRealmName(), "GetRealmName returns nil?")
	local members = SV.members
	if members[realm] 
	   and members[realm][guild]
	   and #(members[realm][guild]) > 0
	then
		-- moved the normal case below
	else
		-- new user, or new guild, or any number of things
		self:Print(L["GuildDelta initializing roster..."])
		members[realm] = members[realm] or {}
		members[realm][guild] = self:current_guild_info(current_n)
		return self:cleanup()
	end

	-- table.insert with notes if available
	-- concatentation of string-only args faster than string.format of same
	local function tins (t, x)
		local s = x.name
		if x.onote and (x.onote ~= "") then
			s = s .. "(" .. L["Oabbrev"] .. ": " .. x.onote .. ")"
		end
		if x.pnote and (x.pnote ~= "") then
			s = s .. "(" .. x.pnote .. ")"
		end
		tinsert(t, s)
	end

	-- build the current list
	local previous, current = members[realm][guild],
							  self:current_guild_info(current_n)
	local previous_n = #previous

	-- walk both and do equivalence comparison
	local joined, left, rank, level, notes = {}, {}, {}, {}, {}
	local p, c = 1, 1
	while p <= previous_n and c <= current_n do
		local P, C = previous[p], current[c]

		if P.name == C.name then
			-- normal case
			p = p + 1
			c = c + 1
			-- but can now compare details
			if C.rank and P.rank and (P.rank ~= C.rank) then
				tinsert(rank, {C.name, P.rank.." --> "..C.rank})
			end
			if C.level and P.level and (P.level ~= C.level) then
				tinsert(level, {C.name, P.level.." --> "..C.level})
			end
			if C.pnote and (P.pnote ~= C.pnote) then
				tinsert(notes, {C.name, C.pnote})
			end
			if C.onote and (P.onote ~= C.onote) then
				tinsert(notes, {C.name, "["..L["Oabbrev"].."]: "..C.onote})
			end

		elseif P.name < C.name then
			-- entry at index p not at c -> somebody has left
			tins (left, P)
			p = p + 1

		else
			-- entry at index c not at p -> somebody has joined
			tins (joined, C)
			c = c + 1
		end
	end

	-- leftovers
	for i = p, previous_n do
		tins (left, previous[i])
	end
	for i = c, current_n do
		tins (joined, current[i])
	end

	-- show results
	if SV.logging then self.logquay = {} end
	local m, changes
	if #left > 0 then
		changes = true
		m = L["LEFT"] .. table.concat(left, ", ")
		cprt(m)
	end

	if #joined > 0 then
		changes = true
		m = L["JOINED"] .. table.concat(joined, ", ")
		cprt(m)
	end

	if #rank > 0 then
		changes = true
		cprt(L["RANK"])
		for i = 1, #rank do
			cprt(rank[i][1]..':  '..rank[i][2])
		end
	end

	if #level > 0 then
		changes = true
		cprt(L["LEVEL"])
		for i = 1, #level do
			cprt(level[i][1]..':  '..level[i][2])
		end
	end

	if #notes > 0 then
		changes = true
		cprt(L["NOTES"])
		for i = 1, #notes do
			cprt(notes[i][1]..': "'..notes[i][2]..'"')
		end
	end

	if SV.logging then self:FinishLog(guild,realm) end
	if SV.notify_nochange and (not changes) then cprt(L["NOCHANGE"]) end
	members[realm][guild] = current
	SVc = current
	self:cleanup()
end


function addon:MakeFieldList()
	if not fieldlist then
		fieldlist = {}
		for name in pairs(SV.fields) do
			fieldlist[name] = L["FIELD_"..name]
		end
	end
	return fieldlist
end

function addon:MakeGuildList()
	local list = {}
	local K
	for rname,rdata in pairs(SV.members) do
		for g in pairs(rdata) do
			K = ("<%s> - %s"):format(g,rname)
			list[K] = K
		end
	end
	return list
end

function addon:AddLogNote (txt)
	tinsert(self.logquay,txt)
end

function addon:FinishLog(g,r)
	if #self.logquay > 0 then
		Calendar_LoadUI()
		local _,M,D,Y = CalendarGetDate()
		local h,m = GetGameTime()
		local timestamp = ("%.4d/%.2d/%.2d %.2d:%.2d    <%s> - %s\n"):format(Y,M,D,h,m,g,r)
		SV.logtext = timestamp
		             .. table.concat(self.logquay, '\n')
		             .. (SV.logtext and ('\n\n'..SV.logtext) or '')
	end
end

-- vim:noet