Atlas Developers
Dashboard

Java API · 1.0

The SDK for modern Minecraft infrastructure

The Atlas Java SDK gives every Minecraft server and proxy one view of the whole network. Read projected state from memory, make authoritative claims asynchronously, and keep running when the control plane is far away.

This is the SDK that runs inside your plugins.

To drive Atlas itself from a script or a service — create groups, publish templates, start and stop servers, read analytics — see the HTTP API.

Local queriesHeap reads, safe on the tick thread
Async claimsFutures for authoritative placement
Resilient by defaultReconnects without killing the server
JavaAtlasClient.java
try (AtlasClient atlas = AtlasClient.connect()) {
    atlas.awaitReady(Duration.ofSeconds(5));

    // Local: no network call.
    int online = atlas.playerCount("squidgame");

    // Central: returns immediately.
    atlas.reserveSlot("squidgame", playerId)
        .thenAccept(slot -> connect(slot.serverId()));
}

Get started

Installation

Atlas requires Java 17 or newer. The SDK is published to the Atlas Maven repository at repo.atlasgg.com, which is public — no credentials, no account. Add the repository and the dependency to your plugin build.

You do not add the platform adapters. Atlas installs the Paper, Velocity, and BungeeCord adapters onto your servers itself, the same way it installs any managed plugin; declaring a dependency on one would bind your plugin to an internal.

Runtime identity is injected for you.

An Atlas-managed process receives ATLAS_SDK_SOCKET and ATLAS_SERVER_ID from its local agent. Do not put server IDs or control-plane credentials in your plugin.

Artifact

Group
com.atlasgg
Artifact
atlas-sdk
Current build
0.1.0
Java
17+
Maven
<repositories>
  <repository>
    <id>atlas</id>
    <url>https://repo.atlasgg.com</url>
  </repository>
</repositories>

<dependency>
  <groupId>com.atlasgg</groupId>
  <artifactId>atlas-sdk</artifactId>
  <version>0.1.0</version>
</dependency>

Gradle: maven { url "https://repo.atlasgg.com" }, then implementation "com.atlasgg:atlas-sdk:0.1.0".

Get started

Connect to the local agent

Call AtlasClient.connect() during plugin startup. It opens the local Unix socket and identifies the current Atlas server using agent-provided environment values.

The client initially has an empty registry. If “not loaded yet” must be different from “the network is empty,” wait for the first projection with awaitReady.

Only Atlas-managed processes can connect.

A process started outside the agent will fail clearly because its socket path and server identity are absent.

Java
private AtlasClient atlas;

public void start() throws IOException {
    atlas = AtlasClient.connect();
    atlas.awaitReady(Duration.ofSeconds(5));
}

public void stop() throws IOException {
    if (atlas != null) atlas.close();
}

Get started

The execution model is part of the API

Method shape tells you what the call costs. Synchronous query methods read the SDK’s in-process projection. Methods that need authoritative agreement return CompletableFuture. Fire-and-forget mutations can throw IOException if they cannot reach the local agent.

KindShapeExamplesUse it for
QueryT method()servers(), playerCount()Advisory state on hot paths
ClaimCompletableFuture<T>reserveSlot(), locate()Placement and central truth
Mutationvoid … throws IOExceptionsetStatus(), send()Reports and best-effort delivery

Get started

A complete plugin: /hub

One file, one command: send a player to a lobby. It is small enough to read in a minute and still exercises the three things every Atlas plugin has to get right.

  1. The local query. routableServers reads this process’s own copy of the registry — no socket, no network — so an empty lobby group is answered instantly instead of after a failed round trip.
  2. The central claim. transfer reserves a seat against live capacity and then asks the proxy holding the player to move them. Picking a server yourself would skip the reservation and can land someone on one that filled in the meantime.
  3. The hop back. The callback arrives on the SDK reader thread. Touching a Bukkit API from there is a race, so it goes through the scheduler first.
Relocate protobuf when you shade.

The SDK depends on protobuf-java, and so do other plugins, at other versions. Left in its own package the first plugin loaded wins and the rest fail with NoSuchMethodError at runtime — usually under load, usually blamed on the wrong plugin. Atlas relocates it in its own managed adapters for exactly this reason.

HubPlugin.java
public final class HubPlugin extends JavaPlugin {

    private static final String LOBBY_GROUP = "lobby";

    private AtlasClient atlas;

    @Override public void onEnable() {
        try {
            // Reads ATLAS_SDK_SOCKET and ATLAS_SERVER_ID, injected by the agent.
            atlas = AtlasClient.connect();

            // Without this, a /hub in the first second cannot tell
            // "no lobbies exist" from "not loaded yet".
            atlas.awaitReady(Duration.ofSeconds(5));
        } catch (IOException e) {
            getLogger().severe("Atlas unavailable, /hub disabled: " + e.getMessage());
            getServer().getPluginManager().disablePlugin(this);
        }
    }

