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.
Compiling against it
Section titled “Compiling against it”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:
dependencies: server: AtlasRealms: load: BEFORE required: false join-classpath: truerequired: false and a null check is the right shape unless your plugin is
useless without realms.
Getting the API
Section titled “Getting the API”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”.
The threading rule
Section titled “The threading rule”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.
Reading a realm
Section titled “Reading a realm”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.
Changing a realm
Section titled “Changing a realm”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:
createRealmcharges nothing.economy.create-costis what/realm createasks 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.ymlwhen the server owner changes it, and it is the same rule the menu obeys. - An owner is made by
transferRealm, never bysetMember. Two rows have to move with the realm, or you get a player owning two realms, or a realm owning nobody.setMemberwithAccessLevel.OWNERis 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.
enterchecks the ban and the status before it loads anything. It answersfalsefor a realm that would not have them, rather than pulling a closed world into memory that nobody will ever empty again.- Check
busyfirst for anything that races. A reset takes seconds, builds a world, and writes the realm it captured when it finishes.
When a call is refused
Section titled “When a call is refused”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.
Events
Section titled “Events”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.RealmDeleteEventis 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 worked example
Section titled “A worked example”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; })); }}Placeholders
Section titled “Placeholders”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.
What is not the API
Section titled “What is not the API”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.