From 6505bda43e32df11c2dc1639e614683616581f17 Mon Sep 17 00:00:00 2001 From: besiece Date: Sun, 8 Nov 2026 13:04:30 +0100 Subject: [PATCH] feat: implement feedback system for silence nametag actions with action bar updates --- .../ActionBarNametagFeedback.java | 46 +++++++++++ .../silenceNametag/NametagEntityListener.java | 7 ++ .../silenceNametag/NametagFeedback.java | 7 ++ .../NametagFeedbackService.java | 82 +++++++++++++++++++ .../silenceNametag/NametagFeedbackUpdate.java | 18 ++++ .../silenceNametag/SilenceNametag.java | 12 +++ 6 files changed, 172 insertions(+) create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/ActionBarNametagFeedback.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedback.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackService.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackUpdate.java diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/ActionBarNametagFeedback.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/ActionBarNametagFeedback.java new file mode 100644 index 0000000..e196287 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/ActionBarNametagFeedback.java @@ -0,0 +1,46 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +final class ActionBarNametagFeedback implements NametagFeedback { + @Override + public void send(Player player, NametagFeedbackUpdate update) { + player.sendActionBar(this.render(update)); + } + + Component render(NametagFeedbackUpdate update) { + if(update.summary()) { + var counts = update.counts(); + List parts = new ArrayList<>(); + addCount(parts, counts.silenced(), "verstummt", NamedTextColor.GREEN); + addCount(parts, counts.unsilenced(), "entstummt", NamedTextColor.GREEN); + addCount(parts, counts.alreadySilent(), "bereits stumm", NamedTextColor.YELLOW); + addCount(parts, counts.alreadyAudible(), "bereits hörbar", NamedTextColor.YELLOW); + addCount(parts, counts.unsupported(), "nicht unterstützt", NamedTextColor.RED); + return Component.join(JoinConfiguration.separator(Component.text(" · ", NamedTextColor.GRAY)), parts); + } + + boolean silence = update.action() == NametagAction.SILENCE; + String suffix = switch(update.result()) { + case CHANGED -> silence ? " ist jetzt stumm." : " ist jetzt wieder hörbar."; + case UNCHANGED -> silence ? " ist bereits stumm." : " ist bereits hörbar."; + case UNSUPPORTED -> " unterstützt dieses Namensschild nicht."; + }; + NamedTextColor color = switch(update.result()) { + case CHANGED -> NamedTextColor.GREEN; + case UNCHANGED -> NamedTextColor.YELLOW; + case UNSUPPORTED -> NamedTextColor.RED; + }; + return Component.empty().color(color).append(update.targetName()).append(Component.text(suffix)); + } + + private static void addCount(List parts, int count, String label, NamedTextColor color) { + if(count > 0) parts.add(Component.text(count + " " + label, color)); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagEntityListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagEntityListener.java index 13f5923..e82f897 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagEntityListener.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagEntityListener.java @@ -6,6 +6,7 @@ import org.bukkit.Material; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -25,6 +26,7 @@ public class NametagEntityListener extends ApplianceListener { if(action == null) return; SilenceNametag.ActionResult result = this.getAppliance().applyNametagAction(event.getRightClicked(), action); + this.getAppliance().feedback().send(event.getPlayer(), event.getRightClicked(), action, result); if(result == SilenceNametag.ActionResult.UNSUPPORTED) return; // Named tags retain vanilla naming and consumption, even if the mob is already in the desired state. @@ -36,4 +38,9 @@ public class NametagEntityListener extends ApplianceListener { item.setAmount(item.getAmount() - 1); } } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + this.getAppliance().feedback().forget(event.getPlayer().getUniqueId()); + } } diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedback.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedback.java new file mode 100644 index 0000000..a7b9e92 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedback.java @@ -0,0 +1,7 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag; + +import org.bukkit.entity.Player; + +interface NametagFeedback { + void send(Player player, NametagFeedbackUpdate update); +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackService.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackService.java new file mode 100644 index 0000000..289f771 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackService.java @@ -0,0 +1,82 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag; + +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; + +final class NametagFeedbackService { + private static final long SERIES_TIMEOUT = TimeUnit.SECONDS.toNanos(1); + private final LongSupplier clock; + private final List outputs; + private final Map seriesByPlayer = new HashMap<>(); + + NametagFeedbackService(LongSupplier clock, List outputs) { + this.clock = clock; + this.outputs = List.copyOf(outputs); + } + + void send(Player player, Entity target, NametagAction action, SilenceNametag.ActionResult result) { + Component name = target.customName(); + if(name == null) name = Component.translatable(target.getType().translationKey()); + NametagFeedbackUpdate update = this.recordInteraction(player.getUniqueId(), target.getUniqueId(), name, action, result); + this.outputs.forEach(output -> output.send(player, update)); + } + + // Called synchronously by the entity listener. Series retain no Bukkit objects or display names. + NametagFeedbackUpdate recordInteraction(UUID playerId, UUID targetId, Component name, + NametagAction action, SilenceNametag.ActionResult result) { + long now = this.clock.getAsLong(); + this.seriesByPlayer.values().removeIf(series -> now - series.lastInteraction >= SERIES_TIMEOUT); + boolean newSeries = !this.seriesByPlayer.containsKey(playerId); + Series series = this.seriesByPlayer.computeIfAbsent(playerId, ignored -> new Series()); + series.lastInteraction = now; + if(result == SilenceNametag.ActionResult.UNSUPPORTED) { + series.unsupported.add(targetId); + } else { + series.results.merge(new TargetAction(targetId, action), result, (previous, current) -> + previous == SilenceNametag.ActionResult.CHANGED ? previous : current); + } + return new NametagFeedbackUpdate(targetId, name, action, result, newSeries, !newSeries, series.counts()); + } + + void forget(UUID playerId) { + this.seriesByPlayer.remove(playerId); + } + + void clear() { + this.seriesByPlayer.clear(); + } + + private record TargetAction(UUID targetId, NametagAction action) { + } + + private static final class Series { + private long lastInteraction; + private final Map results = new HashMap<>(); + private final Set unsupported = new HashSet<>(); + + private NametagFeedbackUpdate.Counts counts() { + int silenced = 0, unsilenced = 0, alreadySilent = 0, alreadyAudible = 0; + for(var entry : this.results.entrySet()) { + boolean changed = entry.getValue() == SilenceNametag.ActionResult.CHANGED; + if(entry.getKey().action() == NametagAction.SILENCE) { + if(changed) silenced++; + else alreadySilent++; + } else { + if(changed) unsilenced++; + else alreadyAudible++; + } + } + return new NametagFeedbackUpdate.Counts(silenced, unsilenced, alreadySilent, alreadyAudible, this.unsupported.size()); + } + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackUpdate.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackUpdate.java new file mode 100644 index 0000000..5e9a9bc --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/NametagFeedbackUpdate.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag; + +import net.kyori.adventure.text.Component; + +import java.util.UUID; + +record NametagFeedbackUpdate( + UUID targetId, + Component targetName, + NametagAction action, + SilenceNametag.ActionResult result, + boolean newSeries, + boolean summary, + Counts counts +) { + record Counts(int silenced, int unsilenced, int alreadySilent, int alreadyAudible, int unsupported) { + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/SilenceNametag.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/SilenceNametag.java index 4ad51d1..c4691cb 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/SilenceNametag.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/silenceNametag/SilenceNametag.java @@ -10,6 +10,18 @@ import java.util.List; import java.util.Objects; public class SilenceNametag extends Appliance { + private final NametagFeedbackService feedback = new NametagFeedbackService( + System::nanoTime, List.of(new ActionBarNametagFeedback())); + + NametagFeedbackService feedback() { + return this.feedback; + } + + @Override + public void onDisable() { + this.feedback.clear(); + } + public enum ActionResult { CHANGED, UNCHANGED,