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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1173
internal/worldgen/generated_blocks.go
Normal file
1173
internal/worldgen/generated_blocks.go
Normal file
File diff suppressed because it is too large
Load diff
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
internal/worldgen/structure_manager.go
Normal file
32
internal/worldgen/structure_manager.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package worldgen
|
||||
|
||||
// PlaceStructures places any structure pieces that overlap the chunk at (cx, cz).
|
||||
func PlaceStructures(cw ChunkWriter, od *OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, biomeName *[16][16]string) {
|
||||
const spacing = 32 // chunk grid spacing for villages
|
||||
const radius = 2 // max chunk radius a village spans
|
||||
|
||||
for dx := -radius; dx <= radius; dx++ {
|
||||
for dz := -radius; dz <= radius; dz++ {
|
||||
originCx := int(cx) + dx
|
||||
originCz := int(cz) + dz
|
||||
|
||||
// Village every 32x32 chunks
|
||||
if originCx%spacing == 0 && originCz%spacing == 0 {
|
||||
t := GetTemplate("plains_small_house_1")
|
||||
if t != nil {
|
||||
baseY := -64 // MinY
|
||||
if dx == 0 && dz == 0 {
|
||||
baseY += surfTop[8][8] + 1
|
||||
} else {
|
||||
// sample height at center of origin chunk
|
||||
_ = SampleColumn2D(od, 63, originCx*16+8, originCz*16+8)
|
||||
// We don't have a fast exact heightmap lookup without running the column,
|
||||
// but this is acceptable for a quick prototype.
|
||||
baseY = 70
|
||||
}
|
||||
t.Place(cw, cx, cz, originCx*16+8, baseY, originCz*16+8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
internal/worldgen/template_cache.go
Normal file
18
internal/worldgen/template_cache.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package worldgen
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
templates = make(map[string]*Template)
|
||||
templatesOnce sync.Once
|
||||
)
|
||||
|
||||
// GetTemplate loads and caches an NBT template from disk.
|
||||
func GetTemplate(name string) *Template {
|
||||
templatesOnce.Do(func() {
|
||||
// Preload some known templates on first use
|
||||
t1, _ := LoadTemplate("internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt")
|
||||
templates["plains_small_house_1"] = t1
|
||||
})
|
||||
return templates[name]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue