diff --git a/gradle.properties b/gradle.properties index 7aa24fa..fe295a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.1+build.10 loader_version=0.16.7 # Mod Properties -mod_version=1.13.0 +mod_version=1.14.0 maven_group=cc.reconnected archives_base_name=rcc-server diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 01f5f15..e7bece2 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -1,8 +1,15 @@ package cc.reconnected.server; -import cc.reconnected.server.commands.*; +import cc.reconnected.server.commands.home.*; +import cc.reconnected.server.commands.misc.*; +import cc.reconnected.server.commands.spawn.*; +import cc.reconnected.server.commands.teleport.*; +import cc.reconnected.server.commands.tell.*; +import cc.reconnected.server.commands.warp.*; import cc.reconnected.server.core.*; +import cc.reconnected.server.data.StateManager; import cc.reconnected.server.database.PlayerData; +import cc.reconnected.server.events.PlayerUsernameChange; import cc.reconnected.server.events.PlayerWelcome; import cc.reconnected.server.events.Ready; import net.fabricmc.api.ModInitializer; @@ -17,6 +24,7 @@ import net.luckperms.api.LuckPermsProvider; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; +import net.minecraft.util.WorldSavePath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +37,8 @@ public class RccServer implements ModInitializer { public static final cc.reconnected.server.RccServerConfig CONFIG = cc.reconnected.server.RccServerConfig.createAndLoad(); + public static final StateManager state = new StateManager(); + private static RccServer INSTANCE; public static RccServer getInstance() { @@ -59,21 +69,40 @@ public class RccServer implements ModInitializer { public void onInitialize() { LOGGER.info("Starting rcc-server"); - ServerLifecycleEvents.SERVER_STARTING.register(server -> this.adventure = FabricServerAudiences.of(server)); + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + state.register(server.getSavePath(WorldSavePath.ROOT).resolve("data").resolve(RccServer.MOD_ID)); + this.adventure = FabricServerAudiences.of(server); + }); ServerLifecycleEvents.SERVER_STOPPED.register(server -> this.adventure = null); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { RccCommand.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); + + SetSpawnCommand.register(dispatcher, registryAccess, environment); + SpawnCommand.register(dispatcher, registryAccess, environment); + + HomeCommand.register(dispatcher, registryAccess, environment); + SetHomeCommand.register(dispatcher, registryAccess, environment); + DeleteHomeCommand.register(dispatcher, registryAccess, environment); + + WarpCommand.register(dispatcher, registryAccess, environment); + SetWarpCommand.register(dispatcher, registryAccess, environment); + DeleteWarpCommand.register(dispatcher, registryAccess, environment); }); AfkTracker.register(); @@ -89,25 +118,32 @@ public class RccServer implements ModInitializer { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { var player = handler.getPlayer(); - var playerData = PlayerData.getPlayer(player.getUuid()); - playerData.set(PlayerData.KEYS.username, player.getName().getString()); - var firstJoinedDate = playerData.getDate(PlayerData.KEYS.firstJoinedDate); - boolean isNewPlayer = false; - if (firstJoinedDate == null) { - playerData.setDate(PlayerData.KEYS.firstJoinedDate, new Date()); - isNewPlayer = true; - } - if (isNewPlayer) { - PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, playerData, server); - LOGGER.info("Player {} joined for the first time!", player.getName().getString()); - } - }); - } - public void broadcastMessage(MinecraftServer server, Text message) { - for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { - player.sendMessage(message, false); - } + var playerState = state.getPlayerState(player.getUuid()); + var playerData = PlayerData.getPlayer(player.getUuid()); + + if (playerState.firstJoinedDate == null) { + LOGGER.info("Player {} joined for the first time!", player.getGameProfile().getName()); + playerState.firstJoinedDate = new Date(); + PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, server); + var serverState = state.getServerState(); + var spawnPosition = serverState.spawn; + + if (spawnPosition != null) { + spawnPosition.teleport(player, false); + } + + playerData.setDate(PlayerData.KEYS.firstJoinedDate, new Date()); + } + + if (playerState.username != null && !playerState.username.equals(player.getGameProfile().getName())) { + LOGGER.info("Player {} has changed their username from {}", player.getGameProfile().getName(), playerState.username); + PlayerUsernameChange.PLAYER_USERNAME_CHANGE.invoker().changeUsername(player, playerState.username); + playerData.set(PlayerData.KEYS.username, player.getName().getString()); + } + playerState.username = player.getGameProfile().getName(); + state.savePlayerState(player.getUuid(), playerState); + }); } public void broadcastMessage(MinecraftServer server, Component message) { diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index eade7b2..b18b853 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -28,4 +28,6 @@ public class RccServerConfigModel { public ArrayList tabFooter = new ArrayList<>(List.of( " " )); + + public String playerTabName = "%player:displayname_visual% %player:playtime%"; } diff --git a/src/main/java/cc/reconnected/server/commands/home/DeleteHomeCommand.java b/src/main/java/cc/reconnected/server/commands/home/DeleteHomeCommand.java new file mode 100644 index 0000000..5f3c1fb --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/home/DeleteHomeCommand.java @@ -0,0 +1,64 @@ +package cc.reconnected.server.commands.home; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +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.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class DeleteHomeCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("delhome") + .requires(Permissions.require("rcc.command.delhome", true)) + .executes(context -> execute(context, "home")) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isExecutedByPlayer()) + return CommandSource.suggestMatching(new String[]{}, builder); + + var playerState = RccServer.state.getPlayerState(context.getSource().getPlayer().getUuid()); + return CommandSource.suggestMatching(playerState.homes.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name) { + 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 playerState = RccServer.state.getPlayerState(player.getUuid()); + var homes = playerState.homes; + + if (!homes.containsKey(name)) { + context.getSource().sendFeedback(() -> Text.literal("The home ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" does not exist!") + .formatted(Formatting.RED), false); + return 1; + } + + homes.remove(name); + RccServer.state.savePlayerState(player.getUuid(), playerState); + + context.getSource().sendFeedback(() -> Text + .literal("Home ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" deleted!") + .formatted(Formatting.GREEN), false); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/home/HomeCommand.java b/src/main/java/cc/reconnected/server/commands/home/HomeCommand.java new file mode 100644 index 0000000..dcdd39c --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/home/HomeCommand.java @@ -0,0 +1,63 @@ +package cc.reconnected.server.commands.home; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +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.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.*; + +public class HomeCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("home") + .requires(Permissions.require("rcc.command.home", true)) + .executes(context -> execute(context, "home")) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isExecutedByPlayer()) + return CommandSource.suggestMatching(new String[]{}, builder); + + var playerState = RccServer.state.getPlayerState(context.getSource().getPlayer().getUuid()); + return CommandSource.suggestMatching(playerState.homes.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name) { + 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 playerState = RccServer.state.getPlayerState(player.getUuid()); + var homes = playerState.homes; + + if (!homes.containsKey(name)) { + context.getSource().sendFeedback(() -> Text.literal("The home ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" does not exist!") + .formatted(Formatting.RED), false); + return 1; + } + + context.getSource().sendFeedback(() -> Text + .literal("Teleporting to ") + .append(Text.literal(name).formatted(Formatting.GREEN)) + .append("...") + .formatted(Formatting.GOLD), false); + + var homePosition = homes.get(name); + homePosition.teleport(player); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/home/SetHomeCommand.java b/src/main/java/cc/reconnected/server/commands/home/SetHomeCommand.java new file mode 100644 index 0000000..623ca6a --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/home/SetHomeCommand.java @@ -0,0 +1,76 @@ +package cc.reconnected.server.commands.home; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.struct.ServerPosition; +import cc.reconnected.server.util.Components; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +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.*; + +public class SetHomeCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("sethome") + .requires(Permissions.require("rcc.command.sethome", true)) + .executes(context -> execute(context, + "home", + false)) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, + StringArgumentType.getString(context, "name"), + false)) + .then(argument("force", BoolArgumentType.bool()) + .executes(context -> execute(context, + StringArgumentType.getString(context, "name"), + BoolArgumentType.getBool(context, "force"))))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name, boolean forced) { + 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 playerState = RccServer.state.getPlayerState(player.getUuid()); + + var homes = playerState.homes; + + if (homes.containsKey(name) && !forced) { + var text = Component.text("You already have set this home.") + .appendNewline().appendSpace() + .append(Components.makeButton( + Component.text("Force set home", NamedTextColor.GOLD), + Component.text("Click to force set the home"), + "/sethome " + name + " true" + )); + + context.getSource().sendFailure(text); + + return 1; + } + + var homePosition = new ServerPosition(player); + homes.put(name, homePosition); + + RccServer.state.savePlayerState(player.getUuid(), playerState); + + context.getSource().sendFeedback(() -> Text.literal("New home ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" set!") + .formatted(Formatting.GREEN), false); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/AfkCommand.java b/src/main/java/cc/reconnected/server/commands/misc/AfkCommand.java similarity index 96% rename from src/main/java/cc/reconnected/server/commands/AfkCommand.java rename to src/main/java/cc/reconnected/server/commands/misc/AfkCommand.java index 50886b9..c65bd20 100644 --- a/src/main/java/cc/reconnected/server/commands/AfkCommand.java +++ b/src/main/java/cc/reconnected/server/commands/misc/AfkCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.misc; import cc.reconnected.server.core.AfkTracker; import com.mojang.brigadier.CommandDispatcher; diff --git a/src/main/java/cc/reconnected/server/commands/BackCommand.java b/src/main/java/cc/reconnected/server/commands/misc/BackCommand.java similarity index 97% rename from src/main/java/cc/reconnected/server/commands/BackCommand.java rename to src/main/java/cc/reconnected/server/commands/misc/BackCommand.java index 6a3ac6b..6038079 100644 --- a/src/main/java/cc/reconnected/server/commands/BackCommand.java +++ b/src/main/java/cc/reconnected/server/commands/misc/BackCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.misc; import cc.reconnected.server.core.BackTracker; import com.mojang.brigadier.CommandDispatcher; diff --git a/src/main/java/cc/reconnected/server/commands/FlyCommand.java b/src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java similarity index 96% rename from src/main/java/cc/reconnected/server/commands/FlyCommand.java rename to src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java index 6375454..97198bb 100644 --- a/src/main/java/cc/reconnected/server/commands/FlyCommand.java +++ b/src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java @@ -1,8 +1,6 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.misc; -import cc.reconnected.server.RccServer; import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.FloatArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.command.CommandRegistryAccess; diff --git a/src/main/java/cc/reconnected/server/commands/GodCommand.java b/src/main/java/cc/reconnected/server/commands/misc/GodCommand.java similarity index 98% rename from src/main/java/cc/reconnected/server/commands/GodCommand.java rename to src/main/java/cc/reconnected/server/commands/misc/GodCommand.java index 30d687e..969c8eb 100644 --- a/src/main/java/cc/reconnected/server/commands/GodCommand.java +++ b/src/main/java/cc/reconnected/server/commands/misc/GodCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.misc; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; diff --git a/src/main/java/cc/reconnected/server/commands/RccCommand.java b/src/main/java/cc/reconnected/server/commands/misc/RccCommand.java similarity index 97% rename from src/main/java/cc/reconnected/server/commands/RccCommand.java rename to src/main/java/cc/reconnected/server/commands/misc/RccCommand.java index e9c62ca..948e66c 100644 --- a/src/main/java/cc/reconnected/server/commands/RccCommand.java +++ b/src/main/java/cc/reconnected/server/commands/misc/RccCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.misc; import cc.reconnected.server.RccServer; import com.mojang.brigadier.CommandDispatcher; diff --git a/src/main/java/cc/reconnected/server/commands/spawn/SetSpawnCommand.java b/src/main/java/cc/reconnected/server/commands/spawn/SetSpawnCommand.java new file mode 100644 index 0000000..2962ebc --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/spawn/SetSpawnCommand.java @@ -0,0 +1,42 @@ +package cc.reconnected.server.commands.spawn; + +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 SetSpawnCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("setspawn") + .requires(Permissions.require("rcc.command.setspawn", 3)) + .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 spawnPosition = new ServerPosition(player); + + var serverState = RccServer.state.getServerState(); + serverState.spawn = spawnPosition; + RccServer.state.saveServerState(); + + context.getSource().sendFeedback(() -> Text.literal("Server spawn set to ") + .append(Text.literal(String.format("%.1f %.1f %.1f", spawnPosition.x, spawnPosition.y, spawnPosition.z)) + .formatted(Formatting.GOLD)) + .formatted(Formatting.GREEN), true); + + return 1; + }); + + dispatcher.register(rootCommand); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/spawn/SpawnCommand.java b/src/main/java/cc/reconnected/server/commands/spawn/SpawnCommand.java new file mode 100644 index 0000000..df88fa4 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/spawn/SpawnCommand.java @@ -0,0 +1,42 @@ +package cc.reconnected.server.commands.spawn; + +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 SpawnCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("spawn") + .requires(Permissions.require("rcc.command.spawn", 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 serverState = RccServer.state.getServerState(); + var spawnPosition = serverState.spawn; + if(spawnPosition == null) { + var server = context.getSource().getServer(); + var spawnPos = server.getOverworld().getSpawnPos(); + spawnPosition = new ServerPosition(spawnPos.getX(), spawnPos.getY(), spawnPos.getZ(), 0, 0, server.getOverworld()); + } + + context.getSource().sendFeedback(() -> Text.literal("Teleporting to spawn...").formatted(Formatting.GOLD), false); + spawnPosition.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/teleport/TeleportAcceptCommand.java similarity index 95% rename from src/main/java/cc/reconnected/server/commands/TeleportAcceptCommand.java rename to src/main/java/cc/reconnected/server/commands/teleport/TeleportAcceptCommand.java index 17621c3..bf3b4e2 100644 --- a/src/main/java/cc/reconnected/server/commands/TeleportAcceptCommand.java +++ b/src/main/java/cc/reconnected/server/commands/teleport/TeleportAcceptCommand.java @@ -1,6 +1,7 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.teleport; import cc.reconnected.server.core.TeleportTracker; +import cc.reconnected.server.struct.ServerPosition; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; import net.minecraft.command.CommandRegistryAccess; @@ -88,6 +89,7 @@ public class TeleportAcceptCommand { targetPlayer.sendMessage(Text.empty().append(player.getDisplayName()).append(Text.literal(" accepted your teleport request.").formatted(Formatting.GREEN)), false); } - TeleportTracker.teleport(sourcePlayer, targetPlayer); + var targetPosition = new ServerPosition(targetPlayer); + targetPosition.teleport(sourcePlayer); } } diff --git a/src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java b/src/main/java/cc/reconnected/server/commands/teleport/TeleportAskCommand.java similarity index 87% rename from src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java rename to src/main/java/cc/reconnected/server/commands/teleport/TeleportAskCommand.java index 3a76565..bb8d217 100644 --- a/src/main/java/cc/reconnected/server/commands/TeleportAskCommand.java +++ b/src/main/java/cc/reconnected/server/commands/teleport/TeleportAskCommand.java @@ -1,6 +1,7 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.teleport; import cc.reconnected.server.core.TeleportTracker; +import cc.reconnected.server.util.Components; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; @@ -60,9 +61,9 @@ public class TeleportAskCommand { .appendSpace() .append(Component.text("requested to teleport to you.", NamedTextColor.GOLD)) .appendNewline().appendSpace() - .append(TeleportTracker.makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) + .append(Components.makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) .appendSpace() - .append(TeleportTracker.makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); + .append(Components.makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); target.sendMessage(requestMessage); diff --git a/src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java b/src/main/java/cc/reconnected/server/commands/teleport/TeleportAskHereCommand.java similarity index 87% rename from src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java rename to src/main/java/cc/reconnected/server/commands/teleport/TeleportAskHereCommand.java index 14ab56f..b7e043d 100644 --- a/src/main/java/cc/reconnected/server/commands/TeleportAskHereCommand.java +++ b/src/main/java/cc/reconnected/server/commands/teleport/TeleportAskHereCommand.java @@ -1,6 +1,7 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.teleport; import cc.reconnected.server.core.TeleportTracker; +import cc.reconnected.server.util.Components; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; @@ -61,9 +62,9 @@ public class TeleportAskHereCommand { .appendSpace() .append(Component.text("requested you to teleport to them.", NamedTextColor.GOLD)) .appendNewline().appendSpace() - .append(TeleportTracker.makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) + .append(Components.makeButton(Component.text("Accept", NamedTextColor.GREEN), Component.text("Click to accept request"), "/tpaccept " + request.requestId)) .appendSpace() - .append(TeleportTracker.makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); + .append(Components.makeButton(Component.text("Refuse", NamedTextColor.RED), Component.text("Click to refuse request"), "/tpdeny " + request.requestId)); target.sendMessage(requestMessage); diff --git a/src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java b/src/main/java/cc/reconnected/server/commands/teleport/TeleportDenyCommand.java similarity index 98% rename from src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java rename to src/main/java/cc/reconnected/server/commands/teleport/TeleportDenyCommand.java index b10a970..0890e1f 100644 --- a/src/main/java/cc/reconnected/server/commands/TeleportDenyCommand.java +++ b/src/main/java/cc/reconnected/server/commands/teleport/TeleportDenyCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.teleport; import cc.reconnected.server.core.TeleportTracker; import com.mojang.brigadier.CommandDispatcher; diff --git a/src/main/java/cc/reconnected/server/commands/ReplyCommand.java b/src/main/java/cc/reconnected/server/commands/tell/ReplyCommand.java similarity index 97% rename from src/main/java/cc/reconnected/server/commands/ReplyCommand.java rename to src/main/java/cc/reconnected/server/commands/tell/ReplyCommand.java index fb09b9d..24ccd44 100644 --- a/src/main/java/cc/reconnected/server/commands/ReplyCommand.java +++ b/src/main/java/cc/reconnected/server/commands/tell/ReplyCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.tell; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; diff --git a/src/main/java/cc/reconnected/server/commands/TellCommand.java b/src/main/java/cc/reconnected/server/commands/tell/TellCommand.java similarity index 82% rename from src/main/java/cc/reconnected/server/commands/TellCommand.java rename to src/main/java/cc/reconnected/server/commands/tell/TellCommand.java index e424588..9776127 100644 --- a/src/main/java/cc/reconnected/server/commands/TellCommand.java +++ b/src/main/java/cc/reconnected/server/commands/tell/TellCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands; +package cc.reconnected.server.commands.tell; import cc.reconnected.server.RccServer; import cc.reconnected.server.parser.MarkdownParser; @@ -6,6 +6,9 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import me.lucko.fabric.api.permissions.v0.Permissions; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.minecraft.command.CommandRegistryAccess; @@ -67,6 +70,17 @@ public class TellCommand { } var parsedMessage = MarkdownParser.defaultParser.parseNode(message); + var you = Component.text("You", NamedTextColor.GRAY, TextDecoration.ITALIC); + var sourceText = MiniMessage.miniMessage().deserialize(RccServer.CONFIG.tellMessage(), + Placeholder.component("source", you), + Placeholder.component("target", targetDisplayName), + Placeholder.component("message", parsedMessage.toText())); + + var targetText = MiniMessage.miniMessage().deserialize(RccServer.CONFIG.tellMessage(), + Placeholder.component("source", source.getDisplayName()), + Placeholder.component("target", you), + Placeholder.component("message", parsedMessage.toText())); + var text = MiniMessage.miniMessage().deserialize(RccServer.CONFIG.tellMessage(), Placeholder.component("source", source.getDisplayName()), Placeholder.component("target", targetDisplayName), @@ -76,16 +90,16 @@ public class TellCommand { lastSender.put(source.getName(), targetName); if (!source.getName().equals(targetName)) { - source.sendMessage(text); + source.sendMessage(sourceText); } if (targetPlayer != null) { - targetPlayer.sendMessage(text); + targetPlayer.sendMessage(targetText); if (source.isExecutedByPlayer()) { source.getServer().sendMessage(text); } } else { // avoid duped message - source.getServer().sendMessage(text); + source.getServer().sendMessage(targetText); } var lp = RccServer.getInstance().luckPerms(); @@ -103,7 +117,6 @@ public class TellCommand { if (playerPerms.checkPermission("rcc.tell.spy").asBoolean()) { player.sendMessage(spyText); } - ; }); } } diff --git a/src/main/java/cc/reconnected/server/commands/warp/DeleteWarpCommand.java b/src/main/java/cc/reconnected/server/commands/warp/DeleteWarpCommand.java new file mode 100644 index 0000000..a33c9fa --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/warp/DeleteWarpCommand.java @@ -0,0 +1,63 @@ +package cc.reconnected.server.commands.warp; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +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.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class DeleteWarpCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("delwarp") + .requires(Permissions.require("rcc.command.delwarp", 3)) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isExecutedByPlayer()) + return CommandSource.suggestMatching(new String[]{}, builder); + + var serverState = RccServer.state.getServerState(); + return CommandSource.suggestMatching(serverState.warps.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name) { + 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 serverState = RccServer.state.getServerState(); + var warps = serverState.warps; + + if (!warps.containsKey(name)) { + context.getSource().sendFeedback(() -> Text.literal("The warp ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" does not exist!") + .formatted(Formatting.RED), false); + return 1; + } + + warps.remove(name); + RccServer.state.saveServerState(); + + context.getSource().sendFeedback(() -> Text + .literal("Warp ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" deleted!") + .formatted(Formatting.GREEN), false); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/warp/SetWarpCommand.java b/src/main/java/cc/reconnected/server/commands/warp/SetWarpCommand.java new file mode 100644 index 0000000..7e15b5d --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/warp/SetWarpCommand.java @@ -0,0 +1,50 @@ +package cc.reconnected.server.commands.warp; + +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 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.*; + +public class SetWarpCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("setwarp") + .requires(Permissions.require("rcc.command.setwarp", 3)) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, + StringArgumentType.getString(context, "name")))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name) { + 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 serverState = RccServer.state.getServerState(); + + var warps = serverState.warps; + + var warpPosition = new ServerPosition(player); + warps.put(name, warpPosition); + + RccServer.state.saveServerState(); + + context.getSource().sendFeedback(() -> Text.literal("New warp ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" set!") + .formatted(Formatting.GREEN), false); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/warp/WarpCommand.java b/src/main/java/cc/reconnected/server/commands/warp/WarpCommand.java new file mode 100644 index 0000000..5430359 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/warp/WarpCommand.java @@ -0,0 +1,62 @@ +package cc.reconnected.server.commands.warp; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +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.Text; +import net.minecraft.util.Formatting; + +import static net.minecraft.server.command.CommandManager.*; + +public class WarpCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("warp") + .requires(Permissions.require("rcc.command.warp", true)) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isExecutedByPlayer()) + return CommandSource.suggestMatching(new String[]{}, builder); + + var serverState = RccServer.state.getServerState(); + return CommandSource.suggestMatching(serverState.warps.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, String name) { + 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 serverState = RccServer.state.getServerState(); + var warps = serverState.warps; + + if (!warps.containsKey(name)) { + context.getSource().sendFeedback(() -> Text.literal("The warp ") + .append(Text.literal(name).formatted(Formatting.GOLD)) + .append(" does not exist!") + .formatted(Formatting.RED), false); + return 1; + } + + context.getSource().sendFeedback(() -> Text + .literal("Teleporting to ") + .append(Text.literal(name).formatted(Formatting.GREEN)) + .append("...") + .formatted(Formatting.GOLD), false); + + var warpPosition = warps.get(name); + warpPosition.teleport(player); + + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/core/AfkTracker.java b/src/main/java/cc/reconnected/server/core/AfkTracker.java index c60d130..8e6c689 100644 --- a/src/main/java/cc/reconnected/server/core/AfkTracker.java +++ b/src/main/java/cc/reconnected/server/core/AfkTracker.java @@ -1,7 +1,6 @@ package cc.reconnected.server.core; import cc.reconnected.server.RccServer; -import cc.reconnected.server.data.StateSaverAndLoader; import cc.reconnected.server.events.PlayerActivityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.player.*; @@ -16,16 +15,17 @@ import net.minecraft.text.Text; import net.minecraft.util.ActionResult; import net.minecraft.util.TypedActionResult; -import java.util.HashMap; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class AfkTracker { private static final int cycleDelay = 1; private static final int absentTimeTrigger = RccServer.CONFIG.afkTimeTrigger() * 20; // seconds * 20 ticks - private final HashMap playerStates = new HashMap<>(); + private final ConcurrentHashMap playerActivityStates = new ConcurrentHashMap<>(); private static final AfkTracker instance = new AfkTracker(); + public static AfkTracker getInstance() { return instance; } @@ -63,20 +63,16 @@ public class AfkTracker { } private AfkTracker() { - ServerTickEvents.END_SERVER_TICK.register(server -> { - if (server.getTicks() % cycleDelay == 0) { - updatePlayers(server); - } - }); + ServerTickEvents.END_SERVER_TICK.register(this::updatePlayers); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { final var player = handler.getPlayer(); - playerStates.put(player.getUuid(), new PlayerState(player, server.getTicks())); + playerActivityStates.put(player.getUuid(), new PlayerActivityState(player, server.getTicks())); }); ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { updatePlayerActiveTime(handler.getPlayer(), server.getTicks()); - playerStates.remove(handler.getPlayer().getUuid()); + playerActivityStates.remove(handler.getPlayer().getUuid()); // sync to LP //var activeTime = String.valueOf(getActiveTime(handler.getPlayer())); @@ -125,7 +121,7 @@ public class AfkTracker { private void updatePlayer(ServerPlayerEntity player, MinecraftServer server) { var currentTick = server.getTicks(); - var playerState = playerStates.computeIfAbsent(player.getUuid(), uuid -> new PlayerState(player, currentTick)); + var playerState = playerActivityStates.computeIfAbsent(player.getUuid(), uuid -> new PlayerActivityState(player, currentTick)); var oldPosition = playerState.position; var newPosition = new PlayerPosition(player); @@ -147,11 +143,11 @@ public class AfkTracker { } private void updatePlayerActiveTime(ServerPlayerEntity player, int currentTick) { - var playerState = playerStates.get(player.getUuid()); - if (!playerState.isAfk) { - var worldPlayerData = StateSaverAndLoader.getPlayerState(player); - var interval = currentTick - playerState.activeStart; - worldPlayerData.activeTime += interval / 20; + var playerActivityState = playerActivityStates.get(player.getUuid()); + if (!playerActivityState.isAfk) { + var playerState = RccServer.state.getPlayerState(player.getUuid()); + var interval = currentTick - playerActivityState.activeStart; + playerState.activeTime += interval / 20; } } @@ -163,10 +159,10 @@ public class AfkTracker { } private void resetAfkState(ServerPlayerEntity player, MinecraftServer server) { - if (!playerStates.containsKey(player.getUuid())) + if (!playerActivityStates.containsKey(player.getUuid())) return; - var playerState = playerStates.get(player.getUuid()); + var playerState = playerActivityStates.get(player.getUuid()); playerState.lastUpdate = server.getTicks(); if (playerState.isAfk) { playerState.isAfk = false; @@ -200,13 +196,13 @@ public class AfkTracker { } } - public static class PlayerState { + public static class PlayerActivityState { public PlayerPosition position; public int lastUpdate; public boolean isAfk; public int activeStart; - public PlayerState(ServerPlayerEntity player, int lastUpdate) { + public PlayerActivityState(ServerPlayerEntity player, int lastUpdate) { this.position = new PlayerPosition(player); this.lastUpdate = lastUpdate; this.isAfk = false; @@ -215,21 +211,21 @@ public class AfkTracker { } public boolean isPlayerAfk(UUID playerUuid) { - if (!playerStates.containsKey(playerUuid)) { + if (!playerActivityStates.containsKey(playerUuid)) { return false; } - return playerStates.get(playerUuid).isAfk; + return playerActivityStates.get(playerUuid).isAfk; } public void setPlayerAfk(ServerPlayerEntity player, boolean afk) { - if (!playerStates.containsKey(player.getUuid())) { + if (!playerActivityStates.containsKey(player.getUuid())) { return; } var server = player.getWorld().getServer(); if (afk) { - playerStates.get(player.getUuid()).lastUpdate = -absentTimeTrigger - 20; // just to be sure + playerActivityStates.get(player.getUuid()).lastUpdate = -absentTimeTrigger - 20; // just to be sure } else { resetAfkState(player, server); } @@ -238,7 +234,7 @@ public class AfkTracker { } public int getActiveTime(ServerPlayerEntity player) { - var worldPlayerData = StateSaverAndLoader.getPlayerState(player); - return worldPlayerData.activeTime; + var playerState = RccServer.state.getPlayerState(player.getUuid()); + return playerState.activeTime; } } diff --git a/src/main/java/cc/reconnected/server/core/BackTracker.java b/src/main/java/cc/reconnected/server/core/BackTracker.java index 9b9d69e..af2ac0c 100644 --- a/src/main/java/cc/reconnected/server/core/BackTracker.java +++ b/src/main/java/cc/reconnected/server/core/BackTracker.java @@ -14,7 +14,7 @@ public class BackTracker { public static void register() { ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { - BackTracker.lastPlayerPositions.remove(handler.getPlayer().getUuid()); + lastPlayerPositions.remove(handler.getPlayer().getUuid()); }); PlayerTeleport.EVENT.register((player, origin, destination) -> { diff --git a/src/main/java/cc/reconnected/server/core/TabList.java b/src/main/java/cc/reconnected/server/core/TabList.java index 2155053..b69c880 100644 --- a/src/main/java/cc/reconnected/server/core/TabList.java +++ b/src/main/java/cc/reconnected/server/core/TabList.java @@ -1,13 +1,12 @@ package cc.reconnected.server.core; import cc.reconnected.server.RccServer; +import cc.reconnected.server.util.Components; import eu.pb4.placeholders.api.PlaceholderContext; import eu.pb4.placeholders.api.Placeholders; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; -import net.minecraft.text.Text; public class TabList { public static void register() { @@ -42,8 +41,8 @@ public class TabList { footerComponent = footerComponent.append(minimessage.deserialize(line)); } - var parsedHeader = Placeholders.parseText(toText(headerComponent), playerContext); - var parsedFooter = Placeholders.parseText(toText(footerComponent), playerContext); + var parsedHeader = Placeholders.parseText(Components.toText(headerComponent), playerContext); + var parsedFooter = Placeholders.parseText(Components.toText(footerComponent), playerContext); var audience = RccServer.getInstance().adventure().player(player.getUuid()); audience.sendPlayerListHeaderAndFooter(parsedHeader, parsedFooter); @@ -52,8 +51,4 @@ public class TabList { } - public static Text toText(Component component) { - var json = JSONComponentSerializer.json().serialize(component); - return Text.Serializer.fromJson(json); - } } diff --git a/src/main/java/cc/reconnected/server/core/TeleportTracker.java b/src/main/java/cc/reconnected/server/core/TeleportTracker.java index 36fac37..59c512c 100644 --- a/src/main/java/cc/reconnected/server/core/TeleportTracker.java +++ b/src/main/java/cc/reconnected/server/core/TeleportTracker.java @@ -1,16 +1,8 @@ package cc.reconnected.server.core; import cc.reconnected.server.RccServer; -import cc.reconnected.server.events.PlayerTeleport; -import cc.reconnected.server.struct.ServerPosition; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.ComponentLike; -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minecraft.server.network.ServerPlayerEntity; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -30,36 +22,9 @@ public class TeleportTracker { }); }); - ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - teleportRequests.put(handler.getPlayer().getUuid(), new ConcurrentLinkedDeque<>()); - }); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> teleportRequests.put(handler.getPlayer().getUuid(), new ConcurrentLinkedDeque<>())); - ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { - TeleportTracker.teleportRequests.remove(handler.getPlayer().getUuid()); - }); - } - - public static Component makeButton(ComponentLike text, ComponentLike hoverText, String command) { - 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) { - PlayerTeleport.EVENT.invoker().teleport(sourcePlayer, new ServerPosition(sourcePlayer), new ServerPosition(targetPlayer)); - - sourcePlayer.teleport( - targetPlayer.getServerWorld(), - targetPlayer.getX(), - targetPlayer.getY(), - targetPlayer.getZ(), - targetPlayer.getYaw(), - targetPlayer.getPitch() - ); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> TeleportTracker.teleportRequests.remove(handler.getPlayer().getUuid())); } public static class TeleportRequest { diff --git a/src/main/java/cc/reconnected/server/data/PlayerState.java b/src/main/java/cc/reconnected/server/data/PlayerState.java new file mode 100644 index 0000000..f6f03b6 --- /dev/null +++ b/src/main/java/cc/reconnected/server/data/PlayerState.java @@ -0,0 +1,29 @@ +package cc.reconnected.server.data; + +import cc.reconnected.server.struct.ServerPosition; +import com.google.gson.annotations.Expose; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class PlayerState { + public boolean dirty = false; + public boolean saving = false; + + @Expose + public UUID uuid; + @Expose + public String username; + @Expose + public @Nullable Date firstJoinedDate; + @Expose + public @Nullable ServerPosition logoffPosition = null; + @Expose + public int activeTime = 0; + @Expose + public ConcurrentHashMap homes = new ConcurrentHashMap<>(); + + +} diff --git a/src/main/java/cc/reconnected/server/data/ServerState.java b/src/main/java/cc/reconnected/server/data/ServerState.java new file mode 100644 index 0000000..01cab37 --- /dev/null +++ b/src/main/java/cc/reconnected/server/data/ServerState.java @@ -0,0 +1,18 @@ +package cc.reconnected.server.data; + +import cc.reconnected.server.struct.ServerPosition; +import com.google.gson.annotations.Expose; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.ConcurrentHashMap; + +public class ServerState { + public boolean dirty = false; + public boolean saving = false; + + @Expose + public @Nullable ServerPosition spawn; + @Expose + public ConcurrentHashMap warps = new ConcurrentHashMap<>(); + +} diff --git a/src/main/java/cc/reconnected/server/data/StateManager.java b/src/main/java/cc/reconnected/server/data/StateManager.java new file mode 100644 index 0000000..1cdc20d --- /dev/null +++ b/src/main/java/cc/reconnected/server/data/StateManager.java @@ -0,0 +1,161 @@ +package cc.reconnected.server.data; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.struct.ServerPosition; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class StateManager { + private static final Charset charset = StandardCharsets.UTF_8; + private ServerState serverState; + private final ConcurrentHashMap playerStates = new ConcurrentHashMap<>(); + private final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .excludeFieldsWithoutExposeAnnotation() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX") // iso 8601 my beloved + .create(); + + private Path basePath; + private Path playersPath; + private Path serverDataPath; + + public StateManager() { + } + + public void register(Path path) { + basePath = path; + playersPath = basePath.resolve("players"); + serverDataPath = basePath.resolve("data.json"); + + if (!basePath.toFile().exists()) { + if (!basePath.toFile().mkdirs()) { + RccServer.LOGGER.error("Could not create directory: {}", basePath.toAbsolutePath()); + } + } + + if (!playersPath.toFile().exists()) { + if (!playersPath.toFile().mkdirs()) { + RccServer.LOGGER.error("Could not create directory: {}", playersPath.toAbsolutePath()); + } + } + + if (serverDataPath.toFile().exists()) { + try (var br = new BufferedReader(new FileReader(serverDataPath.toFile(), charset))) { + serverState = gson.fromJson(br, ServerState.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + serverState = new ServerState(); + serverState.dirty = true; + } + + ServerLifecycleEvents.SERVER_STOPPING.register(serverState -> save()); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> loadPlayerState(handler.player.getUuid())); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + var playerState = getPlayerState(handler.getPlayer().getUuid()); + playerState.logoffPosition = new ServerPosition(handler.getPlayer()); + savePlayerState(handler.getPlayer().getUuid(), playerState); + playerStates.remove(handler.player.getUuid()); + }); + + } + + public void saveServerState() { + if (!serverState.dirty) + return; + + if (serverState.saving) + return; + serverState.saving = true; + + var json = gson.toJson(serverState, ServerState.class); + try (var fw = new FileWriter(serverDataPath.toFile(), charset)) { + fw.write(json); + serverState.dirty = false; + } catch (IOException e) { + RccServer.LOGGER.error("Could not save server state", e); + } + serverState.saving = false; + } + + public void savePlayerState(UUID uuid, PlayerState playerState) { + if (!playerState.dirty) + return; + + if (playerState.saving) + return; + playerState.saving = true; + + var path = playersPath.resolve(uuid.toString() + ".json"); + var json = gson.toJson(playerState, PlayerState.class); + try (var fw = new FileWriter(path.toFile(), charset)) { + fw.write(json); + playerState.dirty = false; + } catch (IOException e) { + RccServer.LOGGER.error("Could not save player state", e); + } + playerState.saving = false; + } + + public void save() { + saveServerState(); + playerStates.forEach(this::savePlayerState); + } + + private PlayerState loadPlayerState(UUID uuid) { + var path = playersPath.resolve(uuid.toString() + ".json"); + PlayerState playerState; + if (path.toFile().exists()) { + try (var br = new BufferedReader(new FileReader(path.toFile(), charset))) { + playerState = gson.fromJson(br, PlayerState.class); + } catch (Exception e) { + RccServer.LOGGER.error("Could not load player state: " + path.toAbsolutePath(), e); + return null; + } + } else { + playerState = new PlayerState(); + playerState.dirty = true; + } + + playerStates.put(uuid, playerState); + playerState.uuid = uuid; + + return playerState; + } + + public PlayerState getPlayerState(UUID uuid) { + PlayerState playerState; + if (playerStates.containsKey(uuid)) { + playerState = playerStates.get(uuid); + } else { + playerState = loadPlayerState(uuid); + } + + if (playerState == null) + return null; + + playerState.dirty = true; + return playerState; + } + + public ServerState getServerState() { + serverState.dirty = true; + return serverState; + } +} diff --git a/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java b/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java deleted file mode 100644 index 204eebc..0000000 --- a/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java +++ /dev/null @@ -1,59 +0,0 @@ -package cc.reconnected.server.data; - -import cc.reconnected.server.RccServer; -import net.minecraft.entity.LivingEntity; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.server.MinecraftServer; -import net.minecraft.world.PersistentState; -import net.minecraft.world.World; - -import java.util.HashMap; -import java.util.UUID; - -public class StateSaverAndLoader extends PersistentState { - public final HashMap players = new HashMap<>(); - - @Override - public NbtCompound writeNbt(NbtCompound nbt) { - var playersNbt = new NbtCompound(); - players.forEach((uuid, data) -> { - var playerNbt = new NbtCompound(); - playerNbt.putInt("activeTime", data.activeTime); - playersNbt.put(uuid.toString(), playerNbt); - }); - nbt.put("players", playersNbt); - - return nbt; - } - - public static StateSaverAndLoader createFromNbt(NbtCompound nbt) { - var state = new StateSaverAndLoader(); - - var playersNbt = nbt.getCompound("players"); - playersNbt.getKeys().forEach(key -> { - var playerData = new WorldPlayerData(); - - playerData.activeTime = playersNbt.getCompound(key).getInt("activeTime"); - UUID uuid = UUID.fromString(key); - state.players.put(uuid, playerData); - }); - - return state; - } - - public static StateSaverAndLoader getServerState(MinecraftServer server) { - var persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); - var state = persistentStateManager.getOrCreate( - StateSaverAndLoader::createFromNbt, - StateSaverAndLoader::new, - RccServer.MOD_ID - ); - state.markDirty(); - return state; - } - - public static WorldPlayerData getPlayerState(LivingEntity player) { - var serverState = getServerState(player.getWorld().getServer()); - return serverState.players.computeIfAbsent(player.getUuid(), uuid -> new WorldPlayerData()); - } -} diff --git a/src/main/java/cc/reconnected/server/data/WorldPlayerData.java b/src/main/java/cc/reconnected/server/data/WorldPlayerData.java deleted file mode 100644 index fcec6f1..0000000 --- a/src/main/java/cc/reconnected/server/data/WorldPlayerData.java +++ /dev/null @@ -1,5 +0,0 @@ -package cc.reconnected.server.data; - -public class WorldPlayerData { - public int activeTime = 0; -} diff --git a/src/main/java/cc/reconnected/server/events/PlayerUsernameChange.java b/src/main/java/cc/reconnected/server/events/PlayerUsernameChange.java new file mode 100644 index 0000000..74472de --- /dev/null +++ b/src/main/java/cc/reconnected/server/events/PlayerUsernameChange.java @@ -0,0 +1,16 @@ +package cc.reconnected.server.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface PlayerUsernameChange { + Event PLAYER_USERNAME_CHANGE = EventFactory.createArrayBacked(PlayerUsernameChange.class, + (listeners) -> (player, previousUsername) -> { + for (PlayerUsernameChange listener : listeners) { + listener.changeUsername(player, previousUsername); + } + }); + + void changeUsername(ServerPlayerEntity player, String previousUsername); +} diff --git a/src/main/java/cc/reconnected/server/events/PlayerWelcome.java b/src/main/java/cc/reconnected/server/events/PlayerWelcome.java index 9cfd34d..40b29f2 100644 --- a/src/main/java/cc/reconnected/server/events/PlayerWelcome.java +++ b/src/main/java/cc/reconnected/server/events/PlayerWelcome.java @@ -1,6 +1,5 @@ package cc.reconnected.server.events; -import cc.reconnected.server.database.PlayerData; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; import net.minecraft.server.MinecraftServer; @@ -8,11 +7,11 @@ import net.minecraft.server.network.ServerPlayerEntity; public interface PlayerWelcome { Event PLAYER_WELCOME = EventFactory.createArrayBacked(PlayerWelcome.class, - (listeners) -> (player, playerData, server) -> { + (listeners) -> (player, server) -> { for (PlayerWelcome listener : listeners) { - listener.playerWelcome(player, playerData, server); + listener.playerWelcome(player, server); } }); - void playerWelcome(ServerPlayerEntity player, PlayerData playerData, MinecraftServer server); + void playerWelcome(ServerPlayerEntity player, MinecraftServer server); } diff --git a/src/main/java/cc/reconnected/server/mixin/ServerPlayNetworkManagerMixin.java b/src/main/java/cc/reconnected/server/mixin/ServerPlayNetworkManagerMixin.java new file mode 100644 index 0000000..735497e --- /dev/null +++ b/src/main/java/cc/reconnected/server/mixin/ServerPlayNetworkManagerMixin.java @@ -0,0 +1,39 @@ +package cc.reconnected.server.mixin; + +import cc.reconnected.server.RccServer; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.EnumSet; +import java.util.List; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkManagerMixin { + @Shadow + @Final + private MinecraftServer server; + @Shadow + public ServerPlayerEntity player; + + @Shadow + public abstract void sendPacket(Packet packet); + + @Inject(method = "tick", at = @At("TAIL")) + private void rccServer$updatePlayerList(CallbackInfo ci) { + if(RccServer.CONFIG.enableTabList()) { + var packet = new PlayerListS2CPacket(EnumSet.of(PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME, PlayerListS2CPacket.Action.UPDATE_LISTED), List.of(this.player)); + this.sendPacket(packet); + } + } + + +} diff --git a/src/main/java/cc/reconnected/server/mixin/ServerPlayerEntityMixin.java b/src/main/java/cc/reconnected/server/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 0000000..1edb20e --- /dev/null +++ b/src/main/java/cc/reconnected/server/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,30 @@ +package cc.reconnected.server.mixin; + +import cc.reconnected.server.RccServer; +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.placeholders.api.Placeholders; +import eu.pb4.placeholders.api.parsers.NodeParser; +import eu.pb4.placeholders.api.parsers.TextParserV1; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayerEntity.class) +public class ServerPlayerEntityMixin { + @Unique + private static final NodeParser parser = NodeParser.merge(TextParserV1.DEFAULT, Placeholders.DEFAULT_PLACEHOLDER_PARSER); + + @Inject(method = "getPlayerListName", at = @At("HEAD"), cancellable = true) + private void rccServer$customizePlayerListName(CallbackInfoReturnable callback) { + if (RccServer.CONFIG.enableTabList()) { + var player = (ServerPlayerEntity) (Object) this; + var playerContext = PlaceholderContext.of(player); + var text = Placeholders.parseText(parser.parseNode(RccServer.CONFIG.playerTabName()), playerContext); + callback.setReturnValue(text); + } + } +} diff --git a/src/main/java/cc/reconnected/server/struct/ServerPosition.java b/src/main/java/cc/reconnected/server/struct/ServerPosition.java index 35e08b0..929322d 100644 --- a/src/main/java/cc/reconnected/server/struct/ServerPosition.java +++ b/src/main/java/cc/reconnected/server/struct/ServerPosition.java @@ -1,16 +1,26 @@ package cc.reconnected.server.struct; import cc.reconnected.server.core.BackTracker; +import com.google.gson.annotations.Expose; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; public class ServerPosition { + @Expose public double x; + @Expose public double y; + @Expose public double z; + @Expose public float yaw; + @Expose public float pitch; - public ServerWorld world; + @Expose + public String world; public ServerPosition(double x, double y, double z, float yaw, float pitch, ServerWorld world) { this.x = x; @@ -18,7 +28,7 @@ public class ServerPosition { this.z = z; this.yaw = yaw; this.pitch = pitch; - this.world = world; + this.world = world.getRegistryKey().getValue().toString(); } public ServerPosition(ServerPlayerEntity player) { @@ -27,15 +37,19 @@ public class ServerPosition { this.z = player.getZ(); this.yaw = player.getYaw(); this.pitch = player.getPitch(); - this.world = player.getServerWorld(); + this.world = player.getServerWorld().getRegistryKey().getValue().toString(); } - public void teleport(ServerPlayerEntity player) { - var currentPosition = new ServerPosition(player); - BackTracker.lastPlayerPositions.put(player.getUuid(), currentPosition); + public void teleport(ServerPlayerEntity player, boolean setBackPosition) { + if (setBackPosition) { + var currentPosition = new ServerPosition(player); + BackTracker.lastPlayerPositions.put(player.getUuid(), currentPosition); + } + + var serverWorld = player.getServer().getWorld(RegistryKey.of(RegistryKeys.WORLD, new Identifier(this.world))); player.teleport( - this.world, + serverWorld, this.x, this.y, this.z, @@ -43,4 +57,8 @@ public class ServerPosition { this.pitch ); } + + public void teleport(ServerPlayerEntity player) { + teleport(player, true); + } } diff --git a/src/main/java/cc/reconnected/server/util/Components.java b/src/main/java/cc/reconnected/server/util/Components.java new file mode 100644 index 0000000..f622527 --- /dev/null +++ b/src/main/java/cc/reconnected/server/util/Components.java @@ -0,0 +1,26 @@ +package cc.reconnected.server.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; +import net.minecraft.text.Text; + +public class Components { + public static Component makeButton(ComponentLike text, ComponentLike hoverText, String command) { + return Component.empty() + .append(Component.text("[")) + .append(text) + .append(Component.text("]")) + .color(NamedTextColor.AQUA) + .hoverEvent(HoverEvent.showText(hoverText)) + .clickEvent(ClickEvent.runCommand(command)); + } + + public static Text toText(Component component) { + var json = JSONComponentSerializer.json().serialize(component); + return Text.Serializer.fromJson(json); + } +} diff --git a/src/main/resources/rcc-server.mixins.json b/src/main/resources/rcc-server.mixins.json index 265b2a4..b7f77c1 100644 --- a/src/main/resources/rcc-server.mixins.json +++ b/src/main/resources/rcc-server.mixins.json @@ -3,10 +3,13 @@ "minVersion": "0.8", "package": "cc.reconnected.server.mixin", "compatibilityLevel": "JAVA_17", - "mixins": [], + "mixins": [ + "ServerPlayNetworkManagerMixin" + ], "client": [], "server": [ - "MessageCommandMixin" + "MessageCommandMixin", + "ServerPlayerEntityMixin" ], "injectors": { "defaultRequire": 1