Skip to content

Commit 454fdda

Browse files
authored
Merge pull request #592 from captainswain/feat/viewmodel_offsets
feat: Add ability to retrieve player viewmodel settings
2 parents 0722cfc + 20e6497 commit 454fdda

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Player Viewmodel Settings
2+
3+
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.
4+
5+
## Running the example
6+
7+
`go run viewmodel_settings.go -demo /path/to/cs2-demo.dem`
8+
9+
### Sample output
10+
11+
```
12+
Player viewmodels:
13+
degster: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
14+
kyxsan: Viewmodel Offset=(1.0, 1.0, -1.0), FOV=60.0
15+
NiKo: Viewmodel Offset=(-1.0, 1.5, -2.0), FOV=60.0
16+
SOMEBODY: Viewmodel Offset=(2.5, 2.0, -2.0), FOV=60.0
17+
Summer: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
18+
L1haNg: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
19+
ChildKing: Viewmodel Offset=(2.5, 1.0, -1.5), FOV=60.0
20+
TeSeS: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
21+
Magisk: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
22+
kaze: Viewmodel Offset=(2.5, 0.0, -1.5), FOV=60.0
23+
```
24+
25+
Note: Viewmodel settings are only available in CS2 demos. CS:GO demos will show zero values.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
ex "github.com/markus-wa/demoinfocs-golang/v4/examples"
8+
demoinfocs "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs"
9+
events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events"
10+
)
11+
12+
// Run like this: go run viewmodel_settings.go -demo /path/to/cs2-demo.dem
13+
func main() {
14+
f, err := os.Open(ex.DemoPathFromArgs())
15+
if err != nil {
16+
panic(err)
17+
}
18+
19+
defer f.Close()
20+
21+
p := demoinfocs.NewParser(f)
22+
defer p.Close()
23+
24+
// Register handler on round start to collect viewmodel settings
25+
p.RegisterEventHandler(func(e events.RoundStart) {
26+
fmt.Println("Player viewmodels:")
27+
gs := p.GameState()
28+
29+
// Get all connected players
30+
players := gs.Participants().Playing()
31+
32+
for _, player := range players {
33+
if player == nil {
34+
continue
35+
}
36+
37+
// Get viewmodel settings
38+
offset := player.ViewmodelOffset()
39+
fov := player.ViewmodelFOV()
40+
41+
fmt.Printf("%s: Viewmodel Offset=(%.1f, %.1f, %.1f), FOV=%.1f\n",
42+
player.Name, offset.X, offset.Y, offset.Z, fov)
43+
}
44+
fmt.Println() // Empty line for readability
45+
})
46+
47+
// Parse to end
48+
err = p.ParseToEnd()
49+
if err != nil {
50+
panic(err)
51+
}
52+
}

pkg/demoinfocs/common/player.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,40 @@ func (p *Player) CrosshairCode() string {
651651
return val.StringVal
652652
}
653653

654+
// ViewmodelOffset returns the player's viewmodel offset as a 3D vector (X, Y, Z).
655+
// Returns zero vector if not available (CS:GO demos or player not alive).
656+
func (p *Player) ViewmodelOffset() r3.Vector {
657+
if !p.demoInfoProvider.IsSource2() {
658+
return r3.Vector{}
659+
}
660+
661+
pawn := p.PlayerPawnEntity()
662+
if pawn == nil {
663+
return r3.Vector{}
664+
}
665+
666+
return r3.Vector{
667+
X: float64(getFloat(pawn, "m_flViewmodelOffsetX")),
668+
Y: float64(getFloat(pawn, "m_flViewmodelOffsetY")),
669+
Z: float64(getFloat(pawn, "m_flViewmodelOffsetZ")),
670+
}
671+
}
672+
673+
// ViewmodelFOV returns the player's viewmodel field of view.
674+
// Returns 0 if not available (CS:GO demos or player not alive).
675+
func (p *Player) ViewmodelFOV() float32 {
676+
if !p.demoInfoProvider.IsSource2() {
677+
return 0
678+
}
679+
680+
pawn := p.PlayerPawnEntity()
681+
if pawn == nil {
682+
return 0
683+
}
684+
685+
return getFloat(pawn, "m_flViewmodelFOV")
686+
}
687+
654688
// Ping returns the players latency to the game server.
655689
func (p *Player) Ping() int {
656690
// TODO change this func return type to uint64? (small BC)

pkg/demoinfocs/common/player_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,65 @@ func TestPlayer_IsGrabbingHostage(t *testing.T) {
656656
assert.True(t, pl.IsGrabbingHostage())
657657
}
658658

659+
func TestPlayer_ViewmodelOffsetS1(t *testing.T) {
660+
// Test CS:GO demo
661+
pl := newPlayer(0)
662+
assert.Equal(t, r3.Vector{}, pl.ViewmodelOffset())
663+
}
664+
665+
func TestPlayer_ViewmodelOffsetS2(t *testing.T) {
666+
// Set up controller entity with pawn references
667+
controllerEntity := entityWithProperties([]fakeProp{
668+
{propName: "m_hPlayerPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
669+
{propName: "m_hPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
670+
})
671+
672+
// Set up pawn entity with viewmodel offset properties
673+
pawnEntity := entityWithProperties([]fakeProp{
674+
{propName: "m_flViewmodelOffsetX", value: st.PropertyValue{FloatVal: -1.5}},
675+
{propName: "m_flViewmodelOffsetY", value: st.PropertyValue{FloatVal: 2.0}},
676+
{propName: "m_flViewmodelOffsetZ", value: st.PropertyValue{FloatVal: -0.5}},
677+
})
678+
679+
pl := &Player{Entity: controllerEntity}
680+
pl.demoInfoProvider = demoInfoProviderMock{
681+
isSource2: true,
682+
entitiesByHandle: map[uint64]st.Entity{
683+
1: pawnEntity,
684+
},
685+
}
686+
687+
assert.Equal(t, r3.Vector{X: -1.5, Y: 2.0, Z: -0.5}, pl.ViewmodelOffset())
688+
}
689+
690+
func TestPlayer_ViewmodelFOVS1(t *testing.T) {
691+
// Test CS:GO demo (should return 0 even with property)
692+
pl := playerWithProperty("m_flViewmodelFOV", st.PropertyValue{FloatVal: 60})
693+
pl.demoInfoProvider = s1DemoInfoProvider
694+
assert.Equal(t, float32(0), pl.ViewmodelFOV())
695+
}
696+
697+
func TestPlayer_ViewmodelFOVS2(t *testing.T) {
698+
// Set up controller entity with pawn references
699+
controllerEntity := entityWithProperties([]fakeProp{
700+
{propName: "m_hPlayerPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
701+
{propName: "m_hPawn", value: st.PropertyValue{Any: uint64(1), S2: true}},
702+
})
703+
704+
// Set up pawn entity with viewmodel FOV property
705+
pawnEntity := entityWithProperty("m_flViewmodelFOV", st.PropertyValue{FloatVal: 60})
706+
707+
pl := &Player{Entity: controllerEntity}
708+
pl.demoInfoProvider = demoInfoProviderMock{
709+
isSource2: true,
710+
entitiesByHandle: map[uint64]st.Entity{
711+
1: pawnEntity,
712+
},
713+
}
714+
715+
assert.Equal(t, float32(60), pl.ViewmodelFOV())
716+
}
717+
659718
func newPlayer(tick int) *Player {
660719
return NewPlayer(mockDemoInfoProvider(128, tick))
661720
}

0 commit comments

Comments
 (0)