Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b43549fe1c | ||
|
|
1ac3ab0a72 | ||
|
|
64bd3141dc | ||
|
|
2edff88cac | ||
|
|
080a838028 |
+1
-1
@@ -4,5 +4,5 @@ yarn_mappings=1.21.11+build.6
|
||||
loader_version=0.19.2
|
||||
mod_version=1.0
|
||||
maven_group=org.example1
|
||||
archives_base_name=localhost
|
||||
archives_base_name=msnexus
|
||||
fabric_version=0.155.2+26.1.2
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package org.examp.john.client;
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
|
||||
public class LocalchatClient implements ClientModInitializer {
|
||||
public class MSNexusClient implements ClientModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
+1
-1
@@ -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) {
|
||||
@@ -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<Component> 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);
|
||||
}
|
||||
}
|
||||
+1
@@ -4,6 +4,7 @@
|
||||
"package": "org.examp.john.mixin.client",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"client": [
|
||||
"PlayerTabOverlayMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.examp.john;
|
||||
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Детектить патерн пошуку гравців через локальний чат.
|
||||
*
|
||||
* Алгоритм:
|
||||
* 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 Map<UUID, List<MessageRecord>> history = new HashMap<>();
|
||||
|
||||
public static void recordMessage(ServerPlayer player, String message) {
|
||||
UUID uuid = player.getUUID();
|
||||
List<MessageRecord> records = history.computeIfAbsent(uuid, k -> new ArrayList<>());
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
records.add(new MessageRecord(message, now, player.getX(), player.getZ()));
|
||||
|
||||
long cutoff = now - TIME_WINDOW_MS;
|
||||
records.removeIf(r -> r.timestamp < cutoff);
|
||||
|
||||
if (records.size() >= REPEAT_THRESHOLD) {
|
||||
checkForAbuse(player, records);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkForAbuse(ServerPlayer player, List<MessageRecord> records) {
|
||||
Map<String, int[]> counts = new HashMap<>();
|
||||
Map<String, double[]> bounds = new HashMap<>();
|
||||
|
||||
for (MessageRecord r : records) {
|
||||
counts.merge(r.message, new int[]{1}, (a, b) -> {
|
||||
a[0]++;
|
||||
return a;
|
||||
});
|
||||
|
||||
double[] b = bounds.computeIfAbsent(r.message, k -> new double[]{r.x, r.x, r.z, r.z});
|
||||
if (r.x < b[0]) b[0] = r.x;
|
||||
if (r.x > b[1]) b[1] = r.x;
|
||||
if (r.z < b[2]) b[2] = r.z;
|
||||
if (r.z > b[3]) b[3] = r.z;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, int[]> entry : counts.entrySet()) {
|
||||
if (entry.getValue()[0] < REPEAT_THRESHOLD) continue;
|
||||
|
||||
double[] b = bounds.get(entry.getKey());
|
||||
double spread = Math.max(b[1] - b[0], b[3] - b[2]);
|
||||
|
||||
if (spread > DISTANCE_THRESHOLD) {
|
||||
MSNexus.magicModLog.warn(
|
||||
"[ANTI-ABUSE] Гравець {} ймовірно шукає інших: "
|
||||
+ "'{}' ×{} разів, переміщення ~{} блоків",
|
||||
player.getName().getString(),
|
||||
entry.getKey(),
|
||||
entry.getValue()[0],
|
||||
(int) spread
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record MessageRecord(String message, long timestamp, double x, double z) {}
|
||||
}
|
||||
@@ -5,13 +5,17 @@ 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.server.MinecraftServer;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.examp.john.stealth.StealthStorage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatHandler {
|
||||
@@ -22,78 +26,148 @@ public class ChatHandler {
|
||||
ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> {
|
||||
String text = message.signedContent();
|
||||
ServerLevel serverLevel = sender.level();
|
||||
MinecraftServer server = serverLevel.getServer();
|
||||
List<ServerPlayer> players = server.getPlayerList().getPlayers();
|
||||
List<ServerPlayer> players = serverLevel.getServer().getPlayerList().getPlayers();
|
||||
|
||||
String playerPrefix = "";
|
||||
try {
|
||||
LuckPerms luckPerms = LuckPermsProvider.get();
|
||||
User user = luckPerms.getUserManager().getUser(sender.getUUID());
|
||||
if (user != null) {
|
||||
String metaPrefix = user.getCachedData().getMetaData().getPrefix();
|
||||
if (metaPrefix != null) {
|
||||
playerPrefix = metaPrefix;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
MutableComponent formattedPrefix = Component.literal(playerPrefix.replace("&", "§"));
|
||||
MutableComponent formattedName = Component.empty()
|
||||
.append(formattedPrefix)
|
||||
.append(sender.getDisplayName());
|
||||
MutableComponent formattedName = buildFormattedName(sender);
|
||||
|
||||
if (text.startsWith(prefix)) {
|
||||
// ГЛОБАЛЬНИЙ ЧАТ
|
||||
String actualMessage = text.substring(1).trim();
|
||||
Component globalMessage = Component.empty()
|
||||
.append(formattedName)
|
||||
.append(Component.literal(": " + actualMessage));
|
||||
.append(Component.literal(": "))
|
||||
.append(buildMentionMessage(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<ServerPlayer> hearers = new ArrayList<>();
|
||||
for (ServerPlayer player : players) {
|
||||
if (player.level() == serverLevel && player.distanceTo(sender) <= LOCAL_CHAT_RADIUS) {
|
||||
if (player != sender) {
|
||||
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));
|
||||
.append(Component.literal(": ").withStyle(ChatFormatting.GRAY))
|
||||
.append(buildMentionMessage(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("Вас ніхто не почув, додайте на початок повідомлення \"!\" аби написати в глобальний чат")
|
||||
Component.literal("Вас ніхто не почув, додайте \"!\" на початок для глобального чату")
|
||||
.withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
// Логування: ніхто не почув
|
||||
ChatLogger.logUnheard(sender, text);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private static MutableComponent buildMentionMessage(String text, List<ServerPlayer> onlinePlayers) {
|
||||
List<ServerPlayer> 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;
|
||||
|
||||
for (ServerPlayer player : sorted) {
|
||||
String name = player.getName().getString();
|
||||
int index = remaining.toLowerCase().indexOf(name.toLowerCase());
|
||||
if (index >= 0 && index < earliestIndex) {
|
||||
earliestIndex = index;
|
||||
mentioned = player;
|
||||
}
|
||||
}
|
||||
|
||||
if (mentioned == null) {
|
||||
result.append(Component.literal(remaining));
|
||||
break;
|
||||
}
|
||||
|
||||
if (earliestIndex > 0) {
|
||||
result.append(Component.literal(remaining.substring(0, earliestIndex)));
|
||||
}
|
||||
|
||||
result.append(createMentionComponent(mentioned));
|
||||
|
||||
remaining = remaining.substring(earliestIndex + mentioned.getName().getString().length());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Component createMentionComponent(ServerPlayer player) {
|
||||
String lpPrefix = getLuckPermsPrefix(player);
|
||||
String formattedPrefix = lpPrefix.replace("&", "\u00a7");
|
||||
String name = player.getName().getString();
|
||||
|
||||
return Component.literal(formattedPrefix + name)
|
||||
.withStyle(Style.EMPTY
|
||||
.withClickEvent(new ClickEvent.SuggestCommand("/msg " + name + " "))
|
||||
.withHoverEvent(new HoverEvent.ShowText(
|
||||
Component.literal("Натисніть щоб написати " + name)
|
||||
.withStyle(ChatFormatting.GRAY)))
|
||||
);
|
||||
}
|
||||
|
||||
private static MutableComponent buildFormattedName(ServerPlayer player) {
|
||||
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) {
|
||||
return metaPrefix;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public class ChatLogger {
|
||||
|
||||
public static void logGlobal(ServerPlayer sender, String message, List<ServerPlayer> recipients) {
|
||||
// Для глобального чату ігноруємо список отримувачів і просто пишемо "everyone"
|
||||
Localchat.magicModLog.info(
|
||||
MSNexus.magicModLog.info(
|
||||
"[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}",
|
||||
sender.getName().getString(),
|
||||
message,
|
||||
@@ -30,7 +30,7 @@ public class ChatLogger {
|
||||
.map(p -> p.getName().getString())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
Localchat.magicModLog.info(
|
||||
MSNexus.magicModLog.info(
|
||||
"[CHAT][LOCAL] {} said: \"{}\" | heard by: {} | pos: {}",
|
||||
sender.getName().getString(),
|
||||
message,
|
||||
@@ -40,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,
|
||||
|
||||
+6
-3
@@ -1,16 +1,19 @@
|
||||
package org.examp.john;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import org.examp.john.stealth.StealthCommand;
|
||||
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() {
|
||||
// Тепер ChatHandler викликається з того ж пакета без помилок імпорту
|
||||
StealthStorage.load();
|
||||
ChatHandler.register();
|
||||
StealthCommand.register();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.examp.john.stealth;
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/**
|
||||
* Команда /stealth — керує режимом прихованості гравця.
|
||||
*
|
||||
* Підкоманди:
|
||||
* /stealth — переключити стан (on/off)
|
||||
* /stealth on — увімкнути stealth
|
||||
* /stealth off — вимкнути stealth
|
||||
* /stealth info — показати інформацію про команду
|
||||
*
|
||||
* Коли stealth увімкнено, повідомлення локального чату від гравця
|
||||
* не доходять до інших — гравець бачить "Вас ніхто не почув..."
|
||||
* Глобальний чат (! префікс) працює як зазвичай.
|
||||
*/
|
||||
public class StealthCommand {
|
||||
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
|
||||
dispatcher.register(buildCommand())
|
||||
);
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> buildCommand() {
|
||||
return Commands.literal("stealth")
|
||||
.executes(StealthCommand::executeToggle)
|
||||
.then(Commands.literal("on").executes(StealthCommand::executeOn))
|
||||
.then(Commands.literal("off").executes(StealthCommand::executeOff))
|
||||
.then(Commands.literal("info").executes(StealthCommand::executeInfo));
|
||||
}
|
||||
|
||||
private static int executeToggle(CommandContext<CommandSourceStack> context) {
|
||||
ServerPlayer player = getPlayer(context);
|
||||
if (player == null) return 0;
|
||||
|
||||
boolean newState = StealthStorage.toggle(player.getUUID());
|
||||
sendStatus(player, newState);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeOn(CommandContext<CommandSourceStack> context) {
|
||||
ServerPlayer player = getPlayer(context);
|
||||
if (player == null) return 0;
|
||||
|
||||
StealthStorage.set(player.getUUID(), true);
|
||||
sendStatus(player, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeOff(CommandContext<CommandSourceStack> context) {
|
||||
ServerPlayer player = getPlayer(context);
|
||||
if (player == null) return 0;
|
||||
|
||||
StealthStorage.set(player.getUUID(), false);
|
||||
sendStatus(player, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int executeInfo(CommandContext<CommandSourceStack> context) {
|
||||
ServerPlayer player = getPlayer(context);
|
||||
if (player == null) return 0;
|
||||
|
||||
boolean enabled = StealthStorage.isEnabled(player.getUUID());
|
||||
|
||||
player.sendSystemMessage(Component.literal("=== Stealth Mode ===").withStyle(ChatFormatting.GOLD));
|
||||
player.sendSystemMessage(Component.empty());
|
||||
player.sendSystemMessage(Component.literal("Статус: ").append(
|
||||
enabled
|
||||
? Component.literal("УВІМКНЕНО").withStyle(ChatFormatting.GREEN)
|
||||
: Component.literal("ВИМКНЕНО").withStyle(ChatFormatting.RED)
|
||||
));
|
||||
player.sendSystemMessage(Component.empty());
|
||||
|
||||
player.sendSystemMessage(Component.literal("Навіщо це потрібно:").withStyle(ChatFormatting.YELLOW));
|
||||
player.sendSystemMessage(Component.literal(" Локальний чат можна зловживати для пошуку")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" гравців: хтось пише в чат, бігає по карті,")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" і дивиться хто відповів — так можна")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" визначити місцеперебування іншого гравця.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" Stealth прибирає вас з локального чату:")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" ваші повідомлення не бачить ніхто,")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" але ви можете бачити чужі якщо вони поруч.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.empty());
|
||||
|
||||
player.sendSystemMessage(Component.literal("Підкоманди:").withStyle(ChatFormatting.YELLOW));
|
||||
player.sendSystemMessage(Component.literal(" /stealth").withStyle(ChatFormatting.AQUA)
|
||||
.append(Component.literal(" — переключити стан (on/off)").withStyle(ChatFormatting.GRAY)));
|
||||
player.sendSystemMessage(Component.literal(" /stealth on").withStyle(ChatFormatting.AQUA)
|
||||
.append(Component.literal(" — увімкнути stealth").withStyle(ChatFormatting.GRAY)));
|
||||
player.sendSystemMessage(Component.literal(" /stealth off").withStyle(ChatFormatting.AQUA)
|
||||
.append(Component.literal(" — вимкнути stealth").withStyle(ChatFormatting.GRAY)));
|
||||
player.sendSystemMessage(Component.literal(" /stealth info").withStyle(ChatFormatting.AQUA)
|
||||
.append(Component.literal(" — ця інформація").withStyle(ChatFormatting.GRAY)));
|
||||
player.sendSystemMessage(Component.empty());
|
||||
|
||||
player.sendSystemMessage(Component.literal("Як працює:").withStyle(ChatFormatting.YELLOW));
|
||||
player.sendSystemMessage(Component.literal(" Звичайний чат: повідомлення бачать лише")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" ті хто в радіусі 100 блоків.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" Якщо stealth ON: ваші повідомлення")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" не доходять взагалі — ви бачите")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" 'Вас ніхто не почув...' замість цього.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" Глобальний чат (! префікс) працює")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
player.sendSystemMessage(Component.literal(" завжди, навіть з stealth.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void sendStatus(ServerPlayer player, boolean enabled) {
|
||||
if (enabled) {
|
||||
player.sendSystemMessage(Component.literal("Stealth-режим УВІМКНЕНО.")
|
||||
.withStyle(ChatFormatting.GREEN));
|
||||
player.sendSystemMessage(Component.literal("Ваші повідомлення в локальному чаті тепер не доходять.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
} else {
|
||||
player.sendSystemMessage(Component.literal("Stealth-режим ВИМКНЕНО.")
|
||||
.withStyle(ChatFormatting.RED));
|
||||
player.sendSystemMessage(Component.literal("Ваші повідомлення в локальному чаті знову чутні.")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerPlayer getPlayer(CommandContext<CommandSourceStack> context) {
|
||||
if (!(context.getSource().getEntity() instanceof ServerPlayer player)) {
|
||||
context.getSource().sendFailure(
|
||||
Component.literal("Цю команду можна викликати лише в грі."));
|
||||
return null;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.examp.john.stealth;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.examp.john.MSNexus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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 Map<UUID, Boolean> data = new HashMap<>();
|
||||
|
||||
public static void load() {
|
||||
try {
|
||||
if (!Files.exists(CONFIG_PATH)) {
|
||||
data = new HashMap<>();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Boolean> raw = GSON.fromJson(reader, Map.class);
|
||||
data = new HashMap<>();
|
||||
if (raw != null) {
|
||||
for (Map.Entry<String, Boolean> entry : raw.entrySet()) {
|
||||
data.put(UUID.fromString(entry.getKey()), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | JsonSyntaxException | IllegalArgumentException e) {
|
||||
MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage());
|
||||
data = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEnabled(UUID uuid) {
|
||||
return data.getOrDefault(uuid, false);
|
||||
}
|
||||
|
||||
public static boolean toggle(UUID uuid) {
|
||||
boolean current = isEnabled(uuid);
|
||||
boolean newValue = !current;
|
||||
data.put(uuid, newValue);
|
||||
save();
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public static void set(UUID uuid, boolean value) {
|
||||
data.put(uuid, value);
|
||||
save();
|
||||
}
|
||||
|
||||
private static void save() {
|
||||
try {
|
||||
Files.createDirectories(CONFIG_PATH.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||
Map<String, Boolean> raw = new HashMap<>();
|
||||
for (Map.Entry<UUID, Boolean> entry : data.entrySet()) {
|
||||
raw.put(entry.getKey().toString(), entry.getValue());
|
||||
}
|
||||
GSON.toJson(raw, writer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
MSNexus.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "localchat",
|
||||
"id": "msnexus",
|
||||
"version": "${version}",
|
||||
"name": "localchat",
|
||||
"name": "msnexus",
|
||||
"description": "",
|
||||
"authors": [],
|
||||
"contact": {},
|
||||
"license": "All-Rights-Reserved",
|
||||
"icon": "assets/localchat/icon.png",
|
||||
"icon": "assets/msnexus/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"fabric-datagen": [
|
||||
"org.examp.john.client.LocalchatDataGenerator"
|
||||
"org.examp.john.client.MSNexusDataGenerator"
|
||||
],
|
||||
"client": [
|
||||
"org.examp.john.client.LocalchatClient"
|
||||
"org.examp.john.client.MSNexusClient"
|
||||
],
|
||||
"main": [
|
||||
"org.examp.john.Localchat"
|
||||
"org.examp.john.MSNexus"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"localchat.mixins.json",
|
||||
"msnexus.mixins.json",
|
||||
{
|
||||
"config": "localchat.client.mixins.json",
|
||||
"config": "msnexus.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user