Astral Realms Documentation Help

Teleport Requests

/tpa and /tpahere implement a two-party teleport-request handshake that works across the whole network, not just the current server. All state for a pending request — who asked whom, in which direction, and when — lives in Redis with a five-minute TTL rather than in plugin memory, and TeleportationRequestService resolves whether the recipient is local or on another server before deciding how to answer. See Commands for full command syntax and aliases.

Sending a request

/tpa <player> and /tpahere <player> both call TeleportationRequestService.sendRequest(Player sender, MinecraftPlayer target, boolean here)here=false for /tpa (the sender is moved to the target), here=true for /tpahere (the target is moved to the sender). Both commands reject a self-target before the service is ever invoked, replying with REQUEST_SELF.

sendRequest runs, in order:

  1. Duplicate checkhasPendingRequest(sender, target) looks for an existing sender → target entry in the cache. If found, the sender gets REQUEST_ALREADY_PENDING and nothing else happens. This check is direction-specific: it does not see a request already pending in the opposite direction (target → sender), so both directions can be pending at once.

  2. Pre-flight check — resolves whether the request can go through at all; see World protection and Cross-server delivery.

  3. Request creation — only on a SUCCESS pre-flight result. A TeleportationRequest(senderId, recipientId, here, timestamp) is written to the cache (see below); the sender gets REQUEST_SENT (HERE_REQUEST_SENT for /tpahere) and the target gets REQUEST_RECEIVED (HERE_REQUEST_RECEIVED).

Pre-flight result

Sender sees

Request created?

SUCCESS

REQUEST_SENT/HERE_REQUEST_SENT

Yes

REQUESTS_DISABLED

REQUESTS_DISABLED

No

PROTECTED_WORLD

WORLD_PROTECTED

No

ALREADY_PENDING

REQUEST_ALREADY_PENDING

No — short-circuited before pre-flight runs

FAILED

(nothing — silent)

No

FAILED only occurs if a remote server's reply can't be read as a TeleportationRequestStatusPacket; neither TpaCommand nor TpaHereCommand inspects the resolved status at all, so this case produces no feedback to the player. If the cross-server RPC times out instead of returning FAILED (see below), the future completes exceptionally and the command's .exceptionally handler sends UNEXPECTED_ERROR.

Responding: accept or decline

/tpaaccept [player] (aliases /tpaccept, /tpyes) and /tpadecline [player] (aliases /tpdeny, /tpn) both resolve to TeleportationRequestService.sendResponse, differing only in the accepted boolean.

  • No [player] argument — looks up the most recently received request via the homes:requests:latest:<you> cache key (getMostRecentForRecipient) and resolves the original sender from it.

  • [player] given explicitly — skips that lookup and reads the player → you cache entry directly (findRequest). Targeting yourself here is rejected with REQUEST_SELF.

Either way, if no matching request is found — already answered, cancelled, or expired past its 5-minute TTL — the responder gets REQUEST_NO_PENDING.

On a match:

  • Accept — the responder gets REQUEST_ACCEPTED, the original sender gets REQUEST_ACCEPTED_OTHER, the cache entry (and both latest-lookup keys) are deleted, and the plugin resolves who actually moves — see Movement.

  • Decline — the responder gets REQUEST_DECLINED, the original sender gets REQUEST_DECLINED_OTHER, the cache entry is deleted, and nobody teleports.

Cancelling

/tpacancel [player] cancels a request you sent, from the sender's side.

  • No [player] argument — cancels your own most recently sent request (cancelLastRequest), resolved via homes:requests:sender:<you> (getMostRecentForSender).

  • [player] given explicitly — cancels the specific you → player request (cancelRequest, via findRequest). Targeting yourself is rejected with REQUEST_SELF.

If there's nothing to cancel, you get REQUEST_NO_PENDING. On success the cache entry is deleted, you get REQUEST_CANCELLED, and the original target gets REQUEST_CANCELLED_OTHER. As noted on the Commands page, /tpacancel's declared @Syntax is <player> even though the argument is @Optional and behaves the same as /tpaaccept//tpadecline in practice.

Toggling requests

