diff --git a/src/main/java/net/minestom/server/entity/Metadata.java b/src/main/java/net/minestom/server/entity/Metadata.java index f71041a7a..cead98da3 100644 --- a/src/main/java/net/minestom/server/entity/Metadata.java +++ b/src/main/java/net/minestom/server/entity/Metadata.java @@ -8,6 +8,7 @@ import net.minestom.server.entity.metadata.animal.tameable.CatMeta; import net.minestom.server.item.ItemStack; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.server.play.EntityMetaDataPacket; +import net.minestom.server.particle.Particle; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -126,6 +127,10 @@ public final class Metadata { return new MetadataImpl.EntryImpl<>(TYPE_QUATERNION, value, NetworkBuffer.QUATERNION); } + public static Entry Particle(@NotNull Particle particle) { + return new MetadataImpl.EntryImpl<>(TYPE_PARTICLE, particle, NetworkBuffer.PARTICLE); + } + public static final byte TYPE_BYTE = 0; public static final byte TYPE_VARINT = 1; public static final byte TYPE_LONG = 2; diff --git a/src/main/java/net/minestom/server/entity/metadata/other/AreaEffectCloudMeta.java b/src/main/java/net/minestom/server/entity/metadata/other/AreaEffectCloudMeta.java index 3ad21ae9f..4b4381f20 100644 --- a/src/main/java/net/minestom/server/entity/metadata/other/AreaEffectCloudMeta.java +++ b/src/main/java/net/minestom/server/entity/metadata/other/AreaEffectCloudMeta.java @@ -3,6 +3,7 @@ package net.minestom.server.entity.metadata.other; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Metadata; import net.minestom.server.entity.metadata.EntityMeta; +import net.minestom.server.particle.Particle; import org.jetbrains.annotations.NotNull; public class AreaEffectCloudMeta extends EntityMeta { @@ -37,12 +38,12 @@ public class AreaEffectCloudMeta extends EntityMeta { super.metadata.setIndex(OFFSET + 2, Metadata.Boolean(value)); } -// public ParticleWrapper getParticle() { -// return super.metadata.getIndex((byte) 10, new ParticleWrapper(Particle.EFFECT, null)); -// } -// -// public void setParticle(ParticleWrapper value) { -// super.metadata.setIndex((byte) 11, Metadata.Particle(value)); -// } + public @NotNull Particle getParticle() { + return super.metadata.getIndex(OFFSET + 3, Particle.DUST); + } + + public void setParticle(@NotNull Particle value) { + super.metadata.setIndex(OFFSET + 3, Metadata.Particle(value)); + } } diff --git a/src/main/java/net/minestom/server/network/NetworkBuffer.java b/src/main/java/net/minestom/server/network/NetworkBuffer.java index d41c191b7..c470bccb9 100644 --- a/src/main/java/net/minestom/server/network/NetworkBuffer.java +++ b/src/main/java/net/minestom/server/network/NetworkBuffer.java @@ -1,5 +1,13 @@ package net.minestom.server.network; +import java.util.BitSet; +import java.util.Collection; + +import java.util.EnumSet; +import java.util.List; + +import java.util.UUID; + import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; @@ -8,6 +16,7 @@ import net.minestom.server.entity.metadata.animal.SnifferMeta; import net.minestom.server.entity.metadata.animal.tameable.CatMeta; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.server.play.data.DeathLocation; +import net.minestom.server.particle.Particle; import net.minestom.server.utils.Direction; import net.minestom.server.utils.Either; import net.minestom.server.utils.validate.Check; @@ -20,7 +29,6 @@ import org.jglrxavpok.hephaistos.nbt.NBTWriter; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -69,6 +77,7 @@ public final class NetworkBuffer { public static final Type VECTOR3 = NetworkBufferTypes.VECTOR3; public static final Type VECTOR3D = NetworkBufferTypes.VECTOR3D; public static final Type QUATERNION = NetworkBufferTypes.QUATERNION; + public static final Type PARTICLE = NetworkBufferTypes.PARTICLE; ByteBuffer nioBuffer; final boolean resizable; diff --git a/src/main/java/net/minestom/server/network/NetworkBufferTypes.java b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java index cf65858de..bbaaeadaa 100644 --- a/src/main/java/net/minestom/server/network/NetworkBufferTypes.java +++ b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java @@ -12,6 +12,8 @@ import net.minestom.server.entity.metadata.animal.tameable.CatMeta; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.network.packet.server.play.data.DeathLocation; +import net.minestom.server.particle.Particle; +import net.minestom.server.particle.data.ParticleData; import net.minestom.server.utils.Direction; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; @@ -626,6 +628,17 @@ final class NetworkBufferTypes { final float w = buffer.read(FLOAT); return new float[]{x, y, z, w}; }); + static final TypeImpl PARTICLE = new TypeImpl<>(Particle.class, + (buffer, value) -> { + Check.stateCondition(value.data() != null && !value.data().validate(value.id()), "Particle data {0} is not valid for this particle type {1}", value.data(), value.namespace()); + Check.stateCondition(value.data() == null && ParticleData.requiresData(value.id()), "Particle data is required for this particle type {0}", value.namespace()); + + buffer.write(VAR_INT, value.id()); + + if (value.data() != null) value.data().write(buffer); + return -1; + }, + buffer -> null); record TypeImpl(@NotNull Class type, @NotNull TypeWriter writer, diff --git a/src/main/java/net/minestom/server/network/packet/server/play/ParticlePacket.java b/src/main/java/net/minestom/server/network/packet/server/play/ParticlePacket.java index c810c4130..f61706ce5 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/ParticlePacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/ParticlePacket.java @@ -4,24 +4,53 @@ import net.minestom.server.network.ConnectionState; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; +import net.minestom.server.particle.Particle; +import net.minestom.server.particle.data.ParticleData; import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static net.minestom.server.network.NetworkBuffer.*; -public record ParticlePacket(int particleId, boolean longDistance, - double x, double y, double z, - float offsetX, float offsetY, float offsetZ, - float particleData, int particleCount, byte[] data) implements ServerPacket { +public record ParticlePacket(int particleId, boolean longDistance, double x, double y, double z, float offsetX, float offsetY, float offsetZ, float maxSpeed, int particleCount, @Nullable ParticleData data) implements ServerPacket { + private ParticlePacket(ParticlePacket copy) { + this(copy.particleId, copy.longDistance, copy.x, copy.y, copy.z, copy.offsetX, copy.offsetY, copy.offsetZ, copy.maxSpeed, copy.particleCount, copy.data); + } + public ParticlePacket(@NotNull NetworkBuffer reader) { - this(reader.read(VAR_INT), reader.read(BOOLEAN), - reader.read(DOUBLE), reader.read(DOUBLE), reader.read(DOUBLE), - reader.read(FLOAT), reader.read(FLOAT), reader.read(FLOAT), - reader.read(FLOAT), reader.read(INT), reader.read(RAW_BYTES)); + this(readPacket(reader)); + } + + public ParticlePacket(@NotNull Particle particle, boolean longDistance, double x, double y, double z, int offsetX, int offsetY, int offsetZ, int maxSpeed, int particleCount) { + this(particle.id(), longDistance, x, y, z, offsetX, offsetY, offsetZ, maxSpeed, particleCount, particle.data()); + } + + public ParticlePacket(@NotNull Particle particle, double x, double y, double z, int offsetX, int offsetY, int offsetZ, int maxSpeed, int particleCount) { + this(particle.id(), false, x, y, z, offsetX, offsetY, offsetZ, maxSpeed, particleCount, particle.data()); + } + + private static ParticlePacket readPacket(NetworkBuffer reader) { + int particleId = reader.read(VAR_INT); + Boolean longDistance = reader.read(BOOLEAN); + Double x = reader.read(DOUBLE); + Double y = reader.read(DOUBLE); + Double z = reader.read(DOUBLE); + Float offsetX = reader.read(FLOAT); + Float offsetY = reader.read(FLOAT); + Float offsetZ = reader.read(FLOAT); + Float maxSpeed = reader.read(FLOAT); + Integer particleCount = reader.read(INT); + ParticleData data = ParticleData.read(particleId, reader); + + return new ParticlePacket(particleId, longDistance, x, y, z, offsetX, offsetY, offsetZ, maxSpeed, particleCount, data); } @Override public void write(@NotNull NetworkBuffer writer) { + Check.stateCondition(data != null && !data.validate(particleId), "Particle data {0} is not valid for this particle type {1}", data, Particle.fromId(particleId)); + Check.stateCondition(data == null && ParticleData.requiresData(particleId), "Particle data is required for this particle type {0}", Particle.fromId(particleId)); + writer.write(VAR_INT, particleId); writer.write(BOOLEAN, longDistance); writer.write(DOUBLE, x); @@ -30,10 +59,10 @@ public record ParticlePacket(int particleId, boolean longDistance, writer.write(FLOAT, offsetX); writer.write(FLOAT, offsetY); writer.write(FLOAT, offsetZ); - writer.write(FLOAT, particleData); + writer.write(FLOAT, maxSpeed); writer.write(INT, particleCount); - writer.write(RAW_BYTES, data); + if (data != null) data.write(writer); } @Override diff --git a/src/main/java/net/minestom/server/particle/Particle.java b/src/main/java/net/minestom/server/particle/Particle.java index cf9562c92..a66130d0d 100644 --- a/src/main/java/net/minestom/server/particle/Particle.java +++ b/src/main/java/net/minestom/server/particle/Particle.java @@ -1,5 +1,6 @@ package net.minestom.server.particle; +import net.minestom.server.particle.data.ParticleData; import net.minestom.server.registry.StaticProtocolObject; import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; @@ -24,4 +25,7 @@ public sealed interface Particle extends StaticProtocolObject, Particles permits static @Nullable Particle fromId(int id) { return ParticleImpl.getId(id); } + + @NotNull Particle withData(@Nullable ParticleData data); + @Nullable ParticleData data(); } diff --git a/src/main/java/net/minestom/server/particle/ParticleCreator.java b/src/main/java/net/minestom/server/particle/ParticleCreator.java deleted file mode 100644 index f005c4e93..000000000 --- a/src/main/java/net/minestom/server/particle/ParticleCreator.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.minestom.server.particle; - -import net.minestom.server.network.packet.server.play.ParticlePacket; -import net.minestom.server.utils.binary.BinaryWriter; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -/** - * Small utils class to create particle packet - */ -public class ParticleCreator { - - public static ParticlePacket createParticlePacket(Particle particleType, boolean distance, - double x, double y, double z, - float offsetX, float offsetY, float offsetZ, - float particleData, int count, @Nullable Consumer dataWriter) { - byte[] data; - if (dataWriter != null) { - BinaryWriter writer = new BinaryWriter(); - dataWriter.accept(writer); - data = writer.toByteArray(); - } else { - data = new byte[0]; - } - return new ParticlePacket(particleType.id(), distance, x, y, z, offsetX, offsetY, offsetZ, particleData, count, data); - } - - public static ParticlePacket createParticlePacket(Particle particleType, - double x, double y, double z, - float offsetX, float offsetY, float offsetZ, - int count) { - return createParticlePacket(particleType, true, x, y, z, - offsetX, offsetY, offsetZ, 0, count, null); - } -} diff --git a/src/main/java/net/minestom/server/particle/ParticleImpl.java b/src/main/java/net/minestom/server/particle/ParticleImpl.java index 82348e115..d70ea41bd 100644 --- a/src/main/java/net/minestom/server/particle/ParticleImpl.java +++ b/src/main/java/net/minestom/server/particle/ParticleImpl.java @@ -1,14 +1,16 @@ package net.minestom.server.particle; +import net.minestom.server.particle.data.ParticleData; import net.minestom.server.registry.Registry; import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collection; -record ParticleImpl(NamespaceID namespace, int id) implements Particle { +record ParticleImpl(NamespaceID namespace, int id, ParticleData data) implements Particle { private static final Registry.Container CONTAINER = Registry.createStaticContainer(Registry.Resource.PARTICLES, - (namespace, properties) -> new ParticleImpl(NamespaceID.from(namespace), properties.getInt("id"))); + (namespace, properties) -> new ParticleImpl(NamespaceID.from(namespace), properties.getInt("id"), ParticleData.defaultData(namespace))); static Particle get(@NotNull String namespace) { return CONTAINER.get(namespace); @@ -26,8 +28,17 @@ record ParticleImpl(NamespaceID namespace, int id) implements Particle { return CONTAINER.values(); } + public @NotNull Particle withData(@Nullable ParticleData object) { + return new ParticleImpl(namespace, id, object); + } + @Override - public String toString() { + public @Nullable ParticleData data() { + return data; + } + + @Override + public @NotNull String toString() { return name(); } } diff --git a/src/main/java/net/minestom/server/particle/data/BlockMarkerParticleData.java b/src/main/java/net/minestom/server/particle/data/BlockMarkerParticleData.java new file mode 100644 index 000000000..e64bb0bd7 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/BlockMarkerParticleData.java @@ -0,0 +1,34 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +public record BlockMarkerParticleData(@NotNull Block block) implements ParticleData { + BlockMarkerParticleData(NetworkBuffer reader) { + this(read(reader)); + } + + BlockMarkerParticleData() { + this(Block.STONE); + } + + private static Block read(NetworkBuffer reader) { + short blockState = reader.read(NetworkBuffer.VAR_INT).shortValue(); + Block block = Block.fromStateId(blockState); + Check.stateCondition(block == null, "Block state " + blockState + " is invalid"); + return block; + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.VAR_INT, (int) block.stateId()); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.BLOCK_MARKER.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/BlockParticleData.java b/src/main/java/net/minestom/server/particle/data/BlockParticleData.java new file mode 100644 index 000000000..83a414948 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/BlockParticleData.java @@ -0,0 +1,34 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +public record BlockParticleData(Block block) implements ParticleData { + BlockParticleData(NetworkBuffer reader) { + this(read(reader)); + } + + BlockParticleData() { + this(Block.STONE); + } + + private static Block read(NetworkBuffer reader) { + short blockState = reader.read(NetworkBuffer.VAR_INT).shortValue(); + Block block = Block.fromStateId(blockState); + Check.stateCondition(block == null, "Block state " + blockState + " is invalid"); + return block; + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.VAR_INT, (int) block.stateId()); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.BLOCK.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/DustColorTransitionParticleData.java b/src/main/java/net/minestom/server/particle/data/DustColorTransitionParticleData.java new file mode 100644 index 000000000..75a8eee45 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/DustColorTransitionParticleData.java @@ -0,0 +1,46 @@ +package net.minestom.server.particle.data; + +import net.kyori.adventure.util.RGBLike; +import net.minestom.server.color.Color; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +public record DustColorTransitionParticleData(@NotNull RGBLike from, float scale, @NotNull RGBLike to) implements ParticleData { + public DustColorTransitionParticleData { + Check.argCondition(scale < 0.01 || scale > 4, "scale must be between 0.01 and 4: was {0}", scale); + } + + DustColorTransitionParticleData() { + this(new Color(255, 255, 255), 1, new Color(255, 255, 255)); + } + + DustColorTransitionParticleData(NetworkBuffer buffer) { + this(new Color( + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255) + ), buffer.read(NetworkBuffer.FLOAT), new Color( + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255) + )); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.FLOAT, from.red() / 255f); + writer.write(NetworkBuffer.FLOAT, from.green() / 255f); + writer.write(NetworkBuffer.FLOAT, from.blue() / 255f); + writer.write(NetworkBuffer.FLOAT, scale); + writer.write(NetworkBuffer.FLOAT, to.red() / 255f); + writer.write(NetworkBuffer.FLOAT, to.green() / 255f); + writer.write(NetworkBuffer.FLOAT, to.blue() / 255f); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.DUST_COLOR_TRANSITION.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/DustParticleData.java b/src/main/java/net/minestom/server/particle/data/DustParticleData.java new file mode 100644 index 000000000..8fb19b701 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/DustParticleData.java @@ -0,0 +1,39 @@ +package net.minestom.server.particle.data; + +import net.kyori.adventure.util.RGBLike; +import net.minestom.server.color.Color; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +public record DustParticleData(@NotNull RGBLike color, float scale) implements ParticleData { + public DustParticleData { + Check.argCondition(scale < 0.01 || scale > 4, "scale must be between 0.01 and 4"); + } + + DustParticleData(NetworkBuffer buffer) { + this(new Color( + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255), + (int) (buffer.read(NetworkBuffer.FLOAT) * 255) + ), buffer.read(NetworkBuffer.FLOAT)); + } + + DustParticleData() { + this(new Color(255, 255, 255), 1); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.FLOAT, color.red() / 255f); + writer.write(NetworkBuffer.FLOAT, color.green() / 255f); + writer.write(NetworkBuffer.FLOAT, color.blue() / 255f); + writer.write(NetworkBuffer.FLOAT, scale); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.DUST.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/FallingDustParticleData.java b/src/main/java/net/minestom/server/particle/data/FallingDustParticleData.java new file mode 100644 index 000000000..831b1f0dc --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/FallingDustParticleData.java @@ -0,0 +1,34 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +public record FallingDustParticleData(Block block) implements ParticleData { + FallingDustParticleData(NetworkBuffer reader) { + this(read(reader)); + } + + FallingDustParticleData() { + this(Block.STONE); + } + + private static Block read(NetworkBuffer reader) { + short blockState = reader.read(NetworkBuffer.VAR_INT).shortValue(); + Block block = Block.fromStateId(blockState); + Check.stateCondition(block == null, "Block state {0} is invalid", blockState); + return block; + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.VAR_INT, (int) block.stateId()); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.FALLING_DUST.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/ItemParticleData.java b/src/main/java/net/minestom/server/particle/data/ItemParticleData.java new file mode 100644 index 000000000..58ddfb0d3 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/ItemParticleData.java @@ -0,0 +1,27 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import org.jetbrains.annotations.NotNull; + +public record ItemParticleData(ItemStack item) implements ParticleData { + ItemParticleData(NetworkBuffer reader) { + this(reader.read(NetworkBuffer.ITEM)); + } + + ItemParticleData() { + this(ItemStack.of(Material.STONE)); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.ITEM, item); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.ITEM.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/ParticleData.java b/src/main/java/net/minestom/server/particle/data/ParticleData.java new file mode 100644 index 000000000..4feb802a1 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/ParticleData.java @@ -0,0 +1,51 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import org.jetbrains.annotations.NotNull; + +public interface ParticleData { + void write(@NotNull NetworkBuffer writer); + + static ParticleData read(int particleId, NetworkBuffer reader) { + if (particleId == Particle.BLOCK.id()) return new BlockParticleData(reader); + else if (particleId == Particle.BLOCK_MARKER.id()) return new BlockMarkerParticleData(reader); + else if (particleId == Particle.DUST.id()) return new DustParticleData(reader); + else if (particleId == Particle.DUST_COLOR_TRANSITION.id()) return new DustColorTransitionParticleData(reader); + else if (particleId == Particle.FALLING_DUST.id()) return new FallingDustParticleData(reader); + else if (particleId == Particle.SCULK_CHARGE.id()) return new SculkChargeParticleData(reader); + else if (particleId == Particle.ITEM.id()) return new ItemParticleData(reader); + else if (particleId == Particle.VIBRATION.id()) return new VibrationParticleData(reader); + else if (particleId == Particle.SHRIEK.id()) return new ShriekParticleData(reader); + else return null; + } + + boolean validate(int particleId); + + static boolean requiresData(int particleId) { + return particleId == Particle.BLOCK.id() + || particleId == Particle.BLOCK_MARKER.id() + || particleId == Particle.DUST.id() + || particleId == Particle.DUST_COLOR_TRANSITION.id() + || particleId == Particle.FALLING_DUST.id() + || particleId == Particle.SCULK_CHARGE.id() + || particleId == Particle.ITEM.id() + || particleId == Particle.VIBRATION.id() + || particleId == Particle.SHRIEK.id(); + } + + static ParticleData defaultData(String id) { + return switch (id) { + case "minecraft:block" -> new BlockParticleData(); + case "minecraft:block_marker" -> new BlockMarkerParticleData(); + case "minecraft:dust" -> new DustParticleData(); + case "minecraft:dust_color_transition" -> new DustColorTransitionParticleData(); + case "minecraft:falling_dust" -> new FallingDustParticleData(); + case "minecraft:sculk_charge" -> new SculkChargeParticleData(); + case "minecraft:item" -> new ItemParticleData(); + case "minecraft:vibration" -> new VibrationParticleData(); + case "minecraft:shriek" -> new ShriekParticleData(); + default -> null; + }; + } +} diff --git a/src/main/java/net/minestom/server/particle/data/SculkChargeParticleData.java b/src/main/java/net/minestom/server/particle/data/SculkChargeParticleData.java new file mode 100644 index 000000000..706a24862 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/SculkChargeParticleData.java @@ -0,0 +1,25 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import org.jetbrains.annotations.NotNull; + +public record SculkChargeParticleData(float roll) implements ParticleData { + SculkChargeParticleData(NetworkBuffer reader) { + this(reader.read(NetworkBuffer.FLOAT)); + } + + SculkChargeParticleData() { + this(0); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.FLOAT, roll); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.SCULK_CHARGE.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/ShriekParticleData.java b/src/main/java/net/minestom/server/particle/data/ShriekParticleData.java new file mode 100644 index 000000000..518e7acfe --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/ShriekParticleData.java @@ -0,0 +1,25 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import org.jetbrains.annotations.NotNull; + +public record ShriekParticleData(int delay) implements ParticleData { + ShriekParticleData(NetworkBuffer reader) { + this(reader.read(NetworkBuffer.VAR_INT)); + } + + ShriekParticleData() { + this(0); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.write(NetworkBuffer.VAR_INT, delay); + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.SHRIEK.id(); + } +} diff --git a/src/main/java/net/minestom/server/particle/data/VibrationParticleData.java b/src/main/java/net/minestom/server/particle/data/VibrationParticleData.java new file mode 100644 index 000000000..6a7b5f324 --- /dev/null +++ b/src/main/java/net/minestom/server/particle/data/VibrationParticleData.java @@ -0,0 +1,55 @@ +package net.minestom.server.particle.data; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import org.jetbrains.annotations.NotNull; + +public record VibrationParticleData(@NotNull VibrationSource type, @NotNull Point source, int entityId, float entityEyeHeight, int ticks) implements ParticleData { + public enum VibrationSource { + BLOCK, + ENTITY + } + + VibrationParticleData(NetworkBuffer buffer) { + this(read(buffer)); + } + + VibrationParticleData() { + this(VibrationSource.BLOCK, Vec.ZERO, 0, 0, 0); + } + + private VibrationParticleData(VibrationParticleData copy) { + this(copy.type, copy.source, copy.entityId, copy.entityEyeHeight, copy.ticks); + } + + private static VibrationParticleData read(NetworkBuffer buffer) { + VibrationSource type = buffer.readEnum(VibrationSource.class); + + if (type == VibrationSource.BLOCK) { + return new VibrationParticleData(type, buffer.read(NetworkBuffer.BLOCK_POSITION), 0, 0, buffer.read(NetworkBuffer.VAR_INT)); + } else { + return new VibrationParticleData(type, Vec.ZERO, buffer.read(NetworkBuffer.VAR_INT), buffer.read(NetworkBuffer.FLOAT), buffer.read(NetworkBuffer.VAR_INT)); + } + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + writer.writeEnum(VibrationSource.class, type); + + if (type == VibrationSource.BLOCK) { + writer.write(NetworkBuffer.BLOCK_POSITION, source); + writer.write(NetworkBuffer.VAR_INT, ticks); + } else { + writer.write(NetworkBuffer.VAR_INT, entityId); + writer.write(NetworkBuffer.FLOAT, entityEyeHeight); + writer.write(NetworkBuffer.VAR_INT, ticks); + } + } + + @Override + public boolean validate(int particleId) { + return particleId == Particle.VIBRATION.id(); + } +} diff --git a/src/test/java/net/minestom/server/entity/AreaEffectCloudTest.java b/src/test/java/net/minestom/server/entity/AreaEffectCloudTest.java new file mode 100644 index 000000000..c2280022b --- /dev/null +++ b/src/test/java/net/minestom/server/entity/AreaEffectCloudTest.java @@ -0,0 +1,164 @@ +package net.minestom.server.entity; + +import net.minestom.server.color.Color; +import net.minestom.server.entity.metadata.other.AreaEffectCloudMeta; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.particle.Particle; +import net.minestom.server.particle.data.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class AreaEffectCloudTest { + @Test + public void createWithDustParticle() { + int colour = 0x5505FF01; + + int b = (colour & 0x000000FF); + int g = (colour & 0x0000FF00) >> 8; + int r = (colour & 0x00FF0000) >> 16; + + float size = 0.1f; + + Particle particle = Particle.DUST.withData(new DustParticleData(new Color(r, g, b), size)); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + DustParticleData gotData = (DustParticleData) gotParticle.data(); + assertNotNull(gotData); + assert gotData.color().red() == r; + assert gotData.color().green() == g; + assert gotData.color().blue() == b; + assert gotData.scale() == size; + } + + @Test + public void createWithDustTransition() { + int colour = 0xFF05FF01; + int colourAfter = 0xFF05FF01; + + int b = (colour & 0x000000FF); + int g = (colour & 0x0000FF00) >> 8; + int r = (colour & 0x00FF0000) >> 16; + + int b2 = (colourAfter & 0x000000FF); + int g2 = (colourAfter & 0x0000FF00) >> 8; + int r2 = (colourAfter & 0x00FF0000) >> 16; + + float size = 0.1f; + + Particle particle = Particle.DUST_COLOR_TRANSITION.withData(new DustColorTransitionParticleData(new Color(r, g, b), size, new Color(r2, g2, b2))); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + DustColorTransitionParticleData gotData = (DustColorTransitionParticleData) gotParticle.data(); + assertNotNull(gotData); + assert gotData.from().red() == r; + assert gotData.from().green() == g; + assert gotData.from().blue() == b; + assert gotData.scale() == size; + assert gotData.to().red() == r2; + assert gotData.to().green() == g2; + assert gotData.to().blue() == b2; + } + + @Test + public void createWithBlockParticle() { + Block block = Block.GRASS_BLOCK; + Particle particle = Particle.BLOCK.withData(new BlockParticleData(block)); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + BlockParticleData gotBlock = (BlockParticleData) gotParticle.data(); + assert gotBlock.block() == block; + } + + @Test + public void createWithBlockMarkerParticle() { + Block block = Block.GRASS_BLOCK; + Particle particle = Particle.BLOCK_MARKER.withData(new BlockMarkerParticleData(block)); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + BlockMarkerParticleData gotBlock = (BlockMarkerParticleData) gotParticle.data(); + assert gotBlock.block() == block; + } + + @Test + public void createWithItemParticle() { + Particle particle = Particle.ITEM.withData(new ItemParticleData(ItemStack.of(Material.ACACIA_LOG))); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + ItemParticleData gotBlock = (ItemParticleData) gotParticle.data(); + assert gotBlock.item().material() == Material.ACACIA_LOG; + } + + @Test + public void createWithSculkChargeParticle() { + Particle particle = Particle.SCULK_CHARGE.withData(new SculkChargeParticleData(3)); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + SculkChargeParticleData gotBlock = (SculkChargeParticleData) gotParticle.data(); + assert gotBlock.roll() == 3; + } + + @Test + public void createWithDustParticleIncorrectType() { + Particle particle = Particle.DUST.withData(new FallingDustParticleData(Block.GLOWSTONE)); + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + assertThrows(IllegalStateException.class, () -> entity.getMetadataPacket().write(new NetworkBuffer())); + } + + @Test + public void createWithComposterParticle() { + Particle particle = Particle.COMPOSTER; + + Entity entity = new Entity(EntityTypes.AREA_EFFECT_CLOUD); + AreaEffectCloudMeta meta = (AreaEffectCloudMeta) entity.getEntityMeta(); + meta.setParticle(particle); + + var gotParticle = meta.getParticle(); + assert gotParticle == particle; + + ParticleData gotBlock = gotParticle.data(); + assertNull(gotBlock); + } +} diff --git a/src/test/java/net/minestom/server/particle/ParticleDataTest.java b/src/test/java/net/minestom/server/particle/ParticleDataTest.java new file mode 100644 index 000000000..6eea521a9 --- /dev/null +++ b/src/test/java/net/minestom/server/particle/ParticleDataTest.java @@ -0,0 +1,62 @@ +package net.minestom.server.particle; + +import net.minestom.server.color.Color; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.data.BlockParticleData; +import net.minestom.server.particle.data.DustParticleData; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ParticleDataTest { + + @Test + public void testDustParticleDefault() { + Particle particle = Particle.DUST; + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertDoesNotThrow(() -> packet.write(new NetworkBuffer())); + } + + @Test + public void testDustParticleInvalid() { + var particle = Particle.DUST.withData(null); + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertThrows(IllegalStateException.class, () -> packet.write(new NetworkBuffer())); + } + + @Test + public void testDustParticleWrongData() { + var particle = Particle.DUST.withData(new BlockParticleData(Block.STONE)); + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertThrows(IllegalStateException.class, () -> packet.write(new NetworkBuffer())); + } + + @Test + public void testDustParticleWrongParameters() { + assertThrows(IllegalArgumentException.class, () -> Particle.DUST.withData(new DustParticleData(new Color(255, 255, 255), 0))); + } + + @Test + public void testParticleValid() { + var particle = Particle.AMBIENT_ENTITY_EFFECT; + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertDoesNotThrow(() -> packet.write(new NetworkBuffer())); + } + + @Test + public void testParticleData() { + var particle = Particle.AMBIENT_ENTITY_EFFECT; + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertDoesNotThrow(() -> packet.write(new NetworkBuffer())); + } + + @Test + public void invalidBlock() { + var particle = Particle.BLOCK.withData(new BlockParticleData(null)); + ParticlePacket packet = new ParticlePacket(particle, true, 0, 0, 0, 0, 0, 0, 0, 0); + assertThrows(NullPointerException.class, () -> packet.write(new NetworkBuffer())); + } +}