Skip to content

Commit

Permalink
WIP: frame rate pow2 stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
markus-wa committed Jun 5, 2021
1 parent d92b1e8 commit eeea582
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 51 deletions.
8 changes: 4 additions & 4 deletions pkg/demoinfocs/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ type DemoHeader struct {
// Not necessarily the tick-rate the server ran on during the game.
//
// Returns 0 if PlaybackTime or PlaybackFrames are 0 (corrupt demo headers).
// Deprecated, see Parser.FrameRate() for a more resilient implementation that should work with corrupt demo headers.
func (h *DemoHeader) FrameRate() float64 {
// Deprecated, see Parser.FrameRatePow2() for a more resilient implementation that should work with corrupt demo headers.
func (h DemoHeader) FrameRate() float64 {
if h.PlaybackTime == 0 {
return 0
}
Expand All @@ -54,8 +54,8 @@ func (h *DemoHeader) FrameRate() float64 {
// FrameTime returns the time a frame / demo-tick takes in seconds.
//
// Returns 0 if PlaybackTime or PlaybackFrames are 0 (corrupt demo headers).
// Deprecated, see Parser.FrameTime() for a more resilient implementation that should work with corrupt demo headers.
func (h *DemoHeader) FrameTime() time.Duration {
// Deprecated, see Parser.FrameTimePow2() for a more resilient implementation that should work with corrupt demo headers.
func (h DemoHeader) FrameTime() time.Duration {
if h.PlaybackFrames == 0 {
return 0
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/demoinfocs/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,17 @@ type TickRateInfoAvailable struct {
TickTime time.Duration // See Parser.TickTime()
}

// FrameRateCalibrated signals that the demo's frame rate is available.
// This can happen either by reading the demo header,
// or if that is corrupt then once enough frames have passed to calibrate th frame-rate manually.
// See also ParserConfig.FrameRateCalibrationFrames.
type FrameRateCalibrated struct {
FrameRatePow2 float64 // The frame rate as a power of two, this is likely to be more accurate than FrameRateCalculated for most demos
FrameTimePow2 time.Duration // The frame time calculated with FrameRatePow2, this is likely to be more accurate than FrameTimeCalculated for most demos
FrameRateCalculated float64 // FrameRatePow2 might be more accurate
FrameTimeCalculated time.Duration // FrameTimePow2 might be more accurate
}

// ChatMessage signals a player generated chat message.
// Since team chat is generally not recorded IsChatAll will probably always be false.
// See SayText for admin / console messages and SayText2 for raw network package data.
Expand Down
20 changes: 20 additions & 0 deletions pkg/demoinfocs/fake/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ func (p *Parser) TickTime() time.Duration {
return p.Called().Get(0).(time.Duration)
}

// FrameRateCalculated is a mock-implementation of Parser.FrameRateCalculated().
func (p *Parser) FrameRateCalculated() float64 {
return p.Called().Get(0).(float64)
}

// FrameTimeCalculated is a mock-implementation of Parser.FrameTimeCalculated().
func (p *Parser) FrameTimeCalculated() time.Duration {
return p.Called().Get(0).(time.Duration)
}

// FrameRatePow2 is a mock-implementation of Parser.FrameRatePow2().
func (p *Parser) FrameRatePow2() float64 {
return p.Called().Get(0).(float64)
}

// FrameTimePow2 is a mock-implementation of Parser.FrameTimePow2().
func (p *Parser) FrameTimePow2() time.Duration {
return p.Called().Get(0).(time.Duration)
}

// Progress is a mock-implementation of Parser.Progress().
func (p *Parser) Progress() float32 {
return p.Called().Get(0).(float32)
Expand Down
7 changes: 0 additions & 7 deletions pkg/demoinfocs/game_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@ type lastFlash struct {
projectileByPlayer map[*common.Player]*common.GrenadeProjectile
}

type ingameTickNumber int

func (gs *gameState) handleIngameTickNumber(n ingameTickNumber) {
gs.ingameTick = int(n)
debugIngameTick(gs.ingameTick)
}

// IngameTick returns the latest actual tick number of the server during the game.
//
// Watch out, I've seen this return wonky negative numbers at the start of demos.
Expand Down
101 changes: 77 additions & 24 deletions pkg/demoinfocs/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type parser struct {
userMessageHandler userMessageHandler
eventDispatcher *dp.Dispatcher
currentFrame int // Demo-frame, not ingame-tick
calibratedFrameRate float64 // Calibrated frame-rate for corrupt demo headers, only available after calibration
calibratedFrameRatePow2 float64 // Calibrated frame-rate for corrupt demo headers as power of 2, only available after calibration
tickInterval float32 // Duration between ticks in seconds
header *common.DemoHeader // Pointer so we can check for nil
gameState *gameState
Expand All @@ -64,16 +66,18 @@ type parser struct {

// Additional fields, mainly caching & tracking things

bombsiteA bombsite
bombsiteB bombsite
equipmentMapping map[*st.ServerClass]common.EquipmentType // Maps server classes to equipment-types
rawPlayers map[int]*playerInfo // Maps entity IDs to 'raw' player info
modelPreCache []string // Used to find out whether a weapon is a p250 or cz for example (same id)
triggers map[int]*boundingBoxInformation // Maps entity IDs to triggers (used for bombsites)
gameEventDescs map[int32]*msg.CSVCMsg_GameEventListDescriptorT // Maps game-event IDs to descriptors
grenadeModelIndices map[int]common.EquipmentType // Used to map model indices to grenades (used for grenade projectiles)
stringTables []*msg.CSVCMsg_CreateStringTable // Contains all created sendtables, needed when updating them
delayedEventHandlers []func() // Contains event handlers that need to be executed at the end of a tick (e.g. flash events because FlashDuration isn't updated before that)
bombsiteA bombsite
bombsiteB bombsite
equipmentMapping map[*st.ServerClass]common.EquipmentType // Maps server classes to equipment-types
rawPlayers map[int]*playerInfo // Maps entity IDs to 'raw' player info
modelPreCache []string // Used to find out whether a weapon is a p250 or cz for example (same id)
triggers map[int]*boundingBoxInformation // Maps entity IDs to triggers (used for bombsites)
gameEventDescs map[int32]*msg.CSVCMsg_GameEventListDescriptorT // Maps game-event IDs to descriptors
grenadeModelIndices map[int]common.EquipmentType // Used to map model indices to grenades (used for grenade projectiles)
stringTables []*msg.CSVCMsg_CreateStringTable // Contains all created sendtables, needed when updating them
delayedEventHandlers []func() // Contains event handlers that need to be executed at the end of a tick (e.g. flash events because FlashDuration isn't updated before that)
tickDiffs map[int]int // Used for frame rate calibration if the demo header is corrupt
frameRateCalibrationFrames int // See ParserConfig.FrameRateCalibrationFrames
}

// NetMessageCreator creates additional net-messages to be dispatched to net-message handlers.
Expand Down Expand Up @@ -174,19 +178,31 @@ func legayTickTime(h common.DemoHeader) time.Duration {
return time.Duration(h.PlaybackTime.Nanoseconds() / int64(h.PlaybackTicks))
}

// FrameRate returns the frame rate of the demo (frames / demo-ticks per second).
// FrameRateCalculated returns the frame rate of the demo (frames aka. demo-ticks per second).
// Not necessarily the tick-rate the server ran on during the game.
// See FrameRatePow2() for a possibly more accurate number.
//
// Returns frame rate from DemoHeader if it's not corrupt.
// Otherwise returns frame rate based on the current frame, tick-rate and ingame-tick.
// May also return -1 before parsing has started.
func (p *parser) FrameRate() float64 {
// Otherwise returns frame rate that has automatically bee calibrated.
// May also return -1 before calibration has finished.
// See also events.FrameRateCalibrated.
func (p *parser) FrameRateCalculated() float64 {
if p.header != nil && p.header.PlaybackTime != 0 && p.header.PlaybackFrames != 0 {
return legacyFrameRate(*p.header)
}

if p.gameState.ingameTick > 0 && p.currentFrame > 0 && p.TickRate() > 0 {
return float64(p.currentFrame) * p.TickRate() / float64(p.gameState.ingameTick)
if p.calibratedFrameRate > 0 {
return p.calibratedFrameRate
}

return -1
}

// FrameRatePow2 returns the frame rate of the demo (frames aka. demo-ticks per second) as a power of 2 (16, 32, 64 ...).
// Returns -1 before calibration has finished.
func (p *parser) FrameRatePow2() float64 {
if p.calibratedFrameRatePow2 > 0 {
return p.calibratedFrameRatePow2
}

return -1
Expand All @@ -196,13 +212,28 @@ func legacyFrameRate(h common.DemoHeader) float64 {
return float64(h.PlaybackFrames) / h.PlaybackTime.Seconds()
}

// FrameTime returns the time a frame / demo-tick takes in seconds.
// FrameTimeCalculated returns the time a frame / demo-tick takes in seconds.
// See FrameTimePow2() for a possibly more accurate number.
//
// Returns frame time from DemoHeader if it's not corrupt.
// Otherwise returns frame time based on the current frame, tick-rate and ingame-tick.
// May also return -1 before parsing has started.
func (p *parser) FrameTime() time.Duration {
if frameRate := p.FrameRate(); frameRate > 0 {
// Otherwise returns frame time that has automatically bee calibrated.
// May also return -1 before calibration has finished.
// See also events.FrameRateCalibrated.
func (p *parser) FrameTimeCalculated() time.Duration {
if frameRate := p.FrameRateCalculated(); frameRate > 0 {
return time.Duration(float64(time.Second) / frameRate)
}

return -1
}

// FrameTimePow2 returns the time a frame / demo-tick takes in seconds.
//
// Returns -1 before calibration has finished.
// See also events.FrameRateCalibrated.
// See also FrameRatePow2().
func (p *parser) FrameTimePow2() time.Duration {
if frameRate := p.FrameRatePow2(); frameRate > 0 {
return time.Duration(float64(time.Second) / frameRate)
}

Expand Down Expand Up @@ -324,13 +355,28 @@ type ParserConfig struct {
// The creators should return a new instance of the correct protobuf-message type (from the msg package).
// Interesting net-message-IDs can easily be discovered with the build-tag 'debugdemoinfocs'; when looking for 'UnhandledMessage'.
// Check out parsing.go to see which net-messages are already being parsed by default.
// This is a beta feature and may be changed or replaced without notice.
AdditionalNetMessageCreators map[int]NetMessageCreator

// FrameRateCalibrationFrames defines the number of frames the parser should wait until determining the frame rate of the demo.
// This value is only used if the frame rate cannot be determined from the demo header.
// This determines at what point the FrameRateCalibrated event will be raised.
// Negative values will raise the event at the end of the demo.
// Values below demoinfocs.MinFrameRateCalibrationFrames will set the value to MinFrameRateCalibrationFrames (defaults to 1000).
// See also https://github.com/markus-wa/demoinfocs-golang/issues/235
FrameRateCalibrationFrames int
}

const (
minFrameRateCalibrationFramesDefault = 1000
frameRateCalibrationFramesDefault = 1000
)

var MinFrameRateCalibrationFrames = minFrameRateCalibrationFramesDefault

// DefaultParserConfig is the default Parser configuration used by NewParser().
var DefaultParserConfig = ParserConfig{
MsgQueueBufferSize: -1,
MsgQueueBufferSize: -1,
FrameRateCalibrationFrames: frameRateCalibrationFramesDefault,
}

// NewParserWithConfig returns a new Parser with a custom configuration.
Expand All @@ -351,6 +397,13 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
p.gameEventHandler = newGameEventHandler(&p)
p.userMessageHandler = newUserMessageHandler(&p)
p.currentFrame = -1
p.tickDiffs = make(map[int]int)

if config.FrameRateCalibrationFrames >= 0 && config.FrameRateCalibrationFrames < MinFrameRateCalibrationFrames {
p.frameRateCalibrationFrames = MinFrameRateCalibrationFrames
} else {
p.frameRateCalibrationFrames = config.FrameRateCalibrationFrames
}

dispatcherCfg := dp.Config{
PanicHandler: func(v interface{}) {
Expand All @@ -370,7 +423,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
p.msgDispatcher.RegisterHandler(p.handleSetConVar)
p.msgDispatcher.RegisterHandler(p.handleFrameParsed)
p.msgDispatcher.RegisterHandler(p.handleServerInfo)
p.msgDispatcher.RegisterHandler(p.gameState.handleIngameTickNumber)
p.msgDispatcher.RegisterHandler(p.handleIngameTickNumber)

if config.MsgQueueBufferSize >= 0 {
p.initMsgQueue(config.MsgQueueBufferSize)
Expand Down
33 changes: 23 additions & 10 deletions pkg/demoinfocs/parser_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,32 @@ type Parser interface {
// Returns tick time based on CSVCMsg_ServerInfo if possible.
// Otherwise returns tick time based on demo header or -1 if the header info isn't available.
TickTime() time.Duration
// FrameRate returns the frame rate of the demo (frames / demo-ticks per second).
// FrameRateCalculated returns the frame rate of the demo (frames aka. demo-ticks per second).
// Not necessarily the tick-rate the server ran on during the game.
// See FrameRatePow2() for a possibly more accurate number.
//
// Returns frame rate based on GameState.IngameTick() if possible.
// Otherwise returns tick rate based on demo header or -1 if the header info isn't available.
// May also return 0 before parsing has started if DemoHeader.PlaybackTime or DemoHeader.PlaybackFrames are 0 (corrupt demo headers).
FrameRate() float64
// FrameTime returns the time a frame / demo-tick takes in seconds.
// Returns frame rate from DemoHeader if it's not corrupt.
// Otherwise returns frame rate that has automatically bee calibrated.
// May also return -1 before calibration has finished.
// See also events.FrameRateCalibrated.
FrameRateCalculated() float64
// FrameRatePow2 returns the frame rate of the demo (frames aka. demo-ticks per second) as a power of 2 (16, 32, 64 ...).
// Returns -1 before calibration has finished.
FrameRatePow2() float64
// FrameTimeCalculated returns the time a frame / demo-tick takes in seconds.
// See FrameTimePow2() for a possibly more accurate number.
//
// Returns frame rate based on GameState.IngameTick() if possible.
// Otherwise returns tick rate based on demo header or -1 if the header info isn't available.
// May also return 0 before parsing has started if DemoHeader.PlaybackTime or DemoHeader.PlaybackFrames are 0 (corrupt demo headers).
FrameTime() time.Duration
// Returns frame time from DemoHeader if it's not corrupt.
// Otherwise returns frame time that has automatically bee calibrated.
// May also return -1 before calibration has finished.
// See also events.FrameRateCalibrated.
FrameTimeCalculated() time.Duration
// FrameTimePow2 returns the time a frame / demo-tick takes in seconds.
//
// Returns -1 before calibration has finished.
// See also events.FrameRateCalibrated.
// See also FrameRatePow2().
FrameTimePow2() time.Duration
// Progress returns the parsing progress from 0 to 1.
// Where 0 means nothing has been parsed yet and 1 means the demo has been parsed to the end.
//
Expand Down
12 changes: 6 additions & 6 deletions pkg/demoinfocs/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestParser_FrameRate(t *testing.T) {
},
}

assert.Equal(t, float64(128), p.FrameRate())
assert.Equal(t, float64(128), p.FrameRateCalculated())
}

func TestParser_FrameRate_FallbackToHeader(t *testing.T) {
Expand All @@ -86,13 +86,13 @@ func TestParser_FrameRate_FallbackToHeader(t *testing.T) {
gameState: new(gameState),
}

assert.Equal(t, float64(128), p.FrameRate())
assert.Equal(t, float64(128), p.FrameRateCalculated())
}

func TestParser_FrameRate_Minus1(t *testing.T) {
p := &parser{gameState: new(gameState)}

assert.Equal(t, float64(-1), p.FrameRate())
assert.Equal(t, float64(-1), p.FrameRateCalculated())
}

func TestParser_FrameTime(t *testing.T) {
Expand All @@ -107,7 +107,7 @@ func TestParser_FrameTime(t *testing.T) {
},
}

assert.Equal(t, 200*time.Millisecond, p.FrameTime())
assert.Equal(t, 200*time.Millisecond, p.FrameTimeCalculated())
}

func TestParser_FrameTime_FallbackToHeader(t *testing.T) {
Expand All @@ -119,13 +119,13 @@ func TestParser_FrameTime_FallbackToHeader(t *testing.T) {
gameState: new(gameState),
}

assert.Equal(t, 200*time.Millisecond, p.FrameTime())
assert.Equal(t, 200*time.Millisecond, p.FrameTimeCalculated())
}

func TestParser_FrameTime_Minus1(t *testing.T) {
p := &parser{gameState: new(gameState)}

assert.Equal(t, time.Duration(-1), p.FrameTime())
assert.Equal(t, time.Duration(-1), p.FrameTimeCalculated())
}

func TestParser_Progress_NoHeader(t *testing.T) {
Expand Down

0 comments on commit eeea582

Please sign in to comment.