MARS Group System

From Multiverse

Jump to: navigation, search

Contents

Introduction

The MARS group system enables you to create groups for quests, voice chat, and other purposes. This article provides a basic overview using and configuring the MARS group system. It describes how to work with the group system on the server side and provides an example of implementing grouping on the client side.

This article is not intended to provide a step-by-step process of implementing the grouping system. You must already know how to work with server plug-ins, and use client scripting.

All examples covered in this article are included with the Sampleworld assets.

Server implementation

Overview

The group system server plug-in is defined in the GroupPlugin class. This class handles messages for inviting players to groups, responses to invitations, group-specific chat messages, group member removal, property updates and logout messages. Additionally, the GroupClient class contains static properties for message types and extension messages for grouping.

Groups, group members, and group leaders

Two API objects define groups:

  • MarsGroup - contains information about an existing group, including a unique identifier, the OID of the group leader, and information about each member of the group. Also has methods that you can use to add and remove group members. These methods are used by the MarsGroupPlugin to manage the group the make-up.
  • MarsGroupMember - contains information about an individual member of a group.

NOTE: the group leader is determined by the order that characters joined the group, and initially will be the character the formed the group. That is, when someone forms a group by inviting another person to join it, the inviter is added to the group first, and therefore anointed the group leader. If the leader leaves the group, the next person who joined the group automatically becomes the group leader, and so on.

The MarsGroupMember object is added to a sub-collection contained on the associated MarsGroup object that it belongs to. MarsGroupMember holds basic information that is sent to the client application such as a unique OID, the player name, and a collection of MarsStats values as defined by the group configuration file that will be discussed in the next section.

The CombatInfo class includes a flag to determine if the entity is grouped or not. For example you could write the following Python code to check group status:

if cInfo.isGrouped():
	... <do something>

Messages

To initiate the creation of a group, send a group invitation message (GroupClient.MSG_TYPE_GROUP_INVITE) that is then handled by the group plug-in GroupPlugin. The GroupPlugin then sends off a message (GroupClient.EXTMSG_GROUP_INVITE_REQUEST) to the invitee for them to either accept or decline the invitation request. The GroupPlugin then handles the invitation response message (GroupClient.MSG_TYPE_GROUP_INVITE_RESPONSE) to determine if the invitee should be added to the group or not.

Example

To get information about a group from another plug-in, use the GroupClient.MSG_TYPE_REQUEST_GROUP_INFO message to determine who a given player is grouped with.

For example, Java code to divide loot based on members of a group wouldlook like this:

CombatInfo player = CombatPlugin.getCombatInfo(somePlayerOid);

ExtensionMessage groupInfoRequest = new ExtensionMessage(GroupClient.MSG_TYPE_REQUEST_GROUP_INFO,
                                                         "mv.REQUEST_GROUP_INFO", 
                                                         player.getOwnerOid());

Engine.getAgent().sendBroadcast(groupInfoRequest);

To receive the information back, subscribe to the GroupClient.MSG_TYPE_GROUP_INFO_RESPONSE message. This message returns the unique OID of the group, the OID of the group leader and a list of all of the OIDs for each group member.

So, again in Java, the code to handle the response might look something like this:

class GroupInfoRequestHook implements Hook{
	public Boolean processMessage(Message msg, int flags){
		ExtensionMessage infoRequestMessage = (ExtensionMessage)msg;

		Set<Long> groupMembers = (HashSet) infoRequestMessage.getProperty("groupMembers");
		for(Long playeOid : groupMembers){
			... <write some code for handling loot here>
		}
	}
}

Certainly the above code would get a lot more complicated for handling dividing out loot, but this gives a basic example of how you can determine who is in a player’s group from a different plug-in on the server.

See Sample Client implementation for description of additional group-related messages.

Group configuration

Use the Python file multiverse/config/world-name/group.py to configure the group system. In Sampleworld assets, the file is multiverse/config/sampleworld/group.py. To add one for your world, simply add it to your world directory on the server.

Set the maximum number of players that can be in a group in group.py with the following code:

GroupPlugin.SetMaxGroupSize(<some number here>)

You can also determine what statistics are tracked by the group system and sent to each group member’s client. For example, to have a health bar display for the group member, you must track health information. To track player’s health, add the following lines to group.py:

GroupPlugin.RegisterStat("health")
GroupPlugin.RegisterStat("health-max")

Currently the only stats that are supported are ones based on MarsStat. Registering these two stats sends the character's health and health-max values to the client. The group plug-in sends stat info with the group update message as well as with property update messages.

The client section discusses more about the property update message, but know that when a registered stat is updated, the group plug-in sends a message to the client advising that a group stat has changed. So if a player takes damage, the health property will send out an update.

Voice Integration

The group system is integrated with the Multiverse Voice Server to provide group-specific voice communications. The group system handles the creating and removing non-positional voice groups that group members can join. NOTE: you must supply a unique OID for a voice group. That is, it must be unique across all voice groups. To ensure this, when the GroupPlugin generates a new MarsGroup object, the MarsGroup object automatically makes a call to the VoiceClient to create a voice group associated with that MarsGroup. For example:

int error = 0;

// Create a new voice chat group specific to this group that is non-positional
error = VoiceClient.addVoiceGroup(this.GetGroupOid(), false, 4);
if(error != VoiceClient.SUCCESS){
   Log.error("MarsGroup.SetupGroupVoice : Create Voice Group Response - " + 
             VoiceClient.errorString(error));            
}

Note the call to this.GetGroupOid() to get the unique object odentifier for the group and use it as the OID for the voice group. This ensures that the voice group identifier is unique and links the voice group and the MarsGroup object. This is useful on the client side, since the group OID is sent to the client and can then be used for players to connect to a voice gorup, as you will see in the client section.

The GroupPlugin also exposes messages to enable muting group members, or mute the entire group. The GroupPlugin also subscribes to messages from the client that tells the group the status of a group member's voice client. This way the server can update each client to advise if a player has their voice client on or off. For additional information, see GroupPlugin.

Sample Client implementation

This section covers a basic example of the grouping system provided in the Sampleworld assets. To follow this article, you should have already installed the Sampleworld asset repository.

MarsGroup API

Sampleworld includes a client script that handles interaction with the group system, in the file /Scripts/MarsGroup.py.

The MarsGroup API handles messages from the server, and stores information about the group to which the player belongs and provides methods for accessing the information.

User interface elements in Sampleworld

The Sampleworld asset repository provides a basic example of a user interface using the MARS group system, including an example of:

  • The group information bar that displays group information.
  • A dialog box to accept or decline a group invite.
  • Group text chat and related UI.
  • Group voice chat and related UI.

Information bar

The player's basic UI includes a "GRP" button that displays the group information screen. The following code is from \Interface\FrameXML\MvInfoBar.xml:

      <Button name="MvGroupInfoButton" inherits="InfoBarButton" text="GRP" hidden="false" id="1">
        <Size>
          <AbsDimension x="34" y="34"/>
        </Size>
        <Anchors>
          <Anchor point="LEFT" relativeTo="MvInventoryButton"  relativePoint="RIGHT">
            <Offset>
              <AbsDimension x="7" y="0"/>
            </Offset>
          </Anchor>
        </Anchors>
        <NormalTexture file="Interface\Tooltips\UI-Tooltip-Background"/>
        <NormalText inherits="NormalFont" justifyH="CENTER" justifyV="MIDDLE">
          <Color r="1.0" g="1.0" b="1.0" a="1.0"/>
        </NormalText>
        <HitRectInsets>
          <AbsInset left="-10" right="10" top="0" bottom="0"/>
        </HitRectInsets>
        <Scripts language="python">
          <OnClick>MvGroupInfoButton_OnClick( this )</OnClick>
        </Scripts>
      </Button>

The related Python file, \Interface\FrameXML\MvInfoBar.py, contains the click event handler:

def MvGroupInfoButton_OnClick(frame):
	if MvGroupInfoFrame.IsVisible():
		MvGroupInfoFrame.Hide()
	else:
		MvGroupInfoFrame.SetPoint("TOP", frame.GetParent().Name, "BOTTOM")
		MvGroupInfoFrame.Show()

Group information screen

The above code calls MvGroupInfoFrame, the group information dialog that displays all group members and provides some basic group commands. As implemented for this example, there are eight spots for group members, which matches the default maximum group size, and buttons for inviting members to the group and leaveing/removing people from the group. One thing we will show here is how we will use the health and health-max properties we registered on the server side of the group configuration to display real time updates to player's health using the statusbar widget.

This interface element is defined in two files: \Interface\FrameXML\MvGroupInfo.xml and \Interface\FrameXML\MvGroupInfo.py.

First, examine the Python file. Notice that when the frame loads it registers two events triggered by the MarsGroup API:

def MvGroupInfoFrame_OnLoad(frame):
        frame.RegisterEvent("GROUP_UPDATE")
        frame.RegisterEvent("GROUP_PROPERTY_UPDATE")

Once part of a group, these two events can be used to update the display elements associated with each group member. The GROUP_UPDATE event is a "bulk update" of all of the group members' data and is generally only triggered when the group initially forms, or when a group member joins or leaves the group. The GROUP_PROPERTY_UPDATE event is triggered any time of the the properties registered with the group system is updated on the server. In this case, when one of the group members is damaged, the health property is updated and reflects in the status bar for that group member.

The following code shows how to hook up and capture the two events:

def MvGroupInfoFrame_OnEvent(this, event):
        if(event.eventType == "GROUP_UPDATE"):
                ClearGroupMembers()
                UpdateGroupInfo()
                SetButtonDisplay()
        if(event.eventType == "GROUP_PROPERTY_UPDATE"):
                SetGroupMemberInfo(int(event.eventArgs[0]))

For the GROUP_UPDATE event, the code first clears out the current cache of group member information held by MvGroupInfo, then runs through updating information obtain about each group member through the MarsGROUP API, and then finally sets the invite button visibility and leave/remove button text display based on if the player is the group leader or just a member. The only tricky part of this three part step is the updating of group member information, so take a closer look at it:

def UpdateGroupInfo():
        numMembers = MarsGroup.GetNumGroupMembers()
        for i in range(1, numMembers+1):
                SetGroupMemberInfo(i)

def SetGroupMemberInfo(slotId):
        playerFrame = getglobal("GroupMember%sFrame" % slotId)
        if not playerFrame.IsVisible():
                playerFrame.Show()
        playerName = getglobal("GroupMember%s" % slotId)
        playerName.SetText(MarsGroup.GetGroupMemberName(slotId))        
        playerHealth = getglobal("GroupMember%sHealthBar" % slotId)
        playerHealth.SetMinMaxValues(0,MarsGroup.GetGroupMemberMaxHealth(slotId))        
        playerHealth.SetValue(MarsGroup.GetGroupMemberHealth(slotId)) 
        ...

The code above defines two methods: UpdateGroupInfo() and SetGroupMemberInfo().

UpdateGroupInfo() loops through the group members and calls SetGroupMemberInfo() with the index being processed in the for loop. Recall that group members are ranked in the order in which they joined the group. When the server generates the information for the group update message, it orders the collection of group members and then assigns each a key starting at one up to the number of members in the group. This makes it easy to query the group members by an ordered index, instead of trying to write a sorting routine on the server. So on the client, the group member at index one will always be the group leader, and the rest will occur in the order in which they joined.

The SetGroupMemberInfo() method gets group member information using methods of the MarsGroup object, passing the index passed in slotId. It then sets up player-specific widgets appropriately. Notice that when setting the properties for the status bar, it gets the health-max value when setting the maximum value for the status bar, and then uses the health property registered as the current value.

Next, look at how to create a group by inviting another player. There are two ways a player can invite other players into a group:

  • Type in "/invite <player name>"
  • Click on the player and click the "Invite" button on the group information screen.

Here is the click event handler function for the latter option:

def MvInvitePlayerButton_OnClick(frame):
        target = MarsTarget.GetCurrentTarget()
        if target == None :
                ClientAPI.Write("No target")
                return
        
        if MarsGroup.CheckTarget(target):
                #Send message to server that we would like to invite the target
                MarsGroup.SendInviteRequestMessage(target.OID)

The first line gets the current target character to invite. After ensuring there is a target, the code calls MarsGroup.CheckTarget(). This is where to put code for client-side validation to ensure that the target is a valid target. In Sampleworld, it simply checks to make sure that the target being invited isn't already in the group. If not, then it sends an invitation request message to the server.

In the example of how a group gets created, recall that the target player character gets a message asking if they would like to join your group. If they decline the invitation, then the person who sent the invitation gets a message that the target rejected the invitation and why. Currently, the group chat box displays a message advising the invitation was simply declined, or that they are already grouped, or already have a pending invitation (a player can only consider one invitation at a time).

The group information screen also gives the option to leave the group, if you are not the leader, or to remove a player from the group if you are the group leader. In this example the only way to remove a player from the group is to click on their name in the group information screen and then click Remove. Notice that if you click on a group member's name they also become your current target. If you are the group leader, then to leave the group you simply click your name in the list and click remove. As a group member, you can simply click the Leave button and will be removed from the group. The following code shows how the leave/remove button works:

def MvRemovePlayerButton_OnClick(frame):
    for i in range(1, 8):
        button = getglobal("GroupMember%d" % i)
        if button.GetChecked():
            MarsGroup.RemoveGroupMember(i)
            return        
    ClientAPI.Write("You must select a group member to remove.")

This function loops through all possible group members, and if the Remove button is checked, removes them from the group.

