Files
Button-Control/src/main/java/viper/ButtonControl.java
2026-03-25 23:34:26 +01:00

1455 lines
62 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package viper;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Lightable;
import org.bukkit.block.data.type.NoteBlock;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.Note;
import org.bukkit.Note.Tone;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class ButtonControl extends JavaPlugin {
private static final String TIMED_CONTAINER_MODE_SIMULTANEOUS = "simultaneous";
private static final String TIMED_CONTAINER_MODE_SEQUENTIAL = "sequential";
private ConfigManager configManager;
private DataManager dataManager;
// Bewegungsmelder-State
private final Map<String, Long> lastMotionDetections = new HashMap<>();
private final Set<String> activeSensors = new HashSet<>();
// Zeitgesteuerte Automation verhindert mehrfaches Auslösen pro Zustandswechsel
private final Map<String, Boolean> timedControllerLastState = new HashMap<>();
// Actionbar-Status pro Spieler für die Namensanzeige
private final Map<java.util.UUID, String> lastControllerActionbar = new HashMap<>();
// Undo-System: letzte Aktion pro Controller (wird nach 5 Minuten gelöscht)
private final Map<String, UndoAction> lastActions = new HashMap<>();
// Secret-Wall Runtime (offene Wände + Originalzustand)
private final Map<String, List<SecretBlockSnapshot>> openSecretWalls = new HashMap<>();
// Knarrherz-Override: aktivierte Herzen werden zyklisch auf "active=true" gehalten
private final Set<String> forcedActiveCreakingHearts = new HashSet<>();
// Geöffnete Gitter (AIR) mit Originalmaterial für die Wiederherstellung
private final Map<String, Material> openGrates = new HashMap<>();
// Laufende Zeitplan-Shows für Werfer/Spender pro Controller
private final Map<String, Integer> timedContainerTasks = new HashMap<>();
private final Map<String, Integer> timedContainerTaskShotDelays = new HashMap<>();
private final Map<String, String> timedContainerTaskModes = new HashMap<>();
private final Map<String, Integer> timedContainerNextIndices = new HashMap<>();
// Secret-Editor: Spieler -> ausgewählter Controller
private final Map<java.util.UUID, String> selectedSecretController = new HashMap<>();
@Override
public void onEnable() {
configManager = new ConfigManager(this);
dataManager = new DataManager(this);
// Update-Checker beim Start
new UpdateChecker(this, 127702).getVersion(version -> {
String current = getDescription().getVersion();
if (isNewerVersion(strip(version), strip(current))) {
getLogger().info("Update verfügbar: v" + version);
Bukkit.getScheduler().runTask(this, () ->
Bukkit.getOnlinePlayers().stream()
.filter(p -> p.hasPermission("buttoncontrol.update"))
.forEach(p -> sendUpdateMessage(p, current, version)));
} else {
getLogger().info("ButtonControl ist auf dem neuesten Stand (v" + current + ").");
}
});
// Update beim Joinen
getServer().getPluginManager().registerEvents(new org.bukkit.event.Listener() {
@org.bukkit.event.EventHandler
public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) {
Player player = event.getPlayer();
if (!player.hasPermission("buttoncontrol.update")) return;
new UpdateChecker(ButtonControl.this, 127702).getVersion(version -> {
String current = getDescription().getVersion();
if (isNewerVersion(strip(version), strip(current)))
sendUpdateMessage(player, current, version);
});
}
}, this);
if (getCommand("bc") != null) {
getCommand("bc").setExecutor(this);
getCommand("bc").setTabCompleter(new ButtonTabCompleter());
}
getServer().getPluginManager().registerEvents(
new ButtonListener(this, configManager, dataManager), this);
registerRecipes();
MetricsHandler.startMetrics(this);
getServer().getScheduler().runTaskTimer(this, this::checkDaylightSensors, 0L, 20L * 10);
getServer().getScheduler().runTaskTimer(this, this::checkMotionSensors, 0L, 10L);
getServer().getScheduler().runTaskTimer(this, this::checkTimedControllers, 0L, 20L * 5);
getServer().getScheduler().runTaskTimer(this, this::enforceCreakingHeartStates, 1L, 1L);
getServer().getScheduler().runTaskTimer(this, this::updateControllerNameActionBar, 0L, 5L);
// Undo-Actions nach 5 Minuten aufräumen
getServer().getScheduler().runTaskTimer(this, this::cleanupOldUndoActions, 0L, 20L * 60 * 5);
getLogger().info("ButtonControl v" + getDescription().getVersion() + " wurde erfolgreich aktiviert!");
}
@Override
public void onDisable() {
stopAllTimedContainerTasks();
openGrates.clear();
forcedActiveCreakingHearts.clear();
if (dataManager != null) {
dataManager.shutdown();
}
}
// -----------------------------------------------------------------------
// Update-Hilfe
// -----------------------------------------------------------------------
private String strip(String v) {
return v.replaceFirst("(?i)^(version\\s*|v\\.?\\s*)", "").trim();
}
private void sendUpdateMessage(Player player, String current, String latest) {
player.sendMessage("");
player.sendMessage("§8§m-----------------------------------------");
player.sendMessage(" §6§lButtonControl §7- §e§lUpdate verfügbar!");
player.sendMessage("");
player.sendMessage(" §7Aktuelle Version: §c" + current);
player.sendMessage(" §7Neue Version: §a" + latest);
player.sendMessage("");
player.sendMessage(" §eDownload: §bhttps://www.spigotmc.org/resources/127702/");
player.sendMessage("§8§m-----------------------------------------");
player.sendMessage("");
}
private boolean isNewerVersion(String latest, String current) {
try {
String[] lp = latest.split("\\.");
String[] cp = current.split("\\.");
int len = Math.max(lp.length, cp.length);
for (int i = 0; i < len; i++) {
int l = i < lp.length ? Integer.parseInt(lp[i]) : 0;
int c = i < cp.length ? Integer.parseInt(cp[i]) : 0;
if (l > c) return true;
if (l < c) return false;
}
return false;
} catch (NumberFormatException e) {
return !latest.equalsIgnoreCase(current);
}
}
// -----------------------------------------------------------------------
// Rezepte
// -----------------------------------------------------------------------
private void registerRecipes() {
// Alle Holz/Stein-Buttons
for (Material mat : Material.values()) {
if (!mat.name().endsWith("_BUTTON")) continue;
registerColumnRecipe(
"control_" + mat.name().toLowerCase(), mat,
"§6Steuer-" + friendlyName(mat, "_BUTTON"),
Arrays.asList("§7Ein universeller Controller.",
"§7Verbindet Türen, Lampen und mehr.")
);
}
// Tageslichtsensor
registerColumnRecipe("control_daylight", Material.DAYLIGHT_DETECTOR,
"§6Steuer-Tageslichtsensor",
Arrays.asList("§7Öffnet/schließt nach Tageszeit."));
// Notenblock
registerColumnRecipe("control_noteblock", Material.NOTE_BLOCK,
"§6Steuer-Notenblock",
Arrays.asList("§7Spielt einen Klingelton ab."));
// Bewegungsmelder
registerColumnRecipe("control_motion", Material.TRIPWIRE_HOOK,
"§6Steuer-Bewegungsmelder",
Arrays.asList("§7Erkennt Spieler und Mobs in der Nähe."));
// NEU: Schild-Controller
registerColumnRecipe("control_sign", Material.OAK_SIGN,
"§6Steuer-Schild",
Arrays.asList("§7Wandmontierbarer Controller.",
"§7Funktioniert wie ein Button."));
// NEU: Teppich-Sensoren (alle 16 Farben) NUR Spieler
for (Material mat : Material.values()) {
if (!mat.name().endsWith("_CARPET")) continue;
registerColumnRecipe(
"control_carpet_" + mat.name().toLowerCase(), mat,
"§6Steuer-Teppich §8(" + friendlyName(mat, "_CARPET") + "§8)",
Arrays.asList("§7Erkennt NUR Spieler (keine Mobs).",
"§7Bodenbasierter Bewegungsmelder.")
);
}
}
private void registerColumnRecipe(String keyName, Material mat, String displayName, List<String> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(displayName);
meta.setLore(new ArrayList<>(lore));
item.setItemMeta(meta);
}
NamespacedKey key = new NamespacedKey(this, keyName);
if (Bukkit.getRecipe(key) != null) Bukkit.removeRecipe(key);
ShapedRecipe recipe = new ShapedRecipe(key, item);
recipe.shape(" X ", " X ", " X ");
recipe.setIngredient('X', mat);
Bukkit.addRecipe(recipe);
}
/** IRON_DOOR → "§7Iron Door" | OAK_BUTTON → "§7Oak Button" */
String friendlyName(Material mat, String stripSuffix) {
String[] parts = mat.name().replace(stripSuffix, "").split("_");
StringBuilder sb = new StringBuilder("§7");
for (String p : parts) {
if (sb.length() > 2) sb.append(" ");
sb.append(Character.toUpperCase(p.charAt(0)))
.append(p.substring(1).toLowerCase());
}
return sb.toString();
}
// -----------------------------------------------------------------------
// Tageslichtsensor
// -----------------------------------------------------------------------
public void checkDaylightSensors() {
for (String loc : dataManager.getAllPlacedControllers()) {
String buttonId = dataManager.getButtonIdForPlacedController(loc);
if (buttonId == null) continue;
Location location = parseLocation(loc);
if (location == null) continue;
if (location.getBlock().getType() != Material.DAYLIGHT_DETECTOR) continue;
long time = location.getWorld().getTime();
boolean isDay = time >= 0 && time < 13000;
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected == null) continue;
for (String ts : connected) {
Location tl = parseLocation(ts);
if (tl == null) continue;
Block tb = tl.getBlock();
if (isLamp(tb.getType()) && tb.getBlockData() instanceof Lightable) {
Lightable lamp = (Lightable) tb.getBlockData();
lamp.setLit(!isDay);
tb.setBlockData(lamp);
}
}
// Secret Wall: bei Tag öffnen, bei Nacht schließen
if (isDay) {
triggerSecretWall(buttonId, false);
} else {
closeSecretWall(buttonId);
}
}
}
// -----------------------------------------------------------------------
// Zeitgesteuerte Automation (NEU)
// -----------------------------------------------------------------------
/**
* Prüft alle 5 Sekunden ob ein Zeitplan (open-time / close-time) für einen Controller
* aktiv ist und öffnet/schließt die verbundenen Blöcke bei Wechsel.
*
* Ingame-Zeit: 0 = Sonnenaufgang, 6000 = Mittag, 13000 = Sonnenuntergang, 18000 = Mitternacht
* Anzeige: ticksToTime() wandelt in "HH:MM" um (Tag beginnt um 06:00).
*/
public void checkTimedControllers() {
Set<String> activeScheduleButtons = new HashSet<>();
for (String controllerLoc : dataManager.getAllPlacedControllers()) {
String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc);
if (buttonId == null) continue;
long openTime = dataManager.getScheduleOpenTime(buttonId);
long closeTime = dataManager.getScheduleCloseTime(buttonId);
if (openTime < 0 || closeTime < 0) {
stopTimedContainerTask(buttonId);
continue;
}
activeScheduleButtons.add(buttonId);
Location loc = parseLocation(controllerLoc);
if (loc == null) continue;
long worldTime = loc.getWorld().getTime() % 24000;
boolean shouldBeOpen;
if (openTime <= closeTime) {
// Normales Intervall: z.B. öffnen 6000, schließen 18000
shouldBeOpen = worldTime >= openTime && worldTime < closeTime;
} else {
// Über Mitternacht: z.B. öffnen 20000, schließen 4000
shouldBeOpen = worldTime >= openTime || worldTime < closeTime;
}
Boolean lastState = timedControllerLastState.get(controllerLoc);
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected != null && !connected.isEmpty()) {
updateTimedContainerAutomation(buttonId, connected, shouldBeOpen);
} else {
stopTimedContainerTask(buttonId);
}
if (lastState != null && lastState == shouldBeOpen) continue;
timedControllerLastState.put(controllerLoc, shouldBeOpen);
if (connected != null && !connected.isEmpty()) {
setOpenables(connected, shouldBeOpen);
}
}
// Falls Zeitpläne entfernt wurden, zugehörige Show-Tasks sauber stoppen.
List<String> inactiveTaskButtons = new ArrayList<>(timedContainerTasks.keySet());
for (String buttonId : inactiveTaskButtons) {
if (!activeScheduleButtons.contains(buttonId)) {
stopTimedContainerTask(buttonId);
}
}
}
private void updateTimedContainerAutomation(String buttonId, List<String> connected, boolean activeWindow) {
if (!containsTimedContainer(connected)) {
stopTimedContainerTask(buttonId);
return;
}
if (!activeWindow) {
stopTimedContainerTask(buttonId);
return;
}
int configuredDelay = dataManager.getScheduleShotDelayTicks(buttonId);
int shotDelay = configuredDelay >= 0
? Math.max(0, configuredDelay)
: Math.max(0, configManager.getConfig().getInt(
"timed-container-shot-delay-ticks",
configManager.getConfig().getInt("timed-container-interval-ticks", 40)));
String configuredMode = normalizeTimedContainerMode(dataManager.getScheduleTriggerMode(buttonId));
String taskMode = configuredMode != null
? configuredMode
: normalizeTimedContainerMode(configManager.getConfig().getString(
"timed-container-trigger-mode",
TIMED_CONTAINER_MODE_SIMULTANEOUS));
int taskPeriod = Math.max(1, shotDelay);
Integer existingTaskId = timedContainerTasks.get(buttonId);
if (existingTaskId != null) {
Integer existingDelay = timedContainerTaskShotDelays.get(buttonId);
String existingMode = timedContainerTaskModes.get(buttonId);
if (existingDelay != null && existingDelay == taskPeriod && taskMode.equals(existingMode)) {
return;
}
stopTimedContainerTask(buttonId);
}
int taskId = getServer().getScheduler().scheduleSyncRepeatingTask(this, () -> {
List<String> latestConnected = dataManager.getConnectedBlocks(buttonId);
List<String> timedContainers = getTimedContainerLocations(latestConnected);
if (timedContainers.isEmpty()) {
stopTimedContainerTask(buttonId);
return;
}
if (TIMED_CONTAINER_MODE_SEQUENTIAL.equals(taskMode)) {
int nextIndex = timedContainerNextIndices.getOrDefault(buttonId, 0);
if (nextIndex >= timedContainers.size()) nextIndex = 0;
String locStr = timedContainers.get(nextIndex);
Location l = parseLocation(locStr);
if (l != null) {
Block b = l.getBlock();
if (b.getType() == Material.DISPENSER) {
triggerContainer(b, "dispense");
} else if (b.getType() == Material.DROPPER) {
triggerContainer(b, "drop");
}
}
timedContainerNextIndices.put(buttonId, (nextIndex + 1) % timedContainers.size());
} else {
for (String locStr : timedContainers) {
Location l = parseLocation(locStr);
if (l == null) continue;
Block b = l.getBlock();
if (b.getType() == Material.DISPENSER) {
triggerContainer(b, "dispense");
} else if (b.getType() == Material.DROPPER) {
triggerContainer(b, "drop");
}
}
}
}, 0L, taskPeriod);
timedContainerTasks.put(buttonId, taskId);
timedContainerTaskShotDelays.put(buttonId, taskPeriod);
timedContainerTaskModes.put(buttonId, taskMode);
timedContainerNextIndices.put(buttonId, 0);
}
private boolean containsTimedContainer(List<String> connected) {
return !getTimedContainerLocations(connected).isEmpty();
}
private List<String> getTimedContainerLocations(List<String> connected) {
List<String> timedContainers = new ArrayList<>();
for (String locStr : connected) {
Location l = parseLocation(locStr);
if (l == null) continue;
Material m = l.getBlock().getType();
if (m == Material.DISPENSER || m == Material.DROPPER) {
timedContainers.add(locStr);
}
}
return timedContainers;
}
private String normalizeTimedContainerMode(String mode) {
if (mode == null) return null;
String normalized = mode.trim().toLowerCase(Locale.ROOT);
if (TIMED_CONTAINER_MODE_SEQUENTIAL.equals(normalized)) return TIMED_CONTAINER_MODE_SEQUENTIAL;
return TIMED_CONTAINER_MODE_SIMULTANEOUS;
}
private void stopTimedContainerTask(String buttonId) {
Integer taskId = timedContainerTasks.remove(buttonId);
if (taskId != null) getServer().getScheduler().cancelTask(taskId);
timedContainerTaskShotDelays.remove(buttonId);
timedContainerTaskModes.remove(buttonId);
timedContainerNextIndices.remove(buttonId);
}
private void stopAllTimedContainerTasks() {
for (Integer taskId : timedContainerTasks.values()) {
getServer().getScheduler().cancelTask(taskId);
}
timedContainerTasks.clear();
timedContainerTaskShotDelays.clear();
timedContainerTaskModes.clear();
timedContainerNextIndices.clear();
}
private boolean triggerContainer(Block block, String methodName) {
try {
Object state = block.getState();
java.lang.reflect.Method method = state.getClass().getMethod(methodName);
Object result = method.invoke(state);
return !(result instanceof Boolean) || (Boolean) result;
} catch (ReflectiveOperationException ignored) {
return false;
}
}
// -----------------------------------------------------------------------
// Controller-Name bei Blickkontakt (Actionbar)
// -----------------------------------------------------------------------
private void updateControllerNameActionBar() {
if (!configManager.getConfig().getBoolean("controller-name-display.enabled", true)) {
clearAllControllerActionBars();
return;
}
int maxDistance = Math.max(1,
configManager.getConfig().getInt("controller-name-display.max-look-distance", 8));
String format = configManager.getConfig().getString(
"controller-name-display.format", "§6Controller: §f%s");
for (Player player : getServer().getOnlinePlayers()) {
String message = null;
Block target = player.getTargetBlockExact(maxDistance);
if (isValidController(target)) {
String targetLoc = toLoc(target);
String buttonId = dataManager.getButtonIdForLocation(targetLoc);
if (buttonId != null) {
boolean canSee = dataManager.canAccess(buttonId, player.getUniqueId())
|| player.hasPermission("buttoncontrol.admin");
String name = dataManager.getControllerName(buttonId);
if (canSee && name != null && !name.trim().isEmpty()) {
message = String.format(format, name);
}
}
}
java.util.UUID uuid = player.getUniqueId();
String previous = lastControllerActionbar.get(uuid);
if (message == null) {
if (previous != null) {
sendActionBar(player, " ");
lastControllerActionbar.remove(uuid);
}
} else if (!message.equals(previous)) {
sendActionBar(player, message);
lastControllerActionbar.put(uuid, message);
}
}
}
private void clearAllControllerActionBars() {
if (lastControllerActionbar.isEmpty()) return;
for (Player player : getServer().getOnlinePlayers()) {
if (lastControllerActionbar.containsKey(player.getUniqueId())) {
sendActionBar(player, " ");
}
}
lastControllerActionbar.clear();
}
private void sendActionBar(Player player, String message) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message));
}
// -----------------------------------------------------------------------
// Bewegungsmelder
// -----------------------------------------------------------------------
public void checkMotionSensors() {
long now = System.currentTimeMillis();
for (String controllerLoc : dataManager.getAllPlacedControllers()) {
Location loc = parseLocation(controllerLoc);
if (loc == null) continue;
Material bType = loc.getBlock().getType();
boolean isTripwire = bType == Material.TRIPWIRE_HOOK;
boolean isCarpet = bType.name().endsWith("_CARPET");
if (!isTripwire && !isCarpet) continue;
String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc);
if (buttonId == null) continue;
double radius = dataManager.getMotionSensorRadius(controllerLoc);
if (radius == -1) radius = configManager.getConfig().getDouble("motion-detection-radius", 5.0);
long delay = dataManager.getMotionSensorDelay(controllerLoc);
if (delay == -1) delay = configManager.getConfig().getLong("motion-close-delay-ms", 5000L);
final double r = radius;
boolean detected;
if (isCarpet) {
// NEU: Teppich erkennt NUR Spieler
detected = !loc.getWorld()
.getNearbyEntities(loc, r, r, r, e -> e instanceof Player).isEmpty();
} else {
// Tripwire: alle lebenden Entitäten
detected = !loc.getWorld()
.getNearbyEntities(loc, r, r, r,
e -> e instanceof org.bukkit.entity.LivingEntity).isEmpty();
}
List<String> connected = dataManager.getConnectedBlocks(buttonId);
boolean hasConnected = connected != null && !connected.isEmpty();
List<String> secretBlocks = dataManager.getSecretBlocks(buttonId);
boolean hasSecret = secretBlocks != null && !secretBlocks.isEmpty();
if (!hasConnected && !hasSecret) continue;
if (detected) {
if (!activeSensors.contains(controllerLoc)) {
if (hasConnected) setOpenables(connected, true);
triggerSecretWall(buttonId, false);
activeSensors.add(controllerLoc);
}
lastMotionDetections.put(controllerLoc, now);
} else {
Long last = lastMotionDetections.get(controllerLoc);
if (last != null && now - last >= delay) {
if (hasConnected) setOpenables(connected, false);
closeSecretWall(buttonId);
lastMotionDetections.remove(controllerLoc);
activeSensors.remove(controllerLoc);
}
}
}
}
void setOpenables(List<String> connectedBlocks, boolean open) {
for (String locStr : connectedBlocks) {
Location tl = parseLocation(locStr);
if (tl == null) continue;
Block tb = tl.getBlock();
if (tb.getBlockData() instanceof org.bukkit.block.data.Openable) {
org.bukkit.block.data.Openable o = (org.bukkit.block.data.Openable) tb.getBlockData();
o.setOpen(open);
tb.setBlockData(o);
} else if (isGrate(tb.getType()) || (tb.getType() == Material.AIR && openGrates.containsKey(locStr))) {
setGrateOpenState(tb, locStr, open);
}
}
}
// -----------------------------------------------------------------------
// Befehle
// -----------------------------------------------------------------------
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equalsIgnoreCase("bc")) return false;
if (args.length == 0) {
sender.sendMessage("§6[BC] §7/bc <info|reload|note|list|rename|schedule|trust|untrust|public|private|undo|secret>");
return true;
}
String sub = args[0].toLowerCase();
// INFO
if (sub.equals("info")) {
sender.sendMessage("§6§lButtonControl §7v" + getDescription().getVersion() + " §8by §7M_Viper");
sender.sendMessage("§7Features: §fTüren · Lampen · Notenblöcke · Sensoren · Teppiche · Schilder · Zeitpläne");
sender.sendMessage("§7Controller aktiv: §f" + dataManager.getAllPlacedControllers().size());
return true;
}
// RELOAD
if (sub.equals("reload")) {
if (!sender.hasPermission("buttoncontrol.reload")) {
sender.sendMessage(configManager.getMessage("keine-berechtigung")); return true;
}
configManager.reloadConfig();
dataManager.reloadData();
stopAllTimedContainerTasks();
timedControllerLastState.clear();
openGrates.clear();
forcedActiveCreakingHearts.clear();
clearAllControllerActionBars();
sender.sendMessage(configManager.getMessage("konfiguration-neugeladen"));
return true;
}
// NOTE
if (sub.equals("note") && sender instanceof Player) {
Player player = (Player) sender;
if (args.length < 2) { player.sendMessage("§7/bc note <Instrument>"); return true; }
try {
org.bukkit.Instrument.valueOf(args[1].toUpperCase());
dataManager.setPlayerInstrument(player.getUniqueId(), args[1].toUpperCase());
player.sendMessage(String.format(configManager.getMessage("instrument-gesetzt"), args[1].toUpperCase()));
} catch (Exception e) {
player.sendMessage(configManager.getMessage("ungueltiges-instrument"));
}
return true;
}
// Alle folgenden Befehle erfordern Spieler + angescauten Controller
if (!(sender instanceof Player)) {
sender.sendMessage("§cNur Spieler können diesen Befehl verwenden.");
return true;
}
Player player = (Player) sender;
if (sub.equals("secret")) {
return handleSecretCommand(player, args);
}
if (sub.equals("list") || sub.equals("rename") || sub.equals("schedule")
|| sub.equals("trust") || sub.equals("untrust")
|| sub.equals("public") || sub.equals("private") || sub.equals("undo")) {
Block target = player.getTargetBlockExact(5);
if (!isValidController(target)) {
player.sendMessage(configManager.getMessage("kein-controller-im-blick")); return true;
}
String targetLoc = toLoc(target);
String buttonId = dataManager.getButtonIdForLocation(targetLoc);
if (buttonId == null) {
player.sendMessage(configManager.getMessage("keine-bloecke-verbunden")); return true;
}
boolean isAdmin = player.hasPermission("buttoncontrol.admin");
boolean isOwner = dataManager.isOwner(buttonId, player.getUniqueId());
switch (sub) {
case "list":
if (!dataManager.canAccess(buttonId, player.getUniqueId()) && !isAdmin) {
player.sendMessage(configManager.getMessage("keine-berechtigung-controller")); return true;
}
sendListInfo(player, buttonId);
break;
case "rename":
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun.");
return true;
}
if (args.length < 2) { player.sendMessage("§7/bc rename <Name>"); return true; }
String oldRename = dataManager.getControllerName(buttonId);
String newName = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (newName.length() > 32) { player.sendMessage("§cName zu lang (max. 32 Zeichen)."); return true; }
String coloredName = ChatColor.translateAlternateColorCodes('&', newName);
dataManager.setControllerName(buttonId, coloredName);
saveUndoAction(buttonId, new UndoAction(UndoAction.Type.RENAME, oldRename, coloredName));
player.sendMessage(String.format(configManager.getMessage("controller-umbenannt"), coloredName));
break;
case "schedule":
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
new ScheduleGUI(this, player, buttonId).open();
break;
case "trust":
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun.");
return true;
}
if (args.length < 2) { player.sendMessage("§7/bc trust <Spieler>"); return true; }
org.bukkit.OfflinePlayer tp = Bukkit.getOfflinePlayer(args[1]);
if (!tp.hasPlayedBefore() && !tp.isOnline()) {
player.sendMessage(configManager.getMessage("spieler-nicht-gefunden")); return true;
}
dataManager.addTrustedPlayer(buttonId, tp.getUniqueId());
saveUndoAction(buttonId, new UndoAction(UndoAction.Type.TRUST_ADD, tp.getUniqueId().toString(), null));
player.sendMessage(String.format(configManager.getMessage("trust-hinzugefuegt"), args[1]));
break;
case "untrust":
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun.");
return true;
}
if (args.length < 2) { player.sendMessage("§7/bc untrust <Spieler>"); return true; }
org.bukkit.OfflinePlayer untp = Bukkit.getOfflinePlayer(args[1]);
java.util.UUID uuid = untp.getUniqueId();
dataManager.removeTrustedPlayer(buttonId, uuid);
saveUndoAction(buttonId, new UndoAction(UndoAction.Type.TRUST_REMOVE, uuid.toString(), null));
player.sendMessage(String.format(configManager.getMessage("trust-entfernt"), args[1]));
break;
default: // public / private
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können das tun.");
return true;
}
boolean pub = sub.equals("public");
boolean oldStatus = dataManager.isPublic(buttonId);
dataManager.setPublic(buttonId, pub);
saveUndoAction(buttonId, new UndoAction(pub ? UndoAction.Type.PUBLIC : UndoAction.Type.PRIVATE,
String.valueOf(oldStatus), String.valueOf(pub)));
player.sendMessage(String.format(configManager.getMessage("status-geandert"),
pub ? "§aÖffentlich" : "§cPrivat"));
break;
case "undo":
if (lastActions.containsKey(buttonId)) {
UndoAction action = lastActions.get(buttonId);
boolean canUndo = dataManager.isOwner(buttonId, player.getUniqueId()) || player.hasPermission("buttoncontrol.admin");
if (!canUndo) {
player.sendMessage("§c✖ Du kannst nur Aktionen deiner eigenen Controller rückgängig machen.");
return true;
}
undoAction(buttonId, action, player);
} else {
player.sendMessage("§c✖ Keine Aktion zum Rückgängigmachen für diesen Controller.");
}
break;
}
}
return true;
}
private void sendListInfo(Player player, String buttonId) {
String name = dataManager.getControllerName(buttonId);
String header = name != null
? "§6§l" + name
: "§6§lController §8§o(ID: " + buttonId.substring(0, 8) + "...)";
player.sendMessage(header);
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected == null || connected.isEmpty()) {
player.sendMessage(" §cKeine Blöcke verbunden.");
} else {
player.sendMessage("§7Verbundene Blöcke §8(" + connected.size() + ")§7:");
for (int i = 0; i < connected.size(); i++) {
String ls = connected.get(i);
Location l = parseLocation(ls);
String typeLabel = l != null ? "§e" + l.getBlock().getType().name() : "§8unbekannt";
String[] p = ls.split(",");
String coords = p.length == 4
? "§8(" + p[1] + "§7, §8" + p[2] + "§7, §8" + p[3] + " §7in §f" + p[0] + "§8)" : "";
player.sendMessage(" §8" + (i + 1) + ". " + typeLabel + " " + coords);
}
}
player.sendMessage("§7Status: " + (dataManager.isPublic(buttonId) ? "§aÖffentlich" : "§cPrivat"));
long openT = dataManager.getScheduleOpenTime(buttonId);
long closeT = dataManager.getScheduleCloseTime(buttonId);
if (openT >= 0 && closeT >= 0) {
player.sendMessage("§7Zeitplan: §aÖffnet §7um §e" + ticksToTime(openT)
+ " §7· §cSchließt §7um §e" + ticksToTime(closeT));
} else {
player.sendMessage("§7Zeitplan: §8Nicht gesetzt §7(§e/bc schedule§7)");
}
}
// -----------------------------------------------------------------------
// Utility
// -----------------------------------------------------------------------
/** Wandelt Minecraft-Ticks (023999) in "HH:MM" um. Ingame-Tag startet um 06:00. */
public String ticksToTime(long ticks) {
long shifted = (ticks + 6000) % 24000;
long hours = shifted / 1000;
long minutes = (shifted % 1000) * 60 / 1000;
return String.format("%02d:%02d", hours, minutes);
}
/** Wandelt "HH:MM" zurück in Minecraft-Ticks */
public long timeToTicks(int hours, int minutes) {
long totalMinutes = hours * 60L + minutes;
long ticks = (totalMinutes * 1000L / 60L - 6000 + 24000) % 24000;
return ticks;
}
public boolean isValidController(Block b) {
if (b == null) return false;
Material m = b.getType();
return m.name().endsWith("_BUTTON")
|| m == Material.DAYLIGHT_DETECTOR
|| m == Material.TRIPWIRE_HOOK
|| m.name().endsWith("_SIGN")
|| m.name().endsWith("_CARPET");
}
private boolean isLamp(Material m) {
return m == Material.REDSTONE_LAMP
|| "COPPER_BULB".equals(m.name())
|| m.name().endsWith("_COPPER_BULB");
}
public boolean isGrate(Material m) {
return m == Material.IRON_BARS || m.name().endsWith("_GRATE");
}
public boolean isManagedOpenGrateLocation(String locStr) {
return openGrates.containsKey(locStr);
}
public Boolean toggleGrate(Block block) {
if (block == null) return null;
String locStr = toLoc(block);
Material type = block.getType();
if (isGrate(type)) {
openGrates.put(locStr, type);
block.setType(Material.AIR, false);
return true;
}
if (type == Material.AIR) {
Material original = openGrates.get(locStr);
if (original != null) {
block.setType(original, false);
openGrates.remove(locStr);
return false;
}
}
return null;
}
private void setGrateOpenState(Block block, String locStr, boolean open) {
if (open) {
if (isGrate(block.getType())) {
openGrates.put(locStr, block.getType());
block.setType(Material.AIR, false);
}
return;
}
Material original = openGrates.get(locStr);
if (original != null) {
block.setType(original, false);
openGrates.remove(locStr);
}
}
private boolean isCreakingHeart(Material m) {
return "CREAKING_HEART".equals(m.name());
}
public Boolean togglePersistentCreakingHeart(Block block) {
if (block == null || !isCreakingHeart(block.getType())) return null;
String loc = toLoc(block);
boolean targetActive = !forcedActiveCreakingHearts.contains(loc);
if (!applyCreakingHeartActive(block, targetActive)) return null;
if (targetActive) forcedActiveCreakingHearts.add(loc);
else forcedActiveCreakingHearts.remove(loc);
return targetActive;
}
private void enforceCreakingHeartStates() {
if (forcedActiveCreakingHearts.isEmpty()) return;
java.util.Iterator<String> it = forcedActiveCreakingHearts.iterator();
while (it.hasNext()) {
String loc = it.next();
Location l = parseLocation(loc);
if (l == null) {
it.remove();
continue;
}
Block block = l.getBlock();
if (!isCreakingHeart(block.getType())) {
it.remove();
continue;
}
if (!applyCreakingHeartActive(block, true)) {
it.remove();
}
}
}
private boolean applyCreakingHeartActive(Block block, boolean active) {
org.bukkit.block.data.BlockData data = block.getBlockData();
try {
java.lang.reflect.Method setActive = data.getClass().getMethod("setActive", boolean.class);
setActive.invoke(data, active);
block.setBlockData(data);
return true;
} catch (ReflectiveOperationException ignored) {
return false;
}
}
public String toLoc(Block b) {
return b.getWorld().getName() + "," + b.getX() + "," + b.getY() + "," + b.getZ();
}
public Location parseLocation(String locStr) {
String[] parts = locStr.split(",");
if (parts.length != 4) return null;
World world = getServer().getWorld(parts[0]);
if (world == null) return null;
try {
return new Location(world,
Integer.parseInt(parts[1]),
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]));
} catch (NumberFormatException e) { return null; }
}
public void playDoorbellSound(Location loc, String instrument) {
Block block = loc.getBlock();
if (block.getType() != Material.NOTE_BLOCK) return;
NoteBlock noteBlock = (NoteBlock) block.getBlockData();
try {
org.bukkit.Instrument inst = org.bukkit.Instrument.valueOf(instrument.toUpperCase());
noteBlock.setInstrument(inst);
noteBlock.setNote(new Note(0, Tone.C, false));
block.setBlockData(noteBlock);
loc.getWorld().playSound(loc, inst.getSound(), 1.0f, 1.0f);
if (configManager.getConfig().getBoolean("double-note-enabled", true)) {
long delayTicks = (long)(configManager.getConfig().getInt("double-note-delay-ms", 1000) / 50.0);
getServer().getScheduler().runTaskLater(this, () -> {
if (block.getType() == Material.NOTE_BLOCK)
loc.getWorld().playSound(loc, inst.getSound(), 1.0f, 1.0f);
}, delayTicks);
}
} catch (IllegalArgumentException e) {
getLogger().warning("Ungültiges Instrument: " + instrument);
}
}
public ConfigManager getConfigManager() { return configManager; }
public DataManager getDataManager() { return dataManager; }
private boolean handleSecretCommand(Player player, String[] args) {
if (args.length < 2) {
player.sendMessage("§7/bc secret <select|info|add|remove|clear|delay|animation>");
return true;
}
String mode = args[1].toLowerCase();
String buttonId = resolveSecretController(player);
if (mode.equals("select")) {
Block target = player.getTargetBlockExact(5);
if (!isValidController(target)) {
player.sendMessage(configManager.getMessage("kein-controller-im-blick"));
return true;
}
String targetLoc = toLoc(target);
String targetButtonId = dataManager.getButtonIdForLocation(targetLoc);
if (targetButtonId == null) {
player.sendMessage(configManager.getMessage("keine-bloecke-verbunden"));
return true;
}
boolean isAdmin = player.hasPermission("buttoncontrol.admin");
boolean isOwner = dataManager.isOwner(targetButtonId, player.getUniqueId());
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können Secret-Türen verwalten.");
return true;
}
selectedSecretController.put(player.getUniqueId(), targetButtonId);
player.sendMessage("§6[Secret] §7Controller ausgewählt. Jetzt auf Wandblock schauen + /bc secret add");
return true;
}
if (buttonId == null) {
player.sendMessage("§c✖ Kein Controller ausgewählt. Schau auf den Controller und nutze §7/bc secret select§c.");
return true;
}
boolean isAdmin = player.hasPermission("buttoncontrol.admin");
boolean isOwner = dataManager.isOwner(buttonId, player.getUniqueId());
if (!isOwner && !isAdmin) {
player.sendMessage("§c✖ Nur der Besitzer oder Admins können Secret-Türen verwalten.");
return true;
}
if (mode.equals("info")) {
List<String> sb = dataManager.getSecretBlocks(buttonId);
long delayMs = dataManager.getSecretRestoreDelayMs(buttonId);
String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId));
player.sendMessage("§6[Secret] §7Blöcke: §f" + sb.size() + " §8| §7Delay: §f" + (delayMs / 1000.0) + "s");
player.sendMessage("§6[Secret] §7Animation: §f" + animation);
return true;
}
if (mode.equals("clear")) {
dataManager.clearSecret(buttonId);
closeSecretWall(buttonId);
player.sendMessage("§6[Secret] §7Alle Secret-Blöcke entfernt.");
return true;
}
if (mode.equals("delay")) {
if (args.length < 3) {
player.sendMessage("§7/bc secret delay <sekunden>");
return true;
}
try {
int sec = Integer.parseInt(args[2]);
if (sec < 1 || sec > 300) {
player.sendMessage("§c✖ Delay muss zwischen 1 und 300 Sekunden liegen.");
return true;
}
dataManager.setSecretRestoreDelayMs(buttonId, sec * 1000L);
player.sendMessage("§6[Secret] §7Wiederherstellung: §f" + sec + "s");
} catch (NumberFormatException e) {
player.sendMessage("§c✖ Ungültige Zahl.");
}
return true;
}
if (mode.equals("animation")) {
if (args.length < 3) {
player.sendMessage("§7/bc secret animation <instant|wave|reverse|center>");
return true;
}
String animation = normalizeSecretAnimation(args[2]);
if (!isValidSecretAnimation(animation)) {
player.sendMessage("§c✖ Nutze: instant, wave, reverse oder center");
return true;
}
dataManager.setSecretAnimation(buttonId, animation);
player.sendMessage("§6[Secret] §7Animation gesetzt: §f" + animation);
return true;
}
Block wallTarget = player.getTargetBlockExact(6);
if (wallTarget == null || wallTarget.getType() == Material.AIR) {
player.sendMessage("§c✖ Schau auf einen Block innerhalb von 6 Blöcken.");
return true;
}
String wallLoc = toLoc(wallTarget);
List<String> secretBlocks = new ArrayList<>(dataManager.getSecretBlocks(buttonId));
if (mode.equals("add")) {
if (isUnsafeSecretBlock(wallTarget)) {
player.sendMessage("§c✖ Dieser Block trägt/enthält einen Controller oder Schalter. Nicht als Secret-Block erlaubt.");
return true;
}
if (secretBlocks.contains(wallLoc)) {
player.sendMessage("§c✖ Dieser Block ist bereits als Secret-Block gesetzt.");
return true;
}
secretBlocks.add(wallLoc);
dataManager.setSecretBlocks(buttonId, secretBlocks);
player.sendMessage("§6[Secret] §7Block hinzugefügt. §8(" + secretBlocks.size() + ")");
return true;
}
if (mode.equals("remove")) {
if (!secretBlocks.remove(wallLoc)) {
player.sendMessage("§c✖ Dieser Block ist kein Secret-Block.");
return true;
}
dataManager.setSecretBlocks(buttonId, secretBlocks);
player.sendMessage("§6[Secret] §7Block entfernt. §8(" + secretBlocks.size() + ")");
return true;
}
player.sendMessage("§7/bc secret <select|info|add|remove|clear|delay|animation>");
return true;
}
private String resolveSecretController(Player player) {
Block target = player.getTargetBlockExact(5);
if (isValidController(target)) {
String id = dataManager.getButtonIdForLocation(toLoc(target));
if (id != null) {
selectedSecretController.put(player.getUniqueId(), id);
return id;
}
}
return selectedSecretController.get(player.getUniqueId());
}
private boolean isUnsafeSecretBlock(Block wallBlock) {
String wallLoc = toLoc(wallBlock);
for (String controllerLoc : dataManager.getAllPlacedControllers()) {
Location l = parseLocation(controllerLoc);
if (l == null) continue;
Block controller = l.getBlock();
// Controller selbst nie entfernen
if (controllerLoc.equals(wallLoc)) return true;
// Trägerblock des Controllers nie entfernen
Block support = getSupportBlock(controller);
if (support != null && toLoc(support).equals(wallLoc)) return true;
}
return false;
}
private Block getSupportBlock(Block controller) {
Material m = controller.getType();
// Aufliegende Controller: Teppich / Tageslichtsensor / Tripwire Hook
if (m.name().endsWith("_CARPET") || m == Material.DAYLIGHT_DETECTOR || m == Material.TRIPWIRE_HOOK) {
return controller.getRelative(BlockFace.DOWN);
}
// Buttons & wall-attached Blöcke
if (controller.getBlockData() instanceof org.bukkit.block.data.FaceAttachable) {
org.bukkit.block.data.FaceAttachable fa = (org.bukkit.block.data.FaceAttachable) controller.getBlockData();
switch (fa.getAttachedFace()) {
case FLOOR:
return controller.getRelative(BlockFace.DOWN);
case CEILING:
return controller.getRelative(BlockFace.UP);
case WALL:
if (controller.getBlockData() instanceof org.bukkit.block.data.Directional) {
org.bukkit.block.data.Directional d = (org.bukkit.block.data.Directional) controller.getBlockData();
return controller.getRelative(d.getFacing().getOppositeFace());
}
break;
default:
break;
}
}
// Schilder
if (m.name().endsWith("_SIGN")) {
if (m.name().contains("WALL") && controller.getBlockData() instanceof org.bukkit.block.data.Directional) {
org.bukkit.block.data.Directional d = (org.bukkit.block.data.Directional) controller.getBlockData();
return controller.getRelative(d.getFacing().getOppositeFace());
}
return controller.getRelative(BlockFace.DOWN);
}
return null;
}
/** Löst eine Secret Wall aus. autoClose=false: kein automatisches Schließen (für Sensoren). */
public boolean triggerSecretWall(String buttonId) {
return triggerSecretWall(buttonId, true);
}
public boolean triggerSecretWall(String buttonId, boolean autoClose) {
if (buttonId == null) return false;
List<String> secretBlocks = dataManager.getSecretBlocks(buttonId);
if (secretBlocks == null || secretBlocks.isEmpty()) return false;
if (openSecretWalls.containsKey(buttonId)) return false;
List<SecretBlockSnapshot> snapshots = new ArrayList<>();
for (String locStr : secretBlocks) {
Location l = parseLocation(locStr);
if (l == null) continue;
Block b = l.getBlock();
if (b.getType() == Material.AIR) continue;
snapshots.add(new SecretBlockSnapshot(locStr, b.getType().name(), b.getBlockData().getAsString(), l.getBlockX(), l.getBlockY(), l.getBlockZ()));
}
if (snapshots.isEmpty()) return false;
openSecretWalls.put(buttonId, snapshots);
String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId));
List<SecretBlockSnapshot> openOrder = getOrderedSecretSnapshots(snapshots, animation, true);
long openStepTicks = getSecretStepTicks(animation);
long[] openDelays = computeDelays(openOrder, animation, openStepTicks);
scheduleSecretOpen(openOrder, openDelays);
if (autoClose) {
long delayMs = Math.max(1000L, dataManager.getSecretRestoreDelayMs(buttonId));
long delayTicks = Math.max(1L, delayMs / 50L);
long openDurationTicks = openDelays.length == 0 ? 0L : openDelays[openDelays.length - 1];
getServer().getScheduler().runTaskLater(this,
() -> closeSecretWall(buttonId), openDurationTicks + delayTicks);
}
return true;
}
public void closeSecretWall(String buttonId) {
List<SecretBlockSnapshot> snapshots = openSecretWalls.remove(buttonId);
if (snapshots == null || snapshots.isEmpty()) return;
String animation = normalizeSecretAnimation(dataManager.getSecretAnimation(buttonId));
List<SecretBlockSnapshot> closeOrder = getOrderedSecretSnapshots(snapshots, animation, false);
long closeStepTicks = getSecretStepTicks(animation);
long[] closeDelays = computeDelays(closeOrder, animation, closeStepTicks);
scheduleSecretClose(closeOrder, closeDelays);
}
private void scheduleSecretOpen(List<SecretBlockSnapshot> snapshots, long[] delays) {
for (int i = 0; i < snapshots.size(); i++) {
SecretBlockSnapshot s = snapshots.get(i);
long when = delays[i];
getServer().getScheduler().runTaskLater(this, () -> {
Location l = parseLocation(s.loc);
if (l == null) return;
Block b = l.getBlock();
if (b.getType() != Material.AIR) {
b.setType(Material.AIR, false);
}
}, when);
}
}
private void scheduleSecretClose(List<SecretBlockSnapshot> snapshots, long[] delays) {
for (int i = 0; i < snapshots.size(); i++) {
SecretBlockSnapshot s = snapshots.get(i);
long when = delays[i];
getServer().getScheduler().runTaskLater(this, () -> {
Location l = parseLocation(s.loc);
if (l == null) return;
Block b = l.getBlock();
Material m = Material.matchMaterial(s.materialName);
if (m == null || m == Material.AIR) return;
b.setType(m, false);
try {
b.setBlockData(Bukkit.createBlockData(s.blockData), false);
} catch (IllegalArgumentException ignored) {
// Fallback: Material wurde bereits gesetzt
}
}, when);
}
}
private List<SecretBlockSnapshot> getOrderedSecretSnapshots(List<SecretBlockSnapshot> source, String animation, boolean opening) {
List<SecretBlockSnapshot> ordered = new ArrayList<>(source);
switch (animation) {
case "instant":
case "wave":
if (!opening) {
java.util.Collections.reverse(ordered);
}
return ordered;
case "reverse":
if (opening) {
java.util.Collections.reverse(ordered);
}
return ordered;
case "center":
double centerX = 0;
double centerY = 0;
double centerZ = 0;
for (SecretBlockSnapshot s : ordered) {
centerX += s.x;
centerY += s.y;
centerZ += s.z;
}
centerX /= ordered.size();
centerY /= ordered.size();
centerZ /= ordered.size();
final double cx = centerX;
final double cy = centerY;
final double cz = centerZ;
ordered.sort((a, b) -> Double.compare(distanceSquared(a, cx, cy, cz), distanceSquared(b, cx, cy, cz)));
if (!opening) {
java.util.Collections.reverse(ordered);
}
return ordered;
default:
return ordered;
}
}
private long[] computeDelays(List<SecretBlockSnapshot> ordered, String animation, long stepTicks) {
long[] delays = new long[ordered.size()];
if (!animation.equals("center")) {
for (int i = 0; i < ordered.size(); i++) {
delays[i] = i * stepTicks;
}
return delays;
}
// Für center: Blöcke im gleichen Abstandsring bekommen denselben Tick
double cx = 0, cy = 0, cz = 0;
for (SecretBlockSnapshot s : ordered) { cx += s.x; cy += s.y; cz += s.z; }
cx /= ordered.size(); cy /= ordered.size(); cz /= ordered.size();
final double fcx = cx, fcy = cy, fcz = cz;
double prev = -1;
int rank = -1;
for (int i = 0; i < ordered.size(); i++) {
double d = distanceSquared(ordered.get(i), fcx, fcy, fcz);
if (i == 0 || Math.abs(d - prev) > 0.01) {
rank++;
prev = d;
}
delays[i] = rank * stepTicks;
}
return delays;
}
private double distanceSquared(SecretBlockSnapshot s, double centerX, double centerY, double centerZ) {
double dx = s.x - centerX;
double dy = s.y - centerY;
double dz = s.z - centerZ;
return dx * dx + dy * dy + dz * dz;
}
private long getSecretStepTicks(String animation) {
return animation.equals("instant") ? 0L : 2L;
}
private boolean isValidSecretAnimation(String animation) {
return animation.equals("instant") || animation.equals("wave")
|| animation.equals("reverse") || animation.equals("center");
}
private String normalizeSecretAnimation(String animation) {
if (animation == null) return "wave";
return animation.trim().toLowerCase();
}
// ─────────────────────────────────────────────────────────────────────────
// Undo-System
// ─────────────────────────────────────────────────────────────────────────
private void saveUndoAction(String buttonId, UndoAction action) {
lastActions.put(buttonId, action);
}
private void undoAction(String buttonId, UndoAction action, Player player) {
switch (action.type) {
case RENAME:
dataManager.setControllerName(buttonId, (String) action.oldValue);
player.sendMessage("§a✔ Rename rückgängig gemacht.");
break;
case TRUST_ADD:
dataManager.removeTrustedPlayer(buttonId, java.util.UUID.fromString((String) action.oldValue));
player.sendMessage("§a✔ Trust-Hinzufügung rückgängig gemacht.");
break;
case TRUST_REMOVE:
dataManager.addTrustedPlayer(buttonId, java.util.UUID.fromString((String) action.oldValue));
player.sendMessage("§a✔ Trust-Entfernung rückgängig gemacht.");
break;
case PUBLIC:
case PRIVATE:
boolean wasPublic = Boolean.parseBoolean((String) action.oldValue);
dataManager.setPublic(buttonId, wasPublic);
player.sendMessage("§a✔ Status rückgängig gemacht: " + (wasPublic ? "§aÖffentlich" : "§cPrivat"));
break;
}
lastActions.remove(buttonId);
}
private void cleanupOldUndoActions() {
long currentTime = System.currentTimeMillis();
long timeout = 5 * 60 * 1000; // 5 Minuten
lastActions.entrySet().removeIf(entry ->
currentTime - entry.getValue().timestamp > timeout);
}
// ─────────────────────────────────────────────────────────────────────────
// Undo Action Klasse
// ─────────────────────────────────────────────────────────────────────────
public static class UndoAction {
public enum Type { RENAME, TRUST_ADD, TRUST_REMOVE, PUBLIC, PRIVATE }
public Type type;
public Object oldValue;
public Object newValue;
public long timestamp;
public UndoAction(Type type, Object oldValue, Object newValue) {
this.type = type;
this.oldValue = oldValue;
this.newValue = newValue;
this.timestamp = System.currentTimeMillis();
}
}
public static class SecretBlockSnapshot {
public final String loc;
public final String materialName;
public final String blockData;
public final int x;
public final int y;
public final int z;
public SecretBlockSnapshot(String loc, String materialName, String blockData, int x, int y, int z) {
this.loc = loc;
this.materialName = materialName;
this.blockData = blockData;
this.x = x;
this.y = y;
this.z = z;
}
}
}