Quantcast

Merge branch 'master' into mop

F16Gaming [08-23-12 - 22:07]
Merge branch 'master' into mop

Conflicts:
	BattleNetTools.lua (RESOLVED)
Filename
BattleNetTools.lua
ChatManager.lua
Command.lua
CommandManager.lua
Events.lua
GroupTools.lua
Number.lua
PlayerManager.lua
RoleManager.lua
SummonManager.lua
load.xml
locales/enUS.lua
locales/svSE.lua
diff --git a/BattleNetTools.lua b/BattleNetTools.lua
index 58a9776..98094ad 100644
--- a/BattleNetTools.lua
+++ b/BattleNetTools.lua
@@ -19,6 +19,7 @@

 -- Upvalues
 local select = select
+local tinsert = table.insert
 local tonumber = tonumber

 -- API Upvalues
@@ -158,30 +159,50 @@ function BNT:GetFriendById(id)
 	return ParseBNFriendResult(BNGetFriendInfoByID(id))
 end

-function BNT:GetFriendByName(name)
+function BNT:GetFriendByName(name, startIndex)
 	local n = BNGetNumFriends()
 	if n <= 0 then return nil end
-	for i = 1, n do
+	for i = startIndex or 1, n do
 		local friend = ParseBNFriendResult(BNGetFriendInfo(i))
 		if (friend.ToonName or ""):lower() == name:lower() then
-			return friend
+			return friend, i
 		end
 	end
 	return nil
 end

+function BNT:GetAllFriendsByName(name)
+	local result = {}
+	local friend, lastIndex
+	repeat
+		friend, lastIndex = self:GetFriendByName(name, (lastIndex or 0) + 1)
+		if friend then tinsert(result, friend) end
+	until friend == nil
+	return result
+end
+
 function BNT:GetToon(id)
 	return ParseBNToonResult(BNGetToonInfo(id))
 end

-function BNT:GetToonByName(name)
+function BNT:GetToonByName(name, startIndex)
 	local numF = BNGetNumFriends()
 	if numF <= 0 then return nil end
-	for i = 1, numF do
+	for i = startIndex or 1, numF do
 		for t = 1, BNGetNumFriendToons(i) do
 			local toon = ParseBNToonResult(BNGetFriendToonInfo(i, t))
-			if not CES:IsNullOrEmpty(toon.Name) and toon.Name:lower() == name:lower() then return toon end
+			if not CES:IsNullOrEmpty(toon.Name) and toon.Name:lower() == name:lower() then return toon, i end
 		end
 	end
 	return nil
 end
+
+function BNT:GetAllToonsByName(name)
+	local result = {}
+	local toon, lastIndex
+	repeat
+		toon, lastIndex = self:GetToonByName(name, (lastIndex or 0) + 1)
+		if toon then tinsert(result, toon) end
+	until toon == nil
+	return result
+end
diff --git a/ChatManager.lua b/ChatManager.lua
index 0a0bb08..b39dc76 100644
--- a/ChatManager.lua
+++ b/ChatManager.lua
@@ -68,6 +68,7 @@ local PM = C.PlayerManager
 local L = C.LocaleManager
 local GT = C.GroupTools
 local AC = C.AddonComm
+local BNT = C.BattleNetTools
 local CCM = C.CommandManager
 local CES = C.Extensions.String