The code below shows player name click event which shows how auto-targeting is done:

def MvGroupMember_OnClick(frame):
	if not frame.IsEnabled():
		return
	
	if frame.GetChecked():
		frame.SetChecked(0)
		frame.SetTextColor(1.0,1.0,1.0)
	else:
		for i in range(1, 8):
			skillButton = getglobal("GroupMember%d" % i)
			if skillButton.IsEnabled():				
				skillButton.SetChecked(0)	
				skillButton.SetTextColor(1.0,1.0,1.0)
		frame.SetChecked(1)
		frame.SetTextColor(1.0,0.0,0.0)
                ## Set current target to group member selected
                MarsTarget.TargetByOID(MarsGroup.GetGroupMemberOid(frame.GetID()))

	return	

The above code resets each of the player name widgets back to their original state, and then based on the ID assigned to the frame clicked on, uses that as an index to pass to MarsGroup.GetGroupMemberOid() to return the player's OID. It uses that to set the current target by calling MarsTarget.targetByOID(). The SetTextColor() methods are simply used to give visual clues on which group member is currently targeted.

Group invitation dialog box

The other UI element used by the group system is the Group Invitatiopn dialog box that appears when you are the target of a group invitation. This element is defined in \Interface\FrameXML\MvGroupInviteDialog.xml and \Interface\FrameXML\MvGroupInviteDialog.py. It simply advises the player that they are being invited to a group and by whom. It gives the player an option to either accept or decline the group invitation.

The following code shows the event information that displays the dialog:

def MvGroupInviteDialog_OnLoad(frame):
    frame.RegisterEvent("GROUP_INVITE_REQUEST")

def MvGroupInviteDialog_OnEvent(frame, event):
    global _groupLeaderOid

    if(event.eventType == "GROUP_INVITE_REQUEST"):
        inviteMessage = event.eventArgs[1] + " has invited you to join their group."
        _groupLeaderOid = long(event.eventArgs[0])
        MvGroupInviteMessage.SetText(inviteMessage)
        MvGroupInviteDialogFrame.Show()

The code above registers the GROUP_INVITE_REQUEST event and then captures it in the frame's OnEvent event handler. Nothing fancy here, it just sets out dialog text and displays it to the screen.

The code for accepting or declining is fairly straightforward as well:

def MvGroupInviteAcceptButton_OnClick(this):
    MarsGroup.SendInviteResponseMessage(_groupLeaderOid, "accept")
    MvGroupInviteDialogFrame.Hide()

def MvGroupInviteDeclineButton_OnClick(this):
    MarsGroup.SendInviteResponseMessage(_groupLeaderOid, "decline")    
    MvGroupInviteDialogFrame.Hide()

This code calls the MarsGroup.SendInviteRequestResponseMessage() method with OID of the player who sent the group invitation and a message that depends on whether you accept or deny the group invitation. The response texts are very important as the server only recognizes "accept" and "decline" as valid responses.

Group text chat

The last aspect of the MARS group system is the group-specific text chat channel. The Sampleworld asset repository implements private group communication. Unlike other types of chat messages, group chat messages are handled by the group plug-in. This allows the system to easily broadcast the message to members of the group associated with the user that is sending the group chat message. A user may send a group-specific chat message by typing "/group <message>" or by selecting the "Party" tab and typing their message into the chat box. The code to define group chat is in the \Interface\FrameXML\MvChat.xml and \Interface\FrameXML\MvChat.py files.

Group chat must handle both outgoing messages and incoming messages. For outgoing messages, MarsStandardCommands has the following command handler:

def HandleGroup(args_str):
    MarsGroup.SendGroupChatMessage(args_str)

MarsCommand.RegisterCommandHandler("group", HandleGroup)

The above code registers a command handler that captures "/group" commands that the user enteres. When a "/group" command is detected, the code calls MarsGroup.SendGroupChatMessage(). So, when the user types "/group <message>", the client notifies the server that the message should be processed by the GroupPlugin and broadcasted to all members of the player's group.

The following code is from \Interface\FrameXML\MvChat.py :

def MvChatFrameInputFrameEditBox_OnEnterPressed(frame):
    global _currentOutputFrame
    text = frame.GetText()
    if _currentOutputFrame == 2 and not text.startswith("/"):
        text = "/group " + text
    MarsCommand.HandleCommand(text)
    if len(text) > 0:
        frame.AddHistoryLine(text)
    MvChatFrameInputFrameEditBox_OnEscapePressed(frame)

