Repository: markus-wa/demoinfocs-golang
Pull request #206 (2020-04-19 11:53:50)

v2: unit tests and cleanup


Patch:
diff --git a/pkg/demoinfocs/common/equipment.go b/pkg/demoinfocs/common/equipment.go
index bc5dfa0..9617a36 100644
--- a/pkg/demoinfocs/common/equipment.go
+++ b/pkg/demoinfocs/common/equipment.go
@@ -273,9 +273,9 @@ func MapEquipment(eqName string) EquipmentType {
 type ZoomLevel int
 
 const (
-	ZoomNone = 0
-	ZoomHalf = 1
-	ZoomFull = 2
+	ZoomNone ZoomLevel = 0
+	ZoomHalf ZoomLevel = 1
+	ZoomFull ZoomLevel = 2
 )
 
 // Equipment is a weapon / piece of equipment belonging to a player.
@@ -315,12 +315,17 @@ func (e Equipment) AmmoInMagazine() int {
 		return 1
 	}
 
+	if e.Entity == nil {
+		return 0
+	}
+
 	val, ok := e.Entity.PropertyValue("m_iClip1")
 	if !ok {
 		return -1
 	}
 
-	return val.IntVal
+	// need to subtract 1 as m_iClip1 is nrOfBullets + 1
+	return val.IntVal - 1
 }
 
 // AmmoType returns the weapon's ammo type, mostly (only?) relevant for grenades.
@@ -354,6 +359,10 @@ func (e Equipment) AmmoReserve() int {
 		return 0
 	}
 
+	if e.Entity == nil {
+		return 0
+	}
+
 	// if the property doesn't exist we return 0 by default
 	val, _ := e.Entity.PropertyValue("m_iPrimaryReserveAmmoCount")
 
diff --git a/pkg/demoinfocs/common/player.go b/pkg/demoinfocs/common/player.go
index 284c255..9c1c7ee 100644
--- a/pkg/demoinfocs/common/player.go
+++ b/pkg/demoinfocs/common/player.go
@@ -278,9 +278,9 @@ func (p *Player) Velocity() r3.Vector {
 	}
 
 	return r3.Vector{
-		X: p.Entity.PropertyValueMust("localdata.m_vecVelocity[0]").Float64Val(),
-		Y: p.Entity.PropertyValueMust("localdata.m_vecVelocity[1]").Float64Val(),
-		Z: p.Entity.PropertyValueMust("localdata.m_vecVelocity[2]").Float64Val(),
+		X: float64(p.Entity.PropertyValueMust("localdata.m_vecVelocity[0]").FloatVal),
+		Y: float64(p.Entity.PropertyValueMust("localdata.m_vecVelocity[1]").FloatVal),
+		Z: float64(p.Entity.PropertyValueMust("localdata.m_vecVelocity[2]").FloatVal),
 	}
 }
 
diff --git a/pkg/demoinfocs/datatables.go b/pkg/demoinfocs/datatables.go
index b1bed7a..cb23a60 100644
--- a/pkg/demoinfocs/datatables.go
+++ b/pkg/demoinfocs/datatables.go
@@ -527,6 +527,7 @@ func (p *parser) infernoExpired(inf *common.Inferno) {
 	p.gameEventHandler.deleteThrownGrenade(inf.Thrower(), common.EqIncendiary)
 }
 
