Giter VIP home page Giter VIP logo

demoinfocs-golang's Introduction

demoinfocs-golang - Counter-Strike 2 & CS:GO Demo Parser

A blazing fast, feature complete and production ready Go library for parsing and analysing of Counter-Strike 2 and Counter-Strike: Global Offensive (CS:GO) demos (aka replays).

go.dev reference Build Status codecov Go Report License FOSSA Status Snyk Scan

Discussions / Chat

You can use Discord or GitHub Discussions to ask questions and discuss ideas about this project.
For business inquiries please use the contact information found on the GitHub profile.

Discord Chat

Go Get

Counter-Strike 2

go get -u github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs

CS:GO

go get -u github.com/markus-wa/demoinfocs-golang/v3/pkg/demoinfocs

Table of Contents

Requirements

This library requires at least go 1.20 to run. You can download the latest version of Go here.

Quickstart Guide

  1. Download and install the latest version of Go from golang.org or via your favourite package manager

  2. Create a new Go Modules project

mkdir my-project
cd my-project
go mod init my-project
go get -u github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs
  1. Create a main.go file with the example below

  2. Run go run main.go

Example

This is a simple example on how to handle game events using this library. It prints all kills in a given demo (killer, weapon, victim, was it a wallbang/headshot?) by registering a handler for events.Kill.

Check out the godoc of the events package for some information about the other available events and their purpose.

package main

import (
	"fmt"
	"log"
	"os"

	dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs"
	events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events"
)

func main() {
	f, err := os.Open("/path/to/demo.dem")
	if err != nil {
		log.Panic("failed to open demo file: ", err)
	}
	defer f.Close()

	p := dem.NewParser(f)
	defer p.Close()

	// Register handler on kill events
	p.RegisterEventHandler(func(e events.Kill) {
		var hs string
		if e.IsHeadshot {
			hs = " (HS)"
		}
		var wallBang string
		if e.PenetratedObjects > 0 {
			wallBang = " (WB)"
		}
		fmt.Printf("%s <%v%s%s> %s\n", e.Killer, e.Weapon, hs, wallBang, e.Victim)
	})

	// Parse to end
	err = p.ParseToEnd()
	if err != nil {
		log.Panic("failed to parse demo: ", err)
	}
}

Sample Output

Running the code above will print something like this:

xms <AK-47 (HS)> crisby
tiziaN <USP-S (HS)> Ex6TenZ
tiziaN <USP-S> mistou
tiziaN <USP-S (HS)> ALEX
xms <Glock-18 (HS)> tiziaN
...
keev <AWP (HS) (WB)> to1nou
...

More Examples

Check out the examples folder for more examples, like how to generate heatmaps like this one:

sample heatmap

Documentation

The full API documentation is available here on pkg.go.dev.

Features

  • Game events (kills, shots, round starts/ends, footsteps etc.) - docs / example
  • Tracking of game-state (players, teams, grenades, ConVars etc.) - docs
  • Grenade projectiles / trajectories - docs / example
  • Access to entities, server-classes & data-tables - docs / example
  • Access to all net-messages - docs / example
  • Chat & console messages 1 - docs / example
  • Matchmaking ranks (official MM demos only) - docs
  • Full POV demo support
  • Support for encrypted net-messages (if the decryption key is provided)
  • JavaScript (browser / Node.js) support via WebAssembly - example
  • Easy debugging via build-flags
  • Built with performance & concurrency in mind
  1. In MM demos the chat is encrypted, so ParserConfig.NetMessageDecryptionKey needs to be set - see also MatchInfoDecryptionKey().

Performance / Benchmarks

Two of the top priorities of this parser are performance and concurrency.

Here are some benchmark results from a system with an Intel i7 6700k CPU and a SSD disk running Windows 10 and a demo with 85'000 frames.

Overview

Benchmark Description Average Duration Speed
BenchmarkConcurrent Read and parse 8 demos concurrently 2.06 s (per 8 demos) ~330'000 ticks / s
BenchmarkDemoInfoCs Read demo from drive and parse 0.89 s ~95'000 ticks / s
BenchmarkInMemory Read demo from memory and parse 0.88 s ~96'000 ticks / s

That's almost 1.5 hours of gameplay per second when parsing in parallel (recorded at 64 ticks per second) - or 25 minues per second when only parsing a single demo at a time.

Raw Output

$ go test -run _NONE_ -bench . -benchtime 30s -benchmem -concurrentdemos 8
goos: windows
goarch: amd64
pkg: github.com/markus-wa/demoinfocs-golang
BenchmarkDemoInfoCs-8             50     894500010 ns/op    257610127 B/op    914355 allocs/op
BenchmarkInMemory-8               50     876279984 ns/op    257457271 B/op    914143 allocs/op
BenchmarkConcurrent-8             20    2058303680 ns/op    2059386582 B/op  7313145 allocs/op
--- BENCH: BenchmarkConcurrent-8
    demoinfocs_test.go:315: Running concurrency benchmark with 8 demos
    demoinfocs_test.go:315: Running concurrency benchmark with 8 demos
PASS
ok      github.com/markus-wa/demoinfocs-golang  134.244s

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository. There is one caveat however: Beta features - which are marked as such via comments and in release notes - may change in minor releases.

Projects Using demoinfocs-golang

  • noesis.gg - A suite of explorative tools to help you analyze and improve your CS:GO performance
  • esportal.com - An alternative Matchmaking service that aims to provide a friendly environment free from trolls and misbehaving individuals
  • esportslab.gg - Building ML/AI tools for professional esports players
  • scope.gg - Analytical and advisory service for advanced CS:GO players
  • PureSkill.gg - An automated coach to help you get better at CS:GO.
  • cs-demo-minifier - Converts demos to JSON, MessagePack and more
  • csgo_spray_pattern_plotter - A tool to extract and plot spray patterns from CS:GO replays
  • CS:GO Player Skill Prediction - Machine learning master thesis by @quancore about predicting player performance
  • csgoverview - A 2D demo replay tool for CS:GO
  • csgo-coach-bug-detector - Detects the abuse of an exploit used by some team coaches in professional matches
  • megaclan3000 - A CS:GO stats page for clans with recent matches and player statistics
  • awpy - A wrapper for the Golang parser in Python

If your project is using this library feel free to submit a PR or send a message via Discord to be included in the list.

Useful Tools & Libraries

  • csgo-centrifuge - Get historic radar overview images to make 2D replays & heatmaps accurate on all map versions
  • head-position-model - Approximate the player's head position (rather than just the camera position)

Development

Debugging

You can use the build tag debugdemoinfocs to print out debugging information - such as game events or unhandled demo-messages - during the parsing process.

e.g.

go run -tags debugdemoinfocs examples/print-events/print_events.go -demo example.dem

Side-note: The tag isn't called debug to avoid naming conflicts with other libs (and underscores in tags don't work, apparently).

To change the default debugging behavior, Go's ldflags parameter can be used. Example for additionally printing out all server-classes with their properties: -ldflags="-X 'github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs.debugServerClasses=YES'".

e.g.

go run -tags debugdemoinfocs -ldflags="-X 'github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs.debugServerClasses=YES'" examples/print-events/print_events.go -demo example.dem

Check out debug_on.go for any other settings that can be changed.

Testing

Unit Tests

For any new features, Test Driven Development should be practiced where possible. However, due to some design flaws in some parts of the code it's currently not always practical to do so.

Running unit tests:

scripts/unit-tests.sh
# or (identical)
go test -short ./...

Regression Tests

For the full regression suite you will need to download the test demo-set.

Prerequisites:

  • Git LFS must be installed
  • 7z must be in your PATH environment variable (p7zip or p7zip-full package on most Linux distros)

Downloading demos + running regression tests:

scripts/regression-tests.sh

Updating the default.golden File

The file test/default.golden file contains a serialized output of all expected game events in test/cs-demos/default.dem.

If there is a change to game events (new fields etc.) it is necessary to update this file so the regression tests pass. To update it you can run the following command:

go test -run TestDemoInfoCs -update

Please don't update the .golden file if you are not sure it's required. Maybe the failing CI is just pointing out a regression.

Generating Interfaces

We generate interfaces such as GameState from structs to make it easier to keep docs in synch over structs and interfaces. For this we use @vburenin's ifacemaker tool.

You can download the latest version here. After adding it to your PATH you can use scripts/generate-interfaces.sh to update interfaces.

Generating Protobuf Code

Should you need to re-generate the protobuf generated code in the msg package, you will need the following tools:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Make sure both are inside your PATH variable.

After installing these use go generate ./msg to generate the protobuf code. If you're on Windows you'll need to run go generate from CMD, not Bash.

Git Hooks

To install some (optional, but quite handy) pre-commit and pre-push hooks, you can run the following script.

scripts/git-hooks/link-git-hooks.sh

pre-commit:

pre-push:

  • run regression tests

Acknowledgements

This library was originally based on Valve's demoinfogo and SatsHelix's demoinfo (although today it shares little resemblence with these two).

For Counter-Strike 2, dotabuff/manta was an amazing resource for how to parse Source 2 demos and CS2 support would not have been possible without it.
I would also like to specifically thank @akiver & @LaihoE for their brilliant help with CS2.

And a very special thanks goes out to all the ⭐contributors⭐️, be it in the form of PRs, issues or anything else.

Further shoutouts go to:

License

This project is licensed under the MIT license.

FOSSA Status

demoinfocs-golang's People

Contributors

akiver avatar allenymin avatar bestawperever avatar captainswain avatar david-durst avatar dependabot-preview[bot] avatar dependabot[bot] avatar ektaros avatar esbengc avatar falderebet avatar fossabot avatar github-actions[bot] avatar julien2313 avatar juliusmh avatar makindotcc avatar marksamman avatar markus-wa avatar mateusmaia avatar micvbang avatar n1c avatar o40 avatar piku7 avatar pnxenopoulos avatar srjnm avatar statist32 avatar supermaciz avatar xavier-rodet avatar xxxhaxmeister42xxx avatar zoidyzoidzoid avatar zyphie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

