diff --git a/README.md b/README.md index 978223e..7263c72 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ zones: enabled: true ``` -`fade-distance` expands the cuboid's audible envelope. With `playback.follow-player: true` (the default), the sound is attached to each listener and its volume fades from the configured volume inside the cuboid to silence at the edge of the fade envelope. Set it to `false` to use the fixed `origin` as a directional source instead. +The player hears the sound at the configured volume everywhere inside the cuboid. Entering starts playback and leaving stops it; `fade-distance` is retained for configuration compatibility but is not used for playback selection in this entry/exit mode. The sound is attached to the listener, so distance from `origin` does not change its volume. ## Overlaps and performance @@ -61,14 +61,14 @@ By default, only the audible zone with the highest `priority` is selected. Set ` ```yaml playback: check-interval-ticks: 10 - leave-grace-ticks: 100 + leave-grace-ticks: 0 blend-overlapping-zones: false follow-player: true fade-volume-updates: false loop-restart-ticks: 2680 ``` -When a selected zone remains selected, Ambient Audio Zone normally sends no new sound packet, so walking cannot restart or stack the audio. `fade-volume-updates` is disabled by default because Bukkit has no mutable-volume packet; enabling it approximates the fade by stopping and re-emitting the sound, which necessarily restarts it. For smooth native distance attenuation, disable `follow-player` and use a central `origin`. Leaving starts a configurable grace timer; returning before it expires preserves the existing client playback instead of restarting it. `loop-restart-ticks` is a fallback for sounds whose resource-pack definition does not loop; set it to the exact sound length, while resource-pack `loop: true` remains the seamless option. Reloading or editing a zone intentionally replaces its active sound. +When a selected zone remains selected, Ambient Audio Zone sends no new sound packet, so walking cannot restart or stack the audio. Playback stops on the first update after leaving the cuboid. `loop-restart-ticks` is a fallback for sounds whose resource-pack definition does not loop; set it to the exact sound length, while resource-pack `loop: true` remains the seamless option. Reloading or editing a zone intentionally replaces its active sound. ## Resource-pack looping note diff --git a/src/main/java/com/lexian/ambientaudio/model/AudioZone.java b/src/main/java/com/lexian/ambientaudio/model/AudioZone.java index 44a3d9b..6d0e727 100644 --- a/src/main/java/com/lexian/ambientaudio/model/AudioZone.java +++ b/src/main/java/com/lexian/ambientaudio/model/AudioZone.java @@ -21,11 +21,13 @@ public record AudioZone(String name, String worldName, String sound, Point origi public Location originLocation(World world) { return new Location(world, origin.x(), origin.y(), origin.z()); } public String normalizedName() { return name.toLowerCase(Locale.ROOT); } public double distanceToAudibleAreaSquared(Location location) { return region.distanceSquared(location.getX(), location.getY(), location.getZ()); } - public boolean isAudible(Location location) { return belongsTo(location.getWorld()) && distanceToAudibleAreaSquared(location) <= fadeDistance * fadeDistance; } + /** Returns true only while the listener is inside the configured cuboid. */ + public boolean isInside(Location location) { return belongsTo(location.getWorld()) && region.contains(location.getX(), location.getY(), location.getZ()); } public record Point(double x, double y, double z) { } /** Inclusive axis-aligned cuboid. */ public record Cuboid(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { public Cuboid { if (minX > maxX || minY > maxY || minZ > maxZ) throw new IllegalArgumentException("Cuboid minimum exceeds maximum"); } + public boolean contains(double x, double y, double z) { return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ; } public double distanceSquared(double x, double y, double z) { double dx = delta(x, minX, maxX), dy = delta(y, minY, maxY), dz = delta(z, minZ, maxZ); return dx * dx + dy * dy + dz * dz; diff --git a/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java b/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java index 9432f22..718eae0 100644 --- a/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java +++ b/src/main/java/com/lexian/ambientaudio/service/PlaybackService.java @@ -26,7 +26,7 @@ public final class PlaybackService { private void tick() { Bukkit.getOnlinePlayers().forEach(this::update); } private void update(Player player) { long now = Bukkit.getCurrentTick(); - List audible = index.candidates(player.getLocation()).stream().filter(zone -> zone.isAudible(player.getLocation())).sorted(Comparator.comparingInt(AudioZone::priority).reversed()).toList(); + List audible = index.candidates(player.getLocation()).stream().filter(zone -> zone.isInside(player.getLocation())).sorted(Comparator.comparingInt(AudioZone::priority).reversed()).toList(); if (!blending && !audible.isEmpty()) audible = List.of(audible.getFirst()); Set desired = audible.stream().map(AudioZone::normalizedName).collect(Collectors.toSet()); Map current = active.computeIfAbsent(player.getUniqueId(), unused -> new HashMap<>()); diff --git a/src/main/java/com/lexian/ambientaudio/service/ZoneService.java b/src/main/java/com/lexian/ambientaudio/service/ZoneService.java index 12d6d7b..c830089 100644 --- a/src/main/java/com/lexian/ambientaudio/service/ZoneService.java +++ b/src/main/java/com/lexian/ambientaudio/service/ZoneService.java @@ -18,5 +18,5 @@ public final class ZoneService { public AudioZone get(String name) { return zones.get(name.toLowerCase(Locale.ROOT)); } public void put(AudioZone zone) throws IOException { zones.put(zone.normalizedName(), zone); store.save(zones.values()); refreshPlayback(); } public AudioZone remove(String name) throws IOException { AudioZone removed = zones.remove(name.toLowerCase(Locale.ROOT)); if (removed != null) { store.save(zones.values()); refreshPlayback(); } return removed; } - private void refreshPlayback() { FileConfiguration c = plugin.getConfig(); playback.configure(zones.values(), Math.max(1, c.getInt("playback.check-interval-ticks", 10)), Math.max(0, c.getLong("playback.leave-grace-ticks", 100)), c.getBoolean("playback.blend-overlapping-zones", false), c.getBoolean("playback.follow-player", true), c.getBoolean("playback.fade-volume-updates", false), Math.max(0, c.getLong("playback.loop-restart-ticks", 2680))); } + private void refreshPlayback() { FileConfiguration c = plugin.getConfig(); playback.configure(zones.values(), Math.max(1, c.getInt("playback.check-interval-ticks", 10)), 0, c.getBoolean("playback.blend-overlapping-zones", false), true, false, Math.max(0, c.getLong("playback.loop-restart-ticks", 2680))); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 149bb4e..2220048 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -2,12 +2,11 @@ # "loop": true in sounds.json for true seamless, client-side looping. playback: check-interval-ticks: 10 - leave-grace-ticks: 100 + leave-grace-ticks: 0 blend-overlapping-zones: false - # Entity-attached sounds follow each player instead of remaining at origin. + # Sounds follow each player and remain at full configured volume while inside. follow-player: true - # Disabled by default: Bukkit has no mutable-volume packet, so enabling this - # necessarily restarts the sound when the calculated volume changes. + # Volume adjustment is intentionally disabled: zones use entry/exit playback. fade-volume-updates: false # Fallback replay interval for loop: true sounds. 2680 ticks is 134 seconds. # Set to the actual length of custom sounds; resource-pack loop=true is preferred.