Getting started

Dependency & Service Discovery

NobleSteeds 2.0.0 ships public API contract 1.0 inside its single server JAR. Use provided/compileOnly scope; never shade NobleSteeds or copy API classes into your addon. Bukkit must load the same API classes as the registered service.

Get the NobleSteeds server JAR
Install your obtained JAR into local Maven
mvn install:install-file -Dfile=NobleSteeds-2.0.0.jar -DgroupId=com.rismr1 -DartifactId=NobleSteeds -Dversion=2.0.0 -Dpackaging=jar
Maven provided dependency
<dependency>
  <groupId>com.rismr1</groupId>
  <artifactId>NobleSteeds</artifactId>
  <version>2.0.0</version>
  <scope>provided</scope>
</dependency>
Gradle local JAR
dependencies {
    compileOnly(files("libs/NobleSteeds-2.0.0.jar"))
}
Required versus optional plugin dependency
name: ExampleMountAddon
version: 1.0.0
main: example.ExampleMountAddon
api-version: "1.18"
depend: [NobleSteeds]

# If genuinely optional: softdepend: [NobleSteeds]
# Guard missing-plugin/API class access in optional integrations.
Minimal required-integration plugin
package example;

import com.rismr1.noblesteeds.api.NobleSteedsApi;
import com.rismr1.noblesteeds.api.NobleSteedsApiProvider;
import org.bukkit.plugin.java.JavaPlugin;

public final class ExampleMountAddon extends JavaPlugin {
    @Override
    public void onEnable() {
        NobleSteedsApi api = NobleSteedsApiProvider.get().orElse(null);
        if (api == null || !NobleSteedsApi.API_VERSION.equals(api.version())
                || !api.capabilities().contains("mount-queries")) {
            getLogger().warning("Compatible NobleSteeds API is unavailable.");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }
        getLogger().info("NobleSteeds API " + api.version() + " available.");
    }
}

NobleSteedsApiProvider.get() wraps Bukkit.getServicesManager().load(NobleSteedsApi.class). Discover after enable; check version/capabilities, and stop using service references after disable. Required dependencies provide load order; optional integrations need guarded class loading and lifecycle handling.

Back to top ↑

API reference

Version & Capability Contract

version() returns API contract 1.0, not plugin release 2.0.0. capabilities() returns the immutable supported names below.

CapabilitySurface
mount-queriesMount records
mount-actionsRename, call, stable, store, restore
gui-openingPermission-checked menus
persistent-rewardsIdempotent rewards and claims
cached-placeholdersMemory-only placeholder values
mount-eventsPublic events
market-queriesRead-only listings
shop-queriesRead-only templates
payout-queriesRead-only ledger
stable-queriesNamed presets
lineage-queriesRecorded ancestry
Back to top ↑

API reference

Snapshots, Queries & Identity

SignatureResult
mount(UUID mountId)Optional<MountSnapshot> using persistent logical ID
mounts(UUID owner)Immutable owned mount snapshots
pendingRewards(UUID owner)Uncompleted pending/review rewards
listings()Read-only market listings
shopEntries()Read-only shop templates
payouts(UUID owner)Seller payment ledger views
stables(UUID owner)Named presets, not every individually assigned location
lineage(UUID mountId)Optional lineage; parent fields may be null/unknown
placeholder(UUID player, String parameter)Cached parameter lookup without surrounding %noblesteeds_...%

Reads are asynchronous-safe immutable cached snapshots: no database queries, chunk loads or spawning. Cache refresh is every 20 ticks and after API operations. Separate calls are not an atomic transaction. Pass valid UUIDs and enforce your addon’s authorization before displaying global server data.

Field / conceptRule
mountId()Persistent logical identity for actions and UUID placeholders
entityId()Physical entity UUID; changes on restore/revive. /nsa inspect expects this saved physical UUID
slot()Owner position; can change on compaction/transfers
type / variantSeven type names; horse/llama colors use Bukkit enums
speed / jump / health0–100 percentages, not raw attributes or blocks per second
breedingQualityNullable when ineligible/disabled
Stored rewardsMay have no physical entity and empty world; placeholder entity ID is not an existing entity
LineageUnknown parents can be null; no invented ancestry
Payout state / mailId / queueIdLedger progress/delivery references, not attachment-claim proof
Memory-only query fragments
NobleSteedsApi api = NobleSteedsApiProvider.get().orElseThrow();
var owned = api.mounts(playerUuid);
var queued = api.pendingRewards(playerUuid);
var presets = api.stables(playerUuid);
String activeCount = api.placeholder(playerUuid, "claimed");