The above code processes the chat input box OnEnterPressed event. When a user types a message or command into the chat input box and presses enter, this function is executed. If the text entered starts with a "/" then the function passes the command to the command handler. In the following portion of that code:

    if _currentOutputFrame == 2 and not text.startswith("/"):
        text = "/group " + text

This code checks which tab is currently active. This is set each time the user selects a tab. For group chat, if the user selects the tab with an index of two (2), the party tab, then we append the "/group" command to the front of the text, if they did not start the text with a "/". This forces the command handler to run the message as a group chat message, otherwise it would pass through as a "/say" command.

To handle incoming group chat messages we had to make some small changes to the \Scripts\MarsEvent.py file. Typically the way we can filter incoming chat messages is to filter based on the associated channel ID that the message comes across with. The following code illustrates handling group chat channel four (4):

def _HandleComm(message):
    # This will work with any widget that uses the event
    # registration system.  In the future, more cases should be handled here.
    # We could do the CHAT_MSG_GUILD, CHAT_MSG_PARTY, etc..
    # A better mechanism for this is to actually use server logic
    # instead to send these ui events directly
    node = ClientAPI.World.GetObjectByOID(message.Oid)
    nodeName = "Unknown entity"
    if node is not None:
        nodeName = node.Name
    if message.ChannelId == 1:
        # Say channel (1)
        ClientAPI.Interface.DispatchEvent("CHAT_MSG_SAY", [message.Message, nodeName, ""])
    elif message.ChannelId == 2:
        # ServerInfo channel (2)
        ClientAPI.Interface.DispatchEvent("CHAT_MSG_SYSTEM", [message.Message, ""])
    elif message.ChannelId == 3:
        # CombatInfo channel (3)
        ClientAPI.Interface.DispatchEvent("CHAT_MSG_COMBAT_MISC_INFO", [message.Message])
    elif message.ChannelId == 4:
	# Group channel (4)
	ClientAPI.Interface.DispatchEvent("CHAT_MSG_GROUP", [message.Message, ""])        
    else:
        channelName = "unknown"
        longChannel = "%d. %s" % (message.ChannelId, channelName)
        ClientAPI.Interface.DispatchEvent("CHAT_MSG_CHANNEL", 
                                          [message.Message, nodeName, 
                                          "", 
                                          longChannel, 
                                          "", 
                                          "", 
                                          str(message.ChannelId), 
                                          str(message.ChannelId), 
                                          channelName])

When a comm message comes across with e channel ID of 4, a "CHAT_MSG_GROUP" event is dispatched. To display the group-specific chat message, the code must capture this event. Going back to \Interface\FrameXML\MvChat.py, you can see that the OnLoad event for the ScrollingMessageFrames checks the ID of the frame that is loading to determine which one is being processed, if the ID matches that of the group chat frame, then it registers the group chat event:

