feat: LoHub Action system + Join system implementation

This commit is contained in:
loki5512344 2026-02-23 17:27:34 +01:00
parent 69cae99476
commit 6a4c8a7886
55 changed files with 3180 additions and 1022 deletions

View file

@ -1,27 +0,0 @@
name: Build & Test
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone --depth 1 https://oauth2:${{ secrets.RELEASE_TOKEN }}@git.lokili.xyz/${{ gitea.repository }}.git .
- name: Setup Java 21
run: |
curl -sL https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz | tar xz
- name: Build & Test
run: |
export JAVA_HOME=$PWD/jdk-21.0.2
export PATH=$JAVA_HOME/bin:$PATH
chmod +x gradlew
./gradlew build test --no-daemon

View file

@ -1,49 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone --depth 1 --branch ${{ gitea.ref_name }} https://oauth2:${{ secrets.RELEASE_TOKEN }}@git.lokili.xyz/${{ gitea.repository }}.git .
- name: Setup Java 21
run: |
curl -sL https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz | tar xz
- name: Build
run: |
export JAVA_HOME=$PWD/jdk-21.0.2
export PATH=$JAVA_HOME/bin:$PATH
chmod +x gradlew
./gradlew build --no-daemon
- name: Create Release
run: |
VERSION=${{ gitea.ref_name }}
VERSION=${VERSION#v}
JAR_FILE=$(ls build/libs/LoChat-*.jar | head -1)
curl -X POST \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
"https://git.lokili.xyz/api/v1/repos/${{ gitea.repository }}/releases" \
-d "{\"tag_name\": \"${{ gitea.ref_name }}\", \"name\": \"LoChat $VERSION\", \"body\": \"Release $VERSION\"}"
sleep 2
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
"https://git.lokili.xyz/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
curl -X POST \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
"https://git.lokili.xyz/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets?name=LoChat-$VERSION.jar" \
--data-binary @"$JAR_FILE"

432
README.md
View file

@ -1,304 +1,188 @@
# LoChat # 🎮 Loki Network Plugins
**Продвинутый чат-плагин для Minecraft Paper 1.20.1 с интегрированным модулем градиентных ников** Набор плагинов для Minecraft серверов: чат, хаб и прокси.
**Автор:** loki ## 📦 Модули
**Версия:** 1.5.5-1.20.1
--- ### 1. LoChat - Чат плагин (Paper/Folia)
Продвинутый чат с градиентными никами, модерацией и фильтрами.
## 🚀 Установка **Основные функции:**
- Глобальный/локальный чат
- PM система с игнором
- Мут система с историей
- Фильтры (мат, спам, CAPS, URL, IP)
- Градиентные ники (LuckPerms)
- Markdown форматирование
- Упоминания (@player, @everyone, @here, @role)
- 300+ эмодзи
1. Скачайте `LoChat-1.5.5-1.20.1.jar` из папки `build/libs/` ### 2. LoHub - Hub плагин (Paper/Folia)
2. Поместите файл в папку `plugins/` вашего сервера Плагин для лобби сервера с защитой мира и интерактивными элементами.
3. Перезапустите сервер
4. Настройте конфигурацию в файлах `config.yml` и других
--- **Основные функции:**
- Spawn система с телепортом
- WorldGuard регионы (авто-создание)
- Защита мира (блоки, PvP, мобы)
- Launchpad и Double Jump
- Player Hider
- Custom Join Items
- Action система для гибкой настройки
## ✨ Основной функционал ### 3. LoVelocity - Velocity плагин
Простой плагин для прокси с командой /hub.
### 💬 Система чата **Основные функции:**
- **Глобальный чат** - `!сообщение` или `/g <сообщение>` - Команда /hub для телепорта на lobby
- **Локальный чат** - радиус 100 блоков (настраивается) - Кастомные сообщения
- **Личные сообщения** - `/msg`, `/reply` с поддержкой игнорирования - PlaceholderAPI поддержка
- **Автосообщения** - многострочные сообщения с настраиваемым порядком
- **Смайлики** - 200+ эмодзи для украшения сообщений
### 🛡️ Модерация ## 🏗️ Структура проекта
- **Система мутов** - `/lmute`, `/lunmute` с поддержкой времени и причин
- **Фильтр мата** - умная система с обходом символов (censor/block/warn)
- **Анти-спам** - защита от CAPS, повторов и похожих сообщений
- **Кулдауны** - настраиваемые задержки между сообщениями
- **Шпион режим** - `/chatspy` для мониторинга всех сообщений
- **Очистка чата** - `/clearchat` с настраиваемым сообщением
### 🌈 Градиентные ники (встроенный LoPreff)
- **Кастомные градиенты** - `/color <hex1> [hex2] ...` до 7 цветов
- **Кастомные префиксы** - `/prefix <текст>` с поддержкой цветов
- **Предустановки** - fire, ocean, forest, sunset, rainbow и другие
- **Интеграция с LuckPerms** - автоматическое обновление префиксов
- **Покупка за PlayerPoints** - опциональная система оплаты
- **GUI подтверждения** - красивый интерфейс для покупок
### 🎭 Дополнительные возможности
- **Головы игроков** - `:head_nickname:` и `@head nickname` в чате
- **Кастомные команды** - создание собственных команд через конфиг
- **Упоминания** - @nickname с звуковыми уведомлениями
- **Интеграция с SkinsRestorer** - поддержка кастомных скинов
---
## 📋 Команды
### Чат
| Команда | Описание | Право |
|---------|----------|-------|
| `!<сообщение>` | Глобальный чат | `chat.global.use` |
| `/g <сообщение>` | Глобальный чат | `chat.global.use` |
| `/globalchat toggle` | Переключить глобальный чат | `chat.global.toggle` |
| `/l <сообщение>` | Локальный чат | `chat.local.use` |
| `/msg <ник> <сообщение>` | Личное сообщение | `chat.pm.use` |
| `/reply <сообщение>` | Ответить на последнее PM | `chat.pm.use` |
| `/ignore <ник>` | Добавить в игнор | `chat.pm.ignore` |
| `/unignore <ник>` | Убрать из игнора | `chat.pm.ignore` |
| `/announce <сообщение>` | Объявление | `chat.announce.use` |
### Модерация
| Команда | Описание | Право |
|---------|----------|-------|
| `/chatspy` | Включить/выключить шпион режим | `chat.spy` |
| `/clearchat` | Очистить чат | `chat.clear` |
| `/clearchatconfig <действие>` | Настроить сообщение очистки | `lochat.clearchat.config` |
| `/lmute <ник> [время] [-s] [причина]` | Замутить игрока | `lochat.mute` |
| `/lunmute <ник> [-s]` | Размутить игрока | `lochat.unmute` |
| `/lmutelist` | Список замученных | `lochat.mutelist` |
| `/lmutehistory <ник>` | История мутов игрока | `lochat.mutehistory` |
| `/lmuteblame <модератор>` | Муты выданные модератором | `lochat.muteblame` |
### Градиентные ники
| Команда | Описание | Право |
|---------|----------|-------|
| `/color <hex1> [hex2] ...` | Установить градиент | `gradient.color` |
| `/color on/off/reset` | Управление градиентом | `gradient.color` |
| `/color copy <ник>` | Скопировать градиент | `gradient.color` |
| `/color preset <название>` | Использовать предустановку | `gradient.color` |
| `/prefix <текст>` | Установить префикс | `gradient.prefix` |
| `/prefix on/off/reset` | Управление префиксом | `gradient.prefix` |
### Админские команды
| Команда | Описание | Право |
|---------|----------|-------|
| `/lochat reload` | Перезагрузить конфиг | `lochat.admin` |
| `/aprefix <подкоманда>` | Админские команды градиентов | `gradient.admin` |
---
## 🔐 Права доступа
### Чат
| Право | По умолчанию | Описание |
|-------|--------------|----------|
| `chat.global.use` | `true` | Использование глобального чата |
| `chat.global.toggle` | `true` | Переключение глобального чата |
| `chat.local.use` | `true` | Использование локального чата |
| `chat.pm.use` | `true` | Личные сообщения |
| `chat.pm.ignore` | `true` | Игнорирование игроков |
| `chat.mention.use` | `true` | Упоминания @nickname |
### Модерация
| Право | По умолчанию | Описание |
|-------|--------------|----------|
| `chat.announce.use` | `op` | Объявления |
| `chat.spy` | `op` | Шпион режим |
| `chat.clear` | `op` | Очистка чата |
| `chat.bypass.cooldown` | `op` | Обход кулдаунов |
| `chat.bypass.filter` | `op` | Обход фильтра мата |
| `chat.bypass.antispam` | `op` | Обход анти-спама |
| `lochat.mute` | `op` | Выдача мутов |
| `lochat.unmute` | `op` | Снятие мутов |
| `lochat.clearchat.config` | `op` | Настройка очистки чата |
### Градиенты
| Право | По умолчанию | Описание |
|-------|--------------|----------|
| `gradient.color` | `true` | Градиентные ники |
| `gradient.prefix` | `true` | Кастомные префиксы |
| `gradient.admin` | `op` | Админские команды |
| `gradient.bypass.cooldown` | `op` | Обход кулдаунов |
| `gradient.bypass.cost` | `op` | Бесплатные градиенты |
### Дополнительные
| Право | По умолчанию | Описание |
|-------|--------------|----------|
| `lochat.heademoji.use` | `true` | Головы игроков в чате |
| `lochat.admin` | `op` | Админские команды |
---
## 🎨 PlaceholderAPI
### Основные плейсхолдеры
``` ```
%lochat_muted% — замучен (true/false) LoChat/ # Root проекта
%lochat_mute_time% — оставшееся время мута │
%lochat_mute_reason% — причина мута ├── src/main/ # LoChat (основной модуль)
%lochat_ignored_count% — количество игнорируемых │ ├── java/com/loki/lochat/
%lochat_global_enabled% — глобальный чат включен │ │ ├── api/ # API интерфейсы
%lochat_spy_enabled% — шпион режим включен │ │ │ ├── filter/ # Фильтры сообщений
│ │ │ └── service/ # Сервисы (Chat, Mute, PM и т.д.)
│ │ ├── commands/ # Команды плагина
│ │ ├── config/ # Конфигурация
│ │ ├── core/ # Реализации
│ │ │ ├── filter/ # Реализации фильтров
│ │ │ ├── registry/ # ServiceRegistry (DI)
│ │ │ └── service/ # Реализации сервисов
│ │ ├── gradient/ # Градиентные ники
│ │ │ ├── commands/ # Команды градиентов
│ │ │ ├── config/ # Конфиг градиентов
│ │ │ ├── hook/ # LuckPerms интеграция
│ │ │ └── util/ # Утилиты градиентов
│ │ ├── listener/ # Слушатели событий
│ │ ├── util/ # Утилиты (Folia)
│ │ ├── utils/ # Утилиты (Formatter, Mention)
│ │ └── LoChat.java # Главный класс
│ └── resources/ # Конфиги
│ ├── config.yml # Основной конфиг
│ ├── messages.yml # Сообщения
│ ├── chat.yml # Настройки чата
│ ├── gradient-config.yml # Конфиг градиентов
│ ├── gradient-messages.yml
│ ├── custom-commands.yml
│ ├── automessages.yml
│ └── plugin.yml
│
├── lohub/ # LoHub модуль
│ ├── src/main/
│ │ ├── java/com/loki/lohub/
│ │ │ ├── actions/ # Action система
│ │ │ │ ├── impl/ # Реализации действий
│ │ │ │ ├── Action.java
│ │ │ │ └── ActionType.java
│ │ │ ├── commands/ # Команды
│ │ │ │ ├── HubCommand.java
│ │ │ │ ├── SetHubCommand.java
│ │ │ │ └── LoHubCommand.java
│ │ │ ├── config/ # Конфигурация
│ │ │ │ └── ConfigManager.java
│ │ │ ├── listeners/ # Слушатели
│ │ │ │ ├── HubProtectionListener.java
│ │ │ │ ├── PlayerJoinListener.java
│ │ │ │ ├── LaunchpadListener.java
│ │ │ │ └── DoubleJumpListener.java
│ │ │ ├── managers/ # Менеджеры
│ │ │ │ ├── SpawnManager.java
│ │ │ │ ├── RegionManager.java
│ │ │ │ ├── ActionManager.java
│ │ │ │ ├── CooldownManager.java
│ │ │ │ ├── HotbarManager.java
│ │ │ │ └── PlayerHiderManager.java
│ │ │ ├── utils/ # Утилиты
│ │ │ │ ├── ItemBuilder.java
│ │ │ │ ├── TextUtil.java
│ │ │ │ └── PlaceholderUtil.java
│ │ │ └── LoHub.java # Главный класс
│ │ └── resources/ # Конфиги
│ │ ├── config.yml
│ │ └── plugin.yml
│ └── build.gradle.kts # Gradle конфиг
│
├── lovelocity/ # LoVelocity модуль
│ ├── src/main/
│ │ ├── java/com/loki/lovelocity/
│ │ │ ├── commands/
│ │ │ │ └── HubCommand.java
│ │ │ └── LoVelocity.java
│ │ └── resources/
│ │ └── velocity-plugin.json
│ └── build.gradle.kts
│
├── build.gradle.kts # Root Gradle (Kotlin DSL)
├── settings.gradle.kts # Настройки проектов
├── gradle.properties # Gradle свойства
├── TODO.md # Список задач
└── README.md # Этот файл
``` ```
### Градиентные плейсхолдеры ## 🚀 Сборка
```
%lochat_gradient_full% — полное имя (префикс + ник) ```bash
%lochat_gradient_name% — только градиентный ник # Собрать все модули
%lochat_gradient_prefix% — только префикс ./gradlew build
%lochat_full% — алиас для gradient_full
%lochat_name% — алиас для gradient_name # Собрать только LoChat
%lochat_prefix% — алиас для gradient_prefix ./gradlew :build
# Собрать только LoHub
./gradlew :lohub:build
# Собрать только LoVelocity
./gradlew :lovelocity:build
# Без тестов
./gradlew build -x test
``` ```
--- ## 📦 Артефакты
## 📁 Конфигурационные файлы После сборки jar файлы находятся в:
- `build/libs/LoChat-1.5.5-1.20.1.jar`
| Файл | Описание | - `lohub/build/libs/LoHub-1.5.5-1.20.1.jar`
|------|----------| - `lovelocity/build/libs/LoVelocity-1.5.5.jar`
| `config.yml` | Основные настройки плагина |
| `messages.yml` | Сообщения плагина |
| `automessages.yml` | Настройки автосообщений |
| `emojis.yml` | Список смайликов |
| `custom-commands.yml` | Кастомные команды |
| `gradient-config.yml` | Настройки градиентов |
| `gradient-messages.yml` | Сообщения градиентного модуля |
| `ignores.yml` | Сохранённые игноры (создается автоматически) |
| `mutes.yml` | Активные муты (создается автоматически) |
| `gradient-data.yml` | Данные градиентов (создается автоматически) |
---
## 🎯 Кастомные команды
LoChat позволяет создавать собственные команды через `custom-commands.yml`. После изменения файла выполните `/lochat reload`.
### Структура команды
```yaml
command-name:
permission: "право.для.использования" # Опционально
message: "Текст сообщения" # Обязательно
aliases: ["alias1", "alias2"] # Опционально
type: "chat" # chat, broadcast, title, actionbar
target: "sender" # sender, all, permission:право
```
### Типы команд
- **chat** — отправляет сообщение в чат
- **broadcast** — отправляет сообщение всем игрокам
- **title** — показывает title (разделение строк через `\n`)
- **actionbar** — показывает сообщение в actionbar
### Целевая аудитория
- **sender** — только отправителю команды
- **all** — всем игрокам на сервере
- **permission:право** — только игрокам с указанным правом
### Плейсхолдеры в командах
- `{player}` — имя игрока
- `{x}`, `{y}`, `{z}` — координаты игрока
- `{world}` — название мира
- `{arg0}`, `{arg1}`, `{arg2}` — аргументы команды
- `{args}` — все аргументы через пробел
- Все плейсхолдеры PlaceholderAPI
---
## 🎭 Головы игроков
### Использование
- `:head_nickname:` — показать ASCII голову игрока
- `@head nickname` — показать интерактивную голову с информацией
### Требования
- Право `lochat.heademoji.use`
- Включенная настройка `head-emoji.enabled: true`
---
## 🎨 Форматирование цветов
### MiniMessage (рекомендуется)
```
<red>Красный текст</red>
<gradient:red:blue>Градиент от красного к синему</gradient>
<rainbow>Радужный текст</rainbow>
<bold>Жирный</bold>
<italic>Курсив</italic>
```
### HEX цвета
```
<#FF0000>Красный через HEX</color>
<gradient:#FF0000:#0000FF>HEX градиент</gradient>
```
### Legacy коды (поддерживаются)
```
&cКрасный
&aЗеленый
&lЖирный
&nПодчеркнутый
```
---
## 🔧 Требования ## 🔧 Требования
### Обязательные - **Java 21+**
- **Paper 1.20.1** или новее - **Paper 1.20.1+** (для LoChat и LoHub)
- **Java 21** - **Velocity 3.3.0+** (для LoVelocity)
- **Gradle 9.1+**
### Опциональные ## 📝 Зависимости
- **PlaceholderAPI** — для плейсхолдеров
- **PlayerPoints** — для покупки градиентов за поинты
- **LuckPerms** — для интеграции с префиксами
- **SkinsRestorer** — для поддержки кастомных скинов
- **LibertyBans** — для интеграции с банами
--- ### LoChat
- PlaceholderAPI (опционально)
- LuckPerms (опционально)
- PlayerPoints (опционально)
## 🏗️ Сборка ### LoHub
- WorldGuard 7.0.9+ (опционально)
- PlaceholderAPI (опционально)
```bash ### LoVelocity
./gradlew clean build - Нет зависимостей
```
Готовый jar файл: `build/libs/LoChat-1.5.5-1.20.1.jar` ## 🎯 Статус разработки
--- - ✅ **LoChat** - Готов к использованию
- 🔄 **LoHub** - В разработке (21% готово)
- ✅ **LoVelocity** - Готов к использованию
## 📝 Changelog Подробности в [TODO.md](TODO.md)
### v1.5.5-1.20.1 ## 📄 Лицензия
- ✅ Адаптация для Minecraft 1.20.1 Paper
- ✅ Исправлен фильтр мата с поддержкой обходов
- ✅ Исправлена команда spy для мониторинга всех сообщений
- ✅ Добавлена поддержка голов игроков в чате
- ✅ Добавлена команда настройки сообщения очистки чата
- ✅ Интеграция с SkinsRestorer
- ✅ Улучшено качество кода (SOLID, DRY, KISS)
- ✅ Обновлены все конфиги в едином стиле
- ✅ Исправлены все предупреждения компилятора
--- Proprietary - Все права защищены
## 🤝 Поддержка ## 👥 Авторы
Если у вас возникли проблемы или вопросы: Loki Development Team
1. Проверьте конфигурационные файлы
2. Убедитесь в совместимости версий
3. Проверьте права доступа игроков
4. Создайте issue в репозитории
---
**© 2024 loki. Все права защищены.**

View file

@ -1,365 +0,0 @@
# План рефакторинга LoChat
## Цель
Убрать legacy код, перевести всё на новую архитектуру (SOLID + DI через ServiceRegistry)
---
## Текущая структура
### ✅ Новая архитектура (уже готово)
```
src/main/java/com/loki/lochat/
├── api/ # Интерфейсы сервисов
│ ├── service/
│ │ ├── ChatService.java
│ │ ├── MuteService.java
│ │ ├── AntiSpamService.java
│ │ ├── CooldownService.java
│ │ ├── MessageService.java
│ │ └── PlayerDataService.java
│ └── filter/
│ └── MessageFilter.java
├── core/ # Реализации
│ ├── service/
│ │ ├── ChatServiceImpl.java
│ │ ├── MuteServiceImpl.java
│ │ ├── AntiSpamServiceImpl.java
│ │ ├── CooldownServiceImpl.java
│ │ ├── MessageServiceImpl.java
│ │ └── PlayerDataServiceImpl.java
│ ├── filter/
│ │ ├── MuteFilter.java
│ │ ├── CooldownFilter.java
│ │ ├── SpamFilter.java
│ │ └── WordFilter.java
│ └── registry/
│ └── ServiceRegistry.java
└── listener/ # Новые слушатели
├── ChatEventListener.java (35 строк)
└── PlayerEventListener.java (23 строки)
```
### ❌ Legacy код (нужно удалить/переписать)
```
src/main/java/com/loki/lochat/
├── managers/ # Старые менеджеры
│ ├── MuteManager.java (409 строк) ❌
│ ├── ChatManager.java (131 строк) ❌
│ ├── AntiSpamManager.java ❌
│ ├── CooldownManager.java ❌
│ ├── PMManager.java ❌
│ ├── IgnoreManager.java ❌
│ ├── SpyManager.java ❌
│ ├── MentionManager.java (123 строки) ❌
│ ├── AutoMessageManager.java (159 строк) ⚠️
│ └── CustomCommandManager.java (269 строк) ⚠️
└── listeners/
└── ChatListener.java (185 строк) ❌
```
---
## Этап 1: Анализ зависимостей
### 1.1 Проверить что использует legacy менеджеры
```bash
# Найти все вызовы getMuteManager(), getChatManager() и т.д.
grep -r "getMuteManager\|getChatManager\|getAntiSpamManager" src/main/java/com/loki/lochat/commands/
```
### 1.2 Составить список команд для миграции
- [x] ChatEventListener - уже использует ServiceRegistry
- [ ] MuteCommand - использует MuteManager
- [ ] UnmuteCommand - использует MuteManager
- [ ] MuteListCommand - использует MuteManager
- [ ] MuteHistoryCommand - использует MuteManager
- [ ] MuteBlameCommand - использует MuteManager
- [ ] GlobalChatCommand - использует ChatManager
- [ ] LocalChatCommand - использует ChatManager
- [ ] MsgCommand - использует PMManager, IgnoreManager
- [ ] ReplyCommand - использует PMManager, IgnoreManager
- [ ] IgnoreCommand - использует IgnoreManager
- [ ] UnignoreCommand - использует IgnoreManager
- [ ] ChatSpyCommand - использует SpyManager
---
## Этап 2: Создание новых сервисов
### 2.1 PMService (личные сообщения)
```java
// api/service/PMService.java
public interface PMService {
void sendPrivateMessage(Player sender, Player receiver, String message);
Optional<UUID> getLastConversation(UUID player);
void setLastConversation(UUID player, UUID target);
}
// core/service/PMServiceImpl.java
public class PMServiceImpl implements PMService {
private final Map<UUID, UUID> conversations = new ConcurrentHashMap<>();
// ...
}
```
### 2.2 IgnoreService (игнорирование)
```java
// api/service/IgnoreService.java
public interface IgnoreService {
boolean isIgnoring(UUID player, UUID target);
void addIgnore(UUID player, UUID target);
void removeIgnore(UUID player, UUID target);
int getIgnoredCount(UUID player);
}
// core/service/IgnoreServiceImpl.java
public class IgnoreServiceImpl implements IgnoreService {
private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>();
// ...
}
```
### 2.3 SpyService (шпионаж за чатом)
```java
// api/service/SpyService.java
public interface SpyService {
boolean toggleSpy(UUID player);
boolean isSpying(UUID player);
void sendToSpies(Player sender, Object message, boolean isGlobal);
}
// core/service/SpyServiceImpl.java
public class SpyServiceImpl implements SpyService {
private final Set<UUID> spies = ConcurrentHashMap.newKeySet();
// ...
}
```
### 2.4 MentionService (упоминания)
```java
// api/service/MentionService.java
public interface MentionService {
String processMentions(String message, Set<Player> mentioned);
void notifyMentioned(Set<Player> players);
}
// core/service/MentionServiceImpl.java
public class MentionServiceImpl implements MentionService {
// ...
}
```
---
## Этап 3: Миграция команд
### 3.1 Mute команды
**Приоритет: ВЫСОКИЙ** (используют MuteManager 409 строк)
```java
// Было:
public class MuteCommand {
private final LoChat plugin;
// plugin.getMuteManager().mute(...)
}
// Станет:
public class MuteCommand {
private final MuteService muteService;
public MuteCommand(LoChat plugin) {
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
}
```
**Файлы для миграции:**
- [ ] MuteCommand.java
- [ ] UnmuteCommand.java
- [ ] MuteListCommand.java
- [ ] MuteHistoryCommand.java
- [ ] MuteBlameCommand.java
### 3.2 Chat команды
**Приоритет: ВЫСОКИЙ**
```java
// Было:
plugin.getChatManager().sendGlobalMessage(...)
// Станет:
ChatService chatService = plugin.getServiceRegistry().get(ChatService.class);
chatService.sendGlobalMessage(...)
```
**Файлы для миграции:**
- [ ] GlobalChatCommand.java
- [ ] LocalChatCommand.java
### 3.3 PM команды
**Приоритет: СРЕДНИЙ**
**Файлы для миграции:**
- [ ] MsgCommand.java
- [ ] ReplyCommand.java
### 3.4 Ignore команды
**Приоритет: СРЕДНИЙ**
**Файлы для миграции:**
- [ ] IgnoreCommand.java
- [ ] UnignoreCommand.java
### 3.5 Spy команды
**Приоритет: НИЗКИЙ**
**Файлы для миграции:**
- [ ] ChatSpyCommand.java
---
## Этап 4: Удаление legacy кода
### 4.1 Удалить старые менеджеры
После миграции всех команд:
```bash
rm src/main/java/com/loki/lochat/managers/MuteManager.java
rm src/main/java/com/loki/lochat/managers/ChatManager.java
rm src/main/java/com/loki/lochat/managers/AntiSpamManager.java
rm src/main/java/com/loki/lochat/managers/CooldownManager.java
rm src/main/java/com/loki/lochat/managers/PMManager.java
rm src/main/java/com/loki/lochat/managers/IgnoreManager.java
rm src/main/java/com/loki/lochat/managers/SpyManager.java
rm src/main/java/com/loki/lochat/managers/MentionManager.java
```
### 4.2 Удалить старый ChatListener
```bash
rm src/main/java/com/loki/lochat/listeners/ChatListener.java
```
### 4.3 Очистить LoChat.java
Удалить все legacy геттеры:
```java
// Удалить:
public PMManager getPmManager() { return pmManager; }
public IgnoreManager getIgnoreManager() { return ignoreManager; }
public SpyManager getSpyManager() { return spyManager; }
public MuteManager getMuteManager() { return muteManager; }
public ChatManager getChatManager() { return chatManager; }
public AntiSpamManager getAntiSpamManager() { return antiSpamManager; }
public CooldownManager getCooldownManager() { return cooldownManager; }
public MentionManager getMentionManager() { return mentionManager; }
```
---
## Этап 5: Оптимизация больших классов
### 5.1 ConfigManager (270 строк)
**Проблема:** Слишком много методов
**Решение:** Разбить на категории
```java
// config/ChatConfig.java - настройки чата
// config/MuteConfig.java - настройки мутов
// config/SpamConfig.java - настройки антиспама
```
### 5.2 CustomCommandManager (269 строк)
**Проблема:** Много логики в одном классе
**Решение:** Вынести парсинг команд
```java
// commands/custom/CustomCommandParser.java
// commands/custom/CustomCommandExecutor.java
```
### 5.3 GradientModule (249 строк)
**Статус:** Оставить как есть (это отдельный модуль)
### 5.4 AutoMessageManager (159 строк)
**Статус:** Оставить как есть (нормальный размер)
---
## Этап 6: Тестирование
### 6.1 Написать тесты для новых сервисов
```java
// test/java/com/loki/lochat/core/service/
├── PMServiceImplTest.java
├── IgnoreServiceImplTest.java
├── SpyServiceImplTest.java
└── MentionServiceImplTest.java
```
### 6.2 Интеграционные тесты
```java
// test/java/com/loki/lochat/integration/
└── ChatFlowTest.java - тест полного флоу отправки сообщения
```
---
## Этап 7: Финальная проверка
### 7.1 Проверить размеры классов
```bash
# Все классы должны быть < 150 строк (кроме модулей)
find src/main/java -name "*.java" -exec wc -l {} \; | sort -rn | head -20
```
### 7.2 Проверить архитектуру
- [ ] Все сервисы через интерфейсы
- [ ] Все зависимости через ServiceRegistry
- [ ] Нет прямых вызовов new Service()
- [ ] Все команды используют DI
### 7.3 Проверить производительность
- [ ] Нет утечек памяти
- [ ] Нет блокирующих операций в главном потоке
- [ ] Folia совместимость
---
## Метрики успеха
### До рефакторинга
- Главный класс: 166 строк
- Legacy менеджеры: 8 классов, ~1500 строк
- Старый ChatListener: 185 строк
- Дублирование кода: высокое
- Тестовое покрытие: 52 теста
### После рефакторинга (цель)
- Главный класс: <100 строк
- Новые сервисы: 8 интерфейсов + 8 реализаций, ~800 строк
- Новые слушатели: 2 класса, ~60 строк
- Дублирование кода: минимальное
- Тестовое покрытие: >80 тестов
- Все классы: <150 строк
---
## Порядок выполнения
1. ✅ Создать новую архитектуру (api + core + registry)
2. ✅ Мигрировать ChatEventListener
3. ✅ Удалить эмодзи
4. **→ Создать PMService, IgnoreService, SpyService, MentionService**
5. **→ Мигрировать все команды на новые сервисы**
6. **→ Удалить legacy менеджеры**
7. **→ Оптимизировать большие классы**
8. **→ Написать тесты**
9. **→ Финальная проверка**
---
## Примечания
- Миграция должна быть постепенной (по одной команде)
- После каждого шага - компиляция и тесты
- Сохранять обратную совместимость до полной миграции
- Документировать изменения в CHANGELOG.md

227
TODO.md
View file

@ -1,102 +1,167 @@
# LoChat - Статус проекта # 🚀 Loki Plugins - TODO
## ✅ Готово ## 📦 Проекты
### Чат ### 1. LoChat - Чат плагин (Paper/Folia)
- [x] Глобальный чат (`!сообщение`, `/g`) **Статус:** ✅ Готов к использованию
- [x] Локальный чат (100 блоков)
- [x] PM система (`/msg`, `/reply`)
- [x] Игнор (`/ignore`, `/unignore`) — сохраняется в файл
- [x] Упоминания @ник со звуком
### Модерация #### Функции
- [x] Мут система (`/mute`, `/unmute`) — сохраняется в файл - ✅ Глобальный/локальный чат
- [x] Фильтр мата (censor/block/warn) - ✅ PM система с игнором
- [x] Анти-спам (CAPS, повторы, похожие) - ✅ Мут система с историей
- [x] Кулдауны на сообщения - ✅ Фильтры (мат, спам, CAPS)
- [x] Чат-шпион (`/chatspy`) - ✅ Градиентные ники (LuckPerms интеграция)
- [x] Очистка чата (`/clearchat`) - ✅ Смайлики (300+ эмодзи)
- ✅ PlaceholderAPI
### Градиентные ники (LoPreff интеграция) - ✅ Markdown форматирование (**bold**, *italic*, ~~strike~~, `code`)
- [x] Команды `/color`, `/prefix` - ✅ Упоминания (@player, @everyone, @here, @role:permission)
- [x] Градиенты на ники и префиксы - ✅ Фильтры URL/IP/повторов
- [x] Интеграция с LuckPerms
- [x] PlayerPoints поддержка
- [x] TAB плагин совместимость (`%lochat_full%`)
- [x] Админ команды `/aprefix`
### Дополнительно
- [x] Цвет чата (`/chatcolor`)
- [x] Смайлики (300+ эмодзи)
- [x] Кастомные команды
- [x] Автосообщения
### Система
- [x] Объявления (Title + ActionBar)
- [x] PlaceholderAPI интеграция
- [x] HEX цвета `&#RRGGBB` и `§x§...`
- [x] MiniMessage форматирование
- [x] Folia совместимость
- [x] Горячая перезагрузка
--- ---
## 📊 PlaceholderAPI ### 2. LoHub - Hub плагин (Paper/Folia)
**Статус:** 🔄 В разработке (21% готово)
#### ✅ Готово
- ✅ Spawn система с телепортом
- ✅ WorldGuard регионы (авто-создание)
- ✅ Защита мира (блоки, PvP, мобы)
- ✅ Команды /hub, /sethub, /lohub
- ✅ Базовые утилиты (ItemBuilder, TextUtil, PlaceholderUtil)
#### 🔄 В работе - ЭТАП 2: Action система
- [ ] Action интерфейс и ActionType enum
- [ ] MessageAction - отправка сообщений
- [ ] SoundAction - звуки
- [ ] TitleAction - title/subtitle
- [ ] CommandAction - команды от игрока
- [ ] ConsoleAction - команды от консоли
- [ ] GamemodeAction - смена режима
- [ ] EffectAction - эффекты
- [ ] ActionManager - парсинг и выполнение
#### 📋 Запланировано
**ЭТАП 3: Join система**
- [ ] PlayerJoinListener
- [ ] Телепорт на спавн при входе
- [ ] Хил и очистка эффектов
- [ ] Кастомные сообщения входа/выхода
- [ ] Интеграция с Action системой
**ЭТАП 4: Custom Join Items**
- [ ] HotbarManager
- [ ] Выдача предметов при входе
- [ ] Обработка кликов (actions)
- [ ] Защита от выбрасывания
**ЭТАП 5: Launchpad**
- [ ] LaunchpadListener
- [ ] Прыжковые площадки (плита + редстоун)
**ЭТАП 6: Double Jump**
- [ ] DoubleJumpListener
- [ ] CooldownManager
- [ ] Двойной прыжок в воздухе
**ЭТАП 7: Player Hider**
- [ ] PlayerHiderManager
- [ ] Скрытие/показ игроков
- [ ] Предмет для переключения
**ЭТАП 8-10: Опционально**
- [ ] Scoreboard модуль
- [ ] Tablist модуль
- [ ] Announcements модуль
---
### 3. LoVelocity - Velocity плагин
**Статус:** ✅ Готов
#### Функции
- ✅ Команда /hub для телепорта на lobby/hub сервер
- ✅ Кастомные сообщения
- ✅ Поддержка PlaceholderAPI
---
## 🏗️ Структура проекта
``` ```
%lochat_muted% — замучен (true/false) LoChat/ # Root проекта
%lochat_mute_time% — время мута ├── src/main/ # LoChat (основной модуль)
%lochat_mute_reason% — причина мута │ ├── java/com/loki/lochat/
%lochat_ignored_count% — кол-во игнорируемых │ │ ├── api/ # API интерфейсы
%lochat_global_enabled% — глобальный чат вкл │ │ ├── commands/ # Команды
%lochat_last_pm% — последний собеседник │ │ ├── config/ # Конфигурация
%lochat_spy_enabled% — шпион режим │ │ ├── core/ # Реализации
%lochat_full% — префикс + градиентный ник (для TAB) │ │ ├── gradient/ # Градиентные ники
%lochat_name% — только градиентный ник │ │ ├── listener/ # Слушатели событий
%lochat_prefix% — только префикс с градиентом │ │ ├── utils/ # Утилиты
%lochat_lp_prefix% — LuckPerms префикс │ │ └── LoChat.java # Главный класс
│ └── resources/ # Конфиги
│
├── lohub/ # LoHub модуль
│ ├── src/main/
│ │ ├── java/com/loki/lohub/
│ │ │ ├── actions/ # Action система
│ │ │ ├── commands/ # Команды
│ │ │ ├── config/ # Конфигурация
│ │ │ ├── listeners/ # Слушатели
│ │ │ ├── managers/ # Менеджеры
│ │ │ ├── utils/ # Утилиты
│ │ │ └── LoHub.java # Главный класс
│ │ └── resources/ # Конфиги
│ └── build.gradle.kts # Gradle конфиг
│
├── lovelocity/ # LoVelocity модуль
│ ├── src/main/
│ │ └── java/com/loki/lovelocity/
│ └── build.gradle.kts # Gradle конфиг
│
├── build.gradle.kts # Root Gradle (Kotlin DSL)
├── settings.gradle.kts # Настройки проектов
└── TODO.md # Этот файл
``` ```
--- ---
## 📁 Файлы данных ## 🎯 Текущий фокус
- `ignores.yml` — список игнорируемых игроков **LoHub - ЭТАП 2: Action система**
- `mutes.yml` — активные муты
- `chatcolors.yml` — цвета чата игроков Создание гибкой системы действий для настройки поведения через конфиг:
- `gradient-data.yml` — данные градиентных ников
```yaml
actions:
- "[MESSAGE] &aПривет, %player%!"
- "[SOUND] ENTITY_PLAYER_LEVELUP"
- "[TITLE] &b&lДобро пожаловать;&fНа сервер;1;3;1"
- "[COMMAND] spawn"
- "[CONSOLE] give %player% diamond 1"
- "[GAMEMODE] survival"
- "[EFFECT] SPEED;1"
```
--- ---
## 🔑 Права ## 📝 Правила разработки
| Право | Описание | 1. **Никакого русского текста в Java файлах** - только в конфигах
|-------|----------| 2. **Цвета только в формате `&#RRGGBB`** - унифицированный стиль
| chat.global.use | Глобальный чат | 3. **SOLID принципы** - чистая архитектура
| chat.global.color | Цвета в глобальном чате | 4. **Классы до 100-150 строк** - читаемость кода
| chat.local.use | Локальный чат | 5. **Gradle Kotlin DSL** - современный подход
| chat.local.color | Цвета в локальном чате | 6. **Без лишних MD файлов** - только TODO.md и docs/
| chat.pm.use | Личные сообщения |
| chat.pm.ignore | Игнор игроков |
| chat.announce.use | Объявления |
| chat.spy | Шпион режим |
| chat.clear | Очистка чата |
| chat.mute | Мут игроков |
| chat.bypass.cooldown | Обход кулдауна |
| chat.bypass.filter | Обход фильтра |
| chat.bypass.antispam | Обход анти-спама |
| lochat.chatcolor | Цвет чата |
| gradient.color | Градиентные цвета |
| gradient.prefix | Кастомные префиксы |
| gradient.admin | Админ команды градиентов |
| lochat.admin | Админ команды плагина |
--- ---
## 🎯 Возможные улучшения ## 🔧 Технологии
- [ ] GUI для настройки градиентов - **Java 17+**
- [ ] Анимированные градиенты - **Paper API 1.20.1**
- [ ] Больше форматов времени в муте - **Velocity API 3.3.0**
- [ ] Статистика сообщений - **Gradle 9.1+ (Kotlin DSL)**
- [ ] Бэкап системы данных - **WorldGuard 7.0.9**
- **PlaceholderAPI 2.11.6**
- **LuckPerms API**

View file

@ -1,55 +0,0 @@
plugins {
id 'java'
}
group = 'com.loki'
version = '1.5.5-1.20.1'
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
maven { url = 'https://repo.rosewooddev.io/repository/public/' }
maven { url = 'https://jitpack.io' }
maven { url = 'https://nexus.skinsrestorer.net/repository/maven-public/' }
}
dependencies {
compileOnly 'io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT'
compileOnly 'me.clip:placeholderapi:2.11.6'
compileOnly 'org.black_ixx:playerpoints:3.2.7'
compileOnly 'net.luckperms:api:5.4'
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.0'
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.compilerArgs += ['-Xlint:all', '-Xlint:-processing']
options.fork = true
options.forkOptions.jvmArgs += ['-XX:+UseParallelGC']
}
test {
useJUnitPlatform()
// Параллельное выполнение тестов
maxParallelForks = Runtime.runtime.availableProcessors()
forkEvery = 100
}
jar {
archiveFileName = 'LoChat-1.5.5-1.20.1.jar'
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

54
build.gradle.kts Normal file
View file

@ -0,0 +1,54 @@
plugins {
java
}
group = "com.loki"
version = "1.5.5-1.20.1"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
maven("https://repo.rosewooddev.io/repository/public/")
maven("https://jitpack.io")
maven("https://nexus.skinsrestorer.net/repository/maven-public/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("org.black_ixx:playerpoints:3.2.7")
compileOnly("net.luckperms:api:5.4")
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.addAll(listOf("-Xlint:all", "-Xlint:-processing"))
options.isFork = true
options.forkOptions.jvmArgs = listOf("-XX:+UseParallelGC")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors()
setForkEvery(100)
}
tasks.jar {
archiveFileName.set("LoChat-1.5.5-1.20.1.jar")
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

28
lohub/build.gradle.kts Normal file
View file

@ -0,0 +1,28 @@
plugins {
java
}
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
maven("https://maven.enginehub.org/repo/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.9")
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
testImplementation("org.mockito:mockito-core:5.11.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
}
tasks.jar {
archiveFileName.set("LoHub-1.5.5-1.20.1.jar")
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View file

@ -0,0 +1,78 @@
package com.loki.lohub;
import com.loki.lohub.config.ConfigManager;
import com.loki.lohub.listeners.HubProtectionListener;
import com.loki.lohub.listeners.PlayerJoinListener;
import com.loki.lohub.managers.ActionManager;
import com.loki.lohub.managers.CooldownManager;
import com.loki.lohub.managers.RegionManager;
import com.loki.lohub.managers.SpawnManager;
import com.loki.lohub.utils.PlaceholderUtil;
import org.bukkit.plugin.java.JavaPlugin;
public class LoHub extends JavaPlugin {
private ConfigManager configManager;
private SpawnManager spawnManager;
private RegionManager regionManager;
private ActionManager actionManager;
private CooldownManager cooldownManager;
@Override
public void onEnable() {
getLogger().info("Loading LoHub v" + getDescription().getVersion());
saveDefaultConfig();
PlaceholderUtil.init();
this.configManager = new ConfigManager(this);
this.spawnManager = new SpawnManager(this);
this.regionManager = new RegionManager(this);
this.actionManager = new ActionManager(this);
this.cooldownManager = new CooldownManager();
registerListeners();
registerCommands();
regionManager.createHubRegion();
getLogger().info("LoHub successfully loaded!");
}
@Override
public void onDisable() {
getLogger().info("LoHub disabled!");
}
private void registerListeners() {
getServer().getPluginManager().registerEvents(new HubProtectionListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
}
private void registerCommands() {
getCommand("hub").setExecutor(new com.loki.lohub.commands.HubCommand(this));
getCommand("sethub").setExecutor(new com.loki.lohub.commands.SetHubCommand(this));
getCommand("lohub").setExecutor(new com.loki.lohub.commands.LoHubCommand(this));
}
public ConfigManager getConfigManager() {
return configManager;
}
public SpawnManager getSpawnManager() {
return spawnManager;
}
public RegionManager getRegionManager() {
return regionManager;
}
public ActionManager getActionManager() {
return actionManager;
}
public CooldownManager getCooldownManager() {
return cooldownManager;
}
}

View file

@ -0,0 +1,10 @@
package com.loki.lohub.actions;
import org.bukkit.entity.Player;
public interface Action {
void execute(Player player);
ActionType getType();
}

View file

@ -0,0 +1,23 @@
package com.loki.lohub.actions;
public enum ActionType {
MESSAGE,
BROADCAST,
TITLE,
ACTIONBAR,
SOUND,
COMMAND,
CONSOLE,
GAMEMODE,
EFFECT,
CLOSE,
UNKNOWN;
public static ActionType fromString(String type) {
try {
return valueOf(type.toUpperCase());
} catch (IllegalArgumentException e) {
return UNKNOWN;
}
}
}

View file

@ -0,0 +1,28 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import com.loki.lohub.utils.TextUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
public class ActionbarAction implements Action {
private final String message;
public ActionbarAction(String message) {
this.message = message;
}
@Override
public void execute(Player player) {
String parsed = PlaceholderUtil.parse(message, player);
player.sendActionBar(Component.text(TextUtil.colorize(parsed)));
}
@Override
public ActionType getType() {
return ActionType.ACTIONBAR;
}
}

View file

@ -0,0 +1,28 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import com.loki.lohub.utils.TextUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class BroadcastAction implements Action {
private final String message;
public BroadcastAction(String message) {
this.message = message;
}
@Override
public void execute(Player player) {
String parsed = PlaceholderUtil.parse(message, player);
Bukkit.broadcastMessage(TextUtil.colorize(parsed));
}
@Override
public ActionType getType() {
return ActionType.BROADCAST;
}
}

View file

@ -0,0 +1,18 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import org.bukkit.entity.Player;
public class CloseAction implements Action {
@Override
public void execute(Player player) {
player.closeInventory();
}
@Override
public ActionType getType() {
return ActionType.CLOSE;
}
}

View file

@ -0,0 +1,26 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import org.bukkit.entity.Player;
public class CommandAction implements Action {
private final String command;
public CommandAction(String command) {
this.command = command;
}
@Override
public void execute(Player player) {
String parsed = PlaceholderUtil.parse(command, player);
player.performCommand(parsed);
}
@Override
public ActionType getType() {
return ActionType.COMMAND;
}
}

View file

@ -0,0 +1,27 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class ConsoleAction implements Action {
private final String command;
public ConsoleAction(String command) {
this.command = command;
}
@Override
public void execute(Player player) {
String parsed = PlaceholderUtil.parse(command, player);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed);
}
@Override
public ActionType getType() {
return ActionType.CONSOLE;
}
}

View file

@ -0,0 +1,47 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public class EffectAction implements Action {
private final PotionEffectType effectType;
private final int amplifier;
private final int duration;
public EffectAction(String effectData) {
String[] parts = effectData.split(";");
PotionEffectType parsed;
try {
parsed = PotionEffectType.getByName(parts[0].toUpperCase());
} catch (Exception e) {
parsed = PotionEffectType.SPEED;
}
this.effectType = parsed != null ? parsed : PotionEffectType.SPEED;
this.amplifier = parts.length > 1 ? parseInt(parts[1], 0) : 0;
this.duration = parts.length > 2 ? parseInt(parts[2], 999999) : 999999;
}
@Override
public void execute(Player player) {
player.addPotionEffect(new PotionEffect(effectType, duration * 20, amplifier, false, false));
}
@Override
public ActionType getType() {
return ActionType.EFFECT;
}
private int parseInt(String value, int defaultValue) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}

View file

@ -0,0 +1,37 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
public class GamemodeAction implements Action {
private final GameMode gameMode;
public GamemodeAction(String mode) {
GameMode parsed;
try {
parsed = GameMode.valueOf(mode.toUpperCase());
} catch (IllegalArgumentException e) {
parsed = switch (mode.toLowerCase()) {
case "0", "s" -> GameMode.SURVIVAL;
case "1", "c" -> GameMode.CREATIVE;
case "2", "a" -> GameMode.ADVENTURE;
case "3", "sp" -> GameMode.SPECTATOR;
default -> GameMode.SURVIVAL;
};
}
this.gameMode = parsed;
}
@Override
public void execute(Player player) {
player.setGameMode(gameMode);
}
@Override
public ActionType getType() {
return ActionType.GAMEMODE;
}
}

View file

@ -0,0 +1,27 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import com.loki.lohub.utils.TextUtil;
import org.bukkit.entity.Player;
public class MessageAction implements Action {
private final String message;
public MessageAction(String message) {
this.message = message;
}
@Override
public void execute(Player player) {
String parsed = PlaceholderUtil.parse(message, player);
player.sendMessage(TextUtil.colorize(parsed));
}
@Override
public ActionType getType() {
return ActionType.MESSAGE;
}
}

View file

@ -0,0 +1,46 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
public class SoundAction implements Action {
private final Sound sound;
private final float volume;
private final float pitch;
public SoundAction(String soundData) {
String[] parts = soundData.split(";");
Sound parsedSound;
try {
parsedSound = Sound.valueOf(parts[0].toUpperCase());
} catch (IllegalArgumentException e) {
parsedSound = Sound.ENTITY_EXPERIENCE_ORB_PICKUP;
}
this.sound = parsedSound;
this.volume = parts.length > 1 ? parseFloat(parts[1], 1.0f) : 1.0f;
this.pitch = parts.length > 2 ? parseFloat(parts[2], 1.0f) : 1.0f;
}
@Override
public void execute(Player player) {
player.playSound(player.getLocation(), sound, volume, pitch);
}
@Override
public ActionType getType() {
return ActionType.SOUND;
}
private float parseFloat(String value, float defaultValue) {
try {
return Float.parseFloat(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}

View file

@ -0,0 +1,61 @@
package com.loki.lohub.actions.impl;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.utils.PlaceholderUtil;
import com.loki.lohub.utils.TextUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import org.bukkit.entity.Player;
import java.time.Duration;
public class TitleAction implements Action {
private final String title;
private final String subtitle;
private final int fadeIn;
private final int stay;
private final int fadeOut;
public TitleAction(String titleData) {
String[] parts = titleData.split(";");
this.title = parts.length > 0 ? parts[0] : "";
this.subtitle = parts.length > 1 ? parts[1] : "";
this.fadeIn = parts.length > 2 ? parseInt(parts[2], 10) : 10;
this.stay = parts.length > 3 ? parseInt(parts[3], 40) : 40;
this.fadeOut = parts.length > 4 ? parseInt(parts[4], 10) : 10;
}
@Override
public void execute(Player player) {
String parsedTitle = PlaceholderUtil.parse(title, player);
String parsedSubtitle = PlaceholderUtil.parse(subtitle, player);
Component titleComponent = Component.text(TextUtil.colorize(parsedTitle));
Component subtitleComponent = Component.text(TextUtil.colorize(parsedSubtitle));
Title.Times times = Title.Times.times(
Duration.ofMillis(fadeIn * 50L),
Duration.ofMillis(stay * 50L),
Duration.ofMillis(fadeOut * 50L)
);
Title title = Title.title(titleComponent, subtitleComponent, times);
player.showTitle(title);
}
@Override
public ActionType getType() {
return ActionType.TITLE;
}
private int parseInt(String value, int defaultValue) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}

View file

@ -0,0 +1,35 @@
package com.loki.lohub.commands;
import com.loki.lohub.LoHub;
import com.loki.lohub.utils.TextUtil;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class HubCommand implements CommandExecutor {
private final LoHub plugin;
public HubCommand(LoHub plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(TextUtil.colorize("&cThis command is only for players!"));
return true;
}
if (plugin.getSpawnManager().getSpawn() == null) {
player.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.no-spawn")));
return true;
}
plugin.getSpawnManager().teleportToSpawn(player);
player.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.spawn-teleport")));
return true;
}
}

View file

@ -0,0 +1,38 @@
package com.loki.lohub.commands;
import com.loki.lohub.LoHub;
import com.loki.lohub.utils.TextUtil;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
public class LoHubCommand implements CommandExecutor {
private final LoHub plugin;
public LoHubCommand(LoHub plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("lohub.admin")) {
sender.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.no-permission")));
return true;
}
if (args.length == 0) {
sender.sendMessage(TextUtil.colorize("&7LoHub v" + plugin.getDescription().getVersion()));
sender.sendMessage(TextUtil.colorize("&7Use: &e/lohub reload"));
return true;
}
if (args[0].equalsIgnoreCase("reload")) {
plugin.getConfigManager().reload();
sender.sendMessage(TextUtil.colorize("&aConfiguration reloaded!"));
return true;
}
return true;
}
}

View file

@ -0,0 +1,42 @@
package com.loki.lohub.commands;
import com.loki.lohub.LoHub;
import com.loki.lohub.utils.TextUtil;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class SetHubCommand implements CommandExecutor {
private final LoHub plugin;
public SetHubCommand(LoHub plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(TextUtil.colorize("&cThis command is only for players!"));
return true;
}
if (!player.hasPermission("lohub.admin")) {
player.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.no-permission")));
return true;
}
plugin.getSpawnManager().setSpawn(player.getLocation());
player.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.spawn-set")));
plugin.getRegionManager().deleteHubRegion();
plugin.getRegionManager().createHubRegion();
if (plugin.getConfig().getBoolean("worldguard.auto-create-region")) {
player.sendMessage(TextUtil.colorize(plugin.getConfig().getString("messages.region-created")));
}
return true;
}
}

View file

@ -0,0 +1,56 @@
package com.loki.lohub.config;
import com.loki.lohub.LoHub;
import org.bukkit.configuration.file.FileConfiguration;
public class ConfigManager {
private final LoHub plugin;
private FileConfiguration config;
public ConfigManager(LoHub plugin) {
this.plugin = plugin;
this.config = plugin.getConfig();
}
public void reload() {
plugin.reloadConfig();
this.config = plugin.getConfig();
}
public FileConfiguration getConfig() {
return config;
}
public boolean isProtectionEnabled() {
return config.getBoolean("protection.enabled", true);
}
public boolean isBlockBreakProtected() {
return config.getBoolean("protection.block-break", true);
}
public boolean isBlockPlaceProtected() {
return config.getBoolean("protection.block-place", true);
}
public boolean isPvPProtected() {
return config.getBoolean("protection.pvp", true);
}
public boolean isItemDropProtected() {
return config.getBoolean("protection.item-drop", true);
}
public boolean isItemPickupProtected() {
return config.getBoolean("protection.item-pickup", true);
}
public boolean isFoodLevelProtected() {
return config.getBoolean("protection.food-level", true);
}
public boolean isMobSpawningProtected() {
return config.getBoolean("protection.mob-spawning", true);
}
}

View file

@ -0,0 +1,104 @@
package com.loki.lohub.listeners;
import com.loki.lohub.LoHub;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerPickupItemEvent;
public class HubProtectionListener implements Listener {
private final LoHub plugin;
public HubProtectionListener(LoHub plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (plugin.getConfig().getBoolean("spawn.teleport-on-join", true)) {
plugin.getSpawnManager().teleportToSpawn(event.getPlayer());
}
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
Player player = event.getPlayer();
if (player.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isBlockBreakProtected()) {
event.setCancelled(true);
}
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
Player player = event.getPlayer();
if (player.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isBlockPlaceProtected()) {
event.setCancelled(true);
}
}
@EventHandler
public void onPvP(EntityDamageByEntityEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
if (!(event.getDamager() instanceof Player)) return;
if (!(event.getEntity() instanceof Player)) return;
Player damager = (Player) event.getDamager();
if (damager.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isPvPProtected()) {
event.setCancelled(true);
}
}
@EventHandler
public void onItemDrop(PlayerDropItemEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
Player player = event.getPlayer();
if (player.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isItemDropProtected()) {
event.setCancelled(true);
}
}
@EventHandler
public void onItemPickup(PlayerPickupItemEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
Player player = event.getPlayer();
if (player.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isItemPickupProtected()) {
event.setCancelled(true);
}
}
@EventHandler
public void onFoodLevelChange(FoodLevelChangeEvent event) {
if (!plugin.getConfigManager().isProtectionEnabled()) return;
if (!(event.getEntity() instanceof Player)) return;
Player player = (Player) event.getEntity();
if (player.hasPermission("lohub.bypass.protection")) return;
if (plugin.getConfigManager().isFoodLevelProtected()) {
event.setCancelled(true);
}
}
}

View file

@ -0,0 +1,208 @@
package com.loki.lohub.listeners;
import com.loki.lohub.LoHub;
import com.loki.lohub.utils.TextUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.Color;
import org.bukkit.FireworkEffect;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Firework;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.meta.FireworkMeta;
import java.util.ArrayList;
import java.util.List;
public class PlayerJoinListener implements Listener {
private final LoHub plugin;
public PlayerJoinListener(LoHub plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (isWorldDisabled(player.getWorld().getName())) {
return;
}
handleJoinMessage(event, player);
handleJoinSettings(player);
handleJoinEvents(player);
handleFirework(player);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
if (isWorldDisabled(player.getWorld().getName())) {
return;
}
if (plugin.getConfig().getBoolean("join_leave_messages.enabled", true)) {
String quitMessage = plugin.getConfig().getString("join_leave_messages.quit_message", "");
if (quitMessage.isEmpty()) {
event.quitMessage(null);
} else {
event.quitMessage(Component.text(TextUtil.colorize(quitMessage.replace("%player%", player.getName()))));
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
if (isWorldDisabled(player.getWorld().getName())) {
return;
}
if (plugin.getConfig().getBoolean("spawn.teleport-on-respawn", true)) {
Location spawn = plugin.getSpawnManager().getSpawn();
if (spawn != null) {
event.setRespawnLocation(spawn);
}
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerDeath(PlayerDeathEvent event) {
if (isWorldDisabled(event.getPlayer().getWorld().getName())) {
return;
}
if (plugin.getConfig().getBoolean("world_settings.disable_death_message", true)) {
event.deathMessage(null);
}
}
private void handleJoinMessage(PlayerJoinEvent event, Player player) {
if (plugin.getConfig().getBoolean("join_leave_messages.enabled", true)) {
String joinMessage = plugin.getConfig().getString("join_leave_messages.join_message", "");
if (joinMessage.isEmpty()) {
event.joinMessage(null);
} else {
event.joinMessage(Component.text(TextUtil.colorize(joinMessage.replace("%player%", player.getName()))));
}
}
}
private void handleJoinSettings(Player player) {
if (plugin.getConfig().getBoolean("join_settings.spawn_join", true)) {
Location spawn = plugin.getSpawnManager().getSpawn();
if (spawn != null) {
player.teleport(spawn);
}
}
if (plugin.getConfig().getBoolean("join_settings.heal", true)) {
player.setHealth(player.getMaxHealth());
player.setFoodLevel(20);
player.setSaturation(20);
}
if (plugin.getConfig().getBoolean("join_settings.extinguish", true)) {
player.setFireTicks(0);
}
if (plugin.getConfig().getBoolean("join_settings.clear_inventory", false)) {
player.getInventory().clear();
}
}
private void handleJoinEvents(Player player) {
List<String> actions = plugin.getConfig().getStringList("join_events");
if (!actions.isEmpty()) {
plugin.getActionManager().executeActions(player, actions, false);
}
}
private void handleFirework(Player player) {
if (!plugin.getConfig().getBoolean("join_settings.firework.enabled", false)) {
return;
}
boolean firstJoinOnly = plugin.getConfig().getBoolean("join_settings.firework.first_join_only", true);
if (firstJoinOnly && player.hasPlayedBefore()) {
return;
}
Location loc = player.getLocation();
Firework firework = (Firework) loc.getWorld().spawnEntity(loc, EntityType.FIREWORK);
FireworkMeta meta = firework.getFireworkMeta();
String typeStr = plugin.getConfig().getString("join_settings.firework.type", "BALL_LARGE");
FireworkEffect.Type type;
try {
type = FireworkEffect.Type.valueOf(typeStr);
} catch (IllegalArgumentException e) {
type = FireworkEffect.Type.BALL_LARGE;
}
List<Color> colors = new ArrayList<>();
for (String colorStr : plugin.getConfig().getStringList("join_settings.firework.colors")) {
Color color = parseColor(colorStr);
if (color != null) {
colors.add(color);
}
}
if (colors.isEmpty()) {
colors.add(Color.AQUA);
}
FireworkEffect effect = FireworkEffect.builder()
.with(type)
.withColor(colors)
.flicker(plugin.getConfig().getBoolean("join_settings.firework.flicker", true))
.trail(plugin.getConfig().getBoolean("join_settings.firework.trail", true))
.build();
meta.addEffect(effect);
meta.setPower(plugin.getConfig().getInt("join_settings.firework.power", 1));
firework.setFireworkMeta(meta);
}
private Color parseColor(String colorStr) {
return switch (colorStr.toUpperCase()) {
case "AQUA" -> Color.AQUA;
case "BLACK" -> Color.BLACK;
case "BLUE" -> Color.BLUE;
case "FUCHSIA" -> Color.FUCHSIA;
case "GRAY" -> Color.GRAY;
case "GREEN" -> Color.GREEN;
case "LIME" -> Color.LIME;
case "MAROON" -> Color.MAROON;
case "NAVY" -> Color.NAVY;
case "OLIVE" -> Color.OLIVE;
case "ORANGE" -> Color.ORANGE;
case "PURPLE" -> Color.PURPLE;
case "RED" -> Color.RED;
case "SILVER" -> Color.SILVER;
case "TEAL" -> Color.TEAL;
case "WHITE" -> Color.WHITE;
case "YELLOW" -> Color.YELLOW;
default -> null;
};
}
private boolean isWorldDisabled(String worldName) {
List<String> disabledWorlds = plugin.getConfig().getStringList("disabled-worlds.worlds");
boolean invert = plugin.getConfig().getBoolean("disabled-worlds.invert", false);
boolean isInList = disabledWorlds.contains(worldName);
return invert ? !isInList : isInList;
}
}

View file

@ -0,0 +1,98 @@
package com.loki.lohub.managers;
import com.loki.lohub.LoHub;
import com.loki.lohub.actions.Action;
import com.loki.lohub.actions.ActionType;
import com.loki.lohub.actions.impl.*;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ActionManager {
private final LoHub plugin;
private static final Pattern ACTION_PATTERN = Pattern.compile("\\[([A-Z]+)]\\s*(.*)");
public ActionManager(LoHub plugin) {
this.plugin = plugin;
}
public List<Action> parseActions(List<String> actionStrings) {
List<Action> actions = new ArrayList<>();
for (String actionString : actionStrings) {
Action action = parseAction(actionString);
if (action != null) {
actions.add(action);
}
}
return actions;
}
public Action parseAction(String actionString) {
if (actionString == null || actionString.isEmpty()) {
return null;
}
Matcher matcher = ACTION_PATTERN.matcher(actionString);
if (!matcher.matches()) {
plugin.getLogger().warning("Invalid action format: " + actionString);
return null;
}
String typeStr = matcher.group(1);
String data = matcher.group(2);
ActionType type = ActionType.fromString(typeStr);
return switch (type) {
case MESSAGE -> new MessageAction(data);
case BROADCAST -> new BroadcastAction(data);
case TITLE -> new TitleAction(data);
case ACTIONBAR -> new ActionbarAction(data);
case SOUND -> new SoundAction(data);
case COMMAND -> new CommandAction(data);
case CONSOLE -> new ConsoleAction(data);
case GAMEMODE -> new GamemodeAction(data);
case EFFECT -> new EffectAction(data);
case CLOSE -> new CloseAction();
default -> {
plugin.getLogger().warning("Unknown action type: " + typeStr);
yield null;
}
};
}
public void executeActions(Player player, List<Action> actions) {
for (Action action : actions) {
try {
action.execute(player);
} catch (Exception e) {
plugin.getLogger().severe("Error executing action " + action.getType() + ": " + e.getMessage());
e.printStackTrace();
}
}
}
public void executeActions(Player player, List<String> actionStrings, boolean parse) {
if (parse) {
List<Action> actions = parseActions(actionStrings);
executeActions(player, actions);
} else {
for (String actionString : actionStrings) {
Action action = parseAction(actionString);
if (action != null) {
try {
action.execute(player);
} catch (Exception e) {
plugin.getLogger().severe("Error executing action: " + e.getMessage());
}
}
}
}
}
}

View file

@ -0,0 +1,60 @@
package com.loki.lohub.managers;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class CooldownManager {
private final Map<String, Map<UUID, Long>> cooldowns = new HashMap<>();
public void setCooldown(String key, UUID playerId, int seconds) {
cooldowns.computeIfAbsent(key, k -> new HashMap<>())
.put(playerId, System.currentTimeMillis() + (seconds * 1000L));
}
public boolean hasCooldown(String key, UUID playerId) {
Map<UUID, Long> keyCooldowns = cooldowns.get(key);
if (keyCooldowns == null) {
return false;
}
Long expireTime = keyCooldowns.get(playerId);
if (expireTime == null) {
return false;
}
if (System.currentTimeMillis() >= expireTime) {
keyCooldowns.remove(playerId);
return false;
}
return true;
}
public int getRemainingSeconds(String key, UUID playerId) {
Map<UUID, Long> keyCooldowns = cooldowns.get(key);
if (keyCooldowns == null) {
return 0;
}
Long expireTime = keyCooldowns.get(playerId);
if (expireTime == null) {
return 0;
}
long remaining = expireTime - System.currentTimeMillis();
return remaining > 0 ? (int) (remaining / 1000) : 0;
}
public void removeCooldown(String key, UUID playerId) {
Map<UUID, Long> keyCooldowns = cooldowns.get(key);
if (keyCooldowns != null) {
keyCooldowns.remove(playerId);
}
}
public void clearAll() {
cooldowns.clear();
}
}

View file

@ -0,0 +1,130 @@
package com.loki.lohub.managers;
import com.loki.lohub.LoHub;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.regions.RegionContainer;
import org.bukkit.Location;
import org.bukkit.World;
public class RegionManager {
private final LoHub plugin;
private static final String HUB_REGION_NAME = "lohub_spawn";
public RegionManager(LoHub plugin) {
this.plugin = plugin;
}
public void createHubRegion() {
if (!plugin.getConfig().getBoolean("worldguard.auto-create-region", true)) {
plugin.getLogger().info("WorldGuard region auto-creation is disabled");
return;
}
if (!isWorldGuardAvailable()) {
plugin.getLogger().warning("WorldGuard not found! Region will not be created.");
return;
}
Location spawn = plugin.getSpawnManager().getSpawn();
if (spawn == null || spawn.getWorld() == null) {
plugin.getLogger().warning("Spawn not set! Use /sethub");
return;
}
World world = spawn.getWorld();
int radius = plugin.getConfig().getInt("worldguard.region-radius", 100);
try {
RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
com.sk89q.worldguard.protection.managers.RegionManager wgRegionManager = container.get(BukkitAdapter.adapt(world));
if (wgRegionManager == null) {
plugin.getLogger().warning("Failed to get RegionManager for world " + world.getName());
return;
}
ProtectedRegion existingRegion = wgRegionManager.getRegion(HUB_REGION_NAME);
if (existingRegion != null) {
plugin.getLogger().info("Region '" + HUB_REGION_NAME + "' already exists");
return;
}
BlockVector3 min = BlockVector3.at(
spawn.getBlockX() - radius,
world.getMinHeight(),
spawn.getBlockZ() - radius
);
BlockVector3 max = BlockVector3.at(
spawn.getBlockX() + radius,
world.getMaxHeight(),
spawn.getBlockZ() + radius
);
ProtectedCuboidRegion region = new ProtectedCuboidRegion(HUB_REGION_NAME, min, max);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.PVP,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.BLOCK_BREAK,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.BLOCK_PLACE,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.DAMAGE_ANIMALS,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.MOB_SPAWNING,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.LEAF_DECAY,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.ITEM_DROP,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
region.setFlag(com.sk89q.worldguard.protection.flags.Flags.ITEM_PICKUP,
com.sk89q.worldguard.protection.flags.StateFlag.State.DENY);
wgRegionManager.addRegion(region);
plugin.getLogger().info("Region '" + HUB_REGION_NAME + "' created around spawn (radius: " + radius + " blocks)");
plugin.getLogger().info("Coordinates: " + min + " -> " + max);
} catch (Exception e) {
plugin.getLogger().severe("Error creating WorldGuard region: " + e.getMessage());
e.printStackTrace();
}
}
public void deleteHubRegion() {
if (!isWorldGuardAvailable()) {
return;
}
Location spawn = plugin.getSpawnManager().getSpawn();
if (spawn == null || spawn.getWorld() == null) {
return;
}
try {
RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
com.sk89q.worldguard.protection.managers.RegionManager wgRegionManager = container.get(BukkitAdapter.adapt(spawn.getWorld()));
if (wgRegionManager != null) {
wgRegionManager.removeRegion(HUB_REGION_NAME);
plugin.getLogger().info("Region '" + HUB_REGION_NAME + "' deleted");
}
} catch (Exception e) {
plugin.getLogger().severe("Error deleting region: " + e.getMessage());
}
}
private boolean isWorldGuardAvailable() {
try {
Class.forName("com.sk89q.worldguard.WorldGuard");
return plugin.getServer().getPluginManager().isPluginEnabled("WorldGuard");
} catch (ClassNotFoundException e) {
return false;
}
}
}

View file

@ -0,0 +1,86 @@
package com.loki.lohub.managers;
import com.loki.lohub.LoHub;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
public class SpawnManager {
private final LoHub plugin;
private final File spawnFile;
private FileConfiguration spawnConfig;
private Location spawnLocation;
public SpawnManager(LoHub plugin) {
this.plugin = plugin;
this.spawnFile = new File(plugin.getDataFolder(), "spawn.yml");
loadSpawn();
}
private void loadSpawn() {
if (!spawnFile.exists()) {
try {
spawnFile.createNewFile();
} catch (IOException e) {
plugin.getLogger().severe("Failed to create spawn.yml: " + e.getMessage());
}
}
spawnConfig = YamlConfiguration.loadConfiguration(spawnFile);
if (spawnConfig.contains("spawn")) {
String worldName = spawnConfig.getString("spawn.world");
double x = spawnConfig.getDouble("spawn.x");
double y = spawnConfig.getDouble("spawn.y");
double z = spawnConfig.getDouble("spawn.z");
float yaw = (float) spawnConfig.getDouble("spawn.yaw");
float pitch = (float) spawnConfig.getDouble("spawn.pitch");
World world = Bukkit.getWorld(worldName);
if (world != null) {
spawnLocation = new Location(world, x, y, z, yaw, pitch);
plugin.getLogger().info("Spawn loaded: " + worldName + " (" + x + ", " + y + ", " + z + ")");
} else {
plugin.getLogger().warning("World '" + worldName + "' not found!");
}
}
}
public void setSpawn(Location location) {
this.spawnLocation = location;
spawnConfig.set("spawn.world", location.getWorld().getName());
spawnConfig.set("spawn.x", location.getX());
spawnConfig.set("spawn.y", location.getY());
spawnConfig.set("spawn.z", location.getZ());
spawnConfig.set("spawn.yaw", location.getYaw());
spawnConfig.set("spawn.pitch", location.getPitch());
try {
spawnConfig.save(spawnFile);
plugin.getLogger().info("Spawn set: " + location.getWorld().getName() +
" (" + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ() + ")");
} catch (IOException e) {
plugin.getLogger().severe("Failed to save spawn.yml: " + e.getMessage());
}
}
public Location getSpawn() {
return spawnLocation;
}
public void teleportToSpawn(Player player) {
if (spawnLocation == null) {
return;
}
player.teleport(spawnLocation);
}
}

View file

@ -0,0 +1,156 @@
package com.loki.lohub.utils;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ItemBuilder {
private final ItemStack itemStack;
public ItemBuilder(Material material) {
this.itemStack = new ItemStack(material);
}
public ItemBuilder(ItemStack itemStack) {
this.itemStack = itemStack.clone();
}
public static ItemBuilder fromConfig(ConfigurationSection section) {
return fromConfig(section, null);
}
public static ItemBuilder fromConfig(ConfigurationSection section, Player player) {
String materialName = section.getString("material", "STONE").toUpperCase();
Material material;
try {
material = Material.valueOf(materialName);
} catch (IllegalArgumentException e) {
material = Material.STONE;
}
ItemBuilder builder = new ItemBuilder(material);
if (section.contains("amount")) {
builder.amount(section.getInt("amount", 1));
}
if (section.contains("display_name")) {
String name = section.getString("display_name");
builder.name(player != null ? PlaceholderUtil.parse(name, player) : name);
}
if (section.contains("lore")) {
List<String> lore = section.getStringList("lore");
builder.lore(player != null ?
lore.stream().map(line -> PlaceholderUtil.parse(line, player)).collect(Collectors.toList()) :
lore);
}
if (section.contains("glow") && section.getBoolean("glow")) {
builder.glow();
}
if (section.contains("custom_model_data")) {
builder.customModelData(section.getInt("custom_model_data"));
}
if (section.contains("item_flags")) {
section.getStringList("item_flags").forEach(flagName -> {
try {
builder.flag(ItemFlag.valueOf(flagName.toUpperCase()));
} catch (IllegalArgumentException ignored) {}
});
}
if (material == Material.PLAYER_HEAD && section.contains("skull_owner")) {
String owner = section.getString("skull_owner");
builder.skullOwner(player != null ? owner.replace("%player%", player.getName()) : owner);
}
return builder;
}
public ItemBuilder amount(int amount) {
itemStack.setAmount(Math.max(1, amount));
return this;
}
public ItemBuilder name(String name) {
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
meta.setDisplayName(TextUtil.colorize(name));
itemStack.setItemMeta(meta);
}
return this;
}
public ItemBuilder lore(List<String> lore) {
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
List<String> coloredLore = lore.stream()
.map(TextUtil::colorize)
.collect(Collectors.toList());
meta.setLore(coloredLore);
itemStack.setItemMeta(meta);
}
return this;
}
public ItemBuilder lore(String... lines) {
return lore(List.of(lines));
}
public ItemBuilder flag(ItemFlag... flags) {
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
meta.addItemFlags(flags);
itemStack.setItemMeta(meta);
}
return this;
}
public ItemBuilder glow() {
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
meta.addEnchant(Enchantment.LUCK, 1, true);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
itemStack.setItemMeta(meta);
}
return this;
}
public ItemBuilder customModelData(int data) {
ItemMeta meta = itemStack.getItemMeta();
if (meta != null) {
meta.setCustomModelData(data);
itemStack.setItemMeta(meta);
}
return this;
}
public ItemBuilder skullOwner(String owner) {
if (itemStack.getType() == Material.PLAYER_HEAD) {
SkullMeta meta = (SkullMeta) itemStack.getItemMeta();
if (meta != null) {
meta.setOwner(owner);
itemStack.setItemMeta(meta);
}
}
return this;
}
public ItemStack build() {
return itemStack;
}
}

View file

@ -0,0 +1,55 @@
package com.loki.lohub.utils;
import me.clip.placeholderapi.PlaceholderAPI;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
public class PlaceholderUtil {
private static boolean placeholderAPIEnabled = false;
public static void init() {
placeholderAPIEnabled = Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI");
}
public static String parse(String text, Player player) {
if (text == null) {
return "";
}
text = parseInternalPlaceholders(text, player);
if (placeholderAPIEnabled && player != null) {
text = PlaceholderAPI.setPlaceholders(player, text);
}
return text;
}
private static String parseInternalPlaceholders(String text, Player player) {
if (player != null) {
text = text.replace("%player%", player.getName());
text = text.replace("%player_displayname%", player.getDisplayName());
if (text.contains("%location%")) {
Location loc = player.getLocation();
text = text.replace("%location%",
loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ());
}
if (text.contains("%world%")) {
text = text.replace("%world%", player.getWorld().getName());
}
}
text = text.replace("%online%", String.valueOf(Bukkit.getOnlinePlayers().size()));
text = text.replace("%online_max%", String.valueOf(Bukkit.getMaxPlayers()));
return text;
}
public static boolean isPlaceholderAPIEnabled() {
return placeholderAPIEnabled;
}
}

View file

@ -0,0 +1,72 @@
package com.loki.lohub.utils;
import net.md_5.bungee.api.ChatColor;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class TextUtil {
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
public static String colorize(String text) {
if (text == null || text.isEmpty()) {
return text;
}
text = translateHexCodes(text);
text = ChatColor.translateAlternateColorCodes('&', text);
return text;
}
public static List<String> colorize(List<String> lines) {
return lines.stream()
.map(TextUtil::colorize)
.collect(Collectors.toList());
}
private static String translateHexCodes(String text) {
Matcher matcher = HEX_PATTERN.matcher(text);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String hexCode = matcher.group(1);
String replacement = ChatColor.of("#" + hexCode).toString();
matcher.appendReplacement(buffer, replacement);
}
matcher.appendTail(buffer);
return buffer.toString();
}
public static String strip(String text) {
return ChatColor.stripColor(text);
}
public static String joinFrom(int startIndex, String[] args) {
if (args == null || startIndex >= args.length) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = startIndex; i < args.length; i++) {
builder.append(args[i]);
if (i < args.length - 1) {
builder.append(" ");
}
}
return builder.toString();
}
public static String listToString(List<String> list) {
if (list == null || list.isEmpty()) {
return "";
}
return String.join("\n", list);
}
}

View file

@ -0,0 +1,343 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoHub Configuration ║
# ║ Hub plugin for Paper/Folia 1.20.1+ ║
# ║ Version: 1.5.5-1.20.1 ║
# ║ Compatible with DeluxeHub config format ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
#
# BUILT-IN PLACEHOLDERS:
# %player% - Player name
# %player_displayname% - Player display name
# %location% - Player location (x, y, z)
# %world% - World name
# %online% - Online players count
# %online_max% - Max players
#
# ACTIONS (for join_events, custom_join_items, etc):
# [MESSAGE] text - Send message to player
# [BROADCAST] text - Broadcast to all players
# [TITLE] title;subtitle;fadeIn;stay;fadeOut - Send title
# [ACTIONBAR] text - Send actionbar message
# [SOUND] sound - Play sound
# [COMMAND] command - Execute as player
# [CONSOLE] command - Execute as console
# [GAMEMODE] mode - Change gamemode (survival/creative/adventure/spectator)
# [EFFECT] effect;level - Give potion effect
# [CLOSE] - Close inventory
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | GENERAL SETTINGS |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Check for updates (recommended)
check-updates: false
# Disable plugin in specific worlds
disabled-worlds:
invert: false
worlds:
- world_nether
- world_the_end
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | SPAWN SETTINGS |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
spawn:
# Teleport players to spawn on join
teleport-on-join: true
# Teleport players to spawn on respawn
teleport-on-respawn: true
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | WORLDGUARD INTEGRATION |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
worldguard:
# Auto-create WorldGuard region around spawn
auto-create-region: true
# Region radius in blocks
region-radius: 100
# Region name (don't change unless you know what you're doing)
region-name: "lohub_spawn"
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | WORLD PROTECTION |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
world_settings:
# Player
disable_hunger_loss: true
disable_fall_damage: true
disable_player_pvp: true
disable_void_death: true
disable_fire_damage: true
disable_drowning: true
disable_off_hand_swap: true
# Misc
disable_weather_change: true
disable_death_message: true
disable_mob_spawning: true
# Item entities
disable_item_drop: true
disable_item_pickup: true
# Blocks
disable_block_break: true
disable_block_place: true
disable_block_interact: true
disable_block_burn: true
disable_block_fire_spread: true
disable_block_leaf_decay: true
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | LAUNCHPAD |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
launchpad:
enabled: true
launch_power: 3.0
launch_power_y: 1.0
# Blocks required: pressure plate on top of redstone block
top_block: STONE_PRESSURE_PLATE
bottom_block: REDSTONE_BLOCK
actions:
- '[SOUND] ENTITY_BAT_TAKEOFF'
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | DOUBLE JUMP |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
double_jump:
enabled: true
launch_power: 1.0
launch_power_y: 0.9
cooldown: 3
actions:
- '[SOUND] ENTITY_BAT_TAKEOFF'
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | COMMAND BLOCKER |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
command_block:
enabled: true
blocked_commands:
- /pl
- /plugins
- /?
- /bukkit:?
- /bukkit:plugins
- /bukkit:pl
- /bukkit:help
- /ver
- /version
- /bukkit:version
- /bukkit:ver
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | ANTI-SWEAR |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
anti_swear:
enabled: false
blocked_words:
- fuck
- shit
- bitch
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | PLAYER JOIN |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
join_leave_messages:
enabled: true
join_message: "&#6BCB77+ &#FFD700%player% &#AAAAAAconnected"
quit_message: "&#FF6B6B- &#FFD700%player% &#AAAAAAdisconnected"
join_events:
- "[MESSAGE] &#FFD700╔═══════════════════════════════╗"
- "[MESSAGE] &r"
- "[MESSAGE] &r &#6BCB77Welcome, &#FFD700%player%&#6BCB77!"
- "[MESSAGE] &r"
- "[MESSAGE] &r &#00D4FFWebsite: &#FFFFFFwww.example.net"
- "[MESSAGE] &r &#FF6B6BStore: &#FFFFFFstore.example.net"
- "[MESSAGE] &r &#9B59B6Discord: &#FFFFFFdiscord.example.net"
- "[MESSAGE] &r"
- "[MESSAGE] &#FFD700╚═══════════════════════════════╝"
- "[TITLE] &#6BCB77W&#62C973E&#5AC76FL&#51C56BC&#49C367O&#40C163M&#38BF5FE;&#FFD700%player%;10;40;10"
- "[SOUND] ENTITY_PLAYER_LEVELUP"
- "[GAMEMODE] survival"
join_settings:
spawn_join: true
heal: true
extinguish: true
clear_inventory: false
firework:
enabled: true
first_join_only: true
type: BALL_LARGE
power: 1
flicker: true
trail: true
colors:
- AQUA
- RED
- TEAL
- WHITE
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | CUSTOM JOIN ITEMS |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
custom_join_items:
enabled: true
disable_inventory_movement: true
items:
info:
material: BOOK
amount: 1
slot: 0
display_name: "&#FFD700I&#FFC700n&#FFB700f&#FFA700o&#FF9700r&#FF8700m&#FF7700a&#FF6700t&#FF5700i&#FF4700o&#FF3700n"
lore:
- "&#AAAAAARight-click for server info!"
actions:
- "[MESSAGE] &r"
- "[MESSAGE] &#FFD700╔═══ Server Information ═══╗"
- "[MESSAGE] &r"
- "[MESSAGE] &#00D4FFWebsite: &#FFFFFFwww.example.net"
- "[MESSAGE] &#FF6B6BStore: &#FFFFFFstore.example.net"
- "[MESSAGE] &#9B59B6Discord: &#FFFFFFdiscord.example.net"
- "[MESSAGE] &r"
- "[MESSAGE] &#FFD700╚═══════════════════════════╝"
- "[SOUND] BLOCK_NOTE_BLOCK_PLING"
compass:
material: COMPASS
amount: 1
slot: 4
display_name: "&#6BCB77S&#62C973e&#5AC76Fr&#51C56Bv&#49C367e&#40C163r &#38BF5FS&#2FBD5Be&#27BB57l&#1EB953e&#16B74Fc&#0DB54Bt&#05B347o&#05B347r"
lore:
- "&#AAAAAARight-click to choose server!"
actions:
- "[MESSAGE] &#FF6B6BServer selector not implemented yet!"
- "[SOUND] ENTITY_VILLAGER_NO"
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | PLAYER HIDER |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
player_hider:
enabled: true
slot: 8
disable_inventory_movement: true
cooldown: 3
not_hidden:
material: LIME_DYE
amount: 1
display_name: '&#6BCB77Players &#AAAAAA> &#6BCB77Shown &#888888(Right-Click)'
lore:
- '&#AAAAAAClick to hide all players!'
hidden:
material: GRAY_DYE
amount: 1
display_name: '&#FF6B6BPlayers &#AAAAAA> &#FF6B6BHidden &#888888(Right-Click)'
lore:
- '&#AAAAAAClick to show all players!'
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | SCOREBOARD |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
scoreboard:
enabled: false
display_delay:
server_enter: 60
world_change: 40
refresh:
enabled: true
rate: 200
title: "&#FFD700L&#FFC700o&#FFB700H&#FFA700u&#FF9700b &#888888(%online%/%online_max%)"
lines:
- "&#AAAAAA━━━━━━━━━━━━━━━━━━━━━"
- "&#6BCB77Player:"
- " &#FFFFFF%player%"
- ""
- "&#6BCB77Rank:"
- " &#FFFFFF%vault_rank%"
- "&#AAAAAA━━━━━━━━━━━━━━━━━━━━━"
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | TABLIST |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
tablist:
enabled: false
refresh:
enabled: true
rate: 400
header:
- ""
- "&#FFD700L&#FFC700o&#FFB700H&#FFA700u&#FF9700b &#888888(%online%/%online_max%)"
- ""
footer:
- ""
- "&#00D4FFWebsite &#FFFFFFwww.example.net"
- "&#FF6B6BStore &#FFFFFFstore.example.net"
- ""
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | ANNOUNCEMENTS |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
announcements:
enabled: false
delay: 60
required_players: 1
sound:
enabled: true
value: BLOCK_NOTE_BLOCK_PLING
volume: 1.0
pitch: 1.0
announcements:
welcome:
- "&r"
- "&#FFD700╔═══ Announcement ═══╗"
- "&#AAAAAAWelcome to our server!"
- "&#FFD700╚═══════════════════════╝"
- "&r"
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# | MESSAGES |
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
messages:
prefix: "&#00D4FF[&#00D4FFL&#00CAF7o&#00C0EEH&#00B7E6u&#00ADDDb&#0099CC]"
spawn-set: "&#6BCB77Spawn set successfully!"
spawn-teleport: "&#6BCB77Teleporting to spawn..."
no-spawn: "&#FF6B6BSpawn not set! Use /sethub"
no-permission: "&#FF6B6BYou don't have permission!"
players-only: "&#FF6B6BThis command is only for players!"
config-reloaded: "&#6BCB77Configuration reloaded!"
usage: "&#FFD700Usage: /lohub reload"

View file

@ -0,0 +1,40 @@
name: LoHub
version: '1.5.5-1.20.1'
main: com.loki.lohub.LoHub
api-version: '1.20'
folia-supported: true
author: loki
description: Hub plugin for Paper/Folia with WorldGuard integration
softdepend:
- WorldGuard
- WorldEdit
commands:
hub:
description: Teleport to spawn
usage: /hub
permission: lohub.hub
aliases: [spawn, хаб, спавн]
sethub:
description: Set hub spawn location
usage: /sethub
permission: lohub.admin
aliases: [setspawn, установитьспавн]
lohub:
description: LoHub admin command
usage: /lohub [reload]
permission: lohub.admin
permissions:
lohub.hub:
description: Can use /hub command
default: true
lohub.admin:
description: Admin permissions
default: op
lohub.bypass.protection:
description: Bypass world protection
default: op

View file

@ -0,0 +1,25 @@
plugins {
java
}
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.velocitypowered.com/snapshots/")
}
dependencies {
compileOnly("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
}
tasks.jar {
archiveFileName.set("LoVelocity-1.5.5.jar")
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View file

@ -0,0 +1,65 @@
package com.loki.lovelocity;
import com.google.inject.Inject;
import com.loki.lovelocity.commands.HubCommand;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import org.slf4j.Logger;
import java.nio.file.Path;
@Plugin(
id = "lovelocity",
name = "LoVelocity",
version = "1.5.5",
description = "Velocity integration for LoNetwork",
authors = {"loki"}
)
public class LoVelocity {
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
@Inject
public LoVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
}
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
logger.info("LoVelocity loaded!");
logger.info("Version: 1.5.5");
registerCommands();
}
private void registerCommands() {
CommandMeta hubMeta = server.getCommandManager().metaBuilder("hub")
.aliases("lobby")
.plugin(this)
.build();
server.getCommandManager().register(hubMeta, new HubCommand(this));
logger.info("Commands registered: /hub, /lobby");
}
public ProxyServer getServer() {
return server;
}
public Logger getLogger() {
return logger;
}
public Path getDataDirectory() {
return dataDirectory;
}
}

