diff --git a/pom.xml b/pom.xml
index edfc8c0..68489c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
zander-velocity
zander-hub
zander-auth
+ zander-addon
diff --git a/zander-addon/pom.xml b/zander-addon/pom.xml
new file mode 100644
index 0000000..06e59af
--- /dev/null
+++ b/zander-addon/pom.xml
@@ -0,0 +1,112 @@
+
+
+ zander
+ org.modularsoft
+ 1.0
+
+ 4.0.0
+
+ zander-addon
+ 0.1.0
+
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+ io.papermc.paper
+ paper-api
+ 1.21.4-R0.1-SNAPSHOT
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+ provided
+
+
+ io.github.ModularEnigma
+ Requests
+ 1.0.3
+
+
+ com.jayway.jsonpath
+ json-path
+ 2.9.0
+
+
+ com.google.code.gson
+ gson
+ 2.8.9
+ compile
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 21
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+
+ io.github.ModularEnigma
+ org.modularsoft.zander.addon.libs.requests
+
+
+ com.jayway.jsonpath
+ org.modularsoft.zander.addon.libs.jsonpath
+
+
+ com.google.gson
+ org.modularsoft.zander.addon.libs.gson
+
+
+
+
+ *:*
+
+ META-INF/*.MF
+ META-INF/*.txt
+
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/ZanderAddonMain.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/ZanderAddonMain.java
new file mode 100644
index 0000000..289b51a
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/ZanderAddonMain.java
@@ -0,0 +1,62 @@
+package org.modularsoft.zander.addon;
+
+import lombok.Getter;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.modularsoft.zander.addon.api.PolicyApiServer;
+import org.modularsoft.zander.addon.commands.PetTrustCommand;
+import org.modularsoft.zander.addon.commands.PolicyCommand;
+import org.modularsoft.zander.addon.commands.SocialCommand;
+import org.modularsoft.zander.addon.events.PetTrustDamageListener;
+import org.modularsoft.zander.addon.events.PetTrustInteractListener;
+import org.modularsoft.zander.addon.events.PlayerEvents;
+import org.modularsoft.zander.addon.gui.PolicyGUI;
+import org.modularsoft.zander.addon.gui.SocialGUI;
+import org.modularsoft.zander.addon.service.PetTrustService;
+import org.modularsoft.zander.addon.service.PolicyService;
+
+public class ZanderAddonMain extends JavaPlugin {
+ @Getter
+ private static ZanderAddonMain instance;
+ @Getter
+ private PolicyService policyService;
+ @Getter
+ private PetTrustService petTrustService;
+ private PolicyApiServer apiServer;
+
+ @Override
+ public void onEnable() {
+ instance = this;
+
+ saveDefaultConfig();
+
+ this.policyService = new PolicyService(this);
+ this.petTrustService = new PetTrustService(this);
+
+ if (getConfig().getBoolean("api-server.enabled", true)) {
+ this.apiServer = new PolicyApiServer(this);
+ this.apiServer.start();
+ }
+
+ PolicyGUI policyGUI = new PolicyGUI(this);
+ SocialGUI socialGUI = new SocialGUI(this);
+ getServer().getPluginManager().registerEvents(policyGUI, this);
+ getServer().getPluginManager().registerEvents(socialGUI, this);
+ getServer().getPluginManager().registerEvents(new PlayerEvents(this, policyGUI, socialGUI), this);
+ getServer().getPluginManager().registerEvents(new PetTrustInteractListener(this, petTrustService), this);
+ getServer().getPluginManager().registerEvents(new PetTrustDamageListener(this, petTrustService), this);
+
+ getCommand("policy").setExecutor(new PolicyCommand(this, policyService));
+ getCommand("social").setExecutor(new SocialCommand(this, socialGUI));
+ getCommand("pettrust").setExecutor(new PetTrustCommand(this, petTrustService));
+
+ getLogger().info("Zander Addon has been enabled.");
+ }
+
+ @Override
+ public void onDisable() {
+ if (this.apiServer != null) {
+ this.apiServer.stop();
+ }
+ getLogger().info("Zander Addon has been disabled.");
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/api/PolicyApiServer.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/api/PolicyApiServer.java
new file mode 100644
index 0000000..d921490
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/api/PolicyApiServer.java
@@ -0,0 +1,101 @@
+package org.modularsoft.zander.addon.api;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+
+public class PolicyApiServer {
+ private final ZanderAddonMain plugin;
+ private HttpServer server;
+ private final Gson gson = new Gson();
+
+ public PolicyApiServer(ZanderAddonMain plugin) {
+ this.plugin = plugin;
+ }
+
+ public void start() {
+ int port = plugin.getConfig().getInt("api-server.port", 8080);
+ try {
+ server = HttpServer.create(new InetSocketAddress(port), 0);
+ server.createContext("/api/config/policy", new PolicyHandler());
+ server.createContext("/api/config/social", new SocialHandler());
+ server.setExecutor(null); // creates a default executor
+ server.start();
+ plugin.getLogger().info("API Server started on port " + port);
+ } catch (IOException e) {
+ plugin.getLogger().severe("Could not start API Server: " + e.getMessage());
+ }
+ }
+
+ public void stop() {
+ if (server != null) {
+ server.stop(0);
+ plugin.getLogger().info("API Server stopped.");
+ }
+ }
+
+ private class PolicyHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) {
+ exchange.sendResponseHeaders(405, -1); // Method Not Allowed
+ return;
+ }
+
+ JsonObject data = new JsonObject();
+ data.addProperty("termsOfService", "https://raw.githubusercontent.com/craftingforchrist/Legal/master/terms.md");
+ data.addProperty("rules", "https://raw.githubusercontent.com/craftingforchrist/Legal/master/rules.md");
+ data.addProperty("privacy", "https://raw.githubusercontent.com/craftingforchrist/Legal/master/privacy.md");
+ data.addProperty("refund", "https://raw.githubusercontent.com/craftingforchrist/Legal/master/refund.md");
+
+ JsonObject response = new JsonObject();
+ response.addProperty("success", true);
+ response.add("data", data);
+
+ String jsonResponse = gson.toJson(response);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, jsonResponse.getBytes().length);
+ OutputStream os = exchange.getResponseBody();
+ os.write(jsonResponse.getBytes());
+ os.close();
+ }
+ }
+
+ private class SocialHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) {
+ exchange.sendResponseHeaders(405, -1);
+ return;
+ }
+
+ JsonObject data = new JsonObject();
+ data.addProperty("discord", "https://discord.gg/fGWNchS");
+ data.addProperty("facebook", "https://www.facebook.com/craft4christ/");
+ data.addProperty("twitter", "https://twitter.com/craft4christmc");
+ data.addProperty("instagram", "https://instagram.com/craftingforchrist");
+ data.addProperty("twitch", "https://www.twitch.tv/craftingforchrist");
+ data.addProperty("youtube", "https://www.youtube.com/channel/UCeijz6MNnya85LprMjPmYag");
+ data.addProperty("linkedin", "https://www.linkedin.com/company/68885022/");
+ data.addProperty("tiktok", "https://www.tiktok.com/@craftingforchrist");
+
+ JsonObject response = new JsonObject();
+ response.addProperty("success", true);
+ response.add("data", data);
+
+ String jsonResponse = gson.toJson(response);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, jsonResponse.getBytes().length);
+ OutputStream os = exchange.getResponseBody();
+ os.write(jsonResponse.getBytes());
+ os.close();
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PetTrustCommand.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PetTrustCommand.java
new file mode 100644
index 0000000..214c021
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PetTrustCommand.java
@@ -0,0 +1,212 @@
+package org.modularsoft.zander.addon.commands;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Tameable;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.model.pettrust.PetTrust;
+import org.modularsoft.zander.addon.model.pettrust.PlayerTrust;
+import org.modularsoft.zander.addon.model.pettrust.TrustLevel;
+import org.modularsoft.zander.addon.service.PetTrustService;
+import org.modularsoft.zander.addon.util.PetTargetResolver;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class PetTrustCommand implements CommandExecutor {
+ private final ZanderAddonMain plugin;
+ private final PetTrustService trustService;
+
+ public PetTrustCommand(ZanderAddonMain plugin, PetTrustService trustService) {
+ this.plugin = plugin;
+ this.trustService = trustService;
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("This command can only be used by players.");
+ return true;
+ }
+
+ if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
+ sendHelp(player);
+ return true;
+ }
+
+ String subCommand = args[0].toLowerCase();
+
+ switch (subCommand) {
+ case "trust" -> handleTrust(player, args);
+ case "untrust" -> handleUntrust(player, args);
+ case "public" -> handlePublic(player, args);
+ case "private" -> handlePrivate(player, args);
+ case "list" -> handleList(player);
+ default -> sendHelp(player);
+ }
+
+ return true;
+ }
+
+ private void handleTrust(Player player, String[] args) {
+ if (args.length < 2) {
+ player.sendMessage(Component.text("Usage: /pettrust trust [level]").color(NamedTextColor.RED));
+ return;
+ }
+
+ Entity pet = PetTargetResolver.resolvePet(player);
+ if (pet == null) {
+ player.sendMessage(Component.text("You must be looking at or riding a tamed pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ if (!isOwner(player, pet)) {
+ player.sendMessage(Component.text("You are not the owner of this pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ Player targetPlayer = Bukkit.getPlayer(args[1]);
+ if (targetPlayer == null) {
+ player.sendMessage(Component.text("Player not found.").color(NamedTextColor.RED));
+ return;
+ }
+
+ TrustLevel level = TrustLevel.ACCESS;
+ if (args.length >= 3) {
+ try {
+ level = TrustLevel.valueOf(args[2].toUpperCase());
+ } catch (IllegalArgumentException e) {
+ player.sendMessage(Component.text("Invalid trust level. Use ACCESS or MANAGE.").color(NamedTextColor.RED));
+ return;
+ }
+ }
+
+ PetTrust trust = trustService.getOrCreatePetTrust(pet);
+ trustService.setPlayerTrust(trust, targetPlayer, level);
+ player.sendMessage(Component.text("Trusted " + targetPlayer.getName() + " with level " + level.name() + " for this pet.").color(NamedTextColor.GREEN));
+ }
+
+ private void handleUntrust(Player player, String[] args) {
+ if (args.length < 2) {
+ player.sendMessage(Component.text("Usage: /pettrust untrust ").color(NamedTextColor.RED));
+ return;
+ }
+
+ Entity pet = PetTargetResolver.resolvePet(player);
+ if (pet == null) {
+ player.sendMessage(Component.text("You must be looking at or riding a tamed pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ if (!isOwner(player, pet)) {
+ player.sendMessage(Component.text("You are not the owner of this pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ // Try to get UUID from online player or just use the name for removal if possible
+ // But our storage uses UUID. For simplicity in a real plugin we might need an offline player lookup.
+ Player targetPlayer = Bukkit.getPlayer(args[1]);
+ UUID targetUuid;
+ String targetName;
+ if (targetPlayer != null) {
+ targetUuid = targetPlayer.getUniqueId();
+ targetName = targetPlayer.getName();
+ } else {
+ // Very basic offline support if we can't find them, though we should ideally use UUIDs everywhere.
+ player.sendMessage(Component.text("Player must be online to untrust (simple implementation).").color(NamedTextColor.RED));
+ return;
+ }
+
+ PetTrust trust = trustService.getOrCreatePetTrust(pet);
+ trustService.removePlayerTrust(trust, targetUuid);
+ player.sendMessage(Component.text("Untrusted " + targetName + " from this pet.").color(NamedTextColor.GREEN));
+ }
+
+ private void handlePublic(Player player, String[] args) {
+ Entity pet = PetTargetResolver.resolvePet(player);
+ if (pet == null) {
+ player.sendMessage(Component.text("You must be looking at or riding a tamed pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ if (!isOwner(player, pet)) {
+ player.sendMessage(Component.text("You are not the owner of this pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ TrustLevel level = TrustLevel.ACCESS;
+ if (args.length >= 2) {
+ try {
+ level = TrustLevel.valueOf(args[1].toUpperCase());
+ } catch (IllegalArgumentException e) {
+ player.sendMessage(Component.text("Invalid trust level. Use ACCESS or MANAGE.").color(NamedTextColor.RED));
+ return;
+ }
+ }
+
+ PetTrust trust = trustService.getOrCreatePetTrust(pet);
+ trustService.setPublicTrust(trust, true, level);
+ player.sendMessage(Component.text("This pet is now PUBLIC with level " + level.name() + ".").color(NamedTextColor.GREEN));
+ }
+
+ private void handlePrivate(Player player, String[] args) {
+ Entity pet = PetTargetResolver.resolvePet(player);
+ if (pet == null) {
+ player.sendMessage(Component.text("You must be looking at or riding a tamed pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ if (!isOwner(player, pet)) {
+ player.sendMessage(Component.text("You are not the owner of this pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ PetTrust trust = trustService.getOrCreatePetTrust(pet);
+ trustService.setPublicTrust(trust, false, TrustLevel.ACCESS);
+ player.sendMessage(Component.text("This pet is now PRIVATE.").color(NamedTextColor.GREEN));
+ }
+
+ private void handleList(Player player) {
+ Entity pet = PetTargetResolver.resolvePet(player);
+ if (pet == null) {
+ player.sendMessage(Component.text("You must be looking at or riding a tamed pet.").color(NamedTextColor.RED));
+ return;
+ }
+
+ PetTrust trust = trustService.getPetTrust(pet.getUniqueId());
+ if (trust == null) {
+ player.sendMessage(Component.text("This pet has no trust entries.").color(NamedTextColor.YELLOW));
+ return;
+ }
+
+ player.sendMessage(Component.text("--- Pet Trust Info ---").color(NamedTextColor.GOLD));
+ player.sendMessage(Component.text("Type: " + trust.getPetType()).color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("Public: " + (trust.isPublicEnabled() ? "Yes (" + trust.getPublicLevel() + ")" : "No")).color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("Trusted Players:").color(NamedTextColor.YELLOW));
+ for (PlayerTrust pt : trust.getTrustedPlayers().values()) {
+ player.sendMessage(Component.text("- " + pt.getName() + " (" + pt.getLevel() + ")").color(NamedTextColor.WHITE));
+ }
+ }
+
+ private boolean isOwner(Player player, Entity entity) {
+ if (entity instanceof Tameable tameable) {
+ return tameable.getOwner() != null && tameable.getOwner().getUniqueId().equals(player.getUniqueId());
+ }
+ return false;
+ }
+
+ private void sendHelp(Player player) {
+ player.sendMessage(Component.text("--- Pet Trust Help ---").color(NamedTextColor.GOLD));
+ player.sendMessage(Component.text("/pettrust trust [level] - Trust a player").color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("/pettrust untrust - Untrust a player").color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("/pettrust public [level] - Make pet public").color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("/pettrust private - Make pet private").color(NamedTextColor.YELLOW));
+ player.sendMessage(Component.text("/pettrust list - List trust entries for this pet").color(NamedTextColor.YELLOW));
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PolicyCommand.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PolicyCommand.java
new file mode 100644
index 0000000..5234623
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/PolicyCommand.java
@@ -0,0 +1,291 @@
+package org.modularsoft.zander.addon.commands;
+
+import net.kyori.adventure.inventory.Book;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.service.PolicyService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class PolicyCommand implements CommandExecutor {
+ private final ZanderAddonMain plugin;
+ private final PolicyService policyService;
+
+ public PolicyCommand(ZanderAddonMain plugin, PolicyService policyService) {
+ this.plugin = plugin;
+ this.policyService = policyService;
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Component.text("This command can only be used by players.", NamedTextColor.RED));
+ return true;
+ }
+
+ if (args.length == 0) {
+ player.sendMessage(Component.text("Usage: /policy ", NamedTextColor.RED));
+ return true;
+ }
+
+ String type = args[0].toLowerCase();
+ policyService.fetchPolicyUrls().thenAccept(config -> {
+ String url;
+ String title;
+ switch (type) {
+ case "tos":
+ url = config.getTermsOfService();
+ title = "Terms of Service";
+ break;
+ case "rules":
+ url = config.getRules();
+ title = "Rules";
+ break;
+ case "privacy":
+ url = config.getPrivacy();
+ title = "Privacy Policy";
+ break;
+ case "refund":
+ url = config.getRefund();
+ title = "Refund Policy";
+ break;
+ default:
+ player.sendMessage(Component.text("Invalid policy type. Use tos, rules, privacy, or refund.", NamedTextColor.RED));
+ return;
+ }
+
+ policyService.fetchPolicyContent(url).thenAccept(content -> {
+ // Perform UI operation on the main thread
+ Bukkit.getScheduler().runTask(plugin, () -> openBook(player, title, content));
+ }).exceptionally(ex -> {
+ player.sendMessage(Component.text("Failed to fetch policy content.", NamedTextColor.RED));
+ return null;
+ });
+ }).exceptionally(ex -> {
+ player.sendMessage(Component.text("Failed to fetch policy configuration.", NamedTextColor.RED));
+ return null;
+ });
+
+ return true;
+ }
+
+ private void openBook(Player player, String title, String content) {
+ List pages = paginate(player, content);
+ Book book = Book.book(Component.text(title), Component.text("CraftingForChrist"), pages);
+ player.openBook(book);
+ }
+
+ private List paginate(Player player, String content) {
+ List pages = new ArrayList<>();
+ int maxLinesPerPage = 14;
+
+ String[] rawLines = content.split("\n");
+ List formattedLines = new ArrayList<>();
+
+ for (String line : rawLines) {
+ formattedLines.addAll(parseMarkdownLine(line));
+ }
+
+ TextComponent.Builder pageBuilder = Component.text();
+ int lineCount = 0;
+
+ for (Component line : formattedLines) {
+ pageBuilder.append(line).append(Component.newline());
+ lineCount++;
+
+ if (lineCount >= maxLinesPerPage) {
+ pages.add(pageBuilder.build());
+ pageBuilder = Component.text();
+ lineCount = 0;
+ }
+ }
+
+ if (lineCount > 0) {
+ pages.add(pageBuilder.build());
+ }
+
+ if (pages.isEmpty()) {
+ pages.add(Component.empty());
+ }
+
+ return pages;
+ }
+
+ private List parseMarkdownLine(String line) {
+ // Pre-process HTML entities and common tags
+ line = line.replace(" ", " ")
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace(""", "\"")
+ .replace("'", "'")
+ .replace("
", "\n")
+ .replace("
", "\n");
+
+ if (line.contains("\n")) {
+ String[] parts = line.split("\n");
+ List result = new ArrayList<>();
+ for (String part : parts) {
+ result.addAll(parseMarkdownLine(part));
+ }
+ return result;
+ }
+
+ if (line.trim().isEmpty()) {
+ return List.of(Component.empty());
+ }
+
+ // Headings
+ if (line.startsWith("# ")) {
+ return wrapComponent(parseInlineMarkdown(line.substring(2)).color(NamedTextColor.DARK_BLUE).decorate(TextDecoration.BOLD), 18);
+ } else if (line.startsWith("## ")) {
+ return wrapComponent(parseInlineMarkdown(line.substring(3)).color(NamedTextColor.BLUE).decorate(TextDecoration.BOLD), 18);
+ } else if (line.startsWith("### ")) {
+ return wrapComponent(parseInlineMarkdown(line.substring(4)).color(NamedTextColor.DARK_AQUA).decorate(TextDecoration.BOLD), 18);
+ }
+
+ // Blockquotes
+ if (line.startsWith("> ")) {
+ return wrapComponent(Component.text("> ", NamedTextColor.GRAY).append(parseInlineMarkdown(line.substring(2)).color(NamedTextColor.GRAY)), 22);
+ }
+
+ return wrapComponent(parseInlineMarkdown(line), 22);
+ }
+
+ private Component parseInlineMarkdown(String text) {
+ TextComponent.Builder builder = Component.text();
+ int lastIdx = 0;
+
+ int i = 0;
+ while (i < text.length()) {
+ // Markdown Links [text](url)
+ if (text.startsWith("[", i)) {
+ int endBracket = text.indexOf(']', i);
+ if (endBracket != -1 && endBracket + 1 < text.length() && text.charAt(endBracket + 1) == '(') {
+ int endParen = text.indexOf(')', endBracket + 2);
+ if (endParen != -1) {
+ builder.append(Component.text(text.substring(lastIdx, i)));
+ String linkText = text.substring(i + 1, endBracket);
+ String url = text.substring(endBracket + 2, endParen);
+ builder.append(Component.text(linkText).color(NamedTextColor.BLUE).decorate(TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl(url)));
+ i = endParen + 1;
+ lastIdx = i;
+ continue;
+ }
+ }
+ }
+ // HTML-style Links text or mailto
+ else if (text.startsWith("", i);
+ int endTag = text.indexOf("", tagClose);
+ if (hrefEnd != -1 && tagClose != -1 && endTag != -1 && hrefEnd < tagClose) {
+ builder.append(Component.text(text.substring(lastIdx, i)));
+ String url = text.substring(hrefStart + 6, hrefEnd);
+ String linkText = text.substring(tagClose + 1, endTag);
+ builder.append(Component.text(linkText).color(NamedTextColor.BLUE).decorate(TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl(url)));
+ i = endTag + 4;
+ lastIdx = i;
+ continue;
+ }
+ }
+ }
+ // Simple URL/Email in brackets
+ else if (text.startsWith("<", i)) {
+ int endBracket = text.indexOf(">", i);
+ if (endBracket != -1) {
+ String potentialUrl = text.substring(i + 1, endBracket);
+ if (potentialUrl.contains("://") || potentialUrl.contains("@")) {
+ builder.append(Component.text(text.substring(lastIdx, i)));
+ String url = potentialUrl.contains("@") && !potentialUrl.startsWith("mailto:") ? "mailto:" + potentialUrl : potentialUrl;
+ builder.append(Component.text(potentialUrl).color(NamedTextColor.BLUE).decorate(TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl(url)));
+ i = endBracket + 1;
+ lastIdx = i;
+ continue;
+ }
+ }
+ }
+ i++;
+ }
+
+ builder.append(Component.text(text.substring(lastIdx)));
+ return builder.build();
+ }
+
+ private List wrapComponent(Component component, int maxCharsPerLine) {
+ List wrappedLines = new ArrayList<>();
+ TextComponent.Builder currentLine = Component.text();
+ int currentLength = 0;
+
+ // Flatten the component and its children
+ List parts = new ArrayList<>();
+ flatten(component, parts);
+
+ for (Component part : parts) {
+ if (!(part instanceof TextComponent textPart)) {
+ // Should not happen as we flatten into text components
+ continue;
+ }
+
+ String text = textPart.content();
+ if (text.isEmpty()) continue;
+
+ String[] words = text.split("(?<=\\s)|(?=\\s)"); // Split but keep whitespace
+
+ for (String word : words) {
+ if (currentLength + word.length() > maxCharsPerLine && currentLength > 0) {
+ wrappedLines.add(currentLine.build());
+ currentLine = Component.text();
+ currentLength = 0;
+ // If word is whitespace at start of line, skip it
+ if (word.matches("\\s+")) continue;
+ }
+
+ // If it's a link (has click event), we try to wrap it nicely but if it's longer than a line...
+ if (textPart.clickEvent() != null && word.length() > maxCharsPerLine) {
+ // Split word and preserve formatting
+ int i = 0;
+ while (i < word.length()) {
+ int end = Math.min(i + maxCharsPerLine, word.length());
+ currentLine.append(Component.text(word.substring(i, end)).style(textPart.style()));
+ wrappedLines.add(currentLine.build());
+ currentLine = Component.text();
+ currentLength = 0;
+ i = end;
+ }
+ continue;
+ }
+
+ currentLine.append(Component.text(word).style(textPart.style()));
+ currentLength += word.length();
+ }
+ }
+
+ if (currentLength > 0) {
+ wrappedLines.add(currentLine.build());
+ }
+
+ return wrappedLines;
+ }
+
+ private void flatten(Component component, List parts) {
+ parts.add(component.children(Collections.emptyList()));
+ for (Component child : component.children()) {
+ flatten(child, parts);
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/SocialCommand.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/SocialCommand.java
new file mode 100644
index 0000000..a2b96cc
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/commands/SocialCommand.java
@@ -0,0 +1,32 @@
+package org.modularsoft.zander.addon.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.jetbrains.annotations.NotNull;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.gui.SocialGUI;
+
+public class SocialCommand implements CommandExecutor {
+ private final ZanderAddonMain plugin;
+ private final SocialGUI socialGUI;
+
+ public SocialCommand(ZanderAddonMain plugin, SocialGUI socialGUI) {
+ this.plugin = plugin;
+ this.socialGUI = socialGUI;
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Component.text("This command can only be used by players.", NamedTextColor.RED));
+ return true;
+ }
+
+ socialGUI.open(player);
+ return true;
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustDamageListener.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustDamageListener.java
new file mode 100644
index 0000000..d0ee7ec
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustDamageListener.java
@@ -0,0 +1,44 @@
+package org.modularsoft.zander.addon.events;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Tameable;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.model.pettrust.TrustLevel;
+import org.modularsoft.zander.addon.service.PetTrustService;
+
+public class PetTrustDamageListener implements Listener {
+ private final ZanderAddonMain plugin;
+ private final PetTrustService trustService;
+
+ public PetTrustDamageListener(ZanderAddonMain plugin, PetTrustService trustService) {
+ this.plugin = plugin;
+ this.trustService = trustService;
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onPetDamage(EntityDamageByEntityEvent event) {
+ if (!plugin.getConfig().getBoolean("petTrust.enabled", true)) return;
+ if (!plugin.getConfig().getBoolean("petTrust.preventDamageWithoutManage", true)) return;
+
+ Entity entity = event.getEntity();
+ if (!(entity instanceof Tameable tameable) || !tameable.isTamed() || tameable.getOwner() == null) return;
+
+ Entity damagerEntity = event.getDamager();
+ if (!(damagerEntity instanceof Player player)) return;
+ if (player.hasPermission("zander.pettrust.bypass")) return;
+
+ // Damage requires MANAGE level
+ if (!trustService.hasPermission(player, entity, TrustLevel.MANAGE)) {
+ event.setCancelled(true);
+ String message = plugin.getConfig().getString("petTrust.noPermissionMessage", "&cYou are not trusted to use this pet.");
+ player.sendMessage(Component.text(message.replace("&", "§")).color(NamedTextColor.RED));
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustInteractListener.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustInteractListener.java
new file mode 100644
index 0000000..91e76ae
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PetTrustInteractListener.java
@@ -0,0 +1,42 @@
+package org.modularsoft.zander.addon.events;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Tameable;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.model.pettrust.TrustLevel;
+import org.modularsoft.zander.addon.service.PetTrustService;
+
+public class PetTrustInteractListener implements Listener {
+ private final ZanderAddonMain plugin;
+ private final PetTrustService trustService;
+
+ public PetTrustInteractListener(ZanderAddonMain plugin, PetTrustService trustService) {
+ this.plugin = plugin;
+ this.trustService = trustService;
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onPetInteract(PlayerInteractEntityEvent event) {
+ if (!plugin.getConfig().getBoolean("petTrust.enabled", true)) return;
+
+ Entity entity = event.getRightClicked();
+ if (!(entity instanceof Tameable tameable) || !tameable.isTamed() || tameable.getOwner() == null) return;
+
+ Player player = event.getPlayer();
+ if (player.hasPermission("zander.pettrust.bypass")) return;
+
+ // Check for ACCESS level for interaction (mounting, etc.)
+ if (!trustService.hasPermission(player, entity, TrustLevel.ACCESS)) {
+ event.setCancelled(true);
+ String message = plugin.getConfig().getString("petTrust.noPermissionMessage", "&cYou are not trusted to use this pet.");
+ player.sendMessage(Component.text(message.replace("&", "§")).color(NamedTextColor.RED));
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PlayerEvents.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PlayerEvents.java
new file mode 100644
index 0000000..8435c9c
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/events/PlayerEvents.java
@@ -0,0 +1,109 @@
+package org.modularsoft.zander.addon.events;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.gui.PolicyGUI;
+import org.modularsoft.zander.addon.gui.SocialGUI;
+
+import java.util.List;
+
+public class PlayerEvents implements Listener {
+ private final ZanderAddonMain plugin;
+ private final PolicyGUI policyGUI;
+ private final SocialGUI socialGUI;
+ private final NamespacedKey policyBookKey;
+ private final NamespacedKey socialPaperKey;
+
+ public PlayerEvents(ZanderAddonMain plugin, PolicyGUI policyGUI, SocialGUI socialGUI) {
+ this.plugin = plugin;
+ this.policyGUI = policyGUI;
+ this.socialGUI = socialGUI;
+ this.policyBookKey = new NamespacedKey(plugin, "policy_book");
+ this.socialPaperKey = new NamespacedKey(plugin, "social_paper");
+ }
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ Player player = event.getPlayer();
+ if (plugin.getConfig().getBoolean("policy-book.enabled", true)) {
+ givePolicyBook(player);
+ }
+ if (plugin.getConfig().getBoolean("social-paper.enabled", true)) {
+ giveSocialPaper(player);
+ }
+ }
+
+ private void givePolicyBook(Player player) {
+ int slot = plugin.getConfig().getInt("policy-book.slot", 8);
+ ItemStack currentItem = player.getInventory().getItem(slot);
+
+ // Only overwrite if slot is empty or already has our policy book
+ if (currentItem != null && currentItem.getType() != Material.AIR) {
+ ItemMeta currentMeta = currentItem.getItemMeta();
+ if (currentMeta == null || !currentMeta.getPersistentDataContainer().has(policyBookKey, PersistentDataType.BYTE)) {
+ return;
+ }
+ }
+
+ ItemStack book = new ItemStack(Material.BOOK);
+ ItemMeta meta = book.getItemMeta();
+ meta.displayName(Component.text("Server Policies", NamedTextColor.GOLD));
+ meta.lore(List.of(Component.text("Right-click to view server policies", NamedTextColor.GRAY)));
+ meta.getPersistentDataContainer().set(policyBookKey, PersistentDataType.BYTE, (byte) 1);
+ book.setItemMeta(meta);
+
+ player.getInventory().setItem(slot, book);
+ }
+
+ private void giveSocialPaper(Player player) {
+ int slot = plugin.getConfig().getInt("social-paper.slot", 7);
+ ItemStack currentItem = player.getInventory().getItem(slot);
+
+ if (currentItem != null && currentItem.getType() != Material.AIR) {
+ ItemMeta currentMeta = currentItem.getItemMeta();
+ if (currentMeta == null || !currentMeta.getPersistentDataContainer().has(socialPaperKey, PersistentDataType.BYTE)) {
+ return;
+ }
+ }
+
+ ItemStack paper = new ItemStack(Material.PAPER);
+ ItemMeta meta = paper.getItemMeta();
+ meta.displayName(Component.text("Social Media", NamedTextColor.LIGHT_PURPLE));
+ meta.lore(List.of(Component.text("Right-click to view our social media", NamedTextColor.GRAY)));
+ meta.getPersistentDataContainer().set(socialPaperKey, PersistentDataType.BYTE, (byte) 1);
+ paper.setItemMeta(meta);
+
+ player.getInventory().setItem(slot, paper);
+ }
+
+ @EventHandler
+ public void onPlayerInteract(PlayerInteractEvent event) {
+ if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) {
+ ItemStack item = event.getItem();
+ if (item != null) {
+ ItemMeta meta = item.getItemMeta();
+ if (meta == null) return;
+
+ if (meta.getPersistentDataContainer().has(policyBookKey, PersistentDataType.BYTE)) {
+ policyGUI.open(event.getPlayer());
+ event.setCancelled(true);
+ } else if (meta.getPersistentDataContainer().has(socialPaperKey, PersistentDataType.BYTE)) {
+ socialGUI.open(event.getPlayer());
+ event.setCancelled(true);
+ }
+ }
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/PolicyGUI.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/PolicyGUI.java
new file mode 100644
index 0000000..55e3520
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/PolicyGUI.java
@@ -0,0 +1,76 @@
+package org.modularsoft.zander.addon.gui;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+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.addon.ZanderAddonMain;
+
+import java.util.List;
+
+public class PolicyGUI implements Listener {
+ private final ZanderAddonMain plugin;
+ private final Component inventoryTitle = Component.text("Server Policies", NamedTextColor.DARK_BLUE);
+
+ public PolicyGUI(ZanderAddonMain plugin) {
+ this.plugin = plugin;
+ }
+
+ public void open(Player player) {
+ Inventory gui = Bukkit.createInventory(null, 9, inventoryTitle);
+
+ gui.setItem(1, createPolicyItem(Material.BOOK, "Terms of Service"));
+ gui.setItem(3, createPolicyItem(Material.BOOK, "Rules"));
+ gui.setItem(5, createPolicyItem(Material.BOOK, "Privacy Policy"));
+ gui.setItem(7, createPolicyItem(Material.BOOK, "Refund Policy"));
+
+ player.openInventory(gui);
+ }
+
+ private ItemStack createPolicyItem(Material material, String name) {
+ ItemStack item = new ItemStack(material);
+ ItemMeta meta = item.getItemMeta();
+ meta.displayName(Component.text(name, NamedTextColor.GOLD));
+ meta.lore(List.of(Component.text("Click to read our " + name, NamedTextColor.GRAY)));
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @EventHandler
+ public void onInventoryClick(InventoryClickEvent event) {
+ // Use Component comparison properly or check inventory instance/holder if applicable.
+ // For now, title comparison via Adventure should be fine if used correctly.
+ if (!event.getView().title().equals(inventoryTitle)) {
+ return;
+ }
+
+ event.setCancelled(true);
+ if (!(event.getWhoClicked() instanceof Player player)) {
+ return;
+ }
+
+ ItemStack clickedItem = event.getCurrentItem();
+ if (clickedItem == null || clickedItem.getType() != Material.BOOK) {
+ return;
+ }
+
+ int slot = event.getRawSlot();
+ String type = null;
+ if (slot == 1) type = "tos";
+ else if (slot == 3) type = "rules";
+ else if (slot == 5) type = "privacy";
+ else if (slot == 7) type = "refund";
+
+ if (type != null) {
+ player.closeInventory();
+ player.performCommand("policy " + type);
+ }
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/SocialGUI.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/SocialGUI.java
new file mode 100644
index 0000000..7f2821d
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/gui/SocialGUI.java
@@ -0,0 +1,119 @@
+package org.modularsoft.zander.addon.gui;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+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.bukkit.persistence.PersistentDataType;
+import org.bukkit.NamespacedKey;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.model.SocialConfig;
+
+import java.util.List;
+import java.util.Map;
+
+public class SocialGUI implements Listener {
+ private final ZanderAddonMain plugin;
+ private final Component inventoryTitle = Component.text("Social Media", NamedTextColor.DARK_PURPLE);
+ private final NamespacedKey platformKey;
+
+ public SocialGUI(ZanderAddonMain plugin) {
+ this.plugin = plugin;
+ this.platformKey = new NamespacedKey(plugin, "social_platform");
+ }
+
+ public void open(Player player) {
+ plugin.getPolicyService().fetchSocialLinks().thenAccept(config -> {
+ Bukkit.getScheduler().runTask(plugin, () -> {
+ Map platforms = config.getPlatforms();
+ int size = 9 * ((platforms.size() / 9) + 1);
+ Inventory gui = Bukkit.createInventory(null, size, inventoryTitle);
+
+ int slot = 0;
+ for (Map.Entry entry : platforms.entrySet()) {
+ String platform = entry.getKey();
+ String url = entry.getValue();
+
+ ItemStack item = createSocialItem(platform, url);
+ gui.setItem(slot++, item);
+ }
+
+ player.openInventory(gui);
+ });
+ }).exceptionally(ex -> {
+ player.sendMessage(Component.text("Failed to fetch social media links.", NamedTextColor.RED));
+ return null;
+ });
+ }
+
+ private ItemStack createSocialItem(String platform, String url) {
+ Material material = Material.PAPER;
+ // Basic mapping of platforms to materials for better visual
+ switch (platform.toLowerCase()) {
+ case "discord": material = Material.BLUE_WOOL; break;
+ case "facebook": material = Material.LIGHT_BLUE_WOOL; break;
+ case "twitter": material = Material.CYAN_WOOL; break;
+ case "instagram": material = Material.MAGENTA_WOOL; break;
+ case "twitch": material = Material.PURPLE_WOOL; break;
+ case "youtube": material = Material.RED_WOOL; break;
+ case "tiktok": material = Material.BLACK_WOOL; break;
+ }
+
+ ItemStack item = new ItemStack(material);
+ ItemMeta meta = item.getItemMeta();
+ String capitalized = platform.substring(0, 1).toUpperCase() + platform.substring(1);
+ meta.displayName(Component.text(capitalized, NamedTextColor.GOLD).decorate(TextDecoration.BOLD));
+ meta.lore(List.of(
+ Component.text("Click to get the link to our " + capitalized, NamedTextColor.GRAY),
+ Component.text(url, NamedTextColor.DARK_GRAY).decorate(TextDecoration.ITALIC)
+ ));
+ meta.getPersistentDataContainer().set(platformKey, PersistentDataType.STRING, platform);
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @EventHandler
+ public void onInventoryClick(InventoryClickEvent event) {
+ if (!event.getView().title().equals(inventoryTitle)) {
+ return;
+ }
+
+ event.setCancelled(true);
+ if (!(event.getWhoClicked() instanceof Player player)) {
+ return;
+ }
+
+ ItemStack clickedItem = event.getCurrentItem();
+ if (clickedItem == null || clickedItem.getType() == Material.AIR) {
+ return;
+ }
+
+ ItemMeta meta = clickedItem.getItemMeta();
+ if (meta == null || !meta.getPersistentDataContainer().has(platformKey, PersistentDataType.STRING)) {
+ return;
+ }
+
+ String platform = meta.getPersistentDataContainer().get(platformKey, PersistentDataType.STRING);
+ plugin.getPolicyService().fetchSocialLinks().thenAccept(config -> {
+ String url = config.getPlatforms().get(platform);
+ if (url != null) {
+ String capitalized = platform.substring(0, 1).toUpperCase() + platform.substring(1);
+ Component message = Component.text("Click here to visit our " + capitalized + ": ", NamedTextColor.GREEN)
+ .append(Component.text(url, NamedTextColor.AQUA)
+ .decorate(TextDecoration.UNDERLINED)
+ .clickEvent(ClickEvent.openUrl(url)));
+ player.sendMessage(message);
+ Bukkit.getScheduler().runTask(plugin, (Runnable) player::closeInventory);
+ }
+ });
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/model/PolicyConfig.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/PolicyConfig.java
new file mode 100644
index 0000000..c958de1
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/PolicyConfig.java
@@ -0,0 +1,11 @@
+package org.modularsoft.zander.addon.model;
+
+import lombok.Data;
+
+@Data
+public class PolicyConfig {
+ private String termsOfService;
+ private String rules;
+ private String privacy;
+ private String refund;
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/model/SocialConfig.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/SocialConfig.java
new file mode 100644
index 0000000..36079bf
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/SocialConfig.java
@@ -0,0 +1,9 @@
+package org.modularsoft.zander.addon.model;
+
+import lombok.Data;
+import java.util.Map;
+
+@Data
+public class SocialConfig {
+ private Map platforms;
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PetTrust.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PetTrust.java
new file mode 100644
index 0000000..6316399
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PetTrust.java
@@ -0,0 +1,31 @@
+package org.modularsoft.zander.addon.model.pettrust;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PetTrust {
+ private UUID petUuid;
+ private UUID ownerUuid;
+ private String ownerName;
+ private String petType;
+ private boolean publicEnabled;
+ private TrustLevel publicLevel;
+ private Map trustedPlayers = new HashMap<>();
+
+ public PetTrust(UUID petUuid, UUID ownerUuid, String ownerName, String petType) {
+ this.petUuid = petUuid;
+ this.ownerUuid = ownerUuid;
+ this.ownerName = ownerName;
+ this.petType = petType;
+ this.publicEnabled = false;
+ this.publicLevel = TrustLevel.ACCESS;
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PlayerTrust.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PlayerTrust.java
new file mode 100644
index 0000000..98a0487
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/PlayerTrust.java
@@ -0,0 +1,18 @@
+package org.modularsoft.zander.addon.model.pettrust;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PlayerTrust {
+ private UUID uuid;
+ private String name;
+ private TrustLevel level;
+ private LocalDateTime addedAt;
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/TrustLevel.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/TrustLevel.java
new file mode 100644
index 0000000..2412de0
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/model/pettrust/TrustLevel.java
@@ -0,0 +1,10 @@
+package org.modularsoft.zander.addon.model.pettrust;
+
+public enum TrustLevel {
+ ACCESS,
+ MANAGE;
+
+ public boolean isAtLeast(TrustLevel other) {
+ return this.ordinal() >= other.ordinal();
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PetTrustService.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PetTrustService.java
new file mode 100644
index 0000000..d444dba
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PetTrustService.java
@@ -0,0 +1,94 @@
+package org.modularsoft.zander.addon.service;
+
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Tameable;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.modularsoft.zander.addon.model.pettrust.PetTrust;
+import org.modularsoft.zander.addon.model.pettrust.PlayerTrust;
+import org.modularsoft.zander.addon.model.pettrust.TrustLevel;
+import org.modularsoft.zander.addon.storage.PetTrustRepository;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class PetTrustService {
+ private final JavaPlugin plugin;
+ private final PetTrustRepository repository;
+ private final Map petTrustCache = new ConcurrentHashMap<>();
+
+ public PetTrustService(JavaPlugin plugin) {
+ this.plugin = plugin;
+ this.repository = new PetTrustRepository(plugin);
+ loadAll();
+ }
+
+ private void loadAll() {
+ petTrustCache.putAll(repository.loadAll());
+ }
+
+ public PetTrust getOrCreatePetTrust(Entity entity) {
+ if (!(entity instanceof Tameable tameable)) return null;
+ if (!tameable.isTamed() || tameable.getOwner() == null) return null;
+
+ UUID petUuid = entity.getUniqueId();
+ PetTrust trust = petTrustCache.get(petUuid);
+
+ if (trust == null) {
+ UUID ownerUuid = tameable.getOwner().getUniqueId();
+ String ownerName = tameable.getOwner().getName();
+ String petType = entity.getType().name();
+ trust = new PetTrust(petUuid, ownerUuid, ownerName, petType);
+ petTrustCache.put(petUuid, trust);
+ repository.savePet(trust);
+ }
+ return trust;
+ }
+
+ public PetTrust getPetTrust(UUID petUuid) {
+ return petTrustCache.get(petUuid);
+ }
+
+ public boolean hasPermission(Player player, Entity entity, TrustLevel requiredLevel) {
+ if (!(entity instanceof Tameable tameable)) return true;
+ if (!tameable.isTamed() || tameable.getOwner() == null) return true;
+
+ // Owner always has permission
+ if (tameable.getOwner().getUniqueId().equals(player.getUniqueId())) return true;
+
+ PetTrust trust = getPetTrust(entity.getUniqueId());
+ if (trust == null) return false;
+
+ // Check explicit player trust
+ PlayerTrust playerTrust = trust.getTrustedPlayers().get(player.getUniqueId());
+ if (playerTrust != null && playerTrust.getLevel().isAtLeast(requiredLevel)) {
+ return true;
+ }
+
+ // Check public trust
+ if (trust.isPublicEnabled() && trust.getPublicLevel().isAtLeast(requiredLevel)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public void setPlayerTrust(PetTrust trust, Player player, TrustLevel level) {
+ PlayerTrust pt = new PlayerTrust(player.getUniqueId(), player.getName(), level, LocalDateTime.now());
+ trust.getTrustedPlayers().put(player.getUniqueId(), pt);
+ repository.savePet(trust);
+ }
+
+ public void removePlayerTrust(PetTrust trust, UUID playerUuid) {
+ trust.getTrustedPlayers().remove(playerUuid);
+ repository.savePet(trust);
+ }
+
+ public void setPublicTrust(PetTrust trust, boolean enabled, TrustLevel level) {
+ trust.setPublicEnabled(enabled);
+ trust.setPublicLevel(level);
+ repository.savePet(trust);
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PolicyService.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PolicyService.java
new file mode 100644
index 0000000..bc8c520
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/service/PolicyService.java
@@ -0,0 +1,101 @@
+package org.modularsoft.zander.addon.service;
+
+import com.jayway.jsonpath.JsonPath;
+import io.github.ModularEnigma.Request;
+import io.github.ModularEnigma.Response;
+import com.google.gson.reflect.TypeToken;
+import org.modularsoft.zander.addon.ZanderAddonMain;
+import org.modularsoft.zander.addon.model.PolicyConfig;
+import org.modularsoft.zander.addon.model.SocialConfig;
+
+import java.lang.reflect.Type;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public class PolicyService {
+ private final ZanderAddonMain plugin;
+
+ public PolicyService(ZanderAddonMain plugin) {
+ this.plugin = plugin;
+ }
+
+ public CompletableFuture fetchPolicyUrls() {
+ return CompletableFuture.supplyAsync(() -> {
+ String apiUrl = plugin.getConfig().getString("api-url");
+ try {
+ Request req = Request.builder()
+ .setURL(apiUrl + "/config/policy")
+ .setMethod(Request.Method.GET)
+ .build();
+
+ Response res = req.execute();
+ if (res.getStatusCode() != 200) {
+ throw new RuntimeException("Failed to fetch policy URLs: " + res.getStatusCode());
+ }
+
+ String json = res.getBody();
+ PolicyConfig config = new PolicyConfig();
+ config.setTermsOfService(JsonPath.read(json, "$.data.termsOfService"));
+ config.setRules(JsonPath.read(json, "$.data.rules"));
+ config.setPrivacy(JsonPath.read(json, "$.data.privacy"));
+ config.setRefund(JsonPath.read(json, "$.data.refund"));
+ return config;
+ } catch (Exception e) {
+ plugin.getLogger().severe("Error fetching policy URLs: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ public CompletableFuture fetchPolicyContent(String url) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ Request req = Request.builder()
+ .setURL(url)
+ .setMethod(Request.Method.GET)
+ .build();
+
+ Response res = req.execute();
+ if (res.getStatusCode() != 200) {
+ throw new RuntimeException("Failed to fetch policy content: " + res.getStatusCode());
+ }
+
+ return res.getBody();
+ } catch (Exception e) {
+ plugin.getLogger().severe("Error fetching policy content: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ public CompletableFuture fetchSocialLinks() {
+ return CompletableFuture.supplyAsync(() -> {
+ String apiUrl = plugin.getConfig().getString("api-url");
+ try {
+ Request req = Request.builder()
+ .setURL(apiUrl + "/config/social")
+ .setMethod(Request.Method.GET)
+ .build();
+
+ Response res = req.execute();
+ if (res.getStatusCode() != 200) {
+ throw new RuntimeException("Failed to fetch social links: " + res.getStatusCode());
+ }
+
+ String json = res.getBody();
+ Map responseMap = JsonPath.read(json, "$.data");
+
+ SocialConfig config = new SocialConfig();
+ Map platforms = new java.util.HashMap<>();
+ for (Map.Entry entry : responseMap.entrySet()) {
+ platforms.put(entry.getKey(), String.valueOf(entry.getValue()));
+ }
+ config.setPlatforms(platforms);
+ return config;
+ } catch (Exception e) {
+ plugin.getLogger().severe("Error fetching social links: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ });
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/storage/PetTrustRepository.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/storage/PetTrustRepository.java
new file mode 100644
index 0000000..7312ed1
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/storage/PetTrustRepository.java
@@ -0,0 +1,115 @@
+package org.modularsoft.zander.addon.storage;
+
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.modularsoft.zander.addon.model.pettrust.PetTrust;
+import org.modularsoft.zander.addon.model.pettrust.PlayerTrust;
+import org.modularsoft.zander.addon.model.pettrust.TrustLevel;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Level;
+
+public class PetTrustRepository {
+ private final JavaPlugin plugin;
+ private final File file;
+ private FileConfiguration config;
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+ public PetTrustRepository(JavaPlugin plugin) {
+ this.plugin = plugin;
+ this.file = new File(plugin.getDataFolder(), "pettrust.yml");
+ load();
+ }
+
+ public void load() {
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ plugin.getLogger().log(Level.SEVERE, "Could not create pettrust.yml", e);
+ }
+ }
+ config = YamlConfiguration.loadConfiguration(file);
+ }
+
+ public void save() {
+ try {
+ config.save(file);
+ } catch (IOException e) {
+ plugin.getLogger().log(Level.SEVERE, "Could not save pettrust.yml", e);
+ }
+ }
+
+ public Map loadAll() {
+ Map pets = new HashMap<>();
+ ConfigurationSection petsSection = config.getConfigurationSection("pets");
+ if (petsSection == null) return pets;
+
+ for (String key : petsSection.getKeys(false)) {
+ try {
+ UUID petUuid = UUID.fromString(key);
+ ConfigurationSection section = petsSection.getConfigurationSection(key);
+ if (section == null) continue;
+
+ PetTrust petTrust = new PetTrust();
+ petTrust.setPetUuid(petUuid);
+ petTrust.setOwnerUuid(UUID.fromString(section.getString("owner")));
+ petTrust.setOwnerName(section.getString("ownerName"));
+ petTrust.setPetType(section.getString("petType"));
+ petTrust.setPublicEnabled(section.getBoolean("public.enabled", false));
+ petTrust.setPublicLevel(TrustLevel.valueOf(section.getString("public.level", "ACCESS")));
+
+ ConfigurationSection trustedSection = section.getConfigurationSection("trusted");
+ if (trustedSection != null) {
+ for (String playerKey : trustedSection.getKeys(false)) {
+ UUID playerUuid = UUID.fromString(playerKey);
+ ConfigurationSection playerSection = trustedSection.getConfigurationSection(playerKey);
+
+ PlayerTrust playerTrust = new PlayerTrust();
+ playerTrust.setUuid(playerUuid);
+ playerTrust.setName(playerSection.getString("name"));
+ playerTrust.setLevel(TrustLevel.valueOf(playerSection.getString("level", "ACCESS")));
+ playerTrust.setAddedAt(LocalDateTime.parse(playerSection.getString("addedAt"), FORMATTER));
+
+ petTrust.getTrustedPlayers().put(playerUuid, playerTrust);
+ }
+ }
+ pets.put(petUuid, petTrust);
+ } catch (Exception e) {
+ plugin.getLogger().log(Level.WARNING, "Failed to load pet trust for UUID: " + key, e);
+ }
+ }
+ return pets;
+ }
+
+ public void savePet(PetTrust petTrust) {
+ String path = "pets." + petTrust.getPetUuid().toString();
+ config.set(path + ".owner", petTrust.getOwnerUuid().toString());
+ config.set(path + ".ownerName", petTrust.getOwnerName());
+ config.set(path + ".petType", petTrust.getPetType());
+ config.set(path + ".public.enabled", petTrust.isPublicEnabled());
+ config.set(path + ".public.level", petTrust.getPublicLevel().name());
+
+ config.set(path + ".trusted", null); // Clear existing trusted
+ for (PlayerTrust pt : petTrust.getTrustedPlayers().values()) {
+ String playerPath = path + ".trusted." + pt.getUuid().toString();
+ config.set(playerPath + ".name", pt.getName());
+ config.set(playerPath + ".level", pt.getLevel().name());
+ config.set(playerPath + ".addedAt", pt.getAddedAt().format(FORMATTER));
+ }
+ save();
+ }
+
+ public void deletePet(UUID petUuid) {
+ config.set("pets." + petUuid.toString(), null);
+ save();
+ }
+}
diff --git a/zander-addon/src/main/java/org/modularsoft/zander/addon/util/PetTargetResolver.java b/zander-addon/src/main/java/org/modularsoft/zander/addon/util/PetTargetResolver.java
new file mode 100644
index 0000000..34e4bcd
--- /dev/null
+++ b/zander-addon/src/main/java/org/modularsoft/zander/addon/util/PetTargetResolver.java
@@ -0,0 +1,33 @@
+package org.modularsoft.zander.addon.util;
+
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Tameable;
+
+import java.util.List;
+
+public class PetTargetResolver {
+ public static Entity resolvePet(Player player) {
+ // 1. Check if mounted
+ Entity vehicle = player.getVehicle();
+ if (isTamedPet(vehicle)) {
+ return vehicle;
+ }
+
+ // 2. Ray trace or look at entity
+ List nearbyEntities = player.getNearbyEntities(5, 5, 5);
+ Entity target = player.getTargetEntity(5);
+ if (isTamedPet(target)) {
+ return target;
+ }
+
+ return null;
+ }
+
+ private static boolean isTamedPet(Entity entity) {
+ if (entity instanceof Tameable tameable) {
+ return tameable.isTamed() && tameable.getOwner() != null;
+ }
+ return false;
+ }
+}
diff --git a/zander-addon/src/main/resources/config.yml b/zander-addon/src/main/resources/config.yml
new file mode 100644
index 0000000..1067347
--- /dev/null
+++ b/zander-addon/src/main/resources/config.yml
@@ -0,0 +1,28 @@
+# Zander Addon Configuration
+
+# --- API Server Settings ---
+# Enable the built-in API server
+# If enabled, this server will host the /api/config/policy endpoint
+api-server:
+ enabled: true
+ port: 8080
+
+# --- Plugin Settings ---
+# The base URL for the API (where the plugin fetches policy URLs from)
+api-url: "https://craftingforchrist.net/api"
+
+# Settings for the in-game policy book
+policy-book:
+ enabled: true
+ slot: 8
+
+# Settings for the social media paper
+social-paper:
+ enabled: true
+ slot: 7
+
+# --- Pet Trust Settings ---
+petTrust:
+ enabled: true
+ noPermissionMessage: "&cYou are not trusted to use this pet."
+ preventDamageWithoutManage: true
diff --git a/zander-addon/src/main/resources/plugin.yml b/zander-addon/src/main/resources/plugin.yml
new file mode 100644
index 0000000..0016226
--- /dev/null
+++ b/zander-addon/src/main/resources/plugin.yml
@@ -0,0 +1,16 @@
+main: org.modularsoft.zander.addon.ZanderAddonMain
+name: zander-addon
+version: ${project.version}
+author: ModularSoft
+api-version: 1.21
+
+commands:
+ policy:
+ description: View server policies.
+ usage: /policy
+ social:
+ description: View our social media platforms.
+ usage: /social
+ pettrust:
+ description: Manage trust for your tamed pets.
+ usage: /pettrust