Author SHA1 Message Date
MineTec b601eba016 Merge branch 'master' into feature/silence-named-mobs 2026-11-09 18:44:39 +00:00
besiece 6505bda43e feat: implement feedback system for silence nametag actions with action bar updates 2026-11-08 13:04:30 +01:00
besiece fb78a450b8 refactor: separate nametag item handling from silence actions and add rename aliases 2026-11-08 12:38:27 +01:00
besiece 5ae2ac933a feat: refactor silence and unsilence logic into applyNametagAction method in SilenceNametag 2026-09-25 17:56:08 +02:00
besiece f1250f2879 feat: streamline silence and unsilence logic in NametagEntityListener 2026-09-25 17:37:25 +02:00
besiece 961a22beac feat: improve silence and unsilence feedback for mobs in NametagEntityListener 2026-09-25 17:24:50 +02:00
besiece 98dc59ca15 feat: enhance silence and unsilence functionality and manage nametag item amount 2026-09-25 17:19:54 +02:00
besiece 091bc8f10c feat: implement silence and unsilence actions in NametagEntityListener 2026-09-25 16:53:22 +02:00
besiece 879dfb9bdd feat: add NametagEntityListener to handle special nametag interactions 2026-09-25 16:47:48 +02:00
besiece 3513477265 refactor: enhance nametag action handling and improve lore management 2026-09-25 16:22:32 +02:00
besiece 167ed811b6 fix: update lore text for silence nametag and improve nullability handling 2026-09-19 21:12:36 +02:00
besiece 0f415b9aba refactor: streamline nametag action handling and update lore management 2026-08-18 17:12:02 +02:00
besiece aebd7f7bc8 fix: ensure lore is correctly initialized when editing item meta 2026-08-18 16:45:14 +02:00
besiece 410658259d setting the result item in the anvil 2026-08-18 16:36:59 +02:00
besiece 1dbc9b29a3 adding logging to debug 2026-08-18 16:32:39 +02:00
besiece b1c92310cb setting the lore of the result item 2026-08-18 16:25:18 +02:00
besiece 15ee6ee79e catch anvil event and return if not a nametag 2026-08-18 15:40:12 +02:00
besiece dd2d71d754 Created necessary classes and implement console logs for PrepareAnvilEvent 2026-08-18 15:28:11 +02:00
besiece 3221450cde Clean up test scripts and to prepare for development of the feature 2026-08-18 14:53:13 +02:00
besiece f615dc867a Rerunning my first test, to verify if my install worked properly 2026-08-13 13:09:14 +02:00
besiece 673bae4d50 Fixing my first test, to verify if my install worked properly 2026-08-13 10:33:38 +02:00
besiece 6ad9407cb4 Creating a first test, to verify if my install worked properly 2026-08-13 10:14:38 +02:00
10 changed files with 370 additions and 8 deletions

No files matched your search

