Skip to content

Commit 5b6712b

Browse files
committed
get ready for public release
1 parent 8d756f7 commit 5b6712b

File tree

10 files changed

+351
-98
lines changed

10 files changed

+351
-98
lines changed

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Saiko Technology Ltd.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
# bsp-tracer
22

3-
BSP (Source Engine Map) Ray Tracer / Ray Caster Library
3+
BSP (Source Engine Map) Ray Tracer / Ray Caster Library.
4+
5+
Allows to do static / out-of-engine visibility checks and ray casting on BSP map files.<br>
6+
Can be used to get more accurate visibility info between players than there is available in CS:GO demos/replays (.dem files).
7+
8+
## Features
9+
10+
- [x] Faces (basic map geometry)
11+
- [x] Brushes (walls / level shape)
12+
- [x] Static Props (boxes, barrels, etc.)
13+
- [ ] Displacements (terrain bumps and slopes)
14+
- [ ] Entities ("dynamic" props - doors, vents, etc.)
15+
16+
## Example
17+
18+
```go
19+
package main
20+
21+
import (
22+
"fmt"
23+
"os"
24+
25+
"github.com/go-gl/mathgl/mgl32"
26+
"github.com/saiko-tech/bsp-tracer/pkg/bsptracer"
27+
)
28+
29+
func main() {
30+
csgoDir := os.Getenv("CSGO_DIR") // should point to "SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
31+
32+
m, err := bsptracer.LoadMapFromFileSystem(csgoDir+"/csgo/maps/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
33+
if err != nil {
34+
panic(err)
35+
}
36+
37+
fmt.Println("A site -> A site, open:", m.IsVisible(mgl32.Vec3{-12, 1444, 1751}, mgl32.Vec3{-233, 1343, 1751})) // true
38+
fmt.Println("T spawn -> A site:", m.IsVisible(mgl32.Vec3{3306, 431, 1723}, mgl32.Vec3{-233, 1343, 1751})) // false
39+
fmt.Println("mid through box:", m.IsVisible(mgl32.Vec3{-94, 452, 1677}, mgl32.Vec3{138, 396, 1677})) // false
40+
fmt.Println("T spawn -> T spawn:", m.IsVisible(mgl32.Vec3{3306, 431, 1723}, mgl32.Vec3{3300, 400, 1720})) // true
41+
}
42+
```
443

544
## Development
645

@@ -14,4 +53,6 @@ Follows https://github.com/golang-standards/project-layout
1453

1554
## Acknowledgements
1655

