Add StructureManager and NBT template loader for large structures
This commit is contained in:
parent
e33370a049
commit
788983dc88
12 changed files with 1435 additions and 11 deletions
163
internal/worldgen/structure.go
Normal file
163
internal/worldgen/structure.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package worldgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"regionio/internal/nbt"
|
||||
)
|
||||
|
||||
type ChunkWriter interface {
|
||||
SetBlock(lx, y, lz int, state uint16)
|
||||
GetBlock(lx, y, lz int) uint16
|
||||
}
|
||||
|
||||
// Template represents a loaded NBT structure.
|
||||
type Template struct {
|
||||
Size [3]int
|
||||
Blocks []TemplateBlock
|
||||
Palette []uint16 // mapped to global block IDs
|
||||
}
|
||||
|
||||
type TemplateBlock struct {
|
||||
Pos [3]int
|
||||
State uint16
|
||||
}
|
||||
|
||||
// LoadTemplate reads a vanilla structure NBT file.
|
||||
func LoadTemplate(path string) (*Template, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NBT files are usually gzipped.
|
||||
var r io.Reader = bytes.NewReader(b)
|
||||
if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b {
|
||||
gr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gr.Close()
|
||||
r = gr
|
||||
}
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, root, err := nbt.UnmarshalNamed(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comp, ok := root.(*nbt.Compound)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("root is not a compound")
|
||||
}
|
||||
|
||||
tmpl := &Template{}
|
||||
|
||||
if tag, ok := comp.Get("size"); ok {
|
||||
if sizeList, ok := tag.(nbt.List); ok && len(sizeList.Elems) == 3 {
|
||||
tmpl.Size[0] = int(sizeList.Elems[0].(nbt.Int))
|
||||
tmpl.Size[1] = int(sizeList.Elems[1].(nbt.Int))
|
||||
tmpl.Size[2] = int(sizeList.Elems[2].(nbt.Int))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse palette
|
||||
if tag, ok := comp.Get("palette"); ok {
|
||||
if paletteList, ok := tag.(nbt.List); ok {
|
||||
for _, v := range paletteList.Elems {
|
||||
stateComp, ok := v.(*nbt.Compound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nameTag, _ := stateComp.Get("Name")
|
||||
name := string(nameTag.(nbt.String))
|
||||
props := make(map[string]string)
|
||||
|
||||
if propTag, ok := stateComp.Get("Properties"); ok {
|
||||
if propComp, ok := propTag.(*nbt.Compound); ok {
|
||||
for _, k := range propComp.Keys() {
|
||||
pval, _ := propComp.Get(k)
|
||||
if str, ok := pval.(nbt.String); ok {
|
||||
props[k] = string(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
id := surfaceBlockID(name, props)
|
||||
if id == 0 && name != "minecraft:air" {
|
||||
// Fallback to default block ID for the name
|
||||
id = defaultBlockIDs[name]
|
||||
}
|
||||
tmpl.Palette = append(tmpl.Palette, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blocks
|
||||
if tag, ok := comp.Get("blocks"); ok {
|
||||
if blocksList, ok := tag.(nbt.List); ok {
|
||||
for _, v := range blocksList.Elems {
|
||||
blockComp, ok := v.(*nbt.Compound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
stateTag, _ := blockComp.Get("state")
|
||||
stateIdx := int(stateTag.(nbt.Int))
|
||||
|
||||
var pos [3]int
|
||||
if posTag, ok := blockComp.Get("pos"); ok {
|
||||
if posList, ok := posTag.(nbt.List); ok && len(posList.Elems) == 3 {
|
||||
pos[0] = int(posList.Elems[0].(nbt.Int))
|
||||
pos[1] = int(posList.Elems[1].(nbt.Int))
|
||||
pos[2] = int(posList.Elems[2].(nbt.Int))
|
||||
}
|
||||
}
|
||||
|
||||
if stateIdx >= 0 && stateIdx < len(tmpl.Palette) {
|
||||
state := tmpl.Palette[stateIdx]
|
||||
tmpl.Blocks = append(tmpl.Blocks, TemplateBlock{
|
||||
Pos: pos,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// Place applies the template blocks to the ChunkWriter if they fall within the given chunk boundaries.
|
||||
// cx, cz are the chunk coordinates we are currently generating.
|
||||
// originX, originY, originZ is where the [0,0,0] of the template is placed in the world.
|
||||
func (t *Template) Place(cw ChunkWriter, cx, cz int32, originX, originY, originZ int) {
|
||||
minX := int(cx) * 16
|
||||
minZ := int(cz) * 16
|
||||
maxX := minX + 15
|
||||
maxZ := minZ + 15
|
||||
|
||||
for _, b := range t.Blocks {
|
||||
wx := originX + b.Pos[0]
|
||||
wy := originY + b.Pos[1]
|
||||
wz := originZ + b.Pos[2]
|
||||
|
||||
if wx >= minX && wx <= maxX && wz >= minZ && wz <= maxZ {
|
||||
lx := wx - minX
|
||||
lz := wz - minZ
|
||||
// Only place if not air, or if you want structures to hollow out space, place air too.
|
||||
// Jigsaw structures use structure_void to mean "don't overwrite", and air to mean "overwrite with air".
|
||||
// Since we fallback to 0 for unknown blocks, we need to be careful.
|
||||
// For now, place everything.
|
||||
cw.SetBlock(lx, wy, lz, b.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue