diff --git a/modules/API/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/modules/API/src/main/java/com/comphenix/protocol/events/PacketContainer.java
index 1e5debe5..b7be62e0 100644
--- a/modules/API/src/main/java/com/comphenix/protocol/events/PacketContainer.java
+++ b/modules/API/src/main/java/com/comphenix/protocol/events/PacketContainer.java
@@ -42,6 +42,8 @@ import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.StreamSerializer;
import com.comphenix.protocol.wrappers.*;
import com.comphenix.protocol.wrappers.EnumWrappers.*;
+import com.comphenix.protocol.wrappers.EnumWrappers.Difficulty;
+import com.comphenix.protocol.wrappers.EnumWrappers.SoundCategory;
import com.comphenix.protocol.wrappers.nbt.NbtBase;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
@@ -53,10 +55,8 @@ import com.google.common.collect.Sets;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator;
-import org.bukkit.Material;
-import org.bukkit.Sound;
-import org.bukkit.World;
-import org.bukkit.WorldType;
+import org.bukkit.*;
+import org.bukkit.Particle;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffectType;
@@ -831,16 +831,28 @@ public class PacketContainer implements Serializable {
}
/**
- * Retrieve a read/write structure for the Particle enum in 1.8.
+ * Retrieve a read/write structure for the Particle enum in 1.8-1.12.
+ * NOTE: This will produce undesirable results in 1.13
* @return A modifier for Particle enum fields.
*/
- public StructureModifier getParticles() {
+ public StructureModifier getParticles() {
// Convert to and from the wrapper
return structureModifier.withType(
EnumWrappers.getParticleClass(),
EnumWrappers.getParticleConverter());
}
+ /**
+ * Retrieve a read/write structure for ParticleParams in 1.13
+ * @return A modifier for ParticleParam fields.
+ */
+ public StructureModifier getNewParticles() {
+ return structureModifier.withType(
+ MinecraftReflection.getMinecraftClass("ParticleParam"),
+ BukkitConverters.getParticleConverter()
+ );
+ }
+
/**
* Retrieve a read/write structure for the MobEffectList class in 1.9.
* @return A modifier for MobEffectList fields.
diff --git a/modules/API/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/modules/API/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
index 1c939e07..5339bf31 100644
--- a/modules/API/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
+++ b/modules/API/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java
@@ -47,11 +47,12 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
-import org.bukkit.Material;
-import org.bukkit.Sound;
-import org.bukkit.World;
-import org.bukkit.WorldType;
+import net.minecraft.server.v1_13_R1.ParticleParam;
+import net.minecraft.server.v1_13_R1.ParticleParamBlock;
+
+import org.bukkit.*;
import org.bukkit.advancement.Advancement;
+import org.bukkit.craftbukkit.v1_13_R1.CraftParticle;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
@@ -929,6 +930,10 @@ public class BukkitConverters {
});
}
+ public static EquivalentConverter getParticleConverter() {
+ return ignoreNull(handle(WrappedParticle::getHandle, WrappedParticle::fromHandle));
+ }
+
public static EquivalentConverter getAdvancementConverter() {
return ignoreNull(new EquivalentConverter() {
@Override
diff --git a/modules/API/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java b/modules/API/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java
index d8a5d320..7b7114c6 100644
--- a/modules/API/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java
+++ b/modules/API/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java
@@ -12,6 +12,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.google.common.collect.Maps;
+import org.bukkit.EntityEffect;
import org.bukkit.GameMode;
/**
diff --git a/modules/API/src/main/java/com/comphenix/protocol/wrappers/WrappedParticle.java b/modules/API/src/main/java/com/comphenix/protocol/wrappers/WrappedParticle.java
new file mode 100644
index 00000000..0095365d
--- /dev/null
+++ b/modules/API/src/main/java/com/comphenix/protocol/wrappers/WrappedParticle.java
@@ -0,0 +1,151 @@
+package com.comphenix.protocol.wrappers;
+
+import java.lang.reflect.Modifier;
+
+import com.comphenix.protocol.reflect.FuzzyReflection;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.comphenix.protocol.reflect.accessors.Accessors;
+import com.comphenix.protocol.reflect.accessors.MethodAccessor;
+import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
+import com.comphenix.protocol.utility.MinecraftReflection;
+
+import org.bukkit.Color;
+import org.bukkit.Particle;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Represents an immutable wrapped ParticleParam in 1.13
+ */
+public class WrappedParticle {
+ private static MethodAccessor toBukkit;
+ private static MethodAccessor toNMS;
+ private static MethodAccessor toCraftData;
+
+ private static void ensureMethods() {
+ if (toBukkit != null && toNMS != null) {
+ return;
+ }
+
+ FuzzyReflection fuzzy = FuzzyReflection.fromClass(MinecraftReflection.getCraftBukkitClass("CraftParticle"));
+ FuzzyMethodContract contract = FuzzyMethodContract
+ .newBuilder()
+ .requireModifier(Modifier.STATIC)
+ .returnTypeExact(Particle.class)
+ .parameterExactType(MinecraftReflection.getMinecraftClass("ParticleParam"))
+ .build();
+ toBukkit = Accessors.getMethodAccessor(fuzzy.getMethod(contract));
+
+ contract = FuzzyMethodContract
+ .newBuilder()
+ .requireModifier(Modifier.STATIC)
+ .returnTypeExact(MinecraftReflection.getMinecraftClass("ParticleParam"))
+ .parameterCount(2)
+ .build();
+ toNMS = Accessors.getMethodAccessor(fuzzy.getMethod(contract));
+
+ Class> cbData = MinecraftReflection.getCraftBukkitClass("block.data.CraftBlockData");
+ fuzzy = FuzzyReflection.fromClass(cbData);
+ contract = FuzzyMethodContract
+ .newBuilder()
+ .requireModifier(Modifier.STATIC)
+ .returnTypeExact(cbData)
+ .parameterExactArray(MinecraftReflection.getIBlockDataClass())
+ .build();
+ toCraftData = Accessors.getMethodAccessor(fuzzy.getMethod(contract));
+ }
+
+ private final Particle particle;
+ private final T data;
+ private final Object handle;
+
+ private WrappedParticle(Object handle, Particle particle, T data) {
+ this.handle = handle;
+ this.particle = particle;
+ this.data = data;
+ }
+
+ /**
+ * @return This particle's Bukkit type
+ */
+ public Particle getParticle() {
+ return particle;
+ }
+
+ /**
+ * Gets this Particle's Bukkit/ProtocolLib data. The type of this data depends on the
+ * {@link #getParticle() Particle type}. For Block particles it will be {@link WrappedBlockData},
+ * for Item crack particles, it will be an {@link ItemStack}, and for redstone particles it will
+ * be {@link Particle.DustOptions}
+ *
+ * @return The particle data
+ */
+ public T getData() {
+ return data;
+ }
+
+ /**
+ * @return NMS handle
+ */
+ public Object getHandle() {
+ return handle;
+ }
+
+ public static WrappedParticle fromHandle(Object handle) {
+ Particle bukkit = (Particle) toBukkit.invoke(null, handle);
+ Object data = null;
+
+ switch (bukkit) {
+ case BLOCK_CRACK:
+ case BLOCK_DUST:
+ case FALLING_DUST:
+ data = getBlockData(handle);
+ break;
+ case ITEM_CRACK:
+ data = getItem(handle);
+ break;
+ case REDSTONE:
+ data = getRedstone(handle);
+ break;
+ default:
+ break;
+ }
+
+ return new WrappedParticle<>(handle, bukkit, data);
+ }
+
+ private static WrappedBlockData getBlockData(Object handle) {
+ return new StructureModifier<>(handle.getClass())
+ .withTarget(handle)
+ .withType(MinecraftReflection.getIBlockDataClass(), BukkitConverters.getWrappedBlockDataConverter())
+ .read(0);
+ }
+
+ private static Object getItem(Object handle) {
+ return new StructureModifier<>(handle.getClass())
+ .withTarget(handle)
+ .withType(MinecraftReflection.getItemStackClass(), BukkitConverters.getItemStackConverter())
+ .read(0);
+ }
+
+ private static Object getRedstone(Object handle) {
+ StructureModifier modifier = new StructureModifier<>(handle.getClass()).withTarget(handle).withType(float.class);
+ return new Particle.DustOptions(Color.fromRGB(
+ (int) (modifier.read(0) * 255),
+ (int) (modifier.read(1) * 255),
+ (int) (modifier.read(2) * 255)),
+ modifier.read(3));
+ }
+
+ public static WrappedParticle create(Particle particle, T data) {
+ ensureMethods();
+
+ Object bukkitData = data;
+ if (data instanceof WrappedBlockData) {
+ WrappedBlockData blockData = (WrappedBlockData) data;
+ bukkitData = toCraftData.invoke(null, blockData.getHandle());
+ }
+
+ Object handle = toNMS.invoke(null, particle, bukkitData);
+ return new WrappedParticle<>(handle, particle, data);
+ }
+}
diff --git a/modules/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedParticleTest.java b/modules/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedParticleTest.java
new file mode 100644
index 00000000..0cfa979e
--- /dev/null
+++ b/modules/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedParticleTest.java
@@ -0,0 +1,61 @@
+package com.comphenix.protocol.wrappers;
+
+import com.comphenix.protocol.BukkitInitialization;
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketContainer;
+
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.Particle;
+import org.bukkit.Particle.DustOptions;
+import org.bukkit.inventory.ItemStack;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static com.comphenix.protocol.utility.TestUtils.assertItemsEqual;
+import static org.junit.Assert.assertEquals;
+
+public class WrappedParticleTest {
+ @BeforeClass
+ public static void beforeClass() {
+ BukkitInitialization.initializeItemMeta();
+ }
+
+ @Test
+ public void testBlockData() {
+ PacketContainer packet = new PacketContainer(PacketType.Play.Server.WORLD_PARTICLES);
+ WrappedParticle before = WrappedParticle.create(Particle.BLOCK_CRACK,
+ WrappedBlockData.createData(Material.LAPIS_BLOCK));
+ packet.getNewParticles().write(0, before);
+
+ WrappedParticle after = packet.getNewParticles().read(0);
+ assertEquals(before.getParticle(), after.getParticle());
+ assertEquals(before.getData(), after.getData());
+ }
+
+ @Test
+ public void testItemStacks() {
+ PacketContainer packet = new PacketContainer(PacketType.Play.Server.WORLD_PARTICLES);
+ WrappedParticle before = WrappedParticle.create(Particle.ITEM_CRACK, new ItemStack(Material.FLINT_AND_STEEL));
+ packet.getNewParticles().write(0, before);
+
+ WrappedParticle after = packet.getNewParticles().read(0);
+ assertEquals(before.getParticle(), after.getParticle());
+ assertItemsEqual((ItemStack) before.getData(), (ItemStack) after.getData());
+ }
+
+ @Test
+ public void testRedstone() {
+ PacketContainer packet = new PacketContainer(PacketType.Play.Server.WORLD_PARTICLES);
+ WrappedParticle before = WrappedParticle.create(Particle.REDSTONE, new DustOptions(Color.BLUE, 1));
+ packet.getNewParticles().write(0, before);
+
+ WrappedParticle after = packet.getNewParticles().read(0);
+ assertEquals(before.getParticle(), after.getParticle());
+
+ DustOptions beforeDust = (DustOptions) before.getData();
+ DustOptions afterDust = (DustOptions) after.getData();
+ assertEquals(beforeDust.getColor(), afterDust.getColor());
+ assertEquals(beforeDust.getSize(), afterDust.getSize(), 0);
+ }
+}