Warps & Spawn
Warps are global, network-wide teleport destinations — unlike homes, they aren't tied to a player or a server group. They're persisted in MariaDB (warps table, via a CrudRepository<Warp>) and mirrored into an in-memory ConcurrentHashMap<String, Warp> keyed by lowercase name. The map, not the database, is what every command and listener actually reads from at runtime; the repository is only touched on create/update/delete. Spawn is not a separate concept — it's simply the warp literally named spawn.
Model
The Warp entity (@Entity("warps")) maps onto the warps table:
Java field | Column | Type | Notes |
|---|---|---|---|
|
|
| Primary key. Always set explicitly with |
|
|
| Warp name, exactly as typed to |
|
|
| Server group the warp was created on, or last overwritten on ( |
|
|
| Location coordinates. |
|
|
| Look direction. |
|
|
| World name on the owning server. |
(unmapped) |
|
| Managed by MariaDB ( |
Warp.location()/Warp.location(MinecraftLocation) bundle x/y/z/yaw/pitch/world into (and out of) a single MinecraftLocation, which is what gets handed to the teleportation service. See Configuration for the raw schema.sql table definition and how database.properties is provisioned.
Loading
WarpService's constructor — run once, during onEnable — calls repository.findAll() asynchronously, logging Loading warps... and then, on completion, the count and elapsed time. Every returned row is put into the in-memory map keyed by warp.name().toLowerCase(). Because the load is asynchronous, the map can briefly be empty immediately after onEnable returns; in practice it fills before players can reach /warp or /spawn. WarpService.warps() exposes an unmodifiable view of the same map (used by tab completion).
Creating and updating a warp
Permission: warps.set. Tab-completes existing names via @warps. WarpService.set captures the player's current location (PaperAdapter.toMinecraftLocation) and the current server's group, then branches on whether name.toLowerCase() is already a key in the map:
New name — builds a fresh
Warpwith a new random UUID and inserts it (repository.insert).Existing name — looks the warp up case-insensitively and mutates its
locationandgroupin place, then persists withrepository.update. The warp keeps its originaluniqueIdand its originalnamecasing — retyping/warp set Spawnagainst an existingspawnwarp moves it but does not rename it toSpawn. There is no separate rename command; the only way to change a warp's name is to delete it and create it again under the new name.
Either path sends the same message on success — WARP_CREATED, with placeholders %warp_name%, %warp_x%, %warp_y%, %warp_z% — there is no distinct "warp updated" message. A repository failure logs the error and sends UNEXPECTED_ERROR instead. Once the database write completes, the in-memory map is updated in the same callback, so /warp set never leaves the map and the database out of sync with each other.
Deleting a warp
Aliases: del, remove, rm. Permission: warps.delete. The <name> argument is typed Warp, resolved by WarpContextResolver (see Tab completion and argument resolution) — an unknown name never reaches the command method at all. WarpService.delete removes the row via repository.delete(warp.uniqueId()), then removes the entry from the in-memory map, then sends WARP_REMOVED. A repository failure logs the error and sends UNEXPECTED_ERROR instead, leaving the warp in memory and in the database untouched.
Teleporting to a warp
No permission node. <name> resolves to a Warp the same way /warp delete does. The command runs the warp through the shared warmup first (see Warmup — instant if the player's resolved duration is zero), then calls:
This is the TeleportationService overload keyed by server group plus a MinecraftLocation, so it's cross-server aware by construction — the plugin never checks whether the warp's group matches the server the player is currently on. On completion:
Outcome | Message |
|---|---|
The future completes exceptionally |
|
|
|
|
|
WARP_TELEPORT_FAILURE covers every non-success TeleportationResponse alike (TIMEOUT, SERVER_OFFLINE, PLAYER_NOT_FOUND, SERVER_NOT_FOUND, NO_AVAILABLE_SERVER, …) — the command does not distinguish between them.
Spawn
No permission node, player-only. SpawnCommand calls WarpService.findByName("spawn") — the same case-insensitive lookup as everything else, so a warp created as /warp set Spawn still satisfies it. If it exists, the player goes through the identical warmup-then-teleport flow as /warp <name>, replying with SPAWN_TELEPORT_SUCCESS or SPAWN_TELEPORT_FAILURE. If no warp named spawn exists, the player receives NO_SPAWN_SET and nothing is attempted. There is no dedicated spawn-location config key — an admin configures spawn purely by standing where they want it and running /warp set spawn.
Vanilla respawn and the fall safety net
SpawnListener (registered alongside the other listeners in onEnable) hooks the spawn warp into two more places beyond the /spawn command:
AsyncPlayerSpawnLocationEvent— on every vanilla respawn calculation, the listener looks up thespawnwarp. If it's missing, a warning is logged and vanilla respawn logic is left alone. If it exists and its storedgroupequals the current server's group, the event's spawn location is overridden to the warp's location — so beds/respawn anchors are ignored in favor of the group's configured spawn. On a server whose group differs from the one thespawnwarp was set on, this override does not apply.PlayerMoveEventfall/void safety net — skipped entirely if the current server's environment isEnvironment.DEVELOPMENT, or if the current server's group is not literally"spawn"(a hardcoded literal, independent of whatever group thespawnwarp itself was saved under). Otherwise, the first move event where a player's Y position is below-50or their accumulated fall distance exceeds70blocks triggers an immediate plainplayer.teleport(...)to thespawnwarp's location — provided that warp's stored group also matches the current server's group. This path is silent (no message), synchronous, and does not go throughWarmupServiceorTeleportationService— it only ever moves the player within the same server.
Tab completion and argument resolution
Two ACF hooks back every warp-name argument:
@warps(WarpCompletionHandler, registered asregisterAsyncCompletion("warps", ...)) returnsplugin.warps().warps().keySet()— every currently loaded warp name, lowercase, unfiltered by permission. Used by/warp <name>,/warp set <name>, and/warp delete <name>.WarpContextResolver(registerContext(Warp.class, ...)) backs any command parameter typedWarp—/warp <name>and/warp delete <name>. It pops the first argument, resolves it withWarpService.findByName(case-insensitive), and throwsInvalidCommandArgument("Warp not found: <name>")if nothing matches, which ACF surfaces to the player as a command error before the command method ever runs.
/warp set <name> takes name as a plain String instead, since it must accept names that don't exist yet.
Message placeholders
Each warp/spawn message is built with a fresh PlaceholderContainer seeded with the relevant Warp for that call — these tokens only resolve inside their own message template in messages.yml, unlike the globally-registered %homes_...% tokens on Placeholders:
Message | Placeholders |
|---|---|
|
|
|
|
|
|
|
|
| — |
| — |
| — |
See Commands for the full /warp//spawn syntax and permission summary, and Overview for how warp storage compares to home/last-location storage.