Execute world-aware placement modifiers
This commit is contained in:
parent
053c5bec6a
commit
11440f556c
5 changed files with 336 additions and 4 deletions
|
|
@ -123,8 +123,10 @@ type FeaturePosition struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacementContext struct {
|
type PlacementContext struct {
|
||||||
MinY, Height int
|
MinY, Height int
|
||||||
BiomeAllows func(FeaturePosition) bool
|
BiomeAllows func(FeaturePosition) bool
|
||||||
|
HeightAt func(string, int, int) int
|
||||||
|
BlockPredicate func(json.RawMessage, FeaturePosition) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CountProvider struct {
|
type CountProvider struct {
|
||||||
|
|
@ -549,6 +551,75 @@ func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin Feat
|
||||||
}
|
}
|
||||||
position.Y = plan.SampleY(r, context.MinY, context.Height)
|
position.Y = plan.SampleY(r, context.MinY, context.Height)
|
||||||
return next(position)
|
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: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:biome":
|
case "minecraft:biome":
|
||||||
if context.BiomeAllows == nil || context.BiomeAllows(position) {
|
if context.BiomeAllows == nil || context.BiomeAllows(position) {
|
||||||
return next(position)
|
return next(position)
|
||||||
|
|
@ -564,6 +635,75 @@ func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin Feat
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func placementHeightPlan(raw json.RawMessage) (PlacementPlan, error) {
|
||||||
var value struct {
|
var value struct {
|
||||||
Height struct {
|
Height struct {
|
||||||
|
|
|
||||||
|
|
@ -143,3 +143,90 @@ func TestPlacementPositionsPreservesModifierOrder(t *testing.T) {
|
||||||
t.Fatalf("positions = %v, want %v", got, want)
|
t.Fatalf("positions = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlacementPositionsWorldAwareModifiers(t *testing.T) {
|
||||||
|
modifier := func(raw string) PlacementModifier {
|
||||||
|
var value PlacementModifier
|
||||||
|
value.Raw = json.RawMessage(raw)
|
||||||
|
if err := json.Unmarshal(value.Raw, &value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
set := &FeatureSet{Placed: map[string]PlacedFeature{
|
||||||
|
"test": {Placement: []PlacementModifier{
|
||||||
|
modifier(`{"type":"minecraft:heightmap","heightmap":"MOTION_BLOCKING"}`),
|
||||||
|
modifier(`{"type":"minecraft:surface_water_depth_filter","max_water_depth":2}`),
|
||||||
|
modifier(`{"type":"minecraft:block_predicate_filter","predicate":{"type":"minecraft:matching_blocks","blocks":"minecraft:air"}}`),
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
var calls []string
|
||||||
|
got, err := set.PlacementPositions("test", NewLegacy(1), FeaturePosition{X: 8, Y: 0, Z: 9}, PlacementContext{
|
||||||
|
MinY: -64,
|
||||||
|
HeightAt: func(kind string, x, z int) int {
|
||||||
|
calls = append(calls, kind)
|
||||||
|
if kind == "MOTION_BLOCKING" {
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
if kind == "OCEAN_FLOOR" {
|
||||||
|
return 40
|
||||||
|
}
|
||||||
|
return 41
|
||||||
|
},
|
||||||
|
BlockPredicate: func(predicate json.RawMessage, position FeaturePosition) (bool, error) {
|
||||||
|
if string(predicate) != `{"type":"minecraft:matching_blocks","blocks":"minecraft:air"}` {
|
||||||
|
t.Fatalf("predicate = %s", predicate)
|
||||||
|
}
|
||||||
|
return position.Y == 42, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []FeaturePosition{{X: 8, Y: 42, Z: 9}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("positions = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(calls, []string{"MOTION_BLOCKING", "OCEAN_FLOOR", "WORLD_SURFACE"}) {
|
||||||
|
t.Fatalf("heightmap calls = %v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlacementPositionsMatchesVanillaRandomOffsetVector(t *testing.T) {
|
||||||
|
modifier := func(raw string) PlacementModifier {
|
||||||
|
var value PlacementModifier
|
||||||
|
value.Raw = json.RawMessage(raw)
|
||||||
|
if err := json.Unmarshal(value.Raw, &value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
set := &FeatureSet{Placed: map[string]PlacedFeature{
|
||||||
|
"test": {Placement: []PlacementModifier{
|
||||||
|
modifier(`{"type":"minecraft:count","count":3}`),
|
||||||
|
modifier(`{"type":"minecraft:random_offset","xz_spread":{"type":"minecraft:trapezoid","max":4,"min":-4,"plateau":0},"y_spread":{"type":"minecraft:trapezoid","max":2,"min":-2,"plateau":0}}`),
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
got, err := set.PlacementPositions("test", NewLegacy(12345), FeaturePosition{X: 32, Y: 10, Z: -16}, PlacementContext{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []FeaturePosition{{33, 10, -20}, {30, 11, -16}, {31, 11, -17}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("positions = %v, want vanilla %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClampedNormalIntMatchesVanillaRuntimeVector(t *testing.T) {
|
||||||
|
provider, err := parsePlacementIntProvider(json.RawMessage(`{"type":"minecraft:clamped_normal","mean":0.0,"deviation":3.0,"min_inclusive":-10,"max_inclusive":10}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := NewLegacy(12345)
|
||||||
|
want := []int{0, 1, 2, -1, -3, -2, -2, 6}
|
||||||
|
for i, value := range want {
|
||||||
|
if got := provider.Sample(r); got != value {
|
||||||
|
t.Fatalf("sample[%d] = %d, want vanilla %d", i, got, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ package worldgen
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -50,6 +51,7 @@ type RandomSource interface {
|
||||||
NextDouble() float64
|
NextDouble() float64
|
||||||
NextFloat() float32
|
NextFloat() float32
|
||||||
NextBoolean() bool
|
NextBoolean() bool
|
||||||
|
NextGaussian() float64
|
||||||
// ForkPositional returns a factory for deriving deterministic child sources
|
// ForkPositional returns a factory for deriving deterministic child sources
|
||||||
// (used to seed noise octaves by name).
|
// (used to seed noise octaves by name).
|
||||||
ForkPositional() PositionalRandomFactory
|
ForkPositional() PositionalRandomFactory
|
||||||
|
|
@ -77,7 +79,11 @@ func positionSeed(x, y, z int) int64 {
|
||||||
// --- Xoroshiro128++ ---
|
// --- Xoroshiro128++ ---
|
||||||
|
|
||||||
// Xoroshiro is XoroshiroRandomSource backed by Xoroshiro128PlusPlus.
|
// Xoroshiro is XoroshiroRandomSource backed by Xoroshiro128PlusPlus.
|
||||||
type Xoroshiro struct{ lo, hi uint64 }
|
type Xoroshiro struct {
|
||||||
|
lo, hi uint64
|
||||||
|
gaussian float64
|
||||||
|
haveGaussian bool
|
||||||
|
}
|
||||||
|
|
||||||
// NewXoroshiro seeds a Xoroshiro source from a 64-bit seed.
|
// NewXoroshiro seeds a Xoroshiro source from a 64-bit seed.
|
||||||
func NewXoroshiro(seed int64) *Xoroshiro {
|
func NewXoroshiro(seed int64) *Xoroshiro {
|
||||||
|
|
@ -132,6 +138,25 @@ func (x *Xoroshiro) NextFloat() float32 {
|
||||||
|
|
||||||
func (x *Xoroshiro) NextBoolean() bool { return x.nextBits()&1 != 0 }
|
func (x *Xoroshiro) NextBoolean() bool { return x.nextBits()&1 != 0 }
|
||||||
|
|
||||||
|
func (x *Xoroshiro) NextGaussian() float64 {
|
||||||
|
if x.haveGaussian {
|
||||||
|
x.haveGaussian = false
|
||||||
|
return x.gaussian
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
u := 2*x.NextDouble() - 1
|
||||||
|
v := 2*x.NextDouble() - 1
|
||||||
|
s := u*u + v*v
|
||||||
|
if s == 0 || s >= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
factor := math.Sqrt(-2 * math.Log(s) / s)
|
||||||
|
x.gaussian = v * factor
|
||||||
|
x.haveGaussian = true
|
||||||
|
return u * factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ConsumeCount advances the underlying generator n times.
|
// ConsumeCount advances the underlying generator n times.
|
||||||
func (x *Xoroshiro) ConsumeCount(n int) {
|
func (x *Xoroshiro) ConsumeCount(n int) {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
|
|
@ -168,7 +193,11 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Legacy is LegacyRandomSource: java.util.Random's 48-bit LCG.
|
// Legacy is LegacyRandomSource: java.util.Random's 48-bit LCG.
|
||||||
type Legacy struct{ seed uint64 }
|
type Legacy struct {
|
||||||
|
seed uint64
|
||||||
|
gaussian float64
|
||||||
|
haveGaussian bool
|
||||||
|
}
|
||||||
|
|
||||||
// NewLegacy seeds a Legacy source, applying Java's seed scramble.
|
// NewLegacy seeds a Legacy source, applying Java's seed scramble.
|
||||||
func NewLegacy(seed int64) *Legacy {
|
func NewLegacy(seed int64) *Legacy {
|
||||||
|
|
@ -180,6 +209,7 @@ func NewLegacy(seed int64) *Legacy {
|
||||||
// SetSeed is java.util.Random.setSeed, which worldgen reseeds in place.
|
// SetSeed is java.util.Random.setSeed, which worldgen reseeds in place.
|
||||||
func (r *Legacy) SetSeed(seed int64) {
|
func (r *Legacy) SetSeed(seed int64) {
|
||||||
r.seed = (uint64(seed) ^ lcgMultiplier) & lcgMask
|
r.seed = (uint64(seed) ^ lcgMultiplier) & lcgMask
|
||||||
|
r.haveGaussian = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLargeFeatureSeed is WorldgenRandom.setLargeFeatureSeed: seed from the
|
// SetLargeFeatureSeed is WorldgenRandom.setLargeFeatureSeed: seed from the
|
||||||
|
|
@ -246,6 +276,25 @@ func (r *Legacy) NextDouble() float64 {
|
||||||
func (r *Legacy) NextFloat() float32 { return float32(r.next(24)) * 0x1.0p-24 }
|
func (r *Legacy) NextFloat() float32 { return float32(r.next(24)) * 0x1.0p-24 }
|
||||||
func (r *Legacy) NextBoolean() bool { return r.next(1) != 0 }
|
func (r *Legacy) NextBoolean() bool { return r.next(1) != 0 }
|
||||||
|
|
||||||
|
func (r *Legacy) NextGaussian() float64 {
|
||||||
|
if r.haveGaussian {
|
||||||
|
r.haveGaussian = false
|
||||||
|
return r.gaussian
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
u := 2*r.NextDouble() - 1
|
||||||
|
v := 2*r.NextDouble() - 1
|
||||||
|
s := u*u + v*v
|
||||||
|
if s == 0 || s >= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
factor := math.Sqrt(-2 * math.Log(s) / s)
|
||||||
|
r.gaussian = v * factor
|
||||||
|
r.haveGaussian = true
|
||||||
|
return u * factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ConsumeCount advances the LCG n times.
|
// ConsumeCount advances the LCG n times.
|
||||||
func (r *Legacy) ConsumeCount(n int) {
|
func (r *Legacy) ConsumeCount(n int) {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,12 @@ func TestLegacyVectors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLegacySetSeedResetsGaussianCache(t *testing.T) {
|
||||||
|
r := NewLegacy(12345)
|
||||||
|
first := r.NextGaussian()
|
||||||
|
r.SetSeed(12345)
|
||||||
|
if got := r.NextGaussian(); got != first {
|
||||||
|
t.Fatalf("NextGaussian after SetSeed = %v, want %v", got, first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
47
tools/VanillaPlacementVectors.java
Normal file
47
tools/VanillaPlacementVectors.java
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.util.valueproviders.ClampedNormalInt;
|
||||||
|
import net.minecraft.util.valueproviders.TrapezoidInt;
|
||||||
|
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.CountPlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacementModifier;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.RandomOffsetPlacement;
|
||||||
|
|
||||||
|
// Emits placement stream vectors from the official 26.1.2 runtime. These
|
||||||
|
// modifiers do not read PlacementContext, so no world or registry is required.
|
||||||
|
public final class VanillaPlacementVectors {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
|
||||||
|
RandomSource offsetRandom = new LegacyRandomSource(12345L);
|
||||||
|
List<PlacementModifier> modifiers = List.of(
|
||||||
|
CountPlacement.of(3),
|
||||||
|
RandomOffsetPlacement.of(
|
||||||
|
TrapezoidInt.of(-4, 4, 0),
|
||||||
|
TrapezoidInt.of(-2, 2, 0)));
|
||||||
|
Stream<BlockPos> positions = Stream.of(new BlockPos(32, 10, -16));
|
||||||
|
for (PlacementModifier modifier : modifiers) {
|
||||||
|
positions = positions.flatMap(position -> modifier.getPositions(null, offsetRandom, position));
|
||||||
|
}
|
||||||
|
System.out.println("offset=" + positions.map(VanillaPlacementVectors::position).toList());
|
||||||
|
|
||||||
|
RandomSource normalRandom = new LegacyRandomSource(12345L);
|
||||||
|
ClampedNormalInt normal = ClampedNormalInt.of(0.0F, 3.0F, -10, 10);
|
||||||
|
StringBuilder samples = new StringBuilder();
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if (i > 0) samples.append(',');
|
||||||
|
samples.append(normal.sample(normalRandom));
|
||||||
|
}
|
||||||
|
System.out.println("normal=" + samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String position(BlockPos position) {
|
||||||
|
return position.getX() + ":" + position.getY() + ":" + position.getZ();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue