Creating a Basic Custom Service #

To demonstrate the basic steps to create a custom service, we’ll be building a simple Echo service that receives a payload containing a message and responds with that same message.

Build Pragma Engine #

You must have Pragma Engine and its associated protos A format for efficiently serializing structured data that's cross-platform and compatible with several languages. built and have a boilerplate 5-ext project ready.

Run the following command in the terminal from the platform directory:

make skip-tests protos engine ext


Define service calls with protobufs #

Before writing any business logic, we’ll define the structure of the service calls for our simple service. Because our Echo service only receives a message and echos it back, it only has one request and one response.

Define protos for an Echo service #

  1. Create a proto file at 5-ext/ext-protos/src/main/proto/demo/echo/echoRpc.proto. You will need to create the directories for demo and echo.
  2. Add the following code into echoRpc.proto:
syntax = "proto3";

package demo.echo;
option csharp_namespace = "Demo.Echo";
option (pragma.unreal_namespace) = "Echo";

import "pragmaOptions.proto";

message EchoV1Request {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = REQUEST;

    string message = 1;
}

message EchoV1Response {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = RESPONSE;

    string response_message = 1;
}
Method overview

In this code block, we define two messages: EchoV1Request and EchoV1Response. This is a naming convention for service calls that we adhere to throughout Pragma Engine. The service call is named Echo and is followed by the version of the service call. In this case, we’re dealing with a service call sent by a client, so we have a Request and a corresponding Response.

The two option lines in each message define which entity the service call is for and the type of the message. For example, EchoV1Request has a pragma.pragma_session_type of PLAYER and a pragma.pragma_message_type of REQUEST. This is how Pragma Engine recognizes that this call is sent by a player client to the service.

Because we want our Echo service to receive and echo back a string, we expect the payload for both the request and the response to simply be a string. That’s exactly what we see in both the EchoV1Request and EchoV1Response messages.

Build protos #

We now need to compile the new proto before we author our business logic.

Run make ext using a terminal from the platform directory.

Create the Echo service #

Now we’ll author the code that constitutes our custom Echo service.

  1. Create a Kotlin file at 5-ext/ext/src/main/kotlin/demo/echo/EchoService.kt. You will need to make the directories for demo and echo.
You may have to mark the Kotlin directory as a sources root first.
  1. Add the following code to EchoService.kt:
package demo.echo

import demo.echo.EchoRpc.EchoV1Request
import demo.echo.EchoRpc.EchoV1Response
import java.util.UUID
import pragma.PlayerSession
import pragma.PragmaNode
import pragma.config.ConfigHandler
import pragma.rpcs.PragmaRPC
import pragma.rpcs.RoutingMethod
import pragma.rpcs.SessionType
import pragma.services.DistributedService
import pragma.services.PragmaService
import pragma.settings.BackendType

@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
@PragmaService(backendTypes = [BackendType.GAME])
class EchoService(pragmaNode: PragmaNode, instanceId: UUID) :
    DistributedService(pragmaNode, instanceId),
    ConfigHandler<EchoServiceConfig> {
    private lateinit var config: EchoServiceConfig

    override suspend fun onConfigChanged(serviceConfig: EchoServiceConfig) {
        config = serviceConfig
    }

    @PragmaRPC(SessionType.PLAYER, RoutingMethod.SESSION_PRAGMA_ID)
    suspend fun echoV1(
        session: PlayerSession,
        request: EchoV1Request
    ): EchoV1Response {
        val message = request.message
        return EchoV1Response.newBuilder()
            .setResponseMessage("response: $message")
            .build()
    }
}
Method overview

The @PragmaService annotation is how Pragma Engine recognizes that the code we’re authoring is a custom service.

Pragma Engine provides two backend types: game and social. We’re building this Echo service against the game backend, hence the backendTypes = [BackendType.GAME] line.

In our constructor, we pass in a PragmaNode and UUID. The PragmaNode gives our service access to all of Pragma Engine’s features, while the UUID allows us to uniquely identify each running instance of the Echo service.

ConfigHandler<EchoServiceConfig> allows us to add configuration to our service; we’ll be creating a config file in the next step.

The actual business logic is contained in echoV1 and is fairly simple. We pull the message from the request and respond with that message (with the text response: prepended to the message).

  1. Create a configuration file EchoServiceConfig.kt in the same directory as EchoService.kt.
  2. Add the following code to EchoServiceConfig.kt:
package demo.echo

import pragma.config.ConfigBackendModeFactory
import pragma.config.ServiceConfig
import pragma.settings.BackendType

class EchoServiceConfig private constructor(type: BackendType) :
    ServiceConfig<EchoServiceConfig>(type) {
    override val description = "Configuration for the Echo Service"

    companion object : ConfigBackendModeFactory<EchoServiceConfig> {

        override fun getFor(type: BackendType): EchoServiceConfig {
            return EchoServiceConfig(type)
        }
    }
}
Method overview
The description value contains human-readable text describing the purpose of this configuration file. Our Echo service does not contain much configuration due to its simplicity.

