Skip to content

Commit c60e2e7

Browse files
committed
feat: improve POV demos support
- Avoid recreating known entities when they re-enter in the PVS based on the entity serial number - Detect if the demo is a client demo and use correct props to detect position (local vs non local) - Add a parser.IsPOV function
1 parent 0c6c50c commit c60e2e7

File tree

8 files changed

+95
-45
lines changed

8 files changed

+95
-45
lines changed

pkg/demoinfocs/net_messages.go

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package demoinfocs
33
import (
44
"bytes"
55
"encoding/binary"
6+
"fmt"
67

78
"github.com/markus-wa/ice-cipher-go/pkg/ice"
89
"google.golang.org/protobuf/proto"
@@ -19,34 +20,30 @@ func (p *parser) handlePacketEntities(pe *msg.CSVCMsg_PacketEntities) {
1920

2021
r := bit.NewSmallBitReader(bytes.NewReader(pe.EntityData))
2122

22-
currentEntity := -1
23+
entityIndex := -1
24+
2325
for i := 0; i < int(pe.GetUpdatedEntries()); i++ {
24-
currentEntity += 1 + int(r.ReadUBitInt())
25-
26-
cmd := r.ReadBitsToByte(2)
27-
if cmd&1 == 0 {
28-
if cmd&2 != 0 {
29-
// Enter PVS
30-
if existing := p.gameState.entities[currentEntity]; existing != nil {
31-
// Sometimes entities don't get destroyed when they should be
32-
// For instance when a player is replaced by a BOT
33-
existing.Destroy()
34-
}
26+
entityIndex += 1 + int(r.ReadUBitInt())
3527

36-
p.gameState.entities[currentEntity] = p.stParser.ReadEnterPVS(r, currentEntity)
37-
} else {
38-
// Delta Update
39-
if entity := p.gameState.entities[currentEntity]; entity != nil {
40-
entity.ApplyUpdate(r)
28+
if r.ReadBit() {
29+
// FHDR_LEAVEPVS => LeavePVS
30+
31+
if r.ReadBit() {
32+
// FHDR_LEAVEPVS | FHDR_DELETE => LeavePVS with force delete. Should never happen on full update
33+
if existingEntity := p.gameState.entities[entityIndex]; existingEntity != nil {
34+
existingEntity.Destroy()
35+
delete(p.gameState.entities, entityIndex)
4136
}
4237
}
38+
} else if r.ReadBit() {
39+
// FHDR_ENTERPVS => EnterPVS
40+
p.gameState.entities[entityIndex] = p.stParser.ReadEnterPVS(r, entityIndex, p.gameState.entities, p.recordingPlayerSlot)
4341
} else {
44-
if cmd&2 != 0 {
45-
// Leave PVS
46-
if entity := p.gameState.entities[currentEntity]; entity != nil {
47-
entity.Destroy()
48-
delete(p.gameState.entities, currentEntity)
49-
}
42+
// Delta update
43+
if p.gameState.entities[entityIndex] != nil {
44+
p.gameState.entities[entityIndex].ApplyUpdate(r)
45+
} else {
46+
panic(fmt.Sprintf("Entity with index %d doesn't exist but got an update", entityIndex))
5047
}
5148
}
5249
}

pkg/demoinfocs/parser.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ type parser struct {
6464
err error // Contains a error that occurred during parsing if any
6565
errLock sync.Mutex // Used to sync up error mutations between parsing & handling go-routines
6666
decryptionKey []byte // Stored in `match730_*.dem.info` see MatchInfoDecryptionKey().
67+
/**
68+
* Set to the client slot of the recording player.
69+
* Always -1 for GOTV demos.
70+
*/
71+
recordingPlayerSlot int
6772

6873
// Additional fields, mainly caching & tracking things
6974

@@ -190,6 +195,11 @@ func (p *parser) Progress() float32 {
190195
return float32(p.currentFrame) / float32(p.header.PlaybackFrames)
191196
}
192197

198+
// IsPOV indicates if the demo being parsed is a POV demo.
199+
func (p *parser) IsPOV() bool {
200+
return p.recordingPlayerSlot != -1
201+
}
202+
193203
/*
194204
RegisterEventHandler registers a handler for game events.
195205
@@ -350,6 +360,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
350360
p.bombsiteA.index = -1
351361
p.bombsiteB.index = -1
352362
p.decryptionKey = config.NetMessageDecryptionKey
363+
p.recordingPlayerSlot = -1
353364

354365
dispatcherCfg := dp.Config{
355366
PanicHandler: func(v any) {

pkg/demoinfocs/sendtables/entity.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import (
1515
type entity struct {
1616
serverClass *ServerClass
1717
id int
18+
serialNum int
1819
props []property
1920

20-
onCreateFinished []func()
21-
onDestroy []func()
22-
position func() r3.Vector
21+
onCreateFinished []func()
22+
onDestroy []func()
23+
position func() r3.Vector
24+
positionPropNameXY string
25+
positionPropNameZ string
2326
}
2427

2528
// ServerClass returns the entity's server-class.
@@ -32,6 +35,11 @@ func (e *entity) ID() int {
3235
return e.id
3336
}
3437

38+
// SerialNum returns the entity's serial number.
39+
func (e *entity) SerialNum() int {
40+
return e.serialNum
41+
}
42+
3543
// Properties returns all properties of the entity.
3644
func (e *entity) Properties() (out []Property) {
3745
for i := range e.props {
@@ -189,25 +197,36 @@ func (e *entity) applyBaseline(baseline map[int]PropertyValue) {
189197
const (
190198
maxCoordInt = 16384
191199

192-
propCellBits = "m_cellbits"
193-
propCellX = "m_cellX"
194-
propCellY = "m_cellY"
195-
propCellZ = "m_cellZ"
196-
propVecOrigin = "m_vecOrigin"
197-
propVecOriginPlayerXY = "cslocaldata.m_vecOrigin"
198-
propVecOriginPlayerZ = "cslocaldata.m_vecOrigin[2]"
200+
propCellBits = "m_cellbits"
201+
propCellX = "m_cellX"
202+
propCellY = "m_cellY"
203+
propCellZ = "m_cellZ"
204+
propVecOrigin = "m_vecOrigin"
205+
propVecOriginPlayerXY = "cslocaldata.m_vecOrigin"
206+
propVecOriginPlayerZ = "cslocaldata.m_vecOrigin[2]"
207+
nonLocalPropVecOriginPlayerXY = "csnonlocaldata.m_vecOrigin"
208+
nonLocalPropVecOriginPlayerZ = "csnonlocaldata.m_vecOrigin[2]"
199209

200210
serverClassPlayer = "CCSPlayer"
201211
)
202212

203213
// Sets up the entity.Position() function
204214
// Necessary because Property() is fairly slow
205215
// This way we only need to find the necessary properties once
206-
func (e *entity) initialize() {
216+
func (e *entity) initialize(recordingPlayerSlot int) {
207217
// Player positions are calculated differently
208218
if e.isPlayer() {
209-
xyProp := e.Property(propVecOriginPlayerXY)
210-
zProp := e.Property(propVecOriginPlayerZ)
219+
isGOTV := recordingPlayerSlot == -1
220+
isRecording := recordingPlayerSlot == e.id-1
221+
if isGOTV || isRecording {
222+
e.positionPropNameXY = propVecOriginPlayerXY
223+
e.positionPropNameZ = propVecOriginPlayerZ
224+
} else {
225+
e.positionPropNameXY = nonLocalPropVecOriginPlayerXY
226+
e.positionPropNameZ = nonLocalPropVecOriginPlayerZ
227+
}
228+
xyProp := e.Property(e.positionPropNameXY)
229+
zProp := e.Property(e.positionPropNameZ)
211230

212231
e.position = func() r3.Vector {
213232
xy := xyProp.Value().VectorVal
@@ -266,8 +285,8 @@ func (e *entity) OnPositionUpdate(h func(pos r3.Vector)) {
266285
}
267286

268287
if e.isPlayer() {
269-
e.Property(propVecOriginPlayerXY).OnUpdate(firePosUpdate)
270-
e.Property(propVecOriginPlayerZ).OnUpdate(firePosUpdate)
288+
e.Property(e.positionPropNameXY).OnUpdate(firePosUpdate)
289+
e.Property(e.positionPropNameZ).OnUpdate(firePosUpdate)
271290
} else {
272291
e.Property(propCellX).OnUpdate(firePosUpdate)
273292
e.Property(propCellY).OnUpdate(firePosUpdate)
@@ -366,6 +385,7 @@ func (pe *property) firePropertyUpdate() {
366385
Bind binds a property's value to a pointer.
367386
368387
Example:
388+
369389
var i int
370390
property.Bind(&i, ValTypeInt)
371391

pkg/demoinfocs/sendtables/entity_interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type Entity interface {
1414
ServerClass() *ServerClass
1515
// ID returns the entity's ID.
1616
ID() int
17+
// SerialNum returns the entity's serial number.
18+
SerialNum() int
1719
// Properties returns all properties of the entity.
1820
Properties() (out []Property)
1921
// Property finds a property on the entity by name.

pkg/demoinfocs/sendtables/fake/entity.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ func (e *Entity) ID() int {
3939
return e.Called().Int(0)
4040
}
4141

42+
// SerialNum is a mock-implementation of Entity.SerialNum().
43+
func (e *Entity) SerialNum() int {
44+
return e.Called().Int(0)
45+
}
46+
4247
// Properties is a mock-implementation of Entity.Properties().
4348
func (e *Entity) Properties() []st.Property {
4449
return e.Called().Get(0).([]st.Property)

pkg/demoinfocs/sendtables/sendtables.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,16 @@ func (sc *ServerClass) PropertyEntryDefinitions() []PropertyEntry {
120120
return res
121121
}
122122

123-
func (sc *ServerClass) newEntity(entityDataReader *bit.BitReader, entityID int) *entity {
123+
func (sc *ServerClass) newEntity(entityDataReader *bit.BitReader, entityID int, classID int, serialNum int, recordingPlayerSlot int) *entity {
124124
props := make([]property, len(sc.flattenedProps))
125125

126126
for i := range sc.flattenedProps {
127127
props[i] = property{entry: &sc.flattenedProps[i]}
128128
}
129129

130-
entity := &entity{serverClass: sc, id: entityID, props: props}
130+
entity := &entity{serverClass: sc, id: entityID, serialNum: serialNum, props: props}
131131

132-
entity.initialize()
132+
entity.initialize(recordingPlayerSlot)
133133

134134
if sc.preprocessedBaseline != nil {
135135
entity.applyBaseline(sc.preprocessedBaseline)

pkg/demoinfocs/sendtables/st_parser.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,23 @@ func (p *SendTableParser) SetInstanceBaseline(scID int, data []byte) {
306306
// ReadEnterPVS reads an entity entering the PVS (potentially visible system).
307307
//
308308
// Intended for internal use only.
309-
func (p *SendTableParser) ReadEnterPVS(r *bit.BitReader, entityID int) Entity {
310-
scID := int(r.ReadInt(p.classBits()))
309+
func (p *SendTableParser) ReadEnterPVS(r *bit.BitReader, entityID int, existingEntities map[int]Entity, recordingPlayerSlot int) Entity {
310+
classID := int(r.ReadInt(p.classBits()))
311+
serialNum := int(r.ReadInt(constants.EntityHandleSerialNumberBits))
312+
existingEntity := existingEntities[entityID]
313+
314+
if existingEntity != nil && existingEntity.SerialNum() == serialNum {
315+
existingEntity.ApplyUpdate(r)
316+
return existingEntity
317+
}
311318

312-
r.Skip(constants.EntityHandleSerialNumberBits) // Serial Number
319+
// Serial numbers are different, delete the entity
320+
if existingEntity != nil {
321+
existingEntity.Destroy()
322+
delete(existingEntities, entityID)
323+
}
313324

314-
return p.serverClasses[scID].newEntity(r, entityID)
325+
return p.serverClasses[classID].newEntity(r, entityID, classID, serialNum, recordingPlayerSlot)
315326
}
316327

317328
// classBits seems to calculate how many bits must be read for the server-class ID.

pkg/demoinfocs/stringtables.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ func (p *parser) processStringTable(tab *msg.CSVCMsg_CreateStringTable) {
239239
case stNameUserInfo:
240240
player := parsePlayerInfo(bytes.NewReader(userdata))
241241

242+
if p.header.ClientName == player.Name {
243+
p.recordingPlayerSlot = entryIndex
244+
}
245+
242246
p.setRawPlayer(entryIndex, player)
243247

244248
case stNameInstanceBaseline:

0 commit comments

Comments
 (0)