@@ -28,13 +28,17 @@ func (s *state) onGrenadeProjectileThrow(e events.GrenadeProjectileThrow) {
2828 pos .Y = tp .Y
2929 pos .Z = tp .Z
3030 }
31+ s .grenadeSeq ++
32+ gid := s .grenadeSeq
33+
3134 ev := EventGrenadeThrow {
32- Tick : s .parser .GameState ().IngameTick (),
33- Round : s .currentRound ,
34- Type : gtype ,
35- OriginX : float32 (pos .X ),
36- OriginY : float32 (pos .Y ),
37- OriginZ : float32 (pos .Z ),
35+ Tick : s .parser .GameState ().IngameTick (),
36+ Round : s .currentRound ,
37+ GrenadeID : gid ,
38+ Type : gtype ,
39+ OriginX : float32 (pos .X ),
40+ OriginY : float32 (pos .Y ),
41+ OriginZ : float32 (pos .Z ),
3842 }
3943 if thrower != nil {
4044 ev .ThrowerSteamID = steamIDStr (thrower )
@@ -45,6 +49,7 @@ func (s *state) onGrenadeProjectileThrow(e events.GrenadeProjectileThrow) {
4549 if e .Projectile .Entity != nil {
4650 entID := e .Projectile .Entity .ID ()
4751 s .grenadePos [entID ] = grenadeProjectile {
52+ id : gid ,
4853 x : float32 (pos .X ),
4954 y : float32 (pos .Y ),
5055 z : float32 (pos .Z ),
@@ -68,6 +73,7 @@ func (s *state) onGrenadeProjectileDestroy(e events.GrenadeProjectileDestroy) {
6873 g .x = float32 (pos .X )
6974 g .y = float32 (pos .Y )
7075 g .z = float32 (pos .Z )
76+ g .destroyTick = s .parser .GameState ().IngameTick ()
7177 s .grenadePos [entID ] = g
7278 }
7379}
@@ -115,6 +121,11 @@ func (s *state) onFrameDoneGrenades() {
115121// emitDetonate is the shared handler for the four detonation events.
116122// Prefers the tracked projectile position over the event's Position
117123// since demoinfocs reports stale/(0,0,0) Position on some CS2 demos.
124+ const (
125+ maxDetonateLagTicks = 64
126+ maxMatchDistSq = float32 (250 * 250 )
127+ )
128+
118129func (s * state ) emitDetonate (base events.GrenadeEvent , typeOverride string ) {
119130 gtype := typeOverride
120131 if gtype == "" {
@@ -124,22 +135,20 @@ func (s *state) emitDetonate(base events.GrenadeEvent, typeOverride string) {
124135 return
125136 }
126137
138+ tick := s .parser .GameState ().IngameTick ()
127139 x := float32 (base .Position .X )
128140 y := float32 (base .Position .Y )
129141 z := float32 (base .Position .Z )
130- if g , ok := s .grenadePos [base .GrenadeEntityID ]; ok {
131- // Tracked projectile position is the source of truth; only use
132- // event Position if it's plausibly non-stale (non-zero AND
133- // within a sane range of the tracked one).
134- if g .x != 0 || g .y != 0 {
135- x = g .x
136- y = g .y
137- z = g .z
138- }
142+
143+ key , proj , ok := s .matchProjectile (base .GrenadeEntityID , gtype , x , y , tick )
144+ if ok && (proj .x != 0 || proj .y != 0 ) {
145+ x = proj .x
146+ y = proj .y
147+ z = proj .z
139148 }
140149
141150 ev := EventGrenadeDetonate {
142- Tick : s . parser . GameState (). IngameTick () ,
151+ Tick : tick ,
143152 Round : s .currentRound ,
144153 Type : gtype ,
145154 X : x ,
@@ -148,12 +157,46 @@ func (s *state) emitDetonate(base events.GrenadeEvent, typeOverride string) {
148157 }
149158 if base .Thrower != nil {
150159 ev .ThrowerSteamID = steamIDStr (base .Thrower )
151- } else if g , ok := s .grenadePos [base .GrenadeEntityID ]; ok && g .thrower != "" {
152- ev .ThrowerSteamID = g .thrower
160+ } else if ok && proj .thrower != "" {
161+ ev .ThrowerSteamID = proj .thrower
162+ }
163+ if ok {
164+ ev .GrenadeID = proj .id
165+ proj .matched = true
166+ s .grenadePos [key ] = proj
153167 }
154168 s .res .GrenadeDetonations = append (s .res .GrenadeDetonations , ev )
155169}
156170
171+ func (s * state ) matchProjectile (entID int , gtype string , x , y float32 , tick int ) (int , grenadeProjectile , bool ) {
172+ if g , ok := s .grenadePos [entID ]; ok && ! g .matched && g .gtype == gtype {
173+ return entID , g , true
174+ }
175+ bestKey := - 1
176+ var best grenadeProjectile
177+ bestDist := float32 (- 1 )
178+ for k , g := range s .grenadePos {
179+ if g .matched || g .gtype != gtype {
180+ continue
181+ }
182+ if g .destroyTick == 0 || g .destroyTick > tick || tick - g .destroyTick > maxDetonateLagTicks {
183+ continue
184+ }
185+ dx := g .x - x
186+ dy := g .y - y
187+ dist := dx * dx + dy * dy
188+ if bestDist < 0 || dist < bestDist {
189+ bestDist = dist
190+ bestKey = k
191+ best = g
192+ }
193+ }
194+ if bestKey >= 0 && bestDist <= maxMatchDistSq {
195+ return bestKey , best , true
196+ }
197+ return - 1 , grenadeProjectile {}, false
198+ }
199+
157200func (s * state ) onHeExplode (e events.HeExplode ) {
158201 if ! s .matchStarted {
159202 return
0 commit comments