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

@@ -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");