Load vanilla feature stages and place ores
This commit is contained in:
parent
28c11e2fc2
commit
dd2ea56850
10 changed files with 779 additions and 6 deletions
128
cmd/genfeatures/main.go
Normal file
128
cmd/genfeatures/main.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Command genfeatures extracts the vanilla feature datapack subset from the
|
||||||
|
// Mojang bundler jar into one deterministic archive embedded by worldgen.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var prefixes = []string{
|
||||||
|
"data/minecraft/worldgen/configured_feature/",
|
||||||
|
"data/minecraft/worldgen/placed_feature/",
|
||||||
|
"data/minecraft/worldgen/biome/",
|
||||||
|
"data/minecraft/tags/block/",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
server := flag.String("server", "server.jar", "Mojang bundler server.jar")
|
||||||
|
output := flag.String("output", "internal/worldgen/feature_data.zip", "output archive")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
outer, err := zip.OpenReader(*server)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
defer outer.Close()
|
||||||
|
var innerBytes []byte
|
||||||
|
for _, file := range outer.File {
|
||||||
|
if strings.HasPrefix(file.Name, "META-INF/versions/") && strings.HasSuffix(file.Name, ".jar") {
|
||||||
|
innerBytes, err = readZipFile(file)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(innerBytes) == 0 {
|
||||||
|
fatal(fmt.Errorf("%s contains no inner server jar", *server))
|
||||||
|
}
|
||||||
|
inner, err := zip.NewReader(bytes.NewReader(innerBytes), int64(len(innerBytes)))
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
type entry struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
entries := make([]entry, 0, 1024)
|
||||||
|
for _, file := range inner.File {
|
||||||
|
if file.FileInfo().IsDir() || !strings.HasSuffix(file.Name, ".json") || !selected(file.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := readZipFile(file)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
entries = append(entries, entry{name: file.Name, data: data})
|
||||||
|
}
|
||||||
|
sort.Slice(entries, func(i, j int) bool { return entries[i].name < entries[j].name })
|
||||||
|
if len(entries) < 500 {
|
||||||
|
fatal(fmt.Errorf("only %d feature datapack files found; wrong server version", len(entries)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
tmp := *output + ".tmp"
|
||||||
|
f, err := os.Create(tmp)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
for _, entry := range entries {
|
||||||
|
header := &zip.FileHeader{Name: entry.name, Method: zip.Deflate}
|
||||||
|
header.SetModTime(time.Unix(0, 0).UTC())
|
||||||
|
writer, err := zw.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := writer.Write(entry.data); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.Sync(); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, *output); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("wrote %s with %d vanilla datapack files\n", *output, len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
func selected(name string) bool {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func readZipFile(file *zip.File) ([]byte, error) {
|
||||||
|
r, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatal(err error) {
|
||||||
|
fmt.Fprintln(os.Stderr, "genfeatures:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package world
|
package world
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
// Ore-vein block states, from OreVeinifier.VeinType. Copper's ore is the plain
|
// Ore-vein block states, from OreVeinifier.VeinType. Copper's ore is the plain
|
||||||
// stone variant and iron's is the deepslate one; neither switches with depth.
|
// stone variant and iron's is the deepslate one; neither switches with depth.
|
||||||
|
|
@ -21,7 +25,18 @@ const (
|
||||||
// the sign of the veininess noise but its window comes from the type, so a
|
// the sign of the veininess noise but its window comes from the type, so a
|
||||||
// single block outside a window means the guard is wrong.
|
// single block outside a window means the guard is wrong.
|
||||||
func TestOreVeins(t *testing.T) {
|
func TestOreVeins(t *testing.T) {
|
||||||
gen := NewVanillaGenerator(12345)
|
const seed = 12345
|
||||||
|
od, err := worldgen.LoadOverworldFinalDensity(seed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
picker := worldgen.OverworldFluidPicker(od.SeaLevel)
|
||||||
|
veins := worldgen.NewOreVeinifier(od)
|
||||||
|
carver, err := worldgen.NewCarver(od, seed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initCarverReplaceable(carver.ReplaceableBlocks())
|
||||||
counts := map[uint16]int{}
|
counts := map[uint16]int{}
|
||||||
lowest := map[uint16]int{}
|
lowest := map[uint16]int{}
|
||||||
highest := map[uint16]int{}
|
highest := map[uint16]int{}
|
||||||
|
|
@ -33,7 +48,10 @@ func TestOreVeins(t *testing.T) {
|
||||||
}
|
}
|
||||||
for cx := int32(-60); cx <= 60; cx += 20 {
|
for cx := int32(-60); cx <= 60; cx += 20 {
|
||||||
for cz := int32(-60); cz <= 60; cz += 20 {
|
for cz := int32(-60); cz <= 60; cz += 20 {
|
||||||
ch := gen(cx, cz)
|
// Call the terrain/material pass directly and skip decorate. Ordinary
|
||||||
|
// placed ores share several states with mega-veins and cannot be
|
||||||
|
// distinguished by block ID after decoration.
|
||||||
|
ch := generateVanillaWithoutDecoration(od, picker, veins, carver, seed, cx, cz)
|
||||||
for wy := MinY; wy < 60; wy++ {
|
for wy := MinY; wy < 60; wy++ {
|
||||||
for lx := 0; lx < 16; lx++ {
|
for lx := 0; lx < 16; lx++ {
|
||||||
for lz := 0; lz < 16; lz++ {
|
for lz := 0; lz < 16; lz++ {
|
||||||
|
|
|
||||||
190
internal/world/placed_features.go
Normal file
190
internal/world/placed_features.go
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
const undergroundOresStage = 6
|
||||||
|
|
||||||
|
type resolvedOreTarget struct {
|
||||||
|
state uint16
|
||||||
|
replaceables map[uint16]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeVanillaOres(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string) {
|
||||||
|
set, err := worldgen.LoadFeatureSet()
|
||||||
|
if err != nil {
|
||||||
|
panic("world: loading feature datapack: " + err.Error())
|
||||||
|
}
|
||||||
|
random, decorationSeed := worldgen.DecorationRandom(seed, int(cx), int(cz))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for bx := 0; bx < 16; bx += 4 {
|
||||||
|
for bz := 0; bz < 16; bz += 4 {
|
||||||
|
biome := set.Biomes[biomes[bx][bz]]
|
||||||
|
if len(biome.Features) <= undergroundOresStage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for featureIndex, name := range biome.Features[undergroundOresStage] {
|
||||||
|
if seen[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = true
|
||||||
|
placed := set.Placed[name]
|
||||||
|
configured := set.Configured[placed.Feature]
|
||||||
|
if configured.Type != "minecraft:ore" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
config, err := set.Ore(placed.Feature)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
plan, err := set.Placement(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
targets, ok := resolveOreTargets(set, config)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
random.SetFeatureSeed(decorationSeed, featureIndex, undergroundOresStage)
|
||||||
|
if plan.RarityChance > 0 && random.NextIntN(int32(plan.RarityChance)) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attempts := plan.Count.Sample(random)
|
||||||
|
for attempt := 0; attempt < attempts; attempt++ {
|
||||||
|
x := int(random.NextIntN(16))
|
||||||
|
z := int(random.NextIntN(16))
|
||||||
|
y := plan.SampleY(random, MinY, WorldHeight)
|
||||||
|
placeOreEllipsoid(c, random, x, y, z, config.Size, config.DiscardAirExposure, targets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveOreTargets(set *worldgen.FeatureSet, config worldgen.OreFeatureConfig) ([]resolvedOreTarget, bool) {
|
||||||
|
targets := make([]resolvedOreTarget, 0, len(config.Targets))
|
||||||
|
for _, target := range config.Targets {
|
||||||
|
state, ok := nameToStateID(target.State.Name, nil)
|
||||||
|
if !ok || target.Target.PredicateType != "minecraft:tag_match" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
replaceables := make(map[uint16]bool)
|
||||||
|
for _, name := range set.BlockTags[target.Target.Tag] {
|
||||||
|
id, ok := nameToStateID(name, nil)
|
||||||
|
if ok {
|
||||||
|
replaceables[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(replaceables) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
targets = append(targets, resolvedOreTarget{state: state, replaceables: replaceables})
|
||||||
|
}
|
||||||
|
return targets, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) {
|
||||||
|
angle := float64(random.NextFloat()) * math.Pi
|
||||||
|
extent := float64(size) / 8.0
|
||||||
|
x0 := float64(originX+8) + math.Sin(angle)*extent
|
||||||
|
x1 := float64(originX+8) - math.Sin(angle)*extent
|
||||||
|
z0 := float64(originZ+8) + math.Cos(angle)*extent
|
||||||
|
z1 := float64(originZ+8) - math.Cos(angle)*extent
|
||||||
|
y0 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||||
|
y1 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||||
|
|
||||||
|
type sphere struct{ x, y, z, radius float64 }
|
||||||
|
spheres := make([]sphere, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
t := float64(i) / float64(size)
|
||||||
|
radius := (math.Sin(math.Pi*t) + 1.0) * (float64(random.NextDouble())*float64(size)/16.0 + 1.0) / 2.0
|
||||||
|
spheres[i] = sphere{
|
||||||
|
x: x0 + (x1-x0)*t,
|
||||||
|
y: y0 + (y1-y0)*t,
|
||||||
|
z: z0 + (z1-z0)*t,
|
||||||
|
radius: radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range spheres {
|
||||||
|
if spheres[i].radius < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j := i + 1; j < len(spheres); j++ {
|
||||||
|
if spheres[j].radius < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dx, dy, dz := spheres[i].x-spheres[j].x, spheres[i].y-spheres[j].y, spheres[i].z-spheres[j].z
|
||||||
|
dr := spheres[i].radius - spheres[j].radius
|
||||||
|
if dr*dr > dx*dx+dy*dy+dz*dz {
|
||||||
|
if dr > 0 {
|
||||||
|
spheres[j].radius = -1
|
||||||
|
} else {
|
||||||
|
spheres[i].radius = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited := make(map[[3]int]bool)
|
||||||
|
for _, sphere := range spheres {
|
||||||
|
if sphere.radius < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minX, maxX := int(math.Floor(sphere.x-sphere.radius)), int(math.Floor(sphere.x+sphere.radius))
|
||||||
|
minY, maxY := int(math.Floor(sphere.y-sphere.radius)), int(math.Floor(sphere.y+sphere.radius))
|
||||||
|
minZ, maxZ := int(math.Floor(sphere.z-sphere.radius)), int(math.Floor(sphere.z+sphere.radius))
|
||||||
|
for x := minX; x <= maxX; x++ {
|
||||||
|
if x < 0 || x >= 16 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dx := (float64(x) + 0.5 - sphere.x) / sphere.radius
|
||||||
|
if dx*dx >= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for y := minY; y <= maxY; y++ {
|
||||||
|
if y < MinY || y >= MinY+WorldHeight {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dy := (float64(y) + 0.5 - sphere.y) / sphere.radius
|
||||||
|
if dx*dx+dy*dy >= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for z := minZ; z <= maxZ; z++ {
|
||||||
|
if z < 0 || z >= 16 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dz := (float64(z) + 0.5 - sphere.z) / sphere.radius
|
||||||
|
pos := [3]int{x, y, z}
|
||||||
|
if dx*dx+dy*dy+dz*dz >= 1 || visited[pos] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[pos] = true
|
||||||
|
current := c.GetBlock(x, y, z)
|
||||||
|
for _, target := range targets {
|
||||||
|
if !target.replaceables[current] || discard > 0 && random.NextFloat() < float32(discard) && exposedToAir(c, x, y, z) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.SetBlock(x, y, z, target.state)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exposedToAir(c *Chunk, x, y, z int) bool {
|
||||||
|
for _, offset := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} {
|
||||||
|
nx, ny, nz := x+offset[0], y+offset[1], z+offset[2]
|
||||||
|
if nx < 0 || nx >= 16 || nz < 0 || nz >= 16 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.GetBlock(nx, ny, nz) == StateAir {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
45
internal/world/placed_features_test.go
Normal file
45
internal/world/placed_features_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPlacedOresUseStoneAndDeepslateTargets(t *testing.T) {
|
||||||
|
gen := NewVanillaGenerator(12345)
|
||||||
|
want := map[uint16]string{
|
||||||
|
131: "iron ore",
|
||||||
|
132: "deepslate iron ore",
|
||||||
|
5307: "diamond ore",
|
||||||
|
5308: "deepslate diamond ore",
|
||||||
|
}
|
||||||
|
counts := make(map[uint16]int)
|
||||||
|
for cx := int32(-8); cx <= 8; cx += 4 {
|
||||||
|
for cz := int32(-8); cz <= 8; cz += 4 {
|
||||||
|
chunk := gen(cx, cz)
|
||||||
|
for y := MinY; y < 128; y++ {
|
||||||
|
for x := 0; x < 16; x++ {
|
||||||
|
for z := 0; z < 16; z++ {
|
||||||
|
counts[chunk.GetBlock(x, y, z)]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for state, name := range want {
|
||||||
|
if counts[state] == 0 {
|
||||||
|
t.Errorf("no %s generated by placed ore features", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlacedOresAreDeterministic(t *testing.T) {
|
||||||
|
gen := NewVanillaGenerator(4242)
|
||||||
|
a, b := gen(3, -2), gen(3, -2)
|
||||||
|
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||||
|
for x := 0; x < 16; x++ {
|
||||||
|
for z := 0; z < 16; z++ {
|
||||||
|
if got, want := a.GetBlock(x, y, z), b.GetBlock(x, y, z); got != want {
|
||||||
|
t.Fatalf("block (%d,%d,%d): first %d second %d", x, y, z, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ const dataVersion26 = 4790
|
||||||
// first time it ran: chunkAt prefers the store over the generator, so the
|
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||||
// already-explored area around spawn keeps its old terrain and every later fix
|
// already-explored area around spawn keeps its old terrain and every later fix
|
||||||
// looks like it did nothing in exactly the place you are standing.
|
// looks like it did nothing in exactly the place you are standing.
|
||||||
const generatorVersion = 12
|
const generatorVersion = 13
|
||||||
|
|
||||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||||
// because it is ours, not part of the vanilla chunk format.
|
// because it is ours, not part of the vanilla chunk format.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@ func NewVanillaGenerator(seed int64) Generator {
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
|
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
|
||||||
|
return generateVanillaDecorated(od, fluidPicker, veins, carver, seed, cx, cz, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVanillaWithoutDecoration(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
|
||||||
|
return generateVanillaDecorated(od, fluidPicker, veins, carver, seed, cx, cz, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVanillaDecorated(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32, withDecoration bool) *Chunk {
|
||||||
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
||||||
baseX, baseZ := int(cx)*16, int(cz)*16
|
baseX, baseZ := int(cx)*16, int(cz)*16
|
||||||
|
|
||||||
|
|
@ -171,7 +179,9 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
||||||
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
if withDecoration {
|
||||||
|
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
||||||
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,7 +478,7 @@ func bedrockAt(rng *chunkRand, d int) bool {
|
||||||
func decorate(c *Chunk, od *worldgen.OverworldDensity, 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)
|
placeVanillaOres(c, seed, cx, cz, biomeName)
|
||||||
placeFlora(c, &r, surfTop, grass, biomeName)
|
placeFlora(c, &r, surfTop, grass, biomeName)
|
||||||
placeDesertFeatures(c, &r, surfTop, biomeName)
|
placeDesertFeatures(c, &r, surfTop, biomeName)
|
||||||
placeRocks(c, &r, surfTop, grass, biomeName)
|
placeRocks(c, &r, surfTop, grass, biomeName)
|
||||||
|
|
|
||||||
BIN
internal/worldgen/feature_data.zip
Normal file
BIN
internal/worldgen/feature_data.zip
Normal file
Binary file not shown.
329
internal/worldgen/features.go
Normal file
329
internal/worldgen/features.go
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed feature_data.zip
|
||||||
|
var featureData []byte
|
||||||
|
|
||||||
|
// FeatureSet is the validated vanilla datapack graph used by decoration.
|
||||||
|
type FeatureSet struct {
|
||||||
|
Configured map[string]ConfiguredFeature
|
||||||
|
Placed map[string]PlacedFeature
|
||||||
|
Biomes map[string]BiomeGeneration
|
||||||
|
BlockTags map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfiguredFeature struct {
|
||||||
|
Type string
|
||||||
|
Config json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlacedFeature struct {
|
||||||
|
Feature string
|
||||||
|
Placement []PlacementModifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlacementModifier struct {
|
||||||
|
Type string
|
||||||
|
Raw json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type OreFeatureConfig struct {
|
||||||
|
Size int
|
||||||
|
DiscardAirExposure float64 `json:"discard_chance_on_air_exposure"`
|
||||||
|
Targets []OreTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
type OreTarget struct {
|
||||||
|
State struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
} `json:"state"`
|
||||||
|
Target struct {
|
||||||
|
PredicateType string `json:"predicate_type"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
} `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlacementPlan struct {
|
||||||
|
Count CountProvider
|
||||||
|
RarityChance int
|
||||||
|
HeightDistribution string
|
||||||
|
MinY HeightProvider
|
||||||
|
MaxY HeightProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
type CountProvider struct {
|
||||||
|
Min, Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeightProvider struct {
|
||||||
|
Absolute *int
|
||||||
|
AboveBottom *int
|
||||||
|
BelowTop *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p CountProvider) Sample(r RandomSource) int {
|
||||||
|
if p.Max <= p.Min {
|
||||||
|
return p.Min
|
||||||
|
}
|
||||||
|
return p.Min + int(r.NextIntN(int32(p.Max-p.Min+1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p HeightProvider) Resolve(minY, height int) int {
|
||||||
|
if p.Absolute != nil {
|
||||||
|
return *p.Absolute
|
||||||
|
}
|
||||||
|
if p.AboveBottom != nil {
|
||||||
|
return minY + *p.AboveBottom
|
||||||
|
}
|
||||||
|
if p.BelowTop != nil {
|
||||||
|
return minY + height - 1 - *p.BelowTop
|
||||||
|
}
|
||||||
|
return minY
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PlacementPlan) SampleY(r RandomSource, minY, height int) int {
|
||||||
|
lo, hi := p.MinY.Resolve(minY, height), p.MaxY.Resolve(minY, height)
|
||||||
|
if hi < lo {
|
||||||
|
return lo
|
||||||
|
}
|
||||||
|
span := hi - lo + 1
|
||||||
|
if p.HeightDistribution == "minecraft:trapezoid" {
|
||||||
|
plateau := 0
|
||||||
|
triangle := span - plateau
|
||||||
|
return lo + int(r.NextIntN(int32((triangle+1)/2))) + int(r.NextIntN(int32(triangle/2+1)))
|
||||||
|
}
|
||||||
|
return lo + int(r.NextIntN(int32(span)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecorationRandom(seed int64, chunkX, chunkZ int) (*Legacy, int64) {
|
||||||
|
random := NewLegacy(0)
|
||||||
|
decorationSeed := random.SetDecorationSeed(seed, chunkX<<4, chunkZ<<4)
|
||||||
|
return random, decorationSeed
|
||||||
|
}
|
||||||
|
|
||||||
|
type BiomeGeneration struct {
|
||||||
|
Carvers json.RawMessage `json:"carvers"`
|
||||||
|
Features [][]string `json:"features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
featureSetOnce sync.Once
|
||||||
|
featureSet *FeatureSet
|
||||||
|
featureSetErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadFeatureSet parses and validates the committed vanilla 26.1.2 feature
|
||||||
|
// datapack archive. The result is immutable and shared by all worlds.
|
||||||
|
func LoadFeatureSet() (*FeatureSet, error) {
|
||||||
|
featureSetOnce.Do(func() { featureSet, featureSetErr = loadFeatureSet(featureData) })
|
||||||
|
return featureSet, featureSetErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FeatureSet) Ore(name string) (OreFeatureConfig, error) {
|
||||||
|
configured, ok := s.Configured[name]
|
||||||
|
if !ok || configured.Type != "minecraft:ore" {
|
||||||
|
return OreFeatureConfig{}, fmt.Errorf("worldgen: %s is not an ore feature", name)
|
||||||
|
}
|
||||||
|
var config OreFeatureConfig
|
||||||
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
||||||
|
return OreFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
||||||
|
}
|
||||||
|
if config.Size < 1 || len(config.Targets) == 0 {
|
||||||
|
return OreFeatureConfig{}, fmt.Errorf("worldgen: invalid ore config %s", name)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FeatureSet) Placement(name string) (PlacementPlan, error) {
|
||||||
|
placed, ok := s.Placed[name]
|
||||||
|
if !ok {
|
||||||
|
return PlacementPlan{}, fmt.Errorf("worldgen: placed feature %s missing", name)
|
||||||
|
}
|
||||||
|
plan := PlacementPlan{Count: CountProvider{Min: 1, Max: 1}, MinY: HeightProvider{AboveBottom: intPtr(0)}, MaxY: HeightProvider{Absolute: intPtr(320)}}
|
||||||
|
for _, modifier := range placed.Placement {
|
||||||
|
switch modifier.Type {
|
||||||
|
case "minecraft:rarity_filter":
|
||||||
|
var value struct {
|
||||||
|
Chance int `json:"chance"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Chance < 1 {
|
||||||
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s invalid rarity filter", name)
|
||||||
|
}
|
||||||
|
plan.RarityChance = value.Chance
|
||||||
|
case "minecraft:count":
|
||||||
|
var value struct {
|
||||||
|
Count json.RawMessage `json:"count"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil {
|
||||||
|
return PlacementPlan{}, err
|
||||||
|
}
|
||||||
|
count, err := parseIntProvider(value.Count)
|
||||||
|
if err != nil {
|
||||||
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s count: %w", name, err)
|
||||||
|
}
|
||||||
|
plan.Count = count
|
||||||
|
case "minecraft:height_range":
|
||||||
|
var value struct {
|
||||||
|
Height struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Min json.RawMessage `json:"min_inclusive"`
|
||||||
|
Max json.RawMessage `json:"max_inclusive"`
|
||||||
|
} `json:"height"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil {
|
||||||
|
return PlacementPlan{}, err
|
||||||
|
}
|
||||||
|
min, err := parseFeatureHeight(value.Height.Min)
|
||||||
|
if err != nil {
|
||||||
|
return PlacementPlan{}, err
|
||||||
|
}
|
||||||
|
max, err := parseFeatureHeight(value.Height.Max)
|
||||||
|
if err != nil {
|
||||||
|
return PlacementPlan{}, err
|
||||||
|
}
|
||||||
|
if value.Height.Type != "minecraft:uniform" && value.Height.Type != "minecraft:trapezoid" {
|
||||||
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s unsupported height distribution %q", name, value.Height.Type)
|
||||||
|
}
|
||||||
|
plan.HeightDistribution, plan.MinY, plan.MaxY = value.Height.Type, min, max
|
||||||
|
case "minecraft:in_square", "minecraft:biome":
|
||||||
|
// Coordinate spreading and biome validation are applied by the world
|
||||||
|
// executor. Keeping them in the parsed plan preserves their order.
|
||||||
|
default:
|
||||||
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s unsupported placement modifier %q", name, modifier.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntProvider(raw json.RawMessage) (CountProvider, error) {
|
||||||
|
var fixed int
|
||||||
|
if err := json.Unmarshal(raw, &fixed); err == nil {
|
||||||
|
return CountProvider{Min: fixed, Max: fixed}, nil
|
||||||
|
}
|
||||||
|
var uniform struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Min int `json:"min_inclusive"`
|
||||||
|
Max int `json:"max_inclusive"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &uniform); err != nil || uniform.Type != "minecraft:uniform" {
|
||||||
|
return CountProvider{}, fmt.Errorf("unsupported count provider %s", raw)
|
||||||
|
}
|
||||||
|
return CountProvider{Min: uniform.Min, Max: uniform.Max}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFeatureHeight(raw json.RawMessage) (HeightProvider, error) {
|
||||||
|
var value struct {
|
||||||
|
Absolute *int `json:"absolute"`
|
||||||
|
AboveBottom *int `json:"above_bottom"`
|
||||||
|
BelowTop *int `json:"below_top"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &value); err != nil {
|
||||||
|
return HeightProvider{}, err
|
||||||
|
}
|
||||||
|
if value.Absolute == nil && value.AboveBottom == nil && value.BelowTop == nil {
|
||||||
|
return HeightProvider{}, fmt.Errorf("unsupported height provider %s", raw)
|
||||||
|
}
|
||||||
|
return HeightProvider{Absolute: value.Absolute, AboveBottom: value.AboveBottom, BelowTop: value.BelowTop}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func intPtr(value int) *int { return &value }
|
||||||
|
|
||||||
|
func loadFeatureSet(data []byte) (*FeatureSet, error) {
|
||||||
|
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: feature archive: %w", err)
|
||||||
|
}
|
||||||
|
set := &FeatureSet{
|
||||||
|
Configured: make(map[string]ConfiguredFeature),
|
||||||
|
Placed: make(map[string]PlacedFeature),
|
||||||
|
Biomes: make(map[string]BiomeGeneration),
|
||||||
|
BlockTags: make(map[string][]string),
|
||||||
|
}
|
||||||
|
for _, file := range zr.File {
|
||||||
|
r, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var raw json.RawMessage
|
||||||
|
if err := json.NewDecoder(r).Decode(&raw); err != nil {
|
||||||
|
r.Close()
|
||||||
|
return nil, fmt.Errorf("worldgen: decode %s: %w", file.Name, err)
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
name := resourceName(file.Name)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(file.Name, "/configured_feature/"):
|
||||||
|
var value ConfiguredFeature
|
||||||
|
if err := json.Unmarshal(raw, &value); err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: configured feature %s: %w", name, err)
|
||||||
|
}
|
||||||
|
set.Configured[name] = value
|
||||||
|
case strings.Contains(file.Name, "/placed_feature/"):
|
||||||
|
var value struct {
|
||||||
|
Feature string `json:"feature"`
|
||||||
|
Placement []json.RawMessage `json:"placement"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &value); err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: placed feature %s: %w", name, err)
|
||||||
|
}
|
||||||
|
placed := PlacedFeature{Feature: value.Feature, Placement: make([]PlacementModifier, len(value.Placement))}
|
||||||
|
for i, modifierRaw := range value.Placement {
|
||||||
|
if err := json.Unmarshal(modifierRaw, &placed.Placement[i]); err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: placed feature %s modifier %d: %w", name, i, err)
|
||||||
|
}
|
||||||
|
placed.Placement[i].Raw = modifierRaw
|
||||||
|
}
|
||||||
|
set.Placed[name] = placed
|
||||||
|
case strings.Contains(file.Name, "/biome/"):
|
||||||
|
var biome BiomeGeneration
|
||||||
|
if err := json.Unmarshal(raw, &biome); err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: biome %s: %w", name, err)
|
||||||
|
}
|
||||||
|
set.Biomes[name] = biome
|
||||||
|
case strings.Contains(file.Name, "/tags/block/"):
|
||||||
|
var tag struct {
|
||||||
|
Values []string `json:"values"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &tag); err != nil {
|
||||||
|
return nil, fmt.Errorf("worldgen: block tag %s: %w", name, err)
|
||||||
|
}
|
||||||
|
set.BlockTags[name] = tag.Values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(set.Configured) < 200 || len(set.Placed) < 250 || len(set.Biomes) < 60 {
|
||||||
|
return nil, fmt.Errorf("worldgen: incomplete feature archive: %d configured, %d placed, %d biomes",
|
||||||
|
len(set.Configured), len(set.Placed), len(set.Biomes))
|
||||||
|
}
|
||||||
|
for name, placed := range set.Placed {
|
||||||
|
if strings.HasPrefix(placed.Feature, "minecraft:") {
|
||||||
|
if _, ok := set.Configured[placed.Feature]; !ok {
|
||||||
|
return nil, fmt.Errorf("worldgen: placed feature %s references missing %s", name, placed.Feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for biomeName, biome := range set.Biomes {
|
||||||
|
for stage, names := range biome.Features {
|
||||||
|
for _, name := range names {
|
||||||
|
if _, ok := set.Placed[name]; !ok {
|
||||||
|
return nil, fmt.Errorf("worldgen: biome %s stage %d references missing %s", biomeName, stage, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceName(file string) string {
|
||||||
|
base := strings.TrimSuffix(path.Base(file), ".json")
|
||||||
|
return "minecraft:" + base
|
||||||
|
}
|
||||||
36
internal/worldgen/features_test.go
Normal file
36
internal/worldgen/features_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
||||||
|
set, err := LoadFeatureSet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
placed, ok := set.Placed["minecraft:ore_diamond"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("ore_diamond placed feature missing")
|
||||||
|
}
|
||||||
|
if placed.Feature != "minecraft:ore_diamond_small" || len(placed.Placement) != 4 {
|
||||||
|
t.Fatalf("ore_diamond = feature %q, %d modifiers", placed.Feature, len(placed.Placement))
|
||||||
|
}
|
||||||
|
configured := set.Configured[placed.Feature]
|
||||||
|
if configured.Type != "minecraft:ore" {
|
||||||
|
t.Fatalf("configured type = %q", configured.Type)
|
||||||
|
}
|
||||||
|
plains := set.Biomes["minecraft:plains"]
|
||||||
|
if len(plains.Features) != 11 || len(plains.Features[6]) < 20 {
|
||||||
|
t.Fatalf("plains stages=%d underground ores=%d", len(plains.Features), len(plains.Features[6]))
|
||||||
|
}
|
||||||
|
if len(set.BlockTags["minecraft:stone_ore_replaceables"]) == 0 {
|
||||||
|
t.Fatal("stone_ore_replaceables tag missing")
|
||||||
|
}
|
||||||
|
ore, err := set.Ore("minecraft:ore_diamond_small")
|
||||||
|
if err != nil || ore.Size != 4 || len(ore.Targets) != 2 {
|
||||||
|
t.Fatalf("diamond config = %+v, err=%v", ore, err)
|
||||||
|
}
|
||||||
|
plan, err := set.Placement("minecraft:ore_diamond")
|
||||||
|
if err != nil || plan.Count.Min != 7 || plan.Count.Max != 7 || plan.MinY.AboveBottom == nil {
|
||||||
|
t.Fatalf("diamond placement = %+v, err=%v", plan, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -196,6 +196,23 @@ func (r *Legacy) SetLargeFeatureSeed(seed int64, chunkX, chunkZ int) {
|
||||||
r.SetSeed(int64(chunkX)*a ^ int64(chunkZ)*b ^ seed)
|
r.SetSeed(int64(chunkX)*a ^ int64(chunkZ)*b ^ seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDecorationSeed is WorldgenRandom.setDecorationSeed. It returns the seed
|
||||||
|
// used for the chunk's feature stages; individual features derive their seeds
|
||||||
|
// from this value, stage index, and feature index.
|
||||||
|
func (r *Legacy) SetDecorationSeed(seed int64, blockX, blockZ int) int64 {
|
||||||
|
r.SetSeed(seed)
|
||||||
|
a := r.NextLong() | 1
|
||||||
|
b := r.NextLong() | 1
|
||||||
|
decorationSeed := int64(blockX)*a + int64(blockZ)*b ^ seed
|
||||||
|
r.SetSeed(decorationSeed)
|
||||||
|
return decorationSeed
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeatureSeed selects one configured feature in one decoration stage.
|
||||||
|
func (r *Legacy) SetFeatureSeed(decorationSeed int64, featureIndex, stage int) {
|
||||||
|
r.SetSeed(decorationSeed + int64(featureIndex) + int64(10000*stage))
|
||||||
|
}
|
||||||
|
|
||||||
// next returns the top `b` bits of the next LCG state.
|
// next returns the top `b` bits of the next LCG state.
|
||||||
func (r *Legacy) next(b uint) int32 {
|
func (r *Legacy) next(b uint) int32 {
|
||||||
r.seed = (r.seed*lcgMultiplier + lcgAddend) & lcgMask
|
r.seed = (r.seed*lcgMultiplier + lcgAddend) & lcgMask
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue