[{"data":1,"prerenderedAt":664},["ShallowReactive",2],{"docs-\u002Fplatforms\u002Fcore":3},{"id":4,"title":5,"body":6,"description":654,"extension":655,"meta":656,"navTitle":657,"navigation":658,"path":659,"rawbody":660,"seo":661,"stem":662,"__hash__":663},"docs\u002Fplatforms\u002Fcore.md","Core \u002F Common Logic",{"type":7,"value":8,"toc":636},"minimark",[9,13,26,29,42,47,57,60,89,93,96,123,129,151,155,161,186,189,193,196,204,207,219,223,229,232,236,243,274,277,281,286,403,436,440,449,454,457,463,469,495,499,502,508,513,539,549,553,556,562,584,588,591,597,615,618,622,627],[10,11,5],"h1",{"id":12},"core-common-logic",[14,15,16,17,21,22,25],"p",{},"Use this page when your plugin or mod has a thin platform entrypoint and most of\nthe real logic lives in shared ",[18,19,20],"code",{},"common"," or ",[18,23,24],{},"core"," code.",[14,27,28],{},"That usually means:",[30,31,32,36,39],"ul",{},[33,34,35],"li",{},"Bukkit\u002FFabric\u002FVelocity\u002FNeoForge only bootstrap the runtime",[33,37,38],{},"config, lang, logger, HTTP, placeholders, and business logic live in common",[33,40,41],{},"platform modules stay as adapters instead of owning the feature logic",[43,44,46],"h2",{"id":45},"typical-module-layout","Typical Module Layout",[48,49,55],"pre",{"className":50,"code":52,"language":53,"meta":54},[51],"language-text","my-plugin\u002F\n|- common\u002F\n|- bukkit\u002F\n|- fabric\u002F\n`- velocity\u002F\n","text","",[18,56,52],{"__ignoreMap":54},[14,58,59],{},"In this structure:",[30,61,62,76,86],{},[33,63,64,67,68,71,72,75],{},[18,65,66],{},"bukkit\u002F",", ",[18,69,70],{},"fabric\u002F",", and ",[18,73,74],{},"velocity\u002F"," bootstrap MagicUtils for their runtime",[33,77,78,81,82,85],{},[18,79,80],{},"common\u002F"," receives ",[18,83,84],{},"MagicRuntime"," and owns the actual feature logic",[33,87,88],{},"platform modules do not reimplement the same services three times",[43,90,92],{"id":91},"the-recommended-split","The Recommended Split",[14,94,95],{},"Keep these parts in the platform layer:",[30,97,98,111,114,117,120],{},[33,99,100,101,67,104,107,108],{},"bootstrap helpers such as ",[18,102,103],{},"BukkitBootstrap",[18,105,106],{},"FabricBootstrap",",\n",[18,109,110],{},"VelocityBootstrap",[33,112,113],{},"platform event registration",[33,115,116],{},"command registration against the platform dispatcher",[33,118,119],{},"external placeholder bridge setup",[33,121,122],{},"plugin or mod lifecycle entrypoints",[14,124,125,126,128],{},"Keep these parts in ",[18,127,20],{}," or other shared modules:",[30,130,131,136,139,142,145,148],{},[33,132,133,134],{},"services built on ",[18,135,84],{},[33,137,138],{},"config models and reload logic",[33,140,141],{},"logger and language driven messaging",[33,143,144],{},"HTTP clients and runtime config bindings",[33,146,147],{},"placeholder logic used by your own code",[33,149,150],{},"business rules that should work across every platform",[43,152,154],{"id":153},"common-code-should-depend-on-shared-abstractions","Common Code Should Depend On Shared Abstractions",[14,156,157,158,160],{},"Inside common code, depend on ",[18,159,84],{}," or the core services it exposes:",[30,162,163,168,173,178,183],{},[33,164,165],{},[18,166,167],{},"Platform",[33,169,170],{},[18,171,172],{},"ConfigManager",[33,174,175],{},[18,176,177],{},"LoggerCore",[33,179,180],{},[18,181,182],{},"LanguageManager",[33,184,185],{},"named runtime resources and config bindings",[14,187,188],{},"This keeps the shared layer free from Bukkit, BungeeCord, Velocity, Fabric, or NeoForge\nclasses.",[43,190,192],{"id":191},"wiring-shared-services-from-the-platform-layer","Wiring Shared Services From The Platform Layer",[14,194,195],{},"The platform entrypoint should bootstrap MagicUtils and hand the runtime to your\ncommon services:",[48,197,202],{"className":198,"code":200,"language":201,"meta":54},[199],"language-java","public final class MyPlugin extends JavaPlugin {\n    private BukkitBootstrap.RuntimeResult magic;\n    private CommonBootstrap bootstrap;\n\n    @Override\n    public void onEnable() {\n        magic = BukkitBootstrap.forPlugin(this)\n                .enableCommands()\n                .buildRuntime();\n\n        bootstrap = new CommonBootstrap(magic.runtime());\n        bootstrap.start();\n    }\n\n    @Override\n    public void onDisable() {\n        if (magic != null) {\n            magic.runtime().close();\n            magic = null;\n        }\n    }\n}\n","java",[18,203,200],{"__ignoreMap":54},[14,205,206],{},"The same structure works on Fabric, Velocity, and NeoForge: platform code\ncreates the runtime, shared code consumes it.",[14,208,209,67,212,215,216,218],{},[18,210,211],{},"forPlugin(...)",[18,213,214],{},"forMod(...)",", and the other bootstrap helpers belong to the\nplatform module only. ",[18,217,20],{}," should consume the resulting runtime, not create\nit.",[43,220,222],{"id":221},"example-shared-service","Example Shared Service",[48,224,227],{"className":225,"code":226,"language":201,"meta":54},[199],"public final class CommonBootstrap {\n    private final MagicRuntime runtime;\n    private final ConfigManager configManager;\n    private final LoggerCore logger;\n    private final Optional\u003CLanguageManager> languages;\n\n    public CommonBootstrap(MagicRuntime runtime) {\n        this.runtime = runtime;\n        this.configManager = runtime.configManager();\n        this.logger = runtime.logger();\n        this.languages = runtime.findComponent(LanguageManager.class);\n    }\n\n    public void start() {\n        logger.info(\"Starting shared services\");\n\n        languages.ifPresent(manager -> logger.info(\"Language manager is available\"));\n\n        runtime.bindConfig(\n                \"http.backend\",\n                BackendConfig.class,\n                config -> MagicHttpClient.builder(runtime.platform(), configManager)\n                        .baseUrl(config.baseUrl)\n                        .build(),\n                \"backend\"\n        );\n    }\n}\n",[18,228,226],{"__ignoreMap":54},[14,230,231],{},"This is the typical multi-platform pattern: the shared layer receives one\nruntime from the adapter layer and builds everything else on top of it.",[43,233,235],{"id":234},"what-works-well-in-common","What Works Well In Common",[14,237,238,239,242],{},"The ",[18,240,241],{},"magicutils-core"," path is especially good for:",[30,244,245,249,253,257,262,265,268],{},[33,246,247],{},[18,248,84],{},[33,250,251],{},[18,252,172],{},[33,254,255],{},[18,256,177],{},[33,258,259,260],{},"optional ",[18,261,182],{},[33,263,264],{},"runtime-managed HTTP or WebSocket clients",[33,266,267],{},"shared placeholder evaluation",[33,269,270,271],{},"reloadable services built with ",[18,272,273],{},"bindConfig(...)",[14,275,276],{},"If your modules already target multiple platforms, this is usually where the\nmajority of the code should live.",[43,278,280],{"id":279},"the-platform-interface","The Platform interface",[14,282,283,285],{},[18,284,167],{}," is the single abstraction the shared layer depends on instead of any\none server API. Beyond the player events below, it exposes:",[30,287,288,294,311,328,338,372],{},[33,289,290,293],{},[18,291,292],{},"configDir()"," — base directory for configs and language files.",[33,295,296,299,300,303,304,67,307,310],{},[18,297,298],{},"logger()"," — a ",[18,301,302],{},"PlatformLogger"," (",[18,305,306],{},"info\u002Fwarn\u002Ferror\u002Fdebug",[18,308,309],{},"isDebugEnabled()",").",[33,312,313,316,317,320,321,324,325,327],{},[18,314,315],{},"console()"," — the console as an ",[18,318,319],{},"Audience","; ",[18,322,323],{},"onlinePlayers()"," — online players\nas ",[18,326,319],{},"s.",[33,329,330,333,334,337],{},[18,331,332],{},"runOnMain(task)"," — run on the platform's main thread (immediate on proxies\nthat have none); ",[18,335,336],{},"runForAudience(audience, task)"," — run on the region owning\nthat audience (Folia) or the main thread.",[33,339,340,343,344,347,348,351,352,67,355,67,358,67,361,67,364,367,368,371],{},[18,341,342],{},"isMainThread()"," and ",[18,345,346],{},"threadContext()"," — the current ",[18,349,350],{},"ThreadContext","\n(",[18,353,354],{},"MAIN",[18,356,357],{},"EVENT_LOOP",[18,359,360],{},"NETWORK",[18,362,363],{},"WORKER",[18,365,366],{},"UNKNOWN","); use\n",[18,369,370],{},"threadContext().isBlockingSensitive()"," before doing blocking I\u002FO.",[33,373,374,299,377,380,381,67,384,67,387,390,391,394,395,398,399,402],{},[18,375,376],{},"scheduler()",[18,378,379],{},"TaskScheduler"," with ",[18,382,383],{},"cpu()",[18,385,386],{},"io()",[18,388,389],{},"async()"," executors and\n",[18,392,393],{},"runIo(...)"," \u002F ",[18,396,397],{},"supplyIo(...)"," helpers returning ",[18,400,401],{},"CompletableFuture","s, so you\nkeep disk and network work off the main thread.",[14,404,405,407,408,67,411,67,414,107,417,420,421,424,425,428,429,432,433,310],{},[18,406,319],{}," normalises a message target: ",[18,409,410],{},"send(Component)",[18,412,413],{},"id()",[18,415,416],{},"name()",[18,418,419],{},"hasPermission(node)",". ",[18,422,423],{},"ListenerSubscription"," is the ",[18,426,427],{},"AutoCloseable"," handle\nreturned by every ",[18,430,431],{},"subscribe*"," call; close it to unsubscribe (or use\n",[18,434,435],{},"ListenerSubscription.noop()",[43,437,439],{"id":438},"player-events","Player Events",[14,441,442,444,445,448],{},[18,443,167],{}," provides normalized player events, so shared code can react to\nplayers without importing Bukkit, Fabric, or Velocity types. Coverage differs by\nplatform: lifecycle (join\u002Fleave) and locale events are available everywhere;\nchat\u002Fcommand message events are available on Bukkit, BungeeCord, Velocity, and\nFabric, but not on NeoForge (there ",[18,446,447],{},"subscribePlayerMessages"," returns a no-op\nsubscription).",[450,451,453],"h3",{"id":452},"player-lifecycle","Player Lifecycle",[14,455,456],{},"Subscribe to join\u002Fleave events:",[48,458,461],{"className":459,"code":460,"language":201,"meta":54},[199],"ListenerSubscription sub = platform.subscribePlayerLifecycle(event -> {\n    if (event.type() == PlayerLifecycleType.JOIN) {\n        logger.info(event.playerName() + \" joined\");\n    }\n});\n",[18,462,460],{"__ignoreMap":54},[14,464,465,468],{},[18,466,467],{},"PlayerLifecycle"," contains:",[30,470,471,477,483],{},[33,472,473,476],{},[18,474,475],{},"playerId()"," — player UUID (when available)",[33,478,479,482],{},[18,480,481],{},"playerName()"," — display\u002Flogin name",[33,484,485,488,489,21,492],{},[18,486,487],{},"type()"," — ",[18,490,491],{},"JOIN",[18,493,494],{},"LEAVE",[450,496,498],{"id":497},"player-messages","Player Messages",[14,500,501],{},"Subscribe to chat messages and commands:",[48,503,506],{"className":504,"code":505,"language":201,"meta":54},[199],"ListenerSubscription sub = platform.subscribePlayerMessages(event -> {\n    if (event.type() == PlayerMessageType.CHAT) {\n        logger.info(event.playerName() + \": \" + event.message());\n    }\n});\n",[18,507,505],{"__ignoreMap":54},[14,509,510,468],{},[18,511,512],{},"PlayerMessage",[30,514,515,519,523,529],{},[33,516,517,476],{},[18,518,475],{},[33,520,521,482],{},[18,522,481],{},[33,524,525,528],{},[18,526,527],{},"message()"," — raw chat content or command line",[33,530,531,488,533,21,536],{},[18,532,487],{},[18,534,535],{},"CHAT",[18,537,538],{},"COMMAND",[14,540,541,542,544,545,548],{},"Both subscriptions return ",[18,543,423],{}," which can be closed to\nunsubscribe. Both records expose ",[18,546,547],{},"isValid()"," for null-safety checks.",[450,550,552],{"id":551},"player-locale","Player Locale",[14,554,555],{},"Subscribe to the language a player's client reports, so you can react when it\nchanges:",[48,557,560],{"className":558,"code":559,"language":201,"meta":54},[199],"ListenerSubscription sub = platform.subscribePlayerLocales(event -> {\n    logger.info(event.playerName() + \" speaks \" + event.localeTag());\n});\n",[18,561,559],{"__ignoreMap":54},[14,563,564,567,568,67,570,71,572,575,576,579,580,583],{},[18,565,566],{},"PlayerLocale"," contains ",[18,569,475],{},[18,571,481],{},[18,573,574],{},"localeTag()"," (a BCP-47\ntag such as ",[18,577,578],{},"en_US","). The lang module uses this internally for\n",[18,581,582],{},"bindClientLocaleSync(...)","; subscribe directly only when you need the raw event.",[43,585,587],{"id":586},"what-should-stay-out-of-common","What Should Stay Out Of Common",[14,589,590],{},"Try not to leak platform-specific APIs into the shared layer.",[14,592,593,594,596],{},"Avoid putting these directly into ",[18,595,20],{},":",[30,598,599,606,609,612],{},[33,600,601,602,605],{},"Bukkit ",[18,603,604],{},"JavaPlugin",", Fabric callbacks, Velocity annotations, NeoForge events",[33,607,608],{},"platform command dispatcher registration",[33,610,611],{},"direct calls to platform plugin managers or server APIs",[33,613,614],{},"external bridge setup that only exists on one runtime",[14,616,617],{},"Keep those in the platform module and pass only the shared abstractions\ndownward.",[43,619,621],{"id":620},"if-you-really-need-a-custom-platform","If You Really Need A Custom Platform",[14,623,624,625,25],{},"That is a separate case from normal ",[18,626,20],{},[14,628,629,630,107,632,635],{},"If you are actually building a new adapter around ",[18,631,167],{},[18,633,634],{},"ShutdownHookRegistrar",", or your own bootstrap path, use the Runtime guide and\nthe platform API as the source of truth, but keep that adapter layer small and\nlet the feature logic remain in shared services.",{"title":54,"searchDepth":637,"depth":637,"links":638},3,[639,641,642,643,644,645,646,647,652,653],{"id":45,"depth":640,"text":46},2,{"id":91,"depth":640,"text":92},{"id":153,"depth":640,"text":154},{"id":191,"depth":640,"text":192},{"id":221,"depth":640,"text":222},{"id":234,"depth":640,"text":235},{"id":279,"depth":640,"text":280},{"id":438,"depth":640,"text":439,"children":648},[649,650,651],{"id":452,"depth":637,"text":453},{"id":497,"depth":637,"text":498},{"id":551,"depth":637,"text":552},{"id":586,"depth":640,"text":587},{"id":620,"depth":640,"text":621},"Keep feature logic in shared common modules while thin platform adapters only bootstrap MagicRuntime. The core pattern behind multi-platform MagicUtils projects.","md",{},null,true,"\u002Fplatforms\u002Fcore","---\ntitle: Core \u002F Common Logic\ndescription: Keep feature logic in shared common modules while thin platform adapters only bootstrap MagicRuntime. The core pattern behind multi-platform MagicUtils projects.\n---\n\n# Core \u002F Common Logic\n\nUse this page when your plugin or mod has a thin platform entrypoint and most of\nthe real logic lives in shared `common` or `core` code.\n\nThat usually means:\n\n- Bukkit\u002FFabric\u002FVelocity\u002FNeoForge only bootstrap the runtime\n- config, lang, logger, HTTP, placeholders, and business logic live in common\n- platform modules stay as adapters instead of owning the feature logic\n\n## Typical Module Layout\n\n```text\nmy-plugin\u002F\n|- common\u002F\n|- bukkit\u002F\n|- fabric\u002F\n`- velocity\u002F\n```\n\nIn this structure:\n\n- `bukkit\u002F`, `fabric\u002F`, and `velocity\u002F` bootstrap MagicUtils for their runtime\n- `common\u002F` receives `MagicRuntime` and owns the actual feature logic\n- platform modules do not reimplement the same services three times\n\n## The Recommended Split\n\nKeep these parts in the platform layer:\n\n- bootstrap helpers such as `BukkitBootstrap`, `FabricBootstrap`,\n  `VelocityBootstrap`\n- platform event registration\n- command registration against the platform dispatcher\n- external placeholder bridge setup\n- plugin or mod lifecycle entrypoints\n\nKeep these parts in `common` or other shared modules:\n\n- services built on `MagicRuntime`\n- config models and reload logic\n- logger and language driven messaging\n- HTTP clients and runtime config bindings\n- placeholder logic used by your own code\n- business rules that should work across every platform\n\n## Common Code Should Depend On Shared Abstractions\n\nInside common code, depend on `MagicRuntime` or the core services it exposes:\n\n- `Platform`\n- `ConfigManager`\n- `LoggerCore`\n- `LanguageManager`\n- named runtime resources and config bindings\n\nThis keeps the shared layer free from Bukkit, BungeeCord, Velocity, Fabric, or NeoForge\nclasses.\n\n## Wiring Shared Services From The Platform Layer\n\nThe platform entrypoint should bootstrap MagicUtils and hand the runtime to your\ncommon services:\n\n```java\npublic final class MyPlugin extends JavaPlugin {\n    private BukkitBootstrap.RuntimeResult magic;\n    private CommonBootstrap bootstrap;\n\n    @Override\n    public void onEnable() {\n        magic = BukkitBootstrap.forPlugin(this)\n                .enableCommands()\n                .buildRuntime();\n\n        bootstrap = new CommonBootstrap(magic.runtime());\n        bootstrap.start();\n    }\n\n    @Override\n    public void onDisable() {\n        if (magic != null) {\n            magic.runtime().close();\n            magic = null;\n        }\n    }\n}\n```\n\nThe same structure works on Fabric, Velocity, and NeoForge: platform code\ncreates the runtime, shared code consumes it.\n\n`forPlugin(...)`, `forMod(...)`, and the other bootstrap helpers belong to the\nplatform module only. `common` should consume the resulting runtime, not create\nit.\n\n## Example Shared Service\n\n```java\npublic final class CommonBootstrap {\n    private final MagicRuntime runtime;\n    private final ConfigManager configManager;\n    private final LoggerCore logger;\n    private final Optional\u003CLanguageManager> languages;\n\n    public CommonBootstrap(MagicRuntime runtime) {\n        this.runtime = runtime;\n        this.configManager = runtime.configManager();\n        this.logger = runtime.logger();\n        this.languages = runtime.findComponent(LanguageManager.class);\n    }\n\n    public void start() {\n        logger.info(\"Starting shared services\");\n\n        languages.ifPresent(manager -> logger.info(\"Language manager is available\"));\n\n        runtime.bindConfig(\n                \"http.backend\",\n                BackendConfig.class,\n                config -> MagicHttpClient.builder(runtime.platform(), configManager)\n                        .baseUrl(config.baseUrl)\n                        .build(),\n                \"backend\"\n        );\n    }\n}\n```\n\nThis is the typical multi-platform pattern: the shared layer receives one\nruntime from the adapter layer and builds everything else on top of it.\n\n## What Works Well In Common\n\nThe `magicutils-core` path is especially good for:\n\n- `MagicRuntime`\n- `ConfigManager`\n- `LoggerCore`\n- optional `LanguageManager`\n- runtime-managed HTTP or WebSocket clients\n- shared placeholder evaluation\n- reloadable services built with `bindConfig(...)`\n\nIf your modules already target multiple platforms, this is usually where the\nmajority of the code should live.\n\n## The Platform interface\n\n`Platform` is the single abstraction the shared layer depends on instead of any\none server API. Beyond the player events below, it exposes:\n\n- `configDir()` — base directory for configs and language files.\n- `logger()` — a `PlatformLogger` (`info\u002Fwarn\u002Ferror\u002Fdebug`, `isDebugEnabled()`).\n- `console()` — the console as an `Audience`; `onlinePlayers()` — online players\n  as `Audience`s.\n- `runOnMain(task)` — run on the platform's main thread (immediate on proxies\n  that have none); `runForAudience(audience, task)` — run on the region owning\n  that audience (Folia) or the main thread.\n- `isMainThread()` and `threadContext()` — the current `ThreadContext`\n  (`MAIN`, `EVENT_LOOP`, `NETWORK`, `WORKER`, `UNKNOWN`); use\n  `threadContext().isBlockingSensitive()` before doing blocking I\u002FO.\n- `scheduler()` — a `TaskScheduler` with `cpu()`, `io()`, `async()` executors and\n  `runIo(...)` \u002F `supplyIo(...)` helpers returning `CompletableFuture`s, so you\n  keep disk and network work off the main thread.\n\n`Audience` normalises a message target: `send(Component)`, `id()`, `name()`,\n`hasPermission(node)`. `ListenerSubscription` is the `AutoCloseable` handle\nreturned by every `subscribe*` call; close it to unsubscribe (or use\n`ListenerSubscription.noop()`).\n\n## Player Events\n\n`Platform` provides normalized player events, so shared code can react to\nplayers without importing Bukkit, Fabric, or Velocity types. Coverage differs by\nplatform: lifecycle (join\u002Fleave) and locale events are available everywhere;\nchat\u002Fcommand message events are available on Bukkit, BungeeCord, Velocity, and\nFabric, but not on NeoForge (there `subscribePlayerMessages` returns a no-op\nsubscription).\n\n### Player Lifecycle\n\nSubscribe to join\u002Fleave events:\n\n```java\nListenerSubscription sub = platform.subscribePlayerLifecycle(event -> {\n    if (event.type() == PlayerLifecycleType.JOIN) {\n        logger.info(event.playerName() + \" joined\");\n    }\n});\n```\n\n`PlayerLifecycle` contains:\n\n- `playerId()` — player UUID (when available)\n- `playerName()` — display\u002Flogin name\n- `type()` — `JOIN` or `LEAVE`\n\n### Player Messages\n\nSubscribe to chat messages and commands:\n\n```java\nListenerSubscription sub = platform.subscribePlayerMessages(event -> {\n    if (event.type() == PlayerMessageType.CHAT) {\n        logger.info(event.playerName() + \": \" + event.message());\n    }\n});\n```\n\n`PlayerMessage` contains:\n\n- `playerId()` — player UUID (when available)\n- `playerName()` — display\u002Flogin name\n- `message()` — raw chat content or command line\n- `type()` — `CHAT` or `COMMAND`\n\nBoth subscriptions return `ListenerSubscription` which can be closed to\nunsubscribe. Both records expose `isValid()` for null-safety checks.\n\n### Player Locale\n\nSubscribe to the language a player's client reports, so you can react when it\nchanges:\n\n```java\nListenerSubscription sub = platform.subscribePlayerLocales(event -> {\n    logger.info(event.playerName() + \" speaks \" + event.localeTag());\n});\n```\n\n`PlayerLocale` contains `playerId()`, `playerName()`, and `localeTag()` (a BCP-47\ntag such as `en_US`). The lang module uses this internally for\n`bindClientLocaleSync(...)`; subscribe directly only when you need the raw event.\n\n## What Should Stay Out Of Common\n\nTry not to leak platform-specific APIs into the shared layer.\n\nAvoid putting these directly into `common`:\n\n- Bukkit `JavaPlugin`, Fabric callbacks, Velocity annotations, NeoForge events\n- platform command dispatcher registration\n- direct calls to platform plugin managers or server APIs\n- external bridge setup that only exists on one runtime\n\nKeep those in the platform module and pass only the shared abstractions\ndownward.\n\n## If You Really Need A Custom Platform\n\nThat is a separate case from normal `common` code.\n\nIf you are actually building a new adapter around `Platform`,\n`ShutdownHookRegistrar`, or your own bootstrap path, use the Runtime guide and\nthe platform API as the source of truth, but keep that adapter layer small and\nlet the feature logic remain in shared services.\n",{"title":5,"description":654},"platforms\u002Fcore","IBzqKa0dEd4M2OxaMyozwSMXUOCBvnyNjzg-vuYsNDk",1783944486469]