diff --git a/README.md b/README.md index 284c534..60b4877 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,5 @@ Documentation: [https://modularsoft.org/docs/products/zander](https://modularsof Product docs: - [Private messaging (Zander Velocity)](docs/private-messaging.md) +- [Hall of Supporters (Zander Hub)](docs/hall-of-supporters.md) +- [Statue Customization Guide (Zander Hub)](docs/statue-customization-guide.md) diff --git a/docs/hall-of-supporters.md b/docs/hall-of-supporters.md new file mode 100644 index 0000000..ca496a2 --- /dev/null +++ b/docs/hall-of-supporters.md @@ -0,0 +1,50 @@ +# Hall of Supporters / Hall of Roles + +The Hall of Supporters is a feature for the Zander Hub plugin that automatically populates armor stand statues and signs with eligible players based on their LuckPerms groups. + +## Quick Start Guide (First Time Setup) + +Follow these steps to get your Hall of Supporters running in minutes: + +1. **Create a Section**: A section represents a group of players sharing a rank. + * `/hall section create staff` + * `/hall section setlabel staff Staff` (This label appears on the signs below the statues) + * `/hall section addgroup staff admin` (Link the section to a LuckPerms group) +2. **Create Slots**: A slot is a physical location where a statue will stand. + * Find the exact spot where you want a statue. **Statues will face the direction you are looking when you run the command.** + * **Place a sign block** exactly one block below your feet. The plugin uses this sign to display the player's name and rank label. **Note: The sign block is meant to stay there as a permanent part of the pedestal.** + * Run: `/hall slot create staff staff-1` +3. **Refresh**: Trigger the auto-population system. + * `/hall refresh` +4. **Verification**: Check your work. + * `/hall slot list` + +## Features +- **Auto-population**: Players are automatically assigned to available slots based on their LuckPerms groups and priority. +- **Customizable Statues**: Players can customize their own statue's armor and items via a GUI. +- **Forced Identity**: Statues always display the player's real skin head and username. +- **Particle Effects**: Supporters can choose from various particle effects to surround their statue. +- **Plugin-controlled Signs**: Signs below statues automatically display the player's name and their role label. +- **Manual Overrides**: Admins can manually assign or lock slots for specific players. +- **YAML Persistence**: All data is stored in simple YAML files for easy maintenance. + +## Detailed Administration + +### Sections +Sections are the "containers" for ranks. +* **Priority**: If a player has multiple ranks, they appear in the section with the highest priority number. Use `/hall section setpriority `. +* **Sign Labels**: Set what appears on the sign below the statue using `/hall section setlabel `. + +### Slots +Slots are physical locations. +* **Rotation**: When creating a slot with `/hall slot create`, the statue's facing direction is set to your current yaw. +* **Signs**: The sign block below the slot is a mandatory part of the pedestal. The plugin will take control of its text. +* **Dynamic Sorting**: By default, slots are filled automatically based on player priority (usually LuckPerms weight). If a new, higher-priority player joins the server or gains a rank, they may "bump" an existing player out of their slot or move them to a different one. +* **Slot Locking**: If you want a specific player to stay in a specific slot permanently, you can use `/hall lock `. This prevents the auto-assignment system from moving or replacing the player in that slot. + +## Documentation Links +- [User Customization Guide](statue-customization-guide.md) +- [Admin Command Reference](hall-of-supporters.md#commands) + +## Administration Commands +... (see [hall-of-supporters.md](hall-of-supporters.md) for full reference) diff --git a/docs/statue-customization-guide.md b/docs/statue-customization-guide.md new file mode 100644 index 0000000..3dcb9ac --- /dev/null +++ b/docs/statue-customization-guide.md @@ -0,0 +1,37 @@ +# Statue Customization Guide + +As a supporter with an assigned statue in the Hall of Supporters, you have the ability to customize its appearance! + +## How to Customize +There are two ways to open the customization menu: + +1. **Command**: Use `/mystatue` while in the hub. +2. **Interaction**: **Left Shift + Right Click** your own statue with an empty hand. + +## Customization Options + +### 1. Armor +You can cycle through various armor pieces for your statue: +* **Chestplate**: Cycle through Leather, Chainmail, Iron, Gold, Diamond, and Netherite. +* **Leggings**: Cycle through different leg protection. +* **Boots**: Choose your statue's footwear. + +### 2. Held Items +* **Main Hand**: Cycle through weapons like swords, axes, bows, and tridents. +* **Off Hand**: If you have the required rank, you can equip items like shields, totems, or torches. + +### 3. Poses +Change the stance of your statue by cycling through presets: +* **Presets**: Default, Zombie (arms forward), Running, and Dancing. + +### 4. Particle Effects +Add some flair to your statue with particle effects: +* Options include Hearts, Happy Villager, Flame, Witch, and more. + +### 5. Reset +If you want to start over, use the **Reset Statue** option (Barrier icon) in the menu to return your statue to its default state. + +## Rules & Limitations +* **Identity**: Your statue will always use your current Minecraft skin head and your username. This cannot be changed. +* **Location**: You cannot move your statue; it is fixed to its assigned slot. +* **Signs**: The sign below your statue is controlled by the plugin and displays your name and rank. diff --git a/zander-hub/pom.xml b/zander-hub/pom.xml index fb5049d..7539fd8 100644 --- a/zander-hub/pom.xml +++ b/zander-hub/pom.xml @@ -10,16 +10,6 @@ zander-hub - - - - - src/main/resources - true - - - - papermc @@ -62,6 +52,12 @@ 5.3.0 provided + + net.luckperms + api + 5.4 + provided + diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/ZanderHubMain.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/ZanderHubMain.java index b85dc30..83e296c 100644 --- a/zander-hub/src/main/java/org/modularsoft/zander/hub/ZanderHubMain.java +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/ZanderHubMain.java @@ -12,6 +12,11 @@ import org.modularsoft.zander.hub.events.HubPlayerLeave; import org.modularsoft.zander.hub.events.HubPlayerVoid; import org.modularsoft.zander.hub.gui.HubCompassItem; +import org.modularsoft.zander.hub.hall.commands.HallAdminCommand; +import org.modularsoft.zander.hub.hall.commands.MyStatueCommand; +import org.modularsoft.zander.hub.hall.events.HallListeners; +import org.modularsoft.zander.hub.hall.events.HallProtection; +import org.modularsoft.zander.hub.hall.manager.HallManager; import org.modularsoft.zander.hub.protection.HubCreatureSpawnProtection; import org.modularsoft.zander.hub.protection.HubInteractionProtection; import org.modularsoft.zander.hub.protection.HubProtection; @@ -19,6 +24,7 @@ public class ZanderHubMain extends JavaPlugin { public static ZanderHubMain plugin; + private HallManager hallManager; public void onEnable() { plugin = this; @@ -59,12 +65,28 @@ public void onEnable() { // Item Event Registry pluginmanager.registerEvents(new HubCompassItem(), this); + // Hall of Supporters + this.hallManager = new HallManager(this); + this.hallManager.init(); + pluginmanager.registerEvents(this.hallManager.getCustomizationGui(), this); + pluginmanager.registerEvents(new HallListeners(this), this); + pluginmanager.registerEvents(new HallProtection(this), this); + // Command Registry this.getCommand("fly").setExecutor(new fly()); + this.getCommand("hall").setExecutor(new HallAdminCommand(this)); + this.getCommand("mystatue").setExecutor(new MyStatueCommand(this)); + } + + public HallManager getHallManager() { + return hallManager; } // load defaults from the embedded resource & don't override existing values @Override public void onDisable() { + if (hallManager != null) { + hallManager.stop(); + } } } diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/HallAdminCommand.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/HallAdminCommand.java new file mode 100644 index 0000000..527c006 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/HallAdminCommand.java @@ -0,0 +1,301 @@ +package org.modularsoft.zander.hub.hall.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.models.HallSection; +import org.modularsoft.zander.hub.hall.models.HallSlot; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.UUID; +import java.util.stream.Collectors; + +public class HallAdminCommand implements CommandExecutor { + private final ZanderHubMain plugin; + private final HallManager hallManager; + + public HallAdminCommand(ZanderHubMain plugin) { + this.plugin = plugin; + this.hallManager = plugin.getHallManager(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sendHelp(sender); + return true; + } + + switch (args[0].toLowerCase()) { + case "reload": + if (!sender.hasPermission("hall.admin")) return noPerm(sender); + hallManager.getHallConfig().load(); + sender.sendMessage(Component.text("Hall config reloaded.", NamedTextColor.GREEN)); + break; + case "refresh": + if (!sender.hasPermission("hall.refresh")) return noPerm(sender); + hallManager.forceRefresh(); + sender.sendMessage(Component.text("Hall refresh triggered.", NamedTextColor.GREEN)); + break; + case "section": + if (!sender.hasPermission("hall.section.manage")) return noPerm(sender); + handleSection(sender, args); + break; + case "slot": + if (!sender.hasPermission("hall.slot.manage")) return noPerm(sender); + handleSlot(sender, args); + break; + case "assign": + if (!sender.hasPermission("hall.assign.manage")) return noPerm(sender); + handleAssign(sender, args); + break; + case "unassign": + if (!sender.hasPermission("hall.assign.manage")) return noPerm(sender); + handleUnassign(sender, args); + break; + case "lock": + if (!sender.hasPermission("hall.slot.manage")) return noPerm(sender); + handleLock(sender, args, true); + break; + case "unlock": + if (!sender.hasPermission("hall.slot.manage")) return noPerm(sender); + handleLock(sender, args, false); + break; + case "preview": + if (!sender.hasPermission("hall.preview")) return noPerm(sender); + handlePreview(sender, args); + break; + default: + sendHelp(sender); + break; + } + + return true; + } + + private boolean noPerm(CommandSender sender) { + sender.sendMessage(Component.text("No permission.", NamedTextColor.RED)); + return true; + } + + private void handleSection(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(Component.text("Usage: /hall section [id]", NamedTextColor.RED)); + return; + } + + String sub = args[1].toLowerCase(); + + if (sub.equals("list")) { + sender.sendMessage(Component.text("Sections:", NamedTextColor.GOLD)); + for (HallSection sec : hallManager.getSectionManager().getSections().values()) { + sender.sendMessage(Component.text("- " + sec.getId() + " (" + sec.getDisplayName() + ") P:" + sec.getPriority(), NamedTextColor.YELLOW)); + } + return; + } + + if (args.length < 3) { + sender.sendMessage(Component.text("Provide section id.", NamedTextColor.RED)); + return; + } + + String id = args[2]; + HallSection sec = hallManager.getSectionManager().getSections().get(id); + + if (sub.equals("create")) { + if (sec != null) { + sender.sendMessage(Component.text("Section already exists.", NamedTextColor.RED)); + return; + } + sec = new HallSection(id, id, id, new ArrayList<>(), 0, "weight_desc", true, true); + hallManager.getSectionManager().addSection(sec); + sender.sendMessage(Component.text("Section created: " + id, NamedTextColor.GREEN)); + return; + } + + if (sec == null) { + sender.sendMessage(Component.text("Section not found: " + id, NamedTextColor.RED)); + return; + } + + switch (sub) { + case "setlabel": + if (args.length < 4) { sender.sendMessage("Provide label."); return; } + StringBuilder labelBuilder = new StringBuilder(); + for (int i = 3; i < args.length; i++) { + labelBuilder.append(args[i]).append(i == args.length - 1 ? "" : " "); + } + sec.setSignLabel(labelBuilder.toString()); + hallManager.getSectionManager().save(); + sender.sendMessage(Component.text("Label set for " + id + " to: " + sec.getSignLabel(), NamedTextColor.GREEN)); + break; + case "setpriority": + if (args.length < 4) { sender.sendMessage("Provide priority."); return; } + sec.setPriority(Integer.parseInt(args[3])); + hallManager.getSectionManager().save(); + sender.sendMessage(Component.text("Priority set for " + id, NamedTextColor.GREEN)); + break; + case "addgroup": + if (args.length < 4) { sender.sendMessage("Provide group."); return; } + sec.getGroups().add(args[3]); + hallManager.getSectionManager().save(); + sender.sendMessage(Component.text("Group added to " + id, NamedTextColor.GREEN)); + break; + case "info": + sender.sendMessage(Component.text("Section Info: " + id, NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Label: " + sec.getSignLabel(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Priority: " + sec.getPriority(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Groups: " + String.join(", ", sec.getGroups()), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Mode: " + sec.getSortMode(), NamedTextColor.YELLOW)); + break; + } + } + + private void handleSlot(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(Component.text("Usage: /hall slot [args]", NamedTextColor.RED)); + return; + } + + String sub = args[1].toLowerCase(); + + if (sub.equals("list")) { + sender.sendMessage(Component.text("Slots:", NamedTextColor.GOLD)); + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + HallAssignment a = hallManager.getAssignmentForSlot(slot.getId()); + String pName = "Empty"; + if (a != null) { + OfflinePlayer op = Bukkit.getOfflinePlayer(a.getPlayerUuid()); + pName = op.getName() != null ? op.getName() : "Unknown"; + } + sender.sendMessage(Component.text("- " + slot.getId() + " [" + slot.getSectionId() + "] -> " + pName + (slot.isLocked() ? " (LOCKED)" : ""), NamedTextColor.YELLOW)); + } + return; + } + + if (sub.equals("create")) { + if (!(sender instanceof Player player)) { sender.sendMessage("Players only."); return; } + if (args.length < 3) { sender.sendMessage("Provide section id."); return; } + String sectionId = args[2]; + String id = (args.length > 3) ? args[3] : UUID.randomUUID().toString().substring(0, 8); + + if (hallManager.getSlotManager().getSlots().containsKey(id)) { + sender.sendMessage(Component.text("Slot ID already exists.", NamedTextColor.RED)); + return; + } + + Location loc = player.getLocation(); + Location signLoc = loc.clone().add(0, -1, 0); + HallSlot slot = new HallSlot(id, sectionId, loc, signLoc, false, 0); + hallManager.getSlotManager().addSlot(slot); + sender.sendMessage(Component.text("Slot created: " + id + " for section " + sectionId, NamedTextColor.GREEN)); + } else if (sub.equals("remove")) { + if (args.length < 3) { sender.sendMessage("Provide slot id."); return; } + hallManager.getSlotManager().removeSlot(args[2]); + sender.sendMessage(Component.text("Slot removed: " + args[2], NamedTextColor.GREEN)); + } else if (sub.equals("info")) { + if (args.length < 3) { sender.sendMessage("Provide slot id."); return; } + HallSlot slot = hallManager.getSlotManager().getSlots().get(args[2]); + if (slot == null) { sender.sendMessage("Slot not found."); return; } + sender.sendMessage(Component.text("Slot Info: " + slot.getId(), NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Section: " + slot.getSectionId(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Location: " + slot.getLocation().getWorld().getName() + " " + slot.getLocation().getBlockX() + "," + slot.getLocation().getBlockY() + "," + slot.getLocation().getBlockZ(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Locked: " + slot.isLocked(), NamedTextColor.YELLOW)); + HallAssignment a = hallManager.getAssignmentForSlot(slot.getId()); + if (a != null) { + OfflinePlayer op = Bukkit.getOfflinePlayer(a.getPlayerUuid()); + sender.sendMessage(Component.text("Assigned: " + (op.getName() != null ? op.getName() : "Unknown") + " (" + a.getType() + ")", NamedTextColor.YELLOW)); + } + } + } + + private void handleAssign(CommandSender sender, String[] args) { + if (args.length < 3) { + sender.sendMessage(Component.text("Usage: /hall assign ", NamedTextColor.RED)); + return; + } + + String playerName = args[1]; + String slotId = args[2]; + OfflinePlayer op = Bukkit.getOfflinePlayer(playerName); + + HallSlot slot = hallManager.getSlotManager().getSlots().get(slotId); + if (slot == null) { + sender.sendMessage(Component.text("Slot not found.", NamedTextColor.RED)); + return; + } + + HallAssignment assignment = new HallAssignment(slotId, op.getUniqueId(), slot.getSectionId(), HallAssignment.AssignmentType.MANUAL, System.currentTimeMillis()); + hallManager.getAssignments().removeIf(a -> a.getSlotId().equals(slotId)); + hallManager.getAssignments().add(assignment); + hallManager.saveAssignments(); + hallManager.getRenderService().renderSlot(slot); + sender.sendMessage(Component.text("Assigned " + playerName + " to slot " + slotId, NamedTextColor.GREEN)); + } + + private void handleUnassign(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(Component.text("Usage: /hall unassign ", NamedTextColor.RED)); + return; + } + String slotId = args[1]; + hallManager.getAssignments().removeIf(a -> a.getSlotId().equals(slotId)); + hallManager.saveAssignments(); + + HallSlot slot = hallManager.getSlotManager().getSlots().get(slotId); + if (slot != null) hallManager.getRenderService().renderSlot(slot); + sender.sendMessage(Component.text("Unassigned slot " + slotId, NamedTextColor.GREEN)); + } + + private void handleLock(CommandSender sender, String[] args, boolean lock) { + if (args.length < 2) { + sender.sendMessage(Component.text("Usage: /hall " + (lock ? "lock" : "unlock") + " ", NamedTextColor.RED)); + return; + } + String slotId = args[1]; + HallSlot slot = hallManager.getSlotManager().getSlots().get(slotId); + if (slot == null) { + sender.sendMessage(Component.text("Slot not found.", NamedTextColor.RED)); + return; + } + slot.setLocked(lock); + hallManager.getSlotManager().save(); + sender.sendMessage(Component.text("Slot " + slotId + (lock ? " locked." : " unlocked."), NamedTextColor.GREEN)); + } + + private void handlePreview(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) return; + if (args.length < 2) { sender.sendMessage("Provide player."); return; } + OfflinePlayer op = Bukkit.getOfflinePlayer(args[1]); + + sender.sendMessage(Component.text("Previewing " + op.getName() + " (not fully implemented, re-render triggered for their slots)", NamedTextColor.YELLOW)); + HallAssignment assignment = hallManager.getAssignmentForPlayer(op.getUniqueId()); + if (assignment != null) { + HallSlot slot = hallManager.getSlotManager().getSlots().get(assignment.getSlotId()); + if (slot != null) hallManager.getRenderService().renderSlot(slot); + } + } + + private void sendHelp(CommandSender sender) { + sender.sendMessage(Component.text("Hall Admin Commands:", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("/hall reload", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall refresh", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall section ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall slot ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall assign ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall unassign ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall lock|unlock ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/hall preview ", NamedTextColor.YELLOW)); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/MyStatueCommand.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/MyStatueCommand.java new file mode 100644 index 0000000..5af21d9 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/commands/MyStatueCommand.java @@ -0,0 +1,34 @@ +package org.modularsoft.zander.hub.hall.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.jetbrains.annotations.NotNull; + +public class MyStatueCommand implements CommandExecutor { + private final ZanderHubMain plugin; + + public MyStatueCommand(ZanderHubMain plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Players only."); + return true; + } + + if (!player.hasPermission("hall.customize")) { + sender.sendMessage(Component.text("You do not have permission to customize your statue.", NamedTextColor.RED)); + return true; + } + + plugin.getHallManager().getCustomizationGui().open(player); + return true; + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallListeners.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallListeners.java new file mode 100644 index 0000000..9bb921f --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallListeners.java @@ -0,0 +1,40 @@ +package org.modularsoft.zander.hub.hall.events; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; + +public class HallListeners implements Listener { + private final ZanderHubMain plugin; + private final HallManager hallManager; + + public HallListeners(ZanderHubMain plugin) { + this.plugin = plugin; + this.hallManager = plugin.getHallManager(); + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + // Potential refresh on join if they are eligible for a section + hallManager.getAssignmentService().refreshAssignments(); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + // Refresh on quit to potentially fill the slot with someone else if they are no longer "available" + // depending on how we define "eligible" pool. + hallManager.getAssignmentService().refreshAssignments(); + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + // Re-render statues in the loaded chunk + hallManager.getSlotManager().getSlots().values().stream() + .filter(slot -> slot.getLocation().getChunk().equals(event.getChunk())) + .forEach(slot -> hallManager.getRenderService().renderSlot(slot)); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallProtection.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallProtection.java new file mode 100644 index 0000000..eb1cb45 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/events/HallProtection.java @@ -0,0 +1,73 @@ +package org.modularsoft.zander.hub.hall.events; + +import org.bukkit.Material; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.models.HallSlot; + +public class HallProtection implements Listener { + private final ZanderHubMain plugin; + private final HallManager hallManager; + + public HallProtection(ZanderHubMain plugin) { + this.plugin = plugin; + this.hallManager = plugin.getHallManager(); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onArmorStandManipulate(PlayerArmorStandManipulateEvent event) { + if (isHallArmorStand(event.getRightClicked())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onDamage(EntityDamageEvent event) { + if (event.getEntity() instanceof ArmorStand && isHallArmorStand((ArmorStand) event.getEntity())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onInteract(PlayerInteractAtEntityEvent event) { + if (!(event.getRightClicked() instanceof ArmorStand as)) return; + if (!isHallArmorStand(as)) return; + + event.setCancelled(true); + + Player player = event.getPlayer(); + if (player.isSneaking() && player.getInventory().getItemInMainHand().getType() == Material.AIR) { + HallAssignment assignment = hallManager.getAssignmentForPlayer(player.getUniqueId()); + if (assignment != null) { + // Check if this is their statue + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + if (slot.getId().equals(assignment.getSlotId())) { + if (slot.getLocation().distanceSquared(as.getLocation()) < 0.25) { + hallManager.getCustomizationGui().open(player); + return; + } + } + } + } + } + } + + private boolean isHallArmorStand(org.bukkit.entity.Entity entity) { + if (!(entity instanceof ArmorStand)) return false; + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + if (slot.getLocation().distanceSquared(entity.getLocation()) < 0.25) { + return true; + } + } + return false; + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/gui/CustomizationGui.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/gui/CustomizationGui.java new file mode 100644 index 0000000..1d0ec75 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/gui/CustomizationGui.java @@ -0,0 +1,216 @@ +package org.modularsoft.zander.hub.hall.gui; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.models.HallCustomization; + +import java.util.Arrays; +import java.util.List; + +public class CustomizationGui implements Listener { + private final ZanderHubMain plugin; + private final String title = "Statue Customization"; + + private final List chestplates = Arrays.asList( + Material.LEATHER_CHESTPLATE, Material.CHAINMAIL_CHESTPLATE, Material.IRON_CHESTPLATE, + Material.GOLDEN_CHESTPLATE, Material.DIAMOND_CHESTPLATE, Material.NETHERITE_CHESTPLATE, Material.AIR + ); + + private final List leggings = Arrays.asList( + Material.LEATHER_LEGGINGS, Material.CHAINMAIL_LEGGINGS, Material.IRON_LEGGINGS, + Material.GOLDEN_LEGGINGS, Material.DIAMOND_LEGGINGS, Material.NETHERITE_LEGGINGS, Material.AIR + ); + + private final List boots = Arrays.asList( + Material.LEATHER_BOOTS, Material.CHAINMAIL_BOOTS, Material.IRON_BOOTS, + Material.GOLDEN_BOOTS, Material.DIAMOND_BOOTS, Material.NETHERITE_BOOTS, Material.AIR + ); + + private final List weapons = Arrays.asList( + Material.IRON_SWORD, Material.GOLDEN_SWORD, Material.DIAMOND_SWORD, Material.NETHERITE_SWORD, + Material.IRON_AXE, Material.GOLDEN_AXE, Material.DIAMOND_AXE, Material.NETHERITE_AXE, + Material.BOW, Material.CROSSBOW, Material.TRIDENT, Material.AIR + ); + + private final List offhandItems = Arrays.asList( + Material.SHIELD, Material.TOTEM_OF_UNDYING, Material.FIREWORK_ROCKET, Material.TORCH, Material.AIR + ); + + private final List posePresets = Arrays.asList("DEFAULT", "ZOMBIE", "RUNNING", "DANCING"); + + private final List particlePresets = Arrays.asList("NONE", "HEART", "HAPPY_VILLAGER", "FLAME", "SOUL_FIRE_FLAME", "WITCH", "TOTEM_OF_UNDYING"); + + public CustomizationGui(ZanderHubMain plugin) { + this.plugin = plugin; + } + + public void open(Player player) { + if (plugin.getHallManager().getAssignmentForPlayer(player.getUniqueId()) == null) { + player.sendMessage(Component.text("You don't have an assigned statue to customize!", NamedTextColor.RED)); + return; + } + + Inventory inv = Bukkit.createInventory(null, 27, Component.text(title)); + + // Armor Slots + inv.setItem(10, createItem(Material.DIAMOND_CHESTPLATE, "Chestplate")); + inv.setItem(11, createItem(Material.DIAMOND_LEGGINGS, "Leggings")); + inv.setItem(12, createItem(Material.DIAMOND_BOOTS, "Boots")); + + // Items + inv.setItem(14, createItem(Material.IRON_SWORD, "Main Hand")); + inv.setItem(15, createItem(Material.SHIELD, "Off Hand")); + + // Effects + inv.setItem(16, createPoseItem()); + inv.setItem(17, createItem(Material.BLAZE_POWDER, "Cycle Particles")); + + // Reset + inv.setItem(22, createItem(Material.BARRIER, "Reset Statue")); + + player.openInventory(inv); + } + + private ItemStack createItem(Material m, String name) { + ItemStack item = new ItemStack(m == Material.AIR ? Material.BARRIER : m); + ItemMeta meta = item.getItemMeta(); + meta.displayName(Component.text(name, NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)); + item.setItemMeta(meta); + return item; + } + + private ItemStack createPoseItem() { + ItemStack item = new ItemStack(Material.ARMOR_STAND); + ItemMeta meta = item.getItemMeta(); + meta.displayName(Component.text("Cycle Pose", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)); + meta.lore(Arrays.asList( + Component.text("Use ", NamedTextColor.GRAY).append(Component.text("Left Shift + Right Button", NamedTextColor.LIGHT_PURPLE)).append(Component.text(" with", NamedTextColor.GRAY)).decoration(TextDecoration.ITALIC, false), + Component.text("an empty hand to open", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false), + Component.text("configuration screen.", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false) + )); + item.setItemMeta(meta); + return item; + } + + @EventHandler + public void onClick(InventoryClickEvent event) { + if (!event.getView().title().equals(Component.text(title))) return; + event.setCancelled(true); + + Player player = (Player) event.getWhoClicked(); + ItemStack clicked = event.getCurrentItem(); + if (clicked == null) return; + + HallCustomization custom = plugin.getHallManager().getCustomizationService().getCustomization(player.getUniqueId()); + if (custom == null) { + custom = new HallCustomization(player.getUniqueId()); + } + + switch (event.getSlot()) { + case 10: cycleArmor(player, custom, "chest"); break; + case 11: cycleArmor(player, custom, "legs"); break; + case 12: cycleArmor(player, custom, "boots"); break; + case 14: cycleItem(player, custom, true); break; + case 15: + if (player.hasPermission("hall.customize.offhand")) { + cycleItem(player, custom, false); + } else { + player.sendMessage(Component.text("No permission for offhand customization.", NamedTextColor.RED)); + } + break; + case 16: cyclePose(player, custom); break; + case 17: cycleParticles(player, custom); break; + case 22: + plugin.getHallManager().getCustomizationService().saveCustomization(new HallCustomization(player.getUniqueId())); + player.sendMessage(Component.text("Statue reset!", NamedTextColor.GREEN)); + player.closeInventory(); + return; + default: return; + } + + plugin.getHallManager().getCustomizationService().saveCustomization(custom); + } + + private void cycleArmor(Player player, HallCustomization custom, String type) { + List pool = switch (type) { + case "chest" -> chestplates; + case "legs" -> leggings; + case "boots" -> boots; + default -> Arrays.asList(Material.AIR); + }; + + Material current = switch (type) { + case "chest" -> custom.getChestplate(); + case "legs" -> custom.getLeggings(); + case "boots" -> custom.getBoots(); + default -> null; + }; + + int index = (current == null) ? pool.indexOf(Material.AIR) : pool.indexOf(current); + index = (index + 1) % pool.size(); + Material next = pool.get(index); + if (next == Material.AIR) next = null; + + switch (type) { + case "chest" -> custom.setChestplate(next); + case "legs" -> custom.setLeggings(next); + case "boots" -> custom.setBoots(next); + } + player.sendMessage(Component.text("Updated " + type + "!", NamedTextColor.GREEN)); + } + + private void cycleItem(Player player, HallCustomization custom, boolean mainHand) { + List pool = mainHand ? weapons : offhandItems; + Material current = mainHand ? custom.getMainHand() : custom.getOffHand(); + + int index = (current == null) ? pool.indexOf(Material.AIR) : pool.indexOf(current); + index = (index + 1) % pool.size(); + Material next = pool.get(index); + if (next == Material.AIR) next = null; + + if (mainHand) custom.setMainHand(next); + else custom.setOffHand(next); + + player.sendMessage(Component.text("Updated " + (mainHand ? "main hand" : "off hand") + "!", NamedTextColor.GREEN)); + } + + private void cyclePose(Player player, HallCustomization custom) { + String current = custom.getPosePreset(); + int index = (current == null) ? 0 : posePresets.indexOf(current); + index = (index + 1) % posePresets.size(); + String next = posePresets.get(index); + + custom.setPosePreset(next); + custom.setBodyPose(null); + custom.setHeadPose(null); + custom.setLeftArmPose(null); + custom.setRightArmPose(null); + custom.setLeftLegPose(null); + custom.setRightLegPose(null); + + player.sendMessage(Component.text("Updated pose to " + next + "!", NamedTextColor.GREEN)); + } + + private void cycleParticles(Player player, HallCustomization custom) { + String current = custom.getParticleEffect(); + if (current == null) current = "NONE"; + + int index = particlePresets.indexOf(current); + index = (index + 1) % particlePresets.size(); + String next = particlePresets.get(index); + + custom.setParticleEffect(next.equals("NONE") ? null : next); + player.sendMessage(Component.text("Updated particles to " + next + "!", NamedTextColor.GREEN)); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallConfig.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallConfig.java new file mode 100644 index 0000000..531b19e --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallConfig.java @@ -0,0 +1,56 @@ +package org.modularsoft.zander.hub.hall.manager; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.modularsoft.zander.hub.ZanderHubMain; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class HallConfig { + private final ZanderHubMain plugin; + private boolean enabled; + private int refreshIntervalMinutes; + private List allowedMaterials; + private boolean debug; + + public HallConfig(ZanderHubMain plugin) { + this.plugin = plugin; + load(); + } + + public void load() { + FileConfiguration config = plugin.getConfig(); + this.enabled = config.getBoolean("hall.enabled", true); + this.refreshIntervalMinutes = config.getInt("hall.refresh-interval", 30); + this.debug = config.getBoolean("hall.debug", false); + + List materials = config.getStringList("hall.allowed-materials"); + if (materials.isEmpty()) { + this.allowedMaterials = new ArrayList<>(); + // Add some defaults if empty + this.allowedMaterials.add(Material.IRON_CHESTPLATE); + this.allowedMaterials.add(Material.GOLDEN_CHESTPLATE); + this.allowedMaterials.add(Material.DIAMOND_CHESTPLATE); + this.allowedMaterials.add(Material.NETHERITE_CHESTPLATE); + this.allowedMaterials.add(Material.LEATHER_CHESTPLATE); + } else { + this.allowedMaterials = materials.stream() + .map(s -> { + try { + return Material.valueOf(s.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + }) + .filter(m -> m != null) + .collect(Collectors.toList()); + } + } + + public boolean isEnabled() { return enabled; } + public int getRefreshIntervalMinutes() { return refreshIntervalMinutes; } + public List getAllowedMaterials() { return allowedMaterials; } + public boolean isDebug() { return debug; } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallManager.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallManager.java new file mode 100644 index 0000000..80973da --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/HallManager.java @@ -0,0 +1,100 @@ +package org.modularsoft.zander.hub.hall.manager; + +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.gui.CustomizationGui; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.persistence.HallPersistence; +import org.modularsoft.zander.hub.hall.services.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class HallManager { + private final ZanderHubMain plugin; + private final HallConfig hallConfig; + private final HallPersistence persistence; + private final SectionManager sectionManager; + private final SlotManager slotManager; + private final LuckPermsService luckPermsService; + private final AssignmentService assignmentService; + private final RenderService renderService; + private final CustomizationService customizationService; + private final CustomizationGui customizationGui; + + private List assignments; + private BukkitTask refreshTask; + + public HallManager(ZanderHubMain plugin) { + this.plugin = plugin; + this.hallConfig = new HallConfig(plugin); + this.persistence = new HallPersistence(plugin); + this.sectionManager = new SectionManager(persistence); + this.slotManager = new SlotManager(persistence); + this.luckPermsService = new LuckPermsService(plugin); + this.customizationService = new CustomizationService(plugin, this); + this.customizationGui = new CustomizationGui(plugin); + this.renderService = new RenderService(plugin, this); + this.assignmentService = new AssignmentService(plugin, this, luckPermsService); + + this.assignments = persistence.loadAssignments(); + } + + public void init() { + if (!hallConfig.isEnabled()) return; + + renderService.renderAll(); + startRefreshTask(); + } + + public void stop() { + if (refreshTask != null) { + refreshTask.cancel(); + } + } + + private void startRefreshTask() { + long interval = hallConfig.getRefreshIntervalMinutes() * 60 * 20L; + if (interval <= 0) return; + + refreshTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + assignmentService.refreshAssignments(); + }, interval, interval); + } + + public void forceRefresh() { + assignmentService.refreshAssignments(); + } + + public HallConfig getHallConfig() { return hallConfig; } + public SectionManager getSectionManager() { return sectionManager; } + public SlotManager getSlotManager() { return slotManager; } + public LuckPermsService getLuckPermsService() { return luckPermsService; } + public AssignmentService getAssignmentService() { return assignmentService; } + public RenderService getRenderService() { return renderService; } + public CustomizationService getCustomizationService() { return customizationService; } + public CustomizationGui getCustomizationGui() { return customizationGui; } + + public List getAssignments() { return assignments; } + public void setAssignments(List assignments) { this.assignments = assignments; } + + public HallAssignment getAssignmentForSlot(String slotId) { + return assignments.stream() + .filter(a -> a.getSlotId().equals(slotId)) + .findFirst() + .orElse(null); + } + + public HallAssignment getAssignmentForPlayer(UUID uuid) { + return assignments.stream() + .filter(a -> a.getPlayerUuid().equals(uuid)) + .findFirst() + .orElse(null); + } + + public void saveAssignments() { + persistence.saveAssignments(assignments); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SectionManager.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SectionManager.java new file mode 100644 index 0000000..db385dd --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SectionManager.java @@ -0,0 +1,34 @@ +package org.modularsoft.zander.hub.hall.manager; + +import org.modularsoft.zander.hub.hall.models.HallSection; +import org.modularsoft.zander.hub.hall.persistence.HallPersistence; + +import java.util.Map; + +public class SectionManager { + private final HallPersistence persistence; + private Map sections; + + public SectionManager(HallPersistence persistence) { + this.persistence = persistence; + this.sections = persistence.loadSections(); + } + + public Map getSections() { + return sections; + } + + public void addSection(HallSection section) { + sections.put(section.getId(), section); + persistence.saveSections(sections); + } + + public void removeSection(String id) { + sections.remove(id); + persistence.saveSections(sections); + } + + public void save() { + persistence.saveSections(sections); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SlotManager.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SlotManager.java new file mode 100644 index 0000000..c38d3bc --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/manager/SlotManager.java @@ -0,0 +1,34 @@ +package org.modularsoft.zander.hub.hall.manager; + +import org.modularsoft.zander.hub.hall.models.HallSlot; +import org.modularsoft.zander.hub.hall.persistence.HallPersistence; + +import java.util.Map; + +public class SlotManager { + private final HallPersistence persistence; + private Map slots; + + public SlotManager(HallPersistence persistence) { + this.persistence = persistence; + this.slots = persistence.loadSlots(); + } + + public Map getSlots() { + return slots; + } + + public void addSlot(HallSlot slot) { + slots.put(slot.getId(), slot); + persistence.saveSlots(slots); + } + + public void removeSlot(String id) { + slots.remove(id); + persistence.saveSlots(slots); + } + + public void save() { + persistence.saveSlots(slots); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallAssignment.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallAssignment.java new file mode 100644 index 0000000..ed27328 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallAssignment.java @@ -0,0 +1,29 @@ +package org.modularsoft.zander.hub.hall.models; + +import java.util.UUID; + +public class HallAssignment { + private String slotId; + private UUID playerUuid; + private String sectionId; + private AssignmentType type; + private long updatedAt; + + public enum AssignmentType { + AUTO, MANUAL + } + + public HallAssignment(String slotId, UUID playerUuid, String sectionId, AssignmentType type, long updatedAt) { + this.slotId = slotId; + this.playerUuid = playerUuid; + this.sectionId = sectionId; + this.type = type; + this.updatedAt = updatedAt; + } + + public String getSlotId() { return slotId; } + public UUID getPlayerUuid() { return playerUuid; } + public String getSectionId() { return sectionId; } + public AssignmentType getType() { return type; } + public long getUpdatedAt() { return updatedAt; } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallCustomization.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallCustomization.java new file mode 100644 index 0000000..8e7c956 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallCustomization.java @@ -0,0 +1,59 @@ +package org.modularsoft.zander.hub.hall.models; + +import org.bukkit.Material; +import org.bukkit.util.EulerAngle; +import java.util.UUID; + +public class HallCustomization { + private UUID playerUuid; + private Material chestplate; + private Material leggings; + private Material boots; + private Material mainHand; + private Material offHand; + private String posePreset; + private String particleEffect; + private EulerAngle bodyPose; + private EulerAngle headPose; + private EulerAngle leftArmPose; + private EulerAngle rightArmPose; + private EulerAngle leftLegPose; + private EulerAngle rightLegPose; + private long updatedAt; + + public HallCustomization(UUID playerUuid) { + this.playerUuid = playerUuid; + this.updatedAt = System.currentTimeMillis(); + } + + // Getters and Setters + public UUID getPlayerUuid() { return playerUuid; } + public Material getChestplate() { return chestplate; } + public void setChestplate(Material chestplate) { this.chestplate = chestplate; } + public Material getLeggings() { return leggings; } + public void setLeggings(Material leggings) { this.leggings = leggings; } + public Material getBoots() { return boots; } + public void setBoots(Material boots) { this.boots = boots; } + public Material getMainHand() { return mainHand; } + public void setMainHand(Material mainHand) { this.mainHand = mainHand; } + public Material getOffHand() { return offHand; } + public void setOffHand(Material offHand) { this.offHand = offHand; } + public String getPosePreset() { return posePreset; } + public void setPosePreset(String posePreset) { this.posePreset = posePreset; } + public String getParticleEffect() { return particleEffect; } + public void setParticleEffect(String particleEffect) { this.particleEffect = particleEffect; } + public EulerAngle getBodyPose() { return bodyPose; } + public void setBodyPose(EulerAngle bodyPose) { this.bodyPose = bodyPose; } + public EulerAngle getHeadPose() { return headPose; } + public void setHeadPose(EulerAngle headPose) { this.headPose = headPose; } + public EulerAngle getLeftArmPose() { return leftArmPose; } + public void setLeftArmPose(EulerAngle leftArmPose) { this.leftArmPose = leftArmPose; } + public EulerAngle getRightArmPose() { return rightArmPose; } + public void setRightArmPose(EulerAngle rightArmPose) { this.rightArmPose = rightArmPose; } + public EulerAngle getLeftLegPose() { return leftLegPose; } + public void setLeftLegPose(EulerAngle leftLegPose) { this.leftLegPose = leftLegPose; } + public EulerAngle getRightLegPose() { return rightLegPose; } + public void setRightLegPose(EulerAngle rightLegPose) { this.rightLegPose = rightLegPose; } + public long getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(long updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSection.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSection.java new file mode 100644 index 0000000..7702c9c --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSection.java @@ -0,0 +1,41 @@ +package org.modularsoft.zander.hub.hall.models; + +import java.util.List; + +public class HallSection { + private String id; + private String displayName; + private String signLabel; + private List groups; + private int priority; + private String sortMode; + private boolean allowCustomization; + private boolean enabled; + + public HallSection(String id, String displayName, String signLabel, List groups, int priority, String sortMode, boolean allowCustomization, boolean enabled) { + this.id = id; + this.displayName = displayName; + this.signLabel = signLabel; + this.groups = groups; + this.priority = priority; + this.sortMode = sortMode; + this.allowCustomization = allowCustomization; + this.enabled = enabled; + } + + public String getId() { return id; } + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getSignLabel() { return signLabel; } + public void setSignLabel(String signLabel) { this.signLabel = signLabel; } + public List getGroups() { return groups; } + public void setGroups(List groups) { this.groups = groups; } + public int getPriority() { return priority; } + public void setPriority(int priority) { this.priority = priority; } + public String getSortMode() { return sortMode; } + public void setSortMode(String sortMode) { this.sortMode = sortMode; } + public boolean isAllowCustomization() { return allowCustomization; } + public void setAllowCustomization(boolean allowCustomization) { this.allowCustomization = allowCustomization; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSlot.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSlot.java new file mode 100644 index 0000000..4f1a4ee --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/models/HallSlot.java @@ -0,0 +1,33 @@ +package org.modularsoft.zander.hub.hall.models; + +import org.bukkit.Location; + +public class HallSlot { + private String id; + private String sectionId; + private Location location; + private Location signLocation; + private boolean locked; + private int displayOrder; + + public HallSlot(String id, String sectionId, Location location, Location signLocation, boolean locked, int displayOrder) { + this.id = id; + this.sectionId = sectionId; + this.location = location; + this.signLocation = signLocation; + this.locked = locked; + this.displayOrder = displayOrder; + } + + public String getId() { return id; } + public String getSectionId() { return sectionId; } + public void setSectionId(String sectionId) { this.sectionId = sectionId; } + public Location getLocation() { return location; } + public void setLocation(Location location) { this.location = location; } + public Location getSignLocation() { return signLocation; } + public void setSignLocation(Location signLocation) { this.signLocation = signLocation; } + public boolean isLocked() { return locked; } + public void setLocked(boolean locked) { this.locked = locked; } + public int getDisplayOrder() { return displayOrder; } + public void setDisplayOrder(int displayOrder) { this.displayOrder = displayOrder; } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/CustomizationPersistence.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/CustomizationPersistence.java new file mode 100644 index 0000000..33edc91 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/CustomizationPersistence.java @@ -0,0 +1,95 @@ +package org.modularsoft.zander.hub.hall.persistence; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.util.EulerAngle; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.models.HallCustomization; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class CustomizationPersistence { + private final ZanderHubMain plugin; + private final File folder; + + public CustomizationPersistence(ZanderHubMain plugin) { + this.plugin = plugin; + this.folder = new File(plugin.getDataFolder(), "hall/customizations"); + if (!folder.exists()) folder.mkdirs(); + } + + public HallCustomization load(UUID playerUuid) { + File file = new File(folder, playerUuid.toString() + ".yml"); + if (!file.exists()) return null; + + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + HallCustomization c = new HallCustomization(playerUuid); + c.setChestplate(getMaterial(config.getString("chestplate"))); + c.setLeggings(getMaterial(config.getString("leggings"))); + c.setBoots(getMaterial(config.getString("boots"))); + c.setMainHand(getMaterial(config.getString("main-hand"))); + c.setOffHand(getMaterial(config.getString("off-hand"))); + c.setPosePreset(config.getString("pose-preset")); + c.setParticleEffect(config.getString("particle-effect")); + c.setBodyPose(getEulerAngle(config, "body-pose")); + c.setHeadPose(getEulerAngle(config, "head-pose")); + c.setLeftArmPose(getEulerAngle(config, "left-arm-pose")); + c.setRightArmPose(getEulerAngle(config, "right-arm-pose")); + c.setLeftLegPose(getEulerAngle(config, "left-leg-pose")); + c.setRightLegPose(getEulerAngle(config, "right-leg-pose")); + c.setUpdatedAt(config.getLong("updated-at")); + return c; + } + + public void save(HallCustomization c) { + File file = new File(folder, c.getPlayerUuid().toString() + ".yml"); + FileConfiguration config = new YamlConfiguration(); + config.set("chestplate", c.getChestplate() != null ? c.getChestplate().name() : null); + config.set("leggings", c.getLeggings() != null ? c.getLeggings().name() : null); + config.set("boots", c.getBoots() != null ? c.getBoots().name() : null); + config.set("main-hand", c.getMainHand() != null ? c.getMainHand().name() : null); + config.set("off-hand", c.getOffHand() != null ? c.getOffHand().name() : null); + config.set("pose-preset", c.getPosePreset()); + config.set("particle-effect", c.getParticleEffect()); + setEulerAngle(config, "body-pose", c.getBodyPose()); + setEulerAngle(config, "head-pose", c.getHeadPose()); + setEulerAngle(config, "left-arm-pose", c.getLeftArmPose()); + setEulerAngle(config, "right-arm-pose", c.getRightArmPose()); + setEulerAngle(config, "left-leg-pose", c.getLeftLegPose()); + setEulerAngle(config, "right-leg-pose", c.getRightLegPose()); + config.set("updated-at", c.getUpdatedAt()); + + try { + config.save(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Material getMaterial(String name) { + if (name == null) return null; + try { + return Material.valueOf(name); + } catch (IllegalArgumentException e) { + return null; + } + } + + private EulerAngle getEulerAngle(FileConfiguration config, String path) { + if (!config.contains(path)) return null; + double x = config.getDouble(path + ".x"); + double y = config.getDouble(path + ".y"); + double z = config.getDouble(path + ".z"); + return new EulerAngle(x, y, z); + } + + private void setEulerAngle(FileConfiguration config, String path, EulerAngle angle) { + if (angle == null) return; + config.set(path + ".x", angle.getX()); + config.set(path + ".y", angle.getY()); + config.set(path + ".z", angle.getZ()); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/HallPersistence.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/HallPersistence.java new file mode 100644 index 0000000..8ee3c53 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/persistence/HallPersistence.java @@ -0,0 +1,153 @@ +package org.modularsoft.zander.hub.hall.persistence; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.models.HallSection; +import org.modularsoft.zander.hub.hall.models.HallSlot; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class HallPersistence { + private final ZanderHubMain plugin; + private final File sectionsFile; + private final File slotsFile; + private final File assignmentsFile; + + public HallPersistence(ZanderHubMain plugin) { + this.plugin = plugin; + File hallFolder = new File(plugin.getDataFolder(), "hall"); + if (!hallFolder.exists()) hallFolder.mkdirs(); + + this.sectionsFile = new File(hallFolder, "sections.yml"); + this.slotsFile = new File(hallFolder, "slots.yml"); + this.assignmentsFile = new File(hallFolder, "assignments.yml"); + } + + public Map loadSections() { + Map sections = new HashMap<>(); + if (!sectionsFile.exists()) return sections; + + FileConfiguration config = YamlConfiguration.loadConfiguration(sectionsFile); + for (String id : config.getKeys(false)) { + ConfigurationSection sec = config.getConfigurationSection(id); + if (sec == null) continue; + + HallSection section = new HallSection( + id, + sec.getString("display-name"), + sec.getString("sign-label"), + sec.getStringList("groups"), + sec.getInt("priority"), + sec.getString("sort-mode", "weight_desc"), + sec.getBoolean("allow-customization", true), + sec.getBoolean("enabled", true) + ); + sections.put(id, section); + } + return sections; + } + + public void saveSections(Map sections) { + FileConfiguration config = new YamlConfiguration(); + for (HallSection sec : sections.values()) { + ConfigurationSection s = config.createSection(sec.getId()); + s.set("display-name", sec.getDisplayName()); + s.set("sign-label", sec.getSignLabel()); + s.set("groups", sec.getGroups()); + s.set("priority", sec.getPriority()); + s.set("sort-mode", sec.getSortMode()); + s.set("allow-customization", sec.isAllowCustomization()); + s.set("enabled", sec.isEnabled()); + } + try { + config.save(sectionsFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Map loadSlots() { + Map slots = new HashMap<>(); + if (!slotsFile.exists()) return slots; + + FileConfiguration config = YamlConfiguration.loadConfiguration(slotsFile); + for (String id : config.getKeys(false)) { + ConfigurationSection sec = config.getConfigurationSection(id); + if (sec == null) continue; + + HallSlot slot = new HallSlot( + id, + sec.getString("section-id"), + sec.getLocation("location"), + sec.getLocation("sign-location"), + sec.getBoolean("locked"), + sec.getInt("display-order") + ); + slots.put(id, slot); + } + return slots; + } + + public void saveSlots(Map slots) { + FileConfiguration config = new YamlConfiguration(); + for (HallSlot slot : slots.values()) { + ConfigurationSection s = config.createSection(slot.getId()); + s.set("section-id", slot.getSectionId()); + s.set("location", slot.getLocation()); + s.set("sign-location", slot.getSignLocation()); + s.set("locked", slot.isLocked()); + s.set("display-order", slot.getDisplayOrder()); + } + try { + config.save(slotsFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public List loadAssignments() { + List assignments = new ArrayList<>(); + if (!assignmentsFile.exists()) return assignments; + + FileConfiguration config = YamlConfiguration.loadConfiguration(assignmentsFile); + ConfigurationSection sec = config.getConfigurationSection("assignments"); + if (sec == null) return assignments; + + for (String slotId : sec.getKeys(false)) { + ConfigurationSection a = sec.getConfigurationSection(slotId); + HallAssignment assignment = new HallAssignment( + slotId, + UUID.fromString(a.getString("player-uuid")), + a.getString("section-id"), + HallAssignment.AssignmentType.valueOf(a.getString("type")), + a.getLong("updated-at") + ); + assignments.add(assignment); + } + return assignments; + } + + public void saveAssignments(List assignments) { + FileConfiguration config = new YamlConfiguration(); + ConfigurationSection sec = config.createSection("assignments"); + for (HallAssignment a : assignments) { + ConfigurationSection s = sec.createSection(a.getSlotId()); + s.set("player-uuid", a.getPlayerUuid().toString()); + s.set("section-id", a.getSectionId()); + s.set("type", a.getType().name()); + s.set("updated-at", a.getUpdatedAt()); + } + try { + config.save(assignmentsFile); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/AssignmentService.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/AssignmentService.java new file mode 100644 index 0000000..7629af6 --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/AssignmentService.java @@ -0,0 +1,146 @@ +package org.modularsoft.zander.hub.hall.services; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.models.HallSection; +import org.modularsoft.zander.hub.hall.models.HallSlot; + +import java.util.*; +import java.util.stream.Collectors; + +public class AssignmentService { + private final ZanderHubMain plugin; + private final HallManager hallManager; + private final LuckPermsService luckPermsService; + + public AssignmentService(ZanderHubMain plugin, HallManager hallManager, LuckPermsService luckPermsService) { + this.plugin = plugin; + this.hallManager = hallManager; + this.luckPermsService = luckPermsService; + } + + public void refreshAssignments() { + // Run refresh logic asynchronously to avoid blocking main thread + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Set pool = new HashSet<>(); + Bukkit.getOnlinePlayers().forEach(p -> pool.add(p.getUniqueId())); + hallManager.getAssignments().forEach(a -> pool.add(a.getPlayerUuid())); + + // Fetch global UUIDs from all configured sections + List allGroups = hallManager.getSectionManager().getSections().values().stream() + .flatMap(s -> s.getGroups().stream()) + .distinct() + .collect(Collectors.toList()); + + luckPermsService.getUuidsInGroups(allGroups).thenCompose(globalUuids -> { + pool.addAll(globalUuids); + // Preload users in LuckPerms asynchronously + return luckPermsService.preloadUsers(pool); + }).thenRun(() -> { + // Now run the assignment logic (back on main thread for safety with collections and rendering) + Bukkit.getScheduler().runTask(plugin, () -> { + performAssignment(pool); + }); + }); + }); + } + + private void performAssignment(Set pool) { + List sections = new ArrayList<>(hallManager.getSectionManager().getSections().values()); + sections.sort(Comparator.comparingInt(HallSection::getPriority).reversed()); + + Map> slotsBySection = hallManager.getSlotManager().getSlots().values().stream() + .collect(Collectors.groupingBy(HallSlot::getSectionId)); + + List newAssignments = new ArrayList<>(); + Set assignedPlayers = new HashSet<>(); + + // Handle locked slots and manual assignments first + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + if (slot.isLocked()) { + HallAssignment current = hallManager.getAssignmentForSlot(slot.getId()); + if (current != null) { + newAssignments.add(current); + assignedPlayers.add(current.getPlayerUuid()); + } + } + } + + for (HallAssignment manual : hallManager.getAssignments()) { + if (manual.getType() == HallAssignment.AssignmentType.MANUAL) { + if (newAssignments.stream().noneMatch(a -> a.getSlotId().equals(manual.getSlotId()))) { + newAssignments.add(manual); + assignedPlayers.add(manual.getPlayerUuid()); + } + } + } + + for (HallSection section : sections) { + if (!section.isEnabled()) continue; + + List availableSlots = slotsBySection.getOrDefault(section.getId(), new ArrayList<>()).stream() + .filter(slot -> !slot.isLocked() && !isSlotManuallyAssigned(slot.getId(), newAssignments)) + .sorted(Comparator.comparingInt(HallSlot::getDisplayOrder)) + .collect(Collectors.toList()); + + if (availableSlots.isEmpty()) continue; + + // Get eligible players for this section from the preloaded pool + List eligible = pool.stream() + .filter(uuid -> luckPermsService.isInAnyGroup(uuid, section.getGroups())) + .collect(Collectors.toList()); + + // Filter out already assigned players + eligible.removeIf(assignedPlayers::contains); + + // Sort eligible players + sortEligible(eligible, section.getSortMode()); + + // Fill slots + for (int i = 0; i < Math.min(availableSlots.size(), eligible.size()); i++) { + HallSlot slot = availableSlots.get(i); + UUID playerUuid = eligible.get(i); + + newAssignments.add(new HallAssignment( + slot.getId(), + playerUuid, + section.getId(), + HallAssignment.AssignmentType.AUTO, + System.currentTimeMillis() + )); + assignedPlayers.add(playerUuid); + } + } + + hallManager.setAssignments(newAssignments); + hallManager.saveAssignments(); + hallManager.getRenderService().renderAll(); + } + + private boolean isSlotManuallyAssigned(String slotId, List assignments) { + return assignments.stream().anyMatch(a -> a.getSlotId().equals(slotId)); + } + + private void sortEligible(List players, String mode) { + switch (mode.toLowerCase()) { + case "weight_desc": + players.sort(Comparator.comparingInt(luckPermsService::getWeight).reversed()); + break; + case "username_asc": + players.sort(Comparator.comparing(uuid -> { + OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + return op.getName() != null ? op.getName() : ""; + })); + break; + case "username_desc": + players.sort(Collections.reverseOrder(Comparator.comparing(uuid -> { + OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + return op.getName() != null ? op.getName() : ""; + }))); + break; + } + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/CustomizationService.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/CustomizationService.java new file mode 100644 index 0000000..598ee8d --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/CustomizationService.java @@ -0,0 +1,46 @@ +package org.modularsoft.zander.hub.hall.services; + +import org.bukkit.Material; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; +import org.modularsoft.zander.hub.hall.models.HallCustomization; +import org.modularsoft.zander.hub.hall.persistence.CustomizationPersistence; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class CustomizationService { + private final ZanderHubMain plugin; + private final HallManager hallManager; + private final CustomizationPersistence persistence; + private final Map cache = new HashMap<>(); + + public CustomizationService(ZanderHubMain plugin, HallManager hallManager) { + this.plugin = plugin; + this.hallManager = hallManager; + this.persistence = new CustomizationPersistence(plugin); + } + + public HallCustomization getCustomization(UUID uuid) { + if (cache.containsKey(uuid)) return cache.get(uuid); + HallCustomization c = persistence.load(uuid); + if (c != null) cache.put(uuid, c); + return c; + } + + public void saveCustomization(HallCustomization c) { + cache.put(c.getPlayerUuid(), c); + persistence.save(c); + + // Trigger re-render if they have an active assignment + if (hallManager.getAssignmentForPlayer(c.getPlayerUuid()) != null) { + hallManager.getRenderService().renderAll(); + } + } + + public boolean isValidMaterial(Material m) { + // Now lax, allow all materials + return m != null; + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/LuckPermsService.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/LuckPermsService.java new file mode 100644 index 0000000..ecda71e --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/LuckPermsService.java @@ -0,0 +1,89 @@ +package org.modularsoft.zander.hub.hall.services; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.matcher.NodeMatcher; +import net.luckperms.api.node.types.InheritanceNode; +import org.bukkit.Bukkit; +import org.modularsoft.zander.hub.ZanderHubMain; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class LuckPermsService { + private final ZanderHubMain plugin; + private LuckPerms luckPerms; + + public LuckPermsService(ZanderHubMain plugin) { + this.plugin = plugin; + try { + this.luckPerms = LuckPermsProvider.get(); + } catch (IllegalStateException | NoClassDefFoundError e) { + plugin.getLogger().warning("LuckPerms not found! Hall auto-population will not work."); + } + } + + public boolean isAvailable() { + return luckPerms != null; + } + + public CompletableFuture preloadUsers(Collection uuids) { + if (!isAvailable()) return CompletableFuture.completedFuture(null); + + List> futures = new ArrayList<>(); + for (UUID uuid : uuids) { + if (luckPerms.getUserManager().getUser(uuid) == null) { + futures.add(luckPerms.getUserManager().loadUser(uuid)); + } + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + + public boolean isInAnyGroup(UUID uuid, List groups) { + if (!isAvailable()) return false; + User user = luckPerms.getUserManager().getUser(uuid); + if (user == null) return false; + + Collection inheritedGroups = user.getInheritedGroups(user.getQueryOptions()); + for (Group group : inheritedGroups) { + if (groups.contains(group.getName())) return true; + } + return false; + } + + public int getWeight(UUID uuid) { + if (!isAvailable()) return 0; + User user = luckPerms.getUserManager().getUser(uuid); + if (user == null) return 0; + + return user.getNodes().stream() + .filter(node -> node instanceof InheritanceNode) + .map(node -> (InheritanceNode) node) + .map(node -> luckPerms.getGroupManager().getGroup(node.getGroupName())) + .filter(Objects::nonNull) + .map(group -> group.getWeight().orElse(0)) + .max(Integer::compare) + .orElse(0); + } + + public CompletableFuture> getUuidsInGroups(List groups) { + if (!isAvailable()) return CompletableFuture.completedFuture(Collections.emptySet()); + + List>>> futures = new ArrayList<>(); + for (String group : groups) { + futures.add(luckPerms.getUserManager().searchAll(NodeMatcher.key(InheritanceNode.builder(group).build()))); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> { + Set uuids = new HashSet<>(); + for (CompletableFuture>> future : futures) { + uuids.addAll(future.join().keySet()); + } + return uuids; + }); + } +} diff --git a/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/RenderService.java b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/RenderService.java new file mode 100644 index 0000000..d386dff --- /dev/null +++ b/zander-hub/src/main/java/org/modularsoft/zander/hub/hall/services/RenderService.java @@ -0,0 +1,239 @@ +package org.modularsoft.zander.hub.hall.services; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Particle; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Rotatable; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.util.EulerAngle; +import org.modularsoft.zander.hub.ZanderHubMain; +import org.modularsoft.zander.hub.hall.manager.HallManager; +import org.modularsoft.zander.hub.hall.models.HallAssignment; +import org.modularsoft.zander.hub.hall.models.HallCustomization; +import org.modularsoft.zander.hub.hall.models.HallSection; +import org.modularsoft.zander.hub.hall.models.HallSlot; + +import java.util.Collection; +import java.util.UUID; + +public class RenderService { + private final ZanderHubMain plugin; + private final HallManager hallManager; + + public RenderService(ZanderHubMain plugin, HallManager hallManager) { + this.plugin = plugin; + this.hallManager = hallManager; + startParticleTask(); + } + + public void renderAll() { + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + renderSlot(slot); + } + } + + public void renderSlot(HallSlot slot) { + if (!slot.getLocation().isChunkLoaded()) return; + + HallAssignment assignment = hallManager.getAssignmentForSlot(slot.getId()); + if (assignment == null) { + clearSlot(slot); + return; + } + + updateArmorStand(slot, assignment); + updateSign(slot, assignment); + } + + private void clearSlot(HallSlot slot) { + removeArmorStandsAt(slot.getLocation()); + clearSign(slot.getSignLocation()); + } + + private void updateArmorStand(HallSlot slot, HallAssignment assignment) { + Location loc = slot.getLocation(); + + // Remove duplicates if any and find an existing one + Collection entities = loc.getWorld().getNearbyEntities(loc, 0.5, 0.5, 0.5); + ArmorStand as = null; + for (Entity e : entities) { + if (e instanceof ArmorStand) { + if (as == null) { + as = (ArmorStand) e; + } else { + e.remove(); + } + } + } + + if (as == null) { + as = (ArmorStand) loc.getWorld().spawnEntity(loc, EntityType.ARMOR_STAND); + } + + // Fix rotation and location + as.teleport(loc); + + OfflinePlayer player = Bukkit.getOfflinePlayer(assignment.getPlayerUuid()); + HallCustomization custom = hallManager.getCustomizationService().getCustomization(assignment.getPlayerUuid()); + + as.setBasePlate(false); + as.setArms(true); + as.setGravity(false); + as.setCustomNameVisible(true); + as.customName(Component.text(player.getName() != null ? player.getName() : "Unknown")); + + // Forced Head + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) head.getItemMeta(); + meta.setOwningPlayer(player); + head.setItemMeta(meta); + as.getEquipment().setHelmet(head); + + // Customization + if (custom != null) { + as.getEquipment().setChestplate(new ItemStack(custom.getChestplate() != null ? custom.getChestplate() : Material.AIR)); + as.getEquipment().setLeggings(new ItemStack(custom.getLeggings() != null ? custom.getLeggings() : Material.AIR)); + as.getEquipment().setBoots(new ItemStack(custom.getBoots() != null ? custom.getBoots() : Material.AIR)); + as.getEquipment().setItemInMainHand(new ItemStack(custom.getMainHand() != null ? custom.getMainHand() : Material.AIR)); + as.getEquipment().setItemInOffHand(new ItemStack(custom.getOffHand() != null ? custom.getOffHand() : Material.AIR)); + + if (custom.getPosePreset() != null) { + applyPosePreset(as, custom.getPosePreset()); + } else { + if (custom.getBodyPose() != null) as.setBodyPose(custom.getBodyPose()); + if (custom.getHeadPose() != null) as.setHeadPose(custom.getHeadPose()); + if (custom.getLeftArmPose() != null) as.setLeftArmPose(custom.getLeftArmPose()); + if (custom.getRightArmPose() != null) as.setRightArmPose(custom.getRightArmPose()); + if (custom.getLeftLegPose() != null) as.setLeftLegPose(custom.getLeftLegPose()); + if (custom.getRightLegPose() != null) as.setRightLegPose(custom.getRightLegPose()); + } + } else { + as.getEquipment().setChestplate(null); + as.getEquipment().setLeggings(null); + as.getEquipment().setBoots(null); + as.getEquipment().setItemInMainHand(null); + as.getEquipment().setItemInOffHand(null); + } + } + + private void updateSign(HallSlot slot, HallAssignment assignment) { + Location loc = slot.getSignLocation(); + if (loc == null) return; + Block block = loc.getBlock(); + if (!(block.getState() instanceof Sign)) return; + + // Try to match sign rotation to armor stand yaw if it's a standing sign + BlockData data = block.getBlockData(); + if (data instanceof Rotatable rotatable) { + float yaw = slot.getLocation().getYaw(); + // Normalize yaw to 0-360 + yaw = (yaw % 360 + 360) % 360; + // Map 0-360 to 0-15 (16 positions) + int rotationIndex = Math.round(yaw / 22.5f) % 16; + + BlockFace[] faces = { + BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, + BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, + BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, + BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST + }; + // Note: Minecraft Sign rotation 0 is South, 4 is West, 8 is North, 12 is East. + // But Bukkit/Minecraft Yaw: 0 is South, 90 is West, 180 is North, 270 is East. + // So the mapping index matches the rotation index. + if (rotationIndex >= 0 && rotationIndex < faces.length) { + rotatable.setRotation(faces[rotationIndex]); + block.setBlockData(rotatable); + } + } + + Sign sign = (Sign) block.getState(); + OfflinePlayer player = Bukkit.getOfflinePlayer(assignment.getPlayerUuid()); + HallSection section = hallManager.getSectionManager().getSections().get(assignment.getSectionId()); + + sign.line(0, Component.text(player.getName() != null ? player.getName() : "")); + sign.line(1, Component.text(section != null ? section.getSignLabel() : "")); + sign.line(2, Component.empty()); + sign.line(3, Component.empty()); + sign.update(); + } + + private void clearSign(Location loc) { + if (loc == null) return; + Block block = loc.getBlock(); + if (!(block.getState() instanceof Sign)) return; + + Sign sign = (Sign) block.getState(); + for (int i = 0; i < 4; i++) sign.line(i, Component.empty()); + sign.update(); + } + + private ArmorStand getArmorStandAt(Location loc) { + Collection entities = loc.getWorld().getNearbyEntities(loc, 0.5, 0.5, 0.5); + for (Entity e : entities) { + if (e instanceof ArmorStand) return (ArmorStand) e; + } + return null; + } + + private void removeArmorStandsAt(Location loc) { + Collection entities = loc.getWorld().getNearbyEntities(loc, 0.5, 0.5, 0.5); + for (Entity e : entities) { + if (e instanceof ArmorStand) e.remove(); + } + } + + private void applyPosePreset(ArmorStand as, String preset) { + switch (preset.toUpperCase()) { + case "ZOMBIE": + as.setLeftArmPose(new EulerAngle(Math.toRadians(-90), 0, 0)); + as.setRightArmPose(new EulerAngle(Math.toRadians(-90), 0, 0)); + break; + case "RUNNING": + as.setLeftArmPose(new EulerAngle(Math.toRadians(-40), 0, 0)); + as.setRightArmPose(new EulerAngle(Math.toRadians(40), 0, 0)); + as.setLeftLegPose(new EulerAngle(Math.toRadians(40), 0, 0)); + as.setRightLegPose(new EulerAngle(Math.toRadians(-40), 0, 0)); + break; + case "DANCING": + as.setLeftArmPose(new EulerAngle(Math.toRadians(-120), Math.toRadians(40), 0)); + as.setRightArmPose(new EulerAngle(Math.toRadians(-120), Math.toRadians(40), 0)); + break; + default: // DEFAULT + as.setBodyPose(EulerAngle.ZERO); + as.setLeftArmPose(new EulerAngle(Math.toRadians(-10), 0, Math.toRadians(-10))); + as.setRightArmPose(new EulerAngle(Math.toRadians(-10), 0, Math.toRadians(10))); + as.setLeftLegPose(new EulerAngle(Math.toRadians(1), 0, Math.toRadians(1))); + as.setRightLegPose(new EulerAngle(Math.toRadians(1), 0, Math.toRadians(-1))); + break; + } + } + + private void startParticleTask() { + Bukkit.getScheduler().runTaskTimer(plugin, () -> { + for (HallSlot slot : hallManager.getSlotManager().getSlots().values()) { + if (!slot.getLocation().isChunkLoaded()) continue; + HallAssignment assignment = hallManager.getAssignmentForSlot(slot.getId()); + if (assignment == null) continue; + + HallCustomization custom = hallManager.getCustomizationService().getCustomization(assignment.getPlayerUuid()); + if (custom == null || custom.getParticleEffect() == null) continue; + + try { + Particle particle = Particle.valueOf(custom.getParticleEffect()); + slot.getLocation().getWorld().spawnParticle(particle, slot.getLocation().clone().add(0, 1, 0), 3, 0.3, 0.5, 0.3, 0.02); + } catch (Exception ignored) {} + } + }, 20L, 10L); + } +} diff --git a/zander-hub/src/main/resources/plugin.yml b/zander-hub/src/main/resources/plugin.yml index d1a090b..ae8ef93 100644 --- a/zander-hub/src/main/resources/plugin.yml +++ b/zander-hub/src/main/resources/plugin.yml @@ -12,6 +12,12 @@ commands: connect: description: Connect to a Server. usage: /connect + hall: + description: Hall of Supporters admin command. + usage: /hall + mystatue: + description: Customize your Hall of Supporters statue. + usage: /mystatue permissions: zander.fly: