Skip to content

Commit 4126b7b

Browse files
markus-waclaude
andcommitted
fix(events): pair PlayerFlashed victims by detonation, not FIFO order
PlayerFlashed was synthesized by attaching every m_flFlashDuration update and every detonation to the head of a FIFO queue of in-flight flashbangs. A detonation with zero direct victims while another flashbang was airborne shifted the queue by one, so every subsequent flash credited the wrong thrower for the rest of the round. Mark the detonated entry by the destroyed projectile's identity, buffer the pawns flashed each frame, and attribute them to the flashbang that detonated that frame (closest by detonation position when several detonate at once). The thrower (proj.Thrower) was already correct, matching FlashExplode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e1e45f5 commit 4126b7b

3 files changed

Lines changed: 73 additions & 25 deletions

File tree

‎pkg/demoinfocs/datatables.go‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,12 @@ func (p *parser) bindNewPlayerPawn(pawnEntity st.Entity) {
520520

521521
pl.FlashDuration = val.Float()
522522

523-
if pl.FlashDuration > 0 {
524-
if len(p.gameState.flyingFlashbangs) == 0 {
525-
return
526-
}
527-
528-
flashbang := p.gameState.flyingFlashbangs[0]
529-
flashbang.flashedEntityIDs = append(flashbang.flashedEntityIDs, pl.EntityID)
523+
if pl.FlashDuration > 0 && len(p.gameState.flyingFlashbangs) > 0 {
524+
// The detonation (projectile OnDestroy) and these m_flFlashDuration updates arrive in the
525+
// same frame but in an unspecified order, so we can't yet know which projectile caused
526+
// this. Buffer the flashed pawn and pair it with the flashbang that detonated this frame
527+
// at the end of the frame (see processFlyingFlashbangs).
528+
p.gameState.flashedEntitiesThisFrame = append(p.gameState.flashedEntitiesThisFrame, pl.EntityID)
530529
}
531530
})
532531

@@ -753,8 +752,7 @@ func (p *parser) bindGrenadeProjectiles(entity st.Entity) {
753752

754753
if proj.WeaponInstance.Type == common.EqFlash {
755754
p.gameState.flyingFlashbangs = append(p.gameState.flyingFlashbangs, &FlyingFlashbang{
756-
projectile: proj,
757-
flashedEntityIDs: []int{},
755+
projectile: proj,
758756
})
759757
}
760758

@@ -796,12 +794,17 @@ func (p *parser) bindGrenadeProjectiles(entity st.Entity) {
796794
},
797795
})
798796

799-
if len(p.gameState.flyingFlashbangs) == 0 {
800-
return
801-
}
797+
// Mark the entry for the projectile that actually detonated (entity-keyed), not the
798+
// head of the queue - otherwise a zero-victim detonation while another flashbang is
799+
// airborne shifts victim attribution by one for the rest of the round.
800+
for _, flashbang := range p.gameState.flyingFlashbangs {
801+
if flashbang.projectile == proj {
802+
flashbang.explodedFrame = p.currentFrame
803+
flashbang.position = proj.Position()
802804

803-
flashbang := p.gameState.flyingFlashbangs[0]
804-
flashbang.explodedFrame = p.currentFrame
805+
break
806+
}
807+
}
805808
}
806809

807810
p.nadeProjectileDestroyed(proj)

‎pkg/demoinfocs/game_events.go‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ func (geh gameEventHandler) clearGrenadeProjectiles() {
287287
// Thrown grenades could not be deleted at the end of the round (if they are thrown at the very end, they never get destroyed)
288288
geh.gameState().thrownGrenades = make(map[*common.Player]map[common.EquipmentType][]*common.Equipment)
289289
geh.gameState().flyingFlashbangs = make([]*FlyingFlashbang, 0)
290+
geh.gameState().flashedEntitiesThisFrame = nil
290291
}
291292

292293
func (geh gameEventHandler) roundStart(data map[string]*msg.CMsgSource1LegacyGameEventKeyT) {
@@ -1249,34 +1250,72 @@ func (p *parser) processRoundProgressEvents() {
12491250
}
12501251

12511252
func (p *parser) processFlyingFlashbangs() {
1253+
victims := p.gameState.flashedEntitiesThisFrame
1254+
p.gameState.flashedEntitiesThisFrame = nil
1255+
12521256
if len(p.gameState.flyingFlashbangs) == 0 {
12531257
return
12541258
}
12551259

1256-
flashbang := p.gameState.flyingFlashbangs[0]
1257-
if len(flashbang.flashedEntityIDs) == 0 {
1258-
// Flashbang exploded and didn't flash any players, remove it from the queue
1259-
if flashbang.explodedFrame > 0 && flashbang.explodedFrame < p.currentFrame {
1260-
p.gameState.flyingFlashbangs = p.gameState.flyingFlashbangs[1:]
1260+
// Split the in-flight flashbangs into those that detonated this frame and those still airborne.
1261+
// Detonation is keyed by the destroyed projectile's identity (see the OnDestroy handler), so the
1262+
// queue no longer relies on FIFO order and a zero-victim detonation can't desync attribution.
1263+
var detonated []*FlyingFlashbang
1264+
1265+
kept := p.gameState.flyingFlashbangs[:0]
1266+
1267+
for _, flashbang := range p.gameState.flyingFlashbangs {
1268+
if flashbang.explodedFrame == p.currentFrame {
1269+
detonated = append(detonated, flashbang)
1270+
} else {
1271+
kept = append(kept, flashbang)
12611272
}
1273+
}
1274+
1275+
p.gameState.flyingFlashbangs = kept
12621276

1277+
if len(detonated) == 0 {
12631278
return
12641279
}
12651280

1266-
for _, entityID := range flashbang.flashedEntityIDs {
1281+
// Attribute each pawn flashed this frame to the flashbang that detonated this frame. When more
1282+
// than one detonated simultaneously, pick the closest by detonation position.
1283+
for _, entityID := range victims {
12671284
player := p.gameState.Participants().ByEntityID()[entityID]
12681285
if player == nil {
12691286
continue
12701287
}
12711288

1289+
flashbang := nearestDetonatedFlashbang(detonated, player)
1290+
12721291
p.gameEventHandler.dispatch(events.PlayerFlashed{
12731292
Player: player,
12741293
Attacker: flashbang.projectile.Thrower,
12751294
Projectile: flashbang.projectile,
12761295
})
12771296
}
1297+
}
1298+
1299+
// nearestDetonatedFlashbang returns the flashbang whose detonation position is closest to the
1300+
// player - used to attribute a victim when multiple flashbangs detonate on the same frame.
1301+
func nearestDetonatedFlashbang(flashbangs []*FlyingFlashbang, player *common.Player) *FlyingFlashbang {
1302+
nearest := flashbangs[0]
1303+
1304+
if len(flashbangs) == 1 {
1305+
return nearest
1306+
}
1307+
1308+
pos := player.Position()
1309+
minDist := pos.Sub(nearest.position).Norm()
1310+
1311+
for _, flashbang := range flashbangs[1:] {
1312+
if dist := pos.Sub(flashbang.position).Norm(); dist < minDist {
1313+
minDist = dist
1314+
nearest = flashbang
1315+
}
1316+
}
12781317

1279-
p.gameState.flyingFlashbangs = p.gameState.flyingFlashbangs[1:]
1318+
return nearest
12801319
}
12811320

12821321
// Do some processing to dispatch game events at the end of the frame in correct order.

‎pkg/demoinfocs/game_state.go‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66
"time"
77

8+
"github.com/golang/geo/r3"
89
common "github.com/markus-wa/demoinfocs-golang/v5/pkg/demoinfocs/common"
910
"github.com/markus-wa/demoinfocs-golang/v5/pkg/demoinfocs/constants"
1011
"github.com/markus-wa/demoinfocs-golang/v5/pkg/demoinfocs/events"
@@ -61,14 +62,19 @@ type gameState struct {
6162
// As a solution, we keep track of flashbang projectiles created and all m_flFlashDuration prop updates related
6263
// to this projectile. As all m_flFlashDuration prop updates occur during the same frame, we batch dispatch
6364
// player-flashed events at the end of the frame if there are any.
64-
// This slice acts like a FIFO queue, the first projectile inserted is the first one to be removed when it exploded.
65+
// Flashbangs currently in flight (thrown but not yet detonated). Entries are keyed by the
66+
// projectile identity, not by position - a flashbang is removed when its own projectile entity
67+
// is destroyed, so a zero-victim detonation no longer desyncs attribution for the round.
6568
flyingFlashbangs []*FlyingFlashbang
69+
// Pawn entity-IDs whose m_flFlashDuration was set during the current frame. They are paired with
70+
// the flashbang(s) that detonated in the same frame at the end of the frame.
71+
flashedEntitiesThisFrame []int
6672
}
6773

6874
type FlyingFlashbang struct {
69-
projectile *common.GrenadeProjectile
70-
flashedEntityIDs []int
71-
explodedFrame int
75+
projectile *common.GrenadeProjectile
76+
position r3.Vector // Detonation position, set when the projectile is destroyed. Used to pair victims when multiple flashbangs detonate on the same frame.
77+
explodedFrame int
7278
}
7379

7480
type lastFlash struct {

0 commit comments

Comments
 (0)