Persist random Age traits
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-22 16:11:45 -04:00
1 parent 733028b943
commit 73dec80ae6
6 files changed
+139 -7

No files matched your search

+7
View File
@@ -262,6 +262,13 @@ from Book Stands. Snapshot chunk loading is independent from whether the Explore
control is enabled, so mounted books retain live terrain without allowing the
player to enter Explore mode.
Version 0.2.17.0 begins persistent random Age traits. New page-free Ages select
and save a legacy-inspired biome distribution, lighting mode, weather mode, and
celestial period, angle, and phase. The celestial ranges follow Legacy's random
symbol fallbacks: period 0.2–2.0, angle 0–360 degrees, and phase 0–1. Existing
Ages load with normal Legacy defaults instead of being rerolled. This immutable
trait layer is the foundation for applying each choice to modern worldgen.
Version 0.2.14.0 restores the legacy Linking and Descriptive Book artwork for
inventory items and gives each book its matching open-book texture when placed
on a Book Stand. Return Linking Books created inside another Age also retain and
+1 -1
View File
@@ -35,7 +35,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.2.16.15
mod_version=0.2.17.0
immersive_portals_version=6.0.7
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
@@ -15,18 +15,20 @@ public record AgeDefinition(
int generationVersion,
long createdGameTime,
ResourceLocation dimensionId,
BlockPos spawn
BlockPos spawn,
AgeTraits traits
) {
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "random_vanilla"
);
public static final int CURRENT_GENERATION_VERSION = 2;
public static final int CURRENT_GENERATION_VERSION = 3;
public AgeDefinition {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(name, "name");
Objects.requireNonNull(generationProfile, "generationProfile");
Objects.requireNonNull(dimensionId, "dimensionId");
Objects.requireNonNull(traits, "traits");
}
public static ResourceLocation dimensionId(UUID ageId) {
@@ -57,6 +59,6 @@ public record AgeDefinition(
public AgeDefinition withSpawn(BlockPos spawn) {
return new AgeDefinition(id, name, seed, generationProfile, generationVersion,
createdGameTime, dimensionId, spawn.immutable());
createdGameTime, dimensionId, spawn.immutable(), traits);
}
}
@@ -28,6 +28,13 @@ public final class AgeRegistrySavedData extends SavedData {
private static final String SPAWN_X_TAG = "SpawnX";
private static final String SPAWN_Y_TAG = "SpawnY";
private static final String SPAWN_Z_TAG = "SpawnZ";
private static final String TRAITS_TAG = "Traits";
private static final String BIOME_DISTRIBUTION_TAG = "BiomeDistribution";
private static final String LIGHTING_TAG = "Lighting";
private static final String WEATHER_TAG = "Weather";
private static final String CELESTIAL_PERIOD_TAG = "CelestialPeriod";
private static final String CELESTIAL_ANGLE_TAG = "CelestialAngle";
private static final String CELESTIAL_PHASE_TAG = "CelestialPhase";
private static final Factory<AgeRegistrySavedData> FACTORY = new Factory<>(
AgeRegistrySavedData::new,
AgeRegistrySavedData::load,
@@ -88,7 +95,8 @@ public final class AgeRegistrySavedData extends SavedData {
AgeDefinition.CURRENT_GENERATION_VERSION,
createdGameTime,
dimensionId,
null
null,
AgeTraits.select(seed)
);
ages.put(created.id(), created);
setDirty();
@@ -123,7 +131,8 @@ public final class AgeRegistrySavedData extends SavedData {
entry.contains(VERSION_TAG) ? entry.getInt(VERSION_TAG) : 1,
entry.getLong(CREATED_TIME_TAG),
dimensionId,
spawn
spawn,
readTraits(entry)
));
}
return data;
@@ -148,12 +157,40 @@ public final class AgeRegistrySavedData extends SavedData {
entry.putInt(SPAWN_Y_TAG, age.spawn().getY());
entry.putInt(SPAWN_Z_TAG, age.spawn().getZ());
}
CompoundTag traits = new CompoundTag();
traits.putString(BIOME_DISTRIBUTION_TAG, age.traits().biomeDistribution().name());
traits.putString(LIGHTING_TAG, age.traits().lighting().name());
traits.putString(WEATHER_TAG, age.traits().weather().name());
traits.putDouble(CELESTIAL_PERIOD_TAG, age.traits().celestialPeriod());
traits.putFloat(CELESTIAL_ANGLE_TAG, age.traits().celestialAngle());
traits.putFloat(CELESTIAL_PHASE_TAG, age.traits().celestialPhase());
entry.put(TRAITS_TAG, traits);
serializedAges.add(entry);
});
tag.put(AGES_TAG, serializedAges);
return tag;
}
private static AgeTraits readTraits(CompoundTag age) {
if (!age.contains(TRAITS_TAG, CompoundTag.TAG_COMPOUND)) {
return AgeTraits.LEGACY_DEFAULTS;
}
CompoundTag traits = age.getCompound(TRAITS_TAG);
AgeTraits defaults = AgeTraits.LEGACY_DEFAULTS;
try {
return new AgeTraits(
AgeTraits.parseOrDefault(traits.getString(BIOME_DISTRIBUTION_TAG), defaults.biomeDistribution()),
AgeTraits.parseOrDefault(traits.getString(LIGHTING_TAG), defaults.lighting()),
AgeTraits.parseOrDefault(traits.getString(WEATHER_TAG), defaults.weather()),
traits.contains(CELESTIAL_PERIOD_TAG) ? traits.getDouble(CELESTIAL_PERIOD_TAG) : defaults.celestialPeriod(),
traits.contains(CELESTIAL_ANGLE_TAG) ? traits.getFloat(CELESTIAL_ANGLE_TAG) : defaults.celestialAngle(),
traits.contains(CELESTIAL_PHASE_TAG) ? traits.getFloat(CELESTIAL_PHASE_TAG) : defaults.celestialPhase()
);
} catch (IllegalArgumentException exception) {
return defaults;
}
}
private static String normalizedName(String name) {
String normalized = name.strip();
return normalized.isEmpty() ? "Unnamed Age" : normalized;
@@ -0,0 +1,79 @@
package net.dnorberg.rivencraft.book;
import java.util.Random;
/** Immutable random-Age traits selected once from the Age seed. */
public record AgeTraits(
BiomeDistribution biomeDistribution,
Lighting lighting,
Weather weather,
double celestialPeriod,
float celestialAngle,
float celestialPhase
) {
public static final double MIN_CELESTIAL_PERIOD = 0.2D;
public static final double MAX_CELESTIAL_PERIOD = 2.0D;
public static final float MIN_CELESTIAL_ANGLE = 0.0F;
public static final float MAX_CELESTIAL_ANGLE = 360.0F;
public static final float MIN_CELESTIAL_PHASE = 0.0F;
public static final float MAX_CELESTIAL_PHASE = 1.0F;
public static final AgeTraits LEGACY_DEFAULTS = new AgeTraits(
BiomeDistribution.NATIVE, Lighting.NORMAL, Weather.NORMAL,
1.0D, 0.0F, 0.0F
);
public AgeTraits {
if (biomeDistribution == null || lighting == null || weather == null) {
throw new IllegalArgumentException("Age trait choices cannot be null");
}
if (celestialPeriod < MIN_CELESTIAL_PERIOD || celestialPeriod > MAX_CELESTIAL_PERIOD) {
throw new IllegalArgumentException("Celestial period is outside the legacy range");
}
if (celestialAngle < MIN_CELESTIAL_ANGLE || celestialAngle >= MAX_CELESTIAL_ANGLE) {
throw new IllegalArgumentException("Celestial angle is outside the legacy range");
}
if (celestialPhase < MIN_CELESTIAL_PHASE || celestialPhase >= MAX_CELESTIAL_PHASE) {
throw new IllegalArgumentException("Celestial phase is outside the legacy range");
}
}
public static AgeTraits select(long seed) {
Random random = new Random(seed ^ 0x524956454E435241L);
return new AgeTraits(
randomChoice(random, BiomeDistribution.values()),
randomChoice(random, Lighting.values()),
randomChoice(random, Weather.values()),
MIN_CELESTIAL_PERIOD
+ random.nextDouble() * (MAX_CELESTIAL_PERIOD - MIN_CELESTIAL_PERIOD),
random.nextFloat() * MAX_CELESTIAL_ANGLE,
random.nextFloat()
);
}
public static <E extends Enum<E>> E parseOrDefault(String value, E fallback) {
try {
@SuppressWarnings("unchecked")
Class<E> type = (Class<E>) fallback.getDeclaringClass();
return Enum.valueOf(type, value);
} catch (IllegalArgumentException exception) {
return fallback;
}
}
private static <E> E randomChoice(Random random, E[] values) {
return values[random.nextInt(values.length)];
}
public enum BiomeDistribution {
NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE
}
public enum Lighting {
NORMAL, DARK, BRIGHT
}
public enum Weather {
NORMAL, FAST, SLOW, OFF, ALWAYS, CLOUDY, RAIN, SNOW, STORM
}
}
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
import net.dnorberg.rivencraft.book.AgeTraits;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
@@ -118,6 +119,11 @@ public final class LinkingGameTests {
"A page-free book must select a supported legacy-inspired terrain profile");
helper.assertValueEqual(first.generationVersion(), AgeDefinition.CURRENT_GENERATION_VERSION,
"New Ages must record the current generation algorithm version");
helper.assertValueEqual(first.traits(), AgeTraits.select(first.seed()),
"A page-free book must persist the deterministic traits selected from its seed");
helper.assertTrue(first.traits().celestialPeriod() >= AgeTraits.MIN_CELESTIAL_PERIOD
&& first.traits().celestialPeriod() <= AgeTraits.MAX_CELESTIAL_PERIOD,
"The celestial period must remain inside Mystcraft Legacy's random range");
helper.assertTrue(first.seed() != another.seed(), "Distinct Age UUIDs must receive distinct random seeds");
helper.succeed();
}
@@ -158,7 +164,8 @@ public final class LinkingGameTests {
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "gametest/" + profile.getPath() + "_" + id
),
null
null,
AgeTraits.select(id.getMostSignificantBits())
);
var level = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), definition);
helper.assertTrue(level != null, "The " + profile + " profile must load a runtime dimension");