Skip to content

Commit 2cd8e77

Browse files
authored
Merge pull request #521 from markus-wa/cs2-pov-support
CS2 POV demo support
2 parents 28f108a + 09c8137 commit 2cd8e77

File tree

7 files changed

+156
-76
lines changed

7 files changed

+156
-76
lines changed

pkg/demoinfocs/datatables.go

+62-16
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,30 @@ func (p *parser) bindBomb() {
164164

165165
if p.isSource2() {
166166
ownerProp := bombEntity.PropertyValueMust("m_hOwnerEntity")
167-
planter := p.gameState.Participants().FindByPawnHandle(ownerProp.Handle())
168-
if planter == nil {
169-
return
167+
168+
var planter *common.Player
169+
170+
if ownerProp.Any != nil {
171+
planter = p.gameState.Participants().FindByPawnHandle(ownerProp.Handle())
172+
173+
if planter != nil {
174+
planter.IsPlanting = false
175+
}
170176
}
177+
171178
isTicking := true
172-
planter.IsPlanting = false
173179

174-
siteNumber := bombEntity.PropertyValueMust("m_nBombSite").Int()
180+
siteNumberVal := bombEntity.PropertyValueMust("m_nBombSite")
181+
175182
site := events.BomsiteUnknown
176-
if siteNumber == 0 {
177-
site = events.BombsiteA
178-
} else if siteNumber == 1 {
179-
site = events.BombsiteB
183+
184+
if siteNumberVal.Any != nil {
185+
siteNumber := siteNumberVal.Int()
186+
if siteNumber == 0 {
187+
site = events.BombsiteA
188+
} else if siteNumber == 1 {
189+
site = events.BombsiteB
190+
}
180191
}
181192

182193
if !p.disableMimicSource1GameEvents {
@@ -190,6 +201,10 @@ func (p *parser) bindBomb() {
190201

191202
// Set to true when the bomb has been planted and to false when it has been defused or has exploded.
192203
bombEntity.Property("m_bBombTicking").OnUpdate(func(val st.PropertyValue) {
204+
if val.Any == nil {
205+
return
206+
}
207+
193208
isTicking = val.BoolVal()
194209
if isTicking {
195210
return
@@ -214,31 +229,51 @@ func (p *parser) bindBomb() {
214229

215230
// Updated when a player starts/stops defusing the bomb
216231
bombEntity.Property("m_hBombDefuser").OnUpdate(func(val st.PropertyValue) {
232+
if val.Any == nil {
233+
return
234+
}
235+
217236
isValidPlayer := val.Handle() != constants.InvalidEntityHandleSource2
218237
if isValidPlayer {
219238
defuser := p.gameState.Participants().FindByPawnHandle(val.Handle())
220239
p.gameState.currentDefuser = defuser
240+
hasKit := false
241+
242+
// defuser may be nil for POV demos
243+
if defuser != nil {
244+
hasKit = defuser.HasDefuseKit()
245+
}
246+
221247
if !p.disableMimicSource1GameEvents {
222248
p.eventDispatcher.Dispatch(events.BombDefuseStart{
223249
Player: defuser,
224-
HasKit: defuser.HasDefuseKit(),
250+
HasKit: hasKit,
225251
})
226252
}
253+
227254
return
228255
}
229256

230-
isDefused := bombEntity.PropertyValueMust("m_bBombDefused").BoolVal()
231-
if !isDefused && p.gameState.currentDefuser != nil {
232-
p.eventDispatcher.Dispatch(events.BombDefuseAborted{
233-
Player: p.gameState.currentDefuser,
234-
})
257+
isDefusedVal := bombEntity.PropertyValueMust("m_bBombDefused")
258+
259+
if isDefusedVal.Any != nil {
260+
isDefused := isDefusedVal.BoolVal()
261+
if !isDefused && p.gameState.currentDefuser != nil {
262+
p.eventDispatcher.Dispatch(events.BombDefuseAborted{
263+
Player: p.gameState.currentDefuser,
264+
})
265+
}
235266
}
236267

237268
p.gameState.currentDefuser = nil
238269
})
239270

240271
// Updated when the bomb has been planted and defused.
241272
bombEntity.Property("m_bBombDefused").OnUpdate(func(val st.PropertyValue) {
273+
if val.Any == nil {
274+
return
275+
}
276+
242277
isDefused := val.BoolVal()
243278
if isDefused && !p.disableMimicSource1GameEvents {
244279
defuser := p.gameState.Participants().FindByPawnHandle(bombEntity.PropertyValueMust("m_hBombDefuser").Handle())
@@ -1003,7 +1038,18 @@ func (p *parser) nadeProjectileDestroyed(proj *common.GrenadeProjectile) {
10031038

10041039
func (p *parser) bindWeaponS2(entity st.Entity) {
10051040
entityID := entity.ID()
1006-
itemIndex := entity.PropertyValueMust("m_iItemDefinitionIndex").S2UInt64()
1041+
itemIndexVal := entity.PropertyValueMust("m_iItemDefinitionIndex")
1042+
1043+
if itemIndexVal.Any == nil {
1044+
p.eventDispatcher.Dispatch(events.ParserWarn{
1045+
Type: events.WarnTypeMissingItemDefinitionIndex,
1046+
Message: "missing m_iItemDefinitionIndex property in weapon entity",
1047+
})
1048+
1049+
return
1050+
}
1051+
1052+
itemIndex := itemIndexVal.S2UInt64()
10071053
wepType := common.EquipmentIndexMapping[itemIndex]
10081054

10091055
if wepType == common.EqUnknown {

pkg/demoinfocs/demoinfocs_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
retakeDemPath = csDemosPath + "/retake_unknwon_bombsite_index.dem"
3939
unexpectedEndOfDemoPath = csDemosPath + "/unexpected_end_of_demo.dem"
4040
s2DemPath = demSetPathS2 + "/s2.dem"
41+
s2POVDemPath = demSetPathS2 + "/pov.dem"
4142
)
4243

4344
var concurrentDemos = flag.Int("concurrentdemos", 2, "The `number` of current demos")
@@ -231,6 +232,26 @@ func TestS2(t *testing.T) {
231232
assertions.NoError(err, "error occurred in ParseToEnd()")
232233
}
233234

235+
func TestS2POV(t *testing.T) {
236+
t.Parallel()
237+
238+
if testing.Short() {
239+
t.Skip("skipping test due to -short flag")
240+
}
241+
242+
f, err := os.Open(s2POVDemPath)
243+
assertions := assert.New(t)
244+
assertions.NoError(err, "error opening demo %q", s2POVDemPath)
245+
246+
defer mustClose(t, f)
247+
248+
p := demoinfocs.NewParser(f)
249+
250+
t.Log("Parsing to end")
251+
err = p.ParseToEnd()
252+
assertions.NoError(err, "error occurred in ParseToEnd()")
253+
}
254+
234255
func TestEncryptedNetMessages(t *testing.T) {
235256
t.Parallel()
236257

@@ -524,6 +545,10 @@ func testDemoSet(t *testing.T, path string) {
524545
t.Log("expected known issue with team swaps occurred:", warn.Message)
525546
return
526547

548+
case events.WarnTypeMissingItemDefinitionIndex:
549+
t.Log("expected known issue with missing item definition index occurred:", warn.Message)
550+
return
551+
527552
case events.WarnTypeGameEventBeforeDescriptors:
528553
if strings.Contains(name, "POV-orbit-skytten-vs-cloud9-gfinity15sm1-nuke.dem") {
529554
t.Log("expected known issue for POV demos occurred:", warn.Message)

pkg/demoinfocs/events/events.go

+1
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ const (
581581
WarnTypeCantReadEncryptedNetMessage
582582

583583
WarnTypeUnknownEquipmentIndex
584+
WarnTypeMissingItemDefinitionIndex
584585
)
585586

586587
// ParserWarn signals that a non-fatal problem occurred during parsing.

pkg/demoinfocs/game_events.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ func newGameEventHandler(parser *parser, ignoreBombsiteIndexNotFound bool) gameE
240240
"item_remove": geh.itemRemove, // Dropped?
241241
"jointeam_failed": nil, // Dunno, only in locally recorded (POV) demos
242242
"other_death": geh.otherDeath, // Other deaths, like chickens.
243+
"player_activate": nil, // CS2 POV demos
243244
"player_blind": delay(geh.playerBlind), // Player got blinded by a flash. Delayed because Player.FlashDuration hasn't been updated yet
244245
"player_changename": nil, // Name change
245246
"player_connect": geh.playerConnect, // Bot connected or player reconnected, players normally come in via string tables & data tables
@@ -491,12 +492,13 @@ func (geh gameEventHandler) playerHurt(data map[string]*msg.CSVCMsg_GameEventKey
491492
armorDamageTaken = 100
492493
}
493494

494-
if player != nil {
495-
if health == 0 {
495+
if player != nil && (!geh.parser.isSource2() || (player.PlayerPawnEntity() != nil)) {
496+
// m_iHealth & m_ArmorValue check for CS2 POV demos
497+
if health == 0 && (!geh.parser.isSource2() || player.PlayerPawnEntity().Property("m_iHealth") != nil) {
496498
healthDamageTaken = player.Health()
497499
}
498500

499-
if armor == 0 {
501+
if armor == 0 && (!geh.parser.isSource2() || player.PlayerPawnEntity().Property("m_ArmorValue") != nil) {
500502
armorDamageTaken = player.Armor()
501503
}
502504
}

pkg/demoinfocs/sendtables2/entity.go

+24-20
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,23 @@ func coordFromCell(cell uint64, offset float32) float64 {
204204
}
205205

206206
func (e *Entity) Position() r3.Vector {
207-
cellXProp := e.Property(propCellX)
208-
cellYProp := e.Property(propCellY)
209-
cellZProp := e.Property(propCellZ)
210-
offsetXProp := e.Property(propVecX)
211-
offsetYProp := e.Property(propVecY)
212-
offsetZProp := e.Property(propVecZ)
213-
214-
cellX := cellXProp.Value().S2UInt64()
215-
cellY := cellYProp.Value().S2UInt64()
216-
cellZ := cellZProp.Value().S2UInt64()
217-
offsetX := offsetXProp.Value().Float()
218-
offsetY := offsetYProp.Value().Float()
219-
offsetZ := offsetZProp.Value().Float()
207+
cellXVal := e.Property(propCellX).Value()
208+
cellYVal := e.Property(propCellY).Value()
209+
cellZVal := e.Property(propCellZ).Value()
210+
offsetXVal := e.Property(propVecX).Value()
211+
offsetYVal := e.Property(propVecY).Value()
212+
offsetZVal := e.Property(propVecZ).Value()
213+
214+
if cellXVal.Any == nil || cellYVal.Any == nil || cellZVal.Any == nil || offsetXVal.Any == nil || offsetYVal.Any == nil || offsetZVal.Any == nil {
215+
return r3.Vector{} // CS2 POV demos
216+
}
217+
218+
cellX := cellXVal.S2UInt64()
219+
cellY := cellYVal.S2UInt64()
220+
cellZ := cellZVal.S2UInt64()
221+
offsetX := offsetXVal.Float()
222+
offsetY := offsetYVal.Float()
223+
offsetZ := offsetZVal.Float()
220224

221225
return r3.Vector{
222226
X: coordFromCell(cellX, offsetX),
@@ -519,16 +523,16 @@ func (p *Parser) OnPacketEntities(m *msgs2.CSVCMsg_PacketEntities) error {
519523
_panicf("unable to find new class %d", classID)
520524
}
521525

522-
baseline := p.classBaselines[classID]
523-
if baseline == nil {
524-
_panicf("unable to find new baseline %d", classID)
525-
}
526-
527526
e = newEntity(index, serial, class)
528527
p.entities[index] = e
529528

530-
e.readFields(newReader(baseline), &paths)
531-
paths = paths[:0]
529+
baseline := p.classBaselines[classID]
530+
531+
if baseline != nil {
532+
// POV demos are missing some baselines?
533+
e.readFields(newReader(baseline), &paths)
534+
paths = paths[:0]
535+
}
532536

533537
e.readFields(r, &paths)
534538
paths = paths[:0]

pkg/demoinfocs/stringtables.go

+38-36
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ func parseStringTable(
303303
flags int32,
304304
variantBitCount bool) (items []*stringTableItem) {
305305
items = make([]*stringTableItem, 0)
306+
// Some tables have no data
307+
if len(buf) == 0 {
308+
return items
309+
}
306310

307311
// Create a reader for the buffer
308312
r := bit.NewSmallBitReader(bytes.NewReader(buf))
@@ -312,14 +316,9 @@ func parseStringTable(
312316
index := int32(-1)
313317
keys := make([]string, 0, stringtableKeyHistorySize+1)
314318

315-
// Some tables have no data
316-
if len(buf) == 0 {
317-
return items
318-
}
319-
320319
// Loop through entries in the data structure
321320
//
322-
// Each entry is a tuple consisting of {index, key, value}
321+
// Each entry is a tuple consisting of {index, missing m_iItemDefinitionIndex property key, value}
323322
//
324323
// Index can either be incremented from the previous position or
325324
// overwritten with a given entry.
@@ -373,46 +372,49 @@ func parseStringTable(
373372
if len(keys) > stringtableKeyHistorySize {
374373
keys = keys[1:]
375374
}
376-
}
377375

378-
// Some entries have a value.
379-
hasValue := r.ReadBit()
380-
if hasValue {
381-
bitSize := uint(0)
382-
isCompressed := false
383-
if userDataFixed {
384-
bitSize = uint(userDataSize)
385-
} else {
386-
if (flags & 0x1) != 0 {
387-
isCompressed = r.ReadBit()
388-
}
376+
// Some entries have a value.
377+
hasValue := r.ReadBit()
378+
if hasValue {
379+
bitSize := uint(0)
380+
isCompressed := false
389381

390-
if variantBitCount {
391-
bitSize = r.ReadUBitInt() * 8
382+
if userDataFixed {
383+
bitSize = uint(userDataSize)
392384
} else {
393-
bitSize = r.ReadInt(17) * 8
385+
if (flags & 0x1) != 0 {
386+
isCompressed = r.ReadBit()
387+
}
388+
389+
if variantBitCount {
390+
bitSize = r.ReadUBitInt() * 8
391+
} else {
392+
bitSize = r.ReadInt(17) * 8
393+
}
394394
}
395-
}
396-
value = r.ReadBits(int(bitSize))
397395

398-
if isCompressed {
399-
tmp, err := snappy.Decode(nil, value)
400-
if err != nil {
401-
panic(fmt.Sprintf("unable to decode snappy compressed stringtable item (%s, %d, %s): %s", name, index, key, err))
396+
value = r.ReadBits(int(bitSize))
397+
398+
if isCompressed {
399+
tmp, err := snappy.Decode(nil, value)
400+
if err != nil {
401+
panic(fmt.Sprintf("unable to decode snappy compressed stringtable item (%s, %d, %s): %s", name, index, key, err))
402+
}
403+
404+
value = tmp
402405
}
403-
value = tmp
404406
}
405-
}
406407

407-
items = append(items, &stringTableItem{index, key, value})
408+
items = append(items, &stringTableItem{index, key, value})
409+
}
408410
}
409411

410412
return items
411413
}
412414

413415
var instanceBaselineKeyRegex = regexp.MustCompile(`^\d+:\d+$`)
414416

415-
func (p *parser) processStringTableS2(tab createStringTable, br *bit.BitReader) {
417+
func (p *parser) processStringTableS2(tab createStringTable) {
416418
items := parseStringTable(tab.StringData, tab.GetNumEntries(), tab.GetName(), tab.GetUserDataFixedSize(), tab.GetUserDataSize(), tab.GetFlags(), tab.GetUsingVarintBitcounts())
417419

418420
for _, item := range items {
@@ -452,23 +454,23 @@ func (p *parser) processStringTable(tab createStringTable) {
452454
tab.StringData = b
453455
}
454456

455-
br := bit.NewSmallBitReader(bytes.NewReader(tab.StringData))
456-
457457
if tab.isS2 {
458-
p.processStringTableS2(tab, br)
458+
p.processStringTableS2(tab)
459459
} else {
460+
br := bit.NewSmallBitReader(bytes.NewReader(tab.StringData))
461+
460462
if br.ReadBit() {
461463
panic("unknown stringtable format")
462464
}
463465

464466
p.processStringTableS1(tab, br)
467+
468+
p.poolBitReader(br)
465469
}
466470

467471
if tab.GetName() == stNameModelPreCache {
468472
p.processModelPreCacheUpdate()
469473
}
470-
471-
p.poolBitReader(br)
472474
}
473475

474476
func parsePlayerInfo(reader io.Reader) common.PlayerInfo {

0 commit comments

Comments
 (0)