Skip to content

Commit 2cc6861

Browse files
committed
Participants: fix disconnected players being returned (#98)
Sometimes bots and players dont get deleted from GameState.playersByUserID due to missing player_disconnect events. Some of these cases seem to be re-connects and some are inexplicable to me. This change excludes these disconnected players being returned by Participants.All() etc. - also made sendtables.Entity mockable for datatables_test.go
1 parent a52bbf6 commit 2cc6861

File tree

9 files changed

+278
-31
lines changed

9 files changed

+278
-31
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ script:
5252
- go test -v -race -run "$race_tests" -timeout 15m
5353

5454
# We run all tests again to get full coverage, technically unncecessary tho
55-
- coverpkg_ignore='/msg'
55+
- coverpkg_ignore='/(msg|fake)'
5656
# coverpkg for multiple mains is broken in 1.12, so we need to exclude the examples from coverage
5757
# https://github.com/golang/go/issues/30374
5858
- if ! [[ "$TRAVIS_GO_VERSION" =~ ^1\.11 ]]; then coverpkg_ignore="${coverpkg_ignore}|/examples"; fi

common/player.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type Player struct {
2525
ActiveWeaponID int // Used internally to set the active weapon, see ActiveWeapon()
2626
RawWeapons map[int]*Equipment // All weapons the player is currently carrying
2727
AmmoLeft [32]int // Ammo left in the various weapons, index corresponds to key of RawWeapons
28-
Entity *st.Entity
28+
Entity st.IEntity
2929
AdditionalPlayerInformation *AdditionalPlayerInformation // Mostly scoreboard information such as kills, deaths, etc.
3030
ViewDirectionX float32
3131
ViewDirectionY float32

datatables.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func (p *Parser) bindPlayers() {
178178
})
179179
}
180180

