Skip to content

Commit

Permalink
Implementation of Local SDK Server Player Tracking (googleforgames#1496)
Browse files Browse the repository at this point in the history
Implementation and unit tests for Player Tracking for the local sdk
server.

Conformance tests will come after this.

Work on googleforgames#1033
  • Loading branch information
markmandel authored and ilkercelikyilmaz committed Oct 23, 2020
1 parent 1049974 commit 8c036ff
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 17 deletions.
139 changes: 123 additions & 16 deletions pkg/sdkserver/localsdk.go
Expand Up @@ -20,6 +20,7 @@ import (
"math/rand"
"os"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
130 changes: 129 additions & 1 deletion pkg/sdkserver/localsdk_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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{}
Expand Down

0 comments on commit 8c036ff

Please sign in to comment.