Listen and handle different AGS events
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.
- Go
- C#
- Java
- Python
git clone https://github.com/AccelByte/extend-event-handler-go.git
git clone https://github.com/AccelByte/extend-event-handler-csharp.git
git clone https://github.com/AccelByte/extend-event-handler-java.git
git clone https://github.com/AccelByte/extend-event-handler-python.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
- Go
- C#
- Java
- Python
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
...
...
Put the iam
directory you have downloaded in the src/AccelByte.PluginArch.EventHandler.Demo.Server/Protos
directory in your Extend Event Handler project.
Then, you need to include the specific protobuf file to be generated by .NET gRPC Tool. Modify src/AccelByte.PluginArch.EventHandler.Demo.Server/AccelByte.PluginArch.EventHandler.Demo.Server.csproj
file and add or update the following section.
<ItemGroup>
<Protobuf Include="Protos\iam\account\v1\account.proto" GrpcServices="Server" />
</ItemGroup>
You can include more than one protobuf
file. Just add more <Protobuf>
lines inside <ItemGroup>
.
Next, use the following command to generate code from the included protobuf files.
dotnet build
Put the iam
directory you have downloaded in the src/main/proto/accelbyte-asyncapi
directory in your Extend Event Handler project.
...
├── src
│ ├── main
│ │ ├── proto
│ │ │ └── accelbyte-asyncapi
| | │ └── iam # Directory containing protobuf files
...
Next, use the following command to generate code from the included protobuf files.
make build
Put the iam
directory structure you have downloaded in the src/app/proto
directory in your Extend Event Handler project.
...
├── src
│ ├── app
│ │ ├── proto
Next, use the following command to generate code from the included protobuf files.
make build
Creating your callback implementation
- Go
- C#
- Java
- Python
Create a New Go File: Go to directory
pkg/service
and create a new Go file. For exampleloginHandler.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 seeUnimplementedUserAuthenticationUserLoggedInServiceServer
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 namedOnMessage
. This method should have the same signature as theOnMessage
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
}
Create a New C# File: Go to directory
src/AccelByte.PluginArch.EventHandler.Demo.Server/Services
and create a new C# file. For exampleUserLoggedInService.cs
.Embed the Generated Stub: In the new C# file, create a new class which inherits
UserAuthenticationUserLoggedInService.UserAuthenticationUserLoggedInServiceBase
. For example,UserLoggedInService
.namespace AccelByte.PluginArch.EventHandler.Demo.Server.Services
{
public class UserLoggedInService : UserAuthenticationUserLoggedInService.UserAuthenticationUserLoggedInServiceBase
{
...
}
}Define
OnMessage
Methods: Override theOnMessage
method and implement the logic which will be executed when the AGS event is received.public override Task<Empty> OnMessage(UserLoggedIn request, ServerCallContext context)
{
...
}
Create a New Java File: Go to directory
src/main/java/net/accelbyte/service
and create a new Java file. For exampleLoginHandler.java
.Embed the Generated Stub: In the new Java file, create a new class which inherits
UserAuthenticationUserLoggedInServiceGrpc.UserAuthenticationUserLoggedInServiceImplBase
. For example,LoginHandler
.public class LoginHandler extends UserAuthenticationUserLoggedInServiceGrpc.UserAuthenticationUserLoggedInServiceImplBase {
...
}Define
OnMessage
Methods: Override theonMessage
method and implement the logic which will be executed when the AGS event is received.
@Override
public void onMessage(UserLoggedIn request, StreamObserver<Empty> responseObserver) {
...
}
Create a New Python File: Go to directory
src/app/services
and create a new Python file. For examplelogin_handler.py
.Embed the Generated Stub: In the new Python file, create a new class which inherits
UserAuthenticationUserLoggedInServiceServicer
. For example,AsyncLoginHandlerService
.from app.proto.account_pb2_grpc import UserAuthenticationUserLoggedInServiceServicer
class AsyncLoginHandlerService(UserAuthenticationUserLoggedInServiceServicer):
...Define
OnMessage
Methods: Override theOnMessage
method and implement the logic which will be executed when the AGS event is received.async def OnMessage(self, request: UserLoggedIn, context):
...
The Extend Event Handler app's messaging system employs the Kafka ecosystem and depends on the "at least once" semantic for message processing guarantee and idempotency. To learn more, see Manage message processing and idempotency.
Register the event handler struct into the service
- Go
- C#
- Java
- Python
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)
Go to src/AccelByte.PluginArch.EventHandler.Demo.Server/Program.cs
in the Event Handler project and register the gRPC service we have created as follows.
app.MapGrpcService<UserLoggedInService>();
Go to src/main/java/net/accelbyte/serviceLoginHandler.java
in the Event Handler project and register the gRPC service you have created by adding @GRpcService
annotation.
@GRpcService
public class LoginHandler extends UserAuthenticationUserLoggedInServiceGrpc.UserAuthenticationUserLoggedInServiceImplBase {
...
Go to src/app/__main__.py
in the Event Handler project and register the gRPC service we have created as follows.
opts.append(
AppGRPCServiceOpt(
AsyncLoginHandlerService(
logger=logger,
namespace=namespace
),
AsyncLoginHandlerService.full_name,
add_UserAuthenticationUserLoggedInServiceServicer_to_server,
)
)
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
: Agoogle.protobuf.Value
that can store any valid JSON value (e.g., a string, number, boolean, null, object, or array).list_value
: Agoogle.protobuf.ListValue
representing a JSON array, where each element can be of a different type.struct
: Agoogle.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.
- Go
- C#
- Java
- Python
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())
// EventData eventData = ...
AssertEquals("id0", eventData.Id);
AssertEquals("name1", eventData.Name);
AssertEquals("value2", eventData.Value.StringValue);
AssertEquals(5, eventData.ListValue.Values.Count);
AssertEquals(true, eventData.ListValue.Values[0].BoolValue);
AssertEquals(123, eventData.ListValue.Values[1].NumberValue);
AssertEquals("abc", eventData.ListValue.Values[2].StringValue);
AssertEquals(2, eventData.ListValue.Values[3].ListValue.Values.Count);
AssertEquals("x", eventData.ListValue.Values[3].ListValue.Values[0].StringValue);
AssertEquals("y", eventData.ListValue.Values[3].ListValue.Values[1].StringValue);
AssertEquals(1, eventData.ListValue.Values[4].StructValue.Fields.Count);
AssertEquals("UFOs are real", eventData.ListValue.Values[4].StructValue.Fields["secret"].StringValue);
AssertEquals(5, eventData.Struct.Fields.Count);
AssertEquals(true, eventData.Struct.Fields["bool"].BoolValue);
AssertEquals(123, eventData.Struct.Fields["number"].NumberValue);
AssertEquals("abc", eventData.Struct.Fields["string"].StringValue);
AssertEquals(2, eventData.Struct.Fields["array"].ListValue.Values.Count);
AssertEquals("x", eventData.Struct.Fields["array"].ListValue.Values[0].StringValue);
AssertEquals("y", eventData.Struct.Fields["array"].ListValue.Values[1].StringValue);
AssertEquals(1, eventData.Struct.Fields["object"].StructValue.Fields.Count);
AssertEquals("UFOs are real", eventData.Struct.Fields["object"].StructValue.Fields["secret"].StringValue);
// EventData eventData = ...
assertEquals("id0", eventData.getId());
assertEquals("name1", eventData.getName());
assertEquals("value2", eventData.getValue().getStringValue());
assertEquals(5, eventData.getListValue().getValuesCount());
assertEquals(true, eventData.getListValue().getValues(0).getBoolValue());
assertEquals(123, eventData.getListValue().getValues(1).getNumberValue());
assertEquals("abc", eventData.getListValue().getValues(2).getStringValue());
assertEquals(2, eventData.getListValue().getValues(3).getListValue().getValuesCount());
assertEquals("x", eventData.getListValue().getValues(3).getListValue().getValues(0).getStringValue());
assertEquals("y", eventData.getListValue().getValues(3).getListValue().getValues(1).getStringValue());
assertEquals(1, eventData.getListValue().getValues(4).getStructValue().getFieldsCount());
assertEquals("UFOs are real", eventData.getListValue().getValues(4).getStructValue().getFieldsOrThrow("secret").getStringValue());
assertEquals(5, eventData.getStruct().getFieldsCount());
assertEquals(true, eventData.getStruct().getFieldsOrThrow("bool").getBoolValue());
assertEquals(123, eventData.getStruct().getFieldsOrThrow("number").getNumberValue());
assertEquals("abc", eventData.getStruct().getFieldsOrThrow("string").getStringValue());
assertEquals(2, eventData.getStruct().getFieldsOrThrow("array").getListValue().getValuesCount());
assertEquals("x", eventData.getStruct().getFieldsOrThrow("array").getListValue().getValues(0).getStringValue());
assertEquals("y", eventData.getStruct().getFieldsOrThrow("array").getListValue().getValues(1).getStringValue());
assertEquals(1, eventData.getStruct().getFieldsOrThrow("object").getStructValue().getFieldsCount());
assertEquals("UFOs are real", eventData.getStruct().getFieldsOrThrow("object").getStructValue().getFieldsOrThrow("secret").getStringValue());
event_data = EventData()
# event_data = ...
assert_equals("id0", event_data.id)
assert_equals("name1", event_data.name)
assert_equals("value2", event_data.value)
assert_equals(5, len(event_data.list_value))
assert_equals(True, event_data.list_value[0])
assert_equals(123, event_data.list_value[1])
assert_equals("abc", event_data.list_value[2])
assert_equals(2, len(event_data.list_value[3]))
assert_equals("x", event_data.list_value[3][0])
assert_equals("y", event_data.list_value[3][1])
assert_equals(1, len(event_data.list_value[4]))
assert_equals("UFOs are real", event_data.list_value[4]["secret"])
assert_equals(5, len(event_data.struct))
assert_equals(True, event_data.struct["bool"])
assert_equals(123, event_data.struct["number"])
assert_equals("abc", event_data.struct["string"])
assert_equals(2, len(event_data.struct["array"]))
assert_equals("x", event_data.struct["array"][0])
assert_equals("y", event_data.struct["array"][1])
assert_equals(1, len(event_data.struct["object"]))
assert_equals("UFOs are real", event_data.struct["object"]["secret"])