feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+179
View File
@@ -42,6 +42,16 @@ type LobbyMembershipRejectedEvent struct {
GameID string
}
// LobbyMembershipBlockedEvent is the independent Go representation of
// `notification.LobbyMembershipBlockedEvent`. Reason carries the upstream
// lifecycle event that triggered the cascade
// (`permanent_blocked`, `deleted`).
type LobbyMembershipBlockedEvent struct {
GameID string
MembershipUserID string
Reason string
}
// LobbyInviteCreatedEvent is the independent Go representation of
// `notification.LobbyInviteCreatedEvent`.
type LobbyInviteCreatedEvent struct {
@@ -56,6 +66,20 @@ type LobbyInviteRedeemedEvent struct {
InviteeUserID string
}
// LobbyRaceNameRegistrationEligibleEvent is the independent Go
// representation of `notification.LobbyRaceNameRegistrationEligibleEvent`.
type LobbyRaceNameRegistrationEligibleEvent struct {
GameID string
RaceName string
EligibleUntilMs int64
}
// LobbyRaceNameRegisteredEvent is the independent Go representation of
// `notification.LobbyRaceNameRegisteredEvent`.
type LobbyRaceNameRegisteredEvent struct {
RaceName string
}
// GameTurnReadyEventToPayload converts GameTurnReadyEvent to FlatBuffers bytes
// suitable for the authenticated gateway push transport.
func GameTurnReadyEventToPayload(event *GameTurnReadyEvent) ([]byte, error) {
@@ -272,6 +296,68 @@ func PayloadToLobbyMembershipRejectedEvent(data []byte) (result *LobbyMembership
return &LobbyMembershipRejectedEvent{GameID: gameID}, nil
}
// LobbyMembershipBlockedEventToPayload converts LobbyMembershipBlockedEvent
// to FlatBuffers bytes suitable for the authenticated gateway push
// transport.
func LobbyMembershipBlockedEventToPayload(event *LobbyMembershipBlockedEvent) ([]byte, error) {
if event == nil {
return nil, errors.New("encode lobby membership blocked payload: event is nil")
}
if event.GameID == "" {
return nil, errors.New("encode lobby membership blocked payload: game_id is empty")
}
if event.MembershipUserID == "" {
return nil, errors.New("encode lobby membership blocked payload: membership_user_id is empty")
}
if event.Reason == "" {
return nil, errors.New("encode lobby membership blocked payload: reason is empty")
}
builder := flatbuffers.NewBuilder(96)
gameID := builder.CreateString(event.GameID)
membershipUserID := builder.CreateString(event.MembershipUserID)
reason := builder.CreateString(event.Reason)
notificationfbs.LobbyMembershipBlockedEventStart(builder)
notificationfbs.LobbyMembershipBlockedEventAddGameId(builder, gameID)
notificationfbs.LobbyMembershipBlockedEventAddMembershipUserId(builder, membershipUserID)
notificationfbs.LobbyMembershipBlockedEventAddReason(builder, reason)
offset := notificationfbs.LobbyMembershipBlockedEventEnd(builder)
notificationfbs.FinishLobbyMembershipBlockedEventBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// PayloadToLobbyMembershipBlockedEvent converts FlatBuffers payload bytes
// into LobbyMembershipBlockedEvent.
func PayloadToLobbyMembershipBlockedEvent(data []byte) (result *LobbyMembershipBlockedEvent, err error) {
if len(data) == 0 {
return nil, errors.New("decode lobby membership blocked payload: data is empty")
}
defer recoverNotificationDecodePanic("decode lobby membership blocked payload", &result, &err)
event := notificationfbs.GetRootAsLobbyMembershipBlockedEvent(data, 0)
gameID, err := requiredNotificationString(event.GameId(), "game_id")
if err != nil {
return nil, fmt.Errorf("decode lobby membership blocked payload: %w", err)
}
membershipUserID, err := requiredNotificationString(event.MembershipUserId(), "membership_user_id")
if err != nil {
return nil, fmt.Errorf("decode lobby membership blocked payload: %w", err)
}
reason, err := requiredNotificationString(event.Reason(), "reason")
if err != nil {
return nil, fmt.Errorf("decode lobby membership blocked payload: %w", err)
}
return &LobbyMembershipBlockedEvent{
GameID: gameID,
MembershipUserID: membershipUserID,
Reason: reason,
}, nil
}
// LobbyInviteCreatedEventToPayload converts LobbyInviteCreatedEvent to
// FlatBuffers bytes suitable for the authenticated gateway push transport.
func LobbyInviteCreatedEventToPayload(event *LobbyInviteCreatedEvent) ([]byte, error) {
@@ -374,6 +460,99 @@ func PayloadToLobbyInviteRedeemedEvent(data []byte) (result *LobbyInviteRedeemed
}, nil
}
// LobbyRaceNameRegistrationEligibleEventToPayload converts
// LobbyRaceNameRegistrationEligibleEvent to FlatBuffers bytes suitable for
// the authenticated gateway push transport.
func LobbyRaceNameRegistrationEligibleEventToPayload(event *LobbyRaceNameRegistrationEligibleEvent) ([]byte, error) {
if event == nil {
return nil, errors.New("encode lobby race name registration eligible payload: event is nil")
}
if event.GameID == "" {
return nil, errors.New("encode lobby race name registration eligible payload: game_id is empty")
}
if event.RaceName == "" {
return nil, errors.New("encode lobby race name registration eligible payload: race_name is empty")
}
builder := flatbuffers.NewBuilder(96)
gameID := builder.CreateString(event.GameID)
raceName := builder.CreateString(event.RaceName)
notificationfbs.LobbyRaceNameRegistrationEligibleEventStart(builder)
notificationfbs.LobbyRaceNameRegistrationEligibleEventAddGameId(builder, gameID)
notificationfbs.LobbyRaceNameRegistrationEligibleEventAddRaceName(builder, raceName)
notificationfbs.LobbyRaceNameRegistrationEligibleEventAddEligibleUntilMs(builder, event.EligibleUntilMs)
offset := notificationfbs.LobbyRaceNameRegistrationEligibleEventEnd(builder)
notificationfbs.FinishLobbyRaceNameRegistrationEligibleEventBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// PayloadToLobbyRaceNameRegistrationEligibleEvent converts FlatBuffers
// payload bytes into LobbyRaceNameRegistrationEligibleEvent.
func PayloadToLobbyRaceNameRegistrationEligibleEvent(data []byte) (result *LobbyRaceNameRegistrationEligibleEvent, err error) {
if len(data) == 0 {
return nil, errors.New("decode lobby race name registration eligible payload: data is empty")
}
defer recoverNotificationDecodePanic("decode lobby race name registration eligible payload", &result, &err)
event := notificationfbs.GetRootAsLobbyRaceNameRegistrationEligibleEvent(data, 0)
gameID, err := requiredNotificationString(event.GameId(), "game_id")
if err != nil {
return nil, fmt.Errorf("decode lobby race name registration eligible payload: %w", err)
}
raceName, err := requiredNotificationString(event.RaceName(), "race_name")
if err != nil {
return nil, fmt.Errorf("decode lobby race name registration eligible payload: %w", err)
}
return &LobbyRaceNameRegistrationEligibleEvent{
GameID: gameID,
RaceName: raceName,
EligibleUntilMs: event.EligibleUntilMs(),
}, nil
}
// LobbyRaceNameRegisteredEventToPayload converts LobbyRaceNameRegisteredEvent
// to FlatBuffers bytes suitable for the authenticated gateway push transport.
func LobbyRaceNameRegisteredEventToPayload(event *LobbyRaceNameRegisteredEvent) ([]byte, error) {
if event == nil {
return nil, errors.New("encode lobby race name registered payload: event is nil")
}
if event.RaceName == "" {
return nil, errors.New("encode lobby race name registered payload: race_name is empty")
}
builder := flatbuffers.NewBuilder(48)
raceName := builder.CreateString(event.RaceName)
notificationfbs.LobbyRaceNameRegisteredEventStart(builder)
notificationfbs.LobbyRaceNameRegisteredEventAddRaceName(builder, raceName)
offset := notificationfbs.LobbyRaceNameRegisteredEventEnd(builder)
notificationfbs.FinishLobbyRaceNameRegisteredEventBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// PayloadToLobbyRaceNameRegisteredEvent converts FlatBuffers payload bytes
// into LobbyRaceNameRegisteredEvent.
func PayloadToLobbyRaceNameRegisteredEvent(data []byte) (result *LobbyRaceNameRegisteredEvent, err error) {
if len(data) == 0 {
return nil, errors.New("decode lobby race name registered payload: data is empty")
}
defer recoverNotificationDecodePanic("decode lobby race name registered payload", &result, &err)
event := notificationfbs.GetRootAsLobbyRaceNameRegisteredEvent(data, 0)
raceName, err := requiredNotificationString(event.RaceName(), "race_name")
if err != nil {
return nil, fmt.Errorf("decode lobby race name registered payload: %w", err)
}
return &LobbyRaceNameRegisteredEvent{RaceName: raceName}, nil
}
func requiredNotificationString(value []byte, field string) (string, error) {
if len(value) == 0 {
return "", fmt.Errorf("%s is missing", field)
+34
View File
@@ -71,6 +71,26 @@ func TestNotificationPayloadRoundTrips(t *testing.T) {
},
decode: func(data []byte) (any, error) { return PayloadToLobbyInviteRedeemedEvent(data) },
},
{
name: "lobby race name registration eligible",
source: &LobbyRaceNameRegistrationEligibleEvent{
GameID: "game-8",
RaceName: "Skylancer",
EligibleUntilMs: 1775208100000,
},
encode: func(value any) ([]byte, error) {
return LobbyRaceNameRegistrationEligibleEventToPayload(value.(*LobbyRaceNameRegistrationEligibleEvent))
},
decode: func(data []byte) (any, error) { return PayloadToLobbyRaceNameRegistrationEligibleEvent(data) },
},
{
name: "lobby race name registered",
source: &LobbyRaceNameRegisteredEvent{RaceName: "Skylancer"},
encode: func(value any) ([]byte, error) {
return LobbyRaceNameRegisteredEventToPayload(value.(*LobbyRaceNameRegisteredEvent))
},
decode: func(data []byte) (any, error) { return PayloadToLobbyRaceNameRegisteredEvent(data) },
},
}
for _, tt := range tests {
@@ -151,6 +171,20 @@ func TestNotificationPayloadEncodersRejectNilInputs(t *testing.T) {
return err
},
},
{
name: "lobby race name registration eligible",
call: func() error {
_, err := LobbyRaceNameRegistrationEligibleEventToPayload(nil)
return err
},
},
{
name: "lobby race name registered",
call: func() error {
_, err := LobbyRaceNameRegisteredEventToPayload(nil)
return err
},
},
}
for _, tt := range tests {
+14 -6
View File
@@ -47,10 +47,10 @@ func UpdateMyProfileRequestToPayload(request *usermodel.UpdateMyProfileRequest)
}
builder := flatbuffers.NewBuilder(128)
raceName := builder.CreateString(request.RaceName)
displayName := builder.CreateString(request.DisplayName)
userfbs.UpdateMyProfileRequestStart(builder)
userfbs.UpdateMyProfileRequestAddRaceName(builder, raceName)
userfbs.UpdateMyProfileRequestAddDisplayName(builder, displayName)
offset := userfbs.UpdateMyProfileRequestEnd(builder)
userfbs.FinishUpdateMyProfileRequestBuffer(builder, offset)
@@ -68,7 +68,7 @@ func PayloadToUpdateMyProfileRequest(data []byte) (result *usermodel.UpdateMyPro
request := userfbs.GetRootAsUpdateMyProfileRequest(data, 0)
return &usermodel.UpdateMyProfileRequest{
RaceName: string(request.RaceName()),
DisplayName: string(request.DisplayName()),
}, nil
}
@@ -236,7 +236,11 @@ func encodeAccount(builder *flatbuffers.Builder, account usermodel.Account) (fla
userID := builder.CreateString(account.UserID)
email := builder.CreateString(account.Email)
raceName := builder.CreateString(account.RaceName)
userName := builder.CreateString(account.UserName)
var displayName flatbuffers.UOffsetT
if account.DisplayName != "" {
displayName = builder.CreateString(account.DisplayName)
}
preferredLanguage := builder.CreateString(account.PreferredLanguage)
timeZone := builder.CreateString(account.TimeZone)
@@ -248,7 +252,10 @@ func encodeAccount(builder *flatbuffers.Builder, account usermodel.Account) (fla
userfbs.AccountViewStart(builder)
userfbs.AccountViewAddUserId(builder, userID)
userfbs.AccountViewAddEmail(builder, email)
userfbs.AccountViewAddRaceName(builder, raceName)
userfbs.AccountViewAddUserName(builder, userName)
if displayName != 0 {
userfbs.AccountViewAddDisplayName(builder, displayName)
}
userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage)
userfbs.AccountViewAddTimeZone(builder, timeZone)
if declaredCountry != 0 {
@@ -284,7 +291,8 @@ func decodeAccount(account *userfbs.AccountView) (usermodel.Account, error) {
result := usermodel.Account{
UserID: string(account.UserId()),
Email: string(account.Email()),
RaceName: string(account.RaceName()),
UserName: string(account.UserName()),
DisplayName: string(account.DisplayName()),
PreferredLanguage: string(account.PreferredLanguage()),
TimeZone: string(account.TimeZone()),
DeclaredCountry: string(account.DeclaredCountry()),
+7 -6
View File
@@ -29,7 +29,7 @@ func TestUserRequestPayloadRoundTrips(t *testing.T) {
t.Fatalf("get my account request mismatch: %#v", getDecoded)
}
profileSource := &usermodel.UpdateMyProfileRequest{RaceName: "Nova Prime"}
profileSource := &usermodel.UpdateMyProfileRequest{DisplayName: "NovaPrime"}
profilePayload, err := UpdateMyProfileRequestToPayload(profileSource)
if err != nil {
t.Fatalf("encode update my profile request: %v", err)
@@ -72,7 +72,8 @@ func TestAccountResponsePayloadRoundTrip(t *testing.T) {
Account: usermodel.Account{
UserID: "user-123",
Email: "pilot@example.com",
RaceName: "Pilot Nova",
UserName: "player-abcdefgh",
DisplayName: "PilotNova",
PreferredLanguage: "en",
TimeZone: "Europe/Kaliningrad",
DeclaredCountry: "DE",
@@ -342,14 +343,14 @@ func TestPayloadToAccountResponseRejectsMissingEntitlement(t *testing.T) {
payload := buildAccountResponsePayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
userID := builder.CreateString("user-123")
email := builder.CreateString("pilot@example.com")
raceName := builder.CreateString("Pilot Nova")
userName := builder.CreateString("player-abcdefgh")
preferredLanguage := builder.CreateString("en")
timeZone := builder.CreateString("Europe/Kaliningrad")
userfbs.AccountViewStart(builder)
userfbs.AccountViewAddUserId(builder, userID)
userfbs.AccountViewAddEmail(builder, email)
userfbs.AccountViewAddRaceName(builder, raceName)
userfbs.AccountViewAddUserName(builder, userName)
userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage)
userfbs.AccountViewAddTimeZone(builder, timeZone)
userfbs.AccountViewAddCreatedAtMs(builder, 1)
@@ -411,14 +412,14 @@ func TestPayloadToAccountResponseRejectsOverflowLimitValue(t *testing.T) {
userID := builder.CreateString("user-123")
email := builder.CreateString("pilot@example.com")
raceName := builder.CreateString("Pilot Nova")
userName := builder.CreateString("player-abcdefgh")
preferredLanguage := builder.CreateString("en")
timeZone := builder.CreateString("Europe/Kaliningrad")
userfbs.AccountViewStart(builder)
userfbs.AccountViewAddUserId(builder, userID)
userfbs.AccountViewAddEmail(builder, email)
userfbs.AccountViewAddRaceName(builder, raceName)
userfbs.AccountViewAddUserName(builder, userName)
userfbs.AccountViewAddPreferredLanguage(builder, preferredLanguage)
userfbs.AccountViewAddTimeZone(builder, timeZone)
userfbs.AccountViewAddEntitlement(builder, entitlementOffset)