owned.forEach(mount -> {
    UUID logicalId = mount.mountId();
    UUID physicalId = mount.entityId();
    // Do not use Bukkit entity/chunk APIs off-thread.
});
Back to top ↑

API reference

Actions, Permissions & Menus

MethodChecks
call(Player, UUID mountId)Owner online, noblesteeds.call, living Active unlisted, cooldown/lock
sendToStable(Player, UUID mountId)Owner online, noblesteeds.stable.send, eligible state and assigned world/location
rename(Player, UUID mountId, String name)noblesteeds.rename, Active claimed record (including unloaded), bounded nonblank name
store(Player, UUID mountId)noblesteeds.storage.deposit, living unlisted Active mount, Stored capacity
restore(Player, UUID mountId)noblesteeds.storage.withdraw, living unlisted Stored mount, Active capacity
openMenu(Player, NobleSteedsApi.Menu)Online and menu permission; MARKET/SHOP usable
grantMount(Plugin source, RewardRequest)Enabled source, validated persistent request; never bypass capacity
claimReward(Player, UUID rewardId, MountDestination)Online recipient, noblesteeds.rewards.claim, ownership, state/capacity

Actions return CompletableFuture<OperationResult> and do not grant administrator authority. Ownership/state/permissions are rechecked around async work and events. Command-entry configuration is not necessarily every GUI/API-entry gate; disabling a command alone is not blanket integration disablement.

MenuPermission
ACTIVEnoblesteeds.manage
STORAGEnoblesteeds.storage
MARKETnoblesteeds.market
SHOPnoblesteeds.shop
REWARDSnoblesteeds.rewards.claim
Action fragments
api.mounts(player.getUniqueId()).stream()
    .filter(mount -> mount.status().equals("ACTIVE"))
    .findFirst()
    .ifPresent(mount -> api.call(player, mount.mountId()));

api.openMenu(player, NobleSteedsApi.Menu.STORAGE)
    .thenAccept(result -> {
        // Inspect status; opening a menu is not granting a mount.
    });
Back to top ↑

Integration workflows

Persistent Quest Rewards & Retries

Persist one rewardKey per earned reward. RewardRequest includes the key, recipient UUID, MountTemplate, ACTIVE/STORED destination and optional sourceLabel. Keys are namespaced by source plugin name; keep that name and your earned completion identity stable across retries.

Fragment inside an enabled quest plugin
NobleSteedsApi api = NobleSteedsApiProvider.get().orElseThrow();
RewardRequest request = new RewardRequest(
    "dragon-hunt:" + questCompletionId,
    playerUuid,
    new MountTemplate("HORSE", "BLACK", "Dragon Runner", 80, 70, 75, 60.0),
    MountDestination.STORED,
    "Dragon Hunt");

api.grantMount(this, request).thenAccept(result -> {
    switch (result.status()) {
        case SUCCESS, ALREADY_GRANTED -> markRewardAccepted(questCompletionId);
        case PENDING_CAPACITY -> markRewardAccepted(questCompletionId);
        case QUEUE_FULL -> retainUndeliveredQuestReward(questCompletionId);
        default -> logRewardOutcome(result);
    }
});

The callback methods are yours, not NobleSteeds methods; this is your enabled Bukkit plugin instance. Persist earned/accepted state. Retry the exact request after timeout: changed recipient/template/destination returns REQUEST_CONFLICT. Never generate a fresh key to retry.

ValueConstraint
rewardKey1–160: letters/digits/underscore/dot/colon/slash/hyphen
sourceLabelMax 128; null becomes empty; blank uses source name
typeHORSE, DONKEY, MULE, LLAMA, TRADER_LLAMA, SKELETON_HORSE, ZOMBIE_HORSE
variantHorse/llama color enum; null normalizes BROWN; other types ignore color
nameNonblank visual text; max 512 raw characters / 64 visible code points
speed / jump / healthFinite 0–100
breedingQualitynull or finite 0–100 for eligible types; null for Mules/undead
sourceNon-null enabled plugin
  • SUCCESS / ALREADY_GRANTED record delivery. PENDING_CAPACITY is durable acceptance without a mount delivered yet.
  • QUEUE_FULL, invalid input and cancelled acceptance are not acceptance. A non-null reward ID alone is not proof.
  • Active grants use an online recipient’s position; offline Active grants queue. Stored delivery can work offline using default capacity.
  • Pending rewards never expire, appear in both collection menus and can be claimed to Active or Stored.
  • The queue cap defaults to 100. Rejected new requests remain the source addon’s responsibility.
  • Completed identities remain persistent; exact grant/claim retries do not create another mount.
Player queued reward instructions
Back to top ↑

Integration workflows

Results, Threading & Recovery

OperationResult carries operationId, status and nullable mountId/rewardId. delivered() checks SUCCESS or ALREADY_GRANTED; meaning still depends on the action. Successful menu opening is not reward delivery, and payout queries cannot change payment state.

StatusHandling
SUCCESSOperation completed
ALREADY_GRANTEDExact delivered reward retry; no extra grant
PENDING_CAPACITYDurably queued, not yet delivered
LIMIT_REACHEDAction lacks destination capacity
QUEUE_FULLNew request rejected; retain source responsibility
INVALID_STATSInvalid mount template
INVALID_REQUESTInvalid source, player, request/name/state
REQUEST_CONFLICTSame identity, different reward parameters
NOT_FOUNDMount/location/reward absent
NOT_OWNEROwnership mismatch
NO_PERMISSIONPermission missing
BUSYListing/operation/cooldown guard
DISABLEDUnavailable/shutdown
CANCELLEDPre-action rejected
FAILEDInspect outcome before retry
REVIEW_REQUIREDInvestigate uncertain delivery; do not grant again

Off-thread actions marshal onto the server scheduler. Normal completions are on the server thread; immediate shutdown rejection can complete on the calling thread. Never block the server thread with join() or get(). If you change callback executors, schedule Bukkit work explicitly on the server thread.

Shutdown completes pending futures with DISABLED even though a request may already be durable. After restart inspect/retry its original key. Interrupted DELIVERING records reconcile against saved ownership; uncertain outcomes become REVIEW_REQUIRED rather than spawn twice.

Back to top ↑

Integration workflows

Public Events & Guarantees

EventGuarantees / limits
MountActionEventCancellable API pre-action only, not every command/GUI action. GRANT, CLAIM, RENAME, RELOCATE, STORE, RESTORE. getMountId(), getActor(), getAction(); reward actions can identify prospective reward identity.
MountChangedEventNon-cancellable persisted MountSnapshot change; getMount(), getAction(), currently RECORD_SAVED. Not transactional sale/payment/attachment claiming.
MountOperationCompletedEventNon-cancellable server-thread successful API GRANT, CLAIM, RENAME, CALL, STABLE, STORE, RESTORE, OPEN_MENU. getAction()/getResult(). No queued, rejected or ALREADY_GRANTED notification.
Listener fragments
@EventHandler(ignoreCancelled = true)
public void beforeApiMove(MountActionEvent event) {
    if (event.getAction() == MountActionEvent.Action.RELOCATE
            && shouldBlockMove(event.getActor(), event.getMountId())) {
        event.setCancelled(true);
    }
}

@EventHandler
public void afterApiOperation(MountOperationCompletedEvent event) {
    OperationResult result = event.getResult();
    // Observe success; retain durable source reward-key tracking.
}

Register the Bukkit listener and supply your own shouldBlockMove implementation. Pre-events must not perform irreversible work assuming success. Reentrant acceptance and ownership/capacity rechecks still apply. Completion events do not replace future results or durable reward bookkeeping.

Back to top ↑

Reference & support

Placeholder Integration

Bundled PlaceholderAPI delegates to api.placeholder(ownerUuid, parameter). Direct API parameters are claimed, stored, mount_1_name etc., without %noblesteeds_ prefix/suffix. See the wiki for every external placeholder and player-context rule.

Complete PlaceholderAPI setup and list

Counts without known context are 0; unknown limits/missing mounts/ineligible quality are empty; unknown keys return null. Reads use immutable cached data. Offline owners retain cached permission limits where available, otherwise defaults. Trusted configured-text parsing is opt-in and never executes player names/descriptions as external placeholders.

Back to top ↑

Reference & support

Complete Public Contract Reference

These 12 top-level public types and their nested records/enums are copied from 2.0.0 source. Internal service/storage/payout managers are excluded. Compile against the obtained plugin JAR; do not copy these classes into your addon.

