Key Concepts #

The Party Data Classes #

There are two main data classes within the Party service: Party and PartyPlayer. These classes are created by the platform and handed to plugins to customize at the extension points described later on this page.

The Party Class #

propertydescription
partyIdunique identifier for the party
maxPlayerCountnumber representing the maximum amount of players for the party set via the PartyService config
disableMaxPlayerCountboolean indicating if the automatic check for maximum player count is disabled or not, set via the PartyService config
gameServerVersioncomputed game server version based on the PartyPlayers in the party, updated whenever a player joins or leaves the party
preferredGameServerZoneslist of game server zones that the party can play a match on
inviteCodeinvite code for the party that players can use to join
overrideGameServerVersionboolean indicating if the Game Server Version Compatibility Plugin should validate this party or not, set via PartyService config
partyMembersByPlayerIdmap of all players in the party index by their playerId
extPartySelectionsext proto of selections for the party
extHiddenPartyDataext proto of sensitive data for the party that is not sent to players’ game clients

The PartyPlayer Class #

propertydescription
sessionKeyidentification object including the player’s pragmaId, also known as their playerId
displayNameobject containing the player’s name and discriminator
inventorysnapshot of the player’s inventory from when they entered the party or last completed a match
gameClientVersionversion of the game client the player is currently running
gameServerZoneToPingmap of this player’s ping to various game server zones
leaderboolean indicating if this player is a leader in this party
readyboolean indicating if this player ready to play a match
extPlayerSelectionsext proto of player selection state within a party
extPrivatePlayerSelectionsext proto of private player selection state within a party
extHiddenPlayerDataext proto containing sensitive data about a player that can only be used on the server in plugins

Types of Extension Data #

During a party’s construction, you can populate all the custom data that will be present on the party throughout the entire game flow lifecycle. This data persists on the party. On Match End, the pieces of data that are changed can be defined in PartyPlugin.returnFromMatch.

There are two types of data related to the Party service: player data and party data. These are further defined by visibility; data can be visible to the entire party, to an individual player, or to the server only (hidden). To initialize data, populate the related ext fields in the Party Plugin during a party’s construction when the initializeParty function is triggered. These fields are stored on the Party or PartyPlayer classes, and are passed to other plugins while the party journeys through the game loop. By default, this data can be referenced by other plugins, but cannot be changed.

Below is a chart containing descriptions and examples for every type of Party service data.

namevisibilitydescriptionexample
ExtPartySelectionspartydesignated by party leader and applies to entire partymap or game mode
ExtHiddenPartyDataserverscoped to the entire party but hidden to all playersteam win/loss streak tracker

The Party Plugin #

The Party Plugin provides a location for developers to implement custom party functionality. For a full list of plugin methods, look at All Calls and Methods.

Configuration #

The PartyConfig configuration class includes everything that can be defined in the main configuration YAML file.

configdescription
maxPlayersPerPartymaximum players allowed in each party
disableMaxPlayerCountwhether to disable maximum player count limits in each party
enableLocalGameServerVersionswhether to allow local game clients to override their game server version for local development purposes
repeatInviteDelaySecondsdefines the delay between a player inviting another player consecutive times
inventoryTagsToIncludetags used to filter player inventories, and items that match one or more tags will be included
enableTransferPartyLeaderwhether to transfer party leader status from one to another when invoking AssignPartyLeader

Player Inventory #

A snapshot of a player’s inventory is stored on the PartyPlayer class that is available when implementing the Party Plugin. This data can be used to validate party and player selections against the player’s inventory–for example, players should only be able to select skins they have purchased.

To minimize the amount of unnecessary data passed between services, use the inventoryTagsToInclude configuration map to determine which parts of a player’s inventory to include in the snapshot.

A player’s inventory snapshot is updated when they join a party or complete a match, after the match processing has completed.

Party State #

When players make changes to the party state, such as by making party or player selections, all players receive an up-to-date payload along with a PartyDetailsV1Notification that contains information about any changes from other players.

