メインコンテンツまでスキップ

Listen and handle different AGS events

Last updated on December 17, 2024

Overview

This guide provides information on how to modify the Extend Event Handler app template to listen and handle different events from AccelByte Gaming Services (AGS).

Prerequisites

Clone an Extend Event Handler app template.

git clone https://github.com/AccelByte/extend-event-handler-go.git

Identify and download specific event descriptors

Here is how you can get the protobuf event descriptor for the AGS event you are looking for.

  • Identify your AGS event: Go to the API Events page and find the AGS event you are looking for.

  • Locate the Protobuf event descriptor: After finding the AGS event, get the URL to the protobuf (*.proto) file on the same page.

  • Download the protobuf event descriptor: As an example for this guide, we need the userLoggedIn event. Therefore, we need to download the IAM directory which contains the userLoggedIn event.

Generate stub from event descriptor

Put the iam directory you have downloaded in the pkg/proto/accelbyte-asyncapi directory in your Extend Event Handler project.

...
|__ main.go
|__ pkg
...
├── proto
│ └── accelbyte-asyncapi
│ └── iam # Directory structure containing protobuf files
├── pb
...
...

Next, in the top-level directory of your Extend Event Handler project, run the following command.

make proto

You will see your stub generated at pkg/pb/accelbyte-asyncapi.

...
|__ main.go
|__ pkg
...
├── proto
│ └── accelbyte-asyncapi
│ └── iam # Directory containing protobuf files
├── pb
│ └── accelbyte-asyncapi
│ └── iam # Directory containing "stub" generated code
...
...

Creating your callback implementation

  • Create a New Go File: Go to directory pkg/service and create a new Go file. For example loginHandler.go.

  • Embed the Generated Stub: In the new Go file, define a struct type that embeds the stub type. For example, if your event is userLoggedIn you will see UnimplementedUserAuthenticationUserLoggedInServiceServer stub and your struct declaration might look something like the following.

    import pb "extend-event-handler/pkg/pb/accelbyte-asyncapi/iam/account/v1"

    type LoginHandler struct {
    pb.UnimplementedUserAuthenticationUserLoggedInServiceServer
    // your fields here
    }
  • Define OnMessage Methods: Define a method on your new struct named OnMessage. This method should have the same signature as the OnMessage method in the stub, but with your own implementation. This is the method that will be invoked when the AGS event is received.

    import pb "extend-event-handler/pkg/pb/accelbyte-asyncapi/iam/account/v1"

    func (o *LoginHandler) OnMessage(ctxt context.Context, msg *pb.UserLoggedIn) (*emptypb.Empty, error) {

    // your event handling code here

    return &emptypb.Empty{}, nil
    }

Register the event handler struct into the service

Go to main.go in the Event Handler project and register the gRPC service we have created as follows.

import "extend-event-handler/pkg/service"

// gRPC server that you already initialized in the service
s := grpc.NewServer(...)

// Register IAM Handler
loginHandler := service.LoginHandler{} // or service.NewLoginHandler() if you created the constructor method
pb.RegisterUserAuthenticationUserLoggedInServiceServer(s, loginHandler)

Protobuf Event Descriptors limitations

Protocol Buffers (Protobuf) has specific limitations when it comes to nesting data structures. Direct nesting of repeated fields or maps is not allowed; for instance, you cannot have a repeated field within another repeated field or a map within another map.

Similarly, Protobuf does not support using repeated fields or maps as the values of a map. To achieve nested structures, we opted to replace these types with google.protobuf.ListValue and google.protobuf.Struct. However, this approach introduces slight changes to how users access values in the Protobuf-generated code, as dynamic types like google.protobuf.ListValue and google.protobuf.Struct require additional parsing and handling compared to statically defined fields, though they ensure seamless compatibility with the original JSON data structure.

Example payload for the matchmakingChannelCreated event:

