Repository: markus-wa/demoinfocs-golang
Pull request #96 (2019-03-02 21:14:39)

common: fix Player.IsBlinded() inaccuracies
This should fix ~~any~~ most issues with `Player.IsBlinded()`.
It may still return wrong values if the demo's header is lacking the tickrate.

It also adds another utility function `Player.FlashDurationTimeRemaining()` which returns the remaining duration of the flash. This may be useful as the player may be able to partially see if the remaining time is fairly low.


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

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

markus-wa: @quancore could you verify if this fixes your false-positive flash-assists. Note that it does not attempt to fix anything else related to flashes, only false-positive assists.
quancore: I have checked and it seems that it solves false positive blinded situations. However, the false negative blinded situation still exists and cause wrong stats.
markus-wa: Good to hear.

Again, if you want me to solve false negatives you'll need to provide me a specific demo + tick where there should be a flash assist, otherwise there's nothing I can do.
markus-wa: Just an update: I'm not quite happy with this yet.

First off, what we're doing here is sort of hacky. Second (the real deal breaker), it doesn't always work as we've seen - it breaks when the tick rate is missing from the demo header.

@micvbang have you got an idea how we could fix this nicely?
codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/cef132c0e2da16a7758f43cda58efd8d4c80bb8b?src=pr&el=desc) will **decrease** coverage by `0.29%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff            @@
##           master      #96     +/-   ##
=========================================
- Coverage   90.05%   89.76%   -0.3%     
=========================================
  Files          31       31             
  Lines        2353     2384     +31     
=========================================
+ Hits         2119     2140     +21     
- Misses        184      194     +10     
  Partials       50       50
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [game\_state.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9zdGF0ZS5nbw==) | `73.68% <0%> (-13.16%)` | :arrow_down: |
| [game\_events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9ldmVudHMuZ28=) | `94.43% <0%> (+0.12%)` | :arrow_up: |
| [parsing.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-cGFyc2luZy5nbw==) | `88.23% <0%> (+0.4%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [cef132c...9e40477](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/cef132c0e2da16a7758f43cda58efd8d4c80bb8b?src=pr&el=desc) will **decrease** coverage by `0.29%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff            @@
##           master      #96     +/-   ##
=========================================
- Coverage   90.05%   89.76%   -0.3%     
=========================================
  Files          31       31             
  Lines        2353     2384     +31     
=========================================
+ Hits         2119     2140     +21     
- Misses        184      194     +10     
  Partials       50       50
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [game\_state.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9zdGF0ZS5nbw==) | `73.68% <0%> (-13.16%)` | :arrow_down: |
| [game\_events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9ldmVudHMuZ28=) | `94.43% <0%> (+0.12%)` | :arrow_up: |
| [parsing.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-cGFyc2luZy5nbw==) | `88.23% <0%> (+0.4%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [cef132c...9e40477](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

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

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

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

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

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

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

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/a3164c51d27d8f6709140d21d551f801290e9593?src=pr&el=desc) will **increase** coverage by `0.08%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master      #96      +/-   ##
==========================================
+ Coverage   90.04%   90.13%   +0.08%     
==========================================
  Files          31       31              
  Lines        2351     2371      +20     
==========================================
+ Hits         2117     2137      +20     
  Misses        184      184              
  Partials       50       50
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [game\_state.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9zdGF0ZS5nbw==) | `86.84% <0%> (ø)` | :arrow_up: |
| [sendtables/entity.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-c2VuZHRhYmxlcy9lbnRpdHkuZ28=) | `93.03% <0%> (+0.04%)` | :arrow_up: |
| [sendtables/sendtables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-c2VuZHRhYmxlcy9zZW5kdGFibGVzLmdv) | `96.55% <0%> (+0.06%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [a3164c5...b2964cc](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/a3164c51d27d8f6709140d21d551f801290e9593?src=pr&el=desc) will **increase** coverage by `0.08%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master      #96      +/-   ##
==========================================
+ Coverage   90.04%   90.13%   +0.08%     
==========================================
  Files          31       31              
  Lines        2351     2371      +20     
==========================================
+ Hits         2117     2137      +20     
  Misses        184      184              
  Partials       50       50
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [game\_state.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Z2FtZV9zdGF0ZS5nbw==) | `86.84% <0%> (ø)` | :arrow_up: |
| [sendtables/entity.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-c2VuZHRhYmxlcy9lbnRpdHkuZ28=) | `93.03% <0%> (+0.04%)` | :arrow_up: |
| [sendtables/sendtables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-c2VuZHRhYmxlcy9zZW5kdGFibGVzLmdv) | `96.55% <0%> (+0.06%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [a3164c5...b2964cc](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

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

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

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/a3164c51d27d8f6709140d21d551f801290e9593?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `92%`.

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

```diff
@@            Coverage Diff             @@
##           master      #96      +/-   ##
==========================================
- Coverage   90.04%   90.04%   -0.01%     
==========================================
  Files          31       31              
  Lines        2351     2371      +20     
==========================================
+ Hits         2117     2135      +18     
- Misses        184      185       +1     
- Partials       50       51       +1
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `93.33% <88.88%> (-6.67%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [a3164c5...3c28cd0](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=h1) Report
> Merging [#96](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=desc) into [master](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/a3164c51d27d8f6709140d21d551f801290e9593?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `92%`.

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

```diff
@@            Coverage Diff             @@
##           master      #96      +/-   ##
==========================================
- Coverage   90.04%   90.04%   -0.01%     
==========================================
  Files          31       31              
  Lines        2351     2371      +20     
==========================================
+ Hits         2117     2135      +18     
- Misses        184      185       +1     
- Partials       50       51       +1
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-ZGF0YXRhYmxlcy5nbw==) | `98.76% <100%> (+0.01%)` | :arrow_up: |
| [common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96/diff?src=pr&el=tree#diff-Y29tbW9uL3BsYXllci5nbw==) | `93.33% <88.88%> (-6.67%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?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/96?src=pr&el=footer). Last update [a3164c5...3c28cd0](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/96?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

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

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

markus-wa: I've changed it so that if `DemoHeader.TickRate() == 0` then a fallback to `FlashDuration` is used (behaves as it is right now).

Hopefully in the future we'll find a way to get the tick rate for the demos where it's missing now so we can ditch the fallback.

@quancore - let me know if this does the trick
quancore: > I've changed it so that if `DemoHeader.TickRate() == 0` then a fallback to `FlashDuration` is used (behaves as it is right now).
> 
> Hopefully in the future we'll find a way to get the tick rate for the demos where it's missing now so we can ditch the fallback.
> 
> @quancore - let me know if this does the trick

Right now, I have no demo file with missing tick rate to check it. If you are okay for you, it is okay.
codeclimate[bot]: Code Climate has analyzed commit 5d33ac04 and detected **0 issues** on this pull request.

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

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

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


Patch:
diff --git a/common/player.go b/common/player.go
index d5fa8c5..32b5e2d 100644
--- a/common/player.go
+++ b/common/player.go
@@ -30,6 +30,7 @@ type Player struct {
 	ViewDirectionX              float32
 	ViewDirectionY              float32
 	FlashDuration               float32 // Blindness duration from the flashbang currently affecting the player (seconds)
+	FlashTick                   int     // In-game tick at which the player was last flashed
 	Team                        Team
 	TeamState                   *TeamState // When keeping the reference make sure you notice when the player changes teams
 	IsBot                       bool
@@ -37,6 +38,9 @@ type Player struct {
 	IsDefusing                  bool
 	HasDefuseKit                bool
 	HasHelmet                   bool
+
+	tickRate           float64            // the in-game tick rate, used for IsBlinded()
+	ingameTickProvider ingameTickProvider // provider for the current in-game tick, used for IsBlinded()
 }
 
 // IsAlive returns true if the Hp of the player are > 0.
@@ -44,16 +48,42 @@ func (p *Player) IsAlive() bool {
 	return p.Hp > 0
 }
 
-// IsBlinded returns true if the player is currently flashed (FlashDuration > 0).
+// IsBlinded returns true if the player is currently flashed.
+// This is more accurate than 'FlashDuration != 0' as it also takes into account FlashTick, DemoHeader.TickRate() and GameState.IngameTick().
 func (p *Player) IsBlinded() bool {
-	return p.FlashDuration > 0
+	return p.FlashDurationTimeRemaining() > 0
 }
 
 // FlashDurationTime returns the duration of the blinding effect as time.Duration instead of float32 in seconds.
+// Will return 0 if IsBlinded() returns false.
 func (p *Player) FlashDurationTime() time.Duration {
+	if !p.IsBlinded() {
+		return time.Duration(0)
+	}
+	return p.flashDurationTimeFull()
+}
+
+func (p *Player) flashDurationTimeFull() time.Duration {
 	return time.Duration(float32(time.Second) * p.FlashDuration)
 }
 
+// FlashDurationTimeRemaining returns the remaining duration of the blinding effect (or 0 if the player is not currently blinded).
+// It takes into consideration FlashDuration, FlashTick, DemoHeader.TickRate() and GameState.IngameTick().
+func (p *Player) FlashDurationTimeRemaining() time.Duration {
+	// In case the demo header is broken
+	// TODO: read tickRate from CVARs as fallback
+	if p.tickRate == 0 {
+		return time.Duration(p.FlashDuration) * time.Second
+	}
+
+	timeSinceFlash := time.Duration(float64(p.ingameTickProvider()-p.FlashTick) / p.tickRate * float64(time.Second))
+	remaining := p.flashDurationTimeFull() - timeSinceFlash
+	if remaining < 0 {
+		return 0
+	}
+	return remaining
+}
+
 /*
 Some interesting data regarding flashes.
 
@@ -95,9 +125,16 @@ type AdditionalPlayerInformation struct {
 	TotalCashSpent int
 }
 
+// ingameTickProvider is a function that returns the current ingame tick of the demo related to a player.
+type ingameTickProvider func() int
+
 // NewPlayer creates a *Player with an initialized equipment map.
 //
 // Intended for internal use only.
-func NewPlayer() *Player {
-	return &Player{RawWeapons: make(map[int]*Equipment)}
+func NewPlayer(tickRate float64, ingameTickProvider ingameTickProvider) *Player {
+	return &Player{
+		RawWeapons:         make(map[int]*Equipment),
+		tickRate:           tickRate,
+		ingameTickProvider: ingameTickProvider,
+	}
 }
diff --git a/datatables.go b/datatables.go
index 6b738a9..02a5948 100644
--- a/datatables.go
+++ b/datatables.go
@@ -190,7 +190,8 @@ func (p *Parser) bindNewPlayer(playerEntity st.IEntity) {
 		if pl == nil {
 			isNew = true
 
-			pl = common.NewPlayer()
+			// TODO: read tickRate from CVARs as fallback
+			pl = common.NewPlayer(p.header.TickRate(), p.gameState.IngameTick)
 			pl.Name = rp.name
 			pl.SteamID = rp.xuid
 			pl.IsBot = rp.isFakePlayer || rp.guid == "BOT"
@@ -231,7 +232,14 @@ func (p *Parser) bindNewPlayer(playerEntity st.IEntity) {
 
 	playerEntity.BindProperty("m_angEyeAngles[1]", &pl.ViewDirectionX, st.ValTypeFloat32)
 	playerEntity.BindProperty("m_angEyeAngles[0]", &pl.ViewDirectionY, st.ValTypeFloat32)
-	playerEntity.BindProperty("m_flFlashDuration", &pl.FlashDuration, st.ValTypeFloat32)
+	playerEntity.FindProperty("m_flFlashDuration").OnUpdate(func(val st.PropertyValue) {
+		if val.FloatVal == 0 {
+			pl.FlashTick = 0
+		} else {
+			pl.FlashTick = p.gameState.ingameTick
+		}
+		pl.FlashDuration = val.FloatVal
+	})
 
 	// Velocity
 	playerEntity.BindProperty("localdata.m_vecVelocity[0]", &pl.Velocity.X, st.ValTypeFloat64)


Test patch:
diff --git a/common/player_test.go b/common/player_test.go
index c6e6854..02aac22 100644
--- a/common/player_test.go
+++ b/common/player_test.go
@@ -12,7 +12,7 @@ func TestPlayerActiveWeapon(t *testing.T) {
 	glock := NewEquipment(EqGlock)
 	ak47 := NewEquipment(EqAK47)
 
-	pl := NewPlayer()
+	pl := newPlayer(0)
 	pl.RawWeapons[1] = &knife
 	pl.RawWeapons[2] = &glock
 	pl.RawWeapons[3] = &ak47
@@ -20,12 +20,13 @@ func TestPlayerActiveWeapon(t *testing.T) {
 
 	assert.Equal(t, &ak47, pl.ActiveWeapon(), "Should have AK-47 equipped")
 }
+
 func TestPlayerWeapons(t *testing.T) {
 	knife := NewEquipment(EqKnife)
 	glock := NewEquipment(EqGlock)
 	ak47 := NewEquipment(EqAK47)
 
-	pl := NewPlayer()
+	pl := newPlayer(0)
 	pl.RawWeapons[1] = &knife
 	pl.RawWeapons[2] = &glock
 	pl.RawWeapons[3] = &ak47
@@ -35,7 +36,7 @@ func TestPlayerWeapons(t *testing.T) {
 }
 
 func TestPlayerAlive(t *testing.T) {
-	pl := NewPlayer()
+	pl := newPlayer(0)
 
 	pl.Hp = 100
 	assert.Equal(t, true, pl.IsAlive(), "Should be alive")
@@ -51,20 +52,79 @@ func TestPlayerAlive(t *testing.T) {
 }
 
 func TestPlayerFlashed(t *testing.T) {
-	pl := NewPlayer()
+	pl := newPlayer(128)
 
 	assert.False(t, pl.IsBlinded(), "Should not be flashed")
 
 	pl.FlashDuration = 2.3
+	pl.FlashTick = 50
 	assert.True(t, pl.IsBlinded(), "Should be flashed")
 }
 
+func TestPlayerFlashed_FlashDuration_Over(t *testing.T) {
+	pl := newPlayer(128 * 3)
+
+	pl.FlashDuration = 1.9
+	pl.FlashTick = 128
+	assert.False(t, pl.IsBlinded(), "Should not be flashed")
+}
+
+func TestPlayer_FlashDurationTime_Default(t *testing.T) {
+	pl := newPlayer(0)
+
+	assert.Equal(t, time.Duration(0), pl.FlashDurationTime())
+}
+
 func TestPlayer_FlashDurationTime(t *testing.T) {
-	p := NewPlayer()
+	pl := newPlayer(0)
+
+	pl.FlashDuration = 2.3
 
-	assert.Equal(t, time.Duration(0), p.FlashDurationTime())
+	assert.Equal(t, 2300*time.Millisecond, pl.FlashDurationTime())
+}
+
+func TestPlayer_FlashDurationTimeRemaining_Default(t *testing.T) {
+	pl := NewPlayer(0, tickProvider(128))
+
+	assert.Equal(t, time.Duration(0), pl.FlashDurationTimeRemaining())
+}
 
-	p.FlashDuration = 2.3
+func TestPlayer_FlashDurationTimeRemaining(t *testing.T) {
+	pl := newPlayer(128 * 2)
+
+	pl.FlashDuration = 3
+	pl.FlashTick = 128
+	assert.Equal(t, 2*time.Second, pl.FlashDurationTimeRemaining())
+}
+
+func TestPlayer_FlashDurationTimeRemaining_Zero(t *testing.T) {
+	pl := newPlayer(128 * 4)
+
+	pl.FlashDuration = 3
+	pl.FlashTick = 128
+	assert.Equal(t, time.Duration(0), pl.FlashDurationTimeRemaining())
+}
+
+func TestPlayer_FlashDurationTimeRemaining_FlashDuration_Over(t *testing.T) {
+	pl := newPlayer(128 * 4)
+
+	pl.FlashDuration = 1
+	pl.FlashTick = 128
+	assert.Equal(t, time.Duration(0), pl.FlashDurationTimeRemaining())
+}
+
+func TestPlayer_FlashDurationTimeRemaining_Fallback(t *testing.T) {
+	pl := NewPlayer(0, tickProvider(128))
+
+	pl.FlashDuration = 2
+	pl.FlashTick = 128 * 2
+	assert.Equal(t, 2*time.Second, pl.FlashDurationTimeRemaining())
+}
+
+func newPlayer(tick int) *Player {
+	return NewPlayer(128, tickProvider(tick))
+}
 
-	assert.Equal(t, 2300*time.Millisecond, p.FlashDurationTime())
+func tickProvider(tick int) ingameTickProvider {
+	return func() int { return tick }
 }
diff --git a/datatables_test.go b/datatables_test.go
index f24cd31..a1b6f58 100644
--- a/datatables_test.go
+++ b/datatables_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 
+	"github.com/markus-wa/demoinfocs-golang/common"
 	"github.com/markus-wa/demoinfocs-golang/events"
 	st "github.com/markus-wa/demoinfocs-golang/sendtables"
 	fakest "github.com/markus-wa/demoinfocs-golang/sendtables/fake"
@@ -19,7 +20,7 @@ func (DevNullReader) Read(p []byte) (n int, err error) {
 }
 
 func TestParser_BindNewPlayer_Issue98(t *testing.T) {
-	p := NewParser(new(DevNullReader))
+	p := newParser()
 
 	p.rawPlayers = map[int]*playerInfo{
 		0: {
@@ -45,7 +46,7 @@ func TestParser_BindNewPlayer_Issue98(t *testing.T) {
 }
 
 func TestParser_BindNewPlayer_Issue98_Reconnect(t *testing.T) {
-	p := NewParser(new(DevNullReader))
+	p := newParser()
 
 	p.rawPlayers = map[int]*playerInfo{
 		0: {
@@ -69,6 +70,12 @@ func TestParser_BindNewPlayer_Issue98_Reconnect(t *testing.T) {
 
 }
 
+func newParser() *Parser {
+	p := NewParser(new(DevNullReader))
+	p.header = &common.DemoHeader{}
+	return p
+}
+
 func fakePlayerEntity(id int) st.IEntity {
 	entity := new(fakest.Entity)
 	entity.On("ID").Return(id)
diff --git a/events/events_test.go b/events/events_test.go
index 0d423a1..01a02b2 100644
--- a/events/events_test.go
+++ b/events/events_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestPlayerFlashed_FlashDuration(t *testing.T) {
-	p := common.NewPlayer()
+	p := common.NewPlayer(128, func() int { return 0 })
 	e := PlayerFlashed{Player: p}
 
 	assert.Equal(t, time.Duration(0), e.FlashDuration())
diff --git a/fake/parser_test.go b/fake/parser_test.go
index 68a83e3..7dbfa5a 100644
--- a/fake/parser_test.go
+++ b/fake/parser_test.go
@@ -52,9 +52,9 @@ func TestParseNextFrameEvents(t *testing.T) {
 func kill(wepType common.EquipmentElement) events.Kill {
 	wep := common.NewEquipment(wepType)
 	return events.Kill{
-		Killer: common.NewPlayer(),
+		Killer: new(common.Player),
 		Weapon: &wep,
-		Victim: common.NewPlayer(),
+		Victim: new(common.Player),
 	}
 }
 
diff --git a/game_state_test.go b/game_state_test.go
index c5832c3..479d5f7 100644
--- a/game_state_test.go
+++ b/game_state_test.go
@@ -138,7 +138,7 @@ func TestParticipants_SuppressNoEntity(t *testing.T) {
 	gs := newGameState()
 	pl := newPlayer()
 	gs.playersByUserID[0] = pl
-	gs.playersByUserID[1] = common.NewPlayer()
+	gs.playersByUserID[1] = common.NewPlayer(0, func() int { return 0 })
 
 	allPlayers := gs.Participants().All()
 
@@ -146,7 +146,7 @@ func TestParticipants_SuppressNoEntity(t *testing.T) {
 }
 
 func newPlayer() *common.Player {
-	pl := common.NewPlayer()
+	pl := common.NewPlayer(0, func() int { return 0 })
 	pl.Entity = new(st.Entity)
 	return pl
 }