Schedule source chunk features globally
This commit is contained in:
parent
11440f556c
commit
ea66091f85
4 changed files with 125 additions and 0 deletions
21
internal/world/feature_scheduler.go
Normal file
21
internal/world/feature_scheduler.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
// decorationSource is a source chunk whose feature pass may inspect or write a
|
||||||
|
// target chunk. Vanilla FEATURES has a one-chunk block-state write radius.
|
||||||
|
type decorationSource struct {
|
||||||
|
X, Z int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// decorationSources returns source chunks in deterministic X-major/Z-minor
|
||||||
|
// order. Replaying all nine against one mutable 3x3 terrain region makes target
|
||||||
|
// output independent of cache request order while preserving each source's own
|
||||||
|
// decoration seed and placement origin.
|
||||||
|
func decorationSources(targetX, targetZ int32) []decorationSource {
|
||||||
|
sources := make([]decorationSource, 0, 9)
|
||||||
|
for sourceX := targetX - 1; sourceX <= targetX+1; sourceX++ {
|
||||||
|
for sourceZ := targetZ - 1; sourceZ <= targetZ+1; sourceZ++ {
|
||||||
|
sources = append(sources, decorationSource{X: sourceX, Z: sourceZ})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
27
internal/world/feature_scheduler_test.go
Normal file
27
internal/world/feature_scheduler_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecorationSourcesCoverVanillaFeatureWriteRadius(t *testing.T) {
|
||||||
|
got := decorationSources(4, -7)
|
||||||
|
want := []decorationSource{
|
||||||
|
{3, -8}, {3, -7}, {3, -6},
|
||||||
|
{4, -8}, {4, -7}, {4, -6},
|
||||||
|
{5, -8}, {5, -7}, {5, -6},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("sources = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecorationSourcesAreRequestOrderIndependent(t *testing.T) {
|
||||||
|
first := decorationSources(-2, 9)
|
||||||
|
_ = decorationSources(100, -100)
|
||||||
|
second := decorationSources(-2, 9)
|
||||||
|
if !reflect.DeepEqual(first, second) {
|
||||||
|
t.Fatalf("sources changed after unrelated request: %v != %v", first, second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,14 @@ type IndexedFeature struct {
|
||||||
Index int
|
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 ConfiguredFeature struct {
|
||||||
Type string
|
Type string
|
||||||
Config json.RawMessage
|
Config json.RawMessage
|
||||||
|
|
@ -332,6 +340,37 @@ func IndexedFeatures(step []string, wanted map[string]bool) []IndexedFeature {
|
||||||
return result
|
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) {
|
func (s *FeatureSet) Ore(name string) (OreFeatureConfig, error) {
|
||||||
configured, ok := s.Configured[name]
|
configured, ok := s.Configured[name]
|
||||||
if !ok || configured.Type != "minecraft:ore" {
|
if !ok || configured.Type != "minecraft:ore" {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,44 @@ func TestFeatureStepsRejectsCycles(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFeatureScheduleUsesGlobalOrderAndSourceUnion(t *testing.T) {
|
||||||
|
set := &FeatureSet{Biomes: map[string]BiomeGeneration{
|
||||||
|
"a": {Features: [][]string{{"f1"}, {"f3", "f4"}}},
|
||||||
|
"b": {Features: [][]string{{"f2", "f1"}, {"f5", "f4"}}},
|
||||||
|
"c": {Features: [][]string{{"f1"}, {"f5", "f3"}}},
|
||||||
|
}}
|
||||||
|
got, err := set.FeatureSchedule([]string{"a", "b", "c"}, []string{"b", "c"}, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []ScheduledFeature{{Name: "f5", Index: 0}, {Name: "f3", Index: 1}, {Name: "f4", Index: 2}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("schedule = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeatureScheduleDoesNotRenumberFilteredFeatures(t *testing.T) {
|
||||||
|
set := &FeatureSet{Biomes: map[string]BiomeGeneration{
|
||||||
|
"a": {Features: [][]string{{"f1", "f2", "f3"}}},
|
||||||
|
"b": {Features: [][]string{{"f3"}}},
|
||||||
|
}}
|
||||||
|
got, err := set.FeatureSchedule([]string{"a", "b"}, []string{"b"}, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []ScheduledFeature{{Name: "f3", Index: 2}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("schedule = %v, want global index %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeatureScheduleRejectsInvalidStage(t *testing.T) {
|
||||||
|
set := &FeatureSet{Biomes: map[string]BiomeGeneration{"a": {Features: [][]string{{"f1"}}}}}
|
||||||
|
if _, err := set.FeatureSchedule([]string{"a"}, []string{"a"}, 1); err == nil {
|
||||||
|
t.Fatal("invalid stage succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlacementHeightDistributionsStayWithinInclusiveBounds(t *testing.T) {
|
func TestPlacementHeightDistributionsStayWithinInclusiveBounds(t *testing.T) {
|
||||||
r := NewLegacy(12345)
|
r := NewLegacy(12345)
|
||||||
for _, distribution := range []string{"minecraft:trapezoid", "minecraft:very_biased_to_bottom", "minecraft:uniform"} {
|
for _, distribution := range []string{"minecraft:trapezoid", "minecraft:very_biased_to_bottom", "minecraft:uniform"} {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue