SLS-LITE exposes a versioned in-process Java API for trusted Velocity plugins.
API 1.2 supports capability discovery, immutable blueprint and instance
inspection, asynchronous lifecycle, installation, definition reload,
maintenance, exact-instance transfer, and player-matchmaking requests, queue
inspection/cancellation, ordered lifecycle events, and bounded namespaced
blueprint-readiness and operational-diagnostic contributions.
The JVM contract is enforced by a checked signature baseline. Breaking changes require a new API major version. Distribution verification covers checksums, public-package boundaries, and clean Maven/Gradle example compilation. See the API scope and compatibility policy.
This is not the Protocube HTTP API or an S4J endpoint. It cannot manage remote nodes, containers, or another SLS installation.
Build an extension against sls-lite-<version>-api.jar as a compile-only or
provided dependency. The API classifier contains only the supported public
package and its license; the full shaded plugin remains the runtime provider.
Declare sls-lite as a required Velocity plugin dependency so its plugin
instance is available before the extension initializes.
Server operators install only sls-lite-<version>.jar. The public API classes
are already bundled in that plugin. Do not place the -api.jar in Velocity's
plugins directory: it is a smaller compile-time artifact for extension
developers, comparable to an SDK. Keeping it separate prevents extension code
from accidentally importing SLS-LITE implementation packages and avoids using
the large shaded runtime JAR as a development dependency.
The accepted 1.2 JVM class, field, constructor, and method descriptors have a
checked SHA-256 baseline in
src/test/resources/api/public-api-1.2.sha256; the immutable 1.0 and 1.1
fingerprints are retained beside it. The build derives the current signature directly from
compiled class files (not reflection), writes the reviewable form to
target/api-signature/public-api-1.2.txt, and fails on any descriptor or
visibility change. Updating the current baseline requires an explicit
compatibility review; a passing hash does not authorize an undocumented API
change.
One verified build produces four relevant JARs:
| Artifact | Consumer |
|---|---|
sls-lite-<version>.jar |
Server operators; install this in Velocity. |
sls-lite-<version>-api.jar |
Extension compilers; never install it as a plugin. |
sls-lite-<version>-api-sources.jar |
IDE source attachment for the public API only. |
sls-lite-<version>-api-javadoc.jar |
Offline HTML reference for the public API only. |
The sources and Javadocs exclude net.slimelabs.slslite.api.internal. Javadoc
generation validates references, HTML, syntax, and accessibility with doclint
and fails the build on warnings. Release builds publish this reviewed artifact
set together. The plugin JAR is the only artifact an operator needs.
For Maven, install or resolve the reviewed artifact set and use a provided classifier dependency:
<dependency>
<groupId>net.slimelabs</groupId>
<artifactId>sls-lite</artifactId>
<version>${sls-lite.version}</version>
<classifier>api</classifier>
<scope>provided</scope>
</dependency>For Gradle Kotlin DSL, use the same classifier as a compile-only dependency:
dependencies {
compileOnly("net.slimelabs:sls-lite:$slsLiteVersion:api")
}When consuming the classifier directly from a GitHub Release rather than a Maven repository, keep the reviewed JAR outside the plugin output and use a compile-only file dependency:
dependencies {
compileOnly(files("libs/sls-lite-<version>-api.jar"))
}The retained example includes complete
Maven and
Gradle builds. Because the
pinned Velocity 4 snapshot advertises Java 25 Gradle metadata, its Gradle build
selects a Java 25 compile classpath while javac --release 21 continues to emit
Java 21-compatible example bytecode.
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Dependency;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import net.slimelabs.slslite.api.SLSLiteApi;
import net.slimelabs.slslite.api.SLSLiteApiProvider;
@Plugin(
id = "example-sls-extension",
name = "Example SLS Extension",
version = "1.0.0",
dependencies = @Dependency(id = "sls-lite"))
public final class ExampleExtension {
private final ProxyServer proxy;
public ExampleExtension(ProxyServer proxy) {
this.proxy = proxy;
}
@Subscribe
public void initialize(ProxyInitializeEvent event) {
SLSLiteApi api =
SLSLiteApiProvider.find(proxy)
.orElseThrow(() -> new IllegalStateException("SLS-LITE API unavailable"));
api.ready().thenRun(() -> api.blueprints().forEach(blueprint ->
System.out.println(blueprint.type() + "/" + blueprint.id())));
}
}api() and SLSLiteApiProvider.find(...) return the same stable facade for the
plugin lifetime. Check status() or wait for ready() before inspection or
operations. Initialization failure completes ready() exceptionally with a
public SLSLiteApiException; shutdown changes the status to CLOSED.
| Method | Purpose |
|---|---|
version() |
Return the Java API version. |
status() / ready() |
Inspect or await provider readiness. |
capabilities() |
Discover supported optional features. |
extension(namespace) |
Create an owned extension callback/subscription context. |
diagnostics() |
Capture redacted bounded operational diagnostics. |
blueprints() / blueprint(id) |
Inspect immutable blueprint views. |
instances() / instance(id) |
Inspect immutable instance views. |
start(request) |
Start and register an instance. |
stop(id) / delete(id) |
Stop or delete through normal lifecycle rules. |
restart(id) / reset(id) |
Safely evacuate and cycle an ordinary persistent instance. |
install(request) |
Ensure one configured software release is installed. |
reload(scope) |
Atomically reload blueprint and/or software definitions. |
setMaintenance(enabled, reason) |
Change new-instance admission state. |
enqueue(request) |
Match and transfer an online player. |
transfer(request) |
Transfer an online player to one exact READY instance. |
queued(playerId) / dequeue(playerId) |
Inspect or cancel a queued request. |
extensionDiagnostics() |
Read the bounded namespaced diagnostic cache and schedule an asynchronous refresh. |
subscribe(listener) |
Receive ordered lifecycle, matchmaking, and failure events. |
API 1.2 advertises BLUEPRINT_INSPECTION, INSTANCE_INSPECTION,
INSTANCE_START, INSTANCE_STOP, INSTANCE_DELETE, PLAYER_QUEUE,
MATCHMAKING_EVENTS, INSTANCE_FAILURE_EVENTS, CATALOG_RELOAD_EVENTS,
LOBBY_STATUS_EVENTS, SOFTWARE_INSTALLATION_EVENTS, RECONCILIATION_EVENTS,
API_SHUTDOWN_EVENTS, DIAGNOSTICS, EXTENSION_CONTEXTS,
EXTENSION_ACTIONS, EXTENSION_BLUEPRINT_READINESS, INSTANCE_RESTART,
INSTANCE_RESET, SOFTWARE_INSTALLATION_REQUESTS,
DEFINITION_RELOAD_REQUESTS, MAINTENANCE_CONTROL,
EXACT_INSTANCE_TRANSFER, EXTENSION_DIAGNOSTICS, and LIFECYCLE_EVENTS.
Start completes only after the backend is ready and registered with Velocity:
api.start(new StartRequest("block_hunt"))
.thenAccept(instance -> System.out.println("Ready: " + instance.id()))
.exceptionally(failure -> {
// Unwrap CompletionException and inspect SLSLiteApiException.code().
return null;
});InstanceOverrides exposes only the same reviewed local fields as /sls create: memory, persistence, seed, view distance, simulation distance, and
command blocks. Node, image, software, environment, and container-resource
overrides are deliberately absent.
stop(instanceId) performs graceful lifecycle shutdown. delete(instanceId)
uses the same ownership-aware persistent/ephemeral deletion transaction as the
operator command. These calls return CompletionStage and must not be blocked
on Velocity's event thread.
restart(instanceId) and reset(instanceId) first evacuate players through the
configured safe lobby, then reuse the persistent lifecycle transaction and
complete after the replacement backend is READY. They deliberately reject the
protected managed lobby; cycling that routing-critical service remains an
explicit operator command. reset rebuilds from current definitions while
restart reuses the existing persistent instance.
install(new SoftwareInstallationRequest(software, version)) uses the existing
provider, EULA, checksum, cache, and shared-installation ownership rules without
returning a filesystem path. reload(CatalogReloadScope) runs atomic definition
reload, readiness refresh, and dynamic-registration reconciliation on a bounded
administrative worker. The result contains bounded deltas, a correlation ID,
and a DefinitionReloadImpact telling the caller how many running and
persistent instances use changed definitions and what lifecycle choice to
present next. Reload never modifies those instances. Rejected blueprint and
registration details remain in the SLS-LITE detail log. See
Applying Changes Safely for the authoritative model.
Host config.yml is never live-reloaded. setMaintenance(...) changes only
new-instance admission and does not stop existing instances.
Queue an online player through normal capacity-aware matchmaking:
api.enqueue(new QueueRequest(playerId, "minigame", "block_hunt"))
.thenAccept(result -> {
if (!result.connected()) {
// The request completed, but Velocity rejected the final transfer.
}
});queued(playerId) inspects a ticket and dequeue(playerId) atomically removes
one. Queue requests still enforce blueprint pools, maximum instances, player
capacity, maintenance mode, memory admission, and normal connection behavior.
Use transfer(new InstanceTransferRequest(playerId, instanceId, force)) for an
exact READY instance rather than matchmaking. Its typed InstanceTransferStatus
distinguishes offline players, missing/not-ready/unregistered instances,
ordinary capacity, reserved force capacity, and final Velocity connection
failure. Force bypasses only ordinary public instance capacity; it cannot bypass
backend headroom, readiness, registration, protocol/connection handling, or
lifecycle safety. Trusted extensions must authorize the user who caused the
request.
diagnostics() returns one immutable point-in-time DiagnosticsSnapshot.
It includes system and queue counts, maintenance state, effective primary and
holding-lobby health, up to 100 recent installations, 64 startup host probes,
256 instance statistics and redacted 20-line output tails, and the latest 64
sanitized correlated instance failures. It also contains up to 128 cached,
namespaced extension diagnostic views, with no wait for extension code. System
counts remain exact when the detailed instance lists are truncated.
Messages are single-line, redacted, and limited to 512 characters. The view does not expose credentials, filesystem paths, download URLs, process IDs or handles, mutable log buffers, internal exceptions, coordinators, or repository objects. Callers receive defensive list copies and must request a new snapshot to observe later changes.
A complete Velocity consumer is available in
examples/velocity-extension and
is compiled against only the API classifier in CI.
Use one ExtensionContext for each extension plugin and close it during that
plugin's shutdown:
ExtensionContext context = api.extension("example-plugin");
context.subscribe(event -> handle(event));
context.onComplete(api.start(request), (instance, failure) -> handle(instance, failure));
context.onInstanceReady(action -> initializeBackend(action.instance(), action.annotations()));
context.onPostTransfer(action -> recordArrival(action.ticket(), action.annotations()));
context.onBlueprintReadiness((blueprint, annotations) -> checkDependencies(annotations));
context.onDiagnostics(() -> inspectExtensionHealth());
// During extension shutdown:
context.close();Namespaces are case-normalized plugin-style identifiers and remain unique
while owned. At most 128 contexts and 256 registrations per context are
accepted. Event subscriptions and completion registrations return independently
idempotent Subscription handles, while closing the context releases all of
them at once. A completed future automatically releases its registration.
Completion callbacks execute on the thread completing their source stage;
event callbacks use the bounded SLS-LITE event dispatcher. Callbacks must remain
non-blocking. Closing a context gates callbacks that have not begun but cannot
interrupt user code already executing. SLS-LITE shutdown first delivers the
terminal ApiShutdownEvent, then closes every remaining context; incomplete
future callbacks are suppressed as soon as API shutdown begins.
The owned extension-context surface selects only its namespace's top-level
blueprint annotation object. For a context named example-plugin, the matching
blueprint shape is:
annotations:
example-plugin:
mode: ranked
rewards:
- dailycontext.annotations(blueprint) returns a deeply immutable
NamespacedAnnotations value. Maps and lists are limited to 256 entries, the
tree to 16 levels and 4,096 total values, and strings to 4,096 characters.
Only null, strings, booleans, immutable numeric values, maps, and lists cross
the API boundary.
onBlueprintReadiness(checker) registers the namespace's single read-only
preflight checker. It runs only for blueprints containing that annotation
namespace and may return up to eight BlueprintReadinessFinding values. An
empty list means the extension is ready. ACTION_NEEDED identifies an operator
input that must change; TEMPORARILY_UNAVAILABLE identifies a dependency that
may recover without editing the blueprint. Findings are namespaced and merged
into startup/reload aggregates, /sls blueprints, /sls blueprint <id>,
startup/reload detail-log entries, and new-instance admission. Existing running or persistent
instances retain their established lifecycle.
Checkers receive only immutable BlueprintView and NamespacedAnnotations
values. They must be non-blocking, non-mutating, and must not perform downloads,
mounts, instance assembly, or unbounded I/O. SLS-LITE isolates exceptions,
limits the combined refresh to two seconds on four bounded daemon workers, and
reports an affected annotated blueprint as temporarily unavailable when its
checker fails, times out, or cannot be scheduled. A context owns at most one
checker; closing its registration or context immediately removes its findings.
onDiagnostics(contributor) registers one read-only operational status
contributor for the context namespace. Each inspection may return up to 16
single-line ExtensionDiagnosticFinding values with INFO, WARNING, or
ERROR severity. extensionDiagnostics() returns immutable per-namespace
views; /sls system shows only bounded aggregate counts while exact findings go
to the detail log. Inspection calls return the latest completed immutable cache
immediately and request an asynchronous refresh. SLS-LITE evaluates contributors
on four bounded workers with a shared two-second deadline, redacts common secret and absolute-path patterns,
and substitutes a safe error finding for timeout, saturation, excessive output,
null output, or failure. Closing the registration or context removes the
contributor.
onInstanceReady(action) runs after the instance is registered with Velocity
and immediately after its public READY event is queued. onPostTransfer(action)
runs only after a queued Velocity connection request actually moves the player,
immediately after TRANSFER_SUCCEEDED is queued. An ALREADY_CONNECTED no-op
still produces the established success event but does not invoke the action.
Payloads capture the immutable instance or ticket, the original occurrence
time, and the extension's annotation namespace at publication; later catalog
reloads cannot change them. Registrations are
included in the context's 256-registration limit, use the same bounded ordered
dispatcher as events, run in namespace then registration order, and are disabled
after their first callback failure.
These hooks cannot replace matchmaking, lobby providers, software installers, storage/COW strategies, process supervision, backend registration, or resource admission. Those remain internal implementation boundaries.
Subscription subscription = api.subscribe(event -> {
if (event instanceof InstanceLifecycleEvent lifecycle) {
System.out.println(lifecycle.instanceId() + ": "
+ lifecycle.previousStatus() + " -> " + lifecycle.currentStatus());
}
});
// During extension shutdown:
subscription.close();Events are delivered in transition order by one bounded SLS-LITE dispatcher.
Recipients are captured when an event is published, so a new subscriber never
receives ordinary events that were already queued before it subscribed.
Callbacks must return quickly and offload blocking work. A subscriber that
throws is disabled after its first logged failure. If a slow extension fills
the 1,024-event queue, SLS-LITE drops later notifications and warns at most once
per minute; sequence gaps let consumers detect this. At most 128 simultaneous
subscriptions are allowed.
An event records an accepted transition, not a retained object reference; a
terminal instance may already be absent when a delayed subscriber inspects it.
The public READY transition is published only after Velocity registration;
the earlier child-process readiness signal remains internal. Likewise,
start(...) completes only after that registration succeeds.
Matchmaking subscriptions receive PlayerMatchmakingEvent with the immutable
queue ticket, whether the request created its assigned instance, and one of the
following accepted statuses:
QUEUEDincludes the assigned instance;TRANSFER_STARTEDmeans cancellation can no longer claim the request;TRANSFER_SUCCEEDED,TRANSFER_REJECTED, andTRANSFER_FAILEDare transfer terminal states; andCANCELLED,DISCONNECTED,TIMED_OUT,INSTANCE_FAILED,BACKEND_UNAVAILABLE, andSHUTDOWNexplain non-transfer terminal states.
The status is deliberately machine-readable and contains no internal exception message or Velocity result object. A request emits exactly one terminal status.
An accepted instance failure emits one InstanceFailureEvent. Its
InstanceFailurePhase identifies where the failure occurred and its
InstanceFailureCategory provides a stable reason family. The event includes
only the instance, blueprint, type, and correlation identifiers needed to join
it to operator diagnostics. It never exposes an exception, exception message,
filesystem path, child-process output, or mutable coordinator. A process exit
after successful Velocity registration uses phase RUNTIME; startup and
readiness exits remain distinct.
An operator /sls reload all|blueprints|software attempt emits one
CatalogReloadEvent after the atomic transaction commits or rejects. Committed
events expose only added, updated, and removed counts for blueprints and
software. Rejected events report zero committed changes and one sanitized
CatalogReloadFailureCategory: IO, VALIDATION, or INTERNAL. The event's
correlation ID joins it to operator logs without exposing definition IDs, file
names, paths, or parser messages.
LobbyStatusEvent provides a deduplicated snapshot of the effective lobby
service. It reports the primary and holding-lobby LobbyServiceStatus values
and the currently selected LobbyRoute: PRIMARY, HOLDING, or NONE.
Recovery can therefore be observed even while players remain safely routed to
the holding lobby. available() is false only for route NONE. Server names,
addresses, ports, child processes, retry details, and failure messages remain
internal.
Actual shared automatic-install jobs emit SoftwareInstallationEvent states:
STARTED, then exactly one READY, FAILED, or CANCELLED. Concurrent callers
waiting for the same target do not create duplicate event streams. Failures use
only IO, INSTALLER, or INTERNAL; shutdown cancellation uses CANCELLED.
Cache hits and requests rejected before work is accepted emit no installation
event. Software/version, source, and release channel are exposed, while cache
paths, download URLs, checksums, provider logs, progress text, and exception
messages remain internal.
Startup produces one ReconciliationEvent containing bounded outcome counts
and the startup correlation ID. Because reconciliation finishes before the API
becomes ready, this is the only retained event: it is replayed once to each
subscriber that registers after reconciliation. Its original sequence and
timestamp are preserved, and it is delivered before that subscriber's later
live events. No instance identifiers, paths, mount details, or failure messages
are exposed.
Closing SLS-LITE publishes one terminal ApiShutdownEvent to the subscribers
registered at shutdown, then drains the dispatcher within the normal bounded
shutdown deadline. The terminal notification reserves queue capacity by evicting at
most one older queued notification if a saturated extension has filled the
queue. As with other callbacks, a subscriber that blocks past the shutdown
deadline can prevent delivery. Subscriptions and new API operations are
rejected after closure.
The Java API is for installed, trusted plugins and does not repeat player permission checks. An extension exposing commands, plugin messages, or network endpoints must authenticate and authorize its callers. SLS-LITE still enforces its internal lifecycle, resource, instance-limit, ownership, and path-safety rules.
Only packages below net.slimelabs.slslite.api are public. Do not import
api.internal or any other SLS-LITE implementation package. Public views never
expose mutable repositories, coordinators, child processes, filesystem paths,
Velocity connection results, or internal exceptions.
API versions use major.minor semantics. A
major change may break source or binary compatibility. A minor change may add
new types, capabilities, event implementations, exception codes, or interface
default methods; existing method signatures and record shapes remain stable
within a major version.
Extensions should test the advertised version and capability set rather than
infer support from the SLS-LITE plugin version.
SLS-LITE core has no authenticated HTTP administration/event API and opens no administration listener. A trusted extension may build a network-facing surface on the Java API, but that extension owns authentication, authorization, rate and request-size limits, privacy, TLS or reverse-proxy guidance, and shutdown.