diff --git a/docs/PORTING.md b/docs/PORTING.md index 4cc7b96..f6b5f36 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -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 diff --git a/gradle.properties b/gradle.properties index 4024be6..3046453 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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. diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java index a0d6263..fcd1e65 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java @@ -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); } } diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java index f7355b5..47be6a7 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java @@ -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 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; diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java b/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java new file mode 100644 index 0000000..64fbdc1 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java @@ -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 parseOrDefault(String value, E fallback) { + try { + @SuppressWarnings("unchecked") + Class type = (Class) fallback.getDeclaringClass(); + return Enum.valueOf(type, value); + } catch (IllegalArgumentException exception) { + return fallback; + } + } + + private static 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 + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index 1c54fee..e2d38d4 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -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");