Core / Common Logic
Use this page when your plugin or mod has a thin platform entrypoint and most of
the real logic lives in shared common or core code.
That usually means:
- Bukkit/Fabric/Velocity/NeoForge only bootstrap the runtime
- config, lang, logger, HTTP, placeholders, and business logic live in common
- platform modules stay as adapters instead of owning the feature logic
Typical Module Layout
In this structure:
bukkit/,fabric/, andvelocity/bootstrap MagicUtils for their runtimecommon/receivesMagicRuntimeand owns the actual feature logic- platform modules do not reimplement the same services three times
The Recommended Split
Keep these parts in the platform layer:
- bootstrap helpers such as
BukkitBootstrap,FabricBootstrap,VelocityBootstrap - platform event registration
- command registration against the platform dispatcher
- external placeholder bridge setup
- plugin or mod lifecycle entrypoints
Keep these parts in common or other shared modules:
- services built on
MagicRuntime - config models and reload logic
- logger and language driven messaging
- HTTP clients and runtime config bindings
- placeholder logic used by your own code
- business rules that should work across every platform
Common Code Should Depend On Shared Abstractions
Inside common code, depend on MagicRuntime or the core services it exposes:
PlatformConfigManagerLoggerCoreLanguageManager- named runtime resources and config bindings
This keeps the shared layer free from Bukkit, BungeeCord, Velocity, Fabric, or NeoForge classes.
Wiring Shared Services From The Platform Layer
The platform entrypoint should bootstrap MagicUtils and hand the runtime to your common services:
The same structure works on Fabric, Velocity, and NeoForge: platform code creates the runtime, shared code consumes it.
forPlugin(...), forMod(...), and the other bootstrap helpers belong to the
platform module only. common should consume the resulting runtime, not create
it.
Example Shared Service
This is the typical multi-platform pattern: the shared layer receives one runtime from the adapter layer and builds everything else on top of it.
What Works Well In Common
The magicutils-core path is especially good for:
MagicRuntimeConfigManagerLoggerCore- optional
LanguageManager - runtime-managed HTTP or WebSocket clients
- shared placeholder evaluation
- reloadable services built with
bindConfig(...)
If your modules already target multiple platforms, this is usually where the majority of the code should live.
The Platform interface
Platform is the single abstraction the shared layer depends on instead of any
one server API. Beyond the player events below, it exposes:
configDir()— base directory for configs and language files.logger()— aPlatformLogger(info/warn/error/debug,isDebugEnabled()).console()— the console as anAudience;onlinePlayers()— online players asAudiences.runOnMain(task)— run on the platform's main thread (immediate on proxies that have none);runForAudience(audience, task)— run on the region owning that audience (Folia) or the main thread.isMainThread()andthreadContext()— the currentThreadContext(MAIN,EVENT_LOOP,NETWORK,WORKER,UNKNOWN); usethreadContext().isBlockingSensitive()before doing blocking I/O.scheduler()— aTaskSchedulerwithcpu(),io(),async()executors andrunIo(...)/supplyIo(...)helpers returningCompletableFutures, so you keep disk and network work off the main thread.
Audience normalises a message target: send(Component), id(), name(),
hasPermission(node). ListenerSubscription is the AutoCloseable handle
returned by every subscribe* call; close it to unsubscribe (or use
ListenerSubscription.noop()).
Player Events
Platform provides normalized player events, so shared code can react to
players without importing Bukkit, Fabric, or Velocity types. Coverage differs by
platform: lifecycle (join/leave) and locale events are available everywhere;
chat/command message events are available on Bukkit, BungeeCord, Velocity, and
Fabric, but not on NeoForge (there subscribePlayerMessages returns a no-op
subscription).
Player Lifecycle
Subscribe to join/leave events:
PlayerLifecycle contains:
playerId()— player UUID (when available)playerName()— display/login nametype()—JOINorLEAVE
Player Messages
Subscribe to chat messages and commands:
PlayerMessage contains:
playerId()— player UUID (when available)playerName()— display/login namemessage()— raw chat content or command linetype()—CHATorCOMMAND
Both subscriptions return ListenerSubscription which can be closed to
unsubscribe. Both records expose isValid() for null-safety checks.
Player Locale
Subscribe to the language a player's client reports, so you can react when it changes:
PlayerLocale contains playerId(), playerName(), and localeTag() (a BCP-47
tag such as en_US). The lang module uses this internally for
bindClientLocaleSync(...); subscribe directly only when you need the raw event.
What Should Stay Out Of Common
Try not to leak platform-specific APIs into the shared layer.
Avoid putting these directly into common:
- Bukkit
JavaPlugin, Fabric callbacks, Velocity annotations, NeoForge events - platform command dispatcher registration
- direct calls to platform plugin managers or server APIs
- external bridge setup that only exists on one runtime
Keep those in the platform module and pass only the shared abstractions downward.
If You Really Need A Custom Platform
That is a separate case from normal common code.
If you are actually building a new adapter around Platform,
ShutdownHookRegistrar, or your own bootstrap path, use the Runtime guide and
the platform API as the source of truth, but keep that adapter layer small and
let the feature logic remain in shared services.