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 <DeidaraMC>
Co-authored-by: mworzala <mattheworzala@gmail.com>
This commit is contained in:
DeidaraMC 2024-03-19 12:44:51 -04:00 committed by GitHub
parent 9cfffc6ee4
commit 32f96683ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 212 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<SendablePacket> createWeatherPackets() {
return List.of(createIsRainingPacket(), createRainLevelPacket(), createThunderLevelPacket());
}
}

View File

@ -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<ChangeGameStatePacket> 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());
}
}