diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 5697b94..5400a79 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.DS_Store -/build \ No newline at end of file +/build +.idea +bin \ No newline at end of file diff --git a/.gradle/9.0.0/checksums/checksums.lock b/.gradle/9.0.0/checksums/checksums.lock index b9d96cc..2166cd9 100644 Binary files a/.gradle/9.0.0/checksums/checksums.lock and b/.gradle/9.0.0/checksums/checksums.lock differ diff --git a/.gradle/9.0.0/checksums/md5-checksums.bin b/.gradle/9.0.0/checksums/md5-checksums.bin index 4affa5a..cf5df98 100644 Binary files a/.gradle/9.0.0/checksums/md5-checksums.bin and b/.gradle/9.0.0/checksums/md5-checksums.bin differ diff --git a/.gradle/9.0.0/checksums/sha1-checksums.bin b/.gradle/9.0.0/checksums/sha1-checksums.bin index aac8d91..342fb2c 100644 Binary files a/.gradle/9.0.0/checksums/sha1-checksums.bin and b/.gradle/9.0.0/checksums/sha1-checksums.bin differ diff --git a/.gradle/9.0.0/executionHistory/executionHistory.bin b/.gradle/9.0.0/executionHistory/executionHistory.bin index 575db80..8ed04a8 100644 Binary files a/.gradle/9.0.0/executionHistory/executionHistory.bin and b/.gradle/9.0.0/executionHistory/executionHistory.bin differ diff --git a/.gradle/9.0.0/executionHistory/executionHistory.lock b/.gradle/9.0.0/executionHistory/executionHistory.lock index e39881b..83a130f 100644 Binary files a/.gradle/9.0.0/executionHistory/executionHistory.lock and b/.gradle/9.0.0/executionHistory/executionHistory.lock differ diff --git a/.gradle/9.0.0/fileHashes/fileHashes.bin b/.gradle/9.0.0/fileHashes/fileHashes.bin index c939746..6616e34 100644 Binary files a/.gradle/9.0.0/fileHashes/fileHashes.bin and b/.gradle/9.0.0/fileHashes/fileHashes.bin differ diff --git a/.gradle/9.0.0/fileHashes/fileHashes.lock b/.gradle/9.0.0/fileHashes/fileHashes.lock index b86b994..fe0c161 100644 Binary files a/.gradle/9.0.0/fileHashes/fileHashes.lock and b/.gradle/9.0.0/fileHashes/fileHashes.lock differ diff --git a/.gradle/9.0.0/fileHashes/resourceHashesCache.bin b/.gradle/9.0.0/fileHashes/resourceHashesCache.bin index ec5b7f4..eddab94 100644 Binary files a/.gradle/9.0.0/fileHashes/resourceHashesCache.bin and b/.gradle/9.0.0/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/9.2.1/checksums/checksums.lock b/.gradle/9.2.1/checksums/checksums.lock deleted file mode 100644 index 675c9c1..0000000 Binary files a/.gradle/9.2.1/checksums/checksums.lock and /dev/null differ diff --git a/.gradle/9.2.1/executionHistory/executionHistory.bin b/.gradle/9.2.1/executionHistory/executionHistory.bin deleted file mode 100644 index fa8e2da..0000000 Binary files a/.gradle/9.2.1/executionHistory/executionHistory.bin and /dev/null differ diff --git a/.gradle/9.2.1/executionHistory/executionHistory.lock b/.gradle/9.2.1/executionHistory/executionHistory.lock deleted file mode 100644 index d345fe9..0000000 Binary files a/.gradle/9.2.1/executionHistory/executionHistory.lock and /dev/null differ diff --git a/.gradle/9.2.1/fileChanges/last-build.bin b/.gradle/9.2.1/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/.gradle/9.2.1/fileChanges/last-build.bin and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/fileHashes.bin b/.gradle/9.2.1/fileHashes/fileHashes.bin deleted file mode 100644 index 1706964..0000000 Binary files a/.gradle/9.2.1/fileHashes/fileHashes.bin and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/fileHashes.lock b/.gradle/9.2.1/fileHashes/fileHashes.lock deleted file mode 100644 index b3373fe..0000000 Binary files a/.gradle/9.2.1/fileHashes/fileHashes.lock and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/resourceHashesCache.bin b/.gradle/9.2.1/fileHashes/resourceHashesCache.bin deleted file mode 100644 index 639fd98..0000000 Binary files a/.gradle/9.2.1/fileHashes/resourceHashesCache.bin and /dev/null differ diff --git a/.gradle/9.2.1/gc.properties b/.gradle/9.2.1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index dcd81e7..51243fe 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties index 0d457f8..68dfd29 100644 --- a/.gradle/buildOutputCleanup/cache.properties +++ b/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Fri Jan 16 17:25:37 NOVT 2026 +#Fri Jan 09 22:24:47 NOVT 2026 gradle.version=9.0.0 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index f4525b2..af4c59a 100644 Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and b/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index e916975..bd9c370 100644 Binary files a/.gradle/file-system.probe and b/.gradle/file-system.probe differ diff --git a/.gradle/nb-cache/AdminMode-1632350505/project-info.ser b/.gradle/nb-cache/AdminMode-1632350505/project-info.ser deleted file mode 100644 index 7cd7358..0000000 Binary files a/.gradle/nb-cache/AdminMode-1632350505/project-info.ser and /dev/null differ diff --git a/.gradle/nb-cache/subprojects.ser b/.gradle/nb-cache/subprojects.ser deleted file mode 100644 index d99aa50..0000000 Binary files a/.gradle/nb-cache/subprojects.ser and /dev/null differ diff --git a/.gradle/nb-cache/trust/F67B3012358146C8F2D19F069E2295F6FA6FA2C78DAF19CD6F43A89A11C6725E b/.gradle/nb-cache/trust/F67B3012358146C8F2D19F069E2295F6FA6FA2C78DAF19CD6F43A89A11C6725E deleted file mode 100644 index 0a136f6..0000000 --- a/.gradle/nb-cache/trust/F67B3012358146C8F2D19F069E2295F6FA6FA2C78DAF19CD6F43A89A11C6725E +++ /dev/null @@ -1 +0,0 @@ -A0EDDFD9DCFA42B28E98A596AFD8237AB506F0D6CE2E10D64BEA80C21CB3949A diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/compiler.xml b/.idea/compiler.xml old mode 100644 new mode 100755 diff --git a/.idea/easycode.ignore b/.idea/easycode.ignore old mode 100644 new mode 100755 diff --git a/.idea/gradle.xml b/.idea/gradle.xml old mode 100644 new mode 100755 diff --git a/.idea/misc.xml b/.idea/misc.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100644 new mode 100755 diff --git a/.idea/modules/AdminMode.main.iml b/.idea/modules/AdminMode.main.iml old mode 100644 new mode 100755 index bbeeb3e..a376b96 --- a/.idea/modules/AdminMode.main.iml +++ b/.idea/modules/AdminMode.main.iml @@ -4,7 +4,6 @@ - PAPER ADVENTURE 1 diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100755 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LUCKPERMS_SETUP.md b/LUCKPERMS_SETUP.md new file mode 100644 index 0000000..fc06363 --- /dev/null +++ b/LUCKPERMS_SETUP.md @@ -0,0 +1,234 @@ +# Настройка LuckPerms для AdminMode + +Эта инструкция рассчитана на LuckPerms 5.5.x и стандартный `config.yml`, который поставляется с AdminMode. + +## Как AdminMode использует LuckPerms + +LuckPerms является обязательной зависимостью: без него Paper не загрузит AdminMode. + +Плагин использует две разновидности групп: + +- `moder` — постоянная группа обычного модератора. Она разрешает включать AdminMode. +- `admin_moder` — временная группа. AdminMode сам добавляет её при входе в режим и удаляет при выходе. +- `chief_moder` — рекомендуемая постоянная группа главного модератора. Главный модератор определяется правом `adminmode.chief`, а не названием группы. + +Не назначайте `admin_moder` игрокам вручную как постоянную группу. Также не добавляйте постоянные группы в `admin_mode.luckperms.groups`: все перечисленные там группы плагин удаляет у игрока при выходе из AdminMode. + +Проверяйте настройку на игроке без OP. Права `adminmode.use`, `adminmode.admin`, `adminmode.chief` и `adminmode.reload` имеют значение `default: op`, поэтому оператор сервера получит их даже без настроенного LuckPerms. + +Права самого AdminMode: + +- `adminmode.use` — вход в режим и открытие панели. +- `adminmode.admin` — сильные действия, принудительное отключение режима и `/admfix`. +- `adminmode.chief` — управление доступом других модераторов и Creative в режиме, если он разрешён конфигом. +- `adminmode.reload` — перезагрузка конфига AdminMode. +- `adminmode.special.belzebool` — специальная способность, которая дополнительно привязана в коде к нику `BelzeBool`; обычным группам её выдавать не нужно. + +## 1. Установка LuckPerms + +1. Скачайте Bukkit-версию LuckPerms 5.5.x. +2. Поместите JAR в папку `plugins/` рядом с AdminMode. +3. Полностью перезапустите сервер. +4. Проверьте из консоли: + +```text +lp info +``` + +Команды ниже можно выполнять в консоли без `/` или в игре от имени OP с `/`. + +Если удобнее работать через браузер, команда `lp editor` открывает официальный веб-редактор LuckPerms. После сохранения изменений редактор выдаст команду, которую нужно выполнить на сервере для применения данных. Команды из следующих разделов дают тот же результат без веб-редактора. + +## 2. Создание групп + +Выполните: + +```text +lp creategroup moder +lp creategroup admin_moder +lp creategroup chief_moder +``` + +Если LuckPerms сообщает, что группа уже существует, это не ошибка — переходите дальше. + +## 3. Права обычного модератора + +Выдайте постоянной группе `moder` право входить в режим: + +```text +lp group moder permission set adminmode.use true +``` + +Обычный модератор сможет использовать `/adminmode`, `/adm` и `/am`, но не сможет ограничивать других модераторов или выполнять административные команды повышенного риска. + +## 4. Права, доступные только внутри AdminMode + +Группа `admin_moder` автоматически появляется у игрока после включения режима. Рекомендуемый минимальный набор: + +```text +lp group admin_moder permission set adminmode.admin true +``` + +`adminmode.admin` разрешает сильные действия AdminMode, включая принудительное отключение режима другого игрока и `/admfix`. + +Если сильные действия обычным модераторам не нужны, не выдавайте этот узел. Вход и выход из режима продолжат работать по `adminmode.use`. + +Права на кнопки наказаний выдаются отдельно. Пример полного набора для временной группы: + +```text +lp group admin_moder permission set adminmode.punishment.warn true +lp group admin_moder permission set adminmode.punishment.unwarn true +lp group admin_moder permission set adminmode.punishment.mute true +lp group admin_moder permission set adminmode.punishment.unmute true +lp group admin_moder permission set adminmode.punishment.kick true +lp group admin_moder permission set adminmode.punishment.ban true +lp group admin_moder permission set adminmode.punishment.unban true +``` + +Выдавайте только нужные узлы. `adminmode.punishment.ban` и `adminmode.punishment.unban` позволяют менять доступ игроков к серверу и рекомендуются только доверенным ролям. AdminMode проверяет эти права сам, в том числе когда внешняя команда выполняется от консоли. + +### CoreProtect + +Если установлен CoreProtect и модераторы должны пользоваться его инструментами из панели AdminMode, добавьте необходимые права в временную группу: + +```text +lp group admin_moder permission set coreprotect.inspect true +lp group admin_moder permission set coreprotect.lookup true +``` + +Rollback и restore меняют мир, поэтому выдавайте их только доверенным сотрудникам: + +```text +lp group admin_moder permission set coreprotect.rollback true +lp group admin_moder permission set coreprotect.restore true +``` + +Не используйте `coreprotect.*`, если модератору не нужен полный доступ к CoreProtect. + +### InvSee и команды наказаний + +AdminMode вызывает команду `invsee` от имени модератора, поэтому право на неё зависит от установленного у вас плагина инвентарей. Добавьте соответствующий permission этого плагина в `admin_moder`. + +По умолчанию команды warn, mute, ban и kick выполняются от консоли: + +```yaml +moderation_actions: + dispatch_as_console: true +``` + +При такой настройке отдельные права на команды наказаний модератору не нужны. Если установить `dispatch_as_console: false`, эти команды начнут выполняться от имени игрока, и их permissions также потребуется выдать группе `admin_moder`. + +Чтобы узнать неизвестное право конкретной команды, используйте LuckPerms verbose: + +```text +lp verbose command <ник_модератора> "invsee <ник_цели>" +``` + +## 5. Главный модератор + +Пусть `chief_moder` наследует обычную группу и получает специальные права: + +```text +lp group chief_moder parent add moder +lp group chief_moder permission set adminmode.chief true +lp group chief_moder permission set adminmode.reload true +``` + +- `adminmode.chief` разрешает ограничивать и восстанавливать доступ других модераторов, смотреть статус ограничений и использовать Creative внутри AdminMode, если это разрешено конфигом. +- `adminmode.reload` разрешает `/adminmode reload`. + +Не добавляйте `admin_moder` как родителя `chief_moder`, иначе временные права будут действовать у главного модератора постоянно. + +## 6. Назначение игроков + +Обычный модератор: + +```text +lp user <ник> parent add moder +``` + +Главный модератор: + +```text +lp user <ник> parent add chief_moder +``` + +Используется `parent add`, а не `parent set`, чтобы случайно не удалить другие существующие группы игрока. + +Примеры: + +```text +lp user Steve parent add moder +lp user Alex parent add chief_moder +``` + +## 7. Конфигурация AdminMode + +Проверьте файл `plugins/AdminMode/config.yml`: + +```yaml +admin_mode: + luckperms: + enabled: true + groups: + - "admin_moder" + remove_on_disable: true + +groups: + base: "moder" + admin: "admin_moder" + chief: "chief_moder" +``` + +Рекомендации: + +- Оставьте `enabled: true`. +- Оставьте `remove_on_disable: true`, чтобы временные группы снимались при выходе игрока из режима. +- Значение `groups.admin` должно совпадать с основной временной группой. +- В `admin_mode.luckperms.groups` перечисляйте только группы, которые должны выдаваться временно. +- `groups.base` не назначает игроку группу автоматически. +- Название `groups.chief` само по себе не даёт полномочий: обязательно право `adminmode.chief`. + +После изменения конфига выполните: + +```text +adminmode reload +``` + +или перезапустите сервер. + +## 8. Проверка настройки + +Проверьте постоянную группу и право игрока: + +```text +lp user <ник> info +lp user <ник> permission check adminmode.use +``` + +Затем зайдите на сервер этим игроком: + +1. Выполните `/adminmode on`. +2. Проверьте группы командой `lp user <ник> parent info` — должна появиться `admin_moder`. +3. Выполните `/adminmode off`. +4. Снова проверьте группы — `admin_moder` должна исчезнуть, а `moder` или `chief_moder` остаться. + +## 9. Если временная группа не снялась + +Удалите её вручную: + +```text +lp user <ник> parent remove admin_moder +``` + +После аварийной остановки сервера AdminMode сохраняет активную сессию и продолжает её после следующего входа игрока. Обычно достаточно войти на сервер и корректно выполнить `/adminmode off`. + +Если команда недоступна, проверьте: + +```text +lp user <ник> permission check adminmode.use +lp user <ник> permission check adminmode.admin +lp user <ник> permission check adminmode.chief +``` + +Официальная документация: [LuckPerms Usage](https://luckperms.net/wiki/Usage), [Command Usage](https://luckperms.net/wiki/Command-Usage), [Verbose](https://luckperms.net/wiki/Verbose). diff --git a/MODERATION_PLUGIN_CONTRACT.md b/MODERATION_PLUGIN_CONTRACT.md new file mode 100644 index 0000000..77396bf --- /dev/null +++ b/MODERATION_PLUGIN_CONTRACT.md @@ -0,0 +1,149 @@ +# Контракт moderation-плагина для AdminMode + +Этот документ описывает минимальные команды и поведение отдельного moderation-плагина, необходимого для работы кнопок Mute, Unmute, Kick и Ban в AdminMode 1.2 на Paper 26.1.2. + +AdminMode выполняет команды от консоли и передаёт реального модератора отдельным аргументом `actor:<ник>`. Активные баны должны храниться только в стандартном Paper ProfileBanList, иначе BanList и кнопка разбана в AdminMode не увидят их. + +## Формат сроков + +Плагин обязан принимать как минимум: + +- `1h` — один час; +- `1d` — один день; +- `7d` — семь дней; +- `30d` — тридцать дней; +- `permanent` — бессрочно. + +Рекомендуется дополнительно принимать любое положительное значение в формате `<число>`. Нулевые, отрицательные, неизвестные и переполненные значения должны отклоняться без применения наказания. + +## Автор наказания + +При вызове из AdminMode команды выглядят так: + +```text +mute Steve 1h actor:Moderator флуд +ban Steve 7d actor:Moderator читы +kick Steve actor:Moderator нарушение правил +``` + +Правила обработки `actor:`: + +- учитывать аргумент только когда `CommandSender` является консолью; +- для команды игрока всегда записывать автором самого игрока; +- не разрешать игроку подменять автора через `actor:`; +- убрать `actor:<ник>` из сохраняемой причины. + +## Команды + +### Mute + +```text +/mute [actor:] +/unmute [actor:] [reason...] +``` + +Из AdminMode команда вызывается по полному Bukkit namespace `moderationcommands:unmute`, чтобы её не перехватывали FlectonePulse и другие плагины с собственной командой `/unmute`. Namespace предполагает, что имя плагина в его `plugin.yml` — `ModerationCommands`. + +Обязательное поведение: + +- разрешать мутить онлайн- и офлайн-игрока по UUID; +- хранить UUID, последнее имя, автора, причину, время выдачи и `expiresAt` между перезапусками; +- автоматически считать временный мут истёкшим; +- отменять сообщения через Paper `AsyncChatEvent`, пока мут активен; +- блокировать настраиваемый список чат-команд, минимум `/msg`, `/tell`, `/w`, `/whisper`, `/reply`, `/r` и `/me`; +- при попытке писать показывать причину и оставшийся срок; +- `/unmute` должен немедленно снимать активный мут; +- повторный mute обновляет существующее активное наказание, а не создаёт конфликтующие записи. + +Permissions: + +```text +moderation.command.mute +moderation.command.unmute +``` + +AdminMode вызывает `/mute` и `/unmute` из карточки игрока. Кнопка Unmute доступна по отдельному праву и передаёт причину, если модератор её указал. + +### Ban + +```text +/ban [actor:] +/unban [actor:] [reason...] +``` + +Обязательное поведение: + +- разрешать банить онлайн- и офлайн-профили; +- разрешать профиль до UUID, не создавать отдельные name-only баны; +- получать `ProfileBanList` через `Bukkit.getBanList(BanListType.PROFILE)`; +- добавлять бан через typed ProfileBanList с причиной, `Instant` окончания или `null` для `permanent`, а также фактическим автором; +- после добавления бана отключать игрока онлайн с сообщением о причине и сроке; +- `/unban` удаляет тот же профиль из Paper ProfileBanList; +- не использовать собственный кэш или базу как источник активного бана: AdminMode снимает бан напрямую через `BanEntry.remove()`. + +Permissions: + +```text +moderation.command.ban +moderation.command.unban +``` + +Если нужна история, её можно вести отдельным append-only журналом, но актуальный статус всегда нужно проверять в Paper ProfileBanList. + +### Kick + +```text +/kick [actor:] +``` + +Обязательное поведение: + +- команда работает только для игрока онлайн; +- отключает игрока Adventure-компонентом с причиной и именем модератора; +- не создаёт постоянное наказание; +- возвращает понятную ошибку, если игрок уже вышел. + +Permission: + +```text +moderation.command.kick +``` + +## Существующая команда Warn + +Warn остаётся в ядре сервера и должен принимать: + +```text +/warn +``` + +Минимально поддерживаемые сроки совпадают со списком выше, включая `permanent`. Источник истории, срока и статуса варнов — AdminMode (`plugins/AdminMode/warn-history.json`), поэтому отдельная команда `/unwarn` для интеграции не требуется. + +## Возвращаемый результат команд + +Все обработчики Bukkit-команд должны: + +- возвращать `true`, только если команда распознана и наказание принято; +- возвращать `false` при неверном синтаксисе, неизвестном сроке или невозможности применить действие; +- не изменять данные при ошибке валидации; +- корректно работать от консоли без OP/LuckPerms-проверки; +- проверять соответствующий permission при вызове игроком. + +AdminMode создаёт локальную запись warn только после успешного `dispatchCommand`. Для Mute/Ban/Kick состояние и сообщения об ошибках полностью отвечает moderation-плагин. + +## Настройка AdminMode + +Стандартные шаблоны `plugins/AdminMode/config.yml`: + +```yaml +moderation_actions: + dispatch_as_console: true + commands: + warn: "warn {target} {duration} {reason}" + mute: "mute {target} {duration} actor:{moderator} {reason}" + unmute: "moderationcommands:unmute {target} actor:{moderator} {reason}" + ban: "ban {target} {duration} actor:{moderator} {reason}" + kick: "kick {target} actor:{moderator} {reason}" +``` + +После установки moderation-плагина выполните `/adminmode reload` или перезапустите сервер, затем проверьте каждую команду сначала вручную из консоли и только потом через Dialog. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3d36c3 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# AdminMode + +Плагин режима адмистратора для Paper 26.1.2. + +## Описание + +AdminMode - это плагин, который позволяет модераторам и администраторам входить в специальный режим с сохранением их текущего состояния. При активации режима администратора: + +- Сохраняется инвентарь, броня, предметы +- Сохраняется позиция игрока +- Сохраняется здоровье, голод, опыт +- Сохраняются эффекты зелий +- Выдаются временные права через LuckPerms (или OP) + +При выходе из режима все восстанавливается обратно. + +## Возможности + +- **Сохранение состояния в JSON** - все данные сохраняются на диск +- **Интеграция с LuckPerms** - автоматическая выдача/отзыв групп +- **Гибкие ограничения** - настройка что можно/нельзя делать в режиме +- **Защита от читерства** - предотвращение переноса предметов + +## Команды + +- `/adminmode` (алиасы: `/adm`, `/am`) - включить/выключить режим администратора + +## Права + +- `adminmode.use` - разрешает использовать режим администратора (по умолчанию: OP) +- `adminmode.punishment.warn` / `unwarn` - выдача и снятие предупреждений +- `adminmode.punishment.mute` / `unmute` - выдача и снятие мута +- `adminmode.punishment.kick` - отключение игрока +- `adminmode.punishment.ban` / `unban` - выдача и снятие бана + +## Установка + +1. Установите Paper 26.1.2 build 74 или новее и Java 25. +2. Установите LuckPerms 5.5.x. +3. Скопируйте `AdminMode-1.2+26.1.2.jar` в папку `plugins/`. +4. Перезапустите сервер и настройте `config.yml`. + +## Зависимости + +- **Обязательные**: Paper 26.1.2, Java 25, LuckPerms 5.5.x +- **Опциональные**: CoreProtect + +Подробная пошаговая настройка групп и прав: [LUCKPERMS_SETUP.md](LUCKPERMS_SETUP.md). + +Контракт команд для отдельного плагина наказаний: [MODERATION_PLUGIN_CONTRACT.md](MODERATION_PLUGIN_CONTRACT.md). + +Команда снятия мута вызывается как `moderationcommands:unmute`, чтобы исключить конфликт с `/unmute` из FlectonePulse. + +## Сборка + +```bash +./gradlew clean build +``` + +Готовый JAR: `build/libs/AdminMode-1.2+26.1.2.jar`. + +## Автор + +BelzeBool + +## Лицензия + +Эксклюзивно для сервера Шлакоблок diff --git a/bin/main/config.yml b/bin/main/config.yml old mode 100644 new mode 100755 index a206973..0dbd2bf --- a/bin/main/config.yml +++ b/bin/main/config.yml @@ -13,15 +13,23 @@ admin_mode: # Группы которые выдаются при активации режима # Можно указать несколько групп через запятую groups: - - "moder_adm" + - "soft_moder_adm" + profile_groups: + soft_moder: + - "soft_moder_adm" + hard_moder: + - "hard_moder_adm" # Удалять ли группы при деактивации (если false - группы останутся) remove_on_disable: true + + dialogs: + enabled: true # Настройки игрового режима gamemode: - # Принудительно ставить спектатор (если false - креатив) - force_spectator: true + # Принудительно ставить спектатор (если false - survival) + force_spectator: false # Мобы игнорируют игрока в админ моде mobs_ignore_player: true @@ -41,14 +49,15 @@ admin_mode: # Разрешенные режимы игры для переключения в админ моде # Если список пустой - можно переключаться на любые режимы # Доступные: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR + # ВАЖНО: CREATIVE убран - модеры не должны иметь доступ к креативу! allowed_gamemodes: - - "CREATIVE" + - "SURVIVAL" - "SPECTATOR" # Ограничения в режиме администратора restrictions: # Запретить выбрасывание предметов - prevent_item_drop: true + prevent_item_drop: false # Запретить поднятие предметов prevent_item_pickup: false @@ -57,7 +66,7 @@ admin_mode: prevent_container_transfer: true # Запретить использование предметов (кроме открытия контейнеров) - prevent_item_use: true + prevent_item_use: false # Разрешить управление своим инвентарем allow_own_inventory: true @@ -89,13 +98,9 @@ admin_mode: # Если список пустой - можно ставить любые блоки # Если список заполнен - можно ставить только эти блоки allowed_place_blocks: - - "BARRIER" - - "LIGHT" - - "STRUCTURE_VOID" - - "STRUCTURE_BLOCK" - + # Выдавать пресет блоков при входе в админ мод - give_preset_blocks: true + give_preset_blocks: false # Пресет блоков (формат: МАТЕРИАЛ:КОЛИЧЕСТВО) preset_blocks: @@ -117,3 +122,30 @@ admin_mode: item_use_denied: "✖ В режиме администратора нельзя использовать предметы!" block_place_denied: "✖ Вы можете ставить только разрешенные блоки!" reloaded: "✓ Конфигурация AdminMode перезагружена!" + +groups: + base: "moder" + soft: "soft_moder_adm" + hard: "hard_moder_adm" + +items: + soft_panel_material: COMPASS + hard_panel_material: NETHER_STAR + cp_inspector_material: STICK + +coreprotect: + enabled: true + default_radius: 10 + default_time: "24h" + +messages: + prefix: "§8[§bAdminMode§8] " + enter_soft: "§aВошёл в режим §bSOFT MODER" + enter_hard: "§aВошёл в режим §cHARD MODER" + exit: "§7Режим деактивирован. Состояние восстановлено." + cp_unavailable: "§cCoreProtect не найден. Hard режим работает без CP." + session_recovered: "§eВаша активная сессия модерации была восстановлена и корректно завершена." + +logging: + enabled: true + log_file: "logs/sessions.log" diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml old mode 100644 new mode 100755 index f027dda..db70652 --- a/bin/main/plugin.yml +++ b/bin/main/plugin.yml @@ -1,21 +1,34 @@ name: AdminMode -version: '1.1.0' +version: '1.21.11' main: org.blz.adminmode.AdminMode api-version: '1.21' author: BelzeBool, Kilka_v_HJIebe description: Плагин режима администратора с сохранением состояния игрока -softdepend: [LuckPerms] +depend: [LuckPerms] +softdepend: [CoreProtect] commands: adminmode: description: Режим администратора с сохранением состояния usage: / aliases: [adm, am] + admfix: + description: Отключить все способности у игрока (полет, бессмертие) + usage: / <игрок> permissions: adminmode.use: description: Разрешает использовать режим администратора default: op + adminmode.profile.soft: + description: Разрешает использовать профиль soft_moder + default: op + adminmode.profile.hard: + description: Разрешает использовать профиль hard_moder + default: op adminmode.reload: description: Разрешает перезагружать конфигурацию AdminMode default: op + adminmode.admin: + description: Разрешает использовать сильные действия и админ команды (disable, admfix) + default: op diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 2c00fc2..f4cc80a --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'org.blz' -version = '1.21.11' +version = '1.3+26.1.2' repositories { mavenCentral() @@ -14,23 +14,24 @@ repositories { } dependencies { - compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT' - compileOnly 'net.luckperms:api:5.4' - compileOnly 'com.google.code.gson:gson:2.10.1' + compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable' + compileOnly 'net.luckperms:api:5.5' + compileOnly 'com.google.code.gson:gson:2.13.2' } java { - toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) } processResources { def props = [version: version] inputs.properties props - filteringCharset 'UTF-8' + filteringCharset = 'UTF-8' filesMatching('plugin.yml') { expand props } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 index db3a6ac..5eed7ee --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..7b0d367 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000..55c1fcb Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 0000000..e6dc460 Binary files /dev/null and b/src/main/java/.DS_Store differ diff --git a/src/main/java/org/.DS_Store b/src/main/java/org/.DS_Store new file mode 100644 index 0000000..051659f Binary files /dev/null and b/src/main/java/org/.DS_Store differ diff --git a/src/main/java/org/blz/.DS_Store b/src/main/java/org/blz/.DS_Store new file mode 100644 index 0000000..ec1094a Binary files /dev/null and b/src/main/java/org/blz/.DS_Store differ diff --git a/src/main/java/org/blz/adminmode/AdminMode.java b/src/main/java/org/blz/adminmode/AdminMode.java old mode 100644 new mode 100755 index 6c9462d..bb91109 --- a/src/main/java/org/blz/adminmode/AdminMode.java +++ b/src/main/java/org/blz/adminmode/AdminMode.java @@ -3,6 +3,8 @@ package org.blz.adminmode; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.dialog.AdminDialogService; +import org.blz.adminmode.dialog.DialogPreferenceStore; +import org.blz.adminmode.dialog.MenuEmojiPreferenceStore; import org.blz.adminmode.integrations.CoreProtectIntegration; import org.blz.adminmode.integrations.LuckPermsIntegration; import org.blz.adminmode.listeners.AdminModeListener; @@ -10,8 +12,12 @@ import org.blz.adminmode.listeners.GameModeChangeListener; import org.blz.adminmode.listeners.MobTargetListener; import org.blz.adminmode.listeners.PlayerDeathListener; import org.blz.adminmode.listeners.PlayerSessionListener; +import org.blz.adminmode.moderation.ModeratorAccessManager; import org.blz.adminmode.moderation.ModeManager; import org.blz.adminmode.moderation.ModeSessionLogger; +import org.blz.adminmode.moderation.ModeratorPreferencesStore; +import org.blz.adminmode.moderation.ModeratorWarningStore; +import org.blz.adminmode.moderation.PlayerFreezeManager; import org.blz.adminmode.session.SessionStorage; import org.bukkit.plugin.java.JavaPlugin; @@ -22,6 +28,9 @@ public class AdminMode extends JavaPlugin { private ModeManager modeManager; private AdminModeCommand adminModeCommand; private AdminDialogService dialogService; + private org.blz.adminmode.managers.BelzeBoolManager belzeBoolManager; + private ModeratorAccessManager moderatorAccessManager; + private PlayerFreezeManager freezeManager; @Override public void onEnable() { @@ -30,15 +39,24 @@ public class AdminMode extends JavaPlugin { configManager = new ConfigManager(this); SessionStorage sessionStorage = new SessionStorage(this); + moderatorAccessManager = new ModeratorAccessManager(this); + ModeratorPreferencesStore preferencesStore = new ModeratorPreferencesStore(this); + ModeratorWarningStore warningStore = new ModeratorWarningStore(this); LuckPermsIntegration luckPermsIntegration = new LuckPermsIntegration(this, configManager); CoreProtectIntegration coreProtectIntegration = new CoreProtectIntegration(this, configManager); ModeSessionLogger sessionLogger = new ModeSessionLogger(this, configManager); - modeManager = new ModeManager(this, configManager, luckPermsIntegration, sessionStorage, sessionLogger); - adminModeCommand = new AdminModeCommand(configManager, modeManager, coreProtectIntegration); - dialogService = new AdminDialogService(this, configManager, adminModeCommand); + modeManager = new ModeManager(this, configManager, luckPermsIntegration, sessionStorage, sessionLogger, preferencesStore); + adminModeCommand = new AdminModeCommand(configManager, modeManager, coreProtectIntegration, moderatorAccessManager); + DialogPreferenceStore dialogPreferenceStore = new DialogPreferenceStore(this); + MenuEmojiPreferenceStore emojiPreferenceStore = new MenuEmojiPreferenceStore(this); + dialogService = new AdminDialogService(this, configManager, adminModeCommand, dialogPreferenceStore, emojiPreferenceStore, preferencesStore, warningStore); adminModeCommand.setDialogService(dialogService); + adminModeCommand.setDialogPreferenceStore(dialogPreferenceStore); getCommand("adminmode").setExecutor(adminModeCommand); - getCommand("admfix").setExecutor(new org.blz.adminmode.commands.AdminFixCommand(adminModeCommand)); + getCommand("adminmode").setTabCompleter(adminModeCommand); + org.blz.adminmode.commands.AdminFixCommand adminFixCommand = new org.blz.adminmode.commands.AdminFixCommand(adminModeCommand); + getCommand("admfix").setExecutor(adminFixCommand); + getCommand("admfix").setTabCompleter(adminFixCommand); getServer().getPluginManager().registerEvents(new AdminModeListener(adminModeCommand, configManager, modeManager, dialogService), this); @@ -47,10 +65,13 @@ public class AdminMode extends JavaPlugin { this); getServer().getPluginManager().registerEvents(new PlayerSessionListener(adminModeCommand, modeManager), this); getServer().getPluginManager().registerEvents(new PlayerDeathListener(modeManager), this); + freezeManager = new PlayerFreezeManager(this); + adminModeCommand.setFreezeManager(freezeManager); + getServer().getPluginManager().registerEvents(freezeManager, this); for (org.bukkit.entity.Player onlinePlayer : getServer().getOnlinePlayers()) { if (modeManager.hasSession(onlinePlayer.getUniqueId())) { - getServer().getScheduler().runTaskLater(this, () -> adminModeCommand.recoverSession(onlinePlayer), 1L); + getServer().getScheduler().runTaskLater(this, () -> adminModeCommand.resumeMode(onlinePlayer), 1L); } } @@ -58,7 +79,8 @@ public class AdminMode extends JavaPlugin { new org.blz.adminmode.utils.WorldLoader(this, "pocket_dimension").loadWorld(); // Register Abilities - new org.blz.adminmode.managers.BelzeBoolManager(this); + belzeBoolManager = new org.blz.adminmode.managers.BelzeBoolManager(this); + adminModeCommand.setBelzeBoolManager(belzeBoolManager); getLogger().info("AdminMode успешно включен!"); } diff --git a/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java b/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java old mode 100644 new mode 100755 index 92bd224..ea58c59 --- a/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java +++ b/src/main/java/org/blz/adminmode/commands/AdminFixCommand.java @@ -1,13 +1,19 @@ package org.blz.adminmode.commands; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -public class AdminFixCommand implements CommandExecutor { +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class AdminFixCommand implements CommandExecutor, TabCompleter { private final AdminModeCommand adminModeCommand; @@ -18,26 +24,45 @@ public class AdminFixCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("adminmode.admin")) { - sender.sendMessage(ChatColor.RED + "У вас нет прав для использования этой команды!"); + sender.sendMessage(Component.text("У вас нет прав для использования этой команды!", NamedTextColor.RED)); return true; } if (args.length < 1) { - sender.sendMessage(ChatColor.RED + "Использование: /admfix <игрок>"); + sender.sendMessage(Component.text("Использование: /admfix <игрок>", NamedTextColor.RED)); return true; } Player target = Bukkit.getPlayer(args[0]); if (target == null) { - sender.sendMessage(ChatColor.RED + "Игрок не найден!"); + sender.sendMessage(Component.text("Игрок не найден!", NamedTextColor.RED)); return true; } - adminModeCommand.forceDisableAbilities(target); + if (!adminModeCommand.forceDisableAbilities(target)) { + sender.sendMessage(Component.text("У игрока нет активных состояний AdminMode для сброса.", NamedTextColor.YELLOW)); + return true; + } - sender.sendMessage(ChatColor.GREEN + "✓ Все способности отключены для игрока " + target.getName()); - target.sendMessage(ChatColor.YELLOW + "⚠ Ваши способности были сброшены администратором"); + sender.sendMessage(Component.text("✓ Все способности отключены для игрока " + target.getName(), NamedTextColor.GREEN)); + target.sendMessage(Component.text("⚠ Ваши способности были сброшены администратором", NamedTextColor.YELLOW)); return true; } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length != 1) { + return Collections.emptyList(); + } + String prefix = args[0].toLowerCase(); + List names = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (prefix.isEmpty() || player.getName().toLowerCase().startsWith(prefix)) { + names.add(player.getName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } } diff --git a/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java b/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java old mode 100644 new mode 100755 index a64e823..b3df180 --- a/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java +++ b/src/main/java/org/blz/adminmode/commands/AdminModeCommand.java @@ -5,38 +5,64 @@ import java.util.UUID; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.dialog.AdminDialogService; +import org.blz.adminmode.dialog.DialogPreferenceStore; import org.blz.adminmode.integrations.CoreProtectIntegration; +import org.blz.adminmode.managers.BelzeBoolManager; +import org.blz.adminmode.moderation.ModeratorAccessManager; import org.blz.adminmode.moderation.ModeManager; -import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.moderation.PlayerFreezeManager; import org.blz.adminmode.moderation.PlayerListAction; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import net.kyori.adventure.text.Component; -public class AdminModeCommand implements CommandExecutor { +import java.util.ArrayList; +import java.util.Collections; + +public class AdminModeCommand implements CommandExecutor, TabCompleter { private final ConfigManager configManager; private final ModeManager modeManager; private final CoreProtectIntegration coreProtectIntegration; + private final ModeratorAccessManager moderatorAccessManager; + private BelzeBoolManager belzeBoolManager; + private PlayerFreezeManager freezeManager; private AdminDialogService dialogService; + private DialogPreferenceStore dialogPreferenceStore; public AdminModeCommand( ConfigManager configManager, ModeManager modeManager, - CoreProtectIntegration coreProtectIntegration) { + CoreProtectIntegration coreProtectIntegration, + ModeratorAccessManager moderatorAccessManager) { this.configManager = configManager; this.modeManager = modeManager; this.coreProtectIntegration = coreProtectIntegration; + this.moderatorAccessManager = moderatorAccessManager; } public void setDialogService(AdminDialogService dialogService) { this.dialogService = dialogService; } + public void setDialogPreferenceStore(DialogPreferenceStore store) { + this.dialogPreferenceStore = store; + } + + public void setBelzeBoolManager(BelzeBoolManager belzeBoolManager) { + this.belzeBoolManager = belzeBoolManager; + } + + public void setFreezeManager(PlayerFreezeManager freezeManager) { + this.freezeManager = freezeManager; + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { @@ -54,16 +80,29 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return true; } - if (args.length < 2) { sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode disable <игрок>")); return true; } - forceDisableTarget(sender, args[1]); return true; } + if (args.length > 0 && args[0].equalsIgnoreCase("restrict")) { + handleRestrictCommand(sender, args); + return true; + } + + if (args.length > 0 && args[0].equalsIgnoreCase("unrestrict")) { + handleUnrestrictCommand(sender, args); + return true; + } + + if (args.length > 0 && args[0].equalsIgnoreCase("status")) { + handleStatusCommand(sender, args); + return true; + } + if (!(sender instanceof Player player)) { sender.sendMessage("Эта команда доступна только игрокам!"); return true; @@ -75,59 +114,49 @@ public class AdminModeCommand implements CommandExecutor { } if (args.length > 0 && args[0].equalsIgnoreCase("dialog")) { - if (!openRelevantDialog(player)) { - player.sendMessage(configManager.getPrefixedMessage("§eDialog-панель сейчас недоступна.")); + if (!configManager.isDialogsEnabled()) { + player.sendMessage(configManager.getPrefixedMessage("§cDialog-меню отключено администратором сервера.")); + return true; + } + if (!player.hasPermission("adminmode.use")) { + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return true; + } + boolean nowEnabled = dialogPreferenceStore.toggle(player.getUniqueId()); + if (nowEnabled) { + player.sendMessage(configManager.getPrefixedMessage("§aDialog-меню §2включено§a. Используйте §f/adminmode §aдля открытия панели.")); + } else { + player.sendMessage(configManager.getPrefixedMessage("§7Dialog-меню §8отключено§7.")); } return true; } - if (args.length > 0 && (args[0].equalsIgnoreCase("enable") || args[0].equalsIgnoreCase("profile"))) { - if (args.length < 2) { - openRelevantDialog(player); - return true; - } - - ModeratorProfile profile = ModeratorProfile.fromInput(args[1]).orElse(null); - if (profile == null) { - player.sendMessage(configManager.getPrefixedMessage("§cНеизвестный профиль. Доступно: soft, hard.")); - return true; - } - - activateProfile(player, profile); - return true; - } - - if (!isInAdminMode(player.getUniqueId()) && getAvailableProfiles(player).isEmpty()) { + if (!player.hasPermission("adminmode.use")) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return true; } - openRelevantDialog(player); - return true; - } - - public boolean activateProfile(Player player, ModeratorProfile profile) { - if (profile == null) { - return openRelevantDialog(player); + if (args.length > 0 && args[0].equalsIgnoreCase("on")) { + return handleEnableMode(player); } - if (!profile.isAvailableFor(player)) { - player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); - return false; + if (args.length > 0 && args[0].equalsIgnoreCase("off")) { + return handleDisableMode(player); } - if (modeManager.hasSession(player.getUniqueId())) { - ModeratorProfile activeProfile = getActiveProfile(player.getUniqueId()); - if (activeProfile == profile) { - return openRelevantDialog(player); - } + if (args.length > 0 && args[0].equalsIgnoreCase("toggle")) { + return handleHardToggle(player); + } + if (!isInAdminMode(player.getUniqueId())) { + return handleEnableMode(player); + } + + if (!openRelevantDialog(player)) { player.sendMessage(configManager.getPrefixedMessage( - "§eУ вас уже активен профиль §f" + activeProfile.getDisplayName() + "§e. Сначала выйдите из режима.")); - return false; + "§aВы в режиме администратора. Используйте §f/adminmode dialog §aдля включения GUI-панели.")); } - - return modeManager.enterMode(player, profile); + return true; } public boolean exitMode(Player player) { @@ -138,12 +167,8 @@ public class AdminModeCommand implements CommandExecutor { return modeManager.recoverSession(player); } - public List getAvailableProfiles(Player player) { - return modeManager.getAvailableProfiles(player); - } - - public ModeratorProfile getActiveProfile(UUID playerId) { - return modeManager.getProfile(playerId); + public boolean resumeMode(Player player) { + return modeManager.resumeMode(player); } public boolean isInAdminMode(UUID playerId) { @@ -155,37 +180,54 @@ public class AdminModeCommand implements CommandExecutor { } public void applyAdminSettings(Player player, float flySpeed, float walkSpeed, boolean godMode) { - if (!modeManager.hasSession(player.getUniqueId())) { - return; - } - + if (!modeManager.hasSession(player.getUniqueId())) return; modeManager.applyAdminSettings(player, flySpeed, walkSpeed, godMode); player.sendMessage(configManager.getPrefixedMessage("§aНастройки режима обновлены.")); } - public boolean performPlayerListAction(Player moderator, PlayerListAction action, String targetName) { - Player target = validateActiveTarget(moderator, targetName); - if (target == null) { + public boolean toggleVanish(Player player) { + return modeManager.toggleVanish(player); + } + + public boolean isVanished(UUID playerId) { + return modeManager.isVanished(playerId); + } + + public boolean openBanList(Player player) { + return dialogService != null && dialogService.openBanListDialog(player); + } + + public boolean toggleFreeze(Player player) { + if (freezeManager == null) { return false; } + return freezeManager.toggleFreeze(player); + } + + public boolean isFrozen(UUID playerId) { + return freezeManager != null && freezeManager.isFrozen(playerId); + } + + public boolean performPlayerListAction(Player moderator, PlayerListAction action, String targetName) { + Player target = validateActiveTarget(moderator, targetName); + if (target == null) return false; return switch (action) { case SPECTATE -> modeManager.spectatePlayer(moderator, target); case TELEPORT -> modeManager.teleportToPlayer(moderator, target); case INVSEE -> modeManager.openInvsee(moderator, target); + case MANAGE -> { + dialogService.openManagePlayerDialog(moderator, target); + yield true; + } }; } - public boolean teleportToPlayer(Player moderator, String targetName) { - Player target = validateActiveTarget(moderator, targetName); - return target != null && modeManager.teleportToPlayer(moderator, target); - } - public boolean teleportPlayerHere(Player moderator, String targetName) { - if (!validateHardActionPlayer(moderator)) { + if (!modeManager.hasSession(moderator.getUniqueId())) { + moderator.sendMessage(configManager.getPrefixedMessage("§cСначала войдите в режим модерации.")); return false; } - Player target = resolveOnlinePlayer(targetName, moderator); return target != null && modeManager.teleportPlayerHere(moderator, target); } @@ -195,77 +237,242 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); return false; } - Player target = resolveOnlinePlayer(targetName, sender); - if (target == null) { + if (target == null) return false; + if (!forceDisableAbilities(target)) { + sender.sendMessage(configManager.getPrefixedMessage("§eУ игрока нет активных состояний AdminMode для сброса.")); return false; } - - forceDisableAbilities(target); sender.sendMessage(configManager.getPrefixedMessage("§aРежим и способности игрока §f" + target.getName() + "§a сброшены.")); target.sendMessage(configManager.getPrefixedMessage("§eВаш режим модерации был принудительно отключен.")); return true; } public boolean executeCPLookup(Player player, String targetPlayer, int radius, String time) { - if (!validateHardActionPlayer(player)) { - return false; - } + if (!modeManager.hasSession(player.getUniqueId())) return false; return coreProtectIntegration.lookup(player, targetPlayer, radius, time); } - public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer) { - if (!validateHardActionPlayer(player)) { - return false; - } - return coreProtectIntegration.rollback(player, radius, time, targetPlayer); + public boolean executeCPLookup(Player player, String targetPlayer, String action, String block, String exclude, + String world, int radius, String time) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.lookup(player, targetPlayer, action, block, exclude, world, radius, time); + } + + public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer, String action, String block, String exclude) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.rollback(player, radius, time, targetPlayer, action, block, exclude); + } + + public boolean executeCPRollback(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.rollback(player, radius, time, targetPlayer, action, block, exclude, world, extraFilters); + } + + public boolean executeCPRestore(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!modeManager.hasSession(player.getUniqueId())) return false; + return coreProtectIntegration.restore(player, radius, time, targetPlayer, action, block, exclude, world, extraFilters); } public boolean toggleInspector(Player player) { - if (!validateHardActionPlayer(player)) { - return false; - } + if (!modeManager.hasSession(player.getUniqueId())) return false; return coreProtectIntegration.toggleInspector(player); } - public void forceDisableAbilities(Player player) { + public boolean forceDisableAbilities(Player player) { + boolean changed = false; + if (modeManager.hasSession(player.getUniqueId())) { - modeManager.exitMode(player); - return; + changed = modeManager.exitMode(player); } - player.setInvulnerable(false); - player.setFlying(false); - player.setAllowFlight(false); - player.setFlySpeed(0.1f); - player.setWalkSpeed(0.2f); + if (belzeBoolManager != null) { + changed = belzeBoolManager.forceDisable(player) || changed; + } + + return changed; } public boolean canUseHardActions(CommandSender sender) { return modeManager.canUseHardActions(sender); } - private boolean openRelevantDialog(Player player) { - if (dialogService == null || !configManager.isDialogsEnabled()) { - return false; - } - - if (modeManager.hasSession(player.getUniqueId())) { - return dialogService.openCurrentPanel(player); - } - - return dialogService.openModeSelectDialog(player); + public boolean canManageModeratorAccess(Player actor, Player target) { + return actor != null + && target != null + && actor.hasPermission("adminmode.chief") + && !actor.getUniqueId().equals(target.getUniqueId()) + && target.hasPermission("adminmode.use"); } - private boolean validateHardActionPlayer(Player moderator) { - if (!canUseHardActions(moderator)) { - moderator.sendMessage(configManager.getPrefixedMessage("§cДля этого действия нужен активный профиль Hard Moder.")); + public boolean isModeratorRestricted(UUID playerId) { + return moderatorAccessManager.isRestricted(playerId); + } + + public boolean restrictModerator(Player actor, OfflinePlayer target, String durationInput, String reason) { + if (actor == null || target == null || !actor.hasPermission("adminmode.chief")) { + return false; + } + return applyRestriction(actor, target, durationInput, reason); + } + + public boolean unrestrictModerator(Player actor, OfflinePlayer target) { + if (actor == null || target == null || !actor.hasPermission("adminmode.chief")) { + return false; + } + return clearRestriction(actor, target); + } + + private boolean handleEnableMode(Player player) { + if (moderatorAccessManager.isRestricted(player.getUniqueId())) { + player.sendMessage(configManager.getPrefixedMessage(moderatorAccessManager.describeRestriction(player.getUniqueId()))); + return true; + } + if (isInAdminMode(player.getUniqueId())) { + if (!openRelevantDialog(player)) { + player.sendMessage(configManager.getPrefixedMessage("§eAdmin mode уже включён.")); + } + return true; + } + if (!modeManager.enterMode(player)) { + player.sendMessage(configManager.getPrefixedMessage("§cНе удалось включить admin mode. Проверьте права и состояние игрока.")); + } + return true; + } + + private boolean handleDisableMode(Player player) { + if (!isInAdminMode(player.getUniqueId())) { + player.sendMessage(configManager.getPrefixedMessage("§eAdmin mode уже выключен.")); + return true; + } + modeManager.exitMode(player); + return true; + } + + private boolean handleHardToggle(Player player) { + if (isInAdminMode(player.getUniqueId())) { + return handleDisableMode(player); + } + return handleEnableMode(player); + } + + private boolean applyRestriction(CommandSender sender, OfflinePlayer target, String durationInput, String reason) { + Long expiresAt = ModeratorAccessManager.parseDurationToExpiry(durationInput); + if (expiresAt == Long.MIN_VALUE) { + sender.sendMessage(configManager.getPrefixedMessage("§cНеверный срок. Примеры: 30s, 30m, 2h, 7d, 1y, perm")); return false; } + String targetName = target.getName() != null ? target.getName() : target.getUniqueId().toString(); + ModeratorAccessManager.RestrictionEntry entry = moderatorAccessManager.restrict( + target, + targetName, + sender.getName(), + expiresAt, + reason); + + Player onlineTarget = Bukkit.getPlayer(target.getUniqueId()); + if (onlineTarget != null) { + forceDisableAbilities(onlineTarget); + onlineTarget.sendMessage(configManager.getPrefixedMessage(moderatorAccessManager.describeRestriction(onlineTarget.getUniqueId()))); + } + + sender.sendMessage(configManager.getPrefixedMessage( + "§aДоступ к admin mode для §f" + entry.getPlayerName() + "§a " + + (entry.isPermanent() ? "отключён навсегда." : "ограничен по времени."))); return true; } + private boolean clearRestriction(CommandSender sender, OfflinePlayer target) { + if (!moderatorAccessManager.clearRestriction(target.getUniqueId())) { + sender.sendMessage(configManager.getPrefixedMessage("§eДля этого игрока нет активного ограничения.")); + return false; + } + + String targetName = target.getName() != null ? target.getName() : target.getUniqueId().toString(); + sender.sendMessage(configManager.getPrefixedMessage("§aОграничение для §f" + targetName + "§a снято.")); + Player onlineTarget = Bukkit.getPlayer(target.getUniqueId()); + if (onlineTarget != null) { + onlineTarget.sendMessage(configManager.getPrefixedMessage("§aДоступ к admin mode снова разрешён.")); + } + return true; + } + + private void handleRestrictCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 3) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode restrict <игрок> <1s..1y|perm> [причина]")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (target.getUniqueId() == null) { + sender.sendMessage(configManager.getPrefixedMessage("§cНе удалось определить игрока.")); + return; + } + + applyRestriction(sender, target, args[2], joinArgs(args, 3)); + } + + private void handleUnrestrictCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 2) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode unrestrict <игрок>")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + clearRestriction(sender, target); + } + + private void handleStatusCommand(CommandSender sender, String[] args) { + if (!sender.hasPermission("adminmode.chief")) { + sender.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (args.length < 2) { + sender.sendMessage(configManager.getPrefixedMessage("§cИспользование: /adminmode status <игрок>")); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!moderatorAccessManager.isRestricted(target.getUniqueId())) { + sender.sendMessage(configManager.getPrefixedMessage("§aУ игрока нет ограничений на admin mode.")); + return; + } + + sender.sendMessage(configManager.getPrefixedMessage("§eСтатус для §f" + (target.getName() != null ? target.getName() : args[1]) + "§e: " + + moderatorAccessManager.describeRestriction(target.getUniqueId()))); + } + + private String joinArgs(String[] args, int startIndex) { + if (startIndex >= args.length) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (int i = startIndex; i < args.length; i++) { + if (i > startIndex) { + builder.append(' '); + } + builder.append(args[i]); + } + return builder.toString(); + } + + private boolean openRelevantDialog(Player player) { + if (dialogService == null || !configManager.isDialogsEnabled()) return false; + if (dialogPreferenceStore == null || !dialogPreferenceStore.isEnabled(player.getUniqueId())) return false; + return dialogService.openAdminPanel(player); + } + private Player validateActiveTarget(Player moderator, String targetName) { if (!modeManager.hasSession(moderator.getUniqueId())) { moderator.sendMessage(configManager.getPrefixedMessage("§cСначала войдите в режим модерации.")); @@ -279,17 +486,83 @@ public class AdminModeCommand implements CommandExecutor { sender.sendMessage(configManager.getPrefixedMessage("§cУкажите ник игрока.")); return null; } - Player target = Bukkit.getPlayerExact(targetName.trim()); - if (target == null) { - target = Bukkit.getPlayer(targetName.trim()); - } - + if (target == null) target = Bukkit.getPlayer(targetName.trim()); if (target == null) { sender.sendMessage(configManager.getPrefixedMessage("§cИгрок не найден!")); return null; } - return target; } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (command.getName().equalsIgnoreCase("adminmode")) { + return completeAdminMode(sender, args); + } + return Collections.emptyList(); + } + + private List completeAdminMode(CommandSender sender, String[] args) { + if (args.length == 1) { + List suggestions = new ArrayList<>(); + suggestions.add("on"); + suggestions.add("off"); + suggestions.add("toggle"); + suggestions.add("dialog"); + suggestions.add("disable"); + suggestions.add("restrict"); + suggestions.add("unrestrict"); + suggestions.add("status"); + suggestions.add("reload"); + return filterPrefix(suggestions, args[0]); + } + + String subcommand = args[0].toLowerCase(); + if (args.length == 2) { + return switch (subcommand) { + case "disable" -> filterPrefix(getOnlinePlayerNames(), args[1]); + case "restrict", "status" -> filterPrefix(getKnownPlayerNames(), args[1]); + case "unrestrict" -> filterPrefix(moderatorAccessManager.getRestrictedPlayerNames(), args[1]); + default -> Collections.emptyList(); + }; + } + + if (args.length == 3 && subcommand.equals("restrict")) { + return filterPrefix(List.of("30s", "1m", "30m", "2h", "1d", "7d", "1y", "perm"), args[2]); + } + + return Collections.emptyList(); + } + + private List getOnlinePlayerNames() { + List names = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + names.add(player.getName()); + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + private List getKnownPlayerNames() { + List names = new ArrayList<>(getOnlinePlayerNames()); + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + if (offlinePlayer.getName() != null && !offlinePlayer.getName().isBlank() && !names.contains(offlinePlayer.getName())) { + names.add(offlinePlayer.getName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + private List filterPrefix(List source, String prefix) { + String normalized = prefix == null ? "" : prefix.toLowerCase(); + List result = new ArrayList<>(); + for (String value : source) { + if (normalized.isEmpty() || value.toLowerCase().startsWith(normalized)) { + result.add(value); + } + } + return result; + } } diff --git a/src/main/java/org/blz/adminmode/config/ConfigManager.java b/src/main/java/org/blz/adminmode/config/ConfigManager.java old mode 100644 new mode 100755 index c51f9b6..3b32239 --- a/src/main/java/org/blz/adminmode/config/ConfigManager.java +++ b/src/main/java/org/blz/adminmode/config/ConfigManager.java @@ -1,11 +1,10 @@ package org.blz.adminmode.config; import java.util.ArrayList; -import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.moderation.PunishmentDuration; import org.bukkit.Material; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.Plugin; @@ -17,20 +16,28 @@ public class ConfigManager { private boolean enabled; private boolean useLuckPerms; - private Map> luckPermsGroups; + private List adminLuckPermsGroups; private boolean removeGroupsOnDisable; private boolean dialogsEnabled; private String baseGroup; - private String softGroup; - private String hardGroup; - private Material softPanelMaterial; - private Material hardPanelMaterial; + private String adminGroup; + private Material panelMaterial; private Material cpInspectorMaterial; private boolean coreProtectEnabled; private int coreProtectDefaultRadius; private String coreProtectDefaultTime; private boolean sessionLoggingEnabled; private String sessionLogFile; + private boolean moderationActionsAsConsole; + private String warnCommandTemplate; + private String muteCommandTemplate; + private String unmuteCommandTemplate; + private String banCommandTemplate; + private String kickCommandTemplate; + private List punishmentDurationOptions; + private String defaultWarnDuration; + private String defaultMuteDuration; + private String defaultBanDuration; private boolean preventItemDrop; private boolean preventItemPickup; @@ -46,6 +53,7 @@ public class ConfigManager { private boolean mobsIgnorePlayer; private boolean allowFlightInSurvival; private boolean godMode; + private boolean chiefCreativeMode; private float customFlySpeed; private float customWalkSpeed; private List allowedGameModes; @@ -63,8 +71,7 @@ public class ConfigManager { private String msgBlockPlaceDenied; private String msgReloaded; private String msgPrefix; - private String msgEnterSoft; - private String msgEnterHard; + private String msgEnter; private String msgExitMode; private String msgCoreProtectUnavailable; private String msgSessionRecovered; @@ -79,17 +86,20 @@ public class ConfigManager { config = plugin.getConfig(); enabled = config.getBoolean("admin_mode.enabled", true); - useLuckPerms = config.getBoolean("admin_mode.luckperms.enabled", true); removeGroupsOnDisable = config.getBoolean("admin_mode.luckperms.remove_on_disable", true); dialogsEnabled = config.getBoolean("admin_mode.dialogs.enabled", true); baseGroup = config.getString("groups.base", "moder"); - softGroup = config.getString("groups.soft", "soft_moder_adm"); - hardGroup = config.getString("groups.hard", "hard_moder_adm"); + adminGroup = config.getString("groups.admin", "admin_moder"); - softPanelMaterial = readMaterial("items.soft_panel_material", Material.COMPASS); - hardPanelMaterial = readMaterial("items.hard_panel_material", Material.NETHER_STAR); + List groups = config.getStringList("admin_mode.luckperms.groups"); + if (groups.isEmpty()) { + groups = List.of(adminGroup); + } + adminLuckPermsGroups = new ArrayList<>(groups); + + panelMaterial = readMaterial("items.panel_material", Material.NETHER_STAR); cpInspectorMaterial = readMaterial("items.cp_inspector_material", Material.STICK); coreProtectEnabled = config.getBoolean("coreprotect.enabled", true); @@ -98,42 +108,38 @@ public class ConfigManager { sessionLoggingEnabled = config.getBoolean("logging.enabled", true); sessionLogFile = config.getString("logging.log_file", "logs/sessions.log"); + moderationActionsAsConsole = config.getBoolean("moderation_actions.dispatch_as_console", true); + punishmentDurationOptions = readPunishmentDurations(); + defaultWarnDuration = readDefaultPunishmentDuration("warn", "7d"); + defaultMuteDuration = readDefaultPunishmentDuration("mute", "1h"); + defaultBanDuration = readDefaultPunishmentDuration("ban", "7d"); + warnCommandTemplate = readModerationCommand("warn", + "warn {target} {reason}", "warn {target} {duration} {reason}", true); + muteCommandTemplate = readModerationCommand("mute", + "mute {target} {reason}", "mute {target} {duration} actor:{moderator} {reason}", true); + unmuteCommandTemplate = readModerationCommand("unmute", + "unmute {target} actor:{moderator} {reason}", + "moderationcommands:unmute {target} actor:{moderator} {reason}", false); + banCommandTemplate = readModerationCommand("ban", + "ban {target} {reason}", "ban {target} {duration} actor:{moderator} {reason}", true); + kickCommandTemplate = readModerationCommand("kick", + "kick {target} {reason}", "kick {target} actor:{moderator} {reason}", false); - List legacyGroups = config.getStringList("admin_mode.luckperms.groups"); - List softGroups = config.getStringList("admin_mode.luckperms.profile_groups.soft_moder"); - List hardGroups = config.getStringList("admin_mode.luckperms.profile_groups.hard_moder"); - if (softGroups.isEmpty()) { - if (!legacyGroups.isEmpty()) { - softGroups = new ArrayList<>(legacyGroups); - } else { - softGroups = new ArrayList<>(List.of(softGroup)); - } - } - if (hardGroups.isEmpty()) { - if (!legacyGroups.isEmpty()) { - hardGroups = new ArrayList<>(legacyGroups); - } else { - hardGroups = new ArrayList<>(List.of(hardGroup)); - } - } - luckPermsGroups = new EnumMap<>(ModeratorProfile.class); - luckPermsGroups.put(ModeratorProfile.SOFT_MODER, new ArrayList<>(softGroups)); - luckPermsGroups.put(ModeratorProfile.HARD_MODER, new ArrayList<>(hardGroups)); - - preventItemDrop = config.getBoolean("admin_mode.restrictions.prevent_item_drop", true); + preventItemDrop = config.getBoolean("admin_mode.restrictions.prevent_item_drop", false); preventItemPickup = config.getBoolean("admin_mode.restrictions.prevent_item_pickup", false); preventContainerTransfer = config.getBoolean("admin_mode.restrictions.prevent_container_transfer", true); - preventItemUse = config.getBoolean("admin_mode.restrictions.prevent_item_use", true); + preventItemUse = config.getBoolean("admin_mode.restrictions.prevent_item_use", false); allowOwnInventory = config.getBoolean("admin_mode.restrictions.allow_own_inventory", true); allowedBlocks = config.getStringList("admin_mode.restrictions.allowed_blocks"); allowedPlaceBlocks = config.getStringList("admin_mode.restrictions.allowed_place_blocks"); - givePresetBlocks = config.getBoolean("admin_mode.restrictions.give_preset_blocks", true); + givePresetBlocks = config.getBoolean("admin_mode.restrictions.give_preset_blocks", false); presetBlocks = config.getStringList("admin_mode.restrictions.preset_blocks"); forceSpectator = config.getBoolean("admin_mode.gamemode.force_spectator", false); mobsIgnorePlayer = config.getBoolean("admin_mode.gamemode.mobs_ignore_player", true); allowFlightInSurvival = config.getBoolean("admin_mode.gamemode.allow_flight_in_survival", true); godMode = config.getBoolean("admin_mode.gamemode.god_mode", true); + chiefCreativeMode = config.getBoolean("admin_mode.gamemode.chief_creative", true); customFlySpeed = (float) config.getDouble("admin_mode.gamemode.custom_fly_speed", 0.2); customWalkSpeed = (float) config.getDouble("admin_mode.gamemode.custom_walk_speed", 0.2); allowedGameModes = config.getStringList("admin_mode.gamemode.allowed_gamemodes"); @@ -143,8 +149,7 @@ public class ConfigManager { msgPermissionsGranted = config.getString("admin_mode.messages.permissions_granted", "⚠ Вам выданы временные права администратора!"); msgDisabled = config.getString("admin_mode.messages.disabled", "✓ Режим администратора отключен!"); - msgDisabledSubtitle = config.getString("admin_mode.messages.disabled_subtitle", - "Ваше состояние восстановлено."); + msgDisabledSubtitle = config.getString("admin_mode.messages.disabled_subtitle", "Ваше состояние восстановлено."); msgPermissionsRemoved = config.getString("admin_mode.messages.permissions_removed", "✓ Права администратора отозваны!"); msgNoPermission = config.getString("admin_mode.messages.no_permission", @@ -160,241 +165,158 @@ public class ConfigManager { msgReloaded = config.getString("admin_mode.messages.reloaded", "✓ Конфигурация AdminMode перезагружена!"); msgPrefix = config.getString("messages.prefix", "§8[§bAdminMode§8] "); - msgEnterSoft = config.getString("messages.enter_soft", "§aВошёл в режим §bSOFT MODER"); - msgEnterHard = config.getString("messages.enter_hard", "§aВошёл в режим §cHARD MODER"); + msgEnter = config.getString("messages.enter", "§aВошёл в режим §cADMIN MODE"); msgExitMode = config.getString("messages.exit", "§7Режим деактивирован. Состояние восстановлено."); msgCoreProtectUnavailable = config.getString("messages.cp_unavailable", - "§cCoreProtect не найден. Hard режим работает без CP."); + "§cCoreProtect не найден или отключён."); msgSessionRecovered = config.getString("messages.session_recovered", "§eВаша активная сессия модерации была восстановлена и корректно завершена."); + + validatePresetBlocks(); } - public boolean isEnabled() { - return enabled; - } + // --- Getters --- - public boolean isUseLuckPerms() { - return useLuckPerms; - } - - public List getLuckPermsGroups() { - return getLuckPermsGroups(ModeratorProfile.SOFT_MODER); - } - - public List getLuckPermsGroups(ModeratorProfile profile) { - return new ArrayList<>(luckPermsGroups.getOrDefault(profile, List.of())); - } - - public boolean isRemoveGroupsOnDisable() { - return removeGroupsOnDisable; - } - - public boolean isDialogsEnabled() { - return dialogsEnabled; - } - - public String getBaseGroup() { - return baseGroup; - } - - public String getSoftGroup() { - return softGroup; - } - - public String getHardGroup() { - return hardGroup; - } - - public String getActiveGroup(ModeratorProfile profile) { - return profile != null && profile.isHardProfile() ? hardGroup : softGroup; - } - - public Material getSoftPanelMaterial() { - return softPanelMaterial; - } - - public Material getHardPanelMaterial() { - return hardPanelMaterial; - } - - public Material getCpInspectorMaterial() { - return cpInspectorMaterial; - } - - public boolean isCoreProtectEnabled() { - return coreProtectEnabled; - } - - public int getCoreProtectDefaultRadius() { - return coreProtectDefaultRadius; - } - - public String getCoreProtectDefaultTime() { - return coreProtectDefaultTime; - } - - public boolean isSessionLoggingEnabled() { - return sessionLoggingEnabled; - } - - public String getSessionLogFile() { - return sessionLogFile; - } - - public boolean isPreventItemDrop() { - return preventItemDrop; - } - - public boolean isPreventItemPickup() { - return preventItemPickup; - } - - public boolean isPreventContainerTransfer() { - return preventContainerTransfer; - } - - public boolean isPreventItemUse() { - return preventItemUse; - } - - public boolean isAllowOwnInventory() { - return allowOwnInventory; - } - - public List getAllowedBlocks() { - return allowedBlocks; - } - - public List getAllowedPlaceBlocks() { - return allowedPlaceBlocks; - } - - public boolean isGivePresetBlocks() { - return givePresetBlocks; - } - - public List getPresetBlocks() { - return presetBlocks; - } - - public boolean isForceSpectator() { - return forceSpectator; - } - - public boolean isMobsIgnorePlayer() { - return mobsIgnorePlayer; - } - - public boolean isAllowFlightInSurvival() { - return allowFlightInSurvival; - } - - public boolean isGodMode() { - return godMode; - } - - public float getCustomFlySpeed() { - return customFlySpeed; - } - - public float getCustomWalkSpeed() { - return customWalkSpeed; - } - - public List getAllowedGameModes() { - return allowedGameModes; - } - - public String getMsgEnabled() { - return msgEnabled; - } - - public String getMsgEnabledSubtitle() { - return msgEnabledSubtitle; - } - - public String getMsgPermissionsGranted() { - return msgPermissionsGranted; - } - - public String getMsgDisabled() { - return msgDisabled; - } - - public String getMsgDisabledSubtitle() { - return msgDisabledSubtitle; - } - - public String getMsgPermissionsRemoved() { - return msgPermissionsRemoved; - } - - public String getMsgNoPermission() { - return msgNoPermission; - } - - public String getMsgItemDropDenied() { - return msgItemDropDenied; - } - - public String getMsgContainerTransferDenied() { - return msgContainerTransferDenied; - } - - public String getMsgItemUseDenied() { - return msgItemUseDenied; - } - - public String getMsgBlockPlaceDenied() { - return msgBlockPlaceDenied; - } - - public String getMsgReloaded() { - return msgReloaded; - } - - public String getMsgPrefix() { - return msgPrefix; - } - - public String getMsgEnterSoft() { - return msgEnterSoft; - } - - public String getMsgEnterHard() { - return msgEnterHard; - } - - public String getMsgExitMode() { - return msgExitMode; - } - - public String getMsgCoreProtectUnavailable() { - return msgCoreProtectUnavailable; - } - - public String getMsgSessionRecovered() { - return msgSessionRecovered; + public boolean isEnabled() { return enabled; } + public boolean isUseLuckPerms() { return useLuckPerms; } + public List getLuckPermsGroups() { return new ArrayList<>(adminLuckPermsGroups); } + public boolean isRemoveGroupsOnDisable() { return removeGroupsOnDisable; } + public boolean isDialogsEnabled() { return dialogsEnabled; } + public String getBaseGroup() { return baseGroup; } + public String getAdminGroup() { return adminGroup; } + public Material getPanelMaterial() { return panelMaterial; } + public Material getCpInspectorMaterial() { return cpInspectorMaterial; } + public boolean isCoreProtectEnabled() { return coreProtectEnabled; } + public int getCoreProtectDefaultRadius() { return coreProtectDefaultRadius; } + public String getCoreProtectDefaultTime() { return coreProtectDefaultTime; } + public boolean isSessionLoggingEnabled() { return sessionLoggingEnabled; } + public String getSessionLogFile() { return sessionLogFile; } + public boolean isModerationActionsAsConsole() { return moderationActionsAsConsole; } + public String getWarnCommandTemplate() { return warnCommandTemplate; } + public String getMuteCommandTemplate() { return muteCommandTemplate; } + public String getUnmuteCommandTemplate() { return unmuteCommandTemplate; } + public String getBanCommandTemplate() { return banCommandTemplate; } + public String getKickCommandTemplate() { return kickCommandTemplate; } + public List getPunishmentDurationOptions() { return new ArrayList<>(punishmentDurationOptions); } + public String getDefaultWarnDuration() { return defaultWarnDuration; } + public String getDefaultMuteDuration() { return defaultMuteDuration; } + public String getDefaultBanDuration() { return defaultBanDuration; } + public boolean isPunishmentDurationAllowed(String value) { + String normalized = PunishmentDuration.normalizeToken(value); + return !normalized.isEmpty() && punishmentDurationOptions.contains(normalized); } + public boolean isPreventItemDrop() { return preventItemDrop; } + public boolean isPreventItemPickup() { return preventItemPickup; } + public boolean isPreventContainerTransfer() { return preventContainerTransfer; } + public boolean isPreventItemUse() { return preventItemUse; } + public boolean isAllowOwnInventory() { return allowOwnInventory; } + public List getAllowedBlocks() { return allowedBlocks; } + public List getAllowedPlaceBlocks() { return allowedPlaceBlocks; } + public boolean isGivePresetBlocks() { return givePresetBlocks; } + public List getPresetBlocks() { return presetBlocks; } + public boolean isForceSpectator() { return forceSpectator; } + public boolean isMobsIgnorePlayer() { return mobsIgnorePlayer; } + public boolean isAllowFlightInSurvival() { return allowFlightInSurvival; } + public boolean isGodMode() { return godMode; } + public boolean isChiefCreativeMode() { return chiefCreativeMode; } + public float getCustomFlySpeed() { return customFlySpeed; } + public float getCustomWalkSpeed() { return customWalkSpeed; } + public List getAllowedGameModes() { return allowedGameModes; } + public String getMsgEnabled() { return msgEnabled; } + public String getMsgEnabledSubtitle() { return msgEnabledSubtitle; } + public String getMsgPermissionsGranted() { return msgPermissionsGranted; } + public String getMsgDisabled() { return msgDisabled; } + public String getMsgDisabledSubtitle() { return msgDisabledSubtitle; } + public String getMsgPermissionsRemoved() { return msgPermissionsRemoved; } + public String getMsgNoPermission() { return msgNoPermission; } + public String getMsgItemDropDenied() { return msgItemDropDenied; } + public String getMsgContainerTransferDenied() { return msgContainerTransferDenied; } + public String getMsgItemUseDenied() { return msgItemUseDenied; } + public String getMsgBlockPlaceDenied() { return msgBlockPlaceDenied; } + public String getMsgReloaded() { return msgReloaded; } + public String getMsgPrefix() { return msgPrefix; } + public String getMsgEnter() { return msgEnter; } + public String getMsgExitMode() { return msgExitMode; } + public String getMsgCoreProtectUnavailable() { return msgCoreProtectUnavailable; } + public String getMsgSessionRecovered() { return msgSessionRecovered; } public String getPrefixedMessage(String message) { - if (message == null || message.isEmpty()) { - return msgPrefix; - } + if (message == null || message.isEmpty()) return msgPrefix; return msgPrefix + message; } private Material readMaterial(String path, Material fallback) { String value = config.getString(path); - if (value == null || value.isBlank()) { - return fallback; - } - + if (value == null || value.isBlank()) return fallback; try { return Material.valueOf(value.trim().toUpperCase()); - } catch (IllegalArgumentException exception) { + } catch (IllegalArgumentException e) { plugin.getLogger().warning("Неизвестный material в конфиге " + path + ": " + value); return fallback; } } + + private void validatePresetBlocks() { + for (String blockString : presetBlocks) { + try { + String[] parts = blockString.split(":"); + Material.valueOf(parts[0].trim().toUpperCase()); + } catch (Exception e) { + plugin.getLogger().warning("Неверный блок в preset_blocks: " + blockString); + } + } + } + + private List readPunishmentDurations() { + List configured = config.getStringList("moderation_actions.durations.options"); + if (configured.isEmpty()) { + configured = List.of("1h", "1d", "7d", "30d", "permanent"); + } + + LinkedHashSet normalized = new LinkedHashSet<>(); + for (String value : configured) { + String token = PunishmentDuration.normalizeToken(value); + if (token.isEmpty()) { + plugin.getLogger().warning("Неизвестный срок наказания в config.yml: " + value); + } else { + normalized.add(token); + } + } + if (normalized.isEmpty()) { + normalized.addAll(List.of("1h", "1d", "7d", "30d", "permanent")); + } + return new ArrayList<>(normalized); + } + + private String readDefaultPunishmentDuration(String action, String fallback) { + String configured = config.getString("moderation_actions.durations.defaults." + action, fallback); + String normalized = PunishmentDuration.normalizeToken(configured); + if (!normalized.isEmpty() && punishmentDurationOptions.contains(normalized)) { + return normalized; + } + plugin.getLogger().warning("Срок по умолчанию для " + action + " отсутствует в списке options: " + configured); + return punishmentDurationOptions.contains(fallback) ? fallback : punishmentDurationOptions.getFirst(); + } + + private String readModerationCommand(String action, String legacyTemplate, String newTemplate, boolean requiresDuration) { + String path = "moderation_actions.commands." + action; + String configured = config.getString(path, newTemplate); + if (configured == null || configured.isBlank()) { + return ""; + } + configured = configured.trim(); + if (configured.equals(legacyTemplate)) { + plugin.getLogger().info("Шаблон команды " + action + " обновлён в памяти до формата наказаний со сроком/автором."); + configured = newTemplate; + } + if (requiresDuration && !configured.contains("{duration}")) { + plugin.getLogger().warning("Команда " + action + " отключена: шаблон " + path + " должен содержать {duration}."); + return ""; + } + if (!configured.contains("{target}")) { + plugin.getLogger().warning("Команда " + action + " отключена: шаблон " + path + " должен содержать {target}."); + return ""; + } + return configured; + } } diff --git a/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java b/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java old mode 100644 new mode 100755 index bf57a3c..0de361e --- a/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java +++ b/src/main/java/org/blz/adminmode/data/AdminModeDataManager.java @@ -1,29 +1,23 @@ package org.blz.adminmode.data; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.UUID; +import org.blz.adminmode.utils.ItemStackCodec; +import org.blz.adminmode.utils.PotionEffectCodec; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -187,16 +181,7 @@ public class AdminModeDataManager { private String serializeItemArray(ItemStack[] items) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); - dataOutput.writeInt(items.length); - - for (ItemStack item : items) { - dataOutput.writeObject(item); - } - - dataOutput.close(); - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItems(items); } catch (Exception e) { e.printStackTrace(); return ""; @@ -205,17 +190,7 @@ public class AdminModeDataManager { private ItemStack[] deserializeItemArray(String data) { try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - int length = dataInput.readInt(); - ItemStack[] items = new ItemStack[length]; - - for (int i = 0; i < length; i++) { - items[i] = (ItemStack) dataInput.readObject(); - } - - dataInput.close(); - return items; + return ItemStackCodec.deserializeItems(data); } catch (Exception e) { e.printStackTrace(); return new ItemStack[0]; @@ -224,11 +199,7 @@ public class AdminModeDataManager { private String serializeItem(ItemStack item) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); - dataOutput.writeObject(item); - dataOutput.close(); - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItem(item); } catch (Exception e) { e.printStackTrace(); return ""; @@ -238,56 +209,23 @@ public class AdminModeDataManager { private ItemStack deserializeItem(String data) { try { if (data.isEmpty()) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); - ItemStack item = (ItemStack) dataInput.readObject(); - dataInput.close(); - return item; + return ItemStackCodec.deserializeItem(data); } catch (Exception e) { e.printStackTrace(); - return new ItemStack(Material.AIR); + return ItemStack.empty(); } } private String serializePotionEffects(Collection effects) { - StringBuilder sb = new StringBuilder(); - for (PotionEffect effect : effects) { - sb.append(effect.getType().getName()).append(":") - .append(effect.getDuration()).append(":") - .append(effect.getAmplifier()).append(":") - .append(effect.isAmbient()).append(":") - .append(effect.hasParticles()).append(":") - .append(effect.hasIcon()).append(";"); - } - return sb.toString(); + return PotionEffectCodec.serialize(effects); } private Collection deserializePotionEffects(String data) { - Collection effects = new ArrayList<>(); - if (data.isEmpty()) return effects; - - String[] effectStrings = data.split(";"); - for (String effectStr : effectStrings) { - if (effectStr.isEmpty()) continue; - - try { - String[] parts = effectStr.split(":"); - PotionEffectType type = PotionEffectType.getByName(parts[0]); - int duration = Integer.parseInt(parts[1]); - int amplifier = Integer.parseInt(parts[2]); - boolean ambient = Boolean.parseBoolean(parts[3]); - boolean particles = Boolean.parseBoolean(parts[4]); - boolean icon = Boolean.parseBoolean(parts[5]); - - effects.add(new PotionEffect(type, duration, amplifier, ambient, particles, icon)); - } catch (Exception e) { - plugin.getLogger().warning("Ошибка десериализации эффекта: " + effectStr); - } - } - - return effects; + return PotionEffectCodec.deserialize(data, + encodedEffect -> plugin.getLogger().warning( + "Ошибка десериализации эффекта: " + encodedEffect)); } public static class PlayerStateData { diff --git a/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java b/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java old mode 100644 new mode 100755 index fae1e6c..f65ff84 --- a/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java +++ b/src/main/java/org/blz/adminmode/dialog/AdminDialogService.java @@ -1,5 +1,7 @@ package org.blz.adminmode.dialog; +import com.destroystokyo.paper.profile.PlayerProfile; +import io.papermc.paper.ban.BanListType; import io.papermc.paper.dialog.Dialog; import io.papermc.paper.registry.data.dialog.ActionButton; import io.papermc.paper.registry.data.dialog.DialogBase; @@ -7,22 +9,32 @@ import io.papermc.paper.registry.data.dialog.action.DialogAction; import io.papermc.paper.registry.data.dialog.action.DialogActionCallback; import io.papermc.paper.registry.data.dialog.body.DialogBody; import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.input.SingleOptionDialogInput; import io.papermc.paper.registry.data.dialog.type.DialogType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.format.NamedTextColor; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; -import org.blz.adminmode.moderation.ModeratorProfile; -import org.blz.adminmode.moderation.PlayerListAction; +import org.blz.adminmode.moderation.ModeratorPreferencesStore; +import org.blz.adminmode.moderation.ModeratorWarningStore; +import org.blz.adminmode.moderation.PunishmentDuration; +import org.bukkit.BanEntry; import org.bukkit.Bukkit; -import org.bukkit.entity.Player; +import org.bukkit.GameMode; +import org.bukkit.ban.ProfileBanList; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; import java.util.List; -import java.util.UUID; +import java.util.Set; @SuppressWarnings("UnstableApiUsage") public class AdminDialogService { @@ -31,276 +43,525 @@ public class AdminDialogService { .uses(1) .lifetime(Duration.ofMinutes(5)) .build(); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + .withZone(ZoneId.systemDefault()); private final Plugin plugin; private final ConfigManager configManager; private final AdminModeCommand adminModeCommand; + private final DialogPreferenceStore dialogPreferenceStore; + private final MenuEmojiPreferenceStore emojiPreferenceStore; + private final ModeratorPreferencesStore preferencesStore; + private final ModeratorWarningStore warningStore; - public AdminDialogService(Plugin plugin, ConfigManager configManager, AdminModeCommand adminModeCommand) { + public AdminDialogService( + Plugin plugin, + ConfigManager configManager, + AdminModeCommand adminModeCommand, + DialogPreferenceStore dialogPreferenceStore, + MenuEmojiPreferenceStore emojiPreferenceStore, + ModeratorPreferencesStore preferencesStore, + ModeratorWarningStore warningStore) { this.plugin = plugin; this.configManager = configManager; this.adminModeCommand = adminModeCommand; + this.dialogPreferenceStore = dialogPreferenceStore; + this.emojiPreferenceStore = emojiPreferenceStore; + this.preferencesStore = preferencesStore; + this.warningStore = warningStore; } - public boolean openModeSelectDialog(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - List profiles = adminModeCommand.getAvailableProfiles(player); - if (profiles.isEmpty()) { - player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); - return false; - } - - if (profiles.size() == 1) { - adminModeCommand.activateProfile(player, profiles.get(0)); - return true; - } + public boolean openAdminPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { List buttons = new ArrayList<>(); - for (ModeratorProfile profile : profiles) { - buttons.add(actionButton( - profile.isHardProfile() ? "🔨 Hard Moder" : "🔍 Soft Moder", - profile.isHardProfile() - ? "Полный режим модерации, CoreProtect и сильные действия" - : "Наблюдение, spectate, invsee и безопасные действия", - (response, audience) -> withPlayer(audience, moderator -> { - adminModeCommand.activateProfile(moderator, profile); - plugin.getServer().getScheduler().runTask(plugin, () -> { - if (adminModeCommand.isInAdminMode(moderator.getUniqueId())) { - openCurrentPanel(moderator); - } - }); - }))); - } + buttons.add(actionButton(player, "🎯", "Действия с игроком", NamedTextColor.RED, + "Поиск игрока и модераторские действия", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPlayerSearchDialog(m))))); + buttons.add(actionButton(player, "📋", "Список онлайн", NamedTextColor.GOLD, + "Игроки онлайн и их координаты", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openOnlineListDialog(m))))); + buttons.add(actionButton(player, "📕", "BanList", NamedTextColor.DARK_RED, + "Посмотреть список банов и причины", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanSearchDialog(m))))); + buttons.add(actionButton(player, "🧰", "CoreProtect", NamedTextColor.AQUA, + "Инспектор, lookup, rollback и restore", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectMenu(m))))); + buttons.add(actionButton(player, "⚙", "Настройки режима", NamedTextColor.GREEN, + "Полет, vanish, текущий режим и скорости", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openSettingsPanel(m))))); + buttons.add(actionButton(player, "🪪", "Личные настройки", NamedTextColor.LIGHT_PURPLE, + "Тема, акцент, смайлики и режим по умолчанию", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPersonalSettingsPanel(m))))); + buttons.add(actionButton(player, "🚪", "Выйти из режима", NamedTextColor.RED, + "Восстановить состояние и снять права", true, + (res, aud) -> withPlayer(aud, adminModeCommand::exitMode))); - buttons.add(actionButton( - "Закрыть", - "Закрыть выбор режима", - (response, audience) -> { + DialogBase base = DialogBase.builder(title(player, "🛡", "Admin Mode", NamedTextColor.AQUA)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Панель модератора: игроки, CoreProtect, BanList, режим и личные настройки."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, closeButton(player), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть admin panel dialog: " + e.getMessage()); + return false; + } + } + + public boolean openPlayerSearchDialog(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ActionButton openButton = actionButton(player, "🔎", "Открыть список", NamedTextColor.RED, + "Показать игроков по фильтру", false, + (response, audience) -> withPlayer(audience, moderator -> { + String query = normalize(response.getText("player_query")); + schedule(() -> openManagePlayerListDialog(moderator, query)); })); - DialogBase base = DialogBase.builder(Component.text("⚙ Режим модерации")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Выберите режим входа. Soft — наблюдение и безопасная модерация. Hard — полный контроль, телепорты и CoreProtect."), - 360))) - .inputs(List.of()) + DialogBase base = DialogBase.builder(title(player, "🎯", "Поиск игрока", NamedTextColor.RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Введите часть ника или оставьте пусто для полного списка."), 360))) + .inputs(List.of(DialogInput.text("player_query", 320, bodyText(player, "Игрок"), false, "", 32, null))) .build(); - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 1)); + showDialog(player, base, DialogType.multiAction(List.of(openButton), backButton(player, () -> openAdminPanel(player)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть dialog выбора профиля: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть поиск игрока: " + e.getMessage()); return false; } } - public boolean openCurrentPanel(Player player) { - UUID playerId = player.getUniqueId(); - ModeratorProfile profile = adminModeCommand.getActiveProfile(playerId); - if (profile == null) { - return false; - } - - if (profile.isHardProfile()) { - return openHardPanel(player); - } - - return openSoftPanel(player); - } - - public boolean openSoftPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } + public boolean openManagePlayerListDialog(Player moderator, String query) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; try { - List buttons = new ArrayList<>(); - buttons.add(actionButton( - "👁 Spectate игрока", - "Выбрать игрока для режима наблюдения", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.SPECTATE))))); - buttons.add(actionButton( - "📍 TP к игроку", - "Выбрать игрока для телепортации", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.TELEPORT))))); - buttons.add(actionButton( - "🎒 Invsee игрока", - "Открыть инвентарь игрока только для просмотра", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.INVSEE))))); - buttons.add(actionButton( - "📋 Список онлайн", - "Показать онлайн-игроков и их координаты", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openOnlineListDialog(moderator))))); - buttons.add(actionButton( - "🎛 Настройки режима", - "Скорость полёта, ходьбы и бессмертие", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openSpeedPanel(moderator))))); - buttons.add(actionButton( - "🚪 Выйти из режима", - "Восстановить сохраненное состояние и снять временные права", - (response, audience) -> withPlayer(audience, moderator -> adminModeCommand.exitMode(moderator)))); - - List lines = new ArrayList<>(); - lines.add(Component.text("Soft Moder предназначен для наблюдения, инвентарей и безопасной телепортации.")); - lines.add(Component.text("Используйте компас в руке как panel item или /adminmode для повторного открытия.")); - - DialogBase base = DialogBase.builder(Component.text("🔍 Soft Moder")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 2)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть soft panel dialog: " + exception.getMessage()); - return false; - } - } - - public boolean openHardPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - - try { - List buttons = new ArrayList<>(); - buttons.add(actionButton( - "👁 Spectate игрока", - "Выбрать игрока для режима наблюдения", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.SPECTATE))))); - buttons.add(actionButton( - "📍 TP к игроку", - "Телепорт к выбранному игроку", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.TELEPORT))))); - buttons.add(actionButton( - "🎒 Invsee игрока", - "Открыть инвентарь игрока только для просмотра", - (response, audience) -> withPlayer(audience, moderator -> schedule( - () -> openPlayerListDialog(moderator, PlayerListAction.INVSEE))))); - buttons.add(actionButton( - "📋 Список онлайн", - "Показать онлайн-игроков и координаты", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openOnlineListDialog(moderator))))); - buttons.add(actionButton( - "🔍 Lookup логи CP", - "Открыть форму поиска по логам CoreProtect", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openLookupDialog(moderator))))); - buttons.add(actionButton( - "↩ Откат области CP", - "Настроить rollback с подтверждением", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openRollbackDialog(moderator))))); - buttons.add(actionButton( - "🎛 Настройки режима", - "Скорость полёта, ходьбы и бессмертие", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openSpeedPanel(moderator))))); - buttons.add(actionButton( - "🚪 Выйти из режима", - "Восстановить сохраненное состояние и выйти из hard режима", - (response, audience) -> withPlayer(audience, moderator -> adminModeCommand.exitMode(moderator)))); - - List lines = new ArrayList<>(); - lines.add(Component.text("Hard Moder включает полный доступ к сильным действиям и CoreProtect.")); - lines.add(Component.text("Используйте Nether Star для панели и Stick для переключения co inspect.")); - - DialogBase base = DialogBase.builder(Component.text("🔨 Hard Moder")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(buttons, closeButton(), 2)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть hard panel dialog: " + exception.getMessage()); - return false; - } - } - - public boolean openPlayerListDialog(Player player, PlayerListAction action) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - - try { - List buttons = new ArrayList<>(); + List filtered = new ArrayList<>(); for (Player target : Bukkit.getOnlinePlayers()) { - if (target.getUniqueId().equals(player.getUniqueId())) { - continue; + if (target.getUniqueId().equals(moderator.getUniqueId())) continue; + if (matchesQuery(target.getName(), query)) { + filtered.add(target); } + } + filtered.sort(Comparator.comparing(Player::getName, String.CASE_INSENSITIVE_ORDER)); + List buttons = new ArrayList<>(); + for (Player target : filtered) { buttons.add(actionButton( - target.getName(), - target.getWorld().getName() + " | " + target.getLocation().getBlockX() + ", " - + target.getLocation().getBlockY() + ", " + target.getLocation().getBlockZ(), - (response, audience) -> withPlayer(audience, moderator -> - adminModeCommand.performPlayerListAction(moderator, action, target.getName())))); + playerLabel(moderator, target.getName(), NamedTextColor.WHITE, false), + bodyText(moderator, target.getWorld().getName() + " | " + target.getLocation().getBlockX() + ", " + + target.getLocation().getBlockY() + ", " + target.getLocation().getBlockZ()), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openManagePlayerDialog(m, target))))); } if (buttons.isEmpty()) { - buttons.add(actionButton( - "Нет целей", - "Кроме вас сейчас никого нет онлайн", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator))))); + buttons.add(actionButton(playerLabel(moderator, "Никого не найдено", NamedTextColor.DARK_GRAY, false), + bodyText(moderator, "Измените фильтр поиска"), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPlayerSearchDialog(m))))); } - ActionButton backButton = actionButton( - "Назад", - "Вернуться к основной панели", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); - - DialogBase base = DialogBase.builder(Component.text(dialogTitleForAction(action))) + DialogBase base = DialogBase.builder(title(moderator, "👥", "Игроки", NamedTextColor.RED)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text(dialogBodyForAction(action)), 360))) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, query.isBlank() + ? "Все игроки онлайн." + : "Результаты поиска по фильтру: " + query), 360))) .inputs(List.of()) .build(); - showDialog(player, base, DialogType.multiAction(buttons, backButton, 2)); + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openPlayerSearchDialog(moderator)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть список игроков: " + e.getMessage()); + return false; + } + } + + public boolean openManagePlayerDialog(Player moderator, Player target) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + List recentWarnings = warningStore.getRecentWarnings(target.getUniqueId(), 3); + int activeWarnCount = warningStore.getActiveWarnings(target.getUniqueId()).size(); + + List buttons = new ArrayList<>(); + buttons.add(actionButton(moderator, "👁", "Spectate игрока", NamedTextColor.AQUA, + "Наблюдать за игроком", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.SPECTATE, target.getName())))); + buttons.add(actionButton(moderator, "📍", "TP к игроку", NamedTextColor.GREEN, + "Телепортироваться к игроку", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.TELEPORT, target.getName())))); + buttons.add(actionButton(moderator, "🎒", "InvSee игрока", NamedTextColor.GOLD, + "Открыть инвентарь игрока только для чтения", false, + (res, aud) -> withPlayer(aud, m -> adminModeCommand.performPlayerListAction(m, org.blz.adminmode.moderation.PlayerListAction.INVSEE, target.getName())))); + buttons.add(actionButton( + moderator, + adminModeCommand.isFrozen(target.getUniqueId()) ? "🧊" : "❄", + adminModeCommand.isFrozen(target.getUniqueId()) ? "Разморозить" : "Заморозить", + NamedTextColor.AQUA, + "Полный freeze игрока", + true, + (res, aud) -> withPlayer(aud, m -> { + boolean frozen = adminModeCommand.toggleFreeze(target); + m.sendMessage(Component.text(frozen ? "§bИгрок " + target.getName() + " заморожен." : "§aИгрок " + target.getName() + " разморожен.")); + schedule(() -> openManagePlayerDialog(m, target)); + }))); + buttons.add(actionButton(moderator, "📝", "Последние warn", NamedTextColor.YELLOW, + "Открыть последние предупреждения игрока", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnListDialog(m, target, false))))); + buttons.add(actionButton(moderator, "📌", "Активные warn", NamedTextColor.GOLD, + "Открыть активные предупреждения игрока", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnListDialog(m, target, true))))); + if (adminModeCommand.canManageModeratorAccess(moderator, target)) { + if (adminModeCommand.isModeratorRestricted(target.getUniqueId())) { + buttons.add(actionButton(moderator, "🔓", "Вернуть admin mode", NamedTextColor.GREEN, + "Снять ограничение с модератора", true, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.unrestrictModerator(m, target); + schedule(() -> openManagePlayerDialog(m, target)); + }))); + } else { + buttons.add(actionButton(moderator, "🔒", "Забрать admin mode", NamedTextColor.RED, + "Ограничить модератору доступ к admin mode", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openRestrictModeratorDialog(m, target))))); + } + } + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.WARN); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.MUTE); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.UNMUTE); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.KICK); + addPunishmentActionButton(buttons, moderator, target, PunishmentAction.BAN); + + List bodyLines = new ArrayList<>(); + bodyLines.add(bodyLine(moderator, "Мир: " + target.getWorld().getName(), false)); + bodyLines.add(bodyLine(moderator, "Режим: " + target.getGameMode().name(), false)); + if (target.hasPermission("adminmode.use")) { + bodyLines.add(bodyLine(moderator, "Модератор: да", true)); + bodyLines.add(bodyLine(moderator, + "Доступ к admin mode: " + (adminModeCommand.isModeratorRestricted(target.getUniqueId()) ? "ограничен" : "разрешён"), + true)); + } + bodyLines.add(bodyLine(moderator, "Активных warn: " + activeWarnCount, true)); + if (recentWarnings.isEmpty()) { + bodyLines.add(bodyLine(moderator, "Последние warn: нет записей", false)); + } else { + bodyLines.add(bodyLine(moderator, "Последние warn:", true)); + for (ModeratorWarningStore.WarningEntry warning : recentWarnings) { + bodyLines.add(bodyLine(moderator, "• " + formatWarnInline(warning), false)); + } + } + + DialogBase base = DialogBase.builder(title(moderator, "🎯", "Действия: " + target.getName(), NamedTextColor.RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(bodyLines), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openPlayerSearchDialog(moderator)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть manage dialog: " + e.getMessage()); + return false; + } + } + + public boolean openRestrictModeratorDialog(Player moderator, Player target) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + if (!adminModeCommand.canManageModeratorAccess(moderator, target)) return false; + try { + ActionButton applyButton = actionButton(moderator, "🔒", "Ограничить", NamedTextColor.RED, + "Снять модератору доступ к admin mode", true, + (response, audience) -> withPlayer(audience, m -> { + String duration = normalize(response.getText("restrict_duration")); + String reason = sanitizeReason(response.getText("restrict_reason")); + adminModeCommand.restrictModerator(m, target, duration, reason); + schedule(() -> openManagePlayerDialog(m, target)); + })); + + DialogBase base = DialogBase.builder(title(moderator, "🔒", "Ограничение: " + target.getName(), NamedTextColor.RED)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(List.of( + bodyLine(moderator, "Главный модератор может временно или навсегда забрать доступ к admin mode.", true), + bodyLine(moderator, "Поддерживаются сроки от 1s до 1y, либо perm.", false))), 360))) + .inputs(List.of( + DialogInput.text("restrict_duration", 180, bodyText(moderator, "Срок"), true, "30m", 16, null), + DialogInput.text("restrict_reason", 320, bodyText(moderator, "Причина"), false, "", 128, null))) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(applyButton), backButton(moderator, () -> openManagePlayerDialog(moderator, target)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог ограничения для " + target.getName() + ": " + e.getMessage()); + return false; + } + } + + public boolean openWarnListDialog(Player moderator, Player target, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + List warnings = activeOnly + ? warningStore.getActiveWarnings(target.getUniqueId()) + : warningStore.getRecentWarnings(target.getUniqueId(), 12); + + List buttons = new ArrayList<>(); + for (ModeratorWarningStore.WarningEntry warning : warnings) { + buttons.add(actionButton( + playerLabel(moderator, formatWarnHeader(warning), NamedTextColor.WHITE, true), + bodyText(moderator, warningStatusLabel(warning) + " | " + warning.reason()), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnDetailsDialog(m, target, warning, activeOnly))))); + } + + if (buttons.isEmpty()) { + buttons.add(actionButton(playerLabel(moderator, "Warn не найдены", NamedTextColor.DARK_GRAY, false), + bodyText(moderator, "У игрока пока нет записей."), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openManagePlayerDialog(m, target))))); + } + + DialogBase base = DialogBase.builder(title(moderator, activeOnly ? "📌" : "📝", + (activeOnly ? "Активные warn: " : "Последние warn: ") + target.getName(), + NamedTextColor.YELLOW)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + activeOnly ? "Список активных предупреждений игрока." : "История последних предупреждений игрока."), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(buttons, backButton(moderator, () -> openManagePlayerDialog(moderator, target)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть список warn для " + target.getName() + ": " + e.getMessage()); + return false; + } + } + + public boolean openWarnDetailsDialog(Player moderator, Player target, ModeratorWarningStore.WarningEntry warning, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId())) return false; + try { + ModeratorWarningStore.WarningEntry current = warningStore.findWarning(warning.id()).orElse(null); + if (current == null) { + moderator.sendMessage(configManager.getPrefixedMessage("§eЗапись warn больше не существует.")); + schedule(() -> openWarnListDialog(moderator, target, activeOnly)); + return true; + } + + List lines = new ArrayList<>(); + lines.add(bodyLine(moderator, "Игрок: " + current.playerName(), false)); + lines.add(bodyLine(moderator, "Модератор: " + current.moderatorName(), false)); + lines.add(bodyLine(moderator, "Выдан: " + formatTimestamp(current.createdAt()), false)); + lines.add(bodyLine(moderator, "Истекает: " + formatWarnExpiration(current), false)); + lines.add(bodyLine(moderator, "Статус: " + warningStatusLabel(current), true)); + lines.add(bodyLine(moderator, "Причина: " + current.reason(), false)); + if (current.status() == ModeratorWarningStore.WarningStatus.REVOKED) { + lines.add(bodyLine(moderator, "Снят: " + formatOptionalTimestamp(current.revokedAt()), false)); + lines.add(bodyLine(moderator, "Кем снят: " + blankFallback(current.revokedBy(), "неизвестно"), false)); + lines.add(bodyLine(moderator, "Причина снятия: " + blankFallback(current.revocationReason(), "не указана"), false)); + } + + List actions = new ArrayList<>(); + if (current.status() == ModeratorWarningStore.WarningStatus.ACTIVE + && moderator.hasPermission(PunishmentAction.UNWARN.permission)) { + actions.add(actionButton(moderator, "✅", "Снять warn", NamedTextColor.GREEN, + "Архивировать активное предупреждение", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openWarnRevocationDialog(m, target, current, activeOnly))))); + } + + DialogBase base = DialogBase.builder(title(moderator, "📄", "Warn: " + target.getName(), NamedTextColor.YELLOW)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(actions, + backButton(moderator, () -> openWarnListDialog(moderator, target, activeOnly)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть детали warn: " + e.getMessage()); + return false; + } + } + + private boolean openWarnRevocationDialog(Player moderator, Player target, + ModeratorWarningStore.WarningEntry warning, boolean activeOnly) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(PunishmentAction.UNWARN.permission)) { + return false; + } + try { + ActionButton confirmButton = actionButton(moderator, "✅", "Подтвердить снятие", NamedTextColor.GREEN, + "Снять предупреждение и сохранить его в истории", true, + (response, audience) -> withPlayer(audience, m -> schedule(() -> { + if (!adminModeCommand.isInAdminMode(m.getUniqueId()) + || !m.hasPermission(PunishmentAction.UNWARN.permission)) { + m.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + String reason = sanitizeReason(response.getText("revocation_reason")); + if (reason.isBlank()) { + m.sendMessage(configManager.getPrefixedMessage("§cУкажите причину снятия warn.")); + openWarnRevocationDialog(m, target, warning, activeOnly); + return; + } + if (!warningStore.revokeWarning(warning.id(), m, reason)) { + m.sendMessage(configManager.getPrefixedMessage("§eWarn уже снят или истёк.")); + } else { + m.sendMessage(configManager.getPrefixedMessage("§aWarn для §f" + warning.playerName() + "§a снят и сохранён в истории.")); + } + openWarnListDialog(m, target, activeOnly); + }))); + + DialogBase base = DialogBase.builder(title(moderator, "✅", "Снять warn: " + warning.playerName(), NamedTextColor.GREEN)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + "Warn останется в истории со сведениями о снятии."), 360))) + .inputs(List.of(DialogInput.text("revocation_reason", 320, + bodyText(moderator, "Причина снятия"), true, "", 256, null))) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(confirmButton), + backButton(moderator, () -> openWarnDetailsDialog(moderator, target, warning, activeOnly)), 1)); return true; } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть список игроков: " + exception.getMessage()); + plugin.getLogger().warning("Не удалось открыть снятие warn: " + exception.getMessage()); return false; } } public boolean openOnlineListDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List lines = adminModeCommand.getOnlineSummary(player); + player.sendMessage(Component.text("--- Список игроков онлайн ---", themedColor(player, NamedTextColor.GOLD, true))); + for (Component line : lines) player.sendMessage(line); + player.sendMessage(Component.text("----------------------------------", themedColor(player, NamedTextColor.GOLD, true))); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось вывести онлайн список: " + e.getMessage()); return false; } + } + public boolean openBanSearchDialog(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { - List lines = new ArrayList<>(adminModeCommand.getOnlineSummary(player)); - ActionButton backButton = actionButton( - "Назад", - "Вернуться к панели режима", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); + ActionButton searchButton = actionButton(player, "🔎", "Открыть BanList", NamedTextColor.DARK_RED, + "Показать баны по фильтру", true, + (response, audience) -> withPlayer(audience, moderator -> { + String query = normalize(response.getText("ban_query")); + schedule(() -> openBanListDialog(moderator, query)); + })); - DialogBase base = DialogBase.builder(Component.text("📋 Онлайн список")) + DialogBase base = DialogBase.builder(title(player, "📕", "Поиск в BanList", NamedTextColor.DARK_RED)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, "Введите ник или его часть. Пусто = все баны."), 360))) + .inputs(List.of(DialogInput.text("ban_query", 320, bodyText(player, "Игрок"), false, "", 32, null))) + .build(); + + showDialog(player, base, DialogType.multiAction(List.of(searchButton), backButton(player, () -> openAdminPanel(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть поиск по банам: " + e.getMessage()); + return false; + } + } + + public boolean openBanListDialog(Player player) { + return openBanListDialog(player, ""); + } + + public boolean openBanListDialog(Player player, String query) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ProfileBanList banList = Bukkit.getBanList(BanListType.PROFILE); + Set> entries = banList.getEntries(); + List> sortedEntries = new ArrayList<>(); + for (BanEntry entry : entries) { + String targetName = profileName(entry.getBanTarget()); + if (!isExpiredBan(entry) && matchesQuery(targetName, query)) { + sortedEntries.add(entry); + } + } + sortedEntries.sort(Comparator.comparing( + entry -> profileName(entry.getBanTarget()), + String.CASE_INSENSITIVE_ORDER)); + + List buttons = new ArrayList<>(); + for (BanEntry entry : sortedEntries) { + PlayerProfile target = entry.getBanTarget(); + String targetName = profileName(target); + String targetIdentifier = profileIdentifier(target); + buttons.add(actionButton( + playerLabel(player, targetName, NamedTextColor.WHITE, true), + bodyText(player, buildBanSummary(entry)), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanDetailsDialog(m, targetIdentifier))))); + } + + if (buttons.isEmpty()) { + buttons.add(actionButton(playerLabel(player, "Баны не найдены", NamedTextColor.DARK_GRAY, false), + bodyText(player, "Измените фильтр поиска"), + (res, aud) -> withPlayer(aud, m -> schedule(() -> openBanSearchDialog(m))))); + } + + DialogBase base = DialogBase.builder(title(player, "📕", "BanList", NamedTextColor.DARK_RED)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, query.isBlank() + ? "Все активные баны." + : "Результаты поиска по фильтру: " + query), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openBanSearchDialog(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть banlist dialog: " + e.getMessage()); + return false; + } + } + + public boolean openBanDetailsDialog(Player player, String targetName) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ProfileBanList banList = Bukkit.getBanList(BanListType.PROFILE); + BanEntry entry = findBanEntry(banList, targetName); + if (entry == null) { + player.sendMessage(Component.text("§eБан для " + targetName + " уже снят.")); + schedule(() -> openBanListDialog(player)); + return true; + } + + String displayName = profileName(entry.getBanTarget()); + String targetIdentifier = profileIdentifier(entry.getBanTarget()); + List lines = new ArrayList<>(); + lines.add(bodyLine(player, "Игрок: " + displayName, false)); + lines.add(bodyLine(player, "Причина: " + blankFallback(entry.getReason(), "не указана"), true)); + lines.add(bodyLine(player, "Кем выдан: " + blankFallback(entry.getSource(), "неизвестно"), false)); + lines.add(bodyLine(player, "Выдан: " + formatDate(entry.getCreated()), false)); + lines.add(bodyLine(player, "Истекает: " + formatDate(entry.getExpiration()), false)); + + List actions = new ArrayList<>(); + if (player.hasPermission(PunishmentAction.UNBAN.permission)) { + actions.add(actionButton(player, "✅", "Разбанить", NamedTextColor.GREEN, + "Открыть подтверждение снятия бана", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> + openUnbanConfirmationDialog(m, targetIdentifier, displayName))))); + } + + DialogBase base = DialogBase.builder(title(player, "📄", "Бан: " + displayName, NamedTextColor.DARK_RED)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) @@ -308,217 +569,453 @@ public class AdminDialogService { .inputs(List.of()) .build(); - showDialog(player, base, DialogType.multiAction(List.of(), backButton, 1)); + showDialog(player, base, DialogType.multiAction(actions, + backButton(player, () -> openBanListDialog(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть детали бана " + targetName + ": " + e.getMessage()); + return false; + } + } + + private boolean openUnbanConfirmationDialog(Player moderator, String targetIdentifier, String displayName) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(PunishmentAction.UNBAN.permission)) { + return false; + } + try { + ActionButton confirmButton = actionButton(moderator, "✅", "Подтвердить разбан", NamedTextColor.GREEN, + "Удалить профиль из Paper BanList", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> { + if (!adminModeCommand.isInAdminMode(m.getUniqueId()) + || !m.hasPermission(PunishmentAction.UNBAN.permission)) { + m.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + BanEntry currentEntry = findBanEntry( + Bukkit.getBanList(BanListType.PROFILE), targetIdentifier); + if (currentEntry == null) { + m.sendMessage(configManager.getPrefixedMessage("§eБан для " + displayName + " уже снят.")); + } else { + currentEntry.remove(); + plugin.getLogger().info("UNBAN | Moderator: " + m.getName() + " | Player: " + displayName); + m.sendMessage(configManager.getPrefixedMessage("§aИгрок §f" + displayName + "§a разбанен.")); + } + openBanListDialog(m); + }))); + + DialogBase base = DialogBase.builder(title(moderator, "✅", "Разбанить: " + displayName, NamedTextColor.GREEN)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + "Подтвердите снятие активного бана из Paper BanList."), 360))) + .inputs(List.of()) + .build(); + + showDialog(moderator, base, DialogType.multiAction(List.of(confirmButton), + backButton(moderator, () -> openBanDetailsDialog(moderator, targetIdentifier)), 1)); return true; } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть онлайн список: " + exception.getMessage()); + plugin.getLogger().warning("Не удалось открыть подтверждение разбана " + displayName + ": " + exception.getMessage()); + return false; + } + } + + public boolean openCoreProtectMenu(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, "🕵", "Inspector", NamedTextColor.AQUA, + "Переключить режим инспектора CoreProtect", false, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.toggleInspector(m); + schedule(() -> openCoreProtectMenu(m)); + }))); + buttons.add(actionButton(player, "🔎", "Lookup", NamedTextColor.GREEN, + "Поиск в логах CoreProtect", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "lookup"))))); + buttons.add(actionButton(player, "↩", "Rollback", NamedTextColor.RED, + "Откатить изменения по фильтрам", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "rollback"))))); + buttons.add(actionButton(player, "♻", "Restore", NamedTextColor.YELLOW, + "Вернуть изменения по фильтрам", true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openCoreProtectActionDialog(m, "restore"))))); + + DialogBase base = DialogBase.builder(title(player, "🧰", "CoreProtect", NamedTextColor.AQUA)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Полный набор действий CoreProtect: inspector, lookup, rollback, restore."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть меню CoreProtect: " + e.getMessage()); + return false; + } + } + + public boolean openCoreProtectActionDialog(Player player, String operation) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ActionButton confirmButton = actionButton(player, operation.equals("lookup") ? "🔎" : operation.equals("rollback") ? "↩" : "♻", + operation.equals("lookup") ? "Выполнить Lookup" : operation.equals("rollback") ? "Подтвердить Rollback" : "Подтвердить Restore", + operation.equals("lookup") ? NamedTextColor.GREEN : operation.equals("rollback") ? NamedTextColor.RED : NamedTextColor.YELLOW, + "Выполнить действие CoreProtect", + true, + (response, audience) -> withPlayer(audience, m -> { + String targetPlayer = normalize(response.getText("cp_player")); + String action = normalize(response.getText("cp_action")); + String block = normalize(response.getText("cp_block")); + String exclude = normalize(response.getText("cp_exclude")); + String world = normalize(response.getText("cp_world")); + String extra = normalize(response.getText("cp_extra")); + int radius = parseInt(response.getText("cp_radius"), configManager.getCoreProtectDefaultRadius()); + String time = normalizeTime(response.getText("cp_time"), configManager.getCoreProtectDefaultTime()); + + switch (operation) { + case "lookup" -> adminModeCommand.executeCPLookup(m, targetPlayer, action, block, exclude, world, radius, time); + case "rollback" -> adminModeCommand.executeCPRollback(m, radius, time, targetPlayer, action, block, exclude, world, extra); + case "restore" -> adminModeCommand.executeCPRestore(m, radius, time, targetPlayer, action, block, exclude, world, extra); + default -> { } + } + schedule(() -> openCoreProtectMenu(m)); + })); + + DialogBase base = DialogBase.builder(title(player, "🧰", "CoreProtect: " + operation.toUpperCase(), NamedTextColor.AQUA)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Фильтры: player, action, block, exclude, world и extra (через ;)."), 360))) + .inputs(List.of( + DialogInput.text("cp_player", 260, bodyText(player, "Игрок"), false, "", 32, null), + DialogInput.text("cp_action", 260, bodyText(player, "Action"), false, "", 48, null), + DialogInput.text("cp_block", 260, bodyText(player, "Block"), false, "", 64, null), + DialogInput.text("cp_exclude", 260, bodyText(player, "Exclude"), false, "", 64, null), + DialogInput.text("cp_world", 260, bodyText(player, "World"), false, "", 48, null), + DialogInput.text("cp_extra", 260, bodyText(player, "Extra filters (; separated)"), false, "", 128, null), + DialogInput.text("cp_radius", 180, bodyText(player, "Радиус"), true, + String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), + DialogInput.text("cp_time", 180, bodyText(player, "Время"), true, + configManager.getCoreProtectDefaultTime(), 8, null))) + .build(); + + showDialog(player, base, DialogType.multiAction(List.of(confirmButton), backButton(player, () -> openCoreProtectMenu(player)), 1)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог CoreProtect " + operation + ": " + e.getMessage()); + return false; + } + } + + public boolean openSettingsPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, player.getAllowFlight() ? "🕊" : "🪶", + player.getAllowFlight() ? "Полет: ВКЛ" : "Полет: ВЫКЛ", + player.getAllowFlight() ? NamedTextColor.GREEN : NamedTextColor.RED, + "Переключить полет", false, + (res, aud) -> withPlayer(aud, m -> { + m.setAllowFlight(!m.getAllowFlight()); + m.setFlying(m.getAllowFlight()); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, player.getGameMode() == GameMode.SURVIVAL ? "🧍" : "👁", + player.getGameMode() == GameMode.SURVIVAL ? "Режим: SURVIVAL" : "Режим: SPECTATOR", + player.getGameMode() == GameMode.SURVIVAL ? NamedTextColor.GREEN : NamedTextColor.AQUA, + "Переключить SURVIVAL/SPECTATOR", false, + (res, aud) -> withPlayer(aud, m -> { + m.setGameMode(m.getGameMode() == GameMode.SURVIVAL ? GameMode.SPECTATOR : GameMode.SURVIVAL); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, adminModeCommand.isVanished(player.getUniqueId()) ? "🫥" : "👁", + adminModeCommand.isVanished(player.getUniqueId()) ? "Vanish: ВКЛ" : "Vanish: ВЫКЛ", + adminModeCommand.isVanished(player.getUniqueId()) ? NamedTextColor.AQUA : NamedTextColor.GRAY, + "Переключить vanish", true, + (res, aud) -> withPlayer(aud, m -> { + adminModeCommand.toggleVanish(m); + schedule(() -> openSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "🎛", "Скорости и бессмертие", NamedTextColor.LIGHT_PURPLE, + "Тонкая настройка режима", false, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openSpeedPanel(m))))); + + DialogBase base = DialogBase.builder(title(player, "⚙", "Настройки режима", NamedTextColor.GREEN)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(player, + "Управление полетом, vanish, текущим режимом и боевыми параметрами."), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть настройки режима: " + e.getMessage()); + return false; + } + } + + public boolean openPersonalSettingsPanel(Player player) { + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; + try { + ModeratorPreferencesStore.ModeratorPreferences preferences = preferencesStore.get(player.getUniqueId()); + + List buttons = new ArrayList<>(); + buttons.add(actionButton(player, "🎮", + "Режим по умолчанию: " + preferences.defaultEntryMode().displayName(), + NamedTextColor.GREEN, + "Цикл: По серверу -> Survival -> Spectator", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleDefaultEntryMode(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "🎨", + "Тема: " + preferences.theme().displayName(), + NamedTextColor.LIGHT_PURPLE, + "Радужная, светлая акцентная или белая", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleTheme(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, "💡", + "Акцент: " + preferences.accentColor().displayName(), + preferences.accentColor().textColor(), + "Цвет важных элементов для светлой темы", + true, + (res, aud) -> withPlayer(aud, m -> { + preferencesStore.cycleAccentColor(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + buttons.add(actionButton(player, emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "😀" : "□", + emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "Смайлики: ВКЛ" : "Смайлики: ВЫКЛ", + emojiPreferenceStore.isEnabled(player.getUniqueId()) ? NamedTextColor.YELLOW : NamedTextColor.GRAY, + "Включить или выключить смайлики в меню", false, + (res, aud) -> withPlayer(aud, m -> { + emojiPreferenceStore.toggle(m.getUniqueId()); + schedule(() -> openPersonalSettingsPanel(m)); + }))); + + DialogBase base = DialogBase.builder(title(player, "🪪", "Личные настройки", NamedTextColor.LIGHT_PURPLE)) + .canCloseWithEscape(true) + .pause(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(joinLines(List.of( + bodyLine(player, "Персональные настройки модератора сохраняются между перезаходами.", false), + bodyLine(player, "Текущая тема: " + preferences.theme().displayName(), true), + bodyLine(player, "Акцентный цвет: " + preferences.accentColor().displayName(), preferences.theme() == ModeratorPreferencesStore.MenuTheme.CLEAN_ACCENT), + bodyLine(player, "Смайлики: " + (emojiPreferenceStore.isEnabled(player.getUniqueId()) ? "включены" : "выключены"), false))), 360))) + .inputs(List.of()) + .build(); + + showDialog(player, base, DialogType.multiAction(buttons, backButton(player, () -> openAdminPanel(player)), 2)); + return true; + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть личные настройки: " + e.getMessage()); return false; } } public boolean openSpeedPanel(Player player) { - if (!configManager.isDialogsEnabled()) { - return false; - } - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } - + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return false; try { float currentFly = clamp(player.getFlySpeed(), 0.1f, 1.0f); float currentWalk = clamp(player.getWalkSpeed(), 0.1f, 1.0f); - ActionButton applyButton = actionButton( - "Применить", - "Сохранить выбранные значения скорости и бессмертия", - (response, audience) -> withPlayer(audience, moderator -> { + ActionButton applyButton = actionButton(player, "✅", "Применить", NamedTextColor.GREEN, + "Сохранить выбранные значения", true, + (response, audience) -> withPlayer(audience, m -> { Float flySpeed = response.getFloat("fly_speed"); Float walkSpeed = response.getFloat("walk_speed"); Boolean godMode = response.getBoolean("god_mode"); adminModeCommand.applyAdminSettings( - moderator, + m, flySpeed == null ? currentFly : flySpeed, walkSpeed == null ? currentWalk : walkSpeed, - godMode != null && godMode); - schedule(() -> openCurrentPanel(moderator)); + godMode == null || godMode); + schedule(() -> openSettingsPanel(m)); })); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в основную панель модератора", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openCurrentPanel(moderator)))); - - DialogBase base = DialogBase.builder(Component.text("🎛 Настройка режима")) + DialogBase base = DialogBase.builder(title(player, "🎛", "Скорости и бессмертие", NamedTextColor.LIGHT_PURPLE)) .canCloseWithEscape(true) .pause(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Настройте скорость полёта, скорость ходьбы и бессмертие для текущей сессии модерации."), - 360))) + .body(List.of(DialogBody.plainMessage(bodyText(player, "Настройте скорость полёта, ходьбы и бессмертие."), 360))) .inputs(List.of( - DialogInput.numberRange( - "fly_speed", - 320, - Component.text("Скорость полета"), - "%s: %s", - 0.1f, - 1.0f, - currentFly, - 0.05f), - DialogInput.numberRange( - "walk_speed", - 320, - Component.text("Скорость ходьбы"), - "%s: %s", - 0.1f, - 1.0f, - currentWalk, - 0.05f), - DialogInput.bool( - "god_mode", - Component.text("Бессмертие"), - player.isInvulnerable(), - "true", - "false"))) + DialogInput.numberRange("fly_speed", 320, bodyText(player, "Скорость полета"), + "%s: %s", 0.1f, 1.0f, currentFly, 0.05f), + DialogInput.numberRange("walk_speed", 320, bodyText(player, "Скорость ходьбы"), + "%s: %s", 0.1f, 1.0f, currentWalk, 0.05f), + DialogInput.bool("god_mode", bodyText(player, "Бессмертие"), + player.isInvulnerable(), "true", "false"))) .build(); - showDialog(player, base, DialogType.multiAction(List.of(applyButton), backButton, 1)); + showDialog(player, base, DialogType.multiAction(List.of(applyButton), backButton(player, () -> openSettingsPanel(player)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть dialog настройки скоростей: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть dialog настройки: " + e.getMessage()); return false; } } - public boolean openLookupDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { + private void addPunishmentActionButton(List buttons, Player moderator, Player target, + PunishmentAction action) { + String template = punishmentTemplate(action); + if (template == null || template.isBlank() || !moderator.hasPermission(action.permission)) { + return; + } + buttons.add(actionButton(moderator, action.emoji, action.label, action.color, action.tooltip, true, + (res, aud) -> withPlayer(aud, m -> schedule(() -> openPunishmentActionDialog(m, target, action))))); + } + + private boolean openPunishmentActionDialog(Player moderator, Player target, PunishmentAction action) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(action.permission)) { return false; } - try { - ActionButton lookupButton = actionButton( - "🔍 Найти", - "Выполнить co lookup с указанными параметрами", - (response, audience) -> withPlayer(audience, moderator -> { - String targetName = normalizeTarget(response.getText("cp_player")); - Integer radius = parseInteger(response.getText("cp_radius"), configManager.getCoreProtectDefaultRadius()); - String time = normalizeTime(response.getText("cp_time"), configManager.getCoreProtectDefaultTime()); - adminModeCommand.executeCPLookup(moderator, targetName, radius, time); - schedule(() -> openHardPanel(moderator)); + ActionButton applyButton = actionButton(moderator, "✅", "Подтвердить", NamedTextColor.GREEN, + "Применить действие", true, + (response, audience) -> withPlayer(audience, m -> { + String reason = sanitizeReason(response.getText("reason")); + String duration = action.hasDuration + ? PunishmentDuration.normalizeToken(response.getText("punishment_duration")) + : ""; + schedule(() -> executePunishment(m, target, action, duration, reason)); })); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в Hard Moder panel", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openHardPanel(moderator)))); + List inputs = new ArrayList<>(); + if (action.hasDuration) { + String selected = defaultPunishmentDuration(action); + List options = configManager.getPunishmentDurationOptions().stream() + .map(token -> SingleOptionDialogInput.OptionEntry.create( + token, + bodyText(moderator, punishmentDurationLabel(token)), + token.equals(selected))) + .toList(); + inputs.add(DialogInput.singleOption("punishment_duration", 300, options, + bodyText(moderator, "Срок действия"), true)); + } + inputs.add(DialogInput.text("reason", 320, bodyText(moderator, "Причина"), false, "", 256, null)); - DialogBase base = DialogBase.builder(Component.text("🔍 Поиск в логах CP")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Фильтр по игроку опционален. Радиус и время можно оставить стандартными."), 360))) - .inputs(List.of( - DialogInput.text("cp_player", 300, Component.text("Игрок (пусто = все)"), false, "", 16, null), - DialogInput.text("cp_radius", 180, Component.text("Радиус"), true, - String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), - DialogInput.text("cp_time", 180, Component.text("Время"), true, - configManager.getCoreProtectDefaultTime(), 4, null))) + DialogBase base = DialogBase.builder(title(moderator, action.emoji, action.title, action.color)) + .canCloseWithEscape(true).pause(false).afterAction(DialogBase.DialogAfterAction.CLOSE) + .body(List.of(DialogBody.plainMessage(bodyText(moderator, + (action.hasDuration ? "Выберите срок и укажите причину для " : "Укажите причину для ") + + target.getName()), 360))) + .inputs(inputs) .build(); - showDialog(player, base, DialogType.multiAction(List.of(lookupButton), backButton, 1)); + showDialog(moderator, base, DialogType.multiAction(List.of(applyButton), + backButton(moderator, () -> openManagePlayerOrSearch(moderator, target)), 1)); return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть lookup dialog: " + exception.getMessage()); + } catch (Exception e) { + plugin.getLogger().warning("Не удалось открыть диалог действия " + action.title + ": " + e.getMessage()); return false; } } - public boolean openRollbackDialog(Player player) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; + private void executePunishment(Player moderator, Player target, PunishmentAction action, String duration, String reason) { + if (!adminModeCommand.isInAdminMode(moderator.getUniqueId()) + || !moderator.hasPermission(action.permission)) { + moderator.sendMessage(configManager.getPrefixedMessage(configManager.getMsgNoPermission())); + return; + } + if (!target.isOnline()) { + moderator.sendMessage(configManager.getPrefixedMessage("§eИгрок уже вышел с сервера. Действие отменено.")); + openPlayerSearchDialog(moderator); + return; + } + if (action.hasDuration && !configManager.isPunishmentDurationAllowed(duration)) { + moderator.sendMessage(configManager.getPrefixedMessage("§cВыбран неизвестный срок наказания.")); + openPunishmentActionDialog(moderator, target, action); + return; } - try { - ActionButton confirmButton = actionButton( - "✅ Далее к подтверждению", - "Открыть подтверждение rollback", - (response, audience) -> withPlayer(audience, moderator -> { - int radius = parseInteger(response.getText("rollback_radius"), configManager.getCoreProtectDefaultRadius()); - String time = normalizeTime(response.getText("rollback_time"), configManager.getCoreProtectDefaultTime()); - String targetPlayer = normalizeTarget(response.getText("rollback_player")); - schedule(() -> openRollbackConfirmDialog(moderator, radius, time, targetPlayer)); - })); + String sanitizedReason = reason == null || reason.isBlank() ? "-" : reason; + String command = punishmentTemplate(action) + .replace("{target}", target.getName()) + .replace("{moderator}", moderator.getName()) + .replace("{duration}", duration) + .replace("{reason}", sanitizedReason) + .trim(); - ActionButton backButton = actionButton( - "Назад", - "Вернуться в Hard Moder panel", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openHardPanel(moderator)))); + boolean accepted = configManager.isModerationActionsAsConsole() + ? Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command) + : moderator.getServer().dispatchCommand(moderator, command); + if (!accepted) { + moderator.sendMessage(configManager.getPrefixedMessage( + "§cКоманда наказания не принята сервером. Warn/действие не сохранено.")); + plugin.getLogger().warning("Команда наказания отклонена: " + action.name() + " для " + target.getName()); + openManagePlayerOrSearch(moderator, target); + return; + } - DialogBase base = DialogBase.builder(Component.text("↩ Rollback CoreProtect")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(Component.text( - "Укажите радиус, временной диапазон и при необходимости игрока. Затем подтвердите откат."), 360))) - .inputs(List.of( - DialogInput.text("rollback_radius", 180, Component.text("Радиус"), true, - String.valueOf(configManager.getCoreProtectDefaultRadius()), 3, null), - DialogInput.text("rollback_time", 180, Component.text("Время"), true, - configManager.getCoreProtectDefaultTime(), 4, null), - DialogInput.text("rollback_player", 300, Component.text("Игрок (опционально)"), false, - "", 16, null))) - .build(); - - showDialog(player, base, DialogType.multiAction(List.of(confirmButton), backButton, 1)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть rollback dialog: " + exception.getMessage()); - return false; + if (action == PunishmentAction.WARN) { + warningStore.recordWarn(target, moderator, sanitizedReason, duration); + } + moderator.sendMessage(configManager.getPrefixedMessage("§aДействие " + action.label + " применено к §f" + target.getName() + "§a.")); + if (action == PunishmentAction.BAN || action == PunishmentAction.KICK) { + openPlayerSearchDialog(moderator); + } else { + openManagePlayerOrSearch(moderator, target); } } - public boolean openRollbackConfirmDialog(Player player, int radius, String time, String targetPlayer) { - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return false; - } + private boolean matchesQuery(String value, String query) { + return query == null || query.isBlank() || (value != null && value.toLowerCase().contains(query.toLowerCase())); + } - try { - List lines = new ArrayList<>(); - lines.add(Component.text("Радиус: " + radius + " блоков")); - lines.add(Component.text("Время: " + time)); - lines.add(Component.text(targetPlayer.isEmpty() ? "Игрок: все" : "Игрок: " + targetPlayer)); - - ActionButton confirmButton = actionButton( - "✅ Откатить", - "Подтвердить rollback по указанным параметрам", - (response, audience) -> withPlayer(audience, moderator -> { - adminModeCommand.executeCPRollback(moderator, radius, time, targetPlayer); - schedule(() -> openHardPanel(moderator)); - })); - ActionButton cancelButton = actionButton( - "❌ Отмена", - "Отменить rollback и вернуться назад", - (response, audience) -> withPlayer(audience, moderator -> schedule(() -> openRollbackDialog(moderator)))); - - DialogBase base = DialogBase.builder(Component.text("⚠ Подтвердить откат?")) - .canCloseWithEscape(true) - .pause(false) - .afterAction(DialogBase.DialogAfterAction.CLOSE) - .body(List.of(DialogBody.plainMessage(joinLines(lines), 360))) - .inputs(List.of()) - .build(); - - showDialog(player, base, DialogType.multiAction(List.of(confirmButton, cancelButton), closeButton(), 1)); - return true; - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось открыть подтверждение rollback: " + exception.getMessage()); - return false; + private void openManagePlayerOrSearch(Player moderator, Player target) { + if (target != null && target.isOnline()) { + openManagePlayerDialog(moderator, target); + } else { + openPlayerSearchDialog(moderator); } } - private void showDialog(Player player, DialogBase base, io.papermc.paper.registry.data.dialog.type.DialogType type) { + private String punishmentTemplate(PunishmentAction action) { + return switch (action) { + case WARN -> configManager.getWarnCommandTemplate(); + case MUTE -> configManager.getMuteCommandTemplate(); + case UNMUTE -> configManager.getUnmuteCommandTemplate(); + case KICK -> configManager.getKickCommandTemplate(); + case BAN -> configManager.getBanCommandTemplate(); + case UNWARN, UNBAN -> ""; + }; + } + + private String defaultPunishmentDuration(PunishmentAction action) { + return switch (action) { + case WARN -> configManager.getDefaultWarnDuration(); + case MUTE -> configManager.getDefaultMuteDuration(); + case BAN -> configManager.getDefaultBanDuration(); + case KICK, UNMUTE, UNWARN, UNBAN -> ""; + }; + } + + private String punishmentDurationLabel(String token) { + return switch (token) { + case "1h" -> "1 час"; + case "1d" -> "1 день"; + case "7d" -> "7 дней"; + case "30d" -> "30 дней"; + case "permanent" -> "Permanently"; + default -> token; + }; + } + + private ActionButton backButton(Player player, Runnable callback) { + return actionButton(player, "↩", "Назад", NamedTextColor.GRAY, "Вернуться назад", true, + (res, aud) -> withPlayer(aud, m -> schedule(callback))); + } + + private void showDialog(Player player, DialogBase base, DialogType type) { player.closeInventory(); Dialog dialog = Dialog.create(factory -> { var builder = factory.empty(); @@ -528,32 +1025,74 @@ public class AdminDialogService { player.showDialog(dialog); } - private ActionButton actionButton(String label, String tooltip, DialogActionCallback callback) { - return ActionButton.builder(Component.text(label)) - .tooltip(Component.text(tooltip)) + private ActionButton actionButton(Player player, String emoji, String text, NamedTextColor color, String tooltip, + boolean important, DialogActionCallback callback) { + return actionButton(label(player, emoji, text, color, important), bodyText(player, tooltip), callback); + } + + private ActionButton actionButton(Component label, Component tooltip, DialogActionCallback callback) { + return ActionButton.builder(label) + .tooltip(tooltip) .width(170) .action(DialogAction.customClick(callback, CALLBACK_OPTIONS)) .build(); } - private ActionButton closeButton() { - return ActionButton.builder(Component.text("Закрыть")) - .width(140) - .build(); + private ActionButton closeButton(Player player) { + return ActionButton.builder(label(player, "✖", "Закрыть", NamedTextColor.GRAY, false)).width(140).build(); + } + + private Component title(Player player, String emoji, String text, NamedTextColor defaultColor) { + return label(player, emoji, text, defaultColor, true); + } + + private Component label(Player player, String emoji, String text, NamedTextColor color, boolean important) { + return Component.text(renderLabel(player, emoji, text), themedColor(player, color, important)); + } + + private Component playerLabel(Player player, String text, NamedTextColor color, boolean important) { + return Component.text(text, themedColor(player, color, important)); + } + + private Component bodyText(Player player, String text) { + return bodyText(player, text, false); + } + + private Component bodyText(Player player, String text, boolean important) { + return Component.text(text, themedColor(player, NamedTextColor.WHITE, important)); + } + + private Component bodyLine(Player player, String text, boolean important) { + return bodyText(player, text, important); + } + + private NamedTextColor themedColor(Player player, NamedTextColor defaultColor, boolean important) { + if (player == null) { + return defaultColor; + } + ModeratorPreferencesStore.ModeratorPreferences preferences = preferencesStore.get(player.getUniqueId()); + return switch (preferences.theme()) { + case RAINBOW -> defaultColor; + case CLEAN_ACCENT -> important ? preferences.accentColor().textColor() : NamedTextColor.WHITE; + case WHITE -> NamedTextColor.WHITE; + }; + } + + private String renderLabel(Player player, String emoji, String text) { + if (player != null && !emojiPreferenceStore.isEnabled(player.getUniqueId())) { + return text; + } + return emoji + " " + text; } private void withPlayer(net.kyori.adventure.audience.Audience audience, java.util.function.Consumer consumer) { - if (audience instanceof Player player) { - consumer.accept(player); - } + if (audience instanceof Player player) consumer.accept(player); } private Component joinLines(List lines) { Component result = Component.empty(); for (int i = 0; i < lines.size(); i++) { - if (i > 0) { - result = result.append(Component.newline()); - } + if (i > 0) result = result.append(Component.newline()); result = result.append(lines.get(i)); } return result; @@ -563,51 +1102,143 @@ public class AdminDialogService { plugin.getServer().getScheduler().runTask(plugin, runnable); } - private String normalizeTarget(String targetName) { - return targetName == null ? "" : targetName.trim(); + private String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private String sanitizeReason(String reason) { + if (reason == null) { + return ""; + } + return reason.replace('\n', ' ').replace('\r', ' ').trim(); + } + + private String buildBanSummary(BanEntry entry) { + String reason = blankFallback(entry.getReason(), "без причины"); + if (reason.length() > 48) { + reason = reason.substring(0, 45) + "..."; + } + return "Причина: " + reason + " | До: " + formatDate(entry.getExpiration()); + } + + private boolean isExpiredBan(BanEntry entry) { + return entry.getExpiration() != null && !entry.getExpiration().after(new Date()); + } + + private BanEntry findBanEntry(ProfileBanList banList, String identifier) { + for (BanEntry entry : banList.>getEntries()) { + PlayerProfile profile = entry.getBanTarget(); + if (profileIdentifier(profile).equalsIgnoreCase(identifier) + || profileName(profile).equalsIgnoreCase(identifier)) { + return entry; + } + } + return null; + } + + private String profileIdentifier(PlayerProfile profile) { + if (profile.getId() != null) { + return profile.getId().toString(); + } + return profileName(profile); + } + + private String profileName(PlayerProfile profile) { + return blankFallback(profile.getName(), + profile.getId() == null ? "неизвестно" : profile.getId().toString()); + } + + private String formatWarnInline(ModeratorWarningStore.WarningEntry warning) { + return formatTimestamp(warning.createdAt()) + " | " + warningStatusLabel(warning) + " | " + warning.reason(); + } + + private String formatWarnHeader(ModeratorWarningStore.WarningEntry warning) { + return formatTimestamp(warning.createdAt()) + " • " + warningStatusLabel(warning) + " • " + warning.moderatorName(); + } + + private String warningStatusLabel(ModeratorWarningStore.WarningEntry warning) { + return switch (warning.status()) { + case ACTIVE -> "активен"; + case EXPIRED -> "истёк"; + case REVOKED -> "снят"; + }; + } + + private String formatWarnExpiration(ModeratorWarningStore.WarningEntry warning) { + return warning.isPermanent() ? "Permanently" : formatTimestamp(warning.expiresAt()); + } + + private String formatOptionalTimestamp(long timestamp) { + return timestamp <= 0L ? "данные отсутствуют" : formatTimestamp(timestamp); + } + + private String blankFallback(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } + + private String formatDate(Date date) { + if (date == null) { + return "никогда"; + } + return DATE_FORMATTER.format(date.toInstant()); + } + + private String formatTimestamp(long timestamp) { + return DATE_FORMATTER.format(Instant.ofEpochMilli(timestamp)); } private String normalizeTime(String input, String fallback) { - if (input == null || input.isBlank()) { - return fallback; - } - + if (input == null || input.isBlank()) return fallback; String normalized = input.trim().toLowerCase(); - if (normalized.matches("\\d+[smhdw]")) { - return normalized; - } - return fallback; + return normalized.matches("\\d+[smhdw]") ? normalized : fallback; } - private Integer parseInteger(String value, int fallback) { - if (value == null || value.isBlank()) { - return fallback; - } - + private int parseInt(String value, int fallback) { + if (value == null || value.isBlank()) return fallback; try { return Integer.parseInt(value.trim()); - } catch (NumberFormatException exception) { + } catch (NumberFormatException e) { return fallback; } } - private String dialogTitleForAction(PlayerListAction action) { - return switch (action) { - case SPECTATE -> "👁 Spectate игрока"; - case TELEPORT -> "📍 TP к игроку"; - case INVSEE -> "🎒 Invsee игрока"; - }; - } - - private String dialogBodyForAction(PlayerListAction action) { - return switch (action) { - case SPECTATE -> "Выберите игрока, за которым хотите наблюдать."; - case TELEPORT -> "Выберите игрока, к которому хотите телепортироваться."; - case INVSEE -> "Выберите игрока, чей инвентарь хотите открыть."; - }; - } - private float clamp(float value, float min, float max) { return Math.max(min, Math.min(max, value)); } + + private enum PunishmentAction { + WARN("⚠", "Warn", "Предупреждение", "Выдать предупреждение", NamedTextColor.YELLOW, + "adminmode.punishment.warn", true), + MUTE("🛑", "Mute", "Мут", "Выдать мут", NamedTextColor.GOLD, + "adminmode.punishment.mute", true), + UNMUTE("🔊", "Unmute", "Снять мут", "Снять активный мут", NamedTextColor.GREEN, + "adminmode.punishment.unmute", false), + KICK("👢", "Kick", "Кик", "Кикнуть игрока", NamedTextColor.RED, + "adminmode.punishment.kick", false), + BAN("🔨", "Ban", "Бан", "Забанить игрока", NamedTextColor.DARK_RED, + "adminmode.punishment.ban", true), + UNWARN("✅", "Unwarn", "Снять warn", "Снять предупреждение", NamedTextColor.GREEN, + "adminmode.punishment.unwarn", false), + UNBAN("✅", "Unban", "Разбан", "Снять бан", NamedTextColor.GREEN, + "adminmode.punishment.unban", false); + + private final String emoji; + private final String label; + private final String title; + private final String tooltip; + private final NamedTextColor color; + private final String permission; + private final boolean hasDuration; + + PunishmentAction(String emoji, String label, String title, String tooltip, NamedTextColor color, + String permission, boolean hasDuration) { + this.emoji = emoji; + this.label = label; + this.title = title; + this.tooltip = tooltip; + this.color = color; + this.permission = permission; + this.hasDuration = hasDuration; + } + } } diff --git a/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java b/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java new file mode 100755 index 0000000..0862fc5 --- /dev/null +++ b/src/main/java/org/blz/adminmode/dialog/DialogPreferenceStore.java @@ -0,0 +1,63 @@ +package org.blz.adminmode.dialog; + +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Persists which players have personally enabled the Dialog-based admin panel. + * Stored in /dialog_prefs.dat (one UUID per line). + * Default: disabled for every player until they explicitly toggle on. + */ +public class DialogPreferenceStore { + + private final File file; + private final Set enabledPlayers = new HashSet<>(); + + public DialogPreferenceStore(Plugin plugin) { + file = new File(plugin.getDataFolder(), "dialog_prefs.dat"); + load(); + } + + public boolean isEnabled(UUID playerId) { + return enabledPlayers.contains(playerId); + } + + /** Toggles the preference and persists. Returns the new state (true = enabled). */ + public boolean toggle(UUID playerId) { + if (enabledPlayers.remove(playerId)) { + save(); + return false; + } + enabledPlayers.add(playerId); + save(); + return true; + } + + private void load() { + if (!file.exists()) return; + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + try { + enabledPlayers.add(UUID.fromString(line)); + } catch (IllegalArgumentException ignored) {} + } + } catch (IOException ignored) {} + } + + private void save() { + file.getParentFile().mkdirs(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (UUID uuid : enabledPlayers) { + writer.write(uuid.toString()); + writer.newLine(); + } + } catch (IOException ignored) {} + } +} diff --git a/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java b/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java new file mode 100755 index 0000000..809c5dd --- /dev/null +++ b/src/main/java/org/blz/adminmode/dialog/MenuEmojiPreferenceStore.java @@ -0,0 +1,72 @@ +package org.blz.adminmode.dialog; + +import org.bukkit.plugin.Plugin; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Stores players who disabled emojis in the moderation dialogs. + * Default: emojis are enabled. + */ +public class MenuEmojiPreferenceStore { + + private final File file; + private final Set disabledPlayers = new HashSet<>(); + + public MenuEmojiPreferenceStore(Plugin plugin) { + this.file = new File(plugin.getDataFolder(), "menu_emoji_prefs.dat"); + load(); + } + + public boolean isEnabled(UUID playerId) { + return !disabledPlayers.contains(playerId); + } + + public boolean toggle(UUID playerId) { + if (disabledPlayers.remove(playerId)) { + save(); + return true; + } + disabledPlayers.add(playerId); + save(); + return false; + } + + private void load() { + if (!file.exists()) return; + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + try { + disabledPlayers.add(UUID.fromString(line)); + } catch (IllegalArgumentException ignored) { + } + } + } catch (IOException ignored) { + } + } + + private void save() { + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (UUID uuid : disabledPlayers) { + writer.write(uuid.toString()); + writer.newLine(); + } + } catch (IOException ignored) { + } + } +} diff --git a/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java b/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java old mode 100644 new mode 100755 index b86a659..b446ca8 --- a/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java +++ b/src/main/java/org/blz/adminmode/integrations/CoreProtectIntegration.java @@ -29,31 +29,67 @@ public class CoreProtectIntegration { } public boolean lookup(Player player, String targetPlayer, int radius, String time) { + return lookup(player, targetPlayer, "", "", "", "", radius, time); + } + + public boolean lookup(Player player, String targetPlayer, String action, String block, String exclude, + String world, int radius, String time) { if (!isAvailable()) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); return false; } StringBuilder command = new StringBuilder("co lookup"); - appendPlayerFilter(command, targetPlayer); - command.append(" r:").append(normalizeRadius(radius)); - command.append(" t:").append(normalizeTime(time)); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); return Bukkit.dispatchCommand(player, command.toString()); } - public boolean rollback(Player player, int radius, String time, String targetPlayer) { + public boolean rollback(Player player, int radius, String time, String targetPlayer, String action, String block, String exclude) { + return rollback(player, radius, time, targetPlayer, action, block, exclude, "", ""); + } + + public boolean rollback(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { if (!isAvailable()) { player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); return false; } StringBuilder command = new StringBuilder("co rollback"); - appendPlayerFilter(command, targetPlayer); - command.append(" r:").append(normalizeRadius(radius)); - command.append(" t:").append(normalizeTime(time)); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); + appendExtraFilters(command, extraFilters); return Bukkit.dispatchCommand(player, command.toString()); } + public boolean restore(Player player, int radius, String time, String targetPlayer, String action, String block, + String exclude, String world, String extraFilters) { + if (!isAvailable()) { + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgCoreProtectUnavailable())); + return false; + } + + StringBuilder command = new StringBuilder("co restore"); + appendCommonFilters(command, targetPlayer, action, block, exclude, world, radius, time); + appendExtraFilters(command, extraFilters); + return Bukkit.dispatchCommand(player, command.toString()); + } + + private String sanitizeAction(String action) { + String normalized = action.trim(); + if (normalized.matches("[a-zA-Z0-9_,:+\\-]+")) { + return normalized; + } + return ""; + } + + private String sanitizeBlock(String block) { + String normalized = block.trim(); + if (normalized.matches("[a-zA-Z0-9_,:]+")) { + return normalized; + } + return ""; + } + private void appendPlayerFilter(StringBuilder command, String targetPlayer) { String sanitized = sanitizePlayerName(targetPlayer); if (!sanitized.isEmpty()) { @@ -61,6 +97,58 @@ public class CoreProtectIntegration { } } + private void appendActionFilter(StringBuilder command, String action) { + String sanitized = sanitizeAction(action); + if (!sanitized.isEmpty()) { + command.append(" a:").append(sanitized); + } + } + + private void appendBlockFilter(StringBuilder command, String block) { + String sanitized = sanitizeBlock(block); + if (!sanitized.isEmpty()) { + command.append(" b:").append(sanitized); + } + } + + private void appendExcludeFilter(StringBuilder command, String exclude) { + String sanitized = sanitizeBlock(exclude); + if (!sanitized.isEmpty()) { + command.append(" e:").append(sanitized); + } + } + + private void appendWorldFilter(StringBuilder command, String world) { + String sanitized = sanitizeWorld(world); + if (!sanitized.isEmpty()) { + command.append(" w:").append(sanitized); + } + } + + private void appendCommonFilters(StringBuilder command, String targetPlayer, String action, String block, + String exclude, String world, int radius, String time) { + appendPlayerFilter(command, targetPlayer); + appendActionFilter(command, action); + appendBlockFilter(command, block); + appendExcludeFilter(command, exclude); + appendWorldFilter(command, world); + command.append(" r:").append(normalizeRadius(radius)); + command.append(" t:").append(normalizeTime(time)); + } + + private void appendExtraFilters(StringBuilder command, String extraFilters) { + if (extraFilters == null || extraFilters.isBlank()) { + return; + } + String[] tokens = extraFilters.split(";"); + for (String token : tokens) { + String normalized = token.trim(); + if (normalized.matches("[a-zA-Z]+:[A-Za-z0-9_.,:+\\-]+")) { + command.append(' ').append(normalized); + } + } + } + private int normalizeRadius(int radius) { return Math.max(1, Math.min(100, radius)); } @@ -89,4 +177,15 @@ public class CoreProtectIntegration { } return ""; } + + private String sanitizeWorld(String world) { + if (world == null || world.isBlank()) { + return ""; + } + String normalized = world.trim(); + if (normalized.matches("[A-Za-z0-9_:\\-]+")) { + return normalized; + } + return ""; + } } diff --git a/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java b/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java old mode 100644 new mode 100755 index 48f4dc4..833929a --- a/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java +++ b/src/main/java/org/blz/adminmode/integrations/LuckPermsIntegration.java @@ -6,7 +6,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.node.Node; import org.blz.adminmode.config.ConfigManager; import org.blz.adminmode.moderation.ModeSession; -import org.blz.adminmode.moderation.ModeratorProfile; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; @@ -21,64 +20,54 @@ public class LuckPermsIntegration { } public String resolveCurrentGroup(Player player) { - if (!configManager.isUseLuckPerms()) { - return configManager.getBaseGroup(); - } - + if (!configManager.isUseLuckPerms()) return configManager.getBaseGroup(); try { User user = resolveUser(player); - if (user == null) { - return configManager.getBaseGroup(); - } - return user.getPrimaryGroup(); - } catch (IllegalStateException exception) { + return user != null ? user.getPrimaryGroup() : configManager.getBaseGroup(); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось определить текущую группу LuckPerms для " + player.getName() + ": " + e.getMessage()); return configManager.getBaseGroup(); } } - public void enterMode(Player player, ModeratorProfile profile) { + public void enterMode(Player player) { if (!configManager.isUseLuckPerms()) { - player.setOp(true); + plugin.getLogger().warning("LuckPerms-интеграция отключена в конфиге. Временные группы для " + player.getName() + " не применены."); return; } - try { User user = resolveUser(player); - if (user == null) { - return; + if (user == null) return; + for (String group : configManager.getLuckPermsGroups()) { + user.data().add(groupNode(group)); } - - removeKnownGroups(user); - user.data().add(groupNode(configManager.getActiveGroup(profile))); saveUser(user); - } catch (IllegalStateException exception) { - player.setOp(true); - plugin.getLogger().warning("LuckPerms не найден, используется OP для " + player.getName()); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось выдать временные группы LuckPerms для " + player.getName() + ": " + e.getMessage()); } } public void exitMode(Player player, ModeSession session) { if (!configManager.isUseLuckPerms()) { - player.setOp(false); + plugin.getLogger().warning("LuckPerms-интеграция отключена в конфиге. Группы для " + player.getName() + " не менялись."); return; } - try { User user = resolveUser(player); - if (user == null) { - return; + if (user == null) return; + if (configManager.isRemoveGroupsOnDisable()) { + removeTemporaryGroups(user); } - - removeKnownGroups(user); - String restoreGroup = session.getPreviousGroup(); - if (restoreGroup == null || restoreGroup.isBlank() || "op".equalsIgnoreCase(restoreGroup)) { - restoreGroup = configManager.getBaseGroup(); - } - user.data().add(groupNode(restoreGroup)); saveUser(user); - } catch (IllegalStateException exception) { - player.setOp(false); - plugin.getLogger().warning("LuckPerms не найден, снят OP у " + player.getName()); + } catch (RuntimeException e) { + plugin.getLogger().warning("Не удалось восстановить группы LuckPerms для " + player.getName() + ": " + e.getMessage()); + } + } + + private void removeTemporaryGroups(User user) { + user.data().remove(groupNode(configManager.getAdminGroup())); + for (String group : configManager.getLuckPermsGroups()) { + user.data().remove(groupNode(group)); } } @@ -91,12 +80,6 @@ public class LuckPermsIntegration { return user; } - private void removeKnownGroups(User user) { - user.data().remove(groupNode(configManager.getBaseGroup())); - user.data().remove(groupNode(configManager.getSoftGroup())); - user.data().remove(groupNode(configManager.getHardGroup())); - } - private Node groupNode(String group) { return Node.builder("group." + group).build(); } diff --git a/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java b/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java old mode 100644 new mode 100755 index 535c5d0..5b64824 --- a/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java +++ b/src/main/java/org/blz/adminmode/listeners/AdminModeListener.java @@ -97,21 +97,14 @@ public class AdminModeListener implements Listener { if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; - if ((e.getAction() == Action.RIGHT_CLICK_AIR || e.getAction() == Action.RIGHT_CLICK_BLOCK) - && e.getItem() != null - && modeManager.isPanelItem(e.getItem())) { + if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.getClickedBlock() != null + && e.getClickedBlock().getType() == Material.ENDER_CHEST) { e.setCancelled(true); - - String panelType = modeManager.getPanelType(e.getItem()); - if (ModeManager.PANEL_CP_INSPECTOR.equals(panelType)) { - adminModeCommand.toggleInspector(player); - return; - } - - dialogService.openCurrentPanel(player); + player.sendMessage(configManager.getPrefixedMessage("§cВ admin mode нельзя взаимодействовать с эндер-сундуком.")); return; } + if (!configManager.isPreventItemUse()) return; if (e.getAction() == Action.RIGHT_CLICK_BLOCK) { diff --git a/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java b/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java old mode 100644 new mode 100755 index da3d7b2..f27a296 --- a/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java +++ b/src/main/java/org/blz/adminmode/listeners/GameModeChangeListener.java @@ -4,8 +4,6 @@ import java.util.List; import org.blz.adminmode.commands.AdminModeCommand; import org.blz.adminmode.config.ConfigManager; -import org.blz.adminmode.moderation.ModeratorProfile; -import org.bukkit.GameMode; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -26,29 +24,11 @@ public class GameModeChangeListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onGameModeChange(PlayerGameModeChangeEvent event) { Player player = event.getPlayer(); - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return; - } - - ModeratorProfile activeProfile = adminModeCommand.getActiveProfile(player.getUniqueId()); - boolean hardProfile = activeProfile != null && activeProfile.isHardProfile(); - - if (event.getNewGameMode() == GameMode.CREATIVE && !hardProfile) { - event.setCancelled(true); - player.sendMessage(configManager.getPrefixedMessage("§c✖ В режиме Soft Moder креатив запрещен!")); - return; - } - - if (event.getNewGameMode() == GameMode.CREATIVE && hardProfile) { - return; - } + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; List allowedModes = configManager.getAllowedGameModes(); - if (!allowedModes.isEmpty()) { String newMode = event.getNewGameMode().name(); - if (!allowedModes.contains(newMode)) { event.setCancelled(true); player.sendMessage(configManager.getPrefixedMessage("§c✖ В админ моде можно переключаться только на разрешенные режимы!")); @@ -57,15 +37,18 @@ public class GameModeChangeListener implements Listener { } } - // Включаем полет после смены режима, если это разрешено + if (event.getNewGameMode() == org.bukkit.GameMode.CREATIVE && !player.hasPermission("adminmode.chief")) { + event.setCancelled(true); + player.sendMessage(configManager.getPrefixedMessage("§cКреатив в admin mode доступен только главным модераторам.")); + return; + } + if (configManager.isAllowFlightInSurvival()) { player.getServer().getScheduler().runTaskLater( player.getServer().getPluginManager().getPlugin("AdminMode"), () -> { player.setAllowFlight(true); - if (!player.isFlying()) { - player.setFlying(true); - } + if (!player.isFlying()) player.setFlying(true); }, 1L); } @@ -74,18 +57,8 @@ public class GameModeChangeListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onToggleFlight(PlayerToggleFlightEvent event) { Player player = event.getPlayer(); - - if (!adminModeCommand.isInAdminMode(player.getUniqueId())) { - return; - } - - if (!configManager.isAllowFlightInSurvival()) { - return; - } - - // Всегда разрешаем полет в админ моде - if (!player.getAllowFlight()) { - player.setAllowFlight(true); - } + if (!adminModeCommand.isInAdminMode(player.getUniqueId())) return; + if (!configManager.isAllowFlightInSurvival()) return; + if (!player.getAllowFlight()) player.setAllowFlight(true); } } diff --git a/src/main/java/org/blz/adminmode/listeners/MobTargetListener.java b/src/main/java/org/blz/adminmode/listeners/MobTargetListener.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java b/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java old mode 100644 new mode 100755 index 5d60b65..8088e2b --- a/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java +++ b/src/main/java/org/blz/adminmode/listeners/PlayerDeathListener.java @@ -15,6 +15,6 @@ public class PlayerDeathListener implements Listener { @EventHandler public void onPlayerDeath(PlayerDeathEvent event) { - modeManager.handleHardModeDeath(event); + modeManager.handleAdminDeath(event); } } diff --git a/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java b/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java old mode 100644 new mode 100755 index b353cc5..bf8ee4c --- a/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java +++ b/src/main/java/org/blz/adminmode/listeners/PlayerSessionListener.java @@ -26,13 +26,14 @@ public class PlayerSessionListener implements Listener { @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); + modeManager.refreshVanishFor(player); if (!modeManager.hasSession(player.getUniqueId())) { return; } player.getServer().getScheduler().runTaskLater( org.blz.adminmode.AdminMode.getInstance(), - () -> adminModeCommand.recoverSession(player), + () -> adminModeCommand.resumeMode(player), 1L); } } diff --git a/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java b/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java old mode 100644 new mode 100755 index 9b33ad7..e09e5ae --- a/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java +++ b/src/main/java/org/blz/adminmode/managers/BelzeBoolManager.java @@ -10,6 +10,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; @@ -23,6 +24,7 @@ import java.util.*; public class BelzeBoolManager implements Listener { + private static final String SPECIAL_PERMISSION = "adminmode.special.belzebool"; private final Plugin plugin; private final String BELZEBOOL_NAME = "BelzeBool"; private final String POCKET_DIMENSION_WORLD = "pocket_dimension"; @@ -39,6 +41,9 @@ public class BelzeBoolManager implements Listener { // Vanish State: UUID -> isVanished private final Set vanishedPlayers = new HashSet<>(); + // Pocket return state: player UUID -> original location before abduction + private final Map pocketReturnLocations = new HashMap<>(); + // Input Tracking private final Map> rightClickTimestamps = new HashMap<>(); @@ -49,7 +54,7 @@ public class BelzeBoolManager implements Listener { } public boolean isBelzeBool(Player player) { - return player.getName().equalsIgnoreCase(BELZEBOOL_NAME); + return player.hasPermission(SPECIAL_PERMISSION) && player.getName().equalsIgnoreCase(BELZEBOOL_NAME); } // --- Casting Mode Logic --- @@ -278,6 +283,9 @@ public class BelzeBoolManager implements Listener { target.getWorld().spawnParticle(Particle.SCULK_SOUL, target.getLocation(), 50, 0.5, 1, 0.5, 0.1); Location originalLoc = target.getLocation(); + if (target instanceof Player targetPlayer) { + pocketReturnLocations.put(targetPlayer.getUniqueId(), originalLoc.clone()); + } // 2. Teleport after 5 seconds new BukkitRunnable() { @@ -320,6 +328,8 @@ public class BelzeBoolManager implements Listener { if (target instanceof Player targetP) { targetP.removePotionEffect(PotionEffectType.LEVITATION); targetP.removePotionEffect(PotionEffectType.DARKNESS); + targetP.removePotionEffect(PotionEffectType.SLOWNESS); + pocketReturnLocations.remove(targetP.getUniqueId()); } } } @@ -410,4 +420,52 @@ public class BelzeBoolManager implements Listener { player.sendMessage(Component.text("You have vanished.", NamedTextColor.AQUA)); } } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Location returnLocation = pocketReturnLocations.remove(player.getUniqueId()); + if (returnLocation == null) { + return; + } + + Bukkit.getScheduler().runTask(plugin, () -> { + player.teleport(returnLocation); + player.removePotionEffect(PotionEffectType.LEVITATION); + player.removePotionEffect(PotionEffectType.DARKNESS); + player.removePotionEffect(PotionEffectType.SLOWNESS); + }); + } + + public boolean forceDisable(Player player) { + boolean changed = false; + UUID uuid = player.getUniqueId(); + + if (castingModePlayers.remove(uuid)) { + changed = true; + } + rightClickTimestamps.remove(uuid); + selectedSpellIndex.remove(uuid); + cooldowns.remove(uuid); + + if (vanishedPlayers.remove(uuid)) { + for (Player online : Bukkit.getOnlinePlayers()) { + online.showPlayer(plugin, player); + } + changed = true; + } + + Location returnLocation = pocketReturnLocations.remove(uuid); + if (returnLocation != null) { + changed = true; + if (player.isOnline()) { + player.teleport(returnLocation); + } + player.removePotionEffect(PotionEffectType.LEVITATION); + player.removePotionEffect(PotionEffectType.DARKNESS); + player.removePotionEffect(PotionEffectType.SLOWNESS); + } + + return changed; + } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeManager.java b/src/main/java/org/blz/adminmode/moderation/ModeManager.java old mode 100644 new mode 100755 index 79f00b5..92ffa6d --- a/src/main/java/org/blz/adminmode/moderation/ModeManager.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeManager.java @@ -8,7 +8,6 @@ import org.blz.adminmode.session.SessionStorage; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.attribute.Attribute; import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; @@ -16,46 +15,44 @@ import org.bukkit.entity.Player; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; public class ModeManager { - public static final String PANEL_SOFT = "soft"; - public static final String PANEL_HARD = "hard"; - public static final String PANEL_CP_INSPECTOR = "cp_inspector"; - private final Plugin plugin; private final ConfigManager configManager; private final LuckPermsIntegration luckPermsIntegration; private final SessionStorage sessionStorage; private final ModeSessionLogger sessionLogger; - private final NamespacedKey panelTypeKey; + private final ModeratorPreferencesStore preferencesStore; private final Map activeSessions = new HashMap<>(); + private final Set vanishedModerators = new HashSet<>(); public ModeManager( Plugin plugin, ConfigManager configManager, LuckPermsIntegration luckPermsIntegration, SessionStorage sessionStorage, - ModeSessionLogger sessionLogger) { + ModeSessionLogger sessionLogger, + ModeratorPreferencesStore preferencesStore) { this.plugin = plugin; this.configManager = configManager; this.luckPermsIntegration = luckPermsIntegration; this.sessionStorage = sessionStorage; this.sessionLogger = sessionLogger; - this.panelTypeKey = new NamespacedKey(plugin, "panel_type"); + this.preferencesStore = preferencesStore; this.activeSessions.putAll(sessionStorage.loadSessions()); } @@ -71,37 +68,32 @@ public class ModeManager { return player == null ? null : getSession(player.getUniqueId()); } - public ModeratorProfile getProfile(UUID playerId) { - ModeSession session = getSession(playerId); - return session == null ? null : session.getProfile(); - } - - public List getAvailableProfiles(Player player) { - return ModeratorProfile.availableFor(player); - } - public Collection getActiveSessions() { return new ArrayList<>(activeSessions.values()); } - public boolean enterMode(Player player, ModeratorProfile profile) { - if (player == null || profile == null || hasSession(player.getUniqueId())) { - return false; - } + public boolean enterMode(Player player) { + if (player == null || hasSession(player.getUniqueId())) return false; + if (!player.hasPermission(ModeratorProfile.ADMIN.getPermission())) return false; String previousGroup = luckPermsIntegration.resolveCurrentGroup(player); - String activeGroup = configManager.getActiveGroup(profile); - ModeSession session = ModeSession.capture(player, profile, previousGroup, activeGroup); - activeSessions.put(player.getUniqueId(), session); - persistSessions(); + ModeSession session = ModeSession.capture(player, ModeratorProfile.ADMIN, previousGroup, configManager.getAdminGroup()); + try { + activeSessions.put(player.getUniqueId(), session); + persistSessions(); - preparePlayerForModeration(player, profile); - luckPermsIntegration.enterMode(player, profile); - sessionLogger.logEnter(session); + preparePlayerForModeration(player); + luckPermsIntegration.enterMode(player); + sessionLogger.logEnter(session); - String enterMessage = profile.isHardProfile() ? configManager.getMsgEnterHard() : configManager.getMsgEnterSoft(); - player.sendMessage(configManager.getPrefixedMessage(enterMessage)); - return true; + player.sendMessage(configManager.getPrefixedMessage(configManager.getMsgEnter())); + return true; + } catch (RuntimeException exception) { + activeSessions.remove(player.getUniqueId()); + persistSessions(); + plugin.getLogger().warning("AdminMode: не удалось включить режим для " + player.getName() + ": " + exception.getMessage()); + return false; + } } public boolean exitMode(Player player) { @@ -112,6 +104,19 @@ public class ModeManager { return finishSession(player, true); } + public boolean resumeMode(Player player) { + if (player == null || !hasSession(player.getUniqueId())) { + return false; + } + + applyResumedModerationState(player); + luckPermsIntegration.enterMode(player); + if (isVanished(player.getUniqueId())) { + reapplyVanish(player); + } + return true; + } + public void markQuit(Player player) { if (player != null && hasSession(player.getUniqueId())) { persistSessions(); @@ -119,33 +124,48 @@ public class ModeManager { } public boolean canUseHardActions(CommandSender sender) { - if (sender.hasPermission("adminmode.admin")) { - return true; - } + return sender.hasPermission("adminmode.admin"); + } - if (!(sender instanceof Player player)) { + public boolean isVanished(UUID playerId) { + return vanishedModerators.contains(playerId); + } + + public boolean toggleVanish(Player player) { + if (player == null || !hasSession(player.getUniqueId())) { return false; } + if (isVanished(player.getUniqueId())) { + disableVanish(player); + player.sendMessage(configManager.getPrefixedMessage("§7Vanish отключён.")); + return false; + } + enableVanish(player); + player.sendMessage(configManager.getPrefixedMessage("§aVanish включён.")); + return true; + } - ModeSession session = getSession(player); - return session != null && session.getProfile().isHardProfile(); + public void refreshVanishFor(Player viewer) { + if (viewer == null) { + return; + } + for (UUID vanishedId : vanishedModerators) { + Player vanishedPlayer = plugin.getServer().getPlayer(vanishedId); + if (vanishedPlayer != null && !viewer.getUniqueId().equals(vanishedId)) { + viewer.hidePlayer(plugin, vanishedPlayer); + } + } } public boolean teleportToPlayer(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; moderator.teleport(target.getLocation()); moderator.sendMessage(configManager.getPrefixedMessage("§aТелепортация к §f" + target.getName() + "§a выполнена.")); return true; } public boolean teleportPlayerHere(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; target.teleport(moderator.getLocation()); moderator.sendMessage(configManager.getPrefixedMessage("§aИгрок §f" + target.getName() + "§a телепортирован к вам.")); target.sendMessage(configManager.getPrefixedMessage("§eВас телепортировал модератор §f" + moderator.getName())); @@ -153,15 +173,11 @@ public class ModeManager { } public boolean spectatePlayer(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - + if (moderator == null || target == null) return false; if (moderator.equals(target)) { moderator.sendMessage(configManager.getPrefixedMessage("§cНельзя спектейтить самого себя.")); return false; } - moderator.setGameMode(GameMode.SPECTATOR); moderator.setSpectatorTarget((Entity) target); moderator.sendMessage(configManager.getPrefixedMessage("§bВы наблюдаете за §f" + target.getName())); @@ -169,20 +185,14 @@ public class ModeManager { } public boolean openInvsee(Player moderator, Player target) { - if (moderator == null || target == null) { - return false; - } - - moderator.openInventory(target.getInventory()); + if (moderator == null || target == null) return false; + moderator.getServer().dispatchCommand(moderator, "invsee " + target.getName()); moderator.sendMessage(configManager.getPrefixedMessage("§7Открыт инвентарь игрока §f" + target.getName())); return true; } public void applyAdminSettings(Player player, float flySpeed, float walkSpeed, boolean invulnerable) { - if (!hasSession(player.getUniqueId())) { - return; - } - + if (!hasSession(player.getUniqueId())) return; player.setAllowFlight(true); player.setFlying(true); player.setFlySpeed(clampSpeed(flySpeed, 0.1f)); @@ -192,7 +202,7 @@ public class ModeManager { public List listOtherOnlinePlayers(Player viewer) { List players = new ArrayList<>(plugin.getServer().getOnlinePlayers()); - players.removeIf(player -> viewer != null && player.getUniqueId().equals(viewer.getUniqueId())); + players.removeIf(p -> viewer != null && p.getUniqueId().equals(viewer.getUniqueId())); players.sort(Comparator.comparing(Player::getName, String.CASE_INSENSITIVE_ORDER)); return players; } @@ -200,10 +210,10 @@ public class ModeManager { public List buildOnlineSummary(Player viewer) { List lines = new ArrayList<>(); for (Player target : listOtherOnlinePlayers(viewer)) { - Location location = target.getLocation(); + Location loc = target.getLocation(); lines.add(Component.text( target.getName() + " | " + target.getWorld().getName() + " | " + - location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ(), + loc.getBlockX() + ", " + loc.getBlockY() + ", " + loc.getBlockZ(), NamedTextColor.GRAY)); } if (lines.isEmpty()) { @@ -212,12 +222,11 @@ public class ModeManager { return lines; } - public void handleHardModeDeath(PlayerDeathEvent event) { + /** Keep inventory/levels and teleport back on death while in admin mode. */ + public void handleAdminDeath(PlayerDeathEvent event) { Player player = event.getEntity(); ModeSession session = getSession(player); - if (session == null || !session.getProfile().isHardProfile()) { - return; - } + if (session == null) return; event.setKeepInventory(true); event.setKeepLevel(true); @@ -228,45 +237,18 @@ public class ModeManager { } } - public boolean isPanelItem(ItemStack itemStack) { - return getPanelType(itemStack) != null; - } - - public String getPanelType(ItemStack itemStack) { - if (itemStack == null || itemStack.getType() == Material.AIR || !itemStack.hasItemMeta()) { - return null; - } - - ItemMeta itemMeta = itemStack.getItemMeta(); - PersistentDataContainer container = itemMeta.getPersistentDataContainer(); - if (!container.has(panelTypeKey, PersistentDataType.STRING)) { - return null; - } - return container.get(panelTypeKey, PersistentDataType.STRING); - } - public boolean isReadOnlyViewedInventory(Player viewer, org.bukkit.inventory.Inventory inventory) { - if (viewer == null || inventory == null || inventory.getHolder() == null) { - return false; - } - - if (!(inventory.getHolder() instanceof Player target)) { - return false; - } - + if (viewer == null || inventory == null || inventory.getHolder() == null) return false; + if (!(inventory.getHolder() instanceof Player target)) return false; return hasSession(viewer.getUniqueId()) && !viewer.getUniqueId().equals(target.getUniqueId()); } private boolean finishSession(Player player, boolean recovered) { - if (player == null) { - return false; - } - + if (player == null) return false; ModeSession session = activeSessions.remove(player.getUniqueId()); - if (session == null) { - return false; - } + if (session == null) return false; + disableVanish(player); restorePlayerState(player, session); luckPermsIntegration.exitMode(player, session); persistSessions(); @@ -281,14 +263,14 @@ public class ModeManager { return true; } - private void preparePlayerForModeration(Player player, ModeratorProfile profile) { + private void preparePlayerForModeration(Player player) { PlayerInventory inventory = player.getInventory(); inventory.clear(); inventory.setArmorContents(new ItemStack[4]); - inventory.setItemInOffHand(new ItemStack(Material.AIR)); + inventory.setItemInOffHand(ItemStack.empty()); player.closeInventory(); - player.setSpectatorTarget(null); + clearSpectatorTargetIfNeeded(player); for (PotionEffect effect : player.getActivePotionEffects()) { player.removePotionEffect(effect.getType()); } @@ -305,27 +287,35 @@ public class ModeManager { player.setFlying(true); player.setFlySpeed(clampSpeed(configManager.getCustomFlySpeed(), 0.1f)); player.setWalkSpeed(clampSpeed(configManager.getCustomWalkSpeed(), 0.1f)); - player.setInvulnerable(true); - player.setGameMode(profile.isHardProfile() ? GameMode.CREATIVE : GameMode.SPECTATOR); - - inventory.setItem(0, createPanelItem(profile.isHardProfile() ? configManager.getHardPanelMaterial() : configManager.getSoftPanelMaterial(), - profile.isHardProfile() ? "Hard Panel" : "Soft Panel", - profile.isHardProfile() ? NamedTextColor.RED : NamedTextColor.AQUA, - profile.isHardProfile() ? PANEL_HARD : PANEL_SOFT)); - - if (profile.isHardProfile()) { - inventory.setItem(1, createPanelItem( - configManager.getCpInspectorMaterial(), - "CP Inspector", - NamedTextColor.YELLOW, - PANEL_CP_INSPECTOR)); - } + player.setInvulnerable(configManager.isGodMode()); + player.setGameMode(resolveAdminGameMode(player)); if (configManager.isGivePresetBlocks()) { givePresetBlocks(player); } } + private void applyResumedModerationState(Player player) { + player.closeInventory(); + clearSpectatorTargetIfNeeded(player); + player.setFireTicks(0); + player.setInvulnerable(configManager.isGodMode()); + player.setFlySpeed(clampSpeed(player.getFlySpeed(), configManager.getCustomFlySpeed())); + player.setWalkSpeed(clampSpeed(player.getWalkSpeed(), configManager.getCustomWalkSpeed())); + + GameMode resumedMode = isAdminGameModeAllowedForPlayer(player, player.getGameMode()) + ? player.getGameMode() + : resolveAdminGameMode(player); + player.setGameMode(resumedMode); + + if (!player.getAllowFlight() && (resumedMode != GameMode.SURVIVAL || configManager.isAllowFlightInSurvival())) { + player.setAllowFlight(true); + } + if (player.getAllowFlight() && resumedMode != GameMode.SURVIVAL) { + player.setFlying(true); + } + } + private void restorePlayerState(Player player, ModeSession session) { PlayerInventory inventory = player.getInventory(); inventory.clear(); @@ -333,7 +323,7 @@ public class ModeManager { inventory.setArmorContents(session.getArmorContents()); inventory.setItemInOffHand(session.getOffHandItem()); - player.setSpectatorTarget(null); + clearSpectatorTargetIfNeeded(player); player.setInvulnerable(false); player.setAllowFlight(false); player.setFlying(false); @@ -346,7 +336,11 @@ public class ModeManager { player.setGameMode(session.getGameMode()); if (session.getLocation() != null) { - player.teleport(session.getLocation()); + player.teleportAsync(session.getLocation()).thenAccept(success -> { + if (!success) { + plugin.getLogger().warning("AdminMode: Не удалось телепортировать " + player.getName() + " обратно."); + } + }); } player.setHealth(Math.min(resolveMaxHealth(player), session.getHealth())); @@ -377,26 +371,96 @@ public class ModeManager { String[] parts = blockString.split(":"); Material material = Material.valueOf(parts[0].trim().toUpperCase()); int amount = parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 64; - player.getInventory().addItem(new ItemStack(material, Math.max(1, amount))); - } catch (Exception exception) { + player.getInventory().addItem(ItemStack.of(material, Math.max(1, amount))); + } catch (Exception e) { plugin.getLogger().warning("Неверный блок в preset_blocks: " + blockString); } } } - private ItemStack createPanelItem(Material material, String title, NamedTextColor color, String panelType) { - ItemStack itemStack = new ItemStack(material); - ItemMeta itemMeta = itemStack.getItemMeta(); - itemMeta.displayName(Component.text("[" + title + "]", color)); - itemMeta.getPersistentDataContainer().set(panelTypeKey, PersistentDataType.STRING, panelType); - itemStack.setItemMeta(itemMeta); - return itemStack; - } - private void persistSessions() { sessionStorage.saveSessions(activeSessions); } + private void clearSpectatorTargetIfNeeded(Player player) { + if (player.getGameMode() == GameMode.SPECTATOR) { + player.setSpectatorTarget(null); + } + } + + private GameMode resolveAdminGameMode(Player player) { + if (configManager.isForceSpectator()) { + return GameMode.SPECTATOR; + } + + ModeratorPreferencesStore.DefaultEntryMode preferredMode = preferencesStore.get(player.getUniqueId()).defaultEntryMode(); + return switch (preferredMode) { + case SURVIVAL -> GameMode.SURVIVAL; + case SPECTATOR -> GameMode.SPECTATOR; + case AUTO -> fallbackDefaultGameMode(player); + }; + } + + private GameMode fallbackDefaultGameMode(Player player) { + if (canUseCreativeInAdminMode(player)) { + return GameMode.CREATIVE; + } + return GameMode.SPECTATOR; + } + + private boolean canUseCreativeInAdminMode(Player player) { + return player.hasPermission("adminmode.chief") && configManager.isChiefCreativeMode(); + } + + private boolean isAdminGameModeAllowedForPlayer(Player player, GameMode gameMode) { + if (gameMode == null) { + return false; + } + if (configManager.isForceSpectator()) { + return gameMode == GameMode.SPECTATOR; + } + return switch (gameMode) { + case SURVIVAL, SPECTATOR -> true; + case CREATIVE -> canUseCreativeInAdminMode(player); + default -> false; + }; + } + + private void enableVanish(Player player) { + vanishedModerators.add(player.getUniqueId()); + player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0, false, false, false)); + player.setSilent(true); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.hidePlayer(plugin, player); + } + } + } + + private void disableVanish(Player player) { + if (!vanishedModerators.remove(player.getUniqueId())) { + return; + } + player.removePotionEffect(PotionEffectType.INVISIBILITY); + player.setSilent(false); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.showPlayer(plugin, player); + } + } + } + + private void reapplyVanish(Player player) { + vanishedModerators.add(player.getUniqueId()); + player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0, false, false, false)); + player.setSilent(true); + for (Player online : plugin.getServer().getOnlinePlayers()) { + if (!online.getUniqueId().equals(player.getUniqueId())) { + online.hidePlayer(plugin, player); + } + } + } + private double resolveMaxHealth(Player player) { if (player.getAttribute(Attribute.MAX_HEALTH) != null) { return player.getAttribute(Attribute.MAX_HEALTH).getValue(); @@ -404,7 +468,8 @@ public class ModeManager { return 20.0D; } - private float clampSpeed(float value, float min) { - return Math.max(min, Math.min(1.0f, value)); + private float clampSpeed(float value, float fallback) { + float normalized = value <= 0.0f ? fallback : value; + return Math.max(0.1f, Math.min(1.0f, normalized)); } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeSession.java b/src/main/java/org/blz/adminmode/moderation/ModeSession.java old mode 100644 new mode 100755 index b2abab5..f84c49c --- a/src/main/java/org/blz/adminmode/moderation/ModeSession.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeSession.java @@ -246,7 +246,7 @@ public final class ModeSession { private static ItemStack cloneItem(ItemStack item) { if (item == null || item.getType() == Material.AIR) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } return item.clone(); } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java b/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java old mode 100644 new mode 100755 index 0d07509..3c63279 --- a/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeSessionLogger.java @@ -60,7 +60,7 @@ public class ModeSessionLogger { } private String modeSuffix(ModeSession session) { - return session.getProfile().isHardProfile() ? "HARD" : "SOFT"; + return "ADMIN"; } private String formatDuration(long millis) { diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java b/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java new file mode 100755 index 0000000..cb07877 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorAccessManager.java @@ -0,0 +1,280 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.ArrayList; +import java.util.List; + +public class ModeratorAccessManager { + + private static final long PERMANENT_EXPIRY = -1L; + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final Map restrictions = new HashMap<>(); + + public ModeratorAccessManager(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "moderator-access.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public Optional getActiveRestriction(UUID playerId) { + RestrictionEntry entry = restrictions.get(playerId); + if (entry == null) { + return Optional.empty(); + } + if (entry.isExpired()) { + restrictions.remove(playerId); + save(); + return Optional.empty(); + } + return Optional.of(entry); + } + + public boolean isRestricted(UUID playerId) { + return getActiveRestriction(playerId).isPresent(); + } + + public RestrictionEntry restrict(OfflinePlayer target, String targetName, String actorName, Long expiresAt, String reason) { + RestrictionEntry entry = new RestrictionEntry( + target.getUniqueId(), + targetName, + actorName, + reason == null ? "" : reason.trim(), + System.currentTimeMillis(), + expiresAt == null ? PERMANENT_EXPIRY : expiresAt); + restrictions.put(target.getUniqueId(), entry); + save(); + return entry; + } + + public boolean clearRestriction(UUID playerId) { + RestrictionEntry removed = restrictions.remove(playerId); + if (removed != null) { + save(); + return true; + } + return false; + } + + public List getRestrictedPlayerNames() { + List names = new ArrayList<>(); + for (RestrictionEntry entry : restrictions.values()) { + if (!entry.isExpired() && entry.getPlayerName() != null && !entry.getPlayerName().isBlank()) { + names.add(entry.getPlayerName()); + } + } + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } + + public String describeRestriction(UUID playerId) { + Optional optional = getActiveRestriction(playerId); + if (optional.isEmpty()) { + return ""; + } + RestrictionEntry entry = optional.get(); + StringBuilder message = new StringBuilder("§cВам запрещено входить в admin mode"); + if (!entry.isPermanent()) { + message.append(" §7(").append(formatRemaining(entry.getExpiresAt() - System.currentTimeMillis())).append("§7)"); + } else { + message.append(" §7(навсегда)"); + } + if (!entry.getReason().isBlank()) { + message.append("§c. Причина: §f").append(entry.getReason()); + } + return message.toString(); + } + + public static Long parseDurationToExpiry(String input) { + if (input == null || input.isBlank()) { + return null; + } + String normalized = input.trim().toLowerCase(); + if (normalized.equals("perm") || normalized.equals("permanent") || normalized.equals("forever") + || normalized.equals("навсегда")) { + return null; + } + if (!normalized.matches("\\d+[smhdwy]")) { + return Long.MIN_VALUE; + } + + long amount = Long.parseLong(normalized.substring(0, normalized.length() - 1)); + char suffix = normalized.charAt(normalized.length() - 1); + Duration duration = switch (suffix) { + case 's' -> Duration.ofSeconds(amount); + case 'm' -> Duration.ofMinutes(amount); + case 'h' -> Duration.ofHours(amount); + case 'd' -> Duration.ofDays(amount); + case 'w' -> Duration.ofDays(amount * 7); + case 'y' -> Duration.ofDays(amount * 365); + default -> null; + }; + if (duration == null || duration.isNegative() || duration.isZero()) { + return Long.MIN_VALUE; + } + return System.currentTimeMillis() + duration.toMillis(); + } + + private void load() { + restrictions.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonArray()) { + return; + } + + for (JsonElement element : root.getAsJsonArray()) { + if (!element.isJsonObject()) { + continue; + } + RestrictionEntry entry = deserialize(element.getAsJsonObject()); + if (entry != null && !entry.isExpired()) { + restrictions.put(entry.getPlayerId(), entry); + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить moderator-access.json: " + exception.getMessage()); + } + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonArray array = new JsonArray(); + for (RestrictionEntry entry : restrictions.values()) { + if (!entry.isExpired()) { + array.add(serialize(entry)); + } + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(array)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить moderator-access.json: " + exception.getMessage()); + } + } + + private JsonObject serialize(RestrictionEntry entry) { + JsonObject json = new JsonObject(); + json.addProperty("playerId", entry.getPlayerId().toString()); + json.addProperty("playerName", entry.getPlayerName()); + json.addProperty("actorName", entry.getActorName()); + json.addProperty("reason", entry.getReason()); + json.addProperty("createdAt", entry.getCreatedAt()); + json.addProperty("expiresAt", entry.getExpiresAt()); + return json; + } + + private RestrictionEntry deserialize(JsonObject json) { + try { + UUID playerId = UUID.fromString(json.get("playerId").getAsString()); + String playerName = json.has("playerName") ? json.get("playerName").getAsString() : playerId.toString(); + String actorName = json.has("actorName") ? json.get("actorName").getAsString() : "unknown"; + String reason = json.has("reason") ? json.get("reason").getAsString() : ""; + long createdAt = json.has("createdAt") ? json.get("createdAt").getAsLong() : System.currentTimeMillis(); + long expiresAt = json.has("expiresAt") ? json.get("expiresAt").getAsLong() : PERMANENT_EXPIRY; + return new RestrictionEntry(playerId, playerName, actorName, reason, createdAt, expiresAt); + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось прочитать запись ограничения admin mode: " + exception.getMessage()); + return null; + } + } + + private String formatRemaining(long millis) { + Duration duration = Duration.ofMillis(Math.max(millis, 0L)); + long days = duration.toDays(); + long hours = duration.toHoursPart(); + long minutes = duration.toMinutesPart(); + + if (days > 0) { + return days + "д " + hours + "ч"; + } + if (duration.toHours() > 0) { + return duration.toHours() + "ч " + minutes + "м"; + } + if (duration.toMinutes() > 0) { + return duration.toMinutes() + "м"; + } + if (duration.toSeconds() > 0) { + return duration.toSeconds() + "с"; + } + return Math.max(1L, duration.toMinutes()) + "м"; + } + + public static final class RestrictionEntry { + private final UUID playerId; + private final String playerName; + private final String actorName; + private final String reason; + private final long createdAt; + private final long expiresAt; + + public RestrictionEntry(UUID playerId, String playerName, String actorName, String reason, long createdAt, long expiresAt) { + this.playerId = playerId; + this.playerName = playerName; + this.actorName = actorName; + this.reason = reason; + this.createdAt = createdAt; + this.expiresAt = expiresAt; + } + + public UUID getPlayerId() { + return playerId; + } + + public String getPlayerName() { + return playerName; + } + + public String getActorName() { + return actorName; + } + + public String getReason() { + return reason; + } + + public long getCreatedAt() { + return createdAt; + } + + public long getExpiresAt() { + return expiresAt; + } + + public boolean isPermanent() { + return expiresAt == PERMANENT_EXPIRY; + } + + public boolean isExpired() { + return !isPermanent() && System.currentTimeMillis() >= expiresAt; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java b/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java new file mode 100755 index 0000000..d3199b6 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorPreferencesStore.java @@ -0,0 +1,230 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +public class ModeratorPreferencesStore { + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final Map preferences = new LinkedHashMap<>(); + + public ModeratorPreferencesStore(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "moderator-preferences.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public ModeratorPreferences get(UUID playerId) { + return preferences.computeIfAbsent(playerId, ignored -> new ModeratorPreferences()); + } + + public DefaultEntryMode cycleDefaultEntryMode(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + DefaultEntryMode[] values = new DefaultEntryMode[]{DefaultEntryMode.AUTO, DefaultEntryMode.SURVIVAL, DefaultEntryMode.SPECTATOR}; + preferences.defaultEntryMode = cycle(values, preferences.defaultEntryMode); + save(); + return preferences.defaultEntryMode; + } + + public MenuTheme cycleTheme(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + preferences.theme = cycle(MenuTheme.values(), preferences.theme); + save(); + return preferences.theme; + } + + public AccentColor cycleAccentColor(UUID playerId) { + ModeratorPreferences preferences = get(playerId); + preferences.accentColor = cycle(AccentColor.values(), preferences.accentColor); + save(); + return preferences.accentColor; + } + + private T cycle(T[] values, T current) { + for (int i = 0; i < values.length; i++) { + if (values[i] == current) { + return values[(i + 1) % values.length]; + } + } + return values[0]; + } + + private void load() { + preferences.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonObject()) { + return; + } + + for (Map.Entry entry : root.getAsJsonObject().entrySet()) { + if (!entry.getValue().isJsonObject()) { + continue; + } + try { + UUID uuid = UUID.fromString(entry.getKey()); + preferences.put(uuid, readPreferences(entry.getValue().getAsJsonObject())); + } catch (IllegalArgumentException ignored) { + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить moderator-preferences.json: " + exception.getMessage()); + } + } + + private ModeratorPreferences readPreferences(JsonObject json) { + ModeratorPreferences result = new ModeratorPreferences(); + result.defaultEntryMode = DefaultEntryMode.fromName(getString(json, "defaultEntryMode")); + result.theme = MenuTheme.fromName(getString(json, "theme")); + result.accentColor = AccentColor.fromName(getString(json, "accentColor")); + return result; + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonObject root = new JsonObject(); + for (Map.Entry entry : preferences.entrySet()) { + JsonObject json = new JsonObject(); + json.addProperty("defaultEntryMode", entry.getValue().defaultEntryMode.name()); + json.addProperty("theme", entry.getValue().theme.name()); + json.addProperty("accentColor", entry.getValue().accentColor.name()); + root.add(entry.getKey().toString(), json); + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(root)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить moderator-preferences.json: " + exception.getMessage()); + } + } + + private String getString(JsonObject json, String key) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : ""; + } + + public static final class ModeratorPreferences { + private DefaultEntryMode defaultEntryMode = DefaultEntryMode.AUTO; + private MenuTheme theme = MenuTheme.RAINBOW; + private AccentColor accentColor = AccentColor.AQUA; + + public DefaultEntryMode defaultEntryMode() { + return defaultEntryMode; + } + + public MenuTheme theme() { + return theme; + } + + public AccentColor accentColor() { + return accentColor; + } + } + + public enum DefaultEntryMode { + AUTO("По серверу"), + SURVIVAL("SURVIVAL"), + SPECTATOR("SPECTATOR"); + + private final String displayName; + + DefaultEntryMode(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + + public static DefaultEntryMode fromName(String value) { + for (DefaultEntryMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + return AUTO; + } + } + + public enum MenuTheme { + RAINBOW("Радужная"), + CLEAN_ACCENT("Светлая акцентная"), + WHITE("Белая"); + + private final String displayName; + + MenuTheme(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + + public static MenuTheme fromName(String value) { + for (MenuTheme theme : values()) { + if (theme.name().equalsIgnoreCase(value)) { + return theme; + } + } + return RAINBOW; + } + } + + public enum AccentColor { + AQUA("Аква", NamedTextColor.AQUA), + GOLD("Золото", NamedTextColor.GOLD), + GREEN("Лайм", NamedTextColor.GREEN), + YELLOW("Жёлтый", NamedTextColor.YELLOW), + RED("Красный", NamedTextColor.RED), + BLUE("Синий", NamedTextColor.BLUE); + + private final String displayName; + private final NamedTextColor textColor; + + AccentColor(String displayName, NamedTextColor textColor) { + this.displayName = displayName; + this.textColor = textColor; + } + + public String displayName() { + return displayName; + } + + public NamedTextColor textColor() { + return textColor; + } + + public static AccentColor fromName(String value) { + for (AccentColor color : values()) { + if (color.name().equalsIgnoreCase(value)) { + return color; + } + } + return AQUA; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java b/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java old mode 100644 new mode 100755 index 5044501..bed2f4e --- a/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorProfile.java @@ -2,14 +2,10 @@ package org.blz.adminmode.moderation; import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; import java.util.Optional; public enum ModeratorProfile { - SOFT_MODER("soft_moder", "Soft Moder", "adminmode.profile.soft"), - HARD_MODER("hard_moder", "Hard Moder", "adminmode.profile.hard"); + ADMIN("admin", "Admin Mode", "adminmode.use"); private final String configKey; private final String displayName; @@ -33,54 +29,12 @@ public enum ModeratorProfile { return permission; } - public boolean isHardProfile() { - return this == HARD_MODER; - } - public boolean isAvailableFor(Player player) { - return switch (this) { - case SOFT_MODER -> player.hasPermission("adminmode.use") - || player.hasPermission(permission) - || player.hasPermission(HARD_MODER.permission); - case HARD_MODER -> player.hasPermission(permission); - }; - } - - public static List availableFor(Player player) { - List profiles = new ArrayList<>(); - for (ModeratorProfile profile : values()) { - if (profile.isAvailableFor(player)) { - profiles.add(profile); - } - } - return profiles; + return player.hasPermission(permission); } + /** Accepts any old key (soft_moder, hard_moder, admin) and returns ADMIN. */ public static Optional fromConfigKey(String key) { - if (key == null || key.isBlank()) { - return Optional.empty(); - } - - String normalized = key.toLowerCase(Locale.ROOT); - for (ModeratorProfile profile : values()) { - if (profile.configKey.equals(normalized)) { - return Optional.of(profile); - } - } - - return Optional.empty(); - } - - public static Optional fromInput(String input) { - if (input == null || input.isBlank()) { - return Optional.empty(); - } - - String normalized = input.trim().toLowerCase(Locale.ROOT); - return switch (normalized) { - case "soft", "soft_moder", "soft-moder", "softmoder" -> Optional.of(SOFT_MODER); - case "hard", "hard_moder", "hard-moder", "hardmoder" -> Optional.of(HARD_MODER); - default -> Optional.empty(); - }; + return Optional.of(ADMIN); } } diff --git a/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java b/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java new file mode 100644 index 0000000..9eb541b --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/ModeratorWarningStore.java @@ -0,0 +1,256 @@ +package org.blz.adminmode.moderation; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class ModeratorWarningStore { + + private static final long NOT_REVOKED = -1L; + + private final Plugin plugin; + private final File storageFile; + private final Gson gson; + private final List entries = new ArrayList<>(); + + public ModeratorWarningStore(Plugin plugin) { + this.plugin = plugin; + this.storageFile = new File(plugin.getDataFolder(), "warn-history.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + load(); + } + + public WarningEntry recordWarn(Player target, Player moderator, String reason, String durationToken) { + if (target == null || moderator == null) { + return null; + } + + long now = System.currentTimeMillis(); + long expiresAt = PunishmentDuration.toExpiry(durationToken, now); + WarningEntry entry = new WarningEntry( + UUID.randomUUID(), + target.getUniqueId(), + target.getName(), + moderator.getName(), + sanitizeReason(reason), + now, + expiresAt, + NOT_REVOKED, + "", + ""); + entries.add(entry); + save(); + return entry; + } + + public List getRecentWarnings(UUID playerId, int limit) { + return entries.stream() + .filter(entry -> entry.playerId.equals(playerId)) + .sorted(Comparator.comparingLong(WarningEntry::createdAt).reversed()) + .limit(Math.max(0, limit)) + .toList(); + } + + public List getActiveWarnings(UUID playerId) { + return entries.stream() + .filter(entry -> entry.playerId.equals(playerId) && entry.status() == WarningStatus.ACTIVE) + .sorted(Comparator.comparingLong(WarningEntry::createdAt).reversed()) + .toList(); + } + + public Optional findWarning(UUID warningId) { + if (warningId == null) { + return Optional.empty(); + } + return entries.stream().filter(entry -> entry.id.equals(warningId)).findFirst(); + } + + public boolean revokeWarning(UUID warningId, Player moderator, String reason) { + if (warningId == null || moderator == null || reason == null || reason.isBlank()) { + return false; + } + + for (int index = 0; index < entries.size(); index++) { + WarningEntry current = entries.get(index); + if (!current.id.equals(warningId) || current.status() != WarningStatus.ACTIVE) { + continue; + } + entries.set(index, current.revokedBy(moderator.getName(), sanitizeReason(reason), System.currentTimeMillis())); + save(); + return true; + } + return false; + } + + private String sanitizeReason(String reason) { + if (reason == null || reason.isBlank()) { + return "без причины"; + } + return reason.replace('\n', ' ').replace('\r', ' ').trim(); + } + + private void load() { + entries.clear(); + if (!storageFile.exists()) { + return; + } + + try (FileReader reader = new FileReader(storageFile)) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonArray()) { + return; + } + for (JsonElement element : root.getAsJsonArray()) { + if (!element.isJsonObject()) { + continue; + } + WarningEntry entry = readEntry(element.getAsJsonObject()); + if (entry != null) { + entries.add(entry); + } + } + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить warn-history.json: " + exception.getMessage()); + } + } + + private WarningEntry readEntry(JsonObject json) { + try { + long createdAt = json.get("createdAt").getAsLong(); + boolean legacyActive = !json.has("active") || json.get("active").getAsBoolean(); + long revokedAt = json.has("revokedAt") + ? json.get("revokedAt").getAsLong() + : legacyActive ? NOT_REVOKED : 0L; + return new WarningEntry( + json.has("id") ? UUID.fromString(json.get("id").getAsString()) : UUID.randomUUID(), + UUID.fromString(json.get("playerId").getAsString()), + json.get("playerName").getAsString(), + json.get("moderatorName").getAsString(), + json.get("reason").getAsString(), + createdAt, + json.has("expiresAt") ? json.get("expiresAt").getAsLong() : PunishmentDuration.PERMANENT_EXPIRY, + revokedAt, + readString(json, "revokedBy", legacyActive ? "" : "неизвестно"), + readString(json, "revocationReason", legacyActive ? "" : "данные отсутствуют")); + } catch (Exception exception) { + plugin.getLogger().warning("Не удалось загрузить запись warn: " + exception.getMessage()); + return null; + } + } + + private String readString(JsonObject json, String key, String fallback) { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : fallback; + } + + private void save() { + File parent = storageFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + JsonArray root = new JsonArray(); + for (WarningEntry entry : entries) { + JsonObject json = new JsonObject(); + json.addProperty("id", entry.id.toString()); + json.addProperty("playerId", entry.playerId.toString()); + json.addProperty("playerName", entry.playerName); + json.addProperty("moderatorName", entry.moderatorName); + json.addProperty("reason", entry.reason); + json.addProperty("createdAt", entry.createdAt); + json.addProperty("expiresAt", entry.expiresAt); + json.addProperty("revokedAt", entry.revokedAt); + json.addProperty("revokedBy", entry.revokedBy); + json.addProperty("revocationReason", entry.revocationReason); + json.addProperty("active", entry.status() == WarningStatus.ACTIVE); + root.add(json); + } + + try (FileWriter writer = new FileWriter(storageFile)) { + writer.write(gson.toJson(root)); + } catch (IOException exception) { + plugin.getLogger().warning("Не удалось сохранить warn-history.json: " + exception.getMessage()); + } + } + + public enum WarningStatus { + ACTIVE, + EXPIRED, + REVOKED + } + + public static final class WarningEntry { + private final UUID id; + private final UUID playerId; + private final String playerName; + private final String moderatorName; + private final String reason; + private final long createdAt; + private final long expiresAt; + private final long revokedAt; + private final String revokedBy; + private final String revocationReason; + + private WarningEntry(UUID id, UUID playerId, String playerName, String moderatorName, String reason, + long createdAt, long expiresAt, long revokedAt, String revokedBy, String revocationReason) { + this.id = id; + this.playerId = playerId; + this.playerName = playerName; + this.moderatorName = moderatorName; + this.reason = reason; + this.createdAt = createdAt; + this.expiresAt = expiresAt; + this.revokedAt = revokedAt; + this.revokedBy = revokedBy; + this.revocationReason = revocationReason; + } + + private WarningEntry revokedBy(String actor, String revocationReason, long timestamp) { + return new WarningEntry(id, playerId, playerName, moderatorName, reason, createdAt, expiresAt, + timestamp, actor, revocationReason); + } + + public UUID id() { return id; } + public UUID playerId() { return playerId; } + public String playerName() { return playerName; } + public String moderatorName() { return moderatorName; } + public String reason() { return reason; } + public long createdAt() { return createdAt; } + public long expiresAt() { return expiresAt; } + public long revokedAt() { return revokedAt; } + public String revokedBy() { return revokedBy; } + public String revocationReason() { return revocationReason; } + + public boolean isPermanent() { + return expiresAt == PunishmentDuration.PERMANENT_EXPIRY; + } + + public WarningStatus status() { + if (revokedAt != NOT_REVOKED) { + return WarningStatus.REVOKED; + } + if (!isPermanent() && System.currentTimeMillis() >= expiresAt) { + return WarningStatus.EXPIRED; + } + return WarningStatus.ACTIVE; + } + + public boolean active() { + return status() == WarningStatus.ACTIVE; + } + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java b/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java new file mode 100755 index 0000000..0a3c626 --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/PlayerFreezeManager.java @@ -0,0 +1,127 @@ +package org.blz.adminmode.moderation; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class PlayerFreezeManager implements Listener { + + private static final long NOTIFY_COOLDOWN_MS = 1500L; + + private final Plugin plugin; + private final Set frozenPlayers = new HashSet<>(); + private final Map lastNotify = new HashMap<>(); + + public PlayerFreezeManager(Plugin plugin) { + this.plugin = plugin; + } + + public boolean toggleFreeze(Player target) { + UUID uuid = target.getUniqueId(); + if (frozenPlayers.remove(uuid)) { + target.sendMessage(Component.text("Вы больше не заморожены.", NamedTextColor.GREEN)); + return false; + } + frozenPlayers.add(uuid); + target.sendMessage(Component.text("Вы заморожены. Любые действия заблокированы.", NamedTextColor.RED)); + return true; + } + + public boolean isFrozen(UUID playerId) { + return frozenPlayers.contains(playerId); + } + + private boolean shouldBlock(Player player) { + return frozenPlayers.contains(player.getUniqueId()); + } + + private void notifyFrozen(Player player) { + long now = System.currentTimeMillis(); + long previous = lastNotify.getOrDefault(player.getUniqueId(), 0L); + if (now - previous < NOTIFY_COOLDOWN_MS) { + return; + } + lastNotify.put(player.getUniqueId(), now); + player.sendActionBar(Component.text("Вы заморожены модератором.", NamedTextColor.RED)); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + if (!shouldBlock(player) || event.getTo() == null) return; + if (event.getFrom().distanceSquared(event.getTo()) > 0.0D) { + event.setTo(event.getFrom()); + notifyFrozen(player); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInteract(PlayerInteractEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onDrop(PlayerDropItemEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onPickup(EntityPickupItemEvent event) { + if (!(event.getEntity() instanceof Player player)) return; + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onCommand(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + if (!shouldBlock(player)) return; + event.setCancelled(true); + notifyFrozen(player); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + if (!shouldBlock(event.getPlayer())) return; + event.setCancelled(true); + notifyFrozen(event.getPlayer()); + } +} diff --git a/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java b/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java old mode 100644 new mode 100755 index dc9005f..90eb3c5 --- a/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java +++ b/src/main/java/org/blz/adminmode/moderation/PlayerListAction.java @@ -3,5 +3,6 @@ package org.blz.adminmode.moderation; public enum PlayerListAction { SPECTATE, TELEPORT, - INVSEE + INVSEE, + MANAGE } diff --git a/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java b/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java new file mode 100644 index 0000000..9457f3a --- /dev/null +++ b/src/main/java/org/blz/adminmode/moderation/PunishmentDuration.java @@ -0,0 +1,57 @@ +package org.blz.adminmode.moderation; + +import java.time.Duration; +import java.util.Locale; + +public final class PunishmentDuration { + + public static final long PERMANENT_EXPIRY = -1L; + + private PunishmentDuration() { + } + + public static String normalizeToken(String input) { + if (input == null || input.isBlank()) { + return ""; + } + + String normalized = input.trim().toLowerCase(Locale.ROOT); + if (normalized.equals("perm") || normalized.equals("permanent") || normalized.equals("permanently")) { + return "permanent"; + } + if (!normalized.matches("[1-9]\\d*[smhdwy]")) { + return ""; + } + + try { + toDuration(normalized); + return normalized; + } catch (ArithmeticException | IllegalArgumentException exception) { + return ""; + } + } + + public static long toExpiry(String token, long now) { + String normalized = normalizeToken(token); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("Unsupported punishment duration: " + token); + } + if (normalized.equals("permanent")) { + return PERMANENT_EXPIRY; + } + return Math.addExact(now, toDuration(normalized).toMillis()); + } + + private static Duration toDuration(String normalized) { + long amount = Long.parseLong(normalized.substring(0, normalized.length() - 1)); + return switch (normalized.charAt(normalized.length() - 1)) { + case 's' -> Duration.ofSeconds(amount); + case 'm' -> Duration.ofMinutes(amount); + case 'h' -> Duration.ofHours(amount); + case 'd' -> Duration.ofDays(amount); + case 'w' -> Duration.ofDays(Math.multiplyExact(amount, 7L)); + case 'y' -> Duration.ofDays(Math.multiplyExact(amount, 365L)); + default -> throw new IllegalArgumentException("Unsupported punishment duration: " + normalized); + }; + } +} diff --git a/src/main/java/org/blz/adminmode/session/SessionStorage.java b/src/main/java/org/blz/adminmode/session/SessionStorage.java old mode 100644 new mode 100755 index 1089afd..e1ffab3 --- a/src/main/java/org/blz/adminmode/session/SessionStorage.java +++ b/src/main/java/org/blz/adminmode/session/SessionStorage.java @@ -8,29 +8,22 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.blz.adminmode.moderation.ModeSession; import org.blz.adminmode.moderation.ModeratorProfile; +import org.blz.adminmode.utils.ItemStackCodec; +import org.blz.adminmode.utils.PotionEffectCodec; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -128,7 +121,7 @@ public class SessionStorage { UUID playerId = UUID.fromString(json.get("playerId").getAsString()); String playerName = json.get("playerName").getAsString(); ModeratorProfile profile = ModeratorProfile.fromConfigKey(json.get("profile").getAsString()) - .orElse(ModeratorProfile.SOFT_MODER); + .orElse(ModeratorProfile.ADMIN); long startTimestamp = json.get("startTimestamp").getAsLong(); String previousGroup = getString(json, "previousGroup"); String activeGroup = getString(json, "activeGroup"); @@ -224,14 +217,7 @@ public class SessionStorage { private String serializeItemArray(ItemStack[] items) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeInt(items.length); - for (ItemStack item : items) { - dataOutput.writeObject(item); - } - } - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItems(items); } catch (Exception exception) { plugin.getLogger().warning("Не удалось сериализовать item array: " + exception.getMessage()); return ""; @@ -244,15 +230,7 @@ public class SessionStorage { } try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { - int length = dataInput.readInt(); - ItemStack[] items = new ItemStack[length]; - for (int i = 0; i < length; i++) { - items[i] = (ItemStack) dataInput.readObject(); - } - return items; - } + return ItemStackCodec.deserializeItems(data); } catch (Exception exception) { plugin.getLogger().warning("Не удалось десериализовать item array: " + exception.getMessage()); return new ItemStack[0]; @@ -261,11 +239,7 @@ public class SessionStorage { private String serializeItem(ItemStack item) { try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) { - dataOutput.writeObject(item); - } - return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + return ItemStackCodec.serializeItem(item); } catch (Exception exception) { plugin.getLogger().warning("Не удалось сериализовать item: " + exception.getMessage()); return ""; @@ -274,62 +248,24 @@ public class SessionStorage { private ItemStack deserializeItem(String data) { if (data == null || data.isEmpty()) { - return new ItemStack(Material.AIR); + return ItemStack.empty(); } try { - ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); - try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { - return (ItemStack) dataInput.readObject(); - } + return ItemStackCodec.deserializeItem(data); } catch (Exception exception) { plugin.getLogger().warning("Не удалось десериализовать item: " + exception.getMessage()); - return new ItemStack(Material.AIR); + return ItemStack.empty(); } } private String serializePotionEffects(Collection effects) { - StringBuilder builder = new StringBuilder(); - for (PotionEffect effect : effects) { - builder.append(effect.getType().getName()).append(':') - .append(effect.getDuration()).append(':') - .append(effect.getAmplifier()).append(':') - .append(effect.isAmbient()).append(':') - .append(effect.hasParticles()).append(':') - .append(effect.hasIcon()).append(';'); - } - return builder.toString(); + return PotionEffectCodec.serialize(effects); } private Collection deserializePotionEffects(String data) { - List effects = new ArrayList<>(); - if (data == null || data.isEmpty()) { - return effects; - } - - String[] effectStrings = data.split(";"); - for (String effectString : effectStrings) { - if (effectString.isEmpty()) { - continue; - } - - try { - String[] parts = effectString.split(":"); - PotionEffectType type = PotionEffectType.getByName(parts[0]); - if (type == null) { - continue; - } - effects.add(new PotionEffect( - type, - Integer.parseInt(parts[1]), - Integer.parseInt(parts[2]), - Boolean.parseBoolean(parts[3]), - Boolean.parseBoolean(parts[4]), - Boolean.parseBoolean(parts[5]))); - } catch (Exception exception) { - plugin.getLogger().warning("Не удалось десериализовать potion effect: " + effectString); - } - } - return effects; + return PotionEffectCodec.deserialize(data, + encodedEffect -> plugin.getLogger().warning( + "Не удалось десериализовать potion effect: " + encodedEffect)); } } diff --git a/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java b/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java new file mode 100644 index 0000000..dac4328 --- /dev/null +++ b/src/main/java/org/blz/adminmode/utils/ItemStackCodec.java @@ -0,0 +1,85 @@ +package org.blz.adminmode.utils; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; + +/** + * Stores item stacks using Paper's data-version-aware NBT format while retaining + * read compatibility with sessions written by older AdminMode releases. + */ +public final class ItemStackCodec { + + private static final String ITEM_FORMAT_PREFIX = "nbt-item-v1:"; + private static final String ITEMS_FORMAT_PREFIX = "nbt-items-v1:"; + + private ItemStackCodec() { + } + + public static String serializeItems(ItemStack[] items) { + ItemStack[] safeItems = items == null ? new ItemStack[0] : items; + return ITEMS_FORMAT_PREFIX + + Base64.getEncoder().encodeToString(ItemStack.serializeItemsAsBytes(safeItems)); + } + + public static ItemStack[] deserializeItems(String data) throws IOException, ClassNotFoundException { + if (data == null || data.isEmpty()) { + return new ItemStack[0]; + } + if (data.startsWith(ITEMS_FORMAT_PREFIX)) { + byte[] bytes = decode(data.substring(ITEMS_FORMAT_PREFIX.length())); + return ItemStack.deserializeItemsFromBytes(bytes); + } + return deserializeLegacyItems(data); + } + + public static String serializeItem(ItemStack item) { + ItemStack safeItem = item == null ? ItemStack.empty() : item; + return ITEM_FORMAT_PREFIX + + Base64.getEncoder().encodeToString(safeItem.serializeAsBytes()); + } + + public static ItemStack deserializeItem(String data) throws IOException, ClassNotFoundException { + if (data == null || data.isEmpty()) { + return ItemStack.empty(); + } + if (data.startsWith(ITEM_FORMAT_PREFIX)) { + byte[] bytes = decode(data.substring(ITEM_FORMAT_PREFIX.length())); + return ItemStack.deserializeBytes(bytes); + } + return deserializeLegacyItem(data); + } + + private static byte[] decode(String data) throws IOException { + try { + return Base64.getDecoder().decode(data); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid Base64 item data", exception); + } + } + + @SuppressWarnings("deprecation") + private static ItemStack[] deserializeLegacyItems(String data) throws IOException, ClassNotFoundException { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { + int length = dataInput.readInt(); + ItemStack[] items = new ItemStack[length]; + for (int i = 0; i < length; i++) { + items[i] = (ItemStack) dataInput.readObject(); + } + return items; + } + } + + @SuppressWarnings("deprecation") + private static ItemStack deserializeLegacyItem(String data) throws IOException, ClassNotFoundException { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) { + ItemStack item = (ItemStack) dataInput.readObject(); + return item == null ? ItemStack.empty() : item; + } + } +} diff --git a/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java b/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java new file mode 100644 index 0000000..97b5fd5 --- /dev/null +++ b/src/main/java/org/blz/adminmode/utils/PotionEffectCodec.java @@ -0,0 +1,86 @@ +package org.blz.adminmode.utils; + +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +/** Encodes potion effects with registry keys and reads the legacy enum-name format. */ +public final class PotionEffectCodec { + + private static final String FORMAT_PREFIX = "potion-v2:"; + + private PotionEffectCodec() { + } + + public static String serialize(Collection effects) { + StringBuilder builder = new StringBuilder(FORMAT_PREFIX); + if (effects == null) { + return builder.toString(); + } + + for (PotionEffect effect : effects) { + NamespacedKey key = Registry.MOB_EFFECT.getKey(effect.getType()); + builder.append(key).append('|') + .append(effect.getDuration()).append('|') + .append(effect.getAmplifier()).append('|') + .append(effect.isAmbient()).append('|') + .append(effect.hasParticles()).append('|') + .append(effect.hasIcon()).append(';'); + } + return builder.toString(); + } + + public static Collection deserialize(String data, Consumer warningSink) { + List effects = new ArrayList<>(); + if (data == null || data.isEmpty()) { + return effects; + } + + boolean currentFormat = data.startsWith(FORMAT_PREFIX); + String payload = currentFormat ? data.substring(FORMAT_PREFIX.length()) : data; + for (String encodedEffect : payload.split(";")) { + if (encodedEffect.isEmpty()) { + continue; + } + + try { + String[] parts = currentFormat + ? encodedEffect.split("\\|", -1) + : encodedEffect.split(":", -1); + if (parts.length != 6) { + throw new IllegalArgumentException("Expected 6 fields"); + } + + PotionEffectType type = resolveType(parts[0], currentFormat); + if (type == null) { + throw new IllegalArgumentException("Unknown effect " + parts[0]); + } + + effects.add(new PotionEffect( + type, + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2]), + Boolean.parseBoolean(parts[3]), + Boolean.parseBoolean(parts[4]), + Boolean.parseBoolean(parts[5]))); + } catch (RuntimeException exception) { + warningSink.accept(encodedEffect); + } + } + return effects; + } + + private static PotionEffectType resolveType(String identifier, boolean currentFormat) { + NamespacedKey key = currentFormat + ? NamespacedKey.fromString(identifier) + : NamespacedKey.minecraft(identifier.toLowerCase(Locale.ROOT)); + return key == null ? null : Registry.MOB_EFFECT.get(key); + } +} diff --git a/src/main/java/org/blz/adminmode/utils/ScreenEffects.java b/src/main/java/org/blz/adminmode/utils/ScreenEffects.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/blz/adminmode/utils/WorldLoader.java b/src/main/java/org/blz/adminmode/utils/WorldLoader.java old mode 100644 new mode 100755 index 0098173..e42d9f8 --- a/src/main/java/org/blz/adminmode/utils/WorldLoader.java +++ b/src/main/java/org/blz/adminmode/utils/WorldLoader.java @@ -38,6 +38,7 @@ public class WorldLoader { return; } } + cleanupSkippedFiles(worldFolder); plugin.getLogger().info("Загрузка мира " + worldName + "..."); WorldCreator creator = new WorldCreator(worldName); @@ -64,8 +65,18 @@ public class WorldLoader { try (ZipInputStream zipIn = new ZipInputStream(in)) { ZipEntry entry = zipIn.getNextEntry(); + String destinationPath = destination.getCanonicalPath() + File.separator; while (entry != null) { + if (shouldSkipEntry(entry.getName())) { + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + continue; + } File file = new File(destination, entry.getName()); + String filePath = file.getCanonicalPath(); + if (!filePath.startsWith(destinationPath)) { + throw new IOException("Blocked unsafe zip entry: " + entry.getName()); + } if (entry.isDirectory()) { file.mkdirs(); } else { @@ -91,4 +102,52 @@ public class WorldLoader { return false; } } + + private boolean shouldSkipEntry(String entryName) { + String normalized = entryName.replace('\\', '/'); + return normalized.equals("level.dat") + || normalized.equals("level.dat_old") + || normalized.equals("session.lock") + || normalized.equals("icon.png") + || normalized.equals("xaeromap.txt") + || normalized.startsWith("serverconfig/") + || normalized.startsWith("playerdata/") + || normalized.startsWith("stats/") + || normalized.startsWith("advancements/"); + } + + private void cleanupSkippedFiles(File destination) { + deleteIfExists(new File(destination, "level.dat")); + deleteIfExists(new File(destination, "level.dat_old")); + deleteIfExists(new File(destination, "session.lock")); + deleteIfExists(new File(destination, "icon.png")); + deleteIfExists(new File(destination, "xaeromap.txt")); + deleteRecursively(new File(destination, "serverconfig")); + deleteRecursively(new File(destination, "playerdata")); + deleteRecursively(new File(destination, "stats")); + deleteRecursively(new File(destination, "advancements")); + } + + private void deleteIfExists(File file) { + if (file.exists() && !file.delete()) { + plugin.getLogger().warning("Не удалось удалить файл " + file.getAbsolutePath()); + } + } + + private void deleteRecursively(File file) { + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + if (!file.delete()) { + plugin.getLogger().warning("Не удалось удалить " + file.getAbsolutePath()); + } + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml old mode 100644 new mode 100755 index 0dbd2bf..ff81769 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,77 +1,43 @@ # AdminMode Configuration -#РЕЖИМ АДМИНИСТРАТОРА admin_mode: - # Включен ли режим администратора enabled: true - - # LuckPerms настройки + luckperms: - # Использовать ли LuckPerms (если false - используется стандартный OP) enabled: true - - # Группы которые выдаются при активации режима - # Можно указать несколько групп через запятую + # LuckPerms-группы, которые выдаются при входе в режим groups: - - "soft_moder_adm" - profile_groups: - soft_moder: - - "soft_moder_adm" - hard_moder: - - "hard_moder_adm" - - # Удалять ли группы при деактивации (если false - группы останутся) + - "admin_moder" remove_on_disable: true dialogs: + # Глобальный мастер-переключатель Dialog API (false = отключить для всего сервера). + # Каждый модератор лично включает/выключает своё GUI командой /adminmode dialog enabled: true - - # Настройки игрового режима + gamemode: - # Принудительно ставить спектатор (если false - survival) + # false = chiefs can use CREATIVE, regular moderators use SPECTATOR + # true = everyone enters in SPECTATOR force_spectator: false - - # Мобы игнорируют игрока в админ моде mobs_ignore_player: true - - # Разрешить полет в режиме выживания (в админ моде) allow_flight_in_survival: true - - # Режим бога (неуязвимость) god_mode: true - - # Кастомная скорость полета (0.1 = медленно, 0.2 = нормально, 0.5 = быстро, 1.0 = очень быстро) + chief_creative: true + # Скорость полёта: 0.1 (медленно) ... 1.0 (максимум) custom_fly_speed: 0.2 - - # Кастомная скорость ходьбы (0.1 = медленно, 0.2 = нормально, 0.5 = быстро) custom_walk_speed: 0.2 - - # Разрешенные режимы игры для переключения в админ моде - # Если список пустой - можно переключаться на любые режимы - # Доступные: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR - # ВАЖНО: CREATIVE убран - модеры не должны иметь доступ к креативу! + # Разрешённые режимы для переключения внутри админ-мода (пусто = любые) allowed_gamemodes: + - "CREATIVE" - "SURVIVAL" - "SPECTATOR" - - # Ограничения в режиме администратора + restrictions: - # Запретить выбрасывание предметов prevent_item_drop: false - - # Запретить поднятие предметов prevent_item_pickup: false - - # Запретить перемещение предметов в контейнеры (сундуки, бочки и т.д.) prevent_container_transfer: true - - # Запретить использование предметов (кроме открытия контейнеров) prevent_item_use: false - - # Разрешить управление своим инвентарем allow_own_inventory: true - - # Разрешенные блоки для взаимодействия (открытие, использование) allowed_blocks: - "CHEST" - "TRAPPED_CHEST" @@ -88,27 +54,18 @@ admin_mode: - "FENCE_GATE" - "BUTTON" - "LEVER" - - "ENDER_CHEST" - "CRAFTING_TABLE" - "ENCHANTING_TABLE" - "ANVIL" - "BREWING_STAND" - - # Разрешенные блоки для постановки (whitelist) - # Если список пустой - можно ставить любые блоки - # Если список заполнен - можно ставить только эти блоки + # Whitelist блоков для постановки (пусто = любые) allowed_place_blocks: - - # Выдавать пресет блоков при входе в админ мод give_preset_blocks: false - - # Пресет блоков (формат: МАТЕРИАЛ:КОЛИЧЕСТВО) preset_blocks: - "BARRIER:64" - "LIGHT:64" - "STRUCTURE_VOID:64" - - # Сообщения + messages: enabled: "✓ Режим администратора активирован!" enabled_subtitle: "Ваше состояние сохранено." @@ -125,12 +82,11 @@ admin_mode: groups: base: "moder" - soft: "soft_moder_adm" - hard: "hard_moder_adm" + admin: "admin_moder" + chief: "chief_moder" items: - soft_panel_material: COMPASS - hard_panel_material: NETHER_STAR + panel_material: NETHER_STAR cp_inspector_material: STICK coreprotect: @@ -140,12 +96,33 @@ coreprotect: messages: prefix: "§8[§bAdminMode§8] " - enter_soft: "§aВошёл в режим §bSOFT MODER" - enter_hard: "§aВошёл в режим §cHARD MODER" + enter: "§aВошёл в §cADMIN MODE" exit: "§7Режим деактивирован. Состояние восстановлено." - cp_unavailable: "§cCoreProtect не найден. Hard режим работает без CP." + cp_unavailable: "§cCoreProtect не найден или отключён." session_recovered: "§eВаша активная сессия модерации была восстановлена и корректно завершена." logging: enabled: true log_file: "logs/sessions.log" + +moderation_actions: + dispatch_as_console: true + durations: + # Варианты, отображаемые в Dialog. permanent показывается как Permanently. + options: + - "1h" + - "1d" + - "7d" + - "30d" + - "permanent" + defaults: + warn: "7d" + mute: "1h" + ban: "7d" + commands: + warn: "warn {target} {duration} {reason}" + mute: "mute {target} {duration} actor:{moderator} {reason}" + # Namespace исключает конфликт с одноимённой командой FlectonePulse. + unmute: "moderationcommands:unmute {target} actor:{moderator} {reason}" + ban: "ban {target} {duration} actor:{moderator} {reason}" + kick: "kick {target} actor:{moderator} {reason}" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml old mode 100644 new mode 100755 index db70652..da47eda --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: AdminMode -version: '1.21.11' +version: '${version}' main: org.blz.adminmode.AdminMode -api-version: '1.21' +api-version: '26.1.2' author: BelzeBool, Kilka_v_HJIebe description: Плагин режима администратора с сохранением состояния игрока depend: [LuckPerms] @@ -10,7 +10,7 @@ softdepend: [CoreProtect] commands: adminmode: description: Режим администратора с сохранением состояния - usage: / + usage: / [on|off|toggle|dialog|disable|restrict|unrestrict|status|reload] aliases: [adm, am] admfix: description: Отключить все способности у игрока (полет, бессмертие) @@ -20,15 +20,36 @@ permissions: adminmode.use: description: Разрешает использовать режим администратора default: op - adminmode.profile.soft: - description: Разрешает использовать профиль soft_moder - default: op - adminmode.profile.hard: - description: Разрешает использовать профиль hard_moder - default: op adminmode.reload: description: Разрешает перезагружать конфигурацию AdminMode default: op adminmode.admin: - description: Разрешает использовать сильные действия и админ команды (disable, admfix) + description: Разрешает использовать сильные действия и команды (disable, admfix) default: op + adminmode.chief: + description: Разрешает главному модератору ограничивать доступ к admin mode + default: op + adminmode.punishment.warn: + description: Разрешает выдавать предупреждения через AdminMode + default: op + adminmode.punishment.unwarn: + description: Разрешает снимать предупреждения через AdminMode + default: op + adminmode.punishment.mute: + description: Разрешает выдавать муты через AdminMode + default: op + adminmode.punishment.unmute: + description: Разрешает снимать муты через AdminMode + default: op + adminmode.punishment.kick: + description: Разрешает кикать игроков через AdminMode + default: op + adminmode.punishment.ban: + description: Разрешает банить игроков через AdminMode + default: op + adminmode.punishment.unban: + description: Разрешает снимать баны через AdminMode + default: op + adminmode.special.belzebool: + description: Разрешает специальные способности BelzeBool + default: false diff --git a/src/main/resources/pocket_dimension.zip b/src/main/resources/pocket_dimension.zip old mode 100644 new mode 100755