Nexal TPA
A comprehensive teleport request plugin for Minecraft servers with warmup, cooldown, cross-server support, and extensive customization options.
Features
- TPA Requests - Players can request to teleport to others or request others teleport to them
- Player Selector GUI - Type
/tpawith no arguments to open a visual player selector; left-click to send TPA, right-click to send TPAHERE, blocked players shown greyed out; in cross-server mode it lists the players on the other servers too - Warmup System - Configurable delay before teleport executes, cancelled on movement or damage, with particle aura visualization and action bar countdown
- Cooldown System - Prevent spam by adding cooldown between requests
- Request Timeout - Requests automatically expire after configurable time
- Block System - Players can block specific players from sending requests
- Toggle System - Players can disable receiving all TPA requests
- Combat Protection - Prevent TPA during combat with configurable combat tag duration
- Request Management GUI - 5-row visual interface with pagination, deny all, and settings access
- Settings GUI - Visual interface to toggle TPA, view status, and manage blocked players
- Blocked Players GUI - Paginated interface to view and unblock players
- Clickable Chat Messages - Accept/deny buttons appear in chat when receiving a request
- Cross-Server Support -
/tpaand/tpahereplayers on other servers of a BungeeCord or Velocity network; on accept the proxy moves the player and they arrive at the other player's location, with the fee refunded if the move fails. Off by default, see Cross-Server Support - World Blacklist - Disable TPA in specific worlds, both for leaving them and for teleporting into them
- Cross-World Control - Optionally prevent TPA between different worlds
- Configurable Damage Cancel - Choose whether damage cancels teleport warmup
- Max Requests Limit - Limit pending incoming requests per player
- Comprehensive Messages - All messages, chat buttons, action bars and menu text customizable in
messages.yml, with sounds and titles - PlaceholderAPI Integration - Placeholders for pending requests, enabled status, and more
- TPA Fees - Optional Vault economy integration to charge per-teleport fees (configurable, disabled by default), waived for players with
nexaltpa.bypass.fee - Teleport History - Persistent per-player history of all TPA requests with paginated GUI viewer, recording how each teleport actually ended and the fee actually charged
- Dual Storage - JSON and SQL (MySQL) storage backends, selectable via config; several servers can share one SQL database
- Import/Export - Move player data between storage types or servers
- Admin Menu -
/tpa adminopens a menu to change the main settings in-game and to reload, export and import
Technologies
- Paper API 1.20.1+ - Minecraft server API (Spigot compatible)
- Utilizer v0.21.2 - Configuration, messages, items, tasks, GUIs
- nexal-core - Storage (JSON and SQL), help menu, license check
- ACF (Aikar Command Framework) - Annotation-based command system
- PlaceholderAPI - Placeholder integration (optional)
- Vault - Economy integration for TPA fees (optional)
Commands
Player Commands
| Command | Description | Permission |
|---|---|---|
/tpa [player] | Open player selector or send a teleport request | nexaltpa.tpa |
/tpahere \<player> | Request another player to teleport to you | nexaltpa.tpahere |
/tpaccept [player] | Accept incoming TPA request | nexaltpa.accept |
/tpdeny [player] | Deny incoming TPA request | nexaltpa.deny |
/tpacancel | Cancel your outgoing request | nexaltpa.cancel |
/tpatoggle | Toggle receiving TPA requests | nexaltpa.toggle |
/tpblock \<player> | Block a player from sending requests | nexaltpa.block |
/tpunblock \<player> | Unblock a player | nexaltpa.unblock |
/tpalist | View all pending requests in GUI | nexaltpa.list |
/tpasettings | Open TPA settings menu | nexaltpa.settings |
/tphistory | View your teleport history | nexaltpa.history |
All commands also work as subcommands of /tpa (e.g., /tpa accept, /tpa settings, /tpa history).
Admin Commands
| Command | Description | Permission |
|---|---|---|
/tpa admin | Open the admin menu (shows the admin help in the console) | nexaltpa.admin |
/tpa admin help | Show the admin commands | nexaltpa.admin |
/tpa admin reload | Reload config.yml and messages.yml | nexaltpa.admin.reload |
/tpa admin export | Write every player profile to export/tpa_users.json | nexaltpa.admin.export |
/tpa admin import | Load export/tpa_users.json into the current storage | nexaltpa.admin.import |
Permissions
| Permission | Description | Default |
|---|---|---|
nexaltpa.tpa | Send TPA requests | true |
nexaltpa.tpahere | Send TPAHERE requests | true |
nexaltpa.accept | Accept requests | true |
nexaltpa.deny | Deny requests | true |
nexaltpa.cancel | Cancel outgoing requests | true |
nexaltpa.toggle | Toggle TPA on/off | true |
nexaltpa.block | Block players | true |
nexaltpa.unblock | Unblock players | true |
nexaltpa.list | View request GUI | true |
nexaltpa.settings | Open settings GUI | true |
nexaltpa.history | View teleport history | true |
nexaltpa.bypass.cooldown | Bypass request cooldown | op |
nexaltpa.bypass.fee | Teleport without paying the economy fee (e.g. for donor ranks) | false |
nexaltpa.admin | Admin menu and all admin commands (includes the three below) | op |
nexaltpa.admin.reload | Reload config | op |
nexaltpa.admin.export | Export data | op |
nexaltpa.admin.import | Import data | op |
Placeholders
Requires PlaceholderAPI.
| Placeholder | Description |
|---|---|
%nexaltpa_pending_requests% | Number of pending incoming requests |
%nexaltpa_tpa_enabled% | Yes/No if TPA is enabled for player |
%nexaltpa_tpa_enabled_status% | Colored enabled/disabled status |
%nexaltpa_blocked_count% | Number of blocked players |
%nexaltpa_has_outgoing% | Yes/No if player has outgoing request |
%nexaltpa_history_count% | Number of history entries |
Configuration
config.yml
# Request timeout in seconds
request-timeout: 60
# Warmup delay in seconds before teleport (0 = instant)
warmup-delay: 3
# Cooldown between sending requests in seconds
cooldown: 30
# Allow TPA while in combat
allow-in-combat: false
# Combat tag duration in seconds
combat-tag-duration: 10
# Particle aura during teleport warmup
warmup-particles:
enabled: true
# e.g. PORTAL, END_ROD, FLAME, ENCHANT, DUST.
# The names from before 1.20.5 (ENCHANTMENT_TABLE, REDSTONE) work too.
type: PORTAL
count: 15
radius: 0.8
update-interval: 2
# Worlds where TPA is disabled
disabled-worlds: []
# Maximum pending incoming requests per player (0 = unlimited)
max-requests-per-player: 10
# Whether cross-world TPA is allowed
allow-cross-world: true
# Whether taking damage cancels teleport warmup
warmup-cancel-on-damage: true
# Clickable accept/deny buttons in chat when receiving a request
clickable-messages:
enabled: true
# Cross-server TPA on a BungeeCord or Velocity network (README: Cross-Server Support).
# Install the plugin on every backend server and give them all the same settings below.
# Velocity needs bungee-plugin-message-channel = true in velocity.toml (the default).
# Messages travel over a player's connection: a server with nobody online can neither send
# nor receive requests.
cross-server:
# Turn on cross-server TPA. Leave off on a single server.
enabled: false
# Shared secret that signs every message between the servers, so nobody can forge one.
# Use the same long random value (at least 16 characters) on every server, and keep it private.
# Cross-server TPA refuses to start while this is empty or still this default.
secret: "change-this-to-a-long-random-secret"
# Seconds a message stays valid. Older ones are refused, so a recorded message cannot be
# sent again later. Raise it only if the servers' clocks differ by more than this.
max-message-age: 30
# Economy settings (requires Vault)
economy:
# Enable TPA fees
enabled: false
# Cost per TPA request (charged to the sender on teleport)
tpa-fee: 100.0
# How fees are shown in messages, menus and history. %amount% is the number
price-format: "$%amount%"
# The number, as a Java DecimalFormat pattern: , groups thousands, .00 gives two decimals
number-format: "#,##0.00"
# Leave out the decimals of whole amounts: $1,000 rather than $1,000.00 (but $1,000.50)
hide-zero-decimals: true
# History settings
history:
# Maximum history entries stored per player
max-entries-per-player: 50
# Storage settings
storage:
# Storage type: json or sql. On a network, point every server at the same SQL database
# to share the TPA toggle, block list and history (JSON keeps them per server).
type: json
# SQL settings (used when type is "sql")
sql:
host: localhost
port: 3306
database: minecraft
username: root
password: ""
messages.yml
All messages are fully customizable with:
- MiniMessage colors and gradients (
<gradient:#00D2D3:#54A0FF>,\<white>); legacy&codes also work - Sounds (Minecraft sound names)
- Titles and subtitles with timing, and action bars (
actionbar:) - Placeholders (listed in a comment above each message)
Messages, and menu and chat button text (gui, request-buttons), added in an update are used
with their default text until you add them to your file. Fee amounts (%amount%, %balance%)
already include the currency from economy.price-format, so a $ written before them in an
older file is left out.
Key message keys:
request-sent- Request sent successfullyrequest-received- Incoming request notificationrequest-accepted- Request accepted messagesrequest-denied- Request denied messagesrequest-expired- Request timeout messageswarmup-started- Teleport warmup countdownwarmup-cancelled- Warmup cancelled (moved/damaged)teleport-complete- Successful teleportcooldown-active- Cooldown remainingcombat-tagged- Cannot TPA in combattarget-max-requests- Target has too many pending requestscross-world-disabled- Cross-world TPA not allowedrequest-received-cross-server,request-received-here-cross-server,request-accepted-cross-server- Requests between serverscross-server-connecting,cross-server-no-response,cross-server-not-ready,cross-server-destination-offline,cross-server-move-failed- Moving between servers, and when the other server does not answer or the move failsfee-refunded- The fee of a cross-server teleport that did not happen was given backworld-disabled,world-disabled-target- A blacklisted world is involvedrequest-buttons- The clickable[✔ Accept]/[✘ Deny]line under a request, and its hover textswarmup-countdown,teleport-complete-actionbar- Action bars during and after the warmupgui- Titles, item names and lore of every player menu, the accent glass colours (gui.accent) and the button click sound (gui.click-sound)- And many more...
How It Works
Basic TPA Flow
- Request: Player A uses
/tpa PlayerB - Notification: Player B receives a notification with clickable
[✔ Accept]/[✘ Deny]buttons - Accept: Player B clicks
[✔ Accept]or uses/tpaccept - Warmup: Player A enters warmup period (configurable, default 3 seconds)
- Teleport: After warmup, Player A teleports to Player B's location
TPAHERE Flow
- Request: Player A uses
/tpahere PlayerB - Accept: Player B accepts
- Result: Player B teleports to Player A (reversed direction)
Request Management
- Timeout: Requests expire after 60 seconds (configurable)
- Cooldown: Players must wait 30 seconds between requests (configurable)
- Max Requests: Limit pending incoming requests per player (default 10)
- Block List: Blocked players cannot send you requests
- Toggle: Completely disable receiving requests with
/tpatoggle
Warmup System
- Configurable Delay: Set warmup time in
config.yml(0 = instant) - Movement Cancel: Moving cancels the teleport
- Damage Cancel: Taking damage cancels the teleport (configurable with
warmup-cancel-on-damage) - Visual Feedback: Countdown messages and warnings
- Action Bar Countdown: Displays "Teleporting in Xs..." on the action bar each second during warmup
- Particle Aura: Spiraling particle ring around the player during warmup with a burst effect on completion (configurable type, radius, and count)
Menus
Every player menu has cyan glass on its corners (the rest is left empty), your own head at the
top with an overview, and a click sound on its buttons; the buttons that do something glow.
Item lore follows ● Label » Value, with ▶ Left Click hints and ✔/✘ for states.
Request GUI
Access with /tpalist to see all pending requests in a 5-row visual interface:
- Player Heads: Shows requester's head with their name and world
- Request Type: Shows TPA or TPAHERE with color coding
- Time Remaining: Live countdown to expiration
- Click to Accept: Left-click any request to accept (the same checks as
/tpacceptapply) - Click to Deny: Right-click any request to deny
- Pagination: Navigate through requests with arrow buttons
- Deny All: Deny all requests on the current page at once
- Settings: Quick access to TPA settings from the request GUI
- Auto-Refresh: Updates every second
Settings GUI
Access with /tpasettings to manage your TPA preferences:
- Toggle TPA: Enable or disable receiving TPA requests (green when on, red when off)
- Blocked Players: View and manage your blocked players list
- Status: Your head at the top shows your TPA status, pending requests and blocked count
Blocked Players GUI
Accessed from the Settings GUI to manage blocked players:
- Player Heads: Shows blocked players with their names
- Click to Unblock: Click any head to unblock that player
- Pagination: Navigate through blocked players with arrow buttons
Combat Protection
- Combat Tag: Taking damage tags you in combat
- Duration: Configurable combat tag duration (default 10 seconds)
- TPA Block: Cannot send TPA requests while combat tagged, from a command or the player selector
- On Accept: The player who would teleport must not be combat tagged when the request is accepted; the request stays pending until they can
- Optional: Can be disabled with
allow-in-combat: true
Teleport History
/tphistory lists each request with its type, direction, result, fee (or None) and how long ago it was:
| Result | Meaning |
|---|---|
| ✔ Accepted | Accepted and teleported |
| Accepted, teleport cancelled | Accepted, but the warmup was cancelled (movement, damage, leaving) |
| Accepted, fee not paid | Accepted, but the sender could not pay the fee |
| Accepted, could not move servers | Cross-server only: the move to the other server failed, and the fee was refunded |
| Denied, Expired, Cancelled | The request was denied, timed out or withdrawn |
Economy Fees
With economy.enabled: true and Vault installed, the request sender pays economy.tpa-fee when the
teleport happens, whoever teleports. If they cannot pay, nobody is teleported and both players are
told. Players with nexaltpa.bypass.fee never pay.
Fees are shown the same way everywhere (messages, menus, history) using economy.price-format,
economy.number-format and economy.hide-zero-decimals: by default 100 reads $100, 1000 reads
$1,000 and 1000.5 reads $1,000.50.
Admin Menu
/tpa admin (permission nexaltpa.admin) opens:
- Configuration: toggles (combat, damage cancel, particles, cross-world, clickable buttons, economy), numbers (warmup, cooldown, timeout, combat tag, max requests, fee) and the warmup particle. Changes are saved to
config.ymlstraight away. - Reload, Export Data and Import Data (shift-click): the same as the admin commands.
Cross-Server Support
Off by default; with it off, nothing changes on a single server. With it on, /tpa and
/tpahere reach players on the other servers of a BungeeCord or Velocity network. It uses the
proxy's BungeeCord plugin messaging channel, so no proxy plugin is needed.
Setup
- Install NexalTPA on every backend server (not on the proxy).
- Set up the network as usual: on BungeeCord,
settings.bungeecord: truein each backend'sspigot.yml; on Velocity, keepbungee-plugin-message-channel = trueinvelocity.toml(the default). - Make a long random secret, for example with
openssl rand -hex 32. On every backend server setcross-server.secretto that same value andcross-server.enabled: true. - Restart the servers (or
/tpa admin reload). The console saysCross-server TPA enabled, and once a player is online,this server is "\<name>" on the proxy. - Keep the servers' clocks in sync (NTP). Messages older than
cross-server.max-message-ageseconds (default 30) are refused.
How it works
/tpa \<name>,/tpahere \<name>, their tab completion and the player selector (/tpawith no name, with the player's server in the lore) include players on the other servers (the list is refreshed every 5 seconds). A player on this server with exactly that name comes first.- The sender's server checks the sender (combat, world, pending request, cooldown, their block
list); the target's server checks the target (TPA toggle, block list, request limit, world) and
shows the request with its
[✔ Accept]/[✘ Deny]buttons. Accept, deny, cancel, the requests menu and timeouts work as on one server. - On accept, the player who teleports warms up on their own server (the same countdown, particles and movement or damage cancel), and the request sender pays the fee. Then the proxy moves them to the other server, where a pending-teleport record puts them at the other player's location. For a TPA, right before the move the sender's server asks the proxy whether the target is still on their server; if they left during the warmup, the sender is told and stays, without paying.
- If the move fails, the fee is refunded, once. A move fails when the player never leaves their own server in the 15 seconds after the switch began (the other server is down or refused them; the proxies only switch a player once the other server let them in), when they never arrive within 30 seconds, or when the other player left before they arrived (they are told and stay at that server's spawn). The last two are reported by the destination. A player who left their server is never treated as failed, even if they come straight back; and a fee is never refunded after a successful arrival, or because a report got lost.
- The servers' reports to each other (a player on their way, how a teleport ended, a player's data changed) wait until the other server has someone online, and are sent again every 5 seconds until it confirms them, for up to 10 minutes. Each sending is signed afresh and is acted on only once.
- Both players' teleport history says how it ended: accepted, teleport cancelled (warmup), fee not paid, or could not move servers. The two servers tell each other.
disabled-worldsapplies on each server;allow-cross-worlddoes not apply between servers.
Player data across the network: the TPA toggle, block list, cooldown and history follow a
player from server to server only when every server uses the same SQL database
(storage.type: sql). Each change writes just that field in one short locked transaction, so
servers sharing the database never overwrite each other; the server that made the change then
tells the others to read that player again, and a server also reads a player again when they
join. With JSON storage each server keeps its own data.json, so these settings stay per server.
Security: every message between the servers is signed with HMAC-SHA256 using the shared secret, and carries a timestamp, a random nonce and the name of the server it is for. A forged, changed, replayed, too old or misaddressed message is ignored, with a console warning (at most once a minute). Cross-server mode refuses to start while the secret is empty, shorter than 16 characters or still the default. A message about a request is only believed from the server that request is with, so one server cannot report another's teleport as failed or announce a player.
What the proxy itself answers (this server's name, who is online on which server) is not signed:
it is trusted because only the proxy can send it. Keep the backends reachable through the proxy
only (modern forwarding on Velocity, a firewall or bungeecord: true with closed ports on
BungeeCord), or a player connecting straight to a backend could pose as the proxy.
Limitations
- Plugin messages travel over a player's connection. A server with nobody online can neither send nor receive them. A server also learns its own name from the proxy only once the first player is online, so cross-server requests work a second or two after that. An answer to a server that has emptied in the meantime is lost, and the request simply times out.
- Reports waiting for a server that stays empty for more than 10 minutes are dropped, and so are the waiting ones of a server that restarts. A dropped failure report means the fee stays charged (never a refund that should not happen); a dropped "player changed" notice is covered by the reload when that player joins.
- A target who leaves in the split second between that last check and the move, as the only player on their server, leaves nobody to receive the sender's arrival notice: the sender then arrives at that server's spawn and the fee stays charged.
Data Storage
NexalTPA supports dual storage backends, selectable via storage.type in config.yml.
JSON Storage (Default)
Data is stored in plugins/NexalTPA/data.json:
{
"tpa_users": {
"player-uuid": {
"id": "player-uuid",
"blocked_players": "[\"uuid1\",\"uuid2\"]",
"tpa_enabled": true,
"last_request_time": 1234567890,
"history": "[]"
}
}
}
Changes are saved as they happen, off the main thread; a profile is only stored once something in it changes.
SQL Storage (MySQL)
Set storage.type: sql in config.yml and configure the connection details under storage.sql. If the connection fails at startup, the plugin logs the error and disables itself.
Several servers may share one database (for example the backends of a network): every change writes only the field it changes, under a row lock, so no server overwrites another's changes. See Player data across the network.
Database Schema
Table tpa_users (the same fields are the keys of each JSON entry):
| Column | Type | Description |
|---|---|---|
id | VARCHAR(36) | Player UUID (primary key) |
blocked_players | TEXT | JSON array of blocked player UUIDs |
tpa_enabled | BOOLEAN | Whether the player receives requests |
last_request_time | BIGINT | When the player last sent a request (for the cooldown) |
history | MEDIUMTEXT | JSON array of history entries |
Import/Export
/tpa admin export - Write every player profile to plugins/NexalTPA/export/tpa_users.json
/tpa admin import - Load that file into the current storage (replaces profiles of the same players)
Switching from JSON to SQL: /tpa admin export, set storage.type: sql, restart, then /tpa admin import.
The file can also be copied to another server's export folder to move player data.
World Blacklist
Disable TPA in specific worlds:
disabled-worlds:
- world_nether
- arena_world
- event_world
A request is refused, when it is sent and again when it is accepted, if either player is in one of these worlds: nobody can teleport out of them or into them.
Installation
- Download the latest
NexalTPA.jar - Place in
/pluginsfolder - Start/restart server
- Configure
config.ymlandmessages.yml - Reload with
/tpa admin reload
Dependencies
Required:
- Paper 1.20.1+ (or compatible fork)
Optional:
- PlaceholderAPI (for placeholders)
- Vault (for TPA fees / economy integration)
- BungeeCord or Velocity (for cross-server TPA; no proxy plugin needed)
Building from Source
# From repository root
mvn clean package
# Or using npm scripts
npm run build
# Select "nexal-tpa" from menu
# Output: plugins/nexal-tpa/target/NexalTPA-\<version>.jar
Support
For issues, feature requests, or questions:
- Check the configuration files
- Review messages for customization options
- Ensure all dependencies are met
- Verify permissions are set correctly