ApiViews • Public API
ApiViews — complete public source
package com.rismr1.noblesteeds.api;

import java.util.UUID;

public final class ApiViews {
    private ApiViews() { }
    public record Listing(UUID listingId, UUID mountId, UUID seller, String name, double money, String itemPrice, String description) { }
    public record ShopEntry(UUID entryId, MountTemplate mount, double money, String itemPrice, boolean enabled) { }
    public record Payout(UUID transactionId, UUID seller, String state, double money, String itemPrice, long createdAt, String mailId, String queueId) { }
    public record Stable(String name, String world, double x, double y, double z) { }
    public record Parent(UUID mountId, String name, Double breedingQuality) { }
    public record Lineage(Parent mother, Parent father) { }
}
MountDestination • Public API
MountDestination — complete public source
package com.rismr1.noblesteeds.api;

public enum MountDestination { ACTIVE, STORED }
MountSnapshot • Public API
MountSnapshot — complete public source
package com.rismr1.noblesteeds.api;

import java.util.List;
import java.util.UUID;

public record MountSnapshot(UUID mountId, UUID entityId, UUID ownerId, int slot, String name,
                            String type, String variant, String status, String rarity,
                            double speed, double jump, double health, Double breedingQuality,
                            String world, double x, double y, double z, List<String> history) {
    public MountSnapshot { history = List.copyOf(history); }
}
MountTemplate • Public API
MountTemplate — complete public source
package com.rismr1.noblesteeds.api;

import java.util.Objects;

public record MountTemplate(String type, String variant, String name, double speed,
                            double jump, double health, Double breedingQuality) {
    public MountTemplate {
        Objects.requireNonNull(type); Objects.requireNonNull(name);
        type = type.toUpperCase(java.util.Locale.ROOT);
        variant = variant == null ? "BROWN" : variant.toUpperCase(java.util.Locale.ROOT);
    }
}
NobleSteedsApi • Public API
NobleSteedsApi — complete public source
package com.rismr1.noblesteeds.api;

import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

public interface NobleSteedsApi {
    String API_VERSION = "1.0";
    enum Menu { ACTIVE, STORAGE, MARKET, SHOP, REWARDS }
    String version();
    Set<String> capabilities();
    Optional<MountSnapshot> mount(UUID mountId);
    List<MountSnapshot> mounts(UUID owner);
    List<PendingMountReward> pendingRewards(UUID owner);
    List<ApiViews.Listing> listings();
    List<ApiViews.ShopEntry> shopEntries();
    List<ApiViews.Payout> payouts(UUID owner);
    List<ApiViews.Stable> stables(UUID owner);
    Optional<ApiViews.Lineage> lineage(UUID mountId);
    CompletableFuture<OperationResult> grantMount(Plugin source, RewardRequest request);
    CompletableFuture<OperationResult> claimReward(Player player, UUID rewardId, MountDestination destination);
    CompletableFuture<OperationResult> rename(Player player, UUID mountId, String name);
    CompletableFuture<OperationResult> call(Player player, UUID mountId);
    CompletableFuture<OperationResult> sendToStable(Player player, UUID mountId);
    CompletableFuture<OperationResult> store(Player player, UUID mountId);
    CompletableFuture<OperationResult> restore(Player player, UUID mountId);
    CompletableFuture<OperationResult> openMenu(Player player, Menu menu);
    String placeholder(UUID player, String parameter);
}
NobleSteedsApiProvider • Public API
NobleSteedsApiProvider — complete public source
package com.rismr1.noblesteeds.api;

import org.bukkit.Bukkit;
import java.util.Optional;

public final class NobleSteedsApiProvider {
    private NobleSteedsApiProvider() { }
    public static Optional<NobleSteedsApi> get() {
        return Optional.ofNullable(Bukkit.getServicesManager().load(NobleSteedsApi.class));
    }
}
OperationResult • Public API
OperationResult — complete public source
package com.rismr1.noblesteeds.api;

import java.util.UUID;