/tpatoggle (alias /tptoggle) flips HomePlayerData.teleportationRequestEnabled for the invoking player — a per-player flag replicated across the network by AstralSync, defaulting to true for a new player. TpaToggleCommand reads the current value through TeleportationRequestService.doesAcceptRequests(UUID) (which itself falls back to true if no HomePlayerData exists yet), writes the opposite value, and replies with TELEPORTATION_REQUEST_DISABLED or TELEPORTATION_REQUEST_ENABLED accordingly.

While disabled, any /tpa or /tpahere sent to that player — local or cross-server — is rejected pre-flight with REQUESTS_DISABLED, before a request is ever created. The same check (doesAcceptRequests(UUID, UUID)) also folds in AstralCore's network-wide IgnoreService: a sender is blocked with REQUESTS_DISABLED if the would-be recipient is ignoring them, even when the recipient's toggle is on.

Cross-server delivery

Pending requests are not held in plugin memory — TeleportationRequestService stores them entirely in Redis via RequestCacheRepository, so a request (and the toggle state that gates it) is visible network-wide regardless of which server the sender or recipient is on.

Key

Value

Purpose

homes:requests:<sender>:<recipient>

<timestamp>:<0 or 1> (1 = here)

The pending request itself.

homes:requests:latest:<recipient>

sender's UUID

Most recent request received by <recipient>, for argument-less /tpaaccept//tpadecline.

homes:requests:sender:<sender>

recipient's UUID

Most recent request sent by <sender>, for argument-less /tpacancel.

All three keys share a five-minute TTL (RequestCacheRepository.TTL), set on write and never refreshed. Once it lapses, the request simply disappears from the cache. HomesMessages defines a REQUEST_EXPIRED string, but nothing in TeleportationRequestService currently sends it — a stale request is only discovered on the next accept/decline/cancel attempt, which reports REQUEST_NO_PENDING the same as any other missing request. Deleting a request (removeRequest) clears the main key plus both latest-lookup keys for that pair.

To decide whether the recipient of a /tpa//tpahere is local, doPreFlightCheck first tries Bukkit.getPlayer(recipientId) on the sender's own server. If that's null, it broadcasts a TeleportationRequestPacket over the homes.teleportation.requests fanout exchange, using MessagingService's default 5-second RPC timeout. Every connected server receives the packet; only the one where the recipient is actually online replies, with a TeleportationRequestStatusPacket carrying that server's own doesAcceptRequests result and the protected-world permission for the recipient's current world (or null if the world isn't protected). If no server has the recipient online, no reply ever arrives and the RPC times out after 5 seconds — surfaced to the sender as UNEXPECTED_ERROR.

World protection

config.yml's worlds-permissions maps world name → permission node (see Configuration). A request is blocked with WORLD_PROTECTED (internally Response.PROTECTED_WORLD) when the recipient's current world is in that map and the sender lacks the mapped permission — the permission is always checked against the sender, never the recipient, even though the world in question is wherever the recipient is standing.

The local and cross-server paths check world protection in a different order relative to the toggle:

  • Local recipient — the check only runs if the recipient accepts requests in the first place; REQUESTS_DISABLED short-circuits before the world is even inspected.

  • Cross-server recipient — the recipient's server always reports its world's permission alongside its accept/deny result, regardless of whether requests are disabled. Back on the sender's server, that world-permission check is applied unconditionally and takes priority over the reported result — so a sender who lacks the permission gets WORLD_PROTECTED even when the remote recipient's requests are also disabled.

Movement

Acceptance never performs the teleport itself. sendResponse resolves a from/to pair from the request's here flag and hands off to AstralCore's TeleportationService.teleport(UUID, UUID), which transparently handles same-server teleports (a direct teleportAsync) and cross-server ones (sending the moving player to the target's server) alike.

Command

here

Who moves

/tpa

false

Sender → target's location

/tpahere

true

Target → sender's location

Declining never calls TeleportationService — the movement branch only runs when accepted is true.

Unlike /home, /warp, /back, and /spawn (all routed through AstralHomes's own WarmupService, see Configuration), an accepted teleport request is not passed through WarmupServiceTeleportationRequestService calls TeleportationService.teleport directly, so there is no action-bar warmup countdown before the teleport happens.

Placeholders

Every message above is sent through a PlaceholderContainer built from the acting player (%player%, %player_name%, …) plus the other party registered under the target namespace (%target_name%, …). See Placeholders for the full token reference.

Last modified: 25 July 2026