diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 8c67165..29b35de 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -6,6 +6,7 @@ import cc.reconnected.server.events.PlayerActivityEvents; import cc.reconnected.server.events.PlayerWelcome; import cc.reconnected.server.events.Ready; import cc.reconnected.server.http.ServiceServer; +import cc.reconnected.server.struct.ServerPosition; import cc.reconnected.server.trackers.AfkTracker; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; @@ -28,6 +29,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Date; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; public class RccServer implements ModInitializer { @@ -80,15 +84,22 @@ public class RccServer implements ModInitializer { INSTANCE = this; } + public static final ConcurrentHashMap> teleportRequests = new ConcurrentHashMap<>(); + public static final ConcurrentHashMap lastPlayerPositions = new ConcurrentHashMap<>(); + @Override public void onInitialize() { - LOGGER.info("Starting rcc-server"); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { AfkCommand.register(dispatcher, registryAccess, environment); TellCommand.register(dispatcher, registryAccess, environment); ReplyCommand.register(dispatcher, registryAccess, environment); + TeleportAskCommand.register(dispatcher, registryAccess, environment); + TeleportAskHereCommand.register(dispatcher, registryAccess, environment); + TeleportAcceptCommand.register(dispatcher, registryAccess, environment); + TeleportDenyCommand.register(dispatcher, registryAccess, environment); + BackCommand.register(dispatcher, registryAccess, environment); FlyCommand.register(dispatcher, registryAccess, environment); GodCommand.register(dispatcher, registryAccess, environment); }); @@ -112,6 +123,14 @@ public class RccServer implements ModInitializer { if (currentMspt != 0) { currentTps = Math.min(20, 1000 / currentMspt); } + + teleportRequests.forEach((recipient, requestList) -> { + requestList.forEach(request -> { + if (request.remainingTicks-- == 0) { + requestList.remove(request); + } + }); + }); }); ServerLifecycleEvents.SERVER_STOPPING.register(server -> { @@ -136,10 +155,15 @@ public class RccServer implements ModInitializer { PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, playerData, server); LOGGER.info("Player {} joined for the first time!", player.getName().getString()); } + + teleportRequests.put(player.getUuid(), new ConcurrentLinkedDeque<>()); }); ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { currentPlayerCount = server.getCurrentPlayerCount() - 1; + + teleportRequests.remove(handler.getPlayer().getUuid()); + lastPlayerPositions.remove(handler.getPlayer().getUuid()); }); PlayerActivityEvents.AFK.register((player, server) -> { diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index 331cc2d..64ab583 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -14,4 +14,6 @@ public class RccServerConfigModel { public String tellMessage = "[ ] "; public String tellMessageSpy = "\uD83D\uDC41 [] "; + + public int teleportRequestTimeout = 120; } diff --git a/src/main/java/cc/reconnected/server/commands/BackCommand.java b/src/main/java/cc/reconnected/server/commands/BackCommand.java new file mode 100644 index 0000000..f37cc81 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/BackCommand.java @@ -0,0 +1,41 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.struct.ServerPosition; +import com.mojang.brigadier.CommandDispatcher; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.literal; + +public class BackCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("back") + .requires(Permissions.require("rcc.command.back", true)) + .executes(context -> { + if(!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var player = context.getSource().getPlayer(); + + var lastPosition = RccServer.lastPlayerPositions.get(player.getUuid()); + if(lastPosition == null) { + context.getSource().sendFeedback(() -> Text.literal("There is no position to return back to.").formatted(Formatting.RED), false); + return 1; + } + + context.getSource().sendFeedback(() -> Text.literal("Teleporting to previous position...").formatted(Formatting.GOLD), false); + lastPosition.teleport(player); + + return 1; + }); + + dispatcher.register(rootCommand); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/TeleportAcceptCommand.java b/src/main/java/cc/reconnected/server/commands/TeleportAcceptCommand.java new file mode 100644 index 0000000..11b85a3 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/TeleportAcceptCommand.java @@ -0,0 +1,100 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.UuidArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.ArrayList; +import java.util.HashMap; + +import static net.minecraft.server.command.CommandManager.*; + +public class TeleportAcceptCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var node = dispatcher.register(literal("tpaccept") + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var playerUuid = context.getSource().getPlayer().getUuid(); + var playerRequests = RccServer.teleportRequests.get(playerUuid); + + + var request = playerRequests.pollLast(); + + if (request == null) { + context.getSource().sendFeedback(() -> Text.literal("You have no pending teleport requests.").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return 1; + } + + execute(context, request); + + return 1; + }) + .then(argument("uuid", UuidArgumentType.uuid()) + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var uuid = UuidArgumentType.getUuid(context, "uuid"); + var playerUuid = context.getSource().getPlayer().getUuid(); + var playerRequests = RccServer.teleportRequests.get(playerUuid); + + var request = playerRequests.stream().filter(req -> req.requestId.equals(uuid)).findFirst().orElse(null); + if (request == null) { + context.getSource().sendFeedback(() -> Text.literal("This request expired or is no longer available.").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return 1; + } + + execute(context, request); + + return 1; + }))); + + dispatcher.register(literal("tpyes").redirect(node)); + } + + private static void execute(CommandContext context, TeleportAskCommand.TeleportRequest request) { + var source = context.getSource(); + request.expire(); + + var player = source.getPlayer(); + + var playerManager = context.getSource().getServer().getPlayerManager(); + + var sourcePlayer = playerManager.getPlayer(request.player); + var targetPlayer = playerManager.getPlayer(request.target); + + if (sourcePlayer == null || targetPlayer == null) { + context.getSource().sendFeedback(() -> Text.literal("The other player is no longer available.").formatted(Formatting.RED), false); + return; + } + + if(player.getUuid().equals(request.target)) { + // accepted a tpa from other to self + context.getSource().sendFeedback(() -> Text.literal("Teleport request accepted.").formatted(Formatting.GREEN), false); + sourcePlayer.sendMessage(Text.literal("Teleporting...").formatted(Formatting.GOLD), false); + } else { + // accepted a tpa from self to other + context.getSource().sendFeedback(() -> Text.literal("Teleporting...").formatted(Formatting.GOLD), false); + targetPlayer.sendMessage(Text.empty().append(player.getDisplayName()).append(Text.literal(" accepted your teleport request.").formatted(Formatting.GREEN)), false); + } + + TeleportAskCommand.teleport(sourcePlayer, targetPlayer); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java b/src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java new file mode 100644 index 0000000..11db5c6 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java @@ -0,0 +1,128 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.struct.ServerPosition; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.time.Duration; +import java.util.UUID; + +import static net.minecraft.server.command.CommandManager.*; + +public class TeleportAskCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var node = dispatcher.register(literal("tpa") + .then(argument("player", StringArgumentType.word()) + .suggests((context, builder) -> { + var playerManager = context.getSource().getServer().getPlayerManager(); + return CommandSource.suggestMatching( + playerManager.getPlayerNames(), + builder); + }) + .executes(context -> { + execute(context); + return 1; + }))); + + dispatcher.register(literal("tpask").redirect(node)); + } + + private static void execute(CommandContext context) { + var source = context.getSource(); + if (!source.isExecutedByPlayer()) { + source.sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return; + } + + var server = source.getServer(); + var player = source.getPlayer(); + var targetName = StringArgumentType.getString(context, "player"); + var playerManager = server.getPlayerManager(); + var target = playerManager.getPlayer(targetName); + if (target == null) { + source.sendFeedback(() -> Text.literal("Player \"" + targetName + "\" not found!").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return; + } + + var request = new TeleportRequest(player.getUuid(), target.getUuid()); + var targetRequests = RccServer.teleportRequests.get(target.getUuid()); + targetRequests.addLast(request); + + var requestMessage = Component.empty() + .append(player.getDisplayName()) + .appendSpace() + .append(Component.text("requested to teleport to you.", NamedTextColor.GOLD)) + .appendNewline().appendSpace() + .append(makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) + .appendSpace() + .append(makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); + + target.sendMessage(requestMessage); + + source.sendFeedback(() -> Text.literal("Teleport request sent.").setStyle(Style.EMPTY.withColor(Formatting.GREEN)), false); + } + + public static Component makeButton(ComponentLike text, ComponentLike hoverText, String command) { + var options = ClickCallback.Options.builder() + .uses(1) + .lifetime(Duration.ofSeconds(RccServer.CONFIG.teleportRequestTimeout())) + .build(); + + return Component.empty() + .append(Component.text("[")) + .append(text) + .append(Component.text("]")) + .color(NamedTextColor.AQUA) + .hoverEvent(HoverEvent.showText(hoverText)) + .clickEvent(ClickEvent.runCommand(command)); + } + + public static void teleport(ServerPlayerEntity sourcePlayer, ServerPlayerEntity targetPlayer) { + RccServer.lastPlayerPositions.put(sourcePlayer.getUuid(), new ServerPosition(sourcePlayer)); + sourcePlayer.teleport( + targetPlayer.getServerWorld(), + targetPlayer.getX(), + targetPlayer.getY(), + targetPlayer.getZ(), + targetPlayer.getYaw(), + targetPlayer.getPitch() + ); + } + + public static class TeleportRequest { + public UUID requestId = UUID.randomUUID(); + public UUID player; + public UUID target; + public int remainingTicks; + + public TeleportRequest(UUID player, UUID target) { + this.player = player; + this.target = target; + // Seconds in config per 20 ticks + this.remainingTicks = RccServer.CONFIG.teleportRequestTimeout() * 20; + } + + public void expire() { + remainingTicks = 0; + } + } + +} diff --git a/src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java b/src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java new file mode 100644 index 0000000..ac44dcb --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java @@ -0,0 +1,72 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class TeleportAskHereCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var node = dispatcher.register(literal("tpahere") + .then(argument("player", StringArgumentType.word()) + .suggests((context, builder) -> { + var playerManager = context.getSource().getServer().getPlayerManager(); + return CommandSource.suggestMatching( + playerManager.getPlayerNames(), + builder); + }) + .executes(context -> { + execute(context); + return 1; + }))); + + dispatcher.register(literal("tpaskhere").redirect(node)); + } + + private static void execute(CommandContext context) { + var source = context.getSource(); + if (!source.isExecutedByPlayer()) { + source.sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return; + } + + var server = source.getServer(); + var player = source.getPlayer(); + var targetName = StringArgumentType.getString(context, "player"); + var playerManager = server.getPlayerManager(); + var target = playerManager.getPlayer(targetName); + if (target == null) { + source.sendFeedback(() -> Text.literal("Player \"" + targetName + "\" not found!").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return; + } + + var request = new TeleportAskCommand.TeleportRequest(target.getUuid(), player.getUuid()); + var targetRequests = RccServer.teleportRequests.get(target.getUuid()); + targetRequests.addLast(request); + + var requestMessage = Component.empty() + .append(player.getDisplayName()) + .appendSpace() + .append(Component.text("requested you to teleport to them.", NamedTextColor.GOLD)) + .appendNewline().appendSpace() + .append(TeleportAskCommand.makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) + .appendSpace() + .append(TeleportAskCommand.makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); + + target.sendMessage(requestMessage); + + source.sendFeedback(() -> Text.literal("Teleport request sent.").setStyle(Style.EMPTY.withColor(Formatting.GREEN)), false); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java b/src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java new file mode 100644 index 0000000..05d59e8 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java @@ -0,0 +1,89 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.argument.UuidArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.ArrayList; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class TeleportDenyCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var node = dispatcher.register(literal("tpdeny") + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var playerUuid = context.getSource().getPlayer().getUuid(); + var playerRequests = RccServer.teleportRequests.get(playerUuid); + + var request = playerRequests.pollLast(); + + if (request == null) { + context.getSource().sendFeedback(() -> Text.literal("You have no pending teleport requests.").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return 1; + } + + execute(context, request); + + return 1; + }) + .then(argument("uuid", UuidArgumentType.uuid()) + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var uuid = UuidArgumentType.getUuid(context, "uuid"); + var playerUuid = context.getSource().getPlayer().getUuid(); + var playerRequests = RccServer.teleportRequests.get(playerUuid); + + var request = playerRequests.stream().filter(req -> req.requestId.equals(uuid)).findFirst().orElse(null); + if (request == null) { + context.getSource().sendFeedback(() -> Text.literal("This request expired or is no longer available.").setStyle(Style.EMPTY.withColor(Formatting.RED)), false); + return 1; + } + + execute(context, request); + + return 1; + }))); + + dispatcher.register(literal("tpno").redirect(node)); + dispatcher.register(literal("tprefuse").redirect(node)); + } + + private static void execute(CommandContext context, TeleportAskCommand.TeleportRequest request) { + var source = context.getSource(); + request.expire(); + + var player = source.getPlayer(); + + var playerManager = context.getSource().getServer().getPlayerManager(); + + ServerPlayerEntity otherPlayer = null; + if (player.getUuid().equals(request.target)) { + otherPlayer = playerManager.getPlayer(request.player); + } else if (player.getUuid().equals(request.player)) { + otherPlayer = playerManager.getPlayer(request.target); + } + + if(otherPlayer != null) { + otherPlayer.sendMessage(Text.empty().append(player.getDisplayName()).append(Text.literal(" denied your teleport request.").formatted(Formatting.RED))); + } + context.getSource().sendFeedback(() -> Text.literal("You denied the teleport request.").formatted(Formatting.GOLD), false); + } +} diff --git a/src/main/java/cc/reconnected/server/struct/ServerPosition.java b/src/main/java/cc/reconnected/server/struct/ServerPosition.java new file mode 100644 index 0000000..ead4b35 --- /dev/null +++ b/src/main/java/cc/reconnected/server/struct/ServerPosition.java @@ -0,0 +1,46 @@ +package cc.reconnected.server.struct; + +import cc.reconnected.server.RccServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +public class ServerPosition { + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public ServerWorld world; + + public ServerPosition(double x, double y, double z, float yaw, float pitch, ServerWorld world) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.world = world; + } + + public ServerPosition(ServerPlayerEntity player) { + this.x = player.getX(); + this.y = player.getY(); + this.z = player.getZ(); + this.yaw = player.getYaw(); + this.pitch = player.getPitch(); + this.world = player.getServerWorld(); + } + + public void teleport(ServerPlayerEntity player) { + var currentPosition = new ServerPosition(player); + RccServer.lastPlayerPositions.put(player.getUuid(), currentPosition); + + player.teleport( + this.world, + this.x, + this.y, + this.z, + this.yaw, + this.pitch + ); + } +}