def MvChatFrameOutputScrollingMessageFrame_OnLoad(frame):
    if frame.GetID() == 2: #Group Chat is only special case right now
        frame.RegisterEvent("CHAT_MSG_GROUP")
    else:
        frame.RegisterEvent("CHAT_MSG_SAY")
        frame.RegisterEvent("CHAT_MSG_SYSTEM")
        frame.RegisterEvent("CHAT_MSG_COMBAT_MISC_INFO")
        frame.RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS")
        frame.RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS")
        frame.RegisterEvent("CHAT_MSG_CHANNEL")
        frame.RegisterEvent("TRAINING_INFO")
        frame.RegisterEvent("CLASSABILITY_REPORT")
        frame.RegisterEvent("CHAT_MSG_COMBAT_ABILITY_MISSED"

When a CHAT_MSG_SAY event occurs, the system processes the message and displays it to the Party chat window. Since there are generic OnLoad and OnEvent methods for the different ScrollingMessageFrames, the code must check the ID associated with the frame that is receiving the event, and then add the message.

The following code is from the MvChatFrameOutputScrollingMessageFrame_OnEvent event handler in \Interface\FrameXML\MvChat.py:

    ...
    elif frame.GetID() == 2: # Group Chat
        if args.eventType == "CHAT_MSG_GROUP":
            text = args.eventArgs[0]
            frame.AddMessage(text, 1.0, 1.0, 1.0, 1) 
    ...

So again, if the ID of the frame being processed is equal to the one assigned to thegroup output frame, and the event is of a type CHAT_MSG_GROUP, then it simply adds the message to the group output window.

Group voice chat

Sampleworld provides the ability to turn on or off the voice chat by pressing ctrl-V and setting the "Enable voice chat" checkbox on or off. The voice configuration dialog box also lets you set the configuration to automatically join Group Voice Chat when joining a group. If both of these options are turned on, then when a player accepts an invite to a group, they are automatically added to their voice group associated with the group they joined. If these options are not on, the user can still join the group chat from the Group Information screen.

The group information screen gives players several voice options, depending on their group status. All group members have access to enable or disable their voice client, reset the volume, or mute themselves directly from the group information screen. The group leader can mute one or more players, or mute the rest of the group. These are just examples of functions that you can make available to players when working with group-specific voice chats.

Joining a voice chat group

As discussed earlier, when a player joins a group the client receives a GROUP_UPDATE message. This message contains data about each group member. The following are some voice properties in MarsGroup for each group member:

  • MarsGroup.GroupMember.voiceEnabled - determines if the player joined to the voice chat group.
  • MarsGroup.GroupMember.allowedSpeaker - determines if the player is allowed to speak in the group's voice chat. Also refered to as muted or not.

In addition to being sent in the GROUP_UPDATE message, these properties are also updated via the GROUP_PROPERTY_UPDATE message. This enables the client and server to communicate that group member's status on these properties as they are changed similarly to health information.

Look at the section of code in the MarsGroup._HanldeGroupUpdate that handles joining a voice group chat:

    if len(_groupMembers) > 0:
        _groupOid = long(props["groupOid"])
        # If client just joined group then set existing flag 
        # and run JoinVoiceGroup
        # We only wanted to run the JoinVoiceGroup once, if disabled 
        # then they can manually join later
        if not _existingGroupMember:
            _existingGroupMember = True
            if GetAutoJoinPartyChat():
                MarsVoice.JoinVoiceGroup(_groupOid)  

The above code first checks to make sure that the player is indeed part of a group; if not, then he will not join voice chat for the group. If the player is in a group, then it sets the global _goupOid property. The group OID is used to connect to the associated voice group. Next the code checks a flag to indicate if the player character is an existing or new group member. The existing group member flag will only be true the first time you receive a group update message, which should happen when you first join a group. This prevents you from trying to join the voice group every time you receive a group update message.

The code then checks to see if the player has the auto-join party chat enabled from the voice configuration screen. If they do have the option enabled, then the code calls MarsVoice.JoinVoiceGroup() method passing in our group OID which tells the voice client to reconfigure itself and connect to the group specified.

Updating the information dialog box

Now that you have an idea of how a player joins a voice group, look at the code behind the group information screen (/Interface/FrameXML/MvGroupInfo.py) and see how it uses this information.

First, look at the OnLoad event:

def MvGroupInfoFrame_OnLoad(frame):
    frame.RegisterEvent("GROUP_UPDATE")
    frame.RegisterEvent("GROUP_PROPERTY_UPDATE")
    frame.RegisterEvent("VOICE_ALLOCATION")
    frame.RegisterEvent("VOICE_DEALLOCATION")

This event handler registers two additional events: VOICE_ALLOCATION and VOICE_DEALLOCATION. These two events tell when a player that you can hear is speaking (VOICE_ALLOCATION) and when they stop speaking (VOICE_DEALLOCATION). These are useful to give visual clues when group members are talking.

Once the events are registered, note the following code in the OnEvent handler (MvGroupInfoFrame_OnEvent):

    elif(event.eventType == "VOICE_ALLOCATION"):
        if MarsGroup.GetNumGroupMembers()>0:
            SetVoiceIndicator(int(event.eventArgs[0]), True)
    elif(event.eventType == "VOICE_DEALLOCATION"):
        if MarsGroup.GetNumGroupMembers()>0:
            SetVoiceIndicator(int(event.eventArgs[0]), False)

This code captures the two events, but only processes them if the player is grouped. This way, if the player later joins a different type of voice group, or even a positional voice group, the group system will ignore them.

Showing the voice indicator

Now I know what you're thinking, what's this SetIndicator stuff. Well, this is what I was referring to when I said you might want to give the players some visual clue to when someone is talking or not. With VOICE_ALLOCATION we pass in True for speaking and on VOICE_DEALLOCATION we pass False for not talking. Lets examine the SetIndicator method:

# SetVoiceIndicator - sets the voice icon indicator next to each group member 
# based on their voice status
def SetVoiceIndicator(playerOid, speaking):
    slotId = MarsGroup.GetGroupMemberSlotIndex(playerOid)   
    if speaking:
        SetVoiceIcon(slotId, "talking")
    else:
    	SetVoiceIcon(slotId, "enabled")

SetVoiceIndicator() receives the OID for the player in question and a value advising if they are talking. It then gets the index of the player by calling the MarsGroup.GetGroupMemberSlotIndex and then passes the information to the <coide>SetVoiceIcon</code> method:

def SetVoiceIcon(slotId, state):
    voiceIndicatorEnabled = getglobal("GroupMemberVoiceIndicator%sEnabled" % slotId)
    voiceIndicatorDisabled = getglobal("GroupMemberVoiceIndicator%sDisabled" % slotId)
    voiceIndicatorTalking = getglobal("GroupMemberVoiceIndicator%sTalking" % slotId)
    voiceIndicatorMuted = getglobal("GroupMemberVoiceIndicator%sMuted" % slotId)
        
    if voiceIndicatorEnabled == None:
        return
    voiceIndicatorEnabled.Hide()
    voiceIndicatorDisabled.Hide()
    voiceIndicatorTalking.Hide()
    voiceIndicatorMuted.Hide()
        
    if state == "enabled":
    	voiceIndicatorEnabled.Show()
    elif state == "disabled":
    	voiceIndicatorDisabled.Show()
    elif state == "talking":
      	voiceIndicatorTalking.Show()
    elif state == "muted":
      	voiceIndicatorMuted.Show()

To see the XML that defines the user interface, see /Interface/FrameXML/MvGroupInfo.xml) in Sampleworld.