-8
View File
@@ -1,11 +1,3 @@
# Chatcoloring
Der Spieler benötigt das Recht "chatcolor" damit dessen Namen blau im Chat angezeigt wird.
# Wandering Trader
Ich würde vorschlagen, das Plugin WanderingTrades zu verwenden, da es uns ermöglicht, eigene Handelsangebote festzulegen und gleichzeitig die Köpfe von Spielern unseres Servers zu verkaufen.
Da WanderingTrades auch Mini-Blöcke unterstützt, sollten wir überlegen, ob wir das Plugin MiniBlocks nicht vollständig weglassen. Was das Plugin nicht kann ist Player Heads zu droppen, wenn ein Spieler einen anderen tötet, dass müssten wir dann noch selber schreiben.
Zu tun wäre dann noch:
Die Config des Plugins um Sniffing Potion und Heart of the Seas hinzuzufügen und Preis für Player Heads hinzufügen.
@@ -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<Component> 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<Component> parts, int count, String label, NamedTextColor color) {
if(count > 0) parts.add(Component.text(count + " " + label, color));
}
}
@@ -0,0 +1,52 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Set;
public enum NametagAction {
SILENCE(0, Set.of("@verstummen", "@stumm", "@mute"), "Dieses Namensschild muted Mobs.", true),
UNSILENCE(1, Set.of("@entstummen", "@laut", "@unmute"), "Dieses Namensschild unmuted Mobs.", false);
private final int persistentId;
private final Set<String> renameTexts;
private final Component lore;
private final boolean silent;
NametagAction(int persistentId, Set<String> renameTexts, String lore, boolean silent) {
this.persistentId = persistentId;
this.renameTexts = Set.copyOf(renameTexts);
this.lore = Component.text(lore);
this.silent = silent;
}
public int persistentId() {
return this.persistentId;
}
public Component lore() {
return this.lore;
}
public boolean silent() {
return this.silent;
}
public static @Nullable NametagAction fromPersistentId(int id) {
for(NametagAction action : values()) {
if(action.persistentId == id) return action;
}
return null;
}
public static @Nullable NametagAction fromRenameText(@Nullable String text) {
if(text == null) return null;
String normalizedText = text.strip().toLowerCase(Locale.ROOT);
for(NametagAction action : values()) {
if(action.renameTexts.contains(normalizedText)) return action;
}
return null;
}
}
@@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.view.AnvilView;
public class NametagAnvilListener extends ApplianceListener<SilenceNametag> {
@EventHandler
public void onAnvilRename(PrepareAnvilEvent event) {
ItemStack resultItem = event.getResult();
if(resultItem == null || resultItem.getType() != Material.NAME_TAG) return;
AnvilView anvilView = event.getView();
ItemStack inputItem = anvilView.getItem(0);
if(inputItem == null || inputItem.getType() != Material.NAME_TAG) return;
NametagAction action = NametagAction.fromRenameText(anvilView.getRenameText());
if(action == null) return;
event.setResult(SilenceNametagItem.create(resultItem, action));
}
}
@@ -0,0 +1,46 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.GameMode;
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;
public class NametagEntityListener extends ApplianceListener<SilenceNametag> {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onNametagUse(PlayerInteractEntityEvent event) {
ItemStack item;
if(event.getHand() == EquipmentSlot.HAND) {
item = event.getPlayer().getInventory().getItemInMainHand();
} else {
item = event.getPlayer().getInventory().getItemInOffHand();
}
if(item.getType() != Material.NAME_TAG) return;
NametagAction action = SilenceNametagItem.readAction(item);
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.
if(item.getItemMeta().customName() != null) return;
// Unnamed special tags are handled entirely here, including no-op interactions.
event.setCancelled(true);
if(result == SilenceNametag.ActionResult.CHANGED && event.getPlayer().getGameMode() != GameMode.CREATIVE) {
item.setAmount(item.getAmount() - 1);
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
this.getAppliance().feedback().forget(event.getPlayer().getUniqueId());
}
}
@@ -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);
}
@@ -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<NametagFeedback> outputs;
private final Map<UUID, Series> seriesByPlayer = new HashMap<>();
NametagFeedbackService(LongSupplier clock, List<NametagFeedback> 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<TargetAction, SilenceNametag.ActionResult> results = new HashMap<>();
private final Set<UUID> 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());
}
}
}
@@ -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) {
}
}
@@ -0,0 +1,44 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag;
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Mob;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
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,
UNSUPPORTED
}
public ActionResult applyNametagAction(Entity entity, NametagAction action) {
Objects.requireNonNull(action, "Action must not be null.");
if(!(entity instanceof Mob)) return ActionResult.UNSUPPORTED;
if(entity.isSilent() == action.silent()) return ActionResult.UNCHANGED;
entity.setSilent(action.silent());
return ActionResult.CHANGED;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new NametagAnvilListener(), new NametagEntityListener());
}
}
@@ -0,0 +1,50 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.silenceNametag;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
final class SilenceNametagItem {
// Keep the key and IDs compatible with existing nametags.
private static final NamespacedKey ACTION_KEY = new NamespacedKey("silence-nametag", "nametag-action");
private SilenceNametagItem() {
}
static @Nullable NametagAction readAction(ItemStack item) {
if(item.getType() != Material.NAME_TAG) return null;
var data = item.getPersistentDataContainer();
if(!data.has(ACTION_KEY, PersistentDataType.INTEGER)) return null;
Integer id = data.get(ACTION_KEY, PersistentDataType.INTEGER);
return id == null ? null : NametagAction.fromPersistentId(id);
}
static ItemStack create(ItemStack source, NametagAction action) {
Objects.requireNonNull(action, "Action must not be null.");
if(source.getType() != Material.NAME_TAG) {
throw new IllegalArgumentException("Item must be a nametag.");
}
ItemStack result = source.clone();
result.editMeta(meta -> {
meta.getPersistentDataContainer().set(ACTION_KEY, PersistentDataType.INTEGER, action.persistentId());
List<Component> existingLore = meta.lore();
List<Component> lore = existingLore == null ? new ArrayList<>() : new ArrayList<>(existingLore);
for(NametagAction knownAction : NametagAction.values()) {
lore.removeIf(knownAction.lore()::equals);
}
lore.add(action.lore());
meta.lore(lore);
meta.customName(null);
});
return result;
}
}