diff --git a/paper-api/src/main/java/org/bukkit/World.java b/paper-api/src/main/java/org/bukkit/World.java index aae1e4943..181d6937a 100644 --- a/paper-api/src/main/java/org/bukkit/World.java +++ b/paper-api/src/main/java/org/bukkit/World.java @@ -4064,6 +4064,53 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient */ public void spawnParticle(@NotNull Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, @Nullable T data, boolean force); + /** + * Spawns a particle that players further away can still see. + * + *

Vanilla refuses to send particles beyond {@code 32} blocks from the + * receiving player's own position, and the vanilla client applies the same + * cut-off when rendering. Both limits exist so that a single particle cannot + * cost bandwidth to the whole server, but for anything a player is meant to + * aim with, the effect is that the ability silently stops being visible + * while still hitting: the reticle disappears past 32 blocks, yet the strike + * lands. The mechanic reaches 120 and the player sees nothing past 32. + * + *

{@link #spawnParticle} cannot express this. Its {@code force} parameter + * maps to vanilla's {@code overrideLimiter}, not to {@code alwaysShow}, and + * CraftBukkit hardcodes {@code alwaysShow = false}, so no Bukkit plugin has + * ever been able to send a particle further than 32 blocks. + * + *

The distance is per call, because the caller knows what the particle is + * for. Server-side it is applied by pre-filtering the receiver list; the + * {@code alwaysShow} flag then additionally lifts the client's own render + * cut-off, so the particle is drawn at the requested range instead of being + * received and dropped. + * + *

Declared {@code default} on purpose: adding an abstract method to this + * interface would break every third-party {@link World} implementation at + * runtime. The fallback here is the stock 32-block behaviour, so an + * implementation that does not override this degrades to "too short a + * range", never to "no particles". + * + * @param particle the particle type + * @param x the x coordinate of the particle + * @param y the y coordinate of the particle + * @param z the z coordinate of the particle + * @param count the amount of particles to spawn + * @param offsetX the maximum random offset on the X axis + * @param offsetY the maximum random offset on the Y axis + * @param offsetZ the maximum random offset on the Z axis + * @param extra the extra data for this particle, depends on the + * particle used (normally speed) + * @param data the data to use for the particle or null, + * the type of this depends on {@link Particle#getDataType()} + * @param viewDistance how far away, in blocks, players may be and still + * receive this particle + */ + public default void spawnParticleLongDistance(@NotNull Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, @Nullable T data, double viewDistance) { + spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data); + } + /** * Find the closest nearby structure of a given {@link StructureType}. * Finding unexplored structures can, and will, block if the world is diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 524189767..8fab42586 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -2296,6 +2296,64 @@ public class CraftWorld extends CraftRegionAccessor implements World { } + @Override + public void spawnParticleLongDistance(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, T data, double viewDistance) { + // CraftBukkit start - extended particle range + data = CraftParticle.convertLegacy(data); + if (data != null) { + Preconditions.checkArgument(particle.getDataType().isInstance(data), "data (%s) should be %s", data.getClass(), particle.getDataType()); + } + + final me.sashegdev.delta.DeltaConfig cfg = deltaConfig(); + // Выключатель и потолок из DeltaConfig. Пока extendedParticleDistanceEnabled + // выключен, поведение остаётся стоковым, и метод работает как spawnParticle. + if (cfg == null || !cfg.extendedParticleDistanceEnabled) { + this.spawnParticle(particle, null, null, x, y, z, count, offsetX, offsetY, offsetZ, extra, data, false); + return; + } + final double limit = Math.min(viewDistance, cfg.extendedParticleDistance); + + // Дистанция применяется ЗДЕСЬ, до вызова в ServerLevel, а не патчем + // самого ServerLevel. Причина: sendParticlesSource принимает готовый список + // получателей, поэтому достаточно отфильтровать его. Ванильная проверка + // (longDistance ? 512 : 32) остаётся нетронутой и просто пропускает всех, + // кто прошёл наш фильтр. + final List targets = new java.util.ArrayList<>(); + for (net.minecraft.server.level.ServerPlayer handle : this.getHandle().players()) { + if (handle.level() != this.getHandle()) { + continue; + } + if (handle.blockPosition().closerToCenterThan(new net.minecraft.world.phys.Vec3(x, y, z), limit)) { + targets.add(handle); + } + } + if (targets.isEmpty()) { + return; + } + + this.getHandle().sendParticlesSource( + targets, + null, + CraftParticle.createParticleParam(particle, data), + false, // overrideLimiter: прицелы редкие, форсировать не нужно + true, // alwaysShow: снимает 32-блочную отсечку на клиенте + x, y, z, + count, + offsetX, offsetY, offsetZ, + extra + ); + // CraftBukkit end + } + + /** Конфиг Delta, либо null если он ещё не инициализирован. */ + private static me.sashegdev.delta.DeltaConfig deltaConfig() { + try { + return io.papermc.paper.configuration.GlobalConfiguration.get().delta; + } catch (Throwable t) { + return null; + } + } + @Deprecated @Override public Location locateNearestStructure(Location origin, org.bukkit.StructureType structureType, int radius, boolean findUnexplored) {