The above code gets the UI controls associated with the specified group member index. These controls are specific to displaying voice status icon to the right of each group member's health bar. After checking that they actually exist, the code hides them so it can then display only the one based on the voice state passed in. As you can see we have icons to show if the group member has their voice status as enabled, disabled, muted or talking. When the VOICE_ALLOCATION event runs you will see a Talking icon appear next to the specified group member, and when they stop it will go back to the Enabled state. Disabled icons show when the user changes or has their voice client disabled or not set to join the group.

When ever a GROUP_PROPERTY_UPDATE event is received, we run a method called SetGroupMemberInfo. This method handles real time updates to properties such as health. We have also now added logic to handle changes to voice status.

    if not MarsGroup.GetGroupMemberVoiceEnabled(slotId):
        _voiceEnabled = False
	SetVoiceIcon(slotId, "disabled")
    elif not MarsGroup.GetGroupMemberAllowedSpeaker(slotId):	
	_voiceEnabled = False
	# if current player is being updated then make sure muted is checked	
	if ClientAPI.GetPlayerObject().OID == MarsGroup.GetGroupMemberOid(slotId):
	    actionFrame = "MvGroupMemberFrame"
	if ClientAPI.GetPlayerObject().OID == MarsGroup.GetGroupLeaderOid():
	    actionFrame = "MvGroupLeaderFrame"
	    muteButton = getglobal(actionFrame + "PartyVoiceConfigMuteSelfButton")
	    muteButton.SetChecked(1)
	    SetVoiceIcon(slotId, "muted")       
    elif MarsGroup.GetGroupMemberVoiceEnabled(slotId) and not _voiceEnabled:
	_voiceEnabled = True
	SetVoiceIcon(slotId, "enabled")
	# if current player is being updated then make sure muted is not checked	
	if ClientAPI.GetPlayerObject().OID == MarsGroup.GetGroupMemberOid(slotId):
            actionFrame = "MvGroupMemberFrame"
	    if ClientAPI.GetPlayerObject().OID == MarsGroup.GetGroupLeaderOid():
		actionFrame = "MvGroupLeaderFrame"
	    muteButton = getglobal(actionFrame + "PartyVoiceConfigMuteSelfButton")
	    muteButton.SetChecked(0)

Above we see examples of how we can detect different voice status and set the appropriate icon, as well as auto-update other voice controls like the mute check box.

MarsGroup.GetGroupMemberVoiceEnabled returns a value back indicating if the player has their voice client enabled and are connected to the voice group. MarsGroup.GetGroupMemberAllowedSpeaker returns a value determining if the player has their voice client muted. The last section updates the UI to indicate that a player who did not previously have their voice client enabled, now does and sets the voice icons and muted check box appropriately.

Voice group UI controls

Depending on if the player is just a group member or the group leader, they have some basic voice control available to them on the group information screen. Player can mute them selves using the "Mute" check box; Join or leave the voice chat group by using the "Enabled" check box; Or if they are the group leader they can select to mute one or more gorup members or mute the entire group excluding themselves.

def MvMuteSelectedButton_OnClick(frame):
    for i in range(1, 8):
        button = getglobal("GroupMember%d" % i)
        if button.GetChecked(): 
            MarsGroup.MuteTarget(i)

The above code shows the code for when the group leader selects a group member, by clicking on their name, and then clicks the "Mute Selected" button. Here we simply loop through the group members, which are setup as CheckButtons, and determine which one is selected. By using the index i we call the MarsGroup.MuteTarget method.

