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
39
cmd/genblocks/main.go
Normal file
39
cmd/genblocks/main.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
f, err := os.Open("generated/reports/blocks.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var data map[string]struct {
|
||||||
|
States []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Default bool `json:"default"`
|
||||||
|
} `json:"states"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(f).Decode(&data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, _ := os.Create("internal/worldgen/generated_blocks.go")
|
||||||
|
fmt.Fprintln(out, "package worldgen")
|
||||||
|
fmt.Fprintln(out, "")
|
||||||
|
fmt.Fprintln(out, "// defaultBlockIDs maps block names to their default state ID.")
|
||||||
|
fmt.Fprintln(out, "var defaultBlockIDs = map[string]uint16{")
|
||||||
|
|
||||||
|
for name, block := range data {
|
||||||
|
for _, state := range block.States {
|
||||||
|
if state.Default {
|
||||||
|
fmt.Fprintf(out, "\t%q: %d,\n", name, state.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(out, "}")
|
||||||
|
out.Close()
|
||||||
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
||||||
decorate(c, cx, cz, seed, &surfTop, &grass, &biomeName)
|
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,7 +300,7 @@ func bedrockAt(rng chunkRand, d int) bool {
|
||||||
// decorate places simple oak trees on grassy columns. Trunks are kept two
|
// decorate places simple oak trees on grassy columns. Trunks are kept two
|
||||||
// blocks inside the chunk so the radius-2 canopy never crosses into a neighbour
|
// blocks inside the chunk so the radius-2 canopy never crosses into a neighbour
|
||||||
// (avoiding cross-chunk coordination); placement is deterministic per chunk.
|
// (avoiding cross-chunk coordination); placement is deterministic per chunk.
|
||||||
func decorate(c *Chunk, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
|
func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
|
||||||
r := newChunkRand(cx, cz, seed)
|
r := newChunkRand(cx, cz, seed)
|
||||||
|
|
||||||
placeOres(c, &r)
|
placeOres(c, &r)
|
||||||
|
|
@ -318,6 +318,9 @@ func decorate(c *Chunk, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[
|
||||||
baseY := MinY + surfTop[lx][lz] + 1
|
baseY := MinY + surfTop[lx][lz] + 1
|
||||||
placeOak(c, lx, baseY, lz, &r)
|
placeOak(c, lx, baseY, lz, &r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Place large structures like villages and strongholds
|
||||||
|
worldgen.PlaceStructures(c, od, cx, cz, seed, surfTop, biomeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
||||||
|
|
|
||||||
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]
|
||||||
|
}
|
||||||
14
scratch.go
14
scratch.go
|
|
@ -1,17 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"regionio/internal/worldgen"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
f, _ := os.Open("generated/reports/blocks.json")
|
tmpl, _ := worldgen.LoadTemplate("internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt")
|
||||||
var data map[string]interface{}
|
for i, b := range tmpl.Palette {
|
||||||
json.NewDecoder(f).Decode(&data)
|
fmt.Printf("Palette[%d] = %d\n", i, b)
|
||||||
|
}
|
||||||
glowstone := data["minecraft:glowstone"]
|
|
||||||
b, _ := json.MarshalIndent(glowstone, "", " ")
|
|
||||||
fmt.Println("glowstone:", string(b))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue