diff --git a/cmd/genfeatures/main.go b/cmd/genfeatures/main.go new file mode 100644 index 0000000..b46f7d1 --- /dev/null +++ b/cmd/genfeatures/main.go @@ -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) +} diff --git a/internal/world/orevein_verify_test.go b/internal/world/orevein_verify_test.go index 4a0f99f..97698a0 100644 --- a/internal/world/orevein_verify_test.go +++ b/internal/world/orevein_verify_test.go @@ -1,6 +1,10 @@ package world -import "testing" +import ( + "testing" + + "regionio/internal/worldgen" +) // 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. @@ -21,7 +25,18 @@ const ( // 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. 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{} lowest := 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 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 lx := 0; lx < 16; lx++ { for lz := 0; lz < 16; lz++ { diff --git a/internal/world/placed_features.go b/internal/world/placed_features.go new file mode 100644 index 0000000..410f6ae --- /dev/null +++ b/internal/world/placed_features.go @@ -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 +} diff --git a/internal/world/placed_features_test.go b/internal/world/placed_features_test.go new file mode 100644 index 0000000..953c0ee --- /dev/null +++ b/internal/world/placed_features_test.go @@ -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) + } + } + } + } +} diff --git a/internal/world/store.go b/internal/world/store.go index b8617a4..91cb0cb 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -33,7 +33,7 @@ const dataVersion26 = 4790 // 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 // 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 // because it is ours, not part of the vanilla chunk format. diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index 4295a6b..4700331 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -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 { + 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 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) - decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName) + if withDecoration { + decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName) + } 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) { r := newChunkRand(cx, cz, seed) - placeOres(c, &r) + placeVanillaOres(c, seed, cx, cz, biomeName) placeFlora(c, &r, surfTop, grass, biomeName) placeDesertFeatures(c, &r, surfTop, biomeName) placeRocks(c, &r, surfTop, grass, biomeName) diff --git a/internal/worldgen/feature_data.zip b/internal/worldgen/feature_data.zip new file mode 100644 index 0000000..0b029e6 Binary files /dev/null and b/internal/worldgen/feature_data.zip differ diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go new file mode 100644 index 0000000..e431f8d --- /dev/null +++ b/internal/worldgen/features.go @@ -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 +} diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go new file mode 100644 index 0000000..cb6fe27 --- /dev/null +++ b/internal/worldgen/features_test.go @@ -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) + } +} diff --git a/internal/worldgen/random.go b/internal/worldgen/random.go index e96500a..16d0089 100644 --- a/internal/worldgen/random.go +++ b/internal/worldgen/random.go @@ -196,6 +196,23 @@ func (r *Legacy) SetLargeFeatureSeed(seed int64, chunkX, chunkZ int) { 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. func (r *Legacy) next(b uint) int32 { r.seed = (r.seed*lcgMultiplier + lcgAddend) & lcgMask