diff --git a/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java b/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java new file mode 100644 index 0000000..e15e967 --- /dev/null +++ b/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java @@ -0,0 +1,49 @@ +package org.examp.john.mixin.client; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.PlayerTabOverlay; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.GameType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Прибирає курсив та сірий колір для spectator у таб-листі. + */ +@Mixin(PlayerTabOverlay.class) +public class PlayerTabOverlayMixin { + + /** + * Прибирає курсив з назви spectator в табі. + */ + @Inject(method = "getNameForDisplay", at = @At("RETURN"), cancellable = true) + private void removeSpectatorItalic(PlayerInfo playerInfo, CallbackInfoReturnable cir) { + if (playerInfo.getGameMode() == GameType.SPECTATOR) { + Component original = cir.getReturnValue(); + cir.setReturnValue(original.copy().withStyle(original.getStyle().withItalic(false))); + } + } + + /** + * Перехоплюємо виклик text(Font, Component, x, y, color) в extractRenderState. + * Якщо колір = -1862270977 (сірий для spectator), замінюємо на -1 (білий). + */ + @Redirect( + method = "extractRenderState", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;III)V" + ) + ) + private void forceWhiteText(GuiGraphicsExtractor instance, Font font, Component text, int x, int y, int color) { + if (color == -1862270977) { + color = -1; // білий + } + instance.text(font, text, x, y, color); + } +} diff --git a/src/client/resources/msnexus.client.mixins.json b/src/client/resources/msnexus.client.mixins.json index b19dc56..f4006f2 100644 --- a/src/client/resources/msnexus.client.mixins.json +++ b/src/client/resources/msnexus.client.mixins.json @@ -4,6 +4,7 @@ "package": "org.examp.john.mixin.client", "compatibilityLevel": "JAVA_25", "client": [ + "PlayerTabOverlayMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/java/org/examp/john/AntiAbuse.java b/src/main/java/org/examp/john/AntiAbuse.java index 5a47ad7..db139a1 100644 --- a/src/main/java/org/examp/john/AntiAbuse.java +++ b/src/main/java/org/examp/john/AntiAbuse.java @@ -65,7 +65,7 @@ public class AntiAbuse { double spread = Math.max(b[1] - b[0], b[3] - b[2]); if (spread > DISTANCE_THRESHOLD) { - Localchat.magicModLog.warn( + MSNexus.magicModLog.warn( "[ANTI-ABUSE] Гравець {} ймовірно шукає інших: " + "'{}' ×{} разів, переміщення ~{} блоків", player.getName().getString(), diff --git a/src/main/java/org/examp/john/ChatHandler.java b/src/main/java/org/examp/john/ChatHandler.java index db75e7b..6031759 100644 --- a/src/main/java/org/examp/john/ChatHandler.java +++ b/src/main/java/org/examp/john/ChatHandler.java @@ -5,8 +5,11 @@ import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.model.user.User; import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import org.examp.john.stealth.StealthStorage; @@ -126,8 +129,8 @@ public class ChatHandler { } private static Component createMentionComponent(ServerPlayer player) { - String prefix = getLuckPermsPrefix(player); - String formattedPrefix = prefix.replace("&", "\u00a7"); + String lpPrefix = getLuckPermsPrefix(player); + String formattedPrefix = lpPrefix.replace("&", "\u00a7"); String name = player.getName().getString(); return Component.literal(formattedPrefix + name) @@ -140,21 +143,31 @@ public class ChatHandler { } private static MutableComponent buildFormattedName(ServerPlayer player) { - String prefix = ""; + String lpPrefix = getLuckPermsPrefix(player); + String playerName = player.getName().getString(); + String formattedPrefix = lpPrefix.replace("&", "\u00a7"); + + Style clickStyle = Style.EMPTY + .withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " ")) + .withHoverEvent(new HoverEvent.ShowText( + Component.literal("Натисніть щоб написати " + playerName) + .withStyle(ChatFormatting.GRAY))); + + return Component.literal(formattedPrefix + playerName).withStyle(clickStyle); + } + + private static String getLuckPermsPrefix(ServerPlayer player) { try { LuckPerms luckPerms = LuckPermsProvider.get(); User user = luckPerms.getUserManager().getUser(player.getUUID()); if (user != null) { String metaPrefix = user.getCachedData().getMetaData().getPrefix(); if (metaPrefix != null) { - prefix = metaPrefix; + return metaPrefix; } } } catch (Throwable ignored) { } - - return Component.empty() - .append(Component.literal(prefix.replace("&", "§"))) - .append(player.getDisplayName()); + return ""; } } diff --git a/src/main/java/org/examp/john/MSNexus.java b/src/main/java/org/examp/john/MSNexus.java index 4b067d9..8eda984 100644 --- a/src/main/java/org/examp/john/MSNexus.java +++ b/src/main/java/org/examp/john/MSNexus.java @@ -1,6 +1,8 @@ package org.examp.john; import net.fabricmc.api.ModInitializer; +import org.examp.john.social.FakeJoinCommand; +import org.examp.john.social.FakeLeaveCommand; import org.examp.john.stealth.StealthCommand; import org.examp.john.stealth.StealthStorage; import org.slf4j.Logger; @@ -15,5 +17,7 @@ public class MSNexus implements ModInitializer { StealthStorage.load(); ChatHandler.register(); StealthCommand.register(); + FakeLeaveCommand.register(); + FakeJoinCommand.register(); } } diff --git a/src/main/java/org/examp/john/mixin/LeaveMessageMixin.java b/src/main/java/org/examp/john/mixin/LeaveMessageMixin.java new file mode 100644 index 0000000..c39537e --- /dev/null +++ b/src/main/java/org/examp/john/mixin/LeaveMessageMixin.java @@ -0,0 +1,35 @@ +package org.examp.john.mixin; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.players.PlayerList; +import org.examp.john.social.FakeLeaveStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Перехоплює відправку повідомлення "гравець вийшов з гри" + * якщо гравець в стані fake leave. + * Використовує @Redirect щоб НЕ блокувати весь disconnect. + */ +@Mixin(ServerGamePacketListenerImpl.class) +public class LeaveMessageMixin { + + @Redirect( + method = "removePlayerFromWorld", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/players/PlayerList;broadcastSystemMessage(Lnet/minecraft/network/chat/Component;Z)V" + ) + ) + private void suppressLeaveMessage(PlayerList instance, Component message, boolean isOverlay) { + ServerGamePacketListenerImpl self = (ServerGamePacketListenerImpl) (Object) this; + if (FakeLeaveStorage.contains(self.player.getUUID())) { + FakeLeaveStorage.remove(self.player.getUUID()); + return; + } + instance.broadcastSystemMessage(message, isOverlay); + } +} diff --git a/src/main/java/org/examp/john/social/FakeJoinCommand.java b/src/main/java/org/examp/john/social/FakeJoinCommand.java new file mode 100644 index 0000000..f54904d --- /dev/null +++ b/src/main/java/org/examp/john/social/FakeJoinCommand.java @@ -0,0 +1,88 @@ +package org.examp.john.social; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.NameAndId; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +/** + * /fakejoin — для опів та гравців з LuckPerms: msnexus.command.fakejoin. + * Надсилає фейкове повідомлення "приєднався до гри", повертає в таб. + */ +public class FakeJoinCommand { + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> + dispatcher.register(buildCommand()) + ); + } + + private static LiteralArgumentBuilder buildCommand() { + return Commands.literal("fakejoin") + .requires(source -> hasPermission(source)) + .executes(FakeJoinCommand::execute); + } + + private static boolean hasPermission(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) return false; + // Опка = доступ за замовчуванням + MinecraftServer server = source.getServer(); + if (server.getPlayerList().isOp(new NameAndId(player.getUUID(), player.getName().getString()))) { + return true; + } + // Інакше — через LuckPerms + try { + LuckPerms api = LuckPermsProvider.get(); + User user = api.getUserManager().getUser(player.getUUID()); + if (user == null) return false; + return user.getCachedData().getPermissionData().checkPermission("msnexus.command.fakejoin").asBoolean(); + } catch (Throwable e) { + return false; + } + } + + private static int execute(CommandContext context) { + if (!(context.getSource().getEntity() instanceof ServerPlayer player)) { + context.getSource().sendFailure( + Component.literal("Цю команду можна викликати лише в грі.")); + return 0; + } + + FakeLeaveStorage.remove(player.getUUID()); + + MinecraftServer server = player.level().getServer(); + + // Фейкове повідомлення "приєднався до гри" + server.getPlayerList().broadcastSystemMessage( + Component.translatable("multiplayer.player.joined", player.getDisplayName()) + .withStyle(ChatFormatting.YELLOW), + false + ); + + // Повернути в таб — надіслати інформацію про гравця ВСІМ + ClientboundPlayerInfoUpdatePacket addPacket = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); + for (ServerPlayer online : server.getPlayerList().getPlayers()) { + online.connection.send(addPacket); + } + + player.sendSystemMessage( + Component.literal("Fake join активовано. Вас повернуто в таб.") + .withStyle(ChatFormatting.GRAY)); + return 1; + } +} diff --git a/src/main/java/org/examp/john/social/FakeLeaveCommand.java b/src/main/java/org/examp/john/social/FakeLeaveCommand.java new file mode 100644 index 0000000..889d484 --- /dev/null +++ b/src/main/java/org/examp/john/social/FakeLeaveCommand.java @@ -0,0 +1,91 @@ +package org.examp.john.social; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.GameType; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + +/** + * /fakeleave — для опів та гравців з LuckPerms: msnexus.command.fakeleave. + * Надсилає фейкове повідомлення "гравець вийшов", ховає з табу. + */ +public class FakeLeaveCommand { + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> + dispatcher.register(buildCommand()) + ); + } + + private static LiteralArgumentBuilder buildCommand() { + return Commands.literal("fakeleave") + .requires(source -> hasPermission(source)) + .executes(FakeLeaveCommand::execute); + } + + private static boolean hasPermission(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) return false; + // Опка = доступ за замовчуванням + MinecraftServer server = source.getServer(); + if (server.getPlayerList().isOp(new NameAndId(player.getUUID(), player.getName().getString()))) { + return true; + } + // Інакше — через LuckPerms + try { + LuckPerms api = LuckPermsProvider.get(); + User user = api.getUserManager().getUser(player.getUUID()); + if (user == null) return false; + return user.getCachedData().getPermissionData().checkPermission("msnexus.command.fakeleave").asBoolean(); + } catch (Throwable e) { + return false; + } + } + + private static int execute(CommandContext context) { + if (!(context.getSource().getEntity() instanceof ServerPlayer player)) { + context.getSource().sendFailure( + Component.literal("Цю команду можна викликати лише в грі.")); + return 0; + } + + FakeLeaveStorage.add(player.getUUID()); + + MinecraftServer server = player.level().getServer(); + + // Фейкове повідомлення "вийшов з гри" + server.getPlayerList().broadcastSystemMessage( + Component.translatable("multiplayer.player.left", player.getDisplayName()) + .withStyle(ChatFormatting.YELLOW), + false + ); + + // Прибрати з табу для ВСІХ гравців (включаючи себе) + ClientboundPlayerInfoRemovePacket removePacket = + new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())); + for (ServerPlayer online : server.getPlayerList().getPlayers()) { + online.connection.send(removePacket); + } + + player.sendSystemMessage( + Component.literal("Fake leave активовано. Вас прибрано з табу.") + .withStyle(ChatFormatting.GRAY)); + return 1; + } +} diff --git a/src/main/java/org/examp/john/social/FakeLeaveStorage.java b/src/main/java/org/examp/john/social/FakeLeaveStorage.java new file mode 100644 index 0000000..ce85304 --- /dev/null +++ b/src/main/java/org/examp/john/social/FakeLeaveStorage.java @@ -0,0 +1,27 @@ +package org.examp.john.social; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Трекінг гравців у стані "fake leave". + * Коли гравець використав /fakeleave, його UUID додається до цього сету. + * Коли гравець реально виходить з сервера — реальний leave message блокується. + */ +public class FakeLeaveStorage { + + private static final Set fakeLeft = new HashSet<>(); + + public static void add(UUID uuid) { + fakeLeft.add(uuid); + } + + public static void remove(UUID uuid) { + fakeLeft.remove(uuid); + } + + public static boolean contains(UUID uuid) { + return fakeLeft.contains(uuid); + } +} diff --git a/src/main/java/org/examp/john/stealth/StealthStorage.java b/src/main/java/org/examp/john/stealth/StealthStorage.java index d26c9b7..c273f9f 100644 --- a/src/main/java/org/examp/john/stealth/StealthStorage.java +++ b/src/main/java/org/examp/john/stealth/StealthStorage.java @@ -3,7 +3,7 @@ package org.examp.john.stealth; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; -import org.examp.john.Localchat; +import org.examp.john.MSNexus; import java.io.IOException; import java.io.Reader; @@ -45,7 +45,7 @@ public class StealthStorage { } } } catch (IOException | JsonSyntaxException | IllegalArgumentException e) { - Localchat.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); + MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); data = new HashMap<>(); } } @@ -78,7 +78,7 @@ public class StealthStorage { GSON.toJson(raw, writer); } } catch (IOException e) { - Localchat.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage()); + MSNexus.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage()); } } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 7584f25..c7fb683 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,5 +1,5 @@ { - "schemaVersion": 1,gi + "schemaVersion": 1, "id": "msnexus", "version": "${version}", "name": "msnexus", diff --git a/src/main/resources/msnexus.mixins.json b/src/main/resources/msnexus.mixins.json index eb1a5f0..7fcfb16 100644 --- a/src/main/resources/msnexus.mixins.json +++ b/src/main/resources/msnexus.mixins.json @@ -4,6 +4,7 @@ "package": "org.examp.john.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ + "LeaveMessageMixin" ], "injectors": { "defaultRequire": 1