From 32f96683ee1340f396839ec3f6461ed20933c0fd Mon Sep 17 00:00:00 2001 From: DeidaraMC <117625071+DeidaraMC@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:44:51 -0400 Subject: [PATCH] feat: add weather to instances (#2032) * feat: instance weather system * chore: weather documentation * chore: remove unused weather fields * feat: linear weather interpolation * chore: register weather command --------- Co-authored-by: DeidaraMC Co-authored-by: mworzala --- .../src/main/java/net/minestom/demo/Main.java | 1 + .../java/net/minestom/demo/PlayerInit.java | 2 +- .../demo/commands/WeatherCommand.java | 29 +++++++ .../net/minestom/server/entity/Entity.java | 1 + .../minestom/server/instance/Instance.java | 51 +++++++++++++ .../net/minestom/server/instance/Weather.java | 75 +++++++++++++++++++ .../minestom/server/instance/WeatherTest.java | 54 +++++++++++++ 7 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 demo/src/main/java/net/minestom/demo/commands/WeatherCommand.java create mode 100644 src/main/java/net/minestom/server/instance/Weather.java create mode 100644 src/test/java/net/minestom/server/instance/WeatherTest.java diff --git a/demo/src/main/java/net/minestom/demo/Main.java b/demo/src/main/java/net/minestom/demo/Main.java index 4b48c7249..f8ddbe0b6 100644 --- a/demo/src/main/java/net/minestom/demo/Main.java +++ b/demo/src/main/java/net/minestom/demo/Main.java @@ -75,6 +75,7 @@ public class Main { commandManager.register(new SetEntityType()); commandManager.register(new RelightCommand()); commandManager.register(new KillCommand()); + commandManager.register(new WeatherCommand()); commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED))); diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 742c0f4e7..7a97a0cc3 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -169,7 +169,7 @@ public class PlayerInit { }); instanceContainer.setChunkSupplier(LightingChunk::new); instanceContainer.setTimeRate(0); - instanceContainer.setTime(18000); + instanceContainer.setTime(6000); // var i2 = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null, NamespaceID.from("minestom:demo")); // instanceManager.registerInstance(i2); diff --git a/demo/src/main/java/net/minestom/demo/commands/WeatherCommand.java b/demo/src/main/java/net/minestom/demo/commands/WeatherCommand.java new file mode 100644 index 000000000..36ace5437 --- /dev/null +++ b/demo/src/main/java/net/minestom/demo/commands/WeatherCommand.java @@ -0,0 +1,29 @@ +package net.minestom.demo.commands; + +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.CommandContext; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Weather; + +public class WeatherCommand extends Command { + public WeatherCommand() { + super("weather"); + + var isRaining = ArgumentType.Boolean("isRaining").setDefaultValue(false); + var rainLevel = ArgumentType.Float("rainLevel").setDefaultValue(0.0f); + var thunderLevel = ArgumentType.Float("thunderLevel").setDefaultValue(0.0f); + var transitionTicks = ArgumentType.Integer("transition").setDefaultValue(0); + addSyntax(this::handleWeather, isRaining, rainLevel, thunderLevel, transitionTicks); + } + + private void handleWeather(CommandSender source, CommandContext context) { + Player player = (Player) source; + boolean isRaining = context.get("isRaining"); + float rainLevel = context.get("rainLevel"); + float thunderLevel = context.get("thunderLevel"); + int transitionTicks = context.get("transition"); + player.getInstance().setWeather(new Weather(isRaining, rainLevel, thunderLevel), transitionTicks); + } +} diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index a2e1eecd8..ca4de999a 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -891,6 +891,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev if (this instanceof Player player) { instance.getWorldBorder().init(player); player.sendPacket(instance.createTimePacket()); + player.sendPackets(instance.getWeather().createWeatherPackets()); } instance.getEntityTracker().register(this, spawnPosition, trackingTarget, trackingUpdate); spawn(); diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index 182fd8717..fa34b1b49 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -83,6 +83,11 @@ public abstract class Instance implements Block.Getter, Block.Setter, private Duration timeUpdate = Duration.of(1, TimeUnit.SECOND); private long lastTimeUpdate; + // Weather of the instance + private Weather targetWeather = new Weather(false, 0, 0); + private Weather currentWeather = new Weather(false, 0, 0); + private int remainingWeatherTransitionTicks; + // Field for tick events private long lastTickAge = System.currentTimeMillis(); @@ -668,6 +673,13 @@ public abstract class Instance implements Block.Getter, Block.Setter, } } + // Weather + if (remainingWeatherTransitionTicks > 0) { + Weather previousWeather = currentWeather; + currentWeather = transitionWeather(remainingWeatherTransitionTicks); + sendWeatherPackets(previousWeather); + remainingWeatherTransitionTicks--; + } // Tick event { // Process tick events @@ -678,6 +690,45 @@ public abstract class Instance implements Block.Getter, Block.Setter, this.worldBorder.update(); } + /** + * Gets the current weather on this instance + * + * @return the current weather + */ + public @NotNull Weather getWeather() { + return currentWeather; + } + + /** + * Sets the weather on this instance, transitions over time + * + * @param weather the new weather + * @param transitionTicks the ticks to transition to new weather + */ + public void setWeather(@NotNull Weather weather, int transitionTicks) { + Check.stateCondition(transitionTicks < 1, "Transition ticks cannot be lower than 1"); + targetWeather = weather; + remainingWeatherTransitionTicks = transitionTicks; + } + + private void sendWeatherPackets(@NotNull Weather previousWeather) { + if (currentWeather.isRaining() != previousWeather.isRaining()) sendGroupedPacket(currentWeather.createIsRainingPacket()); + if (currentWeather.rainLevel() != previousWeather.rainLevel()) sendGroupedPacket(currentWeather.createRainLevelPacket()); + if (currentWeather.thunderLevel() != previousWeather.thunderLevel()) sendGroupedPacket(currentWeather.createThunderLevelPacket()); + } + + private @NotNull Weather transitionWeather(int remainingTicks) { + Weather target = targetWeather; + Weather current = currentWeather; + if (remainingTicks <= 1) { + return new Weather(target.isRaining(), target.isRaining() ? target.rainLevel() : 0, + target.isRaining() ? target.thunderLevel() : 0); + } + float rainLevel = current.rainLevel() + (target.rainLevel() - current.rainLevel()) * (1 / (float)remainingTicks); + float thunderLevel = current.thunderLevel() + (target.thunderLevel() - current.thunderLevel()) * (1 / (float)remainingTicks); + return new Weather(rainLevel > 0, rainLevel, thunderLevel); + } + @Override public @NotNull TagHandler tagHandler() { return tagHandler; diff --git a/src/main/java/net/minestom/server/instance/Weather.java b/src/main/java/net/minestom/server/instance/Weather.java new file mode 100644 index 000000000..0056347a4 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/Weather.java @@ -0,0 +1,75 @@ +package net.minestom.server.instance; + +import it.unimi.dsi.fastutil.floats.FloatUnaryOperator; +import net.minestom.server.network.packet.server.SendablePacket; +import net.minestom.server.network.packet.server.play.ChangeGameStatePacket; +import net.minestom.server.utils.MathUtils; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +/** + * Represents the possible weather properties of an instance + * + * @param isRaining true if the instance is raining, otherwise false + * @param rainLevel a percentage between 0 and 1 + * used to change how heavy the rain is + * higher values darken the sky and increase rain opacity + * @param thunderLevel a percentage between 0 and 1 + * used to change how heavy the thunder is + * higher values further darken the sky + */ +public record Weather(boolean isRaining, float rainLevel, float thunderLevel) { + /** + * @throws IllegalArgumentException if {@code rainLevel} is not between 0 and 1 + * @throws IllegalArgumentException if {@code thunderLevel} is not between 0 and 1 + */ + public Weather { + Check.argCondition(!MathUtils.isBetween(rainLevel, 0, 1), "Rain level should be between 0 and 1"); + Check.argCondition(!MathUtils.isBetween(thunderLevel, 0, 1), "Thunder level should be between 0 and 1"); + } + + @Contract(pure = true) + public @NotNull Weather withRain(boolean isRaining) { + return new Weather(isRaining, rainLevel, thunderLevel); + } + + @Contract(pure = true) + public @NotNull Weather withRainLevel(float rainLevel) { + return new Weather(isRaining, rainLevel, thunderLevel); + } + + @Contract(pure = true) + public @NotNull Weather withRainLevel(@NotNull FloatUnaryOperator operator) { + return withRainLevel(operator.apply(rainLevel)); + } + + @Contract(pure = true) + public @NotNull Weather withThunderLevel(float thunderLevel) { + return new Weather(isRaining, rainLevel, thunderLevel); + } + + @Contract(pure = true) + public @NotNull Weather withThunderLevel(@NotNull FloatUnaryOperator operator) { + return withRainLevel(operator.apply(thunderLevel)); + } + + public ChangeGameStatePacket createIsRainingPacket() { + return new ChangeGameStatePacket(isRaining ? ChangeGameStatePacket.Reason.BEGIN_RAINING : ChangeGameStatePacket.Reason.END_RAINING, 0); + } + + public ChangeGameStatePacket createRainLevelPacket() { + return new ChangeGameStatePacket(ChangeGameStatePacket.Reason.RAIN_LEVEL_CHANGE, rainLevel); + } + + public ChangeGameStatePacket createThunderLevelPacket() { + return new ChangeGameStatePacket(ChangeGameStatePacket.Reason.THUNDER_LEVEL_CHANGE, thunderLevel); + } + + public @NotNull Collection createWeatherPackets() { + return List.of(createIsRainingPacket(), createRainLevelPacket(), createThunderLevelPacket()); + } +} diff --git a/src/test/java/net/minestom/server/instance/WeatherTest.java b/src/test/java/net/minestom/server/instance/WeatherTest.java new file mode 100644 index 000000000..17a1c6239 --- /dev/null +++ b/src/test/java/net/minestom/server/instance/WeatherTest.java @@ -0,0 +1,54 @@ +package net.minestom.server.instance; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.network.packet.server.play.ChangeGameStatePacket; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@EnvTest +public class WeatherTest { + @Test + public void weatherTest(Env env) { + var instance = env.createFlatInstance(); + + // Defaults + Weather weather = instance.getWeather(); + assertFalse(weather.isRaining()); + assertEquals(0, weather.rainLevel()); + assertEquals(0, weather.thunderLevel()); + + instance.setWeather(new Weather(true, 1, 0.5f), 1); + instance.tick(0); + + // Weather sent on instance join + var connection = env.createConnection(); + var tracker = connection.trackIncoming(ChangeGameStatePacket.class); + connection.connect(instance, new Pos(0, 0, 0)).join(); + tracker.assertCount(4); + List packets = tracker.collect(); + var state = packets.get(0); + assertEquals(ChangeGameStatePacket.Reason.BEGIN_RAINING, state.reason()); + + state = packets.get(1); + assertEquals(ChangeGameStatePacket.Reason.RAIN_LEVEL_CHANGE, state.reason()); + assertEquals(1, state.value()); + + state = packets.get(2); + assertEquals(ChangeGameStatePacket.Reason.THUNDER_LEVEL_CHANGE, state.reason()); + assertEquals(0.5f, state.value()); + + // Weather change while inside instance + var tracker2 = connection.trackIncoming(ChangeGameStatePacket.class); + instance.setWeather(new Weather(false, 0, 0), 2); + instance.tick(0); + state = tracker2.collect().get(0); + assertEquals(ChangeGameStatePacket.Reason.RAIN_LEVEL_CHANGE, state.reason()); + assertEquals(0.5f, state.value()); + } +}