diff --git a/paper-api/src/main/java/org/bukkit/World.java b/paper-api/src/main/java/org/bukkit/World.java
index ae342a5ce..6e6f488ae 100644
--- a/paper-api/src/main/java/org/bukkit/World.java
+++ b/paper-api/src/main/java/org/bukkit/World.java
@@ -3616,6 +3616,28 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
*/
void playSound(@NotNull Entity entity, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch, long seed);
+ /**
+ * Delta: plays a sound with an explicit audible range, decoupled from volume.
+ *
+ * In vanilla the audible distance is derived from volume (16 blocks per volume unit),
+ * so a sound that must be heard 150 blocks away requires volume ~10, which then clips
+ * badly for players standing next to the source. This overload lets you pick gain and
+ * distance independently.
+ *
+ * Requires the Delta core with {@code delta.decoupled-sound-range-enabled: true};
+ * on any other server it silently falls back to vanilla behaviour.
+ *
+ * @param location the location to play the sound at
+ * @param sound the sound to play
+ * @param category the category of the sound
+ * @param volume the loudness at the source
+ * @param pitch the pitch of the sound
+ * @param range the audible radius in blocks, independent of volume
+ */
+ 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)
+ }
+
/**
* Play a Sound at the location of the provided entity in the World. For sounds
* with multiple variations passing the same seed will always play the same
diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
index d0c4037c3..48ea39d7d 100644
--- a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
+++ b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java
@@ -139,6 +139,23 @@ public abstract class Configurations {
return instance;
}
+ // Delta start - persist a single global config section
+ // Paper exposes no way to write paper-global.yml back after boot: reloadConfigs()
+ // re-reads from disk and would clobber any in-memory change. This writes just the
+ // requested section and leaves every other node exactly as it was parsed, so it can
+ // never damage unrelated settings.
+ public void saveGlobalSection(final RegistryAccess registryAccess, final String section, final Object value) throws ConfigurateException {
+ final Path configFile = this.globalFolder.resolve(this.globalConfigFileName);
+ final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder(registryAccess)
+ .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build()))
+ .path(configFile)
+ .build();
+ final ConfigurationNode node = loader.load();
+ node.node(section).set(value);
+ trySaveFileNode(loader, node, configFile.toString());
+ }
+ // Delta end
+
protected void verifyGlobalConfigVersion(final ConfigurationNode globalNode) {
final ConfigurationNode version = globalNode.node(Configuration.VERSION_FIELD);
if (version.virtual()) {
diff --git a/paper-server/src/main/java/me/sashegdev/delta/DeltaCommand.java b/paper-server/src/main/java/me/sashegdev/delta/DeltaCommand.java
index b2848145b..b6bce8484 100644
--- a/paper-server/src/main/java/me/sashegdev/delta/DeltaCommand.java
+++ b/paper-server/src/main/java/me/sashegdev/delta/DeltaCommand.java
@@ -73,17 +73,14 @@ public class DeltaCommand extends Command {
cfg.displayTrackingRange = value;
sender.sendMessage(Component.text("Display tracking range set to " + value + " blocks", NamedTextColor.GREEN));
}
- // Paper не даёт публичного API для записи paper-global.yml, поэтому
- // это живая правка только в памяти: после рестарта вернётся значение
- // из конфига. Постоянно — правкой paper-global.yml -> delta.
- sender.sendMessage(Component.text(
- "Runtime-only. For a permanent change edit paper-global.yml -> delta.",
- NamedTextColor.GRAY));
+ // Paper не даёт публичного API для записи paper-global.yml — Delta добавляет
+ // saveGlobalSection(), который пишет только секцию delta и не трогает остальное.
+ persist(sender, "display range");
}
case "clear" -> {
var cfg = io.papermc.paper.configuration.GlobalConfiguration.get().delta;
cfg.displayTrackingRangeEnabled = false;
- sender.sendMessage(Component.text("Runtime override cleared, back to config values", NamedTextColor.YELLOW));
+ persist(sender, "display range");
sender.sendMessage(line("display range ", DeltaTracking.describe()));
}
case "help" -> sender.sendMessage(Component.text(
@@ -114,6 +111,25 @@ public class DeltaCommand extends Command {
return List.of();
}
+ /**
+ * Пишет секцию {@code delta} обратно в paper-global.yml.
+ * Записывается только наша секция — остальные узлы файла остаются как были распарсены,
+ * поэтому сломать чужие настройки этим невозможно.
+ */
+ private void persist(final CommandSender sender, final String what) {
+ final var server = Bukkit.getServer();
+ try {
+ server.paperConfigurations().saveGlobalSection(
+ server.registryAccess(), "delta",
+ io.papermc.paper.configuration.GlobalConfiguration.get().delta);
+ sender.sendMessage(Component.text("Saved to paper-global.yml -> delta (" + what + ")", NamedTextColor.DARK_GREEN));
+ } catch (Throwable t) {
+ sender.sendMessage(Component.text(
+ "Applied at runtime, but failed to persist: " + t.getMessage(),
+ NamedTextColor.RED));
+ }
+ }
+
private Component header() {
return Component.text("---- Delta ----", NamedTextColor.DARK_PURPLE);
}
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 ee7a41690..9ba94246f 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1889,6 +1889,37 @@ public class CraftWorld extends CraftRegionAccessor implements World {
this.playSound(loc, sound, category, volume, pitch, this.getHandle().random.nextLong());
}
+ // Delta start - decoupled sound range
+ // Vanilla derives audible distance from volume (SoundEvent#getRange = 16 * volume), so the
+ // only way to make a boss ability audible at 150 blocks is volume=10, which then clips
+ // badly for anyone standing next to the source. This sends the same packet with an
+ // explicit radius, keeping gain and distance independent.
+ @Override
+ public void playSound(Location loc, Sound sound, org.bukkit.SoundCategory category, float volume, float pitch, float range) {
+ org.spigotmc.AsyncCatcher.catchOp("play sound (decoupled range)"); // Paper
+ if (loc == null || sound == null || category == null) return;
+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().delta.decoupledSoundRangeEnabled) {
+ this.playSound(loc, sound, category, volume, pitch, this.getHandle().random.nextLong()); // feature off: stock behaviour
+ return;
+ }
+
+ final double x = loc.getX();
+ final double y = loc.getY();
+ final double z = loc.getZ();
+
+ 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,
+ volume, pitch, this.getHandle().random.nextLong()
+ );
+
+ this.getServer().getHandle().getPlayerList().broadcast(
+ null, x, y, z, range, this.getHandle().dimension(), packet
+ );
+ }
+ // Delta end
+
@Override
public void playSound(Location loc, Sound sound, org.bukkit.SoundCategory category, float volume, float pitch, long seed) {
org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper