diff --git a/src/main/java/fr/liveinground/admin_craft/AdminCraft.java b/src/main/java/fr/liveinground/admin_craft/AdminCraft.java index 3e299d5..88cb0d0 100644 --- a/src/main/java/fr/liveinground/admin_craft/AdminCraft.java +++ b/src/main/java/fr/liveinground/admin_craft/AdminCraft.java @@ -1,11 +1,10 @@ package fr.liveinground.admin_craft; +import com.mojang.authlib.GameProfile; import com.mojang.brigadier.CommandDispatcher; import com.mojang.logging.LogUtils; -import fr.liveinground.admin_craft.commands.tools.AltCommand; +import fr.liveinground.admin_craft.commands.tools.*; import fr.liveinground.admin_craft.commands.moderation.*; -import fr.liveinground.admin_craft.commands.tools.EchestCommand; -import fr.liveinground.admin_craft.commands.tools.InvseeCommand; import fr.liveinground.admin_craft.moderation.SanctionConfig; import fr.liveinground.admin_craft.mutes.MuteEventsHandler; import fr.liveinground.admin_craft.storage.PlayerDataManager; @@ -13,6 +12,7 @@ import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; @@ -35,9 +35,12 @@ import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.loading.FMLEnvironment; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; +import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; @Mod(AdminCraft.MODID) @@ -87,6 +90,8 @@ public void onCommandRegister(RegisterCommandsEvent event) { TempBanCommand.register(dispatcher); InvseeCommand.register(dispatcher); EchestCommand.register(dispatcher); + OfflineTeleportCommand.register(dispatcher); + OfflineTagCommand.register(dispatcher); // StaffModeCommand.register(dispatcher); } @@ -262,4 +267,22 @@ public void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) { player.sendSystemMessage(Component.literal("You can disable this message by changing the 'configVersion' key to " + AdminCraft._VERSION + " in the configuration.")); } } + + @Nullable + public static GameProfile getOneProfile(Collection profiles) { + if (profiles.isEmpty()) return null; + for (GameProfile p: profiles) { + if (p != null) return p; + } + return null; + } + + public static boolean isOnline(MinecraftServer server, GameProfile profile) { + return getOnlinePlayer(server, profile) != null; + } + + @Nullable + public static ServerPlayer getOnlinePlayer(MinecraftServer server, GameProfile profile) { + return server.getPlayerList().getPlayer(profile.getId()); + } } diff --git a/src/main/java/fr/liveinground/admin_craft/Config.java b/src/main/java/fr/liveinground/admin_craft/Config.java index 2265f5c..9ee0a65 100644 --- a/src/main/java/fr/liveinground/admin_craft/Config.java +++ b/src/main/java/fr/liveinground/admin_craft/Config.java @@ -57,6 +57,12 @@ public class Config { private static final ForgeConfigSpec.IntValue INVSEE_LEVEL; public static int invsee_level; + private static final ForgeConfigSpec.IntValue OTP_LEVEL; + public static int otp_level; + + private static final ForgeConfigSpec.IntValue OTAG_LEVEL; + public static int otag_level; + // --------------- // -- Sanctions -- // --------------- @@ -223,6 +229,8 @@ public class Config { REPORTS_LEVEL = BUILDER.comment("The OP level required to run the /reports command").worldRestart().defineInRange("reports", 3, 0, 4); TEMPBAN_LEVEL = BUILDER.comment("The OP level required to run the /tempban command").worldRestart().defineInRange("tempban", 3, 0,4); INVSEE_LEVEL = BUILDER.comment("The OP level required to run the /invsee and /echest commands").worldRestart().defineInRange("invsee", 2, 0,4); + OTP_LEVEL = BUILDER.comment("The OP level required to run the /otp command").worldRestart().defineInRange("otp", 2, 0, 4); + OTAG_LEVEL = BUILDER.comment("The OP level required to run the /otag command").worldRestart().defineInRange("otp", 2, 0, 4); BUILDER.pop(); } @@ -302,7 +310,7 @@ public class Config { .define("enter", "You are now in the spawn protection"); SPAWN_PROTECTION_LEAVE = BUILDER.comment("Message when leaving spawn protection") - .define("leave", "You are no more in the spawn protection"); + .define("leave", "You are no longer in the spawn protection"); TIME_REMAINING = BUILDER.comment("Message for displaying a sanction duration. Available placeholders: %days%, %hours%, and %minutes%") .define("timeRemainingMessage", "Time remaining: %days% days, %hours%, and %minutes% minutes"); @@ -328,7 +336,7 @@ public class Config { REPORT_FAILED_SELF = BUILDER.comment("The message sent to a player trying to report himself").define("selfReport", "You can't report yourself!"); FREEZE_START = BUILDER.comment("The message sent to the player when he is frozen").define("freezeStartMessage", "You have been frozen by an operator. Please wait for instructions and don't log out."); - FREEZE_STOP = BUILDER.comment("The message sent to the player when he is unfrozen").define("freezeStopMessage", "You are no more frozen. You can continue playing normally."); + FREEZE_STOP = BUILDER.comment("The message sent to the player when he is unfrozen").define("freezeStopMessage", "You are no longer frozen. You can continue playing normally."); BUILDER.pop(); } @@ -424,6 +432,8 @@ static void onLoad(final ModConfigEvent.Loading event) { reports_level = REPORTS_LEVEL.get(); tempban_level = TEMPBAN_LEVEL.get(); invsee_level = INVSEE_LEVEL.get(); + otag_level = OTAG_LEVEL.get(); + otp_level = OTP_LEVEL.get(); // --------------- // -- Sanctions -- diff --git a/src/main/java/fr/liveinground/admin_craft/commands/moderation/MuteCommand.java b/src/main/java/fr/liveinground/admin_craft/commands/moderation/MuteCommand.java index d20675f..c6b1766 100644 --- a/src/main/java/fr/liveinground/admin_craft/commands/moderation/MuteCommand.java +++ b/src/main/java/fr/liveinground/admin_craft/commands/moderation/MuteCommand.java @@ -10,6 +10,7 @@ import fr.liveinground.admin_craft.PlaceHolderSystem; import fr.liveinground.admin_craft.moderation.CustomSanctionSystem; import fr.liveinground.admin_craft.moderation.SanctionConfig; +import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; @@ -31,7 +32,7 @@ public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("mute") .requires(commandSource -> commandSource.hasPermission(Config.mute_level)) - .then(Commands.argument("player", EntityArgument.player()) + .then(Commands.argument("player", GameProfileArgument.gameProfile()) .executes(ctx -> { mute(ctx, null, null); return 1; @@ -80,7 +81,7 @@ public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("tempmute") .requires(commandSource -> commandSource.hasPermission(Config.mute_level)) - .then(Commands.argument("player", EntityArgument.player()) + .then(Commands.argument("player", GameProfileArgument.gameProfile()) .then(Commands.argument("duration", StringArgumentType.word()) .executes(ctx -> { Date duration = SanctionConfig.getDurationAsDate(StringArgumentType.getString(ctx, "duration")); @@ -108,18 +109,22 @@ public static void register(CommandDispatcher dispatcher) { } private static void mute(@NotNull CommandContext ctx, @Nullable String reason, @Nullable Date duration) throws CommandSyntaxException { - ServerPlayer player = EntityArgument.getPlayer(ctx, "player"); + GameProfile profile = AdminCraft.getOneProfile(GameProfileArgument.getGameProfiles(ctx, "player")); + if (profile == null) { + ctx.getSource().sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return; + } if (reason == null) { reason = "Muted by an operator."; } - if (AdminCraft.mutedPlayersUUID.contains(player.getStringUUID())) { - ctx.getSource().sendFailure(Component.literal(PlaceHolderSystem.replacePlaceholders(Config.mute_failed_already_muted, Map.of("player", player.getName().getString())))); + if (AdminCraft.mutedPlayersUUID.contains(String.valueOf(profile.getId()))) { + ctx.getSource().sendFailure(Component.literal(PlaceHolderSystem.replacePlaceholders(Config.mute_failed_already_muted, Map.of("player", profile.getName())))); return; } - CustomSanctionSystem.mutePlayer(player, reason, duration); + CustomSanctionSystem.mutePlayer(ctx.getSource().getServer(), profile, reason, duration); - String msgToOperator = PlaceHolderSystem.replacePlaceholders(Config.mute_success, Map.of("player", player.getName().getString(), "reason", reason)); + String msgToOperator = PlaceHolderSystem.replacePlaceholders(Config.mute_success, Map.of("player", profile.getName(), "reason", reason)); ctx.getSource().sendSuccess(() -> Component.literal(msgToOperator), true); } } \ No newline at end of file diff --git a/src/main/java/fr/liveinground/admin_craft/commands/moderation/SanctionCommand.java b/src/main/java/fr/liveinground/admin_craft/commands/moderation/SanctionCommand.java index eb3d6eb..f3fac5d 100644 --- a/src/main/java/fr/liveinground/admin_craft/commands/moderation/SanctionCommand.java +++ b/src/main/java/fr/liveinground/admin_craft/commands/moderation/SanctionCommand.java @@ -81,11 +81,11 @@ public static void register(CommandDispatcher dispatcher) { reason = template.sanctionMessage(); switch (template.type()) { case BAN: - CustomSanctionSystem.banPlayer(ctx.getSource().getServer(), ctx.getSource().toString(), sanctionedPlayer, reason, null); + CustomSanctionSystem.banPlayer(ctx.getSource().getServer(), ctx.getSource().toString(), sanctionedPlayer.getGameProfile(), reason, null); break; case TEMPBAN: Date banExpiresOn = SanctionConfig.getDurationAsDate(template.duration()); - CustomSanctionSystem.banPlayer(ctx.getSource().getServer(), ctx.getSource().toString(), sanctionedPlayer, reason, banExpiresOn); + CustomSanctionSystem.banPlayer(ctx.getSource().getServer(), ctx.getSource().toString(), sanctionedPlayer.getGameProfile(), reason, banExpiresOn); break; case KICK: CustomSanctionSystem.kickPlayer(sanctionedPlayer, reason); @@ -95,7 +95,7 @@ public static void register(CommandDispatcher dispatcher) { ctx.getSource().sendFailure(Component.literal(DUPLICATE_SANCTION)); return 1; } - CustomSanctionSystem.mutePlayer(sanctionedPlayer, reason, null); + CustomSanctionSystem.mutePlayer(ctx.getSource().getServer(), sanctionedPlayer.getGameProfile(), reason, null); break; case TEMPMUTE: if (AdminCraft.mutedPlayersUUID.contains(sanctionedPlayer.getStringUUID())) { @@ -103,7 +103,7 @@ public static void register(CommandDispatcher dispatcher) { return 1; } Date muteExpiresOn = SanctionConfig.getDurationAsDate(template.duration()); - CustomSanctionSystem.mutePlayer(sanctionedPlayer, reason, muteExpiresOn); + CustomSanctionSystem.mutePlayer(ctx.getSource().getServer(), sanctionedPlayer.getGameProfile(), reason, muteExpiresOn); break; case WARN: CustomSanctionSystem.warnPlayer(sanctionedPlayer, reason, ctx.getSource().getDisplayName().getString()); diff --git a/src/main/java/fr/liveinground/admin_craft/commands/moderation/TempBanCommand.java b/src/main/java/fr/liveinground/admin_craft/commands/moderation/TempBanCommand.java index 15acd2f..0cdc9ca 100644 --- a/src/main/java/fr/liveinground/admin_craft/commands/moderation/TempBanCommand.java +++ b/src/main/java/fr/liveinground/admin_craft/commands/moderation/TempBanCommand.java @@ -1,14 +1,18 @@ package fr.liveinground.admin_craft.commands.moderation; +import com.mojang.authlib.GameProfile; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; +import fr.liveinground.admin_craft.AdminCraft; import fr.liveinground.admin_craft.Config; import fr.liveinground.admin_craft.moderation.CustomSanctionSystem; import fr.liveinground.admin_craft.moderation.SanctionConfig; +import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.GameProfileArgument; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; @@ -19,37 +23,49 @@ public class TempBanCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("tempban") .requires(commandSource -> commandSource.hasPermission(Config.tempban_level)) - .then(Commands.argument("player", EntityArgument.player()) + .then(Commands.argument("player", GameProfileArgument.gameProfile()) .then(Commands.argument("duration", StringArgumentType.word()) .executes(ctx -> { - ServerPlayer sanctionedPlayer = EntityArgument.getPlayer(ctx, "player"); + GameProfile profile = AdminCraft.getOneProfile(GameProfileArgument.getGameProfiles(ctx, "player")); + if (profile == null) { + ctx.getSource().sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } String reason = "Banned by an operator"; Date duration = SanctionConfig.getDurationAsDate(StringArgumentType.getString(ctx, "duration")); + if (duration == null) { ctx.getSource().sendFailure(Component.literal("Invalid duration, expecting format 1d1h1m1s")); return 1; } - tempban(ctx, sanctionedPlayer, duration, reason); + tempban(ctx, profile, duration, reason); return 1; + }) .then(Commands.argument("reason", StringArgumentType.greedyString()) .executes(ctx -> { - ServerPlayer sanctionedPlayer = EntityArgument.getPlayer(ctx, "player"); + GameProfile profile = AdminCraft.getOneProfile(GameProfileArgument.getGameProfiles(ctx, "player")); + if (profile == null) { + ctx.getSource().sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + String reason = StringArgumentType.getString(ctx, "reason"); Date duration = SanctionConfig.getDurationAsDate(StringArgumentType.getString(ctx, "duration")); + if (duration == null) { ctx.getSource().sendFailure(Component.literal("Invalid duration, expecting format 1d1h1m1s")); return 1; } - tempban(ctx, sanctionedPlayer, duration, reason); + tempban(ctx, profile, duration, reason); return 1; }))) ) ); } - private static void tempban(CommandContext ctx, ServerPlayer player, Date duration, String reason) { + private static void tempban(CommandContext ctx, GameProfile player, Date duration, String reason) { CustomSanctionSystem.banPlayer(ctx.getSource().getServer(), ctx.getSource().getTextName(), player, reason, duration); - ctx.getSource().sendSuccess(() -> Component.literal("Banned " + player.getDisplayName().getString() + " " + duration + ": " + reason), true); + ctx.getSource().sendSuccess(() -> Component.literal("Banned " + player.getName() + " " + duration + ": " + reason), true); } } diff --git a/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTagCommand.java b/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTagCommand.java new file mode 100644 index 0000000..ac3edb2 --- /dev/null +++ b/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTagCommand.java @@ -0,0 +1,184 @@ +package fr.liveinground.admin_craft.commands.tools; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import fr.liveinground.admin_craft.Config; +import fr.liveinground.admin_craft.storage.nbt.PlayerDataLoader; +import fr.liveinground.admin_craft.storage.nbt.PlayerDataSaver; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class OfflineTagCommand { + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("otag") + .requires(source -> source.hasPermission(Config.otag_level)) + .then(Commands.argument("target", GameProfileArgument.gameProfile()) + .then(Commands.literal("add") + .then(Commands.argument("tag", StringArgumentType.word()) + .executes(ctx -> addTagToProfile(ctx.getSource(), GameProfileArgument.getGameProfiles(ctx, "target"), StringArgumentType.getString(ctx, "tag"))) + ) + ) + .then(Commands.literal("list") + .executes(ctx -> listProfileTags( + ctx.getSource(), + GameProfileArgument.getGameProfiles(ctx, "target")) + ) + ) + .then(Commands.literal("remove") + .then(Commands.argument("tag", StringArgumentType.word()) + .executes(ctx -> removeTagToProfile( + ctx.getSource(), + GameProfileArgument.getGameProfiles(ctx, "target"), + StringArgumentType.getString(ctx, "tag") + )) + ) + ) + ) + ); + } + + private static int addTagToProfile(CommandSourceStack source, Collection profiles, String tag) { + if (profiles.isEmpty()) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + GameProfile profile = null; + for (GameProfile p: profiles) { + if (p != null) { + profile = p; + break; + } + } + if (profile == null) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + + ServerPlayer player = source.getServer().getPlayerList().getPlayer(profile.getId()); + if (player != null) { + player.addTag(tag); + source.sendSuccess(() -> Component.literal("Added tag '" + tag + "' to " + player.getDisplayName().getString()), true); + return 1; + } else { + try { + int success = PlayerDataSaver.addOfflineTag(source.getLevel(), profile.getId(), tag); + if (success == 200) { + final GameProfile gp = profile; + source.sendSuccess(() -> Component.literal("Added tag '" + tag + "' to offline player " + gp.getName()), true); + } else if (success == 404) { + source.sendFailure(Component.literal("Failure: targeted playerdata file not found").withStyle(ChatFormatting.RED)); + } else if (success == 409) { + source.sendFailure(Component.literal("Failure: the target already has the tag").withStyle(ChatFormatting.RED)); + } else { + source.sendFailure(Component.literal("Failure: something went wrong").withStyle(ChatFormatting.RED)); + } + } catch (IOException e) { + source.sendFailure(Component.literal("Failure: something went wrong (IOException)").withStyle(ChatFormatting.RED)); + } + return 1; + } + } + + private static int removeTagToProfile(CommandSourceStack source, Collection profiles, String tag) { + if (profiles.isEmpty()) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + GameProfile profile = null; + for (GameProfile p: profiles) { + if (p != null) { + profile = p; + break; + } + } + if (profile == null) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + + ServerPlayer player = source.getServer().getPlayerList().getPlayer(profile.getId()); + if (player != null) { + player.addTag(tag); + source.sendSuccess(() -> Component.literal("Removed tag '" + tag + "' to " + player.getDisplayName().getString()), true); + return 1; + } else { + try { + int success = PlayerDataSaver.removeOfflineTag(source.getLevel(), profile.getId(), tag); + if (success == 200) { + final GameProfile gp = profile; + source.sendSuccess(() -> Component.literal("Removed tag '" + tag + "' to offline player " + gp.getName()), true); + } else if (success == 404) { + source.sendFailure(Component.literal("Failure: targeted playerdata file not found").withStyle(ChatFormatting.RED)); + } else if (success == 409) { + source.sendFailure(Component.literal("Failure: the target doesn't have the tag").withStyle(ChatFormatting.RED)); + } else { + source.sendFailure(Component.literal("Failure: something went wrong").withStyle(ChatFormatting.RED)); + } + } catch (IOException e) { + source.sendFailure(Component.literal("Failure: something went wrong (IOException)").withStyle(ChatFormatting.RED)); + } + return 1; + } + } + + private static int listProfileTags(CommandSourceStack source, Collection profiles) { + if (profiles.isEmpty()) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + GameProfile profile = null; + for (GameProfile p: profiles) { + if (p != null) { + profile = p; + break; + } + } + if (profile == null) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return 1; + } + + ServerPlayer player = source.getServer().getPlayerList().getPlayer(profile.getId()); + if (player != null) { + Set tags = player.getTags(); + if (tags.isEmpty()) { + source.sendSuccess(() -> Component.literal(player.getDisplayName().getString() + " has no tags"), false); + return 1; + } + String builder = "Player " + player.getDisplayName().getString() + + " has " + tags.size() + + " tags: " + + String.join(", ", tags); + source.sendSuccess(() -> Component.literal(builder), false); + + } else { + try { + List tags = PlayerDataLoader.listOfflineTags(source.getLevel(), profile.getId()); + final GameProfile gm = profile; + if (tags == null) { + source.sendSuccess(() -> Component.literal(gm.getName() + " has no tags"), false); + return 1; + } + String builder = "Player " + gm.getName() + + " has " + tags.size() + + " tags: " + + String.join(", ", tags); + source.sendSuccess(() -> Component.literal(builder), false); + } catch (IOException e) { + source.sendFailure(Component.literal("Failure: something went wrong (IOException)").withStyle(ChatFormatting.RED)); + } + } + + return 1; + } +} \ No newline at end of file diff --git a/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTeleportCommand.java b/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTeleportCommand.java new file mode 100644 index 0000000..4e2f2e3 --- /dev/null +++ b/src/main/java/fr/liveinground/admin_craft/commands/tools/OfflineTeleportCommand.java @@ -0,0 +1,108 @@ +package fr.liveinground.admin_craft.commands.tools; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.CommandDispatcher; +import fr.liveinground.admin_craft.AdminCraft; +import fr.liveinground.admin_craft.Config; +import fr.liveinground.admin_craft.storage.nbt.PlayerDataLoader; +import fr.liveinground.admin_craft.storage.nbt.PlayerDataSaver; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; + +import java.io.IOException; +import java.util.Collection; +import java.util.Objects; + +public class OfflineTeleportCommand { + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("otp") + .requires(source -> source.hasPermission(Config.otp_level)) + .then(Commands.argument("target", GameProfileArgument.gameProfile()) + .then(Commands.argument("player", EntityArgument.player()) + .executes(ctx -> { + teleportPlayerToPlayer(ctx.getSource(), GameProfileArgument.getGameProfiles(ctx, "target"), EntityArgument.getPlayer(ctx, "player")); + return 1; + }) + ) + .then(Commands.argument("destination", Vec3Argument.vec3()) + .executes(ctx -> { + teleportPlayerToCoordinates(ctx.getSource(), GameProfileArgument.getGameProfiles(ctx, "target"), Vec3Argument.getVec3(ctx, "destination")); + return 1; + }) + ) + ) + .then(Commands.argument("destination", GameProfileArgument.gameProfile()) + .requires(CommandSourceStack::isPlayer) + .executes(ctx -> { + teleportToPlayer(ctx.getSource(), GameProfileArgument.getGameProfiles(ctx, "destination")); + return 1; + })) + ); + } + + private static void teleportPlayerToPlayer(CommandSourceStack source, Collection profiles, ServerPlayer destPlayer) { + teleportPlayerToCoordinates(source, profiles, new Vec3(destPlayer.getX(), destPlayer.getY(), destPlayer.getZ())); + } + + private static void teleportPlayerToCoordinates(CommandSourceStack source, Collection profiles, Vec3 destination) { + GameProfile profile = AdminCraft.getOneProfile(profiles); + if (profile == null) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return; + } + ServerPlayer onlinePlayer = source.getServer().getPlayerList().getPlayer(profile.getId()); + if (onlinePlayer != null) { + onlinePlayer.teleportTo(source.getLevel(), destination.x, destination.y, destination.z, 0, 0); + source.sendSuccess(() -> + Component.literal(String.format( + "Teleported %s to %.2f, %.2f, %.2f", + onlinePlayer.getDisplayName().getString(), + destination.x, destination.y, destination.z + )), + true); + } else { + try { + PlayerDataSaver.setOfflineLocation(source.getLevel(), profile.getId(), destination); + } catch (IOException e) { + AdminCraft.LOGGER.error("Failed to save offline teleport location for {}", profile.getName(), e); + source.sendFailure(Component.literal("Failure: something went wrong (IOException)").withStyle(ChatFormatting.RED)); + } + } + } + + private static void teleportToPlayer(CommandSourceStack source, Collection profiles) { + ServerPlayer sourcePlayer = source.getPlayer(); + + GameProfile profile = AdminCraft.getOneProfile(profiles); + if (profile == null) { + source.sendFailure(Component.literal("No player was found").withStyle(ChatFormatting.RED)); + return; + } + + ServerPlayer player = source.getServer().getPlayerList().getPlayer(profile.getId()); + if (player != null) { + sourcePlayer.teleportTo(Objects.requireNonNull(source.getServer().getLevel(player.level().dimension())), player.getX(), player.getY(), player.getZ(), 0, 0); + source.sendSuccess(() -> Component.literal("Teleported to " + player.getDisplayName().getString()), true); + } else { + try { + Vec3 destination = PlayerDataLoader.getOfflineLocation(source.getLevel(), profile.getId()); + if (destination == null) { + source.sendFailure(Component.literal("Failure: found no data").withStyle(ChatFormatting.RED)); + return; + } + sourcePlayer.teleportTo(destination.x(), destination.y(), destination.z()); + } catch (IOException e) { + AdminCraft.LOGGER.error("Failed to save offline teleport location for {}", profile.getName(), e); + source.sendFailure(Component.literal("Failure: something went wrong (IOException)").withStyle(ChatFormatting.RED)); + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/fr/liveinground/admin_craft/moderation/CustomSanctionSystem.java b/src/main/java/fr/liveinground/admin_craft/moderation/CustomSanctionSystem.java index 7d95e00..58f2d16 100644 --- a/src/main/java/fr/liveinground/admin_craft/moderation/CustomSanctionSystem.java +++ b/src/main/java/fr/liveinground/admin_craft/moderation/CustomSanctionSystem.java @@ -1,5 +1,6 @@ package fr.liveinground.admin_craft.moderation; +import com.mojang.authlib.GameProfile; import fr.liveinground.admin_craft.AdminCraft; import fr.liveinground.admin_craft.Config; import fr.liveinground.admin_craft.PlaceHolderSystem; @@ -7,6 +8,7 @@ import fr.liveinground.admin_craft.storage.types.sanction.Sanction; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; @@ -19,18 +21,32 @@ import java.util.concurrent.TimeUnit; public class CustomSanctionSystem { - public static void banPlayer(MinecraftServer server, String source, ServerPlayer player, String reason, @Nullable Date expiresOn) { + public static void banPlayer(MinecraftServer server, String source, GameProfile player, String reason, @Nullable Date expiresOn) { PlayerList playerList = server.getPlayerList(); UserBanList banList = playerList.getBans(); - if (banList.isBanned(player.getGameProfile())) { + if (banList.isBanned(player)) { return; } - UserBanListEntry banEntry = new UserBanListEntry(player.getGameProfile(), null, source, expiresOn, reason); + UserBanListEntry banEntry = new UserBanListEntry(player, null, source, expiresOn, reason); banList.add(banEntry); - player.connection.disconnect(Component.literal("You are banned on this server:\n" + reason).withStyle(ChatFormatting.RED)); + ServerPlayer serverPlayer = AdminCraft.getOnlinePlayer(server, player); + if (serverPlayer != null) { + MutableComponent banMessage = Component.literal(""); + banMessage.append(Component.literal("You are now banned on this server.\nReason ยป ").withStyle(ChatFormatting.RED)); + banMessage.append(Component.literal(reason + "\n").withStyle(ChatFormatting.YELLOW)); + if (expiresOn == null) { + banMessage.append(Component.literal("This is a permanent ban.\n")).withStyle(ChatFormatting.RED); + } else { + banMessage.append(Component.literal("This sanction will end on ").withStyle(ChatFormatting.RED)); + banMessage.append(Component.literal(expiresOn + "\n").withStyle(ChatFormatting.YELLOW)); + } + + serverPlayer.connection.disconnect(banMessage); - AdminCraft.playerDataManager.addSanction(player.getStringUUID(), Sanction.BAN, reason, expiresOn); + } + + AdminCraft.playerDataManager.addSanction(String.valueOf(player.getId()), Sanction.BAN, reason, expiresOn); } public static void kickPlayer(ServerPlayer player, String reason) { @@ -38,38 +54,42 @@ public static void kickPlayer(ServerPlayer player, String reason) { AdminCraft.playerDataManager.addSanction(player.getStringUUID(), Sanction.KICK, reason, null); } - public static void mutePlayer(ServerPlayer player, String reason, @Nullable Date expiresOn) { - if (!AdminCraft.mutedPlayersUUID.contains(player.getStringUUID())) { + public static void mutePlayer(MinecraftServer server, GameProfile player, String reason, @Nullable Date expiresOn) { + if (!AdminCraft.mutedPlayersUUID.contains(String.valueOf(player.getId()))) { AdminCraft.playerDataManager.addMuteEntry( - new PlayerMuteData(player.getName().getString(), player.getStringUUID(), reason, expiresOn) + new PlayerMuteData(player.getName(), String.valueOf(player.getId()), reason, expiresOn) ); - String msg = PlaceHolderSystem.replacePlaceholders(Config.mute_message, Map.of("reason", reason)); - player.sendSystemMessage(Component.literal(msg).withStyle(ChatFormatting.RED)); + ServerPlayer serverPlayer = AdminCraft.getOnlinePlayer(server, player); + if (serverPlayer != null) { + String msg = PlaceHolderSystem.replacePlaceholders(Config.mute_message, Map.of("reason", reason)); + serverPlayer.sendSystemMessage(Component.literal(msg).withStyle(ChatFormatting.RED)); + - if (expiresOn != null) { - long diff = expiresOn.getTime() - System.currentTimeMillis(); - long days = TimeUnit.MILLISECONDS.toDays(diff); - diff -= TimeUnit.DAYS.toMillis(days); + if (expiresOn != null) { + long diff = expiresOn.getTime() - System.currentTimeMillis(); + long days = TimeUnit.MILLISECONDS.toDays(diff); + diff -= TimeUnit.DAYS.toMillis(days); - long hours = TimeUnit.MILLISECONDS.toHours(diff); - diff -= TimeUnit.HOURS.toMillis(hours); + long hours = TimeUnit.MILLISECONDS.toHours(diff); + diff -= TimeUnit.HOURS.toMillis(hours); - long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); + long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); - String daysStr = String.valueOf(days); - String hoursStr = String.valueOf(hours); - String minutesStr = String.valueOf(minutes); + String daysStr = String.valueOf(days); + String hoursStr = String.valueOf(hours); + String minutesStr = String.valueOf(minutes); - String timeMsg = PlaceHolderSystem.replacePlaceholders( - Config.time_remaining, - Map.of( - "days", daysStr, - "hours", hoursStr, - "minutes", minutesStr - ) - ); - player.sendSystemMessage(Component.literal(timeMsg).withStyle(ChatFormatting.YELLOW)); + String timeMsg = PlaceHolderSystem.replacePlaceholders( + Config.time_remaining, + Map.of( + "days", daysStr, + "hours", hoursStr, + "minutes", minutesStr + ) + ); + serverPlayer.sendSystemMessage(Component.literal(timeMsg).withStyle(ChatFormatting.YELLOW)); + } } } } diff --git a/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataLoader.java b/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataLoader.java index 25419ca..86310b2 100644 --- a/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataLoader.java +++ b/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataLoader.java @@ -8,10 +8,13 @@ import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.phys.Vec3; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; public class PlayerDataLoader { @@ -69,4 +72,47 @@ public static SimpleContainer loadEnderChestFromUUID(ServerLevel level, UUID uui else return loadEnderChestFromNBT(tag); } + @Nullable + public static List listOfflineTags(ServerLevel level, UUID uuid) throws IOException { + File playerDataDir = level.getServer() + .getWorldPath(LevelResource.PLAYER_DATA_DIR) + .toFile(); + + File file = new File(playerDataDir, uuid.toString() + ".dat"); + if (!file.exists()) return null; + + CompoundTag root = NbtIo.readCompressed(file); + ListTag list; + + if (root.contains("Tags", Tag.TAG_LIST)) { + list = root.getList("Tags", Tag.TAG_STRING); + } else { + list = new ListTag(); + } + + List tagStringArray = new ArrayList<>(); + for (Tag t: list) { + tagStringArray.add(t.getAsString()); + } + return tagStringArray; + } + + @Nullable + public static Vec3 getOfflineLocation(ServerLevel level, UUID uuid) throws IOException { + File playerDataDir = level.getServer() + .getWorldPath(LevelResource.PLAYER_DATA_DIR) + .toFile(); + + File file = new File(playerDataDir, uuid.toString() + ".dat"); + if (!file.exists()) return null; + + CompoundTag root = NbtIo.readCompressed(file); + + ListTag list; + if (root.contains("Pos", Tag.TAG_DOUBLE)) { + list = root.getList("Pos", Tag.TAG_DOUBLE); + } else return null; + + return new Vec3(list.getDouble(0), list.getDouble(1), list.getDouble(2)); + } } diff --git a/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataSaver.java b/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataSaver.java index 8ecdd26..c5ae894 100644 --- a/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataSaver.java +++ b/src/main/java/fr/liveinground/admin_craft/storage/nbt/PlayerDataSaver.java @@ -1,9 +1,7 @@ package fr.liveinground.admin_craft.storage.nbt; import fr.liveinground.admin_craft.AdminCraft; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.*; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.SimpleContainer; @@ -11,6 +9,7 @@ import net.minecraft.world.inventory.PlayerEnderChestContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.phys.Vec3; import java.io.File; import java.io.IOException; @@ -106,4 +105,91 @@ public static void saveEnderChestInventory(ServerLevel level, UUID uuid, SimpleC NbtIo.writeCompressed(root, file); } + public static int addOfflineTag(ServerLevel level, UUID uuid, String tag) throws IOException { + File playerDataDir = level.getServer() + .getWorldPath(LevelResource.PLAYER_DATA_DIR) + .toFile(); + + File file = new File(playerDataDir, uuid.toString() + ".dat"); + if (!file.exists()) return 404; + + CompoundTag root = NbtIo.readCompressed(file); + ListTag list; + + if (root.contains("Tags", Tag.TAG_LIST)) { + list = root.getList("Tags", Tag.TAG_STRING); + } else { + list = new ListTag(); + } + + for (int i = 0; i < list.size(); i++) { + if (list.getString(i).equalsIgnoreCase(tag)) { + return 409; + } + } + + list.add(StringTag.valueOf(tag)); + root.put("Tags", list); + + NbtIo.writeCompressed(root, file); + + return 200; + } + + public static int removeOfflineTag(ServerLevel level, UUID uuid, String tag) throws IOException { + File playerDataDir = level.getServer() + .getWorldPath(LevelResource.PLAYER_DATA_DIR) + .toFile(); + + File file = new File(playerDataDir, uuid.toString() + ".dat"); + if (!file.exists()) return 404; + + CompoundTag root = NbtIo.readCompressed(file); + + if (!root.contains("Tags", Tag.TAG_LIST)) { + return 409; + } + + ListTag list = root.getList("Tags", Tag.TAG_STRING); + + boolean removed = false; + for (int i = 0; i < list.size(); i++) { + if (list.getString(i).equalsIgnoreCase(tag)) { + list.remove(i); + removed = true; + break; + } + } + + if (!removed) { + return 409; + } + + root.put("Tags", list); + + NbtIo.writeCompressed(root, file); + + return 200; + } + + public static void setOfflineLocation(ServerLevel level, UUID uuid, Vec3 position) throws IOException { + File playerDataDir = level.getServer() + .getWorldPath(LevelResource.PLAYER_DATA_DIR) + .toFile(); + + File file = new File(playerDataDir, uuid.toString() + ".dat"); + if (!file.exists()) return; + + CompoundTag root = NbtIo.readCompressed(file); + + ListTag newPos = new ListTag(); + newPos.add(DoubleTag.valueOf(position.x)); + newPos.add(DoubleTag.valueOf(position.y)); + newPos.add(DoubleTag.valueOf(position.z)); + + root.put("Pos", newPos); + + NbtIo.writeCompressed(root, file); + } + }