View file

@ -0,0 +1,79 @@
package com.loki.lovelocity.commands;
import com.loki.lovelocity.LoVelocity;
import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class HubCommand implements SimpleCommand {
private final LoVelocity plugin;
private final List<String> hubServers = List.of("hub", "lobby");
public HubCommand(LoVelocity plugin) {
this.plugin = plugin;
}
@Override
public void execute(Invocation invocation) {
if (!(invocation.source() instanceof Player player)) {
invocation.source().sendMessage(Component.text("This command is only for players!", NamedTextColor.RED));
return;
}
Optional<RegisteredServer> currentServer = player.getCurrentServer().map(s -> s.getServer());
if (currentServer.isEmpty()) {
player.sendMessage(Component.text("Failed to determine current server!", NamedTextColor.RED));
return;
}
String currentServerName = currentServer.get().getServerInfo().getName().toLowerCase();
if (isHubServer(currentServerName)) {
player.sendMessage(Component.text("You are already on the hub!", NamedTextColor.YELLOW));
return;
}
Optional<RegisteredServer> hubServer = findHubServer();
if (hubServer.isEmpty()) {
player.sendMessage(Component.text("Hub server not found!", NamedTextColor.RED));
plugin.getLogger().warn("Hub server not found! Available servers: " +
plugin.getServer().getAllServers().stream()
.map(s -> s.getServerInfo().getName())
.toList());
return;
}
player.sendMessage(Component.text("Teleporting to hub...", NamedTextColor.GREEN));
player.createConnectionRequest(hubServer.get()).fireAndForget();
}
private boolean isHubServer(String serverName) {
return hubServers.stream().anyMatch(hub -> serverName.contains(hub));
}
private Optional<RegisteredServer> findHubServer() {
return plugin.getServer().getAllServers().stream()
.filter(server -> isHubServer(server.getServerInfo().getName().toLowerCase()))
.findFirst();
}
@Override
public CompletableFuture<List<String>> suggestAsync(Invocation invocation) {
return CompletableFuture.completedFuture(List.of());
}
@Override
public boolean hasPermission(Invocation invocation) {
return invocation.source().hasPermission("lovelocity.hub");
}
}

View file

@ -1 +0,0 @@
rootProject.name = 'LoChat'

4
settings.gradle.kts Normal file
View file

@ -0,0 +1,4 @@
rootProject.name = "LoNetwork"
include("lohub")
include("lovelocity")

View file

@ -16,6 +16,10 @@ public class CustomCommand extends Command {
public CustomCommand(String name, CustomCommandManager manager) { public CustomCommand(String name, CustomCommandManager manager) {
super(name); super(name);
this.manager = manager; this.manager = manager;
init(name);
}
private void init(String name) {
this.setDescription("Кастомная команда LoChat"); this.setDescription("Кастомная команда LoChat");
this.setUsage("/" + name); this.setUsage("/" + name);
} }

View file

@ -14,9 +14,6 @@ import org.bukkit.entity.Player;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* Команда /nick для установки кастомного ника
*/
public class NickCommand implements CommandExecutor, TabCompleter { public class NickCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin; private final LoChat plugin;
private final NickService nickService; private final NickService nickService;
@ -38,17 +35,14 @@ public class NickCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
// /nick - показать текущий ник
if (args.length == 0) { if (args.length == 0) {
return showCurrentNick(player); return showCurrentNick(player);
} }
// /nick reset - сбросить ник
if (args[0].equalsIgnoreCase("reset")) { if (args[0].equalsIgnoreCase("reset")) {
return resetNick(player); return resetNick(player);
} }
// /nick <ник> - установить ник
String nickname = String.join(" ", args); String nickname = String.join(" ", args);
return setNick(player, nickname); return setNick(player, nickname);
} }
@ -87,27 +81,23 @@ public class NickCommand implements CommandExecutor, TabCompleter {
} }
private boolean setNick(Player player, String nickname) { private boolean setNick(Player player, String nickname) {
// Валидация
if (!nickService.isValidNickname(nickname)) { if (!nickService.isValidNickname(nickname)) {
player.sendMessage(Component.text("Невалидный ник!", NamedTextColor.RED)); player.sendMessage(Component.text("Невалидный ник!", NamedTextColor.RED));
player.sendMessage(Component.text("Требования:", NamedTextColor.GRAY)); player.sendMessage(Component.text("Требования:", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Длина: 3-16 символов", NamedTextColor.GRAY)); player.sendMessage(Component.text("• Длина: 3-16 символов", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Разрешены: буквы, цифры, _, русские буквы", NamedTextColor.GRAY)); player.sendMessage(Component.text("• Разрешены: буквы, цифры, _, русские буквы", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Поддержка цветов: <red>текст</red>, <gradient:#FF0000:#00FF00>текст</gradient>", NamedTextColor.GRAY)); player.sendMessage(Component.text("• Поддержка цветов: &#FF0000текст, <gradient:&#FF0000:&#00FF00>текст</gradient>", NamedTextColor.GRAY));
return true; return true;
} }
// Проверка уникальности
if (nickService.isNicknameTaken(nickname)) { if (nickService.isNicknameTaken(nickname)) {
var currentNick = nickService.getNickname(player.getUniqueId()); var currentNick = nickService.getNickname(player.getUniqueId());
// Если это не свой текущий ник
if (currentNick.isEmpty() || !currentNick.get().equals(nickname)) { if (currentNick.isEmpty() || !currentNick.get().equals(nickname)) {
player.sendMessage(Component.text("Этот ник уже занят!", NamedTextColor.RED)); player.sendMessage(Component.text("Этот ник уже занят!", NamedTextColor.RED));
return true; return true;
} }
} }
// Устанавливаем ник
boolean success = nickService.setNickname(player.getUniqueId(), nickname); boolean success = nickService.setNickname(player.getUniqueId(), nickname);
if (success) { if (success) {
@ -126,8 +116,22 @@ public class NickCommand implements CommandExecutor, TabCompleter {
List<String> completions = new ArrayList<>(); List<String> completions = new ArrayList<>();
if (args.length == 1) { if (args.length == 1) {
String input = args[0].toLowerCase();
if ("reset".startsWith(input)) {
completions.add("reset"); completions.add("reset");
completions.add("<ник>"); }
if (sender instanceof Player player) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isPresent()) {
completions.add(currentNick.get());
}
}
completions.add("&#FF0000Красный");
completions.add("&#00FF00Зелёный");
completions.add("Русский_Ник");
} }
return completions; return completions;

View file

@ -0,0 +1,164 @@
package com.loki.lochat.core.filter;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AdvancedMessageFilter {
private static final Pattern URL_PATTERN = Pattern.compile(
"(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?",
Pattern.CASE_INSENSITIVE
);
private static final Pattern IP_PATTERN = Pattern.compile(
"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"
);
private static final Pattern REPEAT_PATTERN = Pattern.compile(
"(.)\\1{3,}"
);
private final FileConfiguration config;
private final List<String> whitelistedDomains;
private final List<String> blacklistedDomains;
public AdvancedMessageFilter(FileConfiguration config) {
this.config = config;
this.whitelistedDomains = config.getStringList("filters.url.whitelist");
this.blacklistedDomains = config.getStringList("filters.url.blacklist");
}
public FilterResult filterMessage(Player player, String message) {
if (config.getBoolean("filters.url.enabled", true)) {
FilterResult urlResult = filterUrls(player, message);
if (!urlResult.isAllowed()) {
return urlResult;
}
message = urlResult.getFilteredMessage();
}
if (config.getBoolean("filters.ip.enabled", true)) {
FilterResult ipResult = filterIPs(player, message);
if (!ipResult.isAllowed()) {
return ipResult;
}
message = ipResult.getFilteredMessage();
}
if (config.getBoolean("filters.repeat.enabled", true)) {
message = filterRepeatingChars(message);
}
return new FilterResult(true, message, null);
}
private FilterResult filterUrls(Player player, String message) {
if (player.hasPermission("lochat.bypass.urlfilter")) {
return new FilterResult(true, message, null);
}
Matcher matcher = URL_PATTERN.matcher(message);
while (matcher.find()) {
String url = matcher.group();
String domain = extractDomain(url);
if (!whitelistedDomains.isEmpty()) {
boolean isWhitelisted = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!isWhitelisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BСсылки запрещены!"));
}
}
if (!blacklistedDomains.isEmpty()) {
boolean isBlacklisted = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (isBlacklisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BЭта ссылка запрещена!"));
}
}
}
return new FilterResult(true, message, null);
}
private FilterResult filterIPs(Player player, String message) {
if (player.hasPermission("lochat.bypass.ipfilter")) {
return new FilterResult(true, message, null);
}
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
String replacement = config.getString("filters.ip.replacement", "[IP скрыт]");
String filtered = matcher.replaceAll(replacement);
if (config.getBoolean("filters.ip.block", false)) {
return new FilterResult(false, message,
config.getString("filters.ip.blocked-message", "&#FF6B6BIP адреса запрещены!"));
}
return new FilterResult(true, filtered, null);
}
return new FilterResult(true, message, null);
}
private String filterRepeatingChars(String message) {
int maxRepeats = config.getInt("filters.repeat.max-repeats", 3);
Matcher matcher = REPEAT_PATTERN.matcher(message);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String repeated = matcher.group(1);
String replacement = repeated.repeat(Math.min(maxRepeats, matcher.group().length()));
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
private String extractDomain(String url) {
url = url.replaceFirst("^https?://", "");
int slashIndex = url.indexOf('/');
if (slashIndex != -1) {
url = url.substring(0, slashIndex);
}
return url;
}
public static class FilterResult {
private final boolean allowed;
private final String filteredMessage;
private final String blockReason;
public FilterResult(boolean allowed, String filteredMessage, String blockReason) {
this.allowed = allowed;
this.filteredMessage = filteredMessage;
this.blockReason = blockReason;
}
public boolean isAllowed() {
return allowed;
}
public String getFilteredMessage() {
return filteredMessage;
}
public String getBlockReason() {
return blockReason;
}
}
}

View file

@ -9,25 +9,29 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* Реестр сервисов - Dependency Injection контейнер (SOLID DIP)
*/
public class ServiceRegistry { public class ServiceRegistry {
private final Map<Class<?>, Object> services = new HashMap<>(); private final Map<Class<?>, Object> services = new HashMap<>();
private final JavaPlugin plugin; private final JavaPlugin plugin;
public ServiceRegistry(JavaPlugin plugin) { public ServiceRegistry(JavaPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
registerServices(); init();
} }
public ServiceRegistry(JavaPlugin plugin, ConfigManager configManager, MessageConfig messageConfig) { public ServiceRegistry(JavaPlugin plugin, ConfigManager configManager, MessageConfig messageConfig) {
this.plugin = plugin; this.plugin = plugin;
initWithDeps(configManager, messageConfig);
}
private void init() {
registerServices();
}
private void initWithDeps(ConfigManager configManager, MessageConfig messageConfig) {
registerServicesWithDeps(configManager, messageConfig); registerServicesWithDeps(configManager, messageConfig);
} }
private void registerServices() { private void registerServices() {
// Регистрируем все сервисы (базовая версия)
register(ChatService.class, new ChatServiceImpl(plugin, this)); register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin)); register(MuteService.class, new MuteServiceImpl(plugin));
register(CooldownService.class, new CooldownServiceImpl()); register(CooldownService.class, new CooldownServiceImpl());
@ -36,14 +40,12 @@ public class ServiceRegistry {
} }
private void registerServicesWithDeps(ConfigManager configManager, MessageConfig messageConfig) { private void registerServicesWithDeps(ConfigManager configManager, MessageConfig messageConfig) {
// Регистрируем все сервисы с зависимостями
register(ChatService.class, new ChatServiceImpl(plugin, this)); register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin)); register(MuteService.class, new MuteServiceImpl(plugin));
register(CooldownService.class, new CooldownServiceImpl()); register(CooldownService.class, new CooldownServiceImpl());
register(MessageService.class, new MessageServiceImpl(plugin, this)); register(MessageService.class, new MessageServiceImpl(plugin, this));
register(PlayerDataService.class, new PlayerDataServiceImpl(plugin)); register(PlayerDataService.class, new PlayerDataServiceImpl(plugin));
// Новые сервисы
register(PMService.class, new PMServiceImpl()); register(PMService.class, new PMServiceImpl());
register(IgnoreService.class, new IgnoreServiceImpl(plugin)); register(IgnoreService.class, new IgnoreServiceImpl(plugin));
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig)); register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));

View file

@ -1,8 +1,10 @@
package com.loki.lochat.listener; package com.loki.lochat.listener;
import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.util.FoliaUtil; import com.loki.lochat.utils.MentionHandler;
import com.loki.lochat.utils.TextFormatter;
import io.papermc.paper.event.player.AsyncChatEvent; import io.papermc.paper.event.player.AsyncChatEvent;
import io.papermc.paper.chat.ChatRenderer; import io.papermc.paper.chat.ChatRenderer;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
@ -12,24 +14,23 @@ import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Sound;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
/**
* Слушатель событий чата с Paper Chat API
* Поддерживает hover tooltips, click actions, mentions
*/
public class ChatEventListener implements Listener { public class ChatEventListener implements Listener {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final MessageService messageService; private final MessageService messageService;
private final AdvancedMessageFilter advancedFilter;
private final MentionHandler mentionHandler;
public ChatEventListener(JavaPlugin plugin, ServiceRegistry registry) { public ChatEventListener(JavaPlugin plugin, ServiceRegistry registry) {
this.plugin = plugin; this.plugin = plugin;
this.messageService = registry.get(MessageService.class); this.messageService = registry.get(MessageService.class);
this.advancedFilter = new AdvancedMessageFilter(plugin.getConfig());
this.mentionHandler = new MentionHandler(plugin);
} }
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.HIGHEST)
@ -37,7 +38,6 @@ public class ChatEventListener implements Listener {
Player sender = event.getPlayer(); Player sender = event.getPlayer();
String message = PlainTextComponentSerializer.plainText().serialize(event.message()); String message = PlainTextComponentSerializer.plainText().serialize(event.message());
// Проверяем фильтры (мут, кулдаун)
boolean allowed = messageService.processMessage(sender, message); boolean allowed = messageService.processMessage(sender, message);
if (!allowed) { if (!allowed) {
@ -45,13 +45,20 @@ public class ChatEventListener implements Listener {
return; return;
} }
// Устанавливаем кастомный рендерер AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, message);
if (!filterResult.isAllowed()) {
event.setCancelled(true);
sender.sendMessage(com.loki.lochat.utils.ChatFormatter.parse(filterResult.getBlockReason()));
return;
}
String filteredMessage = filterResult.getFilteredMessage();
event.message(Component.text(filteredMessage));
event.renderer(new EnhancedChatRenderer(sender, event.message())); event.renderer(new EnhancedChatRenderer(sender, event.message()));
} }
/**
* Продвинутый рендерер чата с hover/click/mentions
*/
private class EnhancedChatRenderer implements ChatRenderer { private class EnhancedChatRenderer implements ChatRenderer {
private final Player sender; private final Player sender;
private final Component originalMessage; private final Component originalMessage;
@ -63,47 +70,31 @@ public class ChatEventListener implements Listener {
@Override @Override
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) { public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
// Получаем конфиг формата
String format = plugin.getConfig().getString("chat.global.format", "{prefix} {player}: {message}");
String prefix = plugin.getConfig().getString("chat.global.prefix", "[G]"); String prefix = plugin.getConfig().getString("chat.global.prefix", "[G]");
// Создаём компонент с hover и click
Component playerComponent = createPlayerComponent(source, sourceDisplayName); Component playerComponent = createPlayerComponent(source, sourceDisplayName);
// Обрабатываем сообщение
Component processedMessage = message; Component processedMessage = message;
// 1. Обрабатываем URL (делаем кликабельными) processedMessage = TextFormatter.formatMarkdown(processedMessage);
processedMessage = processUrls(processedMessage); processedMessage = processUrls(processedMessage);
// 2. Обрабатываем эмодзи шорткоды (:smile: -> 😊)
processedMessage = processEmojis(processedMessage); processedMessage = processEmojis(processedMessage);
// 3. Обрабатываем упоминания если viewer это Player
if (viewer instanceof Player viewerPlayer) { if (viewer instanceof Player viewerPlayer) {
processedMessage = processMentions(processedMessage, viewerPlayer); processedMessage = mentionHandler.processMentions(processedMessage, source, viewerPlayer);
} }
// Собираем финальное сообщение
return Component.text(prefix + " ") return Component.text(prefix + " ")
.append(playerComponent) .append(playerComponent)
.append(Component.text(": ")) .append(Component.text(": "))
.append(processedMessage); .append(processedMessage);
} }
/**
* Создаёт компонент имени игрока с hover и click
*/
private Component createPlayerComponent(Player player, Component displayName) { private Component createPlayerComponent(Player player, Component displayName) {
// Проверяем включен ли hover
if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) { if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) {
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
} }
// Получаем формат из конфига
java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format"); java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format");
if (hoverLines.isEmpty()) { if (hoverLines.isEmpty()) {
// Дефолтный формат если конфиг пустой
hoverLines = java.util.Arrays.asList( hoverLines = java.util.Arrays.asList(
"<gray>Игрок: <white>{player}", "<gray>Игрок: <white>{player}",
"<gray>Мир: <green>{world}", "<gray>Мир: <green>{world}",
@ -112,12 +103,10 @@ public class ChatEventListener implements Listener {
); );
} }
// Строим hover text
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text(); net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
for (int i = 0; i < hoverLines.size(); i++) { for (int i = 0; i < hoverLines.size(); i++) {
String line = hoverLines.get(i); String line = hoverLines.get(i);
// Заменяем плейсхолдеры
line = line.replace("{player}", player.getName()) line = line.replace("{player}", player.getName())
.replace("{world}", player.getWorld().getName()) .replace("{world}", player.getWorld().getName())
.replace("{ping}", String.valueOf(player.getPing())) .replace("{ping}", String.valueOf(player.getPing()))
@ -125,62 +114,28 @@ public class ChatEventListener implements Listener {
.replace("{health}", String.valueOf(Math.round(player.getHealth()))) .replace("{health}", String.valueOf(Math.round(player.getHealth())))
.replace("{food}", String.valueOf(player.getFoodLevel())); .replace("{food}", String.valueOf(player.getFoodLevel()));
// Парсим MiniMessage
Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line); Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line);
hoverBuilder.append(lineComponent); hoverBuilder.append(lineComponent);
// Добавляем перенос строки если не последняя
if (i < hoverLines.size() - 1) { if (i < hoverLines.size() - 1) {
hoverBuilder.append(Component.newline()); hoverBuilder.append(Component.newline());
} }
} }
// Click: открываем /msg
return displayName return displayName
.hoverEvent(HoverEvent.showText(hoverBuilder.build())) .hoverEvent(HoverEvent.showText(hoverBuilder.build()))
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
} }
/**
* Обрабатывает упоминания (@player) в сообщении
*/
private Component processMentions(Component message, Player viewer) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
// Проверяем упоминание текущего игрока
if (plainText.toLowerCase().contains("@" + viewer.getName().toLowerCase())) {
// Подсвечиваем упоминание
Component highlighted = message.replaceText(builder ->
builder.matchLiteral("@" + viewer.getName())
.replacement(Component.text("@" + viewer.getName(), NamedTextColor.YELLOW, TextDecoration.BOLD))
);
// Воспроизводим звук уведомления
FoliaUtil.runEntityTask(plugin, viewer, () -> {
viewer.playSound(viewer.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f);
});
return highlighted;
}
return message;
}
/**
* Обрабатывает URL в сообщении (делает кликабельными)
*/
private Component processUrls(Component message) { private Component processUrls(Component message) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message); String plainText = PlainTextComponentSerializer.plainText().serialize(message);
String urlPattern = "(https?://[\\w\\-._~:/?#\\[\\]@!35538117-c5a8-4bfe-b9f2-b0dcf9374222'()*+,;=%]+)";
// Простой regex для URL
String urlPattern = "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern);
java.util.regex.Matcher matcher = pattern.matcher(plainText); java.util.regex.Matcher matcher = pattern.matcher(plainText);
Component result = message; Component result = message;
while (matcher.find()) { while (matcher.find()) {
String url = matcher.group(1); String url = matcher.group(1);
// Делаем URL кликабельным с hover
Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED) Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY))) .hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
.clickEvent(ClickEvent.openUrl(url)); .clickEvent(ClickEvent.openUrl(url));
@ -193,35 +148,28 @@ public class ChatEventListener implements Listener {
return result; return result;
} }
/**
* Обрабатывает эмодзи шорткоды (:smile: -> ☺)
* Использует только MC-совместимые Unicode символы
*/
private Component processEmojis(Component message) { private Component processEmojis(Component message) {
Component result = message; Component result = message;
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("❤"));
// MC-совместимые Unicode эмодзи
result = result.replaceText(builder -> builder.matchLiteral(":smile:").replacement("☺"));
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("♥"));
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("※"));
result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★")); result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★"));
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✓")); result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✔"));
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✗")); result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✖"));
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→")); result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→"));
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪")); result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪"));
result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀")); result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀"));
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☾")); result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☽"));
result = result.replaceText(builder -> builder.matchLiteral(":peace:").replacement("☮"));
result = result.replaceText(builder -> builder.matchLiteral(":yin:").replacement("☯"));
result = result.replaceText(builder -> builder.matchLiteral(":flower:").replacement("✿"));
result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄")); result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄"));
result = result.replaceText(builder -> builder.matchLiteral(":lightning:").replacement("⚡")); result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("🔥"));
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("♔"));
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("⛨"));
result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆")); result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆"));
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
result = result.replaceText(builder -> builder.matchLiteral(":pickaxe:").replacement("⛏"));
result = result.replaceText(builder -> builder.matchLiteral(":bow:").replacement("🏹"));
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("🛡"));
result = result.replaceText(builder -> builder.matchLiteral(":potion:").replacement("🧪"));
result = result.replaceText(builder -> builder.matchLiteral(":book:").replacement("📖"));
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("👑"));
result = result.replaceText(builder -> builder.matchLiteral(":gem:").replacement("💎"));
return result; return result;
} }
} }

View file

@ -27,6 +27,10 @@ public class CustomCommandManager {
public CustomCommandManager(LoChat plugin) { public CustomCommandManager(LoChat plugin) {
this.plugin = plugin; this.plugin = plugin;
this.commands = new HashMap<>(); this.commands = new HashMap<>();
init();
}
private void init() {
loadCommands(); loadCommands();
} }

View file

@ -0,0 +1,97 @@
package com.loki.lochat.managers;
import com.loki.lochat.LoChat;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class EmojiManager {
private final LoChat plugin;
private final Map<String, String> emojis = new HashMap<>();
private boolean enabled;
private boolean requirePermission;
public EmojiManager(LoChat plugin) {
this.plugin = plugin;
load();
}
public void load() {
File file = new File(plugin.getDataFolder(), "emojis.yml");
if (!file.exists()) {
plugin.saveResource("emojis.yml", false);
}
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
emojis.clear();
// Настройки
enabled = config.getBoolean("settings.enabled", true);
requirePermission = config.getBoolean("settings.require-permission", false);
// Загружаем все категории
for (String category : config.getKeys(false)) {
if (category.equals("settings")) continue;
ConfigurationSection section = config.getConfigurationSection(category);
if (section != null) {
for (String key : section.getKeys(false)) {
String value = section.getString(key);
if (value != null) {
emojis.put(key, value);
}
}
}
}
plugin.getLogger().info("Загружено " + emojis.size() + " смайликов");
}
public void reload() {
load();
}
/**
* Заменяет смайлики в сообщении
*/
public String process(String message, Player player) {
if (!enabled) return message;
// Проверка права
if (requirePermission && player != null && !player.hasPermission("chat.emoji.use")) {
return message;
}
String result = message;
for (Map.Entry<String, String> entry : emojis.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
public String process(String message) {
if (!enabled) return message;
String result = message;
for (Map.Entry<String, String> entry : emojis.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
public Map<String, String> getEmojis() {
return emojis;
}
public boolean isEnabled() {
return enabled;
}
}

View file

@ -0,0 +1,106 @@
package com.loki.lochat.utils;
import com.loki.lochat.util.FoliaUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MentionHandler {
private static final Pattern EVERYONE_PATTERN = Pattern.compile("@everyone\\b");
private static final Pattern HERE_PATTERN = Pattern.compile("@here\\b");
private static final Pattern ROLE_PATTERN = Pattern.compile("@role:([\\w.]+)");
private static final Pattern PLAYER_PATTERN = Pattern.compile("@(\\w+)");
private final JavaPlugin plugin;
public MentionHandler(JavaPlugin plugin) {
this.plugin = plugin;
}
public Component processMentions(Component message, Player sender, Player viewer) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
List<Player> mentionedPlayers = new ArrayList<>();
if (sender.hasPermission("lochat.mention.everyone")) {
Matcher everyoneMatcher = EVERYONE_PATTERN.matcher(plainText);
if (everyoneMatcher.find()) {
mentionedPlayers.addAll(Bukkit.getOnlinePlayers());
message = highlightMention(message, "@everyone");
}
}
if (sender.hasPermission("lochat.mention.here")) {
Matcher hereMatcher = HERE_PATTERN.matcher(plainText);
if (hereMatcher.find()) {
mentionedPlayers.addAll(Bukkit.getOnlinePlayers());
message = highlightMention(message, "@here");
}
}
if (sender.hasPermission("lochat.mention.role")) {
Matcher roleMatcher = ROLE_PATTERN.matcher(plainText);
while (roleMatcher.find()) {
String permission = roleMatcher.group(1);
String fullMention = "@role:" + permission;
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
mentionedPlayers.add(player);
}
}
message = highlightMention(message, fullMention);
}
}
Matcher playerMatcher = PLAYER_PATTERN.matcher(plainText);
while (playerMatcher.find()) {
String playerName = playerMatcher.group(1);
Player mentioned = Bukkit.getPlayerExact(playerName);
if (mentioned != null && mentioned.isOnline()) {
mentionedPlayers.add(mentioned);
message = highlightMention(message, "@" + playerName);
}
}
if (mentionedPlayers.contains(viewer)) {
playMentionSound(viewer);
message = highlightForViewer(message, viewer);
}
return message;
}
private Component highlightMention(Component message, String mention) {
return message.replaceText(builder ->
builder.matchLiteral(mention)
.replacement(Component.text(mention, NamedTextColor.YELLOW, TextDecoration.BOLD))
);
}
private Component highlightForViewer(Component message, Player viewer) {
String viewerMention = "@" + viewer.getName();
return message.replaceText(builder ->
builder.matchLiteral(viewerMention)
.replacement(Component.text(viewerMention, NamedTextColor.GOLD, TextDecoration.BOLD))
);
}
private void playMentionSound(Player player) {
FoliaUtil.runEntityTask(plugin, player, () -> {
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f);
});
}
}

View file

@ -0,0 +1,95 @@
package com.loki.lochat.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextFormatter {
private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*");
private static final Pattern ITALIC_PATTERN = Pattern.compile("\\*(.+?)\\*");
private static final Pattern UNDERLINE_PATTERN = Pattern.compile("__(.+?)__");
private static final Pattern STRIKETHROUGH_PATTERN = Pattern.compile("~~(.+?)~~");
private static final Pattern CODE_PATTERN = Pattern.compile("`(.+?)`");
public static Component formatMarkdown(Component message) {
String text = PlainTextComponentSerializer.plainText().serialize(message);
text = formatBold(text);
text = formatItalic(text);
text = formatUnderline(text);
text = formatStrikethrough(text);
text = formatCode(text);
return ChatFormatter.parse(text);
}
private static String formatBold(String text) {
Matcher matcher = BOLD_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<bold>" + content + "</bold>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatItalic(String text) {
Matcher matcher = ITALIC_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
if (!content.startsWith("*") && !content.endsWith("*")) {
matcher.appendReplacement(result, "<italic>" + content + "</italic>");
}
}
matcher.appendTail(result);
return result.toString();
}
private static String formatUnderline(String text) {
Matcher matcher = UNDERLINE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<underlined>" + content + "</underlined>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatStrikethrough(String text) {
Matcher matcher = STRIKETHROUGH_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<strikethrough>" + content + "</strikethrough>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatCode(String text) {
Matcher matcher = CODE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "&#888888`" + content + "`");
}
matcher.appendTail(result);
return result.toString();
}
}

View file

@ -10,7 +10,7 @@
settings: settings:
# Префикс автосообщений # Префикс автосообщений
prefix: "<gradient:#00D4FF:#0099CC>[INFO]</gradient>" prefix: "&#00D4FF[&#00D4FFI&#00CAF7N&#00C0EEF&#00B7E6O&#0099CC]"
# Режим отправки: sequential (по порядку), random (случайно), custom (свой порядок) # Режим отправки: sequential (по порядку), random (случайно), custom (свой порядок)
mode: "sequential" mode: "sequential"

View file

@ -12,7 +12,7 @@ config-version: 3
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
# Префикс плагина в сообщениях # Префикс плагина в сообщениях
prefix: "<gradient:#00D4FF:#0099CC>[LoChat]</gradient>" prefix: "&#00D4FF[&#00D4FFL&#00CAF7o&#00C0EEC&#00B7E6h&#00ADDDa&#00A3D5t&#0099CC]"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ЧАТА │ # │ НАСТРОЙКИ ЧАТА │
@ -30,9 +30,8 @@ chat:
# Кулдаун между сообщениями (секунды) # Кулдаун между сообщениями (секунды)
cooldown: 3 cooldown: 3
# Префикс глобального чата # Префикс глобального чата
prefix: "<gradient:#FFD700:#FFA500>[G]</gradient>" prefix: "&#FFD700[G]"
# Формат сообщения: {prefix} {player} {message} format: "{prefix} &#00FF88{player}&#555555: {message}"
format: "{prefix} <gradient:#00FF88:#00CC66>{player}</gradient><dark_gray>:</dark_gray> {message}"
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# ЛОКАЛЬНЫЙ ЧАТ # ЛОКАЛЬНЫЙ ЧАТ
@ -45,9 +44,8 @@ chat:
# Кулдаун между сообщениями (секунды) # Кулдаун между сообщениями (секунды)
cooldown: 1 cooldown: 1
# Префикс локального чата # Префикс локального чата
prefix: "<gradient:#87CEEB:#4682B4>[L]</gradient>" prefix: "&#87CEEB[L]"
# Формат сообщения format: "{prefix} &#00FF88{player}&#555555: {message}"
format: "{prefix} <gradient:#00FF88:#00CC66>{player}</gradient><dark_gray>:</dark_gray> {message}"
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# ЛИЧНЫЕ СООБЩЕНИЯ # ЛИЧНЫЕ СООБЩЕНИЯ
@ -111,9 +109,8 @@ mentions:
# Тип звука # Тип звука
sound-type: "BLOCK_NOTE_BLOCK_PLING" sound-type: "BLOCK_NOTE_BLOCK_PLING"
# Подсветка упоминания в чате # Подсветка упоминания в чате
highlight: "<gradient:#FFFF00:#FFD700>@{player}</gradient>" highlight: "&#FFFF00@{player}"
# Подсветка собственного ника self-highlight: "&#FFD700{player}"
self-highlight: "<gradient:#FFD700:#FFA500>{player}</gradient>"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АВТОСООБЩЕНИЯ │ # │ АВТОСООБЩЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
@ -134,7 +131,7 @@ announcements:
# Показывать title при объявлении # Показывать title при объявлении
show-title: true show-title: true
# Заголовок title # Заголовок title
title-header: "<gradient:#FF6B6B:#FF4757>ОБЪЯВЛЕНИЕ</gradient>" title-header: "&#FF6B6BОБЪЯВЛЕНИЕ"
# Показывать actionbar при объявлении # Показывать actionbar при объявлении
show-actionbar: true show-actionbar: true
# Время показа title (в секундах) # Время показа title (в секундах)
@ -142,7 +139,7 @@ announcements:
# Формат объявления # Формат объявления
formats: formats:
announcement: "<gradient:#FF6B6B:#FF4757>[ОБЪЯВЛЕНИЕ]</gradient> <message>" announcement: "&#FF6B6B[ОБЪЯВЛЕНИЕ] <message>"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ЧАТА │ # │ ОЧИСТКА ЧАТА │
@ -153,7 +150,7 @@ clearchat:
# Показывать сообщение после очистки чата # Показывать сообщение после очистки чата
enabled: true enabled: true
# Текст сообщения ({player} - имя администратора) # Текст сообщения ({player} - имя администратора)
text: "<gradient:#FF6B6B:#FF4757>Чат очищен администратором {player}</gradient>" text: "&#FF6B6BЧат очищен администратором {player}"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГОЛОВЫ ИГРОКОВ │ # │ ГОЛОВЫ ИГРОКОВ │
@ -179,20 +176,14 @@ mute:
# %operator% - модератор # %operator% - модератор
messages: messages:
# Сообщение при муте (broadcast) muted: "&#FF6B6B%player% &#888888был замучен на &#FF6B6B%duration% &#888888модератором &#FF6B6B%operator%&#888888. Причина: &#FF6B6B%reason%"
muted: "<gradient:#FF6B6B:#FF4757>%player%</gradient> <gray>был замучен на</gray> <gradient:#FF6B6B:#FF4757>%duration%</gradient> <gray>модератором</gray> <gradient:#FF6B6B:#FF4757>%operator%</gradient><gray>. Причина:</gray> <gradient:#FF6B6B:#FF4757>%reason%</gradient>" unmuted: "&#6BCB77%player% &#888888был размучен модератором &#6BCB77%operator%"
# Сообщение при размуте (broadcast) you-muted: "&#FF6B6BВы замучены на %duration%! Причина: %reason%"
unmuted: "<gradient:#6BCB77:#4ECDC4>%player%</gradient> <gray>был размучен модератором</gray> <gradient:#6BCB77:#4ECDC4>%operator%</gradient>" you-muted-permanent: "&#FF6B6BВы замучены навсегда! Причина: %reason%"
# Сообщение замученному игроку cannot-chat: "&#FF6B6BВы замучены! Осталось: %duration%"
you-muted: "<gradient:#FF6B6B:#FF4757>Вы замучены на %duration%! Причина: %reason%</gradient>" cannot-chat-permanent: "&#FF6B6BВы замучены навсегда!"
# Сообщение при перманентном муте silent-muted: "&#888888[Тихо] &#FF6B6B%player% &#888888замучен на &#FF6B6B%duration% &#888888(%operator%). Причина: %reason%"
you-muted-permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда! Причина: %reason%</gradient>" silent-unmuted: "&#888888[Тихо] &#6BCB77%player% &#888888размучен (%operator%)"
# Сообщение при попытке написать в чат
cannot-chat: "<gradient:#FF6B6B:#FF4757>Вы замучены! Осталось: %duration%</gradient>"
cannot-chat-permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда!</gradient>"
# Тихий мут (только для тех кто видит)
silent-muted: "<gray>[Тихо]</gray> <gradient:#FF6B6B:#FF4757>%player%</gradient> <gray>замучен на</gray> <gradient:#FF6B6B:#FF4757>%duration%</gradient> <gray>(%operator%). Причина: %reason%</gray>"
silent-unmuted: "<gray>[Тихо]</gray> <gradient:#6BCB77:#4ECDC4>%player%</gradient> <gray>размучен (%operator%)</gray>"
# Причина по умолчанию если не указана # Причина по умолчанию если не указана
default-reason: "Нарушение правил" default-reason: "Нарушение правил"
@ -213,3 +204,45 @@ mute:
- "14d" - "14d"
- "30d" - "30d"
- "perm" - "perm"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФИЛЬТРЫ СООБЩЕНИЙ │
# └─────────────────────────────────────────────────────────────────────────────┘
filters:
url:
enabled: true
whitelist:
- "youtube.com"
- "youtu.be"
- "discord.gg"
- "twitch.tv"
blacklist:
- "scam-site.com"
blocked-message: "&#FF6B6BС&#FF5F5Fс&#FF5353ы&#FF4747л&#FF3B3Bк&#FF2F2Fи &#FF2323з&#FF1717а&#FF0B0Bп&#FF0000р&#FF0000е&#FF0000щ&#FF0000е&#FF0000н&#FF0000ы!"
ip:
enabled: true
block: false
replacement: "[IP скрыт]"
blocked-message: "&#FF6B6BI&#FF5F5FP &#FF5353а&#FF4747д&#FF3B3Bр&#FF2F2Fе&#FF2323с&#FF1717а &#FF0B0Bз&#FF0000а&#FF0000п&#FF0000р&#FF0000е&#FF0000щ&#FF0000е&#FF0000н&#FF0000ы!"
repeat:
enabled: true
max-repeats: 3
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ УПОМИНАНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
advanced-mentions:
enabled: true
everyone:
enabled: true
permission: "lochat.mention.everyone"
here:
enabled: true
permission: "lochat.mention.here"
role:
enabled: true
permission: "lochat.mention.role"

View file

@ -10,7 +10,7 @@
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
# Префикс градиентного модуля # Префикс градиентного модуля
prefix: "<gradient:#9370DB:#8A2BE2>[Gradient]</gradient>" prefix: "&#9370DB[&#9370DBG&#8E6CD9r&#896AD7a&#8467D5d&#7F65D3i&#7A62D1e&#7560CFn&#705DCDt&#6B5BCB]"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КОМАНДА /color │ # │ КОМАНДА /color │

View file

@ -9,23 +9,23 @@
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
# Префикс плагина # Префикс плагина
prefix: "<gradient:#00D4FF:#0099CC>[LoChat]</gradient>" prefix: "&#00D4FF[&#00D4FFL&#00CAF7o&#00C0EEC&#00B7E6h&#00ADDDa&#00A3D5t&#0099CC]"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГЛОБАЛЬНЫЙ ЧАТ │ # │ ГЛОБАЛЬНЫЙ ЧАТ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
global: global:
disabled: "<gradient:#FF6B6B:#FF4757>Глобальный чат отключен</gradient>" disabled: "&#FF6B6BГ&#FF5F5Fл&#FF5353о&#FF4747б&#FF3B3Bа&#FF2F2Fл&#FF2323ь&#FF1717н&#FF0B0Bы&#FF0000й &#FF0000ч&#FF0000а&#FF0000т &#FF0000о&#FF0000т&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
toggled-on: "<gradient:#6BCB77:#4ECDC4>Глобальный чат включен</gradient>" toggled-on: "&#6BCB77Г&#62C973л&#5AC76Fо&#51C56Bб&#49C367а&#40C163л&#38BF5Fь&#2FBD5Bн&#27BB57ы&#1EB953й &#16B74F ч&#0DB54Bа&#05B347т &#05B347в&#05B347к&#05B347л&#05B347ю&#05B347ч&#05B347е&#05B347н"
toggled-off: "<gradient:#FF6B6B:#FF4757>Глобальный чат отключен</gradient>" toggled-off: "&#FF6B6BГ&#FF5F5Fл&#FF5353о&#FF4747б&#FF3B3Bа&#FF2F2Fл&#FF2323ь&#FF1717н&#FF0B0Bы&#FF0000й &#FF0000ч&#FF0000а&#FF0000т &#FF0000о&#FF0000т&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛОКАЛЬНЫЙ ЧАТ │ # │ ЛОКАЛЬНЫЙ ЧАТ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
local: local:
no-players: "<gradient:#FFA726:#FF9800>Рядом нет игроков</gradient>" no-players: "&#FFA726Р&#FFA322я&#FF9F1Eд&#FF9B1Aо&#FF9716м &#FF9312н&#FF8F0Eе&#FF8B0Aт &#FF8706и&#FF8302г&#FF8000р&#FF8000о&#FF8000к&#FF8000о&#FF8000в"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛИЧНЫЕ СООБЩЕНИЯ │ # │ ЛИЧНЫЕ СООБЩЕНИЯ │
@ -47,36 +47,36 @@ pm:
received: "<gray>[PM]</gray> <white>{player}</white> <gray>→</gray> <gradient:#87CEEB:#4682B4>Ты</gradient><gray>:</gray> {message}" received: "<gray>[PM]</gray> <white>{player}</white> <gray>→</gray> <gradient:#87CEEB:#4682B4>Ты</gradient><gray>:</gray> {message}"
# Ошибки PM # Ошибки PM
no-reply: "<gradient:#FF6B6B:#FF4757>Некому ответить</gradient>" no-reply: "&#FF6B6BН&#FF5F5Fе&#FF5353к&#FF4747о&#FF3B3Bм&#FF2F2Fу &#FF2323о&#FF1717т&#FF0B0Bв&#FF0000е&#FF0000т&#FF0000и&#FF0000т&#FF0000ь"
ignored: "<gradient:#FF6B6B:#FF4757>Этот игрок вас игнорирует</gradient>" ignored: "&#FF6B6BЭ&#FF5F5Fт&#FF5353о&#FF4747т &#FF3B3Bи&#FF2F2Fг&#FF2323р&#FF1717о&#FF0B0Bк &#FF0000в&#FF0000а&#FF0000с &#FF0000и&#FF0000г&#FF0000н&#FF0000о&#FF0000р&#FF0000и&#FF0000р&#FF0000у&#FF0000е&#FF0000т"
player-offline: "<gradient:#FF6B6B:#FF4757>Игрок не в сети</gradient>" player-offline: "&#FF6B6BИ&#FF5F5Fг&#FF5353р&#FF4747о&#FF3B3Bк &#FF2F2Fн&#FF2323е &#FF1717в &#FF0B0Bс&#FF0000е&#FF0000т&#FF0000и"
self: "<gradient:#FF6B6B:#FF4757>Нельзя писать самому себе</gradient>" self: "&#FF6B6BН&#FF5F5Fе&#FF5353л&#FF4747ь&#FF3B3Bз&#FF2F2Fя &#FF2323п&#FF1717и&#FF0B0Bс&#FF0000а&#FF0000т&#FF0000ь &#FF0000с&#FF0000а&#FF0000м&#FF0000о&#FF0000м&#FF0000у &#FF0000с&#FF0000е&#FF0000б&#FF0000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ИГНОРИРОВАНИЕ │ # │ ИГНОРИРОВАНИЕ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
ignore: ignore:
added: "<gradient:#6BCB77:#4ECDC4>Игрок {player} добавлен в игнор</gradient>" added: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347д&#05B347о&#05B347б&#05B347а&#05B347в&#05B347л&#05B347е&#05B347н &#05B347в &#05B347и&#05B347г&#05B347н&#05B347о&#05B347р"
removed: "<gradient:#6BCB77:#4ECDC4>Игрок {player} убран из игнора</gradient>" removed: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347у&#05B347б&#05B347р&#05B347а&#05B347н &#05B347и&#05B347з &#05B347и&#05B347г&#05B347н&#05B347о&#05B347р&#05B347а"
already-ignored: "<gradient:#FFA726:#FF9800>Игрок уже в игноре</gradient>" already-ignored: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312у&#FF8F0Eж&#FF8B0Aе &#FF8706в &#FF8302и&#FF8000г&#FF8000н&#FF8000о&#FF8000р&#FF8000е"
not-ignored: "<gradient:#FFA726:#FF9800>Игрок не в игноре</gradient>" not-ignored: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312н&#FF8F0Eе &#FF8B0Aв &#FF8706и&#FF8302г&#FF8000н&#FF8000о&#FF8000р&#FF8000е"
self: "<gradient:#FF6B6B:#FF4757>Нельзя игнорировать себя</gradient>" self: "&#FF6B6BН&#FF5F5Fе&#FF5353л&#FF4747ь&#FF3B3Bз&#FF2F2Fя &#FF2323и&#FF1717г&#FF0B0Bн&#FF0000о&#FF0000р&#FF0000и&#FF0000р&#FF0000о&#FF0000в&#FF0000а&#FF0000т&#FF0000ь &#FF0000с&#FF0000е&#FF0000б&#FF0000я"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОБЪЯВЛЕНИЯ │ # │ ОБЪЯВЛЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
announce: announce:
reloaded: "<gradient:#6BCB77:#4ECDC4>Конфигурация перезагружена</gradient>" reloaded: "&#6BCB77К&#62C973о&#5AC76Fн&#51C56Bф&#49C367и&#40C163г&#38BF5Fу&#2FBD5Bр&#27BB57а&#1EB953ц&#16B74Fи&#0DB54Bя &#05B347п&#05B347е&#05B347р&#05B347е&#05B347з&#05B347а&#05B347г&#05B347р&#05B347у&#05B347ж&#05B347е&#05B347н&#05B347а"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ШПИОН РЕЖИМ │ # │ ШПИОН РЕЖИМ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
spy: spy:
enabled: "<gradient:#6BCB77:#4ECDC4>Шпион режим включен</gradient>" enabled: "&#6BCB77Ш&#62C973п&#5AC76Fи&#51C56Bо&#49C367н &#40C163р&#38BF5Fе&#2FBD5Bж&#27BB57и&#1EB953м &#16B74Fв&#0DB54Bк&#05B347л&#05B347ю&#05B347ч&#05B347е&#05B347н"
disabled: "<gradient:#FF6B6B:#FF4757>Шпион режим выключен</gradient>" disabled: "&#FF6B6BШ&#FF5F5Fп&#FF5353и&#FF4747о&#FF3B3Bн &#FF2F2Fр&#FF2323е&#FF1717ж&#FF0B0Bи&#FF0000м &#FF0000в&#FF0000ы&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
format: "<gray>[SPY]</gray> <gradient:#FFD700:#FFA500>{sender}</gradient> <gray>→</gray> <gradient:#87CEEB:#4682B4>{receiver}</gradient><gray>:</gray> <white>{message}</white>" format: "<gray>[SPY]</gray> <gradient:#FFD700:#FFA500>{sender}</gradient> <gray>→</gray> <gradient:#87CEEB:#4682B4>{receiver}</gradient><gray>:</gray> <white>{message}</white>"
chat-format: "<gray>[SPY]</gray> <gradient:#FFA726:#FF9800>{type}</gradient> <gradient:#00FF88:#00CC66>{sender}</gradient><gray>:</gray> <white>{message}</white>" chat-format: "<gray>[SPY]</gray> <gradient:#FFA726:#FF9800>{type}</gradient> <gradient:#00FF88:#00CC66>{sender}</gradient><gray>:</gray> <white>{message}</white>"
@ -85,49 +85,49 @@ spy:
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
filter: filter:
blocked: "<gradient:#FF6B6B:#FF4757>Сообщение заблокировано (недопустимые слова)</gradient>" blocked: "&#FF6B6BС&#FF5F5Fо&#FF5353о&#FF4747б&#FF3B3Bщ&#FF2F2Fе&#FF2323н&#FF1717и&#FF0B0Bе &#FF0000з&#FF0000а&#FF0000б&#FF0000л&#FF0000о&#FF0000к&#FF0000и&#FF0000р&#FF0000о&#FF0000в&#FF0000а&#FF0000н&#FF0000о"
warning: "<gradient:#FFA726:#FF9800>Предупреждение: не используйте недопустимые слова!</gradient>" warning: "&#FFA726П&#FFA322р&#FF9F1Eе&#FF9B1Aд&#FF9716у&#FF9312п&#FF8F0Eр&#FF8B0Aе&#FF8706ж&#FF8302д&#FF8000е&#FF8000н&#FF8000и&#FF8000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КУЛДАУНЫ │ # │ КУЛДАУНЫ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
cooldown: cooldown:
wait: "<gradient:#FFA726:#FF9800>Подождите {time} сек перед следующим сообщением</gradient>" wait: "&#FFA726П&#FFA322о&#FF9F1Eд&#FF9B1Aо&#FF9716ж&#FF9312д&#FF8F0Eи&#FF8B0Aт&#FF8706е &#FF8302{&#FF8000t&#FF8000i&#FF8000m&#FF8000e&#FF8000}"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АНТИ-СПАМ │ # │ АНТИ-СПАМ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
antispam: antispam:
caps: "<gradient:#FF6B6B:#FF4757>Слишком много заглавных букв!</gradient>" caps: "&#FF6B6BС&#FF5F5Fл&#FF5353и&#FF4747ш&#FF3B3Bк&#FF2F2Fо&#FF2323м &#FF1717м&#FF0B0Bн&#FF0000о&#FF0000г&#FF0000о &#FF0000з&#FF0000а&#FF0000г&#FF0000л&#FF0000а&#FF0000в&#FF0000н&#FF0000ы&#FF0000х"
repeat: "<gradient:#FF6B6B:#FF4757>Не повторяйте символы!</gradient>" repeat: "&#FF6B6BН&#FF5F5Fе &#FF5353п&#FF4747о&#FF3B3Bв&#FF2F2Fт&#FF2323о&#FF1717р&#FF0B0Bя&#FF0000й&#FF0000т&#FF0000е &#FF0000с&#FF0000и&#FF0000м&#FF0000в&#FF0000о&#FF0000л&#FF0000ы"
similar: "<gradient:#FF6B6B:#FF4757>Не отправляйте одинаковые сообщения!</gradient>" similar: "&#FF6B6BН&#FF5F5Fе &#FF5353о&#FF4747т&#FF3B3Bп&#FF2F2Fр&#FF2323а&#FF1717в&#FF0B0Bл&#FF0000я&#FF0000й&#FF0000т&#FF0000е &#FF0000о&#FF0000д&#FF0000и&#FF0000н&#FF0000а&#FF0000к&#FF0000о&#FF0000в&#FF0000ы&#FF0000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ЧАТА │ # │ ОЧИСТКА ЧАТА │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
clearchat: clearchat:
cleared: "<gradient:#6BCB77:#4ECDC4>Чат очищен администратором {player}</gradient>" cleared: "&#6BCB77Ч&#62C973а&#5AC76Fт &#51C56Bо&#49C367ч&#40C163и&#38BF5Fщ&#2FBD5Bе&#27BB57н &#1EB953{&#16B74Fp&#0DB54Bl&#05B347a&#05B347y&#05B347e&#05B347r&#05B347}"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОШИБКИ │ # │ ОШИБКИ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
errors: errors:
no-permission: "<gradient:#FF6B6B:#FF4757>Нет прав!</gradient>" no-permission: "&#FF6B6BН&#FF5F5Fе&#FF5353т &#FF4747п&#FF3B3Bр&#FF2F2Fа&#FF2323в&#FF1717!"
player-not-found: "<gradient:#FF6B6B:#FF4757>Игрок не найден</gradient>" player-not-found: "&#FF6B6BИ&#FF5F5Fг&#FF5353р&#FF4747о&#FF3B3Bк &#FF2F2Fн&#FF2323е &#FF1717н&#FF0B0Bа&#FF0000й&#FF0000д&#FF0000е&#FF0000н"
invalid-usage: "<gradient:#FFA726:#FF9800>Использование: {usage}</gradient>" invalid-usage: "&#FFA726И&#FFA322с&#FF9F1Eп&#FF9B1Aо&#FF9716л&#FF9312ь&#FF8F0Eз&#FF8B0Aо&#FF8706в&#FF8302а&#FF8000н&#FF8000и&#FF8000е&#FF8000: &#FF8000{&#FF8000u&#FF8000s&#FF8000a&#FF8000g&#FF8000e&#FF8000}"
# ┌─────────────────────────────────────────────────────────────────────────────┐ # ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СИСТЕМА МУТОВ │ # │ СИСТЕМА МУТОВ │
# └─────────────────────────────────────────────────────────────────────────────┘ # └─────────────────────────────────────────────────────────────────────────────┘
mute: mute:
muted: "<gradient:#6BCB77:#4ECDC4>Игрок {player} замучен на {time}</gradient>" muted: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347з&#05B347а&#05B347м&#05B347у&#05B347ч&#05B347е&#05B347н &#05B347н&#05B347а &#05B347{&#05B347t&#05B347i&#05B347m&#05B347e&#05B347}"
unmuted: "<gradient:#6BCB77:#4ECDC4>Игрок {player} размучен</gradient>" unmuted: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347р&#05B347а&#05B347з&#05B347м&#05B347у&#05B347ч&#05B347е&#05B347н"
already-muted: "<gradient:#FFA726:#FF9800>Игрок уже замучен</gradient>" already-muted: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312у&#FF8F0Eж&#FF8B0Aе &#FF8706з&#FF8302а&#FF8000м&#FF8000у&#FF8000ч&#FF8000е&#FF8000н"
not-muted: "<gradient:#FFA726:#FF9800>Игрок не замучен</gradient>" not-muted: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312н&#FF8F0Eе &#FF8B0Aз&#FF8706а&#FF8302м&#FF8000у&#FF8000ч&#FF8000е&#FF8000н"
you-muted: "<gradient:#FF6B6B:#FF4757>Вы замучены! Осталось: {time}</gradient>" you-muted: "&#FF6B6BВ&#FF5F5Fы &#FF5353з&#FF4747а&#FF3B3Bм&#FF2F2Fу&#FF2323ч&#FF1717е&#FF0B0Bн&#FF0000ы&#FF0000! &#FF0000О&#FF0000с&#FF0000т&#FF0000а&#FF0000л&#FF0000о&#FF0000с&#FF0000ь&#FF0000: &#FF0000{&#FF0000t&#FF0000i&#FF0000m&#FF0000e&#FF0000}"
permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда!</gradient>" permanent: "&#FF6B6BВ&#FF5F5Fы &#FF5353з&#FF4747а&#FF3B3Bм&#FF2F2Fу&#FF2323ч&#FF1717е&#FF0B0Bн&#FF0000ы &#FF0000н&#FF0000а&#FF0000в&#FF0000с&#FF0000е&#FF0000г&#FF0000д&#FF0000а&#FF0000!"

View file

@ -241,3 +241,18 @@ permissions:
lochat.nick: lochat.nick:
description: Can use custom nicknames description: Can use custom nicknames
default: true default: true
lochat.mention.everyone:
description: Can mention @everyone
default: op
lochat.mention.here:
description: Can mention @here
default: op
lochat.mention.role:
description: Can mention @role:<permission>
default: op
lochat.bypass.urlfilter:
description: Bypass URL filter
default: op
lochat.bypass.ipfilter:
description: Bypass IP filter
default: op