Build and run Pragma Engine #

Now that you’ve finished the Echo service, run make ext using a terminal from the platform directory.

Run Pragma Engine via one of the following methods.

Running via Make
Run make run to start the platform. Run this in a terminal with platform as the working directory.
Running in IntelliJ

From the IntelliJ toolbar in the upper right, ensure 5-ext - LocalConfigured is selected, then click the play button.

If 5-ext - LocalConfigured isn’t available, you will need to configure it. In the IntelliJ toolbar, click the dropdown next to the run button, then click Edit Configurations…. In the Run/Debug Configurations window that appears, expand Kotlin in the left hand side, then select 5-ext - LocalConfigured. Click OK. Click the play button in the IntelliJ toolbar to start Pragma Engine.

Once the engine has started successfully, it prints the message [main] INFO main - Pragma server startup complete.

Test the Echo service #

  1. Once Pragma Engine successfully starts, scroll up and see EchoService listed under the running services.
  2. Open Postman. If necessary, import the latest Postman collection and environment from the pragma-engine/platform/devenv/postman directory.
  3. Open Postman, then send PragmaDev ➨ Public ➨ GetInQueuev1 to enter the login queue.
  4. Log in as a player by sending Player - AuthenticateOrCreateV2.
  5. Navigate to PragmaDev ➨ Game ➨ RPC - Player ➨ Telemetry. Duplicate the PlayerEventV1 service call and name it EchoV1.

If you duplicate the PlayerEventV1 service call outside the Telemetry folder, additional setup is required to make the EchoV1 service call work.

In the new EchoV1 service call, click the Auth tab and change the type to Bearer Token. In the Token field, insert {{test01PragmaPlayerGameToken}}.

In the Headers tab, uncheck the Accept key. Create a new Accept key and insert the value application/json.

  1. Open the EchoV1 call and replace its body with the following JSON:
{
    "requestId": 1,
    "type": "EchoRpc.EchoV1Request",
    "payload": {
        "message": "Hello there!"
    }
}
  1. Click the send button. You should receive a response that looks like:
{
    "sequenceNumber": 0,
    "response": {
        "requestId": 1,
        "type": "EchoRpc.EchoV1Response",
        "payload": {
            "responseMessage": "response: Hello there!"
        }
    }
}

Appendix #

You can view the completed source code files for this section here.

5-ext/ext-protos/src/main/proto/demo/echo/echoRpc.proto
syntax = "proto3";

package demo.echo;
option csharp_namespace = "Demo.Echo";
option (pragma.unreal_namespace) = "Echo";

import "pragmaOptions.proto";

message EchoV1Request {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = REQUEST;

    string message = 1;
}

message EchoV1Response {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = RESPONSE;

    string response_message = 1;
}
5-ext/ext/src/main/kotlin/demo/echo/EchoService.kt
package demo.echo

import demo.echo.EchoRpc.EchoV1Request
import demo.echo.EchoRpc.EchoV1Response
import java.util.UUID
import pragma.PlayerSession
import pragma.PragmaNode
import pragma.config.ConfigHandler
import pragma.rpcs.PragmaRPC
import pragma.rpcs.RoutingMethod
import pragma.rpcs.SessionType
import pragma.services.DistributedService
import pragma.services.PragmaService
import pragma.settings.BackendType

@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
@PragmaService(backendTypes = [BackendType.GAME])
class EchoService(pragmaNode: PragmaNode, instanceId: UUID) :
    DistributedService(pragmaNode, instanceId),
    ConfigHandler<EchoServiceConfig> {
    private lateinit var config: EchoServiceConfig

    override suspend fun onConfigChanged(serviceConfig: EchoServiceConfig) {
        config = serviceConfig
    }

    @PragmaRPC(SessionType.PLAYER, RoutingMethod.SESSION_PRAGMA_ID)
    suspend fun echoV1(
        session: PlayerSession,
        request: EchoV1Request
    ): EchoV1Response {
        val message = request.message
        return EchoV1Response.newBuilder()
            .setResponseMessage("response: $message")
            .build()
    }
}
5-ext/ext/src/main/kotlin/demo/echo/EchoServiceConfig.kt
package demo.echo

import pragma.config.ConfigBackendModeFactory
import pragma.config.ServiceConfig
import pragma.settings.BackendType

class EchoServiceConfig private constructor(type: BackendType) :
    ServiceConfig<EchoServiceConfig>(type) {
    override val description = "Configuration for the Echo Service"

    companion object : ConfigBackendModeFactory<EchoServiceConfig> {

        override fun getFor(type: BackendType): EchoServiceConfig {
            return EchoServiceConfig(type)
        }
    }
}