Astral Realms Documentation Help

Islands

An island is the persisted unit of skyblock gameplay: a database row (name, lock state, level, spawn pose) plus, while active, a loaded SlimeWorld instance. This page covers the island lifecycle, blueprints, world management, multi-server hosting, cobblestone generators, block values, upgrades, the island-scoped GUI surface, and cross-server persistence. For the role/permission/ settings model see Roles, Permissions & Settings; for membership, co-ops and invitations see Members, Co-ops & Invitations.

IslandService (islands()) owns creation, deletion and spawn resolution; WorldService (worlds()) owns the underlying SlimeWorld. The three lifecycle entry points below are all reachable from /is create|delete|go.

Lifecycle

Create

SkyblockCommand#onCreate calls IslandService#create(Player, String name, IslandBlueprint blueprint):

  1. Name resolution — a null/blank name falls back to the player's username; otherwise it's trimmed.

  2. Uniqueness checkIslandRepository#existsByName checks the in-memory name index first, falling back to SELECT 1 FROM islands WHERE name = ? on a miss. If the name is taken, ASMessages.NAME_ALREADY_TAKEN is sent and creation stops.

  3. Row build — a new Island is built in memory: UUID.randomUUID(), the resolved name, locked = true, level = 0, and the spawn pose copied from the blueprint's spawn-location.

  4. Transactional persistIslandRepository#create inserts the island row, every seeded role from roles.yml (RoleService#defaultRoleSeeds), and the owner member in one JDBC transaction. Either everything commits or nothing does.

  5. World creation — only once that transaction commits does WorldService#create(islandId, blueprint) clone the blueprint's slime-world template into a brand-new world named after the island's UUID (see Blueprints and Island worlds).

  6. Rollback on failure — if world creation throws or returns null, the just-inserted island row (and its roles/owner) is deleted so a failed creation never leaves an island with no world behind.

  7. On success — the player is teleported to the new world's spawn, ASMessages.ISLAND_CREATED is sent, and IslandCreateEvent fires.

Go / spawn

SkyblockCommand#onGo calls IslandService#spawnIsland(Island), which resolves where the island's world is (or should be) hosted and returns a NetworkLocation; the command then hands that to AstralCore's TeleportationService for the actual — possibly cross-server — teleport. The whole resolution is bounded by a 15-second timeout. See Multi-server hosting for the exact server-selection order.

Delete

SkyblockCommand#onDelete calls IslandService#delete(Player, Island):

  1. IslandRepository#delete removes the database row, invalidates the island from the local and shared cache, and broadcasts the deletion (see Persistence & cross-server sync).

  2. Only after that succeeds, WorldService#delete(islandId) broadcasts an IslandDeletePacket (see Multi-server hosting) so whichever server currently hosts the world drops it without saving, then unloads/deletes it locally and clears the host-server mapping.

  3. On success: ASMessages.ISLAND_DELETED is sent and IslandDeletedEvent fires.

Neither the command nor the service performs an ownership or permission check — see Commands.

Blueprints