    @Override public void onDisable() {
        if (atlas != null) {
            try { atlas.close(); }
            catch (IOException e) { getLogger().warning(e.getMessage()); }
        }
    }

    @Override public boolean onCommand(CommandSender sender,
            Command command, String label, String[] args) {

        if (!(sender instanceof Player player)) {
            sender.sendMessage("Only a player can be sent to a lobby.");
            return true;
        }

        // Local read. No socket, safe on the tick loop.
        if (atlas.routableServers(LOBBY_GROUP).isEmpty()) {
            player.sendMessage("No lobby is accepting players right now.");
            return true;
        }

        // Central claim: reserve a seat, then move the player.
        atlas.player(player.getUniqueId())
                .transfer(LOBBY_GROUP)
                .whenComplete((ignored, error) ->
                        Bukkit.getScheduler().runTask(this, () -> {
                    // Now on the main thread.
                    if (error == null || !player.isOnline()) {
                        // Success needs no message — they are already gone.
                        return;
                    }
                    player.sendMessage(explain(error));
                }));

        return true;
    }

    private String explain(Throwable error) {
        Throwable cause = error instanceof CompletionException
                ? error.getCause() : error;

        // Retryable means "valid request, not right now" — every
        // lobby full. Non-retryable means repeating it will not help.
        if (cause instanceof AtlasException e && e.retryable()) {
            return "Every lobby is full. Try again in a moment.";
        }

        getLogger().warning("/hub failed: " + cause);
        return "Could not send you to a lobby.";
    }
}

Needs a plugin.yml declaring main: com.example.hub.HubPlugin and a hub command. Group name lobby is whatever you called yours.

Core API

Read network state locally

Every query below is thread-safe and reads memory in the current process. Values describe the whole Atlas network, not just the current node, and are normally about one second behind.

  • playerCount(group)Players across every server in a group.
  • availableSlots(group)Advisory free slots across joinable servers.
  • networkPlayerCount()True total from proxy groups, without double-counting backends.
  • servers(group)All projected servers in one group.
  • servers()All projected servers in the network.
  • routableServers(group)Ready, accepting, addressable servers.
  • self()This server’s view, or null before the projection.
Availability is advisory.

Seeing one available slot does not make it yours. Call reserveSlot before moving a player.

Java
int players = atlas.playerCount("lobby");
int capacity = atlas.availableSlots("lobby");

List<ServerView> targets =
    atlas.routableServers("lobby");

for (ServerView server : targets) {
    logger.info("%s — %d/%d".formatted(
        server.getServerId(),
        server.getPlayerCount(),
        server.getSlotCapacity()));
}

Core API

Report what this server is doing

Atlas does not inspect your game. Report the facts that drive placement and lifecycle decisions: current occupancy, successful arrivals, and your game status.

Status names are yours. Your group configuration decides whether a value such as INGAME stops new placements and starts replacement capacity.

Consume reservations on arrival.

playerArrived releases the short-lived hold and reports the authoritative backend count in one operation.

Paper
// Match begins: remain healthy, stop taking players.
atlas.setStatus("INGAME");

// A reserved player reaches the backend.
int count = Bukkit.getOnlinePlayers().size();
atlas.playerArrived(player.getUniqueId(), count);

// Match ends: restore ordinary joinability.
atlas.clearStatus();

Core API

React to network changes

onEvent derives events from the same projection queries read. Delivery is at-most-once, not durable, and updates may be coalesced. Treat events as a prompt to inspect state—not as a ledger.

Server events

ServerStarting, ServerReady, ServerStopping, ServerStopped, ServerStatusChanged, and ServerJoinabilityChanged.

Player events

PlayerJoinedNetwork, PlayerLeftNetwork, and PlayerSwitchedServer.

Never attach irreversible work to an event.

Payments, punishments, and inventory writes need durable application state. Atlas lifecycle events can be missed during a disconnect.

Paper
atlas.onEvent(event -> {
    // Callback runs on the SDK reader thread.
    Bukkit.getScheduler().runTask(plugin, () -> {
        if (event instanceof
                AtlasEvent.ServerReady ready) {
            announce("Ready: "
                + ready.server().getServerId());
        }
    });
});

Core API

Reach a player anywhere

