-
Notifications
You must be signed in to change notification settings - Fork 0
Core Services
This page documents how to register and consume core services (and public services) from MultiBlockEngine addons.
The services API is centered around the AddonContext.java contract and relies on the engine’s internal service registry (AddonServiceRegistry.java) and, optionally, on Bukkit’s services manager for public exposure (AddonManager.java).
-
Consume core services:
context.getService(Service.class). -
Register a service for other addons to consume:
context.registerService(Service.class, impl). -
Expose a service as a Bukkit Service (visible outside the MBE ecosystem):
context.exposeService(Service.class, impl, priority). -
Key rule: the service type (the interface) must exist in
core-api(otherwise it fails withIllegalArgumentException).
flowchart TD
A[Addon onLoad/onEnable] --> B{"context.getService(X)"}
B -->|provider ENABLED| C[X available]
B -->|provider NOT ENABLED or not registered| D[null]
A --> E{"context.registerService(X, impl)"}
E -->|already registered by another addon| F[IllegalStateException]
A --> G{"context.exposeService(X, impl)"}
G -->|exposure happens at end of onEnable| H[Bukkit Services]
G -->|fails| I[Addon fails in ENABLE]
MultiBlockEngine validates that serviceType.getClassLoader() is the same classloader as core-api.
- Validation (register/resolve): AddonServiceRegistry.java
- Context: AddonContext.java
Practical implication:
- You can register/consume only interfaces that are in the engine API.
- If you need a “new service” between addons, that interface must be added to the
core-apimodule and published with the engine.
-
compileOnlyfor Paper (for the Bukkit/Paper API). -
compileOnlyfor the engine’score-api.jar(forAddonContext, contracts, and service types).
Real example (from the MBE-Storage addon): MBE-Storage/build.gradle
Use: when you want other addons (loaded by MBE) to resolve your implementation via getService.
API:
-
context.registerService(Class<T> serviceType, T service)- Definition: AddonContext.java
- Implementation: SimpleAddonContext.java
Rules:
- A
serviceTypecan have only one provider. - If another addon already registered that type,
IllegalStateExceptionis thrown.- Implemented in: AddonServiceRegistry.register
Example (common case: registering an existing service type already defined in core-api):
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.addon.AddonException;
import com.darkbladedev.engine.api.addon.MultiblockAddon;
import com.darkbladedev.engine.api.storage.StorageRegistry;
public final class MyAddon implements MultiblockAddon {
@Override
public void onLoad(AddonContext context) throws AddonException {
StorageRegistry registry = context.getService(StorageRegistry.class);
if (registry == null) {
context.getLogger().warn("StorageRegistry not avaliable; skipping");
return;
}
context.registerService(StorageRegistry.class, registry);
}
}Notes:
- In this example the registration is redundant (a core
StorageRegistryalready exists). It is included to illustrate the API. - The real-world case is usually registering a service that the core already defines but whose implementation you want to provide.
Use: when you want external plugins (not necessarily addons) to consume it via Bukkit.getServicesManager().
API:
-
context.exposeService(Class<T> api, T implementation, ServicePriority priority)- Definition: AddonContext.java
- Queued: SimpleAddonContext.exposeService
- Actual execution (at the end of the addon’s
onEnable): AddonManager.enableAddons
Important details:
- Exposure happens after your
onEnable()finishes, during the engine enable process. - If exposure fails, the addon is marked as failed in ENABLE.
Example:
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.addon.AddonException;
import com.darkbladedev.engine.api.addon.MultiblockAddon;
import org.bukkit.plugin.ServicePriority;
public final class MyAddon implements MultiblockAddon {
@Override
public void onLoad(AddonContext context) throws AddonException {
context.exposeService(MyCoreApiService.class, new MyCoreApiServiceImpl(context), ServicePriority.High);
}
}For the services system itself:
- There is no specific required YAML configuration.
- Availability depends on the provider state:
- Resolution is blocked if the provider addon is not
ENABLED. - Implemented in: AddonServiceRegistry.resolveIfEnabled
- Resolution is blocked if the provider addon is not
API:
-
<T> T context.getService(Class<T> serviceType)- Definition: AddonContext.java
- Implementation: SimpleAddonContext.getService
Return:
-
Tif it exists and the provider isENABLED. -
nullif it does not exist or is blocked by state.
If an addon exposes a service, another plugin (or addon) can use Bukkit.getServicesManager() like any other Bukkit service.
This flow is enabled via AddonContext.exposeService and implemented in the engine by BukkitServiceBridge.java.
- Required:
serviceType. - Optional: none.
- Required:
api(service type),implementation. - Optional:
priority(defaults toServicePriority.Normal).
-
IllegalArgumentExceptionwhen registering/resolving a service type that does not belong tocore-api.- Guidance message: AddonServiceRegistry.validateCoreApiType
-
IllegalStateExceptionwhen registering a type already registered by another provider. -
nullwhen consuming services whose provider is not yetENABLED.
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.addon.AddonException;
import com.darkbladedev.engine.api.addon.MultiblockAddon;
import com.darkbladedev.engine.api.i18n.I18nService;
public final class MyAddon implements MultiblockAddon {
@Override
public void onEnable() throws AddonException {
// si estás dentro de una clase addon, guarda el context en onLoad.
}
public void doSomething(AddonContext context) {
I18nService i18n = context.getService(I18nService.class);
if (i18n == null) {
context.getLogger().warn("I18nService not avaliable");
return;
}
// use i18n...
}
}- Resolve services in
onEnable()if they depend on other addons;onLoad()is valid for core services. - Treat
getService(...)as nullable; avoid assuming availability. - Do not register services with types outside
core-api. - For persistence: use
PersistentStorageService+persistence.forAddon(context)and create domains/stores per function. - Avoid coupling to engine-internal classes (package
com.darkbladedev.engine.*outsidecore-api).
These services are registered by the plugin at startup (before loading addons) in MultiBlockEngine.java.
ItemServiceItemStackBridgeStorageRegistryPersistentStorageServiceLocaleProviderI18nServiceWrenchDispatcher
Purpose
- Registration and creation of items in the MBE ecosystem (definitions + associated data).
API
- Contract: ItemService.java
Methods
-
ItemRegistry registry()- Returns: definition registry (
ItemDefinition).
- Returns: definition registry (
-
ItemFactory factory()- Returns: factory to create instances (
ItemInstance) from definitions.
- Returns: factory to create instances (
Usage example (register an addon item)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.item.ItemDefinition;
import com.darkbladedev.engine.api.item.ItemKeys;
import com.darkbladedev.engine.api.item.ItemService;
import java.util.Map;
public final class MyItems {
public static void register(AddonContext context) {
ItemService items = context.getService(ItemService.class);
if (items == null) {
context.getLogger().warn("ItemService not avaliable");
return;
}
ItemDefinition def = new ItemDefinition() {
private final com.darkbladedev.engine.api.item.ItemKey key = ItemKeys.of("myaddon:my_item", 0);
@Override
public com.darkbladedev.engine.api.item.ItemKey key() {
return key;
}
@Override
public String displayName() {
return "My Item";
}
@Override
public Map<String, Object> properties() {
return Map.of(
"material", "DIAMOND",
"unstackable", true
);
}
};
items.registry().register(def);
}
}Purpose
- Convert between
ItemInstance(MBE model) andItemStack(Bukkit) while preserving the data model.
API
- Contract: ItemStackBridge.java
Methods
-
ItemStack toItemStack(ItemInstance instance)- Input:
ItemInstance. - Output:
ItemStackwith PDC/data applied (if applicable).
- Input:
-
ItemInstance fromItemStack(ItemStack stack)- Input:
ItemStack. - Output:
ItemInstanceornullif it does not represent an MBE item.
- Input:
Usage example (read data from the item in hand)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.item.ItemInstance;
import com.darkbladedev.engine.item.bridge.ItemStackBridge;
import org.bukkit.entity.Player;
public final class ItemDebug {
public static void inspectHand(AddonContext context, Player player) {
ItemStackBridge bridge = context.getService(ItemStackBridge.class);
if (bridge == null) return;
ItemInstance inst = bridge.fromItemStack(player.getInventory().getItemInMainHand());
if (inst == null) {
context.getLogger().info("No es un item MBE");
return;
}
context.getLogger().info("Item MBE detectado", com.darkbladedev.engine.api.logging.LogKv.kv("id", inst.definition().key().id().toString()));
}
}Purpose
- Registration of
StorageServicefactories by type ("memory", etc.) and creation of storage instances.
API
- Contract: StorageRegistry.java
Methods
-
void registerFactory(String type, StorageServiceFactory factory)- Input:
type(string),factory.
- Input:
-
StorageService create(String type, StorageDescriptor descriptor)- Input:
type,descriptor. - Output: a
StorageServiceimplementation.
- Input:
Example (register addon storage factory)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.storage.StorageRegistry;
import com.darkbladedev.engine.api.storage.StorageServiceFactory;
public final class MyStorage {
public static void register(AddonContext context, StorageServiceFactory factory) {
StorageRegistry registry = context.getService(StorageRegistry.class);
if (registry == null) return;
registry.registerFactory("mytype", factory);
}
}Purpose
- Durable persistence (WAL) with namespaces/domains/stores; foundation for storing addon and core data.
API
- Contract: PersistentStorageService.java
- Addon helper: AddonStorage.java
Main methods
StorageNamespace namespace(String namespace)StorageMetrics metrics()StorageRecoveryReport recover()-
StorageFlushReport flush()/flushAsync() AddonStorage forAddon(AddonContext context[, Options options])
Example (create an addon-namespaced store)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.persistence.AddonStorage;
import com.darkbladedev.engine.api.persistence.PersistentStorageService;
import com.darkbladedev.engine.api.persistence.StorageRecordMeta;
import com.darkbladedev.engine.api.persistence.StorageSchema;
import com.darkbladedev.engine.api.persistence.StorageStore;
import java.nio.charset.StandardCharsets;
public final class MyAddonData {
public static StorageStore open(AddonContext context) {
PersistentStorageService persistence = context.getService(PersistentStorageService.class);
if (persistence == null) return null;
AddonStorage addonStorage = persistence.forAddon(context);
StorageSchema schema = new StorageSchema() {
@Override public int schemaVersion() { return 1; }
@Override public StorageSchemaMigrator migrator() {
return (from, to, payload) -> {
if (from == to && to == 1) return payload;
throw new IllegalStateException("Unsupported migration: " + from + "->" + to);
};
}
};
return addonStorage.domain("example").store("settings", schema);
}
public static void writeHello(AddonContext context) {
StorageStore store = open(context);
if (store == null) return;
store.write(
"hello",
"world".getBytes(StandardCharsets.UTF_8),
StorageRecordMeta.now(context.getAddonId())
);
}
}Purpose
- Resolve a
Localefor aCommandSender/player for i18n.
API
- Contract: LocaleProvider.java
Methods
Locale localeOf(CommandSender sender)Locale localeOf(UUID playerId)Locale fallbackLocale()
Purpose
- Resolve localized messages (
MessageKey) with parameters.
API
- Contract: I18nService.java
Methods
LocaleProvider localeProvider()String tr(CommandSender sender, MessageKey key[, params...])void reload()
Example (message the player)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.i18n.I18nService;
import com.darkbladedev.engine.api.i18n.MessageKey;
import org.bukkit.entity.Player;
public final class Msgs {
public static void send(AddonContext context, Player player) {
I18nService i18n = context.getService(I18nService.class);
if (i18n == null) return;
player.sendMessage(i18n.tr(player, MessageKey.of("myaddon", "messages.hello")));
}
}Purpose
- Register and execute “wrench” actions (standardized interactions) on blocks/entities the engine recognizes.
API
- Contract: WrenchDispatcher.java
Methods
-
void registerAction(String key, WrenchInteractable interactable)- Requires a namespaced key (
<namespace:key>).
- Requires a namespaced key (
WrenchResult dispatch(WrenchContext context)
Example (register a custom wrench action)
import com.darkbladedev.engine.api.addon.AddonContext;
import com.darkbladedev.engine.api.wrench.WrenchDispatcher;
import com.darkbladedev.engine.api.wrench.WrenchInteractable;
public final class WrenchActions {
public static void register(AddonContext context) {
WrenchDispatcher wrench = context.getService(WrenchDispatcher.class);
if (wrench == null) return;
wrench.registerAction("myaddon:inspect", (ctx) -> {
if (ctx.player() != null) {
ctx.player().sendMessage("inspect!");
}
return com.darkbladedev.engine.api.wrench.WrenchResult.SUCCESS;
});
}
}