A blueprint is a named template an island is created from: a slime-world source, a spawn pose, and a menu icon. Blueprints live under plugins/AstralSkyblock/blueprints/*.yml, loaded by BlueprintService#load() (ConfigurationManager#loadFolder) — called on plugin enable and on every /is reload.

id: "default" default: true source-world: "default.slime" spawn-location: x: 0.5 y: 65.0 z: 0.5 yaw: 0.0 pitch: 0.0 world: "default" icon: material: stone name: "Default Blueprint" lore: - "This is the default blueprint." - "It is used when no other blueprint is specified."

Field (Java)

YAML key

Type

Notes

id

id

String

Blueprint identifier. Tab-completed on /is create via @islandBlueprints (blueprints().keys()).

isDefault

default

boolean

Exactly one loaded blueprint must be flagged default: true — used when /is create is given no blueprint. Plugin startup throws IllegalStateException if none is.

sourceWorld

source-world

String

Filename of the slime-world template under plugins/AstralSkyblock/sourceWorlds/. A trailing .slime is stripped before the template is read (WorldService#createNewWorld), so source-world is really an ASP world id, not a literal path.

spawnLocation

spawn-location

MinecraftLocation (x, y, z, yaw, pitch, world)

Becomes the new island's stored spawn pose. Only x/y/z/yaw are written into the cloned world's ASP SlimePropertyMap (SPAWN_X/SPAWN_Y/SPAWN_Z/SPAWN_YAW) — pitch has no ASP world property and only lives on the Island row.

icon

icon

ItemStackWrapper

Menu icon, e.g. rendered by the island-creation blueprint picker via %parameter_blueprint_icon%.

BlueprintService methods:

Method

Returns

Behavior

load()

Clears and reloads every blueprints/*.yml. Skips (warns) a duplicate id, and skips (warns) a blueprint whose source-world file doesn't exist under sourceWorlds/. Then picks the single default: true entry, throwing if none exists.

findById(String)

Optional<IslandBlueprint>

Lookup by id, e.g. used by IslandBlueprintContextResolver.

all()

Collection<IslandBlueprint>

Every loaded blueprint.

keys()

Collection<String>

Every loaded blueprint id — backs the @islandBlueprints tab completion.

defaultBlueprint()

IslandBlueprint

The blueprint flagged default: true. Used by /is create when no <blueprint> argument is given.

Island worlds

WorldService is the AdvancedSlimePaper (ASP) integration layer. It reads blueprint templates from a read-only FileLoader over sourceWorlds/, and reads/writes live island worlds through a MysqlLoader configured from loader.yml, keyed by the island's UUID string. WorldService#load() initializes that MySQL loader — idempotent, so a second call (e.g. from /is reload) is a no-op — and, only on servers where SkyblockConfiguration#isIslandServer() is true, starts a 30-second idle-unload sweep task.

Method

Behavior

create(UUID, IslandBlueprint)

Clones the blueprint's template world under the island's UUID on the MySQL loader, loads it into Bukkit, saves it once, and returns the SlimeWorldInstance. Any failure triggers a best-effort delete() of whatever got partially created.

load(Island)

Returns the already-loaded instance if present. Otherwise coalesces concurrent callers (e.g. two visitors, or a visit racing a cross-server load request) onto one in-flight future so asp.loadWorld runs at most once per island, reads the world off the MySQL loader asynchronously, then loads it on the main thread.

save(UUID)

Saves the loaded instance via ASP. Fails if the world isn't currently loaded on this server.

unload(UUID)/unload(UUID, boolean save)

Unloads the Bukkit world, drops it from the loaded-world/name maps, and clears the host-server mapping. save defaults to true; delete() calls unload(id, false) so a deleted island's world is never re-persisted.

delete(UUID)

Broadcasts an IslandDeletePacket so a remote host drops the world without saving, unloads locally without saving, clears the host mapping, then deletes the world from the MySQL loader (a world absent from storage is treated as already deleted, not an error).

On every successful load, IslandSettingsListener.applyEnvironment re-applies the island's actual IslandSettings to the Bukkit world (overriding some of the fixed defaults below — e.g. PvP), the current server claims the host-server slot via ServerService#setHostServer, and IslandWorldLoadedEvent fires. On unload, IslandWorldUnloadedEvent fires — but only if the island is present in the local island cache; otherwise a warning is logged and the event is skipped.

Every cloned/loaded island world gets the same fixed ASP property map (not currently configurable per blueprint or per island): difficulty normal, monsters and animals allowed, no dragon battle, world-level PvP off (overridden per-island by settings), environment NORMAL, world type DEFAULT, default biome minecraft:plains, and block ticks/fluid ticks/POI saving all disabled.

Idle-unload sweep

sweepIdleWorlds() runs every 30 seconds on island servers. A loaded world with zero online players starts a timer; once it has been empty for world-idle-unload-seconds (config.yml; 0 /absent falls back to 300s, a negative value disables the sweep entirely) it is unloaded, freeing the server's island slot. A player rejoining before the grace period elapses resets the timer. On plugin shutdown, every currently-loaded world is saved and its host-server mapping cleared (best-effort, 5-second timeout) before the loader is closed, so a restart doesn't leave stale island→server routing pointing at a dead server.

Multi-server hosting

ServerService (servers()) tracks which island server currently hosts each island, and how full each island server is.

Method

Behavior

findEmptiestServer()

Fetches every published IslandServer heartbeat, filters to loadedIslands < maximumIslands, and returns the one with the fewest loaded islands (or null if none qualify).

setHostServer(UUID island, UUID server)

Writes island → server into the ISLAND_SERVER_KEY Redis hash (skyblock:islands:server). Called on every successful world load.

findHostServer(UUID island)

Reads that hash; null if the island isn't currently hosted anywhere.

deleteHostServer(UUID island)

Removes the hash entry. Called on unload, delete, and plugin shutdown.

On island servers, ServerService also publishes a self-reported IslandServer(uniqueId, loadedIslands, maximumIslands) heartbeat every 300 ticks (15s) into a 1-minute-TTL cache (SERVER_CACHE_KEY, skyblock:servers); maximumIslands comes from config.yml's maximum-islands.

IslandService#spawnIsland(Island) resolves a NetworkLocation in this order:

  1. findHostServer — if the island is already hosted somewhere, resolve the location against that server.

  2. findEmptiestServer — otherwise pick the least-loaded eligible island server. If it is the current server, load the world locally (WorldService#load) and resolve. If it's a different server, send an IslandLoadRequestPacket and await a reply.

  3. The whole call is wrapped in a 15-second orTimeout.

Cross-server requests travel over ISLAND_MANAGEMENT_CHANNEL (skyblock.island.management), registered in ASPacketRegistry:

Packet

Registry id

Purpose

IslandLoadRequestPacket

0x100

Requests that a target server load an island's world (payload: island id, target server id). Sent with messaging().sendWithReply(...).

IslandLoadResponsePacket

0x101

Reply carrying a success boolean, sent back via messaging().replyTo.

IslandDeletePacket

0x106

Broadcast on delete; tells whichever server currently hosts the island to drop the world without saving (see Delete).

IslandService's constructor registers the receiving side of ISLAND_MANAGEMENT_CHANNEL: an IslandLoadRequestPacket replies success = true immediately if the world is already loaded, otherwise loads it and replies with the outcome; an IslandDeletePacket unloads the world locally without saving if it's currently hosted on this server (the sending server is echo-suppressed, so it handles the local unload itself in WorldService#delete).

Cobblestone generators

A generator is a weighted block table an island's cobblestone/basalt generation re-rolls into. Generators live under plugins/AstralSkyblock/generators/*.yml, loaded by GeneratorService#load().

id: "overworld" blocks: COBBLESTONE: 50 STONE: 5 SAND: 5 DIRT: 10 GRASS_BLOCK: 10 OAK_LOG: 5 OAK_LEAVES: 5

blocks maps a Material to a relative weight; non-block materials are silently filtered out when the table is built. GeneratorConfiguration#randomBlock() lazily builds a RandomCollection<BlockData> from the map (cumulative-weight binary search) and returns a weighted-random pick.

GeneratorService methods:

Method

Behavior

load()

Loads generators/*.yml, skipping (warning on) duplicate ids. Warns — but does not fail startup — if generators.default in config.yml names a generator that wasn't loaded.

defaultGenerator()

Looks up the generator whose id matches config.yml's generators.default.

findById(String)

Optional<GeneratorConfiguration> lookup by id.

GeneratorListener registers only when generators.enabled is true in config.yml (independent of the island-server check). On BlockFormEvent, if the newly-formed block in an island world is COBBLESTONE or BASALT, it's replaced with a random block from generators().defaultGenerator()'s table and the event is cancelled so the vanilla block never actually forms. A second handler for BlockFromToEvent (lava-flow-based generation) exists in source but is currently a no-op stub pending a fix.

Per-island generator selection is not implemented in this version: GeneratorListener always uses the single network-wide defaultGenerator(), regardless of the island's GENERATOR upgrade level — that upgrade type exists in the enum but nothing in source currently reads it.

Island level & block values

block-values.yml assigns a point value to blocks and spawner types:

blocks: DIAMOND_BLOCK: 300 MOSSY_COBBLESTONE: 2 'MOB_SPAWNER:IRON_GOLEM': 10000 # …

Each key is either a bare Material name, or MOB_SPAWNER:<ENTITY> which matches a placed spawner whose CreatureSpawner#getSpawnedType() equals <ENTITY> (case-insensitive). BlockValueConfiguration#blocks() compiles this into a Map<Predicate<Block>, Integer>.

Today this table backs exactly one consumer: IslandPermissionsListener#isValuable. On BlockBreakEvent, a block matched by any predicate in block-values.yml is gated behind IslandPermission.VALUABLE_BREAK instead of the plain BREAK permission (spawners specifically use SPAWNER_BREAK) — see Roles, Permissions & Settings.

Island#level is a persisted int column (islands.level), exposed as island.level() and via the level case of the island placeholder namespace — e.g. main.yml renders it as %parameters_island_level% ("Niveau : ...") since island-main is opened with the island passed in as the island menu parameter. It is written once at creation (always 0) and there is no code path in this version that recalculates it from block-values.yml or anything else afterward — the point values above currently only drive the VALUABLE_BREAK permission gate, not an island-level score.

Upgrades

UpgradeType enumerates nine purchasable tracks:

UpgradeType

WORLDBORDER_SIZE

MEMBERS_LIMIT

COOP_LIMIT

GENERATOR

HOPPERS_LIMIT

CROP_GROWTH_SPEED

SPAWNERS_RATE

MOB_DROPS

MINECART_LIMITS

Each type's price/value tiers are defined in upgrades/<type>.yml, loaded by UpgradeService#load() into an IslandUpgrade blueprint (type, levels: Map<Integer, Level>):

type: WORLDBORDER_SIZE levels: 0: level: 0 price: 0 value: 1000 1: level: 1 price: 1000 currency: "default" currency-display: "Default Currency" value: 2000 unlock-actions: - "[message] gg"

Level field

YAML key

Type

Notes

level

level

int

Tier number; also the map key.

price

price

double

Cost to reach this tier.

currency

currency

String

Currency id charged.

currencyDisplay

currency-display

ComponentWrapper

Display name for the currency.

value

value

double

The tier's effective value, read via IslandUpgrade.Level#value(). Semantics are type-specific (e.g. presumably a worldborder radius for WORLDBORDER_SIZE) but unverifiable from source — see the caveat below.

unlockActions

unlock-actions

PaperActionList

Actions listed for the tier; not currently executed anywhere in source (see caveat below).

UpgradeService (upgrades()) methods:

Method

Behavior

load()

Loads upgrades/*.yml into the blueprint map, keyed by UpgradeType, warning on a duplicate type.

findByType(UpgradeType)

Optional<IslandUpgrade> — the price/tier blueprint for a type.

blueprints()

Collection<IslandUpgrade> — every loaded upgrade blueprint.

level(UUID island, UpgradeType type)

int — the island's current level for that type, cached O(1); 0 if never purchased.

setLevel(UUID island, UpgradeType type, int level)

Persists the new level, rebuilds the island's cached upgrade snapshot (IslandService#refreshUpgrades), and broadcasts the change so other servers refresh too.

findByIsland(UUID island)

CompletableFuture<Map<UpgradeType, Integer>> — every stored level for an island (rows are override-only: a never-purchased upgrade has no row and resolves to 0).

Island GUIs

Island management is almost entirely menu-driven (see Commands for the handful of ACF subcommands). Every screen is a menus/*.yml file with an explicit id, opened via [open-menu] <id> action chains:

Menu id

File

Purpose

island-main

main.yml

Island hub — teleport shortcut, and links to members/roles/settings/coops/bans, each gated by a view-requirements check against the matching IslandPermission. Opened by /is (default).

island-creation

creation.yml

Blueprint picker; each icon's click action includes [create-island].

island-settings

settings.yml

Toggles IslandSettings — see Roles, Permissions & Settings.

island-roles

roles.yml

Role list / role management entry point.

island-permissions

permissions.yml

Per-role IslandPermission toggles.

island-members

members.yml

Member roster.

island-member-manage

member-manage.yml

Per-member action menu (kick/promote/demote/transfer/ban entry points).

island-member-role

member-role.yml

Assigns a member's role.

island-coops

coops.yml

Co-op roster.

island-bans

bans.yml

Banned-player roster.

island-confirm-kick, island-confirm-ban

confirm/confirm-*.yml

Yes/No confirmation step, opened from island-member-manage's kick/ban buttons.

island-confirm-disband, island-confirm-leave

confirm/confirm-*.yml

Shipped but unreferenced — no menu or command in this build opens either one.

Persistence & cross-server sync

IslandRepository extends the shared UUIDSyncedRepository<Island>/SyncedRepository base, a three-tier cache: L1 a local Caffeine cache per server, L2 a shared Redis cache, L3 MySQL (the source of truth). A write (save/create) goes to L3, then L1+L2, and only then publishes an update — so a server reacting to the notification never reads a stale L2 value. A delete removes from L3, invalidates L1+L2, and publishes an invalidation. Other servers' subscribers call invalidateLocally/cache.refresh on receipt (L1 only — L2 is already authoritative since it's shared).

Constant

Value

ISLAND_CACHE_KEY

skyblock:islands (L2 keys are skyblock:islands:<uuid>)

ISLAND_UPDATE_CHANNEL

skyblock.island.update

ISLAND_MANAGEMENT_CHANNEL

skyblock.island.management

ISLAND_SERVER_KEY

skyblock:islands:server

IslandRepository#cascade (its postLoad hook) hydrates every transient relationship on a cache miss and on startup warmup — roles, members (each resolved to its role), owner, coops, settings, and upgrade levels — so a cached Island handed back from findCachedById/findById is always fully populated. IslandService#warmup() pages through every island 500 at a time (ASConstants.ISLAND_WARMUP_PAGE_SIZE), cascading and caching each one locally before the plugin finishes enabling; a warmup failure is logged but does not abort startup (islands missing from L1 are lazily loaded on first access instead). IslandService#refreshRelationships and #refreshUpgrades re-run a narrower slice of this cascade after a membership/role or upgrade write.

Events

Event

Fields

Fired

IslandCreateEvent

player(), island(), world()

After IslandService#create commits the DB row and the world is created — see Create.

IslandDeletedEvent

player(), island()

After IslandService#delete's database delete succeeds — see Delete.

IslandWorldLoadedEvent

island(), world()

From WorldService#loadWorld, after registering the instance, applying IslandSettings, and claiming the host-server slot.

IslandWorldUnloadedEvent

island(), world()

From WorldService#unload, only if the island is present in the local island cache (otherwise skipped with a warning).

All four extend Bukkit's Event with fluent accessors (event.island(), not getIsland()) per the plugin's lombok.accessors.fluent = true config. IslandCreateEvent and IslandDeletedEvent additionally derive their async flag from the calling thread at construction time (super(!Bukkit.isPrimaryThread())) rather than hardcoding it — listeners should not assume synchronous execution. See Developer API for the full event catalogue and listening examples.

See also

Last modified: 25 July 2026