diff --git a/basic_functionality_test.go b/basic_functionality_test.go new file mode 100644 index 0000000..f2186cb --- /dev/null +++ b/basic_functionality_test.go @@ -0,0 +1,34 @@ +package main + +import ( + "math/rand" + "testing" + "time" + + "git.nicholasnovak.io/nnovak/spatial-db/storage" + "git.nicholasnovak.io/nnovak/spatial-db/world" +) + +func BenchmarkInsertSomePoints(b *testing.B) { + var server storage.SimpleServer + + points := make([]world.BlockPos, b.N) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + for i := 0; i < b.N; i++ { + points[i] = world.BlockPos{ + X: int(r.NormFloat64()), + Y: uint(r.NormFloat64()), + Z: int(r.NormFloat64()), + } + } + + b.ResetTimer() + + for _, point := range points { + if err := server.ChangeBlock(point, world.Generic); err != nil { + b.Error(err) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8b0a456 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.nicholasnovak.io/nnovak/spatial-db + +go 1.21.3 + +require github.com/deckarep/golang-set/v2 v2.3.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6b34ec6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..72269b3 --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "git.nicholasnovak.io/nnovak/spatial-db/storage" +) + +func main() { + server := storage.SimpleServer{} + _ = server +} diff --git a/storage/file_operations.go b/storage/file_operations.go new file mode 100644 index 0000000..c7a191c --- /dev/null +++ b/storage/file_operations.go @@ -0,0 +1,18 @@ +package storage + +import ( + "encoding/json" + "os" + + "git.nicholasnovak.io/nnovak/spatial-db/world" +) + +func ReadChunkFromFile(chunkFile *os.File) (world.ChunkData, error) { + var chunkData world.ChunkData + + if err := json.NewDecoder(chunkFile).Decode(&chunkData); err != nil { + return chunkData, err + } + + return chunkData, nil +} diff --git a/storage/simple_server.go b/storage/simple_server.go new file mode 100644 index 0000000..79cb7c8 --- /dev/null +++ b/storage/simple_server.go @@ -0,0 +1,79 @@ +package storage + +import ( + "encoding/json" + "errors" + "io/fs" + "os" + + "git.nicholasnovak.io/nnovak/spatial-db/world" +) + +const fileCacheSize = 8 + +type SimpleServer struct { +} + +// Filesystem operations + +func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) { + chunkFileName := pos.ToFileName() + + var chunkData world.ChunkData + + chunkFile, err := os.Open(chunkFileName) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + // There was no chunk that exists, create a blank one + chunkFile, err = os.Create(chunkFileName) + if err != nil { + return chunkData, err + } + + // Initilize the file with some blank data + if err := json.NewEncoder(chunkFile).Encode(chunkData); err != nil { + return chunkData, err + } + } else { + return chunkData, err + } + } + + return ReadChunkFromFile(chunkFile) +} + +// Voxel server implementation + +func (s *SimpleServer) ChangeBlock( + worldPosition world.BlockPos, + targetState world.BlockID, +) error { + chunk, err := s.FetchChunk(worldPosition.ToChunkPos()) + if err != nil { + return err + } + + chunk.SectionFor(worldPosition).UpdateBlock(worldPosition, targetState) + + return nil +} + +func (s *SimpleServer) ChangeBlockRange( + targetState world.BlockID, + start, end world.BlockPos, +) error { + panic("ChangeBlockRange is unimplemented") +} + +func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) { + chunk, err := s.FetchChunk(pos.ToChunkPos()) + if err != nil { + return world.Empty, err + } + + return chunk.SectionFor(pos).FetchBlock(pos), nil +} + +func (s *SimpleServer) ReadChunkAt(pos world.ChunkPos) (world.ChunkData, error) { + return s.FetchChunk(pos) +} diff --git a/storage/storage_server.go b/storage/storage_server.go new file mode 100644 index 0000000..ce83281 --- /dev/null +++ b/storage/storage_server.go @@ -0,0 +1,13 @@ +package storage + +import "git.nicholasnovak.io/nnovak/spatial-db/world" + +type StorageServer interface { + // Individual block-level interactions + ChangeBlock(targetState world.BlockID, world_position world.BlockPos) error + ChangeBlockRange(targetState world.BlockID, start, end world.BlockPos) error + ReadBlockAt(pos world.BlockPos) error + + // Network-level operations + ReadChunkAt(pos world.ChunkPos) error +} diff --git a/world/data_format.go b/world/data_format.go new file mode 100644 index 0000000..393acf6 --- /dev/null +++ b/world/data_format.go @@ -0,0 +1,114 @@ +package world + +import ( + "fmt" +) + +const ( + // Slice size is the total number of blocks in a horizontal slice of a chunk + sliceSize = 16 * 16 +) + +type BlockPos struct { + X int `json:"x"` + Y uint `json:"y"` + Z int `json:"z"` +} + +func (b BlockPos) ToChunkPos() ChunkPos { + return ChunkPos{ + X: b.X / 16, + Z: b.Z / 16, + } +} + +type ChunkData struct { + Pos ChunkPos `json:"pos"` + Sections [16]ChunkSection `json:"sections"` +} + +func (cd *ChunkData) SectionFor(pos BlockPos) *ChunkSection { + return &cd.Sections[pos.Y%16] +} + +type ChunkPos struct { + X int `json:"x"` + Z int `json:"z"` +} + +func (cp *ChunkPos) ToFileName() string { + return fmt.Sprintf("p.%d.%d.chunk", cp.X, cp.Z) +} + +type ChunkSection struct { + // The count of full blocks in the chunk + BlockCount uint `json:"block_count"` + BlockStates [16 * 16 * 16]BlockID `json:"block_states"` +} + +func rem_euclid(a, b int) int { + return (a%b + b) % b +} + +func (cs *ChunkSection) IndexOfBlock(pos BlockPos) int { + baseX := rem_euclid(pos.X, 16) + baseY := rem_euclid(int(pos.Y), 16) + baseZ := rem_euclid(pos.Z, 16) + + return (baseY * sliceSize) + (baseZ * 16) + baseX +} + +func (cs *ChunkSection) UpdateBlockAtIndex(index int, targetState BlockID) { + // TODO: Keep track of the block count + + cs.BlockStates[index] = targetState +} + +func (cs *ChunkSection) UpdateBlock(pos BlockPos, targetState BlockID) { + cs.BlockStates[cs.IndexOfBlock(pos)] = targetState +} + +func (cs *ChunkSection) FetchBlock(pos BlockPos) BlockID { + return cs.BlockStates[cs.IndexOfBlock(pos)] +} + +type BlockID uint8 + +const ( + Empty BlockID = iota + Generic +) + +func (b *BlockID) UnmarshalJSON(data []byte) error { + idName := string(data) + + if len(idName) < 2 { + return fmt.Errorf("error decoding blockid, input was too short") + } + + switch idName[1 : len(idName)-1] { + case "Empty": + *b = Empty + case "Generic": + *b = Generic + default: + return fmt.Errorf("unknown block id: %s", string(data)) + } + + return nil +} + +func (b BlockID) MarshalJSON() ([]byte, error) { + var encoded []byte + + switch b { + case Empty: + encoded = []byte("\"Empty\"") + case Generic: + encoded = []byte("\"Generic\"") + default: + return []byte{}, fmt.Errorf("could not turn block id %d into data", b) + } + + return encoded, nil +}