Skip to content

Commit 61c683b

Browse files
committed
bug: fix grenades paths
1 parent 1ff176d commit 61c683b

3 files changed

Lines changed: 88 additions & 38 deletions

File tree

internal/parser/grenades.go

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
118129
func (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+
157200
func (s *state) onHeExplode(e events.HeExplode) {
158201
if !s.matchStarted {
159202
return

internal/parser/parser.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ type state struct {
6464
// frame and consulting it on the detonate event gives reliable
6565
// coords.
6666
grenadePos map[int]grenadeProjectile
67+
68+
grenadeSeq int
6769
}
6870

6971
type grenadeProjectile struct {
70-
x, y, z float32
71-
gtype string
72-
thrower string
73-
team string
72+
id int
73+
x, y, z float32
74+
gtype string
75+
thrower string
76+
team string
77+
destroyTick int
78+
matched bool
7479
}
7580

7681
// Parse reads a CS2 demo from r and returns the playback metadata,

internal/parser/types.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ type EventKitDrop struct {
6565
}
6666

6767
type EventShotFired struct {
68-
Tick int `json:"tick"`
69-
Round int `json:"round,omitempty"`
70-
AttackerSteamID string `json:"attacker,omitempty"`
71-
AttackerTeam string `json:"attacker_team,omitempty"`
72-
Weapon string `json:"weapon,omitempty"`
73-
IsRifle bool `json:"is_rifle,omitempty"`
74-
IsCrouched bool `json:"is_crouched,omitempty"`
75-
EnemySpotted bool `json:"enemy_spotted,omitempty"`
68+
Tick int `json:"tick"`
69+
Round int `json:"round,omitempty"`
70+
AttackerSteamID string `json:"attacker,omitempty"`
71+
AttackerTeam string `json:"attacker_team,omitempty"`
72+
Weapon string `json:"weapon,omitempty"`
73+
IsRifle bool `json:"is_rifle,omitempty"`
74+
IsCrouched bool `json:"is_crouched,omitempty"`
75+
EnemySpotted bool `json:"enemy_spotted,omitempty"`
7676
// IsSpray: this shot followed the same attacker's previous shot
7777
// within 250ms — i.e. trigger held. First shot of a burst is false.
7878
IsSpray bool `json:"is_spray,omitempty"`
@@ -176,6 +176,7 @@ type EventSpotted struct {
176176
type EventGrenadeThrow struct {
177177
Tick int `json:"tick"`
178178
Round int `json:"round,omitempty"`
179+
GrenadeID int `json:"gid,omitempty"`
179180
ThrowerSteamID string `json:"thrower,omitempty"`
180181
ThrowerTeam string `json:"thrower_team,omitempty"`
181182
Type string `json:"type"`
@@ -187,6 +188,7 @@ type EventGrenadeThrow struct {
187188
type EventGrenadeDetonate struct {
188189
Tick int `json:"tick"`
189190
Round int `json:"round,omitempty"`
191+
GrenadeID int `json:"gid,omitempty"`
190192
ThrowerSteamID string `json:"thrower,omitempty"`
191193
Type string `json:"type"`
192194
X float32 `json:"x,omitempty"`
@@ -204,15 +206,15 @@ type PlayerInfo struct {
204206
}
205207

206208
type Result struct {
207-
TotalTicks int `json:"total_ticks"`
208-
TickRate float64 `json:"tick_rate"`
209-
MapName string `json:"map_name"`
210-
WorkshopID string `json:"workshop_id,omitempty"`
209+
TotalTicks int `json:"total_ticks"`
210+
TickRate float64 `json:"tick_rate"`
211+
MapName string `json:"map_name"`
212+
WorkshopID string `json:"workshop_id,omitempty"`
211213
// Game-rule signals used by the importer to classify the match type.
212-
ServerName string `json:"server_name,omitempty"`
213-
MaxRounds int `json:"max_rounds,omitempty"`
214-
OvertimeEnabled bool `json:"overtime_enabled,omitempty"`
215-
PlayerCount int `json:"player_count,omitempty"`
214+
ServerName string `json:"server_name,omitempty"`
215+
MaxRounds int `json:"max_rounds,omitempty"`
216+
OvertimeEnabled bool `json:"overtime_enabled,omitempty"`
217+
PlayerCount int `json:"player_count,omitempty"`
216218
RoundTicks []RoundTick `json:"round_ticks"`
217219
Kills []EventKill `json:"kills"`
218220
Bombs []EventBomb `json:"bombs"`

0 commit comments

Comments
 (0)