This notification contains the BroadcastParty payload, which includes the following information.

For the party:

  • party ID
  • a list of all public info about each party member
  • the invite code
  • preferred game server zones

For each player:

  • player ID
  • display name
  • is_ready boolean
  • is_leader boolean

Game Client Versions #

Game servers and game clients must be updated in parallel as updates are added to live service games. Pragma Engine provides the capability to enforce game client versions via the PartyService.gameServerCompatibilityPlugin. Lists of game server versions and corresponding compatible game client versions can be defined.

Note that the same game client version can match to different game server versions. If players in a party are compatible with multiple game server versions, the highest game server version is used, as defined by the keys in versionCompatibility. The {{USER_DEFINED_GAME_SERVER_VERSION}} value is passed to the Matchmaking service.

A valid config must be defined, and it must include:

  • a versionCompatibility config block
  • a clientVersions config block
  • unique versionCompatibility keys which are positive ints–greater ints designate higher priority
  • a unique {{USER_DEFINED_GAME_SERVER_VERSION}} which is not an empty string
  • a unique {{USER_DEFINED_GAME_CLIENT_VERSION}} which is not an empty string
Example: OrderedGameServerCompatibilityPlugin

This is a sample implementation of the Game Server Compatibility Plugin. It has a configuration file and a Kotlin code file.

Configuration:

game:
  pluginConfigs:
    PartyService.gameServerCompatibilityPlugin:
      class: "pragma.party.OrderedGameServerCompatibilityPlugin"
      config:
        versionCompatibility:
          1:
            serverVersion: "gameServerVersion1"
            clientVersions:
              1: "gameClientVersion0"
              2: "gameClientVersion1"
          2:
            serverVersion: "gameServerVersion2"
            clientVersions:
              1: "gameClientVersion1"
              2: "gameClientVersion2"

Kotlin:

@Suppress("unused")
class OrderedGameServerCompatibilityPlugin(
    override val service: Service,
    override val contentDataNodeService: ContentDataNodeService,
) :
    GameServerCompatibilityPlugin,
    ConfigurablePlugin<OrderedGameServerCompatibilityPlugin.Config> {
    var logger: Logger =
        LoggerFactory.getLogger(
            OrderedGameServerCompatibilityPlugin::class.simpleName
        )


    constructor(
        service: Service,
        contentDataNodeService: ContentDataNodeService,
        logger: Logger
    ) : this(service, contentDataNodeService) {
        this.logger = logger
    }


    data class ServerVersionInfo(
        val gameServerVersionName: String,
        val gameServerVersionNumber: Int
    )


    private lateinit var clientVersionToServerVersions:
        Map<String, Set<ServerVersionInfo>>


    private var onStartup = true


    class Config private constructor(type: BackendType) :
        PluginConfig<Config>(type) {
        override val description =
            "OrderedGameServerCompatibility plugin configuration."


        var versionCompatibility by
            types.mapOfObject(
                VersionCompatibility::class,
                "map of all the server versions to compatible game client versions"
            )


        companion object : ConfigBackendModeFactory<Config> {
            override fun getFor(type: BackendType): Config {
                return Config(type)
            }
        }
    }


    override suspend fun onConfigChanged(config: Config) {
        val exception = isValid(config)
        if (exception != null) {
            if (onStartup) {
                throw exception
            } else {
                logger.warn(
                    "Could not parse bad configuration: ${exception.message}"
                )
                return
            }
        }
        onStartup = false


        val clientVersionToServerVersions =
            mutableMapOf<String, MutableSet<ServerVersionInfo>>()
        config.versionCompatibility.entries.map {
            val serverVersionName = it.value.serverVersion
            val serverVersionNumber = it.key.toInt()
            it.value.clientVersions.values.forEach { gameClientVersion ->
                val serverVersionInfo =
                    ServerVersionInfo(serverVersionName, serverVersionNumber)
                if (gameClientVersion in clientVersionToServerVersions) {
                    clientVersionToServerVersions[gameClientVersion]!!.add(
                        serverVersionInfo
                    )
                } else {
                    clientVersionToServerVersions[gameClientVersion] =
                        mutableSetOf(serverVersionInfo)
                }
            }
        }


        if (this::clientVersionToServerVersions.isInitialized) {
            val oldServerVersions =
                this.clientVersionToServerVersions.entries
                    .flatMap {
                        it.value.map { serverVersionInfo ->
                            serverVersionInfo.gameServerVersionName
                        }
                    }
                    .toSet()
            val newServerVersions =
                config.versionCompatibility.entries
                    .map { it.value.serverVersion }
                    .toSet()


            val removedServerVersions = oldServerVersions - newServerVersions
            if (removedServerVersions.any()) {
                service.requestRpc(
                    MatchmakingRpc.RemoveGameServerVersionV1Request.newBuilder()
                        .addAllGameServerVersions(removedServerVersions)
                        .build(),
                    MatchmakingRpc.RemoveGameServerVersionV1Response::class
                )
            }
        }


        this.clientVersionToServerVersions = clientVersionToServerVersions
    }


    private fun isValid(config: Config): Exception? {
        val versionCompatibilityKeys = config.versionCompatibility.keys
        val notValidInts =
            versionCompatibilityKeys.any {
                val num = it.toIntOrNull()
                num == null || num <= 0
            }
        if (notValidInts) {
            return Exception("version compatibility keys be a positive integer")
        }


        config.versionCompatibility.entries.forEach {
            val gameClientVersions = it.value.clientVersions
            if (it.key.isEmpty()) {
                return Exception("serverVersion must not be an empty string")
            }
            if (gameClientVersions.isEmpty()) {
                return Exception(
                    "clientVersions must contain at least one value"
                )
            }
            if (
                gameClientVersions.any { clientVersion ->
                    clientVersion.value.isEmpty()
                }
            ) {
                return Exception("clientVersions must not contain empty values")
            }
        }


        if (
            config.versionCompatibility.entries
                .map { it.value.serverVersion }
                .toSet()
                .size != config.versionCompatibility.entries.size
        ) {
            return Exception("serverVersion was repeated")
        }


        return null
    }


    override fun calculateGameServerVersion(
        party: Party,
        newPlayer: PartyPlayer?
    ): String? {
        var possibleGameServerVersion = setOf<ServerVersionInfo>()
        val allPlayers =
            (party.partyMembersByPlayerId.values + newPlayer).filterNotNull()
        allPlayers.forEachIndexed { index, it ->
            val clientVersion = it.gameClientVersion
            val supportedGameServerVersions =
                clientVersionToServerVersions[clientVersion] ?: emptySet()
            possibleGameServerVersion =
                if (index == 0) {
                    supportedGameServerVersions
                } else {
                    possibleGameServerVersion.intersect(
                        supportedGameServerVersions
                    )
                }


            if (possibleGameServerVersion.isEmpty()) {
                return null
            }
        }


        return possibleGameServerVersion
            .maxByOrNull { it.gameServerVersionNumber }!!
            .gameServerVersionName
    }


    override fun getValidGameClientVersions(): List<String> {
        return clientVersionToServerVersions.keys.toList()
    }
}


class VersionCompatibility private constructor(type: BackendType) :
    ConfigObject<VersionCompatibility>(type) {
    constructor(
        serverVersion: String,
        clientVersions: Map<String, String>
    ) : this(BackendType.GAME) {
        this.serverVersion = serverVersion
        this.clientVersions = ConfigMapOfPrimitive(this)
        this.clientVersions.putAll(clientVersions)
    }


    override val description =
        "clientVersions to serverVersion support configuration."


    var serverVersion by
        types.string("game server version string, must not be empty")
    var clientVersions by
        types.mapOfString(
            "game client versions supported by this server version"
        )


    companion object : ConfigBackendModeFactory<VersionCompatibility> {
        override fun getFor(type: BackendType): VersionCompatibility {
            return VersionCompatibility(type)
        }
    }
}