Skip to content

Commit ecb9302

Browse files
authored
Merge pull request #99 from markus-wa/issue-98-unexpected-player
Parser: Player tracking improvements
2 parents 4067ea9 + 2cc6861 commit ecb9302

File tree

12 files changed

+287
-38
lines changed

12 files changed

+287
-38
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: 3 additions & 2 deletions
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

@@ -193,7 +193,7 @@ func (p *Parser) bindNewPlayer(playerEntity *st.Entity) {
193193
pl = common.NewPlayer()
194194
pl.Name = rp.name
195195
pl.SteamID = rp.xuid
196-
pl.IsBot = rp.isFakePlayer
196+
pl.IsBot = rp.isFakePlayer || rp.guid == "BOT"
197197
pl.UserID = rp.userID
198198
}
199199
}
@@ -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_events.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func (geh gameEventHandler) playerBlind(desc *msg.CSVCMsg_GameEventListDescripto
286286

287287
// Player.FlashDuration hasn't been updated yet,
288288
// so we need to wait until the end of the tick before dispatching
289-
geh.parser.currentFlashEvents = append(geh.parser.currentFlashEvents, events.PlayerFlashed{
289+
geh.parser.delayedEvents = append(geh.parser.delayedEvents, events.PlayerFlashed{
290290
Player: geh.playerByUserID32(data["userid"].GetValShort()),
291291
Attacker: geh.gameState().lastFlasher,
292292
})
@@ -390,7 +390,10 @@ func (geh gameEventHandler) playerTeam(desc *msg.CSVCMsg_GameEventListDescriptor
390390
player.Team = newTeam
391391

392392
oldTeam := common.Team(data["oldteam"].GetValByte())
393-
geh.dispatch(events.PlayerTeamChange{
393+
// Delayed for two reasons
394+
// - team IDs of other players changing teams in the same tick might not have changed yet
395+
// - player entities might not have been re-created yet after a reconnect
396+
geh.parser.delayedEvents = append(geh.parser.delayedEvents, events.PlayerTeamChange{
394397
Player: player,
395398
IsBot: data["isbot"].GetValBool(),
396399
Silent: data["silent"].GetValBool(),

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+
}

parser.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010

1111
bit "github.com/markus-wa/demoinfocs-golang/bitread"
1212
common "github.com/markus-wa/demoinfocs-golang/common"
13-
events "github.com/markus-wa/demoinfocs-golang/events"
1413
msg "github.com/markus-wa/demoinfocs-golang/msg"
1514
st "github.com/markus-wa/demoinfocs-golang/sendtables"
1615
)
@@ -70,7 +69,7 @@ type Parser struct {
7069
gameEventDescs map[int32]*msg.CSVCMsg_GameEventListDescriptorT // Maps game-event IDs to descriptors
7170
grenadeModelIndices map[int]common.EquipmentElement // Used to map model indices to grenades (used for grenade projectiles)
7271
stringTables []*msg.CSVCMsg_CreateStringTable // Contains all created sendtables, needed when updating them
73-
currentFlashEvents []events.PlayerFlashed // Contains flash events that need to be dispatched at the end of a tick
72+
delayedEvents []interface{} // Contains events that need to be dispatched at the end of a tick (e.g. flash events because FlashDuration isn't updated before that)
7473
}
7574

7675
type bombsite struct {

parsing.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,10 @@ var frameParsedToken = new(frameParsedTokenType)
247247
func (p *Parser) handleFrameParsed(*frameParsedTokenType) {
248248
// PlayerFlashed events need to be dispatched at the end of the tick
249249
// because Player.FlashDuration is updated after the game-events are parsed.
250-
for _, e := range p.currentFlashEvents {
250+
for _, e := range p.delayedEvents {
251251
p.eventDispatcher.Dispatch(e)
252252
}
253-
p.currentFlashEvents = p.currentFlashEvents[:0]
253+
p.delayedEvents = p.delayedEvents[:0]
254254

255255
p.currentFrame++
256256
p.eventDispatcher.Dispatch(events.TickDone{})

0 commit comments

Comments
 (0)