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 (
|
||||
biomeTable *worldgen.ParameterTable
|
||||
biomeTableOnce sync.Once
|
||||
biomeOrder []string
|
||||
)
|
||||
|
||||
// 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))
|
||||
}
|
||||
params := make([]worldgen.BiomeParameter, 0, len(raw.Biomes))
|
||||
seen := make(map[string]bool)
|
||||
for _, e := range raw.Biomes {
|
||||
dp, ok := depthRange(e.Param.Depth)
|
||||
if !ok {
|
||||
continue // malformed depth; skip defensively
|
||||
}
|
||||
params = append(params, makeBiomeParameter(e, dp))
|
||||
if !seen[e.Biome] {
|
||||
seen[e.Biome] = true
|
||||
biomeOrder = append(biomeOrder, e.Biome)
|
||||
}
|
||||
}
|
||||
biomeTable = worldgen.NewParameterTable(params)
|
||||
})
|
||||
return biomeTable
|
||||
}
|
||||
|
||||
func possibleBiomeOrder() []string {
|
||||
loadBiomeTable()
|
||||
return biomeOrder
|
||||
}
|
||||
|
||||
// makeBiomeParameter converts a raw JSON entry into a BiomeParameter, mapping
|
||||
// the [min,max] ranges to quantized ClimateRanges. depth is a ClimateRange
|
||||
// (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"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
|
@ -22,6 +23,11 @@ type FeatureSet struct {
|
|||
BlockTags map[string][]string
|
||||
}
|
||||
|
||||
type IndexedFeature struct {
|
||||
Name string
|
||||
Index int
|
||||
}
|
||||
|
||||
type ConfiguredFeature struct {
|
||||
Type string
|
||||
Config json.RawMessage
|
||||
|
|
@ -210,6 +216,105 @@ func LoadFeatureSet() (*FeatureSet, error) {
|
|||
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) {
|
||||
configured, ok := s.Configured[name]
|
||||
if !ok || configured.Type != "minecraft:ore" {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package worldgen
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
||||
set, err := LoadFeatureSet()
|
||||
|
|
@ -42,3 +45,38 @@ func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
|||
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