package com.destroystokyo.paper; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.Collection; import java.util.List; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.util.NumberConversions; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * Helps prepare a particle to be sent to players. *

* Usage of the builder is preferred over the super long {@link World#spawnParticle(Particle, Location, int, double, double, double, double, Object)} API */ @NullMarked public class ParticleBuilder implements Cloneable { private Particle particle; private @Nullable List receivers; private @Nullable Player source; private @Nullable Location location; private int count = 1; private double offsetX = 0, offsetY = 0, offsetZ = 0; private double extra = 1; private @Nullable Object data; private boolean force = true; public ParticleBuilder(final Particle particle) { this.particle = particle; } /** * Sends the particle to all receiving players (or all). This method is safe to use * Asynchronously * * @return a reference to this object. */ public ParticleBuilder spawn() { if (this.location == null) { throw new IllegalStateException("Please specify location for this particle"); } this.location.getWorld().spawnParticle( this.particle, this.receivers, this.source, this.location.getX(), this.location.getY(), this.location.getZ(), this.count, this.offsetX, this.offsetY, this.offsetZ, this.extra, this.data, this.force ); return this; } /** * @return The particle going to be sent */ public Particle particle() { return this.particle; } /** * Changes what particle will be sent * * @param particle The particle * @return a reference to this object. */ public ParticleBuilder particle(final Particle particle) { this.particle = particle; return this; } /** * @return List of players who will receive the particle, or null for all in world */ public @Nullable List receivers() { return this.receivers; } /** * Example use: *

* builder.receivers(16); if (builder.hasReceivers()) { sendParticleAsync(builder); } * * @return If this particle is going to be sent to someone */ public boolean hasReceivers() { return (this.receivers == null && this.location != null && !this.location.getWorld().getPlayers().isEmpty()) || ( this.receivers != null && !this.receivers.isEmpty()); } /** * Sends this particle to all players in the world. This is rather silly, and you should likely not * be doing this. *

* Just be a logical person and use receivers by radius or collection. * * @return a reference to this object. */ public ParticleBuilder allPlayers() { this.receivers = null; return this; } /** * @param receivers List of players to receive this particle, or null for all players in the * world * @return a reference to this object. */ public ParticleBuilder receivers(final @Nullable List receivers) { // Had to keep this as we first made API List<> and not Collection, but removing this may break plugins compiled on older jars // TODO: deprecate? this.receivers = receivers != null ? Lists.newArrayList(receivers) : null; return this; } /** * @param receivers List of players to receive this particle, or null for all players in the * world * @return a reference to this object. */ public ParticleBuilder receivers(final @Nullable Collection receivers) { this.receivers = receivers != null ? Lists.newArrayList(receivers) : null; return this; } /** * @param receivers List of players to receive this particle, or null for all players in the * world * @return a reference to this object. */ public ParticleBuilder receivers(final Player @Nullable... receivers) { this.receivers = receivers != null ? Lists.newArrayList(receivers) : null; return this; } /** * Selects all players within a cuboid selection around the particle location, within the * specified bounding box. If you want a more spherical check, see {@link #receivers(int, * boolean)} * * @param radius amount to add on all axis * @return a reference to this object. */ public ParticleBuilder receivers(final int radius) { return this.receivers(radius, radius); } /** * Selects all players within the specified radius around the particle location. If byDistance is * false, behavior uses cuboid selection the same as {@link #receivers(int, int)} If byDistance is * true, radius is tested by distance in a spherical shape * * @param radius amount to add on each axis * @param byDistance true to use a spherical radius, false to use a cuboid * @return a reference to this object. */ public ParticleBuilder receivers(final int radius, final boolean byDistance) { if (!byDistance) { return this.receivers(radius, radius, radius); } else { if (this.location == null) { throw new IllegalStateException("Please set location first"); } this.receivers = Lists.newArrayList(); for (final Player nearbyPlayer : this.location.getWorld() .getNearbyPlayers(this.location, radius, radius, radius)) { final Location loc = nearbyPlayer.getLocation(); final double x = NumberConversions.square(this.location.getX() - loc.getX()); final double y = NumberConversions.square(this.location.getY() - loc.getY()); final double z = NumberConversions.square(this.location.getZ() - loc.getZ()); if (Math.sqrt(x + y + z) > radius) { continue; } this.receivers.add(nearbyPlayer); } return this; } } /** * Selects all players within a cuboid selection around the particle location, within the * specified bounding box. Allows specifying a different Y size than X and Z If you want a more * cylinder check, see {@link #receivers(int, int, boolean)} If you want a more spherical check, * see {@link #receivers(int, boolean)} * * @param xzRadius amount to add on the x/z axis * @param yRadius amount to add on the y axis * @return a reference to this object. */ public ParticleBuilder receivers(final int xzRadius, final int yRadius) { return this.receivers(xzRadius, yRadius, xzRadius); } /** * Selects all players within the specified radius around the particle location. If byDistance is * false, behavior uses cuboid selection the same as {@link #receivers(int, int)} If byDistance is * true, radius is tested by distance on the y plane and on the x/z plane, in a cylinder shape. * * @param xzRadius amount to add on the x/z axis * @param yRadius amount to add on the y axis * @param byDistance true to use a cylinder shape, false to use cuboid * @return a reference to this object. * @throws IllegalStateException if a location hasn't been specified yet */ public ParticleBuilder receivers(final int xzRadius, final int yRadius, final boolean byDistance) { if (!byDistance) { return this.receivers(xzRadius, yRadius, xzRadius); } else { if (this.location == null) { throw new IllegalStateException("Please set location first"); } this.receivers = Lists.newArrayList(); for (final Player nearbyPlayer : this.location.getWorld() .getNearbyPlayers(this.location, xzRadius, yRadius, xzRadius)) { final Location loc = nearbyPlayer.getLocation(); if (Math.abs(loc.getY() - this.location.getY()) > yRadius) { continue; } final double x = NumberConversions.square(this.location.getX() - loc.getX()); final double z = NumberConversions.square(this.location.getZ() - loc.getZ()); if (x + z > NumberConversions.square(xzRadius)) { continue; } this.receivers.add(nearbyPlayer); } return this; } } /** * Selects all players within a cuboid selection around the particle location, within the * specified bounding box. If you want a more cylinder check, see {@link #receivers(int, int, * boolean)} If you want a more spherical check, see {@link #receivers(int, boolean)} * * @param xRadius amount to add on the x axis * @param yRadius amount to add on the y axis * @param zRadius amount to add on the z axis * @return a reference to this object. */ public ParticleBuilder receivers(final int xRadius, final int yRadius, final int zRadius) { if (this.location == null) { throw new IllegalStateException("Please set location first"); } return this.receivers(this.location.getWorld().getNearbyPlayers(this.location, xRadius, yRadius, zRadius)); } /** * @return The player considered the source of this particle (for Visibility concerns), or null */ public @Nullable Player source() { return this.source; } /** * Sets the source of this particle for visibility concerns (Vanish API) * * @param source The player who is considered the source * @return a reference to this object. */ public ParticleBuilder source(final @Nullable Player source) { this.source = source; return this; } /** * @return Location of where the particle will spawn */ public @Nullable Location location() { return this.location; } /** * Sets the location of where to spawn the particle * * @param location The location of the particle * @return a reference to this object. */ public ParticleBuilder location(final Location location) { this.location = location.clone(); return this; } /** * Sets the location of where to spawn the particle * * @param world World to spawn particle in * @param x X location * @param y Y location * @param z Z location * @return a reference to this object. */ public ParticleBuilder location(final World world, final double x, final double y, final double z) { this.location = new Location(world, x, y, z); return this; } /** * @return Number of particles to spawn */ public int count() { return this.count; } /** * Sets the number of particles to spawn * * @param count Number of particles * @return a reference to this object. */ public ParticleBuilder count(final int count) { this.count = count; return this; } /** * Particle offset X. Varies by particle on how this is used * * @return Particle offset X. */ public double offsetX() { return this.offsetX; } /** * Particle offset Y. Varies by particle on how this is used * * @return Particle offset Y. */ public double offsetY() { return this.offsetY; } /** * Particle offset Z. Varies by particle on how this is used * * @return Particle offset Z. */ public double offsetZ() { return this.offsetZ; } /** * Sets the particle offset. Varies by particle on how this is used * * @param offsetX Particle offset X * @param offsetY Particle offset Y * @param offsetZ Particle offset Z * @return a reference to this object. */ public ParticleBuilder offset(final double offsetX, final double offsetY, final double offsetZ) { this.offsetX = offsetX; this.offsetY = offsetY; this.offsetZ = offsetZ; return this; } /** * Gets the Particle extra data. Varies by particle on how this is used * * @return the extra particle data */ public double extra() { return this.extra; } /** * Sets the particle extra data. Varies by particle on how this is used * * @param extra the extra particle data * @return a reference to this object. */ public ParticleBuilder extra(final double extra) { this.extra = extra; return this; } /** * Gets the particle custom data. Varies by particle on how this is used * * @param The Particle data type * @return the ParticleData for this particle */ public @Nullable T data() { //noinspection unchecked return (T) this.data; } /** * Sets the particle custom data. Varies by particle on how this is used * * @param data The new particle data * @param The Particle data type * @return a reference to this object. */ public ParticleBuilder data(final @Nullable T data) { this.data = data; return this; } /** * @return whether the particle is forcefully shown to players. */ public boolean force() { return this.force; } /** * Sets whether the particle is forcefully shown to the player. If forced, the particle will show * faraway, as far as the player's view distance allows. If false, the particle will show * according to the client's particle settings. * * @param force true to force, false for normal * @return a reference to this object. */ public ParticleBuilder force(final boolean force) { this.force = force; return this; } /** * Sets the particle Color. * Only valid for particles with a data type of {@link Color} or {@link Particle.DustOptions}. * * @param color the new particle color * @return a reference to this object. */ public ParticleBuilder color(final @Nullable Color color) { if (this.particle.getDataType() == Color.class) { return this.data(color); } return this.color(color, 1); } /** * Sets the particle Color and size. * Only valid for particles with a data type of {@link Particle.DustOptions}. * * @param color the new particle color * @param size the size of the particle * @return a reference to this object. */ public ParticleBuilder color(final @Nullable Color color, final float size) { if (this.particle.getDataType() != Particle.DustOptions.class && color != null) { throw new IllegalStateException("The combination of Color and size cannot be set on this particle type."); } // We don't officially support reusing these objects, but here we go if (color == null) { if (this.data instanceof Particle.DustOptions) { return this.data(null); } else { return this; } } return this.data(new Particle.DustOptions(color, size)); } /** * Sets the particle Color. * Only valid for particles with a data type of {@link Color} or {@link Particle.DustOptions}. * * @param r red color component * @param g green color component * @param b blue color component * @return a reference to this object. */ public ParticleBuilder color(final int r, final int g, final int b) { return this.color(Color.fromRGB(r, g, b)); } /** * Sets the particle Color. * Only valid for particles with a data type of {@link Color} or {@link Particle.DustOptions}. *

* This method detects if the provided color integer is in RGB or ARGB format. * If the alpha channel is zero, it treats the color as RGB. Otherwise, it treats it as ARGB. * * @param color an integer representing the color components. If the highest byte (alpha channel) is zero, * the color is treated as RGB. Otherwise, it is treated as ARGB. * @return a reference to this object. */ public ParticleBuilder color(final int color) { final int alpha = (color >> 24) & 0xFF; if (alpha == 0) { return this.color(Color.fromRGB(color)); } return this.color(Color.fromARGB(color)); } /** * Sets the particle Color. * Only valid for particles with a data type of {@link Color} or {@link Particle.DustOptions}. * * @param a alpha color component * @param r red color component * @param g green color component * @param b blue color component * @return a reference to this object. */ public ParticleBuilder color(final int a, final int r, final int g, final int b) { return this.color(Color.fromARGB(a, r, g, b)); } /** * Sets the particle Color Transition. * Only valid for {@link Particle#DUST_COLOR_TRANSITION}. * * @param fromColor the new particle from color * @param toColor the new particle to color * @return a reference to this object. * @throws IllegalArgumentException if the particle builder's {@link #particle()} isn't {@link Particle#DUST_COLOR_TRANSITION}. */ public ParticleBuilder colorTransition(final Color fromColor, final Color toColor) { return this.colorTransition(fromColor, toColor, 1); } /** * Sets the particle Color Transition. * Only valid for {@link Particle#DUST_COLOR_TRANSITION}. * * @param fromRed red color component for the "from" color * @param fromGreen green color component for the "from" color * @param fromBlue blue color component for the "from" color * @param toRed red color component for the to color * @param toGreen green color component for the to color * @param toBlue blue color component for the to color * @return a reference to this object. * @throws IllegalArgumentException if the particle builder's {@link #particle()} isn't {@link Particle#DUST_COLOR_TRANSITION}. */ public ParticleBuilder colorTransition( final int fromRed, final int fromGreen, final int fromBlue, final int toRed, final int toGreen, final int toBlue ) { return this.colorTransition(Color.fromRGB(fromRed, fromGreen, fromBlue), Color.fromRGB(toRed, toGreen, toBlue)); } /** * Sets the particle Color Transition. * Only valid for {@link Particle#DUST_COLOR_TRANSITION}. * * @param fromRgb an integer representing the red, green, and blue color components for the "from" color * @param toRgb an integer representing the red, green, and blue color components for the "to" color * @return a reference to this object. * @throws IllegalArgumentException if the particle builder's {@link #particle()} isn't {@link Particle#DUST_COLOR_TRANSITION}. */ public ParticleBuilder colorTransition(final int fromRgb, final int toRgb) { return this.colorTransition(Color.fromRGB(fromRgb), Color.fromRGB(toRgb)); } /** * Sets the particle Color Transition and size. * Only valid for {@link Particle#DUST_COLOR_TRANSITION}. * * @param fromColor the new particle color for the "from" color. * @param toColor the new particle color for the "to" color. * @param size the size of the particle * @return a reference to this object. * @throws IllegalArgumentException if the particle builder's {@link #particle()} isn't {@link Particle#DUST_COLOR_TRANSITION}. */ public ParticleBuilder colorTransition(final Color fromColor, final Color toColor, final float size) { Preconditions.checkArgument(fromColor != null, "Cannot define color transition with null fromColor."); Preconditions.checkArgument(toColor != null, "Cannot define color transition with null toColor."); Preconditions.checkArgument(this.particle() == Particle.DUST_COLOR_TRANSITION, "Can only define a color transition on particle DUST_COLOR_TRANSITION."); return this.data(new Particle.DustTransition(fromColor, toColor, size)); } @Override public ParticleBuilder clone() { try { final ParticleBuilder builder = (ParticleBuilder) super.clone(); if (this.location != null) builder.location = this.location.clone(); if (this.receivers != null) builder.receivers = new ObjectArrayList<>(this.receivers); return builder; } catch (final CloneNotSupportedException e) { throw new AssertionError(); } } }