@@ -211,8 +212,19 @@ function CM:HandleMessage(msg, sender, channel, target, sourceChannel, isBN, pID
 			table.insert(t, args[i])
 		end
 	end
-	local player = PM:GetOrCreatePlayer(sender)
-	local result, arg, errArg = CCM:HandleCommand(cmd, t, sourceChannel, player)
+	local realm = GetRealmName()
+	local bnetInfo
+	if isBN then
+		local toon = BNT:GetToon(pID)
+		if toon then
+			realm = toon.Realm
+		end
+		bnetInfo = {
+			PresenceID = pID
+		}
+	end
+	local player = PM:GetOrCreatePlayer(sender, realm)
+	local result, arg, errArg = CCM:HandleCommand(cmd, t, sourceChannel, player, bnetInfo)
 	if isBN then
 		target = pID
 		sender = pID
diff --git a/Command.lua b/Command.lua
index 6ae5665..fc40f17 100644
--- a/Command.lua
+++ b/Command.lua
@@ -58,6 +58,7 @@ local DM
 local SM
 local IM
 local CDM
+local CRM
 local log

 --- Initialize Command.
@@ -76,6 +77,7 @@ function C:Init()
 	SM = self.SummonManager
 	IM = self.InviteManager
 	CDM = self.DuelManager
+	CRM = self.RoleManager
 	log = self.Logger
 	self:LoadSavedVars()
 	log:Normal(L("ADDON_LOAD"))
@@ -117,6 +119,7 @@ function C:LoadSavedVars()
 	SM:Init()
 	IM:Init()
 	CDM:Init()
+	CRM:Init()
 	Cmd:Init()
 	log:SetDebug(self.Settings.DEBUG)
 	self.Global.VERSION = self.VarVersion
diff --git a/CommandManager.lua b/CommandManager.lua
index 8ce386f..7d6f747 100644
--- a/CommandManager.lua
+++ b/CommandManager.lua
@@ -70,6 +70,7 @@ local DM = C.DeathManager
 local SM = C.SummonManager
 local IM = C.InviteManager
 local CDM = C.DuelManager
+local CRM = C.RoleManager
 local Chat
 local CES = C.Extensions.String
 local CET = C.Extensions.Table
@@ -167,10 +168,11 @@ end
 -- @param args Table with arguments for the command.
 -- @param isChat Is the command called from chat?
 -- @param player Player object of the calling player (if chat)
+-- @param bnetInfo Information about the B.Net sender (if called from B.Net chat)
 -- @return If successfull, returns result, otherwise false.
 -- @return Error message if not successful, otherwise nil.
 --
-function CM:HandleCommand(command, args, isChat, player)
+function CM:HandleCommand(command, args, isChat, player, bnetInfo)
 	command = tostring(command):lower()
 	local cmd = self:GetCommand(command)
 	if cmd then
@@ -181,7 +183,11 @@ function CM:HandleCommand(command, args, isChat, player)
 				return false, "CM_ERR_NOACCESS", {player.Info.Name, cmd.Access, PM:GetAccess(player)}
 			end
 		end
-		return cmd.Call(args, player, isChat)
+		local result, arg, errArg = cmd.Call(args, player, isChat, bnetInfo)
+		if type(result) == "nil" then
+			return "CM_ERR_UNKNOWN"
+		end
+		return result, arg, errArg
 	else
 		return false, "CM_ERR_NOTREGGED", {tostring(command)}
 	end
@@ -205,7 +211,7 @@ function CM:GetHelp(cmd)
 end


-CM:Register({"__DEFAULT__"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"__DEFAULT__"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat then
 		return "CM_DEFAULT_CHAT"
 	end
@@ -214,7 +220,7 @@ CM:Register({"__DEFAULT__"}, PM.Access.Local, function(args, sender, isChat)
 	return "CM_DEFAULT_END"
 end, "CM_DEFAULT_HELP")

-CM:Register({"help", "h"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"help", "h"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		if isChat then
 			return "CM_DEFAULT_CHAT"
@@ -224,7 +230,7 @@ CM:Register({"help", "h"}, PM.Access.Groups.User.Level, function(args, sender, i
 	return CM:GetHelp(tostring(args[1]):lower())
 end, "CM_HELP_HELP")

-CM:Register({"commands", "cmds", "cmdlist", "listcmds", "listcommands", "commandlist"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"commands", "cmds", "cmdlist", "listcmds", "listcommands", "commandlist"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	local all
 	if #args > 0 then
 		all = args[1] == "all"
@@ -233,11 +239,11 @@ CM:Register({"commands", "cmds", "cmdlist", "listcmds", "listcommands", "command
 	return "RAW_TABLE_OUTPUT", CES:Fit(cmds, 240, ", ") -- Max length is 255, "[Command] " takes up 10. This leaves us with 5 characters grace.
 end, "CM_COMMANDS_HELP")

-CM:Register({"version", "ver", "v"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"version", "ver", "v"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	return "CM_VERSION", {C.Version}
 end, "CM_VERSION_HELP")

-CM:Register({"set", "s"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"set", "s"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_SET_USAGE"
 	end
@@ -405,7 +411,7 @@ CM:Register({"set", "s"}, PM.Access.Groups.Admin.Level, function(args, sender, i
 	return false, "CM_SET_USAGE"
 end, "CM_SET_HELP")

-CM:Register({"locale", "loc"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"locale", "loc"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat then return false, "CM_ERR_NOCHAT" end
 	if #args <= 0 then
 		return "CM_LOCALE_CURRENT", {L.Settings.LOCALE}
@@ -432,7 +438,7 @@ CM:Register({"locale", "loc"}, PM.Access.Local, function(args, sender, isChat)
 	return false, "CM_LOCALE_USAGE"
 end, "CM_LOCALE_HELP")

-CM:Register({"mylocale", "ml"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"mylocale", "ml"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
@@ -451,7 +457,7 @@ CM:Register({"mylocale", "ml"}, PM.Access.Groups.User.Level, function(args, send
 	end
 end, "CM_MYLOCALE_HELP")

-CM:Register({"lock", "lockdown"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"lock", "lockdown"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if type(args[1]) == "string" then
 		return PM:SetLocked(PM:GetOrCreatePlayer(args[1]), true)
 	else
@@ -459,7 +465,7 @@ CM:Register({"lock", "lockdown"}, PM.Access.Groups.Admin.Level, function(args, s
 	end
 end, "CM_LOCK_HELP")

-CM:Register({"unlock", "open"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"unlock", "open"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if type(args[1]) == "string" then
 		return PM:SetLocked(PM:GetOrCreatePlayer(args[1]), false)
 	else
@@ -467,7 +473,7 @@ CM:Register({"unlock", "open"}, PM.Access.Groups.Admin.Level, function(args, sen
 	end
 end, "CM_UNLOCK_HELP")

-CM:Register({"getaccess"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"getaccess"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if type(args[1]) == "string" then
 		local player = PM:GetOrCreatePlayer(args[1])
 		return "CM_GETACCESS_STRING", {player.Info.Name, PM.Access.Groups[player.Info.Group].Level, player.Info.Group}
@@ -476,7 +482,7 @@ CM:Register({"getaccess"}, PM.Access.Groups.User.Level, function(args, sender, i
 	end
 end, "CM_GETACCESS_HELP")

-CM:Register({"setaccess"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"setaccess"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if #args < 1 then
 		return false, "CM_SETACCESS_USAGE"
 	end
@@ -488,7 +494,7 @@ CM:Register({"setaccess"}, PM.Access.Local, function(args, sender, isChat)
 	end
 end, "CM_SETACCESS_HELP")

-CM:Register({"owner"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"owner"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat then
 		return false, "CM_ERR_NOCHAT"
 	end
@@ -496,7 +502,7 @@ CM:Register({"owner"}, PM.Access.Local, function(args, sender, isChat)
 	return PM:SetOwner(player)
 end, "CM_OWNER_HELP")

-CM:Register({"admin"}, PM.Access.Groups.Owner.Level, function(args, sender, isChat)
+CM:Register({"admin"}, PM.Access.Groups.Owner.Level, function(args, sender, isChat, bnetInfo)
 	if isChat then
 		return false, "CM_ERR_NOCHAT"
 	end
@@ -507,7 +513,7 @@ CM:Register({"admin"}, PM.Access.Groups.Owner.Level, function(args, sender, isCh
 	return PM:SetAdmin(player)
 end, "CM_ADMIN_HELP")

-CM:Register({"op"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"op"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if type(args[1]) == "string" then
 		local player = PM:GetOrCreatePlayer(args[1])
 		return PM:SetOp(player)
@@ -516,7 +522,7 @@ CM:Register({"op"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
 	end
 end, "CM_OP_HELP")

-CM:Register({"user"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"user"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if type(args[1]) == "string" then
 		local player = PM:GetOrCreatePlayer(args[1])
 		return PM:SetUser(player)
@@ -525,7 +531,7 @@ CM:Register({"user"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
 	end
 end, "CM_USER_HELP")

-CM:Register({"ban"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"ban"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_BAN_USAGE"
 	end
@@ -533,7 +539,7 @@ CM:Register({"ban"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat
 	return PM:BanUser(player)
 end, "CM_BAN_HELP")

-CM:Register({"auth", "authenticate", "a"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"auth", "authenticate", "a"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat and isChat ~= "WHISPER" then return false, "CM_ERR_NOCHAT" end
 	if #args < 2 then return false, "CM_AUTH_USAGE" end
 	local arg = tostring(args[1]):lower()
@@ -558,72 +564,76 @@ CM:Register({"auth", "authenticate", "a"}, PM.Access.Local, function(args, sende
 	return false, "CM_AUTH_USAGE"
 end, "CM_AUTH_HELP")

-CM:Register({"authme", "authenticateme", "am"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"authme", "authenticateme", "am"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then return false, "CM_ERR_CHATONLY" end
 	if #args <= 0 then return false, "CM_AUTHME_USAGE" end
 	local pass = tostring(args[1])
 	return AM:Authenticate(sender.Info.Name, pass)
 end, "CM_AUTHME_HELP")

-CM:Register({"accept", "acceptinvite", "acceptinv", "join", "joingroup"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"accept", "acceptinvite", "acceptinv", "join", "joingroup"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not IM:IsEnabled() or not IM:IsGroupEnabled() then
 		return "CM_ERR_DISABLED"
 	end
 	return IM:AcceptGroupInvite()
 end, "CM_ACCEPTINVITE_HELP")

-CM:Register({"decline", "declineinvite", "declineinv", "cancelinvite", "cancelinv"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"decline", "declineinvite", "declineinv", "cancelinvite", "cancelinv"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not IM:IsEnabled() or not IM:IsGroupEnabled() then
 		return "CM_ERR_DISABLED"
 	end
 	return IM:DeclineGroupInvite()
 end, "CM_DECLINEINVITE_HELP")

-CM:Register({"acceptguildinvite", "acceptginvite", "acceptguildinv", "acceptginv"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"acceptguildinvite", "acceptginvite", "acceptguildinv", "acceptginv"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not IM:IsEnabled() or not IM:IsGuildEnabled() then
 		return "CM_ERR_DISABLED"
 	end
 	return IM:AcceptGuildInvite()
 end, "CM_ACCEPTGUILDINVITE_HELP")

-CM:Register({"declineguildinvite", "declineginvite", "declineguildinv", "declineginv"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"declineguildinvite", "declineginvite", "declineguildinv", "declineginv"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not IM:IsEnabled() or not IM:IsGuildEnabled() then
 		return "CM_ERR_DISABLED"
 	end
 	return IM:DeclineGuildInvite()
 end, "CM_DECLINEGUILDINVITE_HELP")

-CM:Register({"invite", "inv"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"invite", "inv"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
+	local pID
+	if bnetInfo then pID = bnetInfo.PresenceID end
 	if type(args[1]) == "string" then
 		local player = PM:GetOrCreatePlayer(args[1])
-		return PM:Invite(player, sender)
+		return PM:Invite(player, sender, pID)
 	else
-		return PM:Invite(sender, sender)
+		return PM:Invite(sender, sender, pID)
 	end
 end, "CM_INVITE_HELP")

-CM:Register({"inviteme", "invme"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"inviteme", "invme"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
-	return PM:Invite(sender, sender)
+	local pID
+	if bnetInfo then pID = bnetInfo.PresenceID end
+	return PM:Invite(sender, sender, pID)
 end, "CM_INVITEME_HELP")

-CM:Register({"blockinvites", "blockinvite", "denyinvites", "denyinvite"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"blockinvites", "blockinvite", "denyinvites", "denyinvite"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
 	return PM:DenyInvites(sender, isChat == "WHISPER" or isChat == "BNET")
 end, "CM_DENYINVITE_HELP")

-CM:Register({"allowinvites", "allowinvite"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"allowinvites", "allowinvite"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
 	return PM:AllowInvites(sender, isChat == "WHISPER" or isChat == "BNET")
 end, "CM_ALLOWINVITE_HELP")

-CM:Register({"kick"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"kick"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_KICK_USAGE"
 	end
@@ -637,28 +647,28 @@ CM:Register({"kick"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
 	return PM:Kick(player, sender, reason, override)
 end, "CM_KICK_HELP")

-CM:Register({"kingme", "givelead"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"kingme", "givelead"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
 	return PM:PromoteToLeader(sender)
 end, "CM_KINGME_HELP")

-CM:Register({"opme", "assistant", "assist"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"opme", "assistant", "assist"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
 	return PM:PromoteToAssistant(sender)
 end, "CM_OPME_HELP")

-CM:Register({"deopme", "deassistant", "deassist"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"deopme", "deassistant", "deassist"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if not isChat then
 		return false, "CM_ERR_CHATONLY"
 	end
 	return PM:DemoteAssistant(sender)
 end, "CM_DEOPME_HELP")

-CM:Register({"leader", "lead"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"leader", "lead"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_LEADER_USAGE"
 	end
@@ -666,7 +676,7 @@ CM:Register({"leader", "lead"}, PM.Access.Groups.Op.Level, function(args, sender
 	return PM:PromoteToLeader(player)
 end, "CM_LEADER_HELP")

-CM:Register({"promote"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"promote"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_PROMOTE_USAGE"
 	end
@@ -674,7 +684,7 @@ CM:Register({"promote"}, PM.Access.Groups.Op.Level, function(args, sender, isCha
 	return PM:PromoteToAssistant(player)
 end, "CM_PROMOTE_HELP")

-CM:Register({"demote"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"demote"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_DEMOTE_USAGE"
 	end
@@ -682,7 +692,7 @@ CM:Register({"demote"}, PM.Access.Groups.Op.Level, function(args, sender, isChat
 	return PM:DemoteAssistant(player)
 end, "CM_DEMOTE_HELP")

-CM:Register({"queue", "q"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"queue", "q"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return false, "CM_QUEUE_USAGE"
 	end
@@ -703,7 +713,7 @@ CM:Register({"queue", "q"}, PM.Access.Groups.User.Level, function(args, sender,
 	return QM:Queue(index)
 end, "CM_QUEUE_HELP")

-CM:Register({"leavelfg", "cancellfg", "cancel", "leavelfd", "cancellfd"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"leavelfg", "cancellfg", "cancel", "leavelfd", "cancellfd"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not QM.QueuedByCommand then
 		return false, "CM_LEAVELFG_FAIL"
 	end
@@ -712,7 +722,7 @@ end, "CM_LEAVELFG_HELP")

 -- So apparently Blizzard does not allow accepting invites without HW event... Making this command useless...
 -- I'm keeping this command here for the future, if there will ever be a way to make this work.
-CM:Register({"acceptlfg", "acceptlfd", "joinlfg", "joinlfd"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"acceptlfg", "acceptlfd", "joinlfg", "joinlfd"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not C.Settings.DEBUG then
 		return false, "CM_ERR_PERMDISABLED"
 	end
@@ -725,7 +735,7 @@ CM:Register({"acceptlfg", "acceptlfd", "joinlfg", "joinlfd"}, PM.Access.Groups.U
 	return QM:Accept()
 end, "CM_ACCEPTLFG_HELP")

-CM:Register({"convert", "conv"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"convert", "conv"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if GT:IsLFGGroup() then
 		return false, "CM_CONVERT_LFG"
 	elseif not GT:IsGroup() then
@@ -754,18 +764,18 @@ CM:Register({"convert", "conv"}, PM.Access.Groups.Op.Level, function(args, sende
 	return false, "CM_CONVERT_INVALID"
 end, "CM_CONVERT_HELP")

-CM:Register({"list"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"list"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if not args[1] then
 		return false, "CM_LIST_USAGE"
 	end
 	return PM:ListToggle(args[1]:lower())
 end, "CM_LIST_HELP")

-CM:Register({"listmode", "lm", "lmode"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"listmode", "lm", "lmode"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	return PM:ToggleListMode()
 end, "CM_LISTMODE_HELP")

-CM:Register({"groupallow", "gallow"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"groupallow", "gallow"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_GROUPALLOW_USAGE"
 	end
@@ -774,7 +784,7 @@ CM:Register({"groupallow", "gallow"}, PM.Access.Groups.Admin.Level, function(arg
 	return PM:GroupAccess(group, cmd, true)
 end, "CM_GROUPALLOW_HELP")

-CM:Register({"groupdeny", "gdeny", "deny"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"groupdeny", "gdeny", "deny"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_GROUPDENY_USAGE"
 	end
@@ -783,7 +793,7 @@ CM:Register({"groupdeny", "gdeny", "deny"}, PM.Access.Groups.Admin.Level, functi
 	return PM:GroupAccess(group, cmd, false)
 end, "CM_GROUPDENY_HELP")

-CM:Register({"resetgroupaccess", "groupaccessreset", "removegroupaccess", "groupaccessremove", "rga", "gar"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"resetgroupaccess", "groupaccessreset", "removegroupaccess", "groupaccessremove", "rga", "gar"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_RESETGROUPACCESS_USAGE"
 	end
@@ -792,7 +802,7 @@ CM:Register({"resetgroupaccess", "groupaccessreset", "removegroupaccess", "group
 	return PM:GroupAccessRemove(group, cmd)
 end, "CM_RESETGROUPACCESS_HELP")

-CM:Register({"userallow", "uallow"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"userallow", "uallow"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_USERALLOW_USAGE"
 	end
@@ -801,7 +811,7 @@ CM:Register({"userallow", "uallow"}, PM.Access.Groups.Admin.Level, function(args
 	return PM:PlayerAccess(player, cmd, true)
 end, "CM_USERALLOW_HELP")

-CM:Register({"userdeny", "udeny"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"userdeny", "udeny"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_USERDENY_USAGE"
 	end
@@ -810,7 +820,7 @@ CM:Register({"userdeny", "udeny"}, PM.Access.Groups.Admin.Level, function(args,
 	return PM:PlayerAccess(player, cmd, false)
 end, "CM_USERDENY_HELP")

-CM:Register({"resetuseraccess", "useraccessreset", "removeuseraccess", "useraccessremove", "rua", "uar"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat)
+CM:Register({"resetuseraccess", "useraccessreset", "removeuseraccess", "useraccessremove", "rua", "uar"}, PM.Access.Groups.Admin.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 1 then
 		return false, "CM_RESETUSERACCESS_USAGE"
 	end
@@ -819,21 +829,21 @@ CM:Register({"resetuseraccess", "useraccessreset", "removeuseraccess", "useracce
 	return PM:PlayerAccessRemove(player, cmd)
 end, "CM_RESETUSERACCESS_HELP")

-CM:Register({"toggle", "t"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"toggle", "t"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat then
 		return false, "CM_ERR_NOCHAT"
 	end
 	return C:Toggle()
 end, "CM_TOGGLE_HELP")

-CM:Register({"toggledebug", "td", "debug", "d"}, PM.Access.Local, function(args, sender, isChat)
+CM:Register({"toggledebug", "td", "debug", "d"}, PM.Access.Local, function(args, sender, isChat, bnetInfo)
 	if isChat then
 		return false, "CM_ERR_NOCHAT"
 	end
 	return C:ToggleDebug()
 end, "CM_TOGGLEDEBUG_HELP")

-CM:Register({"readycheck", "rc"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"readycheck", "rc"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		if PM:GetAccess(sender) > PM.Access.Groups.Op.Level then
 			return "CM_ERR_NOACCESS", {sender.Info.Name, PM.Access.Groups.Op.Level, PM:GetAccess(sender)}
@@ -873,7 +883,7 @@ CM:Register({"readycheck", "rc"}, PM.Access.Groups.User.Level, function(args, se
 	return false, "CM_READYCHECK_FAIL"
 end, "CM_READYCHECK_HELP")

-CM:Register({"loot", "l"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"loot", "l"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if GT:IsLFGGroup() then
 		return false, "CM_LOOT_LFG"
 	end
@@ -916,7 +926,7 @@ CM:Register({"loot", "l"}, PM.Access.Groups.Op.Level, function(args, sender, isC
 	return false, usage
 end, "CM_LOOT_HELP")

-CM:Register({"roll", "r"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"roll", "r"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if #args <= 0 then
 		return RM:StartRoll(sender.Info.Name)
 	end
@@ -978,7 +988,7 @@ CM:Register({"roll", "r"}, PM.Access.Groups.Op.Level, function(args, sender, isC
 	return false, "CM_LOOT_USAGE"
 end, "CM_LOOT_HELP")

-CM:Register({"raidwarning", "rw", "raid_warning"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"raidwarning", "rw", "raid_warning"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not GT:IsRaid() then
 		return false, "CM_RAIDWARNING_NORAID"
 	elseif not GT:IsRaidLeaderOrAssistant() then
@@ -999,7 +1009,7 @@ CM:Register({"raidwarning", "rw", "raid_warning"}, PM.Access.Groups.User.Level,
 	return "CM_RAIDWARNING_SENT"
 end, "CM_RAIDWARNING_HELP")

-CM:Register({"dungeondifficulty", "dungeondiff", "dd", "dungeonmode", "dm"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"dungeondifficulty", "dungeondiff", "dd"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if #args < 1 then
 		return GT:GetDungeonDifficultyString()
 	end
@@ -1016,7 +1026,7 @@ CM:Register({"dungeondifficulty", "dungeondiff", "dd", "dungeonmode", "dm"}, PM.
 	return GT:SetDungeonDifficulty(diff)
 end, "CM_DUNGEONMODE_HELP")

-CM:Register({"raiddifficulty", "raiddiff", "rd", "raidmode", "rm"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"raiddifficulty", "raiddiff", "rd"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if #args < 1 then
 		return GT:GetRaidDifficultyString()
 	end
@@ -1037,49 +1047,49 @@ CM:Register({"raiddifficulty", "raiddiff", "rd", "raidmode", "rm"}, PM.Access.Gr
 	return GT:SetRaidDifficulty(diff)
 end, "CM_RAIDMODE_HELP")

-CM:Register({"release", "rel"}, PM.Access.Groups.Op.Level, function(args, sender, isChat)
+CM:Register({"release", "rel"}, PM.Access.Groups.Op.Level, function(args, sender, isChat, bnetInfo)
 	if not DM:IsEnabled() or not DM:IsReleaseEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return DM:Release()
 end, "CM_RELEASE_HELP")

-CM:Register({"resurrect", "ressurrect", "ress", "res"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"resurrect", "ressurrect", "ress", "res"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not DM:IsEnabled() or not DM:IsResurrectEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return DM:Resurrect()
 end, "CM_RESURRECT_HELP")

-CM:Register({"acceptsummon", "as", "acceptsumm", "asumm"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"acceptsummon", "as", "acceptsumm", "asumm"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not SM:IsEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return SM:AcceptSummon()
 end, "CM_ACCEPTSUMMON_HELP")

-CM:Register({"declinesummon", "ds", "declinesumm", "dsumm", "cancelsummon", "csumm"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"declinesummon", "ds", "declinesumm", "dsumm", "cancelsummon", "csumm"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not SM:IsEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return SM:DeclineSummon()
 end, "CM_DECLINESUMMON_HELP")

-CM:Register({"acceptduel", "acceptd"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"acceptduel", "acceptd"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not CDM:IsEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return CDM:AcceptDuel()
 end, "CM_ACCEPTDUEL_HELP")

-CM:Register({"declineduel", "declined"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"declineduel", "declined"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not CDM:IsEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
 	return CDM:DeclineDuel()
 end, "CM_DECLINEDUEL_HELP")

-CM:Register({"startduel", "startd", "challenge"}, PM.Access.Groups.User.Level, function(args, sender, isChat)
+CM:Register({"startduel", "startd", "challenge"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
 	if not CDM:IsEnabled() then
 		return false, "CM_ERR_DISABLED"
 	end
@@ -1089,6 +1099,40 @@ CM:Register({"startduel", "startd", "challenge"}, PM.Access.Groups.User.Level, f
 	return CDM:Challenge(args[1])
 end, "CM_STARTDUEL_HELP")

+CM:Register({"role", "rm"}, PM.Access.Groups.User.Level, function(args, sender, isChat, bnetInfo)
+	if not CRM:IsEnabled() then
+		return false, "CM_ERR_DISABLED"
+	end
+	if #args < 1 then
+		return "CM_ROLE_CURRENT", {CRM.Roles[CRM:GetRole()]}
+	end
+	local arg = args[1]:lower()
+	if arg:match("^st") or arg:match("^i") then -- Start/Initiate/Issue
+		return CRM:Start()
+	elseif arg:match("^[sa]") then -- Set/Assign (Role)
+		if #args < 2 then
+			return false, "CM_ROLE_SET_USAGE"
+		end
+		local role = args[2]:lower()
+		if role:match("^[thd]") then
+			return CRM:SetRole(role)
+		end
+		return false, "CM_ROLE_SET_USAGE"
+	elseif arg:match("^c") then -- Confirm (Role)
+		if #args >= 2 then -- Role supplied as argument #2
+			local role = args[2]:lower()
+			if role:match("^[thd]") then
+				return CRM:SetRole(role)
+			end
+			return false, "CM_ROLE_CONFIRM_USAGE"
+		else -- No role specified
+			-- Confirm the role last chosen by the player
+			return CRM:ConfirmRole()
+		end
+	end
+	return false, "CM_ROLE_USAGE"
+end, "CM_ROLE_HELP")
+
 for i,v in ipairs(CM.Slash) do
 	_G["SLASH_" .. C.Name:upper() .. i] = "/" .. v
 end
diff --git a/Events.lua b/Events.lua
index 8e105d0..9d22211 100644
--- a/Events.lua
+++ b/Events.lua
@@ -36,6 +36,7 @@ local AC = C.AddonComm
 local DM = C.DeathManager
 local SM = C.SummonManager
 local IM = C.InviteManager
+local RM = C.RoleManager
 local CDM = C.DuelManager

 --- Event handler for ADDON_LOADED
@@ -155,3 +156,7 @@ end
 function C.Events.DUEL_REQUESTED(self, ...)
 	CDM:OnDuel((select(1, ...)))
 end
+
+function C.Events.ROLE_POLL_BEGIN(self, ...)
+	RM:OnRoleCheck()
+end
diff --git a/GroupTools.lua b/GroupTools.lua
index 1dd3104..a0f50b2 100644
--- a/GroupTools.lua
+++ b/GroupTools.lua
@@ -96,6 +96,14 @@ function GT:IsLFGGroup()
 	return false
 end

+--- Check if player is in a party.
+-- (Returns false if raid)
+-- @return True if player is in a party, false otherwise.
+--
+function GT:IsParty()
+	return UnitExists("party1") and not self:IsRaid() and not self:IsLFGGroup()
+end
+
 --- Check if player is in a raid.
 -- @return True if the player is in a raid, false otherwise.
 --
diff --git a/Number.lua b/Number.lua
new file mode 100644
index 0000000..9255041
--- /dev/null
+++ b/Number.lua
@@ -0,0 +1,45 @@
+--[[
+	* Copyright (c) 2011-2012 by Adam Hellberg.
+	*
+	* This file is part of Command.
+	*
+	* Command is free software: you can redistribute it and/or modify
+	* it under the terms of the GNU General Public License as published by
+	* the Free Software Foundation, either version 3 of the License, or
+	* (at your option) any later version.
+	*
+	* Command is distributed in the hope that it will be useful,
+	* but WITHOUT ANY WARRANTY; without even the implied warranty of
+	* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	* GNU General Public License for more details.
+	*
+	* You should have received a copy of the GNU General Public License
+	* along with Command. If not, see <http://www.gnu.org/licenses/>.
+--]]
+
+-- Upvalues
+local type = type
+
+local C = Command
+
+if type(C.Extensions) ~= "table" then
+	C.Extensions = {}
+end
+
+C.Extensions.Number = {
+	type = "ext" -- For future use
+}
+
+local L = C.LocaleManager
+local CEN = C.Extensions.Number
+
+function CEN:FormatSeconds(seconds)
+	-- Return plain seconds if less than 60 seconds
+	if seconds < 60 then return ("%d %s"):format(seconds, L("SECONDS"):lower()) end
+	local minutes = floor(seconds / 60) -- Get number of minutes
+	local secs = tostring(seconds - minutes * 60) -- Get number of remaining seconds
+	if not secs:match("%d%d") then -- Check if seconds <= 9
+		secs = "0" .. secs -- Prefix a zero to make it look better
+	end
+	return ("%d:%s"):format(minutes, secs) -- Return in m:ss format
+end
diff --git a/PlayerManager.lua b/PlayerManager.lua
index d80559a..bfcede0 100644
--- a/PlayerManager.lua
+++ b/PlayerManager.lua
@@ -232,6 +232,18 @@ function PM:LoadSavedVars()
 	end
 	Players = self.Data.PLAYERS[GetRealmName()]
 	List = self.Data.LIST
+
+	self:UpdatePlayerData()
+end
+
+function PM:UpdatePlayerData()
+	for realm,players in pairs(self.Data.PLAYERS) do
+		for name,player in pairs(players) do
+			if not player.Info.Realm then
+				player.Info.Realm = realm
+			end
+		end
+	end
 end

 function PM:ParseMessage(message)
@@ -261,20 +273,22 @@ end
 -- @param name Name of player.
 -- @return Player from list of players if exists, otherwise a new player object.
 --
-function PM:GetOrCreatePlayer(name)
+function PM:GetOrCreatePlayer(name, realm)
 	name = (name or UnitName("player")):lower():gsub("^%l", string.upper)
-	if CET:HasKey(Players, name) then
-		return Players[name]
+	realm = realm or GetRealmName()
+	if CET:HasKey(self.Data.PLAYERS[realm], name) then
+		return self.Data.PLAYERS[realm][name]
 	else
 		local player = CET:Copy(Player)
 		player.Info.Name = name
+		player.Info.Realm = realm
 		if player.Info.Name == UnitName("player") then
 			player.Info.Group = self.Access.Groups.Owner.Name
 		else
 			player.Info.Group = self.Access.Groups.User.Name
 		end
-		Players[player.Info.Name] = player
-		log:Normal(L("PM_PLAYER_CREATE"):format(player.Info.Name))
+		self.Data.PLAYERS[realm][player.Info.Name] = player
+		log:Normal(L("PM_PLAYER_CREATE"):format(player.Info.Name, player.Info.Realm))
 		return player
 	end
 end
@@ -283,8 +297,8 @@ end
 -- @param player Player object to update.
 --
 function PM:UpdatePlayer(player)
-	Players[player.Info.Name] = player
-	log:Normal(L("PM_PLAYER_UPDATE"):format(player.Info.Name))
+	self.Data.PLAYERS[player.Info.Realm][player.Info.Name] = player
+	log:Normal(L("PM_PLAYER_UPDATE"):format(player.Info.Name, player.Info.Realm))
 end

 --- Completely remove a command from a group's access list.
@@ -603,10 +617,11 @@ end
 -- Also sends a message to the invited player about the event.
 -- @param player Player object of player to invite.
 -- @param sender Player object of the inviting player.
+-- @param pID Presence ID if this was an Invite(Me) command from B.Net chat
 -- @return String stating the result of the invite, false if error.
 -- @return Error message if unsuccessful, nil otherwise.
 --
-function PM:Invite(player, sender)
+function PM:Invite(player, sender, pID)
 	if not sender then sender = self:GetOrCreatePlayer(UnitName("player")) end
 	if player.Info.Name == UnitName("player") then
 		return false, "PM_INVITE_SELF"
@@ -619,7 +634,11 @@ function PM:Invite(player, sender)
 		if self.Invites[player.Info.Name] then
 			return false, "PM_INVITE_ACTIVE", {player.Info.Name}
 		elseif player.Info.Name == sender.Info.Name then
-			InviteUnit(player.Info.Name)
+			if player.Info.Realm == GetRealmName() or not pID then
+				InviteUnit(player.Info.Name)
+			else -- Invite(Me) command sent from B.Net chat
+				BNInviteFriend(pID)
+			end
 			return "PM_INVITE_NOTIFYTARGET"
 		elseif player.Settings.Invite then
 			InviteUnit(player.Info.Name)
diff --git a/RoleManager.lua b/RoleManager.lua
new file mode 100644
index 0000000..7405ebf
--- /dev/null
+++ b/RoleManager.lua
@@ -0,0 +1,215 @@
+--[[
+	* Copyright (c) 2011-2012 by Adam Hellberg.
+	*
+	* This file is part of Command.
+	*
+	* Command is free software: you can redistribute it and/or modify
+	* it under the terms of the GNU General Public License as published by
+	* the Free Software Foundation, either version 3 of the License, or
+	* (at your option) any later version.
+	*
+	* Command is distributed in the hope that it will be useful,
+	* but WITHOUT ANY WARRANTY; without even the implied warranty of
+	* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	* GNU General Public License for more details.
+	*
+	* You should have received a copy of the GNU General Public License
+	* along with Command. If not, see <http://www.gnu.org/licenses/>.
+--]]
+
+-- Upvalues
+local type = type
+
+-- API Upvalues
+local CreateFrame = CreateFrame
+local UnitSetRole = UnitSetRole
+local UnitGetAvailableRoles = UnitGetAvailableRoles
+local UnitGroupRolesAssigned = UnitGroupRolesAssigned
+
+local C = Command
+
+C.RoleManager = {
+	Roles = {
+		TANK = "CRM_TANK",
+		HEALER = "CRM_HEALER",
+		DAMAGER = "CRM_DAMAGE",
+		NONE = "CRM_UNDEFINED",
+		Tank = "TANK",
+		Healer = "HEALER",
+		Damage = "DAMAGER",
+		Undefined = "NONE"
+	},
+	BlizzRoles = {}
+}
+
+local L = C.LocaleManager
+local RM = C.RoleManager
+local GT = C.GroupTools
+local CM
+local CEN = C.Extensions.Number
+
+local DEFAULT_DELAY = 5
+local MAX_DELAY = 60 -- Arbitrary max delay, unknown if there is an existing delay
+
+function RM:Init()
+	CM = C.ChatManager
+	self:LoadSavedVars()
+end
+
+function RM:LoadSavedVars()
+	if type(C.Global["ROLE_MANAGER"]) ~= "table" then
+		C.Global["ROLE_MANAGER"] = {}
+	end
+
+	self.Settings = C.Global["ROLE_MANAGER"]
+
+	if type(self.Settings.ENABLED) ~= "boolean" then
+		self.Settings.ENABLED = true
+	end
+
+	if type(self.Settings.DELAY) ~= "number" then
+		self.Settings.DELAY = DEFAULT_DELAY
+	end
+end
+
+function RM:OnRoleCheck()
+	if self.Settings.DELAY > 0 then
+		local frame = CreateFrame("Frame")
+		frame.Time = 0
+		frame.Delay = self.Settings.DELAY
+		frame:SetScript("OnUpdate", function(self, elapsed)
+			self.Time = self.Time + elapsed
+			if self.Time >= self.Delay then
+				self:SetScript("OnUpdate", nil)
+				RM:Announce()
+			end
+		end)
+	else
+		self:Announce()
+	end
+end
+
+function RM:Announce()
+	if not self:RolePollActive() then return end
+	local current = self:GetRole()
+	local msg
+	if current ~= self.Roles.Undefined then
+		msg = "CRM_ANNOUNCE_HASROLE"
+	else
+		msg = "CRM_ANNOUNCE_NOROLE"
+	end
+	CM:SendMessage(L(msg):format(L(self.Roles[current])), "SMART")
+end
+
+function RM:SetRole(role)
+	if type(role) ~= "string" then
+		error("RoleManager.SetRole: Argument was of type [" .. type(role) .. "], expected [string].")
+		return nil
+	end
+	role = role:lower()
+	local result = self.Roles.Damage
+	if role:match("^t") then
+		result = self.Roles.Tank
+	elseif role:match("^h") then
+		result = self.Roles.Healer
+	end
+	if not self:CanBeRole(result) then
+		return false, "CRM_SET_INVALID", {L(self.Roles[result])}
+	end
+	UnitSetRole("player", result)
+	if self:RolePollActive() then
+		RolePollPopup:Hide()
+	end
+	return "CRM_SET_SUCCESS", {L(self.Roles[result])}
+end
+
+function RM:GetRole()
+	local role = UnitGroupRolesAssigned("player")
+	if role == "TANK" then
+		return self.Roles.Tank
+	elseif role == "HEALER" then
+		return self.Roles.Healer
+	elseif role == "DAMAGER" then
+		return self.Roles.Damage
+	end
+	return self.Roles.Undefined
+end
+
+function RM:ConfirmRole()
+	local role = self:GetRole()
+	if role == self.Roles.Undefined then
+		return false, "CRM_CONFIRM_NOROLE"
+	end
+	return self:SetRole(role)
+end
+
+function RM:CanBeRole(role)
+	if type(role) ~= "string" then
+		error("RoleManager.CanBeRole: Argument was of type [" .. type(role) .. "], expected [string].")
+		return nil
+	end
+	role = role:lower()
+	local tank, healer, dps = UnitGetAvailableRoles("player")
+	if role:match("^t") then
+		return tank
+	elseif role:match("^h") then
+		return healer
+	end
+	return dps
+end
+
+function RM:RolePollActive()
+	return RolePollPopup and RolePollPopup:IsShown()
+end
+
+function RM:Start()
+	if self:RolePollActive() then
+		return false, "CRM_START_ACTIVE"
+	elseif (GT:IsParty() and GT:IsGroupLeader()) or (GT:IsRaid() and GT:IsRaidLeaderOrAssistant()) then
+
+	else
+		return false, "CRM_START_NOPRIV"
+	end
+end
+
+function RM:Enable()
+	self.Settings.ENABLED = true
+	return "CRM_ENABLED"
+end
+
+function RM:Disable()
+	self.Settings.ENABLED = false
+	return "CRM_DISABLED"
+end
+
+function RM:Toggle()
+	if self:IsEnabled() then
+		return self:Disable()
+	end
+	return self:Enable()
+end
+
+function RM:IsEnabled()
+	return self.Settings.ENABLED
+end
+
+function RM:GetDelay()
+	return CEN:FormatSeconds(self:GetRawDelay())
+end
+
+function RM:GetRawDelay()
+	return self.Settings.DELAY
+end
+
+function RM:SetDelay(amount)
+	if amount < 0 then
+		amount = 0
+	elseif amount > MAX_DELAY then
+		amount = MAX_DELAY
+	end
+	self.Settings.DELAY = amount
+	if amount > 0 then
+		return "CRM_SETDELAY_SUCCESS", {CEN:FormatSeconds(amount)}
+	end
+	return "CRM_SETDELAY_INSTANT"
+end
diff --git a/SummonManager.lua b/SummonManager.lua
index 380f825..3a8c54a 100644
--- a/SummonManager.lua
+++ b/SummonManager.lua
@@ -35,28 +35,21 @@ local GetSummonConfirmTimeLeft = GetSummonConfirmTimeLeft

 local C = Command

-C.SummonManager = {}
+C.SummonManager = {
+	VarVersion = 1
+}

 local L = C.LocaleManager
 local SM = C.SummonManager
 local CM
 local GT = C.GroupTools
+local CEN = C.Extensions.Number

+local DEFAULT_DELAY = 5
 local MAX_DELAY = 110 -- 1 minute 50 seconds, summons expire after 2 minutes (usually)

 local LastSummoner

-local function FormatSeconds(seconds)
-	-- Return plain seconds if less than 60 seconds
-	if seconds < 60 then return ("%d %s"):format(seconds, L("SECONDS"):lower()) end
-	local minutes = floor(seconds / 60) -- Get number of minutes
-	local secs = tostring(seconds - minutes * 60) -- Get number of remaining seconds
-	if not secs:match("%d%d") then -- Check if seconds > 9
-		secs = "0" .. secs -- Prefix a zero to make it look better
-	end
-	return ("%d:%s"):format(minutes, secs) -- Return in m:ss format
-end
-
 function SM:Init()
 	CM = C.ChatManager
 	LastSummoner = L("UNKNOWN")
@@ -70,22 +63,28 @@ function SM:LoadSavedVars()

 	self.Settings = C.Global["SUMMON_MANAGER"]

+	if not self.Settings.VERSION or self.Settings.VERSION < self.VarVersion then
+		wipe(self.Settings)
+	end
+
 	if type(self.Settings.ENABLED) ~= "boolean" then
 		self.Settings.ENABLED = true
 	end

-	if type(self.Settings.TIME) ~= "number" then
-		self.Settings.TIME = 0
+	if type(self.Settings.DELAY) ~= "number" then
+		self.Settings.DELAY = DEFAULT_DELAY
 	end
+
+	self.Settings.VERSION = self.VarVersion
 end

 function SM:OnSummon()
 	if self.DelayActive then return end
-	if self.Settings.TIME > 0 then
+	if self.Settings.DELAY > 0 then
 		self.DelayActive = true
 		local frame = CreateFrame("Frame")
 		frame.Time = 0 -- Current time
-		frame.Delay = self.Settings.TIME -- Delay to wait
+		frame.Delay = self.Settings.DELAY -- Delay to wait
 		frame:SetScript("OnUpdate", function(self, elapsed)
 			self.Time = self.Time + elapsed
 			if self.Time >= self.Delay then
@@ -186,7 +185,7 @@ function SM:SetDelay(amount)
 	end
 	self.Settings.DELAY = amount
 	if amount > 0 then
-		return "SM_SETDELAY_SUCCESS", {FormatSeconds(amount)}
+		return "SM_SETDELAY_SUCCESS", {CEN:FormatSeconds(amount)}
 	end
 	return "SM_SETDELAY_INSTANT"
 end
diff --git a/load.xml b/load.xml
index e52cdbb..4a6775c 100644
--- a/load.xml
+++ b/load.xml
@@ -23,6 +23,7 @@
 	<Script file="Table.lua" />
 	<Script file="String.lua" />
 	<Include file="LocaleLoader.xml" />
+	<Script file="Number.lua" />
 	<Script file="Logger.lua" />
 	<Script file="BattleNetTools.lua" />
 	<Script file="GroupTools.lua" />
@@ -36,6 +37,7 @@
 	<Script file="SummonManager.lua" />
 	<Script file="InviteManager.lua" />
 	<Script file="DuelManager.lua" />
+	<Script file="RoleManager.lua" />
 	<Script file="CommandManager.lua" />
 	<Script file="ChatManager.lua" />
 	<Script file="Events.lua" />
diff --git a/locales/enUS.lua b/locales/enUS.lua
index fbb9852..b536361 100644
--- a/locales/enUS.lua
+++ b/locales/enUS.lua
@@ -90,6 +90,7 @@ local L = {
 	-- CommandManager --
 	--------------------

+	CM_ERR_UNKNOWN = "Unknown error occurred, please contact addon author.",
 	CM_ERR_NOTALLOWED = "%s is not allowed to be used, %s.",
 	CM_ERR_NOACCESS = "You do not have permission to use that command, %s. Required access level: %d. Your access level: %d.",
 	CM_ERR_NOTREGGED = "%q is not a registered command.",
@@ -303,6 +304,12 @@ local L = {
 	CM_STARTDUEL_HELP = "Challenges another player to a duel.",
 	CM_STARTDUEL_USAGE = "Usage: startduel <target>",

+	CM_ROLE_HELP = "Provides various commands for controlling role assignment.",
+	CM_ROLE_USAGE = "Usage: role start|set|confirm",
+	CM_ROLE_CURRENT = "My current role is %s.",
+	CM_ROLE_SET_USAGE = "Usage: role set tank|healer|dps",
+	CM_ROLE_CONFIRM_USAGE = "Usage: role confirm [tank|healer|dps]",
+
 	------------
 	-- Events --
 	------------
@@ -394,8 +401,8 @@ local L = {
 	PM_KICK_NOPRIV = "Unable to kick %s from group. Not group leader or assistant.",
 	PM_KICK_TARGETASSIST = "Unable to kick %s, assistants cannot kick other assistants from group.",

-	PM_PLAYER_CREATE = "Created player %q with default settings.",
-	PM_PLAYER_UPDATE = "Updated player %q.",
+	PM_PLAYER_CREATE = "Created player %q (%s) with default settings.",
+	PM_PLAYER_UPDATE = "Updated player %q (%s).",

 	PM_GA_REMOVED = "%q removed from group %s.",
 	PM_GA_EXISTSALLOW = "%q already has that command on the allow list.",
@@ -583,6 +590,33 @@ local L = {
 	CDM_DELAY_DISABLED = "Announce delay has been disabled, will now announce immediately.",

 	-----------------
+	-- RoleManager --
+	-----------------
+
+	CRM_ENABLED = "RoleManager enabled.",
+	CRM_DISABLED = "RoleManager disabled.",
+
+
+	CRM_TANK = "Tank",
+	CRM_HEALER = "Healer",
+	CRM_DAMAGE = "DPS",
+	CRM_UNDEFINED = "Undefined",
+
+	CRM_ANNOUNCE_HASROLE = "Role check started! Confirm my current role (%s) with !role confirm or set a new one with !role confirm tank|healer|dps",
+	CRM_ANNOUNCE_NOROLE = "Role check started! Set my role with !role confirm tank|healer|dps",
+
+	CRM_SET_INVALID = "I cannot fulfill the role of %s, please specify another role.",
+	CRM_SET_SUCCESS = "Successfully set my role to %s!",
+
+	CRM_CONFIRM_NOROLE = "No role set, confirmation needed with role confirm tank|healer|dps",
+
+	CRM_START_ACTIVE = "A role check is already pending, please wait until it has ended.",
+	CRM_START_NOPRIV = "Unable to start role check, not leader or assistant.",
+
+	CRM_STARTDELAY_SUCCESS = "RoleManager announce delay set to %s!",
+	CRM_STARTDELAY_INSTANT = "RoleManager now announces instantly.",
+
+	-----------------
 	-- AuthManager --
 	-----------------

diff --git a/locales/svSE.lua b/locales/svSE.lua
index 850648e..8854ca0 100644
--- a/locales/svSE.lua
+++ b/locales/svSE.lua
@@ -90,6 +90,7 @@ local L = {
 	-- CommandManager --
 	--------------------

+	CM_ERR_UNKNOWN = "Okänt fel inträffade, kontakta addonskapare.",
 	CM_ERR_NOTALLOWED = "%s är inte tillåtet att användas, %s.",
 	CM_ERR_NOACCESS = "Du har inte tillåtelse att använda det kommandot, %s. Behörighet som krävs: %d. Din behörighet: %d.",
 	CM_ERR_NOTREGGED = "%q är inte ett registrerat kommando.",
@@ -303,6 +304,12 @@ local L = {
 	CM_STARTDUEL_HELP = "Challenges another player to a duel.",
 	CM_STARTDUEL_USAGE = "Usage: startduel <target>",

+	CM_ROLE_HELP = "Provides various commands for controlling role assignment.",
+	CM_ROLE_USAGE = "Användning: role start|set|confirm",
+	CM_ROLE_CURRENT = "My current role is %s.",
+	CM_ROLE_SET_USAGE = "Användning: role set tank|healer|dps",
+	CM_ROLE_CONFIRM_USAGE = "Usage: role confirm [tank|healer|dps]",
+
 	------------
 	-- Events --
 	------------
@@ -394,8 +401,8 @@ local L = {
 	PM_KICK_NOPRIV = "Unable to kick %s from group. Not group leader or assistant.",
 	PM_KICK_TARGETASSIST = "Unable to kick %s, assistants cannot kick other assistants from group.",

-	PM_PLAYER_CREATE = "Created player %q with default settings.",
-	PM_PLAYER_UPDATE = "Updated player %q.",
+	PM_PLAYER_CREATE = "Created player %q (%s) with default settings.",
+	PM_PLAYER_UPDATE = "Updated player %q (%s).",

 	PM_GA_REMOVED = "%q removed from group %s.",
 	PM_GA_EXISTSALLOW = "%q already has that command on the allow list.",
@@ -583,6 +590,33 @@ local L = {
 	CDM_DELAY_DISABLED = "Announce delay has been disabled, will now announce immediately.",

 	-----------------
+	-- RoleManager --
+	-----------------
+
+	CRM_ENABLED = "RoleManager enabled.",
+	CRM_DISABLED = "RoleManager disabled.",
+
+
+	CRM_TANK = "Tank",
+	CRM_HEALER = "Healer",
+	CRM_DAMAGE = "DPS",
+	CRM_UNDEFINED = "Undefined",
+
+	CRM_ANNOUNCE_HASROLE = "Role check started! Confirm my current role (%s) with !role confirm or set a new one with !role confirm tank|healer|dps",
+	CRM_ANNOUNCE_NOROLE = "Role check started! Set my role with !role confirm tank|healer|dps",
+
+	CRM_SET_INVALID = "I cannot fulfill the role of %s, please specify another role.",
+	CRM_SET_SUCCESS = "Successfully set my role to %s!",
+
+	CRM_CONFIRM_NOROLE = "No role set, confirmation needed with role confirm tank|healer|dps",
+
+	CRM_START_ACTIVE = "A role check is already pending, please wait until it has ended.",
+	CRM_START_NOPRIV = "Unable to start role check, not leader or assistant.",
+
+	CRM_STARTDELAY_SUCCESS = "RoleManager announce delay set to %s!",
+	CRM_STARTDELAY_INSTANT = "RoleManager now announces instantly.",
+
+	-----------------
 	-- AuthManager --
 	-----------------