demoinfocs-golang's Issues

Team info from player

Higher goal
It is better to reach team info of a player directly from player struct rather than extraneous method callings. Therefore, we can reach score, team name etc directly using player itself.

Describe the solution you'd like
Probably changing theTeam field to TeamState in Player struct and maintaining this field should work.

Question: yaw, pitch

I could not see in player struct yaw and pitch angles. However, there are viewangle variables in the struct. Are there correspond to yaw and pitch? If yes, what is exactly the view angles? I need to understand because I need to calculate crosshair replacement.

round end loser state

Describe the bug
When round end has been called, getting score by using loser team state return always 0.

To Reproduce
I think it will be produced for all demo files

Code:
Initiate round end event and get loser score by e.LoserState.Score

Expected behavior
Correct score for losing team as well.
Library version
Latest

base class for events

I decided to store all events in an array per round before dispatching them directly with a registered event handler, therefore, after I checked several parameters for the current round, I can decide to discard or dispatch events. However, I do not see a base interface implementation for all events. My question is that how can I store all events in a single event container for delayed dispatch?

Entity.Position() not working for player entities

Describe the bug
Entity.Position() and by extension Entity.OnPositionUpdate() aren't working for Player entities due to the position being networked differently from other entities.

Calling the function results in a nil pointer dereference panic because the m_cellbits property doesn't seem to exist.

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x38 pc=0x78f250]

goroutine 19 [running]:
github.com/markus-wa/demoinfocs-golang.recoverFromUnexpectedEOF(0x81dde0, 0xb519b0, 0xb519b0, 0x0)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/parsing.go:110 +0xbd
github.com/markus-wa/demoinfocs-golang.(*Parser).handleGameEvent.func1(0xc04210a000)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/game_events.go:27 +0x4a
panic(0x81dde0, 0xb519b0)
        C:/Go/src/runtime/panic.go:502 +0x237
github.com/markus-wa/demoinfocs-golang/sendtables.(*Entity).Position(0xc04216a780, 0xc042089020, 0xc, 0xc0427c5208)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/sendtables/entity.go:167 +0x50

To Reproduce
Use Entity.Position() on any player and check agains Player.Position.

Code:

func x() {
    pl := parser.GameState().Participants()[0]
    fmt.Println(parser.GameState().Entities()[pl.EntityID].Position(), pl.Position)
}

Expected behavior
It should print the same (or at least a very similar) value twice.

Library version
v1.0.0-beta.2

events: Is player blinded during Kill event?

Higher goal
It would be wonderful to know whether a player has been killed in a blinded situation or not. Therefore, I can update the blinded kill features.

Describe the solution you'd like
Probably, there should be an addition in event of Kill like:

type Kill struct {
    Weapon            *common.Equipment
    Victim            *common.Player
    Killer            *common.Player
    Assister          *common.Player
    PenetratedObjects int
    IsHeadshot        bool
    IsBlinded         bool
}

Persistent team id

Currently, since CCSTeam entities don't switch on team-switches (players switch instead), the TeamState id field stays the same.

Id should stay consistent on team-switches, and then we can instead have a side field in TeamState to display which side a team is on (which is in effect what TeamState.id tells us currently).

A potential solution is to create logic on player switches. This logic detects team-switches:

	type TeamSwitch struct {
		tickPlayerSwitch int
		numPlayerSwitch  int
		teamSwitchDone   bool
	}

	ts := TeamSwitch{tickPlayerSwitch: -1}

	p.RegisterEventHandler(func(e events.PlayerTeamChangeEvent) {
		tick := p.GameState().IngameTick()
		numPlayers := len(p.GameState().PlayingParticipants())
		if tick > ts.tickPlayerSwitch {
			// First player switch, fix ts
			ts.numPlayerSwitch = 1
			ts.tickPlayerSwitch = tick
			ts.teamSwitchDone = false
		} else if tick == ts.tickPlayerSwitch {
			ts.numPlayerSwitch++
			if (ts.teamSwitchDone == false) && (ts.numPlayerSwitch >= numPlayers-1) {
				// We now assume we have a change of sides.
				// Creates a margin of one in case of missing event
				ts.teamSwitchDone = true
				fmt.Println("Change of sides")
			}
		}
	})

consistency in player ids

I there a consistency in player ids or it is changing on connect/disconnect event? If no consistency, how can I identify this is the same player with the previous player?

roundEnd vs roundOfficiallyEnd

  1. What is all of the differences of round end and round officially ended event?
  2. As far as I read, these events could be in deficit for some of the demo files but Are we sure that on of events will be definitely called to identify a round has been ended such as round end, round officially end or score updated?

Unexpected `EqUnknown`

Describe the bug
Hey!

I unfortunately haven't had time to investigate this myself. I thought that I'd report it so that I don't forget 🙂

I've found that in some demos I get EqUnknown (i.e. uninitialized) for some player's weapons.

My gut tells me that this happens in the weapons mapping in datatables.go, but I have nothing else to base this on.

To Reproduce
Run the following code on dust2 from this match: https://www.hltv.org/matches/2322780/liquid-vs-astralis-esl-pro-league-season-7-finals

In round 2 we see that device has bought a scout that is being returned as EqUnknown.

Code:

package main

import (
	"log"
	"os"

	"github.com/markus-wa/demoinfocs-golang/common"

	dem "github.com/markus-wa/demoinfocs-golang"
	"github.com/markus-wa/demoinfocs-golang/events"
	ex "github.com/markus-wa/demoinfocs-golang/examples"
)

func main() {
	f, err := os.Open(ex.DemoPathFromArgs())
	checkError(err)
	defer f.Close()

	p := dem.NewParser(f)

	_, err = p.ParseHeader()
	checkError(err)

	roundNum := 1
	const playerName = "device"

	p.RegisterEventHandler(func(ev events.RoundEnd) {
		roundNum++
	})

	// Log playerName's weapons
	p.RegisterEventHandler(func(ev events.RoundFreezetimeEnd) {
		if roundNum == 2 {
			for _, p := range p.GameState().Participants().TeamMembers(common.TeamCounterTerrorists) {
				if p.Name != playerName {
					continue
				}
				for i, wep := range p.Weapons() {
					log.Printf("%s weapon[%d]: %+v\n", playerName, i, wep.Weapon)
				}
			}
			p.Cancel()
		}
	})

	err = p.ParseToEnd()
	checkError(err)
}

func checkError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

Expected behavior
Expected to not get EqUnknown for known weapons.

Library version
I'm getting these results on 13db521

events: PlayerFlashed lack of info (Attacker / FlashDuration)

Higher goal
I need to know who is flashed, who did it and the duration of being blind. It is an important feature for my machine learning model.

Describe the solution you'd like
Probably, there should be alteration for event of PlayerFlashed like:

type PlayerFlashed struct {
    Attacker *common.Player
    Defender *common.Player
    duration int
}

Fully Support Parsing of POV Demos

When adding a POV demo to the test demo-set in test/cs-demos the parser crashes.

Parsing header
Registering handlers
Parsing to end
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x18 pc=0x744af6]

goroutine 19 [running]:
github.com/markus-wa/demoinfocs-golang/st.(*Entity).ApplyUpdate(0x0, 0xc0428a8000)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/st/entity.go:53 +0xc6
github.com/markus-wa/demoinfocs-golang.(*Parser).handlePacketEntities(0xc0420ec000, 0xc0425ea0c0)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/packet_handlers.go:47 +0x2d0
github.com/markus-wa/demoinfocs-golang.(*Parser).(github.com/markus-wa/demoinfocs-golang.handlePacketEntities)-fm(0xc0425ea0c0)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/parser.go:120 +0x3b
reflect.Value.call(0x79e7e0, 0xc042041040, 0x13, 0x829e20, 0x4, 0xc042c51f60, 0x1, 0x1, 0xc042c51e78, 0xc042c51e70, ...)
        C:/Go/src/reflect/value.go:434 +0x926
reflect.Value.Call(0x79e7e0, 0xc042041040, 0x13, 0xc042c51f60, 0x1, 0x1, 0x4, 0xc04213e000, 0xa53e00)
        C:/Go/src/reflect/value.go:302 +0xab
github.com/markus-wa/godispatch.(*Dispatcher).Dispatch.func1(0xc0420ec068, 0xc042c51f48, 0xc042c51f60, 0x1, 0x1)
        C:/Dev/GoPath/src/github.com/markus-wa/godispatch/dispatch.go:49 +0xa1
github.com/markus-wa/godispatch.(*Dispatcher).Dispatch(0xc0420ec068, 0x811500, 0xc0425ea0c0)
        C:/Dev/GoPath/src/github.com/markus-wa/godispatch/dispatch.go:50 +0x220
github.com/markus-wa/godispatch.(*Dispatcher).dispatchQueue(0xc0420ec068, 0xc04203eb40)
        C:/Dev/GoPath/src/github.com/markus-wa/godispatch/dispatch.go:93 +0x11e
created by github.com/markus-wa/godispatch.(*Dispatcher).AddQueues
        C:/Dev/GoPath/src/github.com/markus-wa/godispatch/dispatch.go:79 +0x189
exit status 2
FAIL    github.com/markus-wa/demoinfocs-golang  0.245s

Improve Documentation

  • Complete basic GoDoc
  • Improve README.md
  • Add more examples
    • AdditionalEventEmitters
    • ServerClasses
  • Explain some lingo & quirks of CS:GO -> will be done on a per-question basis
  • Document more fields that aren't immediately obvious
  • Maybe document #64 a bit more in depth

Notes by @micvbang:

I think it would be nice to see some general documentation on some of the lingo and quirks of the game on the roadmap
One example could be the life-cycle of grenades (e.g. for fire nades: weapon entity -> projectile entity -> inferno entity)
Another example could be what entity ids are, and why (id reuse) regular users of the library probably want to use the UniqueID instead
(This probably is an extension of bullet 1) documentation on some of the fields that are not immediately, e.g. Player.UserID. I think it would be nice to explain what this is, when compared to e.g. EntityID and SteamID

nill references for events

I am observing nill players for several events such as victim in kill event etc. Is it a bug or normal? If normal how should be treated for this event?

team state and team

Higher goal
Getting complete team information.

Describe the solution you'd like
The problem is that some events return team state which has nothing about the side of the team which is expected. However, there is no connection between team states and team sides (Team). For example, if I trigger scoreUpdated event, it returns me teamstate which I have no idea whether it is t or ct team. Fast solution would be adding a method in gamestate to retrieve team side by using team id.

MatchEnded event?

How can I know the match is ended so I can discard any event happened after the match ended?

Panic "Failed to unmarshal cmd"

With defaultDemName = "/broken/2014-11-01-ESWC2014-fnatic-vs-hellraisers-de_inferno.dem"

The demo appears to work without problems in GOTV.

go test -run TestDemoInfoCs
Parsing header
Registering handlers
Parsing to end
T-side score:  1
T-side score:  2
T-side score:  3
T-side score:  12
T-side score:  13
T-side score:  14
T-side score:  15
T-side score:  16
T-side score:  0
T-side score:  15
T-side score:  18
T-side score:  21
T-side score:  24
T-side score:  27
T-side score:  30
T-side score:  33
--- FAIL: TestDemoInfoCs (7.44s)
panic: Failed to unmarshal cmd 26 [recovered]
        panic: Failed to unmarshal cmd 26

goroutine 18 [running]:
testing.tRunner.func1(0xc04204c820)
        C:/Go/src/testing/testing.go:622 +0x2a4
panic(0x79fc00, 0xc042af2e60)
        C:/Go/src/runtime/panic.go:489 +0x2dd
github.com/markus-wa/demoinfocs-golang.(*Parser).parsePacket(0xc0420ec000)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/demopacket.go:78 +0x380
github.com/markus-wa/demoinfocs-golang.(*Parser).parseTick(0xc0420ec000, 0x7bf960)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/parsing.go:160 +0x1bc
github.com/markus-wa/demoinfocs-golang.(*Parser).ParseNextTick(0xc0420ec000, 0x1)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/parsing.go:72 +0x5a
github.com/markus-wa/demoinfocs-golang.(*Parser).ParseToEnd(0xc0420ec000, 0xc0420410f8)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/parsing.go:56 +0x47
github.com/markus-wa/demoinfocs-golang_test.TestDemoInfoCs(0xc04204c820)
        C:/Dev/GoPath/src/github.com/markus-wa/demoinfocs-golang/demoinfocs_test.go:65 +0x486
testing.tRunner(0xc04204c820, 0x850d08)
        C:/Go/src/testing/testing.go:657 +0x9d
created by testing.(*T).Run
        C:/Go/src/testing/testing.go:697 +0x2d1
exit status 2
FAIL    github.com/markus-wa/demoinfocs-golang  7.595s

player reconnect

When a player connects/disconnect, which fields get reset and which fields get a newly assigned value? I collect information about players and I need to transmit info to a player who reconnected. I am using SteamID to identify players. Any suggestions to do this task in an easy way?
And also it would be wonderful to the creation of playerReconnect event.

Smoke grenade OnDestroy event not triggered as expected

Describe the bug
In datatables.bindGrenadeProjectiles we assume that the projectile entity destroy-event is triggered every time the entity no longer appears in the game. I still haven't found out why, but this does not seem to be the case. It looks to me like projectile destruction event is not triggered on smokes (more entities?) that do not expire normally, but are "removed" because the round has ended.

To Reproduce
I found the bug in round 4 of de_nuke in the following game: https://www.hltv.org/matches/2322780/liquid-vs-astralis-esl-pro-league-season-7-finals

package main

import (
	"log"
	"os"
	"sync"

	dem "github.com/markus-wa/demoinfocs-golang"
	"github.com/markus-wa/demoinfocs-golang/events"
)

func main() {
	f, err := os.Open("liquid-vs-astralis-m2-nuke.dem")
	if err != nil {
		log.Fatal(err)
	}

	p := dem.NewParser(f)
	_, err = p.ParseHeader()
	if err != nil {
		log.Fatal(err)
	}

	wg := &sync.WaitGroup{}

	roundNum := 0
	p.RegisterEventHandler(func(_ events.RoundOfficiallyEndedEvent) {
		roundNum++
	})

	p.RegisterEventHandler(func(_ events.RoundStartedEvent) {
		log.Printf("Round %+v just started. We expect there to be no projectiles. There are: %+v\n", roundNum, len(p.GameState().GrenadeProjectiles()))
	})

	err = p.ParseToEnd()
	if err != nil {
		log.Fatal(err)
	}

	wg.Wait()
}

Expected behavior
I expect GameState().GrenadeProjectiles() to only contain active grenade projectiles, even those that did not expire normally

Library version
v1.0.0

A quick (and dirty) way to solve the problem is to simply reset the gameState.grenadeProjectiles map on RoundOfficiallyEnded event, like so (game_events.go):

...
case "round_officially_ended": // Round ended. . . probably the event where you get teleported to the spawn (=> You can still walk around between round_end and this?)
	for _, proj := range p.gameState.grenadeProjectiles {
		delete(p.gameState.grenadeProjectiles, proj.EntityID)
	}
	p.eventDispatcher.Dispatch(events.RoundOfficiallyEndedEvent{})
case ...

Exposing vendored types to user, e.g. in OnPositionUpdate(r3.Vector)

It may be that I'm just unaware of how this can be circumvented, but I'm having trouble using the callbacks that require the usage of vendored types, e.g. Entity.OnPositionUpdate(r3.Vector).

It makes sense that I can't just use the type as r3.Vector in my source, as it may be different from the vendored version. And the compiler won't let me import vendored packages, which I'm not absolutely certain that I understand why wouldn't work. Anyway, unless there's some way out of this that I can't see, I think we need to look at the types from vendored code used in callbacks to publicly exported code 🙂

The most obvious solution that I can come up with is to create the simplest possible wrappers for these types, e.g. where r3.Vector is used, one could create the type Vector and use it like so:

type Vector struct {
    r3.Vector
}

// example of internal use in demoinfocs-golang
func test(v r3.Vector) Vector {
    return Vector{v}
}

Equipment.Class() returns wrong value

Equipment.Class() currently:

EquipmentClass(int(e.Weapon) / 100)

This returns the wrong value (actual value - 1) as e.Weapon, or EquipmentElement, is rounded to the closest value.

E.g. glock has EquipmentElement value of 2. It should have an EquipmentClass of 1 (pistols), but the returned value is 0.

Naturally, as 2/100 = .02 ~ 0

Solution could be,
i) Add 100 to numerator:
EquipmentClass(int(e.Weapon)+100 / 100)
ii) Round to next integer:
EquipmentClass(math.Ceil(int(e.Weapon) / 100))

Molotov and Incendiary grenade projectile positioning bug

We use the entity id of grenade projectiles when tracking them. This works well for non-fire grenades. It seems that new entities are created for fire grenades they explode, meaning that we can't use the entity id for tracking them and keeping GameState.grenadeProjectiles up to date.

Currently, this means that fire grenades will not get removed from GameState.grenadeProjectiles once they explode, meaning that they will accumulate through the demo.

RoundEnd event not called every round

Describe the bug

Hey Markus!

This is yet another I-found-a-bug-but-did-not-yet-investigate situation 🙂

I've found that in some demos the RoundEnd event is not triggered every round.

Since the internal score reported by the library is correct, at least m_scoreTotal is being called as we would expect.

To Reproduce

Run the following code on this match: https://www.hltv.org/matches/2327343/mibr-vs-azio-ecs-season-6-north-america

package main

import (
	"fmt"
	"log"
	"os"

	dem "github.com/markus-wa/demoinfocs-golang"
	"github.com/markus-wa/demoinfocs-golang/events"
	ex "github.com/markus-wa/demoinfocs-golang/examples"
)

func main() {
	f, err := os.Open(ex.DemoPathFromArgs())
	checkError(err)
	defer f.Close()

	p := dem.NewParser(f)

	_, err = p.ParseHeader()
	checkError(err)

	numRoundEnd := 1
	p.RegisterEventHandler(func(ev events.RoundEnd) {
		numRoundEnd++
	})

	numRoundEndOfficial := 1
	p.RegisterEventHandler(func(ev events.RoundEndOfficial) {
		numRoundEndOfficial++
	})

	err = p.ParseToEnd()
	checkError(err)

	fmt.Println("")
	fmt.Printf("Number of 'round end' events: %d\n", numRoundEnd)
	fmt.Printf("Number of 'round official end' events: %d\n", numRoundEndOfficial)
	fmt.Printf("Final score: %d:%d\n", p.GameState().TeamCounterTerrorists().Score(), p.GameState().TeamTerrorists().Score())
}

func checkError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

Expected behavior

I would expect to see 26 RoundEnd events, as reported by HLTV

Library version

I'm getting these results on 9916e2b4

I want to contribute

Hey Markus!
First of all, congrats for this excellent project and the good code you're writing.
I would love to help on it and discuss few ideas, can you send me an email or gimme yours?
Salut !

Player additional information

Higher goal
There are several fields for players can easy being retrieved from a demo file. My interest is: whether a player is in a buy zone, whether a player in a bomb zone (and the name of the bomb zone), how many seconds alive in this round, whether he is walking, whether he is jumping (I do not know it is possible).
Describe the solution you'd like
Most of the fields are in a demo file and implemented here: https://github.com/saul/demofile/blob/c6d6555/src/entities/player.ts

Unexpected player appears in a team

