MCME-Warps is the network's warp system. Players teleport to named places with /warp <name>, staff keep a set of public landmarks, and players keep their own private warps. A warp records the server and world it points at, so /warp can move a player to another backend server first. The plugin replaced the third-party MyWarp plugin and stores warps as YAML files, not in a database. This page describes 1.0.7, the latest release, from the MCME/MCME-Warps repository.
At a glance
| Repository | MCME/MCME-Warps (public, GPL-3.0) |
| Release branch | master (development holds work in progress) |
| Version described | 1.0.7 (tag v1.0.7; master adds only CI changes on top) |
| Platform | One jar, two plugins: MCME-Warps-Velocity on the proxy (velocity-api 3.4.0) and MCME-Warps-Paper on each backend (api-version: 26.1.2, compiled against paper-api 26.2) |
| Build | Gradle (wrapper included) with the Shadow plugin, JDK 25 |
| Depends on | Paper half: soft-depends on Dynmap; gets Guava and configurate-yaml through plugin.yml libraries. Velocity half: no plugin dependencies |
How the code is organised
The guiding rule is the proxy decides, the backend acts. The Velocity half owns every warp, the config, all commands and permissions, and teleport routing. The Paper half executes teleports and draws Dynmap markers; it holds no state of its own.
src/main/java/com/mcmiddleearth/warps/
├── core/ shared by both halves (must not import Velocity or Bukkit classes)
│ ├── BaseWarp, SimpleLocation, WarpIcon, Utils, WarpLoader, SafeEnumSerializer
│ ├── Channels the three plugin-message channel names
│ └── messageprotocols/ the wire protocol (MessageFrame and the message classes)
├── velocity/ the proxy plugin
│ ├── WarpVelocity @Plugin entry point: registers channels, loads config and warps, builds the commands
│ ├── Permission every permission node, as an enum
│ ├── commands/ one class per Brigadier command, plus helpers/
│ ├── config/ Config record + ConfigManager
│ ├── listener/MessageListener backend -> proxy messages
│ └── warps/ the store: Warp, WarpManager, WarpStore, WarpFileIo, ConfigurateWarpIo, WarpPaths, ...
└── paper/ the backend plugin
├── WarpPaper JavaPlugin entry point
├── WarpWatcher watches the warps folder and updates map markers
├── DynmapAPI, MapAPI, Layer, LayerParser Dynmap markers and layer config
└── listener/MessageListener proxy -> backend messages
Key ideas:
- Storage. The proxy keeps one YAML file per warp:
warps/<server>/<world>/<name>.ymlfor a public warp andwarps/private-warps/<creatorId>/<name>.ymlfor a private one. Public warp names are unique across the network; private warps are keyed by creator and name, so two players can each own ahome. A player's own private warp wins over a public warp of the same name. - The store seam. All state and file writes go through
WarpStore.WarpManageris a thin static facade over it, and disk access sits behind theWarpFileIointerface (ConfigurateWarpIoin production, a fake in tests). A change is written to disk before it shows in memory, a delete removes the file before the warp is forgotten, and a reload swaps in a complete new set in one step. Never load-modify-save a warp file directly. - Names.
Utils.normaliseStringdefines the lookup key (accents stripped, apostrophes dropped, spaces collapsed, lower case).WarpPathsturns a warp into its file path and refuses any path outside the warps folder. - Visit counts are kept in memory and written on proxy shutdown, proxy reload and
/wmanage reload. - The backend's view of the data. The Paper half reads warp files from its own
warps/folder, which the README says to link to the proxy's per-server folder. Private warps live outside the per-server folders, so they never reach a backend or the map. - Version string. Set it once, in
build.gradle. It is filled intoplugin.ymland into a generatedBuildConstantsclass for the Velocity@Pluginannotation.
Building and testing
./gradlew build # tests + the fat jar
./gradlew shadowJar # jar only -> build/libs/MCME-Warps-<version>.jar
./gradlew test # tests only
- JDK 25 is required. paper-api 26.x declares a Java 25 minimum in its Gradle metadata, and the jar is Java 25 bytecode.
- The Shadow plugin bundles the runtime dependencies (commons-text, relocated; mysql-connector-j, SquirrelID and fzf4j).
paper-api,velocity-api, DynmapCoreAPI and configurate-yaml arecompileOnly. - Tests use JUnit Jupiter (
junit-bom6.1.3) and need no running server:WarpStoreTest(the store, including failed writes and deletes),MessageProtocolTest(round trips, stable opcodes, rejecting bad messages),WarpManagerPathTest(path containment),LayerParserTest(backend layer config),WarpTestandConfigTest. Command classes and the two message listeners are not unit-tested; put logic worth testing in a pure helper. ./gradlew runVelocityand./gradlew runServer(the run-paper / run-velocity plugins) start a local Velocity 3.4.0 proxy and a Paper 26.2 server with the jar installed. The DEV_MANUAL lists the forwarding settings that make them trust each other.
CI (.github/workflows/build.yml) builds and tests every push and pull request with JDK 25 and keeps the jar as a workflow artifact named MCME-Warps-<version>-<DEV|SNAPSHOT|RELEASE>-<commit>. A v* tag must match the version in build.gradle and sit on the release branch (a pre-release tag such as v1.1.0-rc1 may also come from development); it then creates a GitHub release with the jar and its SHA-256. MCME-Warps does not publish to the Maven repository.
Configuration
Proxy: plugins/mcme-warps-velocity/config.yml. Copied from default-velocity-config.yml on first start and read through Configurate into the Config record.
| Key | Default | Meaning |
|---|---|---|
warp-name-max-length |
64 |
Longest allowed warp name (required) |
private-warp-limits.default-limit |
2 |
Private warps a player may keep (required) |
private-warp-limits.configured |
empty (examples commented out) | Map of <key>: <limit>. A player with mcmewarps.limits.<key> gets that limit; the highest matching one wins (required, may be empty) |
layer-keys |
[major] |
Layer names /wmanage setLayer accepts. Must match the custom layers in the backend config |
sql.user, sql.password, sql.db-name, sql.ip, sql.port |
commented out | Legacy one-time importer from an old MyWarp database. Used only when the warps folder does not exist yet and every value is set; otherwise ignored |
Backend: plugins/MCME-Warps-Paper/config.yml.
| Key | Default | Meaning |
|---|---|---|
debug |
false |
Extra log lines |
layers.default.label / .min-zoom / .priority |
"Other warps", 2, 9 |
The Dynmap marker set for warps without a custom layer |
layers.custom.<name>.label / .min-zoom / .priority |
major: "Major warps", -1, 8 |
Extra marker sets. min-zoom hides the layer below that zoom (-1: always shown); a higher priority sorts lower in the layer list |
Run /wmanage reload (or reload the proxy) after changing the proxy config.
Commands and permissions
All commands are registered by the Velocity half with Brigadier. Subcommand names are case-sensitive (setPublic, not setpublic). The Paper half registers no commands.
| Command | Aliases | What it does | Permission |
|---|---|---|---|
/warp <name> |
/to |
Teleport to a warp, switching server if needed | mcmewarps.cmd.warp |
/warp random |
/to random |
Teleport to a random public warp on a fixed set of servers named in RandomCommand |
mcmewarps.cmd.random |
/localwarp <name> |
/lwarp |
Go to a warp's coordinates in your current world, without changing server or world | mcmewarps.cmd.local-warp |
/warpmanager create <name> |
/wmanage |
Create a public warp where you stand | mcmewarps.cmd.create-public |
/wmanage pcreate <name> |
Create a private warp where you stand (checks the private-warp limit) | mcmewarps.cmd.create-private |
|
/wmanage move <warp> |
Move a warp to where you stand | mcmewarps.cmd.move |
|
/wmanage rename <warp> <new> |
Rename a warp | mcmewarps.cmd.rename |
|
/wmanage delete <warp> |
Delete a warp | mcmewarps.cmd.delete |
|
/wmanage setPublic <warp> |
Make a private warp public | mcmewarps.cmd.set-public |
|
/wmanage setPrivate <warp> |
Make a public warp private | mcmewarps.cmd.set-private |
|
/wmanage setWelcome <warp> <message> |
Arrival message in MiniMessage; default clears it |
mcmewarps.cmd.welcome-message |
|
/wmanage setIcon <warp> <icon> |
Map icon: DEFAULT, WIP, ON_HOLD, NOT_STARTED, PIRATE |
mcmewarps.cmd.set-icon |
|
/wmanage setLayer <warp> <layer> |
Map layer, one of layer-keys |
mcmewarps.cmd.set-layer |
|
/wmanage members add\|remove <warp> <player> |
Manage who may use a private warp (player must be online) | mcmewarps.cmd.manage-members |
|
/wmanage list [filters] [page] |
List warps; filters -v visibility, -c creator, -n name, -s server, -w world, -o order |
mcmewarps.cmd.list |
|
/wmanage plist [filters] [page] |
Your private warps and the ones you are a member of | mcmewarps.cmd.list |
|
/wmanage assets [filters] [page] |
Your private warps on your current server | mcmewarps.cmd.list |
|
/wmanage reload |
Write visit counts, reload the config and re-read all warps | mcmewarps.cmd.reload |
The edit subcommands (delete, rename, move, members, setWelcome) only show for a player who can modify at least one warp.
These permissions are checked on the proxy, so they come from the proxy's permission plugin; there are no built-in defaults.
| Permission | Grants |
|---|---|
mcmewarps.cmd.* nodes above |
The matching command |
mcmewarps.world-access.<world> |
Teleporting to warps in that world. Needed for every world a player may warp into |
mcmewarps.override.use |
Using any warp, ignoring world access, ownership and membership |
mcmewarps.override.modify |
Editing, moving and deleting any warp, public ones included. Without it nobody can edit an existing public warp |
mcmewarps.limits.ignore.private |
No private-warp limit |
mcmewarps.limits.<key> |
The limit configured under private-warp-limits.configured.<key> |
The warps manual explains the commands for players and staff.
APIs and extension points
MCME-Warps has no Java API for other plugins. Its extension points are inside the repository:
- Adding a command: write a class in
velocity/commands/with aregister(Predicate<CommandSource>)method returning a Brigadier builder, add a node to thePermissionenum, and wire it into the command tree inWarpVelocity.onProxyInitialization. Route every warp change throughWarpManager. - Adding a store operation: add it to
WarpStore(write or delete on disk before changing memory), expose it onWarpManager, and add aWarpStoreTestcase that injects a failed write. - Adding a message or field: see the protocol below. Any layout change means bumping
MessageFrame.PROTOCOL_VERSION. - Re-check permissions inside message handlers. A handler reached through a plugin message must not assume the command that started it was allowed; the create and move handlers check permission and ownership again when the backend's reply arrives.
How it talks to other plugins and systems
The two halves talk over three plugin-message channels (core/Channels.java). Every message is a MessageFrame: byte 0 is the protocol version, byte 1 a stable opcode, then the body. A receiver on another protocol version rejects the whole message and logs that both jars need the same version, so the proxy and the backends must always run the same MCME-Warps version.
| Channel | Proxy to backend | Backend to proxy |
|---|---|---|
mcme:warp |
TeleportMessage (SAME_SERVER, DIFF_SERVER, LOCAL_WARP) |
TeleportResult (SUCCESS) |
mcme:player-location |
RequestLocationMessage (CREATE_PUBLIC, CREATE_PRIVATE, MOVE) |
PlayerLocationMessage (the player's location) |
mcme:misc |
MiscMessage (UPDATE_COMMANDS) |
none |
The proxy registers all three channels and consumes every message on them, so a message sent by a player's client is never passed on to a backend.
sequenceDiagram
participant P as Player
participant V as MCME-Warps-Velocity (proxy)
participant B as MCME-Warps-Paper (target backend)
P->>V: /warp Rivendell
V->>V: look up warp, check access
alt warp is on another server
V->>P: connect player to the warp's server
end
V->>B: mcme:warp TeleportMessage
B->>P: teleport
B->>V: TeleportResult
V->>V: count the visit
Creating or moving a warp is the reverse round trip: the proxy sends RequestLocationMessage, the backend answers with PlayerLocationMessage, and the proxy writes the warp.
| Other system | How |
|---|---|
| Dynmap | Paper half only, soft dependency. Public warps for that server become markers in marker sets named mcmewarps.markerset.<layer>. Without an enabled Dynmap the plugin logs a warning and runs without markers |
| MCME-Connect | No integration. Older Connect builds registered /warp and /to themselves; Connect 3.0.1 stopped that and 3.1.0 removed its MyWarp bridge, so MCME-Warps owns both commands. See MCME-Connect |
| Legacy MyWarp database | Read once by MyWarpDBConnector when the sql block is set and no warps folder exists. Dormant otherwise |
tools/convert_warps.py (Python 3, standard library) is the one-time migration that removed the old zzz-<player>- prefix from private warp names; its README describes it.
Further reading
- README, Developer manual and User manual on GitHub
- Setting up for the JDK and Gradle
- Warps manual