1367 lines
46 KiB
Go
1367 lines
46 KiB
Go
package worldgen
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"path"
|
|
"sort"
|
|
"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 IndexedFeature struct {
|
|
Name string
|
|
Index int
|
|
}
|
|
|
|
// ScheduledFeature is one feature selected for a decoration stage. Index is
|
|
// the stable FeatureSorter index within that stage and must be used when
|
|
// deriving the feature random seed; it is not the index in any one biome.
|
|
type ScheduledFeature struct {
|
|
Name string
|
|
Index int
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
// DiskFeatureConfig is the subset of DiskFeature used by overworld disks.
|
|
// Radius is an inclusive uniform integer provider in the vanilla datapack.
|
|
type DiskFeatureConfig struct {
|
|
HalfHeight int
|
|
RadiusMin, RadiusMax int
|
|
State BlockState
|
|
Fallback BlockState
|
|
Rules []DiskStateRule
|
|
Targets []string
|
|
}
|
|
|
|
type DiskStateRule struct {
|
|
IfTrue json.RawMessage
|
|
Then BlockState
|
|
}
|
|
|
|
type GeodeFeatureConfig struct {
|
|
Outer, Middle, Inner, Filling BlockState
|
|
AlternateInner BlockState
|
|
InnerPlacements []BlockState
|
|
DistributionMin, DistributionMax int
|
|
OuterWallMin, OuterWallMax int
|
|
PointOffsetMin, PointOffsetMax int
|
|
MaxGenOffset, MinGenOffset int
|
|
FillingLayer, InnerLayer float64
|
|
MiddleLayer, OuterLayer float64
|
|
NoiseMultiplier float64
|
|
InvalidBlocksThreshold int
|
|
CannotReplaceTag, InvalidBlocksTag string
|
|
CrackChance, BaseCrackSize float64
|
|
CrackPointOffset int
|
|
UseAlternateLayerChance float64
|
|
PlacementsRequireAlternate bool
|
|
UsePotentialPlacementsChance float64
|
|
}
|
|
|
|
type VegetationPatchFeatureConfig struct {
|
|
Surface string
|
|
DepthMin, DepthMax int
|
|
XZRadiusMin, XZRadiusMax int
|
|
VerticalRange int
|
|
ExtraBottomBlockChance float32
|
|
ExtraEdgeColumnChance float32
|
|
VegetationChance float32
|
|
Ground BlockState
|
|
ReplaceableTag string
|
|
Vegetation FeatureRef
|
|
}
|
|
|
|
// SimpleBlockFeatureConfig is the datapack form used by simple_block. The
|
|
// provider entries retain their vanilla weights and named block properties so
|
|
// the feature can choose a state with the feature RNG at placement time.
|
|
type SimpleBlockFeatureConfig struct {
|
|
States []WeightedBlockState
|
|
}
|
|
|
|
type WeightedBlockState struct {
|
|
State BlockState
|
|
Weight int
|
|
}
|
|
|
|
type UnderwaterMagmaFeatureConfig struct {
|
|
FloorSearchRange int `json:"floor_search_range"`
|
|
PlacementProbability float32 `json:"placement_probability_per_valid_position"`
|
|
PlacementRadiusAroundFloor int `json:"placement_radius_around_floor"`
|
|
}
|
|
|
|
type ProbabilityFeatureConfig struct {
|
|
Probability float32
|
|
}
|
|
|
|
type SpringFeatureConfig struct {
|
|
HoleCount int `json:"hole_count"`
|
|
RequiresBlockBelow bool `json:"requires_block_below"`
|
|
RockCount int `json:"rock_count"`
|
|
State BlockState `json:"state"`
|
|
ValidBlocks []string `json:"valid_blocks"`
|
|
}
|
|
|
|
type TreeFeatureConfig struct {
|
|
TrunkProvider struct {
|
|
Type string `json:"type"`
|
|
State BlockState `json:"state"`
|
|
} `json:"trunk_provider"`
|
|
FoliageProvider struct {
|
|
Type string `json:"type"`
|
|
State BlockState `json:"state"`
|
|
} `json:"foliage_provider"`
|
|
TrunkPlacer struct {
|
|
Type string `json:"type"`
|
|
BaseHeight int `json:"base_height"`
|
|
HeightRandA int `json:"height_rand_a"`
|
|
HeightRandB int `json:"height_rand_b"`
|
|
} `json:"trunk_placer"`
|
|
FoliagePlacer struct {
|
|
Type string `json:"type"`
|
|
Height int `json:"height"`
|
|
Offset int `json:"offset"`
|
|
Radius int `json:"radius"`
|
|
} `json:"foliage_placer"`
|
|
}
|
|
|
|
type FeatureRef struct {
|
|
Name string
|
|
Placement []PlacementModifier
|
|
}
|
|
|
|
type RandomSelectorConfig struct {
|
|
Default FeatureRef
|
|
Features []RandomSelectorEntry
|
|
}
|
|
|
|
type RandomSelectorEntry struct {
|
|
Chance float32
|
|
Feature FeatureRef
|
|
}
|
|
|
|
type BlockState struct {
|
|
Name string `json:"Name"`
|
|
Properties map[string]string `json:"Properties"`
|
|
}
|
|
|
|
type PlacementPlan struct {
|
|
Count CountProvider
|
|
RarityChance int
|
|
HeightDistribution string
|
|
HeightPlateau int
|
|
MinY HeightProvider
|
|
MaxY HeightProvider
|
|
}
|
|
|
|
type FeaturePosition struct {
|
|
X, Y, Z int
|
|
}
|
|
|
|
type PlacementContext struct {
|
|
MinY, Height int
|
|
BiomeAllows func(FeaturePosition) bool
|
|
HeightAt func(string, int, int) int
|
|
BlockPredicate func(json.RawMessage, FeaturePosition) (bool, error)
|
|
}
|
|
|
|
type CountProvider struct {
|
|
Min, Max int
|
|
Weighted []WeightedInt
|
|
}
|
|
|
|
type WeightedInt struct {
|
|
Value, Weight int
|
|
}
|
|
|
|
type HeightProvider struct {
|
|
Absolute *int
|
|
AboveBottom *int
|
|
BelowTop *int
|
|
}
|
|
|
|
func (p CountProvider) Sample(r RandomSource) int {
|
|
if len(p.Weighted) > 0 {
|
|
total := 0
|
|
for _, entry := range p.Weighted {
|
|
total += entry.Weight
|
|
}
|
|
if total <= 0 {
|
|
return 0
|
|
}
|
|
roll := int(r.NextIntN(int32(total)))
|
|
for _, entry := range p.Weighted {
|
|
if roll < entry.Weight {
|
|
return entry.Value
|
|
}
|
|
roll -= entry.Weight
|
|
}
|
|
}
|
|
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" {
|
|
rangeSize := span - 1
|
|
if rangeSize <= 0 {
|
|
return lo
|
|
}
|
|
plateau := p.HeightPlateau
|
|
if plateau < 0 || plateau > rangeSize {
|
|
return lo
|
|
}
|
|
left := (rangeSize - plateau) / 2
|
|
right := rangeSize - left
|
|
return lo + int(r.NextIntN(int32(right+1))) + int(r.NextIntN(int32(left+1)))
|
|
}
|
|
if p.HeightDistribution == "minecraft:very_biased_to_bottom" {
|
|
const inner = 8
|
|
if span-inner <= 0 {
|
|
return lo
|
|
}
|
|
first := int(r.NextIntN(int32(span - inner)))
|
|
return lo + int(r.NextIntN(int32(first+inner)))
|
|
}
|
|
return lo + int(r.NextIntN(int32(span)))
|
|
}
|
|
|
|
func DecorationRandom(seed int64, chunkX, chunkZ int) (*WorldgenRandom, int64) {
|
|
random := NewWorldgenRandom(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
|
|
}
|
|
|
|
// FeatureSteps mirrors FeatureSorter.buildFeaturesPerStep. biomeOrder must be
|
|
// BiomeSource.possibleBiomes encounter order because it assigns the stable
|
|
// identity used to break otherwise unconstrained graph ties.
|
|
func (s *FeatureSet) FeatureSteps(biomeOrder []string) ([][]string, error) {
|
|
type node struct {
|
|
stage, identity int
|
|
name string
|
|
}
|
|
identities := make(map[string]int)
|
|
nodes := make(map[[2]int]node)
|
|
edges := make(map[[2]int]map[[2]int]bool)
|
|
maxStages := 0
|
|
for _, biomeName := range biomeOrder {
|
|
biome, ok := s.Biomes[biomeName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(biome.Features) > maxStages {
|
|
maxStages = len(biome.Features)
|
|
}
|
|
var previous [2]int
|
|
hasPrevious := false
|
|
for stage, names := range biome.Features {
|
|
for _, name := range names {
|
|
identity, ok := identities[name]
|
|
if !ok {
|
|
identity = len(identities)
|
|
identities[name] = identity
|
|
}
|
|
key := [2]int{stage, identity}
|
|
nodes[key] = node{stage: stage, identity: identity, name: name}
|
|
if hasPrevious {
|
|
if edges[previous] == nil {
|
|
edges[previous] = make(map[[2]int]bool)
|
|
}
|
|
edges[previous][key] = true
|
|
}
|
|
previous, hasPrevious = key, true
|
|
}
|
|
}
|
|
}
|
|
less := func(a, b [2]int) bool {
|
|
return a[0] < b[0] || a[0] == b[0] && a[1] < b[1]
|
|
}
|
|
keys := make([][2]int, 0, len(nodes))
|
|
for key := range nodes {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool { return less(keys[i], keys[j]) })
|
|
visited := make(map[[2]int]bool)
|
|
visiting := make(map[[2]int]bool)
|
|
ordered := make([][2]int, 0, len(keys))
|
|
var visit func([2]int) error
|
|
visit = func(key [2]int) error {
|
|
if visited[key] {
|
|
return nil
|
|
}
|
|
if visiting[key] {
|
|
return fmt.Errorf("worldgen: feature order cycle")
|
|
}
|
|
visiting[key] = true
|
|
children := make([][2]int, 0, len(edges[key]))
|
|
for child := range edges[key] {
|
|
children = append(children, child)
|
|
}
|
|
sort.Slice(children, func(i, j int) bool { return less(children[i], children[j]) })
|
|
for _, child := range children {
|
|
if err := visit(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
delete(visiting, key)
|
|
visited[key] = true
|
|
ordered = append(ordered, key)
|
|
return nil
|
|
}
|
|
for _, key := range keys {
|
|
if err := visit(key); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
steps := make([][]string, maxStages)
|
|
for i := len(ordered) - 1; i >= 0; i-- {
|
|
n := nodes[ordered[i]]
|
|
steps[n.stage] = append(steps[n.stage], n.name)
|
|
}
|
|
return steps, nil
|
|
}
|
|
|
|
func IndexedFeatures(step []string, wanted map[string]bool) []IndexedFeature {
|
|
result := make([]IndexedFeature, 0, len(wanted))
|
|
for index, name := range step {
|
|
if wanted[name] {
|
|
result = append(result, IndexedFeature{Name: name, Index: index})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// FeatureSchedule returns the features present in the union of sourceBiomes
|
|
// for one decoration stage. allBiomeOrder is the complete BiomeSource encounter
|
|
// order used to build FeatureSorter, not the order of the local source region.
|
|
// The result is stable and contains each placed feature at most once.
|
|
func (s *FeatureSet) FeatureSchedule(allBiomeOrder, sourceBiomes []string, stage int) ([]ScheduledFeature, error) {
|
|
steps, err := s.FeatureSteps(allBiomeOrder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stage < 0 || stage >= len(steps) {
|
|
return nil, fmt.Errorf("worldgen: feature stage %d out of range", stage)
|
|
}
|
|
wanted := make(map[string]bool)
|
|
for _, biomeName := range sourceBiomes {
|
|
biome, ok := s.Biomes[biomeName]
|
|
if !ok || stage >= len(biome.Features) {
|
|
continue
|
|
}
|
|
for _, name := range biome.Features[stage] {
|
|
wanted[name] = true
|
|
}
|
|
}
|
|
result := make([]ScheduledFeature, 0, len(wanted))
|
|
for index, name := range steps[stage] {
|
|
if wanted[name] {
|
|
result = append(result, ScheduledFeature{Name: name, Index: index})
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
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) Disk(name string) (DiskFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:disk" {
|
|
return DiskFeatureConfig{}, fmt.Errorf("worldgen: %s is not a disk feature", name)
|
|
}
|
|
var raw struct {
|
|
HalfHeight int `json:"half_height"`
|
|
Radius struct {
|
|
Min int `json:"min_inclusive"`
|
|
Max int `json:"max_inclusive"`
|
|
} `json:"radius"`
|
|
StateProvider json.RawMessage `json:"state_provider"`
|
|
Target struct {
|
|
Blocks json.RawMessage `json:"blocks"`
|
|
} `json:"target"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &raw); err != nil {
|
|
return DiskFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
var provider struct {
|
|
State BlockState `json:"state"`
|
|
Fallback struct {
|
|
State BlockState `json:"state"`
|
|
} `json:"fallback"`
|
|
Rules []struct {
|
|
Then struct {
|
|
State BlockState `json:"state"`
|
|
} `json:"then"`
|
|
IfTrue json.RawMessage `json:"if_true"`
|
|
} `json:"rules"`
|
|
}
|
|
if err := json.Unmarshal(raw.StateProvider, &provider); err != nil {
|
|
return DiskFeatureConfig{}, fmt.Errorf("worldgen: decode %s state provider: %w", name, err)
|
|
}
|
|
config := DiskFeatureConfig{
|
|
HalfHeight: raw.HalfHeight, RadiusMin: raw.Radius.Min, RadiusMax: raw.Radius.Max,
|
|
State: provider.State, Fallback: provider.Fallback.State,
|
|
}
|
|
for _, rule := range provider.Rules {
|
|
if len(rule.IfTrue) != 0 && rule.Then.State.Name != "" {
|
|
config.Rules = append(config.Rules, DiskStateRule{IfTrue: rule.IfTrue, Then: rule.Then.State})
|
|
}
|
|
}
|
|
if config.State.Name == "" {
|
|
config.State = config.Fallback
|
|
}
|
|
if config.Fallback.Name == "" {
|
|
config.Fallback = config.State
|
|
}
|
|
if len(raw.Target.Blocks) == 0 {
|
|
return DiskFeatureConfig{}, fmt.Errorf("worldgen: invalid disk target %s", name)
|
|
}
|
|
if raw.Target.Blocks[0] == '"' {
|
|
var block string
|
|
if err := json.Unmarshal(raw.Target.Blocks, &block); err != nil {
|
|
return DiskFeatureConfig{}, err
|
|
}
|
|
config.Targets = []string{block}
|
|
} else if err := json.Unmarshal(raw.Target.Blocks, &config.Targets); err != nil {
|
|
return DiskFeatureConfig{}, err
|
|
}
|
|
if config.RadiusMin < 0 || config.RadiusMax < config.RadiusMin || config.State.Name == "" || len(config.Targets) == 0 {
|
|
return DiskFeatureConfig{}, fmt.Errorf("worldgen: invalid disk config %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) Geode(name string) (GeodeFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:geode" {
|
|
return GeodeFeatureConfig{}, fmt.Errorf("worldgen: %s is not a geode feature", name)
|
|
}
|
|
var raw struct {
|
|
Distribution struct {
|
|
Min int `json:"min_inclusive"`
|
|
Max int `json:"max_inclusive"`
|
|
} `json:"distribution_points"`
|
|
OuterWall struct {
|
|
Min int `json:"min_inclusive"`
|
|
Max int `json:"max_inclusive"`
|
|
} `json:"outer_wall_distance"`
|
|
PointOffset struct {
|
|
Min int `json:"min_inclusive"`
|
|
Max int `json:"max_inclusive"`
|
|
} `json:"point_offset"`
|
|
MinGenOffset int `json:"min_gen_offset"`
|
|
MaxGenOffset int `json:"max_gen_offset"`
|
|
Crack struct {
|
|
BaseSize float64 `json:"base_crack_size"`
|
|
PointOffset int `json:"crack_point_offset"`
|
|
Chance float64 `json:"generate_crack_chance"`
|
|
} `json:"crack"`
|
|
Layers struct {
|
|
Filling float64 `json:"filling"`
|
|
Inner float64 `json:"inner_layer"`
|
|
Middle float64 `json:"middle_layer"`
|
|
Outer float64 `json:"outer_layer"`
|
|
} `json:"layers"`
|
|
NoiseMultiplier float64 `json:"noise_multiplier"`
|
|
InvalidBlocksThreshold int `json:"invalid_blocks_threshold"`
|
|
CannotReplace string `json:"cannot_replace"`
|
|
InvalidBlocks string `json:"invalid_blocks"`
|
|
UseAlternateLayerChance float64 `json:"use_alternate_layer0_chance"`
|
|
PlacementsRequireAlternate bool `json:"placements_require_layer0_alternate"`
|
|
UsePotentialPlacementsChance float64 `json:"use_potential_placements_chance"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &raw); err != nil {
|
|
return GeodeFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
var wrapper struct {
|
|
Blocks map[string]json.RawMessage `json:"blocks"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &wrapper); err != nil {
|
|
return GeodeFeatureConfig{}, err
|
|
}
|
|
blocks := wrapper.Blocks
|
|
stateProvider := func(key string) BlockState {
|
|
var provider struct {
|
|
State BlockState `json:"state"`
|
|
}
|
|
_ = json.Unmarshal(blocks[key], &provider)
|
|
return provider.State
|
|
}
|
|
stringValue := func(key string) string {
|
|
var value string
|
|
_ = json.Unmarshal(blocks[key], &value)
|
|
return value
|
|
}
|
|
var innerPlacements []BlockState
|
|
_ = json.Unmarshal(blocks["inner_placements"], &innerPlacements)
|
|
config := GeodeFeatureConfig{
|
|
Outer: stateProvider("outer_layer_provider"),
|
|
Middle: stateProvider("middle_layer_provider"),
|
|
Inner: stateProvider("inner_layer_provider"),
|
|
Filling: stateProvider("filling_provider"),
|
|
AlternateInner: stateProvider("alternate_inner_layer_provider"),
|
|
InnerPlacements: innerPlacements,
|
|
DistributionMin: raw.Distribution.Min, DistributionMax: raw.Distribution.Max,
|
|
OuterWallMin: raw.OuterWall.Min, OuterWallMax: raw.OuterWall.Max,
|
|
PointOffsetMin: raw.PointOffset.Min, PointOffsetMax: raw.PointOffset.Max,
|
|
MinGenOffset: raw.MinGenOffset, MaxGenOffset: raw.MaxGenOffset,
|
|
FillingLayer: raw.Layers.Filling, InnerLayer: raw.Layers.Inner,
|
|
MiddleLayer: raw.Layers.Middle, OuterLayer: raw.Layers.Outer,
|
|
NoiseMultiplier: raw.NoiseMultiplier, InvalidBlocksThreshold: raw.InvalidBlocksThreshold,
|
|
CannotReplaceTag: stringValue("cannot_replace"), InvalidBlocksTag: stringValue("invalid_blocks"),
|
|
CrackChance: raw.Crack.Chance, BaseCrackSize: raw.Crack.BaseSize,
|
|
CrackPointOffset: raw.Crack.PointOffset,
|
|
UseAlternateLayerChance: raw.UseAlternateLayerChance,
|
|
PlacementsRequireAlternate: raw.PlacementsRequireAlternate,
|
|
UsePotentialPlacementsChance: raw.UsePotentialPlacementsChance,
|
|
}
|
|
if config.Outer.Name == "" || config.Middle.Name == "" || config.Inner.Name == "" || config.Filling.Name == "" ||
|
|
config.DistributionMin < 1 || config.DistributionMax < config.DistributionMin ||
|
|
config.OuterWallMin < 1 || config.OuterWallMax < config.OuterWallMin ||
|
|
config.PointOffsetMin < 0 || config.PointOffsetMax < config.PointOffsetMin ||
|
|
config.MaxGenOffset < config.MinGenOffset || config.FillingLayer <= 0 || config.InnerLayer <= 0 ||
|
|
config.MiddleLayer <= 0 || config.OuterLayer <= 0 || config.NoiseMultiplier < 0 ||
|
|
config.InvalidBlocksThreshold < 0 || config.CannotReplaceTag == "" || config.InvalidBlocksTag == "" {
|
|
return GeodeFeatureConfig{}, fmt.Errorf("worldgen: invalid geode config %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) VegetationPatch(name string) (VegetationPatchFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:vegetation_patch" {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: %s is not a vegetation patch", name)
|
|
}
|
|
var raw struct {
|
|
Surface string `json:"surface"`
|
|
Depth json.RawMessage `json:"depth"`
|
|
XZRadius json.RawMessage `json:"xz_radius"`
|
|
VerticalRange int `json:"vertical_range"`
|
|
ExtraBottomBlockChance float32 `json:"extra_bottom_block_chance"`
|
|
ExtraEdgeColumnChance float32 `json:"extra_edge_column_chance"`
|
|
VegetationChance float32 `json:"vegetation_chance"`
|
|
Replaceable string `json:"replaceable"`
|
|
GroundState struct {
|
|
State BlockState `json:"state"`
|
|
} `json:"ground_state"`
|
|
VegetationFeature json.RawMessage `json:"vegetation_feature"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &raw); err != nil {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
depth, err := parseIntProvider(raw.Depth)
|
|
if err != nil || len(depth.Weighted) != 0 {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: %s depth: %w", name, err)
|
|
}
|
|
radius, err := parseIntProvider(raw.XZRadius)
|
|
if err != nil || len(radius.Weighted) != 0 {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: %s radius: %w", name, err)
|
|
}
|
|
vegetation, err := parseFeatureRef(raw.VegetationFeature)
|
|
if err != nil {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: %s vegetation: %w", name, err)
|
|
}
|
|
config := VegetationPatchFeatureConfig{
|
|
Surface: raw.Surface, DepthMin: depth.Min, DepthMax: depth.Max,
|
|
XZRadiusMin: radius.Min, XZRadiusMax: radius.Max, VerticalRange: raw.VerticalRange,
|
|
ExtraBottomBlockChance: raw.ExtraBottomBlockChance, ExtraEdgeColumnChance: raw.ExtraEdgeColumnChance,
|
|
VegetationChance: raw.VegetationChance, Ground: raw.GroundState.State,
|
|
ReplaceableTag: raw.Replaceable, Vegetation: vegetation,
|
|
}
|
|
if config.Surface != "floor" && config.Surface != "ceiling" || config.DepthMin < 1 ||
|
|
config.DepthMax < config.DepthMin || config.XZRadiusMin < 1 || config.XZRadiusMax < config.XZRadiusMin ||
|
|
config.VerticalRange < 1 || config.Ground.Name == "" || config.ReplaceableTag == "" {
|
|
return VegetationPatchFeatureConfig{}, fmt.Errorf("worldgen: invalid vegetation patch %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) SimpleBlock(name string) (SimpleBlockFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:simple_block" {
|
|
return SimpleBlockFeatureConfig{}, fmt.Errorf("worldgen: %s is not a simple block feature", name)
|
|
}
|
|
var raw struct {
|
|
ToPlace json.RawMessage `json:"to_place"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &raw); err != nil {
|
|
return SimpleBlockFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
var provider struct {
|
|
Type string `json:"type"`
|
|
State BlockState `json:"state"`
|
|
Entries []struct {
|
|
Data BlockState `json:"data"`
|
|
Weight int `json:"weight"`
|
|
} `json:"entries"`
|
|
}
|
|
if err := json.Unmarshal(raw.ToPlace, &provider); err != nil {
|
|
return SimpleBlockFeatureConfig{}, fmt.Errorf("worldgen: decode %s provider: %w", name, err)
|
|
}
|
|
config := SimpleBlockFeatureConfig{}
|
|
switch provider.Type {
|
|
case "minecraft:simple_state_provider":
|
|
config.States = []WeightedBlockState{{State: provider.State, Weight: 1}}
|
|
case "minecraft:weighted_state_provider":
|
|
for _, entry := range provider.Entries {
|
|
if entry.Weight > 0 && entry.Data.Name != "" {
|
|
config.States = append(config.States, WeightedBlockState{State: entry.Data, Weight: entry.Weight})
|
|
}
|
|
}
|
|
default:
|
|
return SimpleBlockFeatureConfig{}, fmt.Errorf("worldgen: unsupported simple block provider %q", provider.Type)
|
|
}
|
|
if len(config.States) == 0 {
|
|
return SimpleBlockFeatureConfig{}, fmt.Errorf("worldgen: invalid simple block feature %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) UnderwaterMagma(name string) (UnderwaterMagmaFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:underwater_magma" {
|
|
return UnderwaterMagmaFeatureConfig{}, fmt.Errorf("worldgen: %s is not underwater magma", name)
|
|
}
|
|
var config UnderwaterMagmaFeatureConfig
|
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
|
return UnderwaterMagmaFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
if config.FloorSearchRange < 1 || config.PlacementRadiusAroundFloor < 0 ||
|
|
config.PlacementProbability <= 0 || config.PlacementProbability > 1 {
|
|
return UnderwaterMagmaFeatureConfig{}, fmt.Errorf("worldgen: invalid underwater magma config %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) Probability(name string) (ProbabilityFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:seagrass" {
|
|
return ProbabilityFeatureConfig{}, fmt.Errorf("worldgen: %s is not seagrass", name)
|
|
}
|
|
var config ProbabilityFeatureConfig
|
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
|
return ProbabilityFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
if config.Probability < 0 || config.Probability > 1 {
|
|
return ProbabilityFeatureConfig{}, fmt.Errorf("worldgen: invalid probability %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) Spring(name string) (SpringFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:spring_feature" {
|
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: %s is not a spring feature", name)
|
|
}
|
|
var config SpringFeatureConfig
|
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
if config.State.Name == "" || len(config.ValidBlocks) == 0 || config.HoleCount < 0 || config.RockCount < 0 {
|
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: invalid spring config %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) Tree(name string) (TreeFeatureConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:tree" {
|
|
return TreeFeatureConfig{}, fmt.Errorf("worldgen: %s is not a tree feature", name)
|
|
}
|
|
var config TreeFeatureConfig
|
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
|
return TreeFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
|
}
|
|
if config.TrunkPlacer.Type == "" || config.FoliagePlacer.Type == "" || config.TrunkProvider.State.Name == "" || config.FoliageProvider.State.Name == "" {
|
|
return TreeFeatureConfig{}, fmt.Errorf("worldgen: invalid tree config %s", name)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *FeatureSet) RandomSelector(name string) (RandomSelectorConfig, error) {
|
|
configured, ok := s.Configured[name]
|
|
if !ok || configured.Type != "minecraft:random_selector" {
|
|
return RandomSelectorConfig{}, fmt.Errorf("worldgen: %s is not a random selector", name)
|
|
}
|
|
var raw struct {
|
|
Default json.RawMessage `json:"default"`
|
|
Features []struct {
|
|
Chance float32 `json:"chance"`
|
|
Feature json.RawMessage `json:"feature"`
|
|
} `json:"features"`
|
|
}
|
|
if err := json.Unmarshal(configured.Config, &raw); err != nil {
|
|
return RandomSelectorConfig{}, err
|
|
}
|
|
selector := RandomSelectorConfig{Features: make([]RandomSelectorEntry, len(raw.Features))}
|
|
var err error
|
|
selector.Default, err = parseFeatureRef(raw.Default)
|
|
if err != nil {
|
|
return RandomSelectorConfig{}, err
|
|
}
|
|
for i, entry := range raw.Features {
|
|
selector.Features[i].Chance = entry.Chance
|
|
selector.Features[i].Feature, err = parseFeatureRef(entry.Feature)
|
|
if err != nil {
|
|
return RandomSelectorConfig{}, err
|
|
}
|
|
}
|
|
return selector, nil
|
|
}
|
|
|
|
func parseFeatureRef(raw json.RawMessage) (FeatureRef, error) {
|
|
var name string
|
|
if err := json.Unmarshal(raw, &name); err == nil {
|
|
return FeatureRef{Name: name}, nil
|
|
}
|
|
var inline struct {
|
|
Feature string `json:"feature"`
|
|
Placement []json.RawMessage `json:"placement"`
|
|
}
|
|
if err := json.Unmarshal(raw, &inline); err != nil || inline.Feature == "" {
|
|
return FeatureRef{}, fmt.Errorf("worldgen: invalid feature reference %s", raw)
|
|
}
|
|
ref := FeatureRef{Name: inline.Feature, Placement: make([]PlacementModifier, len(inline.Placement))}
|
|
for i, modifier := range inline.Placement {
|
|
if err := json.Unmarshal(modifier, &ref.Placement[i]); err != nil {
|
|
return FeatureRef{}, err
|
|
}
|
|
ref.Placement[i].Raw = modifier
|
|
}
|
|
return ref, 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"`
|
|
Plateau int `json:"plateau"`
|
|
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" &&
|
|
value.Height.Type != "minecraft:very_biased_to_bottom" {
|
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s unsupported height distribution %q", name, value.Height.Type)
|
|
}
|
|
plan.HeightDistribution, plan.HeightPlateau, plan.MinY, plan.MaxY = value.Height.Type, value.Height.Plateau, min, max
|
|
case "minecraft:in_square", "minecraft:biome", "minecraft:surface_water_depth_filter",
|
|
"minecraft:heightmap", "minecraft:block_predicate_filter", "minecraft:noise_threshold_count",
|
|
"minecraft:noise_based_count", "minecraft:random_offset", "minecraft:environment_scan", "minecraft:surface_relative_threshold_filter":
|
|
// 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
|
|
}
|
|
|
|
// PlacementPositions executes a placed feature's modifiers in declared order.
|
|
// The recursive walk mirrors Stream.flatMap: every repeated position completes
|
|
// the remaining chain before the next repeated position consumes random draws.
|
|
func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin FeaturePosition, context PlacementContext) ([]FeaturePosition, error) {
|
|
var result []FeaturePosition
|
|
err := s.ForEachPlacementPosition(name, r, origin, context, func(position FeaturePosition) error {
|
|
result = append(result, position)
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// ForEachPlacementPosition preserves vanilla's lazy placement stream: the
|
|
// configured feature consumes random draws for one position before modifiers
|
|
// produce the next repeated position.
|
|
func (s *FeatureSet) ForEachPlacementPosition(name string, r RandomSource, origin FeaturePosition, context PlacementContext, visit func(FeaturePosition) error) error {
|
|
placed, ok := s.Placed[name]
|
|
if !ok {
|
|
return fmt.Errorf("worldgen: placed feature %s missing", name)
|
|
}
|
|
var apply func(int, FeaturePosition) error
|
|
apply = func(index int, position FeaturePosition) error {
|
|
if index == len(placed.Placement) {
|
|
return visit(position)
|
|
}
|
|
modifier := placed.Placement[index]
|
|
next := func(value FeaturePosition) error { return apply(index+1, value) }
|
|
switch modifier.Type {
|
|
case "minecraft:noise_based_count":
|
|
var value struct {
|
|
Ratio int `json:"noise_to_count_ratio"`
|
|
Factor float64 `json:"noise_factor"`
|
|
Offset float64 `json:"noise_offset"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Ratio < 0 || value.Factor == 0 {
|
|
return fmt.Errorf("worldgen: %s invalid noise based count", name)
|
|
}
|
|
noise := BiomeInfoNoise(float64(position.X)/value.Factor, float64(position.Z)/value.Factor)
|
|
for count := int(math.Ceil((noise + value.Offset) * float64(value.Ratio))); count > 0; count-- {
|
|
if err := next(position); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case "minecraft:count":
|
|
var value struct {
|
|
Count json.RawMessage `json:"count"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil {
|
|
return err
|
|
}
|
|
count, err := parseIntProvider(value.Count)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for n := count.Sample(r); n > 0; n-- {
|
|
if err := next(position); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case "minecraft:rarity_filter":
|
|
var value struct {
|
|
Chance int `json:"chance"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Chance < 1 {
|
|
return fmt.Errorf("worldgen: %s invalid rarity filter", name)
|
|
}
|
|
if r.NextFloat() < 1/float32(value.Chance) {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:in_square":
|
|
position.X += int(r.NextIntN(16))
|
|
position.Z += int(r.NextIntN(16))
|
|
return next(position)
|
|
case "minecraft:height_range":
|
|
plan, err := placementHeightPlan(modifier.Raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
position.Y = plan.SampleY(r, context.MinY, context.Height)
|
|
return next(position)
|
|
case "minecraft:heightmap":
|
|
var value struct {
|
|
Heightmap string `json:"heightmap"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Heightmap == "" {
|
|
return fmt.Errorf("worldgen: %s invalid heightmap placement", name)
|
|
}
|
|
if context.HeightAt == nil {
|
|
return fmt.Errorf("worldgen: %s heightmap placement requires HeightAt", name)
|
|
}
|
|
position.Y = context.HeightAt(value.Heightmap, position.X, position.Z)
|
|
if position.Y > context.MinY {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:surface_water_depth_filter":
|
|
var value struct {
|
|
MaxWaterDepth int `json:"max_water_depth"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.MaxWaterDepth < 0 {
|
|
return fmt.Errorf("worldgen: %s invalid surface water depth filter", name)
|
|
}
|
|
if context.HeightAt == nil {
|
|
return fmt.Errorf("worldgen: %s surface water depth filter requires HeightAt", name)
|
|
}
|
|
oceanFloor := context.HeightAt("OCEAN_FLOOR", position.X, position.Z)
|
|
worldSurface := context.HeightAt("WORLD_SURFACE", position.X, position.Z)
|
|
if worldSurface-oceanFloor <= value.MaxWaterDepth {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:surface_relative_threshold_filter":
|
|
var value struct {
|
|
Heightmap string `json:"heightmap"`
|
|
Min *int `json:"min_inclusive"`
|
|
Max *int `json:"max_inclusive"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Heightmap == "" {
|
|
return fmt.Errorf("worldgen: %s invalid surface relative threshold", name)
|
|
}
|
|
if context.HeightAt == nil {
|
|
return fmt.Errorf("worldgen: %s surface relative threshold requires HeightAt", name)
|
|
}
|
|
minOffset, maxOffset := -int(^uint(0)>>1)-1, int(^uint(0)>>1)
|
|
if value.Min != nil {
|
|
minOffset = *value.Min
|
|
}
|
|
if value.Max != nil {
|
|
maxOffset = *value.Max
|
|
}
|
|
surface := context.HeightAt(value.Heightmap, position.X, position.Z)
|
|
positionY, surfaceY := int64(position.Y), int64(surface)
|
|
if positionY >= surfaceY+int64(minOffset) && positionY <= surfaceY+int64(maxOffset) {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:random_offset":
|
|
var value struct {
|
|
XZSpread json.RawMessage `json:"xz_spread"`
|
|
YSpread json.RawMessage `json:"y_spread"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil {
|
|
return err
|
|
}
|
|
xz, err := parsePlacementIntProvider(value.XZSpread)
|
|
if err != nil {
|
|
return fmt.Errorf("worldgen: %s xz spread: %w", name, err)
|
|
}
|
|
y, err := parsePlacementIntProvider(value.YSpread)
|
|
if err != nil {
|
|
return fmt.Errorf("worldgen: %s y spread: %w", name, err)
|
|
}
|
|
position.X += xz.Sample(r)
|
|
position.Y += y.Sample(r)
|
|
position.Z += xz.Sample(r)
|
|
return next(position)
|
|
case "minecraft:block_predicate_filter":
|
|
var value struct {
|
|
Predicate json.RawMessage `json:"predicate"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || len(value.Predicate) == 0 {
|
|
return fmt.Errorf("worldgen: %s invalid block predicate filter", name)
|
|
}
|
|
if context.BlockPredicate == nil {
|
|
return fmt.Errorf("worldgen: %s block predicate filter requires BlockPredicate", name)
|
|
}
|
|
ok, err := context.BlockPredicate(value.Predicate, position)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ok {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:environment_scan":
|
|
var value struct {
|
|
Direction string `json:"direction_of_search"`
|
|
Target json.RawMessage `json:"target_condition"`
|
|
Allowed json.RawMessage `json:"allowed_search_condition"`
|
|
MaxSteps int `json:"max_steps"`
|
|
}
|
|
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.MaxSteps < 1 ||
|
|
value.Direction != "up" && value.Direction != "down" {
|
|
return fmt.Errorf("worldgen: %s invalid environment scan", name)
|
|
}
|
|
if context.BlockPredicate == nil {
|
|
return fmt.Errorf("worldgen: %s environment scan requires BlockPredicate", name)
|
|
}
|
|
allowed, err := context.BlockPredicate(value.Allowed, position)
|
|
if err != nil || !allowed {
|
|
return err
|
|
}
|
|
dy := 1
|
|
if value.Direction == "down" {
|
|
dy = -1
|
|
}
|
|
for step := 0; step < value.MaxSteps; step++ {
|
|
matched, err := context.BlockPredicate(value.Target, position)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
return next(position)
|
|
}
|
|
position.Y += dy
|
|
if position.Y < context.MinY || position.Y >= context.MinY+context.Height {
|
|
return nil
|
|
}
|
|
allowed, err = context.BlockPredicate(value.Allowed, position)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !allowed {
|
|
break
|
|
}
|
|
}
|
|
matched, err := context.BlockPredicate(value.Target, position)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
case "minecraft:biome":
|
|
if context.BiomeAllows == nil || context.BiomeAllows(position) {
|
|
return next(position)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("worldgen: %s unsupported executable placement modifier %q", name, modifier.Type)
|
|
}
|
|
}
|
|
return apply(0, origin)
|
|
}
|
|
|
|
type placementIntProvider struct {
|
|
typeName string
|
|
min, max, plateau int
|
|
mean, deviation float32
|
|
}
|
|
|
|
func (p placementIntProvider) Sample(r RandomSource) int {
|
|
switch p.typeName {
|
|
case "minecraft:uniform":
|
|
return p.min + int(r.NextIntN(int32(p.max-p.min+1)))
|
|
case "minecraft:trapezoid":
|
|
if p.plateau == 0 && p.max == -p.min {
|
|
return int(r.NextIntN(int32(p.max+1))) - int(r.NextIntN(int32(p.max+1)))
|
|
}
|
|
rangeSize := p.max - p.min
|
|
if p.plateau == rangeSize {
|
|
return p.min + int(r.NextIntN(int32(rangeSize+1)))
|
|
}
|
|
left := (rangeSize - p.plateau) / 2
|
|
right := rangeSize - left
|
|
return p.min + int(r.NextIntN(int32(right+1))) + int(r.NextIntN(int32(left+1)))
|
|
case "minecraft:clamped_normal":
|
|
value := float32(r.NextGaussian())*p.deviation + p.mean
|
|
if value < float32(p.min) {
|
|
value = float32(p.min)
|
|
} else if value > float32(p.max) {
|
|
value = float32(p.max)
|
|
}
|
|
return int(value)
|
|
default:
|
|
return p.min
|
|
}
|
|
}
|
|
|
|
func parsePlacementIntProvider(raw json.RawMessage) (placementIntProvider, error) {
|
|
var fixed int
|
|
if err := json.Unmarshal(raw, &fixed); err == nil {
|
|
return placementIntProvider{min: fixed, max: fixed}, nil
|
|
}
|
|
var value struct {
|
|
Type string `json:"type"`
|
|
Min int `json:"min"`
|
|
Max int `json:"max"`
|
|
MinInclusive int `json:"min_inclusive"`
|
|
MaxInclusive int `json:"max_inclusive"`
|
|
Plateau int `json:"plateau"`
|
|
Mean, Deviation float32
|
|
}
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return placementIntProvider{}, err
|
|
}
|
|
provider := placementIntProvider{typeName: value.Type, plateau: value.Plateau, mean: value.Mean, deviation: value.Deviation}
|
|
switch value.Type {
|
|
case "minecraft:trapezoid":
|
|
provider.min, provider.max = value.Min, value.Max
|
|
if provider.max < provider.min || provider.plateau < 0 || provider.plateau > provider.max-provider.min {
|
|
return placementIntProvider{}, fmt.Errorf("invalid trapezoid provider %s", raw)
|
|
}
|
|
case "minecraft:uniform", "minecraft:clamped_normal":
|
|
provider.min, provider.max = value.MinInclusive, value.MaxInclusive
|
|
if provider.max < provider.min || value.Type == "minecraft:clamped_normal" && provider.deviation <= 0 {
|
|
return placementIntProvider{}, fmt.Errorf("invalid %s provider %s", value.Type, raw)
|
|
}
|
|
default:
|
|
return placementIntProvider{}, fmt.Errorf("unsupported int provider %s", raw)
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
func placementHeightPlan(raw json.RawMessage) (PlacementPlan, error) {
|
|
var value struct {
|
|
Height struct {
|
|
Type string `json:"type"`
|
|
Plateau int `json:"plateau"`
|
|
Min json.RawMessage `json:"min_inclusive"`
|
|
Max json.RawMessage `json:"max_inclusive"`
|
|
} `json:"height"`
|
|
}
|
|
if err := json.Unmarshal(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" &&
|
|
value.Height.Type != "minecraft:very_biased_to_bottom" {
|
|
return PlacementPlan{}, fmt.Errorf("worldgen: unsupported height distribution %q", value.Height.Type)
|
|
}
|
|
return PlacementPlan{HeightDistribution: value.Height.Type, HeightPlateau: value.Height.Plateau, MinY: min, MaxY: max}, 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{Min: uniform.Min, Max: uniform.Max}, nil
|
|
}
|
|
var weighted struct {
|
|
Type string `json:"type"`
|
|
Distribution []struct {
|
|
Data int `json:"data"`
|
|
Weight int `json:"weight"`
|
|
} `json:"distribution"`
|
|
}
|
|
if err := json.Unmarshal(raw, &weighted); err == nil && weighted.Type == "minecraft:weighted_list" {
|
|
provider := CountProvider{Weighted: make([]WeightedInt, 0, len(weighted.Distribution))}
|
|
for _, entry := range weighted.Distribution {
|
|
if entry.Weight > 0 {
|
|
provider.Weighted = append(provider.Weighted, WeightedInt{Value: entry.Data, Weight: entry.Weight})
|
|
}
|
|
}
|
|
return provider, nil
|
|
}
|
|
return CountProvider{}, fmt.Errorf("unsupported count provider %s", raw)
|
|
}
|
|
|
|
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
|
|
}
|