Author SHA1 Message Date
john b43549fe1c remove: fakeleave/fakejoin commands and leave 2026-08-19 12:17:38 +02:00
john 1ac3ab0a72 feat: spectator tab fix, fakejoin/fakeleave commands with LP permissions
- PlayerTabOverlayMixin: remove italic and gray color for spectators in tab
  (inject getNameForDisplay + redirect text() color in extractRenderState)
- FakeLeaveCommand/FakeJoinCommand: admin-only via op or LuckPerms
  (msnexus.command.fakeleave/fakejoin), hides/shows player from tab
- LeaveMessageMixin: redirect to suppress leave message for fake-left players
  (was @Inject cancel which broke entire disconnect flow)
- Fix ChatHandler: add missing imports (Style, ClickEvent, HoverEvent),
  extract getLuckPermsPrefix method, fix Localchat->MSNexus references
- Fix fabric.mod.json typo (stray 'gi')
2026-08-16 20:46:18 +02:00
john 64bd3141dc feat: highlight player names in chat with LuckPerms colors 2026-08-16 19:16:25 +02:00
john 2edff88cac feat: add stealth mode and anti-abuse chat detectionD 2026-08-14 16:40:24 +02:00
ProstoMaslo 080a838028 REFACTOR: Change modid, modname e.t.c 2026-08-11 20:44:28 +03:00
17 changed files with 111 additions and 476 deletions
+1 -1
View File
@@ -4,5 +4,5 @@ yarn_mappings=1.21.11+build.6
loader_version=0.19.2 loader_version=0.19.2
mod_version=1.0 mod_version=1.0
maven_group=org.example1 maven_group=org.example1
archives_base_name=localhost archives_base_name=msnexus
fabric_version=0.155.2+26.1.2 fabric_version=0.155.2+26.1.2
Binary file not shown.
@@ -0,0 +1,10 @@
package org.examp.john.client;
import net.fabricmc.api.ClientModInitializer;
public class MSNexusClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
}
}
@@ -3,7 +3,7 @@ package org.examp.john.client;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
public class LocalchatDataGenerator implements DataGeneratorEntrypoint { public class MSNexusDataGenerator implements DataGeneratorEntrypoint {
@Override @Override
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
@@ -46,4 +46,4 @@ public class PlayerTabOverlayMixin {
} }
instance.text(font, text, x, y, color); instance.text(font, text, x, y, color);
} }
} }
+8 -3
View File
@@ -11,11 +11,16 @@ import java.util.UUID;
/** /**
* Детектить патерн пошуку гравців через локальний чат. * Детектить патерн пошуку гравців через локальний чат.
* *
* Якщо одне і те ж повідомлення повторюється >=5 разів за 5 хвилин * Алгоритм:
* і гравець перемістився >50 блоків — логує в консоль. * 1. Записує кожне локальне повідомлення (текст + координати + час)
* 2. Якщо одне і те ж повідомлення повторюється >=5 разів за 5 хвилин
* І гравець перемістився більше ніж на 50 блоків —
* це очевидний пошук гравців.
* 3. Логує в термінал для адміна.
*/ */
public class AntiAbuse { public class AntiAbuse {
private static final int REPEAT_THRESHOLD = 5; private static final int REPEAT_THRESHOLD = 5;
private static final double DISTANCE_THRESHOLD = 50.0; private static final double DISTANCE_THRESHOLD = 50.0;
private static final long TIME_WINDOW_MS = 5 * 60 * 1000; private static final long TIME_WINDOW_MS = 5 * 60 * 1000;
@@ -61,7 +66,7 @@ public class AntiAbuse {
double spread = Math.max(b[1] - b[0], b[3] - b[2]); double spread = Math.max(b[1] - b[0], b[3] - b[2]);
if (spread > DISTANCE_THRESHOLD) { if (spread > DISTANCE_THRESHOLD) {
Localchat.magicModLog.warn( MSNexus.magicModLog.warn(
"[ANTI-ABUSE] Гравець {} ймовірно шукає інших: " "[ANTI-ABUSE] Гравець {} ймовірно шукає інших: "
+ "'{}' ×{} разів, переміщення ~{} блоків", + "'{}' ×{} разів, переміщення ~{} блоків",
player.getName().getString(), player.getName().getString(),
+59 -4
View File
@@ -15,6 +15,7 @@ import net.minecraft.server.level.ServerPlayer;
import org.examp.john.stealth.StealthStorage; import org.examp.john.stealth.StealthStorage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
public class ChatHandler { public class ChatHandler {
@@ -33,7 +34,8 @@ public class ChatHandler {
String actualMessage = text.substring(1).trim(); String actualMessage = text.substring(1).trim();
Component globalMessage = Component.empty() Component globalMessage = Component.empty()
.append(formattedName) .append(formattedName)
.append(Component.literal(": " + actualMessage)); .append(Component.literal(": "))
.append(buildMentionMessage(actualMessage, players));
for (ServerPlayer player : players) { for (ServerPlayer player : players) {
player.sendSystemMessage(globalMessage); player.sendSystemMessage(globalMessage);
@@ -65,7 +67,8 @@ public class ChatHandler {
if (!hearers.isEmpty()) { if (!hearers.isEmpty()) {
Component localMessage = Component.empty() Component localMessage = Component.empty()
.append(formattedName) .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); sender.sendSystemMessage(localMessage);
for (ServerPlayer hearer : hearers) { for (ServerPlayer hearer : hearers) {
@@ -87,10 +90,62 @@ public class ChatHandler {
}); });
} }
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) { private static MutableComponent buildFormattedName(ServerPlayer player) {
String luckPermsPrefix = getLuckPermsPrefix(player); String lpPrefix = getLuckPermsPrefix(player);
String playerName = player.getName().getString(); String playerName = player.getName().getString();
String formattedPrefix = luckPermsPrefix.replace("&", "\u00a7"); String formattedPrefix = lpPrefix.replace("&", "\u00a7");
Style clickStyle = Style.EMPTY Style clickStyle = Style.EMPTY
.withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " ")) .withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " "))
+8 -3
View File
@@ -13,7 +13,8 @@ import java.util.stream.Collectors;
public class ChatLogger { public class ChatLogger {
public static void logGlobal(ServerPlayer sender, String message, List<ServerPlayer> recipients) { public static void logGlobal(ServerPlayer sender, String message, List<ServerPlayer> recipients) {
Localchat.magicModLog.info( // Для глобального чату ігноруємо список отримувачів і просто пишемо "everyone"
MSNexus.magicModLog.info(
"[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}", "[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}",
sender.getName().getString(), sender.getName().getString(),
message, message,
@@ -22,13 +23,14 @@ public class ChatLogger {
} }
public static void logLocal(ServerPlayer sender, String message, List<ServerPlayer> recipients) { public static void logLocal(ServerPlayer sender, String message, List<ServerPlayer> recipients) {
// Для локального чату виводимо лише імена отримувачів, без їхніх координат
String heardBy = recipients.isEmpty() String heardBy = recipients.isEmpty()
? "nobody" ? "nobody"
: recipients.stream() : recipients.stream()
.map(p -> p.getName().getString()) .map(p -> p.getName().getString())
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
Localchat.magicModLog.info( MSNexus.magicModLog.info(
"[CHAT][LOCAL] {} said: \"{}\" | heard by: {} | pos: {}", "[CHAT][LOCAL] {} said: \"{}\" | heard by: {} | pos: {}",
sender.getName().getString(), sender.getName().getString(),
message, message,
@@ -38,7 +40,7 @@ public class ChatLogger {
} }
public static void logUnheard(ServerPlayer sender, String message) { public static void logUnheard(ServerPlayer sender, String message) {
Localchat.magicModLog.info( MSNexus.magicModLog.info(
"[CHAT][LOCAL][UNHEARD] {} said: \"{}\" | heard by: nobody | pos: {}", "[CHAT][LOCAL][UNHEARD] {} said: \"{}\" | heard by: nobody | pos: {}",
sender.getName().getString(), sender.getName().getString(),
message, message,
@@ -48,6 +50,9 @@ public class ChatLogger {
private static String formatPos(ServerPlayer player) { private static String formatPos(ServerPlayer player) {
ServerLevel level = player.level(); ServerLevel level = player.level();
// У ResourceKey дістаємо Identifier/ResourceLocation через .identifier(),
// після чого через .getPath() беремо чисту назву ("overworld", "the_nether", "the_end")
String rawDimension = level.dimension().identifier().getPath(); String rawDimension = level.dimension().identifier().getPath();
return String.format( return String.format(
@@ -1,14 +1,13 @@
package org.examp.john; package org.examp.john;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import org.examp.john.rules.RulesCommand;
import org.examp.john.stealth.StealthCommand; import org.examp.john.stealth.StealthCommand;
import org.examp.john.stealth.StealthStorage; import org.examp.john.stealth.StealthStorage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class Localchat implements ModInitializer { public class MSNexus implements ModInitializer {
public static final String MOD_ID = "localhost"; public static final String MOD_ID = "msnexus";
public static final Logger magicModLog = LoggerFactory.getLogger(MOD_ID); public static final Logger magicModLog = LoggerFactory.getLogger(MOD_ID);
@Override @Override
@@ -16,6 +15,5 @@ public class Localchat implements ModInitializer {
StealthStorage.load(); StealthStorage.load();
ChatHandler.register(); ChatHandler.register();
StealthCommand.register(); StealthCommand.register();
RulesCommand.register();
} }
} }
@@ -1,216 +0,0 @@
package org.examp.john.rules;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.Filterable;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Команда /rules. Повністю server-side: клієнт не потребує мода.
* Книга створюється через Data Components API (WrittenBookContent).
* Відкривається через openItemGui — книга НЕ залишається в інвентарі.
*/
public class RulesCommand {
private static final Map<UUID, Long> cooldowns = new HashMap<>();
private static final long COOLDOWN_MS = 5000;
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
dispatcher.register(buildCommand())
);
}
private static LiteralArgumentBuilder<CommandSourceStack> buildCommand() {
return Commands.literal("rules").executes(RulesCommand::execute);
}
private static int execute(CommandContext<CommandSourceStack> context) {
CommandSourceStack source = context.getSource();
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Цю команду можна викликати лише в грі."));
return 0;
}
long now = System.currentTimeMillis();
Long lastUsed = cooldowns.get(player.getUUID());
if (lastUsed != null && (now - lastUsed) < COOLDOWN_MS) {
long remainingMs = COOLDOWN_MS - (now - lastUsed);
long secondsLeft = (remainingMs + 999) / 1000;
player.sendSystemMessage(Component.literal(
"Зачекайте ще " + secondsLeft + " сек. перед повторним використанням /rules."
));
return 0;
}
cooldowns.put(player.getUUID(), now);
RulesData.RulesRoot rules = RulesLoader.get();
if (rules.sections == null || rules.sections.isEmpty()) {
source.sendFailure(Component.literal("Правила ще не налаштовані. Зверніться до адміністрації."));
return 0;
}
openBook(player, rules);
return 1;
}
private static void openBook(ServerPlayer player, RulesData.RulesRoot rules) {
InteractionHand hand = InteractionHand.MAIN_HAND;
ItemStack originalItem = player.getItemInHand(hand).copy();
ItemStack book = createRuleBook(rules);
Localchat.magicModLog.info("Відкриваємо книгу правил для {}", player.getName().getString());
player.setItemInHand(hand, book);
player.containerMenu.sendAllDataToRemote();
player.openItemGui(book, hand);
player.setItemInHand(hand, originalItem);
}
/**
* Створює книгу правил:
* 1. Сторінка 0 — Легенда + Зміст
* 2. Кожен розділ починається на новій сторінці
* 3. Пагінація по ~280 символів/сторінку (реальний ліміт книги)
*/
private static ItemStack createRuleBook(RulesData.RulesRoot rules) {
ItemStack book = new ItemStack(Items.WRITTEN_BOOK);
List<Filterable<Component>> pages = new ArrayList<>();
// Сторінка 0: Легенда + Зміст
pages.add(Filterable.passThrough(buildLegendAndContentsPage(rules)));
// Сторінки з правилами (кожен розділ починається на новій сторінці)
for (RulesData.Section section : rules.sections) {
pages.addAll(buildSectionPages(section));
}
WrittenBookContent content = new WrittenBookContent(
Filterable.passThrough("Правила серверу"),
"Server",
0,
pages,
true
);
book.set(DataComponents.WRITTEN_BOOK_CONTENT, content);
return book;
}
/** Сторінка 0: Легенда + Зміст */
private static Component buildLegendAndContentsPage(RulesData.RulesRoot rules) {
StringBuilder sb = new StringBuilder();
sb.append("§l§nПравила серверу§r\n\n");
sb.append("§l§nЛегенда важливості§r\n");
sb.append("§c⚠§r — Тяжке порушення (бан 5+ днів або перманент)\n");
sb.append("§e⚠§r — Середнє порушення (бан 1–5 днів)\n");
sb.append("§a⚠§r — Легке порушення (попередження/мут)\n\n");
sb.append("§l§nЗміст§r\n\n");
int page = 1;
for (RulesData.Section section : rules.sections) {
sb.append(" §e► §r§f").append(section.title).append(" §7(стор. ").append(page).append(")§r\n");
page += estimatePagesForSection(section);
}
sb.append("\n§7Стрілки в книзі — навігація.§r");
return Component.literal(sb.toString());
}
/** Оцінює скільки сторінок займе розділ (~180 видимих символів/сторінку — фізичний ліміт книги) */
private static int estimatePagesForSection(RulesData.Section section) {
if (section.entries == null || section.entries.isEmpty()) return 1;
final int CHARS_PER_PAGE = 180;
int totalVisible = section.title.length() + 30;
for (RulesData.Entry entry : section.entries) {
totalVisible += visibleLength(entry.number) + visibleLength(entry.text) + 30;
}
return Math.max(1, (totalVisible + CHARS_PER_PAGE - 1) / CHARS_PER_PAGE);
}
/** Повертає довжину рядка БЕЗ §-кодів (тільки видимі символи) */
private static int visibleLength(String s) {
if (s == null) return 0;
int len = 0;
boolean inCode = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '§') {
inCode = true;
continue;
}
if (inCode) {
inCode = false;
continue;
}
len++;
}
return len;
}
/** Будує сторінки для одного розділу з пагінацією */
private static List<Filterable<Component>> buildSectionPages(RulesData.Section section) {
List<Filterable<Component>> pages = new ArrayList<>();
if (section.entries == null || section.entries.isEmpty()) {
pages.add(Filterable.passThrough(Component.literal("§l§n" + section.title + "§r\n\n§7(розділ порожній)§r")));
return pages;
}
final int CHARS_PER_PAGE = 180;
StringBuilder currentPage = new StringBuilder();
int currentVisible = 0;
boolean firstPage = true;
for (RulesData.Entry entry : section.entries) {
String symbol = entry.severity != null ? entry.severity.colorCode + "⚠§r" : "§a⚠§r";
String entryText = symbol + " " + entry.number + ". " + entry.text + "\n\n";
int entryVisible = visibleLength(entryText);
if (firstPage) {
String header = "§l§n" + section.title + "§r\n\n";
currentPage.append(header);
currentVisible = visibleLength(header);
firstPage = false;
}
// Якщо правило не вміщується на поточній сторінці — переносимо ЦІЛЕ правило на нову сторінку
if (currentVisible + entryVisible > CHARS_PER_PAGE && currentVisible > 0) {
pages.add(Filterable.passThrough(Component.literal(currentPage.toString())));
currentPage = new StringBuilder();
currentVisible = 0;
}
currentPage.append(entryText);
currentVisible += entryVisible;
}
if (currentVisible > 0) {
pages.add(Filterable.passThrough(Component.literal(currentPage.toString())));
}
return pages;
}
}
@@ -1,157 +0,0 @@
package org.examp.john.rules;
import java.util.ArrayList;
import java.util.List;
/**
* Моделі даних для системи правил сервера.
* Структура JSON конфігу:
* {
* "sections": [
* {
* "title": "Правила будівництва",
* "entries": [
* { "number": "1.1", "text": "Заборонено будувати без дозволу...", "severity": "MEDIUM" }
* ]
* }
* ]
* }
*/
public class RulesData {
/**
* Рівень тяжкості порушення. Визначає колір символу-значка в книзі правил
* через ванільні коди форматування (§) — книга рендериться повністю клієнтом,
* без жодного мода на клієнті.
* LIGHT -> зелений (§a)
* MEDIUM -> жовтий (§e)
* HEAVY -> червоний (§c)
*/
public enum Severity {
LIGHT("§a"),
MEDIUM("§e"),
HEAVY("§c");
public final String colorCode;
Severity(String colorCode) {
this.colorCode = colorCode;
}
public static Severity fromString(String raw) {
if (raw == null) return LIGHT;
try {
return Severity.valueOf(raw.trim().toUpperCase());
} catch (IllegalArgumentException e) {
return LIGHT;
}
}
}
/** Один пункт правила всередині розділу. */
public static class Entry {
public String number;
public String text;
public Severity severity;
public Entry(String number, String text, Severity severity) {
this.number = number;
this.text = text;
this.severity = severity;
}
}
/** Розділ правил (вкладка в GUI), напр. "Правила будівництва". */
public static class Section {
public String title;
public List<Entry> entries;
public Section(String title, List<Entry> entries) {
this.title = title;
this.entries = entries;
}
}
/** Корінь конфігу — список усіх розділів. */
public static class RulesRoot {
public List<Section> sections;
public RulesRoot(List<Section> sections) {
this.sections = sections;
}
}
/** Повний набір правил за замовчуванням для створення конфігу при першому запуску. */
public static RulesRoot createDefaultRules() {
List<Section> sections = new ArrayList<>();
// 1. ЧЕСНА ГРА
sections.add(new Section("1. ЧЕСНА ГРА", List.of(
new Entry("1.1", "Чити та заборонені модифікації. Заборонено використовувати чити, модифікації, ресурспаки або інші засоби, які дають перевагу.", Severity.HEAVY),
new Entry("1.2", "Використання багів. Заборонено навмисно використовувати помилки та баги сервера для отримання переваги.", Severity.HEAVY),
new Entry("1.3", "Дюпи піску та подібні механіки дозволені. Заборонено дюпи, що ламають економіку або стабільність сервера.", Severity.HEAVY),
new Entry("1.4", "Обман адміністрації. Заборонено навмисно вводити адміністрацію в оману щодо обставин порушення.", Severity.HEAVY),
new Entry("1.5", "Порушення стабільності сервера. Заборонено створювати навантаження, що спричиняють нестабільність.", Severity.HEAVY),
new Entry("1.6", "Реальні гроші. Заборонено купувати/продавати ігрові ресурси за реальні гроші.", Severity.HEAVY),
new Entry("1.7", "Кількість акаунтів. Один гравець — не більше 5 акаунтів.", Severity.HEAVY),
new Entry("1.8", "Продаж або передача акаунта. Заборонено.", Severity.HEAVY),
new Entry("1.9", "Злам акаунтів. Заборонено намагатися отримати доступ до чужих акаунтів.", Severity.HEAVY),
new Entry("1.10", "Підбурювання до порушень. Заборонено закликати інших гравців порушувати правила.", Severity.MEDIUM),
new Entry("1.11", "Шкідлива дезінформація. Заборонено поширювати неправду, що шкодить гравцям/проєкту.", Severity.MEDIUM),
new Entry("1.12", "Персональні дані. Заборонено використовувати/поширювати дані інших без згоди.", Severity.HEAVY),
new Entry("1.13", "Обхід покарання. Заборонено обходити бан/мут через інші акаунти.", Severity.HEAVY),
new Entry("1.14", "Шкідливі дії проти адміністрації. Заборонено навмисно шкодити адмінам/персоналу.", Severity.HEAVY),
new Entry("1.15", "Атаки на сервер та лаунчер. Заборонено втручатися в роботу інфраструктури.", Severity.HEAVY)
)));
// 2. СПІЛКУВАННЯ
sections.add(new Section("2. СПІЛКУВАННЯ", List.of(
new Entry("2.1", "Мова спілкування. Основні: українська, англійська. Російська заборонена в публічному чаті.", Severity.MEDIUM),
new Entry("2.2", "Образи, приниження, провокації. Заборонено ображати гравців, їхніх родичів або адміністрацію. Порнографія заборонена.", Severity.MEDIUM),
new Entry("2.3", "Ненормативна лексика. Заборонено надмірне використання.", Severity.LIGHT),
new Entry("2.4", "Caps Lock. Заборонено надмірне використання, якщо заважає спілкуванню.", Severity.LIGHT),
new Entry("2.5", "Реклама. Заборонено рекламувати сторонні проєкти без дозволу адміністрації (у тому числі в ПМ).", Severity.HEAVY),
new Entry("2.6", "Провокації та конфлікти. Заборонено навмисно розпалювати конфлікти.", Severity.HEAVY),
new Entry("2.7", "Публічні звинувачення адміністрації. Заборонено безпідставно дискредитувати модерацію. Критику — через офіційні канали.", Severity.LIGHT)
)));
// 3. ТЕРИТОРІЇ ТА БУДІВНИЦТВО
sections.add(new Section("3. ТЕРИТОРІЇ ТА БУДІВНИЦТВО", List.of(
new Entry("3.1", "Територія гравця. Гравець розпоряджається своєю територією. Втручання без дозволу — порушення.", Severity.MEDIUM),
new Entry("3.2", "Гріферство. Заборонено руйнувати будівлі, псувати територію, знищувати майно без дозволу.", Severity.HEAVY),
new Entry("3.3", "Неприйнятні споруди. Заборонено: образливий/провокаційний/порнографічний/політичний/нацистський/наркотичний контент.", Severity.MEDIUM),
new Entry("3.4", "Псування світу. Заборонено створювати безлад: хаотичне знищення ландшафту, залишення занедбаних будівель, розливання рідин, безглузді стовпи.", Severity.MEDIUM),
new Entry("3.5", "Контейнери з чужими ресурсами. Заборонено знищувати скрині/шалкери з чужими речами без причини.", Severity.MEDIUM),
new Entry("3.6", "Назви предметів. Не повинні містити заборонений контент.", Severity.LIGHT)
)));
// 4. НІКНЕЙМИ, СКІНИ ТА ПРОФІЛІ
sections.add(new Section("4. НІКНЕЙМИ, СКІНИ ТА ПРОФІЛІ", List.of(
new Entry("4.1", "Заборонені нікнейми: образи, провокації, порнографія, реклама, наркотики, неприйнятний контент.", Severity.HEAVY),
new Entry("4.2", "Заборонені префікси: образливий, провокаційний, порнографічний, рекламний контент.", Severity.MEDIUM),
new Entry("4.3", "Скіни та плащі. Заборонено порнографічний/неприйнятний контент.", Severity.MEDIUM),
new Entry("4.4", "Імітація адміністрації. Заборонено виглядати як адмін/команда проєкту або відомі медіа-особи.", Severity.HEAVY),
new Entry("4.5", "Імітація іншого гравця. Заборонено створювати схожі ніки з метою обману.", Severity.HEAVY),
new Entry("4.6", "Вхід під чужим акаунтом. Заборонено.", Severity.HEAVY)
)));
// 5. PvP ТА ВЗАЄМОДІЯ
sections.add(new Section("5. PvP ТА ВЗАЄМОДІЯ", List.of(
new Entry("5.1", "Перебування на чужій території. Піти після другого прохання власника. Виняток — обов'язки персоналу.", Severity.LIGHT)
)));
// 6. ТЕХНІЧНІ ТА ІНШІ ПОРУШЕННЯ
sections.add(new Section("6. ТЕХНІЧНІ ТА ІНШІ ПОРУШЕННЯ", List.of(
new Entry("6.1", "Втручання в роботу проєкту. Заборонено втручатися в сервер/лаунчер для переваги/обходу захисту.", Severity.HEAVY),
new Entry("6.2", "Обхід систем захисту. Заборонено обходити авторизацію/захист через сторонні клієнти.", Severity.HEAVY),
new Entry("6.3", "Порушення, не передбачені правилами. Адміністрація може тимчасово обмежити доступ для з'ясування.", Severity.MEDIUM)
)));
// 7. ІВЕНТИ
sections.add(new Section("7. ІВЕНТИ", List.of(
new Entry("7.1", "Правила івентів. Необхідно дотримуватися окремих правил івенту. Порушення карається за умовами івенту.", Severity.LIGHT)
)));
return new RulesRoot(sections);
}
}
@@ -1,76 +0,0 @@
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 java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
/**
* Завантажує та кешує rules.json з папки config мода.
* Файл лежить у config/localchat/rules.json — адміни можуть редагувати
* без перезбирання мода, зміни підхоплюються після /rules reload (додамо пізніше)
* або перезапуску сервера.
*/
public class RulesLoader {
private static final Gson GSON = new GsonBuilder().create();
private static final Path CONFIG_PATH = Path.of("config", "localchat", "rules.json");
private static RulesData.RulesRoot cached;
/** Повертає закешовані правила, або завантажує їх з диску при першому виклику. */
public static RulesData.RulesRoot get() {
if (cached == null) {
cached = load();
}
return cached;
}
/** Примусово перечитує файл з диску (для майбутньої команди /rules reload). */
public static RulesData.RulesRoot reload() {
cached = load();
return cached;
}
private static RulesData.RulesRoot load() {
try {
if (!Files.exists(CONFIG_PATH)) {
createDefaultConfig();
}
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 порожній або невалідний, використовую пустий список правил");
return new RulesData.RulesRoot(Collections.emptyList());
}
return root;
}
} catch (IOException e) {
Localchat.magicModLog.error("Не вдалося прочитати rules.json: {}", e.getMessage());
return new RulesData.RulesRoot(Collections.emptyList());
} catch (JsonSyntaxException e) {
Localchat.magicModLog.error("rules.json містить синтаксичну помилку JSON: {}", e.getMessage());
return new RulesData.RulesRoot(Collections.emptyList());
}
}
private static void createDefaultConfig() throws IOException {
Files.createDirectories(CONFIG_PATH.getParent());
RulesData.RulesRoot defaultRoot = RulesData.createDefaultRules();
try (var writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
new GsonBuilder().setPrettyPrinting().create().toJson(defaultRoot, writer);
}
Localchat.magicModLog.info("Створено конфіг правил за замовчуванням: {}", CONFIG_PATH.toAbsolutePath());
}
}
@@ -17,6 +17,10 @@ import net.minecraft.server.level.ServerPlayer;
* /stealth on — увімкнути stealth * /stealth on — увімкнути stealth
* /stealth off — вимкнути stealth * /stealth off — вимкнути stealth
* /stealth info — показати інформацію про команду * /stealth info — показати інформацію про команду
*
* Коли stealth увімкнено, повідомлення локального чату від гравця
* не доходять до інших — гравець бачить "Вас ніхто не почув..."
* Глобальний чат (! префікс) працює як зазвичай.
*/ */
public class StealthCommand { public class StealthCommand {
@@ -37,6 +41,7 @@ public class StealthCommand {
private static int executeToggle(CommandContext<CommandSourceStack> context) { private static int executeToggle(CommandContext<CommandSourceStack> context) {
ServerPlayer player = getPlayer(context); ServerPlayer player = getPlayer(context);
if (player == null) return 0; if (player == null) return 0;
boolean newState = StealthStorage.toggle(player.getUUID()); boolean newState = StealthStorage.toggle(player.getUUID());
sendStatus(player, newState); sendStatus(player, newState);
return 1; return 1;
@@ -45,6 +50,7 @@ public class StealthCommand {
private static int executeOn(CommandContext<CommandSourceStack> context) { private static int executeOn(CommandContext<CommandSourceStack> context) {
ServerPlayer player = getPlayer(context); ServerPlayer player = getPlayer(context);
if (player == null) return 0; if (player == null) return 0;
StealthStorage.set(player.getUUID(), true); StealthStorage.set(player.getUUID(), true);
sendStatus(player, true); sendStatus(player, true);
return 1; return 1;
@@ -53,6 +59,7 @@ public class StealthCommand {
private static int executeOff(CommandContext<CommandSourceStack> context) { private static int executeOff(CommandContext<CommandSourceStack> context) {
ServerPlayer player = getPlayer(context); ServerPlayer player = getPlayer(context);
if (player == null) return 0; if (player == null) return 0;
StealthStorage.set(player.getUUID(), false); StealthStorage.set(player.getUUID(), false);
sendStatus(player, false); sendStatus(player, false);
return 1; return 1;
@@ -3,7 +3,7 @@ package org.examp.john.stealth;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import org.examp.john.Localchat; import org.examp.john.MSNexus;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
@@ -18,6 +18,7 @@ import java.util.UUID;
/** /**
* Зберігає стан stealth-режиму для кожного гравця. * Зберігає стан stealth-режиму для кожного гравця.
* Файл: config/localchat/stealth.json — {"uuid1": true, "uuid2": false, ...} * Файл: config/localchat/stealth.json — {"uuid1": true, "uuid2": false, ...}
* Зміни зберігаються на диск при кожному toggle.
*/ */
public class StealthStorage { public class StealthStorage {
@@ -44,7 +45,7 @@ public class StealthStorage {
} }
} }
} catch (IOException | JsonSyntaxException | IllegalArgumentException e) { } catch (IOException | JsonSyntaxException | IllegalArgumentException e) {
Localchat.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage());
data = new HashMap<>(); data = new HashMap<>();
} }
} }
@@ -77,7 +78,7 @@ public class StealthStorage {
GSON.toJson(raw, writer); GSON.toJson(raw, writer);
} }
} catch (IOException e) { } catch (IOException e) {
Localchat.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage()); MSNexus.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage());
} }
} }
} }
+10 -7
View File
@@ -1,26 +1,29 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"id": "localchat", "id": "msnexus",
"version": "${version}", "version": "${version}",
"name": "localchat", "name": "msnexus",
"description": "", "description": "",
"authors": [], "authors": [],
"contact": {}, "contact": {},
"license": "All-Rights-Reserved", "license": "All-Rights-Reserved",
"icon": "assets/localchat/icon.png", "icon": "assets/msnexus/icon.png",
"environment": "*", "environment": "*",
"entrypoints": { "entrypoints": {
"fabric-datagen": [ "fabric-datagen": [
"org.examp.john.client.LocalchatDataGenerator" "org.examp.john.client.MSNexusDataGenerator"
],
"client": [
"org.examp.john.client.MSNexusClient"
], ],
"main": [ "main": [
"org.examp.john.Localchat" "org.examp.john.MSNexus"
] ]
}, },
"mixins": [ "mixins": [
"localchat.mixins.json", "msnexus.mixins.json",
{ {
"config": "localchat.client.mixins.json", "config": "msnexus.client.mixins.json",
"environment": "client" "environment": "client"
} }
], ],