Skip to content

Developer API

For plugin developers. Everything AtlasRealms lets another plugin read, change and listen to, with the thread rule for each call.

Added in 2.5.0. Nothing here was reachable before it: until 2.5.0 every class of the plugin sat in the half of the jar the licence server hands out, which the server loads through a classloader of its own — so no other plugin could name one. The studio.oreades.atlasrealms.api package ships in the plugin jar itself and is the whole of the supported surface.

There is no separate artifact. Put the AtlasRealms jar on your compile classpath and leave it off the runtime one.

dependencies {
compileOnly(files("libs/AtlasRealms.jar"))
}

Then declare the dependency, so the server loads AtlasRealms first and puts its classes where you can see them:

paper-plugin.yml
dependencies:
server:
AtlasRealms:
load: BEFORE
required: false
join-classpath: true

required: false and a null check is the right shape unless your plugin is useless without realms.

Through Bukkit’s service manager. There is no static accessor, and nothing to construct.

AtlasRealmsAPI realms = null;
RegisteredServiceProvider<AtlasRealmsAPI> found =
getServer().getServicesManager().getRegistration(AtlasRealmsAPI.class);
if (found != null) {
realms = found.getProvider();
}

It is registered while AtlasRealms enables and unregistered when it stops. A server where AtlasRealms refused to start — a bad config.yml, an unreachable database — registers nothing, so the null is the answer to “is this working”, not only “is this installed”.

Every call divides into two, and the return type says which.

Return type Where it reads Call it from It settles on
a plain value memory a tick thread
CompletableFuture<…> the database, or a world anywhere the global tick thread

A plain return value never waits. Nothing behind it touches the database or the disk, so it is what a placeholder, a scoreboard, a menu or a guarded event should use. The cost is that it is as current as the last thing that read the realm: a realm somebody is standing in is exact, and one that another server on the same MySQL changed since anybody here looked at it is not.

A future may take a world load. Whatever you chain onto it runs on the global tick thread and may touch the Bukkit API — and must not block, because it is a tick.

RealmView is a realm, read only. Its methods are as cheap as the API’s own, with four exceptions that ask the server and therefore want a tick thread: getWorld(), loadedWorlds(), playersInside() and insideBorder(Location).

Call Answers
realm(UUID realmId) that realm, or empty
realmOf(UUID player) the realm that player owns, or empty
realmAt(Location where) the realm whose ground that is, or empty
realms() every realm on the server
hasRealm(UUID player) whether they own one
busy(UUID player) whether a create or a reset of theirs is in flight
flag(RealmView realm, RealmFlag flag) what the flag is worth — the owner’s override, or the server’s default
locked(RealmFlag flag) whether realm.locked-flags takes that switch off owners
may(AccessLevel level, RealmRight right) what roles.yml says a role may do
layoutId() world or grid

Two more go to the database, for a caller that needs the row rather than a copy of it:

Call Answers
fetchRealm(UUID realmId) the row as it stands right now
fetchRealmOf(UUID player) the same, for the realm a player owns

Use them when the answer has to be right rather than fast — a second server on the same MySQL, a web panel, an audit.

Every change writes the row, moves what has to move and fires the same events /realm does, so a listener cannot tell an API call from a player.

Call Does
createRealm(Player owner, String themeId) builds a realm from that theme, or from any theme when the id is null. Charges nothing — see below
resetRealm(UUID realmId) lays the template down again. The admin path: no cooldown, and it does not count against realm.reset.max
deleteRealm(UUID realmId) removes the worlds, the row and the owner’s claim, and pays the bank back to them
transferRealm(UUID realmId, UUID newOwner) hands it over. The old owner stays on as a trusted member
setStatus(UUID realmId, RealmStatus status) who may walk in
setFlag(UUID realmId, RealmFlag flag, boolean value) one flag
setMember(UUID realmId, UUID player, AccessLevel level) adds a member, or moves one
removeMember(UUID realmId, UUID player) takes their membership away
setBanned(UUID realmId, UUID player, boolean banned) bans or unbans
enter(Player player, UUID realmId) loads the realm and puts them inside it

Six things about these are worth knowing before you call one:

  • createRealm charges nothing. economy.create-cost is what /realm create asks of a player who chose to buy a realm. A plugin creating one on somebody’s behalf knows its own reason, so the price is left to the caller.
  • Setting a flag to what the server already says clears the override rather than writing it down. That keeps a realm following config.yml when the server owner changes it, and it is the same rule the menu obeys.
  • An owner is made by transferRealm, never by setMember. Two rows have to move with the realm, or you get a player owning two realms, or a realm owning nobody. setMember with AccessLevel.OWNER is refused.
  • A ban takes the membership with it, drops any open invitation, and puts the player out if they are standing inside. An unban does not put the membership back.
  • enter checks the ban and the status before it loads anything. It answers false for a realm that would not have them, rather than pulling a closed world into memory that nobody will ever empty again.
  • Check busy first for anything that races. A reset takes seconds, builds a world, and writes the realm it captured when it finishes.

