Plugins and Extension Data #

Plugins and extension data are the backbone of Pragma Engine customization. They allow users to implement rich features with custom data and logic within the full fledged capabilities of the engine.

Plugins #

The Plugins service provides a way for developers to inject their own code into Pragma Engine-provided services. These custom plugins can be configuration-free or configurable. The engine handles loading and initializing these custom plugins at runtime.

Plugin declaration #

Plugins are defined as a Kotlin interface with an annotation specifying it as a plugin. Generally, Pragma Engine will define a plugin to support a specific service, such as the Party Plugin for the Party service. Services with plugins will use the PragmaPlugin annotation to define plugins relevant to that service.

@PragmaPlugin("pragma.party.DefaultPragmaPartyPlugin", "partyPlugin")
lateinit var partyPlugin: PartyPlugin

The plugin declaration has three key parts:

  • interface (PartyPlugin): the Kotlin interface defining the plugin
  • configuration name (partyPlugin): the name of the configuration field as it appears in the configuration YAML file (see Plugin configuration)
  • default implementation (DefaultPragmaPartyPlugin): the annotation’s default implementation. The plugin’s default implementation will be used if no other implementation is specified.

Plugin customization #

You can create a customized implementation of a plugin to apply your own logic. These custom plugins can be configuration-free or configurable. The following examples override the Party Plugin’s onAddPlayer method.

Configuration-free plugin implementation

In the following example, we create the MySamplePartyPlugin based on the Party Plugin. We override the PartyPlugin’s onAddPlayer method and use it to validate a player’s rank against a local variable. If the player trying to join the party has a rank too high, Pragma Engine throws an error and prevents the player from joining the party.

class MySamplePartyPlugin(service: Service, contentDataNodeService: ContentDataNodeService) : PartyPlugin {

    val MAX_RANK_RANGE = 4

    override suspend fun onAddPlayer(
        requestExt: ExtPlayerJoinRequest,
        playerToAdd: Party.PartyPlayer,
        party: Party.Party,
        partyConfig: PartyConfig
    ) {
        if (playerToAdd.rank > MAX_RANK_RANGE) {
            throw PragmaException(PragmaError.PartyService_PlayerRankTooHigh)
        }
    }
}

Configurable plugin implementation

Plugins also allow users to define their own configuration parameters by implementing the ConfigurablePlugin interface.

In this example, we’ll create the ConfigurablePartyPlugin, which implements the ConfigurablePlugin in addition to the PartyPlugin. The ConfigurablePartyPlugin performs the same action as the MySamplePartyPlugin, but uses a configuration value (config.maxPlayerRankRange) instead of a locally-defined variable. In the next step we’ll see how to set the configuration value in the configuration YAML file.

class ConfigurablePartyPlugin(service: Service, contentDataNodeService: ContentDataNodeService)
    : PartyPlugin, ConfigurablePlugin<ConfigurablePartyPlugin.Config> {

    lateinit var config: Config

    class Config(mode: BackendMode, type: BackendType) : PluginConfig<Config>(mode, type) {

        override val description = "Configuration for the ConfigurablePartyPlugin"

        var maxPlayerRankRange by types.int("maximum player rank")

        // builder function for default config
        companion object : ConfigBackendModeFactory<Config> {
            override fun getFor(mode: BackendMode, type: BackendType) = Config(mode, type)
        }
    }

    // from ConfigurablePlugin interface
    override suspend fun onConfigChanged(config: Config) {
        this.config = config
    }

    //onAddParty logic is now based on a config value (`config.maxPlayerRankRange`) versus being defined as a variable within the class. We'll set this config value in the next step.
    override fun onAddParty(requestExt: ExtPlayerJoinRequest, playerToAdd: Party.PartyPlayer, party: Party.Party, partyConfig: PartyConfig) {
        if (playerToAdd.rank > config.maxPlayerRankRange) {
            throw PragmaException(PragmaError.PartyService_PlayerRankTooHigh)
        }
    }
}

Plugin configuration #

Plugin configuration values should be nested in the YAML file under the top-level property pluginConfigs section. Plugins are configured on a per-field basis, with a name of <ClassSimpleName>.<fieldName>, such as PartyService.partyPlugin.

In the following example, we tell the Party service to use our ConfigurablePartyPlugin, and set the maxPlayerRankRange value. If your plugin does not have any config values (such as in our MySamplePartyPlugin), omit the config block.

core:
  clusterName: "myClusterName"
serviceConfigs:
  // service specific configs
pluginConfigs:
  PartyService.partyPlugin:
    class: "pragma.party.ConfigurablePartyPlugin"
    config:
      maxPlayerRankRange: 4 //custom config value to use
Per the PragmaPlugin annotation, if no PartyService.partyPlugin is defined in the configuration file, Pragma Engine will use the DefaultPartyPlugin.

Extension data #

Extension data is closely tied to plugins and provides a standard way for users to define custom structured data within the engine.

Extension protobuf types are predefined by the engine and placed in a shared directory with no defined fields. This allows Pragma Engine code to reference, serialize, deserialize, store, and retrieve extension data. Engine code never inspects the internal fields, which allows users to define and manage with game-specific details.

In this way the engine handles validation, routing, and storage while the user is responsible for defining their game-specific data and features for their game-specific logic.

Example #

In the following example, a developer wants a way to update a player’s data based on the end of a game instance.

The Pragma Engine-defined proto PlayerGameResult includes ExtPlayerGameResult, which developers can use to determine behavior when a game instance ends:

// this is a pragma defined proto
message PlayerGameResult {
  // The player being removed from the game
  Fixed128 player_id = 1;

  // A customer-defined ext for declaring any custom data for the player completing the game.
  ExtPlayerGameResult ext = 2;

}

Developers then define the ExtPlayerGameResult fields in 5-ext/ext-protos/src/main/proto/shared/gameInstanceExt.proto:

message ExtPlayerGameResult {
  party.GameModeStats game_mode_stats = 2;
  inventory.ext.MatchEndData match_end_data = 3;
}

Once this is complete, ExtPlayerGameResult data can be seen in InventoryOperationsPlugin.getInventoryOperationsFromMatchEnd, and developers can decide how player inventories are updated on match end.