diff --git a/pom.xml b/pom.xml
index 9f6d7c5..1f69437 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,6 +163,10 @@
jitpack.io
https://jitpack.io
+
+ opencollab-snapshot
+ https://repo.opencollab.dev/main/
+
@@ -204,5 +208,17 @@
1.7
provided
+
+ org.geysermc.floodgate
+ api
+ 2.2.2-SNAPSHOT
+ provided
+
+
+ org.geysermc.cumulus
+ cumulus
+ 1.1.2
+ provided
+
diff --git a/src/main/java/com/zetaplugins/essentialz/EssentialZ.java b/src/main/java/com/zetaplugins/essentialz/EssentialZ.java
index de9f1f5..876d093 100644
--- a/src/main/java/com/zetaplugins/essentialz/EssentialZ.java
+++ b/src/main/java/com/zetaplugins/essentialz/EssentialZ.java
@@ -41,6 +41,7 @@ public final class EssentialZ extends ZetaCorePlugin {
private final boolean hasPlaceholderApi = Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null;
private final boolean hasVault = Bukkit.getPluginManager().getPlugin("Vault") != null;
+ private final boolean hasFloodgate = Bukkit.getPluginManager().getPlugin("floodgate") != null;
@Override
public void onEnable() {
@@ -64,10 +65,22 @@ public void onEnable() {
initPlaceholderAPI();
initBstats();
+ initFloodgate();
getLogger().info("EssentialZ enabled!");
}
+ private void initFloodgate() {
+ if (hasFloodgate) {
+ boolean formsEnabled = getConfig().getBoolean("tpa.bedrock.formsEnabled", true);
+ if (formsEnabled) {
+ getLogger().info("Floodgate detected, TPA Bedrock forms enabled.");
+ } else {
+ getLogger().info("Floodgate detected, but TPA Bedrock forms are disabled in config.");
+ }
+ }
+ }
+
@Override
public void onDisable() {
getLogger().info("EssentialZ disabled!");
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpAcceptCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpAcceptCommand.java
new file mode 100644
index 0000000..7817451
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpAcceptCommand.java
@@ -0,0 +1,125 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.*;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+@AutoRegisterCommand(
+ commands = "tpaccept",
+ description = "Accept a teleport request",
+ usage = "/tpaccept",
+ permission = "essentialz.tpaccept",
+ aliases = {"tpyes"}
+)
+public class TpAcceptCommand extends EszCommand {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ @InjectManager
+ private TpaToggleManager toggleManager;
+
+ @InjectManager
+ private TpaBedrockFormHandler bedrockFormHandler;
+
+ private boolean floodgateEnabled = false;
+
+ public TpAcceptCommand(EssentialZ plugin) {
+ super(plugin);
+ initFloodgate();
+ }
+
+ private void initFloodgate() {
+ if (getPlugin().getServer().getPluginManager().getPlugin("floodgate") != null) {
+ try {
+ Class.forName("org.geysermc.floodgate.api.FloodgateApi");
+ floodgateEnabled = true;
+ } catch (ClassNotFoundException e) {
+ floodgateEnabled = false;
+ }
+ }
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ List requests = tpaManager.getIncomingRequests(player.getUniqueId());
+
+ if (requests.isEmpty()) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_NO_PENDING_REQUESTS));
+ return true;
+ }
+
+ // Bedrock player with multiple requests - show form
+ if (floodgateEnabled && isBedrockPlayer(player) && requests.size() > 1) {
+ bedrockFormHandler.sendPendingRequestsForm(player);
+ return true;
+ }
+
+ // Accept the most recent request
+ TeleportRequest request = requests.get(requests.size() - 1);
+ acceptRequest(player, request);
+ tpaManager.removeRequest(request);
+
+ TpaUtils.playSound(player, getPlugin().getConfig().getString("tpa.sounds.accept", "ENTITY_PLAYER_LEVELUP"), getPlugin());
+
+ return true;
+ }
+
+ private void acceptRequest(Player acceptor, TeleportRequest request) {
+ Player senderPlayer = request.getSenderPlayer();
+ Player targetPlayer = request.getTargetPlayer();
+
+ if (senderPlayer == null || !senderPlayer.isOnline()) {
+ acceptor.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.PLAYER_NOT_FOUND));
+ return;
+ }
+
+ if (targetPlayer == null || !targetPlayer.isOnline()) {
+ return;
+ }
+
+ if (request.getType() == TpaRequestType.TPA) {
+ senderPlayer.teleport(targetPlayer.getLocation());
+ } else {
+ targetPlayer.teleport(senderPlayer.getLocation());
+ }
+
+ senderPlayer.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_ACCEPTED_SENDER,
+ new MessageManager.Replaceable<>("{player}", targetPlayer.getName())
+ ));
+
+ targetPlayer.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_ACCEPTED_TARGET,
+ new MessageManager.Replaceable<>("{player}", senderPlayer.getName())
+ ));
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ return List.of();
+ }
+
+ private boolean isBedrockPlayer(Player player) {
+ try {
+ return org.geysermc.floodgate.api.FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpDenyCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpDenyCommand.java
new file mode 100644
index 0000000..207822d
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpDenyCommand.java
@@ -0,0 +1,76 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.TeleportRequest;
+import com.zetaplugins.essentialz.features.tpa.TpaManager;
+import com.zetaplugins.essentialz.features.tpa.TpaUtils;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+@AutoRegisterCommand(
+ commands = "tpdeny",
+ description = "Deny a teleport request",
+ usage = "/tpdeny",
+ permission = "essentialz.tpdeny",
+ aliases = {"tpno"}
+)
+public class TpDenyCommand extends EszCommand {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ public TpDenyCommand(EssentialZ plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ List requests = tpaManager.getIncomingRequests(player.getUniqueId());
+
+ if (requests.isEmpty()) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_NO_PENDING_REQUESTS));
+ return true;
+ }
+
+ // Deny the most recent request
+ TeleportRequest request = requests.get(requests.size() - 1);
+ denyRequest(player, request);
+ tpaManager.removeRequest(request);
+
+ TpaUtils.playSound(player, getPlugin().getConfig().getString("tpa.sounds.deny", "ENTITY_VILLAGER_NO"), getPlugin());
+
+ return true;
+ }
+
+ private void denyRequest(Player denier, TeleportRequest request) {
+ Player senderPlayer = request.getSenderPlayer();
+
+ if (senderPlayer != null && senderPlayer.isOnline()) {
+ senderPlayer.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_DENIED_SENDER,
+ new MessageManager.Replaceable<>("{player}", denier.getName())
+ ));
+ }
+
+ denier.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_DENIED_TARGET));
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ return List.of();
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCancelCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCancelCommand.java
new file mode 100644
index 0000000..2c93185
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCancelCommand.java
@@ -0,0 +1,64 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.TeleportRequest;
+import com.zetaplugins.essentialz.features.tpa.TpaManager;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+@AutoRegisterCommand(
+ commands = "tpacancel",
+ description = "Cancel all outgoing teleport requests",
+ usage = "/tpacancel",
+ permission = "essentialz.tpacancel"
+)
+public class TpaCancelCommand extends EszCommand {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ public TpaCancelCommand(EssentialZ plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ List requests = tpaManager.getOutgoingRequests(player.getUniqueId());
+
+ if (requests.isEmpty()) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_NO_OUTGOING_REQUESTS));
+ return true;
+ }
+
+ // Cancel all outgoing requests
+ for (TeleportRequest request : requests) {
+ tpaManager.removeRequest(request);
+ }
+
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_REQUESTS_CANCELLED,
+ new MessageManager.Replaceable<>("{count}", String.valueOf(requests.size()))
+ ));
+
+ return true;
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ return List.of();
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCommand.java
new file mode 100644
index 0000000..011313a
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaCommand.java
@@ -0,0 +1,153 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.*;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.geysermc.floodgate.api.FloodgateApi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@AutoRegisterCommand(
+ commands = "tpa",
+ description = "Request to teleport to another player",
+ usage = "/tpa ",
+ permission = "essentialz.tpa"
+)
+public class TpaCommand extends EszCommand {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ @InjectManager
+ private TpaToggleManager toggleManager;
+
+ @InjectManager
+ private TpaBedrockFormHandler bedrockFormHandler;
+
+ private boolean floodgateEnabled = false;
+
+ public TpaCommand(EssentialZ plugin) {
+ super(plugin);
+ initFloodgate();
+ }
+
+ private void initFloodgate() {
+ if (getPlugin().getServer().getPluginManager().getPlugin("floodgate") != null) {
+ try {
+ Class.forName("org.geysermc.floodgate.api.FloodgateApi");
+ floodgateEnabled = true;
+ } catch (ClassNotFoundException e) {
+ floodgateEnabled = false;
+ }
+ }
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ Player target = args.getPlayer(0, getPlugin());
+
+ // Bedrock player with no args - show form
+ if (target == null && floodgateEnabled && isBedrockPlayer(player)) {
+ bedrockFormHandler.sendPlayerSelectionForm(player, TpaRequestType.TPA);
+ return true;
+ }
+
+ if (target == null) {
+ // Try partial match with raw argument
+ String rawArg = args.getJoinedString(0);
+ if (rawArg.isEmpty()) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.USAGE_ERROR,
+ new MessageManager.Replaceable<>("{usage}", "/tpa ")
+ ));
+ return true;
+ }
+ target = TpaUtils.findPlayer(rawArg);
+ }
+ if (target == null) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.PLAYER_NOT_FOUND));
+ return true;
+ }
+
+ if (target.equals(player)) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_CANNOT_SELF));
+ return true;
+ }
+
+ if (!toggleManager.isEnabled(target.getUniqueId())) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_DISABLED_TARGET,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+ return true;
+ }
+
+ TeleportRequest request = tpaManager.createRequest(
+ player.getUniqueId(),
+ target.getUniqueId(),
+ TpaRequestType.TPA
+ );
+
+ if (request == null) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_REQUEST_ALREADY_PENDING));
+ return true;
+ }
+
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_SENT,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+
+ if (floodgateEnabled && isBedrockPlayer(target)) {
+ bedrockFormHandler.sendRequestNotification(target, player, TpaRequestType.TPA);
+ } else {
+ target.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_RECEIVED,
+ new MessageManager.Replaceable<>("{player}", player.getName()),
+ new MessageManager.Replaceable<>("{type}", "teleport to you")
+ ));
+ target.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_REQUEST_INSTRUCTIONS, false));
+ }
+
+ TpaUtils.playSound(target, getPlugin().getConfig().getString("tpa.sounds.request", "ENTITY_EXPERIENCE_ORB_PICKUP"), getPlugin());
+
+ return true;
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ if (args.getCurrentArgIndex() == 0) {
+ return Bukkit.getOnlinePlayers().stream()
+ .filter(p -> !p.equals(sender))
+ .filter(p -> toggleManager.isEnabled(p.getUniqueId()))
+ .map(Player::getName)
+ .filter(name -> name.toLowerCase().startsWith(args.getCurrentArg().toLowerCase()))
+ .collect(Collectors.toList());
+ }
+ return new ArrayList<>();
+ }
+
+ private boolean isBedrockPlayer(Player player) {
+ try {
+ return FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaHereCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaHereCommand.java
new file mode 100644
index 0000000..5ef0f61
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaHereCommand.java
@@ -0,0 +1,152 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.*;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@AutoRegisterCommand(
+ commands = "tpahere",
+ description = "Request another player to teleport to you",
+ usage = "/tpahere ",
+ permission = "essentialz.tpahere"
+)
+public class TpaHereCommand extends EszCommand {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ @InjectManager
+ private TpaToggleManager toggleManager;
+
+ @InjectManager
+ private TpaBedrockFormHandler bedrockFormHandler;
+
+ private boolean floodgateEnabled = false;
+
+ public TpaHereCommand(EssentialZ plugin) {
+ super(plugin);
+ initFloodgate();
+ }
+
+ private void initFloodgate() {
+ if (getPlugin().getServer().getPluginManager().getPlugin("floodgate") != null) {
+ try {
+ Class.forName("org.geysermc.floodgate.api.FloodgateApi");
+ floodgateEnabled = true;
+ } catch (ClassNotFoundException e) {
+ floodgateEnabled = false;
+ }
+ }
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ Player target = args.getPlayer(0, getPlugin());
+
+ // Bedrock player with no args - show form
+ if (target == null && floodgateEnabled && isBedrockPlayer(player)) {
+ bedrockFormHandler.sendPlayerSelectionForm(player, TpaRequestType.TPA_HERE);
+ return true;
+ }
+
+ if (target == null) {
+ // Try partial match with raw argument
+ String rawArg = args.getJoinedString(0);
+ if (rawArg.isEmpty()) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.USAGE_ERROR,
+ new MessageManager.Replaceable<>("{usage}", "/tpahere ")
+ ));
+ return true;
+ }
+ target = TpaUtils.findPlayer(rawArg);
+ }
+ if (target == null) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.PLAYER_NOT_FOUND));
+ return true;
+ }
+
+ if (target.equals(player)) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_CANNOT_SELF));
+ return true;
+ }
+
+ if (!toggleManager.isEnabled(target.getUniqueId())) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_DISABLED_TARGET,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+ return true;
+ }
+
+ TeleportRequest request = tpaManager.createRequest(
+ player.getUniqueId(),
+ target.getUniqueId(),
+ TpaRequestType.TPA_HERE
+ );
+
+ if (request == null) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_REQUEST_ALREADY_PENDING));
+ return true;
+ }
+
+ sender.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_SENT,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+
+ if (floodgateEnabled && isBedrockPlayer(target)) {
+ bedrockFormHandler.sendRequestNotification(target, player, TpaRequestType.TPA_HERE);
+ } else {
+ target.sendMessage(getMessageManager().getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_RECEIVED,
+ new MessageManager.Replaceable<>("{player}", player.getName()),
+ new MessageManager.Replaceable<>("{type}", "teleport you to them")
+ ));
+ target.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_REQUEST_INSTRUCTIONS, false));
+ }
+
+ TpaUtils.playSound(target, getPlugin().getConfig().getString("tpa.sounds.request", "ENTITY_EXPERIENCE_ORB_PICKUP"), getPlugin());
+
+ return true;
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ if (args.getCurrentArgIndex() == 0) {
+ return Bukkit.getOnlinePlayers().stream()
+ .filter(p -> !p.equals(sender))
+ .filter(p -> toggleManager.isEnabled(p.getUniqueId()))
+ .map(Player::getName)
+ .filter(name -> name.toLowerCase().startsWith(args.getCurrentArg().toLowerCase()))
+ .collect(Collectors.toList());
+ }
+ return new ArrayList<>();
+ }
+
+ private boolean isBedrockPlayer(Player player) {
+ try {
+ return org.geysermc.floodgate.api.FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaToggleCommand.java b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaToggleCommand.java
new file mode 100644
index 0000000..1f7e2a7
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/commands/tpa/TpaToggleCommand.java
@@ -0,0 +1,53 @@
+package com.zetaplugins.essentialz.commands.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.features.tpa.TpaToggleManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.essentialz.util.commands.EszCommand;
+import com.zetaplugins.zetacore.annotations.AutoRegisterCommand;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.commands.ArgumentList;
+import com.zetaplugins.zetacore.commands.exceptions.CommandSenderMustBePlayerException;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+@AutoRegisterCommand(
+ commands = "tpatoggle",
+ description = "Toggle receiving teleport requests",
+ usage = "/tpatoggle",
+ permission = "essentialz.tpatoggle"
+)
+public class TpaToggleCommand extends EszCommand {
+
+ @InjectManager
+ private TpaToggleManager toggleManager;
+
+ public TpaToggleCommand(EssentialZ plugin) {
+ super(plugin);
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, Command command, String label, ArgumentList args) throws CommandSenderMustBePlayerException {
+ if (!(sender instanceof Player player)) {
+ throw new CommandSenderMustBePlayerException();
+ }
+
+ toggleManager.toggle(player.getUniqueId());
+
+ if (toggleManager.isEnabled(player.getUniqueId())) {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_TOGGLE_ENABLED));
+ } else {
+ sender.sendMessage(getMessageManager().getAndFormatMsg(PluginMessage.TPA_TOGGLE_DISABLED));
+ }
+
+ return true;
+ }
+
+ @Override
+ public List tabComplete(CommandSender sender, Command command, ArgumentList args) {
+ return List.of();
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TeleportRequest.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TeleportRequest.java
new file mode 100644
index 0000000..945a07c
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TeleportRequest.java
@@ -0,0 +1,66 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import java.util.UUID;
+
+/**
+ * Represents a teleport request between two players.
+ */
+public class TeleportRequest {
+
+ private final UUID sender;
+ private final UUID target;
+ private final TpaRequestType type;
+ private final long creationTime;
+ private final long expiryTime;
+
+ public TeleportRequest(UUID sender, UUID target, TpaRequestType type, int expirySeconds) {
+ this.sender = sender;
+ this.target = target;
+ this.type = type;
+ this.creationTime = System.currentTimeMillis();
+ this.expiryTime = creationTime + (expirySeconds * 1000L);
+ }
+
+ public UUID getSender() {
+ return sender;
+ }
+
+ public UUID getTarget() {
+ return target;
+ }
+
+ public TpaRequestType getType() {
+ return type;
+ }
+
+ public long getCreationTime() {
+ return creationTime;
+ }
+
+ public long getExpiryTime() {
+ return expiryTime;
+ }
+
+ public boolean isExpired() {
+ return System.currentTimeMillis() > expiryTime;
+ }
+
+ /**
+ * Gets the sender player if online.
+ * @return The sender player or null if offline
+ */
+ public Player getSenderPlayer() {
+ return Bukkit.getPlayer(sender);
+ }
+
+ /**
+ * Gets the target player if online.
+ * @return The target player or null if offline
+ */
+ public Player getTargetPlayer() {
+ return Bukkit.getPlayer(target);
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaBedrockFormHandler.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaBedrockFormHandler.java
new file mode 100644
index 0000000..0dbeece
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaBedrockFormHandler.java
@@ -0,0 +1,340 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.essentialz.util.MessageManager;
+import com.zetaplugins.essentialz.util.PluginMessage;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import com.zetaplugins.zetacore.annotations.Manager;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.geysermc.cumulus.form.ModalForm;
+import org.geysermc.cumulus.form.SimpleForm;
+import org.geysermc.cumulus.util.FormImage;
+import org.geysermc.floodgate.api.FloodgateApi;
+import org.geysermc.floodgate.api.player.FloodgatePlayer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles Bedrock forms for TPA functionality.
+ */
+@Manager
+public class TpaBedrockFormHandler {
+
+ private final EssentialZ plugin;
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ @InjectManager
+ private TpaToggleManager toggleManager;
+
+ @InjectManager
+ private MessageManager messageManager;
+
+ public TpaBedrockFormHandler(EssentialZ plugin) {
+ this.plugin = plugin;
+ }
+
+ /**
+ * Sends a player selection form for TPA.
+ * @param sender The player sending the request
+ * @param type The type of TPA request
+ */
+ public void sendPlayerSelectionForm(Player sender, TpaRequestType type) {
+ FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(sender.getUniqueId());
+ if (floodgatePlayer == null) return;
+
+ boolean showHeads = plugin.getConfig().getBoolean("tpa.bedrock.showPlayerHeads", true);
+
+ SimpleForm.Builder builder = SimpleForm.builder()
+ .title(type == TpaRequestType.TPA ? "Teleport To Player" : "Teleport Player Here")
+ .content("Select a player:");
+
+ List availablePlayers = new ArrayList<>();
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (player.equals(sender)) continue;
+ if (!toggleManager.isEnabled(player.getUniqueId())) continue;
+
+ availablePlayers.add(player);
+
+ String buttonText = player.getName();
+ if (showHeads) {
+ builder.button(buttonText, FormImage.Type.URL, "https://mc-heads.net/avatar/" + player.getName() + "/64");
+ } else {
+ builder.button(buttonText);
+ }
+ }
+
+ if (availablePlayers.isEmpty()) {
+ builder.content("No players available for teleportation.");
+ }
+
+ builder.validResultHandler(response -> {
+ int index = response.clickedButtonId();
+ if (index < 0 || index >= availablePlayers.size()) return;
+
+ Player target = availablePlayers.get(index);
+ if (type == TpaRequestType.TPA) {
+ sendTpaConfirmationForm(sender, target);
+ } else {
+ sendTpaRequest(sender, target, TpaRequestType.TPA_HERE);
+ }
+ });
+
+ floodgatePlayer.sendForm(builder);
+ }
+
+ private void sendTpaConfirmationForm(Player sender, Player target) {
+ FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(sender.getUniqueId());
+ if (floodgatePlayer == null) return;
+
+ ModalForm form = ModalForm.builder()
+ .title("Teleport Options")
+ .content("Choose teleport type for " + target.getName())
+ .button1("Teleport to " + target.getName())
+ .button2("Teleport " + target.getName() + " here")
+ .validResultHandler(response -> {
+ if (response.clickedButtonId() == 0) {
+ sendTpaRequest(sender, target, TpaRequestType.TPA);
+ } else {
+ sendTpaRequest(sender, target, TpaRequestType.TPA_HERE);
+ }
+ })
+ .build();
+
+ floodgatePlayer.sendForm(form);
+ }
+
+ /**
+ * Sends a request notification form to a Bedrock player.
+ * @param recipient The player receiving the request
+ * @param sender The player who sent the request
+ * @param type The type of request
+ */
+ public void sendRequestNotification(Player recipient, Player sender, TpaRequestType type) {
+ FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(recipient.getUniqueId());
+ if (floodgatePlayer == null) return;
+
+ String content = type == TpaRequestType.TPA
+ ? sender.getName() + " wants to teleport to you."
+ : sender.getName() + " wants to teleport you to them.";
+
+ long startTime = System.currentTimeMillis();
+
+ ModalForm.Builder builder = ModalForm.builder()
+ .title("Teleport Request")
+ .content(content)
+ .button1("Accept")
+ .button2("Deny");
+
+ builder.validResultHandler(response -> {
+ TeleportRequest request = tpaManager.getLatestRequest(recipient.getUniqueId());
+ if (request == null || !request.getSender().equals(sender.getUniqueId())) {
+ recipient.sendMessage(messageManager.getAndFormatMsg(PluginMessage.TPA_NO_PENDING_REQUESTS));
+ return;
+ }
+
+ if (response.clickedButtonId() == 0) {
+ acceptRequest(recipient, request);
+ } else {
+ denyRequest(recipient, request);
+ }
+
+ tpaManager.removeRequest(request);
+ });
+
+ builder.closedResultHandler(() -> retryRequestForm(recipient, sender, type, startTime));
+ builder.invalidResultHandler(() -> retryRequestForm(recipient, sender, type, startTime));
+
+ floodgatePlayer.sendForm(builder.build());
+ }
+
+ private void retryRequestForm(Player recipient, Player sender, TpaRequestType type, long startTime) {
+ if ((System.currentTimeMillis() - startTime) < 30000) {
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ if (recipient.isOnline() && sender.isOnline()) {
+ sendRequestNotification(recipient, sender, type);
+ }
+ }, 20L);
+ }
+ }
+
+ /**
+ * Sends a form showing all pending requests.
+ * @param player The player viewing requests
+ */
+ public void sendPendingRequestsForm(Player player) {
+ FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(player.getUniqueId());
+ if (floodgatePlayer == null) return;
+
+ List requests = tpaManager.getIncomingRequests(player.getUniqueId());
+
+ if (requests.isEmpty()) {
+ player.sendMessage(messageManager.getAndFormatMsg(PluginMessage.TPA_NO_PENDING_REQUESTS));
+ return;
+ }
+
+ SimpleForm.Builder builder = SimpleForm.builder()
+ .title("Pending Teleport Requests")
+ .content("Select a request to manage:");
+
+ for (TeleportRequest request : requests) {
+ Player sender = Bukkit.getPlayer(request.getSender());
+ if (sender != null) {
+ String typeStr = request.getType() == TpaRequestType.TPA ? "to you" : "you to them";
+ builder.button(sender.getName() + " - Teleport " + typeStr);
+ }
+ }
+
+ builder.validResultHandler(response -> {
+ int index = response.clickedButtonId();
+ if (index < 0 || index >= requests.size()) return;
+
+ TeleportRequest request = requests.get(index);
+ sendRequestManagementForm(player, request);
+ });
+
+ floodgatePlayer.sendForm(builder);
+ }
+
+ /**
+ * Sends a form for managing a specific request.
+ * @param player The player managing the request
+ * @param request The request to manage
+ */
+ public void sendRequestManagementForm(Player player, TeleportRequest request) {
+ FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(player.getUniqueId());
+ if (floodgatePlayer == null) return;
+
+ Player sender = Bukkit.getPlayer(request.getSender());
+ if (sender == null) {
+ player.sendMessage(messageManager.getAndFormatMsg(PluginMessage.PLAYER_NOT_FOUND));
+ return;
+ }
+
+ String content = "Request from " + sender.getName() + "\n"
+ + "Type: " + (request.getType() == TpaRequestType.TPA
+ ? "Teleport to you" : "Teleport you to them");
+
+ long startTime = System.currentTimeMillis();
+
+ ModalForm.Builder builder = ModalForm.builder()
+ .title("Manage Request")
+ .content(content)
+ .button1("Accept")
+ .button2("Deny");
+
+ builder.validResultHandler(response -> {
+ if (response.clickedButtonId() == 0) {
+ acceptRequest(player, request);
+ } else {
+ denyRequest(player, request);
+ }
+ tpaManager.removeRequest(request);
+ });
+
+ builder.closedResultHandler(() -> retryManagementForm(player, request, startTime));
+ builder.invalidResultHandler(() -> retryManagementForm(player, request, startTime));
+
+ floodgatePlayer.sendForm(builder.build());
+ }
+
+ private void retryManagementForm(Player player, TeleportRequest request, long startTime) {
+ if ((System.currentTimeMillis() - startTime) < 30000) {
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ if (player.isOnline() && !request.isExpired()) {
+ sendRequestManagementForm(player, request);
+ }
+ }, 20L);
+ }
+ }
+
+ private void sendTpaRequest(Player sender, Player target, TpaRequestType type) {
+ if (!toggleManager.isEnabled(target.getUniqueId())) {
+ sender.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_DISABLED_TARGET,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+ return;
+ }
+
+ TeleportRequest request = tpaManager.createRequest(
+ sender.getUniqueId(),
+ target.getUniqueId(),
+ type
+ );
+
+ if (request == null) {
+ sender.sendMessage(messageManager.getAndFormatMsg(PluginMessage.TPA_REQUEST_ALREADY_PENDING));
+ return;
+ }
+
+ sender.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_SENT,
+ new MessageManager.Replaceable<>("{player}", target.getName())
+ ));
+
+ if (FloodgateApi.getInstance().isFloodgatePlayer(target.getUniqueId())) {
+ sendRequestNotification(target, sender, type);
+ } else {
+ String typeDescription = type == TpaRequestType.TPA ? "teleport to you" : "teleport you to them";
+ target.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_REQUEST_RECEIVED,
+ new MessageManager.Replaceable<>("{player}", sender.getName()),
+ new MessageManager.Replaceable<>("{type}", typeDescription)
+ ));
+ target.sendMessage(messageManager.getAndFormatMsg(PluginMessage.TPA_REQUEST_INSTRUCTIONS, false));
+ }
+
+ TpaUtils.playSound(target, plugin.getConfig().getString("tpa.sounds.request", "ENTITY_EXPERIENCE_ORB_PICKUP"), plugin);
+ }
+
+ private void acceptRequest(Player acceptor, TeleportRequest request) {
+ Player senderPlayer = request.getSenderPlayer();
+ Player targetPlayer = request.getTargetPlayer();
+
+ if (senderPlayer == null || !senderPlayer.isOnline()) {
+ acceptor.sendMessage(messageManager.getAndFormatMsg(PluginMessage.PLAYER_NOT_FOUND));
+ return;
+ }
+
+ if (targetPlayer == null || !targetPlayer.isOnline()) {
+ return;
+ }
+
+ if (request.getType() == TpaRequestType.TPA) {
+ senderPlayer.teleport(targetPlayer.getLocation());
+ } else {
+ targetPlayer.teleport(senderPlayer.getLocation());
+ }
+
+ senderPlayer.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_ACCEPTED_SENDER,
+ new MessageManager.Replaceable<>("{player}", targetPlayer.getName())
+ ));
+
+ targetPlayer.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_ACCEPTED_TARGET,
+ new MessageManager.Replaceable<>("{player}", senderPlayer.getName())
+ ));
+
+ TpaUtils.playSound(acceptor, plugin.getConfig().getString("tpa.sounds.accept", "ENTITY_PLAYER_LEVELUP"), plugin);
+ }
+
+ private void denyRequest(Player denier, TeleportRequest request) {
+ Player senderPlayer = request.getSenderPlayer();
+
+ if (senderPlayer != null && senderPlayer.isOnline()) {
+ senderPlayer.sendMessage(messageManager.getAndFormatMsg(
+ PluginMessage.TPA_DENIED_SENDER,
+ new MessageManager.Replaceable<>("{player}", denier.getName())
+ ));
+ }
+
+ denier.sendMessage(messageManager.getAndFormatMsg(PluginMessage.TPA_DENIED_TARGET));
+ TpaUtils.playSound(denier, plugin.getConfig().getString("tpa.sounds.deny", "ENTITY_VILLAGER_NO"), plugin);
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaManager.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaManager.java
new file mode 100644
index 0000000..2b9d509
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaManager.java
@@ -0,0 +1,171 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.zetacore.annotations.Manager;
+import com.zetaplugins.zetacore.annotations.PostManagerConstruct;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages TPA requests between players.
+ */
+@Manager
+public class TpaManager {
+
+ private final EssentialZ plugin;
+ private final Map> incomingRequests;
+ private final Map> outgoingRequests;
+
+ private int requestExpiryTime = 120;
+
+ public TpaManager(EssentialZ plugin) {
+ this.plugin = plugin;
+ this.incomingRequests = new ConcurrentHashMap<>();
+ this.outgoingRequests = new ConcurrentHashMap<>();
+ }
+
+ @PostManagerConstruct
+ public void init() {
+ loadConfig();
+ startExpiryTask();
+ }
+
+ private void loadConfig() {
+ requestExpiryTime = plugin.getConfig().getInt("tpa.requestExpiryTime", 120);
+ }
+
+ private void startExpiryTask() {
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ cleanExpiredRequests();
+ }
+ }.runTaskTimer(plugin, 20L, 20L);
+ }
+
+ /**
+ * Creates a new teleport request.
+ * @param sender The UUID of the player sending the request
+ * @param target The UUID of the player receiving the request
+ * @param type The type of request (TPA or TPA_HERE)
+ * @return The created request, or null if a request already exists
+ */
+ public TeleportRequest createRequest(UUID sender, UUID target, TpaRequestType type) {
+ if (hasRequest(sender, target)) {
+ return null;
+ }
+
+ TeleportRequest request = new TeleportRequest(sender, target, type, requestExpiryTime);
+
+ incomingRequests.computeIfAbsent(target, k -> new ArrayList<>()).add(request);
+ outgoingRequests.computeIfAbsent(sender, k -> new ArrayList<>()).add(request);
+
+ return request;
+ }
+
+ /**
+ * Checks if a request already exists between two players.
+ * @param sender The sender's UUID
+ * @param target The target's UUID
+ * @return true if a non-expired request exists
+ */
+ public boolean hasRequest(UUID sender, UUID target) {
+ List requests = incomingRequests.get(target);
+ if (requests == null) return false;
+
+ return requests.stream()
+ .anyMatch(r -> r.getSender().equals(sender) && !r.isExpired());
+ }
+
+ /**
+ * Gets the latest non-expired request for a player.
+ * @param target The target player's UUID
+ * @return The latest request, or null if none exists
+ */
+ public TeleportRequest getLatestRequest(UUID target) {
+ List requests = incomingRequests.get(target);
+ if (requests == null || requests.isEmpty()) return null;
+
+ for (int i = requests.size() - 1; i >= 0; i--) {
+ TeleportRequest request = requests.get(i);
+ if (!request.isExpired()) {
+ return request;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets all incoming non-expired requests for a player.
+ * @param target The target player's UUID
+ * @return List of incoming requests
+ */
+ public List getIncomingRequests(UUID target) {
+ List requests = incomingRequests.get(target);
+ if (requests == null) return new ArrayList<>();
+
+ requests.removeIf(TeleportRequest::isExpired);
+ return new ArrayList<>(requests);
+ }
+
+ /**
+ * Gets all outgoing non-expired requests for a player.
+ * @param sender The sender player's UUID
+ * @return List of outgoing requests
+ */
+ public List getOutgoingRequests(UUID sender) {
+ List requests = outgoingRequests.get(sender);
+ if (requests == null) return new ArrayList<>();
+
+ requests.removeIf(TeleportRequest::isExpired);
+ return new ArrayList<>(requests);
+ }
+
+ /**
+ * Removes a specific request.
+ * @param request The request to remove
+ */
+ public void removeRequest(TeleportRequest request) {
+ List incoming = incomingRequests.get(request.getTarget());
+ if (incoming != null) {
+ incoming.remove(request);
+ if (incoming.isEmpty()) {
+ incomingRequests.remove(request.getTarget());
+ }
+ }
+
+ List outgoing = outgoingRequests.get(request.getSender());
+ if (outgoing != null) {
+ outgoing.remove(request);
+ if (outgoing.isEmpty()) {
+ outgoingRequests.remove(request.getSender());
+ }
+ }
+ }
+
+ /**
+ * Cleans up all expired requests and notifies players.
+ */
+ public void cleanExpiredRequests() {
+ incomingRequests.values().forEach(list -> list.removeIf(TeleportRequest::isExpired));
+ outgoingRequests.values().forEach(list -> list.removeIf(TeleportRequest::isExpired));
+
+ incomingRequests.entrySet().removeIf(entry -> entry.getValue().isEmpty());
+ outgoingRequests.entrySet().removeIf(entry -> entry.getValue().isEmpty());
+ }
+
+ /**
+ * Clears all requests for a player (called on quit).
+ * @param player The player's UUID
+ */
+ public void clearPlayerRequests(UUID player) {
+ incomingRequests.remove(player);
+ outgoingRequests.remove(player);
+ }
+
+ public int getRequestExpiryTime() {
+ return requestExpiryTime;
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaRequestType.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaRequestType.java
new file mode 100644
index 0000000..23106cb
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaRequestType.java
@@ -0,0 +1,19 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+/**
+ * Represents the type of a TPA request.
+ */
+public enum TpaRequestType {
+ TPA("tpa"),
+ TPA_HERE("tpahere");
+
+ private final String name;
+
+ TpaRequestType(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaToggleManager.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaToggleManager.java
new file mode 100644
index 0000000..dc8ebe7
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaToggleManager.java
@@ -0,0 +1,109 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+import com.zetaplugins.essentialz.EssentialZ;
+import com.zetaplugins.zetacore.annotations.Manager;
+import com.zetaplugins.zetacore.annotations.PostManagerConstruct;
+
+import java.io.*;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Manages TPA toggle state for players.
+ */
+@Manager
+public class TpaToggleManager {
+
+ private final EssentialZ plugin;
+ private final Set disabledPlayers;
+ private final File toggleFile;
+
+ public TpaToggleManager(EssentialZ plugin) {
+ this.plugin = plugin;
+ this.disabledPlayers = new HashSet<>();
+ this.toggleFile = new File(plugin.getDataFolder(), "tpa-toggles.dat");
+ }
+
+ @PostManagerConstruct
+ public void init() {
+ loadToggles();
+ }
+
+ /**
+ * Checks if TPA is enabled for a player.
+ * @param player The player's UUID
+ * @return true if TPA is enabled for the player
+ */
+ public boolean isEnabled(UUID player) {
+ return !disabledPlayers.contains(player);
+ }
+
+ /**
+ * Toggles TPA state for a player.
+ * @param player The player's UUID
+ */
+ public void toggle(UUID player) {
+ if (disabledPlayers.contains(player)) {
+ disabledPlayers.remove(player);
+ } else {
+ disabledPlayers.add(player);
+ }
+ saveToggles();
+ }
+
+ /**
+ * Sets the TPA state for a player.
+ * @param player The player's UUID
+ * @param enabled Whether TPA should be enabled
+ */
+ public void setEnabled(UUID player, boolean enabled) {
+ if (enabled) {
+ disabledPlayers.remove(player);
+ } else {
+ disabledPlayers.add(player);
+ }
+ saveToggles();
+ }
+
+ /**
+ * Loads toggle states from file.
+ */
+ public void loadToggles() {
+ if (!toggleFile.exists()) return;
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(toggleFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ try {
+ UUID uuid = UUID.fromString(line.trim());
+ disabledPlayers.add(uuid);
+ } catch (IllegalArgumentException e) {
+ plugin.getLogger().warning("Invalid UUID in tpa-toggles.dat: " + line);
+ }
+ }
+ } catch (IOException e) {
+ plugin.getLogger().severe("Failed to load TPA toggles: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Saves toggle states to file.
+ */
+ public void saveToggles() {
+ try {
+ if (!toggleFile.exists()) {
+ toggleFile.getParentFile().mkdirs();
+ toggleFile.createNewFile();
+ }
+
+ try (PrintWriter writer = new PrintWriter(new FileWriter(toggleFile))) {
+ for (UUID uuid : disabledPlayers) {
+ writer.println(uuid.toString());
+ }
+ }
+ } catch (IOException e) {
+ plugin.getLogger().severe("Failed to save TPA toggles: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaUtils.java b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaUtils.java
new file mode 100644
index 0000000..61009ee
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/features/tpa/TpaUtils.java
@@ -0,0 +1,77 @@
+package com.zetaplugins.essentialz.features.tpa;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Sound;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility methods for TPA functionality.
+ */
+public final class TpaUtils {
+
+ private TpaUtils() {}
+
+ /**
+ * Finds a player by name with partial matching support.
+ * @param name The name to search for
+ * @return The matched player or null if no unique match found
+ */
+ public static Player findPlayer(String name) {
+ // Try exact match first
+ Player exact = Bukkit.getPlayerExact(name);
+ if (exact != null) return exact;
+
+ // Try partial match
+ List matches = new ArrayList<>();
+ String lowerName = name.toLowerCase();
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (player.getName().toLowerCase().startsWith(lowerName)) {
+ matches.add(player);
+ }
+ }
+
+ // Return if single match found
+ if (matches.size() == 1) {
+ return matches.get(0);
+ }
+
+ // Try contains match if no startsWith matches
+ if (matches.isEmpty()) {
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (player.getName().toLowerCase().contains(lowerName)) {
+ matches.add(player);
+ }
+ }
+
+ if (matches.size() == 1) {
+ return matches.get(0);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Plays a sound to a player if sounds are enabled.
+ * @param player The player to play the sound to
+ * @param soundName The name of the sound
+ * @param plugin The plugin instance
+ */
+ public static void playSound(Player player, String soundName, JavaPlugin plugin) {
+ if (player == null || soundName == null) return;
+
+ if (!plugin.getConfig().getBoolean("tpa.sounds.enabled", true)) return;
+
+ try {
+ Sound sound = Sound.valueOf(soundName);
+ player.playSound(player.getLocation(), sound, 1.0f, 1.0f);
+ } catch (IllegalArgumentException e) {
+ plugin.getLogger().warning("Invalid TPA sound: " + soundName);
+ }
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/listeners/TpaPlayerQuitListener.java b/src/main/java/com/zetaplugins/essentialz/listeners/TpaPlayerQuitListener.java
new file mode 100644
index 0000000..8084378
--- /dev/null
+++ b/src/main/java/com/zetaplugins/essentialz/listeners/TpaPlayerQuitListener.java
@@ -0,0 +1,23 @@
+package com.zetaplugins.essentialz.listeners;
+
+import com.zetaplugins.essentialz.features.tpa.TpaManager;
+import com.zetaplugins.zetacore.annotations.AutoRegisterListener;
+import com.zetaplugins.zetacore.annotations.InjectManager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+/**
+ * Listener for cleaning up TPA requests when players quit.
+ */
+@AutoRegisterListener
+public class TpaPlayerQuitListener implements Listener {
+
+ @InjectManager
+ private TpaManager tpaManager;
+
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ tpaManager.clearPlayerRequests(event.getPlayer().getUniqueId());
+ }
+}
diff --git a/src/main/java/com/zetaplugins/essentialz/util/PluginMessage.java b/src/main/java/com/zetaplugins/essentialz/util/PluginMessage.java
index 5e65c2e..01d5333 100644
--- a/src/main/java/com/zetaplugins/essentialz/util/PluginMessage.java
+++ b/src/main/java/com/zetaplugins/essentialz/util/PluginMessage.java
@@ -109,10 +109,24 @@ public enum PluginMessage {
GLOW_ENABLED("glowEnabled", "&7Glow effect enabled for {ac}{player}&7.", MessageStyle.FUN),
GLOW_DISABLED("glowDisabled", "&7Glow effect disabled for {ac}{player}&7.", MessageStyle.FUN),
DEATH_MESSAGE("deathMessage", "&8[&c☠&8] &7{message}", MessageStyle.NONE),
- /*
- * Still missing:
- * - communication
- */
+
+ // TPA Messages
+ TPA_REQUEST_SENT("tpaRequestSent", "&7Teleport request sent to {ac}{player}&7.", MessageStyle.MOVEMENT),
+ TPA_REQUEST_RECEIVED("tpaRequestReceived", "{ac}{player}&7 wants to {type}.", MessageStyle.MOVEMENT),
+ TPA_REQUEST_INSTRUCTIONS("tpaRequestInstructions", "&7Type {ac}/tpaccept &7to accept or {ac}/tpdeny &7to deny.", MessageStyle.MOVEMENT),
+ TPA_ACCEPTED_SENDER("tpaAcceptedSender", "{ac}{player}&7 accepted your teleport request.", MessageStyle.SUCCESS),
+ TPA_ACCEPTED_TARGET("tpaAcceptedTarget", "&7You accepted {ac}{player}&7's teleport request.", MessageStyle.SUCCESS),
+ TPA_DENIED_SENDER("tpaDeniedSender", "{ac}{player}&7 denied your teleport request.", MessageStyle.ERROR),
+ TPA_DENIED_TARGET("tpaDeniedTarget", "&7You denied the teleport request.", MessageStyle.MOVEMENT),
+ TPA_REQUEST_ALREADY_PENDING("tpaRequestAlreadyPending", "{ac}You already have a pending request to this player.", MessageStyle.ERROR),
+ TPA_NO_PENDING_REQUESTS("tpaNoPendingRequests", "{ac}You have no pending teleport requests.", MessageStyle.ERROR),
+ TPA_NO_OUTGOING_REQUESTS("tpaNoOutgoingRequests", "{ac}You have no outgoing teleport requests.", MessageStyle.ERROR),
+ TPA_REQUESTS_CANCELLED("tpaRequestsCancelled", "&7Cancelled {ac}{count}&7 outgoing request(s).", MessageStyle.MOVEMENT),
+ TPA_CANNOT_SELF("tpaCannotSelf", "{ac}You cannot teleport to yourself.", MessageStyle.ERROR),
+ TPA_DISABLED_TARGET("tpaDisabledTarget", "{ac}{player} has teleport requests disabled.", MessageStyle.ERROR),
+ TPA_TOGGLE_ENABLED("tpaToggleEnabled", "&7You have {ac}enabled&7 teleport requests.", MessageStyle.SUCCESS),
+ TPA_TOGGLE_DISABLED("tpaToggleDisabled", "&7You have {ac}disabled&7 teleport requests.", MessageStyle.WARNING),
+ TPA_REQUEST_EXPIRED("tpaRequestExpired", "&7Your teleport request to {ac}{player}&7 has expired.", MessageStyle.WARNING),
;
private final String key;
diff --git a/src/main/java/com/zetaplugins/essentialz/util/permissions/Permission.java b/src/main/java/com/zetaplugins/essentialz/util/permissions/Permission.java
index 92a2db2..f3b8f87 100644
--- a/src/main/java/com/zetaplugins/essentialz/util/permissions/Permission.java
+++ b/src/main/java/com/zetaplugins/essentialz/util/permissions/Permission.java
@@ -71,6 +71,12 @@ public enum Permission implements PermissionNode {
TIME("time", PermissionDefault.OP, "Allows the user to change the time in their world"),
LIGHTNING("lightning", PermissionDefault.OP, "Allows the user to strike lightning at a player's location"),
GLOW("glow", PermissionDefault.OP, "Allows the user to make themselves or another player glow"),
+ TPA("tpa", PermissionDefault.TRUE, "Allows the user to request to teleport to another player"),
+ TPAHERE("tpahere", PermissionDefault.TRUE, "Allows the user to request another player to teleport to them"),
+ TPACCEPT("tpaccept", PermissionDefault.TRUE, "Allows the user to accept teleport requests"),
+ TPDENY("tpdeny", PermissionDefault.TRUE, "Allows the user to deny teleport requests"),
+ TPACANCEL("tpacancel", PermissionDefault.TRUE, "Allows the user to cancel outgoing teleport requests"),
+ TPATOGGLE("tpatoggle", PermissionDefault.TRUE, "Allows the user to toggle receiving teleport requests"),
;
private final String node;
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 4b4ed71..c34c908 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -67,4 +67,27 @@ styles:
prefix: "&8[<#FF4081>🎉&8] "
# This is the server spawn location. Use the format: world,x,y,z,yaw,pitch. You can also use the /setspawn command to set it.
-spawnLocation: ""
\ No newline at end of file
+spawnLocation: ""
+
+# TPA (Teleport Ask) settings
+tpa:
+ # How long (in seconds) before a teleport request expires
+ requestExpiryTime: 120
+
+ # Sound settings for TPA events
+ sounds:
+ # Whether to play sounds for TPA events
+ enabled: true
+ # Sound played when a request is received
+ request: "ENTITY_EXPERIENCE_ORB_PICKUP"
+ # Sound played when a request is accepted
+ accept: "ENTITY_PLAYER_LEVELUP"
+ # Sound played when a request is denied
+ deny: "ENTITY_VILLAGER_NO"
+
+ # Bedrock Edition settings (requires Floodgate)
+ bedrock:
+ # Whether to use GUI forms for Bedrock players
+ formsEnabled: true
+ # Whether to show player heads in selection forms
+ showPlayerHeads: true
\ No newline at end of file
diff --git a/src/main/resources/lang/en-US.yml b/src/main/resources/lang/en-US.yml
index 635678c..6234144 100644
--- a/src/main/resources/lang/en-US.yml
+++ b/src/main/resources/lang/en-US.yml
@@ -146,4 +146,22 @@ invalidLightningAmount: "{ac}The amount of lightning strikes must be between 1 a
lightningStrikes: "&7Struck {ac}{player} &7with lightning {ac}{amount} &7time(s)."
glowEnabled: "&7Glow effect enabled for {ac}{player}&7."
glowDisabled: "&7Glow effect disabled for {ac}{player}&7."
-deathMessage: "&8[&c☠&8] &7{message}"
\ No newline at end of file
+deathMessage: "&8[&c☠&8] &7{message}"
+
+# TPA Messages
+tpaRequestSent: "&7Teleport request sent to {ac}{player}&7."
+tpaRequestReceived: "{ac}{player}&7 wants to {type}."
+tpaRequestInstructions: "&7Type {ac}/tpaccept &7to accept or {ac}/tpdeny &7to deny."
+tpaAcceptedSender: "{ac}{player}&7 accepted your teleport request."
+tpaAcceptedTarget: "&7You accepted {ac}{player}&7's teleport request."
+tpaDeniedSender: "{ac}{player}&7 denied your teleport request."
+tpaDeniedTarget: "&7You denied the teleport request."
+tpaRequestAlreadyPending: "{ac}You already have a pending request to this player."
+tpaNoPendingRequests: "{ac}You have no pending teleport requests."
+tpaNoOutgoingRequests: "{ac}You have no outgoing teleport requests."
+tpaRequestsCancelled: "&7Cancelled {ac}{count}&7 outgoing request(s)."
+tpaCannotSelf: "{ac}You cannot teleport to yourself."
+tpaDisabledTarget: "{ac}{player} has teleport requests disabled."
+tpaToggleEnabled: "&7You have {ac}enabled&7 teleport requests."
+tpaToggleDisabled: "&7You have {ac}disabled&7 teleport requests."
+tpaRequestExpired: "&7Your teleport request to {ac}{player}&7 has expired."
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 9500bfa..e201623 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -10,3 +10,4 @@ website: https://strassburger.org
softdepend:
- PlaceholderAPI
- Vault
+ - floodgate
diff --git a/src/main/resources/storage.yml b/src/main/resources/storage.yml
index a1f7d1b..232cd0c 100644
--- a/src/main/resources/storage.yml
+++ b/src/main/resources/storage.yml
@@ -1,7 +1,7 @@
# === Storage ===
# The type of storage to use. You have the following options:
-# "SQLite", "MySQL", "MariaDB"
+# "SQLite", "MySQL"
type: "SQLite"
# This section is only relevant if you use a MySQL database