From 99ce98fb43a6e8b880e7f62b364adf6d63ad9cd2 Mon Sep 17 00:00:00 2001
From: Steveplays28 <djspaargaren@outlook.com>
Date: Sun, 8 Oct 2023 14:17:12 +0200
Subject: [PATCH] feat: Add exponential sleep speed curve, rework wake up
 timing and grace period

Also improved how night time is determined.
---
 .../config/RealisticSleepConfig.java          |  2 +-
 .../mixin/ServerPlayerEntityMixin.java        | 35 +++++++++----
 .../mixin/ServerWorldMixin.java               | 52 +++++++++----------
 .../realisticsleep/util/SleepMathUtil.java    | 10 ++--
 4 files changed, 57 insertions(+), 42 deletions(-)

diff --git a/src/main/java/com/github/steveplays28/realisticsleep/config/RealisticSleepConfig.java b/src/main/java/com/github/steveplays28/realisticsleep/config/RealisticSleepConfig.java
index e59df24..a53c6d6 100644
--- a/src/main/java/com/github/steveplays28/realisticsleep/config/RealisticSleepConfig.java
+++ b/src/main/java/com/github/steveplays28/realisticsleep/config/RealisticSleepConfig.java
@@ -43,6 +43,6 @@ public class RealisticSleepConfig implements ConfigData {
 	public long tickDelay = -1;
 
 	public enum SleepSpeedCurve {
-		@ConfigEntry.Gui.Tooltip LINEAR
+		LINEAR, EXPONENTIAL
 	}
 }
diff --git a/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerPlayerEntityMixin.java b/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerPlayerEntityMixin.java
index 0f1c928..7f270b0 100644
--- a/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerPlayerEntityMixin.java
+++ b/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerPlayerEntityMixin.java
@@ -1,6 +1,5 @@
 package com.github.steveplays28.realisticsleep.mixin;
 
-import com.github.steveplays28.realisticsleep.util.SleepMathUtil;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
 import net.minecraft.server.network.ServerPlayerEntity;
@@ -14,6 +13,7 @@
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
 import static com.github.steveplays28.realisticsleep.RealisticSleep.config;
+import static com.github.steveplays28.realisticsleep.util.SleepMathUtil.*;
 
 @Mixin(ServerPlayerEntity.class)
 public abstract class ServerPlayerEntityMixin extends Entity {
@@ -24,22 +24,35 @@ public ServerPlayerEntityMixin(EntityType<?> type, World world) {
 	@Shadow
 	public abstract void sendMessage(Text message, boolean overlay);
 
-	@Shadow public abstract ServerWorld getWorld();
+	@Shadow
+	public abstract ServerWorld getWorld();
 
 	@Inject(method = "wakeUp(ZZ)V", at = @At(value = "HEAD"))
 	public void wakeUpInject(boolean skipSleepTimer, boolean updateSleepingPlayers, CallbackInfo ci) {
-		if (!SleepMathUtil.isNightTime(getWorld().getTimeOfDay())) {
-			// Return if we shouldn't send the dawn message
-			if (!config.sendDawnMessage || config.dawnMessage.isEmpty()) return;
+		var timeOfDay = getWorld().getTimeOfDay() % DAY_LENGTH;
+		var ticksUntilDawn = Math.abs(timeOfDay - DAWN_WAKE_UP_TIME);
+		var ticksUntilDusk = Math.abs(timeOfDay - DUSK_WAKE_UP_TIME);
 
-			// Send dawn HUD message to player
-			sendMessage(Text.of(config.dawnMessage), true);
-		} else if (config.allowDaySleeping && SleepMathUtil.isNightTime(getWorld().getTimeOfDay())) { //Only shows this message if day sleeping is allowed
-			// Return if we shouldn't send the dawn or dusk message
-			if (!config.sendDuskMessage || config.duskMessage.isEmpty()) return;
+		if (ticksUntilDawn > WAKE_UP_GRACE_PERIOD_TICKS && ticksUntilDusk > WAKE_UP_GRACE_PERIOD_TICKS) {
+			return;
+		}
 
-			// Send dawn or dusk HUD message to player
+		if (ticksUntilDusk < ticksUntilDawn) {
+			// Return if the dusk message shouldn't be sent
+			if (!config.sendDuskMessage || config.duskMessage.isEmpty()) {
+				return;
+			}
+
+			// Send dusk message to player in the actionbar
 			sendMessage(Text.of(config.duskMessage), true);
+		} else {
+			// Return if the dawn message shouldn't be sent
+			if (!config.sendDawnMessage || config.dawnMessage.isEmpty()) {
+				return;
+			}
+
+			// Send dawn message to player in the actionbar
+			sendMessage(Text.of(config.dawnMessage), true);
 		}
 	}
 }
diff --git a/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerWorldMixin.java b/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerWorldMixin.java
index ad29aea..ad8acc3 100644
--- a/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerWorldMixin.java
+++ b/src/main/java/com/github/steveplays28/realisticsleep/mixin/ServerWorldMixin.java
@@ -33,11 +33,12 @@
 import static com.github.steveplays28.realisticsleep.RealisticSleep.MOD_ID;
 import static com.github.steveplays28.realisticsleep.RealisticSleep.config;
 import static com.github.steveplays28.realisticsleep.util.SleepMathUtil.DAY_LENGTH;
+import static com.github.steveplays28.realisticsleep.util.SleepMathUtil.WAKE_UP_GRACE_PERIOD_TICKS;
 
 @Mixin(ServerWorld.class)
 public abstract class ServerWorldMixin extends World {
 	@Unique
-	public double nightTimeStepPerTick = 1;
+	public double nightTimeStepPerTick = 2;
 	@Unique
 	public int nightTimeStepPerTickRounded = 1;
 	@Unique
@@ -72,7 +73,8 @@ public abstract class ServerWorldMixin extends World {
 	@Shadow
 	public abstract List<ServerPlayerEntity> getPlayers();
 
-	@Shadow protected abstract void wakeSleepingPlayers();
+	@Shadow
+	protected abstract void wakeSleepingPlayers();
 
 	protected ServerWorldMixin(MutableWorldProperties properties, RegistryKey<World> registryRef, RegistryEntry<DimensionType> registryEntry, Supplier<Profiler> profiler, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) {
 		super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed, maxChainedNeighborUpdates);
@@ -80,13 +82,15 @@ protected ServerWorldMixin(MutableWorldProperties properties, RegistryKey<World>
 
 	@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/GameRules;getInt(Lnet/minecraft/world/GameRules$Key;)I"))
 	public void tickInject(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
+		// Calculate seconds until awake
 		int sleepingPlayerCount = sleepManager.getSleeping();
+		int playerCount = getPlayers().size();
+		double sleepingRatio = (double) sleepingPlayerCount / playerCount;
+		nightTimeStepPerTick = SleepMathUtil.calculateNightTimeStepPerTick(
+				sleepingRatio, config.sleepSpeedMultiplier, nightTimeStepPerTick);
+		int timeOfDay = (int) worldProperties.getTimeOfDay() % DAY_LENGTH;
 		// TODO: Don't assume the TPS is 20
-		int secondsUntilAwake = Math.abs(
-				SleepMathUtil.calculateSecondsUntilAwake((int) worldProperties.getTimeOfDay() % DAY_LENGTH, nightTimeStepPerTick, 20));
-
-		//Gets the remainder of the current time of day, as this number never actually resets each day(from my own testing)
-		int ticksUntilAwake = SleepMathUtil.calculateTicksUntilAwake((int) worldProperties.getTimeOfDay() % DAY_LENGTH);
+		int secondsUntilAwake = Math.abs(SleepMathUtil.calculateSecondsUntilAwake(timeOfDay, nightTimeStepPerTick, 20));
 
 		// Check if the night has (almost) ended and the weather should be skipped
 		if (secondsUntilAwake <= 2 && shouldSkipWeather) {
@@ -100,13 +104,10 @@ public void tickInject(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
 		}
 
 		// Fetch config values and do calculations
-		int playerCount = getPlayers().size();
-		double sleepingRatio = (double) sleepingPlayerCount / playerCount;
-		double sleepingPercentage = sleepingRatio * 100;
-
-		nightTimeStepPerTick = SleepMathUtil.calculateNightTimeStepPerTick(sleepingRatio, config.sleepSpeedMultiplier, nightTimeStepPerTick);
 		nightTimeStepPerTickRounded = (int) Math.round(nightTimeStepPerTick);
-		var isNight = SleepMathUtil.isNightTime(worldProperties.getTimeOfDay());
+		int ticksUntilAwake = Math.abs(SleepMathUtil.calculateTicksUntilAwake(timeOfDay));
+		double sleepingPercentage = sleepingRatio * 100;
+		var isNight = SleepMathUtil.isNightTime(timeOfDay);
 		var nightDayOrThunderstormText = Text.translatable(
 				String.format("%s.text.%s", MOD_ID, worldProperties.isThundering() ? "thunderstorm" : isNight ? "night" : "day"));
 
@@ -160,34 +161,33 @@ public void tickInject(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
 				new WorldTimeUpdateS2CPacket(worldProperties.getTime(), worldProperties.getTimeOfDay(), doDayLightCycle), getRegistryKey());
 
 		// Check if players are still supposed to be sleeping, and send a HUD message if so
-		if (secondsUntilAwake > 0) {
+		if (ticksUntilAwake > WAKE_UP_GRACE_PERIOD_TICKS) {
 			shouldSkipWeather = true;
 
 			if (config.sendSleepingMessage) {
 				sleepMessage = Text.translatable(String.format("%s.text.sleep_message", MOD_ID), sleepingPlayerCount, playerCount).append(
 						nightDayOrThunderstormText);
 
-				if(isNight) {
+				if (isNight) {
 					if (config.showTimeUntilDawn) {
 						sleepMessage.append(Text.translatable(String.format("%s.text.time_until_dawn", MOD_ID), secondsUntilAwake));
 					}
-				} else {
-					if(config.showTimeUntilDusk) {
-						sleepMessage.append(Text.translatable(String.format("%s.text.time_until_dusk", MOD_ID), secondsUntilAwake));
-					}
+				} else if (config.showTimeUntilDusk) {
+					sleepMessage.append(Text.translatable(String.format("%s.text.time_until_dusk", MOD_ID), secondsUntilAwake));
 				}
+			}
 
-				for (ServerPlayerEntity player : players) {
-					player.sendMessage(sleepMessage, true);
-				}
+			for (ServerPlayerEntity player : players) {
+				player.sendMessage(sleepMessage, true);
 			}
 		}
 
-		int tickGrace = 30; //The amount of extra ticks for waking up - maybe useful in cases where TPS is low?
-		//In my own testing, using just secondsUntilAwake <= 0 seemed to have a few seconds where
-		//trying to sleep back in the bed would kick the player right out
-		if (secondsUntilAwake <= 0 && ticksUntilAwake <= tickGrace) {
+		if (ticksUntilAwake <= WAKE_UP_GRACE_PERIOD_TICKS) {
+			// Wake up sleeping players
 			this.wakeSleepingPlayers();
+
+			// Reset night time step per tick, to reset the exponential sleep speed curve calculation
+			nightTimeStepPerTick = 2;
 		}
 	}
 
diff --git a/src/main/java/com/github/steveplays28/realisticsleep/util/SleepMathUtil.java b/src/main/java/com/github/steveplays28/realisticsleep/util/SleepMathUtil.java
index 27756ed..3e6e00b 100644
--- a/src/main/java/com/github/steveplays28/realisticsleep/util/SleepMathUtil.java
+++ b/src/main/java/com/github/steveplays28/realisticsleep/util/SleepMathUtil.java
@@ -4,12 +4,14 @@
 
 public class SleepMathUtil {
 	public static final int DAY_LENGTH = 24000;
-	public static final int SUNRISE_WAKE_UP = 23449;
-	public static final int SUNSET_WAKE_UP = 12449;
+	public static final int DAWN_WAKE_UP_TIME = 23449;
+	public static final int DUSK_WAKE_UP_TIME = 12449;
+	public static final double WAKE_UP_GRACE_PERIOD_TICKS = Math.max(config.sleepSpeedMultiplier, 20);
 
 	public static double calculateNightTimeStepPerTick(double sleepingRatio, double multiplier, double lastTimeStepPerTick) {
 		return switch (config.sleepSpeedCurve) {
 			case LINEAR -> sleepingRatio * multiplier;
+			case EXPONENTIAL -> Math.pow(lastTimeStepPerTick, 1 + sleepingRatio * multiplier);
 		};
 	}
 
@@ -18,7 +20,7 @@ public static int calculateTicksToTimeOfDay(int timeOfDay, int targetTimeOfDay)
 	}
 
 	public static int calculateTicksUntilAwake(int currentTimeOfDay) {
-		return calculateTicksToTimeOfDay(currentTimeOfDay, isNightTime(currentTimeOfDay) ? SUNRISE_WAKE_UP : SUNSET_WAKE_UP);
+		return calculateTicksToTimeOfDay(currentTimeOfDay, isNightTime(currentTimeOfDay) ? DAWN_WAKE_UP_TIME : DUSK_WAKE_UP_TIME);
 	}
 
 	public static int calculateSecondsUntilAwake(int currentTimeOfDay, double timeStepPerTick, double tps) {
@@ -30,6 +32,6 @@ public static double getRandomNumberInRange(double min, double max) {
 	}
 
 	public static boolean isNightTime(long currentTimeOfDay) {
-		return currentTimeOfDay % DAY_LENGTH >= SUNSET_WAKE_UP;
+		return currentTimeOfDay > DUSK_WAKE_UP_TIME && currentTimeOfDay < DAWN_WAKE_UP_TIME;
 	}
 }