{ // MatchmakingChannelCreated
"id": "string",
...
"payload": { // MatchmakingChannelCreatedPayload
...
"ruleset": { // Ruleset
...
"alliance": { // Alliance
...
"combination": { // AllianceCombination
...
"alliances": [ // google.protobuf.ListValue
// should have been:
// repeated repeated AllianceCombinationAlliance
// or simply: a list of list of AllianceCombinationAlliance
[
{ // AllianceCombinationAlliance
"name": "string",
"min": 0,
"max": 0
}
]
]
}
}
}
}
}

Accessing values within google.protobuf.Value, google.protobuf.ListValue, google.protobuf.Struct

google.protobuf.Value, google.protobuf.ListValue, and google.protobuf.Struct are dynamic types in Protocol Buffers that provide support for JSON-like data structures. These types are particularly useful for cases where the schema needs to accommodate heterogeneous or nested data, such as arrays and objects with mixed types. The following example demonstrates how to define and interact with these types in the context of an EventData message.

message EventData {
string id = 1 [json_name = "id"];
string name = 2 [json_name = "name"];

google.protobuf.Value value = 10 [json_name = "value"];
google.protobuf.ListValue list_value = 20 [json_name = "listValue"];
google.protobuf.Struct struct = 30 [json_name = "struct"];
}

The EventData message above includes three dynamic fields:

  • value: A google.protobuf.Value that can store any valid JSON value (e.g., a string, number, boolean, null, object, or array).
  • list_value: A google.protobuf.ListValue representing a JSON array, where each element can be of a different type.
  • struct: A google.protobuf.Struct representing a JSON object with key-value pairs, where the values can be of any valid JSON type.

Below is an example JSON payload for the EventData message:

{
"id": "id0",
"name": "name1",
"value": "value2",
"listValue": [
true,
123,
"abc",
["x", "y"],
{"secret": "UFOs are real"}
],
"struct": {
"bool": true,
"number": 123,
"string": "abc",
"array": ["x", "y"],
"object": {"secret": "UFOs are real"}
}
}

This JSON demonstrates the flexibility of these dynamic types, showcasing mixed and nested data types in listValue and struct.

eventData := pb.EventData{}
// eventData = ...

assertEquals("id0", eventData.Id)
assertEquals("name1", eventData.Name)
assertEquals("value2", eventData.Value.GetStringValue())

assertEquals(5, len(eventData.ListValue.Values))
assertEquals(true, eventData.ListValue.Values[0].GetBoolValue())
assertEquals(123, eventData.ListValue.Values[1].GetNumberValue())
assertEquals("abc", eventData.ListValue.Values[2].GetStringValue())
assertEquals(2, len(eventData.ListValue.Values[3].GetListValue().Values))
assertEquals("x", eventData.ListValue.Values[3].GetListValue().Values[0].GetStringValue())
assertEquals("y", eventData.ListValue.Values[3].GetListValue().Values[1].GetStringValue())
assertEquals(1, len(eventData.ListValue.Values[4].GetStructValue().Fields))
assertEquals("UFOs are real", eventData.ListValue.Values[4].GetStructValue().Fields["secret"].GetStringValue())

assertEquals(5, len(eventData.Struct.Fields))
assertEquals(true, eventData.Struct.Fields["bool"].GetBoolValue())
assertEquals(123, eventData.Struct.Fields["number"].GetNumberValue())
assertEquals("abc", eventData.Struct.Fields["string"].GetStringValue())
assertEquals(2, len(eventData.Struct.Fields["array"].GetListValue().Values))
assertEquals("x", eventData.Struct.Fields["array"].GetListValue().Values[0].GetStringValue())
assertEquals("y", eventData.Struct.Fields["array"].GetListValue().Values[1].GetStringValue())
assertEquals(1, len(eventData.Struct.Fields["object"].GetStructValue().Fields))
assertEquals("UFOs are real", eventData.Struct.Fields["object"].GetStructValue().Fields["secret"].GetStringValue())