Every future fails with RealmException, which carries a Reason:

Reason Means
BUSY that player already has a create or a reset in flight
ALREADY_OWNS_REALM a player may own one
NO_SUCH_REALM no realm has that id any more
NO_SUCH_THEME the server has no theme by that name
NO_SUCH_TEMPLATE the theme holds no template for that environment, or the server holds none at all
REFUSED the call will not do that, and another one does — the message says which
UNSUPPORTED_BY_SERVER this server cannot create or unload a world. Folia proper
FAILED the database, a world, or something under them. Read getCause()

There is deliberately no CANNOT_AFFORD and no RESET_REFUSED: the API charges nothing for a realm and resets by the admin path, so neither could ever be handed back.

realms.createRealm(player, "volcano")
.thenAccept(realm -> player.sendMessage("Your realm is ready."))
.exceptionally(error -> {
RealmException refused = (RealmException) error.getCause();
if (refused.reason() == RealmException.Reason.ALREADY_OWNS_REALM) {
player.sendMessage("You already have one.");
}
return null;
});

The plugin’s own exception types are deliberately never handed over: they live in the half of the jar you cannot see, so they would be classes you could name in no catch.

All in studio.oreades.atlasrealms.api.event, all carrying a RealmView.

Event Fired when Cancellable
RealmEnterEvent a player is about to be put inside a realm no
RealmExitEvent a player leaves one, by teleport or by quitting no
RealmSetSpawnEvent somebody sets the realm’s spawn no
RealmSizeUpdateEvent the border is about to change no
RealmMemberSizeUpdateEvent the member limit is about to change no
RealmDeleteEvent a realm has been removed no
RealmPreUnloadEvent a realm nobody is in is about to be written and unloaded yes
  • RealmExitEvent.getInto() is the realm they are moving into, or null when they are leaving realms behind. Going straight from one realm into another fires this for the first, and a handler that cannot tell the two apart takes state off just after the arrival put it on.
  • RealmDeleteEvent is after the fact and not cancellable. A deletion removes worlds and clears folders; a handler that could stop it halfway would leave a realm whose ground is already gone. The view it carries is the last copy anything holds — the members, the bans and the theme exist nowhere else by then.

RealmSizeUpdateEvent and RealmMemberSizeUpdateEvent fire for a rank change and for a purchase alike, and the plugin’s own listener is what writes the row and moves the border — so cancelling one is not supported, but reading one tells you every border change on the server.

A plugin that greets a player when they walk into their own realm, and refuses to let a realm unload while it is running a job in it.

package example;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import studio.oreades.atlasrealms.api.AccessLevel;
import studio.oreades.atlasrealms.api.AtlasRealmsAPI;
import studio.oreades.atlasrealms.api.RealmFlag;
import studio.oreades.atlasrealms.api.RealmView;
import studio.oreades.atlasrealms.api.event.RealmEnterEvent;
import studio.oreades.atlasrealms.api.event.RealmPreUnloadEvent;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class ExamplePlugin extends JavaPlugin implements Listener {
private final Set<UUID> working = ConcurrentHashMap.newKeySet();
private AtlasRealmsAPI realms;
@Override
public void onEnable() {
RegisteredServiceProvider<AtlasRealmsAPI> found =
getServer().getServicesManager().getRegistration(AtlasRealmsAPI.class);
if (found == null) {
getLogger().info("AtlasRealms is not on this server; the realm features stay off.");
return;
}
realms = found.getProvider();
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onEnter(RealmEnterEvent event) {
RealmView realm = event.getRealm();
Player player = event.getPlayer();
if (realm.levelOf(player.getUniqueId()) != AccessLevel.OWNER) {
return;
}
// Read from memory, on the tick thread the event already gave us.
player.sendMessage("Welcome home. PvP here is "
+ (realms.flag(realm, RealmFlag.PVP) ? "on" : "off") + ".");
}
/** The realm has to stay up while a job of ours is running inside it. */
@EventHandler
public void onPreUnload(RealmPreUnloadEvent event) {
if (working.contains(event.getRealm().getId())) {
event.setCancelled(true);
}
}
/** Anything that writes goes through a future, and lands back on a tick. */
public void closeRealmOf(UUID player) {
realms.realmOf(player).ifPresent(realm ->
realms.setStatus(realm.getId(), studio.oreades.atlasrealms.api.RealmStatus.CLOSED)
.thenRun(() -> getLogger().info("Closed " + realm.getId()))
.exceptionally(error -> {
getLogger().warning("Could not close it: " + error.getMessage());
return null;
}));
}
}

With PlaceholderAPI installed, AtlasRealms registers the atlasrealms expansion. Every placeholder is read from memory and answers for an offline player. See Placeholders for the full list.

studio.oreades.atlasrealms.api is the whole of it. Everything else in the plugin — the managers, the storage, the world backends, Realm itself — is renamed on every release and is not on your classpath at runtime, so naming one is a compile against something that will not be there.

If the API is missing something you need, that is worth an issue rather than a workaround.