Conversation
- Added OriginSocialID field to MatchLabel to track social lobby origin - Added OriginSocialID field to MatchSettings for initialization - Modified lobbyCreate to detect and track when private matches are created from social lobbies - Implemented createSocialLobbyRejoin function to create new social lobby with 1-minute reservations - Updated MatchLeave to trigger auto-rejoin when match empties - Updated MatchTerminate to trigger auto-rejoin on match termination - Applied OriginSocialID from settings during match preparation Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
- Added missing evr package import to evr_lobby_create.go - Fixed uuid.UUID handling in createSocialLobbyRejoin function - Corrected LobbyGameServerAllocate parameter types Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
- Added unit tests for OriginSocialID in MatchLabel and MatchSettings - Added tests for IsPrivateMatch helper method - All tests pass successfully Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
- Fixed logic to explicitly exclude social lobbies and only handle Arena/Combat matches - Added clarifying comments explaining that all participants rejoin together - Improved readability of conditional checks Co-authored-by: thesprockee <962164+thesprockee@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements an auto-rejoin feature that sends players back to a new social lobby after they complete a private Arena or Combat match that originated from a social lobby.
Changes:
- Added origin tracking via
OriginSocialIDfield in match metadata - Implemented auto-rejoin logic in
MatchLeaveandMatchTerminatehandlers - Created
createSocialLobbyRejoin()function to spawn new social lobbies with 1-minute player reservations
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
evr_match_social_rejoin_test.go |
Basic unit tests for new OriginSocialID field and IsPrivateMatch() helper |
evr_match_label.go |
Added OriginSocialID field to track social lobby origin |
evr_match.go |
Implemented rejoin logic in match lifecycle hooks and new createSocialLobbyRejoin() helper |
evr_lobby_create.go |
Added origin detection when private matches are created from social lobbies |
| // When a private match empties out, trigger auto-rejoin to social lobby if it originated from one | ||
| if state.OriginSocialID != nil { | ||
| leavingUserIDs := make([]string, 0, len(presences)) | ||
| for _, p := range presences { | ||
| leavingUserIDs = append(leavingUserIDs, p.GetUserId()) | ||
| } | ||
| if err := createSocialLobbyRejoin(ctx, logger, nk, state, leavingUserIDs); err != nil { | ||
| logger.WithField("error", err).Warn("Failed to create social lobby for auto-rejoin") | ||
| } | ||
| } |
There was a problem hiding this comment.
This logic has a critical flaw. When MatchLeave calls createSocialLobbyRejoin on line 749, the state.presenceMap is already empty (as checked on line 738). However, createSocialLobbyRejoin attempts to gather participants from state.presenceMap at lines 1607-1612, which will find no players. This means no participants will be added to the new social lobby, defeating the purpose of the auto-rejoin feature. The function should use the leavingUserIDs parameter to identify who needs rejoin reservations, not the empty presenceMap.
| package server | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/gofrs/uuid/v5" | ||
| "github.com/heroiclabs/nakama/v3/server/evr" | ||
| ) | ||
|
|
||
| func TestMatchLabel_OriginSocialID(t *testing.T) { | ||
| // Test that OriginSocialID can be set and retrieved | ||
| originID := uuid.Must(uuid.NewV4()) | ||
|
|
||
| label := &MatchLabel{ | ||
| Mode: evr.ModeArenaPrivate, | ||
| LobbyType: PrivateLobby, | ||
| OriginSocialID: &originID, | ||
| } | ||
|
|
||
| if label.OriginSocialID == nil { | ||
| t.Fatal("OriginSocialID should not be nil") | ||
| } | ||
|
|
||
| if *label.OriginSocialID != originID { | ||
| t.Errorf("OriginSocialID = %v, want %v", *label.OriginSocialID, originID) | ||
| } | ||
| } | ||
|
|
||
| func TestMatchSettings_OriginSocialID(t *testing.T) { | ||
| // Test that OriginSocialID can be set in MatchSettings | ||
| originID := uuid.Must(uuid.NewV4()) | ||
| groupID := uuid.Must(uuid.NewV4()) | ||
|
|
||
| settings := &MatchSettings{ | ||
| Mode: evr.ModeArenaPrivate, | ||
| Level: evr.LevelArena, | ||
| GroupID: groupID, | ||
| OriginSocialID: &originID, | ||
| } | ||
|
|
||
| if settings.OriginSocialID == nil { | ||
| t.Fatal("OriginSocialID should not be nil") | ||
| } | ||
|
|
||
| if *settings.OriginSocialID != originID { | ||
| t.Errorf("OriginSocialID = %v, want %v", *settings.OriginSocialID, originID) | ||
| } | ||
| } | ||
|
|
||
| func TestMatchLabel_IsPrivateMatch(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| mode evr.Symbol | ||
| want bool | ||
| }{ | ||
| { | ||
| name: "Private Arena", | ||
| mode: evr.ModeArenaPrivate, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "Private Combat", | ||
| mode: evr.ModeCombatPrivate, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "Private Social", | ||
| mode: evr.ModeSocialPrivate, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "Public Arena", | ||
| mode: evr.ModeArenaPublic, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "Public Combat", | ||
| mode: evr.ModeCombatPublic, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "Public Social", | ||
| mode: evr.ModeSocialPublic, | ||
| want: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| label := &MatchLabel{ | ||
| Mode: tt.mode, | ||
| } | ||
| if got := label.IsPrivateMatch(); got != tt.want { | ||
| t.Errorf("IsPrivateMatch() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
The tests only verify that the OriginSocialID field can be set and retrieved, but don't test the actual auto-rejoin functionality. Consider adding integration tests that verify: 1) A social lobby ID is correctly tracked when a private match is created from a social lobby, 2) The createSocialLobbyRejoin function successfully creates a new social lobby with participant reservations when a private match ends, 3) The rejoin doesn't trigger for matches that didn't originate from social lobbies.
| // Gather all participants from this match that need to rejoin | ||
| // We include all participants, not just those leaving, to ensure the whole group rejoins together | ||
| participants := make([]*EvrMatchPresence, 0) |
There was a problem hiding this comment.
The comment says "We include all participants, not just those leaving, to ensure the whole group rejoins together." However, this is misleading because in the MatchLeave case (line 749), state.presenceMap is already empty when this function is called, so no participants will be found. Additionally, the comment suggests intentional behavior of including everyone, but the leavingUserIDs parameter exists yet is unused, creating confusion about the intended design.
| if currentLabel, err := MatchLabelByID(ctx, nk, params.CurrentMatchID); err == nil { | ||
| if currentLabel.Mode == evr.ModeSocialPublic || currentLabel.Mode == evr.ModeSocialPrivate { | ||
| // This private match originated from a social lobby | ||
| originID := params.CurrentMatchID.UUID | ||
| settings.OriginSocialID = &originID | ||
| logger.Info("Private match originated from social lobby", | ||
| zap.String("origin_social_id", originID.String()), | ||
| zap.String("new_mode", params.Mode.String())) | ||
| } |
There was a problem hiding this comment.
The error check silently ignores failures when trying to fetch the current match label. If MatchLabelByID returns an error (e.g., match not found, network issue), the code continues without setting OriginSocialID. While this might be intentional to avoid breaking lobby creation, consider logging the error at least at debug level so it's easier to diagnose why origin tracking might not work in some cases.
| if currentLabel, err := MatchLabelByID(ctx, nk, params.CurrentMatchID); err == nil { | |
| if currentLabel.Mode == evr.ModeSocialPublic || currentLabel.Mode == evr.ModeSocialPrivate { | |
| // This private match originated from a social lobby | |
| originID := params.CurrentMatchID.UUID | |
| settings.OriginSocialID = &originID | |
| logger.Info("Private match originated from social lobby", | |
| zap.String("origin_social_id", originID.String()), | |
| zap.String("new_mode", params.Mode.String())) | |
| } | |
| currentLabel, err := MatchLabelByID(ctx, nk, params.CurrentMatchID) | |
| if err != nil { | |
| logger.Debug("Failed to fetch current match label for origin tracking", | |
| zap.Error(err), | |
| zap.String("current_match_id", params.CurrentMatchID.UUID.String()), | |
| zap.String("mode", params.Mode.String())) | |
| } else if currentLabel.Mode == evr.ModeSocialPublic || currentLabel.Mode == evr.ModeSocialPrivate { | |
| // This private match originated from a social lobby | |
| originID := params.CurrentMatchID.UUID | |
| settings.OriginSocialID = &originID | |
| logger.Info("Private match originated from social lobby", | |
| zap.String("origin_social_id", originID.String()), | |
| zap.String("new_mode", params.Mode.String())) |
| if !state.IsPrivateMatch() { | ||
| return nil | ||
| } | ||
|
|
||
| // Exclude social lobbies - only handle Arena and Combat | ||
| if state.Mode == evr.ModeSocialPrivate || state.Mode == evr.ModeSocialPublic { |
There was a problem hiding this comment.
The logic in this function is redundant. The IsPrivateMatch() method on line 1585 already includes social private lobbies (ModeSocialPrivate), but then lines 1590-1592 explicitly exclude them. This creates confusion. Consider simplifying by removing the IsPrivateMatch() check and only checking for the specific modes you want to handle: ModeArenaPrivate and ModeCombatPrivate.
| if !state.IsPrivateMatch() { | |
| return nil | |
| } | |
| // Exclude social lobbies - only handle Arena and Combat | |
| if state.Mode == evr.ModeSocialPrivate || state.Mode == evr.ModeSocialPublic { | |
| if state.Mode != evr.ModeArenaPrivate && state.Mode != evr.ModeCombatPrivate { | |
| return nil | |
| } | |
| // Check if this match originated from a social lobby | |
| if state.OriginSocialID == nil { |
| } | ||
|
|
||
| // createSocialLobbyRejoin creates a new social lobby for players exiting a private match that originated from a social lobby | ||
| func createSocialLobbyRejoin(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, state *MatchLabel, leavingUserIDs []string) error { |
There was a problem hiding this comment.
The leavingUserIDs parameter is defined but never used in the function body. The function instead gathers all participants from state.presenceMap (lines 1606-1612). Either remove this unused parameter or use it to filter which participants should be included in the rejoin lobby.
When players exit a private Arena/Combat match via the "Social Lobby" button, they should automatically rejoin a new social lobby together if the match originated from one.
Implementation
Tracking origin
OriginSocialIDfield toMatchLabelandMatchSettingslobbyCreatedetects when private matches spawn from social lobbies and stores the origin IDAuto-rejoin trigger
MatchLeave: Creates new social lobby when match emptiesMatchTerminate: Creates new social lobby on match terminationRejoin behavior
createSocialLobbyRejoincreates new social lobby with:Files Changed
evr_match_label.go: AddedOriginSocialIDfieldevr_match.go: Added rejoin logic inMatchLeave/MatchTerminate, implementedcreateSocialLobbyRejoinevr_lobby_create.go: Origin tracking during match creationevr_match_social_rejoin_test.go: Unit tests for new functionalityOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.