Skip to content

feat: Add ability to retrieve player viewmodel settings #592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/viewmodel-settings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Player Viewmodel Settings

This example shows how to use the library to extract player viewmodel settings from CS2 demos. Viewmodel settings include the viewmodel offset (X, Y, Z) and field of view.

## Running the example

`go run viewmodel_settings.go -demo /path/to/cs2-demo.dem`

### Sample output

```
Player viewmodels:
degster: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
kyxsan: Viewmodel Offset=(1.0, 1.0, -1.0), FOV=60.0
NiKo: Viewmodel Offset=(-1.0, 1.5, -2.0), FOV=60.0
SOMEBODY: Viewmodel Offset=(2.5, 2.0, -2.0), FOV=60.0
Summer: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
L1haNg: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
ChildKing: Viewmodel Offset=(2.5, 1.0, -1.5), FOV=60.0
TeSeS: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
Magisk: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
kaze: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
```

Note: Viewmodel settings are only available in CS2 demos. CS:GO demos will show zero values.
52 changes: 52 additions & 0 deletions examples/viewmodel-settings/viewmodel_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"fmt"
"os"

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

// Run like this: go run viewmodel_settings.go -demo /path/to/cs2-demo.dem
func main() {
f, err := os.Open(ex.DemoPathFromArgs())
if err != nil {
panic(err)
}

defer f.Close()

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

// Register handler on round start to collect viewmodel settings
p.RegisterEventHandler(func(e events.RoundStart) {
fmt.Println("Player viewmodels:")
gs := p.GameState()

// Get all connected players
players := gs.Participants().Playing()

for _, player := range players {
if player == nil {
continue
}

// Get viewmodel settings
offset := player.ViewmodelOffset()
fov := player.ViewmodelFOV()

fmt.Printf("%s: Viewmodel Offset=(%.1f, %.1f, %.1f), FOV=%.1f\n",
player.Name, offset.X, offset.Y, offset.Z, fov)
}
fmt.Println() // Empty line for readability
})

// Parse to end
err = p.ParseToEnd()
if err != nil {
panic(err)
}
}
34 changes: 34 additions & 0 deletions pkg/demoinfocs/common/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,40 @@ func (p *Player) CrosshairCode() string {
return val.StringVal
}

// ViewmodelOffset returns the player's viewmodel offset as a 3D vector (X, Y, Z).
// Returns zero vector if not available (CS:GO demos or player not alive).
func (p *Player) ViewmodelOffset() r3.Vector {
if !p.demoInfoProvider.IsSource2() {
return r3.Vector{}
}

pawn := p.PlayerPawnEntity()
if pawn == nil {
return r3.Vector{}
}

return r3.Vector{
X: float64(getFloat(pawn, "m_flViewmodelOffsetX")),
Y: float64(getFloat(pawn, "m_flViewmodelOffsetY")),
Z: float64(getFloat(pawn, "m_flViewmodelOffsetZ")),
}
}

// ViewmodelFOV returns the player's viewmodel field of view.
// Returns 0 if not available (CS:GO demos or player not alive).
func (p *Player) ViewmodelFOV() float32 {
if !p.demoInfoProvider.IsSource2() {
return 0
}

pawn := p.PlayerPawnEntity()
if pawn == nil {
return 0
}

return getFloat(pawn, "m_flViewmodelFOV")
}

// Ping returns the players latency to the game server.
func (p *Player) Ping() int {
// TODO change this func return type to uint64? (small BC)
Expand Down
59 changes: 59 additions & 0 deletions pkg/demoinfocs/common/player_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,65 @@ func TestPlayer_IsGrabbingHostage(t *testing.T) {
assert.True(t, pl.IsGrabbingHostage())
}

func TestPlayer_ViewmodelOffsetS1(t *testing.T) {
// Test CS:GO demo
pl := newPlayer(0)
assert.Equal(t, r3.Vector{}, pl.ViewmodelOffset())
}

func TestPlayer_ViewmodelOffsetS2(t *testing.T) {
// Set up controller entity with pawn references
controllerEntity := entityWithProperties([]fakeProp{
{propName: "m_hPlayerPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
{propName: "m_hPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
})

// Set up pawn entity with viewmodel offset properties
pawnEntity := entityWithProperties([]fakeProp{
{propName: "m_flViewmodelOffsetX", value: st.PropertyValue{FloatVal: -1.5}},
{propName: "m_flViewmodelOffsetY", value: st.PropertyValue{FloatVal: 2.0}},
{propName: "m_flViewmodelOffsetZ", value: st.PropertyValue{FloatVal: -0.5}},
})

pl := &Player{Entity: controllerEntity}
pl.demoInfoProvider = demoInfoProviderMock{
isSource2: true,
entitiesByHandle: map[uint64]st.Entity{
1: pawnEntity,
},
}

assert.Equal(t, r3.Vector{X: -1.5, Y: 2.0, Z: -0.5}, pl.ViewmodelOffset())
}

func TestPlayer_ViewmodelFOVS1(t *testing.T) {
// Test CS:GO demo (should return 0 even with property)
pl := playerWithProperty("m_flViewmodelFOV", st.PropertyValue{FloatVal: 60})
pl.demoInfoProvider = s1DemoInfoProvider
assert.Equal(t, float32(0), pl.ViewmodelFOV())
}

func TestPlayer_ViewmodelFOVS2(t *testing.T) {
// Set up controller entity with pawn references
controllerEntity := entityWithProperties([]fakeProp{
{propName: "m_hPlayerPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
{propName: "m_hPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
})

// Set up pawn entity with viewmodel FOV property
pawnEntity := entityWithProperty("m_flViewmodelFOV", st.PropertyValue{FloatVal: 60})

pl := &Player{Entity: controllerEntity}
pl.demoInfoProvider = demoInfoProviderMock{
isSource2: true,
entitiesByHandle: map[uint64]st.Entity{
1: pawnEntity,
},
}

assert.Equal(t, float32(60), pl.ViewmodelFOV())
}

func newPlayer(tick int) *Player {
return NewPlayer(mockDemoInfoProvider(128, tick))
}
Expand Down
Loading