Skip to content

Core Services

DarkBladeDev edited this page Jan 9, 2026 · 3 revisions

Core Services (Registration and Consumption)

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).

Quick map

  • 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 with IllegalArgumentException).
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]
Loading

1) Registration process

1.1 Prerequisites and dependencies

Requirement: the service type must live in core-api

MultiBlockEngine validates that serviceType.getClassLoader() is the same classloader as core-api.

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-api module and published with the engine.

Typical addon dependencies

  • compileOnly for Paper (for the Bukkit/Paper API).
  • compileOnly for the engine’s core-api.jar (for AddonContext, contracts, and service types).

Real example (from the MBE-Storage addon): MBE-Storage/build.gradle

1.2 Register a new service (engine internal registry)

Use: when you want other addons (loaded by MBE) to resolve your implementation via getService.

API:

Rules:

  • A serviceType can have only one provider.
  • If another addon already registered that type, IllegalStateException is thrown.

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 StorageRegistry already 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.

1.3 Expose a service as a “public service” (Bukkit Services)

Use: when you want external plugins (not necessarily addons) to consume it via Bukkit.getServicesManager().

API:

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);
    }
}

1.4 Required and optional configurations

For the services system itself:

  • There is no specific required YAML configuration.
  • Availability depends on the provider state:

2) Service consumption

2.1 Available methods

a) Resolve from the addon context

API:

Return:

  • T if it exists and the provider is ENABLED.
  • null if it does not exist or is blocked by state.

b) Resolve “public” services from Bukkit

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.

2.2 Required and optional parameters

getService

  • Required: serviceType.
  • Optional: none.

exposeService

  • Required: api (service type), implementation.
  • Optional: priority (defaults to ServicePriority.Normal).

2.3 Error handling and exceptions

Common errors

Recommended consumption pattern

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...
    }
}

2.4 Best practices and recommendations

  • 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.* outside core-api).

3) Built-in core services

These services are registered by the plugin at startup (before loading addons) in MultiBlockEngine.java.

3.1 Complete list

  1. ItemService
  2. ItemStackBridge
  3. StorageRegistry
  4. PersistentStorageService
  5. LocaleProvider
  6. I18nService
  7. WrenchDispatcher

3.2 Service details

ItemService

Purpose

  • Registration and creation of items in the MBE ecosystem (definitions + associated data).

API

Methods

  • ItemRegistry registry()
    • Returns: definition registry (ItemDefinition).
  • ItemFactory factory()
    • Returns: factory to create instances (ItemInstance) from definitions.

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);
    }
}

ItemStackBridge

Purpose

  • Convert between ItemInstance (MBE model) and ItemStack (Bukkit) while preserving the data model.

API

Methods

  • ItemStack toItemStack(ItemInstance instance)
    • Input: ItemInstance.
    • Output: ItemStack with PDC/data applied (if applicable).
  • ItemInstance fromItemStack(ItemStack stack)
    • Input: ItemStack.
    • Output: ItemInstance or null if it does not represent an MBE item.

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()));
    }
}

StorageRegistry

Purpose

  • Registration of StorageService factories by type ("memory", etc.) and creation of storage instances.

API

Methods

  • void registerFactory(String type, StorageServiceFactory factory)
    • Input: type (string), factory.
  • StorageService create(String type, StorageDescriptor descriptor)
    • Input: type, descriptor.
    • Output: a StorageService implementation.

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);
    }
}

PersistentStorageService

Purpose

  • Durable persistence (WAL) with namespaces/domains/stores; foundation for storing addon and core data.

API

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())
        );
    }
}

LocaleProvider

Purpose

  • Resolve a Locale for a CommandSender/player for i18n.

API

Methods

  • Locale localeOf(CommandSender sender)
  • Locale localeOf(UUID playerId)
  • Locale fallbackLocale()

I18nService

Purpose

  • Resolve localized messages (MessageKey) with parameters.

API

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")));
    }
}

WrenchDispatcher

Purpose

  • Register and execute “wrench” actions (standardized interactions) on blocks/entities the engine recognizes.

API

Methods

  • void registerAction(String key, WrenchInteractable interactable)
    • Requires a namespaced key (<namespace: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;
        });
    }
}

Clone this wiki locally