From 8c036ff262f7e0b74729a8fa4f9070b3bd252c88 Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Thu, 30 Apr 2020 13:55:43 -0700 Subject: [PATCH] Implementation of Local SDK Server Player Tracking (#1496) Implementation and unit tests for Player Tracking for the local sdk server. Conformance tests will come after this. Work on #1033 --- pkg/sdkserver/localsdk.go | 139 +++++++++++++++++++++++++++++---- pkg/sdkserver/localsdk_test.go | 130 +++++++++++++++++++++++++++++- 2 files changed, 252 insertions(+), 17 deletions(-) diff --git a/pkg/sdkserver/localsdk.go b/pkg/sdkserver/localsdk.go index 3c4dcb5d09..e100273a0b 100644 --- a/pkg/sdkserver/localsdk.go +++ b/pkg/sdkserver/localsdk.go @@ -20,6 +20,7 @@ import ( "math/rand" "os" "strconv" + "strings" "sync" "time" @@ -197,6 +198,8 @@ func (l *LocalSDKServer) recordRequestWithValue(request string, value string, ob fieldVal = l.gs.ObjectMeta.Uid case "PlayerCapacity": fieldVal = strconv.FormatInt(l.gs.Status.Players.Capacity, 10) + case "PlayerIDs": + fieldVal = strings.Join(l.gs.Status.Players.IDs, ",") default: l.logger.Error("unexpected Field to compare") } @@ -384,28 +387,139 @@ func (l *LocalSDKServer) stopReserveTimer() { // [Stage:Alpha] // [FeatureFlag:PlayerTracking] func (l *LocalSDKServer) PlayerConnect(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) { - panic("implement me") + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled") + } + l.logger.WithField("playerID", id.PlayerID).Info("Player Connected") + l.gsMutex.Lock() + defer l.gsMutex.Unlock() + + if l.gs.Status.Players == nil { + l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{} + } + + // the player is already connected, return false. + for _, playerID := range l.gs.Status.Players.IDs { + if playerID == id.PlayerID { + return &alpha.Bool{Bool: false}, nil + } + } + + if l.gs.Status.Players.Count >= l.gs.Status.Players.Capacity { + return &alpha.Bool{}, errors.New("Players are already at capacity") + } + + l.gs.Status.Players.IDs = append(l.gs.Status.Players.IDs, id.PlayerID) + l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.IDs)) + + l.update <- struct{}{} + l.recordRequestWithValue("playerconnect", "1234", "PlayerIDs") + return &alpha.Bool{Bool: true}, nil } // PlayerDisconnect should be called when a player disconnects. // [Stage:Alpha] // [FeatureFlag:PlayerTracking] func (l *LocalSDKServer) PlayerDisconnect(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) { - panic("implement me") + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled") + } + l.logger.WithField("playerID", id.PlayerID).Info("Player Disconnected") + l.gsMutex.Lock() + defer l.gsMutex.Unlock() + + if l.gs.Status.Players == nil { + l.gs.Status.Players = &sdk.GameServer_Status_PlayerStatus{} + } + + found := -1 + for i, playerID := range l.gs.Status.Players.IDs { + if playerID == id.PlayerID { + found = i + break + } + } + if found == -1 { + return &alpha.Bool{Bool: false}, nil + } + + l.gs.Status.Players.IDs = append(l.gs.Status.Players.IDs[:found], l.gs.Status.Players.IDs[found+1:]...) + l.gs.Status.Players.Count = int64(len(l.gs.Status.Players.IDs)) + + l.update <- struct{}{} + l.recordRequestWithValue("playerdisconnect", "", "PlayerIDs") + return &alpha.Bool{Bool: true}, nil +} + +// IsPlayerConnected returns if the playerID is currently connected to the GameServer. +// [Stage:Alpha] +// [FeatureFlag:PlayerTracking] +func (l *LocalSDKServer) IsPlayerConnected(c context.Context, id *alpha.PlayerID) (*alpha.Bool, error) { + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled") + } + + result := &alpha.Bool{Bool: false} + l.logger.WithField("playerID", id.PlayerID).Info("Is a Player Connected?") + l.gsMutex.Lock() + defer l.gsMutex.Unlock() + + l.recordRequestWithValue("isplayerconnected", id.PlayerID, "PlayerIDs") + + if l.gs.Status.Players == nil { + return result, nil + } + + for _, playerID := range l.gs.Status.Players.IDs { + if id.PlayerID == playerID { + result.Bool = true + break + } + } + + return result, nil } -// IsPlayerConnected returns if the player ID is connected or not +// GetConnectedPlayers returns the list of the currently connected player ids. // [Stage:Alpha] // [FeatureFlag:PlayerTracking] -func (l *LocalSDKServer) IsPlayerConnected(ctx context.Context, id *alpha.PlayerID) (*alpha.Bool, error) { - panic("implement me") +func (l *LocalSDKServer) GetConnectedPlayers(c context.Context, empty *alpha.Empty) (*alpha.PlayerIDList, error) { + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled") + } + l.logger.Info("Getting Connected Players") + + result := &alpha.PlayerIDList{List: []string{}} + + l.gsMutex.Lock() + defer l.gsMutex.Unlock() + l.recordRequest("getconnectedplayers") + + if l.gs.Status.Players == nil { + return result, nil + } + result.List = l.gs.Status.Players.IDs + return result, nil } -// GetConnectedPlayers returns if the players are connected or not +// GetPlayerCount returns the current player count. // [Stage:Alpha] // [FeatureFlag:PlayerTracking] -func (l *LocalSDKServer) GetConnectedPlayers(ctx context.Context, empty *alpha.Empty) (*alpha.PlayerIDList, error) { - panic("implement me") +func (l *LocalSDKServer) GetPlayerCount(ctx context.Context, _ *alpha.Empty) (*alpha.Count, error) { + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + return nil, errors.New(string(runtime.FeaturePlayerTracking) + " not enabled") + } + l.logger.Info("Getting Player Count") + l.recordRequest("getplayercount") + l.gsMutex.RLock() + defer l.gsMutex.RUnlock() + + result := &alpha.Count{} + if l.gs.Status.Players != nil { + result.Count = l.gs.Status.Players.Count + } + + return result, nil } // SetPlayerCapacity to change the game server's player capacity. @@ -454,13 +568,6 @@ func (l *LocalSDKServer) GetPlayerCapacity(_ context.Context, _ *alpha.Empty) (* return result, nil } -// GetPlayerCount returns the current player count. -// [Stage:Alpha] -// [FeatureFlag:PlayerTracking] -func (l *LocalSDKServer) GetPlayerCount(ctx context.Context, _ *alpha.Empty) (*alpha.Count, error) { - panic("implement me") -} - // Close tears down all the things func (l *LocalSDKServer) Close() { l.updateObservers.Range(func(observer, _ interface{}) bool { @@ -498,7 +605,7 @@ func (l *LocalSDKServer) EqualSets(expected, received []string) bool { func (l *LocalSDKServer) compare() { if l.testMode { if !l.EqualSets(l.expectedSequence, l.requestSequence) { - l.logger.Errorf("Testing Failed %v %v", l.expectedSequence, l.requestSequence) + l.logger.WithField("expected", l.expectedSequence).WithField("received", l.requestSequence).Info("Testing Failed") os.Exit(1) } else { l.logger.Info("Received requests match expected list. Test run was successful") diff --git a/pkg/sdkserver/localsdk_test.go b/pkg/sdkserver/localsdk_test.go index 12eafbb696..906b42ea03 100644 --- a/pkg/sdkserver/localsdk_test.go +++ b/pkg/sdkserver/localsdk_test.go @@ -321,6 +321,134 @@ func TestLocalSDKServerPlayerCapacity(t *testing.T) { assert.Equal(t, int64(10), gs.Status.Players.Capacity) } +func TestLocalSDKServerPlayerConnectAndDisconnect(t *testing.T) { + t.Parallel() + + runtime.FeatureTestMutex.Lock() + defer runtime.FeatureTestMutex.Unlock() + assert.NoError(t, runtime.ParseFeatures(string(runtime.FeaturePlayerTracking)+"=true")) + + fixture := &agonesv1.GameServer{ + ObjectMeta: metav1.ObjectMeta{Name: "stuff"}, + Status: agonesv1.GameServerStatus{ + Players: &agonesv1.PlayerStatus{ + Capacity: 1, + }, + }, + } + + e := &alpha.Empty{} + path, err := gsToTmpFile(fixture) + assert.NoError(t, err) + l, err := NewLocalSDKServer(path) + assert.Nil(t, err) + + stream := newGameServerMockStream() + go func() { + err := l.WatchGameServer(&sdk.Empty{}, stream) + assert.Nil(t, err) + }() + + // wait for watching to begin + err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) { + found := false + l.updateObservers.Range(func(_, _ interface{}) bool { + found = true + return false + }) + return found, nil + }) + assert.NoError(t, err) + + count, err := l.GetPlayerCount(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, int64(0), count.Count) + + list, err := l.GetConnectedPlayers(context.Background(), e) + assert.NoError(t, err) + assert.Empty(t, list.List) + + id := &alpha.PlayerID{PlayerID: "one"} + // connect a player + ok, err := l.PlayerConnect(context.Background(), id) + assert.NoError(t, err) + assert.True(t, ok.Bool, "Player should not exist yet") + + count, err = l.GetPlayerCount(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, int64(1), count.Count) + + expected := &sdk.GameServer_Status_PlayerStatus{ + Count: 1, + Capacity: 1, + IDs: []string{id.PlayerID}, + } + assertWatchUpdate(t, stream, expected, func(gs *sdk.GameServer) interface{} { + return gs.Status.Players + }) + + ok, err = l.IsPlayerConnected(context.Background(), id) + assert.NoError(t, err) + assert.True(t, ok.Bool, "player should be connected") + + list, err = l.GetConnectedPlayers(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, []string{id.PlayerID}, list.List) + + // add same player + ok, err = l.PlayerConnect(context.Background(), id) + assert.NoError(t, err) + assert.False(t, ok.Bool, "Player already exists") + + count, err = l.GetPlayerCount(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, int64(1), count.Count) + assertNoWatchUpdate(t, stream) + + list, err = l.GetConnectedPlayers(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, []string{id.PlayerID}, list.List) + + // should return an error if we try to add another, since we're at capacity + nopePlayer := &alpha.PlayerID{PlayerID: "nope"} + _, err = l.PlayerConnect(context.Background(), nopePlayer) + assert.EqualError(t, err, "Players are already at capacity") + + ok, err = l.IsPlayerConnected(context.Background(), nopePlayer) + assert.NoError(t, err) + assert.False(t, ok.Bool) + + // disconnect a player + ok, err = l.PlayerDisconnect(context.Background(), id) + assert.NoError(t, err) + assert.True(t, ok.Bool, "Player should be removed") + count, err = l.GetPlayerCount(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, int64(0), count.Count) + + expected = &sdk.GameServer_Status_PlayerStatus{ + Count: 0, + Capacity: 1, + IDs: []string{}, + } + assertWatchUpdate(t, stream, expected, func(gs *sdk.GameServer) interface{} { + return gs.Status.Players + }) + + list, err = l.GetConnectedPlayers(context.Background(), e) + assert.NoError(t, err) + assert.Empty(t, list.List) + + // remove same player + ok, err = l.PlayerDisconnect(context.Background(), id) + assert.NoError(t, err) + assert.False(t, ok.Bool, "Player already be gone") + count, err = l.GetPlayerCount(context.Background(), e) + assert.NoError(t, err) + assert.Equal(t, int64(0), count.Count) + assertNoWatchUpdate(t, stream) +} + // TestLocalSDKServerStateUpdates verify that SDK functions changes the state of the // GameServer object func TestLocalSDKServerStateUpdates(t *testing.T) { @@ -374,7 +502,7 @@ func TestSDKConformanceFunctionality(t *testing.T) { setAnnotation := "setannotation" l.gs.ObjectMeta.Uid = exampleUID - expected := []string{} + var expected []string expected = append(expected, "", setAnnotation) wg := sync.WaitGroup{}