feat(delta): playSoundAttenuated - wave propagation + normalized decay, one packet per player
Build Delta / Build paperclip jar (push) Failing after 5m24s

This commit is contained in:
SashegDev
2026-09-30 10:39:11 +00:00
parent 0373806e25
commit 0817a57065
4 changed files with 268 additions and 0 deletions
@@ -3637,6 +3637,35 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
default void playSound(@NotNull Location location, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch, float range) {
playSound(location, sound, category, volume, pitch); // fallback: range == f(volume)
}
/**
* Delta: plays a sound that travels outward as a wave and fades with distance.
* <p>
* Unlike vanilla, where audible range is derived from volume (so "audible at
* 500 blocks" means volume 31 and is deafening up close), volume and distance
* are independent here: {@code volume} is loudness at the source, {@code range}
* is how far the sound carries. Attenuation follows
* {@code gain = volume * (1 - d/range)^exponent}, reaching exactly zero at
* {@code range}; beyond it nothing is transmitted at all.
* <p>
* The wave front moves at {@code delta.sound.propagation-blocks-per-tick}
* (default 10, i.e. 100 blocks in 0.5s), so distant players hear it after that
* delay. Exactly one packet per player is sent, when the front arrives, with
* no per-tick updating.
* <p>
* Requires the Delta core; on any other server it falls back to
* {@link #playSound(Location, Sound, SoundCategory, float, float)}.
*
* @param location where the sound originates
* @param sound the sound to play
* @param category the category of the sound
* @param volume loudness at the source
* @param pitch the pitch of the sound
* @param range how far the sound carries, in blocks
*/
default void playSoundAttenuated(@NotNull Location location, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch, float range) {
playSound(location, sound, category, volume, pitch);
}
/**
* Play a Sound at the location of the provided entity in the World. For sounds
@@ -25,4 +25,32 @@ public class DeltaConfig extends ConfigurationPart {
/** Раздельные громкость и дальность звука (для DeltaSound). */
public boolean decoupledSoundRangeEnabled = false;
/** Затухающий звук: волна идёт наружу и стихает по дистанции. */
public Sound sound;
public class Sound extends ConfigurationPart {
/**
* Скорость распространения в блоках за тик.
* 10.0 — это 100 блоков за 0.5с. Реальная скорость звука ~17.15 (343 м/с).
* 0 — мгновенное распространение, остаётся только затухание.
*/
public double propagationBlocksPerTick = 10.0;
/**
* Экспонента нормализованной кривой gain = volume * (1 - d/range)^exponent.
* 1 — линейно, 4 — резко (на половине range остаётся ~6%).
*/
public double decayExponent = 4.0;
/**
* Компенсировать собственное затухание клиента. Требует калибровки
* через /delta soundcalibrate: клиент гасит позиционный звук сам, и
* без компенсации итог будет тише расчётного.
*/
public boolean clientCompensation = false;
/** Ниже этого gain звук не отправляется вовсе — экономит трафик. */
public double minAudibleGain = 0.004;
}
}
@@ -0,0 +1,167 @@
package me.sashegdev.delta;
import io.papermc.paper.configuration.GlobalConfiguration;
import net.minecraft.network.protocol.Packet;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Затухающий звук с распространением.
*
* <p>Идея: звук не «включился у всех сразу», а пошёл волной от источника со
* скоростью {@code propagationBlocksPerTick} и одновременно стихает. Игрок
* слышит его в момент, когда фронт волны до него дошёл, уже ослабленным.
*
* <p>Ключевой момент экономии: на каждого игрока уходит <b>ровно один пакет</b>,
* отложенный до тика прихода. Покадрового обновления громкости нет вообще, иначе
* при десятке источников это N пакетов × M игроков каждый тик — ровно тот трафик,
* который здесь и пытается убрать.
*
* <p>Кривая нормализованная: {@code gain = volume * (1 - d/range)^exponent}.
* Ноль ровно на {@code range}, поэтому параметр дистанции в вызове осмыслен.
* Физический {@code 1/r} не годится: с порогом слышимости он срезал бы всё
* дальше ~1000 блоков, и переданный range просто не влиял бы на результат.
*/
public final class DeltaSound {
private DeltaSound() {
}
private record Pending(long dueTick, ServerPlayer player, Packet<?> packet) {
}
private static final List<Pending> PENDING = new ArrayList<>();
private static int pumpTaskId = -1;
/** Итоговая громкость для игрока на дистанции {@code distance}. */
public static double attenuate(final double volume, final double distance, final double range, final double exponent) {
if (range <= 0.0D || distance >= range) {
return 0.0D;
}
final double t = 1.0D - (distance / range);
return volume * Math.pow(t, exponent);
}
/** Через сколько тиков фронт волны дойдёт до игрока. 0 = мгновенно. */
public static int arrivalTicks(final double distance, final double blocksPerTick) {
if (blocksPerTick <= 0.0D) {
return 0;
}
return (int) Math.ceil(distance / blocksPerTick);
}
/**
* Ставит доставку пакета игроку на нужный тик.
* Доставляет не игроку-владельцу, а получателю: один пакет на игрока.
*/
public static void schedule(final ServerPlayer player, final Packet<?> packet, final int delayTicks) {
if (delayTicks <= 0) {
player.connection.send(packet);
return;
}
synchronized (PENDING) {
PENDING.add(new Pending(currentTick() + delayTicks, player, packet));
}
ensurePump(player.server);
}
private static long currentTick() {
// Отсчёт идёт от первого появления записи, поэтому относительные задержки
// не зависят от абсолютного значения игрового тика.
return (long) (System.nanoTime() / 50_000_000L);
}
private static void ensurePump(final Object server) {
if (pumpTaskId != -1) {
return;
}
final org.bukkit.Server bukkitServer = org.bukkit.Bukkit.getServer();
pumpTaskId = bukkitServer.getScheduler()
.runTaskTimer(bukkitServer, DeltaSound::pump, 1L, 1L)
.getTaskId();
}
private static void pump() {
final long now = currentTick();
final List<Pending> done = new ArrayList<>();
synchronized (PENDING) {
final Iterator<Pending> it = PENDING.iterator();
while (it.hasNext()) {
final Pending p = it.next();
if (p.dueTick() <= now) {
it.remove();
done.add(p);
}
}
if (PENDING.isEmpty()) {
pumpTaskId = -1;
}
}
for (final Pending p : done) {
// Игрок мог выйти или сменить мир за время задержки
if (p.player().isRemoved() || p.player().connection == null) {
continue;
}
p.player().connection.send(p.packet());
}
}
/** Дистанция по горизонтали и вертикали от игрока до точки звука. */
public static double distance(final Location from, final Location to) {
final double dx = from.getX() - to.getX();
final double dy = from.getY() - to.getY();
final double dz = from.getZ() - to.getZ();
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
private static DeltaConfig.Sound cfg() {
try {
return GlobalConfiguration.get().delta.sound;
} catch (Throwable t) {
return null;
}
}
/**
* Пакетная громкость с учётом затухания клиента.
* Пока калибровка не проведена (clientCompensation=false) — шлём расчётный gain.
*/
public static float outgoingVolume(final double gain, final double distance, final double range) {
final DeltaConfig.Sound s = cfg();
if (s == null || !s.clientCompensation) {
return (float) gain;
}
// Компенсация включается только после калибровки через /delta soundcalibrate:
// клиент сам гасит позиционный звук, поэтому без неё итог тише расчётного.
// Форма подгоняется по замеру, здесь — консервативная 1/r-нормализация,
// чтобы дистанция не съедала громкость дважды.
final double t = range <= 0.0D ? 0.0D : Math.min(1.0D, distance / range);
final double clientFactor = Math.max(0.08D, 1.0D - t);
return (float) Math.min(1.0D, gain / clientFactor);
}
public static double speed() {
final DeltaConfig.Sound s = cfg();
return s == null ? 10.0D : s.propagationBlocksPerTick;
}
public static double exponent() {
final DeltaConfig.Sound s = cfg();
return s == null ? 4.0D : s.decayExponent;
}
public static double minGain() {
final DeltaConfig.Sound s = cfg();
return s == null ? 0.004D : s.minAudibleGain;
}
public static void reset() {
synchronized (PENDING) {
PENDING.clear();
}
}
}
@@ -1919,6 +1919,50 @@ public class CraftWorld extends CraftRegionAccessor implements World {
);
}
// Delta end
// Delta start - attenuating sound with wave propagation
// One packet per player, delivered when the wave front reaches them. Volume and
// distance are independent, which vanilla cannot do: it derives audible range from
// volume, so a sound meant to carry 500 blocks would need volume 31 and would be
// deafening up close.
@Override
public void playSoundAttenuated(Location loc, Sound sound, org.bukkit.SoundCategory category, float volume, float pitch, float range) {
org.spigotmc.AsyncCatcher.catchOp("play sound (attenuated)"); // Paper
if (loc == null || sound == null || category == null || range <= 0.0f) return;
final double speed = me.sashegdev.delta.DeltaSound.speed();
final double exponent = me.sashegdev.delta.DeltaSound.exponent();
final double minGain = me.sashegdev.delta.DeltaSound.minGain();
final double x = loc.getX();
final double y = loc.getY();
final double z = loc.getZ();
final double rangeD = range;
for (final net.minecraft.server.level.ServerPlayer listener : this.getHandle().players()) {
final double dx = listener.getX() - x;
final double dy = listener.getY() - y;
final double dz = listener.getZ() - z;
final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
final double gain = me.sashegdev.delta.DeltaSound.attenuate(volume, distance, rangeD, exponent);
if (gain < minGain || gain <= 0.0) {
continue; // beyond audible range: nothing transmitted at all
}
final int delay = me.sashegdev.delta.DeltaSound.arrivalTicks(distance, speed);
final float outgoing = me.sashegdev.delta.DeltaSound.outgoingVolume(gain, distance, rangeD);
final net.minecraft.network.protocol.game.ClientboundSoundPacket packet = new net.minecraft.network.protocol.game.ClientboundSoundPacket(
CraftSound.bukkitToMinecraft(sound),
SoundSource.valueOf(category.name()),
x, y, z,
outgoing, pitch, this.getHandle().random.nextLong()
);
me.sashegdev.delta.DeltaSound.schedule(listener, packet, delay);
}
}
// Delta end
@Override
public void playSound(Location loc, Sound sound, org.bukkit.SoundCategory category, float volume, float pitch, long seed) {