Pin feature ordering to vanilla runtime
This commit is contained in:
parent
a73b689449
commit
e9fe526300
5 changed files with 264 additions and 1 deletions
|
|
@ -62,6 +62,7 @@ func depthRange(v any) (worldgen.ClimateRange, bool) {
|
||||||
var (
|
var (
|
||||||
biomeTable *worldgen.ParameterTable
|
biomeTable *worldgen.ParameterTable
|
||||||
biomeTableOnce sync.Once
|
biomeTableOnce sync.Once
|
||||||
|
biomeOrder []string
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadBiomeTable parses the embedded biome parameters once and returns the full
|
// loadBiomeTable parses the embedded biome parameters once and returns the full
|
||||||
|
|
@ -76,18 +77,28 @@ func loadBiomeTable() *worldgen.ParameterTable {
|
||||||
panic(fmt.Sprintf("world: parsing embedded biome_parameters.json: %v", err))
|
panic(fmt.Sprintf("world: parsing embedded biome_parameters.json: %v", err))
|
||||||
}
|
}
|
||||||
params := make([]worldgen.BiomeParameter, 0, len(raw.Biomes))
|
params := make([]worldgen.BiomeParameter, 0, len(raw.Biomes))
|
||||||
|
seen := make(map[string]bool)
|
||||||
for _, e := range raw.Biomes {
|
for _, e := range raw.Biomes {
|
||||||
dp, ok := depthRange(e.Param.Depth)
|
dp, ok := depthRange(e.Param.Depth)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue // malformed depth; skip defensively
|
continue // malformed depth; skip defensively
|
||||||
}
|
}
|
||||||
params = append(params, makeBiomeParameter(e, dp))
|
params = append(params, makeBiomeParameter(e, dp))
|
||||||
|
if !seen[e.Biome] {
|
||||||
|
seen[e.Biome] = true
|
||||||
|
biomeOrder = append(biomeOrder, e.Biome)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
biomeTable = worldgen.NewParameterTable(params)
|
biomeTable = worldgen.NewParameterTable(params)
|
||||||
})
|
})
|
||||||
return biomeTable
|
return biomeTable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func possibleBiomeOrder() []string {
|
||||||
|
loadBiomeTable()
|
||||||
|
return biomeOrder
|
||||||
|
}
|
||||||
|
|
||||||
// makeBiomeParameter converts a raw JSON entry into a BiomeParameter, mapping
|
// makeBiomeParameter converts a raw JSON entry into a BiomeParameter, mapping
|
||||||
// the [min,max] ranges to quantized ClimateRanges. depth is a ClimateRange
|
// the [min,max] ranges to quantized ClimateRanges. depth is a ClimateRange
|
||||||
// (exact range for scalar depths, explicit range for cave biomes).
|
// (exact range for scalar depths, explicit range for cave biomes).
|
||||||
|
|
|
||||||
28
internal/world/feature_sorter_test.go
Normal file
28
internal/world/feature_sorter_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOverworldFeatureStepsBuildWithoutCycles(t *testing.T) {
|
||||||
|
set, err := worldgen.LoadFeatureSet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
steps, err := set.FeatureSteps(possibleBiomeOrder())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(steps) != 11 {
|
||||||
|
t.Fatalf("feature steps = %d, want 11", len(steps))
|
||||||
|
}
|
||||||
|
ores := steps[undergroundOresStage]
|
||||||
|
for _, name := range []string{"minecraft:ore_clay", "minecraft:ore_diamond", "minecraft:ore_tuff"} {
|
||||||
|
if !slices.Contains(ores, name) {
|
||||||
|
t.Errorf("underground ores missing %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
@ -22,6 +23,11 @@ type FeatureSet struct {
|
||||||
BlockTags map[string][]string
|
BlockTags map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IndexedFeature struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
}
|
||||||
|
|
||||||
type ConfiguredFeature struct {
|
type ConfiguredFeature struct {
|
||||||
Type string
|
Type string
|
||||||
Config json.RawMessage
|
Config json.RawMessage
|
||||||
|
|
@ -210,6 +216,105 @@ func LoadFeatureSet() (*FeatureSet, error) {
|
||||||
return featureSet, featureSetErr
|
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
|
||||||
|
}
|
||||||
|
|
||||||
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" {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package worldgen
|
package worldgen
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
||||||
set, err := LoadFeatureSet()
|
set, err := LoadFeatureSet()
|
||||||
|
|
@ -42,3 +45,38 @@ func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
||||||
t.Fatalf("spring lava placement = %+v, err=%v", lavaPlan, err)
|
t.Fatalf("spring lava placement = %+v, err=%v", lavaPlan, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFeatureStepsAgainstVanillaRuntimeVectors(t *testing.T) {
|
||||||
|
set := &FeatureSet{Biomes: map[string]BiomeGeneration{
|
||||||
|
"a": {Features: [][]string{{"f1"}, {"f3", "f4"}}},
|
||||||
|
"b": {Features: [][]string{{"f2", "f1"}, {"f5", "f4"}}},
|
||||||
|
}}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
order []string
|
||||||
|
want [][]string
|
||||||
|
}{
|
||||||
|
{"ab", []string{"a", "b"}, [][]string{{"f2", "f1"}, {"f5", "f3", "f4"}}},
|
||||||
|
{"ba", []string{"b", "a"}, [][]string{{"f2", "f1"}, {"f3", "f5", "f4"}}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got, err := set.FeatureSteps(test.order)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, test.want) {
|
||||||
|
t.Fatalf("steps = %v, want %v", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeatureStepsRejectsCycles(t *testing.T) {
|
||||||
|
set := &FeatureSet{Biomes: map[string]BiomeGeneration{
|
||||||
|
"a": {Features: [][]string{{"f1", "f2"}}},
|
||||||
|
"b": {Features: [][]string{{"f2", "f1"}}},
|
||||||
|
}}
|
||||||
|
if _, err := set.FeatureSteps([]string{"a", "b"}); err == nil {
|
||||||
|
t.Fatal("cyclic feature order succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
81
tools/VanillaFeatureSorterVectors.java
Normal file
81
tools/VanillaFeatureSorterVectors.java
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||||
|
import net.minecraft.world.level.biome.FeatureSorter;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.CountPlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
|
||||||
|
// Emits FeatureSorter vectors from the official 26.1.2 runtime. The graph is
|
||||||
|
// synthetic so this helper verifies ordering and identity without booting a
|
||||||
|
// world or depending on registry datapack loading.
|
||||||
|
public final class VanillaFeatureSorterVectors {
|
||||||
|
private static final Map<PlacedFeature, String> NAMES = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
|
||||||
|
PlacedFeature f1 = feature("f1", 1);
|
||||||
|
PlacedFeature f2 = feature("f2", 2);
|
||||||
|
PlacedFeature f3 = feature("f3", 3);
|
||||||
|
PlacedFeature f4 = feature("f4", 4);
|
||||||
|
PlacedFeature f5 = feature("f5", 5);
|
||||||
|
|
||||||
|
BiomeGenerationSettings a = settings(
|
||||||
|
List.of(List.of(f1), List.of(f3, f4)));
|
||||||
|
BiomeGenerationSettings b = settings(
|
||||||
|
List.of(List.of(f2, f1), List.of(f5, f4)));
|
||||||
|
|
||||||
|
dump("ab", List.of(a, b));
|
||||||
|
dump("ba", List.of(b, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlacedFeature feature(String name, int count) {
|
||||||
|
ConfiguredFeature<NoneFeatureConfiguration, Feature<NoneFeatureConfiguration>> configured =
|
||||||
|
new ConfiguredFeature<>(Feature.NO_OP, NoneFeatureConfiguration.INSTANCE);
|
||||||
|
PlacedFeature placed = new PlacedFeature(Holder.direct(configured), List.of(CountPlacement.of(count)));
|
||||||
|
NAMES.put(placed, name);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static BiomeGenerationSettings settings(List<List<PlacedFeature>> stages) throws Exception {
|
||||||
|
List<HolderSet<PlacedFeature>> holders = new ArrayList<>();
|
||||||
|
for (List<PlacedFeature> stage : stages) {
|
||||||
|
Holder<PlacedFeature>[] values = stage.stream().map(Holder::direct).toArray(Holder[]::new);
|
||||||
|
holders.add(HolderSet.direct(values));
|
||||||
|
}
|
||||||
|
Constructor<BiomeGenerationSettings> constructor = BiomeGenerationSettings.class
|
||||||
|
.getDeclaredConstructor(HolderSet.class, List.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance(HolderSet.empty(), holders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dump(String label, List<BiomeGenerationSettings> biomes) {
|
||||||
|
Function<BiomeGenerationSettings, List<HolderSet<PlacedFeature>>> stages =
|
||||||
|
BiomeGenerationSettings::features;
|
||||||
|
List<FeatureSorter.StepFeatureData> steps =
|
||||||
|
FeatureSorter.buildFeaturesPerStep(biomes, stages, true);
|
||||||
|
for (int stage = 0; stage < steps.size(); stage++) {
|
||||||
|
FeatureSorter.StepFeatureData step = steps.get(stage);
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (int index = 0; index < step.features().size(); index++) {
|
||||||
|
PlacedFeature feature = step.features().get(index);
|
||||||
|
values.add(NAMES.get(feature) + "@" + step.indexMapping().applyAsInt(feature));
|
||||||
|
}
|
||||||
|
System.out.println(label + ".stage" + stage + "=" + String.join(",", values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue