feat(delta): playSoundAttenuated - wave propagation + normalized decay, one packet per player
Build Delta / Build paperclip jar (push) Failing after 5m24s
Build Delta / Build paperclip jar (push) Failing after 5m24s
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user