Quantcast

Add LibGroupInSpecT-1.1

Peter Eliasson [07-30-16 - 16:15]
Add LibGroupInSpecT-1.1
Filename
.pkgmeta
vendor/LibGroupInSpecT-1.1/Changelog-LibGroupInSpecT-1.1-r84.txt
vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.lua
vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.toc
vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.lua
vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.toc
vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test.lua
vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test2.lua
vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test3.lua
vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test4.lua
vendor/LibGroupInSpecT-1.1/lib.xml
vendor/vendor_include.xml
diff --git a/.pkgmeta b/.pkgmeta
index 3df0ef9..087de5a 100644
--- a/.pkgmeta
+++ b/.pkgmeta
@@ -17,3 +17,6 @@ externals:
     vendor/LibItemUpgradeInfo-1.0:
         url: git://git.wowace.com/wow/libitemupgradeinfo-1-0/mainline.git
         tag: Release-70000-24
+    vendor/LibGroupInSpecT-1.1:
+        url: svn://svn.wowace.com/wow/libgroupinspect/mainline/trunk
+        # TODO(2016-07-30): Specify release tag once available
diff --git a/vendor/LibGroupInSpecT-1.1/Changelog-LibGroupInSpecT-1.1-r84.txt b/vendor/LibGroupInSpecT-1.1/Changelog-LibGroupInSpecT-1.1-r84.txt
new file mode 100644
index 0000000..5cf3ee6
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Changelog-LibGroupInSpecT-1.1-r84.txt
@@ -0,0 +1,44 @@
+------------------------------------------------------------------------
+r84 | greltok | 2016-07-30 07:04:53 +0000 (Sat, 30 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+Added Demon Hunter.
+------------------------------------------------------------------------
+r83 | greltok | 2016-07-29 07:40:55 +0000 (Fri, 29 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+   M /trunk/LibGroupInSpecT-1.1.toc
+
+Removed glyphs.
+------------------------------------------------------------------------
+r82 | greltok | 2016-07-22 06:29:55 +0000 (Fri, 22 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+Survival hunters are melee.
+------------------------------------------------------------------------
+r81 | nebula169 | 2016-07-21 02:34:51 +0000 (Thu, 21 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+Remove Gladiator Stance checks.
+------------------------------------------------------------------------
+r80 | tercioo | 2016-07-21 02:30:42 +0000 (Thu, 21 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+- Tercio's quick fix: the spellid '156291' for gladiator stance doesn't exist on Legion.
+------------------------------------------------------------------------
+r79 | tercioo | 2016-07-18 19:47:24 +0000 (Mon, 18 Jul 2016) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+Quick fix for Legion Pre-Patch: NUM_GLYPH_SLOTS doesn't exists any more.
+------------------------------------------------------------------------
+r78 | greltok | 2015-02-21 05:13:59 +0000 (Sat, 21 Feb 2015) | 1 line
+Changed paths:
+   M /trunk/LibGroupInSpecT-1.1.lua
+
+Added event "GroupInSpecT_InspectReady".  :Rescan() adds units to staleq rather than mainq.
+------------------------------------------------------------------------
diff --git a/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.lua b/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.lua
new file mode 100644
index 0000000..e2edae9
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.lua
@@ -0,0 +1,898 @@
+-- vim: ts=2 sw=2 ai et fenc=utf8
+
+--[[
+-- These events can be registered for using the regular CallbackHandler ways.
+--
+-- "GroupInSpecT_Update", guid, unit, info
+-- "GroupInSpecT_Remove, guid
+-- "GroupInSpecT_InspectReady", guid, unit
+--
+-- Where <info> is a table containing some or all of the following:
+--   .guid
+--   .name
+--   .realm
+--   .race
+--   .race_localized
+--   .class
+--   .class_localized
+--   .class_id
+--   .gender -- 2 = male, 3 = female
+--   .global_spec_id
+--   .spec_index
+--   .spec_name_localized
+--   .spec_description
+--   .spec_icon
+--   .spec_background
+--   .spec_role
+--   .spec_role_detailed
+--   .spec_group -- active spec group (1/2/nil)
+--   .talents = {
+--     [<talent_id>] = {
+--       .tier
+--       .column
+--       .name_localized
+--       .icon
+--       .talent_id
+--       .spell_id
+--     }
+--     ...
+--   }
+--   .lku -- last known unit id
+--   .not_visible
+--
+-- Functions for external use:
+--
+--   lib:Rescan (guid or nil)
+--     Force a rescan of the given group member GUID, or of all current group members if nil.
+--
+--   lib:QueuedInspections ()
+--     Returns an array of GUIDs of outstanding inspects.
+--
+--   lib:StaleInspections ()
+--     Returns an array of GUIDs for which the data has become stale and is
+--     awaiting an update (no action required, the refresh happens internally).
+--     Due to Blizzard exposing no events on (re/un)talent, there will be
+--     frequent marking of inspect data as being stale.
+--
+--   lib:GetCachedInfo (guid)
+--     Returns the cached info for the given GUID, if available, nil otherwise.
+--     Information is cached for current group members only.
+--
+--   lib:GroupUnits ()
+--     Returns an array with the set of unit ids for the current group.
+--]]
+
+local MAJOR, MINOR = "LibGroupInSpecT-1.1", tonumber (("$Revision: 84 $"):match ("(%d+)") or 0)
+
+if not LibStub then error(MAJOR.." requires LibStub") end
+local lib = LibStub:NewLibrary (MAJOR, MINOR)
+if not lib then return end
+
+lib.events = lib.events or LibStub ("CallbackHandler-1.0"):New (lib)
+if not lib.events then error(MAJOR.." requires CallbackHandler") end
+
+
+local UPDATE_EVENT = "GroupInSpecT_Update"
+local REMOVE_EVENT = "GroupInSpecT_Remove"
+local INSPECT_READY_EVENT = "GroupInSpecT_InspectReady"
+
+local COMMS_PREFIX = "LGIST11"
+local COMMS_FMT = "1"
+local COMMS_DELIM = "\a"
+
+local INSPECT_DELAY = 1.5
+local INSPECT_TIMEOUT = 10 -- If we get no notification within 10s, give up on unit
+
+local MAX_ATTEMPTS = 2
+
+--[===[@debug@
+lib.debug = true
+local function debug (...)
+  if lib.debug then  -- allow programmatic override of debug output by client addons
+    print (...)
+  end
+end
+--@end-debug@]===]
+
+function lib.events:OnUsed(target, eventname)
+  if eventname == INSPECT_READY_EVENT then
+    target.inspect_ready_used = true
+  end
+end
+
+function lib.events:OnUnused(target, eventname)
+  if eventname == INSPECT_READY_EVENT then
+    target.inspect_ready_used = nil
+  end
+end
+
+-- Frame for events
+local frame = _G[MAJOR .. "_Frame"] or CreateFrame ("Frame", MAJOR .. "_Frame")
+lib.frame = frame
+frame:Hide()
+frame:UnregisterAllEvents ()
+frame:RegisterEvent ("PLAYER_LOGIN")
+frame:RegisterEvent ("PLAYER_LOGOUT")
+if not frame.OnEvent then
+  frame.OnEvent = function(this, event, ...)
+    local eventhandler = lib[event]
+    return eventhandler and eventhandler (lib, ...)
+  end
+  frame:SetScript ("OnEvent", frame.OnEvent)
+end
+
+
+-- Hide our run-state in an easy-to-dump object
+lib.state = {
+  mainq = {}, staleq = {}, -- inspect queues
+  t = 0,
+  last_inspect = 0,
+  current_guid = nil,
+  throttle = 0,
+  tt = 0,
+  debounce_send_update = 0,
+}
+lib.cache = {}
+lib.static_cache = {}
+
+
+-- Note: if we cache NotifyInspect, we have to hook before we cache it!
+if not lib.hooked then
+  hooksecurefunc("NotifyInspect", function (...) return lib:NotifyInspect (...) end)
+  lib.hooked = true
+end
+function lib:NotifyInspect(unit)
+  self.state.last_inspect = GetTime()
+end
+
+
+-- Get local handles on the key API functions
+local CanInspect                      = _G.CanInspect
+local ClearInspectPlayer              = _G.ClearInspectPlayer
+local GetClassInfo                    = _G.GetClassInfo
+local GetNumSubgroupMembers           = _G.GetNumSubgroupMembers
+local GetNumSpecializationsForClassID = _G.GetNumSpecializationsForClassID
+local GetPlayerInfoByGUID             = _G.GetPlayerInfoByGUID
+local GetInspectSpecialization        = _G.GetInspectSpecialization
+local GetSpecialization               = _G.GetSpecialization
+local GetSpecializationInfo           = _G.GetSpecializationInfo
+local GetSpecializationInfoForClassID = _G.GetSpecializationInfoForClassID
+local GetSpecializationRoleByID       = _G.GetSpecializationRoleByID
+local GetSpellInfo                    = _G.GetSpellInfo
+local GetTalentInfo                   = _G.GetTalentInfo
+local IsInRaid                        = _G.IsInRaid
+--local NotifyInspect                   = _G.NotifyInspect -- Don't cache, as to avoid missing future hooks
+local GetNumClasses                   = _G.GetNumClasses
+local UnitExists                      = _G.UnitExists
+local UnitGUID                        = _G.UnitGUID
+local UnitInParty                     = _G.UnitInParty
+local UnitInRaid                      = _G.UnitInRaid
+local UnitIsConnected                 = _G.UnitIsConnected
+local UnitIsPlayer                    = _G.UnitIsPlayer
+local UnitIsUnit                      = _G.UnitIsUnit
+local UnitName                        = _G.UnitName
+
+
+local global_spec_id_roles_detailed = {
+  -- Death Knight
+  [250] = "tank", -- Blood
+  [251] = "melee", -- Frost
+  [252] = "melee", -- Unholy
+  -- Demon Hunter
+  [577] = "melee", -- Havoc
+  [581] = "tank", -- Vengeance
+  -- Druid
+  [102] = "ranged", -- Balance
+  [103] = "melee", -- Feral
+  [104] = "tank", -- Guardian
+  [105] = "healer", -- Restoration
+  -- Hunter
+  [253] = "ranged", -- Beast Mastery
+  [254] = "ranged", -- Marksmanship
+  [255] = "melee", -- Survival
+  -- Mage
+  [62] = "ranged", -- Arcane
+  [63] = "ranged", -- Fire
+  [64] = "ranged", -- Frost
+  -- Monk
+  [268] = "tank", -- Brewmaster
+  [269] = "melee", -- Windwalker
+  [270] = "healer", -- Mistweaver
+  -- Paladin
+  [65] = "healer", -- Holy
+  [66] = "tank", -- Protection
+  [70] = "melee", -- Retribution
+  -- Priest
+  [256] = "healer", -- Discipline
+  [257] = "healer", -- Holy
+  [258] = "ranged", -- Shadow
+  -- Rogue
+  [259] = "melee", -- Assassination
+  [260] = "melee", -- Combat
+  [261] = "melee", -- Subtlety
+  -- Shaman
+  [262] = "ranged", -- Elemental
+  [263] = "melee", -- Enhancement
+  [264] = "healer", -- Restoration
+  -- Warlock
+  [265] = "ranged", -- Affliction
+  [266] = "ranged", -- Demonology
+  [267] = "ranged", -- Destruction
+  -- Warrior
+  [71] = "melee", -- Arms
+  [72] = "melee", -- Fury
+  [73] = "tank", -- Protection
+}
+
+local class_fixed_roles = {
+  HUNTER = "DAMAGER",
+  MAGE = "DAMAGER",
+  ROGUE = "DAMAGER",
+  WARLOCK = "DAMAGER",
+}
+
+local class_fixed_roles_detailed = {
+  MAGE = "ranged",
+  ROGUE = "melee",
+  WARLOCK = "ranged",
+}
+
+-- Inspects only work after being fully logged in, so track that
+function lib:PLAYER_LOGIN ()
+  self.state.logged_in = true
+
+  self:CacheGameData ()
+
+  frame:RegisterEvent ("INSPECT_READY")
+  frame:RegisterEvent ("GROUP_ROSTER_UPDATE")
+  frame:RegisterEvent ("PLAYER_ENTERING_WORLD")
+  frame:RegisterEvent ("UNIT_LEVEL")
+  frame:RegisterEvent ("PLAYER_TALENT_UPDATE")
+  frame:RegisterEvent ("PLAYER_SPECIALIZATION_CHANGED")
+  frame:RegisterEvent ("UNIT_SPELLCAST_SUCCEEDED")
+  frame:RegisterEvent ("UNIT_NAME_UPDATE")
+  frame:RegisterEvent ("UNIT_AURA")
+  frame:RegisterEvent ("CHAT_MSG_ADDON")
+  RegisterAddonMessagePrefix (COMMS_PREFIX)
+
+  local guid = UnitGUID ("player")
+  local info = self:BuildInfo ("player")
+  self.events:Fire (UPDATE_EVENT, guid, "player", info)
+end
+
+function lib:PLAYER_LOGOUT ()
+  self.state.logged_in = false
+end
+
+
+-- Simple timer
+do
+  lib.state.t = 0
+  if not frame.OnUpdate then -- ticket #4 if the OnUpdate code every changes we should stop borrowing the existing handler
+    frame.OnUpdate = function(this, elapsed)
+      lib.state.t = lib.state.t + elapsed
+      lib.state.tt = lib.state.tt + elapsed
+      if lib.state.t > INSPECT_DELAY then
+        lib:ProcessQueues ()
+        lib.state.t = 0
+      end
+      -- Unthrottle, essentially allowing 1 msg every 3 seconds, but with substantial burst capacity
+      if lib.state.tt > 3 and lib.state.throttle > 0 then
+        lib.state.throttle = lib.state.throttle - 1
+        lib.state.tt = 0
+      end
+      if lib.state.debounce_send_update > 0 then
+        local debounce = lib.state.debounce_send_update - elapsed
+        lib.state.debounce_send_update = debounce
+        if debounce <= 0 then lib:SendLatestSpecData () end
+      end
+    end
+    frame:SetScript("OnUpdate", frame.OnUpdate) -- this is good regardless of the handler check above because otherwise a new anonymous function is created every time the OnUpdate code runs
+  end
+end
+
+
+-- Internal library functions
+
+-- Caches to deal with API shortcomings as well as performance
+lib.static_cache.global_specs = {}           -- [gspec]         -> { .idx, .name_localized, .description, .icon, .background, .role }
+lib.static_cache.class_to_class_id = {}      -- [CLASS]         -> class_id
+
+-- The talents cache can no longer be pre-fetched on login, but is now constructed class-by-class as we inspect people.
+-- This probably means we want to only ever access it through the GetCachedTalentInfo() helper function below.
+lib.static_cache.talents = {}                -- [talent_id]      -> { .spell_id, .talent_id, .name_localized, .icon, .tier, .column }
+
+-- Dridzt: I'd love another way but none of the GetTalent* functions return spellID, GetTalentLink() and parsing the link gives talentID that's not related to spellID as well
+-- A quick tooltip scan is cheap though so elegance aside this is a good workaround considering this only runs once
+local tip = CreateFrame ("GameTooltip", MAJOR.."ScanTip", nil, "GameTooltipTemplate")
+tip:SetOwner (UIParent, "ANCHOR_NONE")
+
+function lib:GetCachedTalentInfo (class_id, tier, col, group, is_inspect, unit)
+  local talents = self.static_cache.talents
+  local talent_id, name, icon, sel, avail = GetTalentInfo (tier, col, group, is_inspect, unit)
+  if not talent_id or not class_id then
+    --[===[@debug@
+    debug ("GetCachedTalentInfo("..tostring(class_id)..","..tier..","..col..","..group..","..tostring(is_inspect)..","..tostring(unit)..") returned nil") --@end-debug@]===]
+    return {}
+  end
+  talents[class_id] = talents[class_id] or {}
+  local class_talents = talents[class_id]
+  if not class_talents[talent_id] then
+    tip:ClearLines ()
+    tip:SetTalent (talent_id, is_inspect, group)
+    local _, _,spell_id = tip:GetSpell ()
+    class_talents[talent_id] = {
+      spell_id = spell_id,
+      talent_id = talent_id,
+      name_localized = name,
+      icon = icon,
+      tier = tier,
+      column = col,
+    }
+  end
+  return class_talents[talent_id], sel
+end
+
+
+function lib:CacheGameData ()
+  local gspecs = self.static_cache.global_specs
+  gspecs[0] = {} -- Handle no-specialization case
+  for class_id = 1, GetNumClasses () do
+    for idx = 1, GetNumSpecializationsForClassID (class_id) do
+      local gspec_id, name, description, icon, background = GetSpecializationInfoForClassID (class_id, idx)
+      gspecs[gspec_id] = {}
+      local gspec = gspecs[gspec_id]
+      gspec.idx = idx
+      gspec.name_localized = name
+      gspec.description = description
+      gspec.icon = icon
+      gspec.background = background
+      gspec.role = GetSpecializationRoleByID (gspec_id)
+    end
+
+    local _, class = GetClassInfo (class_id)
+    self.static_cache.class_to_class_id[class] = class_id
+  end
+end
+
+
+function lib:GuidToUnit (guid)
+  local info = self.cache[guid]
+  if info and info.lku and UnitGUID (info.lku) == guid then return info.lku end
+
+  for i,unit in ipairs (self:GroupUnits ()) do
+    if UnitExists (unit) and UnitGUID (unit) == guid then
+      if info then info.lku = unit end
+      return unit
+    end
+  end
+end
+
+
+function lib:Query (unit)
+  if not UnitIsPlayer (unit) then return end -- NPC
+
+  if UnitIsUnit (unit, "player") then
+    self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
+    return
+  end
+
+  local mainq, staleq = self.state.mainq, self.state.staleq
+
+  local guid = UnitGUID (unit)
+  if not mainq[guid] then
+    mainq[guid] = 1
+    staleq[guid] = nil
+    self.frame:Show () -- Start timer if not already running
+  end
+end
+
+
+function lib:Refresh (unit)
+  local guid = UnitGUID (unit)
+  if not guid then return end
+  --[===[@debug@
+  debug ("Refreshing "..unit) --@end-debug@]===]
+  if not self.state.mainq[guid] then
+    self.state.staleq[guid] = 1
+    self.frame:Show ()
+  end
+end
+
+
+function lib:ProcessQueues ()
+  if not self.state.logged_in then return end
+  if InCombatLockdown () then return end -- Never inspect while in combat
+  if UnitIsDead ("player") then return end -- You can't inspect while dead, so don't even try
+  if InspectFrame and InspectFrame:IsShown () then return end -- Don't mess with the UI's inspections
+
+  local mainq = self.state.mainq
+  local staleq = self.state.staleq
+
+  if not next (mainq) and next(staleq) then
+    --[===[@debug@
+    debug ("Main queue empty, swapping main and stale queues") --@end-debug@]===]
+    self.state.mainq, self.state.staleq = self.state.staleq, self.state.mainq
+    mainq, staleq = staleq, mainq
+  end
+
+  if (self.state.last_inspect + INSPECT_TIMEOUT) < GetTime () then
+    -- If there was an inspect going, it's timed out, so either retry or move it to stale queue
+    local guid = self.state.current_guid
+    if guid then
+      --[===[@debug@
+      debug ("Inspect timed out for "..guid) --@end-debug@]===]
+
+      local count = mainq and mainq[guid] or (MAX_ATTEMPTS + 1)
+      if not self:GuidToUnit (guid) then
+        --[===[@debug@
+        debug ("No longer applicable, removing from queues") --@end-debug@]===]
+        mainq[guid], staleq[guid] = nil, nil
+      elseif count > MAX_ATTEMPTS then
+        --[===[@debug@
+        debug ("Excessive retries, moving to stale queue") --@end-debug@]===]
+        mainq[guid], staleq[guid] = nil, 1
+      else
+        mainq[guid] = count + 1
+      end
+      self.state.current_guid = nil
+    end
+  end
+
+  if self.state.current_guid then return end -- Still waiting on our inspect data
+
+  for guid,count in pairs (mainq) do
+    local unit = self:GuidToUnit (guid)
+    if not unit then
+      --[===[@debug@
+      debug ("No longer applicable, removing from queues") --@end-debug@]===]
+      mainq[guid], staleq[guid] = nil, nil
+    elseif not CanInspect (unit) or not UnitIsConnected (unit) then
+      --[===[@debug@
+      debug ("Cannot inspect "..unit..", aka "..(UnitName(unit) or "nil")..", moving to stale queue") --@end-debug@]===]
+      mainq[guid], staleq[guid] = nil, 1
+    else
+      --[===[@debug@
+      debug ("Inspecting "..unit..", aka "..(UnitName(unit) or "nil")) --@end-debug@]===]
+      mainq[guid] = count + 1
+      self.state.current_guid = guid
+      NotifyInspect (unit)
+      break
+    end
+  end
+
+  if not next (mainq) and not next (staleq) and self.state.throttle == 0 and self.state.debounce_send_update <= 0 then
+    frame:Hide() -- Cancel timer, nothing queued and no unthrottling to be done
+  end
+end
+
+
+function lib:UpdatePlayerInfo (guid, unit, info)
+  info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
+  local class = info.class
+  if info.realm and info.realm == "" then info.realm = nil end
+  info.class_id = class and self.static_cache.class_to_class_id[class]
+  if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
+  if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
+  info.lku = unit
+end
+
+
+function lib:BuildInfo (unit)
+  local guid = UnitGUID (unit)
+  if not guid then return end
+
+  local cache = self.cache
+  local info = cache[guid] or {}
+  cache[guid] = info
+  info.guid = guid
+
+  self:UpdatePlayerInfo (guid, unit, info)
+  -- On a cold login, GetPlayerInfoByGUID() doesn't seem to be usable, so mark as stale
+  local class = info.class
+  if not class and not self.state.mainq[guid] then
+    self.state.staleq[guid] = 1
+    self.frame:Show ()
+  end
+
+  local is_inspect = not UnitIsUnit (unit, "player")
+  local spec = GetSpecialization ()
+  info.global_spec_id = is_inspect and GetInspectSpecialization (unit) or spec and GetSpecializationInfo (spec)
+
+  local gspecs = self.static_cache.global_specs
+  if not info.global_spec_id or not gspecs[info.global_spec_id] then -- not a valid spec_id
+    info.global_spec_id = nil
+  else
+    local gspec_id = info.global_spec_id
+    local spec_info = gspecs[gspec_id]
+    info.spec_index          = spec_info.idx
+    info.spec_name_localized = spec_info.name_localized
+    info.spec_description    = spec_info.description
+    info.spec_icon           = spec_info.icon
+    info.spec_background     = spec_info.background
+    info.spec_role           = spec_info.role
+    info.spec_role_detailed  = global_spec_id_roles_detailed[gspec_id]
+  end
+
+  if not info.spec_role then info.spec_role = class and class_fixed_roles[class] end
+  if not info.spec_role_detailed then info.spec_role_detailed = class and class_fixed_roles_detailed[class] end
+
+  info.talents = info.talents or {}
+
+  -- If GetPlayerInfoByGUID didn't return the class, we can't do talents yet
+  if info.class_id then
+    info.spec_group = GetActiveSpecGroup (is_inspect)
+    wipe (info.talents) -- Due to spec-specific talents we might leave things in on a spec-change otherwise
+    for tier = 1, MAX_TALENT_TIERS do
+      for col = 1, NUM_TALENT_COLUMNS do
+        local talent, sel = self:GetCachedTalentInfo (info.class_id, tier, col, info.spec_group, is_inspect, unit)
+        if talent and talent.talent_id and sel then
+          info.talents[talent.talent_id] = talent
+        end
+      end
+    end
+  end
+
+  info.glyphs = wipe (info.glyphs or {}) -- kept for addons that still refer to this
+
+  if is_inspect and not UnitIsVisible (unit) and UnitIsConnected (unit) then info.not_visible = true end
+
+  return info
+end
+
+
+function lib:INSPECT_READY (guid)
+  local unit = self:GuidToUnit (guid)
+  local finalize = false
+  if unit then
+    if guid == self.state.current_guid then
+      self.state.current_guid = nil -- Got what we asked for
+      finalize = true
+      --[===[@debug@
+      debug ("Got inspection data for requested guid "..guid) --@end-debug@]===]
+    end
+
+    local mainq, staleq = self.state.mainq, self.state.staleq
+    mainq[guid], staleq[guid] = nil, nil
+
+    local gspec_id = GetInspectSpecialization (unit)
+    if not self.static_cache.global_specs[gspec_id] then -- Bah, got garbage, flag as stale and try again
+      staleq[guid] = 1
+      return
+    end
+
+    self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
+    self.events:Fire (INSPECT_READY_EVENT, guid, unit)
+  end
+  if finalize then
+    ClearInspectPlayer ()
+  end
+end
+
+
+function lib:PLAYER_ENTERING_WORLD ()
+  if self.commScope == "INSTANCE_CHAT" then
+    -- Handle moving directly from one LFG to another
+    self.commScope = nil
+    self:UpdateCommScope ()
+  end
+end
+
+
+-- Group handling parts
+
+local members = {}
+function lib:GROUP_ROSTER_UPDATE ()
+  local group = self.cache
+  local units = self:GroupUnits ()
+  -- Find new members
+  for i,unit in ipairs (self:GroupUnits ()) do
+    local guid = UnitGUID (unit)
+    if guid then
+      members[guid] = true
+      if not group[guid] then
+        self:Query (unit)
+        -- Update with what we have so far (guid, unit, name/class/race?)
+        self.events:Fire (UPDATE_EVENT, guid, unit, self:BuildInfo (unit))
+      end
+    end
+  end
+  -- Find removed members
+  for guid in pairs (group) do
+    if not members[guid] then
+      group[guid] = nil
+      self.events:Fire (REMOVE_EVENT, guid, nil)
+    end
+  end
+  wipe (members)
+  self:UpdateCommScope ()
+end
+
+
+function lib:DoPlayerUpdate ()
+  self:Query ("player")
+  self.state.debounce_send_update = 2.5 -- Hold off 2.5sec before sending update
+  self.frame:Show ()
+end
+
+
+function lib:SendLatestSpecData ()
+  local scope = self.commScope
+  if not scope then return end
+
+  local guid = UnitGUID ("player")
+  local info = self.cache[guid]
+  if not info then return end
+
+  -- fmt, guid, global_spec_id, talent1 -> MAX_TALENT_TIERS
+  -- sequentially, allow no gaps for missing talents we decode by index on the receiving end.
+  local datastr = COMMS_FMT..COMMS_DELIM..guid..COMMS_DELIM..(info.global_spec_id or 0)
+  local talentCount = 1
+  for k in pairs(info.talents) do
+    datastr = datastr..COMMS_DELIM..k
+    talentCount = talentCount + 1
+  end
+  for i=talentCount,MAX_TALENT_TIERS do
+    datastr = datastr..COMMS_DELIM..0
+  end
+
+  --[===[@debug@
+  debug ("Sending LGIST update to "..scope) --@end-debug@]===]
+  SendAddonMessage(COMMS_PREFIX, datastr, scope)
+end
+
+
+function lib:UpdateCommScope ()
+  local scope = (IsInGroup (LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT") or (IsInRaid () and "RAID") or (IsInGroup (LE_PARTY_CATEGORY_HOME) and "PARTY")
+  if self.commScope ~= scope then
+    self.commScope = scope
+    self:DoPlayerUpdate ()
+  end
+end
+
+
+-- Indicies for various parts of the split data msg
+local msg_idx = {}
+msg_idx.fmt            = 1
+msg_idx.guid           = msg_idx.fmt + 1
+msg_idx.global_spec_id = msg_idx.guid + 1
+msg_idx.talents        = msg_idx.global_spec_id + 1
+msg_idx.end_talents    = msg_idx.talents + MAX_TALENT_TIERS - 1
+
+function lib:CHAT_MSG_ADDON (prefix, datastr, scope, sender)
+  if prefix ~= COMMS_PREFIX or scope ~= self.commScope then return end
+  --[===[@debug@
+  debug ("Incoming LGIST update from "..(scope or "nil").."/"..(sender or "nil")..": "..(datastr:gsub(COMMS_DELIM,";") or "nil")) --@end-debug@]===]
+
+  local data = { strsplit (COMMS_DELIM,datastr) }
+  local fmt = data[msg_idx.fmt]
+  if fmt ~= COMMS_FMT then return end -- Unknown format, ignore
+
+  local guid = data[msg_idx.guid]
+
+  local senderguid = UnitGUID(sender)
+  if senderguid and senderguid ~= guid then return end
+
+  local info = guid and self.cache[guid]
+  if not info then return end -- Never allow random message to create new group member entries!
+
+  local unit = self:GuidToUnit (guid)
+  if not unit then return end
+  if UnitIsUnit (unit, "player") then return end -- we're already up-to-date, comment out for solo debugging
+
+  self.state.throttle = self.state.throttle + 1
+  self.frame:Show () -- Ensure we're unthrottling
+  if self.state.throttle > 40 then return end -- If we ever hit this, someone's being "funny"
+
+  info.class_localized, info.class, info.race_localized, info.race, info.gender, info.name, info.realm = GetPlayerInfoByGUID (guid)
+  if info.realm and info.realm == "" then info.realm = nil end
+  info.class_id = self.static_cache.class_to_class_id[info.class]
+
+  local gspecs = self.static_cache.global_specs
+
+  local gspec_id           = data[msg_idx.global_spec_id] and tonumber (data[msg_idx.global_spec_id])
+  if not gspec_id or not gspecs[gspec_id] then return end -- Malformed message, avoid throwing errors by using this nil
+
+  info.global_spec_id      = gspec_id
+  info.spec_index          = gspecs[gspec_id].idx
+  info.spec_name_localized = gspecs[gspec_id].name_localized
+  info.spec_description    = gspecs[gspec_id].description
+  info.spec_icon           = gspecs[gspec_id].icon
+  info.spec_background     = gspecs[gspec_id].background
+  info.spec_role           = gspecs[gspec_id].role
+  info.spec_role_detailed  = global_spec_id_roles_detailed[gspec_id]
+
+  local need_inspect = nil
+  info.talents = wipe (info.talents or {})
+  local talents = self.static_cache.talents[info.class_id]
+  if talents then -- The group entry is created before we have inspect-data, so may not have cached talents yet
+    for i = msg_idx.talents, msg_idx.end_talents do
+      local talent_id = tonumber (data[i])
+      if talent_id and talent_id > 0 then
+        if talents[talent_id] then
+          info.talents[talent_id] = talents[talent_id]
+        else
+          -- While we had some talents for this class, we apparently didn't have all for this particular spec, so mark for inspect
+          need_inspect = 1
+        end
+      end
+    end
+  else
+    -- Talents weren't pre-cached, so mark for inspect
+    need_inspect = 1
+  end
+
+  info.glyphs = wipe (info.glyphs or {}) -- kept for addons that still refer to this
+
+  local mainq, staleq = self.state.mainq, self.state.staleq
+  local want_inspect = not need_inspect and self.inspect_ready_used and (mainq[guid] or staleq[guid]) and 1 or nil
+  mainq[guid], staleq[guid] = need_inspect, want_inspect
+  if need_inspect or want_inspect then self.frame:Show () end
+
+  --[===[@debug@
+  debug ("Firing LGIST update event for unit "..unit..", GUID "..guid) --@end-debug@]===]
+  self.events:Fire (UPDATE_EVENT, guid, unit, info)
+end
+
+
+function lib:UNIT_LEVEL (unit)
+  if UnitInRaid (unit) or UnitInParty (unit) then
+    self:Refresh (unit)
+  end
+  if UnitIsUnit (unit, "player") then
+    self:DoPlayerUpdate ()
+  end
+end
+
+
+function lib:PLAYER_TALENT_UPDATE ()
+  self:DoPlayerUpdate ()
+end
+
+
+function lib:PLAYER_SPECIALIZATION_CHANGED (unit)
+--  This event seems to fire a lot, and for no particular reason *sigh*
+--  if UnitInRaid (unit) or UnitInParty (unit) then
+--    self:Refresh (unit)
+--  end
+  if unit and UnitIsUnit (unit, "player") then
+    self:DoPlayerUpdate ()
+  end
+end
+
+
+function lib:UNIT_NAME_UPDATE (unit)
+  local group = self.cache
+  local guid = UnitGUID (unit)
+  local info = guid and group[guid]
+  if info then
+    self:UpdatePlayerInfo (guid, unit, info)
+    if info.name ~= UNKNOWN then
+      self.events:Fire (UPDATE_EVENT, guid, unit, info)
+    end
+  end
+end
+
+
+-- Always get a UNIT_AURA when a unit's UnitIsVisible() changes
+function lib:UNIT_AURA (unit)
+  local group = self.cache
+  local guid = UnitGUID (unit)
+  local info = guid and group[guid]
+  if info then
+    if not UnitIsUnit (unit, "player") then
+      if UnitIsVisible (unit) then
+        if info.not_visible then
+          info.not_visible = nil
+          --[===[@debug@
+          debug (unit..", aka "..(UnitName(unit) or "nil")..", is now visible") --@end-debug@]===]
+          if not self.state.mainq[guid] then
+            self.state.staleq[guid] = 1
+            self.frame:Show ()
+          end
+        end
+      elseif UnitIsConnected (unit) then
+        --[===[@debug@
+        if not info.not_visible then
+          debug (unit..", aka "..(UnitName(unit) or "nil")..", is no longer visible")
+        end
+        --@end-debug@]===]
+        info.not_visible = true
+      end
+    end
+  end
+end
+
+
+local dual_spec_spells = {}
+for i, spellid in ipairs (TALENT_ACTIVATION_SPELLS) do dual_spec_spells[spellid] = true end
+function lib:UNIT_SPELLCAST_SUCCEEDED (unit, spellname, rank, lineid, spellid)
+  if dual_spec_spells[spellid] then
+    self:Query (unit) -- Definitely changed, so high prio refresh
+  end
+end
+
+
+-- External library functions
+
+function lib:QueuedInspections ()
+  local q = {}
+  for guid in pairs (self.state.mainq) do
+    table.insert (q, guid)
+  end
+  return q
+end
+
+
+function lib:StaleInspections ()
+  local q = {}
+  for guid in pairs (self.state.staleq) do
+    table.insert (q, guid)
+  end
+  return q
+end
+
+
+function lib:GetCachedInfo (guid)
+  local group = self.cache
+  return guid and group[guid]
+end
+
+
+function lib:Rescan (guid)
+  local mainq, staleq = self.state.mainq, self.state.staleq
+  if guid then
+    local unit = self:GuidToUnit (guid)
+    if unit then
+      if UnitIsUnit (unit, "player") then
+        self.events:Fire (UPDATE_EVENT, guid, "player", self:BuildInfo ("player"))
+      elseif not mainq[guid] then
+        staleq[guid] = 1
+      end
+    end
+  else
+    for i,unit in ipairs (self:GroupUnits ()) do
+      if UnitExists (unit) then
+        if UnitIsUnit (unit, "player") then
+          self.events:Fire (UPDATE_EVENT, UnitGUID("player"), "player", self:BuildInfo ("player"))
+        else
+          local guid = UnitGUID (unit)
+          if guid and not mainq[guid] then
+            staleq[guid] = 1
+          end
+        end
+      end
+    end
+  end
+  self.frame:Show () -- Start timer if not already running
+
+  -- Evict any stale entries
+  self:GROUP_ROSTER_UPDATE ()
+end
+
+
+local unitstrings = {
+  raid = { "player" }, -- This seems to be needed under certain circumstances. Odd.
+  party = { "player" }, -- Player not part of partyN
+  player = { "player" }
+}
+for i = 1,40 do table.insert (unitstrings.raid, "raid"..i) end
+for i = 1,4  do table.insert (unitstrings.party, "party"..i) end
+
+
+-- Returns an array with the set of unit ids for the current group
+function lib:GroupUnits ()
+  local units
+  if IsInRaid () then
+    units = unitstrings.raid
+  elseif GetNumSubgroupMembers () > 0 then
+    units = unitstrings.party
+  else
+    units = unitstrings.player
+  end
+  return units
+end
+
+
+-- If demand-loaded, we need to synthesize a login event
+if IsLoggedIn () then lib:PLAYER_LOGIN () end
diff --git a/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.toc b/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.toc
new file mode 100644
index 0000000..cc84c10
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/LibGroupInSpecT-1.1.toc
@@ -0,0 +1,15 @@
+## Interface: 70000
+## Title: Lib: GroupInSpecT-1.1
+## Notes: Keeps track of group members and keeps an up-to-date cache of their specialization and talents.
+## Version: r84
+## Author: Anyia of HordeYakka (Jubei'Thos)
+## X-Category: Library
+## X-Curse-Packaged-Version: r84
+## X-Curse-Project-Name: LibGroupInSpecT
+## X-Curse-Project-ID: libgroupinspect
+## X-Curse-Repository-ID: wow/libgroupinspect/mainline
+
+Libs\LibStub\LibStub.lua
+Libs\CallbackHandler-1.0\CallbackHandler-1.0.lua
+
+lib.xml
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua b/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
new file mode 100644
index 0000000..2a64013
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
@@ -0,0 +1,238 @@
+--[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z mikk $ ]]
+local MAJOR, MINOR = "CallbackHandler-1.0", 6
+local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not CallbackHandler then return end -- No upgrade needed
+
+local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
+
+-- Lua APIs
+local tconcat = table.concat
+local assert, error, loadstring = assert, error, loadstring
+local setmetatable, rawset, rawget = setmetatable, rawset, rawget
+local next, select, pairs, type, tostring = next, select, pairs, type, tostring
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: geterrorhandler
+
+local xpcall = xpcall
+
+local function errorhandler(err)
+	return geterrorhandler()(err)
+end
+
+local function CreateDispatcher(argCount)
+	local code = [[
+	local next, xpcall, eh = ...
+
+	local method, ARGS
+	local function call() method(ARGS) end
+
+	local function dispatch(handlers, ...)
+		local index
+		index, method = next(handlers)
+		if not method then return end
+		local OLD_ARGS = ARGS
+		ARGS = ...
+		repeat
+			xpcall(call, eh)
+			index, method = next(handlers, index)
+		until not method
+		ARGS = OLD_ARGS
+	end
+
+	return dispatch
+	]]
+
+	local ARGS, OLD_ARGS = {}, {}
+	for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
+	code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
+	return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
+end
+
+local Dispatchers = setmetatable({}, {__index=function(self, argCount)
+	local dispatcher = CreateDispatcher(argCount)
+	rawset(self, argCount, dispatcher)
+	return dispatcher
+end})
+
+--------------------------------------------------------------------------
+-- CallbackHandler:New
+--
+--   target            - target object to embed public APIs in
+--   RegisterName      - name of the callback registration API, default "RegisterCallback"
+--   UnregisterName    - name of the callback unregistration API, default "UnregisterCallback"
+--   UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
+
+function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
+
+	RegisterName = RegisterName or "RegisterCallback"
+	UnregisterName = UnregisterName or "UnregisterCallback"
+	if UnregisterAllName==nil then	-- false is used to indicate "don't want this method"
+		UnregisterAllName = "UnregisterAllCallbacks"
+	end
+
+	-- we declare all objects and exported APIs inside this closure to quickly gain access
+	-- to e.g. function names, the "target" parameter, etc
+
+
+	-- Create the registry object
+	local events = setmetatable({}, meta)
+	local registry = { recurse=0, events=events }
+
+	-- registry:Fire() - fires the given event/message into the registry
+	function registry:Fire(eventname, ...)
+		if not rawget(events, eventname) or not next(events[eventname]) then return end
+		local oldrecurse = registry.recurse
+		registry.recurse = oldrecurse + 1
+
+		Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
+
+		registry.recurse = oldrecurse
+
+		if registry.insertQueue and oldrecurse==0 then
+			-- Something in one of our callbacks wanted to register more callbacks; they got queued
+			for eventname,callbacks in pairs(registry.insertQueue) do
+				local first = not rawget(events, eventname) or not next(events[eventname])	-- test for empty before. not test for one member after. that one member may have been overwritten.
+				for self,func in pairs(callbacks) do
+					events[eventname][self] = func
+					-- fire OnUsed callback?
+					if first and registry.OnUsed then
+						registry.OnUsed(registry, target, eventname)
+						first = nil
+					end
+				end
+			end
+			registry.insertQueue = nil
+		end
+	end
+
+	-- Registration of a callback, handles:
+	--   self["method"], leads to self["method"](self, ...)
+	--   self with function ref, leads to functionref(...)
+	--   "addonId" (instead of self) with function ref, leads to functionref(...)
+	-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
+	target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
+		if type(eventname) ~= "string" then
+			error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
+		end
+
+		method = method or eventname
+
+		local first = not rawget(events, eventname) or not next(events[eventname])	-- test for empty before. not test for one member after. that one member may have been overwritten.
+
+		if type(method) ~= "string" and type(method) ~= "function" then
+			error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
+		end
+
+		local regfunc
+
+		if type(method) == "string" then
+			-- self["method"] calling style
+			if type(self) ~= "table" then
+				error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
+			elseif self==target then
+				error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
+			elseif type(self[method]) ~= "function" then
+				error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
+			end
+
+			if select("#",...)>=1 then	-- this is not the same as testing for arg==nil!
+				local arg=select(1,...)
+				regfunc = function(...) self[method](self,arg,...) end
+			else
+				regfunc = function(...) self[method](self,...) end
+			end
+		else
+			-- function ref with self=object or self="addonId" or self=thread
+			if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
+				error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
+			end
+
+			if select("#",...)>=1 then	-- this is not the same as testing for arg==nil!
+				local arg=select(1,...)
+				regfunc = function(...) method(arg,...) end
+			else
+				regfunc = method
+			end
+		end
+
+
+		if events[eventname][self] or registry.recurse<1 then
+		-- if registry.recurse<1 then
+			-- we're overwriting an existing entry, or not currently recursing. just set it.
+			events[eventname][self] = regfunc
+			-- fire OnUsed callback?
+			if registry.OnUsed and first then
+				registry.OnUsed(registry, target, eventname)
+			end
+		else
+			-- we're currently processing a callback in this registry, so delay the registration of this new entry!
+			-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
+			registry.insertQueue = registry.insertQueue or setmetatable({},meta)
+			registry.insertQueue[eventname][self] = regfunc
+		end
+	end
+
+	-- Unregister a callback
+	target[UnregisterName] = function(self, eventname)
+		if not self or self==target then
+			error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
+		end
+		if type(eventname) ~= "string" then
+			error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
+		end
+		if rawget(events, eventname) and events[eventname][self] then
+			events[eventname][self] = nil
+			-- Fire OnUnused callback?
+			if registry.OnUnused and not next(events[eventname]) then
+				registry.OnUnused(registry, target, eventname)
+			end
+		end
+		if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
+			registry.insertQueue[eventname][self] = nil
+		end
+	end
+
+	-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
+	if UnregisterAllName then
+		target[UnregisterAllName] = function(...)
+			if select("#",...)<1 then
+				error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
+			end
+			if select("#",...)==1 and ...==target then
+				error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
+			end
+
+
+			for i=1,select("#",...) do
+				local self = select(i,...)
+				if registry.insertQueue then
+					for eventname, callbacks in pairs(registry.insertQueue) do
+						if callbacks[self] then
+							callbacks[self] = nil
+						end
+					end
+				end
+				for eventname, callbacks in pairs(events) do
+					if callbacks[self] then
+						callbacks[self] = nil
+						-- Fire OnUnused callback?
+						if registry.OnUnused and not next(callbacks) then
+							registry.OnUnused(registry, target, eventname)
+						end
+					end
+				end
+			end
+		end
+	end
+
+	return registry
+end
+
+
+-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
+-- try to upgrade old implicit embeds since the system is selfcontained and
+-- relies on closures to work.
+
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml b/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
new file mode 100644
index 0000000..876df83
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
@@ -0,0 +1,4 @@
+<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
+..\FrameXML\UI.xsd">
+	<Script file="CallbackHandler-1.0.lua"/>
+</Ui>
\ No newline at end of file
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.lua b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.lua
new file mode 100644
index 0000000..7e9b5cd
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.lua
@@ -0,0 +1,51 @@
+-- $Id: LibStub.lua 103 2014-10-16 03:02:50Z mikk $
+-- LibStub is a simple versioning stub meant for use in Libraries.  http://www.wowace.com/addons/libstub/ for more info
+-- LibStub is hereby placed in the Public Domain
+-- Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2  -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+local LibStub = _G[LIBSTUB_MAJOR]
+
+-- Check to see is this version of the stub is obsolete
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+	LibStub = LibStub or {libs = {}, minors = {} }
+	_G[LIBSTUB_MAJOR] = LibStub
+	LibStub.minor = LIBSTUB_MINOR
+
+	-- LibStub:NewLibrary(major, minor)
+	-- major (string) - the major version of the library
+	-- minor (string or number ) - the minor version of the library
+	--
+	-- returns nil if a newer or same version of the lib is already present
+	-- returns empty library object or old library object if upgrade is needed
+	function LibStub:NewLibrary(major, minor)
+		assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+		minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+		local oldminor = self.minors[major]
+		if oldminor and oldminor >= minor then return nil end
+		self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+		return self.libs[major], oldminor
+	end
+
+	-- LibStub:GetLibrary(major, [silent])
+	-- major (string) - the major version of the library
+	-- silent (boolean) - if true, library is optional, silently return nil if its not found
+	--
+	-- throws an error if the library can not be found (except silent is set)
+	-- returns the library object if found
+	function LibStub:GetLibrary(major, silent)
+		if not self.libs[major] and not silent then
+			error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
+		end
+		return self.libs[major], self.minors[major]
+	end
+
+	-- LibStub:IterateLibraries()
+	--
+	-- Returns an iterator for the currently registered libraries
+	function LibStub:IterateLibraries()
+		return pairs(self.libs)
+	end
+
+	setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.toc b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.toc
new file mode 100644
index 0000000..b12ce87
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/LibStub.toc
@@ -0,0 +1,13 @@
+## Interface: 60000
+## Title: Lib: LibStub
+## Notes: Universal Library Stub
+## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel
+## X-Website: http://www.wowace.com/addons/libstub/
+## X-Category: Library
+## X-License: Public Domain
+## X-Curse-Packaged-Version: r103
+## X-Curse-Project-Name: LibStub
+## X-Curse-Project-ID: libstub
+## X-Curse-Repository-ID: wow/libstub/mainline
+
+LibStub.lua
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test.lua b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test.lua
new file mode 100644
index 0000000..276ddab
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test.lua
@@ -0,0 +1,41 @@
+debugstack = debug.traceback
+strmatch = string.match
+
+loadfile("../LibStub.lua")()
+
+local lib, oldMinor = LibStub:NewLibrary("Pants", 1) -- make a new thingy
+assert(lib) -- should return the library table
+assert(not oldMinor) -- should not return the old minor, since it didn't exist
+
+-- the following is to create data and then be able to check if the same data exists after the fact
+function lib:MyMethod()
+end
+local MyMethod = lib.MyMethod
+lib.MyTable = {}
+local MyTable = lib.MyTable
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", 1) -- try to register a library with the same version, should silently fail
+assert(not newLib) -- should not return since out of date
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", 0) -- try to register a library with a previous, should silently fail
+assert(not newLib) -- should not return since out of date
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", 2) -- register a new version
+assert(newLib) -- library table
+assert(rawequal(newLib, lib)) -- should be the same reference as the previous
+assert(newOldMinor == 1) -- should return the minor version of the previous version
+
+assert(rawequal(lib.MyMethod, MyMethod)) -- verify that values were saved
+assert(rawequal(lib.MyTable, MyTable)) -- verify that values were saved
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", "Blah 3 Blah") -- register a new version with a string minor version (instead of a number)
+assert(newLib) -- library table
+assert(newOldMinor == 2) -- previous version was 2
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", "Blah 4 and please ignore 15 Blah") -- register a new version with a string minor version (instead of a number)
+assert(newLib)
+assert(newOldMinor == 3) -- previous version was 3 (even though it gave a string)
+
+local newLib, newOldMinor = LibStub:NewLibrary("Pants", 5) -- register a new library, using a normal number instead of a string
+assert(newLib)
+assert(newOldMinor == 4) -- previous version was 4 (even though it gave a string)
\ No newline at end of file
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test2.lua b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test2.lua
new file mode 100644
index 0000000..eae7172
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test2.lua
@@ -0,0 +1,27 @@
+debugstack = debug.traceback
+strmatch = string.match
+
+loadfile("../LibStub.lua")()
+
+for major, library in LibStub:IterateLibraries() do
+	-- check that MyLib doesn't exist yet, by iterating through all the libraries
+	assert(major ~= "MyLib")
+end
+
+assert(not LibStub:GetLibrary("MyLib", true)) -- check that MyLib doesn't exist yet by direct checking
+assert(not pcall(LibStub.GetLibrary, LibStub, "MyLib")) -- don't silently fail, thus it should raise an error.
+local lib = LibStub:NewLibrary("MyLib", 1) -- create the lib
+assert(lib) -- check it exists
+assert(rawequal(LibStub:GetLibrary("MyLib"), lib)) -- verify that :GetLibrary("MyLib") properly equals the lib reference
+
+assert(LibStub:NewLibrary("MyLib", 2))	-- create a new version
+
+local count=0
+for major, library in LibStub:IterateLibraries() do
+	-- check that MyLib exists somewhere in the libraries, by iterating through all the libraries
+	if major == "MyLib" then -- we found it!
+		count = count +1
+		assert(rawequal(library, lib)) -- verify that the references are equal
+	end
+end
+assert(count == 1) -- verify that we actually found it, and only once
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test3.lua b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test3.lua
new file mode 100644
index 0000000..30f7b94
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test3.lua
@@ -0,0 +1,14 @@
+debugstack = debug.traceback
+strmatch = string.match
+
+loadfile("../LibStub.lua")()
+
+local proxy = newproxy() -- non-string
+
+assert(not pcall(LibStub.NewLibrary, LibStub, proxy, 1)) -- should error, proxy is not a string, it's userdata
+local success, ret = pcall(LibStub.GetLibrary, proxy, true)
+assert(not success or not ret) -- either error because proxy is not a string or because it's not actually registered.
+
+assert(not pcall(LibStub.NewLibrary, LibStub, "Something", "No number in here")) -- should error, minor has no string in it.
+
+assert(not LibStub:GetLibrary("Something", true)) -- shouldn't've created it from the above statement
\ No newline at end of file
diff --git a/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test4.lua b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test4.lua
new file mode 100644
index 0000000..43eb338
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/Libs/LibStub/tests/test4.lua
@@ -0,0 +1,41 @@
+debugstack = debug.traceback
+strmatch = string.match
+
+loadfile("../LibStub.lua")()
+
+
+-- Pretend like loaded libstub is old and doesn't have :IterateLibraries
+assert(LibStub.minor)
+LibStub.minor = LibStub.minor - 0.0001
+LibStub.IterateLibraries = nil
+
+loadfile("../LibStub.lua")()
+
+assert(type(LibStub.IterateLibraries)=="function")
+
+
+-- Now pretend that we're the same version -- :IterateLibraries should NOT be re-created
+LibStub.IterateLibraries = 123
+
+loadfile("../LibStub.lua")()
+
+assert(LibStub.IterateLibraries == 123)
+
+
+-- Now pretend that a newer version is loaded -- :IterateLibraries should NOT be re-created
+LibStub.minor = LibStub.minor + 0.0001
+
+loadfile("../LibStub.lua")()
+
+assert(LibStub.IterateLibraries == 123)
+
+
+-- Again with a huge number
+LibStub.minor = LibStub.minor + 1234567890
+
+loadfile("../LibStub.lua")()
+
+assert(LibStub.IterateLibraries == 123)
+
+
+print("OK")
\ No newline at end of file
diff --git a/vendor/LibGroupInSpecT-1.1/lib.xml b/vendor/LibGroupInSpecT-1.1/lib.xml
new file mode 100644
index 0000000..b292e7e
--- /dev/null
+++ b/vendor/LibGroupInSpecT-1.1/lib.xml
@@ -0,0 +1,3 @@
+<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd">
+<Script file="LibGroupInSpecT-1.1.lua"/>
+</Ui>
diff --git a/vendor/vendor_include.xml b/vendor/vendor_include.xml
index c1fa7ae..0ecd64c 100644
--- a/vendor/vendor_include.xml
+++ b/vendor/vendor_include.xml
@@ -17,8 +17,16 @@
 	<Include file="Ace3\AceTimer-3.0\AceTimer-3.0.xml"/>
 	<Include file="Ace3\AceConfig-3.0\AceConfig-3.0.xml"/>

-	<!-- http://www.wowace.com/addons/libitemupgradeinfo-1-0/ -->
+	<!--
+		http://www.wowace.com/addons/libitemupgradeinfo-1-0/
+		Provides information about item upgrades applied to items.
+	-->
 	<Include file="LibItemUpgradeInfo-1.0\LibItemUpgradeInfo-1.0.xml" />

+	<!--
+		http://www.wowace.com/addons/libgroupinspect/
+		Keeps track of group members specialization and talents.
+	-->
+	<Include file="LibGroupInSpecT-1.1\lib.xml" />
 <!--@end-no-lib-strip@-->
 </Ui>
\ No newline at end of file