Skip to content

Commit d98d320

Browse files
authored
add GameState.Rules().{Round,Freeze,Bomb}Time() (#265)
* add GameState.Rules().{Round,Freeze,Bomb}Time() * golangci: gci local lint mode
1 parent 6129f1a commit d98d320

File tree

11 files changed

+223
-19
lines changed

11 files changed

+223
-19
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ linters-settings:
2929
gocritic:
3030
disabled-checks:
3131
- ifElseChain
32+
gci:
33+
local-prefixes: github.com/markus-wa/demoinfocs-golang/v2

pkg/demoinfocs/datatables.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ func (p *parser) bindGameRules() {
533533

534534
gameRules := p.ServerClasses().FindByName("CCSGameRulesProxy")
535535
gameRules.OnEntityCreated(func(entity st.Entity) {
536+
p.gameState.rules.entity = entity
537+
536538
entity.Property(grPrefix("m_gamePhase")).OnUpdate(func(val st.PropertyValue) {
537539
oldGamePhase := p.gameState.gamePhase
538540
p.gameState.gamePhase = common.GamePhase(val.IntVal)

pkg/demoinfocs/fake/game_rules.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package fake
2+
3+
import (
4+
"time"
5+
6+
"github.com/stretchr/testify/mock"
7+
8+
demoinfocs "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs"
9+
)
10+
11+
var _ demoinfocs.GameRules = new(GameRules)
12+
13+
// GameRules is a mock for of demoinfocs.GameRules.
14+
type GameRules struct {
15+
mock.Mock
16+
}
17+
18+
// BombTime is a mock-implementation of GameRules.ByUserID().
19+
func (gr *GameRules) BombTime() (time.Duration, error) {
20+
return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error)
21+
}
22+
23+
// FreezeTime is a mock-implementation of GameRules.ByUserID().
24+
func (gr *GameRules) FreezeTime() (time.Duration, error) {
25+
return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error)
26+
}
27+
28+
// RoundTime is a mock-implementation of GameRules.ByUserID().
29+
func (gr *GameRules) RoundTime() (time.Duration, error) {
30+
return gr.Called().Get(0).(time.Duration), gr.Called().Get(0).(error)
31+
}
32+
33+
// ConVars is a mock-implementation of GameRules.ConVars().
34+
func (gr *GameRules) ConVars() map[string]string {
35+
return gr.Called().Get(0).(map[string]string)
36+
}

pkg/demoinfocs/fake/game_state.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,8 @@ func (gs *GameState) IsMatchStarted() bool {
8989
func (gs *GameState) ConVars() map[string]string {
9090
return gs.Called().Get(0).(map[string]string)
9191
}
92+
93+
// Rules is a mock-implementation of GameState.Rules().
94+
func (gs *GameState) Rules() demoinfocs.GameRules {
95+
return gs.Called().Get(0).(demoinfocs.GameRules)
96+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// DO NOT EDIT: Auto generated
2+
3+
package demoinfocs
4+
5+
import (
6+
"time"
7+
)
8+
9+
// GameRules is an auto-generated interface for gameRules.
10+
type GameRules interface {
11+
// RoundTime returns how long rounds in the current match last for (excluding freeze time).
12+
// May return error if cs_gamerules_data.m_iRoundTime is not set.
13+
RoundTime() (time.Duration, error)
14+
// FreezeTime returns how long freeze time lasts for in the current match (mp_freezetime).
15+
// May return error if mp_freezetime cannot be converted to a time duration.
16+
FreezeTime() (time.Duration, error)
17+
// BombTime returns how long freeze time lasts for in the current match (mp_freezetime).
18+
// May return error if mp_c4timer cannot be converted to a time duration.
19+
BombTime() (time.Duration, error)
20+
// ConVars returns a map of CVar keys and values.
21+
// Not all values might be set.
22+
// See also: https://developer.valvesoftware.com/wiki/List_of_CS:GO_Cvars.
23+
ConVars() map[string]string
24+
}

pkg/demoinfocs/game_state.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package demoinfocs
22

33
import (
4+
"errors"
5+
"strconv"
6+
"time"
7+
48
constants "github.com/markus-wa/demoinfocs-golang/v2/internal/constants"
59
common "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common"
610
st "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/sendtables"
711
)
812

913
//go:generate ifacemaker -f game_state.go -s gameState -i GameState -p demoinfocs -D -y "GameState is an auto-generated interface for gameState." -c "DO NOT EDIT: Auto generated" -o game_state_interface.go
1014
//go:generate ifacemaker -f game_state.go -s participants -i Participants -p demoinfocs -D -y "Participants is an auto-generated interface for participants." -c "DO NOT EDIT: Auto generated" -o participants_interface.go
15+
//go:generate ifacemaker -f game_state.go -s gameRules -i GameRules -p demoinfocs -D -y "GameRules is an auto-generated interface for gameRules." -c "DO NOT EDIT: Auto generated" -o game_rules_interface.go
1116

1217
// gameState contains all game-state relevant information.
1318
type gameState struct {
@@ -20,7 +25,6 @@ type gameState struct {
2025
infernos map[int]*common.Inferno // Maps entity-IDs to active infernos.
2126
weapons map[int]*common.Equipment // Maps entity IDs to weapons. Used to remember what a weapon is (p250 / cz etc.)
2227
entities map[int]st.Entity // Maps entity IDs to entities
23-
conVars map[string]string
2428
bomb common.Bomb
2529
totalRoundsPlayed int
2630
gamePhase common.GamePhase
@@ -30,6 +34,8 @@ type gameState struct {
3034
currentDefuser *common.Player // Player currently defusing the bomb, if any
3135
currentPlanter *common.Player // Player currently planting the bomb, if any
3236
thrownGrenades map[*common.Player][]*common.Equipment // Information about every player's thrown grenades (from the moment they are thrown to the moment their effect is ended)
37+
rules gameRules
38+
demoInfo demoInfoProvider
3339
}
3440

3541
type lastFlash struct {
@@ -88,6 +94,12 @@ func (gs gameState) Participants() Participants {
8894
}
8995
}
9096

97+
// Rules returns the GameRules for the current match.
98+
// Contains information like freeze time duration etc.
99+
func (gs gameState) Rules() GameRules {
100+
return gs.rules
101+
}
102+
91103
// GrenadeProjectiles returns a map from entity-IDs to all live grenade projectiles.
92104
//
93105
// Only constains projectiles currently in-flight or still active (smokes etc.),
@@ -140,23 +152,27 @@ func (gs gameState) IsMatchStarted() bool {
140152
// ConVars returns a map of CVar keys and values.
141153
// Not all values might be set.
142154
// See also: https://developer.valvesoftware.com/wiki/List_of_CS:GO_Cvars.
155+
// Deprecated: see GameRules().ConVars()
143156
func (gs *gameState) ConVars() map[string]string {
144-
return gs.conVars
157+
return gs.rules.ConVars()
145158
}
146159

147-
func newGameState() *gameState {
160+
func newGameState(demoInfo demoInfoProvider) *gameState {
148161
gs := &gameState{
149162
playersByEntityID: make(map[int]*common.Player),
150163
playersByUserID: make(map[int]*common.Player),
151164
grenadeProjectiles: make(map[int]*common.GrenadeProjectile),
152165
infernos: make(map[int]*common.Inferno),
153166
weapons: make(map[int]*common.Equipment),
154167
entities: make(map[int]st.Entity),
155-
conVars: make(map[string]string),
156168
thrownGrenades: make(map[*common.Player][]*common.Equipment),
157169
lastFlash: lastFlash{
158170
projectileByPlayer: make(map[*common.Player]*common.GrenadeProjectile),
159171
},
172+
rules: gameRules{
173+
conVars: make(map[string]string),
174+
},
175+
demoInfo: demoInfo,
160176
}
161177

162178
gs.tState = common.NewTeamState(common.TeamTerrorists, gs.Participants().TeamMembers)
@@ -167,6 +183,57 @@ func newGameState() *gameState {
167183
return gs
168184
}
169185

186+
type gameRules struct {
187+
conVars map[string]string
188+
entity st.Entity
189+
}
190+
191+
var ErrFailedToRetrieveGameRule = errors.New("failed to retrieve GameRule value, it's recommended to have a fallback to a default value for this scenario")
192+
193+
// RoundTime returns how long rounds in the current match last for (excluding freeze time).
194+
// May return error if cs_gamerules_data.m_iRoundTime is not set.
195+
func (gr gameRules) RoundTime() (time.Duration, error) {
196+
if gr.entity == nil {
197+
return 0, ErrFailedToRetrieveGameRule
198+
}
199+
200+
prop := gr.entity.Property("cs_gamerules_data.m_iRoundTime")
201+
if prop == nil {
202+
return 0, ErrFailedToRetrieveGameRule
203+
}
204+
205+
return time.Duration(prop.Value().IntVal) * time.Second, nil
206+
}
207+
208+
// FreezeTime returns how long freeze time lasts for in the current match (mp_freezetime).
209+
// May return error if mp_freezetime cannot be converted to a time duration.
210+
func (gr gameRules) FreezeTime() (time.Duration, error) {
211+
t, err := strconv.Atoi(gr.conVars["mp_freezetime"])
212+
if err != nil {
213+
return 0, ErrFailedToRetrieveGameRule
214+
}
215+
216+
return time.Duration(t) * time.Second, nil
217+
}
218+
219+
// BombTime returns how long freeze time lasts for in the current match (mp_freezetime).
220+
// May return error if mp_c4timer cannot be converted to a time duration.
221+
func (gr gameRules) BombTime() (time.Duration, error) {
222+
t, err := strconv.Atoi(gr.conVars["mp_c4timer"])
223+
if err != nil {
224+
return 0, ErrFailedToRetrieveGameRule
225+
}
226+
227+
return time.Duration(t) * time.Second, nil
228+
}
229+
230+
// ConVars returns a map of CVar keys and values.
231+
// Not all values might be set.
232+
// See also: https://developer.valvesoftware.com/wiki/List_of_CS:GO_Cvars.
233+
func (gr gameRules) ConVars() map[string]string {
234+
return gr.conVars
235+
}
236+
170237
// participants provides helper functions on top of the currently connected players.
171238
// E.g. ByUserID(), ByEntityID(), TeamMembers(), etc.
172239
//

pkg/demoinfocs/game_state_interface.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type GameState interface {
3030
// Participants returns a struct with all currently connected players & spectators and utility functions.
3131
// The struct contains references to the original maps so it's always up-to-date.
3232
Participants() Participants
33+
// Rules returns the GameRules for the current match.
34+
// Contains information like freeze time duration etc.
35+
Rules() GameRules
3336
// GrenadeProjectiles returns a map from entity-IDs to all live grenade projectiles.
3437
//
3538
// Only constains projectiles currently in-flight or still active (smokes etc.),
@@ -55,5 +58,6 @@ type GameState interface {
5558
// ConVars returns a map of CVar keys and values.
5659
// Not all values might be set.
5760
// See also: https://developer.valvesoftware.com/wiki/List_of_CS:GO_Cvars.
61+
// Deprecated: see GameRules().ConVars()
5862
ConVars() map[string]string
5963
}

pkg/demoinfocs/game_state_test.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package demoinfocs
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/stretchr/testify/assert"
78

@@ -12,34 +13,35 @@ import (
1213
)
1314

1415
func TestNewGameState(t *testing.T) {
15-
gs := newGameState()
16+
gs := newGameState(demoInfoProvider{})
1617

1718
assert.NotNil(t, gs.playersByEntityID)
1819
assert.NotNil(t, gs.playersByUserID)
1920
assert.NotNil(t, gs.grenadeProjectiles)
2021
assert.NotNil(t, gs.infernos)
2122
assert.NotNil(t, gs.weapons)
2223
assert.NotNil(t, gs.entities)
24+
assert.NotNil(t, gs.rules.conVars)
2325
assert.Equal(t, common.TeamTerrorists, gs.tState.Team())
2426
assert.Equal(t, common.TeamCounterTerrorists, gs.ctState.Team())
2527
}
2628

2729
func TestNewGameState_TeamState_Pointers(t *testing.T) {
28-
gs := newGameState()
30+
gs := newGameState(demoInfoProvider{})
2931

3032
assert.True(t, gs.TeamCounterTerrorists() == &gs.ctState)
3133
assert.True(t, gs.TeamTerrorists() == &gs.tState)
3234
}
3335

3436
func TestNewGameState_TeamState_Opponent(t *testing.T) {
35-
gs := newGameState()
37+
gs := newGameState(demoInfoProvider{})
3638

3739
assert.True(t, &gs.ctState == gs.tState.Opponent)
3840
assert.True(t, &gs.tState == gs.ctState.Opponent)
3941
}
4042

4143
func TestGameState_Participants(t *testing.T) {
42-
gs := newGameState()
44+
gs := newGameState(demoInfoProvider{})
4345
ptcp := gs.Participants()
4446
byEntity := ptcp.ByEntityID()
4547
byUserID := ptcp.ByUserID()
@@ -70,13 +72,6 @@ func TestGameState_Participants(t *testing.T) {
7072
assert.NotEqual(t, allByUserID, ptcp.ByUserID())
7173
}
7274

73-
func TestGameState_ConVars(t *testing.T) {
74-
cvars := make(map[string]string)
75-
gs := gameState{conVars: cvars}
76-
77-
assert.Equal(t, cvars, gs.ConVars())
78-
}
79-
8075
func TestParticipants_All(t *testing.T) {
8176
pl := newPlayer()
8277
ptcps := participants{
@@ -282,6 +277,69 @@ func TestParticipants_SpottedBy(t *testing.T) {
282277
assert.ElementsMatch(t, []*common.Player{spotted1, spotted2}, spotted)
283278
}
284279

280+
func TestGameRules_ConVars(t *testing.T) {
281+
cvars := make(map[string]string)
282+
gr := gameRules{conVars: cvars}
283+
284+
assert.Equal(t, cvars, gr.ConVars())
285+
286+
gs := gameState{rules: gr}
287+
assert.Equal(t, cvars, gs.ConVars())
288+
}
289+
290+
func TestGameRules_BombTime(t *testing.T) {
291+
gs := gameRules{conVars: map[string]string{"mp_c4timer": "5"}}
292+
293+
bt, err := gs.BombTime()
294+
295+
assert.Nil(t, err)
296+
assert.Equal(t, 5*time.Second, bt)
297+
}
298+
299+
func TestGameRules_FreezeTime(t *testing.T) {
300+
gs := gameRules{conVars: map[string]string{"mp_freezetime": "5"}}
301+
302+
bt, err := gs.FreezeTime()
303+
304+
assert.Nil(t, err)
305+
assert.Equal(t, 5*time.Second, bt)
306+
}
307+
308+
func TestGameRules_RoundTime(t *testing.T) {
309+
prop := new(stfake.Property)
310+
prop.On("Value").Return(st.PropertyValue{IntVal: 115})
311+
ent := new(stfake.Entity)
312+
ent.On("Property", "cs_gamerules_data.m_iRoundTime").Return(prop)
313+
gr := gameRules{entity: ent}
314+
315+
rt, err := gr.RoundTime()
316+
317+
assert.Nil(t, err)
318+
assert.Equal(t, 115*time.Second, rt)
319+
}
320+
321+
func TestGameRules(t *testing.T) {
322+
gr := gameRules{
323+
conVars: map[string]string{},
324+
}
325+
326+
_, err := gr.RoundTime()
327+
assert.Equal(t, ErrFailedToRetrieveGameRule, err)
328+
329+
_, err = gr.BombTime()
330+
assert.Equal(t, ErrFailedToRetrieveGameRule, err)
331+
332+
_, err = gr.FreezeTime()
333+
assert.Equal(t, ErrFailedToRetrieveGameRule, err)
334+
335+
ent := new(stfake.Entity)
336+
ent.On("Property", "cs_gamerules_data.m_iRoundTime").Return(nil)
337+
gr = gameRules{entity: ent}
338+
339+
_, err = gr.RoundTime()
340+
assert.Equal(t, ErrFailedToRetrieveGameRule, err)
341+
}
342+
285343
func newPlayer() *common.Player {
286344
pl := newPlayerWithEntityID(1)
287345
return pl

pkg/demoinfocs/net_messages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (p *parser) handleSetConVar(setConVar *msg.CNETMsg_SetConVar) {
5454
updated := make(map[string]string)
5555
for _, cvar := range setConVar.Convars.Cvars {
5656
updated[cvar.Name] = cvar.Value
57-
p.gameState.conVars[cvar.Name] = cvar.Value
57+
p.gameState.rules.conVars[cvar.Name] = cvar.Value
5858
}
5959

6060
p.eventDispatcher.Dispatch(events.ConVarsUpdated{

pkg/demoinfocs/parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,11 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
305305
p.rawPlayers = make(map[int]*playerInfo)
306306
p.triggers = make(map[int]*boundingBoxInformation)
307307
p.cancelChan = make(chan struct{}, 1)
308-
p.gameState = newGameState()
308+
p.demoInfoProvider = demoInfoProvider{parser: &p}
309+
p.gameState = newGameState(p.demoInfoProvider)
309310
p.grenadeModelIndices = make(map[int]common.EquipmentType)
310311
p.gameEventHandler = newGameEventHandler(&p)
311312
p.userMessageHandler = newUserMessageHandler(&p)
312-
p.demoInfoProvider = demoInfoProvider{parser: &p}
313313

314314
dispatcherCfg := dp.Config{
315315
PanicHandler: func(v interface{}) {

0 commit comments

Comments
 (0)