diff --git a/.gitignore b/.gitignore index d26488b..b191285 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -build/** -run/** -.idea/** -.gradle/** +build/ +run/ +out/ +/org/ +*.class +.idea/ +.gradle/ diff --git a/build.gradle b/build.gradle index 6a79a1d..9b39cf6 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ loom { splitEnvironmentSourceSets() mods { - "localchat" { + "msnexus" { sourceSet sourceSets.main sourceSet sourceSets.client } diff --git a/gradle.properties b/gradle.properties index 1a68be0..82e47c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx2G minecraft_version=26.1.2 yarn_mappings=1.21.11+build.6 loader_version=0.19.2 -mod_version=1.0 +mod_version=1.1 maven_group=org.example1 -archives_base_name=localhost +archives_base_name=msnexus fabric_version=0.155.2+26.1.2 diff --git a/org/examp/john/rules/RulesCommand.class b/org/examp/john/rules/RulesCommand.class deleted file mode 100644 index ea12c27..0000000 Binary files a/org/examp/john/rules/RulesCommand.class and /dev/null differ diff --git a/out/production/resources/fabric.mod.json b/out/production/resources/fabric.mod.json deleted file mode 100644 index 7694098..0000000 --- a/out/production/resources/fabric.mod.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "schemaVersion": 1, - "id": "localchat", - "version": "${version}", - "name": "localchat", - "description": "", - "authors": [], - "contact": {}, - "license": "All-Rights-Reserved", - "icon": "assets/localchat/icon.png", - "environment": "*", - "entrypoints": { - "fabric-datagen": [ - "org.examp.john.client.LocalchatDataGenerator" - ], - "client": [ - "org.examp.john.client.LocalchatClient" - ], - "main": [ - "org.examp.john.Localchat" - ] - }, - "mixins": [ - "localchat.mixins.json", - { - "config": "localchat.client.mixins.json", - "environment": "client" - } - ], - "depends": { - "fabricloader": ">=${loader_version}", - "fabric-api": "*", - "minecraft": "${minecraft_version}" - } -} diff --git a/out/production/resources/localchat.mixins.json b/out/production/resources/localchat.mixins.json deleted file mode 100644 index eb1a5f0..0000000 --- a/out/production/resources/localchat.mixins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "org.examp.john.mixin", - "compatibilityLevel": "JAVA_25", - "mixins": [ - ], - "injectors": { - "defaultRequire": 1 - }, - "overwrites": { - "requireAnnotations": true - } -} diff --git a/src/client/java/org/examp/john/client/MSNexusClient.java b/src/client/java/org/examp/john/client/MSNexusClient.java new file mode 100644 index 0000000..adfb882 --- /dev/null +++ b/src/client/java/org/examp/john/client/MSNexusClient.java @@ -0,0 +1,10 @@ +package org.examp.john.client; + +import net.fabricmc.api.ClientModInitializer; + +public class MSNexusClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + } +} diff --git a/src/client/java/org/examp/john/client/LocalchatDataGenerator.java b/src/client/java/org/examp/john/client/MSNexusDataGenerator.java similarity index 82% rename from src/client/java/org/examp/john/client/LocalchatDataGenerator.java rename to src/client/java/org/examp/john/client/MSNexusDataGenerator.java index caa73e5..ee4aae9 100644 --- a/src/client/java/org/examp/john/client/LocalchatDataGenerator.java +++ b/src/client/java/org/examp/john/client/MSNexusDataGenerator.java @@ -3,7 +3,7 @@ package org.examp.john.client; import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; -public class LocalchatDataGenerator implements DataGeneratorEntrypoint { +public class MSNexusDataGenerator implements DataGeneratorEntrypoint { @Override public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { diff --git a/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java b/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java deleted file mode 100644 index ca8c6a8..0000000 --- a/src/client/java/org/examp/john/mixin/client/PlayerTabOverlayMixin.java +++ /dev/null @@ -1,49 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/src/client/resources/localchat.client.mixins.json b/src/client/resources/localchat.client.mixins.json deleted file mode 100644 index f4006f2..0000000 --- a/src/client/resources/localchat.client.mixins.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "org.examp.john.mixin.client", - "compatibilityLevel": "JAVA_25", - "client": [ - "PlayerTabOverlayMixin" - ], - "injectors": { - "defaultRequire": 1 - }, - "overwrites": { - "requireAnnotations": true - } -} diff --git a/out/client/resources/localchat.client.mixins.json b/src/client/resources/msnexus.client.mixins.json similarity index 100% rename from out/client/resources/localchat.client.mixins.json rename to src/client/resources/msnexus.client.mixins.json diff --git a/src/main/java/org/examp/john/AntiAbuse.java b/src/main/java/org/examp/john/AntiAbuse.java index 7d53289..2d0663e 100644 --- a/src/main/java/org/examp/john/AntiAbuse.java +++ b/src/main/java/org/examp/john/AntiAbuse.java @@ -1,5 +1,6 @@ package org.examp.john; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.server.level.ServerPlayer; import java.util.ArrayList; @@ -11,16 +12,30 @@ import java.util.UUID; /** * Детектить патерн пошуку гравців через локальний чат. * - * Якщо одне і те ж повідомлення повторюється >=5 разів за 5 хвилин - * і гравець перемістився >50 блоків — логує в консоль. + * Алгоритм: + * 1. Записує кожне локальне повідомлення (текст + координати + час) + * 2. Якщо одне і те ж повідомлення повторюється >=5 разів за 5 хвилин + * І гравець перемістився більше ніж на 50 блоків — + * це очевидний пошук гравців. + * 3. Логує в термінал для адміна. */ public class AntiAbuse { private static final int REPEAT_THRESHOLD = 5; private static final double DISTANCE_THRESHOLD = 50.0; private static final long TIME_WINDOW_MS = 5 * 60 * 1000; + private static final long ALERT_COOLDOWN_MS = 30 * 1000; private static final Map> history = new HashMap<>(); + private static final Map lastAlertTime = new HashMap<>(); + + public static void register() { + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + UUID uuid = handler.player.getUUID(); + history.remove(uuid); + lastAlertTime.remove(uuid); + }); + } public static void recordMessage(ServerPlayer player, String message) { UUID uuid = player.getUUID(); @@ -38,6 +53,7 @@ public class AntiAbuse { } private static void checkForAbuse(ServerPlayer player, List records) { + UUID uuid = player.getUUID(); Map counts = new HashMap<>(); Map bounds = new HashMap<>(); @@ -61,7 +77,13 @@ public class AntiAbuse { double spread = Math.max(b[1] - b[0], b[3] - b[2]); if (spread > DISTANCE_THRESHOLD) { - Localchat.magicModLog.warn( + long now = System.currentTimeMillis(); + Long lastAlert = lastAlertTime.get(uuid); + if (lastAlert != null && now - lastAlert < ALERT_COOLDOWN_MS) { + continue; + } + lastAlertTime.put(uuid, now); + 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 901ae29..bc87550 100644 --- a/src/main/java/org/examp/john/ChatHandler.java +++ b/src/main/java/org/examp/john/ChatHandler.java @@ -15,90 +15,236 @@ import net.minecraft.server.level.ServerPlayer; import org.examp.john.stealth.StealthStorage; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Locale; public class ChatHandler { public static void register() { - final String prefix = "!"; - final float LOCAL_CHAT_RADIUS = 100; - ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> { - String text = message.signedContent(); - ServerLevel serverLevel = sender.level(); - List players = serverLevel.getServer().getPlayerList().getPlayers(); - - MutableComponent formattedName = buildFormattedName(sender); - - if (text.startsWith(prefix)) { - String actualMessage = text.substring(1).trim(); - Component globalMessage = Component.empty() - .append(formattedName) - .append(Component.literal(": " + actualMessage)); - - for (ServerPlayer player : players) { - player.sendSystemMessage(globalMessage); - } - - ChatLogger.logGlobal(sender, actualMessage, players); - - } else { - AntiAbuse.recordMessage(sender, text); - - if (StealthStorage.isEnabled(sender.getUUID())) { - sender.sendSystemMessage( - Component.literal("Вас ніхто не почув...") - .withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW) - ); - ChatLogger.logUnheard(sender, text); - return false; - } - - List hearers = new ArrayList<>(); - for (ServerPlayer player : players) { - if (player != sender - && player.level() == serverLevel - && player.distanceTo(sender) <= LOCAL_CHAT_RADIUS) { - hearers.add(player); - } - } - - if (!hearers.isEmpty()) { - Component localMessage = Component.empty() - .append(formattedName) - .append(Component.literal(": " + text).withStyle(ChatFormatting.GRAY)); - - sender.sendSystemMessage(localMessage); - for (ServerPlayer hearer : hearers) { - hearer.sendSystemMessage(localMessage); - } - - ChatLogger.logLocal(sender, text, hearers); - - } else { - sender.sendSystemMessage( - Component.literal("Вас ніхто не почув, додайте \"!\" на початок для глобального чату") - .withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW) - ); - - ChatLogger.logUnheard(sender, text); - } + try { + return handleMessage(message.signedContent(), sender); + } catch (Exception e) { + MSNexus.magicModLog.error( + "[CHAT] Не вдалося обробити повідомлення від {}", sender.getName().getString(), e); + return false; } - return false; }); } - private static MutableComponent buildFormattedName(ServerPlayer player) { - String luckPermsPrefix = getLuckPermsPrefix(player); - String playerName = player.getName().getString(); - String formattedPrefix = luckPermsPrefix.replace("&", "\u00a7"); + private static boolean handleMessage(String text, ServerPlayer sender) { + final String prefix = "!"; + final float LOCAL_CHAT_RADIUS = 100; - Style clickStyle = Style.EMPTY - .withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " ")) - .withHoverEvent(new HoverEvent.ShowText( - Component.literal("Натисніть щоб написати " + playerName) - .withStyle(ChatFormatting.GRAY))); + if (text.isBlank()) { + return false; + } - return Component.literal(formattedPrefix + playerName).withStyle(clickStyle); + ServerLevel serverLevel = sender.level(); + List players = serverLevel.getServer().getPlayerList().getPlayers(); + + MutableComponent formattedName = buildFormattedName(sender); + + if (text.startsWith(prefix)) { + String actualMessage = text.substring(1).trim(); + if (actualMessage.isEmpty()) { + return false; + } + Component globalMessage = Component.empty() + .append(formattedName) + .append(Component.literal(": ")) + .append(buildFormattedMessage(actualMessage, players)); + + for (ServerPlayer player : players) { + player.sendSystemMessage(globalMessage); + } + + ChatLogger.logGlobal(sender, actualMessage, players); + + } else { + AntiAbuse.recordMessage(sender, text); + + if (StealthStorage.isEnabled(sender.getUUID())) { + sender.sendSystemMessage( + Component.literal("Вас ніхто не почув...") + .withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW) + ); + ChatLogger.logUnheard(sender, text); + return false; + } + + List hearers = new ArrayList<>(); + for (ServerPlayer player : players) { + if (player != sender + && player.level() == serverLevel + && player.distanceTo(sender) <= LOCAL_CHAT_RADIUS) { + hearers.add(player); + } + } + + if (!hearers.isEmpty()) { + Component localMessage = Component.empty() + .append(formattedName) + .append(Component.literal(": ").withStyle(ChatFormatting.GRAY)) + .append(buildFormattedMessage(text, players).withStyle(ChatFormatting.GRAY)); + + sender.sendSystemMessage(localMessage); + for (ServerPlayer hearer : hearers) { + hearer.sendSystemMessage(localMessage); + } + + ChatLogger.logLocal(sender, text, hearers); + + } else { + sender.sendSystemMessage( + Component.literal("Вас ніхто не почув, додайте \"!\" на початок для глобального чату") + .withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW) + ); + + ChatLogger.logUnheard(sender, text); + } + } + return false; + } + + /** + * Парсить текст між ** і робить його курсивом, поєднуючи з підсвіткою згадок гравців. + * Приклад: "привіт **світе** як справи?" -> "привіт [курсив]світе[/курсив] як справи?" + */ + private static MutableComponent buildFormattedMessage(String text, List players) { + MutableComponent result = Component.empty(); + StringBuilder buffer = new StringBuilder(); + boolean italic = false; + + for (int i = 0; i < text.length(); i++) { + if (i + 1 < text.length() && text.charAt(i) == '*' && text.charAt(i + 1) == '*') { + appendFormattedSegment(result, buffer.toString(), players, italic); + buffer.setLength(0); + italic = !italic; + i++; + } else { + buffer.append(text.charAt(i)); + } + } + appendFormattedSegment(result, buffer.toString(), players, italic); + + return result; + } + + private static void appendFormattedSegment(MutableComponent result, String segment, List players, boolean italic) { + MutableComponent parsed = buildMentionMessage(segment, players); + if (italic) { + parsed.withStyle(Style.EMPTY.withItalic(true)); + } + result.append(parsed); + } + + private static MutableComponent buildMentionMessage(String text, List onlinePlayers) { + List sorted = onlinePlayers.stream() + .sorted(Comparator.comparingInt((ServerPlayer p) -> p.getName().getString().length()).reversed()) + .toList(); + + MutableComponent result = Component.empty(); + String remaining = text; + + while (!remaining.isEmpty()) { + int earliestIndex = remaining.length(); + ServerPlayer mentioned = null; + String lower = remaining.toLowerCase(Locale.ROOT); + + for (ServerPlayer player : sorted) { + String nameLower = player.getName().getString().toLowerCase(Locale.ROOT); + int index = indexOfBoundary(lower, nameLower); + if (index >= 0 && index < earliestIndex) { + earliestIndex = index; + mentioned = player; + } + } + + if (mentioned == null) { + result.append(Component.literal(remaining)); + break; + } + + boolean withAt = earliestIndex > 0 && remaining.charAt(earliestIndex - 1) == '@'; + int prefixEnd = withAt ? earliestIndex - 1 : earliestIndex; + if (prefixEnd > 0) { + result.append(Component.literal(remaining.substring(0, prefixEnd))); + } + + result.append(createMentionComponent(mentioned, withAt)); + + remaining = remaining.substring(earliestIndex + mentioned.getName().getString().length()); + } + + return result; + } + + private static int indexOfBoundary(String lowerHaystack, String lowerNeedle) { + int index = lowerHaystack.indexOf(lowerNeedle); + while (index >= 0) { + boolean okBefore = index == 0 || !isNameChar(lowerHaystack.charAt(index - 1)); + boolean okAfter = index + lowerNeedle.length() == lowerHaystack.length() + || !isNameChar(lowerHaystack.charAt(index + lowerNeedle.length())); + if (okBefore && okAfter) { + return index; + } + index = lowerHaystack.indexOf(lowerNeedle, index + 1); + } + return -1; + } + + private static boolean isNameChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + private static Component createMentionComponent(ServerPlayer player, boolean withAt) { + String name = player.getName().getString(); + String prefix = formatLegacyPrefix(getLuckPermsPrefix(player)); + String text = (withAt ? "@" : "") + prefix + name; + + return Component.literal(text) + .withStyle(Style.EMPTY + .withColor(ChatFormatting.AQUA) + .withClickEvent(new ClickEvent.SuggestCommand("/msg " + name + " ")) + .withHoverEvent(new HoverEvent.ShowText( + Component.literal("Натисніть щоб написати " + name) + .withStyle(ChatFormatting.GRAY))) + ); + } + + private static String formatLegacyPrefix(String raw) { + if (raw == null || raw.isEmpty()) { + return ""; + } + String s = raw.replace('&', '§'); + StringBuilder out = new StringBuilder(s.length() + 14); + int i = 0; + while (i < s.length()) { + if (s.charAt(i) == '§' && i + 8 <= s.length() && s.charAt(i + 1) == '#' + && isSixHex(s, i + 2)) { + out.append("§x"); + for (int j = 2; j < 8; j++) { + out.append('§').append(s.charAt(i + j)); + } + i += 8; + } else { + out.append(s.charAt(i)); + i++; + } + } + return out.toString(); + } + + private static boolean isSixHex(String s, int start) { + for (int i = start; i < start + 6; i++) { + char c = s.charAt(i); + boolean hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + if (!hex) { + return false; + } + } + return true; } private static String getLuckPermsPrefix(ServerPlayer player) { @@ -115,4 +261,19 @@ public class ChatHandler { } return ""; } + + private static MutableComponent buildFormattedName(ServerPlayer player) { + String prefix = formatLegacyPrefix(getLuckPermsPrefix(player)); + String playerName = player.getName().getString(); + + Style clickStyle = Style.EMPTY + .withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " ")) + .withHoverEvent(new HoverEvent.ShowText( + Component.literal("Натисніть щоб написати " + playerName) + .withStyle(ChatFormatting.GRAY))); + + return Component.empty() + .append(Component.literal(prefix)) + .append(player.getDisplayName().copy().withStyle(clickStyle)); + } } diff --git a/src/main/java/org/examp/john/ChatLogger.java b/src/main/java/org/examp/john/ChatLogger.java index 14adf56..e71cbf2 100644 --- a/src/main/java/org/examp/john/ChatLogger.java +++ b/src/main/java/org/examp/john/ChatLogger.java @@ -13,7 +13,8 @@ import java.util.stream.Collectors; public class ChatLogger { public static void logGlobal(ServerPlayer sender, String message, List recipients) { - Localchat.magicModLog.info( + // Для глобального чату ігноруємо список отримувачів і просто пишемо "everyone" + MSNexus.magicModLog.info( "[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}", sender.getName().getString(), message, @@ -22,13 +23,14 @@ public class ChatLogger { } public static void logLocal(ServerPlayer sender, String message, List recipients) { + // Для локального чату виводимо лише імена отримувачів, без їхніх координат String heardBy = recipients.isEmpty() ? "nobody" : recipients.stream() .map(p -> p.getName().getString()) .collect(Collectors.joining(", ")); - Localchat.magicModLog.info( + MSNexus.magicModLog.info( "[CHAT][LOCAL] {} said: \"{}\" | heard by: {} | pos: {}", sender.getName().getString(), message, @@ -38,7 +40,7 @@ public class ChatLogger { } public static void logUnheard(ServerPlayer sender, String message) { - Localchat.magicModLog.info( + MSNexus.magicModLog.info( "[CHAT][LOCAL][UNHEARD] {} said: \"{}\" | heard by: nobody | pos: {}", sender.getName().getString(), message, @@ -46,8 +48,11 @@ public class ChatLogger { ); } - private static String formatPos(ServerPlayer player) { + static String formatPos(ServerPlayer player) { ServerLevel level = player.level(); + + // У ResourceKey дістаємо Identifier/ResourceLocation через .identifier(), + // після чого через .getPath() беремо чисту назву ("overworld", "the_nether", "the_end") String rawDimension = level.dimension().identifier().getPath(); return String.format( diff --git a/src/main/java/org/examp/john/CommandLogger.java b/src/main/java/org/examp/john/CommandLogger.java new file mode 100644 index 0000000..63cf9e4 --- /dev/null +++ b/src/main/java/org/examp/john/CommandLogger.java @@ -0,0 +1,21 @@ +package org.examp.john; + +import net.minecraft.server.level.ServerPlayer; + +/** + * Логує команди, які виконують гравці, в консоль сервера. + * Формат: хто виконав ➔ яка команда ➔ координати гравця. + */ +public class CommandLogger { + + public static void logCommand(ServerPlayer player, String command) { + String normalized = command.startsWith("/") ? command : "/" + command; + + MSNexus.magicModLog.info( + "[CMD] {} ran: \"{}\" | pos: {}", + player.getName().getString(), + normalized, + ChatLogger.formatPos(player) + ); + } +} diff --git a/src/main/java/org/examp/john/Localchat.java b/src/main/java/org/examp/john/MSNexus.java similarity index 80% rename from src/main/java/org/examp/john/Localchat.java rename to src/main/java/org/examp/john/MSNexus.java index 37321c4..a0c79ae 100644 --- a/src/main/java/org/examp/john/Localchat.java +++ b/src/main/java/org/examp/john/MSNexus.java @@ -7,13 +7,14 @@ import org.examp.john.stealth.StealthStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class Localchat implements ModInitializer { - public static final String MOD_ID = "localhost"; +public class MSNexus implements ModInitializer { + public static final String MOD_ID = "msnexus"; public static final Logger magicModLog = LoggerFactory.getLogger(MOD_ID); @Override public void onInitialize() { StealthStorage.load(); + AntiAbuse.register(); ChatHandler.register(); StealthCommand.register(); RulesCommand.register(); diff --git a/src/main/java/org/examp/john/mixin/CommandLogMixin.java b/src/main/java/org/examp/john/mixin/CommandLogMixin.java new file mode 100644 index 0000000..e159bf8 --- /dev/null +++ b/src/main/java/org/examp/john/mixin/CommandLogMixin.java @@ -0,0 +1,38 @@ +package org.examp.john.mixin; + +import com.mojang.brigadier.ParseResults; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.level.ServerPlayer; +import org.examp.john.CommandLogger; +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.callback.CallbackInfo; + +/** + * Логує виконання команд гравцями в консоль сервера. + * Хукає Commands.performCommand — спільну точку виконання всіх команд. + */ +@Mixin(Commands.class) +public class CommandLogMixin { + + @Inject( + method = "performCommand(Lcom/mojang/brigadier/ParseResults;Ljava/lang/String;)V", + at = @At("HEAD") + ) + private void msnexus$logExecutedCommand(ParseResults parseResults, String command, CallbackInfo ci) { + try { + CommandSourceStack source = parseResults.getContext().getSource(); + if (source == null) { + return; + } + ServerPlayer player = source.getPlayer(); + if (player != null) { + CommandLogger.logCommand(player, command); + } + } catch (Exception e) { + org.examp.john.MSNexus.magicModLog.warn("[CMD] Не вдалося залогувати команду", e); + } + } +} diff --git a/src/main/java/org/examp/john/rules/RulesCommand.java b/src/main/java/org/examp/john/rules/RulesCommand.java index a0e2f1e..9b6486d 100644 --- a/src/main/java/org/examp/john/rules/RulesCommand.java +++ b/src/main/java/org/examp/john/rules/RulesCommand.java @@ -13,7 +13,7 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.WrittenBookContent; -import org.examp.john.Localchat; +import org.examp.john.MSNexus; import java.util.ArrayList; import java.util.HashMap; @@ -77,7 +77,7 @@ public class RulesCommand { ItemStack originalItem = player.getItemInHand(hand).copy(); ItemStack book = createRuleBook(rules); - Localchat.magicModLog.info("Відкриваємо книгу правил для {}", player.getName().getString()); + MSNexus.magicModLog.info("Відкриваємо книгу правил для {}", player.getName().getString()); player.setItemInHand(hand, book); player.containerMenu.sendAllDataToRemote(); diff --git a/src/main/java/org/examp/john/rules/RulesLoader.java b/src/main/java/org/examp/john/rules/RulesLoader.java index 2882984..34eac8e 100644 --- a/src/main/java/org/examp/john/rules/RulesLoader.java +++ b/src/main/java/org/examp/john/rules/RulesLoader.java @@ -3,7 +3,7 @@ package org.examp.john.rules; 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; @@ -48,16 +48,16 @@ public class RulesLoader { try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) { RulesData.RulesRoot root = GSON.fromJson(reader, RulesData.RulesRoot.class); if (root == null || root.sections == null) { - Localchat.magicModLog.warn("rules.json порожній або невалідний, використовую пустий список правил"); + MSNexus.magicModLog.warn("rules.json порожній або невалідний, використовую пустий список правил"); return new RulesData.RulesRoot(Collections.emptyList()); } return root; } } catch (IOException e) { - Localchat.magicModLog.error("Не вдалося прочитати rules.json: {}", e.getMessage()); + MSNexus.magicModLog.error("Не вдалося прочитати rules.json: {}", e.getMessage()); return new RulesData.RulesRoot(Collections.emptyList()); } catch (JsonSyntaxException e) { - Localchat.magicModLog.error("rules.json містить синтаксичну помилку JSON: {}", e.getMessage()); + MSNexus.magicModLog.error("rules.json містить синтаксичну помилку JSON: {}", e.getMessage()); return new RulesData.RulesRoot(Collections.emptyList()); } } @@ -71,6 +71,6 @@ public class RulesLoader { new GsonBuilder().setPrettyPrinting().create().toJson(defaultRoot, writer); } - Localchat.magicModLog.info("Створено конфіг правил за замовчуванням: {}", CONFIG_PATH.toAbsolutePath()); + MSNexus.magicModLog.info("Створено конфіг правил за замовчуванням: {}", CONFIG_PATH.toAbsolutePath()); } } \ No newline at end of file diff --git a/src/main/java/org/examp/john/stealth/StealthCommand.java b/src/main/java/org/examp/john/stealth/StealthCommand.java index 77749d7..b28e300 100644 --- a/src/main/java/org/examp/john/stealth/StealthCommand.java +++ b/src/main/java/org/examp/john/stealth/StealthCommand.java @@ -17,6 +17,10 @@ import net.minecraft.server.level.ServerPlayer; * /stealth on — увімкнути stealth * /stealth off — вимкнути stealth * /stealth info — показати інформацію про команду + * + * Коли stealth увімкнено, повідомлення локального чату від гравця + * не доходять до інших — гравець бачить "Вас ніхто не почув..." + * Глобальний чат (! префікс) працює як зазвичай. */ public class StealthCommand { @@ -37,6 +41,7 @@ public class StealthCommand { private static int executeToggle(CommandContext context) { ServerPlayer player = getPlayer(context); if (player == null) return 0; + boolean newState = StealthStorage.toggle(player.getUUID()); sendStatus(player, newState); return 1; @@ -45,6 +50,7 @@ public class StealthCommand { private static int executeOn(CommandContext context) { ServerPlayer player = getPlayer(context); if (player == null) return 0; + StealthStorage.set(player.getUUID(), true); sendStatus(player, true); return 1; @@ -53,6 +59,7 @@ public class StealthCommand { private static int executeOff(CommandContext context) { ServerPlayer player = getPlayer(context); if (player == null) return 0; + StealthStorage.set(player.getUUID(), false); sendStatus(player, false); return 1; diff --git a/src/main/java/org/examp/john/stealth/StealthStorage.java b/src/main/java/org/examp/john/stealth/StealthStorage.java index ebeb4dd..10576f4 100644 --- a/src/main/java/org/examp/john/stealth/StealthStorage.java +++ b/src/main/java/org/examp/john/stealth/StealthStorage.java @@ -2,8 +2,8 @@ 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 net.fabricmc.loader.api.FabricLoader; +import org.examp.john.MSNexus; import java.io.IOException; import java.io.Reader; @@ -11,6 +11,7 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -18,11 +19,14 @@ import java.util.UUID; /** * Зберігає стан stealth-режиму для кожного гравця. * Файл: config/localchat/stealth.json — {"uuid1": true, "uuid2": false, ...} + * Зміни зберігаються на диск при кожному toggle. */ public class StealthStorage { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private static final Path CONFIG_PATH = Path.of("config", "localchat", "stealth.json"); + private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir() + .resolve("localchat") + .resolve("stealth.json"); private static Map data = new HashMap<>(); @@ -34,23 +38,33 @@ public class StealthStorage { return; } try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) { - @SuppressWarnings("unchecked") - Map raw = GSON.fromJson(reader, Map.class); + Map raw = GSON.fromJson(reader, Map.class); data = new HashMap<>(); if (raw != null) { - for (Map.Entry entry : raw.entrySet()) { - data.put(UUID.fromString(entry.getKey()), entry.getValue()); + for (Map.Entry entry : raw.entrySet()) { + try { + UUID uuid = UUID.fromString(entry.getKey()); + if (entry.getValue() instanceof Boolean value) { + data.put(uuid, value); + } else { + MSNexus.magicModLog.warn( + "stealth.json: пропущено некоректне значення для {}", entry.getKey()); + } + } catch (IllegalArgumentException e) { + MSNexus.magicModLog.warn( + "stealth.json: пропущено некоректний ключ {}", entry.getKey()); + } } } } - } catch (IOException | JsonSyntaxException | IllegalArgumentException e) { - Localchat.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); + } catch (IOException | RuntimeException e) { + MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); data = new HashMap<>(); } } public static boolean isEnabled(UUID uuid) { - return data.getOrDefault(uuid, false); + return Boolean.TRUE.equals(data.get(uuid)); } public static boolean toggle(UUID uuid) { @@ -69,15 +83,22 @@ public class StealthStorage { private static void save() { try { Files.createDirectories(CONFIG_PATH.getParent()); - try (Writer writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) { + Path tmp = CONFIG_PATH.resolveSibling(CONFIG_PATH.getFileName() + ".tmp"); + try (Writer writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8)) { Map raw = new HashMap<>(); for (Map.Entry entry : data.entrySet()) { raw.put(entry.getKey().toString(), entry.getValue()); } GSON.toJson(raw, writer); } + try { + Files.move(tmp, CONFIG_PATH, + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + Files.move(tmp, CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING); + } } 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 e9a0fc4..236da3d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,28 +1,23 @@ { "schemaVersion": 1, - "id": "localchat", + "id": "msnexus", "version": "${version}", - "name": "localchat", + "name": "msnexus", "description": "", "authors": [], "contact": {}, "license": "All-Rights-Reserved", - "icon": "assets/localchat/icon.png", - "environment": "*", + "environment": "server", "entrypoints": { "fabric-datagen": [ - "org.examp.john.client.LocalchatDataGenerator" + "org.examp.john.client.MSNexusDataGenerator" ], "main": [ - "org.examp.john.Localchat" + "org.examp.john.MSNexus" ] }, "mixins": [ - "localchat.mixins.json", - { - "config": "localchat.client.mixins.json", - "environment": "client" - } + "msnexus.mixins.json" ], "depends": { "fabricloader": ">=${loader_version}", diff --git a/src/main/resources/localchat.mixins.json b/src/main/resources/msnexus.mixins.json similarity index 91% rename from src/main/resources/localchat.mixins.json rename to src/main/resources/msnexus.mixins.json index eb1a5f0..8a660c1 100644 --- a/src/main/resources/localchat.mixins.json +++ b/src/main/resources/msnexus.mixins.json @@ -4,6 +4,7 @@ "package": "org.examp.john.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ + "CommandLogMixin" ], "injectors": { "defaultRequire": 1