{"id":47595973,"url":"https://github.com/mzcydev/paper-core","last_synced_at":"2026-04-01T18:05:22.076Z","repository":{"id":344953284,"uuid":"1183751061","full_name":"mzcydev/paper-core","owner":"mzcydev","description":"A professional, annotation-driven plugin framework for Paper 1.21.x built around dependency injection, automatic component scanning, and a clean module lifecycle.","archived":false,"fork":false,"pushed_at":"2026-03-17T02:33:57.000Z","size":370,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-17T13:08:01.974Z","etag":null,"topics":["api","framework","minecraft","minecraft-api","minecraft-framework","minecraft-plugin","minecraft-server-plugin","paper","papermc"],"latest_commit_sha":null,"homepage":"https://www.mzcy.dev/projects/paper-core/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mzcydev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-16T23:15:52.000Z","updated_at":"2026-03-17T02:34:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mzcydev/paper-core","commit_stats":null,"previous_names":["mzcydev/paper-core"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/mzcydev/paper-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzcydev%2Fpaper-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzcydev%2Fpaper-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzcydev%2Fpaper-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzcydev%2Fpaper-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mzcydev","download_url":"https://codeload.github.com/mzcydev/paper-core/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzcydev%2Fpaper-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290743,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","framework","minecraft","minecraft-api","minecraft-framework","minecraft-plugin","minecraft-server-plugin","paper","papermc"],"created_at":"2026-04-01T18:04:53.464Z","updated_at":"2026-04-01T18:05:21.569Z","avatar_url":"https://github.com/mzcydev.png","language":"Java","readme":"# Core Framework\n\n\u003e A professional, annotation-driven plugin framework for **Paper 1.21.x** built around dependency injection, automatic\n\u003e component scanning, and a clean module lifecycle.\n\n```\ndev.mzcy.core  ·  Paper 1.21.x  ·  Java 21  ·  Gradle KTS  ·  Lombok\n```\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Architecture](#architecture)\n- [Getting Started](#getting-started)\n    - [Adding Core as a Dependency](#adding-core-as-a-dependency)\n    - [Project Structure](#project-structure)\n- [Dependency Injection](#dependency-injection)\n    - [Registering Components](#registering-components)\n    - [Injecting Dependencies](#injecting-dependencies)\n    - [Scopes](#scopes)\n    - [Named Qualifiers](#named-qualifiers)\n    - [Lifecycle Callbacks](#lifecycle-callbacks)\n    - [Manual Binding](#manual-binding)\n- [Config Framework](#config-framework)\n    - [Creating a Config](#creating-a-config)\n    - [Accessing Configs](#accessing-configs)\n    - [Reloading](#reloading)\n    - [Formats](#formats)\n- [Command Framework](#command-framework)\n    - [Root Commands](#root-commands)\n    - [Sub-Commands](#sub-commands)\n    - [CommandContext API](#commandcontext-api)\n    - [Tab Completion](#tab-completion)\n    - [Cooldown System](#cooldown-system)\n- [Inventory Framework](#inventory-framework)\n    - [Creating a GUI](#creating-a-gui)\n    - [Opening a GUI](#opening-a-gui)\n    - [Refreshing a GUI](#refreshing-a-gui)\n    - [GuiBuilder API](#guibuilder-api)\n    - [Paged GUI](#paged-gui)\n- [Data Store](#data-store)\n    - [Defining a Store](#defining-a-store)\n    - [CRUD Operations](#crud-operations)\n    - [TTL / Expiry](#ttl--expiry)\n    - [Custom Key Types](#custom-key-types)\n- [Event Listeners](#event-listeners)\n- [Module System](#module-system)\n    - [Creating a Module](#creating-a-module)\n    - [Registering Modules](#registering-modules)\n    - [Lifecycle Order](#lifecycle-order)\n- [Item Builders](#item-builders)\n    - [ItemBuilder](#itembuilder)\n    - [SkullBuilder](#skullbuilder)\n    - [LeatherArmorBuilder](#leatherarmorbuilder)\n    - [BookBuilder](#bookbuilder)\n    - [FireworkBuilder](#fireworkbuilder)\n- [Display Systems](#display-systems)\n    - [Title \u0026 Actionbar Manager](#title--actionbar-manager)\n    - [Boss Bar Manager](#boss-bar-manager)\n    - [Scoreboard Framework](#scoreboard-framework)\n    - [Hologram Framework](#hologram-framework)\n- [NPC Framework](#npc-framework)\n- [Interaction Systems](#interaction-systems)\n    - [Chat Input Handler](#chat-input-handler)\n    - [Form System](#form-system)\n    - [Menu System](#menu-system)\n    - [Conversation System](#conversation-system)\n- [Task Pipeline](#task-pipeline)\n- [Loot Table System](#loot-table-system)\n- [Particle System](#particle-system)\n- [PlaceholderAPI Integration](#placeholderapi-integration)\n- [Update Checker](#update-checker)\n- [Hot-Reload](#hot-reload)\n- [Debug Overlay](#debug-overlay)\n- [Utilities](#utilities)\n    - [SchedulerUtil](#schedulerutil)\n    - [ComponentUtil](#componentutil)\n    - [ColorUtil](#colorutil)\n    - [TimeUtil](#timeutil)\n    - [SoundUtil](#soundutil)\n    - [Preconditions](#preconditions)\n- [Annotation Reference](#annotation-reference)\n- [Exception Hierarchy](#exception-hierarchy)\n- [Boot Sequence](#boot-sequence)\n- [Full Example Plugin](#full-example-plugin)\n\n---\n\n## Overview\n\nCore is a **framework plugin** — it does not add gameplay. It provides the infrastructure that your own plugins build on\ntop of:\n\n| Subsystem               | What it gives you                                                                                      |\n|-------------------------|--------------------------------------------------------------------------------------------------------|\n| **DI Container**        | Constructor, field, and method injection with singleton/prototype scopes                               |\n| **Class Scanner**       | Automatic discovery of `@Component`, `@Command`, `@Config`, `@Listener`, `@DataStore`, `@InventoryGui` |\n| **Config Framework**    | Type-safe YAML/JSON configs as plain Java objects                                                      |\n| **Command Framework**   | Annotation-based commands with sub-command routing, cooldowns, no `plugin.yml` declarations needed     |\n| **Inventory Framework** | Fluent GUI builder with automatic click routing, paged GUIs, and per-player state isolation            |\n| **Data Store**          | Binary, non-human-readable persistent key-value storage per plugin                                     |\n| **Display Systems**     | Title/Actionbar manager, BossBar manager, Scoreboard sidebar, Hologram framework (Display entities)    |\n| **NPC Framework**       | Citizens-free NPC system with hologram labels, skin support, and click actions                         |\n| **Interaction Systems** | Chat input, multi-step forms, context menus, NPC conversation trees                                    |\n| **Task Pipeline**       | Fluent async/sync task chains with `@Task` annotation scheduling                                       |\n| **Loot Tables**         | Weighted, pool-based loot system with conditions and `@LootTableDef` auto-registration                 |\n| **Particle System**     | Typed particle effects, geometric shapes, and animated sequences                                       |\n| **PlaceholderAPI**      | Soft-dependency integration with auto-discovered `PlaceholderProvider` components                      |\n| **Item Builders**       | Modular, typed fluent builders for every item meta variant                                             |\n| **Hot-Reload**          | Config + listener reload without restart via `@Reloadable` and `/core reload`                          |\n| **Debug Overlay**       | `/core debug` with JVM, server, DI, and custom `@Debug` entries — pasteable to pastes.dev              |\n| **Utilities**           | Scheduler, ComponentUtil, ColorUtil, TimeUtil, SoundUtil, Preconditions                                |\n\n---\n\n## Architecture\n\n```\nCorePlugin (Bootstrap)\n    │\n    ├── Container (DI)\n    │       └── Injector\n    │\n    ├── ComponentRegistry\n    │       ├── ClassScanner          ← scans JAR entries\n    │       ├── ScanResult            ← categorized class sets\n    │       └── AnnotationProcessor   ← wires into Container\n    │\n    ├── ConfigManager                 ← loads/saves AbstractConfig subclasses\n    ├── DataStoreManager              ← initializes AbstractDataStore subclasses\n    ├── CommandManager                ← registers BaseCommand subclasses\n    │       └── CooldownManager       ← @Cooldown annotation enforcement\n    ├── InventoryManager              ← tracks AbstractGui instances\n    │       └── GuiListener           ← routes Bukkit click/close events\n    ├── ScoreboardManager             ← named FastSidebar registry\n    ├── HologramManager               ← Display entity holograms\n    ├── NpcManager                    ← Citizens-free NPC system\n    ├── BossBarManager                ← per-player boss bars\n    ├── ActionbarManager              ← priority-queue action bars\n    ├── ChatInputManager              ← chat input sessions\n    ├── FormManager                   ← multi-step form sessions\n    ├── MenuManager                   ← context menu sessions\n    ├── ConversationManager           ← NPC conversation trees\n    ├── TaskManager                   ← @Task scheduling + TaskChain factory\n    ├── LootManager                   ← @LootTableDef registry + rolling\n    ├── PlaceholderManager            ← PAPI soft-dependency\n    ├── HotReloadManager              ← @Reloadable + /core reload\n    ├── DebugOverlay                  ← /core debug + pastes.dev upload\n    │       └── DebugRegistry         ← @Debug entry discovery\n    └── ModuleRegistry                ← load → enable → disable lifecycle\n```\n\nEvery subsystem is registered as a singleton in the DI container, meaning you can inject any manager directly into your\ncomponents.\n\n---\n\n## Getting Started\n\n### Adding Core as a Dependency\n\n**`build.gradle.kts`** (your plugin):\n\n```kotlin\nrepositories {\n    maven(\"https://repo.mzcy.dev/releases\") // or local\n}\n\ndependencies {\n    compileOnly(\"dev.mzcy:core:1.0.0-SNAPSHOT\")\n}\n```\n\n**`plugin.yml`**:\n\n```yaml\ndepend:\n  - Core\n```\n\nThat's it. Core handles all scanning, injection, and registration automatically.\n\n### Project Structure\n\nRecommended package layout for a plugin using Core:\n\n```\ndev.mzcy.myplugin/\n├── MyPlugin.java\n├── command/\n│   └── SpawnCommand.java        ← @Command + extends BaseCommand\n├── config/\n│   └── MainConfig.java          ← @Config + extends AbstractConfig\n├── data/\n│   └── PlayerDataStore.java     ← @DataStore + extends AbstractDataStore\n├── gui/\n│   └── MainMenuGui.java         ← @InventoryGui + extends AbstractGui\n├── listener/\n│   └── JoinListener.java        ← @Component + @Listener\n├── loot/\n│   └── ModLootTables.java       ← @Component with @LootTableDef methods\n└── service/\n    └── PlayerService.java       ← @Component\n```\n\nYour `MyPlugin.java` just needs to trigger Core's scanner:\n\n```java\npublic final class MyPlugin extends JavaPlugin {\n\n    @Override\n    public void onEnable() {\n        final CorePlugin core = CorePlugin.getInstance();\n\n        core.getComponentRegistry().scanAndProcess(\"dev.mzcy.myplugin\");\n        core.getConfigManager().initializeAll(core.getScanResult());\n        core.getDataStoreManager().initializeAll(core.getScanResult());\n        core.getCommandManager().registerAll(core.getScanResult());\n        core.getInventoryManager().initializeAll(core.getScanResult());\n    }\n}\n```\n\n---\n\n## Dependency Injection\n\n### Registering Components\n\n```java\n@Component\npublic class EconomyService {\n    public void addBalance(UUID player, double amount) { ... }\n}\n```\n\nThe scanner discovers `@Component` classes automatically. You never call `new EconomyService()`.\n\n### Injecting Dependencies\n\n```java\n@Component\npublic class ShopService {\n\n    // Field injection\n    @Inject\n    private EconomyService economyService;\n\n    // Constructor injection (preferred — makes dependencies explicit)\n    @Inject\n    public ShopService(EconomyService economyService, MainConfig config) { ... }\n\n    // Method injection (called after field injection)\n    @Inject\n    public void setConfig(MainConfig config) { ... }\n}\n```\n\n### Scopes\n\n| Annotation             | Behavior                                              |\n|------------------------|-------------------------------------------------------|\n| `@Singleton` (default) | One shared instance for the entire container lifetime |\n| `@Prototype`           | New instance created on every injection point         |\n\nGUIs are automatically `@Prototype` — each `open()` call gets a fresh instance with isolated per-player state.\n\n### Named Qualifiers\n\n```java\ncontainer.bind(DataSource.class, MySQLDataSource.class, \"mysql\", Scope.SINGLETON);\ncontainer.bind(DataSource.class, RedisDataSource.class, \"redis\", Scope.SINGLETON);\n\n@Inject @Named(\"mysql\") private DataSource primaryDatabase;\n@Inject @Named(\"redis\") private DataSource cacheDatabase;\n```\n\n### Lifecycle Callbacks\n\n```java\n@Component\npublic class ConnectionPool {\n\n    @PostConstruct  // called after all @Inject fields are resolved\n    public void init() { openConnections(); }\n\n    @PreDestroy     // called before the container destroys this instance\n    public void cleanup() { closeConnections(); }\n}\n```\n\n### Manual Binding\n\n```java\nContainer container = CorePlugin.getInstance().getContainer();\n\ncontainer.bind(PaymentGateway.class, StripeGateway.class);\ncontainer.bindInstance(MyLibrary.class, MyLibrary.create());\ncontainer.bindFactory(Report.class, PdfReport.class,\n    () -\u003e new PdfReport(new FileOutputStream(\"out.pdf\")), Scope.PROTOTYPE);\n\nEconomyService service = container.resolve(EconomyService.class);\n```\n\n---\n\n## Config Framework\n\n### Creating a Config\n\n```java\n@Config(value = \"settings\", format = ConfigFormat.YAML)\npublic class MainConfig extends AbstractConfig {\n\n    public String prefix    = \"\u003cdark_gray\u003e[\u003caqua\u003eMyPlugin\u003cdark_gray\u003e] \";\n    public boolean debug    = false;\n    public int maxHomes     = 5;\n    public List\u003cString\u003e worlds = List.of(\"world\", \"world_nether\");\n\n    public DatabaseSection database = new DatabaseSection();\n\n    public static class DatabaseSection implements java.io.Serializable {\n        public String host = \"localhost\";\n        public int    port = 3306;\n        public String name = \"myplugin\";\n    }\n\n    @Override\n    protected void onLoad() {\n        if (maxHomes \u003c 1) maxHomes = 1;\n    }\n}\n```\n\nThe file is created at `plugins/MyPlugin/settings.yml` on first load. Default values serve as fallback when the file\ndoes not exist.\n\n| Attribute      | Default     | Description                             |\n|----------------|-------------|-----------------------------------------|\n| `value`        | required    | Filename without extension              |\n| `format`       | `YAML`      | `YAML` or `JSON`                        |\n| `directory`    | `\"\"` (root) | Sub-directory within the data folder    |\n| `autoSave`     | `true`      | Save on plugin disable                  |\n| `copyDefaults` | `true`      | Copy from JAR resources if file missing |\n\n### Accessing Configs\n\n```java\n@Inject private MainConfig config;\n\n// Or via manager\nMainConfig config = CorePlugin.getInstance().getConfigManager().get(MainConfig.class);\n```\n\n### Reloading\n\n```java\nconfig.reload();\nCorePlugin.getInstance().getConfigManager().reloadAll();\n```\n\n### Formats\n\n**YAML** (default):\n\n```yaml\nprefix: '\u003cdark_gray\u003e[\u003caqua\u003eMyPlugin\u003cdark_gray\u003e] '\ndebug: false\nmaxHomes: 5\n```\n\n**JSON**:\n\n```json\n{\n  \"prefix\": \"\u003cdark_gray\u003e[\u003caqua\u003eMyPlugin\u003cdark_gray\u003e] \",\n  \"debug\": false,\n  \"maxHomes\": 5\n}\n```\n\n---\n\n## Command Framework\n\n### Root Commands\n\nExtend `BaseCommand` and annotate with `@Command`. No `plugin.yml` declaration needed:\n\n```java\n@Command(\n    name        = \"home\",\n    description = \"Manage your homes\",\n    usage       = \"/home \u003cset|delete|list|tp\u003e\",\n    permission  = \"myplugin.home\",\n    aliases     = {\"homes\", \"h\"},\n    playerOnly  = true\n)\npublic class HomeCommand extends BaseCommand {\n\n    @Inject private HomeService homeService;\n\n    @Override\n    protected void onCommand(@NotNull CommandContext ctx) {\n        ctx.send(\"\u003cyellow\u003eUsage: /home \u003cset|delete|list|tp\u003e\");\n        homeService.listHomes(ctx.playerOrThrow())\n            .forEach(name -\u003e ctx.send(\"\u003cgray\u003e  - \" + name));\n    }\n}\n```\n\n### Sub-Commands\n\n```java\n@SubCommand(value = \"set\", permission = \"myplugin.home.set\",\n            usage = \"/home set \u003cn\u003e\", minArgs = 1, playerOnly = true)\npublic void onSet(CommandContext ctx) {\n    final String name = ctx.arg(0).orElse(\"home\");\n    homeService.setHome(ctx.playerOrThrow(), name);\n    ctx.sendSuccess(\"Home \u003cwhite\u003e\" + name + \"\u003cgreen\u003e set!\");\n}\n\n@SubCommand(value = \"delete\", minArgs = 1, playerOnly = true)\npublic void onDelete(CommandContext ctx) { ... }\n\n@SubCommand(value = \"list\", playerOnly = true)\npublic void onList(CommandContext ctx) { ... }\n\n@SubCommand(value = \"tp\", usage = \"/home tp \u003cn\u003e\", minArgs = 1, playerOnly = true)\npublic void onTeleport(CommandContext ctx) { ... }\n```\n\nRouting happens automatically — `/home set beach` calls `onSet`, `/home list` calls `onList`, etc.\n\n### CommandContext API\n\n```java\nctx.isPlayer();\nctx.player();                  // Optional\u003cPlayer\u003e\nctx.playerOrThrow();           // Player — throws if not player\nctx.hasPermission(\"node\");\nctx.argCount();\nctx.arg(0);                    // Optional\u003cString\u003e\nctx.argInt(1);                 // Optional\u003cInteger\u003e\nctx.argDouble(2);              // Optional\u003cDouble\u003e\nctx.joinArgs(1);               // \"arg1 arg2 arg3\" from index 1 onward\nctx.send(\"\u003cgreen\u003eDone!\");\nctx.sendError(\"Something went wrong.\");\nctx.sendSuccess(\"Action completed.\");\nctx.sendPlain(\"No formatting here.\");\n```\n\n### Tab Completion\n\n```java\n@Override\nprotected List\u003cString\u003e onTabComplete(@NotNull CommandContext ctx) {\n    if (ctx.argCount() == 1) return homeService.listHomes(ctx.playerOrThrow());\n    return super.onTabComplete(ctx);\n}\n\n@Override\nprotected List\u003cString\u003e onSubTabComplete(@NotNull CommandContext ctx,\n                                         @NotNull SubCommandHandler handler) {\n    if (handler.token().equals(\"tp\") || handler.token().equals(\"delete\")) {\n        return homeService.listHomes(ctx.playerOrThrow());\n    }\n    return Collections.emptyList();\n}\n```\n\n### Cooldown System\n\nApply `@Cooldown` to any `@Command` class or `@SubCommand` method:\n\n```java\n@SubCommand(\"heal\")\n@Cooldown(\n    value   = 30,\n    unit    = TimeUnit.SECONDS,\n    message = \"\u003cred\u003eHeal again in \u003cbold\u003e\u003cremaining\u003e\u003c/bold\u003e.\"\n)\npublic void onHeal(CommandContext ctx) {\n    ctx.playerOrThrow().setHealth(20);\n    ctx.sendSuccess(\"Healed!\");\n}\n\n// Global cooldown — shared across all players\n@SubCommand(\"daily\")\n@Cooldown(value = 1, unit = TimeUnit.DAYS, global = true)\npublic void onDaily(CommandContext ctx) { ... }\n\n// Custom bypass permission\n@SubCommand(\"other\")\n@Cooldown(value = 5, bypassPermission = \"myplugin.admin\")\npublic void onOther(CommandContext ctx) { ... }\n```\n\n| Placeholder   | Description                   |\n|---------------|-------------------------------|\n| `\u003cremaining\u003e` | Human-readable time remaining |\n| `\u003ctotal\u003e`     | Total cooldown duration       |\n\nPlayers with `core.cooldown.bypass` skip all cooldowns automatically. Manual API:\n\n```java\nCooldownManager cooldowns = CorePlugin.getInstance().getCooldownManager();\ncooldowns.apply(player, \"my_action\", Duration.ofSeconds(30));\ncooldowns.clear(player, \"my_action\");\ncooldowns.getRemaining(player, \"my_action\");\ncooldowns.isOnCooldown(player, \"my_action\");\n```\n\n---\n\n## Inventory Framework\n\n### Creating a GUI\n\n```java\n@InventoryGui(id = \"main_menu\", title = \"\u003cdark_gray\u003e✦ Main Menu ✦\", rows = 3)\npublic class MainMenuGui extends AbstractGui {\n\n    @Inject private HomeService homeService;\n\n    @Override\n    protected void build(@NotNull GuiBuilder builder) {\n        builder\n            .border(Material.GRAY_STAINED_GLASS_PANE)\n            .slot(13,\n                ItemBuilder.of(Material.NETHER_STAR)\n                    .name(\"\u003cgold\u003eMy Homes\")\n                    .lore(\"\u003cgray\u003eClick to manage homes\")\n                    .build(),\n                event -\u003e {\n                    getViewer().closeInventory();\n                    CorePlugin.getInstance().getInventoryManager()\n                        .open(\"homes_gui\", (Player) event.getWhoClicked());\n                }\n            )\n            .slot(22,\n                ItemBuilder.of(Material.BARRIER).name(\"\u003cred\u003eClose\").build(),\n                event -\u003e event.getWhoClicked().closeInventory()\n            );\n    }\n\n    @Override protected void onOpen(@NotNull Player player) { ... }\n    @Override protected void onClose(@NotNull Player player) { ... }\n}\n```\n\n### Opening a GUI\n\n```java\nCorePlugin.getInstance().getInventoryManager().open(\"main_menu\", player);\n\nMainMenuGui gui = CorePlugin.getInstance().getInventoryManager()\n    .open(MainMenuGui.class, player);\n```\n\n### Refreshing a GUI\n\n```java\nCorePlugin.getInstance().getInventoryManager()\n    .findGui(player.getOpenInventory().getTopInventory())\n    .ifPresent(AbstractGui::refresh);\n```\n\n### GuiBuilder API\n\n```java\nbuilder\n    .slot(index, item, clickAction)          // interactive\n    .slot(index, item)                        // decorative\n    .slotRange(0, 8, fillerItem)              // fill range\n    .fill(Material.BLACK_STAINED_GLASS_PANE)  // fill empty slots\n    .fill(customItem)\n    .border(Material.GRAY_STAINED_GLASS_PANE) // draw border\n    .clear(index);                            // remove slot\n```\n\n### Paged GUI\n\nExtend `PagedGui` for automatic pagination:\n\n```java\n@InventoryGui(id = \"home_list\", title = \"\u003cgold\u003eYour Homes\", rows = 6)\npublic class HomeListGui extends PagedGui {\n\n    @Inject private HomeService homeService;\n\n    @Override\n    protected List\u003cPagedItem\u003e getItems() {\n        return homeService.getHomes(getViewer().getUniqueId()).stream()\n            .map(home -\u003e PagedItem.of(\n                ItemBuilder.of(Material.RED_BED).name(\"\u003cgold\u003e\" + home.getName()).build(),\n                event -\u003e homeService.teleport((Player) event.getWhoClicked(), home)\n            ))\n            .toList();\n    }\n\n    // Optional overrides\n    @Override protected List\u003cInteger\u003e getContentSlots() { ... }\n    @Override protected void decorateBackground(@NotNull GuiBuilder builder) {\n        builder.border(Material.BLACK_STAINED_GLASS_PANE);\n    }\n    @Override protected ItemStack buildPreviousButton(@NotNull PageContext ctx) { ... }\n    @Override protected ItemStack buildNextButton(@NotNull PageContext ctx) { ... }\n    @Override protected ItemStack buildPageIndicator(@NotNull PageContext ctx) { ... }\n}\n```\n\nFor searchable lists, extend `SearchablePagedGui` and implement `getAllItems()` instead of `getItems()`:\n\n```java\n@InventoryGui(id = \"player_list\", title = \"\u003caqua\u003ePlayers\", rows = 6)\npublic class PlayerListGui extends SearchablePagedGui {\n\n    @Override\n    protected List\u003cPagedItem\u003e getAllItems() { ... }\n\n    @Override\n    protected void buildControls(@NotNull GuiBuilder builder, @NotNull PageContext ctx) {\n        super.buildControls(builder, ctx); // prev / indicator / next\n        builder.slot(47, buildSearchButton(), event -\u003e\n            chatInput.builder(getViewer()).prompt(\"\u003cgold\u003eSearch:\").timeout(20)\n                .request().thenAccept(r -\u003e { if (r.isCompleted()) applyFilter(r.getValue()); })\n        );\n        if (hasActiveFilter()) {\n            builder.slot(48, buildClearFilterButton(), event -\u003e clearFilter());\n        }\n    }\n}\n```\n\nNavigation methods: `nextPage()`, `previousPage()`, `goToPage(n)`, `firstPage()`, `lastPage()`.\n\n---\n\n## Data Store\n\n### Defining a Store\n\n```java\n@DataStore(value = \"playerdata\", directory = \"data\")\npublic class PlayerDataStore extends AbstractDataStore\u003cUUID, PlayerData\u003e {\n\n    public PlayerDataStore() { super(new BinaryDataSerializer\u003c\u003e()); }\n\n    @Override protected String keyToFileName(@NotNull UUID key) { return key.toString(); }\n    @Override protected UUID fileNameToKey(@NotNull String f)   { return UUID.fromString(f); }\n}\n```\n\nData is stored as `plugins/MyPlugin/data/playerdata/\u003cuuid\u003e.dat` — binary, XOR-obfuscated, not human-readable.\n\n### CRUD Operations\n\n```java\nstore.put(uuid, data);\nstore.get(uuid);              // Optional\u003cPlayerData\u003e\nstore.remove(uuid);           // returns true if removed\nstore.getAll();               // Map\u003cUUID, PlayerData\u003e\nstore.contains(uuid);\nstore.size();\n```\n\n### TTL / Expiry\n\n```java\nstore.put(uuid, sessionData, Instant.now().plus(Duration.ofHours(24)));\n\nstore.getEntry(uuid).ifPresent(entry -\u003e {\n    System.out.println(\"Created: \" + entry.getCreatedAt());\n    System.out.println(\"Expires: \" + entry.getExpiresAt());\n    System.out.println(\"Expired: \" + entry.isExpired());\n});\n```\n\n### Custom Key Types\n\n```java\n@DataStore(\"factiondata\")\npublic class FactionDataStore extends AbstractDataStore\u003cString, FactionData\u003e {\n\n    public FactionDataStore() { super(new BinaryDataSerializer\u003c\u003e()); }\n\n    @Override protected String keyToFileName(@NotNull String key) {\n        return key.toLowerCase().replaceAll(\"[^a-z0-9_]\", \"_\");\n    }\n    @Override protected String fileNameToKey(@NotNull String fileName) { return fileName; }\n}\n```\n\n---\n\n## Event Listeners\n\n```java\n@Component\n@Listener\npublic class PlayerJoinListener implements Listener {\n\n    @Inject private PlayerService playerService;\n    @Inject private MainConfig    config;\n\n    @EventHandler(priority = EventPriority.NORMAL)\n    public void onJoin(PlayerJoinEvent event) {\n        final Player player = event.getPlayer();\n        event.joinMessage(ComponentUtil.parse(\n            config.prefix + \"\u003cgreen\u003e\" + player.getName() + \" joined the game.\"));\n        playerService.loadOrCreate(player.getUniqueId(), player.getName());\n    }\n\n    @EventHandler\n    public void onQuit(PlayerQuitEvent event) {\n        playerService.savePlayer(event.getPlayer());\n    }\n}\n```\n\nThe framework automatically calls `Bukkit.getPluginManager().registerEvents(...)` — no manual registration needed.\n\n---\n\n## Module System\n\n### Creating a Module\n\n```java\npublic class EconomyModule extends AbstractCoreModule {\n\n    private final Container container;\n\n    public EconomyModule(Container container) {\n        super(\"Economy\");\n        this.container = container;\n    }\n\n    @Override\n    protected void onLoad() {\n        container.bind(PaymentGateway.class, LocalPaymentGateway.class);\n        container.bind(EconomyService.class);\n    }\n\n    @Override\n    protected void onEnable() {\n        SchedulerUtil.repeatAsync(plugin, this::processQueue,\n            SchedulerUtil.seconds(5), SchedulerUtil.seconds(5));\n    }\n\n    @Override\n    protected void onDisable() {\n        container.resolve(TransactionLog.class).flush();\n    }\n}\n```\n\n### Registering Modules\n\n```java\ncore.getModuleRegistry().register(new EconomyModule(core.getContainer()));\n```\n\n### Lifecycle Order\n\n```\nRegistration → loadAll() → enableAll() → [runtime] → disableAll() (reverse order)\n```\n\n---\n\n## Item Builders\n\nAll builders use the **CRTP pattern** — every method returns the most specific builder type, so you never lose the\nsub-type while chaining.\n\n### ItemBuilder\n\n```java\nItemStack sword = ItemBuilder.of(Material.DIAMOND_SWORD)\n    .name(\"\u003cgradient:#FF6B6B:#FFE66D\u003e⚔ Excalibur\u003c/gradient\u003e\")\n    .lore(\"\u003cgray\u003eA blade of legend.\", \"\u003cred\u003e❤ +10 Attack Damage\")\n    .enchant(Enchantment.SHARPNESS, 5)\n    .enchant(Enchantment.UNBREAKING, 3)\n    .unbreakable(true)\n    .hideAllFlags()\n    .build();\n\nItemStack filler    = ItemBuilder.filler();\nItemStack redFiller = ItemBuilder.filler(Material.RED_STAINED_GLASS_PANE);\n```\n\n### SkullBuilder\n\n```java\nSkullBuilder.of().owner(player.getUniqueId()).name(\"\u003cyellow\u003eHead\").build();\nSkullBuilder.of().textureBase64(\"eyJ0ZXh0dXJlcyI6...\").build();\nSkullBuilder.of().textureUrl(\"https://textures.minecraft.net/texture/...\").build();\n```\n\n### LeatherArmorBuilder\n\n```java\nLeatherArmorBuilder.of(Material.LEATHER_CHESTPLATE).colorHex(\"#C0392B\").build();\nLeatherArmorBuilder.of(Material.LEATHER_BOOTS).color(0, 150, 255).build();\nLeatherArmorBuilder.of(Material.LEATHER_HELMET).color(Color.GREEN).build();\n```\n\n### BookBuilder\n\n```java\nBookBuilder.written()\n    .title(\"\u003cgold\u003eCore Manual\").author(\"\u003cgray\u003emzcy\")\n    .page(\"\u003cyellow\u003e\u003cbold\u003eWelcome!\\n\\n\u003creset\u003e\u003cgray\u003ePage one content.\")\n    .page(\"\u003caqua\u003eChapter 1: Getting Started\")\n    .build();\n\nBookBuilder.writable().name(\"\u003cgray\u003eEmpty Journal\").build();\n```\n\n### FireworkBuilder\n\n```java\nFireworkBuilder.of()\n    .power(2)\n    .effect(FireworkBuilder.FireworkEffectBuilder.create()\n        .type(FireworkEffect.Type.STAR)\n        .color(Color.AQUA, Color.WHITE)\n        .fadeColor(Color.BLUE)\n        .trail(true).flicker(true).build())\n    .build();\n\nFireworkBuilder.of().power(3)\n    .ballEffect(Color.RED, Color.ORANGE)\n    .starEffect(Color.YELLOW, Color.WHITE)\n    .build();\n```\n\n---\n\n## Display Systems\n\n### Title \u0026 Actionbar Manager\n\n```java\n// Titles — fluent builder\nTitleBuilder.create()\n    .title(\"\u003cgold\u003e\u003cbold\u003eRound Over!\")\n    .subtitle(\"\u003cgray\u003eYou placed \u003cwhite\u003e3rd\")\n    .fadeIn(Duration.ofMillis(500))\n    .stay(Duration.ofSeconds(3))\n    .fadeOut(Duration.ofMillis(500))\n    .send(player);\n\n// Static shortcuts\nTitleBuilder.send(player, \"\u003cgold\u003eHello!\", \"\u003cgray\u003eWelcome back\");\nTitleBuilder.sendTitle(player, \"\u003cgold\u003eHello!\");\nTitleBuilder.clear(player);\n\n// Actionbar — priority queue prevents mutual overwriting\nactionbarManager.set(player, \"coords\",\n    \"\u003cgray\u003eX: \u003cwhite\u003e\" + loc.getBlockX(), 0);  // priority 0 = background\n\nactionbarManager.setDynamic(player, \"coords\",\n    () -\u003e \"\u003cgray\u003eX: \u003cwhite\u003e\" + player.getLocation().getBlockX(), 0);\n\nactionbarManager.sendTemporarySeconds(player, \"\u003cgreen\u003e✔ Saved!\", 3, 10);\nactionbarManager.clear(player, \"coords\");\nactionbarManager.clearAll(player);\n```\n\nHigher priority messages are shown first. When a temporary message expires, the next highest takes over automatically.\n\n### Boss Bar Manager\n\n```java\n// Countdown bar — progress automatically decreases to 0\nbossBarManager.builder(player, \"respawn\")\n    .title(\"\u003cred\u003eRespawning in...\")\n    .color(BossBar.Color.RED)\n    .overlay(BossBar.Overlay.NOTCHED_10)\n    .countdown(Duration.ofSeconds(5))\n    .show();\n\n// Dynamic bar with live updates\nbossBarManager.builder(player, \"combat\")\n    .dynamicTitle(() -\u003e \"\u003cred\u003e⚔ Combat: \u003cwhite\u003e\" + remaining + \"s\")\n    .dynamicProgress(() -\u003e combatService.getRemainingFraction(player))\n    .color(BossBar.Color.RED)\n    .duration(Duration.ofSeconds(30))\n    .show();\n\n// Permanent notification\nbossBarManager.builder(player, \"event\")\n    .title(\"\u003cgold\u003e⚡ Double XP Weekend Active!\")\n    .color(BossBar.Color.YELLOW)\n    .progress(1.0f)\n    .show();\n\nbossBarManager.hide(player, \"combat\");\nbossBarManager.hideAll(player);\nbossBarManager.has(player, \"combat\");\n```\n\n### Scoreboard Framework\n\nPer-player sidebars with dynamic lines, dirty-check updates (no flicker), auto-show on join:\n\n```java\nscoreboardManager.register(\"main\",\n    SidebarBuilder.create(\"\u003cgradient:#FFD700:#FFA500\u003e\u003cbold\u003eMyServer\u003c/gradient\u003e\")\n        .line(\"\u003cdark_gray\u003e━━━━━━━━━━━━━━━\")\n        .blank()\n        .dynamic(() -\u003e \"\u003cgray\u003ePlayers\u003cdark_gray\u003e: \u003cwhite\u003e\"\n            + Bukkit.getOnlinePlayers().size())\n        .dynamic(() -\u003e \"\u003cgray\u003eTPS\u003cdark_gray\u003e: \u003cwhite\u003e\"\n            + String.format(\"%.1f\", Math.min(20.0, Bukkit.getTPS()[0])))\n        .blank()\n        .line(\"\u003cgray\u003eplay.myserver.net\")\n        .build(plugin)\n);\n\nscoreboardManager.setDefault(\"main\");      // auto-shown to all players on join\nscoreboardManager.startUpdating(\"main\", 20L);\nscoreboardManager.show(player, \"vip\");     // switch sidebar for one player\nscoreboardManager.hide(player);\n\n// Dynamic title\nscoreboardManager.getSidebar(\"main\")\n    .ifPresent(s -\u003e s.setTitle(\"\u003cred\u003eServer Restarting!\"));\n\n// Update a specific line\nscoreboardManager.getSidebar(\"main\")\n    .ifPresent(s -\u003e s.setLine(2, \"\u003cyellow\u003ePlayers: \" + count));\n```\n\n### Hologram Framework\n\nUses modern **Display entities** (1.19.4+) — TextDisplay, ItemDisplay, BlockDisplay. No ArmorStands:\n\n```java\n// Text + item mixed hologram\nhologramManager.builder(\"shop_sign\", location)\n    .item(new ItemStack(Material.DIAMOND),\n        ItemDisplay.ItemDisplayTransform.GROUND, 1.2f)\n    .text(\"\u003caqua\u003e\u003cbold\u003eDiamond Shop\")\n    .text(\"\u003cgray\u003eRight-click the NPC to browse\")\n    .dynamicText(() -\u003e \"\u003cyellow\u003eStock: \u003cwhite\u003e\" + shop.getStock())\n    .lineSpacing(0.08)\n    .spawn();\n\n// Block display hologram\nhologramManager.builder(\"beacon_holo\", location)\n    .block(Material.BEACON.createBlockData(), 0.6f, 45f)\n    .text(\"\u003clight_purple\u003eBeacon Active\")\n    .dynamicText(() -\u003e \"\u003cgray\u003eRange: \u003cwhite\u003e\" + beaconService.getRange() + \" blocks\")\n    .spawn();\n\n// Dynamic leaderboard\nhologramManager.builder(\"kills_lb\", location)\n    .text(\"\u003cred\u003e\u003cbold\u003e⚔ Kill Leaderboard\")\n    .text(\"\u003cdark_gray\u003e──────────────\")\n    .dynamicText(() -\u003e \"\u003cgold\u003e#1 \u003cwhite\u003e\" + lb.getTop(1).getName()\n        + \" \u003cgray\u003e— \u003cred\u003e\" + lb.getTop(1).getKills())\n    .dynamicText(() -\u003e \"\u003cgray\u003e#2 \u003cwhite\u003e\" + lb.getTop(2).getName())\n    .spawn();\n\n// Mutate after spawn\nhologramManager.get(\"shop_sign\")\n    .flatMap(h -\u003e h.getTextLine(2))\n    .ifPresent(line -\u003e line.setText(\"\u003cgreen\u003eOpen Now\"));\n\nhologramManager.setUpdateInterval(10L); // update every 0.5s\nhologramManager.remove(\"shop_sign\");\nhologramManager.removeAll();\n```\n\n---\n\n## NPC Framework\n\nCitizens-free NPC system using ArmorStand proxies with Display entity hologram labels:\n\n```java\nnpcManager.builder(\"shop_keeper\")\n    .name(\"\u003cgold\u003e\u003cbold\u003eShop Keeper\")\n    .location(new Location(world, 0.5, 64, 0.5))\n    .texture(TEXTURE_VALUE, TEXTURE_SIGNATURE)\n    .hologram(\"\u003cgold\u003e\u003cbold\u003eShop Keeper\", \"\u003cgray\u003eRight-click to browse\")\n    .lookAtPlayer(true)\n    .lookAtDistance(6.0)\n    .viewDistance(48)\n    .glowing(false)\n    .collidable(false)\n    .onClick((player, npc, type) -\u003e {\n        if (type == NpcClickType.RIGHT_CLICK) {\n            inventoryManager.open(\"shop_gui\", player);\n        }\n    })\n    .spawn();\n\n// Dynamic hologram update\nnpcManager.get(\"shop_keeper\").ifPresent(npc -\u003e\n    npc.updateHologram(List.of(\n        \"\u003cgold\u003e\u003cbold\u003eShop Keeper\",\n        \"\u003cgray\u003eItems: \u003cwhite\u003e\" + shop.getItemCount(),\n        shop.isOpen() ? \"\u003cgreen\u003eOpen\" : \"\u003cred\u003eClosed\"\n    ))\n);\n\nnpcManager.despawn(\"shop_keeper\");\nnpcManager.despawnAll();\nnpcManager.count();\n```\n\n---\n\n## Interaction Systems\n\n### Chat Input Handler\n\n```java\nchatInputManager.builder(player)\n    .prompt(\"\u003cgold\u003eEnter your home name:\")\n    .cancelKeyword(\"cancel\")\n    .cancelMessage(\"\u003cgray\u003eCancelled.\")\n    .timeoutMessage(\"\u003cred\u003eTimed out.\")\n    .timeout(Duration.ofSeconds(30))\n    .closeInventory(true)\n    .validator(\n        InputValidator.Validators.alphanumeric()\n            .and(InputValidator.Validators.maxLength(16))\n    )\n    .request()\n    .thenAccept(result -\u003e {\n        switch (result.getStatus()) {\n            case COMPLETED    -\u003e homeService.createHome(player, result.getValue());\n            case CANCELLED    -\u003e player.sendMessage(\"\u003cgray\u003eCancelled.\");\n            case TIMED_OUT    -\u003e player.sendMessage(\"\u003cred\u003eToo slow!\");\n            case DISCONNECTED -\u003e log.info(player.getName() + \" disconnected.\");\n        }\n    });\n```\n\nBuilt-in validators: `notBlank()`, `maxLength(n)`, `minLength(n)`, `alphanumeric()`, `integer()`,\n`integerInRange(min, max)`, `positiveDecimal()`, `matches(regex, msg)`, `onlinePlayer()`. Combine with `.and()`.\n\n### Form System\n\nMulti-step sequential input forms:\n\n```java\nForm form = Form.builder(\"create_home\")\n    .title(\"\u003cgold\u003eCreate Home\")\n    .field(FormField.builder(\"name\")\n        .prompt(\"\u003cgold\u003eHome name:\")\n        .placeholder(\"e.g. base, farm\")\n        .validator(InputValidator.Validators.alphanumeric()\n            .and(InputValidator.Validators.maxLength(16)))\n        .build()\n    )\n    .field(FormField.builder(\"icon\")\n        .prompt(\"\u003cgold\u003eIcon material:\")\n        .required(false)\n        .defaultValue(\"RED_BED\")\n        .build()\n    )\n    .field(FormField.builder(\"confirm\")\n        .prompt(\"\u003cyellow\u003eConfirm? (yes/no)\")\n        .inputType(FormField.InputType.CONFIRM)\n        .build()\n    )\n    .onSubmit(response -\u003e {\n        if (response.isConfirmed(\"confirm\")) {\n            homeService.createHome(player,\n                response.get(\"name\"), response.get(\"icon\"));\n        }\n    })\n    .onCancel(r -\u003e player.sendMessage(\"\u003cgray\u003eCancelled at: \" + r.getCancelledAtField()))\n    .build();\n\nformManager.register(form);\nformManager.open(\"create_home\", player);\n\n// Inline (no registration needed)\nformManager.open(form, player).thenAccept(response -\u003e { ... });\n```\n\n`FormResponse` methods: `get(key)`, `getInt(key)`, `getDouble(key)`, `isConfirmed(key)`, `isSubmitted()`,\n`isCancelled()`, `getCancelledAtField()`.\n\n### Menu System\n\nLightweight chat-based context menus — no inventory needed. Players click text or type a number:\n\n```java\nContextMenu.builder(\"home_options\")\n    .title(\"\u003cgold\u003eHome Options — \" + home.getName())\n    .item(MenuItem.of(\"\u003cgreen\u003e⚡ Teleport\",\n        List.of(\"\u003cgray\u003eTeleport to this home\"),\n        (p, m) -\u003e homeService.teleport(p, home)))\n    .item(MenuItem.of(\"\u003cyellow\u003e✏ Rename\",\n        (p, m) -\u003e formManager.open(\"rename_home\", p)))\n    .item(MenuItem.separator())\n    .item(MenuItem.of(\"\u003cred\u003e✗ Delete\",\n        List.of(\"\u003cgray\u003eThis cannot be undone\"),\n        (p, m) -\u003e confirmAndDelete(p, home)))\n    .item(MenuItem.submenu(\"\u003caqua\u003eMore Options\",\n        buildMoreMenu(player)))\n    .timeout(30L)\n    .build()\n    .open(player);\n```\n\nOutput in chat:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nHome Options — base\n▸ [1] ⚡ Teleport\n  │ Teleport to this home\n▸ [2] ✏ Rename\n──────────────\n▸ [3] ✗ Delete\n  │ This cannot be undone\n▸ [4] More Options ▶\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nClick an option or type its number.\n```\n\n`MenuItem` types: `ACTION`, `SEPARATOR`, `DISABLED`, `SUBMENU`.\n\n### Conversation System\n\nBranching NPC dialogue trees with conditions, actions, and player choice:\n\n```java\nConversationTree tree = ConversationTree.builder(\"guard_intro\")\n    .npcName(\"\u003cgold\u003e⚔ Guard\")\n    .startNode(\"greeting\")\n    .dialogue(\"greeting\", List.of(\n        \"\u003cgold\u003e[Guard]\u003c/gold\u003e \u003cwhite\u003eHalt! State your business.\"\n    ), \"check_rank\")\n    .condition(\"check_rank\",\n        (player, ctx) -\u003e rankService.hasRank(player, \"citizen\"),\n        \"citizen_path\", \"stranger_path\"\n    )\n    .dialogue(\"citizen_path\", List.of(\n        \"\u003cgold\u003e[Guard]\u003c/gold\u003e \u003cwhite\u003eWelcome home, citizen.\"\n    ), \"choice\")\n    .choice(\"choice\", null, List.of(\n        ConversationChoice.of(\"quest\", \"Any work available?\", \"offer_quest\"),\n        ConversationChoice.of(\"bye\",   \"Farewell.\",           \"end_friendly\")\n    ))\n    .action(\"offer_quest\",\n        (p, ctx) -\u003e {\n            questService.assign(p, \"wolf_hunt\");\n            ctx.setFlag(\"quest_accepted\");\n        },\n        \"quest_end\"\n    )\n    .end(\"quest_end\",    \"\u003cgold\u003e[Guard]\u003c/gold\u003e \u003cgreen\u003eGood luck.\")\n    .end(\"end_friendly\", \"\u003cgold\u003e[Guard]\u003c/gold\u003e \u003cwhite\u003eSafe travels.\")\n    .end(\"stranger_path\",\"\u003cgold\u003e[Guard]\u003c/gold\u003e \u003cred\u003eMove along.\")\n    .build();\n\nconversationManager.register(tree);\n\n// Start from NPC click\nconversationManager.start(\"guard_intro\", player)\n    .thenAccept(ctx -\u003e {\n        if (ctx.hasFlag(\"quest_accepted\")) {\n            player.sendMessage(\"\u003cgreen\u003eQuest added to your journal!\");\n        }\n    });\n```\n\nNode types: `DIALOGUE`, `CHOICE`, `ACTION`, `CONDITION`, `END`. Choices support `visibleIf` conditions.\n`ConversationContext` provides key-value data store and flag system between nodes.\n\n---\n\n## Task Pipeline\n\nFluent async/sync task chains with automatic thread-switching mid-chain:\n\n```java\ntaskManager.chain()\n    .asyncSupply(() -\u003e database.loadPlayerData(uuid))    // async DB call\n    .syncConsume((ctx, data) -\u003e {                         // → main thread\n        ctx.set(\"data\", data);\n        player.sendMessage(\"\u003cgreen\u003eProfile loading...\");\n    })\n    .async((ctx, data) -\u003e enrichData(data))               // → async thread\n    .delay(40L)                                           // wait 2 seconds (sync)\n    .syncConsume((ctx, data) -\u003e openMainMenu(player))     // → main thread\n    .onError(ex -\u003e player.sendMessage(\"\u003cred\u003eError: \" + ex.getMessage()))\n    .onComplete(ctx -\u003e log.info(\"Chain completed.\"))\n    .onCancel(ctx -\u003e log.info(\"Chain cancelled.\"))\n    .execute();                                           // returns CompletableFuture\u003cTaskContext\u003e\n```\n\nGuard clauses mid-chain:\n\n```java\n.sync((ctx, balance) -\u003e {\n    if (balance \u003c price) { ctx.cancel(); }\n    return balance;\n})\n.onCancel(ctx -\u003e player.sendMessage(\"\u003cred\u003eNot enough money.\"))\n```\n\nScheduled tasks via `@Task` — auto-discovered on boot:\n\n```java\n@Component\npublic class BackupService {\n\n    @Task(name = \"DB Backup\", async = true, repeat = true,\n          delay = 6000L, period = 72000L)\n    public void runBackup() { database.backup(); }\n\n    @Task(name = \"Startup Check\", async = true, delay = 40L)\n    public void startupCheck() { database.verifySchema(); }\n}\n```\n\nManual scheduling:\n\n```java\ntaskManager.schedule(\"leaderboard_update\", true, 0L, 200L,\n    leaderboardService::rebuild);\ntaskManager.cancelTask(\"leaderboard_update\");\ntaskManager.cancelAll();\n```\n\n---\n\n## Loot Table System\n\nWeighted pool-based drop system with conditions and `@LootTableDef` auto-registration:\n\n```java\n@Component\npublic class ModLootTables {\n\n    @LootTableDef\n    public LootTable zombieDrops() {\n        return LootTable.builder(\"zombie_drops\")\n            .pool(LootPool.builder(\"common\")\n                .rolls(1, 3)\n                .bonusRollsPerLooting(1)\n                .entry(LootEntry.of(new ItemStack(Material.ROTTEN_FLESH), 80, 1, 4))\n                .entry(LootEntry.of(new ItemStack(Material.BONE), 30, 1, 2))\n                .entry(LootEntry.empty(20))\n                .build()\n            )\n            .pool(LootPool.builder(\"rare\")\n                .rolls(1)\n                .condition(LootCondition.LootConditions.chance(0.025))\n                .entry(LootEntry.of(new ItemStack(Material.IRON_INGOT), 100))\n                .build()\n            )\n            .build();\n    }\n\n    @LootTableDef\n    public LootTable bossDrops() {\n        return LootTable.builder(\"boss_drops\")\n            .pool(LootPool.builder(\"guaranteed\")\n                .rolls(1)\n                .entry(LootEntry.of(\n                    ItemBuilder.of(Material.NETHER_STAR).name(\"\u003clight_purple\u003eBoss Soul\").build(),\n                    100))\n                .build()\n            )\n            .pool(LootPool.builder(\"bonus\")\n                .rolls(2, 4).bonusRollsPerLooting(1)\n                .entry(LootEntry.of(new ItemStack(Material.DIAMOND), 40, 1, 3))\n                .entry(LootEntry.dynamic(() -\u003e enchantRandomly(new ItemStack(Material.IRON_SWORD)), 20))\n                .entry(LootEntry.empty(10))\n                .build()\n            )\n            .build();\n    }\n}\n```\n\nRolling API:\n\n```java\n// Roll for a player (auto-detects looting enchantment level)\nlootManager.rollForPlayer(\"zombie_drops\", player);\n\n// Roll and drop items at a world location\nlootManager.rollAndDrop(\"boss_drops\", player, entity.getLocation());\n\n// Give directly to player's inventory (overflow drops at feet)\nlootManager.rollAndGive(\"daily_reward\", player);\n\n// Custom context with luck modifier\nLootContext ctx = LootContext.builder()\n    .player(player).lootingLevel(3).luck(0.5).build();\nList\u003cItemStack\u003e items = lootManager.roll(\"boss_drops\", ctx);\n```\n\nBuilt-in conditions: `chance(p)`, `minLevel(n)`, `hasPermission(node)`, `hasFlag(flag)`, `isDaytime()`,\n`hasEnchantment(ench)`, `lootingLevel(n)`. All support `.and()`, `.or()`, `.negate()`.\n\n---\n\n## Particle System\n\n```java\n// One-shot static effects\nParticleUtil.burst(location, Particle.EXPLOSION, 10);\nParticleUtil.ring(location, 2.0, 32, Particle.END_ROD);\nParticleUtil.dust(location, Color.fromRGB(255, 87, 51), 1.5f, 8);\nParticleUtil.sphere(location, 3.0, 60, Particle.END_ROD);\nParticleUtil.line(from, to, 20, Particle.FLAME);\n\n// Typed effects with generics\nParticleEffect.dust(Color.AQUA, 1.5f).spawn(location);\nParticleEffect.dustTransition(Color.RED, Color.BLUE, 2.0f).spawn(location);\nParticleEffect.block(Material.DIAMOND_BLOCK).withCount(15).spawn(location);\nParticleEffect.of(Particle.HEART, 3).withOffset(0.3).spawn(player, location);\n\n// Animated effects\nParticleAnimator helix = ParticleUtil.helix(plugin, location, Particle.END_ROD, 100L);\nParticleAnimator pulse = ParticleUtil.pulse(plugin, location, Particle.TOTEM_OF_UNDYING, 40L);\n\n// Custom animator — full control\nnew ParticleAnimator(plugin)\n    .interval(1L).loop(true)\n    .step(ParticleAnimation.of(\n        ParticleEffect.dust(Color.AQUA, 1.5f),\n        () -\u003e ParticleShape.star(center, 2.0, 1.0, 6)\n    ))\n    .step(ParticleAnimation.of(\n        ParticleEffect.dust(Color.WHITE, 1.0f),\n        () -\u003e ParticleShape.circle(center, 2.5, 24)\n    ))\n    .onComplete(a -\u003e player.sendMessage(\"Done!\"))\n    .start();\n```\n\nAvailable shapes: `circle`, `disk`, `line`, `sphere` (Fibonacci lattice), `cylinder`, `helix`, `star`, `wave`.\n\n---\n\n## PlaceholderAPI Integration\n\nSoft-dependency — gracefully no-ops if PAPI is not installed. Add to `plugin.yml`:\n\n```yaml\nsoftdepend:\n  - PlaceholderAPI\n```\n\n```java\n// Option A — implement PlaceholderProvider on any @Component (auto-discovered)\n@Component\npublic class EconomyPlaceholders implements PlaceholderProvider {\n\n    @Inject private EconomyService economy;\n\n    @Override\n    public void registerPlaceholders(@NotNull PlaceholderRegistry registry) {\n        registry\n            .register(\"balance\", player -\u003e\n                String.valueOf(economy.getBalance(player.getUniqueId())))\n            .registerGlobal(\"total_players\", () -\u003e\n                String.valueOf(Bukkit.getOnlinePlayers().size()))\n            .registerStatic(\"currency_symbol\", \"€\")\n            .registerOnline(\"player_name\", player -\u003e player.getName());\n    }\n}\n\n// Option B — manual registration\nCorePlugin.getInstance().getPlaceholderManager()\n    .getRegistry().register(\"my_key\", player -\u003e \"hello!\");\n\n// Resolve PAPI placeholders in a string\nString msg = placeholderManager.setPlaceholders(player, \"Balance: %core_balance%\");\n\n// Resolve PAPI + parse MiniMessage in one call\nComponent comp = placeholderManager.parseWithPlaceholders(\n    player, \"\u003cgold\u003eBalance: \u003cwhite\u003e%core_balance%\");\n```\n\nBuilt-in placeholders: `%core_version%`, `%core_online%`, `%core_max_players%`, `%core_tps%`, `%core_uptime%`,\n`%core_player_name%`, `%core_player_uuid%`, `%core_player_online%`.\n\nRegistration styles: `register()` (player-aware), `registerGlobal()` (same for all), `registerStatic()` (fixed value),\n`registerOnline()` (null-safe player).\n\n---\n\n## Update Checker\n\nChecks `https://github.com/mzcydev/paper-core` releases via the GitHub API. All network I/O is async.\n\n```java\n// One-liner — async, logs to console automatically\nnew UpdateChecker(this).checkAsync();\n\n// With callback (runs on main thread)\nnew UpdateChecker(this).checkAsync(result -\u003e {\n    if (result.isUpdateAvailable()) {\n        log.warning(\"Update available: \" + result.getLatestVersion());\n        log.warning(\"Download: \" + result.getReleaseUrl());\n        // UpdateNotifier auto-notifies ops on join when registered\n    }\n});\n\n// Sync (blocks calling thread — use only off main thread)\nUpdateResult result = new UpdateChecker(this).checkSync();\n```\n\nStatuses: `UPDATE_AVAILABLE`, `UP_TO_DATE`, `DEV_BUILD`, `FAILED`. Full SemVer comparison: `1.2.3`, `1.2.3-SNAPSHOT`,\n`1.2.3-beta.1`, `1.2.3-rc.1`, leading `v` stripped automatically.\n\n---\n\n## Hot-Reload\n\nReload configs and listeners without restarting the server. Triggered via `/core reload`.\n\n```java\n// Annotate any @Component method — auto-discovered\n@Component\npublic class ShopService {\n\n    @Inject private ShopConfig config;\n\n    @Reloadable(name = \"Shop Service\", order = 20)\n    public void reload() {\n        config.reload();\n        rebuildCache();\n    }\n}\n\n// Register a manual step\nhotReloadManager.addStep(\"Economy Cache\", () -\u003e {\n    economyService.flushCache();\n    economyService.warmCache();\n});\n\n// Trigger programmatically\nReloadResult result = hotReloadManager.reload(sender);\nswitch (result.getStatus()) {\n    case SUCCESS        -\u003e sender.sendMessage(\"All \" + result.totalSteps() + \" steps succeeded.\");\n    case PARTIAL        -\u003e result.getFailedSteps().forEach(s -\u003e log.warn(\"Failed: \" + s));\n    case ALREADY_RUNNING -\u003e sender.sendMessage(\"A reload is already in progress.\");\n}\n```\n\nReload phases (in order):\n\n1. Unregister all managed Bukkit listeners\n2. Execute built-in steps (`Configs.reloadAll()`, listener re-registration)\n3. Execute registered `ReloadStep`s\n4. Execute `@Reloadable` methods sorted by `order`\n5. Re-register all listeners\n6. Re-inject all singleton fields (new config values propagate automatically)\n\n---\n\n## Debug Overlay\n\n```\n/core debug          → Full overlay in chat (color-coded)\n/core debug paste    → Upload report to pastes.dev, returns clickable URL\n/core bindings       → List all DI container bindings with scope + live status\n/core gc             → Request JVM GC + show freed memory delta\n/core version        → Core version, Paper version, Java version\n/core reload         → Trigger hot-reload\n```\n\nBuilt-in sections: **JVM** (heap, uptime, Java version, CPUs), **Server** (TPS×3 color-coded, players, ticks, worlds), *\n*DI Container** (binding count, live singletons), **Configs** (files + existence), **DataStores** (per-store entry\ncount), **Inventories** (open GUIs, registered types), **Scoreboard**, **NPC**, **PlaceholderAPI**.\n\nAdd custom entries via `@Debug` on any `@Component`:\n\n```java\n@Component\n@Debug\npublic class ShopDebugInfo {\n\n    @Inject private ShopService shopService;\n\n    @Debug(category = \"Shop\", label = \"Active Listings\")\n    public String listings() {\n        return String.valueOf(shopService.countActiveListings());\n    }\n\n    @Debug(category = \"Shop\", label = \"Revenue Today\")\n    public String revenue() {\n        return \"\u003cgold\u003e\" + shopService.getRevenue();\n    }\n\n    @Debug(category = \"Shop\", label = \"Category Breakdown\")\n    public Map\u003cString, Integer\u003e breakdown() {\n        return shopService.getCountByCategory();  // Map is formatted automatically\n    }\n}\n\n// Manual registration\ncore.getDebugOverlay().getRegistry()\n    .registerEntry(\"Custom\", \"Server Status\", () -\u003e myService.getStatus());\n```\n\nThe paste upload uses `pastes.dev` API — plain text, all MiniMessage tags stripped, metadata header with timestamp and\ngenerator included.\n\n---\n\n## Utilities\n\n### SchedulerUtil\n\n```java\nSchedulerUtil.run(plugin, () -\u003e player.sendMessage(\"Hello!\"));\nSchedulerUtil.runLater(plugin, () -\u003e teleport(player), SchedulerUtil.seconds(3));\nBukkitTask task = SchedulerUtil.repeat(plugin, this::tick, 0L, SchedulerUtil.seconds(5));\nSchedulerUtil.runAsync(plugin, () -\u003e heavyComputation());\nSchedulerUtil.runLaterAsync(plugin, () -\u003e dbWrite(), SchedulerUtil.seconds(1));\n\nSchedulerUtil.supplyAsync(plugin, () -\u003e database.findPlayer(uuid))\n    .thenAcceptAsync(data -\u003e player.sendMessage(\"Balance: \" + data.getBalance()),\n        SchedulerUtil.syncExecutor(plugin));\n\nSchedulerUtil.cancel(task);\nSchedulerUtil.seconds(5);    // → 100 ticks\nSchedulerUtil.minutes(1);    // → 1200 ticks\n```\n\n### ComponentUtil\n\n```java\nComponentUtil.parse(\"\u003cred\u003eHello \u003cbold\u003eWorld\");\nComponentUtil.parse(\"\u003cprefix\u003e Welcome, \u003cplayer\u003e!\",\n    Map.of(\"prefix\", \"\u003cdark_gray\u003e[Core]\", \"player\", player.getName()));\nComponentUtil.fromLegacy(\"\u0026aHello \u0026bWorld\");\nComponentUtil.toLegacy(component);\nComponentUtil.toMiniMessage(component);\nComponentUtil.toPlain(component);\nComponentUtil.stripFormatting(\"\u003cred\u003e\u003cbold\u003eHello\"); // → \"Hello\"\nComponentUtil.parseList(List.of(\"\u003cred\u003eLine 1\", \"\u003cgreen\u003eLine 2\"));\nComponentUtil.empty();\nComponentUtil.newline();\nComponentUtil.plain(\"No formatting\");\n```\n\n### ColorUtil\n\n```java\nColorUtil.fromHex(\"#FF5733\");\nColorUtil.fromHex(\"33FF57\");       // # is optional\nColorUtil.fromHex(\"#00F\");         // shorthand expands to #0000FF\nColorUtil.fromHexSafe(\"#invalid\"); // returns null on failure\nColorUtil.fromRgb(255, 87, 51);\nColorUtil.toTextColor(bukkit);\nColorUtil.toBukkitColor(textColor);\nColorUtil.toHex(color);            // → \"#FF5733\"\nColorUtil.lerp(Color.RED, Color.BLUE, 0.5f);\nColorUtil.gradient(Color.RED, Color.YELLOW, 10);\nColorUtil.gradientText(\"Hello World\", \"#FF0000\", \"#0000FF\");\nColorUtil.rainbowText(\"Rainbow!\", 0);\n```\n\n### TimeUtil\n\n```java\nTimeUtil.format(Duration.ofSeconds(3661));    // → \"1h 1m 1s\"\nTimeUtil.format(Duration.ofSeconds(90));      // → \"1m 30s\"\nTimeUtil.formatSeconds(45);                   // → \"45s\"\nTimeUtil.formatUntil(Instant.now().plusSeconds(120)); // → \"2m 0s\"\nTimeUtil.parse(\"1h30m\");                      // → Duration.ofMinutes(90)\nTimeUtil.parse(\"2d12h\");                      // → Duration.ofHours(60)\nTimeUtil.parseSafe(\"bad_input\");              // → Duration.ZERO\nTimeUtil.toTicks(Duration.ofSeconds(5));      // → 100\nTimeUtil.fromTicks(200);                      // → 10 seconds\n```\n\n### SoundUtil\n\n```java\n// Presets — zero allocation, all static final\nSoundUtil.play(player, SoundUtil.Presets.SUCCESS);\nSoundUtil.play(player, SoundUtil.Presets.CLICK);\nSoundUtil.play(player, SoundUtil.Presets.ERROR);\nSoundUtil.playAll(players, SoundUtil.Presets.PING);\nSoundUtil.broadcast(server, SoundUtil.Presets.LEVEL_UP);\nSoundUtil.stop(player, Sound.MUSIC_GAME);\nSoundUtil.stopAll(player);\n\n// Custom effect\nSoundEffect.of(Sound.ENTITY_DRAGON_DEATH, 0.5f, 1.2f).play(player);\nSoundEffect.builder()\n    .sound(Sound.UI_BUTTON_CLICK).volume(0.6f).pitch(1.5f)\n    .category(SoundCategory.MASTER).build()\n    .playAt(location);\n\n// Timed sequences\nSoundSequence.create()\n    .then(SoundUtil.Presets.TICK,        0L)\n    .then(SoundUtil.Presets.TICK,       20L)\n    .then(SoundUtil.Presets.TICK_FINAL, 40L)\n    .then(SoundUtil.Presets.TELEPORT_OUT, 60L)\n    .play(plugin, player);\n```\n\nAvailable presets: `CLICK`, `CLICK_SOFT`, `CLICK_HIGH`, `SUCCESS`, `ERROR`, `WARNING`, `LEVEL_UP`, `COIN`, `PURCHASE`,\n`OPEN`, `CLOSE`, `PAGE_PREV`, `PAGE_NEXT`, `PING`, `TICK`, `TICK_FINAL`, `TELEPORT_OUT`, `TELEPORT_IN`, `DEPOSIT`,\n`WITHDRAW`.\n\n### Preconditions\n\n```java\nPreconditions.notNull(player, \"Player must not be null\");\nPreconditions.notBlank(input, \"Name must not be blank\");\nPreconditions.isTrue(balance \u003e= 0, \"Balance cannot be negative\");\nPreconditions.isFalse(banned, \"Banned players cannot do this\");\nPreconditions.inRange(index, 0, 53, \"Slot out of range\");\nPreconditions.notEmpty(homeList, \"Home list must not be empty\");\n```\n\n---\n\n## Annotation Reference\n\n| Annotation           | Target                       | Purpose                                    |\n|----------------------|------------------------------|--------------------------------------------|\n| `@Component`         | Class                        | Register as DI-managed component           |\n| `@Singleton`         | Class                        | Explicit singleton scope (default)         |\n| `@Prototype`         | Class                        | New instance per injection point           |\n| `@Inject`            | Field / Constructor / Method | Mark injection point                       |\n| `@Named(\"id\")`       | Field / Parameter            | Qualify injection by name                  |\n| `@PostConstruct`     | Method                       | Called after all fields injected           |\n| `@PreDestroy`        | Method                       | Called before container destroys instance  |\n| `@Config(...)`       | Class                        | Declare a config file binding              |\n| `@Command(...)`      | Class                        | Register a command handler                 |\n| `@SubCommand(...)`   | Method                       | Register a sub-command on a `BaseCommand`  |\n| `@Cooldown(...)`     | Method / Class               | Apply cooldown to a command or sub-command |\n| `@Listener`          | Class                        | Auto-register as Bukkit event listener     |\n| `@DataStore(...)`    | Class                        | Register a binary data store               |\n| `@InventoryGui(...)` | Class                        | Register a GUI inventory                   |\n| `@Task(...)`         | Method                       | Schedule a recurring or one-shot task      |\n| `@Reloadable(...)`   | Method                       | Invoked during hot-reload                  |\n| `@Debug(...)`        | Method / Class               | Expose info to `/core debug`               |\n| `@LootTableDef`      | Method                       | Register a `LootTable` factory method      |\n\n---\n\n## Exception Hierarchy\n\n```\nCoreException (RuntimeException)\n├── ModuleException       — thrown during module load/enable\n├── InjectionException    — thrown during DI resolution or injection\n├── ConfigException       — thrown during config load/save\n├── CommandException      — thrown during command registration or dispatch\n├── DataStoreException    — thrown during store I/O\n└── InventoryException    — thrown during GUI build or open\n```\n\nAll exceptions are unchecked and carry a descriptive message including the failing component name.\n\n---\n\n## Boot Sequence\n\n```\nonEnable()\n  │\n  ├─ [1]  Container construction\n  │        Self-registers: Plugin, Server, Logger, Path\n  │        Constructs and binds all framework managers\n  │\n  ├─ [2]  ClassScanner → ScanResult\n  │        Scans JAR for all annotated types\n  │        AnnotationProcessor wires everything into Container\n  │\n  ├─ [3]  ConfigManager.initializeAll()\n  │        Wires each @Config with file path + adapter\n  │        Copies defaults from JAR if missing, calls load()\n  │\n  ├─ [4]  DataStoreManager.initializeAll()\n  │        Creates store directories, loads .dat files into cache\n  │\n  ├─ [5]  CommandManager.registerAll()\n  │        Registers each @Command into Paper's CommandMap\n  │        Wires CooldownManager into each BaseCommand instance\n  │\n  ├─ [6]  InventoryManager.initializeAll()\n  │        Registers @InventoryGui types by ID\n  │        Registers GuiListener for click/drag/close\n  │\n  ├─ [7]  PlaceholderManager.initialize()\n  │        Detects PAPI, registers expansion, discovers PlaceholderProviders\n  │\n  ├─ [8]  Bukkit listener registration\n  │        Calls registerEvents() for all @Listener components\n  │\n  ├─ [9]  LootManager.discoverAndRegister()\n  │        Finds and calls all @LootTableDef factory methods\n  │\n  ├─ [10] TaskManager.discoverAndSchedule()\n  │        Finds and schedules all @Task methods\n  │\n  ├─ [11] DebugOverlay.discoverFrom()\n  │        Finds all @Debug-annotated components\n  │\n  ├─ [12] UpdateChecker.checkAsync()\n  │        Async GitHub API check, registers UpdateNotifier if update found\n  │\n  ├─ [13] ModuleRegistry.loadAll()\n  │        Calls load() on all registered modules in order\n  │\n  └─ [14] ModuleRegistry.enableAll()\n           Calls enable() on all registered modules in order\n\nonDisable()\n  ├─ ModuleRegistry.disableAll()       (reverse order)\n  ├─ ConversationManager.shutdown()\n  ├─ FormManager.shutdown()\n  ├─ MenuManager.shutdown()\n  ├─ ChatInputManager.shutdown()\n  ├─ TaskManager.cancelAll()\n  ├─ NpcManager.destroy()\n  ├─ HologramManager.destroy()\n  ├─ ScoreboardManager.destroyAll()\n  ├─ BossBarManager.shutdown()\n  ├─ ActionbarManager.shutdown()\n  ├─ InventoryManager.closeAll()\n  ├─ CommandManager.unregisterAll()\n  ├─ PlaceholderManager.shutdown()\n  ├─ DataStoreManager.flushAll()\n  ├─ ConfigManager.saveAll()\n  └─ Container.destroy()               (@PreDestroy + clear all bindings)\n```\n\n---\n\n## Full Example Plugin\n\n```java\n// MyPlugin.java\npublic final class MyPlugin extends JavaPlugin {\n    @Override\n    public void onEnable() {\n        final CorePlugin core = CorePlugin.getInstance();\n        core.getComponentRegistry().scanAndProcess(\"dev.mzcy.example\");\n        core.getConfigManager().initializeAll(core.getScanResult());\n        core.getDataStoreManager().initializeAll(core.getScanResult());\n        core.getCommandManager().registerAll(core.getScanResult());\n        core.getInventoryManager().initializeAll(core.getScanResult());\n    }\n}\n\n// ExampleConfig.java\n@Config(\"config\")\npublic class ExampleConfig extends AbstractConfig {\n    public String welcomeMessage = \"\u003cgreen\u003eWelcome, \u003cplayer\u003e!\";\n    public int    startBalance   = 100;\n}\n\n// PlayerDataStore.java\n@DataStore(\"players\")\npublic class PlayerDataStore extends AbstractDataStore\u003cUUID, PlayerData\u003e {\n    public PlayerDataStore() { super(new BinaryDataSerializer\u003c\u003e()); }\n    @Override protected String keyToFileName(@NotNull UUID key) { return key.toString(); }\n    @Override protected UUID fileNameToKey(@NotNull String f)   { return UUID.fromString(f); }\n}\n\n// PlayerService.java\n@Component\npublic class PlayerService {\n\n    @Inject private PlayerDataStore store;\n    @Inject private ExampleConfig   config;\n\n    public void onJoin(Player player) {\n        if (!store.contains(player.getUniqueId())) {\n            store.put(player.getUniqueId(),\n                new PlayerData(player.getName(), config.startBalance));\n        }\n    }\n\n    public int getBalance(UUID uuid) {\n        return store.get(uuid).map(PlayerData::getBalance).orElse(0);\n    }\n\n    @Reloadable(name = \"Player Cache\", order = 10)\n    public void reload() {\n        // Cache rebuild after hot-reload\n    }\n}\n\n// JoinListener.java\n@Component\n@Listener\npublic class JoinListener implements Listener {\n\n    @Inject private PlayerService playerService;\n    @Inject private ExampleConfig config;\n\n    @EventHandler\n    public void onJoin(PlayerJoinEvent event) {\n        final Player player = event.getPlayer();\n        playerService.onJoin(player);\n        player.sendMessage(ComponentUtil.parse(\n            config.welcomeMessage,\n            Map.of(\"player\", player.getName())\n        ));\n    }\n}\n\n// BalanceCommand.java\n@Command(name = \"balance\", aliases = {\"bal\"},\n         permission = \"example.balance\", playerOnly = true)\npublic class BalanceCommand extends BaseCommand {\n\n    @Inject private PlayerService playerService;\n\n    @Override\n    protected void onCommand(@NotNull CommandContext ctx) {\n        final int balance = playerService.getBalance(ctx.playerOrThrow().getUniqueId());\n        ctx.send(\"\u003cgold\u003eYour balance: \u003cwhite\u003e\" + balance + \" coins\");\n    }\n\n    @SubCommand(\"set\")\n    @Cooldown(value = 5, unit = TimeUnit.SECONDS, bypassPermission = \"example.admin\")\n    public void onSet(CommandContext ctx) {\n        ctx.argInt(0).ifPresent(amount -\u003e {\n            playerService.setBalance(ctx.playerOrThrow().getUniqueId(), amount);\n            ctx.sendSuccess(\"Balance set to \u003cwhite\u003e\" + amount);\n        });\n    }\n}\n```\n\n---\n\n## License\n\n```\nMIT License — Copyright (c) 2026 mzcy_ and contributors.\n```\n\n---\n\n*Built with ❤ for the Paper ecosystem.*","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmzcydev%2Fpaper-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmzcydev%2Fpaper-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmzcydev%2Fpaper-core/lists"}