From 840320236e245efdcb999c041de91b93d278a737 Mon Sep 17 00:00:00 2001 From: Galaco Date: Tue, 26 Mar 2019 13:38:50 +0000 Subject: [PATCH] Support for Leafv0 format in pre-v20 bsps --- README.md | 15 +- bsp.go | 14 +- bsp_test.go | 10 +- internal/versions/v20.go | 128 +++++++++--------- internal/versions/versions.go | 2 +- lump.go | 25 ++-- lump_test.go | 10 +- lumps/interface.go | 2 + lumps/leaf.go | 49 ++++++- lumps/leaf_test.go | 11 +- primitives/common/common.go | 6 + primitives/leaf/leaf.go | 10 +- .../leafambientlighting.go | 8 +- reader_test.go | 2 +- writer.go | 4 +- 15 files changed, 171 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index f8df67a..f6f45c2 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,16 @@ [![CircleCI](https://circleci.com/gh/Galaco/bsp.svg?style=svg)](https://circleci.com/gh/Galaco/bsp) # Bsp -Go library for manipulating Source Engine .bsp map files. +Go library for handling Source Engine .bsp map files. ### Features: -* Read support for (most) non-xbox360 bsps. -* Freely modify and resize any Lump data. -* Limited write support +* Read support for (most) non-xbox360 bsps (v20,21). v19 support limited, may work +* Freely modify and resize any Lump data +* Limited write support, mostly untested ##### Not all lumps are current supported, but can be freely read and modified, as they are treated as `[]byte` -The following lumps currently have a full implementation for v20 bsp's (tested against CS:S & CS:GO): +The following lumps currently have a full implementation for v20 & v21 bsp's (tested against CS:S & CS:GO): ``` 0: Entdata @@ -82,6 +82,7 @@ package main import ( "github.com/galaco/bsp" + "github.com/galaco/bsp/lumps" "log" "os" ) @@ -99,15 +100,15 @@ func main() { } f.Close() - lump := file.GetLump(bsp.LUMP_ENTITIES).(*lump.Entities) + lump := file.Lump(bsp.LumpEntities).(*lumps.Entities) log.Println(lump.GetData()) } ``` ## Real World examples +* Proof of concept BSP viewer: [https://github.com/Galaco/Lambda-Client](https://github.com/Galaco/Lambda-Client) * Insert game_text newline placeholder characters (avoids Hammer crash) as a compile step: [https://github.com/Galaco/CS-GO-game_text-newline-inserter/tree/golang](https://github.com/Galaco/CS-GO-game_text-newline-inserter/tree/golang) * Bspzip filelist generator from a mountable resource directory: [https://github.com/Galaco/bspzip-traverser](https://github.com/Galaco/bspzip-traverser) -* Proof of concept BSP viewer: [https://github.com/Galaco/Lambda-Client](https://github.com/Galaco/Lambda-Client) # Contributing diff --git a/bsp.go b/bsp.go index 5313886..23ba5a0 100644 --- a/bsp.go +++ b/bsp.go @@ -26,18 +26,18 @@ type HeaderLump struct { Id [4]byte } -// GetHeader gets the header for a bsp. -func (bsp *Bsp) GetHeader() Header { +// Header gets the header for a bsp. +func (bsp *Bsp) Header() Header { return bsp.header } -// GetLump gets the lump for a given id. -func (bsp *Bsp) GetLump(index LumpId) lumps.ILump { - return bsp.GetLumpRaw(index).GetContents() +// Lump gets the lump for a given id. +func (bsp *Bsp) Lump(index LumpId) lumps.ILump { + return bsp.RawLump(index).Contents() } -// GetLumpRaw gets the lump for a given id. -func (bsp *Bsp) GetLumpRaw(index LumpId) *Lump { +// RawLump gets the lump for a given id. +func (bsp *Bsp) RawLump(index LumpId) *Lump { return &bsp.lumps[int(index)] } diff --git a/bsp_test.go b/bsp_test.go index b57e2d8..652d3a0 100644 --- a/bsp_test.go +++ b/bsp_test.go @@ -17,17 +17,17 @@ func TestLumpExports(t *testing.T) { // Verify lump lengths lumpIndex := 0 for lumpIndex < 64 { - lump := file.GetLump(LumpId(lumpIndex)) - rawLump := file.GetLumpRaw(LumpId(lumpIndex)) + lump := file.Lump(LumpId(lumpIndex)) + rawLump := file.RawLump(LumpId(lumpIndex)) lumpBytes, err := lump.Marshall() if err != nil { t.Error(err) } - if len(lumpBytes) != int(file.GetHeader().Lumps[lumpIndex].Length) { + if len(lumpBytes) != int(file.Header().Lumps[lumpIndex].Length) { t.Errorf("Lump: %d length mismatch. Got: %dbytes, expected: %dbytes", lumpIndex, len(lumpBytes), file.header.Lumps[lumpIndex].Length) } else { - log.Printf("Index: %d, Expected: %d, Actual: %d\n", lumpIndex, len(rawLump.GetRawContents()), len(lumpBytes)) - if !bytes.Equal(lumpBytes, rawLump.GetRawContents()) { + log.Printf("Index: %d, Expected: %d, Actual: %d\n", lumpIndex, len(rawLump.RawContents()), len(lumpBytes)) + if !bytes.Equal(lumpBytes, rawLump.RawContents()) { t.Errorf("Lump: %d data mismatch", lumpIndex) } } diff --git a/internal/versions/v20.go b/internal/versions/v20.go index 7b8352c..8a04828 100644 --- a/internal/versions/v20.go +++ b/internal/versions/v20.go @@ -11,133 +11,133 @@ func Getv20Lump(index int) (lumps.ILump, error) { var err error switch index { case 0: - ret = &lumps.EntData{} + ret = new(lumps.EntData) case 1: - ret = &lumps.Planes{} + ret = new(lumps.Planes) case 2: - ret = &lumps.TexData{} + ret = new(lumps.TexData) case 3: - ret = &lumps.Vertex{} + ret = new(lumps.Vertex) case 4: - ret = &lumps.Visibility{} + ret = new(lumps.Visibility) case 5: - ret = &lumps.Node{} + ret = new(lumps.Node) case 6: - ret = &lumps.TexInfo{} + ret = new(lumps.TexInfo) case 7: - ret = &lumps.Face{} + ret = new(lumps.Face) case 8: - ret = &lumps.Lighting{} + ret = new(lumps.Lighting) case 9: - ret = &lumps.Occlusion{} + ret = new(lumps.Occlusion) case 10: - ret = &lumps.Leaf{} + ret = new(lumps.Leaf) case 11: - ret = &lumps.FaceId{} + ret = new(lumps.FaceId) case 12: - ret = &lumps.Edge{} + ret = new(lumps.Edge) case 13: - ret = &lumps.Surfedge{} + ret = new(lumps.Surfedge) case 14: - ret = &lumps.Model{} + ret = new(lumps.Model) case 15: - ret = &lumps.WorldLight{} + ret = new(lumps.WorldLight) case 16: - ret = &lumps.LeafFace{} + ret = new(lumps.LeafFace) case 17: - ret = &lumps.LeafBrush{} + ret = new(lumps.LeafBrush) case 18: - ret = &lumps.Brush{} + ret = new(lumps.Brush) case 19: - ret = &lumps.BrushSide{} + ret = new(lumps.BrushSide) case 20: - ret = &lumps.Area{} + ret = new(lumps.Area) case 21: - ret = &lumps.AreaPortal{} + ret = new(lumps.AreaPortal) case 22: - ret = &lumps.Unimplemented{} //portals | unused0 | propcollision + ret = new(lumps.Unimplemented) //portals | unused0 | propcollision case 23: - ret = &lumps.Unimplemented{} //clusters | unused1 | prophulls + ret = new(lumps.Unimplemented) //clusters | unused1 | prophulls case 24: - ret = &lumps.Unimplemented{} //portalverts | unused2 | prophullverts + ret = new(lumps.Unimplemented) //portalverts | unused2 | prophullverts case 25: - ret = &lumps.Unimplemented{} //clusterportals | unused3 | proptris + ret = new(lumps.Unimplemented) //clusterportals | unused3 | proptris case 26: - ret = &lumps.DispInfo{} + ret = new(lumps.DispInfo) case 27: - ret = &lumps.Face{} + ret = new(lumps.Face) case 28: - ret = &lumps.PhysDisp{} + ret = new(lumps.PhysDisp) case 29: - ret = &lumps.Unimplemented{} //physcollide - IN PROGRESS + ret = new(lumps.Unimplemented) //physcollide - IN PROGRESS case 30: - ret = &lumps.VertNormal{} + ret = new(lumps.VertNormal) case 31: - ret = &lumps.VertNormalIndice{} + ret = new(lumps.VertNormalIndice) case 32: - ret = &lumps.Unimplemented{} //disp lightmap alphas - IS STRIPPED ANYWAY? + ret = new(lumps.Unimplemented) //disp lightmap alphas - IS STRIPPED ANYWAY? case 33: - ret = &lumps.DispVert{} + ret = new(lumps.DispVert) case 34: - ret = &lumps.DispLightmapSamplePosition{} + ret = new(lumps.DispLightmapSamplePosition) case 35: - ret = &lumps.Game{} + ret = new(lumps.Game) case 36: - ret = &lumps.LeafWaterData{} + ret = new(lumps.LeafWaterData) case 37: - ret = &lumps.Unimplemented{} //primitives FIXME - Appears to be 4bytes unaccounted for at end of lump? + ret = new(lumps.Unimplemented) //primitives FIXME - Appears to be 4bytes unaccounted for at end of lump? case 38: - ret = &lumps.PrimVert{} + ret = new(lumps.PrimVert) case 39: - ret = &lumps.PrimIndice{} + ret = new(lumps.PrimIndice) case 40: - ret = &lumps.Pakfile{} //pakfile + ret = new(lumps.Pakfile) //pakfile case 41: - ret = &lumps.ClipPortalVerts{} + ret = new(lumps.ClipPortalVerts) case 42: - ret = &lumps.Cubemap{} + ret = new(lumps.Cubemap) case 43: - ret = &lumps.TexDataStringData{} + ret = new(lumps.TexDataStringData) case 44: - ret = &lumps.TexDataStringTable{} + ret = new(lumps.TexDataStringTable) case 45: - ret = &lumps.Overlay{} + ret = new(lumps.Overlay) case 46: - ret = &lumps.LeafMinDistToWater{} + ret = new(lumps.LeafMinDistToWater) case 47: - ret = &lumps.FaceMacroTextureInfo{} + ret = new(lumps.FaceMacroTextureInfo) case 48: - ret = &lumps.DispTris{} + ret = new(lumps.DispTris) case 49: - ret = &lumps.Unimplemented{} //physcollidesurface | prop blob + ret = new(lumps.Unimplemented) //physcollidesurface | prop blob case 50: - ret = &lumps.Unimplemented{} //wateroverlays + ret = new(lumps.Unimplemented) //wateroverlays case 51: - ret = &lumps.LeafAmbientIndexHDR{} + ret = new(lumps.LeafAmbientIndexHDR) case 52: - ret = &lumps.LeafAmbientIndex{} + ret = new(lumps.LeafAmbientIndex) case 53: - ret = &lumps.Unimplemented{} //lighting hdr + ret = new(lumps.Unimplemented) //lighting hdr case 54: - ret = &lumps.WorldLightHDR{} //worldlights hdr + ret = new(lumps.WorldLightHDR) //worldlights hdr case 55: - ret = &lumps.LeafAmbientLightingHDR{} + ret = new(lumps.LeafAmbientLightingHDR) case 56: - ret = &lumps.LeafAmbientLighting{} //leaf ambient lighting + ret = new(lumps.LeafAmbientLighting) //leaf ambient lighting case 57: - ret = &lumps.Unimplemented{} //xzippakfile + ret = new(lumps.Unimplemented) //xzippakfile case 58: - ret = &lumps.FaceHDR{} + ret = new(lumps.FaceHDR) case 59: - ret = &lumps.MapFlags{} + ret = new(lumps.MapFlags) case 60: - ret = &lumps.OverlayFade{} + ret = new(lumps.OverlayFade) case 61: - ret = &lumps.Unimplemented{} //overlay system levels + ret = new(lumps.Unimplemented) //overlay system levels case 62: - ret = &lumps.Unimplemented{} //physlevel + ret = new(lumps.Unimplemented) //physlevel case 63: - ret = &lumps.Unimplemented{} //disp multiblend + ret = new(lumps.Unimplemented) //disp multiblend default: err = errors.New("invalid lump id") } diff --git a/internal/versions/versions.go b/internal/versions/versions.go index de4b9d7..ccbad56 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -14,6 +14,6 @@ func GetLumpForVersion(bspVersion int, lumpId int) (lumps.ILump, error) { case 21: return Getv21Lump(lumpId) default: - return &lumps.Unimplemented{}, nil + return new(lumps.Unimplemented), nil } } diff --git a/lump.go b/lump.go index 283a8d5..94905a3 100644 --- a/lump.go +++ b/lump.go @@ -17,15 +17,15 @@ type Lump struct { loaded bool } -// SetId Get lump identifier +// SetId sets lump identifier // Id is the lump type id (not the id for the order the lumps are stored) func (l *Lump) SetId(index LumpId) { l.id = index } -// GetContents Get the contents of a lump. +// Contents Get the contents of a lump. // NOTE: Will need to be cast to the relevant lumps -func (l *Lump) GetContents() lumps.ILump { +func (l *Lump) Contents() lumps.ILump { if !l.loaded { if l.data.Unmarshall(l.raw) != nil { return nil @@ -41,9 +41,9 @@ func (l *Lump) SetContents(data lumps.ILump) { l.loaded = false } -// GetRawContents Get the raw []byte contents of a lump. -// N.B. This is the raw imported value. To get the raw value of a modified lump, use GetContents().Marshall() -func (l *Lump) GetRawContents() []byte { +// RawContents Get the raw []byte contents of a lump. +// N.B. This is the raw imported value. To get the raw value of a modified lump, use Contents().Marshall() +func (l *Lump) RawContents() []byte { return l.raw } @@ -52,8 +52,8 @@ func (l *Lump) SetRawContents(raw []byte) { l.raw = raw } -// GetLength Get length of a lump in bytes. -func (l *Lump) GetLength() int32 { +// Length Get length of a lump in bytes. +func (l *Lump) Length() int32 { return l.length } @@ -63,5 +63,12 @@ func getReferenceLumpByIndex(index int, version int32) (lumps.ILump, error) { return nil, fmt.Errorf("invalid lump id: %d provided", index) } - return versions.GetLumpForVersion(int(version), index) + l, err := versions.GetLumpForVersion(int(version), index) + if err != nil { + return nil, err + } + + l.SetVersion(version) + + return l, nil } diff --git a/lump_test.go b/lump_test.go index c316d56..4f2acfe 100644 --- a/lump_test.go +++ b/lump_test.go @@ -20,19 +20,19 @@ func TestGetReferenceLumpByIndex(t *testing.T) { } } -func TestLump_GetContents(t *testing.T) { +func TestLump_Contents(t *testing.T) { } -func TestLump_GetLength(t *testing.T) { +func TestLump_Length(t *testing.T) { sut := Lump{} sut.length = 32 - if sut.GetLength() != 32 { + if sut.Length() != 32 { t.Error("incorrect length returned for lump") } } -func TestLump_GetRawContents(t *testing.T) { +func TestLump_RawContents(t *testing.T) { t.Skip() } @@ -53,7 +53,7 @@ func TestLump_SetRawContents(t *testing.T) { sut := Lump{} data := []byte{0, 1, 4, 3, 2} sut.SetRawContents(data) - for idx, b := range sut.GetRawContents() { + for idx, b := range sut.RawContents() { if data[idx] != b { t.Error("raw lump data mismatch") } diff --git a/lumps/interface.go b/lumps/interface.go index 7c0efdd..fcdca8d 100644 --- a/lumps/interface.go +++ b/lumps/interface.go @@ -8,4 +8,6 @@ type ILump interface { // Marshall Exports lump structure back to []byte. Marshall() ([]byte, error) + + SetVersion(version int32) } diff --git a/lumps/leaf.go b/lumps/leaf.go index 9447b00..a8037b8 100644 --- a/lumps/leaf.go +++ b/lumps/leaf.go @@ -3,8 +3,9 @@ package lumps import ( "bytes" "encoding/binary" + "fmt" + "github.com/galaco/bsp/primitives/common" primitives "github.com/galaco/bsp/primitives/leaf" - "log" "unsafe" ) @@ -13,6 +14,11 @@ const ( MaxMapLeafs = 65536 ) +const ( + // maxBspVersionOfV0Leaf is the last bsp revision to use the old leafv0 structure + maxBspVersionOfV0Leaf = 19 +) + // Leaf is Lump 10: Leaf type Leaf struct { Generic @@ -21,22 +27,36 @@ type Leaf struct { // Unmarshall Imports this lump from raw byte data func (lump *Leaf) Unmarshall(raw []byte) (err error) { - length := len(raw) - lump.data = make([]primitives.Leaf, length/int(unsafe.Sizeof(primitives.Leaf{}))) + // There are 2 version of leaf: + // v0 contains a light sample + // v1 removes the light sample, and is padded by 2 bytes structSize := int(unsafe.Sizeof(primitives.Leaf{})) + if lump.Version() > maxBspVersionOfV0Leaf { + // Correct size of v1+ leafs + structSize -= int(unsafe.Sizeof(common.CompressedLightCube{})) + } + + length := len(raw) + lump.data = make([]primitives.Leaf, length/structSize) numLeafs := len(lump.data) i := 0 + for i < numLeafs { - err = binary.Read(bytes.NewBuffer(raw[(structSize*i):(structSize*i)+structSize]), binary.LittleEndian, &lump.data[i]) + rawLeaf := bytes.NewBuffer(raw[(structSize * i) : (structSize*i)+structSize]) + // Pad the raw data to the correct size of a leaf + if lump.Version() > maxBspVersionOfV0Leaf { + rawLeaf.Write(make([]byte, int(unsafe.Sizeof(common.CompressedLightCube{})))) + } + err = binary.Read(rawLeaf, binary.LittleEndian, &lump.data[i]) if err != nil { return err } i++ if i > MaxMapLeafs { - log.Fatalf("Leaf count overflows maximum allowed size of %d\n", MaxMapLeafs) + return fmt.Errorf("leaf count overflows maximum allowed size of %d", MaxMapLeafs) } } - lump.Metadata.SetLength(length) + lump.SetLength(length) return err } @@ -49,6 +69,21 @@ func (lump *Leaf) GetData() []primitives.Leaf { // Marshall dumps this lump back to raw byte data func (lump *Leaf) Marshall() ([]byte, error) { var buf bytes.Buffer - err := binary.Write(&buf, binary.LittleEndian, lump.data) + var err error + + switch lump.Version() { + case 0: + err = binary.Write(&buf, binary.LittleEndian, lump.data) + default: + structSize := int(unsafe.Sizeof(primitives.Leaf{})) - int(unsafe.Sizeof(common.CompressedLightCube{})) + for _, l := range lump.data { + var leafBuf bytes.Buffer + if err = binary.Write(&leafBuf, binary.LittleEndian, l); err != nil { + return nil, err + } + err = binary.Write(&buf, binary.LittleEndian, leafBuf.Bytes()[0:structSize]) + } + } + return buf.Bytes(), err } diff --git a/lumps/leaf_test.go b/lumps/leaf_test.go index fd7acb8..66bb1ce 100644 --- a/lumps/leaf_test.go +++ b/lumps/leaf_test.go @@ -4,25 +4,18 @@ import ( primitives "github.com/galaco/bsp/primitives/leaf" "log" "testing" - "unsafe" ) -const leafStructSizeRaw = 32 - // Assert leaf data when read from bytes is valid func TestLeafUnmarshall(t *testing.T) { - l := unsafe.Sizeof(primitives.Leaf{}) - - if l != leafStructSizeRaw { - t.Errorf("Leaf struct is of incorrect size, expected: %d, actual: %d", leafStructSizeRaw, l) - } - lump := Leaf{} + lump.SetVersion(20) err := lump.Unmarshall(GetTestDataBytes()) if err != nil { t.Error(err) } expected := GetTestLeafData() + log.Println(lump) actual := lump.GetData()[0] if actual != expected { diff --git a/primitives/common/common.go b/primitives/common/common.go index 77b5fcb..fa87e0d 100644 --- a/primitives/common/common.go +++ b/primitives/common/common.go @@ -104,3 +104,9 @@ type MapDispInfo struct { // Elevation float32 // OffsetNormals []Vector } + +// CompressedLightCube +type CompressedLightCube struct { + // Color + Color [6]ColorRGBExponent32 +} diff --git a/primitives/leaf/leaf.go b/primitives/leaf/leaf.go index feb6e95..acd5e6e 100644 --- a/primitives/leaf/leaf.go +++ b/primitives/leaf/leaf.go @@ -1,5 +1,9 @@ package leaf +import ( + "github.com/galaco/bsp/primitives/common" +) + const bitmaskLower9 = 0x1FF // 511 (2^9 - 1) const bitmaskLower7 = 0x7F // 127 (2^7 - 1) @@ -26,7 +30,11 @@ type Leaf struct { NumLeafBrushes uint16 // LeafWaterDataID LeafWaterDataID int16 - _ [2]byte + + // LightSample + LightSample common.CompressedLightCube + + _ [2]byte } // Area returns area (first 9 bits) diff --git a/primitives/leafambientlighting/leafambientlighting.go b/primitives/leafambientlighting/leafambientlighting.go index a43e13c..6f207cf 100644 --- a/primitives/leafambientlighting/leafambientlighting.go +++ b/primitives/leafambientlighting/leafambientlighting.go @@ -5,7 +5,7 @@ import primitives "github.com/galaco/bsp/primitives/common" // LeafAmbientLighting type LeafAmbientLighting struct { // Cube - Cube CompressedLightCube + Cube primitives.CompressedLightCube // X x X byte // Y y @@ -15,9 +15,3 @@ type LeafAmbientLighting struct { // Pad is padding to 4 bytes (any other purpose unknown) Pad byte } - -// CompressedLightCube -type CompressedLightCube struct { - // Color - Color [6]primitives.ColorRGBExponent32 -} diff --git a/reader_test.go b/reader_test.go index 0881a0b..953beaf 100644 --- a/reader_test.go +++ b/reader_test.go @@ -26,5 +26,5 @@ func TestReadFromStream(t *testing.T) { t.Error(err) } - r.GetLump(LumpGame).(*lumps.Game).GetStaticPropLump() + r.Lump(LumpGame).(*lumps.Game).GetStaticPropLump() } diff --git a/writer.go b/writer.go index 1fd024d..c22c885 100644 --- a/writer.go +++ b/writer.go @@ -32,7 +32,7 @@ func (w *Writer) Write() ([]byte, error) { // We have to handle lump 35 (GameData differently) // Because valve mis-designed the file format and relatively positioned data contains absolute file offsets. if index == LumpGame { - gamelump := w.data.lumps[int(index)].GetContents().(*lumps.Game) + gamelump := w.data.lumps[int(index)].Contents().(*lumps.Game) w.data.lumps[int(index)].SetContents( gamelump.UpdateInternalOffsets(int32(currentOffset) - w.data.header.Lumps[int(index)].Offset)) } @@ -75,7 +75,7 @@ func (w *Writer) Write() ([]byte, error) { // WriteLump Exports a single lump to []byte. func (w *Writer) WriteLump(index LumpId) ([]byte, error) { - lump := w.data.GetLump(index) + lump := w.data.Lump(index) return lump.Marshall() }