Describe the bug
Non-player persons appear on a team. Exact definition: There is a player called Zim in this dem file, which is neither connected nor team changed. I do not know where he is coming from.
To Reproduce
demo link: https://www.hltv.org/download/demo/47811
matchmaking page: https://www.hltv.org/matches/2331369/izako-boars-vs-havu-lantrek-2019
Code:

package testParser

import (
    "fmt"
    "os"
    "testing"

    dem "github.com/markus-wa/demoinfocs-golang"
    p_common "github.com/markus-wa/demoinfocs-golang/common"
    events "github.com/markus-wa/demoinfocs-golang/events"
)

//TestNonExistCOnnect test non exist player situation
func TestNonExistCOnnect(t *testing.T) {
    f, err := os.Open("/home/baran/Desktop/demo_files/test_demos/izako-boars-vs-havu-inferno.dem")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    p := dem.NewParser(f)

    // gs := p.GameState()

    p.RegisterEventHandler(func(e events.PlayerDisconnected) {
        gs := p.GameState()

        if e.Player != nil {
            fmt.Printf("Player has been disconnected: name:%s team:%s tick:%d\n", e.Player.Name, e.Player.TeamState.ClanName, gs.IngameTick())
        }
    })

    p.RegisterEventHandler(func(e events.PlayerTeamChange) {
        gs := p.GameState()
        participants := gs.Participants()
        teamTerrorist := participants.TeamMembers(p_common.TeamTerrorists)
        if e.Player != nil {
            fmt.Printf("Player team changed: name:%s old team:%d new team:%d tick:%d\n", e.Player.Name, e.OldTeam, e.NewTeam, gs.IngameTick())
            for _, t := range teamTerrorist {
                if t != nil {
                    fmt.Printf("t player name:%s team:%d\n", t.Name, t.Team)

                }
            }
        }
    })

    // p.RegisterEventHandler(func(e events.Kill) {
    //     fmt.Printf("Player killed a player: attacker: %s team:%d tick:%d\n", e.Killer.Name, e.Killer.Team, gs.IngameTick())
    //     if e.Killer != nil && reconnectedPlayerID == e.Killer.SteamID {
    //         fmt.Printf("Player killed a player: attacker: %s team:%s tick:%d\n", e.Killer.Name, e.Killer.TeamState.ClanName, gs.IngameTick())
    //     }
    // })

    p.RegisterEventHandler(func(e events.PlayerConnect) {
        gs := p.GameState()

        // teamCT := participants.TeamMembers(p_common.TeamCounterTerrorists)
        if e.Player != nil {
            fmt.Printf("Player has been connected: name:%s team:%d tick:%d\n", e.Player.Name, e.Player.Team, gs.IngameTick())

        }
    })

    // Parse to end
    err = p.ParseToEnd()
    if err != nil {
        panic(err)
    }
}

Expected behavior
No player other than expected ones.

Library version
v1.0.1

WeaponFiredEvent e.Weapon always default

