diff --git a/docs/PORTING.md b/docs/PORTING.md index 5d93d41..ce347b2 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -299,6 +299,11 @@ later loads continue from saved world time. Celestial angle remains persisted for a future custom sky renderer because vanilla dimension data cannot rotate the sun's render plane. +Version 0.2.20.1 restores Legacy's Zero Length celestial modifier as a rarer +random outcome using its effective one-in-seven grammar weight. Zero Length +freezes an Age at its generated phase without changing the global daylight +cycle, while nonzero Ages retain their independently scaled clocks. + 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 9c170fa..f605d64 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.20.0 +mod_version=0.2.20.1 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/Rivencraft.java b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index 8854ddb..2d61c36 100644 --- a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -13,6 +13,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.AgeCelestialService; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; @@ -38,6 +39,7 @@ public final class Rivencraft { ModCreativeTabs.register(modEventBus); NeoForge.EVENT_BUS.addListener(BookStandConversionHandler::onRightClickBlock); NeoForge.EVENT_BUS.addListener(AgeWeatherService::onLevelTickPost); + NeoForge.EVENT_BUS.addListener(AgeCelestialService::onLevelTickPost); LOGGER.info("Initializing Rivencraft"); } } diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeCelestialService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeCelestialService.java new file mode 100644 index 0000000..b4caec6 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeCelestialService.java @@ -0,0 +1,31 @@ +package net.dnorberg.rivencraft.book; + +import java.util.Map; +import java.util.WeakHashMap; +import net.minecraft.server.level.ServerLevel; +import net.neoforged.neoforge.event.tick.LevelTickEvent; + +/** Holds Zero Length Age clocks without changing the server-wide daylight gamerule. */ +public final class AgeCelestialService { + private static final Map FROZEN_TIMES = new WeakHashMap<>(); + + private AgeCelestialService() { + } + + public static void freezeAt(ServerLevel level, long dayTime) { + FROZEN_TIMES.put(level, dayTime); + } + + public static void unfreeze(ServerLevel level) { + FROZEN_TIMES.remove(level); + } + + public static void onLevelTickPost(LevelTickEvent.Post event) { + if (event.getLevel() instanceof ServerLevel level) { + Long frozenTime = FROZEN_TIMES.get(level); + if (frozenTime != null && level.getDayTime() != frozenTime) { + level.setDayTime(frozenTime); + } + } + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java index 4ff9189..5c91056 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDefinition.java @@ -21,7 +21,7 @@ public record AgeDefinition( public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath( Rivencraft.MOD_ID, "random_vanilla" ); - public static final int CURRENT_GENERATION_VERSION = 6; + public static final int CURRENT_GENERATION_VERSION = 7; public AgeDefinition { Objects.requireNonNull(id, "id"); diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index 1298055..dbcbff4 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -103,12 +103,21 @@ public final class AgeDimensionService { } private static void applyCelestialClock(ServerLevel level, AgeDefinition age) { - level.setDayTimePerTick((float)(1.0D / age.traits().celestialPeriod())); + if (age.traits().hasStoppedCelestialCycle()) { + level.setDayTimePerTick(-1.0F); + AgeCelestialService.freezeAt(level, level.getDayTime()); + } else { + AgeCelestialService.unfreeze(level); + level.setDayTimePerTick((float)(1.0D / age.traits().celestialPeriod())); + } } private static void applyInitialCelestialPhase(ServerLevel level, AgeDefinition age) { long phaseTime = (long)(age.traits().celestialPhase() * Level.TICKS_PER_DAY); level.setDayTime(phaseTime); + if (age.traits().hasStoppedCelestialCycle()) { + AgeCelestialService.freezeAt(level, phaseTime); + } } private static void synchronizeImmersivePortals(MinecraftServer server) { diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java b/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java index 64fbdc1..cfd2d04 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java @@ -13,6 +13,7 @@ public record AgeTraits( ) { public static final double MIN_CELESTIAL_PERIOD = 0.2D; public static final double MAX_CELESTIAL_PERIOD = 2.0D; + public static final double STOPPED_CELESTIAL_PERIOD = 0.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; @@ -27,7 +28,8 @@ public record 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) { + if (celestialPeriod != STOPPED_CELESTIAL_PERIOD + && (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) { @@ -40,17 +42,27 @@ public record AgeTraits( public static AgeTraits select(long seed) { Random random = new Random(seed ^ 0x524956454E435241L); + BiomeDistribution biomeDistribution = randomChoice(random, BiomeDistribution.values()); + Lighting lighting = randomChoice(random, Lighting.values()); + Weather weather = randomChoice(random, Weather.values()); + // Legacy's PERIOD_BASIC grammar gives ModZero rank 2 and its other + // three length modifiers rank 1, producing an effective 1-in-7 weight. + boolean stopped = random.nextInt(7) == 0; return new AgeTraits( - randomChoice(random, BiomeDistribution.values()), - randomChoice(random, Lighting.values()), - randomChoice(random, Weather.values()), - MIN_CELESTIAL_PERIOD + biomeDistribution, + lighting, + weather, + stopped ? STOPPED_CELESTIAL_PERIOD : MIN_CELESTIAL_PERIOD + random.nextDouble() * (MAX_CELESTIAL_PERIOD - MIN_CELESTIAL_PERIOD), random.nextFloat() * MAX_CELESTIAL_ANGLE, random.nextFloat() ); } + public boolean hasStoppedCelestialCycle() { + return celestialPeriod == STOPPED_CELESTIAL_PERIOD; + } + public static > E parseOrDefault(String value, E fallback) { try { @SuppressWarnings("unchecked") diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index ab9ed1d..d1244b7 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -121,7 +121,8 @@ public final class LinkingGameTests { "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 + helper.assertTrue(first.traits().hasStoppedCelestialCycle() + || 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"); @@ -186,9 +187,14 @@ public final class LinkingGameTests { ? 0.25F : 0.0F; helper.assertTrue(Math.abs(level.dimensionType().ambientLight() - expectedAmbientLight) < 0.0001F, "The runtime dimension must apply the persisted lighting trait"); - float expectedDaySpeed = (float)(1.0D / definition.traits().celestialPeriod()); - helper.assertTrue(Math.abs(level.getDayTimePerTick() - expectedDaySpeed) < 0.0001F, - "The runtime dimension must apply the persisted celestial period"); + if (definition.traits().hasStoppedCelestialCycle()) { + helper.assertTrue(level.getDayTimePerTick() < 0.0F, + "A Zero Length Age must leave vanilla clock advancement under the freeze controller"); + } else { + float expectedDaySpeed = (float)(1.0D / definition.traits().celestialPeriod()); + helper.assertTrue(Math.abs(level.getDayTimePerTick() - expectedDaySpeed) < 0.0001F, + "The runtime dimension must apply the persisted celestial period"); + } } helper.succeed(); }