feat: implement /rules command with custom networking payload
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
package org.examp.john.client;
|
package org.examp.john.client;
|
||||||
|
|
||||||
import net.fabricmc.api.ClientModInitializer;
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
|
import org.examp.john.client.rules.RulesClientNetworking;
|
||||||
|
import org.examp.john.rules.RulesNetworking;
|
||||||
|
|
||||||
public class LocalchatClient implements ClientModInitializer {
|
public class LocalchatClient implements ClientModInitializer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeClient() {
|
public void onInitializeClient() {
|
||||||
|
RulesClientNetworking.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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<RulesData.Entry> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ 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) {
|
||||||
// Для глобального чату ігноруємо список отримувачів і просто пишемо "everyone"
|
|
||||||
Localchat.magicModLog.info(
|
Localchat.magicModLog.info(
|
||||||
"[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}",
|
"[CHAT][GLOBAL] {} said: \"{}\" | heard by: everyone | pos: {}",
|
||||||
sender.getName().getString(),
|
sender.getName().getString(),
|
||||||
@@ -23,7 +22,6 @@ 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()
|
||||||
@@ -50,9 +48,6 @@ 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,6 +1,8 @@
|
|||||||
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.rules.RulesNetworking;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -10,7 +12,10 @@ public class Localchat implements ModInitializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
// Тепер ChatHandler викликається з того ж пакета без помилок імпорту
|
|
||||||
ChatHandler.register();
|
ChatHandler.register();
|
||||||
|
|
||||||
|
// Реєстрація мережі /rules (спільна частина: обов'язково і на сервері, і на клієнті)
|
||||||
|
RulesNetworking.registerCommon();
|
||||||
|
RulesCommand.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Реєструє команду /rules. Гравець виконує команду -> сервер читає rules.json
|
||||||
|
* -> надсилає RulesPayload через мережу -> клієнт відкриває RulesScreen (кастомний GUI з вкладками).
|
||||||
|
*/
|
||||||
|
public class RulesCommand {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
RulesData.RulesRoot rules = RulesLoader.get();
|
||||||
|
RulesPayload payload = RulesPayload.fromData(rules);
|
||||||
|
|
||||||
|
ServerPlayNetworking.send(player, payload);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package org.examp.john.rules;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Моделі даних для системи правил сервера.
|
||||||
|
* Структура JSON конфігу:
|
||||||
|
* {
|
||||||
|
* "sections": [
|
||||||
|
* {
|
||||||
|
* "title": "Правила будівництва",
|
||||||
|
* "entries": [
|
||||||
|
* { "number": "1.1", "text": "Заборонено будувати без дозволу...", "severity": "MEDIUM" }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class RulesData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рівень тяжкості порушення. Визначає колір трикутника-значка в GUI.
|
||||||
|
* LIGHT -> зелений
|
||||||
|
* MEDIUM -> жовтий
|
||||||
|
* HEAVY -> червоний
|
||||||
|
*/
|
||||||
|
public enum Severity {
|
||||||
|
LIGHT(0x55FF55), // зелений
|
||||||
|
MEDIUM(0xFFFF55), // жовтий
|
||||||
|
HEAVY(0xFF5555); // червоний
|
||||||
|
|
||||||
|
public final int color;
|
||||||
|
|
||||||
|
Severity(int color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; // напр. "1.1" або "IV"
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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.Entry defaultEntry = new RulesData.Entry("1.1", "Пункт 1", RulesData.Severity.LIGHT);
|
||||||
|
RulesData.Section defaultSection = new RulesData.Section("Правила", Collections.singletonList(defaultEntry));
|
||||||
|
RulesData.RulesRoot defaultRoot = new RulesData.RulesRoot(Collections.singletonList(defaultSection));
|
||||||
|
|
||||||
|
try (var writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||||
|
new GsonBuilder().setPrettyPrinting().create().toJson(defaultRoot, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Localchat.magicModLog.info("Створено конфіг за замовчуванням: {}", CONFIG_PATH.toAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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<RulesPayload> TYPE =
|
||||||
|
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(Localchat.MOD_ID, "rules_payload"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, RulesPayload> CODEC =
|
||||||
|
StreamCodec.of(
|
||||||
|
(buf, payload) -> buf.writeUtf(payload.rulesJson(), Short.MAX_VALUE),
|
||||||
|
buf -> new RulesPayload(buf.readUtf(Short.MAX_VALUE))
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user