public record OperationResult(UUID operationId, Status status, UUID mountId, UUID rewardId) {
    public enum Status {
        SUCCESS, ALREADY_GRANTED, PENDING_CAPACITY, LIMIT_REACHED, QUEUE_FULL,
        INVALID_STATS, INVALID_REQUEST, REQUEST_CONFLICT, NOT_FOUND, NOT_OWNER,
        NO_PERMISSION, BUSY, DISABLED, CANCELLED, FAILED, REVIEW_REQUIRED
    }
    public boolean delivered() { return status == Status.SUCCESS || status == Status.ALREADY_GRANTED; }
}
PendingMountReward • Public API
PendingMountReward — complete public source
package com.rismr1.noblesteeds.api;

import java.util.UUID;

public record PendingMountReward(UUID rewardId, UUID recipient, String source, MountTemplate mount,
                                 MountDestination requestedDestination, String state, long createdAt) { }
RewardRequest • Public API
RewardRequest — complete public source
package com.rismr1.noblesteeds.api;

import java.util.Objects;
import java.util.UUID;

public record RewardRequest(String rewardKey, UUID recipient, MountTemplate mount,
                            MountDestination destination, String sourceLabel) {
    public RewardRequest {
        Objects.requireNonNull(rewardKey); Objects.requireNonNull(recipient);
        Objects.requireNonNull(mount); Objects.requireNonNull(destination);
        sourceLabel = sourceLabel == null ? "" : sourceLabel;
    }
}
MountActionEvent • Events
MountActionEvent — complete public source
package com.rismr1.noblesteeds.api.event;

import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.UUID;

public final class MountActionEvent extends Event implements Cancellable {
    public enum Action { GRANT, CLAIM, RENAME, RELOCATE, STORE, RESTORE }
    private static final HandlerList HANDLERS = new HandlerList();
    private final UUID mountId;
    private final UUID actor;
    private final Action action;
    private boolean cancelled;
    public MountActionEvent(UUID mountId, UUID actor, Action action) {
        this.mountId = mountId; this.actor = actor; this.action = action;
    }
    public UUID getMountId() { return mountId; }
    public UUID getActor() { return actor; }
    public Action getAction() { return action; }
    public boolean isCancelled() { return cancelled; }
    public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
    public HandlerList getHandlers() { return HANDLERS; }
    public static HandlerList getHandlerList() { return HANDLERS; }
}
MountChangedEvent • Events
MountChangedEvent — complete public source
package com.rismr1.noblesteeds.api.event;

import com.rismr1.noblesteeds.api.MountSnapshot;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

public final class MountChangedEvent extends Event {
    private static final HandlerList HANDLERS = new HandlerList();
    private final MountSnapshot mount;
    private final String action;
    public MountChangedEvent(MountSnapshot mount, String action) { this.mount = mount; this.action = action; }
    public MountSnapshot getMount() { return mount; }
    public String getAction() { return action; }
    public HandlerList getHandlers() { return HANDLERS; }
    public static HandlerList getHandlerList() { return HANDLERS; }
}
MountOperationCompletedEvent • Events
MountOperationCompletedEvent — complete public source
package com.rismr1.noblesteeds.api.event;

import com.rismr1.noblesteeds.api.OperationResult;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

public final class MountOperationCompletedEvent extends Event {
    public enum Action { GRANT, CLAIM, RENAME, CALL, STABLE, STORE, RESTORE, OPEN_MENU }
    private static final HandlerList HANDLERS = new HandlerList();
    private final Action action;
    private final OperationResult result;
    public MountOperationCompletedEvent(Action action, OperationResult result) { this.action = action; this.result = result; }
    public Action getAction() { return action; }
    public OperationResult getResult() { return result; }
    public HandlerList getHandlers() { return HANDLERS; }
    public static HandlerList getHandlerList() { return HANDLERS; }
}
Back to top ↑

Reference & support

Integration Checklist & Support

  • Compile against 2.0.0 and discover API 1.0 with required capabilities.
  • Test missing-plugin, enable/disable and stale-reference handling.
  • Exercise off-thread work without blocking; test permission/ownership, busy/listed/dead/stored state and capacity.
  • Persist earned identity and retry exactly; cover ALREADY_GRANTED, PENDING_CAPACITY, QUEUE_FULL and REQUEST_CONFLICT.
  • Test cancellation, interrupted delivery and review without new identities or duplicate mounts.
  • Verify logical identity survives restore/revive while entity UUIDs/slots change.
  • Treat global market/shop/payout snapshots as read-only information.

Use the official support guide with a minimal reproduction and sanitized logs. Never publish credentials or private data.

ImagineCraft Plugin SupportBack to NobleSteeds 2.0.0
Back to top ↑