Add data-pack Age runtime profiles (#9)
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-23 02:15:39 -04:00
1 parent b06fbda461
commit 109acc449a
18 files changed
+345 -43

No files matched your search

@@ -17,6 +17,7 @@ import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeWeatherService;
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
@@ -48,6 +49,7 @@ public final class Rivencraft {
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onPlayerLoggedOut);
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onServerStopping);
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
NeoForge.EVENT_BUS.addListener(AgeRuntimeProfiles::register);
LOGGER.info("Initializing Rivencraft");
}
}
@@ -5,7 +5,6 @@ import dev.galacticraft.dynamicdimensions.api.PlayerRemover;
import dev.galacticraft.dynamicdimensions.api.event.DimensionRemovedCallback;
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.List;
import java.util.Comparator;
import java.util.Collections;
@@ -23,7 +22,6 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.biome.Biomes;
@@ -32,7 +30,6 @@ import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterLists;
import net.dnorberg.rivencraft.world.ScaledBiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
@@ -65,7 +62,7 @@ public final class AgeDimensionService {
ServerLevel level = loader.loadDynamicDimension(
age.dimensionId(),
createGenerator(server, age),
createDimensionType(age)
createDimensionType(server, age)
);
if (level != null) {
applyCelestialClock(level, age);
@@ -107,7 +104,7 @@ public final class AgeDimensionService {
ServerLevel loaded = DynamicDimensionRegistry.from(server).loadDynamicDimension(
definition.dimensionId(),
createGenerator(server, definition),
createDimensionType(definition)
createDimensionType(server, definition)
);
if (loaded == null) {
Rivencraft.LOGGER.error("Failed to load Age {} as {}", definition.id(), definition.dimensionId());
@@ -264,15 +261,15 @@ public final class AgeDimensionService {
}
private static ChunkGenerator createGenerator(MinecraftServer server, AgeDefinition age) {
ResourceLocation profile = age.generationProfile();
if (profile.equals(AgeGenerationProfiles.FLAT)) {
AgeRuntimeProfile profile = AgeRuntimeProfiles.resolve(age.generationProfile());
if (profile.terrain() == AgeRuntimeProfile.Terrain.FLAT) {
return new FlatLevelSource(FlatLevelGeneratorSettings.getDefault(
server.registryAccess().lookupOrThrow(Registries.BIOME),
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
));
}
if (profile.equals(AgeGenerationProfiles.VOID)) {
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
FlatLevelGeneratorSettings settings = new FlatLevelGeneratorSettings(
Optional.empty(),
server.registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
@@ -285,22 +282,11 @@ public final class AgeDimensionService {
var biomeParameters = server.registryAccess()
.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
var noiseSettings = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
ResourceKey<NoiseGeneratorSettings> settingsKey;
if (profile.equals(AgeGenerationProfiles.CAVE)) {
settingsKey = NoiseGeneratorSettings.CAVES;
} else if (profile.equals(AgeGenerationProfiles.ISLAND)) {
settingsKey = NoiseGeneratorSettings.FLOATING_ISLANDS;
} else if (profile.equals(AgeGenerationProfiles.AMPLIFIED)) {
settingsKey = NoiseGeneratorSettings.AMPLIFIED;
} else {
settingsKey = NoiseGeneratorSettings.OVERWORLD;
}
// Cave uses Minecraft's enclosed cave density with stone, water, and
// Overworld-like surfaces while retaining the Age's biome source.
// Island/Skylands uses uniform floating-island density without the
// End's central dragon island. Both use the Overworld environment.
// random_vanilla is retained as a migration alias for existing Ages.
BiomeSource biomeSource = createBiomeSource(server, age, biomeParameters);
ResourceKey<NoiseGeneratorSettings> settingsKey = ResourceKey.create(
Registries.NOISE_SETTINGS, profile.noiseSettings());
ResourceKey<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomePreset =
ResourceKey.create(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, profile.biomePreset());
BiomeSource biomeSource = createBiomeSource(server, age, biomeParameters, biomePreset);
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
return new NoiseBasedChunkGenerator(biomeSource, Holder.direct(withBiomeMaterialRules(baseSettings)));
}
@@ -347,7 +333,8 @@ public final class AgeDimensionService {
private static BiomeSource createBiomeSource(
MinecraftServer server,
AgeDefinition age,
net.minecraft.core.HolderGetter<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomeParameters
net.minecraft.core.HolderGetter<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomeParameters,
ResourceKey<net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList> biomePreset
) {
List<net.minecraft.core.Holder.Reference<Biome>> biomes = server.registryAccess()
.lookupOrThrow(Registries.BIOME)
@@ -394,29 +381,32 @@ public final class AgeDimensionService {
);
}
return MultiNoiseBiomeSource.createFromPreset(
biomeParameters.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD));
biomeParameters.getOrThrow(biomePreset));
}
private static DimensionType createDimensionType(AgeDefinition age) {
private static DimensionType createDimensionType(MinecraftServer server, AgeDefinition age) {
AgeRuntimeProfile profile = AgeRuntimeProfiles.resolve(age.generationProfile());
DimensionType base = server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE)
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType())).value();
AgeTraits.Lighting lighting = age.traits().lighting();
boolean hasSkyLight = lighting != AgeTraits.Lighting.DARK;
float ambientLight = lighting == AgeTraits.Lighting.BRIGHT ? 0.25F : 0.0F;
return new DimensionType(
OptionalLong.empty(),
base.fixedTime(),
hasSkyLight,
false,
false,
true,
1.0D,
false,
false,
-64,
384,
384,
BlockTags.INFINIBURN_OVERWORLD,
BuiltinDimensionTypes.OVERWORLD_EFFECTS,
base.hasCeiling(),
base.ultraWarm(),
base.natural(),
base.coordinateScale(),
base.bedWorks(),
base.respawnAnchorWorks(),
base.minY(),
base.height(),
base.logicalHeight(),
base.infiniburn(),
base.effectsLocation(),
ambientLight,
new DimensionType.MonsterSettings(false, true, UniformInt.of(0, 7), 0)
base.monsterSettings()
);
}
@@ -33,6 +33,14 @@ public final class AgeGenerationProfiles {
return RANDOM_PROFILES;
}
/** Maps removed legacy aliases to a stable built-in profile. */
public static ResourceLocation migrationFallback(ResourceLocation profile) {
if (profile.equals(AgeDefinition.RANDOM_VANILLA_PROFILE)) {
return STANDARD;
}
return isSupported(profile) ? profile : STANDARD;
}
private static ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
}
@@ -91,7 +91,7 @@ public final class AgeRegistrySavedData extends SavedData {
reference.id(),
name,
seed,
AgeGenerationProfiles.select(seed),
AgeRuntimeProfiles.select(seed),
AgeDefinition.CURRENT_GENERATION_VERSION,
createdGameTime,
dimensionId,
@@ -0,0 +1,20 @@
package net.dnorberg.rivencraft.book;
import net.minecraft.resources.ResourceLocation;
/** Data-pack-defined resources used to construct an Age runtime dimension. */
public record AgeRuntimeProfile(
int schemaVersion,
Terrain terrain,
ResourceLocation noiseSettings,
ResourceLocation biomePreset,
ResourceLocation dimensionType
) {
public static final int CURRENT_SCHEMA_VERSION = 1;
public enum Terrain {
NOISE,
FLAT,
VOID
}
}
@@ -0,0 +1,140 @@
package net.dnorberg.rivencraft.book;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.neoforged.neoforge.event.AddReloadListenerEvent;
/** Loads {@code data/<namespace>/rivencraft/age_profiles/*.json}. */
public final class AgeRuntimeProfiles {
private static final Gson GSON = new GsonBuilder().create();
private static final String DIRECTORY = "rivencraft/age_profiles";
private static final Map<ResourceLocation, AgeRuntimeProfile> PROFILES = new ConcurrentHashMap<>();
private AgeRuntimeProfiles() {
}
public static void register(AddReloadListenerEvent event) {
event.addListener(new Loader(event.getRegistryAccess()));
}
public static AgeRuntimeProfile resolve(ResourceLocation id) {
AgeRuntimeProfile profile = PROFILES.get(id);
if (profile != null) {
return profile;
}
ResourceLocation fallback = AgeGenerationProfiles.migrationFallback(id);
profile = PROFILES.get(fallback);
if (profile == null) {
throw new IllegalStateException("Missing built-in Age runtime profile " + fallback);
}
Rivencraft.LOGGER.warn("Age runtime profile {} is unavailable; using deterministic fallback {}", id, fallback);
return profile;
}
public static Map<ResourceLocation, AgeRuntimeProfile> profiles() {
return Map.copyOf(PROFILES);
}
/** Selects from all valid loaded profiles in stable resource-ID order. */
public static ResourceLocation select(long seed) {
if (PROFILES.isEmpty()) {
return AgeGenerationProfiles.select(seed);
}
java.util.List<ResourceLocation> ids = PROFILES.keySet().stream().sorted().toList();
return ids.get(Math.floorMod(Long.hashCode(seed), ids.size()));
}
static AgeRuntimeProfile parse(ResourceLocation id, JsonElement json, RegistryAccess registries) {
JsonObject object = json.getAsJsonObject();
int schemaVersion = object.has("schema_version") ? object.get("schema_version").getAsInt() : 1;
if (schemaVersion != AgeRuntimeProfile.CURRENT_SCHEMA_VERSION) {
throw new IllegalArgumentException("unsupported schema_version " + schemaVersion);
}
AgeRuntimeProfile.Terrain terrain = AgeRuntimeProfile.Terrain.valueOf(
requiredString(object, "terrain").toUpperCase(java.util.Locale.ROOT));
ResourceLocation noise = location(object, "noise_settings", "minecraft:overworld");
ResourceLocation biome = location(object, "biome_preset", "minecraft:overworld");
ResourceLocation dimension = location(object, "dimension_type", "minecraft:overworld");
if (terrain == AgeRuntimeProfile.Terrain.NOISE) {
requireRegistryEntry(registries, Registries.NOISE_SETTINGS, noise, id, "noise_settings");
requireRegistryEntry(registries, Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST,
biome, id, "biome_preset");
}
requireRegistryEntry(registries, Registries.DIMENSION_TYPE, dimension, id, "dimension_type");
return new AgeRuntimeProfile(schemaVersion, terrain, noise, biome, dimension);
}
private static String requiredString(JsonObject object, String name) {
if (!object.has(name)) {
throw new IllegalArgumentException("missing " + name);
}
return object.get(name).getAsString();
}
private static ResourceLocation location(JsonObject object, String name, String fallback) {
ResourceLocation id = ResourceLocation.tryParse(object.has(name) ? object.get(name).getAsString() : fallback);
if (id == null) {
throw new IllegalArgumentException("invalid " + name);
}
return id;
}
private static <T> void requireRegistryEntry(
RegistryAccess registries,
ResourceKey<? extends net.minecraft.core.Registry<T>> registry,
ResourceLocation entry,
ResourceLocation profile,
String field
) {
if (registries.lookupOrThrow(registry).get(ResourceKey.create(registry, entry)).isEmpty()) {
throw new IllegalArgumentException(profile + " references missing " + field + " " + entry);
}
}
private static final class Loader extends SimpleJsonResourceReloadListener {
private final RegistryAccess registries;
private Loader(RegistryAccess registries) {
super(GSON, DIRECTORY);
this.registries = registries;
}
@Override
protected void apply(Map<ResourceLocation, JsonElement> resources,
ResourceManager resourceManager,
ProfilerFiller profiler) {
Map<ResourceLocation, AgeRuntimeProfile> loaded = new java.util.HashMap<>();
resources.forEach((id, json) -> {
try {
loaded.put(id, parse(id, json, registries));
} catch (RuntimeException exception) {
Rivencraft.LOGGER.error("Ignoring invalid Age runtime profile {}: {}", id, exception.getMessage());
}
});
for (ResourceLocation required : AgeGenerationProfiles.all()) {
if (!loaded.containsKey(required)) {
throw new IllegalStateException("Required built-in Age runtime profile is missing: " + required);
}
}
PROFILES.clear();
PROFILES.putAll(loaded);
Rivencraft.LOGGER.info("Loaded {} Age runtime profiles", loaded.size());
}
}
}
@@ -6,6 +6,8 @@ import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.AgeRuntimeProfile;
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
@@ -38,6 +40,44 @@ public final class LinkingGameTests {
private LinkingGameTests() {
}
@GameTest(template = "empty")
public static void dataPackAgeProfilesResolveRegisteredResources(GameTestHelper helper) {
var registries = helper.getLevel().registryAccess();
var profiles = AgeRuntimeProfiles.profiles();
helper.assertTrue(profiles.keySet().containsAll(AgeGenerationProfiles.all()),
"Every built-in Age runtime profile must survive data-pack validation");
for (var entry : profiles.entrySet()) {
AgeRuntimeProfile profile = entry.getValue();
helper.assertValueEqual(profile.schemaVersion(), AgeRuntimeProfile.CURRENT_SCHEMA_VERSION,
entry.getKey() + " must use the current runtime-profile schema");
helper.assertTrue(registries.lookupOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE)
.get(net.minecraft.resources.ResourceKey.create(
net.minecraft.core.registries.Registries.DIMENSION_TYPE,
profile.dimensionType())).isPresent(),
entry.getKey() + " must reference a registered dimension type");
if (profile.terrain() == AgeRuntimeProfile.Terrain.NOISE) {
helper.assertTrue(registries.lookupOrThrow(net.minecraft.core.registries.Registries.NOISE_SETTINGS)
.get(net.minecraft.resources.ResourceKey.create(
net.minecraft.core.registries.Registries.NOISE_SETTINGS,
profile.noiseSettings())).isPresent(),
entry.getKey() + " must reference registered noise settings");
helper.assertTrue(registries.lookupOrThrow(
net.minecraft.core.registries.Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)
.get(net.minecraft.resources.ResourceKey.create(
net.minecraft.core.registries.Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST,
profile.biomePreset())).isPresent(),
entry.getKey() + " must reference a registered biome preset");
}
}
var missing = net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("example", "removed_profile");
helper.assertValueEqual(
AgeRuntimeProfiles.resolve(missing),
AgeRuntimeProfiles.resolve(AgeGenerationProfiles.STANDARD),
"A removed custom profile must use the documented deterministic fallback"
);
helper.succeed();
}
@GameTest(template = "empty")
public static void linkTargetSurvivesItemSerialization(GameTestHelper helper) {
LinkTarget expected = new LinkTarget(
@@ -0,0 +1,7 @@
{
"schema_version": 1,
"terrain": "noise",
"noise_settings": "minecraft:amplified",
"biome_preset": "minecraft:overworld",
"dimension_type": "minecraft:overworld"
}
@@ -0,0 +1,7 @@
{
"schema_version": 1,
"terrain": "noise",
"noise_settings": "minecraft:caves",
"biome_preset": "minecraft:overworld",
"dimension_type": "minecraft:overworld"
}
@@ -0,0 +1,5 @@
{
"schema_version": 1,
"terrain": "flat",
"dimension_type": "minecraft:overworld"
}
@@ -0,0 +1,7 @@
{
"schema_version": 1,
"terrain": "noise",
"noise_settings": "minecraft:floating_islands",
"biome_preset": "minecraft:overworld",
"dimension_type": "minecraft:overworld"
}
@@ -0,0 +1,7 @@
{
"schema_version": 1,
"terrain": "noise",
"noise_settings": "minecraft:overworld",
"biome_preset": "minecraft:overworld",
"dimension_type": "minecraft:overworld"
}
@@ -0,0 +1,5 @@
{
"schema_version": 1,
"terrain": "void",
"dimension_type": "minecraft:overworld"
}