Repository: markus-wa/demoinfocs-golang
Pull request #424 (2023-09-29 00:51:19)

fix: possible unknown bombsite in BombPlantBegin event
- Fix the bombsite field in game events always `BombsiteUnknown` when  `disableMimicSource1GameEvents` is set to `true`. Bounding boxes information are now in the server class `CBombTarget`. It was not an issue when `disableMimicSource1GameEvents` is `false` (the default), as it uses props to detect bombsite.
- Fix a rare case where the bombsite in `BombPlantBegin` events is unknown (when `disableMimicSource1GameEvents` is `false`). It happened in this [de_mirage demo](http://replay190.valve.net/730/003641925298976457430_0293543650.dem.bz2) at round 4 when the player struggled to plant on A near boxes. As a fallback (when `m_nWhichBombZone` is `0`), I detect the closest site based on the distance between the player's position and bombsites.



Patch:
diff --git a/pkg/demoinfocs/datatables.go b/pkg/demoinfocs/datatables.go
index ae5e3b9..82cff6f 100644
--- a/pkg/demoinfocs/datatables.go
+++ b/pkg/demoinfocs/datatables.go
@@ -2,6 +2,7 @@ package demoinfocs
 
 import (
 	"fmt"
+	"math"
 	"strings"
 
 	"github.com/golang/geo/r3"
@@ -122,6 +123,8 @@ func (p *parser) bindBomb() {
 						site = events.BombsiteA
 					case 2:
 						site = events.BombsiteB
+					case 0:
+						site = p.getClosestBombsiteFromPosition(planter.Position())
 					}
 
 					if !p.disableMimicSource1GameEvents {
@@ -305,18 +308,36 @@ func (p *parser) bindBombSites() {
 		playerResource.BindProperty("m_bombsiteCenterB", &p.bombsiteB.center, st.ValTypeVector)
 	})
 
-	p.stParser.ServerClasses().FindByName("CBaseTrigger").OnEntityCreated(func(baseTrigger st.Entity) {
+	onBombTargetEntityCreated := func(target st.Entity) {
 		t := new(boundingBoxInformation)
-		p.triggers[baseTrigger.ID()] = t
+		p.triggers[target.ID()] = t
 
+		var (
+			minPropName string
+			maxPropName string
+		)
 		if p.isSource2() {
-			baseTrigger.BindProperty("m_vecMins", &t.min, st.ValTypeVector)
-			baseTrigger.BindProperty("m_vecMaxs", &t.max, st.ValTypeVector)
+			minPropName = "m_vecMins"
+			maxPropName = "m_vecMaxs"
 		} else {
-			baseTrigger.BindProperty("m_Collision.m_vecMins", &t.min, st.ValTypeVector)
-			baseTrigger.BindProperty("m_Collision.m_vecMaxs", &t.max, st.ValTypeVector)
+			minPropName = "m_Collision.m_vecMins"
+			maxPropName = "m_Collision.m_vecMaxs"
 		}
-	})
+
+		target.BindProperty(minPropName, &t.min, st.ValTypeVector)
+		target.BindProperty(maxPropName, &t.max, st.ValTypeVector)
+	}
+
+	if p.isSource2() {
+		// CBombTarget is not available with CS2 demos created in the early days of the limited test.
+		bombTargetClass := p.stParser.ServerClasses().FindByName("CBombTarget")
+		if bombTargetClass != nil {
+			bombTargetClass.OnEntityCreated(onBombTargetEntityCreated)
+			return
+		}
+	}
+
+	p.stParser.ServerClasses().FindByName("CBaseTrigger").OnEntityCreated(onBombTargetEntityCreated)
 }
 
 func (p *parser) bindPlayers() {
@@ -899,10 +920,10 @@ func (p *parser) bindWeaponS2(entity st.Entity) {
 	// - The player is inside the buy zone
 	// - The player's money has increased AND the weapon entity is destroyed at the same tick (unfortunately the money is updated first)
 	var (
-		owner *common.Player
-		oldOwnerMoney int
+		owner               *common.Player
+		oldOwnerMoney       int
 		lastMoneyUpdateTick int
-		lastMoneyIncreased bool
+		lastMoneyIncreased  bool
 	)
 
 	entity.Property("m_hOwnerEntity").OnUpdate(func(val st.PropertyValue) {
@@ -1300,3 +1321,18 @@ func (p *parser) bindHostages() {
 		})
 	})
 }
+
+func getDistanceBetweenVectors(vectorA r3.Vector, vectorB r3.Vector) float64 {
+	return math.Sqrt(math.Pow(vectorA.X-vectorB.X, 2) + math.Pow(vectorA.Y-vectorB.Y, 2) + math.Pow(vectorA.Z-vectorB.Z, 2))
+}
+
+func (p *parser) getClosestBombsiteFromPosition(position r3.Vector) events.Bombsite {
+	distanceFromBombsiteA := getDistanceBetweenVectors(position, p.bombsiteA.center)
+	distanceFromBombsiteB := getDistanceBetweenVectors(position, p.bombsiteB.center)
+
+	if distanceFromBombsiteA < distanceFromBombsiteB {
+		return events.BombsiteA
+	}
+
+	return events.BombsiteB
+}
diff --git a/pkg/demoinfocs/events/events.go b/pkg/demoinfocs/events/events.go
index a2be338..bca4645 100644
--- a/pkg/demoinfocs/events/events.go
+++ b/pkg/demoinfocs/events/events.go
@@ -289,20 +289,20 @@ type BombEventIf interface {
 	implementsBombEventIf()
 }
 
-type bombsite rune
+type Bombsite rune
 
 // Bombsite identifiers
 const (
-	BomsiteUnknown bombsite = 0
-	BombsiteA      bombsite = 'A'
-	BombsiteB      bombsite = 'B'
+	BomsiteUnknown Bombsite = 0
+	BombsiteA      Bombsite = 'A'
+	BombsiteB      Bombsite = 'B'
 )
 
 // BombEvent contains the common attributes of bomb events. Dont register
 // handlers on this tho, you want BombEventIf for that.
 type BombEvent struct {
 	Player *common.Player
-	Site   bombsite
+	Site   Bombsite
 }
 
 // Make BombEvent implement BombEventIf


Test patch:
diff --git a/pkg/demoinfocs/common/player_test.go b/pkg/demoinfocs/common/player_test.go
index 7541217..0db2f1e 100644
--- a/pkg/demoinfocs/common/player_test.go
+++ b/pkg/demoinfocs/common/player_test.go
@@ -409,7 +409,8 @@ func TestPlayer_PositionEyes(t *testing.T) {
 }
 
 func TestPlayer_PositionEyes_EntityNil(t *testing.T) {
-	pl := new(Player)
+	pl := &Player{}
+	pl.demoInfoProvider = s1DemoInfoProvider
 
 	assert.Empty(t, pl.PositionEyes())
 }
diff --git a/pkg/demoinfocs/datatables_test.go b/pkg/demoinfocs/datatables_test.go
index 0f1cbea..bb02553 100644
--- a/pkg/demoinfocs/datatables_test.go
+++ b/pkg/demoinfocs/datatables_test.go
@@ -3,6 +3,7 @@ package demoinfocs
 import (
 	"testing"
 
+	"github.com/golang/geo/r3"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 
@@ -130,6 +131,24 @@ func fakePlayerEntity(id int) *stfake.Entity {
 	return entity
 }
 
+func TestParser_GetClosestBombsiteFromPosition(t *testing.T) {
+	p := newParser()
+	p.bombsiteA = bombsite{
+		center: r3.Vector{X: 2, Y: 3, Z: 1},
+	}
+	p.bombsiteB = bombsite{
+		center: r3.Vector{X: 4, Y: 5, Z: 7},
+	}
+
+	site := p.getClosestBombsiteFromPosition(r3.Vector{X: -2, Y: 2, Z: 2})
+
+	assert.Equal(t, events.BombsiteA, site)
+
+	site = p.getClosestBombsiteFromPosition(r3.Vector{X: 3, Y: 6, Z: 5})
+
+	assert.Equal(t, events.BombsiteB, site)
+}
+
 func configurePlayerEntityMock(id int, entity *stfake.Entity) {
 	entity.On("ID").Return(id)