+//nolint:funlen
 func (p *parser) bindGameRules() {
 	grPrefix := func(s string) string {
 		return fmt.Sprintf("%s.%s", gameRulesPrefix, s)
diff --git a/pkg/demoinfocs/sendtables/entity.go b/pkg/demoinfocs/sendtables/entity.go
index b43829c..4eafc84 100644
--- a/pkg/demoinfocs/sendtables/entity.go
+++ b/pkg/demoinfocs/sendtables/entity.go
@@ -42,7 +42,12 @@ func (e *entity) Properties() (out []Property) {
 }
 
 func (e *entity) property(name string) *property {
-	return &e.props[e.serverClass.propNameToIndex[name]]
+	i, ok := e.serverClass.propNameToIndex[name]
+	if !ok {
+		return nil
+	}
+
+	return &e.props[i]
 }
 
 // Property finds a property on the entity by name.
@@ -271,16 +276,6 @@ func (e *entity) OnPositionUpdate(h func(pos r3.Vector)) {
 	}
 }
 
-// BindPosition binds the entity's position to a pointer variable.
-// The pointer is updated every time a position-relevant property is updated.
-//
-// See also OnPositionUpdate()
-func (e *entity) BindPosition(pos *r3.Vector) {
-	e.OnPositionUpdate(func(newPos r3.Vector) {
-		*pos = newPos
-	})
-}
-
 // Returns a coordinate from a cell + offset
 func coordFromCell(cell, cellWidth int, offset float64) float64 {
 	return float64(cell*cellWidth-maxCoordInt) + offset
diff --git a/pkg/demoinfocs/sendtables/entity_interface.go b/pkg/demoinfocs/sendtables/entity_interface.go
index 4345755..d51deff 100644
--- a/pkg/demoinfocs/sendtables/entity_interface.go
+++ b/pkg/demoinfocs/sendtables/entity_interface.go
@@ -44,11 +44,6 @@ type Entity interface {
 	//
 	// See also Position()
 	OnPositionUpdate(h func(pos r3.Vector))
-	// BindPosition binds the entity's position to a pointer variable.
-	// The pointer is updated every time a position-relevant property is updated.
-	//
-	// See also OnPositionUpdate()
-	BindPosition(pos *r3.Vector)
 	// OnDestroy registers a function to be called on the entity's destruction.
 	OnDestroy(delegate func())
 	// Destroy triggers all via OnDestroy() registered functions.
diff --git a/pkg/demoinfocs/sendtables/propdecoder.go b/pkg/demoinfocs/sendtables/propdecoder.go
index 2729ef9..cd68bf2 100644
--- a/pkg/demoinfocs/sendtables/propdecoder.go
+++ b/pkg/demoinfocs/sendtables/propdecoder.go
@@ -80,16 +80,11 @@ type PropertyValue struct {
 	FloatVal  float32
 }
 
-// BoolVal returns true if IntVal == 1.
+// BoolVal returns true if IntVal > 0.
 func (v PropertyValue) BoolVal() bool {
 	return v.IntVal > 0
 }
 
-// Float64Val returns FloatVal converted from float32 to float64.
-func (v PropertyValue) Float64Val() float64 {
-	return float64(v.FloatVal)
-}
-
 type propertyDecoder struct{}
 
 func (propertyDecoder) decodeProp(prop *property, reader *bit.BitReader) {
diff --git a/pkg/demoinfocs/stringtables.go b/pkg/demoinfocs/stringtables.go
index 414b8ef..ea2c721 100644
--- a/pkg/demoinfocs/stringtables.go
+++ b/pkg/demoinfocs/stringtables.go
@@ -135,6 +135,7 @@ func (p *parser) handleCreateStringTable(tab *msg.CSVCMsg_CreateStringTable) { /
 	p.eventDispatcher.Dispatch(events.StringTableCreated{TableName: tab.Name})
 }
 
+//nolint:funlen
 func (p *parser) processStringTable(tab *msg.CSVCMsg_CreateStringTable) {
 	defer func() {
 		p.setError(recoverFromUnexpectedEOF(recover()))


Test patch:
diff --git a/pkg/demoinfocs/common/common_test.go b/pkg/demoinfocs/common/common_test.go
index 9aab115..0722072 100644
--- a/pkg/demoinfocs/common/common_test.go
+++ b/pkg/demoinfocs/common/common_test.go
@@ -188,7 +188,7 @@ func entityWithProperty(propName string, value st.PropertyValue) *stfake.Entity
 	prop.On("Value").Return(value)
 
 	entity.On("Property", propName).Return(prop)
-	entity.On("PropertyValue", propName).Return(prop, true)
+	entity.On("PropertyValue", propName).Return(value, true)
 	entity.On("PropertyValueMust", propName).Return(value)
 
 	return entity
diff --git a/pkg/demoinfocs/common/entity_util_test.go b/pkg/demoinfocs/common/entity_util_test.go
new file mode 100644
index 0000000..9d93f82
--- /dev/null
+++ b/pkg/demoinfocs/common/entity_util_test.go
@@ -0,0 +1,23 @@
+package common
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetFloat_Nil(t *testing.T) {
+	assert.Zero(t, getFloat(nil, "test"))
+}
+
+func TestGetInt_Nil(t *testing.T) {
+	assert.Zero(t, getInt(nil, "test"))
+}
+
+func TestGetString_Nil(t *testing.T) {
+	assert.Empty(t, getString(nil, "test"))
+}
+
+func TestGetBool_Nil(t *testing.T) {
+	assert.Empty(t, getBool(nil, "test"))
+}
diff --git a/pkg/demoinfocs/common/equipment_test.go b/pkg/demoinfocs/common/equipment_test.go
index 1fac8f7..acd469a 100644
--- a/pkg/demoinfocs/common/equipment_test.go
+++ b/pkg/demoinfocs/common/equipment_test.go
@@ -38,6 +38,29 @@ func TestEquipment_UniqueID(t *testing.T) {
 	assert.NotEqual(t, NewEquipment(EqAK47).UniqueID(), NewEquipment(EqAK47).UniqueID(), "UniqueIDs of different equipment instances should be different")
 }
 
+func TestEquipment_AmmoInMagazine(t *testing.T) {
+	wep := &Equipment{
+		Type:   EqAK47,
+		Entity: entityWithProperty("m_iClip1", st.PropertyValue{IntVal: 31}),
+	}
+
+	// returned value should be minus 1, m_iClip1 is always 1 more than the actual number of bullets
+	assert.Equal(t, 30, wep.AmmoInMagazine())
+}
+
+func TestEquipment_AmmoInMagazine_NotFound(t *testing.T) {
+	entity := entityWithID(1)
+	entity.On("PropertyValue", "m_iClip1").Return(st.PropertyValue{}, false)
+
+	wep := &Equipment{
+		Type:   EqAK47,
+		Entity: entity,
+	}
+
+	// returned value should be minus 1, m_iClip1 is always 1 more than the actual number of bullets
+	assert.Equal(t, -1, wep.AmmoInMagazine())
+}
+
 func TestEquipment_AmmoInMagazine_Grenade(t *testing.T) {
 	wep := &Equipment{
 		Type: EqFlash,
@@ -46,6 +69,24 @@ func TestEquipment_AmmoInMagazine_Grenade(t *testing.T) {
 	assert.Equal(t, 1, wep.AmmoInMagazine())
 }
 
+func TestEquipment_AmmoInMagazine_EntityNil(t *testing.T) {
+	wep := &Equipment{
+		Type: EqAK47,
+	}
+
+	assert.Equal(t, 0, wep.AmmoInMagazine())
+}
+
+func TestEquipment_AmmoReserve(t *testing.T) {
+	entity := entityWithProperty("m_iPrimaryReserveAmmoCount", st.PropertyValue{IntVal: 60})
+	wep := &Equipment{
+		Type:   EqAK47,
+		Entity: entity,
+	}
+
+	assert.Equal(t, 60, wep.AmmoReserve())
+}
+
 func TestEquipment_AmmoReserve_Grenade(t *testing.T) {
 	owner := new(Player)
 	owner.AmmoLeft[1] = 2
@@ -60,7 +101,7 @@ func TestEquipment_AmmoReserve_Grenade(t *testing.T) {
 	assert.Equal(t, 1, wep.AmmoReserve())
 }
 
-func TestEquipment_AmmoReserve2_Grenade_OwnerNil(t *testing.T) {
+func TestEquipment_AmmoReserve_Grenade_OwnerNil(t *testing.T) {
 	wep := &Equipment{
 		Type: EqFlash,
 	}
@@ -68,6 +109,31 @@ func TestEquipment_AmmoReserve2_Grenade_OwnerNil(t *testing.T) {
 	assert.Equal(t, 0, wep.AmmoReserve())
 }
 
+func TestEquipment_AmmoReserve_EntityNil(t *testing.T) {
+	wep := &Equipment{
+		Type: EqAK47,
+	}
+
+	assert.Equal(t, 0, wep.AmmoReserve())
+}
+
+func TestEquipment_ZoomLevel(t *testing.T) {
+	wep := &Equipment{
+		Type:   EqAK47,
+		Entity: entityWithProperty("m_zoomLevel", st.PropertyValue{IntVal: 2}),
+	}
+
+	assert.Equal(t, ZoomFull, wep.ZoomLevel())
+}
+
+func TestEquipment_ZoomLevel_EntityNil(t *testing.T) {
+	wep := &Equipment{
+		Type: EqAK47,
+	}
+
+	assert.Equal(t, ZoomLevel(0), wep.ZoomLevel())
+}
+
 func TestEquipmentAlternative(t *testing.T) {
 	assert.Equal(t, EqUSP, EquipmentAlternative(EqP2000))
 	assert.Equal(t, EqCZ, EquipmentAlternative(EqP250))
diff --git a/pkg/demoinfocs/common/player_test.go b/pkg/demoinfocs/common/player_test.go
index 7e8c744..2adab01 100644
--- a/pkg/demoinfocs/common/player_test.go
+++ b/pkg/demoinfocs/common/player_test.go
@@ -4,9 +4,11 @@ import (
 	"testing"
 	"time"
 
+	"github.com/golang/geo/r3"
 	"github.com/stretchr/testify/assert"
 
 	st "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/sendtables"
+	stfake "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/sendtables/fake"
 )
 
 func TestPlayerActiveWeapon(t *testing.T) {
@@ -206,6 +208,36 @@ func TestPlayer_IsAirborne(t *testing.T) {
 	assert.True(t, pl.IsAirborne())
 }
 
+func TestPlayer_IsDucking(t *testing.T) {
+	pl := playerWithProperty("localdata.m_Local.m_bDucking", st.PropertyValue{IntVal: 0})
+
+	assert.False(t, pl.IsDucking())
+
+	pl = playerWithProperty("localdata.m_Local.m_bDucking", st.PropertyValue{IntVal: 1})
+
+	assert.True(t, pl.IsDucking())
+}
+
+func TestPlayer_HasDefuseKit(t *testing.T) {
+	pl := playerWithProperty("m_bHasDefuser", st.PropertyValue{IntVal: 0})
+
+	assert.False(t, pl.HasDefuseKit())
+
+	pl = playerWithProperty("m_bHasDefuser", st.PropertyValue{IntVal: 1})
+
+	assert.True(t, pl.HasDefuseKit())
+}
+
+func TestPlayer_HasHelmet(t *testing.T) {
+	pl := playerWithProperty("m_bHasHelmet", st.PropertyValue{IntVal: 0})
+
+	assert.False(t, pl.HasHelmet())
+
+	pl = playerWithProperty("m_bHasHelmet", st.PropertyValue{IntVal: 1})
+
+	assert.True(t, pl.HasHelmet())
+}
+
 func TestPlayer_IsControllingBot_NilEntity(t *testing.T) {
 	pl := new(Player)
 
@@ -250,6 +282,107 @@ func TestPlayer_ControlledBot(t *testing.T) {
 	assert.Same(t, dave, pl.ControlledBot())
 }
 
+func TestPlayer_Armor(t *testing.T) {
+	pl := playerWithProperty("m_ArmorValue", st.PropertyValue{IntVal: 95})
+
+	assert.Equal(t, 95, pl.Armor())
+}
+
+func TestPlayer_Money(t *testing.T) {
+	pl := playerWithProperty("m_iAccount", st.PropertyValue{IntVal: 800})
+
+	assert.Equal(t, 800, pl.Money())
+}
+
+func TestPlayer_ViewDirectionX(t *testing.T) {
+	pl := playerWithProperty("m_angEyeAngles[1]", st.PropertyValue{FloatVal: 180})
+
+	assert.Equal(t, float32(180), pl.ViewDirectionX())
+}
+
+func TestPlayer_ViewDirectionY(t *testing.T) {
+	pl := playerWithProperty("m_angEyeAngles[0]", st.PropertyValue{FloatVal: 15})
+
+	assert.Equal(t, float32(15), pl.ViewDirectionY())
+}
+
+func TestPlayer_Position(t *testing.T) {
+	entity := new(stfake.Entity)
+	pos := r3.Vector{X: 1, Y: 2, Z: 3}
+
+	entity.On("Position").Return(pos)
+
+	pl := &Player{Entity: entity}
+
+	assert.Equal(t, pos, pl.Position())
+}
+
+func TestPlayer_Position_EntityNil(t *testing.T) {
+	pl := new(Player)
+
+	assert.Empty(t, pl.Position())
+}
+
+func TestPlayer_Velocity(t *testing.T) {
+	entity := new(stfake.Entity)
+	entity.On("PropertyValueMust", "localdata.m_vecVelocity[0]").Return(st.PropertyValue{FloatVal: 1})
+	entity.On("PropertyValueMust", "localdata.m_vecVelocity[1]").Return(st.PropertyValue{FloatVal: 2})
+	entity.On("PropertyValueMust", "localdata.m_vecVelocity[2]").Return(st.PropertyValue{FloatVal: 3})
+
+	pl := &Player{Entity: entity}
+
+	expected := r3.Vector{X: 1, Y: 2, Z: 3}
+	assert.Equal(t, expected, pl.Velocity())
+}
+
+func TestPlayer_Velocity_EntityNil(t *testing.T) {
+	pl := new(Player)
+
+	assert.Empty(t, pl.Velocity())
+}
+
+func TestPlayer_ClanTag(t *testing.T) {
+	pl := playerWithResourceProperty("m_szClan", st.PropertyValue{StringVal: "SuperClan"})
+
+	assert.Equal(t, "SuperClan", pl.ClanTag())
+}
+
+func TestPlayer_Ping(t *testing.T) {
+	pl := playerWithResourceProperty("m_iPing", st.PropertyValue{IntVal: 45})
+
+	assert.Equal(t, 45, pl.Ping())
+}
+
+func TestPlayer_Score(t *testing.T) {
+	pl := playerWithResourceProperty("m_iScore", st.PropertyValue{IntVal: 10})
+
+	assert.Equal(t, 10, pl.Score())
+}
+
+func TestPlayer_Kills(t *testing.T) {
+	pl := playerWithResourceProperty("m_iKills", st.PropertyValue{IntVal: 5})
+
+	assert.Equal(t, 5, pl.Kills())
+}
+
+func TestPlayer_Deaths(t *testing.T) {
+	pl := playerWithResourceProperty("m_iDeaths", st.PropertyValue{IntVal: 2})
+
+	assert.Equal(t, 2, pl.Deaths())
+}
+
+func TestPlayer_Assists(t *testing.T) {
+	pl := playerWithResourceProperty("m_iAssists", st.PropertyValue{IntVal: 3})
+
+	assert.Equal(t, 3, pl.Assists())
+}
+
+func TestPlayer_MVPs(t *testing.T) {
+	pl := playerWithResourceProperty("m_iMVPs", st.PropertyValue{IntVal: 4})
+
+	assert.Equal(t, 4, pl.MVPs())
+}
+
 func TestPlayer_SteamID32(t *testing.T) {
 	pl := &Player{SteamID64: 76561198012952267}
 
@@ -263,3 +396,12 @@ func newPlayer(tick int) *Player {
 func playerWithProperty(propName string, value st.PropertyValue) *Player {
 	return &Player{Entity: entityWithProperty(propName, value)}
 }
+
+func playerWithResourceProperty(propName string, value st.PropertyValue) *Player {
+	return &Player{
+		EntityID: 1,
+		demoInfoProvider: demoInfoProviderMock{
+			playerResourceEntity: entityWithProperty(propName+".001", value),
+		},
+	}
+}
diff --git a/pkg/demoinfocs/sendtables/entity_test.go b/pkg/demoinfocs/sendtables/entity_test.go
new file mode 100644
index 0000000..f840135
--- /dev/null
+++ b/pkg/demoinfocs/sendtables/entity_test.go
@@ -0,0 +1,70 @@
+package sendtables
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var testData = struct {
+	entity entity
+}{
+	entity: entity{
+		props: []property{
+			{value: PropertyValue{IntVal: 10}},
+			{value: PropertyValue{IntVal: 20}},
+			{value: PropertyValue{IntVal: 30}},
+		},
+		serverClass: &ServerClass{propNameToIndex: map[string]int{
+			"myProp":     0,
+			"test":       1,
+			"anotherOne": 2,
+		}},
+	},
+}
+
+func TestEntity_Properties(t *testing.T) {
+	ent := entity{props: []property{{value: PropertyValue{IntVal: 1}}}}
+
+	assert.Equal(t, &ent.props[0], ent.Properties()[0])
+}
+
+func TestEntity_ServerClass(t *testing.T) {
+	assert.Equal(t, testData.entity.serverClass, testData.entity.ServerClass())
+}
+
+func TestEntity_Property(t *testing.T) {
+	assert.Equal(t, &testData.entity.props[1], testData.entity.Property("test"))
+}
+
+func TestEntity_Property_Nil(t *testing.T) {
+	assert.Nil(t, testData.entity.Property("not_found"))
+}
+
+func TestEntity_Property_Value(t *testing.T) {
+	val, ok := testData.entity.PropertyValue("test")
+
+	assert.True(t, ok)
+	assert.Equal(t, PropertyValue{IntVal: 20}, val)
+}
+
+func TestEntity_PropertyValue_NotFound(t *testing.T) {
+	val, ok := testData.entity.PropertyValue("not_found")
+
+	assert.False(t, ok)
+	assert.Empty(t, val)
+}
+
+func TestEntity_PropertyValueMust_NotFound_Panics(t *testing.T) {
+	f := func() {
+		testData.entity.PropertyValueMust("not_found")
+	}
+
+	assert.Panics(t, f)
+}
+
+func TestProperty_Name(t *testing.T) {
+	prop := property{entry: &flattenedPropEntry{name: "test"}}
+
+	assert.Equal(t, "test", prop.Name())
+}
diff --git a/pkg/demoinfocs/sendtables/propdecoder_test.go b/pkg/demoinfocs/sendtables/propdecoder_test.go
new file mode 100644
index 0000000..20c9873
--- /dev/null
+++ b/pkg/demoinfocs/sendtables/propdecoder_test.go
@@ -0,0 +1,22 @@
+package sendtables
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPropertyValue_BoolVal(t *testing.T) {
+	assert.True(t, PropertyValue{IntVal: 1}.BoolVal())
+	assert.False(t, PropertyValue{IntVal: 0}.BoolVal())
+}
+
+func TestDecodeProp_UnknownType(t *testing.T) {
+	prop := &property{entry: &flattenedPropEntry{prop: &sendTableProperty{rawType: -1}}}
+
+	f := func() {
+		propDecoder.decodeProp(prop, nil)
+	}
+
+	assert.Panics(t, f)
+}