Skip to content

Commit 59fbe07

Browse files
xavier-rodetmarkus-wa
authored andcommitted
improve grenade tracking (#160)
- add GrenadeEvent.Grenade - add GrenadeProjectile.WeaponInstance - deprecated Inferno.Owner() in favor Inferno.Thrower() - internally, thrown grenades are tracked in GameState.thrownGrenades
1 parent af64f51 commit 59fbe07

File tree

9 files changed

+268
-19
lines changed

9 files changed

+268
-19
lines changed

common/common.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,16 @@ func (h DemoHeader) TickTime() time.Duration {
6464
// GrenadeProjectile is a grenade thrown intentionally by a player. It is used to track grenade projectile
6565
// positions between the time at which they are thrown and until they detonate.
6666
type GrenadeProjectile struct {
67-
EntityID int
68-
Weapon EquipmentElement
69-
Thrower *Player // Always seems to be the same as Owner, even if the grenade was picked up
70-
Owner *Player // Always seems to be the same as Thrower, even if the grenade was picked up
71-
Position r3.Vector
72-
Trajectory []r3.Vector // List of all known locations of the grenade up to the current point
67+
EntityID int
68+
// Deprecated: Weapon exists for historical compatibility
69+
// and should not be used. To access the weapon corresponding to his GrenadeProjectile,
70+
// use the WeaponInstance.Weapon instead.
71+
Weapon EquipmentElement
72+
WeaponInstance *Equipment
73+
Thrower *Player // Always seems to be the same as Owner, even if the grenade was picked up
74+
Owner *Player // Always seems to be the same as Thrower, even if the grenade was picked up
75+
Position r3.Vector
76+
Trajectory []r3.Vector // List of all known locations of the grenade up to the current point
7377

7478
// uniqueID is used to distinguish different grenades (which potentially have the same, reused entityID) from each other.
7579
uniqueID int64

common/inferno.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,16 @@ func (inf Inferno) ConvexHull3D() quickhull.ConvexHull {
139139

140140
// Owner returns the player who threw the fire grenade.
141141
// Could be nil if the player disconnected after throwing it.
142+
//
143+
// Deprecated: Owner() exists for historical compatibility
144+
// and should not be used. Use Thrower() instead.
142145
func (inf Inferno) Owner() *Player {
146+
return inf.Thrower()
147+
}
148+
149+
// Thrower returns the player who threw the fire grenade.
150+
// Could be nil if the player disconnected after throwing it.
151+
func (inf Inferno) Thrower() *Player {
143152
return inf.demoInfoProvider.FindPlayerByHandle(inf.Entity.FindPropertyI("m_hOwnerEntity").Value().IntVal)
144153
}
145154

common/inferno_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,14 @@ func TestInferno_Owner(t *testing.T) {
120120

121121
assert.Equal(t, player, NewInferno(provider, entity).Owner())
122122
}
123+
124+
func TestInferno_Thrower(t *testing.T) {
125+
entity := entityWithProperty("m_hOwnerEntity", st.PropertyValue{IntVal: 1})
126+
127+
player := new(Player)
128+
provider := demoInfoProviderMock{
129+
playersByHandle: map[int]*Player{1: player},
130+
}
131+
132+
assert.Equal(t, player, NewInferno(provider, entity).Thrower())
133+
}

datatables.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@ func (p *Parser) bindGrenadeProjectiles(entity *st.Entity) {
376376
p.gameState.grenadeProjectiles[entityID] = proj
377377

378378
entity.OnCreateFinished(func() {
379+
p.gameEventHandler.addThrownGrenade(proj.Thrower, proj.WeaponInstance)
380+
379381
p.eventDispatcher.Dispatch(events.GrenadeProjectileThrow{
380382
Projectile: proj,
381383
})
@@ -387,6 +389,9 @@ func (p *Parser) bindGrenadeProjectiles(entity *st.Entity) {
387389

388390
entity.FindPropertyI("m_nModelIndex").OnUpdate(func(val st.PropertyValue) {
389391
proj.Weapon = p.grenadeModelIndices[val.IntVal]
392+
393+
equipment := common.NewEquipment(p.grenadeModelIndices[val.IntVal])
394+
proj.WeaponInstance = &equipment
390395
})
391396

392397
// @micvbang: not quite sure what the difference between Thrower and Owner is.
@@ -432,9 +437,18 @@ func (p *Parser) nadeProjectileDestroyed(proj *common.GrenadeProjectile) {
432437

433438
delete(p.gameState.grenadeProjectiles, proj.EntityID)
434439

435-
if proj.Weapon == common.EqFlash {
440+
if proj.WeaponInstance.Weapon == common.EqFlash {
436441
p.gameState.lastFlash.projectileByPlayer[proj.Owner] = proj
437442
}
443+
444+
// We delete from the Owner.ThrownGrenades (only if not inferno or smoke, because they will be deleted when they expire)
445+
isInferno := proj.WeaponInstance.Weapon == common.EqMolotov || proj.WeaponInstance.Weapon == common.EqIncendiary
446+
isSmoke := proj.WeaponInstance.Weapon == common.EqSmoke
447+
isDecoy := proj.WeaponInstance.Weapon == common.EqDecoy
448+
449+
if !isInferno && !isSmoke && !isDecoy {
450+
p.gameEventHandler.deleteThrownGrenade(proj.Thrower, proj.WeaponInstance.Weapon)
451+
}
438452
}
439453

440454
func (p *Parser) bindWeapon(entity *st.Entity, wepType common.EquipmentElement) {
@@ -539,6 +553,8 @@ func (p *Parser) infernoExpired(inf *common.Inferno) {
539553
})
540554

541555
delete(p.gameState.infernos, inf.EntityID)
556+
557+
p.gameEventHandler.deleteThrownGrenade(inf.Thrower(), common.EqIncendiary)
542558
}
543559

544560
func (p *Parser) bindGameRules() {

events/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type GrenadeEventIf interface {
172172
// handlers on this tho, you want GrenadeEventIf for that
173173
type GrenadeEvent struct {
174174
GrenadeType common.EquipmentElement
175+
Grenade *common.Equipment // Maybe nil for InfernoStart & InfernoExpired since we don't know the thrower (at least in old demos)
175176
Position r3.Vector
176177
Thrower *common.Player
177178
GrenadeEntityID int

game_events.go

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ func (geh gameEventHandler) roundOfficiallyEnded(data map[string]*msg.CSVCMsg_Ga
238238
geh.parser.infernoExpired(inf)
239239
}
240240

241+
// Thrown grenades could not be deleted at the end of the round (if they are thrown at the very end, they never get destroyed)
242+
geh.gameState().thrownGrenades = make(map[*common.Player][]*common.Equipment)
243+
241244
geh.dispatch(events.RoundEndOfficial{})
242245
}
243246

@@ -307,7 +310,7 @@ func (geh gameEventHandler) playerDeath(data map[string]*msg.CSVCMsg_GameEventKe
307310
Assister: geh.playerByUserID32(data["assister"].GetValShort()),
308311
IsHeadshot: data["headshot"].GetValBool(),
309312
PenetratedObjects: int(data["penetrated"].GetValShort()),
310-
Weapon: getPlayerWeapon(killer, wepType),
313+
Weapon: geh.getEquipmentInstance(killer, wepType),
311314
})
312315
}
313316

@@ -323,7 +326,7 @@ func (geh gameEventHandler) playerHurt(data map[string]*msg.CSVCMsg_GameEventKey
323326
HealthDamage: int(data["dmg_health"].GetValShort()),
324327
ArmorDamage: int(data["dmg_armor"].GetValByte()),
325328
HitGroup: events.HitGroup(data["hitgroup"].GetValByte()),
326-
Weapon: getPlayerWeapon(attacker, wepType),
329+
Weapon: geh.getEquipmentInstance(attacker, wepType),
327330
})
328331
}
329332

@@ -366,9 +369,12 @@ func (geh gameEventHandler) decoyStarted(data map[string]*msg.CSVCMsg_GameEventK
366369
}
367370

368371
func (geh gameEventHandler) decoyDetonate(data map[string]*msg.CSVCMsg_GameEventKeyT) {
372+
event := geh.nadeEvent(data, common.EqDecoy)
369373
geh.dispatch(events.DecoyExpired{
370-
GrenadeEvent: geh.nadeEvent(data, common.EqDecoy),
374+
GrenadeEvent: event,
371375
})
376+
377+
geh.deleteThrownGrenade(event.Thrower, common.EqDecoy)
372378
}
373379

374380
func (geh gameEventHandler) smokeGrenadeDetonate(data map[string]*msg.CSVCMsg_GameEventKeyT) {
@@ -378,9 +384,12 @@ func (geh gameEventHandler) smokeGrenadeDetonate(data map[string]*msg.CSVCMsg_Ga
378384
}
379385

380386
func (geh gameEventHandler) smokeGrenadeExpired(data map[string]*msg.CSVCMsg_GameEventKeyT) {
387+
event := geh.nadeEvent(data, common.EqSmoke)
381388
geh.dispatch(events.SmokeExpired{
382-
GrenadeEvent: geh.nadeEvent(data, common.EqSmoke),
389+
GrenadeEvent: event,
383390
})
391+
392+
geh.deleteThrownGrenade(event.Thrower, common.EqSmoke)
384393
}
385394

386395
func (geh gameEventHandler) infernoStartBurn(data map[string]*msg.CSVCMsg_GameEventKeyT) {
@@ -583,18 +592,80 @@ func (geh gameEventHandler) nadeEvent(data map[string]*msg.CSVCMsg_GameEventKeyT
583592

584593
return events.GrenadeEvent{
585594
GrenadeType: nadeType,
595+
Grenade: geh.getThrownGrenade(thrower, nadeType),
586596
Thrower: thrower,
587597
Position: position,
588598
GrenadeEntityID: nadeEntityID,
589599
}
590600
}
591601

592-
func mapGameEventData(d *msg.CSVCMsg_GameEventListDescriptorT, e *msg.CSVCMsg_GameEvent) map[string]*msg.CSVCMsg_GameEventKeyT {
593-
data := make(map[string]*msg.CSVCMsg_GameEventKeyT)
594-
for i, k := range d.Keys {
595-
data[k.Name] = e.Keys[i]
602+
func (geh gameEventHandler) addThrownGrenade(p *common.Player, wep *common.Equipment) {
603+
if p == nil {
604+
// can happen for "unknown" players (see #162)
605+
return
596606
}
597-
return data
607+
608+
gameState := geh.gameState()
609+
gameState.thrownGrenades[p] = append(gameState.thrownGrenades[p], wep)
610+
}
611+
612+
func (geh gameEventHandler) getThrownGrenade(p *common.Player, wepType common.EquipmentElement) *common.Equipment {
613+
if p == nil {
614+
// can happen for incendiaries or "unknown" players (see #162)
615+
return nil
616+
}
617+
618+
// Get the first weapon we found for this player with this weapon type
619+
for _, thrownGrenade := range geh.gameState().thrownGrenades[p] {
620+
if isSameEquipmentElement(thrownGrenade.Weapon, wepType) {
621+
return thrownGrenade
622+
}
623+
}
624+
625+
// smokes might have duplicate smokegrenade_expired events, so it could have already been deleted.
626+
// if it's not a smoke this should never be reached
627+
unassert.Samef(wepType, common.EqSmoke, "tried to get non-existing grenade from gameState.thrownGrenades")
628+
629+
return nil
630+
}
631+
632+
func (geh gameEventHandler) deleteThrownGrenade(p *common.Player, wepType common.EquipmentElement) {
633+
if p == nil {
634+
// can happen for incendiaries or "unknown" players (see #162)
635+
return
636+
}
637+
638+
gameState := geh.gameState()
639+
640+
// Delete the first weapon we found with this weapon type
641+
for i, weapon := range gameState.thrownGrenades[p] {
642+
// If same weapon type
643+
// OR if it's an EqIncendiary we must check for EqMolotov too because of geh.infernoExpire() handling ?
644+
if isSameEquipmentElement(wepType, weapon.Weapon) {
645+
gameState.thrownGrenades[p] = append(gameState.thrownGrenades[p][:i], gameState.thrownGrenades[p][i+1:]...)
646+
return
647+
}
648+
}
649+
650+
// smokes might have duplicate smokegrenade_expired events, so it might already be deleted.
651+
// besides that this code should never be reached
652+
unassert.Samef(wepType, common.EqSmoke, "trying to delete non-existing grenade from gameState.thrownGrenades")
653+
}
654+
655+
func (geh gameEventHandler) getEquipmentInstance(player *common.Player, wepType common.EquipmentElement) *common.Equipment {
656+
isGrenade := wepType.Class() == common.EqClassGrenade
657+
if isGrenade {
658+
return geh.getThrownGrenade(player, wepType)
659+
}
660+
661+
return getPlayerWeapon(player, wepType)
662+
}
663+
664+
// checks if two EquipmentElements are the same, considering that incendiary and molotov should be treated as identical
665+
func isSameEquipmentElement(a common.EquipmentElement, b common.EquipmentElement) bool {
666+
return a == b ||
667+
(a == common.EqIncendiary && b == common.EqMolotov) ||
668+
(b == common.EqIncendiary && a == common.EqMolotov)
598669
}
599670

600671
// Returns the players instance of the weapon if applicable or a new instance otherwise.
@@ -612,6 +683,15 @@ func getPlayerWeapon(player *common.Player, wepType common.EquipmentElement) *co
612683
return &wep
613684
}
614685

686+
func mapGameEventData(d *msg.CSVCMsg_GameEventListDescriptorT, e *msg.CSVCMsg_GameEvent) map[string]*msg.CSVCMsg_GameEventKeyT {
687+
data := make(map[string]*msg.CSVCMsg_GameEventKeyT)
688+
for i, k := range d.Keys {
689+
data[k.Name] = e.Keys[i]
690+
}
691+
692+
return data
693+
}
694+
615695
// We're all better off not asking questions
616696
const valveMagicNumber = 76561197960265728
617697

game_events_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,129 @@ func TestGetPlayerWeapon_NotFound(t *testing.T) {
8686

8787
assert.Equal(t, common.EqM4A1, wep.Weapon)
8888
}
89+
90+
func TestAddThrownGrenade_NilPlayer(t *testing.T) {
91+
p := NewParser(rand.Reader)
92+
he := common.NewEquipment(common.EqHE)
93+
94+
assert.Empty(t, p.gameState.thrownGrenades)
95+
96+
p.gameEventHandler.addThrownGrenade(nil, &he)
97+
98+
assert.Empty(t, p.gameState.thrownGrenades)
99+
}
100+
101+
func TestAddThrownGrenade(t *testing.T) {
102+
p := NewParser(rand.Reader)
103+
pl := &common.Player{}
104+
he := common.NewEquipment(common.EqHE)
105+
106+
assert.Empty(t, p.gameState.thrownGrenades)
107+
108+
p.gameEventHandler.addThrownGrenade(pl, &he)
109+
110+
assert.NotEmpty(t, p.gameState.thrownGrenades)
111+
assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
112+
assert.Equal(t, p.gameState.thrownGrenades[pl][0], &he)
113+
}
114+
115+
func TestGetThrownGrenade_NilPlayer(t *testing.T) {
116+
p := NewParser(rand.Reader)
117+
he := common.NewEquipment(common.EqHE)
118+
119+
wep := p.gameEventHandler.getThrownGrenade(nil, he.Weapon)
120+
121+
assert.Nil(t, wep)
122+
}
123+
124+
func TestGetThrownGrenade_NotFound(t *testing.T) {
125+
p := NewParser(rand.Reader)
126+
pl := &common.Player{}
127+
128+
he := common.NewEquipment(common.EqSmoke)
129+
130+
wep := p.gameEventHandler.getThrownGrenade(pl, he.Weapon)
131+
132+
assert.Nil(t, wep)
133+
}
134+
135+
func TestGetThrownGrenade_Found(t *testing.T) {
136+
p := NewParser(rand.Reader)
137+
pl := &common.Player{}
138+
he := common.NewEquipment(common.EqHE)
139+
140+
p.gameEventHandler.addThrownGrenade(pl, &he)
141+
wep := p.gameEventHandler.getThrownGrenade(pl, he.Weapon)
142+
143+
assert.Equal(t, wep.Weapon, he.Weapon)
144+
assert.Equal(t, wep, &he)
145+
}
146+
147+
func TestDeleteThrownGrenade_NilPlayer(t *testing.T) {
148+
p := NewParser(rand.Reader)
149+
he := common.NewEquipment(common.EqHE)
150+
151+
// Do nothing, we just keep sure it doesn't crash
152+
p.gameEventHandler.deleteThrownGrenade(nil, he.Weapon)
153+
}
154+
155+
func TestDeleteThrownGrenade_NotFound(t *testing.T) {
156+
p := NewParser(rand.Reader)
157+
pl := &common.Player{}
158+
he := common.NewEquipment(common.EqHE)
159+
160+
assert.Empty(t, p.gameState.thrownGrenades)
161+
162+
p.gameEventHandler.addThrownGrenade(pl, &he)
163+
164+
assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
165+
166+
p.gameEventHandler.deleteThrownGrenade(pl, common.EqSmoke)
167+
168+
assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
169+
}
170+
171+
func TestDeleteThrownGrenade_Found(t *testing.T) {
172+
p := NewParser(rand.Reader)
173+
pl := &common.Player{}
174+
he := common.NewEquipment(common.EqHE)
175+
176+
assert.Empty(t, p.gameState.thrownGrenades)
177+
178+
p.gameEventHandler.addThrownGrenade(pl, &he)
179+
180+
assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
181+
182+
p.gameEventHandler.deleteThrownGrenade(pl, he.Weapon)
183+
184+
assert.Empty(t, p.gameState.thrownGrenades[pl])
185+
}
186+
187+
func TestGetEquipmentInstance_NotGrenade(t *testing.T) {
188+
p := NewParser(rand.Reader)
189+
pl := &common.Player{}
190+
191+
wep := p.gameEventHandler.getEquipmentInstance(pl, common.EqAK47)
192+
193+
assert.Equal(t, common.EqAK47, wep.Weapon)
194+
}
195+
196+
func TestGetEquipmentInstance_Grenade_NotThrown(t *testing.T) {
197+
p := NewParser(rand.Reader)
198+
pl := &common.Player{}
199+
200+
wep := p.gameEventHandler.getEquipmentInstance(pl, common.EqSmoke)
201+
202+
assert.Nil(t, wep)
203+
}
204+
205+
func TestGetEquipmentInstance_Grenade_Thrown(t *testing.T) {
206+
p := NewParser(rand.Reader)
207+
pl := &common.Player{}
208+
he := common.NewEquipment(common.EqHE)
209+
210+
p.gameEventHandler.addThrownGrenade(pl, &he)
211+
wep := p.gameEventHandler.getEquipmentInstance(pl, he.Weapon)
212+
213+
assert.Equal(t, &he, wep)
214+
}

0 commit comments

Comments
 (0)