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 #

Plugins are defined as a Kotlin interface with an annotation specifying it as a plugin. Generally the engine will define a plugin to support a specific service, and it can then be implemented by users and configured for use within the plugin section of the config file. The engine handles loading and initializing plugins at runtime.

The Plugins service provides a way for developers to inject their own code into Pragma Engine-authored services. They can be configuration-free or configurable.

Plugin Configuration #

Plugins support custom configuration, allowing users to define their own configuration parameters. Plugin configs should be nested in the YAML file under the top-level property pluginConfigs.

Plugins are configured on a per-field basies, with a name of <ClassSimpleName>.<fieldName> to account for situations where there are multiple configured plugins; for example, the Accounts service may use an email plugin, and a custom ext plugin may use a different email plugin configured differently.

Example: MatchmakingStrategy

Let’s take a look at a situation where a new annotation has been added to the project @PragmaPlugin. We’ll use MatchMakingStrategy for our example.

Definition #

@PragmaPlugin("pragma.matchmaking.NoopMatchmakingStrategy", "matchmakingStrategy")
lateinit var matchmakingStrategy: MatchmakingStrategy

The plugin system has three key parts:

  • the name of the field: matchmakingStrategy
  • the interface of the field: MatchmakingStrategy
  • the annotation’s default implementation: NoopMatchmakingStrategy

The interface itself is standard:

interface MatchmakingStrategy {
 fun validateParty(matchmakingKey: ExtMatchmakingKey, party: Party)
}

The default implementation must exist on the class path and implements the interface. Note that the `NoopRegistryProvider implements nothing more than the interface; this is an example of a configless plugin.

class NoopMatchmakingStrategy(service: Service) : MatchmakingStrategy {
    override fun validateParty(matchmakingKey: ExtMatchmakingKey, party: Party) {
        // no op
    }
}

Custom Plugin #

This is used to author customized implementations of the interface and register it via configuration. In this case, we’ll use OneVsOneMatchmakingStrategy.

class OneVsOneMatchmakingStrategy(service: Service) : MatchmakingStrategy {
  
  val TEAM_SIZE = 1

  override fun validateParty(matchmakingKey: ExtMatchmakingKey, party: Party) {
    if (party.playerInfos.size > TEAM_SIZE) {
      throw PragmaException(PragmaError.MatchmakingService_PartyTooBig)
    }
  }
}

Configurable Plugin #

Plugins can also support custom configuration by implementing the ConfigurablePlugin interface.

Here is a configured plugin, which requires the following function on the class, per the ConfigurablePlugin interface:

package pragma.matchmaking

class ConfigurableMatchmakingStrategy(service: Service)
  : MatchmakingStrategy,
    ConfigurablePlugin<ConfigurableMatchmakingStrategy.Config> {
  
  lateinit var config: Config

  class Config(mode: BackendMode, type: BackendType) : PluginConfig<Config>(mode, type) {
    override val description = "Configuration for the ConfigurableMatchmaking plugin"

    var teamSize by types.int("team size")


    // 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
  }

  override fun validateParty(matchmakingKey: ExtMatchmakingKey, party: Party) {
    // now logic consults configured team size instead of hardcoded value
    if (party.playerInfos.size > config.teamSize) {
      throw PragmaException(PragmaError.MatchmakingService_PartyTooBig)
    }
  }
}

Configuration #

Now we’ll configure the service to use the new plugin, and configure the custom team size.

core:
  clusterName: "myClusterName"
serviceConfigs:
  // service specific configs
pluginConfigs:
  MatchmakingService.matchmakingStrategy:
    class: "pragma.matchmaking.ConfigurableMatchmakingStrategy"
    config:
      teamSize: 3

Extension Data #

Extension data is closely tied to plugins and provide 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 them to be defined and managed by users 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 this example, a developer wants a way to update a player’s inventory based on data from the end of a match.

The Pragma Engine-defined proto PlayerMatchEndV4 includes ExtPlayerMatchEnd which developers can use to determine inventory behavior on match end:

// this is a pragma defined proto
message PlayerMatchEndV4 {
  // A customer-defined ext for declaring any custom match end data specific to the player.
  ExtPlayerMatchEnd ext = 1; // HERE IS AN EXT for the customer to extend this proto and define their own stuff

  Fixed128 player_id = 2;

  // A list of rewards to be granted.
  repeated inventory.RewardGrant reward_grants = 3;

  // A list of items to be granted.
  repeated inventory.ItemGrantV2 item_grants = 4;

  // A list of items to be updated.
  repeated inventory.ServerItemUpdateV2 server_item_updates = 5;

  // A list of trusted items grants
  repeated inventory.ItemServerGrantV1 item_server_grants = 6;
}

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

message ExtPlayerMatchEnd {
  // customer can define whatever data they want here related to a player's match
  string mission_name = 1;
  map<string, int64> events = 2; 
}

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