hi ;)
first of all very nice library! love it
have not done much in go and playing around with it at the moment
when i register on for example the WeaponFiredEvent the event variable Weapon as well as Player.ActiveWeapon is set to default Equipment ( if i'm correct its because they are both eventually coming from Parser.weapons)

dont know for sure but i think its because in
https://github.com/markus-wa/demoinfocs-golang/blob/4080117fb1ba6a5782e432cb2b82b594653410f5/datatables.go#L301

eq := p.weapons[event.Entity.ID]

shouldnt it be

eq := &p.weapons[event.Entity.ID]

because you are filling eq with values afterwards but p.weapons never gets the value back

thank you
lukas

Parsing very slow due to Entity.BindPosition()

Describe the bug
#44 introduced Entity.BindPosition() for players which the parser uses internally to keep track of the players positions. This function performs very badly since it calls Entity.FindProperty() twice on every update.

     10ms       10ms    198:func (e *Entity) positionPlayer() r3.Vector {
     110ms     42.73s    199:   xy := e.FindProperty(propVecOriginPlayerXY).value.VectorVal
      10ms     35.67s    200:   z := float64(e.FindProperty(propVecOriginPlayerZ).value.FloatVal)
      90ms       90ms    201:   return r3.Vector{
         .          .    202:           X: xy.X,
         .          .    203:           Y: xy.Y,
         .          .    204:           Z: z,
         .          .    205:   }
         .          .    206:}

To Reproduce

git checkout 95349a9
# Fast
go test -run _NONE_ -bench .

git checkout 4b006c8
# Slow
go test -run _NONE_ -bench .

Expected behavior
As fast as before merging #44

Library version
4b006c8

Additional context
Discovered by @micvbang

v1.0.0 Event naming

I was just going through some code and realized that the Event-suffix on all events in the events package is unnecessary and somewhat stuttery since it's already specified by the package name.

What do you think?

Issues with how prop values are currently handled

Currently, there are limitations on how one can use prop values.

1. Ability to get current prop value

Currently we can only get prop value on updates. One can solve this by storing the value in a local variable, but this should perhaps be stored on the entity.

Even if we store the value locally, we face an issue: Let's say we want to store prop values when a FireEntityCreatedEvent happens. FirePropertyUpdate has not been fired yet, so we cannot store this data.

2. Multiple values fired at same tick if value differing for default(?) value on FirePropertyUpdate after a FireEntityCreatedEvent

E.g. "m_iTeamNum" on "CBaseCSGrenadeProjectile" seems to default to 3, so if the value actually is 3, FirePropertyUpdate fires once with this value. If this value is actually 2, FirePropertyUpdate is fired twice on the same tick (first with value 3, then 2).

Broad overview of what I think needs to be done for 1.:

  • Specify props one want stored on a type of entity
  • On FirePropertyUpdate for these props, store the data to Entity
  • Have a handler on the entity that is fired when the data is populated (e.g. at end of tick, as we then should get the latest, valid value from FirePropertyUpdate re: 2.)

Wrong prop values

Prop values seem to be outputting some wrong values. I have tried a couple of props that typically have two different outputs. After the first FireEntityCreatedEvent happens, both values are printed (e.g. 3 and 2 for m_iTeamNum) on the same tick. Then for the rest of the entities created, only value of 2 is printed.

	stp.FindServerClassByName("CBaseCSGrenadeProjectile").RegisterEntityCreatedHandler(func(pr st.EntityCreatedEvent) {
		pr.Entity.FindProperty("m_iTeamNum").RegisterPropertyUpdateHandler(func(val st.PropValue) {
			fmt.Println("VAL", val.IntVal)
		})
	})

This behavior happens on one of two demos I tried.

The same issue happens on m_nModelIndex on the same type of entity, however, then both demos display the behavior (both values printed on first entity created, only one value for the rest).

This is not a issue with the demos, as this does not happen using a different parser.

Example demos:
https://www.hltv.org/matches/2322565/astralis-vs-gambit-ecs-season-5-europe
https://www.hltv.org/matches/2296569/envyus-vs-natus-vincere-sltv-starseries-xiii-finals

Warm up event ignore

Higher goal
An option for a parser to event trigger during warm-up sessions.

Describe the solution you'd like
Probably a bool var. to indicate whether warm-up will be ignored or not in parser conf.

Maps metadata outdated

Describe the bug
The metadata related to radar images seem outdated and should be updated according to the information in SteamApps/common/Counter-Strike Global Offensive/csgo/resource/overviews/*.txt

Using this outdated metadata will cause incorrect translation functionality in the metadata functions.

To Reproduce
Use the TranslateScale function from metadata to translate positions for example de_dust2 (which seem broken). Get a ingame coordinate from a corner somewhere on the map and then try to draw it on the radar image (like in the heatmap example).

Expected behavior
The position on the radar image should be the same as the ingame position.

Library version
Don't know. Latest and greatest.

Improve documentation related to GameState

If references to gamestate data is saved in an event, then it is not clear if the data is still valid in coming events. Documentation could be updated to clarify this use case.

Clean up API for v1.0.0

See v1.0.0 branch

  • Put 'game state' into a separate struct from Parser
  • Replace current warning system with a ParserWarnEvent
  • Clean up the NewParser() / NewParserWithBufferSize() (remove warnHandler from the former and rename the latter)
  • Rename RegisterPropertyUpdateHandler (remove 'Property' because it's stuttery)
  • Remove anything deprecated (marked via comment)
  • Change inconsistent / strange type names in sendtables (PropValue -> PropertyValue?, FlattenedPropEntry -> PropertyEntryMetadata? and many more, allowed since Parser.SendTableParser() is in beta)
  • Move HitGroup, RoundEndReason & RoundMVPReason constants to events package
  • Consider changing beta features / removing beta labels
  • Move Parser.entities to GameState

divide by zero in demoinfocs-golang/common.DemoHeader.FrameTime

Describe the bug
Running the code below on the demo file linked results in a panic (runtime error) because it's dividing by zero in common.DemoHeader.FrameTime.

To Reproduce
https://www.dropbox.com/s/z0xjroygkdec6l6/742301.dem?dl=0

Code:

		f, err := os.Open("742301.dem")
		if err != nil {
			panic(err)
		}
		defer f.Close()

		p := dem.NewParser(f)
		p.RegisterEventHandler(func(e events.SayText2) {
			 p.CurrentTime()
		})
		p.ParseToEnd()

Library version
v1.1.0

User defined events

Higher goal
As a part of my analyzer, I would like to check some pre and post status of the game related an event. For example, I need to check whether player C still alive 2 seconds after player B has been killed. Or Is there any team member near the place just 1 second before player C has been hurt.

Describe the solution you'd like
A very naive way to implement the need is that every pre and post-event entity can be stored in a list and every time tickEnd event triggered, we can check the list whether there is any post/pre entity to proses. This solution will be very slow.
Describe alternatives you've considered
Instead, providing a way to dispatch this user-defined pre/post entities (can be formed as custom events as well) like other events such as playerDeath, kill, etc. Here, I am assuming that I already collected the exact tick number of pre/post events, so the parser only emits these pre/post events like other default events when the tick number has occurred.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.