From 20e6497b694bab63476f425fb2767aea223f6d2d Mon Sep 17 00:00:00 2001 From: Shane Lindsay Date: Thu, 5 Jun 2025 00:05:00 -0700 Subject: [PATCH] Add ViewmodelOffset() and ViewmodelFOV() to player, add example. --- examples/viewmodel-settings/README.md | 25 ++++++++ .../viewmodel-settings/viewmodel_settings.go | 52 ++++++++++++++++ pkg/demoinfocs/common/player.go | 34 +++++++++++ pkg/demoinfocs/common/player_test.go | 59 +++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 examples/viewmodel-settings/README.md create mode 100644 examples/viewmodel-settings/viewmodel_settings.go diff --git a/examples/viewmodel-settings/README.md b/examples/viewmodel-settings/README.md new file mode 100644 index 000000000..94e38953e --- /dev/null +++ b/examples/viewmodel-settings/README.md @@ -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. \ No newline at end of file diff --git a/examples/viewmodel-settings/viewmodel_settings.go b/examples/viewmodel-settings/viewmodel_settings.go new file mode 100644 index 000000000..078a9d7c0 --- /dev/null +++ b/examples/viewmodel-settings/viewmodel_settings.go @@ -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) + } +} diff --git a/pkg/demoinfocs/common/player.go b/pkg/demoinfocs/common/player.go index c59a04c14..238883a2e 100644 --- a/pkg/demoinfocs/common/player.go +++ b/pkg/demoinfocs/common/player.go @@ -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) diff --git a/pkg/demoinfocs/common/player_test.go b/pkg/demoinfocs/common/player_test.go index b3c654835..cf35993ea 100644 --- a/pkg/demoinfocs/common/player_test.go +++ b/pkg/demoinfocs/common/player_test.go @@ -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)) }