# MuteTarget - Mutes the player at the given index in the group
def MuteTarget(slotId):
    global _groupMembers
    if _groupMembers.has_key(slotId):
        groupMember = _groupMembers[slotId]
        props = {"target" : groupMember.memberOid,
                 "setter" : ClientAPI.GetPlayerObject().OID, # Used on server for validation
                 "groupOid" : GetGroupOid()}
        ClientAPI.Network.SendExtensionMessage(0, False, "mv.GROUP_SET_ALLOWED_SPEAKER", props)        

The above code shows how the MarsGroup.MuteTarget works. By accepting the index of the player, it simply gets some information about the player, who is the one muting the player (either self or group leader), and the unique identifier for the group. We then send a message down to the server advising the GroupPlugin to toggle the group members allowedSpeaker property. That information is then sent back up to each group member's client so that we can reset the UI on the player's mute status.

In our sample, we have given the group leader the ability to mute the entire group, except for themselves. This allows the leader to quiet the group during times where they are giving instruction or just wants to advoid interuption. Below is the click event that simply calls the MarsGroup.MuteGroup method.

def MvMuteAllButton_OnClick(frame):
    MarsGroup.MuteGroup()

MarsGroup.MuteGroup simply sends down a message to the server advising the Group plug-in to toggle whether the group should be muted. In the case of the group leader muting the entire group, there is logic that prevents players from un-muting themselves, unless the group leader leaves, then the new group leader can un-mute themselves and the group.

As mentioned previously, all players in the group's voice chat can mute or un-mute themselves. The code below shows the click event for the Mute check box. This simply gets the current player's OID, finds their index in the group, and calls the MarsGroup.MuteTarget as explained above.

                         
def MvMuteSelfButton_OnClick(frame):	
    # Find out which slot we are in and mute ourselves
    slotId = MarsGroup.GetGroupMemberSlotIndex(ClientAPI.GetPlayerObject().OID)
    MarsGroup.MuteTarget(slotId)

Enabling and disabling group voice chat

For players who did not have their voice client enabled prior to joining the group, or for players who want to leave the voice group, this sample provides a check box to enable or disable their voice client directly from the group information screen. Below is the click event code for the check box.

def MvJoinVoiceChatButton_OnClick(frame):
    if frame.GetChecked():
        # If not set to join party chat then enable it
	if not MarsGroup.GetAutoJoinPartyChat():
	    MarsGroup.SetAutoJoinPartyChat(True)
	
	    # Enable voice if not enabled and update voice config which should now 
            # auto-add us to the party voice chat group.
	    if not MarsVoice.GetVoiceEnabled():
	        # Need to set voice group here since SetVoiceEnabled re-configures voice client
		MarsVoice.SetVoiceGroupOid(MarsGroup.GetGroupOid())
		MarsVoice.SetVoiceEnabled(True)
	    else:
		MarsVoice.JoinVoiceGroup(MarsGroup.GetGroupOid())
    else:
	# Leave voice group
	MarsGroup.SetAutoJoinPartyChat(False)
	MarsVoice.JoinVoiceGroup(0)

The above code checks if the auto join party chat is set to true. This flag, defined in MarsGroup, determines if the auto join group chat is enabled. If it is not enabled, then a call to MarsGroup.SetAutoJoinPartyChat() sets the flag to true. By setting this to true, we send an update to the server and to all group members that this player has or is joining the voice group. The next step is to check to see if the player has the voice client enabled. If it is already enabled, then the voice client simply needs to reconfigure itself to connect to the specific voice group. If the voice client is not enabled then we must start it up.

By calling MarsVoice.GetVoiceEnabled we can determine in the voice client is currently running. If it is not, then we call MarsVoice.SetVoiceGroupOid. This method determines in the voice client should connect to a specific voice group. If SetVoiceGroupOid is passed a zero value, it will not connect to a voice group unless a default group is specified in the server configuration. Once we set the group OID to join, we then call MarsVoice.SetVoiceEnabled which simply sets the enabled flag and then starts up the voice client software, which in this case will automatically join the voice group we specified.

If the voice group was already enabled, then we simply call MarsVoice.JoinVoiceGroup() passing in our group's unique identifier. This runs a voice client reconfigure option that connects us to the specified voice group.

If the player un-checks the Enabled setting, then we update the auto join group voice chat setting and pass in a zero value to the MarsVoice.JoinVoiceGroup method, which tells the voice client not to join a voice group, or to join the default voice group if one is configured on the server.

Personal tools