diff AskMrRobot-Serializer/AskMrRobot-Serializer.lua @ 57:01b63b8ed811 v21

total rewrite to version 21
author yellowfive
date Fri, 05 Jun 2015 11:05:15 -0700
parents
children ee701ce45354
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AskMrRobot-Serializer/AskMrRobot-Serializer.lua	Fri Jun 05 11:05:15 2015 -0700
@@ -0,0 +1,1061 @@
+-- AskMrRobot-Serializer will serialize and communicate character data between users.
+-- This is used primarily to associate character information to logs uploaded to askmrrobot.com.
+
+local MAJOR, MINOR = "AskMrRobot-Serializer", 21
+local Amr, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not Amr then return end -- already loaded by something else
+
+-- event and comm used for player snapshotting on entering combat
+LibStub("AceEvent-3.0"):Embed(Amr)
+LibStub("AceComm-3.0"):Embed(Amr)
+
+----------------------------------------------------------------------------------------
+-- Constants
+----------------------------------------------------------------------------------------
+
+-- prefix used for communicating gear snapshots created by the AMR serializer
+Amr.ChatPrefix = "_AMRS"
+
+-- map of region ids to AMR region names
+Amr.RegionNames = {
+	[1] = "US",
+	[2] = "KR",
+	[3] = "EU",
+	[4] = "TW",
+	[5] = "CN"
+}
+
+-- map of the skillLine returned by profession API to the AMR profession name
+Amr.ProfessionSkillLineToName = {
+	[794] = "Archaeology",
+	[171] = "Alchemy",
+	[164] = "Blacksmithing",
+	[185] = "Cooking",
+	[333] = "Enchanting",
+	[202] = "Engineering",
+	[129] = "First Aid",
+	[356] = "Fishing",
+	[182] = "Herbalism",
+	[773] = "Inscription",
+	[755] = "Jewelcrafting",
+	[165] = "Leatherworking",
+	[186] = "Mining",
+	[393] = "Skinning",
+	[197] = "Tailoring"
+}
+
+-- all slot IDs that we care about, ordered in AMR standard display order
+Amr.SlotIds = { 16, 17, 1, 2, 3, 15, 5, 9, 10, 6, 7, 8, 11, 12, 13, 14 }
+
+Amr.SpecIds = {
+    [250] = 1, -- DeathKnightBlood
+    [251] = 2, -- DeathKnightFrost
+    [252] = 3, -- DeathKnightUnholy
+    [102] = 4, -- DruidBalance
+    [103] = 5, -- DruidFeral
+    [104] = 6, -- DruidGuardian
+    [105] = 7, -- DruidRestoration
+    [253] = 8, -- HunterBeastMastery
+    [254] = 9, -- HunterMarksmanship
+    [255] = 10, -- HunterSurvival
+    [62] = 11, -- MageArcane
+    [63] = 12, -- MageFire
+    [64] = 13, -- MageFrost
+    [268] = 14, -- MonkBrewmaster
+    [270] = 15, -- MonkMistweaver
+    [269] = 16, -- MonkWindwalker
+    [65] = 17, -- PaladinHoly
+    [66] = 18, -- PaladinProtection
+    [70] = 19, -- PaladinRetribution
+    [256] = 20, -- PriestDiscipline
+    [257] = 21, -- PriestHoly
+    [258] = 22, -- PriestShadow
+    [259] = 23, -- RogueAssassination
+    [260] = 24, -- RogueCombat
+    [261] = 25, -- RogueSubtlety
+    [262] = 26, -- ShamanElemental
+    [263] = 27, -- ShamanEnhancement
+    [264] = 28, -- ShamanRestoration
+    [265] = 29, -- WarlockAffliction
+    [266] = 30, -- WarlockDemonology
+    [267] = 31, -- WarlockDestruction
+    [71] = 32, -- WarriorArms
+    [72] = 33, -- WarriorFury
+    [73] = 34 -- WarriorProtection
+}
+
+Amr.ClassIds = {
+    ["NONE"] = 0,
+    ["DEATHKNIGHT"] = 1,
+    ["DRUID"] = 2,
+    ["HUNTER"] = 3,
+    ["MAGE"] = 4,
+    ["MONK"] = 5,
+    ["PALADIN"] = 6,
+    ["PRIEST"] = 7,
+    ["ROGUE"] = 8,
+    ["SHAMAN"] = 9,
+    ["WARLOCK"] = 10,
+    ["WARRIOR"] = 11,
+}
+
+Amr.ProfessionIds = {
+    ["None"] = 0,
+    ["Mining"] = 1,
+    ["Skinning"] = 2,
+    ["Herbalism"] = 3,
+    ["Enchanting"] = 4,
+    ["Jewelcrafting"] = 5,
+    ["Engineering"] = 6,
+    ["Blacksmithing"] = 7,
+    ["Leatherworking"] = 8,
+    ["Inscription"] = 9,
+    ["Tailoring"] = 10,
+    ["Alchemy"] = 11,
+    ["Fishing"] = 12,
+    ["Cooking"] = 13,
+    ["First Aid"] = 14,
+    ["Archaeology"] = 15
+}
+
+Amr.RaceIds = {
+    ["None"] = 0,
+    ["BloodElf"] = 1,
+    ["Draenei"] = 2,
+    ["Dwarf"] = 3,
+    ["Gnome"] = 4,
+    ["Human"] = 5,
+    ["NightElf"] = 6,
+    ["Orc"] = 7,
+    ["Tauren"] = 8,
+    ["Troll"] = 9,
+    ["Scourge"] = 10,
+    ["Undead"] = 10,
+    ["Goblin"] = 11,
+    ["Worgen"] = 12,
+    ["Pandaren"] = 13
+}
+
+Amr.FactionIds = {
+    ["None"] = 0,
+    ["Alliance"] = 1,
+    ["Horde"] = 2
+}
+
+Amr.InstanceIds = {
+	Auchindoun = 1182,
+	BloodmaulSlagMines = 1175,
+	GrimrailDepot = 1208,
+	IronDocks = 1195,
+	ShadowmoonBurialGrounds = 1176,
+	Skyreach = 1209,
+	TheEverbloom = 1279,
+	UpperBlackrockSpire = 1358,
+	Highmaul = 1228,
+	BlackrockFoundry = 1205
+}
+
+-- instances that AskMrRobot currently supports logging for
+Amr.SupportedInstanceIds = {
+	--[1182] = true,
+	--[1175] = true,
+	--[1208] = true,
+	--[1195] = true,
+	--[1176] = true,
+	--[1209] = true,
+	--[1279] = true,
+	--[1358] = true,
+	[1228] = true,
+	[1205] = true
+}
+
+Amr.SPEC_WARRIORPROTECTION = 34
+Amr.SUBSPEC_WARRIORPROTECTION = 38
+Amr.SUBSPEC_WARRIORPROTECTIONGLAD = 39
+Amr.SPELL_ID_GLADIATOR_STANCE = 156291
+Amr.SPELL_ID_DEFENSIVE_STANCE = 71
+
+-- IDs of set tokens that we would care about in a player's inventory
+Amr.SetTokenIds = {
+	[120285] = true,
+	[120284] = true,
+	[120283] = true,
+	[120282] = true,
+	[120281] = true,
+	[120280] = true,
+	[120279] = true,
+	[120278] = true,
+	[120277] = true,
+	[120256] = true,
+	[120255] = true,
+	[120254] = true,
+	[120253] = true,
+	[120252] = true,
+	[120251] = true,
+	[120250] = true,
+	[120249] = true,
+	[120248] = true,
+	[120247] = true,
+	[120246] = true,
+	[120245] = true,
+	[120244] = true,
+	[120243] = true,
+	[120242] = true,
+	[120241] = true,
+	[120240] = true,
+	[120239] = true,
+	[120238] = true,
+	[120237] = true,
+	[120236] = true,
+	[120235] = true,
+	[120234] = true,
+	[120233] = true,
+	[120232] = true,
+	[120231] = true,
+	[120230] = true,
+	[120229] = true,
+	[120228] = true,
+	[120227] = true,
+	[120226] = true,
+	[120225] = true,
+	[120224] = true,
+	[120223] = true,
+	[120222] = true,
+	[120221] = true,
+	[120220] = true,
+	[120219] = true,
+	[120218] = true,
+	[120217] = true,
+	[120216] = true,
+	[120215] = true,
+	[120214] = true,
+	[120213] = true,
+	[120212] = true,
+	[120211] = true,
+	[120210] = true,
+	[120209] = true,
+	[120208] = true,
+	[120207] = true,
+	[120206] = true,
+	[119323] = true,
+	[119322] = true,
+	[119321] = true,
+	[119320] = true,
+	[119319] = true,
+	[119318] = true,
+	[119316] = true,
+	[119315] = true,
+	[119314] = true,
+	[119313] = true,
+	[119312] = true,
+	[119311] = true,
+	[119310] = true,
+	[119309] = true,
+	[119308] = true,
+	[119307] = true,
+	[119306] = true,
+	[119305] = true,
+	[105868] = true,
+	[105867] = true,
+	[105866] = true,
+	[105865] = true,
+	[105864] = true,
+	[105863] = true,
+	[105862] = true,
+	[105861] = true,
+	[105860] = true,
+	[105859] = true,
+	[105858] = true,
+	[105857] = true,
+	[99756] = true,
+	[99755] = true,
+	[99754] = true,
+	[99753] = true,
+	[99752] = true,
+	[99751] = true,
+	[99750] = true,
+	[99749] = true,
+	[99748] = true,
+	[99747] = true,
+	[99746] = true,
+	[99745] = true,
+	[99744] = true,
+	[99743] = true,
+	[99742] = true,
+	[99740] = true,
+	[99739] = true,
+	[99738] = true,
+	[99737] = true,
+	[99736] = true,
+	[99735] = true,
+	[99734] = true,
+	[99733] = true,
+	[99732] = true,
+	[99731] = true,
+	[99730] = true,
+	[99729] = true,
+	[99728] = true,
+	[99727] = true,
+	[99726] = true,
+	[99725] = true,
+	[99724] = true,
+	[99723] = true,
+	[99722] = true,
+	[99721] = true,
+	[99720] = true,
+	[99719] = true,
+	[99718] = true,
+	[99717] = true,
+	[99716] = true,
+	[99715] = true,
+	[99714] = true,
+	[99713] = true,
+	[99712] = true,
+	[99711] = true,
+	[99710] = true,
+	[99709] = true,
+	[99708] = true,
+	[99707] = true,
+	[99706] = true,
+	[99705] = true,
+	[99704] = true,
+	[99703] = true,
+	[99702] = true,
+	[99701] = true,
+	[99700] = true,
+	[99699] = true,
+	[99698] = true,
+	[99697] = true,
+	[99696] = true,
+	[99695] = true,
+	[99694] = true,
+	[99693] = true,
+	[99692] = true,
+	[99691] = true,
+	[99690] = true,
+	[99689] = true,
+	[99688] = true,
+	[99687] = true,
+	[99686] = true,
+	[99685] = true,
+	[99684] = true,
+	[99683] = true,
+	[99682] = true,
+	[99681] = true,
+	[99680] = true,
+	[99679] = true,
+	[99678] = true,
+	[99677] = true,
+	[99676] = true,
+	[99675] = true,
+	[99674] = true,
+	[99673] = true,
+	[99672] = true,
+	[99671] = true,
+	[99670] = true,
+	[99669] = true,
+	[99668] = true,
+	[99667] = true,
+	[96701] = true,
+	[96700] = true,
+	[96699] = true,
+	[96633] = true,
+	[96632] = true,
+	[96631] = true,
+	[96625] = true,
+	[96624] = true,
+	[96623] = true,
+	[96601] = true,
+	[96600] = true,
+	[96599] = true,
+	[96568] = true,
+	[96567] = true,
+	[96566] = true,
+	[95957] = true,
+	[95956] = true,
+	[95955] = true,
+	[95889] = true,
+	[95888] = true,
+	[95887] = true,
+	[95881] = true,
+	[95880] = true,
+	[95879] = true,
+	[95857] = true,
+	[95856] = true,
+	[95855] = true,
+	[95824] = true,
+	[95823] = true,
+	[95822] = true,
+	[95583] = true,
+	[95582] = true,
+	[95581] = true,
+	[95580] = true,
+	[95579] = true,
+	[95578] = true,
+	[95577] = true,
+	[95576] = true,
+	[95575] = true,
+	[95574] = true,
+	[95573] = true,
+	[95572] = true,
+	[95571] = true,
+	[95570] = true,
+	[95569] = true,
+	[89278] = true,
+	[89277] = true,
+	[89276] = true,
+	[89275] = true,
+	[89274] = true,
+	[89273] = true,
+	[89272] = true,
+	[89271] = true,
+	[89270] = true,
+	[89269] = true,
+	[89268] = true,
+	[89267] = true,
+	[89266] = true,
+	[89265] = true,
+	[89264] = true,
+	[89263] = true,
+	[89262] = true,
+	[89261] = true,
+	[89260] = true,
+	[89259] = true,
+	[89258] = true,
+	[89257] = true,
+	[89256] = true,
+	[89255] = true,
+	[89254] = true,
+	[89253] = true,
+	[89252] = true,
+	[89251] = true,
+	[89250] = true,
+	[89249] = true,
+	[89248] = true,
+	[89247] = true,
+	[89246] = true,
+	[89245] = true,
+	[89244] = true,
+	[89243] = true,
+	[89242] = true,
+	[89241] = true,
+	[89240] = true,
+	[89239] = true,
+	[89238] = true,
+	[89237] = true,
+	[89236] = true,
+	[89235] = true,
+	[89234] = true,
+	[78876] = true,
+	[78875] = true,
+	[78874] = true,
+	[78873] = true,
+	[78872] = true,
+	[78871] = true,
+	[78867] = true,
+	[78866] = true,
+	[78865] = true,
+	[78864] = true,
+	[78863] = true,
+	[78862] = true,
+	[78861] = true,
+	[78860] = true,
+	[78859] = true,
+	[78858] = true,
+	[78857] = true,
+	[78856] = true,
+	[78855] = true,
+	[78854] = true,
+	[78853] = true,
+	[78849] = true,
+	[78848] = true,
+	[78847] = true,
+	[78184] = true,
+	[78183] = true,
+	[78181] = true,
+	[78180] = true,
+	[78179] = true,
+	[78178] = true,
+	[78176] = true,
+	[78175] = true,
+	[78174] = true,
+	[78173] = true,
+	[78171] = true,
+	[78170] = true,
+	[71687] = true,
+	[71686] = true,
+	[71685] = true,
+	[71683] = true,
+	[71682] = true,
+	[71680] = true,
+	[71679] = true,
+	[71678] = true,
+	[71676] = true,
+	[71675] = true,
+	[71673] = true,
+	[71672] = true,
+	[71671] = true,
+	[71669] = true,
+	[71668] = true,
+	[67431] = true,
+	[67430] = true,
+	[67429] = true,
+	[67428] = true,
+	[67427] = true,
+	[67426] = true,
+	[67425] = true,
+	[67424] = true,
+	[67423] = true,
+	[66998] = true,
+	[65089] = true,
+	[65088] = true,
+	[65087] = true,
+	[63684] = true,
+	[63683] = true,
+	[63682] = true,
+	[45652] = true,
+	[45651] = true,
+	[45650] = true,
+	[45649] = true,
+	[45648] = true,
+	[45647] = true,
+	[45643] = true,
+	[45642] = true,
+	[45641] = true,
+	[40630] = true,
+	[40629] = true,
+	[40628] = true,
+	[40621] = true,
+	[40620] = true,
+	[40619] = true,
+	[40618] = true,
+	[40617] = true,
+	[40616] = true,
+	[34544] = true,
+	[31100] = true,
+	[31099] = true,
+	[31098] = true,
+	[31097] = true,
+	[31096] = true,
+	[31095] = true,
+	[30247] = true,
+	[30246] = true,
+	[30245] = true,
+	[30244] = true,
+	[30243] = true,
+	[30242] = true,
+	[29767] = true,
+	[29766] = true,
+	[29765] = true,
+	[29761] = true,
+	[29760] = true,
+	[29759] = true
+}
+
+
+----------------------------------------------------------------------------------------
+-- Public Utility Methods
+----------------------------------------------------------------------------------------
+
+-- item link format:  |cffa335ee|Hitem:itemID:enchant:gem1:gem2:gem3:gem4:suffixID:uniqueID:level:upgradeId:instanceDifficultyID:numBonusIDs:bonusID1:bonusID2...|h[item name]|h|r
+-- get an object with all of the parts of the item link format that we care about
+function Amr.ParseItemLink(itemLink)
+    if not itemLink then return nil end
+    
+    local str = string.match(itemLink, "|Hitem:([\-%d:]+)|")
+    if not str then return nil end
+    
+    local parts = { strsplit(":", str) }
+    
+    local item = {}
+    item.id = tonumber(parts[1])
+    item.enchantId = tonumber(parts[2])
+    item.gemIds = { tonumber(parts[3]), tonumber(parts[4]), tonumber(parts[5]), tonumber(parts[6]) }
+    item.suffixId = math.abs(tonumber(parts[7])) -- convert suffix to positive number, that's what we use in our code
+    --item.uniqueId = tonumber(parts[8])
+    --item.level = tonumber(parts[9])
+    item.upgradeId = tonumber(parts[10])
+    --item.difficultyId = tonumber(parts[11])
+    
+    local numBonuses = tonumber(parts[12])
+    if numBonuses and numBonuses > 0 then
+        item.bonusIds = {}
+        for i = 13, 12 + numBonuses do
+            table.insert(item.bonusIds, tonumber(parts[i]))
+        end
+		table.sort(item.bonusIds)
+    end
+    
+    return item
+end
+
+-- returns true if this is an instance that AskMrRobot supports for logging
+function Amr.IsSupportedInstanceId(instanceMapID)
+	if Amr.SupportedInstanceIds[tonumber(instanceMapID)] then
+		return true
+	else
+		return false
+	end
+end
+
+-- returns true if currently in a supported instance for logging
+function Amr.IsSupportedInstance()
+	local zone, _, difficultyIndex, _, _, _, _, instanceMapID = GetInstanceInfo()
+	return Amr.IsSupportedInstanceId(instanceMapID)
+end
+
+
+----------------------------------------------------------------------------------------
+-- Character Reading
+----------------------------------------------------------------------------------------
+
+local function readProfessionInfo(prof, ret)
+	if prof then
+		local name, icon, skillLevel, maxSkillLevel, numAbilities, spelloffset, skillLine, skillModifier = GetProfessionInfo(prof);
+		if Amr.ProfessionSkillLineToName[skillLine] ~= nil then
+			ret.Professions[Amr.ProfessionSkillLineToName[skillLine]] = skillLevel;
+		end
+	end
+end
+
+local function getSpecId(specGroup)
+	local spec = GetSpecialization(false, false, specGroup);
+	return spec and GetSpecializationInfo(spec);
+end
+
+local function getTalents(specGroup)	
+    local talentInfo = {}
+    local maxTiers = 7
+    for tier = 1, maxTiers do
+        for col = 1, 3 do
+            local id, name, texture, selected, available = GetTalentInfo(tier, col, specGroup)
+            if selected then
+                talentInfo[tier] = col
+            end
+        end
+    end
+    
+    local str = ""
+    for i = 1, maxTiers do
+    	if talentInfo[i] then
+    		str = str .. talentInfo[i]
+    	else
+    		str = str .. '0'
+    	end
+    end    	
+
+	return str
+end
+
+local function getGlyphs(specGroup)
+	local glyphs = {}
+	for i = 1, NUM_GLYPH_SLOTS do
+		local _, _, _, glyphSpellID, _, glyphID = GetGlyphSocketInfo(i, specGroup)
+		if (glyphID) then
+			table.insert(glyphs, glyphSpellID)
+		end
+	end
+	return glyphs;
+end
+
+-- get specs, talents, and glyphs
+local function readSpecs(ret, subspecs)
+
+    for group = 1, GetNumSpecGroups() do
+        -- spec, convert game spec id to one of our spec ids
+        local specId = getSpecId(group)
+        if specId then
+            ret.Specs[group] = Amr.SpecIds[specId]
+			
+			-- if this is a protection warrior, use buffs to determine subspec
+			if ret.Specs[group] == Amr.SPEC_WARRIORPROTECTION then
+				local subspec = 0
+				
+				if ret.ActiveSpec ~= group then
+					-- this spec isn't active, so we can't use current buffs to determine spec, see if any old data is compatible
+					if subspecs and (subspecs[group] == Amr.SUBSPEC_WARRIORPROTECTION or subspecs[group] == Amr.SUBSPEC_WARRIORPROTECTIONGLAD) then
+						subspec = subspecs[group]
+					end
+				else
+					for i=1,40 do
+						local name,_,_,_,_,_,_,_,_,_,spellId = UnitAura("player", i, "HELPFUL")
+						if not name then break end
+						
+						if spellId == Amr.SPELL_ID_DEFENSIVE_STANCE then
+							subspec = Amr.SUBSPEC_WARRIORPROTECTION
+							break
+						elseif spellId == Amr.SPELL_ID_GLADIATOR_STANCE then
+							subspec = Amr.SUBSPEC_WARRIORPROTECTIONGLAD
+							break
+						end
+					end
+				end
+				
+				if subspec == 0 then
+					ret.SubSpecs[group] = nil
+				else
+					ret.SubSpecs[group] = subspec
+				end
+			end
+        else
+            ret.Specs[group] = 0
+        end
+        
+        ret.Talents[group] = getTalents(group)
+        ret.Glyphs[group] = getGlyphs(group)
+	end
+end
+
+-- get currently equipped items, store with currently active spec
+local function readEquippedItems(ret)
+    local equippedItems = {};
+	for slotNum = 1, #Amr.SlotIds do
+		local slotId = Amr.SlotIds[slotNum]
+		local itemLink = GetInventoryItemLink("player", slotId)
+		if itemLink then
+			equippedItems[slotId] = itemLink
+		end
+	end
+    
+    -- store last-seen equipped gear for each spec
+	ret.Equipped[GetActiveSpecGroup()] = equippedItems
+end
+
+-- Get all data about the player as an object, includes:
+-- serializer version
+-- region/realm/name
+-- guild
+-- race
+-- faction
+-- level
+-- professions
+-- spec/talent/glyphs for both specs
+-- equipped gear for the current spec
+--
+function Amr:GetPlayerData(subspecs)
+
+	local ret = {}
+	
+	ret.Region = Amr.RegionNames[GetCurrentRegion()]
+    ret.Realm = GetRealmName()
+    ret.Name = UnitName("player")
+	ret.Guild = GetGuildInfo("player")
+    ret.ActiveSpec = GetActiveSpecGroup()
+    ret.Level = UnitLevel("player");
+    
+    local cls, clsEn = UnitClass("player")
+    ret.Class = clsEn;
+    
+    local race, raceEn = UnitRace("player")
+	ret.Race = raceEn;
+	ret.Faction = UnitFactionGroup("player")
+    
+	ret.Professions = {};
+    local prof1, prof2, archaeology, fishing, cooking, firstAid = GetProfessions();
+	readProfessionInfo(prof1, ret)
+	readProfessionInfo(prof2, ret)
+	readProfessionInfo(archaeology, ret)
+	readProfessionInfo(fishing, ret)
+	readProfessionInfo(cooking, ret)
+	readProfessionInfo(firstAid, ret)
+	
+	ret.Specs = {}
+	ret.SubSpecs = {} -- only filled in for ambiguous cases, right now just prot/glad warrior
+    ret.Talents = {}
+    ret.Glyphs = {}
+	readSpecs(ret, subspecs)
+	
+	ret.Equipped = {}
+	readEquippedItems(ret)
+	
+	return ret
+end
+
+
+----------------------------------------------------------------------------------------
+-- Serialization
+----------------------------------------------------------------------------------------
+
+local function toCompressedNumberList(list)
+    -- ensure the values are numbers, sorted from lowest to highest
+    local nums = {}
+    for i, v in ipairs(list) do
+        table.insert(nums, tonumber(v))
+    end
+    table.sort(nums)
+    
+    local ret = {}
+    local prev = 0
+    for i, v in ipairs(nums) do
+        local diff = v - prev
+        table.insert(ret, diff)
+        prev = v
+    end
+    
+    return table.concat(ret, ",")
+end
+
+-- make this utility publicly available
+function Amr:ToCompressedNumberList(list)
+	return toCompressedNumberList(list)
+end
+
+-- appends a list of items to the export
+local function appendItemsToExport(fields, itemObjects)
+
+    -- sort by item id so we can compress it more easily
+    table.sort(itemObjects, function(a, b) return a.id < b.id end)
+    
+    -- append to the export string
+    local prevItemId = 0
+    local prevGemId = 0
+    local prevEnchantId = 0
+    local prevUpgradeId = 0
+    local prevBonusId = 0
+    for i, itemData in ipairs(itemObjects) do
+        local itemParts = {}
+        
+        table.insert(itemParts, itemData.id - prevItemId)
+        prevItemId = itemData.id
+        
+        if itemData.slot ~= nil then table.insert(itemParts, "s" .. itemData.slot) end
+        if itemData.suffixId ~= 0 then table.insert(itemParts, "f" .. itemData.suffixId) end
+        if itemData.upgradeId ~= 0 then 
+            table.insert(itemParts, "u" .. (itemData.upgradeId - prevUpgradeId))
+            prevUpgradeId = itemData.upgradeId
+        end
+        if itemData.bonusIds then
+            for bIndex, bValue in ipairs(itemData.bonusIds) do
+                table.insert(itemParts, "b" .. (bValue - prevBonusId))
+                prevBonusId = bValue
+            end
+        end        
+        if itemData.gemIds[1] ~= 0 then 
+            table.insert(itemParts, "x" .. (itemData.gemIds[1] - prevGemId))
+            prevGemId = itemData.gemIds[1]
+        end
+        if itemData.gemIds[2] ~= 0 then 
+            table.insert(itemParts, "y" .. (itemData.gemIds[2] - prevGemId))
+            prevGemId = itemData.gemIds[2]
+        end
+        if itemData.gemIds[3] ~= 0 then 
+            table.insert(itemParts, "z" .. (itemData.gemIds[3] - prevGemId))
+            prevGemId = itemData.gemIds[3]
+        end
+        if itemData.enchantId ~= 0 then 
+            table.insert(itemParts, "e" .. (itemData.enchantId - prevEnchantId))
+            prevEnchantId = itemData.enchantId
+        end
+    
+        table.insert(fields, table.concat(itemParts, ""))
+    end
+end
+
+-- Serialize just the identity portion of a player (region/realm/name) in the same format used by the full serialization
+function Amr:SerializePlayerIdentity(data)
+	local fields = {}    
+    table.insert(fields, MINOR)
+	table.insert(fields, data.Region)
+    table.insert(fields, data.Realm)
+    table.insert(fields, data.Name)	
+	return "$" .. table.concat(fields, ";") .. "$"
+end
+
+-- Serialize player data gathered by GetPlayerData.  This can be augmented with extra data if desired (augmenting used mainly by AskMrRobot addon).
+-- Pass complete = true to do a complete export of this extra information, otherwise it is ignored.
+-- Extra data can include:
+-- equipped gear for the player's inactive spec, slot id to item link dictionary
+-- Reputations
+-- BagItems, BankItems, VoidItems, lists of item links
+--
+function Amr:SerializePlayerData(data, complete)
+
+	local fields = {}
+    
+    -- compressed string uses a fixed order rather than inserting identifiers
+    table.insert(fields, MINOR)
+	table.insert(fields, data.Region)
+    table.insert(fields, data.Realm)
+    table.insert(fields, data.Name)
+
+	-- guild name
+	if data.Guild == nil then
+		table.insert(fields, "")
+	else
+		table.insert(fields, data.Guild)
+    end
+
+    -- race, default to pandaren if we can't read it for some reason
+    local raceval = Amr.RaceIds[data.Race]
+    if raceval == nil then raceval = 13 end
+    table.insert(fields, raceval)
+    
+    -- faction, default to alliance if we can't read it for some reason
+    raceval = Amr.FactionIds[data.Faction]
+    if raceval == nil then raceval = 1 end
+    table.insert(fields, raceval)
+    
+    table.insert(fields, data.Level)
+    
+    local profs = {}
+    local noprofs = true
+    if data.Professions then
+	    for k, v in pairs(data.Professions) do
+	        local profval = Amr.ProfessionIds[k]
+	        if profval ~= nil then
+	            noprofs = false
+	            table.insert(profs, profval .. ":" .. v)
+	        end
+	    end
+	end
+    
+    if noprofs then
+        table.insert(profs, "0:0")
+    end
+    
+    table.insert(fields, table.concat(profs, ","))
+    
+    -- export specs
+    table.insert(fields, data.ActiveSpec)
+    for spec = 1, 2 do
+        if data.Specs[spec] and (complete or spec == data.ActiveSpec) then
+            table.insert(fields, ".s" .. spec) -- indicates the start of a spec block
+			
+			-- we use subspec for some ambiguous specs like prot/glad warrior
+			if data.SubSpecs[spec] then
+				table.insert(fields, string.format("s%s", data.SubSpecs[spec]))
+			else
+				table.insert(fields, data.Specs[spec])
+			end
+			
+            table.insert(fields, data.Talents[spec])
+            table.insert(fields, toCompressedNumberList(data.Glyphs[spec]))
+        end
+    end
+    
+    -- export equipped gear
+    if data.Equipped then
+        for spec = 1, 2 do
+            if data.Equipped[spec] and (complete or spec == data.ActiveSpec) then
+                table.insert(fields, ".q" .. spec) -- indicates the start of an equipped gear block
+                
+                local itemObjects = {}
+                for k, v in pairs(data.Equipped[spec]) do
+                    local itemData = Amr.ParseItemLink(v)
+                    itemData.slot = k
+                    table.insert(itemObjects, itemData)
+                end
+                
+                appendItemsToExport(fields, itemObjects)
+            end
+        end
+	end
+    
+    -- if doing a complete export, include reputations and bank/bag items too
+    if complete then
+    
+        -- export reputations
+        local reps = {}
+        local noreps = true
+        if data.Reputations then
+            for k, v in pairs(data.Reputations) do
+                noreps = false
+                table.insert(reps, k .. ":" .. v)
+            end
+        end
+        if noreps then
+            table.insert(reps, "_")
+        end
+        
+        table.insert(fields, ".r")
+        table.insert(fields, table.concat(reps, ","))    
+    
+        -- export bag and bank
+        local itemObjects = {}
+    	if data.BagItems then
+	        for i, v in ipairs(data.BagItems) do
+				local itemData = Amr.ParseItemLink(v)
+				if itemData ~= nil and (IsEquippableItem(v) or Amr.SetTokenIds[itemData.id]) then
+					table.insert(itemObjects, itemData)
+				end
+	        end
+	    end
+	    if data.BankItems then
+	        for i, v in ipairs(data.BankItems) do
+	        	local itemData = Amr.ParseItemLink(v)
+				if itemData ~= nil and (IsEquippableItem(v) or Amr.SetTokenIds[itemData.id]) then
+					table.insert(itemObjects, itemData)
+				end
+	        end
+	    end
+	    if data.VoidItems then
+	        for i, v in ipairs(data.VoidItems) do
+	        	local itemData = Amr.ParseItemLink(v)
+				if itemData ~= nil and (IsEquippableItem(v) or Amr.SetTokenIds[itemData.id]) then
+					table.insert(itemObjects, itemData)
+				end
+		    end
+	    end
+        
+        table.insert(fields, ".inv")
+        appendItemsToExport(fields, itemObjects)
+    end
+
+    return "$" .. table.concat(fields, ";") .. "$"
+
+end
+
+-- Shortcut for the common use case: serialize the player's currently active setup with no extras.
+function Amr:SerializePlayer()
+	local data = self:GetPlayerData()
+	return self:SerializePlayerData(data)
+end
+
+
+----------------------------------------------------------------------------------------------------------------------
+-- Character Snapshots
+-- This feature snapshots a player's gear/talents/glyphs when entering combat.  It is enabled by default.  Consumers
+-- of this library can create a setting to enable/disable it as desired per a user setting.
+--
+-- You should register for the AMR_SNAPSHOT_STATE_CHANGED message (sent via AceEvent-3.0 messaging) to ensure that
+-- your addon settings stay in sync with any other addon that may also be trying to control the enabled state.
+--
+-- Note that if a user has the main AMR addon installed, it will always enable snapshotting, and override any attempt
+-- to disable it by immediately re-enabling it and thus re-triggering AMR_SNAPSHOT_STATE_CHANGED.
+----------------------------------------------------------------------------------------------------------------------
+Amr._snapshotEnabled = true
+
+-- Enable snapshotting of character data when entering combat.  Sends this player's character data to anyone logging with the AskMrRobot addon.
+function Amr:EnableSnapshots()
+	self._snapshotEnabled = true
+	self:SendMessage("AMR_SNAPSHOT_STATE_CHANGED", self._snapshotEnabled)
+end
+
+-- Disable snapshotting of character data when entering combat.
+function Amr:DisableSnapshots()
+	self._snapshotEnabled = false
+	self:SendMessage("AMR_SNAPSHOT_STATE_CHANGED", self._snapshotEnabled)
+end
+
+function Amr:IsSnapshotEnabled()
+	return self._snapshotEnabled
+end
+
+
+function Amr:PLAYER_REGEN_DISABLED()
+--function Amr:GARRISON_MISSION_NPC_OPENED()
+
+	-- send data about this character when a player enters combat in a supported zone
+	if self._snapshotEnabled and Amr.IsSupportedInstance() then
+		local t = time()
+		local player = self:GetPlayerData()
+		local msg = self:SerializePlayerData(player)
+		msg = string.format("%s\r%s\n%s\n%s\n%s\n%s", MINOR, t, player.Region, player.Realm, player.Name, msg)
+		
+		self:SendCommMessage(Amr.ChatPrefix, msg, "RAID")
+	end
+end
+
+Amr:RegisterEvent("PLAYER_REGEN_DISABLED")
+--Amr:RegisterEvent("GARRISON_MISSION_NPC_OPENED") -- for debugging, fire this event when open mission table
\ No newline at end of file