Repository: markus-wa/demoinfocs-golang
Pull request #161 (2019-11-20 16:29:10)

Track a grenade from the moment a player throws (GrenadeProjectile) i…
…t by the moment it does damage actions (PlayerHurt/Kill) to the moment where the grenade effect is ended (ProjectileDestroyed/GrenadeEvent).

For issue #160 

deepcode-ci-bot[bot]: [**DeepCode Report (#169a15)**](https://www.deepcode.ai/app/gh/markus-wa/demoinfocs-golang/b06cd5bd2f294a149981fcb4352485523167ec1a/markus-wa/demoinfocs-golang/169a15bbef955fb91855d24de6fcd9c3c96dadfc/_/%2F/code/)
---
DeepCode analyzed this pull request.
There are no new issues. 
markus-wa: Just a quick first note. there seems to be a compilation error, see: https://travis-ci.org/markus-wa/demoinfocs-golang/jobs/614612888#L312
codeclimate[bot]: Code Climate has analyzed commit 526670f9 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

xavier-rodet: Should be fixed!
markus-wa: also there's still a compilation error: https://travis-ci.org/markus-wa/demoinfocs-golang/jobs/614654396#L312

I suggest you run at least `bin/unit-tests.sh` and/or `bin/regression-tests.sh`, see [testing](https://github.com/markus-wa/demoinfocs-golang#testing).
(Ideally you could run `bin/ci.sh` but it requires some dependencies)
codeclimate[bot]: Code Climate has analyzed commit fd37ac2e and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit b25e9c5d and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 321d15df and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 870dfe19 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 583f7895 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

xavier-rodet: Thanks for the testing scripts btw it's really handfull, i'm fixing my code right now (as it's clearly broken :p).

Then i'll handle your last remaining request !
codeclimate[bot]: Code Climate has analyzed commit da5a852e and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit b5d23d3c and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit bce956f6 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 6ea0cc6f and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit a0c5e7e5 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 4327edd6 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 9ce15b32 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit e70d746e and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 7130eb8e and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

markus-wa: > Thanks for the testing scripts btw it's really handfull, i'm fixing my code right now (as it's clearly broken :p).

> Then i'll handle your last remaining request !

Cool

Ping me whenever you think the PR is ready and Travis is happy (or if you need help identifying an issue in Travis or something).
codeclimate[bot]: Code Climate has analyzed commit 7288c244 and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 2431f867 and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 1f3e19f9 and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit d4796aac and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 56e23362 and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit c1239257 and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

markus-wa: > So i guess Inferno.Owner() represent the Thrower and not the Owner of the grenade in reality ?

You're right, originally this name comes from the entity property `m_hOwnerEntity`. But it's a bit misleading as you correctly mentioned. So I agree, we should deprecate `Owner()` and replace it with `Thrower()`.
I highly doubt we'll be able to return the original owner of the grenade since that's proably at least two steps too far into the grenade lifecycle (buy -> drop -> pickup -> weapon_fire -> throw / projectile -> explode -> fire)

But let's do this in another PR, this one is already big enough :)
xavier-rodet: Alright i'll commit that !

Yeah about handling a `Owner()` it was just a thought, that's why I talked about "future".
I'm not even sure anyone would really need this ;)
codeclimate[bot]: Code Climate has analyzed commit d099111f and detected **2 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Duplication | 2 |


View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

markus-wa: I had actually meant to also leave out the deprecation of Owner and do this in a separate PR.

But it's ok. However, could you change `Owner()` to just call `Thrower()` instead of duplicating the code?
codeclimate[bot]: Code Climate has analyzed commit 41bf781b and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 4d47b5e1 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 01c4aaa0 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

xavier-rodet: Well for my purpose i'm pretty sure it's working fine :

1. I start "tracking" the weapon instance of the grenade when it get thrown by a player
2. `UniqueID()` will match when a player got hurt or killed by this grenade
3. I delete the tracking when the grenade is destroyed (or inferno is ended)

However I do agree that : 
- There is no matching before the grenade is thrown like : weapon picked at buytime or any events which happens before throwing it (so we could improve this system to start when a grenade is really "initialized" into the world, so at buytime)
- This system is only working for Grenades and could be extended for any kind of weapons

But this will make this PR way bigger i think :p 
codeclimate[bot]: Code Climate has analyzed commit 528616c4 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 6bad4968 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

markus-wa: I've filed a PR against your branch that cleans up some things, fixes conflicts and adds another fix which is actually needed for this feature (c836d1f254ff1ae3efc4e4aa57443a22929e7fb9)

I've also added some checks with  [`unassert`](https://github.com/markus-wa/go-unassert) and added some docs with things i figured out by using it - like the fact that we don't know what player threw an inferno in most contexts (inferno_startburn and inferno_expire for instance)
codeclimate[bot]: Code Climate has analyzed commit 95e38afa and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

codeclimate[bot]: Code Climate has analyzed commit 2e024cf0 and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

markus-wa: very sorry for breaking `golangci-lint` again after all your work 😅

unfortunately I don't have permissions to push to your branch, else I'd fix it.
codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=h1) Report
> Merging [#161](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/af64f5184545ed850dbde91de65580d2850e40da?src=pr&el=desc) will **increase** coverage by `0.23%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/graphs/tree.svg?width=650&token=20emCaZRHz&height=150&src=pr)](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #161      +/-   ##
==========================================
+ Coverage   91.52%   91.76%   +0.23%     
==========================================
  Files          29       29              
  Lines        2524     2573      +49     
==========================================
+ Hits         2310     2361      +51     
+ Misses        161      159       -2     
  Partials       53       53
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [events/events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-ZXZlbnRzL2V2ZW50cy5nbw==) | `93.33% <ø> (ø)` | :arrow_up: |
| [common/common.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-Y29tbW9uL2NvbW1vbi5nbw==) | `82.6% <ø> (ø)` | :arrow_up: |
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.92% <100%> (+0.02%)` | :arrow_up: |
| [common/inferno.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-Y29tbW9uL2luZmVybm8uZ28=) | `88.88% <100%> (+0.36%)` | :arrow_up: |
| [game\_events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-Z2FtZV9ldmVudHMuZ28=) | `96.66% <100%> (+0.29%)` | :arrow_up: |
| [game\_state.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-Z2FtZV9zdGF0ZS5nbw==) | `88.46% <100%> (+0.11%)` | :arrow_up: |
| [parser.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161/diff?src=pr&el=tree#diff-cGFyc2VyLmdv) | `88.73% <0%> (+2.81%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=footer). Last update [af64f51...a6ac9c4](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/161?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codeclimate[bot]: Code Climate has analyzed commit a6ac9c4e and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

xavier-rodet: How dare you confront me with golangcibot again after all the conflicts we have been through :p
codeclimate[bot]: Code Climate has analyzed commit e0d30dde and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/markus-wa/demoinfocs-golang/pull/161).

xavier-rodet: Seems great to me!

I should be able to do the tests this weekend 👍 

Your lib is really important to me, and you're being really reactive which I appreciate a lot, so thank you!
Hopefully it'll be easier for me to make new PR in the future to fit my particular needs ;) (even if i'm really not a big fan of Golang haha)
markus-wa: > Your lib is really important to me, and you're being really reactive which I appreciate a lot, so thank you!

Thanks, that's great to hear 😄 

> even if i'm really not a big fan of Golang haha

haha, yeah that's understandable ^^
xavier-rodet: Hey,

Great news it's working perfectly 👍 

The weapon `UniqueID()` is always the same (for a same grenade) from `GrenadeProjectileThrow`, to `PlayerHurt` and `Kill` events.

I tested it for HE, Molotov & incendiary damages/kills!

\o/

Note: I also created regression tests for my application with 2 demos, I compared endgame scoreboards for utility damage with what I extract from `PlayerHurt` and the number are always equal!
markus-wa: Awesome! Thanks for verifying.

Patch:
diff --git a/common/common.go b/common/common.go
index c278f96..08d0ed0 100644
--- a/common/common.go
+++ b/common/common.go
@@ -64,12 +64,16 @@ func (h DemoHeader) TickTime() time.Duration {
 // GrenadeProjectile is a grenade thrown intentionally by a player. It is used to track grenade projectile
 // positions between the time at which they are thrown and until they detonate.
 type GrenadeProjectile struct {
-	EntityID   int
-	Weapon     EquipmentElement
-	Thrower    *Player // Always seems to be the same as Owner, even if the grenade was picked up
-	Owner      *Player // Always seems to be the same as Thrower, even if the grenade was picked up
-	Position   r3.Vector
-	Trajectory []r3.Vector // List of all known locations of the grenade up to the current point
+	EntityID int
+	// Deprecated: Weapon exists for historical compatibility
+	// and should not be used. To access the weapon corresponding to his GrenadeProjectile,
+	// use the WeaponInstance.Weapon instead.
+	Weapon         EquipmentElement
+	WeaponInstance *Equipment
+	Thrower        *Player // Always seems to be the same as Owner, even if the grenade was picked up
+	Owner          *Player // Always seems to be the same as Thrower, even if the grenade was picked up
+	Position       r3.Vector
+	Trajectory     []r3.Vector // List of all known locations of the grenade up to the current point
 
 	// uniqueID is used to distinguish different grenades (which potentially have the same, reused entityID) from each other.
 	uniqueID int64
diff --git a/common/inferno.go b/common/inferno.go
index 0e3e4b3..3129625 100644
--- a/common/inferno.go
+++ b/common/inferno.go
@@ -139,7 +139,16 @@ func (inf Inferno) ConvexHull3D() quickhull.ConvexHull {
 
 // Owner returns the player who threw the fire grenade.
 // Could be nil if the player disconnected after throwing it.
+//
+// Deprecated: Owner() exists for historical compatibility
+// and should not be used. Use Thrower() instead.
 func (inf Inferno) Owner() *Player {
+	return inf.Thrower()
+}
+
+// Thrower returns the player who threw the fire grenade.
+// Could be nil if the player disconnected after throwing it.
+func (inf Inferno) Thrower() *Player {
 	return inf.demoInfoProvider.FindPlayerByHandle(inf.Entity.FindPropertyI("m_hOwnerEntity").Value().IntVal)
 }
 
diff --git a/datatables.go b/datatables.go
index 276a512..b424764 100644
--- a/datatables.go
+++ b/datatables.go
@@ -376,6 +376,8 @@ func (p *Parser) bindGrenadeProjectiles(entity *st.Entity) {
 	p.gameState.grenadeProjectiles[entityID] = proj
 
 	entity.OnCreateFinished(func() {
+		p.gameEventHandler.addThrownGrenade(proj.Thrower, proj.WeaponInstance)
+
 		p.eventDispatcher.Dispatch(events.GrenadeProjectileThrow{
 			Projectile: proj,
 		})
@@ -387,6 +389,9 @@ func (p *Parser) bindGrenadeProjectiles(entity *st.Entity) {
 
 	entity.FindPropertyI("m_nModelIndex").OnUpdate(func(val st.PropertyValue) {
 		proj.Weapon = p.grenadeModelIndices[val.IntVal]
+
+		equipment := common.NewEquipment(p.grenadeModelIndices[val.IntVal])
+		proj.WeaponInstance = &equipment
 	})
 
 	// @micvbang: not quite sure what the difference between Thrower and Owner is.
@@ -432,9 +437,18 @@ func (p *Parser) nadeProjectileDestroyed(proj *common.GrenadeProjectile) {
 
 	delete(p.gameState.grenadeProjectiles, proj.EntityID)
 
-	if proj.Weapon == common.EqFlash {
+	if proj.WeaponInstance.Weapon == common.EqFlash {
 		p.gameState.lastFlash.projectileByPlayer[proj.Owner] = proj
 	}
+
+	// We delete from the Owner.ThrownGrenades (only if not inferno or smoke, because they will be deleted when they expire)
+	isInferno := proj.WeaponInstance.Weapon == common.EqMolotov || proj.WeaponInstance.Weapon == common.EqIncendiary
+	isSmoke := proj.WeaponInstance.Weapon == common.EqSmoke
+	isDecoy := proj.WeaponInstance.Weapon == common.EqDecoy
+
+	if !isInferno && !isSmoke && !isDecoy {
+		p.gameEventHandler.deleteThrownGrenade(proj.Thrower, proj.WeaponInstance.Weapon)
+	}
 }
 
 func (p *Parser) bindWeapon(entity *st.Entity, wepType common.EquipmentElement) {
@@ -539,6 +553,8 @@ func (p *Parser) infernoExpired(inf *common.Inferno) {
 	})
 
 	delete(p.gameState.infernos, inf.EntityID)
+
+	p.gameEventHandler.deleteThrownGrenade(inf.Thrower(), common.EqIncendiary)
 }
 
 func (p *Parser) bindGameRules() {
diff --git a/events/events.go b/events/events.go
index f519191..03ff427 100644
--- a/events/events.go
+++ b/events/events.go
@@ -172,6 +172,7 @@ type GrenadeEventIf interface {
 // handlers on this tho, you want GrenadeEventIf for that
 type GrenadeEvent struct {
 	GrenadeType     common.EquipmentElement
+	Grenade         *common.Equipment // Maybe nil for InfernoStart & InfernoExpired since we don't know the thrower (at least in old demos)
 	Position        r3.Vector
 	Thrower         *common.Player
 	GrenadeEntityID int
diff --git a/game_events.go b/game_events.go
index 1ef00cf..4492d59 100644
--- a/game_events.go
+++ b/game_events.go
@@ -238,6 +238,9 @@ func (geh gameEventHandler) roundOfficiallyEnded(data map[string]*msg.CSVCMsg_Ga
 		geh.parser.infernoExpired(inf)
 	}
 
+	// Thrown grenades could not be deleted at the end of the round (if they are thrown at the very end, they never get destroyed)
+	geh.gameState().thrownGrenades = make(map[*common.Player][]*common.Equipment)
+
 	geh.dispatch(events.RoundEndOfficial{})
 }
 
@@ -307,7 +310,7 @@ func (geh gameEventHandler) playerDeath(data map[string]*msg.CSVCMsg_GameEventKe
 		Assister:          geh.playerByUserID32(data["assister"].GetValShort()),
 		IsHeadshot:        data["headshot"].GetValBool(),
 		PenetratedObjects: int(data["penetrated"].GetValShort()),
-		Weapon:            getPlayerWeapon(killer, wepType),
+		Weapon:            geh.getEquipmentInstance(killer, wepType),
 	})
 }
 
@@ -323,7 +326,7 @@ func (geh gameEventHandler) playerHurt(data map[string]*msg.CSVCMsg_GameEventKey
 		HealthDamage: int(data["dmg_health"].GetValShort()),
 		ArmorDamage:  int(data["dmg_armor"].GetValByte()),
 		HitGroup:     events.HitGroup(data["hitgroup"].GetValByte()),
-		Weapon:       getPlayerWeapon(attacker, wepType),
+		Weapon:       geh.getEquipmentInstance(attacker, wepType),
 	})
 }
 
@@ -366,9 +369,12 @@ func (geh gameEventHandler) decoyStarted(data map[string]*msg.CSVCMsg_GameEventK
 }
 
 func (geh gameEventHandler) decoyDetonate(data map[string]*msg.CSVCMsg_GameEventKeyT) {
+	event := geh.nadeEvent(data, common.EqDecoy)
 	geh.dispatch(events.DecoyExpired{
-		GrenadeEvent: geh.nadeEvent(data, common.EqDecoy),
+		GrenadeEvent: event,
 	})
+
+	geh.deleteThrownGrenade(event.Thrower, common.EqDecoy)
 }
 
 func (geh gameEventHandler) smokeGrenadeDetonate(data map[string]*msg.CSVCMsg_GameEventKeyT) {
@@ -378,9 +384,12 @@ func (geh gameEventHandler) smokeGrenadeDetonate(data map[string]*msg.CSVCMsg_Ga
 }
 
 func (geh gameEventHandler) smokeGrenadeExpired(data map[string]*msg.CSVCMsg_GameEventKeyT) {
+	event := geh.nadeEvent(data, common.EqSmoke)
 	geh.dispatch(events.SmokeExpired{
-		GrenadeEvent: geh.nadeEvent(data, common.EqSmoke),
+		GrenadeEvent: event,
 	})
+
+	geh.deleteThrownGrenade(event.Thrower, common.EqSmoke)
 }
 
 func (geh gameEventHandler) infernoStartBurn(data map[string]*msg.CSVCMsg_GameEventKeyT) {
@@ -583,18 +592,80 @@ func (geh gameEventHandler) nadeEvent(data map[string]*msg.CSVCMsg_GameEventKeyT
 
 	return events.GrenadeEvent{
 		GrenadeType:     nadeType,
+		Grenade:         geh.getThrownGrenade(thrower, nadeType),
 		Thrower:         thrower,
 		Position:        position,
 		GrenadeEntityID: nadeEntityID,
 	}
 }
 
-func mapGameEventData(d *msg.CSVCMsg_GameEventListDescriptorT, e *msg.CSVCMsg_GameEvent) map[string]*msg.CSVCMsg_GameEventKeyT {
-	data := make(map[string]*msg.CSVCMsg_GameEventKeyT)
-	for i, k := range d.Keys {
-		data[k.Name] = e.Keys[i]
+func (geh gameEventHandler) addThrownGrenade(p *common.Player, wep *common.Equipment) {
+	if p == nil {
+		// can happen for "unknown" players (see #162)
+		return
 	}
-	return data
+
+	gameState := geh.gameState()
+	gameState.thrownGrenades[p] = append(gameState.thrownGrenades[p], wep)
+}
+
+func (geh gameEventHandler) getThrownGrenade(p *common.Player, wepType common.EquipmentElement) *common.Equipment {
+	if p == nil {
+		// can happen for incendiaries or "unknown" players (see #162)
+		return nil
+	}
+
+	// Get the first weapon we found for this player with this weapon type
+	for _, thrownGrenade := range geh.gameState().thrownGrenades[p] {
+		if isSameEquipmentElement(thrownGrenade.Weapon, wepType) {
+			return thrownGrenade
+		}
+	}
+
+	// smokes might have duplicate smokegrenade_expired events, so it could have already been deleted.
+	// if it's not a smoke this should never be reached
+	unassert.Samef(wepType, common.EqSmoke, "tried to get non-existing grenade from gameState.thrownGrenades")
+
+	return nil
+}
+
+func (geh gameEventHandler) deleteThrownGrenade(p *common.Player, wepType common.EquipmentElement) {
+	if p == nil {
+		// can happen for incendiaries or "unknown" players (see #162)
+		return
+	}
+
+	gameState := geh.gameState()
+
+	// Delete the first weapon we found with this weapon type
+	for i, weapon := range gameState.thrownGrenades[p] {
+		// If same weapon type
+		// OR if it's an EqIncendiary we must check for EqMolotov too because of geh.infernoExpire() handling ?
+		if isSameEquipmentElement(wepType, weapon.Weapon) {
+			gameState.thrownGrenades[p] = append(gameState.thrownGrenades[p][:i], gameState.thrownGrenades[p][i+1:]...)
+			return
+		}
+	}
+
+	// smokes might have duplicate smokegrenade_expired events, so it might already be deleted.
+	// besides that this code should never be reached
+	unassert.Samef(wepType, common.EqSmoke, "trying to delete non-existing grenade from gameState.thrownGrenades")
+}
+
+func (geh gameEventHandler) getEquipmentInstance(player *common.Player, wepType common.EquipmentElement) *common.Equipment {
+	isGrenade := wepType.Class() == common.EqClassGrenade
+	if isGrenade {
+		return geh.getThrownGrenade(player, wepType)
+	}
+
+	return getPlayerWeapon(player, wepType)
+}
+
+// checks if two EquipmentElements are the same, considering that incendiary and molotov should be treated as identical
+func isSameEquipmentElement(a common.EquipmentElement, b common.EquipmentElement) bool {
+	return a == b ||
+		(a == common.EqIncendiary && b == common.EqMolotov) ||
+		(b == common.EqIncendiary && a == common.EqMolotov)
 }
 
 // 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
 	return &wep
 }
 
+func mapGameEventData(d *msg.CSVCMsg_GameEventListDescriptorT, e *msg.CSVCMsg_GameEvent) map[string]*msg.CSVCMsg_GameEventKeyT {
+	data := make(map[string]*msg.CSVCMsg_GameEventKeyT)
+	for i, k := range d.Keys {
+		data[k.Name] = e.Keys[i]
+	}
+
+	return data
+}
+
 // We're all better off not asking questions
 const valveMagicNumber = 76561197960265728
 
diff --git a/game_state.go b/game_state.go
index 11b77d9..758c8d4 100644
--- a/game_state.go
+++ b/game_state.go
@@ -24,9 +24,10 @@ type GameState struct {
 	gamePhase          common.GamePhase
 	isWarmupPeriod     bool
 	isMatchStarted     bool
-	lastFlash          lastFlash      // Information about the last flash that exploded, used to find the attacker and projectile for player_blind events
-	currentDefuser     *common.Player // Player currently defusing the bomb, if any
-	currentPlanter     *common.Player // Player currently planting the bomb, if any
+	lastFlash          lastFlash                              // Information about the last flash that exploded, used to find the attacker and projectile for player_blind events
+	currentDefuser     *common.Player                         // Player currently defusing the bomb, if any
+	currentPlanter     *common.Player                         // Player currently planting the bomb, if any
+	thrownGrenades     map[*common.Player][]*common.Equipment // Information about every player's thrown grenades (from the moment they are thrown to the moment their effect is ended)
 }
 
 type lastFlash struct {
@@ -143,6 +144,7 @@ func newGameState() *GameState {
 		infernos:           make(map[int]*common.Inferno),
 		entities:           make(map[int]*st.Entity),
 		conVars:            make(map[string]string),
+		thrownGrenades:     make(map[*common.Player][]*common.Equipment),
 		lastFlash: lastFlash{
 			projectileByPlayer: make(map[*common.Player]*common.GrenadeProjectile),
 		},


Test patch:
diff --git a/common/inferno_test.go b/common/inferno_test.go
index a160bb9..67109e3 100644
--- a/common/inferno_test.go
+++ b/common/inferno_test.go
@@ -120,3 +120,14 @@ func TestInferno_Owner(t *testing.T) {
 
 	assert.Equal(t, player, NewInferno(provider, entity).Owner())
 }
+
+func TestInferno_Thrower(t *testing.T) {
+	entity := entityWithProperty("m_hOwnerEntity", st.PropertyValue{IntVal: 1})
+
+	player := new(Player)
+	provider := demoInfoProviderMock{
+		playersByHandle: map[int]*Player{1: player},
+	}
+
+	assert.Equal(t, player, NewInferno(provider, entity).Thrower())
+}
diff --git a/game_events_test.go b/game_events_test.go
index c32aa6e..6f5823e 100644
--- a/game_events_test.go
+++ b/game_events_test.go
@@ -86,3 +86,129 @@ func TestGetPlayerWeapon_NotFound(t *testing.T) {
 
 	assert.Equal(t, common.EqM4A1, wep.Weapon)
 }
+
+func TestAddThrownGrenade_NilPlayer(t *testing.T) {
+	p := NewParser(rand.Reader)
+	he := common.NewEquipment(common.EqHE)
+
+	assert.Empty(t, p.gameState.thrownGrenades)
+
+	p.gameEventHandler.addThrownGrenade(nil, &he)
+
+	assert.Empty(t, p.gameState.thrownGrenades)
+}
+
+func TestAddThrownGrenade(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+	he := common.NewEquipment(common.EqHE)
+
+	assert.Empty(t, p.gameState.thrownGrenades)
+
+	p.gameEventHandler.addThrownGrenade(pl, &he)
+
+	assert.NotEmpty(t, p.gameState.thrownGrenades)
+	assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
+	assert.Equal(t, p.gameState.thrownGrenades[pl][0], &he)
+}
+
+func TestGetThrownGrenade_NilPlayer(t *testing.T) {
+	p := NewParser(rand.Reader)
+	he := common.NewEquipment(common.EqHE)
+
+	wep := p.gameEventHandler.getThrownGrenade(nil, he.Weapon)
+
+	assert.Nil(t, wep)
+}
+
+func TestGetThrownGrenade_NotFound(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+
+	he := common.NewEquipment(common.EqSmoke)
+
+	wep := p.gameEventHandler.getThrownGrenade(pl, he.Weapon)
+
+	assert.Nil(t, wep)
+}
+
+func TestGetThrownGrenade_Found(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+	he := common.NewEquipment(common.EqHE)
+
+	p.gameEventHandler.addThrownGrenade(pl, &he)
+	wep := p.gameEventHandler.getThrownGrenade(pl, he.Weapon)
+
+	assert.Equal(t, wep.Weapon, he.Weapon)
+	assert.Equal(t, wep, &he)
+}
+
+func TestDeleteThrownGrenade_NilPlayer(t *testing.T) {
+	p := NewParser(rand.Reader)
+	he := common.NewEquipment(common.EqHE)
+
+	// Do nothing, we just keep sure it doesn't crash
+	p.gameEventHandler.deleteThrownGrenade(nil, he.Weapon)
+}
+
+func TestDeleteThrownGrenade_NotFound(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+	he := common.NewEquipment(common.EqHE)
+
+	assert.Empty(t, p.gameState.thrownGrenades)
+
+	p.gameEventHandler.addThrownGrenade(pl, &he)
+
+	assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
+
+	p.gameEventHandler.deleteThrownGrenade(pl, common.EqSmoke)
+
+	assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
+}
+
+func TestDeleteThrownGrenade_Found(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+	he := common.NewEquipment(common.EqHE)
+
+	assert.Empty(t, p.gameState.thrownGrenades)
+
+	p.gameEventHandler.addThrownGrenade(pl, &he)
+
+	assert.NotEmpty(t, p.gameState.thrownGrenades[pl])
+
+	p.gameEventHandler.deleteThrownGrenade(pl, he.Weapon)
+
+	assert.Empty(t, p.gameState.thrownGrenades[pl])
+}
+
+func TestGetEquipmentInstance_NotGrenade(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+
+	wep := p.gameEventHandler.getEquipmentInstance(pl, common.EqAK47)
+
+	assert.Equal(t, common.EqAK47, wep.Weapon)
+}
+
+func TestGetEquipmentInstance_Grenade_NotThrown(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+
+	wep := p.gameEventHandler.getEquipmentInstance(pl, common.EqSmoke)
+
+	assert.Nil(t, wep)
+}
+
+func TestGetEquipmentInstance_Grenade_Thrown(t *testing.T) {
+	p := NewParser(rand.Reader)
+	pl := &common.Player{}
+	he := common.NewEquipment(common.EqHE)
+
+	p.gameEventHandler.addThrownGrenade(pl, &he)
+	wep := p.gameEventHandler.getEquipmentInstance(pl, he.Weapon)
+
+	assert.Equal(t, &he, wep)
+}