181-
func (p *Parser) bindNewPlayer(playerEntity *st.Entity) {
181+
func (p *Parser) bindNewPlayer(playerEntity st.IEntity) {
182182
entityID := playerEntity.ID()
183183
rp := p.rawPlayers[entityID-1]
184184

@@ -206,6 +206,7 @@ func (p *Parser) bindNewPlayer(playerEntity *st.Entity) {
206206

207207
playerEntity.OnDestroy(func() {
208208
delete(p.gameState.playersByEntityID, entityID)
209+
pl.Entity = nil
209210
})
210211

211212
// Position

datatables_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package demoinfocs
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/mock"
8+
9+
"github.com/markus-wa/demoinfocs-golang/events"
10+
st "github.com/markus-wa/demoinfocs-golang/sendtables"
11+
fakest "github.com/markus-wa/demoinfocs-golang/sendtables/fake"
12+
)
13+
14+
type DevNullReader struct {
15+
}
16+
17+
func (DevNullReader) Read(p []byte) (n int, err error) {
18+
return len(p), nil
19+
}
20+
21+
func TestParser_BindNewPlayer_Issue98(t *testing.T) {
22+
p := NewParser(new(DevNullReader))
23+
24+
p.rawPlayers = map[int]*playerInfo{
25+
0: {
26+
userID: 1,
27+
name: "Zim",
28+
guid: "BOT",
29+
},
30+
1: {
31+
userID: 2,
32+
name: "The Suspect",
33+
guid: "123",
34+
},
35+
}
36+
37+
bot := fakePlayerEntity(1)
38+
p.bindNewPlayer(bot)
39+
bot.Destroy()
40+
41+
player := fakePlayerEntity(2)
42+
p.bindNewPlayer(player)
43+
44+
assert.Len(t, p.GameState().Participants().All(), 1)
45+
}
46+
47+
func TestParser_BindNewPlayer_Issue98_Reconnect(t *testing.T) {
48+
p := NewParser(new(DevNullReader))
49+
50+
p.rawPlayers = map[int]*playerInfo{
51+
0: {
52+
userID: 2,
53+
name: "The Suspect",
54+
guid: "123",
55+
xuid: 1,
56+
},
57+
}
58+
59+
player := fakePlayerEntity(1)
60+
p.bindNewPlayer(player)
61+
player.Destroy()
62+
63+
p.RegisterEventHandler(func(events.PlayerConnect) {
64+
t.Error("expected no more PlayerConnect events but got one")
65+
})
66+
p.bindNewPlayer(player)
67+
68+
assert.Len(t, p.GameState().Participants().All(), 1)
69+
70+
}
71+
72+
func fakePlayerEntity(id int) st.IEntity {
73+
entity := new(fakest.Entity)
74+
entity.On("ID").Return(id)
75+
var destroyCallback func()
76+
entity.On("OnDestroy", mock.Anything).Run(func(args mock.Arguments) {
77+
destroyCallback = args.Get(0).(func())
78+
})
79+
entity.On("OnPositionUpdate", mock.Anything).Return()
80+
entity.On("FindProperty", mock.Anything).Return(new(st.Property))
81+
entity.On("BindProperty", mock.Anything, mock.Anything, mock.Anything).Return(new(st.Property))
82+
entity.On("Destroy").Run(func(mock.Arguments) {
83+
destroyCallback()
84+
})
85+
return entity
86+
}

game_state.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ type Participants struct {
148148
func (ptcp Participants) ByUserID() map[int]*common.Player {
149149
res := make(map[int]*common.Player)
150150
for k, v := range ptcp.playersByUserID {
151-
res[k] = v
151+
// We need to check if the player entity hasn't been destroyed yet
152+
// See https://github.com/markus-wa/demoinfocs-golang/issues/98
153+
if v.Entity != nil {
154+
res[k] = v
155+
}
152156
}
153157
return res
154158
}
@@ -167,8 +171,8 @@ func (ptcp Participants) ByEntityID() map[int]*common.Player {
167171
// All returns all currently connected players & spectators.
168172
// The returned slice is a snapshot and is not updated on changes.
169173
func (ptcp Participants) All() []*common.Player {
170-
res := make([]*common.Player, 0, len(ptcp.playersByUserID))
171-
for _, p := range ptcp.playersByUserID {
174+
res, original := ptcp.initalizeSliceFromByUserID()
175+
for _, p := range original {
172176
res = append(res, p)
173177
}
174178
return res
@@ -177,8 +181,8 @@ func (ptcp Participants) All() []*common.Player {
177181
// Playing returns all players that aren't spectating or unassigned.
178182
// The returned slice is a snapshot and is not updated on changes.
179183
func (ptcp Participants) Playing() []*common.Player {
180-
res := make([]*common.Player, 0, len(ptcp.playersByUserID))
181-
for _, p := range ptcp.playersByUserID {
184+
res, original := ptcp.initalizeSliceFromByUserID()
185+
for _, p := range original {
182186
if p.Team != common.TeamSpectators && p.Team != common.TeamUnassigned {
183187
res = append(res, p)
184188
}
@@ -189,10 +193,10 @@ func (ptcp Participants) Playing() []*common.Player {
189193
// TeamMembers returns all players belonging to the requested team at this time.
190194
// The returned slice is a snapshot and is not updated on changes.
191195
func (ptcp Participants) TeamMembers(team common.Team) []*common.Player {
192-
res := make([]*common.Player, 0, len(ptcp.playersByUserID))
193-
for _, ptcp := range ptcp.playersByUserID {
194-
if ptcp.Team == team {
195-
res = append(res, ptcp)
196+
res, original := ptcp.initalizeSliceFromByUserID()
197+
for _, p := range original {
198+
if p.Team == team {
199+
res = append(res, p)
196200
}
197201
}
198202
return res
@@ -209,3 +213,8 @@ func (ptcp Participants) FindByHandle(handle int) *common.Player {
209213

210214
return ptcp.playersByEntityID[handle&entityHandleIndexMask]
211215
}
216+
217+
func (ptcp Participants) initalizeSliceFromByUserID() ([]*common.Player, map[int]*common.Player) {
218+
byUserID := ptcp.ByUserID()
219+
return make([]*common.Player, 0, len(byUserID)), byUserID
220+
}

game_state_test.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/stretchr/testify/assert"
77

88
"github.com/markus-wa/demoinfocs-golang/common"
9+
st "github.com/markus-wa/demoinfocs-golang/sendtables"
910
)
1011

1112
func TestNewGameState(t *testing.T) {
@@ -41,8 +42,8 @@ func TestGameState_Participants(t *testing.T) {
4142
byUserID := ptcp.ByUserID()
4243

4344
// Should update ptcp as well since it uses the same map
44-
gs.playersByEntityID[0] = common.NewPlayer()
45-
gs.playersByUserID[0] = common.NewPlayer()
45+
gs.playersByEntityID[0] = newPlayer()
46+
gs.playersByUserID[0] = newPlayer()
4647

4748
assert.Equal(t, gs.playersByEntityID, ptcp.ByEntityID())
4849
assert.Equal(t, gs.playersByUserID, ptcp.ByUserID())
@@ -54,7 +55,7 @@ func TestGameState_Participants(t *testing.T) {
5455

5556
func TestParticipants_All(t *testing.T) {
5657
gs := newGameState()
57-
pl := common.NewPlayer()
58+
pl := newPlayer()
5859
gs.playersByUserID[0] = pl
5960

6061
allPlayers := gs.Participants().All()
@@ -65,19 +66,19 @@ func TestParticipants_All(t *testing.T) {
6566
func TestParticipants_Playing(t *testing.T) {
6667
gs := newGameState()
6768

68-
terrorist := common.NewPlayer()
69+
terrorist := newPlayer()
6970
terrorist.Team = common.TeamTerrorists
7071
gs.playersByUserID[0] = terrorist
71-
ct := common.NewPlayer()
72+
ct := newPlayer()
7273
ct.Team = common.TeamCounterTerrorists
7374
gs.playersByUserID[1] = ct
74-
unassigned := common.NewPlayer()
75+
unassigned := newPlayer()
7576
unassigned.Team = common.TeamUnassigned
7677
gs.playersByUserID[2] = unassigned
77-
spectator := common.NewPlayer()
78+
spectator := newPlayer()
7879
spectator.Team = common.TeamSpectators
7980
gs.playersByUserID[3] = spectator
80-
def := common.NewPlayer()
81+
def := newPlayer()
8182
gs.playersByUserID[4] = def
8283

8384
playing := gs.Participants().Playing()
@@ -89,19 +90,19 @@ func TestParticipants_Playing(t *testing.T) {
8990
func TestParticipants_TeamMembers(t *testing.T) {
9091
gs := newGameState()
9192

92-
terrorist := common.NewPlayer()
93+
terrorist := newPlayer()
9394
terrorist.Team = common.TeamTerrorists
9495
gs.playersByUserID[0] = terrorist
95-
ct := common.NewPlayer()
96+
ct := newPlayer()
9697
ct.Team = common.TeamCounterTerrorists
9798
gs.playersByUserID[1] = ct
98-
unassigned := common.NewPlayer()
99+
unassigned := newPlayer()
99100
unassigned.Team = common.TeamUnassigned
100101
gs.playersByUserID[2] = unassigned
101-
spectator := common.NewPlayer()
102+
spectator := newPlayer()
102103
spectator.Team = common.TeamSpectators
103104
gs.playersByUserID[3] = spectator
104-
def := common.NewPlayer()
105+
def := newPlayer()
105106
gs.playersByUserID[4] = def
106107

107108
cts := gs.Participants().TeamMembers(common.TeamCounterTerrorists)
@@ -112,7 +113,7 @@ func TestParticipants_TeamMembers(t *testing.T) {
112113
func TestParticipants_FindByHandle(t *testing.T) {
113114
gs := newGameState()
114115

115-
pl := common.NewPlayer()
116+
pl := newPlayer()
116117
pl.Team = common.TeamTerrorists
117118
gs.playersByEntityID[3000&entityHandleIndexMask] = pl
118119

@@ -124,11 +125,28 @@ func TestParticipants_FindByHandle(t *testing.T) {
124125
func TestParticipants_FindByHandle_InvalidEntityHandle(t *testing.T) {
125126
gs := newGameState()
126127

127-
pl := common.NewPlayer()
128+
pl := newPlayer()
128129
pl.Team = common.TeamTerrorists
129130
gs.playersByEntityID[invalidEntityHandle&entityHandleIndexMask] = pl
130131

131132
found := gs.Participants().FindByHandle(invalidEntityHandle)
132133

133134
assert.Nil(t, found)
134135
}
136+
137+
func TestParticipants_SuppressNoEntity(t *testing.T) {
138+
gs := newGameState()
139+
pl := newPlayer()
140+
gs.playersByUserID[0] = pl
141+
gs.playersByUserID[1] = common.NewPlayer()
142+
143+
allPlayers := gs.Participants().All()
144+
145+
assert.ElementsMatch(t, []*common.Player{pl}, allPlayers)
146+
}
147+
148+
func newPlayer() *common.Player {
149+
pl := common.NewPlayer()
150+
pl.Entity = new(st.Entity)
151+
return pl
152+
}

sendtables/entity.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
bit "github.com/markus-wa/demoinfocs-golang/bitread"
1010
)
1111

12+
//go:generate ifacemaker -f entity.go -s Entity -i IEntity -p sendtables -D -y "IEntity is an auto-generated interface for Entity, intended to be used when mockability is needed." -c "DO NOT EDIT: Auto generated" -o entity_interface.go
13+
1214
// Entity stores a entity in the game (e.g. players etc.) with its properties.
1315
type Entity struct {
1416
serverClass *ServerClass
@@ -56,7 +58,7 @@ func (e *Entity) FindProperty(name string) *Property {
5658
// BindProperty combines FindProperty() & Property.Bind() into one.
5759
// Essentially binds a property's value to a pointer.
5860
// See the docs of the two individual functions for more info.
59-
func (e *Entity) BindProperty(name string, variable interface{}, valueType propertyValueType) {
61+
func (e *Entity) BindProperty(name string, variable interface{}, valueType PropertyValueType) {
6062
e.FindProperty(name).Bind(variable, valueType)
6163
}
6264

@@ -296,12 +298,13 @@ func (pe *Property) Value() PropertyValue {
296298
return pe.value
297299
}
298300

299-
type propertyValueType int
301+
// PropertyValueType specifies the type of a PropertyValue
302+
type PropertyValueType int
300303

301304
// Possible types of property values.
302305
// See Property.Bind()
303306
const (
304-
ValTypeInt propertyValueType = iota
307+
ValTypeInt PropertyValueType = iota
305308
ValTypeFloat32
306309
ValTypeFloat64 // Like ValTypeFloat32 but with additional cast to float64
307310
ValTypeString
@@ -339,7 +342,7 @@ This will bind the property's value to i so every time it's updated i is updated
339342
340343
The valueType indicates which field of the PropertyValue to use for the binding.
341344
*/
342-
func (pe *Property) Bind(variable interface{}, valueType propertyValueType) {
345+
func (pe *Property) Bind(variable interface{}, valueType PropertyValueType) {
343346
var binder PropertyUpdateHandler
344347
switch valueType {
345348
case ValTypeInt:

sendtables/entity_interface.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// DO NOT EDIT: Auto generated
2+
3+
package sendtables
4+
5+
import (
6+
r3 "github.com/golang/geo/r3"
7+
bit "github.com/markus-wa/demoinfocs-golang/bitread"
8+
)
9+
10+
// IEntity is an auto-generated interface for Entity, intended to be used when mockability is needed.
11+
// Entity stores a entity in the game (e.g. players etc.) with its properties.
12+
type IEntity interface {
13+
// ServerClass returns the entity's server-class.
14+
ServerClass() *ServerClass
15+
// ID returns the entity's ID.
16+
ID() int
17+
// Properties returns all properties of the Entity.
18+
Properties() []Property
19+
// FindProperty finds a property on the Entity by name.
20+
//
21+
// Returns nil if the property wasn't found.
22+
//
23+
// Panics if more than one property with the same name was found.
24+
FindProperty(name string) *Property
25+
// BindProperty combines FindProperty() & Property.Bind() into one.
26+
// Essentially binds a property's value to a pointer.
27+
// See the docs of the two individual functions for more info.
28+
BindProperty(name string, variable interface{}, valueType PropertyValueType)
29+
// ApplyUpdate reads an update to an Enitiy's properties and
30+
// triggers registered PropertyUpdateHandlers if values changed.
31+
//
32+
// Intended for internal use only.
33+
ApplyUpdate(reader *bit.BitReader)
34+
// Position returns the entity's position in world coordinates.
35+
Position() r3.Vector
36+
// OnPositionUpdate registers a handler for the entity's position update.
37+
// The handler is called with the new position every time a position-relevant property is updated.
38+
//
39+
// See also Position()
40+
OnPositionUpdate(h func(pos r3.Vector))
41+
// BindPosition binds the entity's position to a pointer variable.
42+
// The pointer is updated every time a position-relevant property is updated.
43+
//
44+
// See also OnPositionUpdate()
45+
BindPosition(pos *r3.Vector)
46+
// OnDestroy registers a function to be called on the entity's destruction.
47+
OnDestroy(delegate func())
48+
// Destroy triggers all via OnDestroy() registered functions.
49+
//
50+
// Intended for internal use only.
51+
Destroy()
52+
// OnCreateFinished registers a function to be called once the entity is fully created -
53+
// i.e. once all property updates have been sent out.
54+
OnCreateFinished(delegate func())
55+
}

0 commit comments

Comments
 (0)