Skip to content

Commit 7a893dd

Browse files
committed
test: cover QAngle noscale decode, entity_killed, fatal-hit capping, sorted participants
1 parent cc2ddf5 commit 7a893dd

3 files changed

Lines changed: 220 additions & 0 deletions

File tree

‎pkg/demoinfocs/game_events_test.go‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/rand"
55
"testing"
66

7+
"github.com/golang/geo/r3"
78
"github.com/stretchr/testify/assert"
89
"google.golang.org/protobuf/proto"
910

@@ -348,6 +349,43 @@ func TestPlayerHurt_KnownWeaponDispatchesImmediately(t *testing.T) {
348349
assert.Equal(t, common.EqAK47, got[0].Weapon.Type)
349350
}
350351

352+
func TestPlayerHurt_FatalHitCappedAtPriorHitHealthSameTick(t *testing.T) {
353+
p := NewParser(rand.Reader).(*parser)
354+
p.currentFrame = 100
355+
p.gameState.ingameTick = 5000
356+
357+
victim := newPlayerWithEntityID(3)
358+
victim.UserID = 42
359+
360+
p.gameState.playersByUserID[victim.UserID] = victim
361+
362+
var got []events.PlayerHurt
363+
p.RegisterEventHandler(func(e events.PlayerHurt) {
364+
got = append(got, e)
365+
})
366+
367+
// First hit: non-fatal, victim reported at 35 HP after the hit.
368+
data := playerHurtEventData(int32(victim.UserID), 1, "ak47")
369+
data["health"].ValByte = proto.Int32(35)
370+
data["armor"].ValByte = proto.Int32(50)
371+
data["dmg_health"].ValShort = proto.Int32(15)
372+
373+
p.gameEventHandler.playerHurt(data)
374+
375+
// Second hit, same tick: fatal (health 0). Must be capped at the victim's remaining
376+
// HP right before it (35), not the start-of-tick value (100) which would over-report.
377+
data2 := playerHurtEventData(int32(victim.UserID), 1, "ak47")
378+
data2["health"].ValByte = proto.Int32(0)
379+
data2["armor"].ValByte = proto.Int32(50)
380+
data2["dmg_health"].ValShort = proto.Int32(35)
381+
382+
p.gameEventHandler.playerHurt(data2)
383+
384+
assert.Len(t, got, 2)
385+
assert.Equal(t, 15, got[0].HealthDamageTaken)
386+
assert.Equal(t, 35, got[1].HealthDamageTaken)
387+
}
388+
351389
func playerHurtEventData(userID int32, attacker int32, weapon string) map[string]*msg.CMsgSource1LegacyGameEventKeyT {
352390
return map[string]*msg.CMsgSource1LegacyGameEventKeyT{
353391
"userid": {
@@ -383,6 +421,89 @@ func TestGetCommunityId(t *testing.T) {
383421
assert.Equal(t, uint64(76561198012952267), xuid)
384422
}
385423

424+
func TestEntityKilled_DispatchesOtherDeathForNonPlayerEntity(t *testing.T) {
425+
p := NewParser(rand.Reader).(*parser)
426+
427+
killed := new(stfake.Entity)
428+
killed.On("ID").Return(5)
429+
killed.On("ServerClass").Return(serverClassStub{name: "CDynamicProp"})
430+
killed.On("Position").Return(r3.Vector{X: 1, Y: 2, Z: 3})
431+
432+
p.gameState.entities[5] = killed
433+
434+
inflictor := new(stfake.Entity)
435+
inflictor.On("ServerClass").Return(serverClassStub{name: "CInferno"})
436+
437+
p.gameState.entities[12] = inflictor
438+
439+
var got []events.OtherDeath
440+
p.RegisterEventHandler(func(e events.OtherDeath) {
441+
got = append(got, e)
442+
})
443+
444+
p.gameEventHandler.entityKilled(map[string]*msg.CMsgSource1LegacyGameEventKeyT{
445+
"entindex_killed": {ValLong: proto.Int32(5)},
446+
"entindex_inflictor": {ValLong: proto.Int32(12)},
447+
"entindex_attacker": {ValLong: proto.Int32(0)},
448+
})
449+
450+
assert.Len(t, got, 1)
451+
assert.Equal(t, "CDynamicProp", got[0].OtherType)
452+
assert.Equal(t, int32(5), got[0].OtherID)
453+
assert.Equal(t, r3.Vector{X: 1, Y: 2, Z: 3}, got[0].OtherPosition)
454+
assert.Equal(t, "CInferno", got[0].InflictorType)
455+
}
456+
457+
func TestEntityKilled_SkipsPlayerPawns(t *testing.T) {
458+
p := NewParser(rand.Reader).(*parser)
459+
460+
for _, className := range []string{"CCSPlayerPawn", "CCSPlayerPawnBase"} {
461+
killed := new(stfake.Entity)
462+
killed.On("ServerClass").Return(serverClassStub{name: className})
463+
464+
p.gameState.entities[5] = killed
465+
466+
var got []events.OtherDeath
467+
p.RegisterEventHandler(func(e events.OtherDeath) {
468+
got = append(got, e)
469+
})
470+
471+
p.gameEventHandler.entityKilled(map[string]*msg.CMsgSource1LegacyGameEventKeyT{
472+
"entindex_killed": {ValLong: proto.Int32(5)},
473+
"entindex_inflictor": {ValLong: proto.Int32(0)},
474+
"entindex_attacker": {ValLong: proto.Int32(0)},
475+
})
476+
477+
assert.Empty(t, got, "player-pawn deaths should be surfaced via player_death, not OtherDeath")
478+
}
479+
}
480+
481+
func TestEntityKilled_UnknownEntityIsSkipped(t *testing.T) {
482+
p := NewParser(rand.Reader).(*parser)
483+
484+
var got []events.OtherDeath
485+
p.RegisterEventHandler(func(e events.OtherDeath) {
486+
got = append(got, e)
487+
})
488+
489+
p.gameEventHandler.entityKilled(map[string]*msg.CMsgSource1LegacyGameEventKeyT{
490+
"entindex_killed": {ValLong: proto.Int32(999)},
491+
"entindex_inflictor": {ValLong: proto.Int32(0)},
492+
"entindex_attacker": {ValLong: proto.Int32(0)},
493+
})
494+
495+
assert.Empty(t, got)
496+
}
497+
498+
type serverClassStub struct {
499+
st.ServerClass
500+
name string
501+
}
502+
503+
func (s serverClassStub) Name() string {
504+
return s.name
505+
}
506+
386507
func TestGetCommunityId_BOT(t *testing.T) {
387508
xuid, err := guidToSteamID64("BOT")
388509
assert.Zero(t, xuid)

‎pkg/demoinfocs/game_state_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,55 @@ func TestParticipants_All(t *testing.T) {
8484
assert.ElementsMatch(t, []*common.Player{pl}, allPlayers)
8585
}
8686

87+
func TestParticipants_SortedDeterministically(t *testing.T) {
88+
steamA := uint64(76561198198570973)
89+
steamB := uint64(76561198199343350)
90+
91+
bot := newPlayer()
92+
bot.SteamID64 = 0
93+
bot.UserID = 7
94+
bot.IsConnected = true
95+
bot.Team = common.TeamTerrorists
96+
97+
playerWithHighSteamID := newPlayer()
98+
playerWithHighSteamID.SteamID64 = steamB
99+
playerWithHighSteamID.UserID = 1
100+
playerWithHighSteamID.IsConnected = true
101+
playerWithHighSteamID.Team = common.TeamTerrorists
102+
103+
playerA := newPlayer()
104+
playerA.SteamID64 = steamA
105+
playerA.UserID = 5
106+
playerA.IsConnected = true
107+
playerA.Team = common.TeamTerrorists
108+
109+
// Two bots (SteamID64 == 0) must be ordered by UserID.
110+
bot2 := newPlayer()
111+
bot2.SteamID64 = 0
112+
bot2.UserID = 3
113+
bot2.IsConnected = true
114+
bot2.Team = common.TeamTerrorists
115+
116+
ptcps := participants{
117+
playersByUserID: map[int]*common.Player{
118+
9: bot,
119+
3: playerWithHighSteamID,
120+
6: playerA,
121+
11: bot2,
122+
},
123+
}
124+
125+
// Python-style: sort criteria are SteamID64 ascending, then UserID ascending for ties.
126+
// Two bots share SteamID64=0 so they're ordered by UserID (3 before 7). SteamID64 76561198198570973
127+
// < 76561198199343350, so playerA comes before playerWithHighSteamID.
128+
expectedAll := []*common.Player{bot2, bot, playerA, playerWithHighSteamID}
129+
130+
assert.Equal(t, expectedAll, ptcps.All())
131+
assert.Equal(t, expectedAll, ptcps.Connected())
132+
assert.Equal(t, expectedAll, ptcps.Playing())
133+
assert.Equal(t, expectedAll, ptcps.TeamMembers(common.TeamTerrorists))
134+
}
135+
87136
func TestParticipants_Playing(t *testing.T) {
88137
terrorist := newPlayer()
89138
terrorist.Team = common.TeamTerrorists
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package sendtablescs2
2+
3+
import (
4+
"math"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
// TestQAngleFactory_Noscale32Bit verifies that a QAngle with 32-bit components is decoded as
11+
// full-precision noscale float32s (raw IEEE bits) rather than as scaled bit-angles mapped into
12+
// [0,360). See the m_aimPunchAngle / m_pAimPunchServices.m_predictableBaseAngle cases.
13+
func TestQAngleFactory_Noscale32Bit(t *testing.T) {
14+
t.Parallel()
15+
16+
bc := int32(32)
17+
f := field{bitCount: &bc}
18+
19+
dec := qangleFactory(&f)
20+
21+
raw := []byte{}
22+
for _, v := range []float32{1.5, -4.25, 350.0} {
23+
bits := math.Float32bits(v)
24+
for i := 0; i < 4; i++ {
25+
raw = append(raw, byte(bits>>(8*i))) // little-endian
26+
}
27+
}
28+
29+
val := dec(newReader(raw)).([3]float32)
30+
31+
assert.Equal(t, [3]float32{1.5, -4.25, 350.0}, val)
32+
}
33+
34+
// TestQAngleFactory_ScaledBitAngle still reads scaled bit-angles for sub-32-bit fields, so the
35+
// noscale guard doesn't regress the common smaller encodings.
36+
func TestQAngleFactory_ScaledBitAngle(t *testing.T) {
37+
t.Parallel()
38+
39+
bc := int32(20)
40+
f := field{bitCount: &bc}
41+
42+
dec := qangleFactory(&f)
43+
44+
// A raw 20-bit value of 0x80000 (largest magnitude) maps to ~359.65 degrees.
45+
val := dec(newReader([]byte{0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x08, 0x00})).([3]float32)
46+
47+
assert.Less(t, val[0], float32(360.0))
48+
assert.Less(t, val[1], float32(360.0))
49+
assert.Less(t, val[2], float32(360.0))
50+
}

0 commit comments

Comments
 (0)