feat: add full server rules to /rules book
This commit is contained in:
Binary file not shown.
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"package": "org.examp.john.mixin.client",
|
"package": "org.examp.john.mixin.client",
|
||||||
"compatibilityLevel": "JAVA_25",
|
"compatibilityLevel": "JAVA_25",
|
||||||
"client": [
|
"client": [
|
||||||
|
"PlayerTabOverlayMixin"
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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;
|
||||||
@@ -15,5 +16,6 @@ public class Localchat implements ModInitializer {
|
|||||||
StealthStorage.load();
|
StealthStorage.load();
|
||||||
ChatHandler.register();
|
ChatHandler.register();
|
||||||
StealthCommand.register();
|
StealthCommand.register();
|
||||||
|
RulesCommand.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ import java.util.Map;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Реєструє команду /rules. Повністю server-side: не потребує жодного мода
|
* Команда /rules. Повністю server-side: клієнт не потребує мода.
|
||||||
* на клієнті гравця. Книга створюється програмно через Data Components API
|
* Книга створюється через Data Components API (WrittenBookContent).
|
||||||
* і відкривається через openItemGui (ClientboundOpenBookPacket).
|
* Відкривається через openItemGui — книга НЕ залишається в інвентарі.
|
||||||
* Книга НЕ залишається в інвентарі — тимчасово ставиться в руку,
|
|
||||||
* відкривається, одразу повертається оригінальний предмет.
|
|
||||||
*/
|
*/
|
||||||
public class RulesCommand {
|
public class RulesCommand {
|
||||||
|
|
||||||
@@ -87,12 +85,23 @@ public class RulesCommand {
|
|||||||
player.setItemInHand(hand, originalItem);
|
player.setItemInHand(hand, originalItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Створює книгу правил:
|
||||||
|
* 1. Сторінка 0 — Легенда + Зміст
|
||||||
|
* 2. Кожен розділ починається на новій сторінці
|
||||||
|
* 3. Пагінація по ~280 символів/сторінку (реальний ліміт книги)
|
||||||
|
*/
|
||||||
private static ItemStack createRuleBook(RulesData.RulesRoot rules) {
|
private static ItemStack createRuleBook(RulesData.RulesRoot rules) {
|
||||||
ItemStack book = new ItemStack(Items.WRITTEN_BOOK);
|
ItemStack book = new ItemStack(Items.WRITTEN_BOOK);
|
||||||
|
|
||||||
List<Filterable<Component>> pages = new ArrayList<>();
|
List<Filterable<Component>> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
// Сторінка 0: Легенда + Зміст
|
||||||
|
pages.add(Filterable.passThrough(buildLegendAndContentsPage(rules)));
|
||||||
|
|
||||||
|
// Сторінки з правилами (кожен розділ починається на новій сторінці)
|
||||||
for (RulesData.Section section : rules.sections) {
|
for (RulesData.Section section : rules.sections) {
|
||||||
pages.add(Filterable.passThrough(buildPageComponent(section)));
|
pages.addAll(buildSectionPages(section));
|
||||||
}
|
}
|
||||||
|
|
||||||
WrittenBookContent content = new WrittenBookContent(
|
WrittenBookContent content = new WrittenBookContent(
|
||||||
@@ -107,21 +116,101 @@ public class RulesCommand {
|
|||||||
return book;
|
return book;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Component buildPageComponent(RulesData.Section section) {
|
/** Сторінка 0: Легенда + Зміст */
|
||||||
StringBuilder pageText = new StringBuilder();
|
private static Component buildLegendAndContentsPage(RulesData.RulesRoot rules) {
|
||||||
pageText.append("§l§n").append(section.title).append("§r\n\n");
|
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");
|
||||||
|
|
||||||
if (section.entries != null) {
|
int page = 1;
|
||||||
for (RulesData.Entry entry : section.entries) {
|
for (RulesData.Section section : rules.sections) {
|
||||||
pageText.append(entry.severity.colorCode)
|
sb.append(" §e► §r§f").append(section.title).append(" §7(стор. ").append(page).append(")§r\n");
|
||||||
.append("[!] §r")
|
page += estimatePagesForSection(section);
|
||||||
.append(entry.number)
|
|
||||||
.append(". ")
|
|
||||||
.append(entry.text)
|
|
||||||
.append("\n\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Component.literal(pageText.toString());
|
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,5 +1,6 @@
|
|||||||
package org.examp.john.rules;
|
package org.examp.john.rules;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,8 +50,8 @@ public class RulesData {
|
|||||||
|
|
||||||
/** Один пункт правила всередині розділу. */
|
/** Один пункт правила всередині розділу. */
|
||||||
public static class Entry {
|
public static class Entry {
|
||||||
public String number; // напр. "1.1" або "IV"
|
public String number;
|
||||||
public String text; // текст правила / опис порушення
|
public String text;
|
||||||
public Severity severity;
|
public Severity severity;
|
||||||
|
|
||||||
public Entry(String number, String text, Severity severity) {
|
public Entry(String number, String text, Severity severity) {
|
||||||
@@ -79,4 +80,78 @@ public class RulesData {
|
|||||||
this.sections = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -65,14 +65,12 @@ public class RulesLoader {
|
|||||||
private static void createDefaultConfig() throws IOException {
|
private static void createDefaultConfig() throws IOException {
|
||||||
Files.createDirectories(CONFIG_PATH.getParent());
|
Files.createDirectories(CONFIG_PATH.getParent());
|
||||||
|
|
||||||
RulesData.Entry defaultEntry = new RulesData.Entry("1.1", "Пункт 1", RulesData.Severity.LIGHT);
|
RulesData.RulesRoot defaultRoot = RulesData.createDefaultRules();
|
||||||
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)) {
|
try (var writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
|
||||||
new GsonBuilder().setPrettyPrinting().create().toJson(defaultRoot, writer);
|
new GsonBuilder().setPrettyPrinting().create().toJson(defaultRoot, writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Localchat.magicModLog.info("Створено конфіг за замовчуванням: {}", CONFIG_PATH.toAbsolutePath());
|
Localchat.magicModLog.info("Створено конфіг правил за замовчуванням: {}", CONFIG_PATH.toAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user