diff --git a/src/client/java/org/examp/john/client/LocalchatClient.java b/src/client/java/org/examp/john/client/LocalchatClient.java deleted file mode 100644 index a742344..0000000 --- a/src/client/java/org/examp/john/client/LocalchatClient.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.examp.john.client; - -import net.fabricmc.api.ClientModInitializer; -import org.examp.john.client.rules.RulesClientNetworking; -import org.examp.john.rules.RulesNetworking; - -public class LocalchatClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - RulesClientNetworking.register(); - } -} \ No newline at end of file diff --git a/src/client/java/org/examp/john/client/rules/RulesClientNetworking.java b/src/client/java/org/examp/john/client/rules/RulesClientNetworking.java deleted file mode 100644 index 2819848..0000000 --- a/src/client/java/org/examp/john/client/rules/RulesClientNetworking.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.examp.john.client.rules; - -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; -import net.minecraft.client.Minecraft; -import org.examp.john.rules.RulesData; -import org.examp.john.rules.RulesPayload; - -public class RulesClientNetworking { - - public static void register() { - ClientPlayNetworking.registerGlobalReceiver(RulesPayload.TYPE, (payload, context) -> { - RulesData.RulesRoot data = payload.toData(); - - context.client().execute(() -> - Minecraft.getInstance().setScreen(new RulesScreen(data)) - ); - }); - } -} \ No newline at end of file diff --git a/src/client/java/org/examp/john/client/rules/RulesScreen.java b/src/client/java/org/examp/john/client/rules/RulesScreen.java deleted file mode 100644 index 94c9411..0000000 --- a/src/client/java/org/examp/john/client/rules/RulesScreen.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.examp.john.client.rules; - -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.examp.john.rules.RulesData; - -import java.util.List; - -/** - * GUI екран правил серверу. Зверху ряд кнопок-вкладок (по одній на кожен розділ), - * знизу список пунктів обраного розділу з кольоровим трикутником-значком - * (зелений/жовтий/червоний) залежно від тяжкості порушення. - * - * Дані (розділи, пункти, нумерація, кольори) повністю визначаються rules.json - * на сервері — цей клас нічого не хардкодить, окрім розмірів/відступів GUI. - * - * У Minecraft 26.1.2+ Mojang перейменували GuiGraphics на GuiGraphicsExtractor - * (джерело: docs.fabricmc.net/develop/rendering/gui-graphics). - */ -public class RulesScreen extends Screen { - - // ARGB кольори. З Minecraft 1.21.6+ текстовий колір інтерпретується як ARGB, - // тому альфа-канал (FF) обов'язковий, інакше текст буде прозорим/невидимим. - private static final int COLOR_WHITE = 0xFFFFFFFF; - private static final int COLOR_GRAY = 0xFFAAAAAA; - private static final int COLOR_BG_PANEL = 0xCC101010; - private static final int COLOR_TAB_ACTIVE = 0xFF3A3A3A; - private static final int COLOR_TAB_INACTIVE = 0xFF202020; - - private static final int TAB_HEIGHT = 20; - private static final int TAB_WIDTH = 100; - private static final int ENTRY_HEIGHT = 24; - private static final int PADDING = 8; - - private final RulesData.RulesRoot rulesData; - private int selectedSection = 0; - private int scrollOffset = 0; - - public RulesScreen(RulesData.RulesRoot rulesData) { - super(Component.literal("Правила серверу")); - this.rulesData = rulesData; - } - - @Override - protected void init() { - super.init(); - clearWidgets(); - - if (rulesData == null || rulesData.sections == null || rulesData.sections.isEmpty()) { - return; - } - - int tabX = PADDING; - int tabY = PADDING + 20; - - for (int i = 0; i < rulesData.sections.size(); i++) { - final int index = i; - RulesData.Section section = rulesData.sections.get(i); - - Button tabButton = Button.builder( - Component.literal(section.title), - button -> { - selectedSection = index; - scrollOffset = 0; - } - ).bounds(tabX, tabY, TAB_WIDTH, TAB_HEIGHT).build(); - - addRenderableWidget(tabButton); - - tabX += TAB_WIDTH + 4; - // Переносимо на новий ряд вкладок, якщо не влазить по ширині екрану - if (tabX + TAB_WIDTH > this.width - PADDING) { - tabX = PADDING; - tabY += TAB_HEIGHT + 4; - } - } - - // Кнопка закриття внизу екрану - addRenderableWidget( - Button.builder(Component.literal("Закрити"), button -> onClose()) - .bounds(this.width / 2 - 50, this.height - PADDING - 20, 100, 20) - .build() - ); - } - - @Override - public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { - super.extractRenderState(graphics, mouseX, mouseY, delta); - - graphics.centeredText(this.font, this.title, this.width / 2, PADDING, COLOR_WHITE); - - if (rulesData == null || rulesData.sections == null || rulesData.sections.isEmpty()) { - graphics.centeredText( - this.font, - Component.literal("Правила ще не завантажені."), - this.width / 2, this.height / 2, COLOR_GRAY - ); - return; - } - - renderEntries(graphics); - } - - private void renderEntries(GuiGraphicsExtractor graphics) { - RulesData.Section section = rulesData.sections.get(selectedSection); - List entries = section.entries; - - if (entries == null || entries.isEmpty()) { - return; - } - - int panelTop = PADDING + 50; - int panelBottom = this.height - PADDING - 30; - int panelLeft = PADDING; - int panelRight = this.width - PADDING; - - graphics.fill(panelLeft, panelTop, panelRight, panelBottom, COLOR_BG_PANEL); - - int y = panelTop + 6 - scrollOffset; - - for (RulesData.Entry entry : entries) { - if (y + ENTRY_HEIGHT < panelTop || y > panelBottom) { - y += ENTRY_HEIGHT; - continue; - } - - // Кольоровий трикутник-значок тяжкості порушення - String triangle = "\u26A0"; // ⚠ - graphics.text(this.font, triangle, panelLeft + 6, y, entry.severity.color | 0xFF000000, false); - - // Номер пункту + текст - String line = entry.number + ". " + entry.text; - graphics.text(this.font, line, panelLeft + 24, y, COLOR_WHITE, false); - - y += ENTRY_HEIGHT; - } - } - - @Override - public boolean isPauseScreen() { - return false; - } -} \ No newline at end of file diff --git a/src/main/java/org/examp/john/Localchat.java b/src/main/java/org/examp/john/Localchat.java index be36627..a2fa5e4 100644 --- a/src/main/java/org/examp/john/Localchat.java +++ b/src/main/java/org/examp/john/Localchat.java @@ -2,7 +2,6 @@ package org.examp.john; import net.fabricmc.api.ModInitializer; import org.examp.john.rules.RulesCommand; -import org.examp.john.rules.RulesNetworking; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,9 +12,6 @@ public class Localchat implements ModInitializer { @Override public void onInitialize() { ChatHandler.register(); - - // Реєстрація мережі /rules (спільна частина: обов'язково і на сервері, і на клієнті) - RulesNetworking.registerCommon(); RulesCommand.register(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/examp/john/rules/RulesCommand.java b/src/main/java/org/examp/john/rules/RulesCommand.java index 94c5aca..e5ffa14 100644 --- a/src/main/java/org/examp/john/rules/RulesCommand.java +++ b/src/main/java/org/examp/john/rules/RulesCommand.java @@ -3,18 +3,36 @@ 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.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 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. Гравець виконує команду -> сервер читає rules.json - * -> надсилає RulesPayload через мережу -> клієнт відкриває RulesScreen (кастомний GUI з вкладками). + * Реєструє команду /rules. Повністю server-side: не потребує жодного мода + * на клієнті гравця. Книга створюється програмно через Data Components API + * і відкривається через openItemGui (ClientboundOpenBookPacket). + * Книга НЕ залишається в інвентарі — тимчасово ставиться в руку, + * відкривається, одразу повертається оригінальний предмет. */ public class RulesCommand { + private static final Map cooldowns = new HashMap<>(); + private static final long COOLDOWN_MS = 5000; + public static void register() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(buildCommand()) @@ -33,11 +51,77 @@ public class RulesCommand { 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(); - RulesPayload payload = RulesPayload.fromData(rules); - ServerPlayNetworking.send(player, payload); + 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); + } + + private static ItemStack createRuleBook(RulesData.RulesRoot rules) { + ItemStack book = new ItemStack(Items.WRITTEN_BOOK); + + List> pages = new ArrayList<>(); + for (RulesData.Section section : rules.sections) { + pages.add(Filterable.passThrough(buildPageComponent(section))); + } + + WrittenBookContent content = new WrittenBookContent( + Filterable.passThrough("Правила серверу"), + "Server", + 0, + pages, + true + ); + book.set(DataComponents.WRITTEN_BOOK_CONTENT, content); + + return book; + } + + private static Component buildPageComponent(RulesData.Section section) { + StringBuilder pageText = new StringBuilder(); + pageText.append("§l§n").append(section.title).append("§r\n\n"); + + if (section.entries != null) { + for (RulesData.Entry entry : section.entries) { + pageText.append(entry.severity.colorCode) + .append("[!] §r") + .append(entry.number) + .append(". ") + .append(entry.text) + .append("\n\n"); + } + } + + return Component.literal(pageText.toString()); + } } diff --git a/src/main/java/org/examp/john/rules/RulesData.java b/src/main/java/org/examp/john/rules/RulesData.java index 2250f1a..ec72582 100644 --- a/src/main/java/org/examp/john/rules/RulesData.java +++ b/src/main/java/org/examp/john/rules/RulesData.java @@ -19,20 +19,22 @@ import java.util.List; public class RulesData { /** - * Рівень тяжкості порушення. Визначає колір трикутника-значка в GUI. - * LIGHT -> зелений - * MEDIUM -> жовтий - * HEAVY -> червоний + * Рівень тяжкості порушення. Визначає колір символу-значка в книзі правил + * через ванільні коди форматування (§) — книга рендериться повністю клієнтом, + * без жодного мода на клієнті. + * LIGHT -> зелений (§a) + * MEDIUM -> жовтий (§e) + * HEAVY -> червоний (§c) */ public enum Severity { - LIGHT(0x55FF55), // зелений - MEDIUM(0xFFFF55), // жовтий - HEAVY(0xFF5555); // червоний + LIGHT("§a"), + MEDIUM("§e"), + HEAVY("§c"); - public final int color; + public final String colorCode; - Severity(int color) { - this.color = color; + Severity(String colorCode) { + this.colorCode = colorCode; } public static Severity fromString(String raw) { @@ -77,4 +79,4 @@ public class RulesData { this.sections = sections; } } -} +} \ No newline at end of file diff --git a/src/main/java/org/examp/john/rules/RulesLoader.java b/src/main/java/org/examp/john/rules/RulesLoader.java index e6d47f6..7c0fb06 100644 --- a/src/main/java/org/examp/john/rules/RulesLoader.java +++ b/src/main/java/org/examp/john/rules/RulesLoader.java @@ -75,4 +75,4 @@ public class RulesLoader { Localchat.magicModLog.info("Створено конфіг за замовчуванням: {}", CONFIG_PATH.toAbsolutePath()); } -} +} \ No newline at end of file diff --git a/src/main/java/org/examp/john/rules/RulesNetworking.java b/src/main/java/org/examp/john/rules/RulesNetworking.java deleted file mode 100644 index 271cab4..0000000 --- a/src/main/java/org/examp/john/rules/RulesNetworking.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.examp.john.rules; - -import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; - -/** - * Реєструє тип пакета RulesPayload. Викликається і на клієнті, і на сервері - * (обов'язкова вимога Fabric API: обидві сторони мають знати про CustomPayload). - */ -public class RulesNetworking { - - public static void registerCommon() { - // У Fabric API 26.1+ playS2C() перейменували на clientboundPlay() - // (офіційний гайд порту: docs.fabricmc.net/develop/porting/fabric-api). - PayloadTypeRegistry.clientboundPlay().register(RulesPayload.TYPE, RulesPayload.CODEC); - } -} \ No newline at end of file diff --git a/src/main/java/org/examp/john/rules/RulesPayload.java b/src/main/java/org/examp/john/rules/RulesPayload.java deleted file mode 100644 index d47df5f..0000000 --- a/src/main/java/org/examp/john/rules/RulesPayload.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.examp.john.rules; - -import com.google.gson.Gson; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -import net.minecraft.resources.Identifier; -import org.examp.john.Localchat; - -/** - * Пакет сервер -> клієнт, що передає весь список правил у вигляді JSON-рядка. - * Це найпростіший спосіб не писати окремий StreamCodec для кожного поля - * RulesData: серіалізуємо через Gson в один String і десеріалізуємо назад на клієнті. - */ -public record RulesPayload(String rulesJson) implements CustomPacketPayload { - - public static final CustomPacketPayload.Type TYPE = - new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(Localchat.MOD_ID, "rules_payload")); - - public static final StreamCodec CODEC = - StreamCodec.of( - (buf, payload) -> buf.writeUtf(payload.rulesJson(), Short.MAX_VALUE), - buf -> new RulesPayload(buf.readUtf(Short.MAX_VALUE)) - ); - - @Override - public Type type() { - return TYPE; - } - - /** Створює payload з поточних правил, серіалізуючи їх у JSON. */ - public static RulesPayload fromData(RulesData.RulesRoot data) { - Gson gson = new Gson(); - return new RulesPayload(gson.toJson(data)); - } - - /** Десеріалізує JSON назад у RulesData на клієнтській стороні. */ - public RulesData.RulesRoot toData() { - Gson gson = new Gson(); - return gson.fromJson(rulesJson, RulesData.RulesRoot.class); - } -} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 7694098..e9a0fc4 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,9 +13,6 @@ "fabric-datagen": [ "org.examp.john.client.LocalchatDataGenerator" ], - "client": [ - "org.examp.john.client.LocalchatClient" - ], "main": [ "org.examp.john.Localchat" ]