Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 62 additions & 16 deletions pkg/demoinfocs/datatables.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,30 @@ func (p *parser) bindBomb() {

if p.isSource2() {
ownerProp := bombEntity.PropertyValueMust("m_hOwnerEntity")
planter := p.gameState.Participants().FindByPawnHandle(ownerProp.Handle())
if planter == nil {
return

var planter *common.Player

if ownerProp.Any != nil {
planter = p.gameState.Participants().FindByPawnHandle(ownerProp.Handle())

if planter != nil {
planter.IsPlanting = false
}
}

isTicking := true
planter.IsPlanting = false

siteNumber := bombEntity.PropertyValueMust("m_nBombSite").Int()
siteNumberVal := bombEntity.PropertyValueMust("m_nBombSite")

site := events.BomsiteUnknown
if siteNumber == 0 {
site = events.BombsiteA
} else if siteNumber == 1 {
site = events.BombsiteB

if siteNumberVal.Any != nil {
siteNumber := siteNumberVal.Int()
if siteNumber == 0 {
site = events.BombsiteA
} else if siteNumber == 1 {
site = events.BombsiteB
}
}

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

// Set to true when the bomb has been planted and to false when it has been defused or has exploded.
bombEntity.Property("m_bBombTicking").OnUpdate(func(val st.PropertyValue) {
if val.Any == nil {
return
}

isTicking = val.BoolVal()
if isTicking {
return
Expand All @@ -214,31 +229,51 @@ func (p *parser) bindBomb() {

// Updated when a player starts/stops defusing the bomb
bombEntity.Property("m_hBombDefuser").OnUpdate(func(val st.PropertyValue) {
if val.Any == nil {
return
}

isValidPlayer := val.Handle() != constants.InvalidEntityHandleSource2
if isValidPlayer {
defuser := p.gameState.Participants().FindByPawnHandle(val.Handle())
p.gameState.currentDefuser = defuser
hasKit := false

// defuser may be nil for POV demos
if defuser != nil {
hasKit = defuser.HasDefuseKit()
}

if !p.disableMimicSource1GameEvents {
p.eventDispatcher.Dispatch(events.BombDefuseStart{
Player: defuser,
HasKit: defuser.HasDefuseKit(),
HasKit: hasKit,
})
}

return
}

isDefused := bombEntity.PropertyValueMust("m_bBombDefused").BoolVal()
if !isDefused && p.gameState.currentDefuser != nil {
p.eventDispatcher.Dispatch(events.BombDefuseAborted{
Player: p.gameState.currentDefuser,
})
isDefusedVal := bombEntity.PropertyValueMust("m_bBombDefused")

if isDefusedVal.Any != nil {
isDefused := isDefusedVal.BoolVal()
if !isDefused && p.gameState.currentDefuser != nil {
p.eventDispatcher.Dispatch(events.BombDefuseAborted{
Player: p.gameState.currentDefuser,
})
}
}

p.gameState.currentDefuser = nil
})

// Updated when the bomb has been planted and defused.
bombEntity.Property("m_bBombDefused").OnUpdate(func(val st.PropertyValue) {
if val.Any == nil {
return
}

isDefused := val.BoolVal()
if isDefused && !p.disableMimicSource1GameEvents {
defuser := p.gameState.Participants().FindByPawnHandle(bombEntity.PropertyValueMust("m_hBombDefuser").Handle())
Expand Down Expand Up @@ -1003,7 +1038,18 @@ func (p *parser) nadeProjectileDestroyed(proj *common.GrenadeProjectile) {

func (p *parser) bindWeaponS2(entity st.Entity) {
entityID := entity.ID()
itemIndex := entity.PropertyValueMust("m_iItemDefinitionIndex").S2UInt64()
itemIndexVal := entity.PropertyValueMust("m_iItemDefinitionIndex")

if itemIndexVal.Any == nil {
p.eventDispatcher.Dispatch(events.ParserWarn{
Type: events.WarnTypeMissingItemDefinitionIndex,
Message: "missing m_iItemDefinitionIndex property in weapon entity",
})

return
}

itemIndex := itemIndexVal.S2UInt64()
wepType := common.EquipmentIndexMapping[itemIndex]

if wepType == common.EqUnknown {
Expand Down
25 changes: 25 additions & 0 deletions pkg/demoinfocs/demoinfocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
retakeDemPath = csDemosPath + "/retake_unknwon_bombsite_index.dem"
unexpectedEndOfDemoPath = csDemosPath + "/unexpected_end_of_demo.dem"
s2DemPath = demSetPathS2 + "/s2.dem"
s2POVDemPath = demSetPathS2 + "/pov.dem"
)

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

func TestS2POV(t *testing.T) {
t.Parallel()

if testing.Short() {
t.Skip("skipping test due to -short flag")
}

f, err := os.Open(s2POVDemPath)
assertions := assert.New(t)
assertions.NoError(err, "error opening demo %q", s2POVDemPath)

defer mustClose(t, f)

p := demoinfocs.NewParser(f)

t.Log("Parsing to end")
err = p.ParseToEnd()
assertions.NoError(err, "error occurred in ParseToEnd()")
}

func TestEncryptedNetMessages(t *testing.T) {
t.Parallel()

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

case events.WarnTypeMissingItemDefinitionIndex:
t.Log("expected known issue with missing item definition index occurred:", warn.Message)
return

case events.WarnTypeGameEventBeforeDescriptors:
if strings.Contains(name, "POV-orbit-skytten-vs-cloud9-gfinity15sm1-nuke.dem") {
t.Log("expected known issue for POV demos occurred:", warn.Message)
Expand Down
1 change: 1 addition & 0 deletions pkg/demoinfocs/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ const (
WarnTypeCantReadEncryptedNetMessage

WarnTypeUnknownEquipmentIndex
WarnTypeMissingItemDefinitionIndex
)

// ParserWarn signals that a non-fatal problem occurred during parsing.
Expand Down
8 changes: 5 additions & 3 deletions pkg/demoinfocs/game_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ func newGameEventHandler(parser *parser, ignoreBombsiteIndexNotFound bool) gameE
"item_remove": geh.itemRemove, // Dropped?
"jointeam_failed": nil, // Dunno, only in locally recorded (POV) demos
"other_death": geh.otherDeath, // Other deaths, like chickens.
"player_activate": nil, // CS2 POV demos
"player_blind": delay(geh.playerBlind), // Player got blinded by a flash. Delayed because Player.FlashDuration hasn't been updated yet
"player_changename": nil, // Name change
"player_connect": geh.playerConnect, // Bot connected or player reconnected, players normally come in via string tables & data tables
Expand Down Expand Up @@ -491,12 +492,13 @@ func (geh gameEventHandler) playerHurt(data map[string]*msg.CSVCMsg_GameEventKey
armorDamageTaken = 100
}

if player != nil {
if health == 0 {
if player != nil && (!geh.parser.isSource2() || (player.PlayerPawnEntity() != nil)) {
// m_iHealth & m_ArmorValue check for CS2 POV demos
if health == 0 && (!geh.parser.isSource2() || player.PlayerPawnEntity().Property("m_iHealth") != nil) {
healthDamageTaken = player.Health()
}

if armor == 0 {
if armor == 0 && (!geh.parser.isSource2() || player.PlayerPawnEntity().Property("m_ArmorValue") != nil) {
armorDamageTaken = player.Armor()
}
}
Expand Down
44 changes: 24 additions & 20 deletions pkg/demoinfocs/sendtables2/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,23 @@ func coordFromCell(cell uint64, offset float32) float64 {
}

func (e *Entity) Position() r3.Vector {
cellXProp := e.Property(propCellX)
cellYProp := e.Property(propCellY)
cellZProp := e.Property(propCellZ)
offsetXProp := e.Property(propVecX)
offsetYProp := e.Property(propVecY)
offsetZProp := e.Property(propVecZ)

cellX := cellXProp.Value().S2UInt64()
cellY := cellYProp.Value().S2UInt64()
cellZ := cellZProp.Value().S2UInt64()
offsetX := offsetXProp.Value().Float()
offsetY := offsetYProp.Value().Float()
offsetZ := offsetZProp.Value().Float()
cellXVal := e.Property(propCellX).Value()
cellYVal := e.Property(propCellY).Value()
cellZVal := e.Property(propCellZ).Value()
offsetXVal := e.Property(propVecX).Value()
offsetYVal := e.Property(propVecY).Value()
offsetZVal := e.Property(propVecZ).Value()

if cellXVal.Any == nil || cellYVal.Any == nil || cellZVal.Any == nil || offsetXVal.Any == nil || offsetYVal.Any == nil || offsetZVal.Any == nil {
return r3.Vector{} // CS2 POV demos
}

cellX := cellXVal.S2UInt64()
cellY := cellYVal.S2UInt64()
cellZ := cellZVal.S2UInt64()
offsetX := offsetXVal.Float()
offsetY := offsetYVal.Float()
offsetZ := offsetZVal.Float()

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

baseline := p.classBaselines[classID]
if baseline == nil {
_panicf("unable to find new baseline %d", classID)
}

e = newEntity(index, serial, class)
p.entities[index] = e

e.readFields(newReader(baseline), &paths)
paths = paths[:0]
baseline := p.classBaselines[classID]

if baseline != nil {
// POV demos are missing some baselines?
e.readFields(newReader(baseline), &paths)
paths = paths[:0]
}

e.readFields(r, &paths)
paths = paths[:0]
Expand Down
74 changes: 38 additions & 36 deletions pkg/demoinfocs/stringtables.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ func parseStringTable(
flags int32,
variantBitCount bool) (items []*stringTableItem) {
items = make([]*stringTableItem, 0)
// Some tables have no data
if len(buf) == 0 {
return items
}

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

// Some tables have no data
if len(buf) == 0 {
return items
}

// Loop through entries in the data structure
//
// Each entry is a tuple consisting of {index, key, value}
// Each entry is a tuple consisting of {index, missing m_iItemDefinitionIndex property key, value}
//
// Index can either be incremented from the previous position or
// overwritten with a given entry.
Expand Down Expand Up @@ -373,46 +372,49 @@ func parseStringTable(
if len(keys) > stringtableKeyHistorySize {
keys = keys[1:]
}
}

// Some entries have a value.
hasValue := r.ReadBit()
if hasValue {
bitSize := uint(0)
isCompressed := false
if userDataFixed {
bitSize = uint(userDataSize)
} else {
if (flags & 0x1) != 0 {
isCompressed = r.ReadBit()
}
// Some entries have a value.
hasValue := r.ReadBit()
if hasValue {
bitSize := uint(0)
isCompressed := false

if variantBitCount {
bitSize = r.ReadUBitInt() * 8
if userDataFixed {
bitSize = uint(userDataSize)
} else {
bitSize = r.ReadInt(17) * 8
if (flags & 0x1) != 0 {
isCompressed = r.ReadBit()
}

if variantBitCount {
bitSize = r.ReadUBitInt() * 8
} else {
bitSize = r.ReadInt(17) * 8
}
}
}
value = r.ReadBits(int(bitSize))

if isCompressed {
tmp, err := snappy.Decode(nil, value)
if err != nil {
panic(fmt.Sprintf("unable to decode snappy compressed stringtable item (%s, %d, %s): %s", name, index, key, err))
value = r.ReadBits(int(bitSize))

if isCompressed {
tmp, err := snappy.Decode(nil, value)
if err != nil {
panic(fmt.Sprintf("unable to decode snappy compressed stringtable item (%s, %d, %s): %s", name, index, key, err))
}

value = tmp
}
value = tmp
}
}

items = append(items, &stringTableItem{index, key, value})
items = append(items, &stringTableItem{index, key, value})
}
}

return items
}

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

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

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

br := bit.NewSmallBitReader(bytes.NewReader(tab.StringData))

if tab.isS2 {
p.processStringTableS2(tab, br)
p.processStringTableS2(tab)
} else {
br := bit.NewSmallBitReader(bytes.NewReader(tab.StringData))

if br.ReadBit() {
panic("unknown stringtable format")
}

p.processStringTableS1(tab, br)

p.poolBitReader(br)
}

if tab.GetName() == stNameModelPreCache {
p.processModelPreCacheUpdate()
}

p.poolBitReader(br)
}

func parsePlayerInfo(reader io.Reader) common.PlayerInfo {
Expand Down
Loading