17-
- This is a port of https://github.com/ReactiioN1337/valve-bsp-parser
56+
- This library is based on the C++ [valve-bsp-parser](https://github.com/ReactiioN1337/valve-bsp-parser) by [@ReactiioN1337](https://github.com/ReactiioN1337)
57+
- Thanks to [@jangler](https://github.com/jangler) for porting the C++ library to Go
58+
- Thanks to [@Galaco](https://github.com/Galaco) for creating Go tooling for the BSP file format & source engine

pkg/bsptracer/bsptracer.go

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/galaco/vpk2"
1919
"github.com/go-gl/mathgl/mgl32"
2020
"github.com/pkg/errors"
21+
22+
"github.com/saiko-tech/bsp-tracer/pkg/bsptracer/mollertrumbore"
2123
)
2224

2325
const (
@@ -48,24 +50,7 @@ type Map struct {
4850
entities []map[string]string // TODO: not yet sure if we'll need this
4951
polygons []polygon
5052
models []*studiomodel.StudioModel // TODO: place props in the world and trace against them
51-
staticPropsByLeaf map[uint16][]*studiomodel.StudioModel
52-
}
53-
54-
func staticPropsByLeaf(bspfile *bsp.Bsp, models []*studiomodel.StudioModel) map[uint16][]*studiomodel.StudioModel {
55-
res := make(map[uint16][]*studiomodel.StudioModel)
56-
57-
gameLump := bspfile.Lump(bsp.LumpGame).(*lumps.Game).GetData()
58-
spLump := gameLump.GetStaticPropLump()
59-
60-
for _, p := range spLump.PropLumps {
61-
leafIndices := spLump.LeafLump.Leaf[p.GetFirstLeaf() : p.GetFirstLeaf()+p.GetLeafCount()]
62-
63-
for _, i := range leafIndices {
64-
res[i] = append(res[i], models[p.GetPropType()])
65-
}
66-
}
67-
68-
return res
53+
staticPropsByLeaf map[uint16][]staticProp
6954
}
7055

7156
// LoadMap loads a map from a BSP file and VPKs.
@@ -196,6 +181,21 @@ func (m Map) rayCastNode(nodeIndex int32, startFraction, endFraction float32,
196181
out.Brush = brush
197182
}
198183

184+
for _, p := range m.staticPropsByLeaf[uint16(leafIndex)] {
185+
for _, t := range p.triangles {
186+
r := mollertrumbore.RayIntersectsTriangle(origin, destination, t)
187+
if r.Hit {
188+
out.Fraction = 0 // TODO: should not be 0, should be fraction of ray
189+
190+
return
191+
}
192+
}
193+
194+
// TODO: use bounding box if no phy and model is set up to fall back to bbox
195+
}
196+
197+
// TODO: handle leaf displacements
198+
199199
if out.StartSolid || out.Fraction < 1 {
200200
return
201201
}
@@ -205,15 +205,6 @@ func (m Map) rayCastNode(nodeIndex int32, startFraction, endFraction float32,
205205
origin, destination, out)
206206
}
207207

208-
for _, p := range m.staticPropsByLeaf[uint16(leafIndex)] {
209-
_ = p.Phy.Vertices
210-
_ = p.Phy.TriangleFaces
211-
212-
// TODO: trace against triangle faces (build them up during map load)
213-
}
214-
215-
// TODO: handle leaf displacements
216-
217208
return
218209
}
219210

pkg/bsptracer/bsptracer_test.go

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,54 @@
1-
package bsptracer
1+
package bsptracer_test
22

33
import (
44
"os"
55
"testing"
66

77
"github.com/go-gl/mathgl/mgl32"
88
"github.com/stretchr/testify/assert"
9+
10+
"github.com/saiko-tech/bsp-tracer/pkg/bsptracer"
911
)
1012

11-
func TestBspTracer_NonExisting(t *testing.T) {
12-
t.Parallel()
13+
// expects CS:GO to be installed in "$HOME/games/SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
14+
func csgoDir(tb testing.TB) string {
15+
tb.Helper()
1316

14-
_, err := LoadMapFromFileSystem("../../testdata/does_not_exist.bsp")
15-
assert.NotNil(t, err)
17+
userHome, err := os.UserHomeDir()
18+
assert.NoError(tb, err)
19+
20+
return userHome + "/games/SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
1621
}
1722

18-
func TestBspTracer_BadFile(t *testing.T) {
23+
func TestBspTracer_NonExisting(t *testing.T) {
1924
t.Parallel()
2025

21-
_, err := LoadMapFromFileSystem("./map_test.go")
26+
_, err := bsptracer.LoadMapFromFileSystem("../../testdata/does_not_exist.bsp")
2227
assert.NotNil(t, err)
2328
}
2429

25-
func TestLoadMap_de_cache(t *testing.T) {
30+
func TestBspTracer_BadFile(t *testing.T) {
2631
t.Parallel()
2732

28-
m, err := LoadMapFromFileSystem("../../testdata/de_cache.bsp")
29-
assert.Error(t, err)
30-
assert.ErrorAs(t, err, new(MissingModelsError))
31-
32-
assert.Equal(t, 5560, len(m.brushes))
33-
assert.Equal(t, 39815, len(m.brushSides))
34-
assert.Equal(t, 129415, len(m.edges))
35-
assert.Equal(t, 24072, len(m.leafBrushes))
36-
assert.Equal(t, 18843, len(m.leafFaces))
37-
assert.Equal(t, 8906, len(m.leaves))
38-
assert.Equal(t, 8648, len(m.nodes))
39-
assert.Equal(t, 30626, len(m.planes))
40-
assert.Equal(t, 23221, len(m.surfaces))
41-
assert.Equal(t, 185200, len(m.surfEdges))
42-
assert.Equal(t, 48496, len(m.vertices))
43-
assert.Equal(t, 46442, len(m.polygons))
33+
_, err := bsptracer.LoadMapFromFileSystem("./map_test.go")
34+
assert.NotNil(t, err)
4435
}
4536

4637
func TestMap_TraceRay_de_cache(t *testing.T) {
4738
t.Parallel()
4839

49-
m, err := LoadMapFromFileSystem("../../testdata/de_cache.bsp")
50-
assert.Error(t, err)
51-
assert.ErrorAs(t, err, new(MissingModelsError))
40+
csgoDir := csgoDir(t)
41+
42+
m, err := bsptracer.LoadMapFromFileSystem("../../testdata/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
43+
assert.NoError(t, err)
5244

5345
type args struct {
5446
origin mgl32.Vec3
5547
destination mgl32.Vec3
5648
}
5749
type out struct {
5850
visible bool
59-
trace Trace
51+
trace bsptracer.Trace
6052
}
6153
tests := []struct {
6254
name string
@@ -71,55 +63,57 @@ func TestMap_TraceRay_de_cache(t *testing.T) {
7163
},
7264
want: out{
7365
visible: true,
74-
trace: Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{-233, 1343, 1751}},
66+
trace: bsptracer.Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{-233, 1343, 1751}},
7567
},
7668
},
7769
{
7870
name: "T spawn -> A site",
7971
args: args{
80-
mgl32.Vec3{3306, 431, 1723},
81-
mgl32.Vec3{-233, 1343, 1751},
72+
origin: mgl32.Vec3{3306, 431, 1723},
73+
destination: mgl32.Vec3{-233, 1343, 1751},
8274
},
8375
want: out{
8476
visible: false,
85-
trace: Trace{AllSolid: true, StartSolid: true, FractionLeftSolid: 1, EndPos: mgl32.Vec3{3306, 431, 1723}, Contents: 1, NumBrushSides: 7},
77+
trace: bsptracer.Trace{AllSolid: true, StartSolid: true, FractionLeftSolid: 1, EndPos: mgl32.Vec3{3306, 431, 1723}, Contents: 1, NumBrushSides: 7},
8678
},
8779
},
8880
{
8981
name: "T spawn -> T spawn",
9082
args: args{
91-
mgl32.Vec3{3306, 431, 1723},
92-
mgl32.Vec3{3303, 431, 1723},
83+
origin: mgl32.Vec3{3306, 431, 1723},
84+
destination: mgl32.Vec3{3303, 431, 1723},
9385
},
9486
want: out{
9587
visible: true,
96-
trace: Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{3303, 431, 1723}},
88+
trace: bsptracer.Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{3303, 431, 1723}},
9789
},
9890
},
9991
{
10092
name: "through door",
10193
args: args{
102-
mgl32.Vec3{207, 1948, 1751},
103-
mgl32.Vec3{259, 2251, 1752},
94+
origin: mgl32.Vec3{207, 1948, 1751},
95+
destination: mgl32.Vec3{259, 2251, 1752},
10496
},
10597
want: out{
106-
visible: true,
107-
trace: Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{259, 2251, 1752}},
98+
visible: false,
99+
trace: bsptracer.Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{259, 2251, 1752}},
108100
},
109101
},
110102
{
111103
name: "through mid box",
112104
args: args{
113-
mgl32.Vec3{-94, 452, 1677},
114-
mgl32.Vec3{138, 396, 1677},
105+
origin: mgl32.Vec3{-94, 452, 1677},
106+
destination: mgl32.Vec3{138, 396, 1677},
115107
},
116108
want: out{
117-
visible: true,
118-
trace: Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{138, 396, 1677}},
109+
visible: false,
110+
trace: bsptracer.Trace{AllSolid: true, StartSolid: true, Fraction: 1, EndPos: mgl32.Vec3{138, 396, 1677}},
119111
},
120112
},
121113
}
122114
for _, tt := range tests {
115+
tt := tt
116+
123117
t.Run(tt.name, func(t *testing.T) {
124118
t.Parallel()
125119

@@ -133,15 +127,24 @@ func TestMap_TraceRay_de_cache(t *testing.T) {
133127
}
134128
}
135129

136-
// expects CS:GO to be installed in "$HOME/games/SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
137130
func TestLoadMap_de_cache_with_models(t *testing.T) {
138131
t.Parallel()
139132

140-
userHome, err := os.UserHomeDir()
133+
csgoDir := csgoDir(t)
134+
135+
_, err := bsptracer.LoadMapFromFileSystem("../../testdata/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
141136
assert.NoError(t, err)
137+
}
142138

143-
csgoDir := userHome + "/games/SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
139+
func BenchmarkTraceBox(b *testing.B) {
140+
csgoDir := csgoDir(b)
144141

145-
_, err = LoadMapFromFileSystem("../../testdata/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
146-
assert.NoError(t, err)
142+
m, err := bsptracer.LoadMapFromFileSystem("../../testdata/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
143+
assert.NoError(b, err)
144+
145+
b.ResetTimer()
146+
147+
for i := 0; i < b.N; i++ {
148+
assert.False(b, m.IsVisible(mgl32.Vec3{-94, 452, 1677}, mgl32.Vec3{138, 396, 1677}))
149+
}
147150
}

pkg/bsptracer/bsptracer_unit_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package bsptracer
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestLoadMap_de_cache(t *testing.T) {
10+
t.Parallel()
11+
12+
m, err := LoadMapFromFileSystem("../../testdata/de_cache.bsp")
13+
assert.Error(t, err)
14+
assert.ErrorAs(t, err, new(MissingModelsError))
15+
16+
assert.Equal(t, 5560, len(m.brushes))
17+
assert.Equal(t, 39815, len(m.brushSides))
18+
assert.Equal(t, 129415, len(m.edges))
19+
assert.Equal(t, 24072, len(m.leafBrushes))
20+
assert.Equal(t, 18843, len(m.leafFaces))
21+
assert.Equal(t, 8906, len(m.leaves))
22+
assert.Equal(t, 8648, len(m.nodes))
23+
assert.Equal(t, 30626, len(m.planes))
24+
assert.Equal(t, 23221, len(m.surfaces))
25+
assert.Equal(t, 185200, len(m.surfEdges))
26+
assert.Equal(t, 48496, len(m.vertices))
27+
assert.Equal(t, 46442, len(m.polygons))
28+
}

pkg/bsptracer/example_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package bsptracer_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
"github.com/go-gl/mathgl/mgl32"
9+
10+
"github.com/saiko-tech/bsp-tracer/pkg/bsptracer"
11+
)
12+
13+
func ExampleMap_IsVisible() {
14+
csgoDir := os.Getenv("CSGO_DIR") // should point to "SteamLibrary/steamapps/common/Counter-Strike Global Offensive"
15+
16+
m, err := bsptracer.LoadMapFromFileSystem(csgoDir+"/csgo/maps/de_cache.bsp", csgoDir+"/csgo/pak01", csgoDir+"/platform/platform_pak01")
17+
if err != nil {
18+
panic(err)
19+
}
20+
21+
fmt.Println("A site -> A site, open:", m.IsVisible(mgl32.Vec3{-12, 1444, 1751}, mgl32.Vec3{-233, 1343, 1751})) // true
22+
fmt.Println("T spawn -> A site:", m.IsVisible(mgl32.Vec3{3306, 431, 1723}, mgl32.Vec3{-233, 1343, 1751})) // false
23+
fmt.Println("mid through box:", m.IsVisible(mgl32.Vec3{-94, 452, 1677}, mgl32.Vec3{138, 396, 1677})) // false
24+
fmt.Println("T spawn -> T spawn:", m.IsVisible(mgl32.Vec3{3306, 431, 1723}, mgl32.Vec3{3300, 400, 1720})) // true
25+
}
26+
27+
func TestExample(t *testing.T) {
28+
csgoDir := csgoDir(t)
29+
30+
t.Setenv("CSGO_DIR", csgoDir)
31+
32+
ExampleMap_IsVisible()
33+
}

0 commit comments

Comments
 (0)