atlas.player(uuid) returns a handle whether the player is online or not. Presence stays central, so location and transfer methods are asynchronous.

  • locate()Returns proxy, server, group, node, and placement time.
  • isOnline()Checks for a live presence record.
  • transfer(group)Reserves capacity, then asks the holding proxy to move.
  • transferTo(server)Moves to an exact server without creating a reservation.
  • send(payload)Fire-and-forget delivery to the current holder.

Messages perform no location lookup. The server holding the player owns a live subscription; when that connection disappears, the route disappears with it.

Java
AtlasPlayer player = atlas.player(playerId);

player.locate().thenCompose(presence -> {
    if (!presence.online()) {
        return CompletableFuture.failedFuture(
            new IllegalStateException("offline"));
    }
    return player.transfer("squidgame");
}).exceptionally(error -> {
    logger.warning(error.getMessage());
    return null;
});

player.send("The next match is ready.");

Core API

Place groups atomically

Use atlas.players(…) instead of looping over individual players. A party request considers everyone in one central operation, so separate reservations cannot scatter the group by accident.

ALL_OR_NOTHINGOne server fits everyone, or the request fails.
PREFER_TOGETHERTry one server first, then split across the best options.
SPLITPlace each member using the group strategy.

A completed party transfer may still contain unplaced players. Always inspect isComplete(), placed(), and unplaced().

Java
AtlasParty party = atlas.players(memberIds);

party.transfer("squidgame",
        PartyPolicy.PREFER_TOGETHER)
    .thenAccept(result -> {
        if (!result.isComplete()) {
            result.unplaced().forEach((id, reason) ->
                logger.warning(id + ": " + reason));
        }
    });

Core API

Server-selector NPCs

Atlas draws and routes NPCs itself, in the managed Paper adapter. Nothing below is needed to make them work — it is here for a plugin that wants to place or edit one from its own code, or to react when the set changes.

An NPC belongs to a group, not a server. Every server in a group is built from the same template and so runs the same world, which is what makes one placement appear on all of them. A change is stored centrally and pushed straight back out, so placing one on lobby-1 puts it on lobby-2 a moment later with nothing restarting.

  • npcs()The NPCs this server should be showing. Empty when the module is off.
  • resolveNpcRoute(UUID, String)Resolves ingress-proxy and remembered-group rules centrally. The managed adapter calls this for NPC clicks.
  • onNpcs(Consumer)Called immediately and whenever the set changes.
  • createNpc(NpcPlacement)Places one where you are standing, in this server’s group.
  • updateNpc(NpcEdit)Changes only the fields the edit set.
  • deleteNpc(String)Removes one from every server in its group.
Unset means leave it, never clear it.

NpcEdit is a builder rather than a record for exactly this reason: setting a display name must not silently reset the skin, the item and the click actions to defaults.

The group is not yours to choose.

An NPC is created in the group of the server that asked, which the agent reads from its own registry. A plugin that could name a group could put an NPC in somebody else’s lobby, so there is no parameter for it.

Skins are resolved once by the control plane and stored, so no server ever calls Mojang — twenty lobbies rendering the same NPC would otherwise be twenty requests to a rate-limited endpoint for one answer. An unknown username fails the call by name; Mojang being unreachable places the NPC with the default skin rather than refusing.

Operators place NPCs in game with /atlasnpc, which needs the Minecraft permission atlas.npc.manage. Everything except placing and moving can also be edited from the dashboard.

Routing is configuration, not code.

Where a click sends a player is a JSON document on the NPC, evaluated centrally. NPCs & routing rules covers the commands, the rule format, and worked examples.

Java
// React when Atlas pushes a new set.
atlas.onNpcs(config -> Bukkit.getScheduler().runTask(this, () -> {
    getLogger().info("now showing " + config.getNpcsCount());
}));

// Place one where the player stands.
Location at = player.getLocation();
atlas.createNpc(new NpcPlacement(
        at.getWorld().getName(), at.getX(), at.getY(), at.getZ(),
        at.getYaw(), at.getPitch(), "&aSkyblock", "Notch"))
    .thenAccept(id -> player.sendMessage("placed " + id));

// Change one field. Everything unsaid keeps its stored value.
atlas.updateNpc(NpcEdit.of(npcId).skinOwner("jeb_"));

Callbacks arrive on the SDK reader thread. Touching the world or a player from there is a race — hand it to the scheduler.

Reliability

Survive an agent restart

The client reconnects indefinitely with capped exponential backoff and jitter. After reconnecting, it identifies itself again and replays held players, player count, and status.

Requests already in flight fail with a retryable AtlasConnectionException. Issue a fresh request after the connection-state callback reports true.

Backend safety

The Paper adapter can shut down a backend safely after a sustained agent outage. The default grace is five minutes, followed by a 30-second player warning and save. Set ATLAS_ORPHAN_SHUTDOWN_GRACE_SECONDS to at least 120 seconds, or use 0/off to disable it.

