From 188297714d9188f7ed1deffedb10bd74b60021b2 Mon Sep 17 00:00:00 2001 From: Stanislav Panchenko Date: Thu, 30 Apr 2026 12:28:01 +0500 Subject: [PATCH] Add Russian localization --- astro.config.mjs | 14 +- .../components-demo-en.mdx | 0 examples/components-demo-ru.mdx | 393 ++++++ src/content/docs/en/advanced/animations.md | 4 +- src/content/docs/en/developers/addons.mdx | 160 +-- src/content/docs/en/developers/general.mdx | 16 +- src/content/docs/en/developers/handlers.md | 2 +- src/content/docs/en/developers/migration.mdx | 2 +- src/content/docs/en/general/activators.md | 42 +- src/content/docs/en/general/cheatsheet.md | 4 +- src/content/docs/en/general/commands.md | 14 +- src/content/docs/en/index.mdx | 2 +- src/content/docs/en/start/config.mdx | 2 +- src/content/docs/en/start/faq.mdx | 2 +- src/content/docs/en/start/installation.mdx | 6 +- src/content/docs/en/start/tips.md | 4 +- src/content/docs/ru/advanced/animations.md | 210 ++++ src/content/docs/ru/advanced/drag-and-drop.md | 210 ++++ src/content/docs/ru/advanced/generation.mdx | 284 +++++ src/content/docs/ru/advanced/input.mdx | 519 ++++++++ src/content/docs/ru/advanced/logical.md | 420 +++++++ src/content/docs/ru/advanced/templates.md | 169 +++ src/content/docs/ru/changelog/index.mdx | 31 + src/content/docs/ru/developers/addons.mdx | 233 ++++ src/content/docs/ru/developers/general.mdx | 200 +++ src/content/docs/ru/developers/handlers.md | 150 +++ src/content/docs/ru/developers/migration.mdx | 148 +++ src/content/docs/ru/developers/own-types.md | 384 ++++++ src/content/docs/ru/developers/serializers.md | 186 +++ src/content/docs/ru/developers/utils.md | 88 ++ src/content/docs/ru/developers/variables.md | 76 ++ src/content/docs/ru/general/actions.md | 1068 +++++++++++++++++ src/content/docs/ru/general/activators.md | 402 +++++++ src/content/docs/ru/general/cheatsheet.md | 161 +++ src/content/docs/ru/general/commands.md | 85 ++ src/content/docs/ru/general/examples.mdx | 153 +++ src/content/docs/ru/general/item-format.md | 601 ++++++++++ src/content/docs/ru/general/menu-structure.md | 358 ++++++ src/content/docs/ru/general/placeholders.md | 232 ++++ src/content/docs/ru/general/rules.md | 300 +++++ src/content/docs/ru/general/text-colors.md | 68 ++ src/content/docs/ru/general/variables.md | 60 + src/content/docs/ru/index.mdx | 106 ++ src/content/docs/ru/start/config.mdx | 171 +++ src/content/docs/ru/start/faq.mdx | 110 ++ src/content/docs/ru/start/hocon.md | 252 ++++ src/content/docs/ru/start/how-to.mdx | 230 ++++ src/content/docs/ru/start/installation.mdx | 67 ++ src/content/docs/ru/start/tips.md | 40 +- 49 files changed, 8306 insertions(+), 133 deletions(-) rename src/content/docs/en/demo/components.mdx => examples/components-demo-en.mdx (100%) create mode 100644 examples/components-demo-ru.mdx create mode 100644 src/content/docs/ru/advanced/animations.md create mode 100644 src/content/docs/ru/advanced/drag-and-drop.md create mode 100644 src/content/docs/ru/advanced/generation.mdx create mode 100644 src/content/docs/ru/advanced/input.mdx create mode 100644 src/content/docs/ru/advanced/logical.md create mode 100644 src/content/docs/ru/advanced/templates.md create mode 100644 src/content/docs/ru/changelog/index.mdx create mode 100644 src/content/docs/ru/developers/addons.mdx create mode 100644 src/content/docs/ru/developers/general.mdx create mode 100644 src/content/docs/ru/developers/handlers.md create mode 100644 src/content/docs/ru/developers/migration.mdx create mode 100644 src/content/docs/ru/developers/own-types.md create mode 100644 src/content/docs/ru/developers/serializers.md create mode 100644 src/content/docs/ru/developers/utils.md create mode 100644 src/content/docs/ru/developers/variables.md create mode 100644 src/content/docs/ru/general/actions.md create mode 100644 src/content/docs/ru/general/activators.md create mode 100644 src/content/docs/ru/general/cheatsheet.md create mode 100644 src/content/docs/ru/general/commands.md create mode 100644 src/content/docs/ru/general/examples.mdx create mode 100644 src/content/docs/ru/general/item-format.md create mode 100644 src/content/docs/ru/general/menu-structure.md create mode 100644 src/content/docs/ru/general/placeholders.md create mode 100644 src/content/docs/ru/general/rules.md create mode 100644 src/content/docs/ru/general/text-colors.md create mode 100644 src/content/docs/ru/general/variables.md create mode 100644 src/content/docs/ru/index.mdx create mode 100644 src/content/docs/ru/start/config.mdx create mode 100644 src/content/docs/ru/start/faq.mdx create mode 100644 src/content/docs/ru/start/hocon.md create mode 100644 src/content/docs/ru/start/how-to.mdx create mode 100644 src/content/docs/ru/start/installation.mdx diff --git a/astro.config.mjs b/astro.config.mjs index 1b12adf..bf4b16e 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -45,6 +45,7 @@ export default defineConfig({ sidebar: [ { label: "Getting Started", + translations: { ru: "Начало работы" }, items: [ { slug: "start/installation" }, { slug: "start/config" }, @@ -56,6 +57,7 @@ export default defineConfig({ }, { label: "Authoring menus", + translations: { ru: "Создание меню" }, items: [ { slug: "general/commands" }, { slug: "general/menu-structure" }, @@ -70,6 +72,7 @@ export default defineConfig({ }, { label: "Techniques", + translations: { ru: "Приёмы" }, items: [ { slug: "advanced/logical" }, { slug: "advanced/templates" }, @@ -81,6 +84,7 @@ export default defineConfig({ }, { label: "Reference", + translations: { ru: "Справочник" }, items: [ { slug: "general/cheatsheet" }, { slug: "general/examples" }, @@ -88,6 +92,7 @@ export default defineConfig({ }, { label: "For Developers", + translations: { ru: "Для разработчиков" }, items: [ { slug: "developers/general" }, { slug: "developers/addons" }, @@ -100,14 +105,7 @@ export default defineConfig({ ], }, { - label: "Demo", - collapsed: true, - items: [{ slug: "demo/components" }], - }, - { - label: "Changelog", - link: "/en/changelog/", - translations: { ru: "/ru/changelog/" }, + slug: "changelog", attrs: { "data-changelog-link": "true" }, }, ], diff --git a/src/content/docs/en/demo/components.mdx b/examples/components-demo-en.mdx similarity index 100% rename from src/content/docs/en/demo/components.mdx rename to examples/components-demo-en.mdx diff --git a/examples/components-demo-ru.mdx b/examples/components-demo-ru.mdx new file mode 100644 index 0000000..ed79409 --- /dev/null +++ b/examples/components-demo-ru.mdx @@ -0,0 +1,393 @@ +--- +title: Демо компонентов +description: Сравнение чистого Markdown и компонентов Starlight плюс возможности Expressive Code. +--- + +import { Tabs, TabItem, Steps, FileTree, Badge, LinkCard, CardGrid, Aside } from "@astrojs/starlight/components"; + +Каждое улучшение на странице показано дважды: сначала **версия на чистом Markdown**, потом **версия с компонентами**. Содержимое одинаковое, отличается только рендеринг. + +--- + +## 1. Tabs + +### До (чистый Markdown) + +Для Maven добавь это: + +```xml + + + jitpack + https://jitpack.io + + + + + com.github.AbstractMenus + api + 1.16 + provided + + +``` + +Для Gradle (Groovy): + +```groovy +repositories { + maven { url 'https://jitpack.io' } +} +dependencies { + compileOnly 'com.github.AbstractMenus:api:1.16' +} +``` + +Для Gradle (Kotlin): + +```kotlin +repositories { + maven("https://jitpack.io") +} +dependencies { + compileOnly("com.github.AbstractMenus:api:1.16") +} +``` + +### После (``) + + + + ```xml + + + jitpack + https://jitpack.io + + + + + com.github.AbstractMenus + api + 1.16 + provided + + + ``` + + + ```groovy + repositories { + maven { url 'https://jitpack.io' } + } + dependencies { + compileOnly 'com.github.AbstractMenus:api:1.16' + } + ``` + + + ```kotlin + repositories { + maven("https://jitpack.io") + } + dependencies { + compileOnly("com.github.AbstractMenus:api:1.16") + } + ``` + + + +`syncKey` синхронизирует все группы Tabs на странице - выбрав "Gradle (Kotlin)" один раз, ты обновишь все последующие сниппеты с тем же ключом. + +--- + +## 2. Steps + +### До (чистый Markdown) + +1. Создай файл `.conf` внутри `plugins/AbstractMenus/menus/`. +2. Добавь в файл `title` и `size`. +3. Добавь блок `activators`, который описывает что открывает меню. +4. Выполни `/am reload` на сервере. + +### После (``) + + + +1. Создай файл `.conf` внутри `plugins/AbstractMenus/menus/`. + +2. Добавь `title` и `size` в файл: + + ```hocon + title: "My first menu" + size: 6 + ``` + +3. Добавь блок `activators`, который описывает что открывает меню: + + ```hocon + activators { + command: "mymenu" + } + ``` + +4. Выполни `/am reload` на сервере, затем `/mymenu` в чате. + + + +Вертикальная линия и пронумерованные кружки наглядно показывают, где заканчивается один шаг и начинается следующий. В шаг можно положить целые блоки кода и несколько абзацев. + +--- + +## 3. FileTree + +### До (чистый Markdown) + +Структура папок плагина: + +``` +plugins/AbstractMenus/ + config.conf + menus/ + main.conf + shop.conf + casino/ + blackjack.conf + slots.conf + variables.db +``` + +### После (``) + + + +- plugins/AbstractMenus + - config.conf глобальный конфиг плагина + - menus + - main.conf + - shop.conf + - casino + - blackjack.conf + - slots.conf + - variables.db **SQLite, не редактировать вручную** + + + +Inline-аннотации уходят в правую колонку, папки сворачиваются по наведению, иконки подбираются по расширению файла автоматически. + +--- + +## 4. Badge + +### До (чистый Markdown) + +Действие `takeMoney` *(с 1.16, требует Vault)* - снимает деньги с баланса игрока. + +### После (``) + +`takeMoney` - снимает деньги с баланса игрока. + +Варианты: `note`, `tip`, `caution`, `danger`, `success`. Аккуратно встают рядом с заголовками и inline-ссылками. + +### Inline рядом с заголовком + +#### Действие `openMenu` + +Открывает другое меню, опционально для другого игрока. + +--- + +## 5. LinkCard/CardGrid + +### До (чистый Markdown) + +Смотри также: +- [Справочник по активаторам](/docs/ru/general/activators/) +- [Справочник по правилам](/docs/ru/general/rules/) +- [Формат предмета](/docs/ru/general/item-format/) + +### После (сетка ``) + + + + + + + +--- + +## 6. Варианты Aside + + + + + + + + + +Шорткат `:::note` из обычного Markdown рендерится в тот же компонент, поэтому старые страницы продолжают работать. + +--- + +## 7. Улучшения блоков кода (Expressive Code) + +### Подсветка строк + +Чтобы подсветить конкретные строки, перечисли их после тега языка: + +````md +```hocon {2,4-6} +title: "Shop" +size: 3 +items: [ + { + slot: 0 + material: DIAMOND + } +] +``` +```` + +```hocon {2,4-6} +title: "Shop" +size: 3 +items: [ + { + slot: 0 + material: DIAMOND + } +] +``` + +### Diff-маркеры + +Маркеры `del` и `ins` показывают "до/после" в одном блоке: + +````md +```hocon +title: "My menu" +size: 6 +activators { +- command: "menu" ++ command: "mymenu" +} +``` +```` + +```diff lang="hocon" + title: "My menu" + size: 6 + activators { +- command: "menu" ++ command: "mymenu" + } +``` + +### Имя файла + рамка + +````md +```hocon title="plugins/AbstractMenus/menus/main.conf" +title: "Main menu" +size: 6 +``` +```` + +```hocon title="plugins/AbstractMenus/menus/main.conf" +title: "Main menu" +size: 6 +``` + +### "ins/del" на уровне слов + +Ключевые слова `mark`, `ins`, `del` подсвечивают отдельные токены, а не целые строки: + +````md +```hocon "DIAMOND_SWORD" del="material:" ins="type:" +{ + slot: 0 + material: DIAMOND_SWORD + name: "Excalibur" +} +``` +```` + +```hocon "DIAMOND_SWORD" del="material:" ins="type:" +{ + slot: 0 + material: DIAMOND_SWORD + name: "Excalibur" +} +``` + +--- + +## 8. Комбинация: реальный раздел туториала + +Так выглядит Steps + Tabs + маркеры строк вместе: + + + +1. **Подключи API к сборке** + + + + ```groovy + repositories { + maven { url 'https://jitpack.io' } + } + dependencies { + compileOnly 'com.github.AbstractMenus:api:1.16' + } + ``` + + + ```xml + + com.github.AbstractMenus + api + 1.16 + provided + + ``` + + + +2. **Объяви AbstractMenus как опциональную зависимость в `plugin.yml` (`softdepend`)** + + ```yaml title="plugin.yml" {3-4} + name: MyPlugin + version: 1.0.0 + depend: + - AbstractMenus + ``` + +3. **Получи инстанс плагина** + + ```java title="src/main/java/com/example/MyPlugin.java" + AbstractMenusPlugin plugin = AbstractMenusProvider.get(); + ``` + + + +Сравни с той же последовательностью на [`/docs/ru/developers/general/`](/docs/ru/developers/general/), где ни один из этих компонентов не используется: текст плотнее и читать тяжелее. diff --git a/src/content/docs/en/advanced/animations.md b/src/content/docs/en/advanced/animations.md index 8d8fbf5..1fc0659 100644 --- a/src/content/docs/en/advanced/animations.md +++ b/src/content/docs/en/advanced/animations.md @@ -136,7 +136,7 @@ In the game, it looks like this: Here, our `someItem` item was placed in 9 different frames. In each next frame, we've set the item position to the next slot. Since the `clear` parameter was set to `true` by default, the inventory clears before each frame. -This is just the the simplest example of animation. If you wish, you can create truly complex and beautiful animations. +This is the simplest example. With a bit of experimenting you can build much more elaborate animations. ### Static items @@ -189,7 +189,7 @@ After we created frames, and saved all head textures, they can be added to the ` To add your animation, create a [list of strings](/docs/en/start/hocon/) anywhere in the config, similar to the `anim_eye` block. Each next element in the list is the next frame. Head animation always looped. You should know it when you creating animation frames. After you've added all frames to the file, save it. The animation is created, now it needs to be added to the item. -### Using added animation +### Attaching the animation to an item Any item has a `texture` parameter. Thanks to a special placeholder, this parameter can be used in a new way. Example: diff --git a/src/content/docs/en/developers/addons.mdx b/src/content/docs/en/developers/addons.mdx index 2612b9d..834abb7 100644 --- a/src/content/docs/en/developers/addons.mdx +++ b/src/content/docs/en/developers/addons.mdx @@ -1,6 +1,6 @@ --- title: Addon delivery -description: Two paths for shipping an AbstractMenus addon — plugin-as-addon, or AM-loaded jar — and the lifecycle hooks both share. +description: Two paths for shipping an AbstractMenus addon — AM-loaded jar, or plugin-as-addon — and the lifecycle hooks both share. --- import { Aside, Tabs, TabItem, LinkCard, CardGrid } from "@astrojs/starlight/components"; @@ -11,27 +11,31 @@ import { Aside, Tabs, TabItem, LinkCard, CardGrid } from "@astrojs/starlight/com AbstractMenus 2.0 is in alpha. The API surface may change before the stable release. Pin a specific `compileOnly` version in your build to avoid breaking on a fresh fetch. -An addon is anything that registers types or providers against AbstractMenus. The plugin's own built-in actions, rules, etc. register through the same SPI an external addon uses — there's no privileged path for first-party content. +An addon is anything that registers types or providers against AbstractMenus. The plugin's own built-in actions, rules, etc. register through the same SPI (Service Provider Interface - a standard Java mechanism that lets a plugin expose registration points for third-party jars to plug their implementations into) an external addon uses - there's no privileged path for first-party content. There are two delivery formats. Pick the one that fits your situation. -| | Path 1 — plugin-as-addon | Path 2 — AM-loaded addon | +| | Path 1 — AM-loaded addon | Path 2 — plugin-as-addon | |---|---|---| -| Jar drops into | `plugins/` | `plugins/AbstractMenus/addons/` | -| Loaded by | Bukkit | AbstractMenus | -| Manifest | `plugin.yml` | `addon.conf` (HOCON) | -| Main class extends | `JavaPlugin` + `MenuExtension` | `MenuExtension` only (no-arg ctor) | -| Visible in `/plugins` | Yes | No | -| Visible in `/am addons list` | Yes (tagged `[as-plugin]`) | Yes | -| Reload at runtime | `/reload` (whole server) | `/am addons reload ` | -| Per-jar classloader | No (Bukkit shares) | Yes (isolated, child-first) | -| Cross-plugin dependency order | Bukkit's `depend:` graph | `pluginDependencies:` in addon.conf | +| Jar drops into | `plugins/AbstractMenus/addons/` | `plugins/` | +| Loaded by | AbstractMenus | Bukkit | +| Manifest | `addon.conf` (HOCON) | `plugin.yml` | +| Main class extends | `MenuExtension` only (no-arg ctor) | `JavaPlugin` + `MenuExtension` | +| Visible in `/plugins` | No | Yes | +| Visible in `/am addons list` | Yes | Yes (tagged `[as-plugin]`) | +| Reload at runtime | `/am addons reload ` | `/reload` (whole server) | +| Per-jar classloader | Yes (isolated, child-first) | No (Bukkit shares) | +| Cross-plugin dependency order | `pluginDependencies:` in addon.conf | Bukkit's `depend:` graph | -**Pick Path 1** when your addon is also a real Bukkit plugin: registers Bukkit listeners, owns commands, exposes its own API for other plugins, or needs Bukkit's `depend:` for hard ordering against another plugin. +**Pick Path 1** when your addon is purely an AM extension (a provider bridge, a custom action type, a custom rule). The addon can be reloaded without restarting the server, the classloader is isolated (your dependencies don't clash with anyone else's), and your type ids don't collide with Bukkit plugins. -**Pick Path 2** when your addon is purely an AM extension (a provider bridge, a custom action type, a custom rule). You get hot reload, classloader isolation, and a cleaner namespace. +**Pick Path 2** when your addon is also a real Bukkit plugin: registers Bukkit listeners, owns commands, exposes its own API for other plugins, or needs Bukkit's `depend:` for hard ordering against another plugin. -For a pure provider bridge, both work. The reference [PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) ships both branches side by side as comparison material. +For a pure provider bridge, both work. As an example, [PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) ships both branches side by side for comparison. + +:::tip[Default to Path 1 (`as-addon`)] +Unless you specifically need Bukkit listeners, commands, or a public API exposed to other plugins, go with `as-addon`. It's cleaner and easier to operate. Only fall back to `as-plugin` when you actually require Bukkit-side functionality. +::: ## MenuExtension lifecycle @@ -57,58 +61,7 @@ All three hooks run on the main server thread. -## Path 1 — plugin-as-addon - -Make your `JavaPlugin` also implement `MenuExtension`. Bukkit calls your plugin's own `onEnable`; you forward to the SPI hook from there. - -```java title="MyAddon.java" -package com.example.myaddon; - -import org.bukkit.plugin.java.JavaPlugin; -import ru.abstractmenus.api.AbstractMenusApi; -import ru.abstractmenus.api.MenuExtension; - -public final class MyAddon extends JavaPlugin implements MenuExtension { - - @Override - public void onEnable() { - AbstractMenusApi api = AbstractMenusApi.get(); - if (api == null) { - getLogger().severe("AbstractMenus API not available — disabling."); - getServer().getPluginManager().disablePlugin(this); - return; - } - onEnable(api); - } - - @Override - public void onEnable(AbstractMenusApi api) { - api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); - // ... register the rest of your types and providers - } - - @Override - public String name() { return "MyAddon"; } - - @Override - public String version() { return getPluginMeta().getVersion(); } -} -``` - -```yaml title="plugin.yml" -name: MyAddon -version: 1.0.0 -main: com.example.myaddon.MyAddon -api-version: '1.21' -depend: - - AbstractMenus -``` - -`depend: [AbstractMenus]` is what guarantees AbstractMenus is enabled before your `onEnable` runs. Add other plugins (`PlayerPoints`, `WorldGuard`, etc.) to the same list if your registration logic touches their APIs at startup. - - - -## Path 2 — AM-loaded addon +## Path 1 — AM-loaded addon Implement `MenuExtension` directly. No `JavaPlugin`, no `plugin.yml`. Ship the jar with an `addon.conf` at its root. @@ -177,6 +130,57 @@ Drop the resulting jar into `plugins/AbstractMenus/addons/`. AbstractMenus picks Both list-typed fields accept either a HOCON list or a single string (auto-wrapped). + + +## Path 2 — plugin-as-addon + +Make your `JavaPlugin` also implement `MenuExtension`. Bukkit calls your plugin's own `onEnable`; you forward to the SPI hook from there. + +```java title="MyAddon.java" +package com.example.myaddon; + +import org.bukkit.plugin.java.JavaPlugin; +import ru.abstractmenus.api.AbstractMenusApi; +import ru.abstractmenus.api.MenuExtension; + +public final class MyAddon extends JavaPlugin implements MenuExtension { + + @Override + public void onEnable() { + AbstractMenusApi api = AbstractMenusApi.get(); + if (api == null) { + getLogger().severe("AbstractMenus API not available — disabling."); + getServer().getPluginManager().disablePlugin(this); + return; + } + onEnable(api); + } + + @Override + public void onEnable(AbstractMenusApi api) { + api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); + // ... register the rest of your types and providers + } + + @Override + public String name() { return "MyAddon"; } + + @Override + public String version() { return getPluginMeta().getVersion(); } +} +``` + +```yaml title="plugin.yml" +name: MyAddon +version: 1.0.0 +main: com.example.myaddon.MyAddon +api-version: '1.21' +depend: + - AbstractMenus +``` + +`depend: [AbstractMenus]` is what guarantees AbstractMenus is enabled before your `onEnable` runs. Add other plugins (`PlayerPoints`, `WorldGuard`, etc.) to the same list if your registration logic touches their APIs at startup. + ## Dependency resolution When AbstractMenus loads addons: @@ -192,17 +196,17 @@ If `onEnable` throws, AbstractMenus rolls back any registrations the addon manag ## Owner-based cleanup -Every `register(...)` call takes the `MenuExtension` instance as the last parameter: +Every `register(...)` call takes the `MenuExtension` instance as the last parameter - that's the "owner" of the registration, the addon that made it: ```java api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); // ^^^^ -// owner +// owner — your addon ``` -When the owning addon disables, AbstractMenus drops every type and provider registered under that owner. You don't (and can't) call `unregisterAll` from your addon — the public API doesn't expose it, so one addon can't wipe another's registrations. +When your addon disables, AbstractMenus drops every type and provider it registered. You don't (and can't) call `unregisterAll` from your addon — the public API doesn't expose it, so one addon can't wipe another's registrations. -For Path 1 plugins, "disable" means `onDisable` from `JavaPlugin`. Note that AbstractMenus's auto-cleanup only fires for addons it's tracking through its `AddonManager`, which means Path 2. For Path 1, your registration sits in AbstractMenus's owner-tracking map until AbstractMenus itself shuts down. That's harmless: the next `onEnable` overwrites the existing entry under the same id. +For Path 1 this is automatic: AbstractMenus owns the addon's lifecycle through its `AddonManager` and clears the registrations on disable. For Path 2, "disable" means `JavaPlugin.onDisable` and auto-cleanup doesn't fire — your registration sits in the owner-tracking map until AbstractMenus itself shuts down. That's harmless: the next `onEnable` overwrites the existing entry under the same id. ## /am addons command tree @@ -211,19 +215,19 @@ Operators manage AM-loaded addons through `/am addons`: ```text /am addons list List every loaded addon (Path 1, Path 2, built-in) /am addons info Full metadata: status, version, deps, error -/am addons load Load a Path 2 jar that's in addons/ but not yet loaded -/am addons reload Disable, rebuild classloader, re-enable a Path 2 addon +/am addons load Load a Path 1 jar that's in addons/ but not yet loaded +/am addons reload Disable, rebuild classloader, re-enable a Path 1 addon /am addons rescan Scan addons/ for new jars and load any not yet loaded ``` -`reload` and `load` are Path 2 only (they need a jar in `addons/`). `info` works for all three kinds. +`reload` and `load` are Path 1 only (they need a jar in `addons/`). `info` works for all three kinds. -## Reference implementation +## Example implementation -[PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) is a small reference addon that registers PlayerPoints as an economy provider. The repo has three branches: +[PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) is a small example addon that registers PlayerPoints as an economy provider. The repo has three branches: - [`master`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/master) — README only, comparison overview. -- [`as-plugin`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-plugin) — Path 1 implementation. -- [`as-addon`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-addon) — Path 2 implementation. +- [`as-addon`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-addon) — Path 1 implementation. +- [`as-plugin`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-plugin) — Path 2 implementation. Both functional branches register the same provider through the same SPI; the diff between them is mostly the lifecycle plumbing. diff --git a/src/content/docs/en/developers/general.mdx b/src/content/docs/en/developers/general.mdx index 6e151d5..fa20fe9 100644 --- a/src/content/docs/en/developers/general.mdx +++ b/src/content/docs/en/developers/general.mdx @@ -17,14 +17,14 @@ There are two ways to deliver an addon. Pick one: @@ -88,9 +88,9 @@ The artifactId is `minecraft-plugin` (the repo name). JitPack publishes multi-mo The API depends on Adventure (`net.kyori.adventure.text.*`). Paper bundles Adventure, so on Paper the dependency is satisfied automatically. On plain Spigot you'll need to shade or include Adventure yourself. -## Declare the dependency in plugin.yml (Path 1 only) +## Declare the dependency in plugin.yml (Path 2 only) -Path 1 plugin-as-addons declare AbstractMenus as a hard dependency so Bukkit guarantees ordering — your `onEnable` runs after AbstractMenus is up. +Path 2 plugin-as-addons declare AbstractMenus as a hard dependency so Bukkit guarantees ordering — your `onEnable` runs after AbstractMenus is up. ```yaml title="plugin.yml" ins={3-4} name: MyAddon @@ -101,7 +101,7 @@ depend: Use `softdepend:` if your plugin should still load when AbstractMenus is missing — then guard API calls with `Bukkit.getPluginManager().getPlugin("AbstractMenus") != null`. -Path 2 addons don't have a `plugin.yml`. They use `addon.conf` instead, with a `pluginDependencies` field. See the [addons page](/docs/en/developers/addons/). +Path 1 addons don't have a `plugin.yml`. They use `addon.conf` instead, with a `pluginDependencies` field. See the [addons page](/docs/en/developers/addons/). ## Get the API instance @@ -115,7 +115,7 @@ AbstractMenusApi api = AbstractMenusApi.get(); It returns `null` if AbstractMenus has not enabled yet. With `depend: [AbstractMenus]` declared, your `onEnable` will always see a non-null result. -For Path 2 addons, you don't call `get()` at all — the API is passed as a parameter to the lifecycle methods: +For Path 1 addons, you don't call `get()` at all — the API is passed as a parameter to the lifecycle methods: ```java public final class MyAddon implements MenuExtension { diff --git a/src/content/docs/en/developers/handlers.md b/src/content/docs/en/developers/handlers.md index 9b79482..1592d5d 100644 --- a/src/content/docs/en/developers/handlers.md +++ b/src/content/docs/en/developers/handlers.md @@ -147,4 +147,4 @@ Method names and signatures are stable across the 2.0 line. When your addon's `onDisable` runs, AbstractMenus drops every provider you registered automatically. You don't (and can't) call `unregisterAll` yourself — the public `ProviderRegistry` interface deliberately doesn't expose it. One addon can't wipe another's providers. -For Path 1 addons, "disable" means your `JavaPlugin.onDisable`. Note that AbstractMenus's auto-cleanup only fires for addons it's tracking through its `AddonManager`, which means Path 2 only. For Path 1, the registration sits in the owner-tracking map until AbstractMenus itself shuts down. Re-registering under the same id from a fresh `onEnable` overwrites the live entry, so this isn't a leak in practice. +For Path 1, cleanup is automatic: AbstractMenus tracks the addon through its `AddonManager` and drops the registrations on disable. For Path 2, "disable" means `JavaPlugin.onDisable`, and auto-cleanup doesn't fire — your registration sits in the owner-tracking map until AbstractMenus itself shuts down. Re-registering under the same id from a fresh `onEnable` overwrites the live entry, so this isn't a leak in practice. diff --git a/src/content/docs/en/developers/migration.mdx b/src/content/docs/en/developers/migration.mdx index db6abd9..ee1b1c5 100644 --- a/src/content/docs/en/developers/migration.mdx +++ b/src/content/docs/en/developers/migration.mdx @@ -26,7 +26,7 @@ If your addon was written against the 1.x API (`AbstractMenusProvider`, `Abstrac `AbstractMenusApi` replaces both `AbstractMenusPlugin` and `AbstractMenusProvider`. The static `get()` lookup goes through Bukkit's `ServicesManager` the same way the old provider did. -For Path 2 addons (new in 2.0), you don't call `get()` at all — the API is passed as a parameter to your `MenuExtension` lifecycle methods. See [addon delivery](/docs/en/developers/addons/). +For Path 1 addons (new in 2.0), you don't call `get()` at all — the API is passed as a parameter to your `MenuExtension` lifecycle methods. See [addon delivery](/docs/en/developers/addons/). ## Type registration diff --git a/src/content/docs/en/general/activators.md b/src/content/docs/en/general/activators.md index 64da23e..f35e6c0 100644 --- a/src/content/docs/en/general/activators.md +++ b/src/content/docs/en/general/activators.md @@ -47,9 +47,27 @@ Many activators, such as `command`, save their state so you can read it from the | [table](#activator-table-sign) | Strings list | Open menu when sign with some text clicked | | [swapItems](#activator-swapitems) | None | Open menu when player swaps item. By default it's 'F' key | +## Activator context and placeholders + +Under each activator below you'll see an **Extractor type** line. That's the name of the `%activator_%` placeholder set you can use in the opened menu to read data about whatever triggered the activator: the item in hand, the region, the NPC, the block, the command arguments. + +Usage: drop `%activator_%` into the menu `title`, item `name`/`lore`, rule conditions, action arguments. Find the available keys for each extractor in [Value extractors](/docs/en/general/placeholders/#value-extractors). The **Extractor type** label is a link to that extractor's table. + +Example. `clickItem` uses [`extractor-item`](/docs/en/general/placeholders/#item-extractor). A player right-clicks a `DIAMOND` named "Excalibur": + +```hocon +title: "Clicked on %activator_item_type% (%activator_item_display_name%)" +``` + +Menu opens with the title `Clicked on DIAMOND (Excalibur)`. + +`%activator_name%` is always available too - it returns the activator's own name (`clickItem`, `command`, `regionJoin`...). + +Some activators show `*None*` here, which means they don't carry their own context, so `%activator_%` placeholders don't apply (only `%activator_name%` does). + ## Activator `command` -**Extractor type**: `extractor-cmd` +**Extractor type**: [`extractor-cmd`](/docs/en/general/placeholders/#command-extractor) Open menu if player entered command. This activator has several formats which described below. @@ -113,7 +131,7 @@ In this example, if player's message contains `hey`, `menu` or `or` symbols toge ## WorldGuard regions -**Extractor type**: `extractor-region` +**Extractor type**: [`extractor-region`](/docs/en/general/placeholders/#region-extractor) These activators require the [WorldGuard](https://dev.bukkit.org/projects/worldguard) plugin and `useWorldGuard: true` in `config.conf`. @@ -145,7 +163,7 @@ In this example, the menu will be opened if you leave from `spawn` or `otherRegi ## Activator `clickItem` -**Extractor type**: `extractor-item` +**Extractor type**: [`extractor-item`](/docs/en/general/placeholders/#item-extractor) You can add activator to open menu when some item clicked by right click in player's hand. @@ -162,7 +180,7 @@ Make sure that you specified all item properties. If some property missing, plug ## Activator `clickNPC` -**Extractor type**: `extractor-npc` +**Extractor type**: [`extractor-npc`](/docs/en/general/placeholders/#npc-extractor) Here listed NPC id which will open the menu when click NPC. Example: @@ -184,12 +202,12 @@ To find NPC id just type `/npc sel` while you looking at NPC. After this enter ` Open menu by clicking on entity. There are two types of this activator: for simple clicks and clicks with `Shift` key pressed. :::tip -All entity types can be found [here](https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html). +All entity types can be found [here](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html). ::: ### Activator `clickEntity` -**Extractor type**: `extractor-entity` +**Extractor type**: [`extractor-entity`](/docs/en/general/placeholders/#entity-extractor) The `clickEntity` activator is a [list of objects](/docs/en/start/hocon/). Each object is a simple entity data. Example: @@ -216,7 +234,7 @@ clickEntity: [ Each object have parameters: -- **`type`** - Bukkit's type of the entity. +- **`type`** - [Bukkit entity type](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html). - **`name`** - \[Optional\]. Display name of the entity. @@ -224,7 +242,7 @@ In this example we specified `PLAYER` entity and `ZOMBIE` entity with `&eZombie` ### Activator `shiftClickEntity` -**Extractor type**: `extractor-entity` +**Extractor type**: [`extractor-entity`](/docs/en/general/placeholders/#entity-extractor) Same shape as [`clickEntity`](#activator-clickentity) above. The only difference: this one fires only when the player is sneaking (Shift held) at the moment of the click. Example: @@ -254,7 +272,7 @@ Block click activators handles clocking on block (right and left click). THere a ### Activator `clickBlock` -**Extractor type**: `extractor-block` +**Extractor type**: [`extractor-block`](/docs/en/general/placeholders/#block-extractor) In this code block you can specify location of some world's block. If player click on this block, menu will be opened. @@ -292,7 +310,7 @@ clickBlock: [ ### Activator `clickBlockType` -**Extractor type**: `extractor-block` +**Extractor type**: [`extractor-block`](/docs/en/general/placeholders/#block-extractor) In this code block you can specify type of some world's block. If player click on block of specified type, menu will be opened. Example: @@ -311,7 +329,7 @@ clickBlockType: [ ## Activators `button`, `lever`, `plate` -**Extractor type**: `extractor-block` +**Extractor type**: [`extractor-block`](/docs/en/general/placeholders/#block-extractor) A `button`, `lever` and `plate` activators has same format. Below is example for buttons: @@ -378,5 +396,5 @@ activators { ``` :::note -This activator works only for MC 1.9+ +Only works on MC 1.9+ - that's the version that [introduced the off-hand](https://minecraft.wiki/w/Off-hand) and the `F` key for swapping items between the main hand and the off-hand. ::: diff --git a/src/content/docs/en/general/cheatsheet.md b/src/content/docs/en/general/cheatsheet.md index 331e3da..a4e4dfc 100644 --- a/src/content/docs/en/general/cheatsheet.md +++ b/src/content/docs/en/general/cheatsheet.md @@ -152,8 +152,8 @@ Full reference with examples: [item format](/docs/en/general/item-format/). | `/am version` | Print version | [docs](/docs/en/general/commands/) | | `/am addons list` | List loaded addons | [docs](/docs/en/general/commands/#am-addons) | | `/am addons info ` | Addon metadata | [docs](/docs/en/general/commands/#am-addons) | -| `/am addons load ` | Load a Path 2 addon | [docs](/docs/en/general/commands/#am-addons) | -| `/am addons reload ` | Hot-reload a Path 2 addon | [docs](/docs/en/general/commands/#am-addons) | +| `/am addons load ` | Load an addon | [docs](/docs/en/general/commands/#am-addons) | +| `/am addons reload ` | Hot-reload an addon | [docs](/docs/en/general/commands/#am-addons) | | `/am addons rescan` | Pick up new jars in `addons/` | [docs](/docs/en/general/commands/#am-addons) | | `/var` subcommands | Manage global variables | [docs](/docs/en/general/commands/#var) | | `/varp` subcommands | Manage per-player variables | [docs](/docs/en/general/commands/#varp) | diff --git a/src/content/docs/en/general/commands.md b/src/content/docs/en/general/commands.md index 3ac49ff..decf628 100644 --- a/src/content/docs/en/general/commands.md +++ b/src/content/docs/en/general/commands.md @@ -3,7 +3,7 @@ title: Commands description: Every command AbstractMenus registers, what it does, and the permissions involved. --- -
Operator Menu author
+
Server admin Menu author
All AbstractMenus commands are gated behind `am.admin`. Op players have it implicitly; for staff without op, grant it through your permissions plugin. @@ -21,17 +21,17 @@ The plugin's main command. Subcommands: ### /am addons -Manage addons — both Path 1 plugin-as-addons and Path 2 AM-loaded jars. +Manage addons — both regular addons (jars in `plugins/AbstractMenus/addons/`) and plugin-as-addons. | Subcommand | What it does | |---|---| -| `/am addons list` | List every loaded addon. Path 1 entries are tagged `[as-plugin]`, the built-in core is `[built-in]`, Path 2 has no tag. | -| `/am addons info ` | Full metadata: status, version, authors, description, dependencies. For Path 1 it surfaces the Bukkit `plugin.yml` description; for Path 2 it surfaces `addon.conf`. | -| `/am addons load ` | Load a Path 2 addon that's in `plugins/AbstractMenus/addons/` but not yet loaded. Tab-completes available unloaded addons. | -| `/am addons reload ` | Disable, rebuild the classloader, and re-enable a Path 2 addon. Path 1 addons need a server `/reload` instead. | +| `/am addons list` | List every loaded addon. Regular addons have no tag, plugin-as-addons are tagged `[as-plugin]`, the built-in core is `[built-in]`. | +| `/am addons info ` | Full metadata: status, version, authors, description, dependencies. For a regular addon it surfaces the `addon.conf`; for a plugin-as-addon it surfaces the Bukkit `plugin.yml` description. | +| `/am addons load ` | Load an addon that's in `plugins/AbstractMenus/addons/` but not yet loaded. Tab-completes available unloaded addons. | +| `/am addons reload ` | Disable, rebuild the classloader, and re-enable an addon. Plugin-as-addons can't be hot-reloaded — they need a server `/reload`. | | `/am addons rescan` | Scan `addons/` for new jars and load any that aren't loaded yet. | -Tab completion is wired up for `info`, `reload`, and `load`. `info` completes against everything (Path 1 + Path 2 + core); `reload` and `load` complete against the relevant subset. +Tab completion is wired up for `info`, `reload`, and `load`. `info` completes against everything (including the built-in core); `reload` and `load` complete against the relevant subset. ## /var diff --git a/src/content/docs/en/index.mdx b/src/content/docs/en/index.mdx index 9ba792b..b98196d 100644 --- a/src/content/docs/en/index.mdx +++ b/src/content/docs/en/index.mdx @@ -33,7 +33,7 @@ import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components"; diff --git a/src/content/docs/en/start/config.mdx b/src/content/docs/en/start/config.mdx index 03f59bf..79322e7 100644 --- a/src/content/docs/en/start/config.mdx +++ b/src/content/docs/en/start/config.mdx @@ -5,7 +5,7 @@ description: Every key in plugins/AbstractMenus/config.conf - what it does, defa import { Aside } from "@astrojs/starlight/components"; -
Operator
+
Server admin
`plugins/AbstractMenus/config.conf` is the global plugin configuration. It's HOCON (a superset of JSON — see [HOCON format](/docs/en/start/hocon/) for the syntax primer). Every key is optional; AbstractMenus generates the file on first run with the defaults shown below. diff --git a/src/content/docs/en/start/faq.mdx b/src/content/docs/en/start/faq.mdx index eb69619..54f95d1 100644 --- a/src/content/docs/en/start/faq.mdx +++ b/src/content/docs/en/start/faq.mdx @@ -5,7 +5,7 @@ description: Frequently asked questions about AbstractMenus, with shortcuts to t import { Aside, LinkCard, CardGrid } from "@astrojs/starlight/components"; -
Operator Menu author Addon developer
+
Server admin Menu author Addon developer
Common questions, with a pointer to the page that has the full answer. Use the search box (top right) for anything not listed here. diff --git a/src/content/docs/en/start/installation.mdx b/src/content/docs/en/start/installation.mdx index 98f5a64..05a8975 100644 --- a/src/content/docs/en/start/installation.mdx +++ b/src/content/docs/en/start/installation.mdx @@ -5,7 +5,7 @@ description: Drop the jar in, configure config.conf if you need to, and you're d import { Steps, Aside } from "@astrojs/starlight/components"; -
Operator
+
Server admin
## Requirements @@ -30,7 +30,7 @@ AbstractMenus does not bundle Adventure — it relies on the copy Paper ships. P ├── animated_heads.conf head animation definitions ├── menus/ your menu files (one or more .conf) │ └── menu.conf a starter menu - ├── addons/ Path 2 addon jars (optional) + ├── addons/ addon jars (optional) └── variables.db SQLite store for /var and /varp ``` @@ -52,7 +52,7 @@ After editing, run `/am reload`. | `animated_heads.conf` | Reusable head animation definitions referenced through the `%hanim_::%` placeholder (see [placeholders](/docs/en/general/placeholders/)). | | `menus/menu.conf` | One sample menu opened by `/am open menu`. Delete it once you have your own menus. | | `variables.db` | SQLite. Don't edit manually. | -| `addons/` | Drop Path 2 [addon jars](/docs/en/developers/addons/) here. | +| `addons/` | Drop [addon jars](/docs/en/developers/addons/) here. | ## Updating diff --git a/src/content/docs/en/start/tips.md b/src/content/docs/en/start/tips.md index 8d59731..d89c561 100644 --- a/src/content/docs/en/start/tips.md +++ b/src/content/docs/en/start/tips.md @@ -15,7 +15,7 @@ Small things that save time. Updated as more emerge. Author-time only. Don't leave the file watcher running on a production server — it's an extra IO listener for no benefit once menus stop changing. ::: -## Hot-load Path 2 addons +## Hot-load addons Drop a new `addon.conf`-bearing jar into `plugins/AbstractMenus/addons/`, then run: @@ -25,7 +25,7 @@ Drop a new `addon.conf`-bearing jar into `plugins/AbstractMenus/addons/`, then r The plugin picks up the new jar without a server restart. Use `/am addons reload ` to refresh an already-loaded addon after replacing its jar. -Path 1 plugin-as-addons need a regular `/reload` or server restart — Bukkit owns their lifecycle. +Plugin-as-addons need a regular `/reload` or server restart — Bukkit owns their lifecycle. ## MiniMessage when you need it diff --git a/src/content/docs/ru/advanced/animations.md b/src/content/docs/ru/advanced/animations.md new file mode 100644 index 0000000..8d96d66 --- /dev/null +++ b/src/content/docs/ru/advanced/animations.md @@ -0,0 +1,210 @@ +--- +title: Анимации +description: "Любое меню с покадровой анимацией должно содержать блок `frames` в корне. Если блок `frames` есть, плагин считает меню анимированным..." +--- + +
Автор меню
+ +## Покадровая анимация + +Анимированное меню содержит блок `frames` в корне. Как только плагин видит этот блок, меню считается анимированным, а сам `frames` заменяет привычный `items`. Ниже параметры, которые добавляются к стандартным. + +| Имя | Тип данных | Обязательный | Описание | +|----|----|----|----| +| onAnimStart | Объект | Нет | Действия перед началом анимации | +| onAnimEnd | Объект | Нет | Действия после завершения анимации (не работают для зацикленных анимаций) | +| loop | Boolean | Нет | Зациклить анимацию. По умолчанию `false` | +| frames | Список объектов | Да | Список кадров анимации | +| items | Список объектов | Нет | Список статических предметов | + +Ниже пример пустого меню, готового к добавлению кадров. + +```hocon +title: "&eАнимированное меню" +size: 3 +activators { + command: "anim" +} +loop: true +frames: [ + { + # ... Здесь будет кадр, который добавим позже + }, + { + # ... Второй кадр + } +] +``` + +### Формат кадра + +Кадр - это единица анимации. В каждом кадре лежит список кнопок, которые попадут в инвентарь, плюс ещё несколько параметров. Их таблица ниже. + +| Имя | Тип данных | Обязательный | Описание | +|----|----|----|----| +| delay | Number | Нет | Задержка в тиках перед проигрыванием кадра. По умолчанию `20` | +| clear | Boolean | Нет | Очистить инвентарь перед добавлением предметов нового кадра. По умолчанию `true` | +| rules | Объект | Нет | Правила проигрывания кадра | +| onStart | Объект | Нет | Действия **перед** проигрыванием кадра | +| onEnd | Объект | Нет | Действия **после** проигрывания кадра | +| items | Список объектов | Да | Список предметов для этого кадра | + +Если `clear: false`, при смене кадра предметы из инвентаря не удаляются. Так чуть меньше нагрузки на сервер и компактнее сам файл меню. + +Предметы внутри `items` описываются так же, как и в обычных меню. + +Минимальная задержка между кадрами - 1 тик. + +Если у кадра есть правила, плагин сначала проверит игрока. Не подходит - кадр просто пропускается, в инвентаре остаются предметы предыдущего. + +### Создание простой анимации + +Соберём простую анимацию: предмет должен ползти из левого угла в правый. В примере используются шаблоны, но можно и без них - просто продублировать параметры в каждом кадре (так лучше не делать). Файл выглядит так: + +```hocon +title: "&lAnimation example" +size: 1 +activators { + command: "menu" +} +frames: [ + { + delay: 10 + items: [ + ${someItem} {slot: 0} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 1} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 2} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 3} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 4} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 5} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 6} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 7} + ] + }, + { + delay: 10 + items: [ + ${someItem} {slot: 8} + ] + } +] + +someItem { + material: STONE + name: "&aHello!" +} +``` + +В игре это выглядит так: + +![Результат простой анимации](/docs/img/animations_first_example.gif) + +Предмет `someItem` мы размножили по 9 кадрам, каждый раз меняя слот. Поскольку `clear` по умолчанию `true`, инвентарь чистится перед каждым кадром. + +Это самый простой пример. Если поиграться, можно собрать и куда более навороченные анимации. + +### Статические предметы + +В анимированное меню можно добавить статические предметы - они не меняются, пока крутится анимация. Удобно для фона или служебных кнопок. + +Статические предметы кладутся в блок `items`, как в обычных меню. Пример: + +```hocon +frames: [ + # Здесь какие-то кадры +] + +items: [ + { + slot: 0 + name: "Кнопка выхода" + click { + closeMenu: true + } + } +] +``` + +Здесь статический предмет - кнопка выхода. Пока анимация идёт, она не двигается. + +:::note +Статические предметы кладутся в инвентарь в двух случаях: при открытии меню и после того, как какой-нибудь кадр очистил инвентарь. +::: + +## Анимация головы + +Плагин умеет крутить анимации головы из заранее заготовленных текстур. Делается в два шага: + +1. Подготовить текстуры и сложить их в кадры анимации. +2. Подцепить анимацию к меню через специальный плейсхолдер. + +:::caution +Анимация головы работает только если у меню задан `updateInterval`. Кадр головы меняется вместе с обновлением меню. +::: + +### Добавление анимации головы + +Каждый кадр - это отдельная голова. Сначала нужно сгенерировать все головы для будущей анимации. Удобнее всего через [MineSkin](https://mineskin.org). После генерации копируем ссылку на текстуру - нужное поле показано на скриншоте. + +![Поле текстуры для копирования](/docs/img/anim_tex_upload.png) + +Нам нужен только хеш скина, без адреса `http://textures.minecraft.net/texture/`. + +Когда все головы готовы и текстуры сохранены, их можно прописать в `animated_heads.conf` в папке плагина. Файл из коробки уже содержит одну анимацию - `anim_eye`, её можно сразу попробовать. + +Своя анимация - это [список строк](/docs/ru/start/hocon/) где-нибудь в конфиге, по образу `anim_eye`. Каждый элемент списка - очередной кадр. Анимация головы всегда зациклена, держи это в голове при сборке кадров. Сохраняем файл - анимация готова, осталось привязать её к предмету. + +### Привязка анимации к предмету + +У любого предмета есть параметр `texture`. Со специальным плейсхолдером его можно использовать иначе. Пример: + +```hocon +items: [ + { + slot: 0 + texture: "%hanim_:anim_eye:1%" + name: "&aAnimated head" + } +] +``` + +Вместо статической текстуры в `texture` мы положили плейсхолдер. На каждом тике обновления меню (параметр `updateInterval`) он подменяется текстурой соответствующего кадра. Пример ниже. + +![Пример анимации головы](/docs/img/animations_eye.gif) + +Цифра `1` после имени анимации - идентификатор конкретной кнопки. В пределах одного меню у каждого предмета с одной и той же анимацией идентификатор должен быть свой. Подойдёт любой текст, мы взяли просто число. Если у нескольких кнопок с одной анимацией идентификаторы совпадают, кадры будут отображаться криво. diff --git a/src/content/docs/ru/advanced/drag-and-drop.md b/src/content/docs/ru/advanced/drag-and-drop.md new file mode 100644 index 0000000..3a6b034 --- /dev/null +++ b/src/content/docs/ru/advanced/drag-and-drop.md @@ -0,0 +1,210 @@ +--- +title: Drag and drop +description: "Drag-and-drop (далее DnD) - это возможность класть и забирать предметы из инвентаря. При этом меню может менять внешний вид или поведение в ответ на эти события..." +--- + +
Автор меню
+ +Drag-and-drop (дальше - DnD) позволяет класть и забирать предметы из инвентаря меню. На эти события меню может реагировать - менять внешний вид или поведение. + +:::note +Возможность пока экспериментальная и с ограничениями. Например, часть DnD-действий не работает - в том числе размещение предмета через shift-клик. +::: + +:::tip +Drag-and-drop используется в некоторых [примерах меню](/docs/ru/general/examples/). +::: + +## Включение DnD + +Чтобы игроки могли класть и забирать предметы, добавь в корень меню параметр `draggable`: + +```hocon +title: "Меню" +size: 3 +draggable: 11 +items: [ + // ... +] +``` + +Параметр принимает тот же [формат слотов](/docs/ru/general/item-format/#slot), что и обычные предметы: индекс, диапазон или матрицу. Здесь мы взяли один слот. Теперь предмет можно положить в слот 11 или забрать из него - событие не отменится. + +:::note +Обычный предмет меню не может быть draggable. Если нужно вручную положить draggable-предмет, используй действие `placeItem`. +::: + +## Слушатели DnD-событий + +В корень меню можно добавить три блока действий для DnD: + +- **`onPlaceItem`** - срабатывает, когда предмет **положили** в draggable-слот. + +- **`onTakeItem`** - срабатывает, когда предмет **забрали** из draggable-слота. + +- **`onDragItem`** - срабатывает на **любое** из событий выше. + +Например, нужно ловить момент, когда игрок что-то положил в меню. Берём `onPlaceItem`: + +```hocon +title: "Меню" +size: 3 +draggable: 11 +onPlaceItem { + message: "Ты положил предмет в слот 11" +} +items: [ + // ... +] +``` + +Как только игрок положит предмет в draggable-слот, ему прилетит сообщение. + +События срабатывают на любое изменение предмета в draggable-слоте, даже если игрок просто докинул ещё единиц. + +`onDragItem` пригодится, когда нужно проверять draggable-слот при каждом движении в меню. + +:::tip +Если игрок закрыл меню, а внутри что-то лежит - предметы выпадут на землю. +::: + +## DnD-плейсхолдеры + +Для проверки свойств перетаскиваемого предмета и связанных данных есть специальные плейсхолдеры. Под капотом они тянут информацию через [экстрактор предмета](/docs/ru/general/placeholders/#item-extractor). + +Плейсхолдеры разбиты по типам событий - описаны ниже. С ними удобно поиграться вручную, чтобы понять, что они возвращают. + +### Для положенного предмета + +Префикс `placed_`, данные о последнем **положенном** предмете. Например: + +- `%placed_item_type%` - тип положенного предмета. +- `%placed_item_amount%` - его количество. + +И так далее - полный список см. в [экстракторе предмета](/docs/ru/general/placeholders/#item-extractor). + +Отдельно есть плейсхолдер `placed_slot` - индекс слота, в который положили предмет. + +:::note +Если игрок докладывает в тот же слот, плейсхолдеры покажут именно **положенный** предмет, а не итоговый стек. Чтобы получить итог, используй `changed_` или правило `placedItem`. +::: + +### Для забранного предмета + +Префикс `taken_`, данные о последнем **забранном** предмете. Например: + +- `%taken_item_type%` - тип забранного предмета. +- `%taken_item_amount%` - его количество. + +И отдельный `taken_slot` - индекс слота, из которого забрали предмет. + +### Для изменённого предмета + +Префикс `changed_`, данные об итоговом предмете в слоте после действия. Например, в слоте уже лежало 32 камня, игрок докинул ещё 32 - в итоге `64`. Это значение и вернёт `changed_`. А `placed_` в той же ситуации вернёт `32` - то, что только что положили. + +Пример: + +- `%changed_item_type%` - тип итогового предмета. +- `%changed_item_amount%` - его количество. + +## Правило `placedItem` + +Специальное правило для DnD-меню. Проверяет итоговый предмет в draggable-слоте после действия. Пример: + +```hocon +title: "Меню" +size: 3 +draggable: 11 +onDragItem { + rules { + placedItem { + slot: 11 + material: COBBLESTONE + count: 32 + } + } + actions { + message: "Успех" + } +} +items: [ + // ... +] +``` + +Здесь при каждом изменении слота идёт проверка. Если в слоте лежит хотя бы 32 единицы `COBBLESTONE`, игроку прилетает `Успех`. + +## Специальные действия + +### Действие `placeItem` + +Похоже на `setButton`, но кладёт предмет, который игрок может забрать или изменить через DnD. Пример: + +```hocon +title: "Меню" +size: 1 +activators { + command: "menu" +} +draggable: [ // Слоты 2 и 6 + "--x---x--", +] +onDragItem { + rules { + placedItem { + slot: 2 + material: COBBLESTONE + count: 32 + } + } + actions { + placeItem { + slot: 6 + material: COAL_ORE + count: "%changed_item_amount%" + } + } + denyActions { + removePlaced: 6 + } +} +items: [ + { + slot: [ + "xx-xxx-xx" + ] + material: BLACK_STAINED_GLASS_PANE + name: " " + } +] +``` + +Если игрок положил в слот 2 хотя бы 32 булыжника, в слоте 6 появляется угольная руда. Иначе - убрать ранее положенный предмет из слота 6, если он там был. + +:::tip +В не-draggable-слот этим действием положить предмет нельзя. +::: +:::note +События `onPlaceItem` и компания при таком размещении не срабатывают. +::: + +### Действие `removePlaced` + +Убирает положенный игроком предмет из слота. Например, чтобы вычистить слот `5`: + +```hocon +removePlaced: 5 +``` + +Это полностью убирает предмет из инвентаря. + +Если нужно срезать только часть стека: + +```hocon +removePlaced { + slot: 5 + count: 16 +} +``` + +Если в слоте лежит больше 16, плагин уменьшит стек на 16. Иначе предмет уберётся целиком. diff --git a/src/content/docs/ru/advanced/generation.mdx b/src/content/docs/ru/advanced/generation.mdx new file mode 100644 index 0000000..17d8023 --- /dev/null +++ b/src/content/docs/ru/advanced/generation.mdx @@ -0,0 +1,284 @@ +--- +title: Генератор меню +description: Соберите меню из динамического каталога объектов (игроки, онлайн, предметы) с пагинацией и шаблонами. +--- + +import { Steps } from "@astrojs/starlight/components"; + +
Автор меню
+ +## Основы + +Генерируемое меню состоит из четырёх частей: + + + +1. **Каталог** - источник объектов (игроки в онлайне, кастомные предметы, регионы и т.д.). Можно навесить фильтры, чтобы сузить список. + +2. **Матрица** - сетка ячеек, по которой раскладываются предметы. Строк в ней столько же, сколько в самом меню. + +3. **Шаблоны предметов** - то, как выглядят элементы каталога. В шаблонах работают плейсхолдеры каталога вроде `%ctg_player_name%`. + +4. **Пагинация** - сгенерированное меню может растянуться на несколько страниц, между ними переключаются навигационными действиями. + + + +Полный список каталогов - в конце страницы. Сейчас соберём простое сгенерированное меню. Для примера возьмём каталог `PLAYERS` - он отдаёт всех игроков с сервера. + +```hocon +title: "Generated Menu" +size: 4 + +catalog { + type: PLAYERS +} + +matrix { + cells: [ + "_________", + "_xxxxxxx_", + "_xxxxxxx_", + "_________" + ] + templates { + "x" { + skullOwner: "%ctg_player_name%" + name: "&7Player &e%ctg_player_name%" + } + } +} +``` + +Это меню на 4 строки. Как только в корне появился `catalog`, плагин считает меню автогенерируемым. + +- **`catalog`** - настройки каталога. Внутри обязательно `type` - имя каталога. Остальные параметры зависят от типа. + +- **`cells`** - матрица ячеек в виде списка строк. Каждая строка - строка меню, каждый символ - слот инвентаря. `_` (или любой символ, которого нет в `templates`) - пустой слот, ничего не положим. `x` - тег, который мы объявили в `templates`: в такой слот попадает только предмет шаблона `x`. + +- **`templates`** - привязка предметов к тегам. У каждого шаблона свой тег - любая латинская буква от `a` до `z` или спецсимвол. К тегу можно привязать любой предмет. Слот указывать не нужно, плагин сам вычислит его при генерации. + +Если игроков на сервере хватает, получится примерно такое меню: + +![Пример генерируемого меню](/docs/img/gen_players.png) + +## Переключение страниц + +Объектов в каталоге может быть много, поэтому меню разбивается на страницы. Для переключения есть два действия: `pageNext` и `pagePrev` - на следующую и предыдущую страницу. + +Кнопки страниц задаются как обычные статические предметы. Пример: + +```hocon +title: "Сгенерированное меню" +size: 4 + +catalog { + type: PLAYERS +} + +matrix { + cells: [ + "_________", + "_xxxxxxx_", + "_xxxxxxx_", + "_________" + ] + + templates { + "x" { + skullOwner: "%ctg_player_name%" + name: "&7Игрок &e%ctg_player_name%" + } + } +} + +items: [ + { + slot: 27 + material: ARROW + name: "&e&l<<" + click { + pagePrev: 1 + } + }, + { + slot: 35 + material: ARROW + name: "&e&l>>" + click { + pageNext: 1 + } + } +] +``` + +Это статические предметы с действиями переключения страниц. Они показываются на всех страницах, пока не повесишь на них правила. Так можно добавить любые статические кнопки - в них тоже работают плейсхолдеры каталога. + +## Плейсхолдеры + +Контекстные плейсхолдеры - сердце автогенерируемых меню. Каждый каталог опирается на один из [Экстракторов значений](/docs/ru/developers/own-types/#value-extractor). Логика та же, что и в [контексте активатора](/docs/ru/advanced/input/#activator-context), отличается только префикс: `ctg_` вместо `activator_`. + +При генерации для каждого объекта каталога создаётся свой набор плейсхолдеров - и пользоваться ими можно почти везде в меню. + +Выглядят они так: + +```text +%ctg_% +``` + +`ctg_` - сокращение от `catalog`. `` - конкретный плейсхолдер из экстрактора, который тянет каталог, либо один из общих плейсхолдеров каталога из таблицы ниже. + +| Плейсхолдер | Тип | Описание | +|-------------|--------|--------------------------------| +| page | Number | Индекс текущей страницы | +| pages | Number | Общее количество страниц меню | +| page_next | Number | Индекс следующей страницы | +| page_prev | Number | Индекс предыдущей страницы | +| elements | Number | Общее количество объектов каталога | + +Например, плейсхолдер `page` из таблицы пишется как `%ctg_page%`. + +## Каталоги + +Каталог - это динамическая коллекция объектов одного типа. У каждого каталога свой уникальный идентификатор и набор дополнительных параметров - всё это указывается в блоке `catalog`. + +Плейсхолдеры каталог отдаёт через один из [Экстракторов значений](/docs/ru/developers/own-types/#value-extractor). Чтобы узнать набор плейсхолдеров конкретного каталога, смотри его экстрактор и добавляй префикс `ctg_`. Их можно использовать в шаблонах генерации. Ниже - каталоги, которые AbstractMenus даёт из коробки. + +:::tip +Свои каталоги можно добавлять через API. +::: + +:::note +В статических предметах из `items` плейсхолдеры каталога не работают. Эти предметы создаются и кладутся в инвентарь один раз - ещё до генерации динамических. +::: + +### Players + +**Тип**: `PLAYERS` + +**Тип экстрактора**: `extractor-entity` + +Отдаёт всех игроков в онлайне. Каталог опирается на [`extractor-entity`](/docs/ru/advanced/input/#activator-context), а все объекты тут - игроки, поэтому к любому обычному плейсхолдеру можно приклеить префикс `ctg_`. Пример: + +```hocon +catalog { + type: PLAYERS +} + +matrix { + cells: [ + "_________", + "_xxxxxxx_", + "_xxxxxxx_", + "_________", + ] + templates { + "x" { + skullOwner: "%ctg_player_name%" + name: "Уровень игрока: %ctg_player_level%" + } + } +} +``` + +После `ctg_` пишется любой плейсхолдер без `%` - он раскроется в контексте игрока из каталога. + +В примере мы взяли `player_name` - он есть и в PlaceholderAPI, и среди встроенных плейсхолдеров. + +### Worlds + +**Тип**: `WORLDS` + +**Тип экстрактора**: `extractor-world` + +Возвращает все миры сервера. + +### Entities + +**Тип**: `ENTITIES` + +**Тип экстрактора**: `extractor-entity` + +Отдаёт все сущности из мира игрока (или из указанного мира). Список можно сузить фильтрами. + +- **`world`** - опционально. Имя мира. Если задан, сущности берутся из этого мира, а не из мира зрителя. Поддерживает плейсхолдеры. +- **`allowedTypes`** - опционально. Список строк. Если задан, в результат попадают только сущности указанных типов. Полный список типов - [здесь](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html). + +Пример: + +```hocon +catalog { + type: ENTITIES + world: "world_nether" + allowedTypes: [ ZOMBIE, SKELETON ] +} +``` + + + +### Серверы BungeeCord + +**Тип**: `BUNGEE_SERVERS` + +**Тип экстрактора**: *Собственный экстрактор. См. плейсхолдеры ниже* + +Отдаёт все серверы BungeeCord. Работает только когда в конфиге плагина выставлено `bungeecord: true`. У каталога свой экстрактор с такими плейсхолдерами: + +| Плейсхолдер | Тип | Описание | +|---------------|--------|--------------------------------| +| server_name | String | Имя сервера | +| server_online | Number | Количество игроков на сервере | + +### Iterator + +Тип: `ITERATOR` + +**Тип экстрактора**: *Собственный экстрактор. См. плейсхолдеры ниже* + +Отдаёт список чисел от `A` до `B`. Удобно, когда нужно ровно `N` предметов из шаблона. + +Три параметра: + +- **`start`** - *Number*. Стартовое значение (включительно). + +- **`end`** - *Number*. Конечное значение (включительно). + +- **`desc`** - \[опционально\]. *Boolean*. Если `true`, числа пойдут в обратном порядке. То есть `start: 1`, `end: 10` даст `10, 9, 8, ..., 1`. + +В этих параметрах работают обычные плейсхолдеры (не каталоговые). Например, можно подставить значение переменной. + +У каталога свой экстрактор с такими плейсхолдерами: + +| Плейсхолдер | Тип | Описание | +|-------------|--------|----------------| +| index | Number | Текущее число | + +### Slice + +**Тип**: `slice` + +**Тип экстрактора**: *Собственный экстрактор. См. плейсхолдеры ниже* + +Режет строку на части по разделителю. Удобно, когда плейсхолдер возвращает список через запятую, а нужно по одному элементу меню на каждую часть. + +```hocon +catalog { + type: slice + value: "%my_csv_placeholder%" + separator: "," + trim: true +} +``` + +- **`value`** - строка для разбиения. Плейсхолдеры раскрываются для каждого зрителя. +- **`separator`** - разделитель, уходит в `String.split` (Java regex). +- **`trim`** - опционально. Если `true`, у каждой части обрезаются пробелы по краям. По умолчанию `true`. + +Пустые части каталог отбрасывает: `"a,,b"` с разделителем `,` даст два элемента (`a`, `b`), а не три. + +У каталога свой экстрактор с такими плейсхолдерами: + +| Плейсхолдер | Тип | Описание | +|-----------------|--------|-----------------------| +| slice_element | String | Текущий элемент | + +Это весь список встроенных каталогов. Сама система рассчитана на то, чтобы её расширяли своими каталогами под конкретные задачи. Существующие будем дорабатывать, новые - добавлять. diff --git a/src/content/docs/ru/advanced/input.mdx b/src/content/docs/ru/advanced/input.mdx new file mode 100644 index 0000000..c033bf1 --- /dev/null +++ b/src/content/docs/ru/advanced/input.mdx @@ -0,0 +1,519 @@ +--- +title: Пользовательский ввод +description: Захват контекста из активаторов (команды, клики, регионы) и передача его в меню через контекстные плейсхолдеры. +--- + +import { Steps } from "@astrojs/starlight/components"; + +
Автор меню
+ +Как сохранить ввод игрока и переиспользовать его внутри меню. + + + +## Контекст активатора + +Почти каждый активатор хранит своё состояние - отдельное для каждого игрока. Это состояние мы называем `Context`. В контексте лежит любой объект: + +- команда, которую ввёл игрок; +- блок, по которому он кликнул; +- сущность или NPC, по которому он кликнул; +- регион WorldGuard, в который он вошёл; +- и так далее. + +У каждого свежеоткрытого через активатор меню свой контекст. AbstractMenus даёт доступ к нему прямо через плейсхолдеры. + +## Контекстные плейсхолдеры + +Достаём данные контекста через экстракторы значений. Сами экстракторы описаны в разделе [Экстракторы значений](/docs/ru/general/placeholders/#value-extractors), здесь покажем только **как обратиться к конкретному экстрактору через плейсхолдер**. + +Каждый активатор привязан к одному из готовых экстракторов и через него выдаёт плейсхолдеры своего контекста. + +Формат плейсхолдера активатора: + +```text +%activator_% +``` + +Любой плейсхолдер активатора начинается с `activator_`. `` - это плейсхолдер из конкретного экстрактора (см. [Экстракторы значений](/docs/ru/general/placeholders/#value-extractors)). + +### Как использовать контекстный плейсхолдер + + + +1. Открой [справочник активаторов](/docs/ru/general/activators/). + +2. Найди свой активатор. + +3. Под каждым активатором есть ссылка на **экстрактор** - имя экстрактора одновременно ведёт на таблицу его плейсхолдеров. + + ![Тип экстрактора, который использует активатор](/docs/img/input_extractor_place.png) + +4. Перейди по ссылке к таблице плейсхолдеров экстрактора. + +5. Выбери нужный плейсхолдер. + +6. Приклей к нему префикс `activator_`. + +7. Вставь получившийся `%activator_*%` куда угодно в меню. + + + +### Пример 1. Использование контекстных плейсхолдеров + +Допустим, у нас активатор `clickEntity`: + +```hocon +activators { + clickEntity { + type: SHEEP + } +} +``` + +После клика по любой овце контекстные плейсхолдеры доступны в правилах, действиях и свойствах предметов. Напоминаем: формат - `%activator_%`. Пример: + +```hocon +title: "Меню овцы" +size: 1 +activators { + clickEntity { + type: SHEEP + } +} +items: [ + { + slot: 0 + material: CAKE + name: "Имя овцы: %activator_entity_custom_name%" + click { + message: "&aТекущая локация овцы: %activator_entity_loc_x%, %activator_entity_loc_y%, %activator_entity_loc_z%" + } + } +] +``` + +Здесь мы взяли плейсхолдеры экстрактора `entity_custom_name`, `entity_loc_x`, `entity_loc_y` и `entity_loc_z` - все они описаны в [Entity extractor](/docs/ru/general/placeholders/#entity-extractor). + + + +### Пример 2. Меню личного профиля + +Соберём что-нибудь поинтереснее. Хотим меню профиля: один игрок shift-кликнул по другому - открылось меню со статами того, по кому кликнули. Берём активатор `shiftClickEntity`. + +```hocon +title: "&b%activator_player_name%'s' profile" +size: 1 +activators { + shiftClickEntity { + type: PLAYER + } +} +items: [ + { + slot: 4 + skullOwner: "%activator_player_name%" + name: "&e%activator_player_displayname%" + lore: [ + "", + "&fLevel: %activator_player_level%", + "&fXP: %activator_player_exp%", + "&fHealth: %activator_player_health%", + "", + "&aClick, to say hello", + ] + click { + command { + console: "tell %activator_player_name% Hello!" + } + } + } +] +``` + +Плейсхолдер сюда залез даже в заголовок меню - потому что инвентарь собирается уже после того, как активатор сформировал контекст. Дополнительно мы используем плейсхолдеры PlaceholderAPI, но с префиксом `activator_`: меню точно откроется только после клика по игроку (`type: PLAYER`). + +Обычные плейсхолдеры здесь работают потому, что Entity Extractor принимает их, когда сущность - игрок. Об этом и других экстракторах подробно в разделе [Экстракторы значений](/docs/ru/general/placeholders/#value-extractors). + +Теперь, если игрок shift-кликнет по другому, откроется такое меню: + +![Пример меню профиля игрока](/docs/img/input_profile.png) + +*Пример меню профиля игрока* + +Более сложные сценарии с контекстными плейсхолдерами лежат в разделе [Примеры](/docs/ru/general/examples/). + +:::tip +В примере подключены PlaceholderAPI с расширениями `Player` и `Statistic`. +::: + + + +## Построение команд + +В AbstractMenus встроена мощная система команд - с аргументами, которые можно прокидывать в меню. + +Здесь разбирается продвинутая форма активатора `command` с типизированными аргументами. Базовое описание активатора - на странице [Активаторы](/docs/ru/general/activators/). + +### Формат команды + +Чтобы собрать команду с аргументами, объяви активатор `command` как [объект](/docs/ru/start/hocon/#object). Параметры объекта: + +| Ключ | Тип | Описание | Обязательный | +|----|----|----|----| +| name | String | Базовое имя команды | true | +| aliases | Список строк | Алиасы базового имени | false | +| error | String | Кастомное сообщение об ошибке | false | +| help | String | Кастомный префикс сообщения помощи | false | +| args | Список строк или объектов | Аргументы команды | false | +| override | Boolean | Попытаться переопределить команду из другого плагина | false | + +В `error` можно вставить `%s` - туда подставится сообщение об ошибке от конкретного аргумента. + +В `help` тоже работает `%s` - подставится автогенерируемая справка по команде на основе её структуры. + +Пример с `error` и `help`: + +```hocon +command { + name: "mycmd" + error: "&cНеверный ввод: %s" + help: "&eСтруктура команды: %s" +} +``` + +Поле `args` - список, в котором аргументы задаются одной из двух форм: **простой** или **типизированной**. + +Активатор `command` пробрасывает введённые значения в плейсхолдеры через [Command extractor](/docs/ru/general/placeholders/#command-extractor). Пример - ниже. + +### Простые аргументы + +Простые аргументы - это просто список ключей. Каждый аргумент - строка, ввести можно что угодно. Пример: + +```hocon +command { + name: "mycmd" + args: [ "myarg1", "myarg2" ] +} +``` + +После перезагрузки AbstractMenus ждёт от игрока команду в формате: + +```text +/mycmd +``` + +Если игрок указал все аргументы - меню откроется. Если нет - получит ошибку. + +Чтобы достать введённые значения, используй контекстные плейсхолдеры по принципу из [Контекста активатора](#activator-context): смотри, какие плейсхолдеры даёт активатор, и приклеивай префикс `activator_`. Пример: + +```hocon +message: "Ты ввёл %activator_cmd_arg_myarg1% и %activator_cmd_arg_myarg2%" +``` + +### Типизированные аргументы + +Типизированный аргумент требует от игрока ввод определённого типа. Не подходит - получает ошибку. + +Доступные типы: + +- **`string`** - простая строка без пробелов, можно ввести что угодно. + +- **`number`** - любое число, float или integer. Если из ввода не парсится число, аргумент падает. + +- **`integer`** - только целые числа. Падает, если ввод не парсится в integer. + +- **`player`** - имя онлайн-игрока. На tab-комплите подсказывает всех онлайн-игроков, падает, если такого нет. + +- **`choice`** - выбор из заранее заданных строковых опций. Опции подсказываются на tab-комплите, игрок должен выбрать одну из них. + +Каждый типизированный аргумент - объект с параметрами. Общие поля для всех: + +| Ключ | Тип | Описание | Обязательный | +|----|----|----|----| +| key | String | Уникальное имя аргумента, которое будет использоваться в плейсхолдерах | true | +| type | String | Тип аргумента. См. все типы выше | true | +| error | String | Кастомное сообщение об ошибке | false | +| default | String | Значение по умолчанию. Делает аргумент опциональным | false | + +#### Пример 1. Числовой аргумент + +```hocon +command { + name: "mycmd" + args: [ + { + key: "amount" + type: number + } + ] +} +``` + +Чтобы достать значение, используем плейсхолдер: + +```hocon +message: "&eЧисло: %activator_cmd_arg_amount%" +``` + +#### Пример 2. Аргумент-выбор (choice) + +У типа `choice` есть дополнительное поле `options` - список строк, по одному элементу на каждую опцию. + +```hocon +command { + name: "mycmd" + args: [ + { + key: "variant" + type: choice + options: ["opt1", "opt2", "opt3"] + } + ] +} +``` + +Выбранную опцию достаём так: + +```hocon +message: "&eВыбрано %activator_cmd_arg_variant%" +``` + +#### Пример 3. Аргумент-игрок (player) + +Аргумент `player` принимает только имя онлайн-игрока и подсказывает их на tab-комплите. + +```hocon +command { + name: "mycmd" + args: [ + { + key: "username" + type: player + } + ] +} +``` + +Имя игрока достаём так: + +```hocon +message: "&eИгрок: %activator_cmd_arg_username%" +``` + +### Использование обычных плейсхолдеров для аргумента player + +У `player` есть одна приятная фишка. AbstractMenus сохраняет введённого игрока и даёт доступ к его обычным плейсхолдерам. Пример: + +```hocon +command { + name: "mycmd" + args: [ + { + key: "player1" + type: player + }, + { + key: "player2" + type: player + } + ] +} +``` + +Допустим, нам нужен уровень обоих игроков. Command extractor умеет это и другое - формат плейсхолдера у него `:` (см. [Command extractor](/docs/ru/general/placeholders/#command-extractor)): сначала ключ аргумента, потом нужный плейсхолдер. Нам нужен `player_level`, итог: + +```hocon +%activator_player1:player_level% +и +%activator_player2:player_level% +``` + +### Аргументы по умолчанию + +Подробнее про поле `default`. Бывает, аргумент нужно сделать опциональным. Например, делаем меню профиля: если игрок ввёл имя - откроется профиль того игрока, если нет - своё. + +Для такого добавляется значение по умолчанию. Внутри работают обычные плейсхолдеры. Пример: + +```hocon +command { + name: "profile" + args: [ + { + key: "user" + type: player + default: "%player_name%" + } + ] +} +``` + +При `/profile` аргумент `user` будет равен имени того, кто ввёл команду. + +При `/profile Notch` - `Notch`. + +Тут есть ограничение: опциональный аргумент может быть только один, и стоять обязан последним. Дело в парсере: при двух и более опциональных он не сможет понять, какой аргумент именно ты ввёл. + +### Финальный пример. Открытие меню профиля по команде + +Возьмём пример из [Меню личного профиля](#example-2-personal-profile-menu) и переделаем. Меняем `shiftClickEntity` на `command`, команда принимает имя игрока: + +```hocon +title: "%activator_user:player_name%'s profile" +size: 1 +activators { + command { + name: "profile" + args: [ + { + key: "user" + type: player + default: "%player_name%" + } + ] + } +} +items: [ + { + slot: 4 + skullOwner: "%activator_user:player_name%" + name: "&e%activator_user:player_displayname%" + lore: [ + "", + "&fLevel: %activator_user:player_level%", + "&fKills: %activator_user:statistic_mob_kills%", + "&fDeaths: %activator_user:statistic_deaths%", + "", + "&aClick, to say hello", + ] + click { + command { + console: "tell %activator_user:player_name% Hello!" + } + } + } +] +``` + +Плейсхолдеры тоже подогнали под формат Command Extractor. Результат ниже. + +![Как работают команды](/docs/img/input_example.gif) + +### Переопределение команды + +Иногда нужно перебить команду другого плагина. Например, есть сторонний плагин с `/kit`. + +Допустим, мы хотим, чтобы по той же команде открывалось наше меню со списком китов. По умолчанию AbstractMenus регистрирует команду как обычно - и её может перетереть любой другой плагин. + +Чтобы перебить чужую команду, добавь в активатор `command` параметр `override: true`. Пример: + +```hocon +activators { + command { + name: "kit" + aliases: ["kits"] + override: true + } +} +``` + +Команда с `override: true` вешается ещё и как слушатель чата, а не только как обычная команда. Так её можно "перебить" у стороннего плагина, даже если AbstractMenus загрузился позже. + +Когда игрок такую команду ввёл, плагин обрабатывает её как обычно и отменяет дальнейшую обработку сообщения - чтобы у настоящего "владельца команды" она не отработала. + + + +## Ввод в чат + +Действие `inputChat` запрашивает у игрока текст в чате. Структура такая: + +| Параметр | Тип | Описание | Обязательный | +|----|----|----|----| +| into | String | Имя переменной, в которую сохранятся данные | true | +| global | Boolean | Если `true`, данные сохранятся в глобальной переменной. По умолчанию `false` | false | +| cancelOn | String | "Стоп-слово" или фраза, которая отменит ожидание ввода. Если ввод отменён, выполнятся действия из `onCancel` вместо `onInput` | false | +| onInput | Объект | Действия после успешного ввода | false | +| onCancel | Объект | Действия для ввода, отменённого стоп-словом | false | + +Обязательный только `into` - имя [переменной](/docs/ru/general/variables/). Когда игрок что-то пишет в чат, плагин кладёт текст в переменную с этим именем. Прочитать её потом можно откуда угодно через [плейсхолдер переменной](/docs/ru/general/variables/#access-to-variables). + +Базовый пример: + +```hocon +slot: 0 +material: STONE +click { + message: "Enter player name" + inputChat { + into: "input_username" + } +} +``` + +При клике на предмет меню закроется и пришлёт сообщение - дальше игрок должен что-то написать в чат. + +Чтобы выполнить действия после ввода, используй блок `onInput`. Пример: + +```hocon +slot: 0 +material: STONE +click { + message: "Enter player name" + inputChat { + into: "input_username" + onInput { + command: { + console: "say Hello, %varp_:input_username%!" + } + } + } +} +``` + +Результат: + +![Ожидание ввода](/docs/img/input_chat_1.png) + +Когда игрок что-нибудь напишет в чат, все увидят `Hello, !`, где `` - то, что он ввёл. + +![Ввод завершён](/docs/img/input_chat_2.png) + +:::note +`inputChat` сам закрывает меню. Вручную через `closeMenu` его **не закрывай**. +::: + +### Отмена ввода + +Чтобы игрок мог отменить ввод, добавь в действие параметр `cancelOn`. Пример: + +```hocon +slot: 0 +material: STONE +click { + message: "Enter player name" + inputChat { + into: "input_username" + cancelOn: "cancel" + onInput { + command: { + console: "say Hello, %varp_:input_username%" + } + } + onCancel { + message: "Ok, do not write anything" + } + } +} +``` + +Заодно подцепили блок `onCancel` - чтобы отправить игроку сообщение при отмене. Если он введёт `cancel`, плагин перестанет ждать ввод и выполнит действия из `onCancel`, если они есть. + +![Ввод отменён стоп-словом](/docs/img/input_chat_3.png) + +### Ограничения + +Не всё, что обычно работает в меню, доступно внутри `onInput` и `onCancel`. + +Меню в это время закрыто, поэтому в этих блоках работают только действия, которые не лезут в инвентарь меню. **Нельзя** использовать `refreshMenu`, `closeMenu`, `openMenu` и подобные. + +Контекстные плейсхолдеры тоже не работают - так устроен жизненный цикл меню. Доступны только обычные плейсхолдеры и переменные. diff --git a/src/content/docs/ru/advanced/logical.md b/src/content/docs/ru/advanced/logical.md new file mode 100644 index 0000000..0bd7000 --- /dev/null +++ b/src/content/docs/ru/advanced/logical.md @@ -0,0 +1,420 @@ +--- +title: Логические структуры +description: "В целом все логические структуры в AbstractMenus работают как **if -> then -> else**. Эти структуры можно вкладывать друг в друга." +--- + +
Автор меню
+ +Все логические структуры в AbstractMenus устроены как **if -> then -> else** и могут вкладываться друг в друга. + +## Блок действий + +Блок действий - это сложный объект, в котором лежат правила и другие действия. Реальная структура: + +| Параметр | Тип | Назначение | +|----|----|----| +| rules | Блок правил | Обычные правила | +| actions | Блок действий | Выполняются, если игрок прошёл все правила | +| denyActions | Блок действий | Выполняются, если игрок **не прошёл** хотя бы одно правило | + +Блок легко представить как бесконечное дерево, где каждая ветка - правило или блок действий. + +![Структура блока действий](/docs/img/logical_actions.jpg) + +Пример блока действий посложнее: + +```hocon +title: "Пример" +size: 6 +openActions { + message: "Открываем меню..." + rules { + permission: "super.admin" + } + actions { + message: "У тебя есть право super.admin!" + rules { + money: 1000 + } + actions { + takeMoney: 1000 + } + denyActions { + giveMoney: 1000 + } + } +} +``` + +Что произойдёт при открытии меню, по порядку: + +1. В чат прилетает "Открываем меню...". +2. Проверяется право "super.admin". +3. Если право есть - выполняется вложенный блок и отправляется "У тебя есть право super.admin!". +4. Проверяется, есть ли на балансе 1000 валюты. +5. Если есть - списывается. +6. Если нет - выдаётся 1000. + +:::note +Правила внутри блока действий не влияют на родительский блок. В примере выше проверки права и денег не помешают меню открыться. +::: + +## Блок правил + +### Локальные действия + +Внутри блока правил можно прописать локальные действия. Удобно, когда снаружи `actions` и `denyActions` использовать нельзя. Пример: + +```hocon +items: [ + { + slot: 0 + material: STONE + rules { + permission: "am.admin" + actions { + message: "Да!" + } + denyActions { + message: "Нет" + } + } + } +] +``` + +Логика стандартная: `actions` срабатывают, когда игрок прошёл все правила блока `rules`. `denyActions` - когда не прошёл хотя бы одно правило текущего блока. + +В примере выше "Нет" прилетит игроку без права `am.admin`. Блок `actions`, если бы был, отработал бы наоборот - когда игрок правилам соответствует. + +### Блок правил как список + +На самом деле любой `rules` - это [список объектов](/docs/ru/start/hocon/), где каждый объект - блок правил. Раньше мы просто открывали `rules` и писали внутри: + +```hocon +rules { + permission: "super.admin" + group: "vip" +} +``` + +Раз блок правил - это список других блоков, можно прописать несколько похожих правил и в каждый добавить свои локальные действия. + +```hocon +click { + message: "Ты кликнул по камешку" + rules: [ + { + permission: "my.perm" + actions { + message: "У тебя есть право" + } + }, + { + money: 500 + actions { + message: "У тебя достаточно денег" + } + denyActions { + message: "У тебя недостаточно денег. Но возьми." + giveMoney: 500 + } + } + ] + actions { + message: "У тебя достаточно денег и нужное право!" + } +} +``` + +Здесь блок правил используется как список. По шагам, что происходит при клике: + +1. Сообщение "Ты кликнул по камешку". +2. Проверяется право "my.perm". +3. Если право есть - "У тебя есть право". +4. Проверяется баланс на 500 валюты. +5. Если деньги есть - "У тебя достаточно денег". +6. Если нет - "У тебя недостаточно денег", плюс игроку выдаётся 500 монет. +7. Если обе проверки прошли - "У тебя достаточно денег и нужное право!". + +:::note +Когда правила записаны списком, каждый следующий элемент проверяется независимо от предыдущего. В примере выше: даже если у игрока нет `my.perm`, проверка денег всё равно запустится. Но весь блок `rules` целиком считается проваленным, если хотя бы одно правило не прошло. Поэтому "У тебя достаточно денег и нужное право!" появится только когда сработают все правила. +::: + +## Инвертирование правила (оператор "NOT") + +Любое правило можно инвертировать. Поставь перед его именем `-` - это "NOT". Если правило возвращало `true`, теперь вернёт `false`, и наоборот. + +Пример: + +```hocon +rules { + -permission: "group.admin" +} +actions { + message: "Ты не Админ :(" +} +denyActions { + message: "Ты Админ!" +} +``` + +Здесь `actions` сработает, если у игрока **нет** права `group.admin` - результат правила `permission` инвертирован. + +:::note +Привычный "NOT" (`!`) зарезервирован HOCON, поэтому пришлось взять `-`. +::: + +Эту запись можно навешивать на любое правило, в том числе на логические обёртки. О них дальше. + +## Логические обёртки правил + +По умолчанию блок `rules` склеивает правила через "AND" - даже когда задан списком. Логические обёртки нужны для условий посложнее: можно комбинировать `and`, `or` и оператор "NOT". Под капотом обёртка - это просто правило, внутри которого живут другие правила. Поэтому пишутся обёртки внутри `rules`. + +### Обёртка "AND" + +`rules` и так работает по "AND", поэтому отдельная обёртка `and` нужна в основном внутри `or`. Тем не менее, пример с её прямым использованием: + +```hocon +rules { + and { + permission: "group.vip" + gamemode: CREATIVE + } +} +``` + +Правило `and` вернёт `true`, если у игрока есть право `group.vip` **И** режим игры CREATIVE. + +Ещё пример - обёртка как список групп правил. Формат похож на описанный [выше](/docs/ru/advanced/logical/). + +```hocon +rules { + and: [ + { + permission: "group.vip" + gamemode: CREATIVE + }, + { + permission: "group.helper" + } + ] +} +``` + +Здесь `and` вернёт `true`, если у игрока есть `group.vip` **И** режим CREATIVE **И** право `group.helper`. + +### Обёртка "OR" + +Возвращает `true`, если **хотя бы одно** из вложенных правил вернуло `true`. + +```hocon +rules { + or { + permission: "group.vip" + gamemode: CREATIVE + } +} +``` + +Здесь `or` вернёт `true`, если у игрока есть `group.vip` **ИЛИ** режим CREATIVE. + +С `or` в виде списка групп правил поведение такое же. Пример: + +```hocon +rules { + or: [ + { + permission: "group.vip" + gamemode: CREATIVE + }, + { + permission: "group.helper" + } + ] +} +``` + +Здесь `or` вернёт `true`, если у игрока есть `group.vip` **ИЛИ** режим CREATIVE **ИЛИ** право `group.helper`. + +### Обёртка `oneof` + +`oneof` пригодится, когда правила задаются списком и у каждого свои локальные действия. По логике это `and`, но с одной важной разницей. Если взять обычный `and` как список: + +```hocon +rules { + and: [ + { + permission: "perm1" + actions { + message: "У тебя есть perm1" + } + }, + { + permission: "perm2" + actions { + message: "У тебя есть perm2" + } + } + ] +} +``` + +то даже если у игрока есть право из первого блока (`perm1`), его локальные действия выполнятся, но дальше всё равно прогоняются остальные правила, и игрок получит лишние сообщения. И в итоге сам `and` всё равно вернёт `false`, если хоть одно правило не прошло. + +Иногда это не то, что нужно, - тут и спасает `oneof`. То же самое через `oneof`: + +```hocon +rules { + oneof: [ + { + permission: "perm1" + actions { + message: "У тебя есть perm1" + } + }, + { + permission: "perm2" + actions { + message: "У тебя есть perm2" + } + } + ] +} +``` + +то если у игрока есть `perm1`, локальные действия тоже выполнятся, **но** `oneof` сразу остановится и вернёт `true`. Если ни одно из правил не подошло - вернёт `false`. + +С `oneof` можно спокойно навешивать локальные действия и быть уверенным, что выполнятся ровно те, что относятся к **сработавшему** блоку правил. Пример: + +```hocon +rules { + oneof: [ + { + permission: "perm1" + actions { + message: "У тебя есть perm1" + } + }, + { + permission: "perm2" + actions { + message: "У тебя есть perm2" + } + } + ] + denyActions { // Сработает, только если ни одно правило в списке не прошло + message: "Ни одно из требуемых правил не выполнено" + } +} +``` + +### Обёртка `playerScope` + +Прогоняет вложенные правила в контексте другого игрока. Удобно, когда у тебя есть плейсхолдер, который разворачивается в имя другого игрока (например, цель, выбранная через активатор `command`), и проверять правила нужно по нему, а не по зрителю меню. + +```hocon +rules { + playerScope { + name: "%activator_cmd_arg_target%" + rules { + permission: "myserver.vip" + gamemode: SURVIVAL + } + } +} +``` + +- **`name`** - игрок, в чей контекст переключаемся. Плейсхолдеры раскрываются. +- **`rules`** - блок правил, который проверяется против этого игрока. Структура - как у обычного `rules`. + +Если такой игрок не онлайн, обёртка просто вернёт `false`, без исключения. + +### Комбинирование логических обёрток + +Обёртки можно вкладывать друг в друга и собирать любые условия. Пример: + +```hocon +rules { + or: [ + { + and { + permission: "vip" + gamemode: CREATIVE + } + }, + { + and { + permission: "premium" + gamemode: SURVIVAL + } + } + ] +} +``` + +Здесь `or` вернёт `true`, если: + +у игрока есть `vip` AND режим CREATIVE + +**ИЛИ** + +у игрока есть `premium` AND режим SURVIVAL. + +## Использование одинаковых действий и правил в одном блоке + +Как и многие другие форматы, HOCON не разрешает повторять один и тот же ключ в одном блоке. Например: + +```hocon +click { + message: "Привет" // Ок + message: "Привет снова" // Ошибка парсинга +} +``` + +упадёт парсер - в одном блоке не может быть нескольких параметров с одним именем. Иногда это мешает: например, когда нужно несколько одинаковых действий подряд. В AbstractMenus есть обёртка `bulk`, но читается она так себе. Есть способ проще. + +Чтобы засунуть несколько одинаковых действий или правил в один блок, добавь к имени параметра префикс `_`. Например: + +```hocon +click { + message: "Привет" + _message: "Привет снова" + __message: "Привет снова и снова" + ___message: "Привет снова и снова и снова" +} +``` + +Парсер на это не ругается - имена у параметров разные. После парсинга, но до десериализации действий и правил, плагин обрезает префиксные `_`, и имена правил и действий снова становятся корректными. + +:::note +`_` срезаются только в префиксе. То, что в середине имени, плагин не трогает. +::: + +Через тот же приём можно класть несколько правил в один логический блок. Пример: + +```hocon +rules { + or { + gamemode: SURVIVAL + _gamemode: ADVENTURE + __gamemode: SPECTATOR + } +} +``` + +Обёртка `or` вернёт `true`, если хотя бы одно из правил подходит игроку. + +Эта запись с префиксами эквивалентна такой: + +```hocon +rules { + or: [ + { gamemode: SURVIVAL }, + { gamemode: ADVENTURE }, + { gamemode: SPECTATOR } + ] +} +``` + +То есть, используя префикс `_` для правил, мы избегаем шаблонного кода. diff --git a/src/content/docs/ru/advanced/templates.md b/src/content/docs/ru/advanced/templates.md new file mode 100644 index 0000000..7ae52ef --- /dev/null +++ b/src/content/docs/ru/advanced/templates.md @@ -0,0 +1,169 @@ +--- +title: Шаблоны +description: Переиспользуйте фрагменты меню между файлами - HOCON-подстановки, общие шаблоны предметов, include. +--- + +
Автор меню
+ +HOCON достаточно гибкий, чтобы вынести любой кусок меню в шаблон - одно значение, [объект](/docs/ru/start/hocon/#object), [список](/docs/ru/start/hocon/#list) и т.д. Шаблон может жить рядом с меню в том же файле или лежать отдельно и подключаться через `include`. + +## Основы шаблонов + +Допустим, в нескольких меню есть одинаковая кнопка "Закрыть меню". Опишем её один раз - но не внутри стандартного `items`. Пример: + +```hocon +closeButton { + slot: 5 + texture: "5a6787ba32564e7c2f3a0ce64498ecbb23b89845e5a66b5cec7736f729ed37" + name: "&cЗакрыть" + lore: "&7Кликни, чтобы закрыть меню" + click { + closeMenu: true + } +} +``` + +Раз предмет лежит вне `items` - это шаблон. + +Теперь его можно подключить где угодно в меню. Для этого есть специальный плейсхолдер формата: + +```hocon +${} +``` + +Вместо `` подставь имя своего шаблона - у нас это `closeButton`. Подключаем шаблон в список `items`: + +```hocon +items: [ + ${closeButton} +] +``` + +После перезагрузки плагина предмет появится в меню. + +Учти: путь к шаблону всегда отсчитывается от корня файла - независимо от того, откуда ты его подключаешь. Например, шаблон лежит внутри другого блока: + +```hocon +templates { + items { + closeButton { + slot: 5 + texture: "5a6787ba32564e7c2f3a0ce64498ecbb23b89845e5a66b5cec7736f729ed37" + name: "&cЗакрыть" + lore: "&7Кликни, чтобы закрыть меню" + click { + closeMenu: true + } + } + } +} +``` + +Если `templates` лежит в корне файла, плейсхолдер для `closeButton` будет такой: + +```hocon +${templates.items.closeButton} +``` + +:::tip +В пути к шаблону перечисляются все родительские блоки от корня, через точку. +::: +Главное в шаблонах - их можно подключать сколько угодно раз. Но у нашей кнопки слот зашит намертво. Чтобы его поменять, нужно переопределить параметр в месте подключения. + +## Расширение или переопределение шаблона + +### Переопределение объектов + +Чтобы переопределить объект, синтаксис такой: + +```hocon +items: [ + ${closeButton} { + slot: 0 + } +] +``` + +После плейсхолдера шаблона открываем фигурные скобки, как у обычного объекта, и пишем свойства как обычно. + +Так можно дописать новые поля или переопределить существующие: + +```hocon +items: [ + ${closeButton} { + slot: 0 + name: "Питер" + glow: true + } +] +``` + +### Переопределение списков + +Со списком - похожий синтаксис. Например, задаём меню фон: + +```hocon +// Список items меню +items: ${myTmpl} [ + ${closeButton} { + slot: 0 + } +] + +// Шаблон +myTmpl: [ + { + slot: "0-53" + material: STAINED_GLASS_PANE + name: " " + } +] +``` + +## Шаблоны в отдельном файле + +Общий файл удобен, когда шаблоны нужны в нескольких меню сразу. + +Чтобы плагин понял, что это файл шаблонов, а не меню, в первой строке поставь тег `#invisible`. Без него плагин примет файл за меню и упадёт с ошибкой. + +Пример такого файла: + +```hocon +#invisible + +btnClose { + texture: "5a6787ba32564e7c2f3a0ce64498ecbb23b89845e5a66b5cec7736f729ed37" + name: "&cЗакрыть" + click { + closeMenu: true + } +} + +btnBack { + material: ARROW + name: "&cНазад" +} +``` + +Чтобы пользоваться этими шаблонами, файл нужно подключить в меню такой командой: + + include required(file("./plugins/AbstractMenus/menus/")) + +Вместо `` подставь полный путь к файлу шаблонов от папки `menus`. + +В нашем случае файл лежит прямо в `menus`, поэтому меню выглядит так: + +```hocon +include required(file("./plugins/AbstractMenus/menus/templates.conf")) + +title: "Заголовок меню" +size: 6 +items: [ + { + slot: 0 + material: STONE + name: "Какой-то предмет" + }, + ${btnClose} { slot: 1 } +] +``` diff --git a/src/content/docs/ru/changelog/index.mdx b/src/content/docs/ru/changelog/index.mdx new file mode 100644 index 0000000..2e05095 --- /dev/null +++ b/src/content/docs/ru/changelog/index.mdx @@ -0,0 +1,31 @@ +--- +title: Список изменений +description: Все значимые изменения в AbstractMenus и в этом сайте документации. +--- + +import { getCollection } from "astro:content"; + +export const entries = (await getCollection("changelog")) + .sort((a, b) => +b.data.date - +a.data.date); + +Подписаться через [RSS](/docs/feed.xml). + +{entries.map((entry) => ( +
+
+

{entry.data.title}

+

+ + {entry.data.version ? <> · {entry.data.version} : null} +

+
+ {entry.data.summary &&

{entry.data.summary}

} + {entry.data.tags.length > 0 && ( +

+ {entry.data.tags.map((tag) => #{tag})} +

+ )} +
+))} diff --git a/src/content/docs/ru/developers/addons.mdx b/src/content/docs/ru/developers/addons.mdx new file mode 100644 index 0000000..8e86912 --- /dev/null +++ b/src/content/docs/ru/developers/addons.mdx @@ -0,0 +1,233 @@ +--- +title: Поставка аддона +description: Два пути доставки аддона AbstractMenus - jar, загружаемый AM, или плагин-как-аддон, - и общие для них хуки жизненного цикла. +--- + +import { Aside, Tabs, TabItem, LinkCard, CardGrid } from "@astrojs/starlight/components"; + +
Разработчик аддонов
+ + + +Аддон - это всё, что регистрирует типы или провайдеры в AbstractMenus. Встроенные действия, правила и остальное регистрируются через тот же SPI (Service Provider Interface - стандартный Java-механизм, через который плагин даёт сторонним jar'ам регистрировать свои реализации общих интерфейсов), что и внешний аддон. Никакого "привилегированного" пути для встроенного кода нет. + +Поставлять можно двумя способами. Выбирай по ситуации. + +| | Путь 1 - аддон, загружаемый AM | Путь 2 - плагин-как-аддон | +|---|---|---| +| Jar кладётся в | `plugins/AbstractMenus/addons/` | `plugins/` | +| Загружается | AbstractMenus | Bukkit | +| Манифест | `addon.conf` (HOCON) | `plugin.yml` | +| Главный класс наследует | только `MenuExtension` (конструктор без аргументов) | `JavaPlugin` + `MenuExtension` | +| Видно в `/plugins` | Нет | Да | +| Видно в `/am addons list` | Да | Да (с тегом `[as-plugin]`) | +| Перезагрузка в рантайме | `/am addons reload ` | `/reload` (весь сервер) | +| Отдельный classloader на jar | Да (изолированный, child-first) | Нет (общий с Bukkit) | +| Порядок межплагинных зависимостей | `pluginDependencies:` в addon.conf | граф `depend:` Bukkit | + +**Бери Путь 1**, если аддон - чистое расширение AM (мост к провайдеру, кастомное действие, кастомное правило). Аддон можно перезагружать без рестарта сервера, classloader изолирован (твои зависимости не конфликтуют с чужими), а имена типов не пересекаются с Bukkit-плагинами. + +**Бери Путь 2**, если аддон одновременно полноценный Bukkit-плагин: вешает свои Bukkit-листенеры, владеет командами, выставляет API для других плагинов или ему нужен `depend:` в Bukkit для жёсткой очерёдности. + +Для простого моста к провайдеру подойдут оба пути. В качестве примера - [PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon), он специально лежит в двух ветках, чтобы можно было сравнить. + +:::tip[По умолчанию выбирай Путь 1 (`as-addon`)] +Если нет конкретной нужды в Bukkit-листенерах, командах или публичном API для других плагинов - бери `as-addon`. Он чище и удобнее в эксплуатации. На `as-plugin` переходи только когда без Bukkit-функционала действительно не обойтись. +::: + +## Жизненный цикл MenuExtension + +Оба пути реализуют [`MenuExtension`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/MenuExtension.java). У него три хука жизненного цикла и три метода метаданных. + +```java +public interface MenuExtension { + default void onLoad(AbstractMenusApi api) {} + void onEnable(AbstractMenusApi api); + default void onDisable(AbstractMenusApi api) {} + + default String name() { return getClass().getSimpleName(); } + default String version() { return "unknown"; } + default String targetApiVersion() { return null; } +} +``` + +- **`onLoad`** дёргается у каждого расширения *до* того, как у любого расширения вызвался `onEnable`. Сюда складывай настройку, которой не важен порядок. Типы здесь не регистрируй - остальные расширения ещё могут быть не загружены. +- **`onEnable`** дёргается в порядке зависимостей. Тут уже регистрируй типы и провайдеры. +- **`onDisable`** дёргается при остановке сервера или `/am addons reload`. Регистрации типов и провайдеров AbstractMenus снесёт сам. Тебе остаётся подчистить то, чего он не видит: Bukkit-листенеры, задачи планировщика, JDBC-коннекты, файлы, потоки. + +Все три хука выполняются в главном потоке сервера. + + + +## Путь 1 - аддон, загружаемый AM + +Реализуешь `MenuExtension` напрямую. Никакого `JavaPlugin`, никакого `plugin.yml`. Кидаешь jar с `addon.conf` в корне. + +```java title="MyAddon.java" +package com.example.myaddon; + +import ru.abstractmenus.api.AbstractMenusApi; +import ru.abstractmenus.api.MenuExtension; + +public final class MyAddon implements MenuExtension { + + public MyAddon() { + // Обязательный конструктор без аргументов. AbstractMenus создаёт + // класс рефлексией после успешного парсинга addon.conf. + } + + @Override + public void onEnable(AbstractMenusApi api) { + api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); + } + + @Override + public String name() { return "MyAddon"; } + + @Override + public String version() { return "1.0.0"; } +} +``` + +```hocon title="addon.conf" +name = "MyAddon" +version = "1.0.0" +main = "com.example.myaddon.MyAddon" +authors = ["yourname"] +description = "Добавляет в AbstractMenus кастомный тип действия." +targetApiVersion = "2.0.0" + +# Имена других AM-загружаемых аддонов, которые должны включиться раньше этого. +# AbstractMenus делает топосорт графа зависимостей и сообщает о циклах. +addonDependencies = [] + +# Bukkit-плагины, которые должны присутствовать и быть включены. Если хотя бы +# одного нет, AbstractMenus пропускает аддон (с warning). +pluginDependencies = [] + +# Bukkit-плагины, с которыми этот аддон опционально интегрируется. Отсутствие +# soft-зависимости даёт уведомление в лог, но не блокирует аддон. +pluginSoftDependencies = [] +``` + +Готовый jar кидай в `plugins/AbstractMenus/addons/`. AbstractMenus подцепит его на старте либо в рантайме через `/am addons rescan`. + +### Справка по полям addon.conf + +| Поле | Обязательное | Тип | Заметки | +|---|---|---|---| +| `name` | Да | string | Отображаемое имя. Показывается в `/am addons list`. Должно быть уникальным среди всех загруженных аддонов. | +| `version` | Да | string | Произвольная строка версии. | +| `main` | Да | string | Полное имя класса. Должен реализовывать `MenuExtension` и иметь конструктор без аргументов. | +| `authors` | Нет | string\[\] или string | Пустой список, если отсутствует. | +| `description` | Нет | string | Пусто, если отсутствует. | +| `targetApiVersion` | Нет | string | Только диагностика. Пишется в лог при включении, показывается в `/am addons info`. | +| `addonDependencies` | Нет | string\[\] или string | Имена других аддонов, загружаемых AM. Сортируются в порядок включения через DAG. | +| `pluginDependencies` | Нет | string\[\] или string | Имена Bukkit-плагинов. Жёстко обязательны. | +| `pluginSoftDependencies` | Нет | string\[\] или string | Имена Bukkit-плагинов. Опциональны. | + +Любое из этих полей-списков принимает и HOCON-список, и одиночную строку - её плагин сам обернёт в список. + + + +## Путь 2 - плагин-как-аддон + +Делаешь так, чтобы твой `JavaPlugin` ещё и реализовывал `MenuExtension`. Bukkit дёрнет родной `onEnable` плагина, а ты оттуда прокинешь вызов в SPI-хук. + +```java title="MyAddon.java" +package com.example.myaddon; + +import org.bukkit.plugin.java.JavaPlugin; +import ru.abstractmenus.api.AbstractMenusApi; +import ru.abstractmenus.api.MenuExtension; + +public final class MyAddon extends JavaPlugin implements MenuExtension { + + @Override + public void onEnable() { + AbstractMenusApi api = AbstractMenusApi.get(); + if (api == null) { + getLogger().severe("AbstractMenus API not available - disabling."); + getServer().getPluginManager().disablePlugin(this); + return; + } + onEnable(api); + } + + @Override + public void onEnable(AbstractMenusApi api) { + api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); + // ... зарегистрируй остальные свои типы и провайдеры + } + + @Override + public String name() { return "MyAddon"; } + + @Override + public String version() { return getPluginMeta().getVersion(); } +} +``` + +```yaml title="plugin.yml" +name: MyAddon +version: 1.0.0 +main: com.example.myaddon.MyAddon +api-version: '1.21' +depend: + - AbstractMenus +``` + +`depend: [AbstractMenus]` гарантирует, что AbstractMenus включится раньше твоего `onEnable`. Если твоя регистрация на старте дёргает API других плагинов (`PlayerPoints`, `WorldGuard` и т.п.) - дописывай и их. + +## Разрешение зависимостей + +При загрузке аддонов AbstractMenus делает следующее: + +1. Открывает каждый jar из `plugins/AbstractMenus/addons/`. Парсит и валидирует `addon.conf`. На битые ругается в лог и пропускает. +2. Проверяет `pluginDependencies` каждого аддона. Нет обязательной (`required`) зависимости - аддон уходит в FAILED, чтобы админы сервера увидели это в `/am addons list`. По опциональным (`softdepend`) просто пишется уведомление. +3. Топологически сортирует граф `addonDependencies`. Циклы валят весь батч с понятной ошибкой. Аддоны с неудовлетворёнными зависимостями помечаются по отдельности - включая транзитивные сбои: A → B → отсутствующая C, и в FAILED уходят и A, и B. +4. Этап 1: вызывается `onLoad` каждого аддона. +5. Этап 2: вызывается `onEnable` в порядке зависимостей. +6. Парсит меню. Зарегистрированные тобой типы теперь резолвятся из HOCON. + +Если `onEnable` кидает исключение, AbstractMenus откатывает все регистрации, что аддон успел сделать, и ставит его в FAILED. Остальные аддоны продолжают работать как обычно. + +## Чистка по владельцу + +Каждый вызов `register(...)` принимает экземпляр `MenuExtension` последним параметром - это и есть "владелец" регистрации, то есть тот аддон, который её сделал: + +```java +api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); +// ^^^^ +// owner - твой аддон +``` + +Когда твой аддон выключается, AbstractMenus снимает все типы и провайдеры, которые он зарегистрировал. Сам ты `unregisterAll` не зовёшь - его и нет в публичном API, чтобы один аддон не мог стереть регистрации другого. + +Для Пути 1 это работает автоматически: AbstractMenus сам ведёт жизненный цикл такого аддона через свой `AddonManager` и при выключении подчищает все его регистрации. Для Пути 2 "выключение" - это `onDisable` у `JavaPlugin`, и под автоматическую чистку он не попадает: регистрация остаётся в карте владельцев до остановки самого AbstractMenus. Ничего страшного: следующий `onEnable` перетрёт запись по тому же id. + +## Дерево команд /am addons + +Аддонами Пути 1 админы сервера управляют через `/am addons`: + +```text +/am addons list Список всех загруженных аддонов (Путь 1, Путь 2, встроенные) +/am addons info Полные метаданные: статус, версия, зависимости, ошибка +/am addons load Загрузить jar Пути 1 из addons/, который ещё не загружен +/am addons reload Выключить, пересобрать classloader, заново включить аддон Пути 1 +/am addons rescan Просканировать addons/ на новые jar и подгрузить ещё не загруженные +``` + +`reload` и `load` работают только для Пути 1 - им нужен jar в `addons/`. `info` работает для всех трёх типов. + +## Пример реализации + +[PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) - маленький пример аддона, регистрирует PlayerPoints как провайдера экономики. В репозитории три ветки: + +- [`master`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/master) - только README, обзор для сравнения. +- [`as-addon`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-addon) - реализация Пути 1. +- [`as-plugin`](https://github.com/AbstractMenus/PlayerPointsAddon/tree/as-plugin) - реализация Пути 2. + +Обе рабочие ветки регистрируют один и тот же провайдер через один и тот же SPI - разница в основном в обвязке жизненного цикла. diff --git a/src/content/docs/ru/developers/general.mdx b/src/content/docs/ru/developers/general.mdx new file mode 100644 index 0000000..cf85d4f --- /dev/null +++ b/src/content/docs/ru/developers/general.mdx @@ -0,0 +1,200 @@ +--- +title: С чего начать +description: Подключи AbstractMenus в свою сборку, объяви зависимость и получи дескриптор API. +--- + +import { Steps, Tabs, TabItem, Aside, LinkCard, CardGrid } from "@astrojs/starlight/components"; + +
Разработчик аддонов
+ + + +У AbstractMenus 2.0 есть SPI: через него аддоны в рантайме регистрируют свои действия, правила, свойства предметов, активаторы, каталоги и хендлеры провайдеров (экономика, права, уровни, плейсхолдеры, скины). Встроенные типы тянут тот же SPI. + +Аддон можно поставить двумя способами - выбирай: + + + + + + +Дальше идут страницы про сам API. Про доставку (какой путь выбрать, формат манифеста, жизненный цикл) смотри [страницу аддонов](/docs/ru/developers/addons/). + +## Подключи API в свою сборку + +API AbstractMenus публикуется на [GitHub Packages](https://github.com/AbstractMenus/minecraft-plugin/packages) и собирается под каждый релиз на [JitPack](https://jitpack.io/#AbstractMenus/minecraft-plugin). + + + + + + ```kotlin title="build.gradle.kts" + repositories { + maven("https://jitpack.io") + } + + dependencies { + compileOnly("com.github.AbstractMenus:minecraft-plugin:2.0.0-alpha.2") + } + ``` + + + ```groovy title="build.gradle" + repositories { + maven { url 'https://jitpack.io' } + } + + dependencies { + compileOnly 'com.github.AbstractMenus:minecraft-plugin:2.0.0-alpha.2' + } + ``` + + + ```xml title="pom.xml" + + + jitpack + https://jitpack.io + + + + + + com.github.AbstractMenus + minecraft-plugin + 2.0.0-alpha.2 + provided + + + ``` + + + +artifactId - `minecraft-plugin`, по имени репозитория. JitPack кладёт мульти-модульные Gradle-проекты под одну корневую координату; внутри jar - модуль `api`. + + + +## Объяви зависимость в plugin.yml (только Путь 2) + +Плагины-как-аддоны (Путь 2) объявляют AbstractMenus жёсткой зависимостью - тогда Bukkit гарантирует, что твой `onEnable` запустится после того, как AbstractMenus уже поднялся. + +```yaml title="plugin.yml" ins={3-4} +name: MyAddon +version: 1.0.0 +depend: + - AbstractMenus +``` + +Если плагин должен подниматься и без AbstractMenus, ставь `softdepend:` - и тогда обмазывай вызовы API проверкой `Bukkit.getPluginManager().getPlugin("AbstractMenus") != null`. + +У аддонов Пути 1 `plugin.yml` нет вообще - вместо него `addon.conf` с полем `pluginDependencies`. См. [страницу аддонов](/docs/ru/developers/addons/). + +## Получи экземпляр API + +Точка входа - [`AbstractMenusApi`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/AbstractMenusApi.java). Берётся через статический `get()`: + +```java +import ru.abstractmenus.api.AbstractMenusApi; + +AbstractMenusApi api = AbstractMenusApi.get(); +``` + +Если AbstractMenus ещё не включился - вернёт `null`. С `depend: [AbstractMenus]` в манифесте к моменту твоего `onEnable` он точно поднят. + +В аддонах Пути 1 `get()` дёргать вообще не нужно - API прилетает параметром в методы жизненного цикла: + +```java +public final class MyAddon implements MenuExtension { + @Override + public void onEnable(AbstractMenusApi api) { + // api уже инициализирован + } +} +``` + +## Что выставляет API + +```java +api.actions(); // TypeRegistry +api.rules(); // TypeRegistry +api.activators(); // TypeRegistry +api.itemProperties(); // TypeRegistry +api.catalogs(); // TypeRegistry> + +api.providers(); // ProviderRegistry - экономика, права, уровни, плейсхолдеры, скины +api.serializers(); // общие HOCON NodeSerializers +api.variables(); // VariableManager - чтение/запись персональных и глобальных переменных + +api.openMenu(activator, ctx, player, menu); +api.getOpenedMenu(player); +api.loadMenus(); + +api.apiVersion(); // строка версии для диагностики +api.getPlugin(); // сырой хэндл Bukkit Plugin +``` + +Под каждый реестр, менеджер и хелпер есть отдельная страница в этом разделе. + + + + + + + + + + + +## Опционально: GitHub Packages вместо JitPack + +Если по какой-то причине GitHub Packages обязателен (например, политика CI): + + + +1. Сгенерируй [Personal Access Token (classic)](https://github.com/settings/tokens) со скоупом `read:packages`. + +2. Сложи учётку в `~/.gradle/gradle.properties`: + + ```properties title="~/.gradle/gradle.properties" + gpr.user=your-github-username + gpr.token=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ``` + +3. Добавь репозиторий и переключи координату на родную `ru.abstractmenus:api`: + + ```groovy title="build.gradle" + repositories { + maven { + url = uri('https://maven.pkg.github.com/AbstractMenus/minecraft-plugin') + credentials { + username = providers.gradleProperty('gpr.user') + .orElse(providers.environmentVariable('GITHUB_ACTOR')).getOrNull() + password = providers.gradleProperty('gpr.token') + .orElse(providers.environmentVariable('GITHUB_TOKEN')).getOrNull() + } + } + // JitPack всё ещё нужен для транзитивной зависимости api + // com.github.AbstractMenus:hocon: + maven { url 'https://jitpack.io' } + } + + dependencies { + compileOnly 'ru.abstractmenus:api:2.0.0-alpha.2' + } + ``` + + diff --git a/src/content/docs/ru/developers/handlers.md b/src/content/docs/ru/developers/handlers.md new file mode 100644 index 0000000..195af72 --- /dev/null +++ b/src/content/docs/ru/developers/handlers.md @@ -0,0 +1,150 @@ +--- +title: Хендлеры провайдеров +description: Подключи свой бэкенд экономики, прав, уровней, плейсхолдеров или скинов через ProviderRegistry. +--- + +
Разработчик аддонов
+ +:::caution[Alpha API] +AbstractMenus 2.0 находится в alpha. API может измениться до стабильного релиза. Зафиксируй конкретную версию `compileOnly` в сборке, чтобы свежая выборка не сломала проект. +::: + +:::note +Местами мы намеренно не следуем строгим Java-конвенциям, чтобы код был покороче. Здесь главное показать, как работает API, а не как писать продакшн-код. +::: + +В AbstractMenus пять секций провайдеров: **экономика**, **права**, **уровни**, **плейсхолдеры**, **скины**. Каждая секция - это аксессор [`api.providers().
()`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/ProviderRegistry.java), возвращающий [`ProviderSection`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/ProviderSection.java). В секцию можно зарегистрировать несколько хендлеров с id и приоритетом; какой из них становится дефолтным - выбирает оператор в `config.conf`. Если в HOCON-действии указан конкретный провайдер, он перебивает дефолт из конфига. + +Старого статического фасада `Handlers` (как было до 2.0) больше нет - всё идёт через `ProviderSection`. + +## Секции в двух словах + +| Секция | Возвращает | Назначение | +|---|---|---| +| `api.providers().economy()` | `ProviderSection` | `takeMoney`/`giveMoney`/`hasMoney` | +| `api.providers().permissions()` | `ProviderSection` | правила `permission`/`group`, мутация групп и нод | +| `api.providers().levels()` | `ProviderSection` | `giveXp`/`takeXp`/`giveLevel`/`takeLevel`, правила `xp`/`level` | +| `api.providers().placeholders()` | `ProviderSection` | подстановка плейсхолдеров | +| `api.providers().skins()` | `ProviderSection` | `setSkin`/`resetSkin` | + +Встроенные дефолты регистрируются с приоритетом **50**: + +- **economy** - `vault` (Vault), `playerpoints` если установлен [PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon) +- **permissions** - `vault`, `luckperms` +- **levels** - `bukkit` (ванильный XP) +- **placeholders** - `papi` (PlaceholderAPI), `internal` (встроенные) +- **skins** - `skinsrestorer` + +Аддонные провайдеры обычно регистрируются с приоритетом **100** - так они выигрывают авто-резолв, когда есть и дефолт, и аддон. + +## Регистрация хендлера + +Реализуй нужный интерфейс `*Handler` и зарегистрируй из `MenuExtension.onEnable(api)`: + +```java +public final class MyAddon implements MenuExtension { + + @Override + public void onEnable(AbstractMenusApi api) { + api.providers().economy().register( + "playerpoints", // id + new PlayerPointsEconomy(playerPointsApi), // хендлер + 100, // приоритет - больший выигрывает auto-резолв + this); // владелец - AbstractMenus использует для чистки + } +} +``` + +`id` - это то, на что ссылаются HOCON-меню и `config.conf` (`provider: "playerpoints"`). Регистр не важен. Один и тот же экземпляр хендлера используется для всех вызовов. + +## Резолв в рантайме + +```java +EconomyHandler eco = api.providers().economy().resolve(); // дефолт из конфига или хендлер с наивысшим приоритетом +EconomyHandler vault = api.providers().economy().resolve("vault"); // по id, либо null +boolean hasPP = api.providers().economy().has("playerpoints"); +Set ids = api.providers().economy().ids(); +Collection all = api.providers().economy().all(); +``` + +`resolve()` сначала смотрит в `config.conf providers.
`. Если оператор зафиксировал конкретный id - выиграет он. Если стоит `"auto"` - выиграет хендлер с наибольшим приоритетом. Поиск атомарен: параллельный `unregister` не вернёт устаревший хендлер. + +`resolve(String)` дёргают действия меню, когда в HOCON прописан `provider: "..."`. Указание провайдера на уровне действия всегда перебивает дефолт из конфига. + +## Пример: кастомный бэкенд экономики + +Заменяем дефолтную экономику на свою - с балансами в `Map`: + +```java +public final class MapEconomy implements EconomyHandler { + + private final Map balance = new ConcurrentHashMap<>(); + + @Override + public boolean hasBalance(Player player, double amount) { + return balance.getOrDefault(player.getUniqueId(), 0.0) >= amount; + } + + @Override + public void takeBalance(Player player, double amount) { + balance.merge(player.getUniqueId(), -amount, + (current, delta) -> Math.max(0, current + delta)); + } + + @Override + public void giveBalance(Player player, double amount) { + balance.merge(player.getUniqueId(), amount, Double::sum); + } +} +``` + +Регистрируем из аддона: + +```java +@Override +public void onEnable(AbstractMenusApi api) { + api.providers().economy().register( + "memory", + new MapEconomy(), + 100, + this); +} +``` + +Чтобы сделать его серверным дефолтом, оператор пишет: + +```hocon title="config.conf" +providers { + economy = "memory" +} +``` + +Или точечно, на конкретное меню, не трогая глобальный дефолт: + +```hocon +actions { + click: [ + { type: takeMoney, amount: 100, provider: "memory" } + ] +} +``` + +## Интерфейсы хендлеров + +Все пять интерфейсов лежат в `ru.abstractmenus.api.handler.*`: + +| Интерфейс | Методы | +|---|---| +| [`EconomyHandler`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/handler/EconomyHandler.java) | `hasBalance`, `takeBalance`, `giveBalance` | +| [`PermissionsHandler`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/handler/PermissionsHandler.java) | `addPermission`, `removePermission`, `hasPermission`, `addGroup`, `removeGroup`, `hasGroup` | +| [`LevelHandler`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/handler/LevelHandler.java) | `getXp`, `giveXp`, `takeXp`, `getLevel`, `giveLevel`, `takeLevel` | +| [`PlaceholderHandler`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/handler/PlaceholderHandler.java) | `replacePlaceholder`, `replace(Player, String)`, `replace(Player, List)`, `registerAll` | +| [`SkinHandler`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/handler/SkinHandler.java) | `setSkin`, `resetSkin` | + +Имена и сигнатуры методов стабильны в пределах всей линейки 2.0. + +## Чистка + +При `onDisable` твоего аддона AbstractMenus сам снимает все зарегистрированные тобой провайдеры. Самому `unregisterAll` дёргать нельзя - публичный `ProviderRegistry` его сознательно не выставляет, чтобы один аддон не мог сносить провайдеры другого. + +Для Пути 1 чистка идёт сама собой: AbstractMenus ведёт его через `AddonManager` и при выключении сносит все регистрации. У Пути 2 "выключение" - это `JavaPlugin.onDisable`, и под автоматическую чистку он не попадает: регистрация остаётся в карте владельцев до выключения самого AbstractMenus. Повторная регистрация под тем же id из нового `onEnable` перетрёт живую запись - на практике утечки нет. diff --git a/src/content/docs/ru/developers/migration.mdx b/src/content/docs/ru/developers/migration.mdx new file mode 100644 index 0000000..fe6b80f --- /dev/null +++ b/src/content/docs/ru/developers/migration.mdx @@ -0,0 +1,148 @@ +--- +title: Миграция 1.x → 2.0 +description: Соответствие старых вызовов API (Types, Handlers, AbstractMenusProvider) новым. +--- + +import { Aside, LinkCard, CardGrid } from "@astrojs/starlight/components"; + +
Разработчик аддонов
+ + + +Если аддон писался под API 1.x (`AbstractMenusProvider`, `AbstractMenusPlugin`, `Types`, `Handlers`), на этой странице - таблица соответствий "старый вызов - чем заменить в 2.0". + + + +## Точка входа в API + +```diff lang="java" +- AbstractMenusPlugin plugin = AbstractMenusProvider.get(); ++ AbstractMenusApi api = AbstractMenusApi.get(); +``` + +`AbstractMenusApi` заменяет и `AbstractMenusPlugin`, и `AbstractMenusProvider`. Статический `get()` ищет реализацию через Bukkit-овский `ServicesManager` - так же, как старый провайдер. + +В аддонах Пути 1 (он появился в 2.0) `get()` дёргать не нужно: API прилетает параметром в методы жизненного цикла `MenuExtension`. См. [поставка аддона](/docs/ru/developers/addons/). + +## Регистрация типов + +Каждый `Types.register*` переехал в типизированный реестр на `api`: + +```diff lang="java" +- Types.registerAction("myAction", MyAction.class, new MyAction.Serializer()); ++ api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); + +- Types.registerRule("myRule", MyRule.class, new MyRule.Serializer()); ++ api.rules().register("myRule", MyRule.class, new MyRule.Serializer(), this); + +- Types.registerActivator("myActivator", MyActivator.class, new MyActivator.Serializer()); ++ api.activators().register("myActivator", MyActivator.class, new MyActivator.Serializer(), this); + +- Types.registerItemProperty("myProp", MyProp.class, new MyProp.Serializer()); ++ api.itemProperties().register("myProp", MyProp.class, new MyProp.Serializer(), this); + +- Types.registerCatalog("my_catalog", MyCatalog.class, new MyCatalog.Serializer()); ++ api.catalogs().register("my_catalog", MyCatalog.class, new MyCatalog.Serializer(), this); +``` + +Новый четвёртый параметр (`this`) - твой экземпляр `MenuExtension`. По нему AbstractMenus снимает твои регистрации при выключении аддона - руками `unregister` дёргать не надо. + +## Кастомный HOCON-сериализатор + +```diff lang="java" +- Types.serializers().register(MyType.class, new MyTypeSerializer()); ++ api.serializers().register(MyType.class, new MyTypeSerializer()); +``` + +Сигнатура та же, поменялась только точка доступа. `api.serializers()` возвращает ту же коллекцию `NodeSerializers`. + +## Хендлеры (экономика/права/уровни/плейсхолдеры/скины) + +Статический `Handlers` выпилен. Каждая секция теперь - [`ProviderSection`](/docs/ru/developers/handlers/) на `api.providers()`: + +```diff lang="java" +- Handlers.setEconomyHandler(new MyEconomy()); ++ api.providers().economy().register("myEcoId", new MyEconomy(), 100, this); + +- Handlers.getEconomyHandler(); ++ api.providers().economy().resolve(); + +- Handlers.getPermissionsHandler(); ++ api.providers().permissions().resolve(); + +- Handlers.getLevelHandler(); ++ api.providers().levels().resolve(); + +- Handlers.getPlaceholderHandler(); ++ api.providers().placeholders().resolve(); + +- Handlers.getSkinHandler(); ++ api.providers().skins().resolve(); +``` + +В 2.0 в одной секции может жить несколько провайдеров - поэтому регистрация принимает `id` и `priority`. Операторы фиксируют id из `config.conf` (`providers.economy = "vault"` или `"playerpoints"`), а меню могут точечно перебивать выбор через `provider: "..."`. Поведение 1.x ("побеждает единственный провайдер") тоже работает - когда он один. + +## Переменные + +```diff lang="java" +- VariableManager vm = AbstractMenusProvider.get().getVariableManager(); ++ VariableManager vm = api.variables(); +``` + +Сигнатуры остались такими же - `createBuilder()`, `saveGlobal()`, операции на игрока - но методы для игрока переименованы: `savePlayer` → `savePersonal`, `getPlayer` → `getPersonal`, `deletePlayer` → `deletePersonal`. Поправь вызовы. + +## Жизненный цикл меню + +```diff lang="java" +- AbstractMenusProvider.get().reloadMenus(); ++ api.loadMenus(); + +- AbstractMenusProvider.get().openMenu(player, menu); ++ api.openMenu(player, menu); +``` + +Обе формы остаются: двухаргументная (player + menu) и четырёхаргументная (activator + ctx + player + menu). + +## Планировщик на Folia + +1.x вышел ещё до Folia. Если ты гонял задачи через `Bukkit.getScheduler().runTaskLater(plugin, ...)` и аддон должен жить на Folia, переезжай на entity-aware планировщик для всего, что трогает игрока или другую сущность: + +```diff lang="java" +- Bukkit.getScheduler().runTaskLater(plugin, () -> player.kick(reason), 20L); ++ BukkitTasks.runForEntityLater(player, () -> player.kick(reason), 20L); +``` + +`BukkitTasks` живёт в plugin-модуле, не в api jar, - импортируй из `ru.abstractmenus.util.bukkit.BukkitTasks`. Если на plugin-модуль не завязан, ветвись сам по детекту Folia и зови `Bukkit.getRegionScheduler()`/`getEntityScheduler()` напрямую. + +## Координата Maven/Gradle + +```diff +- compileOnly 'com.github.AbstractMenus:api:1.16' ++ compileOnly 'com.github.AbstractMenus:minecraft-plugin:2.0.0-alpha.2' +``` + +artifactId - `minecraft-plugin`: JitPack публикует мульти-модульные Gradle-проекты под одной корневой координатой (имя репозитория). Внутри jar - модуль `api`. + +Если у тебя GitHub Packages, а не JitPack, родная координата - `ru.abstractmenus:api:2.0.0-alpha.2`. Подробная настройка - в [С чего начать](/docs/ru/developers/general/). + +## Что ушло насовсем + +| Убрано | Заменено на | +|---|---| +| `AbstractMenusPlugin` | `AbstractMenusApi` | +| `AbstractMenusProvider` | `AbstractMenusApi.get()` | +| `Types` (статический фасад) | `api.actions()`/`api.rules()`/`api.activators()`/`api.itemProperties()`/`api.catalogs()`/`api.serializers()` | +| `Handlers` (статический фасад) | `api.providers().
()`, возвращающий `ProviderSection` | +| Adventure в составе плагина | Бери встроенный в Paper (compileOnly Paper API) | + +## Куда дальше + + + + + + diff --git a/src/content/docs/ru/developers/own-types.md b/src/content/docs/ru/developers/own-types.md new file mode 100644 index 0000000..e748267 --- /dev/null +++ b/src/content/docs/ru/developers/own-types.md @@ -0,0 +1,384 @@ +--- +title: Кастомные типы +description: Опиши и зарегистрируй свои действия, правила, свойства предметов, активаторы и каталоги. +--- + +
Разработчик аддонов
+ +:::caution[Alpha API] +AbstractMenus 2.0 находится в alpha. API может измениться до стабильного релиза. Зафиксируй конкретную версию `compileOnly` в сборке, чтобы свежая выборка не сломала проект. +::: + +:::note +В примерах местами игнорируем строгие Java-конвенции - чтобы код был покороче. Здесь главное показать, как работает API, а не как писать прод. +::: + +В AbstractMenus пять реестров типов, у всех одинаковая сигнатура `register(key, class, serializer, owner)`: + +```java +api.actions().register("...", MyAction.class, new MyAction.Serializer(), this); +api.rules().register("...", MyRule.class, new MyRule.Serializer(), this); +api.itemProperties().register("...", MyProperty.class, new MyProperty.Serializer(), this); +api.activators().register("...", MyActivator.class, new MyActivator.Serializer(), this); +api.catalogs().register("...", MyCatalog.class, new MyCatalog.Serializer(), this); +``` + +Ключи нечувствительны к регистру. `owner` - твой экземпляр [`MenuExtension`](/docs/ru/developers/addons/); по нему AbstractMenus снимает твои регистрации при выключении аддона. + +Для своих ключей бери вендорный префикс (`myaddon_action`, `playerpoints_take`) - чтобы будущая встроенная сущность с таким же именем не вступила в конфликт. + +Регистрируй всё в `MenuExtension.onEnable(api)`. Из `onLoad` не регистрируй: аддоны, на которые ты опираешься, могут быть ещё не включены. + +## Действие + +Действие - то, что плагин делает: отправляет сообщение, выдаёт предмет, выполняет команду, открывает другое меню. Класс действия реализует [`Action`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/Action.java): + +```java +public class MessageAction implements Action { + + private final String text; + + private MessageAction(String text) { + this.text = text; + } + + @Override + public void activate(Player player, Menu menu, Item clickedItem) { + player.sendMessage(text); + } + + public static class Serializer implements NodeSerializer { + @Override + public MessageAction deserialize(Class type, ConfigNode node) { + return new MessageAction(node.getString()); + } + } +} +``` + +`clickedItem` может быть `null` - зависит от того, что запустило цепочку действий: клик или, например, цепочка deny-действий, сработавшая ещё до выбора предмета. + +Регистрация: + +```java +api.actions().register("myMessage", MessageAction.class, new MessageAction.Serializer(), this); +``` + +В файле меню: + +```hocon +items: [ + { + slot: 1 + material: STONE + name: "Мой предмет" + click { + myMessage: "Привет! Это моё действие!" + } + } +] +``` + +## Правило + +Правило - булева проверка по игроку. Реализует [`Rule`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/Rule.java): + +```java +public class IsBobRule implements Rule { + + @Override + public boolean check(Player player, Menu menu, Item clickedItem) { + return "Bob".equals(player.getName()); + } + + public static class Serializer implements NodeSerializer { + @Override + public IsBobRule deserialize(Class type, ConfigNode node) { + return new IsBobRule(); + } + } +} +``` + +Регистрация и использование: + +```java +api.rules().register("isBob", IsBobRule.class, new IsBobRule.Serializer(), this); +``` + +```hocon +rules { + isBob: true +} +``` + +Правило без параметров принимает в HOCON `true` - это сокращение для "правило активно". Правила с параметрами принимают ту HOCON-форму, которую парсит их сериализатор. + + + +## Экстрактор значений + +Экстрактор значений достаёт именованные значения из объекта контекста. Активаторы и каталоги используют его, чтобы пробрасывать данные контекста через плейсхолдеры. + +Реализует [`ValueExtractor`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/ValueExtractor.java): + +```java +public class UserExtractor implements ValueExtractor { + + @Override + public String extract(Object obj, String placeholder) { + if (!(obj instanceof User)) return null; + User user = (User) obj; + return switch (placeholder) { + case "user_name" -> user.name; + case "user_age" -> String.valueOf(user.age); + case "user_friends" -> String.valueOf(user.friends); + default -> null; + }; + } +} +``` + +Сами экстракторы напрямую не регистрируются. Их экземпляр возвращает активатор или каталог из своего `getValueExtractor()` / `extractor()`. + +По форме похоже на PlaceholderAPI, только тип контекста любой, не обязательно `Player`. + +## Активатор + +Активатор - слушатель событий, который открывает меню. Наследуется от абстрактного [`Activator`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/Activator.java), а тот - от Bukkit-овского `Listener`. Внутри слушай любое нужное Bukkit-событие: + +```java +public class SneakActivator extends Activator { + + @EventHandler + public void onSneak(PlayerToggleSneakEvent event) { + if (event.isSneaking()) { + openMenu(null, event.getPlayer()); + } + } + + public static class Serializer implements NodeSerializer { + @Override + public SneakActivator deserialize(Class type, ConfigNode node) { + return new SneakActivator(); + } + } +} +``` + +`openMenu(ctx, player)` открывает меню, к которому привязан активатор. `ctx` - контекст открытия, или `null`, если контекста нет. + +:::caution +Сам в Bukkit-е листенер не регистрируй. AbstractMenus делает это сам, когда активатор привязан к меню. +::: + +Регистрация активатора: + +```java +api.activators().register("onSneak", SneakActivator.class, new SneakActivator.Serializer(), this); +``` + +### Активатор с контекстом + +В `ctx` можно положить любой объект, а его поля прокинуть через `ValueExtractor`. В примере ниже контекстом идёт точка респауна, `LocationExtractor` делает её координаты доступными как `%activator_loc_x%` и т.д.: + +```java +public class RespawnActivator extends Activator { + + @EventHandler + public void onRespawn(PlayerRespawnEvent event) { + openMenu(event.getRespawnLocation(), event.getPlayer()); + } + + @Override + public ValueExtractor getValueExtractor() { + return new LocationExtractor(); + } + + public static class Serializer implements NodeSerializer { + @Override + public RespawnActivator deserialize(Class type, ConfigNode node) { + return new RespawnActivator(); + } + } +} +``` + +```java +public class LocationExtractor implements ValueExtractor { + + @Override + public String extract(Object obj, String placeholder) { + if (!(obj instanceof Location)) return null; + Location loc = (Location) obj; + return switch (placeholder) { + case "loc_x" -> String.valueOf(loc.getX()); + case "loc_y" -> String.valueOf(loc.getY()); + case "loc_z" -> String.valueOf(loc.getZ()); + default -> null; + }; + } +} +``` + +```hocon +title: "Тест" +size: 1 +activators { + onRespawn: true +} +items: [ + { + slot: 4 + material: CAKE + name: "Тестовый предмет" + lore: [ + "Loc x: %activator_loc_x%", + "Loc y: %activator_loc_y%", + "Loc z: %activator_loc_z%" + ] + } +] +``` + +## Свойство предмета + +Свойство предмета меняет его внешний вид - имя, лор, материал, custom model data и т.д. + +Реализуй [`ItemProperty`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/inventory/ItemProperty.java): + +- **`canReplaceMaterial`** - возвращает `true`, если свойство меняет материал. Такие свойства выполняются первыми, чтобы дальше остальные работали уже с валидной `ItemMeta`. +- **`isApplyMeta`** - возвращает `true`, если после `apply` AbstractMenus должен сам вызвать `setItemMeta` на стеке. Возвращай `false`, если `apply` уже разбирается с meta самостоятельно. +- **`apply`** - модифицирует `ItemStack` и/или `ItemMeta`. + +Свойство для имени: + +```java +public class DisplayNameProperty implements ItemProperty { + + private final String name; + + private DisplayNameProperty(String name) { + this.name = name; + } + + @Override + public boolean canReplaceMaterial() { return false; } + + @Override + public boolean isApplyMeta() { return true; } + + @Override + public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) { + meta.setDisplayName(name); + } + + public static class Serializer implements NodeSerializer { + @Override + public DisplayNameProperty deserialize(Class type, ConfigNode node) { + return new DisplayNameProperty(node.getString()); + } + } +} +``` + +Подмена материала (параметров нет, просто проставляет тип): + +```java +public class CreeperHeadProperty implements ItemProperty { + + @Override + public boolean canReplaceMaterial() { return true; } + + @Override + public boolean isApplyMeta() { return false; } + + @Override + public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) { + item.setType(Material.CREEPER_HEAD); + } + + public static class Serializer implements NodeSerializer { + @Override + public CreeperHeadProperty deserialize(Class type, ConfigNode node) { + return new CreeperHeadProperty(); + } + } +} +``` + +Регистрация: + +```java +api.itemProperties().register("creeperHead", CreeperHeadProperty.class, new CreeperHeadProperty.Serializer(), this); +``` + +## Каталог + +Каталог отдаёт генерируемому меню динамическую коллекцию объектов. Каждая запись становится своим предметом в меню, а `ValueExtractor` даёт доступ к её полям через плейсхолдеры. + +Класс реализует [`Catalog`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/Catalog.java): + +```java +public class UserCatalog implements Catalog { + + @Override + public Collection snapshot(Player player, Menu menu) { + return List.of( + new User("User 1", 17), + new User("User 2", 18), + new User("User 3", 19) + ); + } + + @Override + public ValueExtractor extractor() { + return new UserExtractor(); + } + + public static class Serializer implements NodeSerializer { + @Override + public UserCatalog deserialize(Class type, ConfigNode node) throws NodeSerializeException { + return new UserCatalog(); + } + } +} +``` + +`snapshot` дёргается один раз на открытие меню (или на refresh). Может вернуть пустую коллекцию, но не `null`. + +Регистрация и использование: + +```java +api.catalogs().register("users", UserCatalog.class, new UserCatalog.Serializer(), this); +``` + +```hocon +title: "Пользователи" +size: 4 + +catalog { + type: users +} + +matrix { + cells: [ + "_x_x_x_x_", + "_x_x_x_x_", + "_x_x_x_x_" + ] + templates { + "x" { + material: CAKE + name: "%activator_user_name%" + lore: "&7Возраст: &e%activator_user_age%" + } + } +} +``` + +Если каталогу нужна конфигурация (фильтр и т.п.), парси её из `ConfigNode` в сериализаторе. + +## Чистка + +Регистрации типов снимаются автоматически при выключении аддона. AbstractMenus запоминает владельца из `register(...)` и сносит всё, что помечено этим владельцем. Самому `unregister` дёргать нельзя. diff --git a/src/content/docs/ru/developers/serializers.md b/src/content/docs/ru/developers/serializers.md new file mode 100644 index 0000000..e3174b8 --- /dev/null +++ b/src/content/docs/ru/developers/serializers.md @@ -0,0 +1,186 @@ +--- +title: HOCON-сериализаторы +description: Десериализация своих типов из HOCON-конфигов меню. +--- + +
Разработчик аддонов
+ +:::caution[Alpha API] +AbstractMenus 2.0 находится в alpha. API может измениться до стабильного релиза. Зафиксируй конкретную версию `compileOnly` в сборке, чтобы свежая выборка не сломала проект. +::: + +:::note +В примерах местами игнорируем строгие Java-конвенции - чтобы код был покороче. Здесь главное показать, как работает API, а не как писать прод. +::: + +AbstractMenus оборачивает [HOCON-библиотеку Lightbend](https://github.com/lightbend/config) своими обёртками. Сериализатор - простая фабрика: принимает [`ConfigNode`](https://github.com/AbstractMenus/hocon/blob/master/src/main/java/ru/abstractmenus/hocon/api/ConfigNode.java), возвращает Java-объект. + +Каждый сериализатор реализует [`NodeSerializer`](https://github.com/AbstractMenus/hocon/blob/master/src/main/java/ru/abstractmenus/hocon/api/serialize/NodeSerializer.java) - в интерфейсе один метод: `deserialize(Class type, ConfigNode node)`. + +## Когда нужно регистрировать сериализатор + +Обычно регистрировать сериализатор руками не приходится. Пять вызовов `register(...)` на реестрах типов (action, rule, item-property, activator, catalog) принимают `NodeSerializer` и сами кладут его в общую коллекцию `NodeSerializers`. + +Регистрировать руками нужно тогда, когда у тебя есть кастомный *объект-параметр*, которым пользуется твоё действие или правило, и его надо уметь читать вложенным в другую структуру через `node.getValue(MyType.class)`. + +```java +api.serializers().register(MyType.class, new MyTypeSerializer()); +``` + +Регистрируй из `MenuExtension.onEnable(api)`. К моменту, когда твой `onEnable` отработает, AbstractMenus ещё не успел загрузить меню - значит, всё, что ты зарегистрировал, попадёт в парсинг. + +## Дефолтные сериализаторы + +Из коробки в AbstractMenus есть сериализаторы для Java-примитивов и нескольких ходовых типов: + +- `Boolean` +- `Integer` +- `Long` +- `Float` +- `Double` +- `String` +- `UUID` + +Плюс свои типы значений: `TypeBool`, `TypeInt`, `TypeDouble`, `TypeString`, `TypeMaterial`, `TypeLocation`, `TypeSlot` и т.д. + +## Пример 1. Десериализация простого объекта + +```java +public class User { + public String name; + public int age; +} +``` + +```hocon +user { + name: "Notch" + age: 42 +} +``` + +```java +public class UserSerializer implements NodeSerializer { + + @Override + public User deserialize(Class type, ConfigNode node) throws NodeSerializeException { + User user = new User(); + user.name = node.node("name").getString(); + user.age = node.node("age").getInt(); + return user; + } +} +``` + +`ConfigNode` - распарсенная HOCON-структура. Сериализатор читает у `node` именованные поля и заливает их в целевой тип. + +## Пример 2. Десериализация вложенных объектов + +Если у поля свой сериализатор - просто зови `getValue(SomeType.class)`, и зарегистрированный сериализатор `SomeType` отработает сам: + +```hocon +user { + name: "Notch" + age: 42 + friend { + name: "Alex" + age: 38 + } +} +``` + +```java +public class User { + public String name; + public int age; + public User friend; +} +``` + +```java +public class UserSerializer implements NodeSerializer { + + @Override + public User deserialize(Class type, ConfigNode node) throws NodeSerializeException { + User user = new User(); + user.name = node.node("name").getString(); + user.age = node.node("age").getInt(); + user.friend = node.node("friend").getValue(User.class); + return user; + } +} +``` + +`getValue(User.class)` найдёт зарегистрированный `UserSerializer` и натравит его на вложенную ноду. Главное, чтобы `UserSerializer` был зарегистрирован до парсинга HOCON, который на него ссылается - иначе `getValue` кинет исключение. + +## Пример 3. Десериализация коллекций + +В HOCON есть списки. API умеет их для любого зарегистрированного типа. + +```hocon +user { + name: "Notch" + age: 42 + friends: [ + { name: "Petya", age: 34 }, + { name: "Alex", age: 38 } + ] +} +``` + +```java +public class User { + public String name; + public int age; + public List friends; +} +``` + +```java +public class UserSerializer implements NodeSerializer { + + @Override + public User deserialize(Class type, ConfigNode node) throws NodeSerializeException { + User user = new User(); + user.name = node.node("name").getString(); + user.age = node.node("age").getInt(); + user.friends = node.node("friends").getList(User.class); + return user; + } +} +``` + +`getList(SomeType.class)` работает для любого типа с зарегистрированным сериализатором, в том числе для примитивов. `node.getList(String.class)` вернёт `List`. + +## Шорткаты ConfigNode + +У `ConfigNode` ожидаемый набор методов чтения: + +```java +node.getString() // примитив-строка +node.getString("default") // примитив-строка со значением по умолчанию +node.getInt() +node.getInt(0) +node.getBoolean() +node.getDouble() +node.getList(String.class) +node.isNull() +node.isPrimitive() +node.isMap() +node.isList() + +node.node("path") // дочерняя нода по dotted-пути (один или несколько сегментов) +node.child("name") // одношаговый поиск ребёнка +node.childrenList() // List для нод-списков +node.childrenMap() // Map для нод-карт +node.hasChildren() +node.key() // имя этой ноды в родителе +node.path() // полный dotted-путь от корня +node.parent() // родительский ConfigNode или null +node.getValue(MyType.class) // запустить зарегистрированный сериализатор +node.getValue(MyType.class, fallback) +``` + +`isNull()` - самый дешёвый способ проверить, есть ли опциональное поле. Стандартный паттерн: `node.node("optional").isNull() ? defaultValue : node.node("optional").getInt()`. + +С нодами-списками и нодами-картами постоянно используешь `childrenList()` / `childrenMap()` - именно так встроенные сериализаторы обходят предметы, действия и биндинги. diff --git a/src/content/docs/ru/developers/utils.md b/src/content/docs/ru/developers/utils.md new file mode 100644 index 0000000..5331482 --- /dev/null +++ b/src/content/docs/ru/developers/utils.md @@ -0,0 +1,88 @@ +--- +title: Утилиты +description: Хелперы цвета, подстановка плейсхолдеров, планировщик. +--- + +
Разработчик аддонов
+ +:::caution[Alpha API] +AbstractMenus 2.0 находится в alpha. API может измениться до стабильного релиза. Зафиксируй конкретную версию `compileOnly` в сборке, чтобы свежая выборка не сломала проект. +::: + +Маленькие хелперы, которые аддонам нужны постоянно. + +## Замена цветовых кодов + +`Colors.of(String)` превращает легаси-коды с `&` (`&a`, `&l` и т.п.), коды с section-sign (`§a`) и hex-коды в HTML-стиле (`<#FFAA00>`) в формат с section-sign, который понимает Minecraft. Если в `config.conf` включён `useMiniMessage`, распознаются ещё и теги MiniMessage (``, ``, ``). + +```java +import ru.abstractmenus.api.text.Colors; + +String coloured = Colors.of("&aПривет, &b%player_name%&a!"); +``` + +`Colors.of` возвращает строку, готовую для `player.sendMessage(...)`. + +## Подстановка плейсхолдеров + +Секция плейсхолдеров в реестре провайдеров одинаково ровно жуёт встроенные плейсхолдеры (`%player_name%`, `%activator_*%` и т.п.) и токены PlaceholderAPI: + +```java +PlaceholderHandler ph = api.providers().placeholders().resolve(); +String resolved = ph.replace(player, "Привет, %player_name%!"); +List resolvedLore = ph.replace(player, lore); +``` + +`replace(Player, String)` - однострочная форма для отображаемых имён, заголовков, аргументов действий. `replace(Player, List)` - многострочная, под лор предметов. Обе нормально переживают `null` и пустой ввод и ничего не кидают: если бэкенд упал, на выход уйдёт исходная строка. + +## Планировщик для сущностей на Folia + +Если ты из планируемой задачи трогаешь игрока или другую сущность, а сервер на Folia, глобальный региональный планировщик трогать состояние сущностей не имеет права. Для таких случаев у AbstractMenus есть Folia-aware хелперы в `BukkitTasks` (живут в plugin-модуле, не в api): + +```java +BukkitTasks.runForEntity(player, () -> player.updateInventory()); +BukkitTasks.runForEntityLater(player, () -> player.kick(reason), 20L); +BukkitTasks.runForEntityTimer(player, () -> player.giveExp(1), 20L, 20L); +``` + +Для задач вне контекста сущности у `BukkitTasks` есть привычные формы планировщика: + +```java +BukkitTasks.runTask(() -> doWork()); +BukkitTasks.runTaskAsync(() -> hitDatabase()); +BukkitTasks.runTaskLater(() -> notify(), 40L); +BukkitTasks.runTaskLaterAsync(() -> upload(), 40L); +BukkitTasks.runTaskTimer(() -> tick(), 0L, 20L); +BukkitTasks.runTaskTimerAsync(() -> poll(), 0L, 20L); + +if (BukkitTasks.isFolia()) { + // ветка, где нужен entity-aware планировщик +} +``` + +На Spigot/Paper без Folia они откатываются в обычный Bukkit-планировщик. На Folia entity-варианты уходят в регион-владелец сущности (единственный поток, которому можно менять её состояние), а глобальные - в global region scheduler. + +Если ты сидишь только на api, без plugin-модуля, делай то же самое руками: ветвись по детекту Folia и зови `Bukkit.getRegionScheduler()` / `getEntityScheduler()` напрямую. + +## Прочие точки доступа + +```java +Plugin am = api.getPlugin(); // сырой хэндл Bukkit Plugin (экземпляр AbstractMenus) +String version = api.apiVersion(); // например "2.0.0-alpha.2" +``` + +`getPlugin()` пригодится, когда какому-то Bukkit API нужна ссылка `Plugin`, а своей у тебя нет (например, надо повесить листенер под владельцем плагина AbstractMenus). По возможности используй собственный дескриптор плагина - иначе листенер, повешенный от имени AbstractMenus, переживёт твой `onDisable`. + +## Логирование + +Чтобы лог шёл единообразно и с префиксом AbstractMenus, используй `Logger`: + +```java +import ru.abstractmenus.api.Logger; + +Logger.info("Addon ready"); +Logger.warning("Skipped quest entry: invalid id"); +Logger.severe("Catalog snapshot failed", throwable); +``` + +Под капотом он пишет через `java.util.logging.Logger` хост-плагина, поэтому в консоли сообщения идут с префиксом `[AbstractMenus]`, кто бы их ни кинул. Обычный `System.out.println` тоже работает, но без тега. diff --git a/src/content/docs/ru/developers/variables.md b/src/content/docs/ru/developers/variables.md new file mode 100644 index 0000000..a8f21b9 --- /dev/null +++ b/src/content/docs/ru/developers/variables.md @@ -0,0 +1,76 @@ +--- +title: Variables API +description: Чтение и запись персональных и глобальных переменных программно. +--- + +
Разработчик аддонов
+ +:::caution[Alpha API] +AbstractMenus 2.0 находится в alpha. API может измениться до стабильного релиза. Зафиксируй конкретную версию `compileOnly` в сборке, чтобы свежая выборка не сломала проект. +::: + +:::note +В примерах местами игнорируем строгие Java-конвенции - чтобы код был покороче. Здесь главное показать, как работает API, а не как писать прод. +::: + +В AbstractMenus есть небольшое CRUD API для тех же переменных, которые авторы меню крутят через `/var` и `/varp`. Javadoc: [api/variables](https://github.com/AbstractMenus/minecraft-plugin/tree/master/api/src/main/java/ru/abstractmenus/api/variables). + +## `Var` + +[`Var`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/variables/Var.java) - это запись одной переменной. Значения хранятся строками, но у `Var` есть типизированные аксессоры, которые парсят при чтении (могут бросить, если в строке не валидное число). + +Срок жизни хранится в миллисекундах UTC. Чтобы понять, жива ли переменная, сравни `expiry()` с `System.currentTimeMillis()`. `expiry() == 0` - "без срока". + +## `VariableManager` + +[`VariableManager`](https://github.com/AbstractMenus/minecraft-plugin/blob/master/api/src/main/java/ru/abstractmenus/api/variables/VariableManager.java) - точка входа в CRUD. Достаём из API: + +```java +AbstractMenusApi api = AbstractMenusApi.get(); +VariableManager vars = api.variables(); +``` + +Имена переменных и игроков, которые ты отдаёшь менеджеру, нечувствительны к регистру - руками в lowercase приводить не нужно. + +## Создание переменной + +```java +Var var = api.variables().createBuilder() + .name("welcome_message") + .value("Привет!") + .expiry(System.currentTimeMillis() + 10_000) // истекает через 10 секунд + .build(); + +api.variables().saveGlobal(var); +``` + +Для персональных переменных - `savePersonal(playerName, var)`. + +`expiry(0)` (либо просто не звать `.expiry(...)`) делает переменную бессрочной. + +## Чтение и удаление + +```java +Var welcome = api.variables().getGlobal("welcome_message"); +Var claimed = api.variables().getPersonal(player.getName(), "dailyReward"); + +api.variables().deleteGlobal("welcome_message"); +api.variables().deletePersonal(player.getName(), "dailyReward"); +``` + +`getGlobal` и `getPersonal` возвращают `null`, если переменной нет или она истекла и уже подметена. Имена переменных и игроков сравниваются без учёта регистра. + +## Аксессоры Var + +`Var` хранит значения строками, но отдаёт типизированные аксессоры, которые парсят на лету: + +```java +Var counter = vars.getPersonal(player.getName(), "kills"); +int kills = counter.intValue(); // бросит, если значение не int +long ts = counter.longValue(); +boolean on = counter.boolValue(); +double pct = counter.doubleValue(); +float rate = counter.floatValue(); +``` + +Для временных переменных есть `Var#hasExpiry()` и `Var#isExpired()`. Чтобы получить новую переменную на основе существующей (например, продлить срок), используй `Var#toBuilder()`. diff --git a/src/content/docs/ru/general/actions.md b/src/content/docs/ru/general/actions.md new file mode 100644 index 0000000..a8b5ee1 --- /dev/null +++ b/src/content/docs/ru/general/actions.md @@ -0,0 +1,1068 @@ +--- +title: Действия +description: "Действие - это то, что плагин выполнит после события: клик по предмету, открытие меню и т.д. Ниже полный список." +--- + +
Автор меню
+ +Действие - это то, что плагин выполнит после события: клик по предмету, открытие меню и т.д. Ниже полный список. + +## Все действия + +| Название | Тип данных | Описание | +|----|----|----| +| openMenu | String | Открыть меню с указанным именем | +| openMenuCtx | String | То же, что `openMenu`, но передаёт [контекст](/docs/ru/advanced/input/) от активатора предыдущего меню | +| closeMenu | Boolean или Number | Закрыть текущее меню. Если вместо boolean указано число, меню закроется после задержки в указанных тиках | +| refreshMenu | Boolean или Number | Обновить всё содержимое меню кроме заголовка. Если вместо boolean указано число, меню обновится после задержки в указанных тиках | +| [message](#message) | Object или String | Отправить сообщение игроку. Можно отправить простой текст, JSON, title и др. | +| [broadcast](#message) | Object или String | Отправить сообщение всем игрокам на сервере. Формат как у `message` | +| [miniMessage](#message) | String | **(Устарело. MiniMessage теперь поддерживается стандартными message-действиями)** Отправить сообщение через `mini-message` | +| playerChat | Список строк | Отправить сообщение от лица игрока, который открыл меню | +| print | String | Вывести сообщение в консоль. Полезно для отладки | +| [command](#command) | Object | Выполнить список команд от лица игрока или сервера | +| [inputChat](#chat-input) | Object | Запросить у игрока ввод текста в чат и сохранить результат в переменную | +| [teleport](#teleport) | Object | Телепортировать игрока в локацию | +| [itemAdd](#add-item) | Список объектов | Выдать игроку любые предметы | +| [itemRemove](#remove-item) | Список объектов | Удалить предметы из инвентаря игрока. Сравнение по указанным свойствам или просто по номеру слота | +| [itemClear](#remove-item) | Список объектов | Удалить предметы из инвентаря игрока так же, как `itemRemove`, но без учёта размера стака. Свойство `count` здесь не работает | +| inventoryClear | Boolean | Полностью очистить инвентарь игрока | +| bungeeConnect | String | Подключить игрока к другому BungeeCord-серверу | +| [giveMoney](#provider-selection) | Number или Object | Начислить деньги. | +| [takeMoney](#provider-selection) | Number или Object | Снять деньги. | +| [givePermission](#provider-selection) | Список строк или Object | Выдать право. | +| [removePermission](#provider-selection) | Список строк или Object | Отозвать право. | +| [addGroup](#provider-selection) | String или Object | Добавить в группу прав. | +| [removeGroup](#provider-selection) | String или Object | Убрать из группы прав. | +| [lpMetaSet](#lp-meta) | Object | Только для LuckPerms: задать meta-значения (`metaList`). Если активный провайдер прав не LuckPerms - пишет warning и пропускает. | +| [lpMetaRemove](#lp-meta) | Список строк | Только для LuckPerms: удалить meta-ключи. Если активный провайдер прав не LuckPerms - пишет warning и пропускает. | +| setGamemode | String | Установить новый режим игры. Все имена режимов [здесь](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/GameMode.html) | +| setHealth | Number | Установить здоровье игрока | +| setFoodLevel | Number | Установить уровень еды игрока | +| [giveXp](#provider-selection) | Number или Object | Выдать XP. | +| [takeXp](#provider-selection) | Number или Object | Снять XP. | +| [giveLevel](#provider-selection) | Number или Object | Повысить уровень. | +| [takeLevel](#provider-selection) | Number или Object | Понизить уровень. | +| [sound](#sound) | Object | Проиграть звук | +| [customSound](#custom-sound) | Object | Проиграть кастомный звук из ресурспака | +| [potionEffect](#add-potion-effect) | Список объектов | Наложить на игрока эффект зелья | +| [removePotionEffect](#remove-potion-effect) | Список строк | Снять с игрока эффект зелья | +| [openBook](#open-book) | Object | Создать и открыть игроку книгу | +| [setProperty](#set-item-property) | Object | Задать новые или перезаписать существующие свойства предмета меню | +| [remProperty](#remove-item-property) | Список объектов | Удалить указанные свойства у предмета меню | +| [refreshItem](#refresh-item) | Multiple | Обновить только один предмет меню без обновления всего меню | +| [setSkin](#set-skin) | Object | Поставить скин. | +| [resetSkin](#set-skin) | Boolean | Сбросить скин. | +| [addRecipe](#add-recipe) | Список объектов | Добавить новые кастомные рецепты крафта | +| [setButton](#set-menu-button) | Список объектов | Добавить новую кнопку в открытое меню | +| [removeButton](#remove-menu-button) | Slot (число, диапазон, матрица) | Удалить кнопку из открытого меню (см. [drag-and-drop](/docs/ru/advanced/drag-and-drop/)) | +| placeItem | Список объектов | Для drag-and-drop. Положить перетаскиваемый предмет в перетаскиваемый слот (см. [drag-and-drop](/docs/ru/advanced/drag-and-drop/)) | +| removePlaced | Slot или Object | Удалить перетаскиваемый предмет из меню (см. [drag-and-drop](/docs/ru/advanced/drag-and-drop/)) | +| **Глобальные переменные** | | | +| [setVar](#global-vars) | Список объектов, Список строк | Создать или заменить глобальную переменную | +| [removeVar](#global-vars) | Список объектов, Список строк | Удалить глобальную переменную | +| [incVar](#global-vars) | Список объектов, Список строк | Прибавить к глобальной числовой переменной | +| [decVar](#global-vars) | Список объектов, Список строк | Вычесть из глобальной числовой переменной | +| [mulVar](#global-vars) | Список объектов, Список строк | Умножить глобальную числовую переменную | +| [divVar](#global-vars) | Список объектов, Список строк | Разделить глобальную числовую переменную | +| **Персональные переменные** | | | +| [setVarp](#personal-vars) | Список объектов, Список строк | Создать или заменить персональную переменную | +| [removeVarp](#personal-vars) | Список объектов, Список строк | Удалить персональную переменную | +| [incVarp](#personal-vars) | Список объектов, Список строк | Прибавить к персональной числовой переменной | +| [decVarp](#personal-vars) | Список объектов, Список строк | Вычесть из персональной числовой переменной | +| [mulVarp](#personal-vars) | Список объектов, Список строк | Умножить персональную числовую переменную | +| [divVarp](#personal-vars) | Список объектов, Список строк | Разделить персональную числовую переменную | +| **Специальные действия** | | | +| [delay](#delay) | Object | Обернуть блок действий, чтобы выполнить их с задержкой | +| [bulk](#bulk) | Список объектов | Выполнить несколько действий, в том числе одного типа | +| [randActions](#random-actions) | Список объектов | Выполнить случайный блок действий из списка | +| [playerScope](#player-scope-actions) | Object | Выполнить действия для другого игрока | +| **Для генерируемых меню** | | | +| pagePrev | Number | Переключить на одну из предыдущих страниц. Работает только с генерируемыми меню | +| pageNext | Number | Переключить на одну из следующих страниц. Работает только с генерируемыми меню | + +## Provider selection + +Действия с деньгами, уровнями, правами и скинами идут через систему провайдеров AbstractMenus. Из коробки за деньги и права отвечает [Vault](https://www.spigotmc.org/resources/vault.34315/), за группы - [LuckPerms](https://www.spigotmc.org/resources/28140/), за уровни - ванильный XP, за скины - [SkinsRestorer](https://www.spigotmc.org/resources/2124/). + +Если у вас экономика не на Vault, а на чём-то ещё (например, PlayerPoints), есть два варианта: поставить готовый аддон-мост вроде [PlayerPointsAddon](https://github.com/AbstractMenus/PlayerPointsAddon), либо написать [свой аддон](/docs/ru/developers/addons/), который зарегистрирует нужный плагин как провайдера. Получите вторую (или третью) экономику на том же сервере, между которыми меню переключаются по полю `provider:`. + +### Как работает `auto` + +В одной категории может быть зарегистрировано несколько провайдеров одновременно. Например, в секции экономики живут Vault (встроенный) и PlayerPoints (через аддон). Каждый провайдер при регистрации указывает свой id и приоритет (число): встроенные провайдеры идут с приоритетом 50, аддонные обычно с 100. + +В `config.conf providers.<секция>` по умолчанию стоит `"auto"`. Это значит: AbstractMenus сам выбирает провайдера с самым высоким приоритетом среди зарегистрированных в этой секции. На сервере, где стоят и Vault, и PlayerPointsAddon, при `economy = "auto"` AbstractMenus возьмёт PlayerPoints (приоритет 100 побеждает 50). + +Если хочется зафиксировать конкретный плагин, вместо `"auto"` пишется его id - `"vault"`, `"playerpoints"` или любой другой зарегистрированный. Тогда даже если придёт аддон с более высоким приоритетом, AbstractMenus продолжит ходить туда, куда сказал оператор. + +Эту настройку можно переопределить ещё и для отдельного действия (`provider: "..."` внутри действия) - см. примеры ниже. + +### Провайдер по умолчанию (без поля `provider:`) + +```hocon +takeMoney: 100 +``` + +Идёт через `config.conf providers.economy`. Если стоит `"auto"` (по умолчанию), плагин возьмёт провайдера с наивысшим приоритетом. Поставь конкретный id вроде `"vault"` или `"playerpoints"`, чтобы зафиксировать выбор. + +### Переопределение в самом действии + +Объектная форма позволяет явно назвать провайдера. Удобно, когда в одном меню смешаны валюты - монеты для покупки предметов, очки для лотереи: + +```hocon +actions { + click: [ + { type: takeMoney, amount: 100, provider: "vault" } // экономика сервера + { type: giveMoney, amount: 5, provider: "playerpoints" } // донат-токены + ] +} +``` + +### Глобальное значение для сервера + +Если все меню должны использовать PlayerPoints, отредактируй `plugins/AbstractMenus/config.conf`: + +```hocon +providers { + economy = "playerpoints" +} +``` + +Теперь скалярная форма (`takeMoney: 100`) автоматически идёт через PlayerPoints. + +### Порядок резолва + +Когда AbstractMenus запускает действие с деньгами/уровнями/правами, хендлер выбирается так: + +1. **`provider: "..."` в самом действии** - если указан, выигрывает всегда. +2. **`config.conf providers.
`** - применяется, если стоит не `auto`. +3. **По приоритету** - побеждает провайдер с наивысшим приоритетом. У встроенных он 50, у аддонов обычно 100 - значит аддон выигрывает, пока в конфиге явно не указан Vault. + +Тот же формат работает для `giveLevel`/`takeLevel` (уровни), `givePermission`/`removePermission`/`addGroup`/`removeGroup` (права) и `setSkin`/`resetSkin` (скины). Просто меняй `economy` на нужную секцию в `config.conf`. + +Как написать своего провайдера, см. [Хендлеры провайдеров](/docs/ru/developers/handlers/). + +## Message + +Отправить игроку текст. Например: + +```hocon +message { + chat: [ + "Строка 1", + "Строка 2" + ] + title: "Заголовок" + subtitle: "Подзаголовок" + fadeIn: 10 + stay: 20 + fadeOut: 10 + actionbar: "&aПривет" + json: "{'text'='Привет'}" +} +``` + +У блока много параметров, и каждый можно использовать сам по себе. + +| Название | Тип данных | Описание | +|----------------------|--------------|------------------------------------------------------| +| chat | Список строк | Отправить личное сообщение в чат | +| actionbar | String | Отправить текст в action bar игрока | +| json | String | Отправить личное JSON-сообщение в чат. Работает на MC 1.9+ | +| **Параметры title** | | | +| title | String | Отправить title | +| subtitle | String | Отправить subtitle | +| fadeIn | Number | Время появления в тиках | +| stay | Number | Время отображения в тиках | +| fadeOut | Number | Время исчезновения в тиках | + +`message` можно задать и просто строкой - тогда действие пошлёт сообщение в чат. + +```hocon +message: "Одиночное сообщение" +``` + +JSON-сообщение можно задать не строкой, а прямо HOCON-синтаксисом. Пример: + +```hocon +message { + json { + text: "&aКакой-то текст" + hoverEvent { + action: "show_text" + value: "&eКакой-то текст" + } + } +} +``` + +Этот блок эквивалентен такому JSON-сообщению: + +```hocon +message { + json: "{'text':'&aКакой-то текст', 'hoverEvent':{'action':'show_text', 'value':'&eКакой-то текст'}}" +} +``` + +:::tip +Подробнее о JSON-тексте [здесь](https://minecraft.gamepedia.com/Commands#Raw_JSON_text) +::: + +## Command + +Выполнить команду от лица игрока или от сервера. Пример: + +```hocon +command { + player: [ + "command 1", + "command 2" + ] + console: [ + "command 1", + "command 2" + ] +} +``` + +Здесь `/command 1` и `/command 2` выполнятся и от игрока, и от сервера. + +Можно отправить только от игрока или только от сервера: + +```hocon +command { player: "command 1" } +``` + +То же самое с блоком `console`. + +По умолчанию во всех командах плейсхолдеры подставляются перед отправкой. Поставь `ignorePlaceholder: true`, чтобы отправить строку как есть - удобно, когда в команде буквально нужны символы `%`. + +```hocon +command { + console: "lp user %player_name% permission set foo.bar true" + ignorePlaceholder: false // по умолчанию; подставляет %player_name% +} +``` + +## Chat Input + +Закрывает меню и просит игрока написать что-нибудь в чат. Введённый текст плагин сохранит в переменную. + +Подробнее - на странице [Chat input](/docs/ru/advanced/input/#chat-input). + +## Teleport + +Телепортирует игрока в указанную точку. Пример: + +```hocon +teleport { + world: "world" + x: 0.0 + y: 100.0 + z: 0.0 + yaw: 0.0 + pitch: 0.0 +} +``` + +У действия есть короткая версия: + +```hocon +teleport: "world, 0.0, 100.0, 0.0, 0.0, 0.0" +``` + +## Add item + +Выдаёт игроку один или несколько предметов. Формат предмета - на странице [формат предмета](/docs/ru/general/item-format/). Пример: + +```hocon +itemAdd: [ + { + slot: 0 + material: STONE + name: "Мой камень" + }, + { + material: CAKE + name: "Мой кекс" + } +] +``` + +Предметы без `slot` лягут в первый свободный слот инвентаря. + +:::tip +Не забывай про [сокращение для одного элемента](/docs/ru/start/hocon/) в HOCON: список из одного элемента можно записать как обычное значение. +::: + +## Remove item + +Зеркало `itemAdd`: предметы удаляются. Если задан `slot` - удаляется именно из него. Без `slot` - по совпадению с указанными свойствами. Пример: + +```hocon +itemRemove: [ + { + slot: 0 + material: STONE + name: "Мой камень" + }, + { + material: CAKE + name: "Мой кекс" + } +] +``` + +## Sound + +Проигрывает звук. Все параметры: + +```hocon +sound { + name: "SOUND_NAME" + volume: 1.0 + pitch: 1.0 + public: false + location { + world: "world" + x: 0.0 + y: 0.0 + z: 0.0 + } +} +``` + +Обязательный только `name`, остальные - опциональные. + +- **`name`** - имя звука из Bukkit. Все имена - [здесь](https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Sound.html). + +- **`volume`** - громкость, от `0.0` до `1.0`. + +- **`pitch`** - pitch, от `0.0` до `10.0`. + +- **`public`** - будут ли звук слышать игроки рядом. + +- **`location`** - где проиграть звук. Формат локации - как в [teleport](#teleport). + +Есть и короткий формат: + +```hocon +sound: "SOUND_NAME" +``` + +Тогда звук проиграет только самому игроку, в его текущей локации. + +## Custom sound + +То же, что [sound](#sound), только имя звука берётся из ресурспака. Пример: + +```hocon +customSound: "name.songs_sound" +``` + +В объектной форме принимает всё то же, что `sound`, плюс `category`: + +```hocon +customSound: { + name: "name.songs_sound" + category: RECORDS + volume: 1.0 + pitch: 1.0 + public: false + location { + world: "world" + x: 0.0 + y: 0.0 + z: 0.0 + } +} +``` + +- **`name`** - имя звука из ресурспака (например, `name.songs_sound`). Обязательно. +- **`category`** - одно из значений из [этого списка](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html). По умолчанию `MASTER`. +- **`volume`** - громкость. По умолчанию `1.0`. +- **`pitch`** - pitch. По умолчанию `1.0`. +- **`public`** - если `true`, звук слышат и игроки рядом. По умолчанию `false`. +- **`location`** - где проиграть звук. Формат - как в [teleport](#teleport). По умолчанию - текущая локация игрока. + +## Add potion effect + +Накладывает на игрока эффект зелья. + +```hocon +potionEffect: [ + { + effectType: FAST_DIGGING + duration: 100 + amplifier: 1 + } +] +``` + +Формат такой же, как у [свойства предмета `potionData`](/docs/ru/general/item-format/). + +## Remove potion effect + +Снимает с игрока эффекты зелий. Пример: + +```hocon +removePotionEffect: [ + FAST_DIGGING, + SPEED +] +``` + +Снимет с игрока сразу два эффекта. + +## Open book + +Открывает игроку книгу. Формат повторяет [свойство предмета `bookData`](/docs/ru/general/item-format/). + +```hocon +openBook { + author: "Питер Пайпер" + title: "&e&lСупер заголовок" + pages: [ + "Содержимое первой страницы", + "Содержимое второй страницы", + "..." + ] +} +``` + +## LP meta + +Действия для чтения и записи `meta`-значений пользователя в LuckPerms. Идут через активный провайдер прав и срабатывают, только когда этот провайдер - LuckPerms. Если нет, плагин пишет warning и пропускает действие. + +### `lpMetaSet` + +```hocon +lpMetaSet { + ignorePlaceholder: false + metaList: [ + { key: "prefix", value: "&7[Участник]" } + { key: "suffix", value: "&8[%player_world%]" } + ] +} +``` + +- **`metaList`** - список пар `{ key, value }`. `value` проходит через подстановку плейсхолдеров, если `ignorePlaceholder` не `true`. +- **`ignorePlaceholder`** - если `true`, `value` отправляется как есть. По умолчанию `false`. + +### `lpMetaRemove` + +Удаляет meta-ключи. Принимает строку или список: + +```hocon +lpMetaRemove: "prefix" +``` + +```hocon +lpMetaRemove: [ "prefix", "suffix" ] +``` + +## Variables + +Эта группа действий создаёт, обновляет, удаляет переменные и считает арифметику над ними. + +:::note +Подробнее про переменные - на странице [Переменные](/docs/ru/general/variables/). +::: + +У каждого действия с переменными две версии - **глобальная** и **персональная**. Например, `setVar` для глобальных и `setVarp` для персональных. + +### Global vars + +Действия для глобальных переменных. + +#### Set + +Создаёт или обновляет глобальную переменную. Один из форматов: + +```hocon +setVar: "::" +setVar: "::::