diff TeamOptimizer.lua @ 57:01b63b8ed811 v21

total rewrite to version 21
author yellowfive
date Fri, 05 Jun 2015 11:05:15 -0700
parents
children adec0972d4e1
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TeamOptimizer.lua	Fri Jun 05 11:05:15 2015 -0700
@@ -0,0 +1,1405 @@
+local Amr = LibStub("AceAddon-3.0"):GetAddon("AskMrRobot")
+local L = LibStub("AceLocale-3.0"):GetLocale("AskMrRobot", true)
+local AceGUI = LibStub("AceGUI-3.0")
+
+local _panelSplash
+local _panelStartLoot
+local _lblStartLoot
+local _btnStartLoot
+local _scrollHistory
+local _tabs
+
+local _messagePrefixes = {
+	RosterRequestGear = "_TRR",
+	RosterGear = "_TRG",
+	ItemExportRequestGear = "_TLR",
+	ItemExportGear = "_TLG",
+	ItemExportLoot = "_TLL",
+	SyncRequest = "_TSR",
+	Sync = "_TSS"
+}
+
+Amr.LootMessagePrefixes = {
+	Start = "_TCS",
+	Roll = "_TCR",
+	Veto = "_TCV",
+	Rand = "_TCD",
+	Give = "_TCG",
+	Finish = "_TCF"
+}
+
+local function renderExportWindow(container, instructions, text)
+
+	local bg = Amr:RenderCoverChrome(container, 800, 450)
+	
+	local lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetWidth(750)
+	lbl:SetText(L.TeamExportHelp)
+	lbl:SetPoint("TOP", bg.content, "TOP", 0, -10)
+	bg:AddChild(lbl)
+	
+	local lbl2 = AceGUI:Create("AmrUiLabel")
+	lbl2:SetWidth(750)
+	lbl2:SetText(instructions)
+	lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -10)
+	bg:AddChild(lbl2)
+
+	local txt = AceGUI:Create("AmrUiTextarea")
+	txt:SetWidth(750)
+	txt:SetHeight(300)
+	txt:SetPoint("TOP", lbl2.frame, "BOTTOM", 0, -10)
+	txt:SetFont(Amr.CreateFont("Regular", 12, Amr.Colors.Text))
+	txt:SetText(text)
+	bg:AddChild(txt)
+	
+	local btn = AceGUI:Create("AmrUiButton")
+	btn:SetText(L.TeamButtonExportClose)
+	btn:SetBackgroundColor(Amr.Colors.Green)
+	btn:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btn:SetWidth(120)
+	btn:SetHeight(28)
+	btn:SetPoint("TOPLEFT", txt.frame, "BOTTOMLEFT", 0, -10)
+	btn:SetCallback("OnClick", function(widget) Amr:HideCover() end)
+	bg:AddChild(btn)
+	
+	return txt
+end
+
+local function renderImportWindow(container)
+
+	local bg = Amr:RenderCoverChrome(container, 700, 450)
+	
+	local lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetWidth(600)
+	lbl:SetText(L.TeamImportRankingsHeader)
+	lbl:SetPoint("TOP", bg.content, "TOP", 0, -10)
+	bg:AddChild(lbl)
+
+	local txt = AceGUI:Create("AmrUiTextarea")
+	txt:SetWidth(600)
+	txt:SetHeight(300)
+	txt:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -10)
+	txt:SetFont(Amr.CreateFont("Regular", 12, Amr.Colors.Text))
+	bg:AddChild(txt)
+	
+	local btnImportOk = AceGUI:Create("AmrUiButton")
+	btnImportOk:SetText(L.ImportButtonOk)
+	btnImportOk:SetBackgroundColor(Amr.Colors.Green)
+	btnImportOk:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btnImportOk:SetWidth(120)
+	btnImportOk:SetHeight(28)
+	btnImportOk:SetPoint("TOPLEFT", txt.frame, "BOTTOMLEFT", 0, -10)
+	bg:AddChild(btnImportOk)
+	
+	local btnImportCancel = AceGUI:Create("AmrUiButton")
+	btnImportCancel:SetText(L.ImportButtonCancel)
+	btnImportCancel:SetBackgroundColor(Amr.Colors.Green)
+	btnImportCancel:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btnImportCancel:SetWidth(120)
+	btnImportCancel:SetHeight(28)
+	btnImportCancel:SetPoint("LEFT", btnImportOk.frame, "RIGHT", 20, 0)
+	btnImportCancel:SetCallback("OnClick", function(widget) Amr:HideCover() end)
+	bg:AddChild(btnImportCancel)
+	
+	local lblErr = AceGUI:Create("AmrUiLabel")
+	lblErr:SetWidth(600)
+	lblErr:SetFont(Amr.CreateFont("Bold", 14, Amr.Colors.Red))
+	lblErr:SetText("")
+	lblErr:SetPoint("TOPLEFT", btnImportOk.frame, "BOTTOMLEFT", 0, -20)
+	bg:AddChild(lblErr)
+	
+	btnImportOk:SetCallback("OnClick", function(widget)
+		local msg = txt:GetText()
+		local err = Amr:ImportRankings(msg)
+		if err then
+			lblErr:SetText(err)
+			txt:SetFocus(true)
+		else
+			Amr:HideCover()
+			Amr:RefreshTeamUi()
+		end
+	end)
+	
+	return txt
+end
+
+local function renderVersionWindow(container)
+
+	local windowWidth = 500
+	local lbl, lbl2
+	local bg, border = Amr:RenderCoverChrome(container, windowWidth, 600)
+	
+	lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetWidth(windowWidth - 60)
+	lbl:SetJustifyH("CENTER")
+	lbl:SetFont(Amr.CreateFont("Bold", 24, Amr.Colors.TextHeaderActive))
+	lbl:SetText(L.TeamVersionTitle)
+	lbl:SetPoint("TOP", bg.content, "TOP", 0, -10)
+	bg:AddChild(lbl)
+	
+	if not IsInGroup() and not IsInRaid() then
+		lbl2 = AceGUI:Create("AmrUiLabel")
+		lbl2:SetWidth(windowWidth - 20)
+		lbl2:SetJustifyH("CENTER")
+		lbl2:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))
+		lbl2:SetText(L.TeamVersionNoGroup)
+		lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -25)
+		bg:AddChild(lbl2)
+		border:SetHeight(150)
+	else
+		local units = Amr:GetGroupUnitIdentifiers()
+	
+		local missing = {}
+		local tooLow = {}
+		
+		for i, unitId in ipairs(units) do
+			local realm, name = Amr:GetRealmAndName(unitId)
+			if realm then
+				local ver = Amr:GetAddonVersion(realm, name)
+				if ver == 0 then
+					table.insert(missing, { unitId, realm, name })
+				elseif ver < Amr.MIN_ADDON_VERSION then
+					table.insert(tooLow, { unitId, realm, name, ver })
+				end
+			end
+		end
+		
+		if #missing == 0 and #tooLow == 0 then
+			lbl2 = AceGUI:Create("AmrUiLabel")
+			lbl2:SetWidth(windowWidth - 20)
+			lbl2:SetJustifyH("CENTER")
+			lbl2:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))
+			lbl2:SetText(L.TeamVersionGood)
+			lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -25)
+			bg:AddChild(lbl2)
+			border:SetHeight(150)
+		else
+			local prev = lbl
+			local h = 0
+			
+			-- helper to render a player name
+			local function renderItem(obj, showVer)
+				lbl = AceGUI:Create("AmrUiLabel")
+				lbl:SetWidth(120)
+				
+				local cls, clsEn = UnitClass(obj[1])
+				local color = clsEn and Amr.Colors.Classes[clsEn] or Amr.Colors.TextHeaderDisabled
+				lbl:SetFont(Amr.CreateFont("Regular", 14, color))
+				
+				lbl:SetText(obj[3])
+				lbl:SetPoint("TOPLEFT", prev.frame, "BOTTOMLEFT", 0, -5)
+				bg:AddChild(lbl)
+				prev = lbl
+				h = h + lbl:GetHeight() + 5
+				
+				if showVer then
+					lbl2 = AceGUI:Create("AmrUiLabel")
+					lbl2:SetWidth(60)
+					lbl2:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White))
+					lbl2:SetText("v" .. obj[4])
+					lbl2:SetPoint("LEFT", lbl.frame, "RIGHT", 5, 0)
+					bg:AddChild(lbl2)
+				end
+			end
+			
+			if #missing > 0 then
+				lbl2 = AceGUI:Create("AmrUiLabel")
+				lbl2:SetWidth(180)
+				lbl2:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.Red))
+				lbl2:SetText(L.TeamVersionMissing)
+				lbl2:SetJustifyH("CENTER")
+				lbl2:SetPoint("TOP", prev.frame, "BOTTOM", 0, -20)
+				bg:AddChild(lbl2)
+				h = h + lbl2:GetHeight() + 20
+				
+				prev = lbl2
+				for i, obj in ipairs(missing) do
+					renderItem(obj)
+				end
+			end
+			
+			if #tooLow > 0 then
+				lbl2 = AceGUI:Create("AmrUiLabel")
+				lbl2:SetWidth(180)
+				lbl2:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.Gold))
+				lbl2:SetText(L.TeamVersionOld)
+				lbl2:SetJustifyH("CENTER")
+				lbl2:SetPoint("TOP", prev.frame, "BOTTOM", 0, -20)
+				bg:AddChild(lbl2)
+				h = h + lbl2:GetHeight() + 20
+				
+				prev = lbl2
+				for i, obj in ipairs(tooLow) do
+					renderItem(obj, true)
+				end
+			end
+			
+			border:SetHeight(h + 100)
+		end
+	end
+	
+	local btn = AceGUI:Create("AmrUiButton")
+	btn:SetText(L.TeamButtonExportClose)
+	btn:SetBackgroundColor(Amr.Colors.Green)
+	btn:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+	btn:SetWidth(120)
+	btn:SetHeight(28)
+	btn:SetPoint("BOTTOM", bg.content, "BOTTOM", 0, 10)
+	btn:SetCallback("OnClick", function(widget) Amr:HideCover() end)
+	bg:AddChild(btn)
+end
+
+local function onVersionClick()
+	-- show a window with players who do not have the addon or too low a version
+	Amr:ShowCover(renderVersionWindow)
+end
+
+local function onExportRosterClick()
+
+	Amr:ShowCover(L.TeamExportRosterLoading)
+	
+	Amr:ExportRosterAsync(function(txt)
+		Amr:HideCover()
+		
+		if not txt then
+			Amr:ShowAlert(L.TeamAlertNoGroup, L.AlertOk)
+			return
+		end
+		
+		Amr:ShowCover(function(container)
+			local textbox = renderExportWindow(container, L.TeamExportRosterText, txt)
+			textbox:SetFocus(true)
+		end)
+	end)
+	
+end
+
+local function onExportLootClick()
+
+	Amr:ShowCover(L.TeamExportRosterLoading)
+
+	Amr:ExportLootAsync(function(txt)
+		Amr:HideCover()
+		
+		if txt == "NOGROUP" then
+			Amr:ShowAlert(L.TeamAlertNoGroup, L.AlertOk)
+			return
+		elseif txt == "NOLOOT" then
+			Amr:ShowAlert(L.TeamAlertNoLoot, L.AlertOk)
+			return
+		else
+			Amr:ShowCover(function(container)
+				local textbox = renderExportWindow(container, L.TeamExportLootText, txt)
+				textbox:SetFocus(true)
+			end)
+		end
+	end)
+end
+
+local function onImportRankingsClick()
+	Amr:ShowCover(function(container)
+		local textbox = renderImportWindow(container)
+		textbox:SetFocus(true)
+	end)
+end
+
+local function renderTab(tab, container)
+
+	local lbl, lbl2
+	
+	if tab == "Member" then
+		local lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetWidth(500)
+		lbl:SetFont(Amr.CreateFont("Regular", 24, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamMemberText)
+		lbl:SetPoint("TOPLEFT", container.content, "TOPLEFT", 0, -40)
+		container:AddChild(lbl)
+		
+		-- if loot is still going on, show a button to re-show the loot window
+		if Amr.db.char.TeamOpt.LootInProgress then
+			lbl2 = AceGUI:Create("AmrUiLabel")
+			lbl2:SetWidth(500)
+			lbl2:SetFont(Amr.CreateFont("Italic", 18, Amr.Colors.TextTan))
+			lbl2:SetText(L.TeamMemberShowLootLabel)
+			lbl2:SetPoint("TOPLEFT", lbl.frame, "BOTTOMLEFT", 0, -60)
+			container:AddChild(lbl2)
+		
+			local btn = AceGUI:Create("AmrUiButton")
+			btn:SetWidth(180)
+			btn:SetHeight(26)
+			btn:SetBackgroundColor(Amr.Colors.Blue)
+			btn:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+			btn:SetText(L.TeamMemberShowLoot)
+			btn:SetPoint("TOPLEFT", lbl2.frame, "BOTTOMLEFT", 0, -10)
+			btn:SetCallback("OnClick", function(widget) 
+				Amr:ShowLootWindow()
+				Amr:RefreshLootWindow()
+				Amr:RefreshLootRolls()
+			end)
+			container:AddChild(btn)
+		end
+		
+	elseif tab == "Leader" then
+	
+		local lblNum = AceGUI:Create("AmrUiLabel")
+		lblNum:SetFont(Amr.CreateFont("Bold", 26, Amr.Colors.White))
+		lblNum:SetText("0.")
+		lblNum:SetWidth(40)
+		lblNum:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, -40)
+		container:AddChild(lblNum)
+	
+		local btnVersion = AceGUI:Create("AmrUiButton")
+		btnVersion:SetText(L.TeamButtonVersionText)
+		btnVersion:SetBackgroundColor(Amr.Colors.Orange)
+		btnVersion:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+		btnVersion:SetWidth(180)
+		btnVersion:SetHeight(26)
+		btnVersion:SetPoint("LEFT", lblNum.frame, "RIGHT", 0, -1)
+		btnVersion:SetCallback("OnClick", onVersionClick)
+		container:AddChild(btnVersion)
+		
+		lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamExportVersionLabel)
+		lbl:SetWidth(400)
+		lbl:SetPoint("TOPLEFT", btnVersion.frame, "TOPRIGHT", 20, 0)
+		container:AddChild(lbl)
+		
+		lblNum = AceGUI:Create("AmrUiLabel")
+		lblNum:SetFont(Amr.CreateFont("Bold", 26, Amr.Colors.White))
+		lblNum:SetText("1.")
+		lblNum:SetWidth(40)
+		lblNum:SetPoint("TOPRIGHT", btnVersion.frame, "BOTTOMLEFT", 0, -39)
+		container:AddChild(lblNum)
+		
+		local btnRoster = AceGUI:Create("AmrUiButton")
+		btnRoster:SetText(L.TeamButtonExportRosterText)
+		btnRoster:SetBackgroundColor(Amr.Colors.Orange)
+		btnRoster:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+		btnRoster:SetWidth(180)
+		btnRoster:SetHeight(26)
+		btnRoster:SetPoint("LEFT", lblNum.frame, "RIGHT", 0, -1)
+		btnRoster:SetCallback("OnClick", onExportRosterClick)
+		container:AddChild(btnRoster)
+		
+		lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamExportRosterLabel)
+		lbl:SetWidth(400)
+		lbl:SetPoint("TOPLEFT", btnRoster.frame, "TOPRIGHT", 20, 0)
+		container:AddChild(lbl)
+
+		lblNum = AceGUI:Create("AmrUiLabel")
+		lblNum:SetFont(Amr.CreateFont("Bold", 26, Amr.Colors.White))
+		lblNum:SetText("2.")
+		lblNum:SetWidth(40)
+		lblNum:SetPoint("TOPRIGHT", btnRoster.frame, "BOTTOMLEFT", 0, -89)
+		container:AddChild(lblNum)
+		
+		local btnLoot = AceGUI:Create("AmrUiButton")
+		btnLoot:SetText(L.TeamButtonExportLootText)
+		btnLoot:SetBackgroundColor(Amr.Colors.Orange)
+		btnLoot:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+		btnLoot:SetWidth(180)
+		btnLoot:SetHeight(26)
+		btnLoot:SetPoint("LEFT", lblNum.frame, "RIGHT", 0, -1)
+		btnLoot:SetCallback("OnClick", onExportLootClick)
+		container:AddChild(btnLoot)
+
+		lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamExportLootLabel)
+		lbl:SetWidth(400)
+		lbl:SetPoint("TOPLEFT", btnLoot.frame, "TOPRIGHT", 20, 0)
+		container:AddChild(lbl)
+		
+		lbl2 = AceGUI:Create("AmrUiLabel")
+		lbl2:SetFont(Amr.CreateFont("Bold", 14, Amr.Colors.Blue))
+		lbl2:SetText(L.TeamExportLootLabel2)
+		lbl2:SetWidth(400)
+		lbl2:SetPoint("TOPLEFT", lbl.frame, "BOTTOMLEFT", 0, -5)
+		container:AddChild(lbl2)
+				
+		lblNum = AceGUI:Create("AmrUiLabel")
+		lblNum:SetFont(Amr.CreateFont("Bold", 26, Amr.Colors.White))
+		lblNum:SetText("3.")
+		lblNum:SetWidth(40)
+		lblNum:SetPoint("TOPRIGHT", btnLoot.frame, "BOTTOMLEFT", 0, -89)
+		container:AddChild(lblNum)
+		
+		local btnRank = AceGUI:Create("AmrUiButton")
+		btnRank:SetText(L.TeamButtonImportRankingsText)
+		btnRank:SetBackgroundColor(Amr.Colors.Green)
+		btnRank:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+		btnRank:SetWidth(180)
+		btnRank:SetHeight(26)
+		btnRank:SetPoint("LEFT", lblNum.frame, "RIGHT", 0, -1)
+		btnRank:SetCallback("OnClick", onImportRankingsClick)
+		container:AddChild(btnRank)
+		
+		lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamImportRankingsLabel)
+		lbl:SetWidth(400)
+		lbl:SetPoint("TOPLEFT", btnRank.frame, "TOPRIGHT", 20, 0)
+		container:AddChild(lbl)
+		
+		_panelStartLoot = AceGUI:Create("AmrUiPanel")
+		_panelStartLoot:SetLayout("None")
+		_panelStartLoot:SetBackgroundColor(Amr.Colors.Black, 0)
+		_panelStartLoot:SetPoint("TOPLEFT", lblNum.frame, "BOTTOMLEFT", 0, -90)
+		container:AddChild(_panelStartLoot)
+		_panelStartLoot:SetVisible(false)
+		
+		lblNum = AceGUI:Create("AmrUiLabel")
+		lblNum:SetFont(Amr.CreateFont("Bold", 26, Amr.Colors.White))
+		lblNum:SetText("4.")
+		lblNum:SetWidth(40)
+		lblNum:SetPoint("TOPLEFT", _panelStartLoot.content, "TOPLEFT")
+		_panelStartLoot:AddChild(lblNum)
+		
+		_btnStartLoot = AceGUI:Create("AmrUiButton")
+		_btnStartLoot:SetText(L.TeamButtonStartLootText)
+		_btnStartLoot:SetBackgroundColor(Amr.Colors.Blue)
+		_btnStartLoot:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White))
+		_btnStartLoot:SetWidth(180)
+		_btnStartLoot:SetHeight(26)
+		_btnStartLoot:SetPoint("LEFT", lblNum.frame, "RIGHT", 0, -1)
+		_btnStartLoot:SetCallback("OnClick", function(widget) 
+			if Amr.db.char.TeamOpt.LootInProgress then
+				Amr:ShowLootWindow()
+				Amr:RefreshLootWindow()
+				Amr:RefreshLootRolls()
+			else
+				Amr:StartLoot()
+			end
+		end)
+		_panelStartLoot:AddChild(_btnStartLoot)
+		
+		_lblStartLoot = AceGUI:Create("AmrUiLabel")
+		_lblStartLoot:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.Text))
+		_lblStartLoot:SetWidth(400)
+		_lblStartLoot:SetPoint("LEFT", _btnStartLoot.frame, "RIGHT", 20, 0)
+		_panelStartLoot:AddChild(_lblStartLoot)
+	end
+	
+	-- loot history shows on either tab
+	lbl = AceGUI:Create("AmrUiLabel")
+	lbl:SetFont(Amr.CreateFont("Regular", 16, Amr.Colors.TextTan))
+	lbl:SetText(L.TeamHistoryTitle)
+	lbl:SetWidth(280)
+	lbl:SetPoint("TOPRIGHT", container.content, "TOPRIGHT", 0, -12)
+	container:AddChild(lbl)
+	
+	local panelHistory = AceGUI:Create("AmrUiPanel")
+	panelHistory:SetLayout("Fill")
+	panelHistory:SetBackgroundColor(Amr.Colors.Black, 0.3)
+	panelHistory:SetPoint("TOPRIGHT", lbl.frame, "BOTTOMRIGHT", 0, -5)
+	panelHistory:SetPoint("BOTTOMLEFT", container.content, "BOTTOMRIGHT", -280, 0)
+	container:AddChild(panelHistory)
+	
+	_scrollHistory = AceGUI:Create("AmrUiScrollFrame")
+	_scrollHistory:SetLayout("List")
+	panelHistory:AddChild(_scrollHistory)
+end
+
+local function renderHistory()
+	if not _scrollHistory then return end
+	_scrollHistory:ReleaseChildren()
+	
+	-- history is list of objects with:
+	-- link, result, class, name
+	
+	local history = Amr.db.char.TeamOpt.History
+	local historyWidth = 260
+	
+	local emptyMsg = nil
+	if not IsInGroup() and not IsInRaid() then
+		emptyMsg = L.TeamHistoryNoGroup
+	elseif not history or #history == 0 then
+		emptyMsg = L.TeamHistoryEmpty
+	end
+	
+	if emptyMsg then
+		local panel = AceGUI:Create("AmrUiPanel")
+		panel:SetLayout("None")
+		panel:SetBackgroundColor(Amr.Colors.Black, 0)
+		panel:SetWidth(historyWidth)
+		panel:SetHeight(30)
+		_scrollHistory:AddChild(panel)
+		
+		local lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetWidth(historyWidth)
+		lbl:SetJustifyH("CENTER")
+		lbl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan))
+		lbl:SetText(emptyMsg)
+		lbl:SetPoint("LEFT", panel.content, "LEFT", 8, 0)
+		panel:AddChild(lbl)
+	else
+		for i = #history, 1, -1 do
+			local obj = history[i]
+			local itemLink = obj.link
+			
+			local panel = AceGUI:Create("AmrUiPanel")
+			panel:SetLayout("None")
+			panel:SetBackgroundColor(Amr.Colors.Black, 0)
+			panel:SetWidth(historyWidth)
+			panel:SetHeight(45)
+			_scrollHistory:AddChild(panel)
+			
+			local lbl = AceGUI:Create("AmrUiLabel")
+			lbl:SetWidth(historyWidth - 5)
+			lbl:SetWordWrap(false)
+			lbl:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.Text))
+			lbl:SetPoint("TOPLEFT", panel.content, "TOPLEFT", 5, -5)
+			panel:AddChild(lbl)
+			
+			Amr.GetItemInfo(itemLink, function(obj, name, link)					
+				-- set item name, tooltip
+				obj:SetText(link:gsub("%[", ""):gsub("%]", ""))
+				Amr:SetItemTooltip(obj, link, "ANCHOR_BOTTOMRIGHT", 0, obj.frame:GetHeight())					
+			end, lbl)
+
+			lbl = AceGUI:Create("AmrUiLabel")
+			lbl:SetWidth(historyWidth - 5)
+			lbl:SetWordWrap(false)
+			lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.White))
+			
+			if obj.result == "Disenchant" then
+				lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.TextHeaderDisabled))
+				lbl:SetText(L.TeamLootOptionDisenchant)
+			else
+				local color = obj.class and Amr.Colors.Classes[obj.class] or Amr.Colors.TextHeaderDisabled
+				lbl:SetText((obj.result == "??" and "" or L["TeamLootOption" .. obj.result] .. ": ") .."|c" .. Amr.ColorToHex(color, 1) .. obj.name .. "|r")
+			end
+			
+			lbl:SetPoint("BOTTOMLEFT", panel.content, "BOTTOMLEFT", 5, 8)			
+			panel:AddChild(lbl)
+			
+			local line = AceGUI:Create("AmrUiPanel")
+			line:SetBackgroundColor(Amr.Colors.Black, 1)
+			line:SetWidth(historyWidth)
+			line:SetHeight(1)
+			line:SetPoint("BOTTOM", panel.content, "BOTTOM")
+			panel:AddChild(line)
+		end
+	end
+end
+
+local function onTabSelected(container, event, group)
+	container:ReleaseChildren()
+	
+	-- clear references to tab elements
+	_panelStartLoot = nil
+	_lblStartLoot = nil
+	_btnStartLoot = nil
+	_scrollHistory = nil
+	
+	Amr.db.char.TeamOpt.Role = group
+	renderTab(group, container)
+	Amr:RefreshTeamUi()
+end
+
+-- renders the main UI for the Team Optimizer tab
+function Amr:RenderTabTeam(container)
+
+	-- splash screen to customize team optimizer ui for the user
+	if not Amr.db.char.TeamOpt.Role then
+		_panelSplash = AceGUI:Create("AmrUiPanel")
+		_panelSplash:SetLayout("None")
+		_panelSplash:SetBackgroundColor(Amr.Colors.Black, 0)
+		_panelSplash:SetPoint("TOPLEFT", container.content, "TOPLEFT")
+		_panelSplash:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT")
+		container:AddChild(_panelSplash)
+	
+		local lblSplash = AceGUI:Create("AmrUiLabel")
+		lblSplash:SetWidth(800)
+		lblSplash:SetJustifyH("CENTER")
+		lblSplash:SetFont(Amr.CreateFont("Regular", 24, Amr.Colors.Text))
+		lblSplash:SetText(L.TeamSplashHeader)
+		lblSplash:SetPoint("TOP", _panelSplash.content, "TOP", 0, -40)
+		_panelSplash:AddChild(lblSplash)
+		
+		local btn = AceGUI:Create("AmrUiButton")
+		btn:SetText(L.TeamTabLeaderText)
+		btn:SetBackgroundColor(Amr.Colors.Orange)
+		btn:SetFont(Amr.CreateFont("Bold", 24, Amr.Colors.White))
+		btn:SetWidth(280)
+		btn:SetHeight(60)
+		btn:SetPoint("TOPRIGHT", lblSplash.frame, "BOTTOM", -50, -50)
+		btn:SetCallback("OnClick", function(widget)
+			Amr.db.char.TeamOpt.Role = "Leader"
+			_panelSplash:SetVisible(false)
+			_tabs:SetVisible(true)
+			_tabs:SelectTab("Leader")
+		end)
+		_panelSplash:AddChild(btn)
+		
+		local lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetWidth(280)
+		lbl:SetJustifyH("CENTER")
+		lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamSplashLeaderLabel)
+		lbl:SetPoint("TOP", btn.frame, "BOTTOM", 0, -20)
+		_panelSplash:AddChild(lbl)
+		
+		btn = AceGUI:Create("AmrUiButton")
+		btn:SetText(L.TeamTabMemberText)
+		btn:SetBackgroundColor(Amr.Colors.Orange)
+		btn:SetFont(Amr.CreateFont("Bold", 24, Amr.Colors.White))
+		btn:SetWidth(280)
+		btn:SetHeight(60)
+		btn:SetPoint("TOPLEFT", lblSplash.frame, "BOTTOM", 50, -50)
+		btn:SetCallback("OnClick", function(widget)
+			Amr.db.char.TeamOpt.Role = "Member"
+			_panelSplash:SetVisible(false)
+			_tabs:SetVisible(true)
+			_tabs:SelectTab("Member")
+		end)
+		_panelSplash:AddChild(btn)
+		
+		lbl = AceGUI:Create("AmrUiLabel")
+		lbl:SetWidth(280)
+		lbl:SetJustifyH("CENTER")
+		lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan))
+		lbl:SetText(L.TeamSplashMemberLabel)
+		lbl:SetPoint("TOP", btn.frame, "BOTTOM", 0, -20)
+		_panelSplash:AddChild(lbl)
+	end
+	
+	-- tabstrip
+	_tabs =  AceGUI:Create("AmrUiTabGroup")
+	_tabs:SetLayout("None")
+	_tabs:SetTabs({
+		{text=L.TeamTabLeaderText, value="Leader", style="bold"}, 
+		{text=L.TeamTabMemberText, value="Member", style="bold"}
+	})
+	_tabs:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, -30)
+	_tabs:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT")
+	_tabs:SetCallback("OnGroupSelected", onTabSelected)
+	container:AddChild(_tabs)
+	
+	local role = Amr.db.char.TeamOpt.Role
+	
+	_tabs:SetVisible(not not role)
+	if role then
+		-- if a role has been chosen, select the proper tab (which will also refresh the UI)
+		_tabs:SelectTab(role)
+	else
+		-- no role, refresh the UI manually
+		self:RefreshTeamUi()
+	end
+	
+end
+
+function Amr:ReleaseTabTeam()
+	_panelSplash = nil
+	_panelStartLoot = nil
+	_lblStartLoot = nil
+	_btnStartLoot = nil
+	_scrollHistory = nil
+	_tabs = nil
+end
+
+function Amr:RefreshTeamUi()
+	
+	-- if rankings have been loaded, render the 'start loot' panel
+	if _panelStartLoot then
+		local rankString = Amr.db.global.TeamOpt.RankingString
+		if rankString then
+			_panelStartLoot:SetVisible(true)
+			_lblStartLoot:SetText(L.TeamStartLootLabel(#Amr.db.global.TeamOpt.Rankings))			
+			_btnStartLoot:SetText(Amr.db.char.TeamOpt.LootInProgress and L.TeamButtonResumeLootText or L.TeamButtonStartLootText)
+		else
+			_panelStartLoot:SetVisible(false)
+		end
+	end
+	
+	-- render loot history
+	renderHistory()
+end
+
+local function getItemIdsFromLinks(all, list)
+	for i, v in ipairs(list) do
+		local obj = Amr.ParseItemLink(v)
+		local id = Amr.GetItemUniqueId(obj)
+		if id then
+			table.insert(all, id)
+		end
+	end
+end
+
+-- update AllItems, used to determine when a new item is actually a new equippable item
+local function snapshotAllItems(data)
+
+	local all = {}
+	for k, v in pairs(data.Equipped[data.ActiveSpec]) do
+		local obj = Amr.ParseItemLink(v)
+		local id = Amr.GetItemUniqueId(obj)
+		if id then
+			table.insert(all, id)
+		end
+	end
+	getItemIdsFromLinks(all, data.BagItems)
+	getItemIdsFromLinks(all, data.BankItems)
+	getItemIdsFromLinks(all, data.VoidItems)
+	
+	table.sort(all)
+	return all
+end
+
+local function sendGear(prefix, empty)
+
+	local region = Amr.RegionNames[GetCurrentRegion()]
+    local realm = GetRealmName()
+    local name = UnitName("player")
+	
+	-- get all data, including inventory
+	local txt = "_"
+	if not empty then
+		local data = Amr:ExportCharacter()
+		txt = Amr.Serializer:SerializePlayerData(data, true)
+
+		-- snapshot items when gear is sent
+		Amr.db.char.TeamOpt.AllItems = snapshotAllItems(data)	
+	end
+	
+	local msg = string.format("%s\n%s\n%s\n%s\n%s", prefix, region, realm, name, txt)
+	Amr:SendAmrCommMessage(msg)
+end
+
+local function toPlayerKey(realm, name)
+	return name .. "-" .. realm
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Loot Export
+------------------------------------------------------------------------------------------------
+
+-- prune out any characters no longer in the player's group
+local function pruneGearForItemExport()
+
+	local newInfo = {}
+	local units = Amr:GetGroupUnitIdentifiers()
+	
+    for i, unitId in ipairs(units) do
+		local realm, name = Amr:GetRealmAndName(unitId)	
+		if realm then
+			local key = toPlayerKey(realm, name)
+			newInfo[key] = Amr.db.global.TeamOpt.LootGear[key]
+		end
+    end
+	
+	Amr.db.global.TeamOpt.LootGear = newInfo	
+end
+
+local function scanMasterLoot()
+	-- only care if we are in a raid or group
+	if not IsInGroup() and not IsInRaid() then return end
+	
+	-- we only care about the master looter
+	if not IsMasterLooter() then return end
+
+	-- guid of the unit being looted
+	local npcGuid = UnitGUID("target")
+	if not npcGuid then
+		-- this could wack shit out... but no raid bosses drop loot from containers right now, so should be fine
+		npcGuid = "container"
+	end
+
+	-- if we already have loot data for this unit, then we can ignore
+	if Amr.db.char.TeamOpt.LootGuid == npcGuid then return end
+	
+	local loot = {}
+	for i = 1, GetNumLootItems() do
+		--local texture, item, quantity, quality, locked = GetLootSlotInfo(i)
+		local lootType = GetLootSlotType(i)
+		if lootType == 1 then
+			local link = GetLootSlotLink(i)
+			table.insert(loot, link)
+		end
+	end
+	
+	Amr.db.char.TeamOpt.LootGuid = npcGuid
+	Amr.db.char.TeamOpt.Loot = loot
+	
+	-- publish loot information to everyone else running the addon in case team optimizer user is not the master looter
+	local msg = _messagePrefixes.ItemExportLoot .. "\n" .. npcGuid .. "\n" .. table.concat(loot, "\n")	
+	Amr:SendAmrCommMessage(msg)
+end
+
+local function onLootReceived(parts)
+
+	Amr.db.char.TeamOpt.LootGuid = parts[2]
+	
+	local loot = {}
+	for i = 3, #parts do
+		table.insert(loot, parts[i])
+	end
+	Amr.db.char.TeamOpt.Loot = loot
+end
+
+local function onLeaveGroup()
+	-- if the current player is no longer in a group or raid, finish any looting in progress
+	Amr:FinishLoot(true)
+	
+	-- clear loot when leave a group
+	Amr.db.char.TeamOpt.Loot = {}
+	Amr.db.char.TeamOpt.LootGuid = nil
+	Amr.db.global.TeamOpt.LootGear = {}
+end
+
+local function onGroupChanged()
+
+	if not IsInGroup() and not IsInRaid() then
+		onLeaveGroup()
+	end
+end
+
+
+local _lootExPlayersRemaining = 0
+local _lootExRoster = nil
+local _lootExCallback = nil
+
+local function serializeLootExport()
+	if not IsInGroup() and not IsInRaid() then return "NOGROUP" end
+	
+	local loot = Amr.db.char.TeamOpt.Loot
+	if not loot or #loot == 0 then return "NOLOOT" end
+
+	local itemObjects = {}
+	for i, link in ipairs(loot) do
+		local obj = Amr.ParseItemLink(link)
+		if obj then
+			table.insert(itemObjects, obj)
+		end
+	end
+	
+	-- DEBUG: just grab all currently equipped items
+	--[[
+	itemObjects = {}
+	local blah = Amr:ExportCharacter()
+	for k, v in pairs(blah.Equipped[blah.ActiveSpec]) do
+		local obj = Amr.ParseItemLink(v)
+		if obj then
+			table.insert(itemObjects, obj)
+		end
+	end
+	]]
+	
+	local parts = {}
+	
+	-- unique ids of items
+	local lootPart = {}
+	for i, obj in ipairs(itemObjects) do
+		table.insert(lootPart, Amr.GetItemUniqueId(obj))
+	end	
+	table.insert(parts, table.concat(lootPart, ";"))
+	
+	-- gear for players who have gained loot since the last item import or roster export
+	pruneGearForItemExport()
+	local lootGear = Amr.db.global.TeamOpt.LootGear
+	for k, v in pairs(lootGear) do
+		table.insert(parts, v)
+	end
+	
+	return table.concat(parts, "\n")
+end
+
+local function onLootExportCompleted()
+
+	-- fill in LootGear with just those players who have changed
+	Amr.db.global.TeamOpt.LootGear = _lootExRoster	
+	
+	if _lootExCallback then
+		local txt = serializeLootExport()
+		_lootExCallback(txt)
+	end
+	
+	-- reset state
+	_lootExPlayersRemaining = 0
+	_lootExRoster = nil
+	_lootExCallback = nil	
+end
+
+-- called when this player's gear info has been requested by someone exporting loot
+local function onGearForLootExportRequested()
+
+	local hasNewItem = false
+	local oldItems = Amr.db.char.TeamOpt.AllItems
+	
+	if oldItems and #oldItems > 0 then
+		-- see if any new equippable items have been gained by comparing to the last snapshot
+		local data = Amr:ExportCharacter()
+		local allItems = snapshotAllItems(data)
+		
+		if #oldItems ~= #allItems then
+			hasNewItem = true
+		else
+			-- go through items from front to back, if there are any that don't match then something has changed
+			for i = 1, #allItems do
+				local oldItem = oldItems[i]
+				local newItem = allItems[i]
+				if oldItem ~= newItem then
+					hasNewItem = true
+					break
+				end
+			end
+		end
+	end
+	
+	-- whenever a new item is received, send out updated gear information that should be added to the next item export
+	sendGear(_messagePrefixes.ItemExportGear, not hasNewItem)
+end
+
+local function onGearForLootExportReceived(region, realm, name, data)
+	-- if I am not listening for incoming gear data for an item export, then ignore this message
+	if _lootExPlayersRemaining == 0 then return end
+	
+	local key = toPlayerKey(realm, name)
+	if not data or data == "_" then
+		_lootExRoster[key] = nil
+	else
+		_lootExRoster[key] = data
+	end
+	
+	_lootExPlayersRemaining = _lootExPlayersRemaining - 1
+	if _lootExPlayersRemaining <= 0 then
+		onLootExportCompleted()
+	end
+end
+
+-- Export the current loot, including any known gear data for players with the in-game addon, but only if it has changed since the last snapshot.
+-- This is asynchronous because it needs to wait for gear data to arrive from each player.
+function Amr:ExportLootAsync(callback)
+
+	if not IsInGroup() and not IsInRaid() then 
+		callback("NOGROUP")
+	end
+	
+	local loot = Amr.db.char.TeamOpt.Loot
+	if not loot or #loot == 0 then 
+		callback("NOLOOT")
+	end
+
+	local playersNoGear = {}
+	_lootExPlayersRemaining = 0
+	
+	local units = self:GetGroupUnitIdentifiers()
+	for i, unitId in ipairs(units) do
+		local realm, name = self:GetRealmAndName(unitId)
+		if realm then
+			local ver = self:GetAddonVersion(realm, name)
+			local key = toPlayerKey(realm, name)
+			
+			if ver >= Amr.MIN_ADDON_VERSION then
+				_lootExPlayersRemaining = _lootExPlayersRemaining + 1
+			else
+				table.insert(playersNoGear, unitId)
+			end
+		end
+	end
+	
+	_lootExRoster = {}	
+	_lootExCallback = callback
+	
+	if _lootExPlayersRemaining > 0 then
+		-- send a message to receive player data, when the last player is received onLootExportCompleted will be called
+		Amr:SendAmrCommMessage(_messagePrefixes.ItemExportRequestGear)
+	else
+		-- don't need to wait for anybody, just call immediately
+		onLootExportCompleted()
+	end
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Roster Export
+------------------------------------------------------------------------------------------------
+
+local _rosterPlayersRemaining = 0
+local _roster = nil
+local _rosterCallback = nil
+
+local function onRosterCompleted()
+
+	if _rosterCallback then
+		-- serialize the roster
+		local parts = {}
+		for key, data in pairs(_roster) do
+			table.insert(parts, data)
+		end
+		local msg = table.concat(parts, "\n")	
+	
+		-- send to callback
+		_rosterCallback(msg)
+	end
+	
+	-- reset state
+	_rosterPlayersRemaining = 0
+	_roster = nil
+	_rosterCallback = nil
+	
+	-- clear out loot gear needed, an export will refresh everyone at the time of export
+	Amr.db.global.TeamOpt.LootGear = {}
+end
+
+-- called when this player's gear info has been requested by someone exporting the raid roster
+local function onGearForRosterRequested()
+
+	sendGear(_messagePrefixes.RosterGear)
+end
+
+local function onGearForRosterReceived(region, realm, name, data)
+	-- if I am not listening for incoming gear data for the roster, then ignore this message
+	if _rosterPlayersRemaining == 0 then return end
+	
+	local key = toPlayerKey(realm, name)
+	_roster[key] = data
+	
+	_rosterPlayersRemaining = _rosterPlayersRemaining - 1
+	if _rosterPlayersRemaining <= 0 then
+		onRosterCompleted()
+	end
+end
+
+-- Export the current roster, including any known gear data for players with the in-game addon.
+-- This is asynchronous because it needs to wait for gear data to arrive from each player.
+function Amr:ExportRosterAsync(callback)
+	if not IsInGroup() and not IsInRaid() then 
+		callback()
+		return 
+	end
+	
+	local playersNoGear = {}
+	_rosterPlayersRemaining = 0
+	
+	local units = self:GetGroupUnitIdentifiers()
+	for i, unitId in ipairs(units) do
+		local realm, name = self:GetRealmAndName(unitId)
+		if realm then
+			local ver = self:GetAddonVersion(realm, name)
+			local key = toPlayerKey(realm, name)
+			
+			if ver >= Amr.MIN_ADDON_VERSION then
+				_rosterPlayersRemaining = _rosterPlayersRemaining + 1
+			else
+				table.insert(playersNoGear, unitId)
+			end
+		end
+	end
+	
+	-- fill the roster with any players who can't send us data
+	_roster = {}
+	for i, unitId in ipairs(playersNoGear) do
+		local realm, name = self:GetRealmAndName(unitId)
+		if realm then
+			local key = toPlayerKey(realm, name)
+			local obj = {
+				Region = Amr.RegionNames[GetCurrentRegion()],
+				Realm = realm,
+				Name = name
+			}
+			_roster[key] = Amr.Serializer:SerializePlayerIdentity(obj)
+		end
+	end
+	
+	_rosterCallback = callback
+	
+	if _rosterPlayersRemaining > 0 then
+		-- send a message to receive player data, when the last player is received onRosterCompleted will be called
+		Amr:SendAmrCommMessage(_messagePrefixes.RosterRequestGear)
+	else
+		-- don't need to wait for anybody, just call immediately
+		onRosterCompleted()
+	end
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Ranking Import
+------------------------------------------------------------------------------------------------
+
+-- helper to parse import item identifier format into an item object
+local function parseItemIdentifier(ident)
+
+	local parts = { strsplit(":", ident) }
+	local item = {}
+	item.id = tonumber(parts[1])
+	item.enchantId = 0
+	item.gemIds = { 0, 0, 0, 0 }
+	item.suffixId = math.abs(tonumber(parts[2]))
+	item.upgradeId = tonumber(parts[3])
+	
+	if #parts > 3 then
+		item.bonusIds = {}
+		for b = 4, #parts do
+			table.insert(item.bonusIds, tonumber(parts[b]))
+		end
+		table.sort(item.bonusIds)
+	end
+	
+	return item
+end
+
+function Amr:ParseRankingString(data)
+	local rankings = {}
+	
+	local player = Amr:ExportCharacter()
+	local myUnitId = Amr:GetUnitId(player.Realm, player.Name)
+	local ml = IsMasterLooter()
+	
+	local itemList = { strsplit("\n", data) }	
+	for i = 1, #itemList do
+		local ranking = {}
+		
+		local itemParts = { strsplit("_", itemList[i]) }
+		
+		-- first part has the item identifier
+		ranking.item = parseItemIdentifier(itemParts[1])
+
+		-- second part has item info
+		local infoParts = { strsplit(";", itemParts[2]) }
+		ranking.itemInfo = {
+			slot = infoParts[1],
+			subclass = infoParts[2],
+			weaponType = infoParts[3],
+			armorType = infoParts[4]
+		}
+		
+		local meInList = false
+		
+		-- parse each ranking
+		ranking.ranks = {}
+		for j = 3, #itemParts do
+			local rankParts = { strsplit(";", itemParts[j]) }
+			
+			local rank = {}
+			rank.realm = rankParts[1]
+			rank.name = rankParts[2]
+			rank.specId = tonumber(rankParts[3])
+			if rankParts[4] ~= "--" then
+				rank.equipped = parseItemIdentifier(rankParts[4])
+			end
+			rank.score = tonumber(rankParts[5])
+			rank.isEquipped = rankParts[6] == "t"
+			rank.notRanked = rankParts[7] == "t"
+			rank.offspec = rankParts[8] == "t"
+			rank.enchantingSkill = tonumber(rankParts[9])
+			
+			table.insert(ranking.ranks, rank)
+			
+			if myUnitId == Amr:GetUnitId(rank.realm, rank.name) then
+				meInList = true
+				rank.isMasterLooter = ml
+			end
+		end
+		
+		-- if the current player is the master looter and he is not in the list, then add him at the end
+		if ml and not meInList then
+			local rank = {
+				realm = player.Realm,
+				name = player.Name,
+				specId = player.Specs[player.ActiveSpec],
+				equipped = "--",
+				score = 0,
+				isEquipped = false,
+				notRanked = true,
+				offspec = false,
+				enchantingSkill = 0,
+				isMasterLooter = true
+			}
+			table.insert(ranking.ranks, rank)
+		end
+		
+		table.insert(rankings, ranking)
+	end
+	
+	return rankings
+end
+
+-- import rankings from the website, save into the database, returns a string error if can't import for some reason
+function Amr:ImportRankings(data)
+
+	if not data or string.len(data) == 0 then
+        return L.ImportErrorEmpty
+    end
+	
+	local success, rankings = pcall(Amr.ParseRankingString, self, data)
+
+	if not success then
+		return L.ImportErrorFormat
+	end
+	
+	-- finish any looting in progress, effectively canceling it, user will have to press Start Loot again
+	Amr:FinishLoot()
+	
+	-- save the rankings
+	Amr.db.global.TeamOpt.Rankings = rankings
+	Amr.db.global.TeamOpt.RankingString = data
+	
+	-- clear loot gear needed on successful ranking import
+	Amr.db.global.TeamOpt.LootGear = {}
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Loot Distribution
+------------------------------------------------------------------------------------------------
+
+function Amr:StartLoot()
+	
+	if not IsInGroup() and not IsInRaid() then
+		Amr:ShowAlert(L.TeamAlertNoGroup, L.AlertOk)
+		return
+	end
+	
+	-- broadcast the loot data to everyone, this triggers the loot window to show
+	local msg = string.format("%s\n%s", Amr.LootMessagePrefixes.Start, Amr.db.global.TeamOpt.RankingString)
+	Amr:SendAmrCommMessage(msg)
+end
+
+function Amr:FinishLoot(clearHistory)
+
+	-- reset all state
+	Amr.db.char.TeamOpt.LootInProgress = false
+	
+	-- don't reset these for now... only reset these if someone leaves a group
+	--Amr.db.char.TeamOpt.Loot = {}
+	--Amr.db.char.TeamOpt.LootGuid = nil
+	--Amr.db.global.TeamOpt.LootGear = {}
+	
+	Amr.db.global.TeamOpt.Rankings = {}
+	Amr.db.global.TeamOpt.RankingString = nil
+		
+	Amr.db.char.TeamOpt.Rolls = {}
+	
+	if clearHistory then
+		Amr.db.char.TeamOpt.History = {}
+	end
+	
+	-- close the loot window
+	Amr:HideLootWindow()
+	
+	-- re-render the team optimizer UI
+	Amr:RefreshTeamUi()
+end
+
+
+------------------------------------------------------------------------------------------------
+-- Synchronization
+------------------------------------------------------------------------------------------------
+local _waitingForSync = false
+
+-- check if we need to synchronize
+local function checkSync()
+	-- if loot is in progress and this person is not the ML, send a request to synchronize on startup, this player may have missed some data
+	if not IsMasterLooter() and Amr.db.char.TeamOpt.LootInProgress then
+		_waitingForSync = true
+		Amr:SendAmrCommMessage(_messagePrefixes.SyncRequest)
+	end
+end
+
+-- send data to anyone who needs to synchronize their loot data: history, rolls, rankings
+local function sendSyncData()
+	-- only the master looter sends sync data to ensure that everyone gets the same stuff and we don't spam
+	if not IsMasterLooter() then return end
+	
+	local msgParts = {}
+	table.insert(msgParts, _messagePrefixes.Sync)
+	table.insert(msgParts, Amr:Serialize(Amr.db.char.TeamOpt.History))
+	table.insert(msgParts, Amr:Serialize(Amr.db.char.TeamOpt.Rolls))
+	table.insert(msgParts, Amr:Serialize(Amr.db.global.TeamOpt.Rankings))
+	
+	Amr:SendAmrCommMessage(table.concat(msgParts, "\n"))
+end
+
+local function receiveSyncData(parts)
+	if not _waitingForSync then return end	
+	_waitingForSync = false
+	
+	local success, obj = Amr:Deserialize(parts[2])
+	if success then
+		Amr.db.char.TeamOpt.History = obj
+	end
+	
+	success, obj = Amr:Deserialize(parts[3])
+	if success then
+		Amr.db.char.TeamOpt.Rolls = obj
+	end
+	
+	success, obj = Amr:Deserialize(parts[4])
+	if success then
+		Amr.db.global.TeamOpt.Rankings = obj
+	end
+	
+	-- refresh any windows that may be visible
+	Amr:RefreshTeamUi()
+	Amr:RefreshLootWindow()
+	Amr:RefreshLootRolls()
+end
+
+
+function Amr:ProcessTeamMessage(message)
+
+    local parts = {}
+	for part in string.gmatch(message, "([^\n]+)") do
+		table.insert(parts, part)
+	end
+    
+    local prefix = parts[1]
+	
+	if prefix == _messagePrefixes.RosterRequestGear then
+		-- request for me to send my gear data
+		onGearForRosterRequested()
+	elseif prefix == _messagePrefixes.ItemExportRequestGear then
+		-- request for me to send my gear data
+		onGearForLootExportRequested()
+	elseif prefix == _messagePrefixes.ItemExportLoot then
+		-- the last loot that dropped
+		onLootReceived(parts)
+	elseif prefix == _messagePrefixes.SyncRequest then
+		sendSyncData()
+	elseif prefix == _messagePrefixes.Sync then
+		receiveSyncData(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Start then
+		Amr:OnStartLootReceived(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Roll then
+		Amr:OnLootRollReceived(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Veto then
+		Amr:OnLootVetoReceived(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Rand then
+		Amr:OnLootRandReceived(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Give then
+		Amr:OnLootGiveReceived(parts)
+	elseif prefix == Amr.LootMessagePrefixes.Finish then
+		Amr:FinishLoot()
+	else
+		-- message will be of format: prefix\nregion\nrealm\nname\n[stuff]	
+		local region = parts[2]
+		local realm = parts[3]
+		local name = parts[4]
+		local data = parts[5]
+
+		if prefix == _messagePrefixes.RosterGear then
+			-- receive gear data from someone
+			onGearForRosterReceived(region, realm, name, data)
+		elseif prefix == _messagePrefixes.ItemExportGear then
+			-- receive gear data for item export
+			onGearForLootExportReceived(region, realm, name, data)
+		end
+	end
+end
+
+function Amr:InitializeTeamOpt()
+
+	if not IsInGroup() and not IsInRaid() then
+		onLeaveGroup()
+	end
+	
+	Amr:AddEventHandler("LOOT_OPENED", scanMasterLoot)
+	Amr:AddEventHandler("GROUP_ROSTER_UPDATE", onGroupChanged)
+	
+	checkSync()
+end