Java
atlas.onConnectionStateChanged(connected -> {
    logger.info("Atlas agent: "
        + (connected ? "connected" : "reconnecting"));
});

atlas.onDisconnectedFor(Duration.ofMinutes(2),
    () -> scheduler.execute(this::enterSafeMode));

if (atlas.isConnected()) {
    submitPlacement();
}

Reliability

Handle rejection and transport failure separately

AtlasException means the agent or control plane rejected an operation. Inspect code() and retryable(). AtlasConnectionException is an I/O failure on the local agent connection and is always safe to retry after reconnection.

Async failures normally arrive wrapped by CompletionException. Unwrap the cause before classifying it.

INVALID_ARGUMENTNOT_FOUNDCONFLICTUNAVAILABLETIMEOUTSEQUENCE_GAPINTERNAL
Java
atlas.reserveSlot(group, playerId)
    .whenComplete((slot, failure) -> {
        if (failure == null) {
            connect(slot.serverId());
            return;
        }

        Throwable cause = failure.getCause() == null
            ? failure : failure.getCause();
        if (cause instanceof AtlasException e
                && e.retryable()) {
            enqueueForLater(playerId);
        }
    });

Reliability

Callbacks run off the platform thread

Event, message, connection, and transfer callbacks run on the SDK reader/reconnect thread. Hand platform work to Bukkit, Velocity, or BungeeCord’s scheduler before touching their APIs.

Do not block a callback. It shares the connection path that delivers events and completes claim replies.

Paper
atlas.onPlayerMessage((playerId, payload) ->
    Bukkit.getScheduler().runTask(plugin, () -> {
        Player player = Bukkit.getPlayer(playerId);
        if (player != null) {
            player.sendMessage(new String(payload,
                StandardCharsets.UTF_8));
        }
    })
);

Reference

Platform adapters

The packaged adapters implement the platform-specific bridge around the same Java client.

Backend

Paper / Spigot

Reports player counts and arrivals, exposes atlas() and setStatus(), delivers messages on the Bukkit scheduler, and owns orphan safety shutdown.

Proxy

Velocity

Synchronizes projected backends, resolves initial placement, reports presence, executes cross-network transfers, and publishes the Atlas player count in pings.

Proxy

BungeeCord

Provides the same placement, presence, transfer, messaging, and ping behavior through BungeeCord’s scheduler and event model.

Reference

Core types

ServerView

Projected protobuf view: IDs, group and node, lifecycle state, players, capacity, status, joinability, address, port, kind, and observation time.

Slot

reservationId, serverId, expiresAtUnixMs, plus expiresAt() and isExpired().

PlayerPresence

online, proxyId, serverId, groupId, nodeId, and sinceUnixMs.

JoinPlacement

The selected groupId, a live slot, and whether the player’s remembered group was used.

PartyPlacement

Primary server, expiry, per-player slots, and per-player failure reasons. Helpers report completeness and togetherness.

TransferRequest

The player, target server, and optional reservation that a proxy adapter must execute on its platform thread.

Reference

Method index

AtlasClient

  • static connect()
  • awaitReady(Duration)
  • static connect(Path, String)
  • playerCount(String)
  • availableSlots(String)
  • networkPlayerCount()
  • servers(String)
  • servers()
  • routableServers(String)
  • self()
  • setStatus(String)
  • clearStatus()
  • setPlayerCount(int)
  • playerArrived(UUID[, int])
  • player(UUID)
  • players(Collection|UUID…)
  • holdPlayer(UUID)
  • releasePlayer(UUID)
  • onPlayerMessage(BiConsumer)
  • onPlayerTransfer(Function)
  • reportPresence(…)
  • reportPlayerLeft(UUID, String)
  • reserveSlot(String, UUID)
  • reserveParty(…)
  • resolveJoin(UUID)
  • onEvent(Consumer)
  • isConnected()
  • onConnectionStateChanged(Consumer)
  • onDisconnectedFor(Duration, Runnable)
  • close()
  • npcs()
  • onNpcs(Consumer)
  • createNpc(NpcPlacement)
  • updateNpc(NpcEdit)
  • deleteNpc(String)

AtlasPlayer

  • uuid()
  • locate()
  • isOnline()
  • transfer(String)
  • transferTo(ServerView)
  • send(byte[])
  • send(String)

AtlasParty

  • members()
  • reserve(String[, PartyPolicy])
  • transfer(String[, PartyPolicy])

Generated protocol types live under com.atlasgg.protocol.v1. Application-facing helpers live under com.atlasgg.sdk.