Ghost
Overview
On a network of several backend servers, two players standing in the same map on different servers cannot see each other. Ghost fixes that visually: each server publishes the position, rotation and skin of its players through Redis, and the other servers draw them as player-shaped ghosts using packets. Nothing is added to the world, and a ghost has no server-side entity, so it cannot be hit, pushed or targeted.
Every player decides whether they see ghosts. The choice is stored in a TypeWriter fact, so it survives restarts and follows the player from server to server. Ghosts only appear between servers that share the same redis.group-id in the MySQL configuration, and a server never draws its own players as ghosts.
Features
- Cross-server ghosts — players of other backend servers appear with their skin, position and rotation.
- Packet-only rendering — built on PacketEvents, no server-side entities are created.
- Folia-safe — each capture and each render runs on the scheduler of the player concerned.
- Visibility switch —
/ghost toggle, a TypeWriter action or a menu button turns ghosts on and off per player. - Persistent choice — the preference is a persistable fact shared across servers.
- Server tag — a configurable suffix such as
[survival-2]shows where a ghost comes from. - Limits — view distance, maximum ghosts per viewer, update rate and world filtering keep the cost bounded.
- Nameplates — if UiCore is installed, ghosts get a nameplate above their head.
- Placeholders — visibility and ghost count for MiniMessage text.
Installation
- Install PacketEvents on every server.
- Install the MySQL extension and enable Redis (or Valkey) on every server. Give each server a unique
redis.server-nameand the sameredis.group-id. Ghost does not have its own Redis settings. - Put the Ghost JAR in
plugins/Typewriter/extensions/and restart. The fileplugins/Typewriter/ghost_player/config.ymlis created on first start. - Grant the permission
typewriter.ghost.useto the players who may use/ghost. Without a permissions plugin, only operators can. - Optional: install UiCore to draw nameplates above ghosts.
Getting started
Let players switch ghosts from a menu or a quest NPC:
- In the TypeWriter web panel, open a page and add a Ghost Visibility Fact fact entry. Keep its default
id,ghost_visibility_fact: the extension reads and writes the fact by this id. - Add a Ghost Toggle Visibility action on a sequence or a menu item. Fill
showMessageandhideMessagewith MiniMessage text, or leave them empty for the default messages. - Optionally add a criteria on the Ghost Visibility fact (value
1means visible,0hidden) to show a different icon in a menu. - Publish and join two servers of the same group. Each player sees the other as a ghost with the server suffix.
A server ignores its own broadcasts. To test, start two servers with the same redis.group-id and different redis.server-name, and stand in worlds that are allowed to match (see below).
How it works
Each server publishes a snapshot of every local player at the configured rate, plus a heartbeat so idle players stay alive. A ghost disappears when its owner leaves, or when no update arrives within timeout-millis. Each message carries a session id and a sequence number, so late or duplicated messages are ignored.
A ghost is drawn for a viewer when the viewer has ghosts enabled, the worlds are compatible, the ghost is within view-distance and the viewer has fewer than max-ghosts-per-viewer ghosts, the nearest first.
Configuration
The file is plugins/Typewriter/ghost_player/config.yml. Its schema-version must be 1 or 2.
schema-version: 2
settings:
update-rate: 4
server-suffix: " <gray>[%server%]</gray>"
team-prefix: "Ghosts"
view-distance: 64.0
max-ghosts-per-viewer: 128
timeout-millis: 10000
allowed-worlds:
- "world"
- "dungeon_"
- "hub"
messages:
no-permission: "<red>You do not have permission to use ghost visibility."
players-only: "<red>This command can only be used by a player."
enabled: "<green>Ghost visibility enabled."
disabled: "<yellow>Ghost visibility disabled."
usage: "<gray>Usage: <white>/ghost toggle"
command:
permission: "typewriter.ghost.use"
| Key | Meaning |
|---|---|
settings.update-rate | Ticks between position captures. Clamped to 1 to 20. |
settings.server-suffix | MiniMessage text added after the ghost's name. %server% becomes the origin server name. |
settings.team-prefix | Prefix of the scoreboard team shown for each origin server. |
settings.view-distance | Maximum distance in blocks, from 0 to 256. 0 disables the distance limit. |
settings.max-ghosts-per-viewer | Ghosts drawn per viewer, from 0 to 2000. 0 shows none. |
settings.timeout-millis | Delay without update before a ghost is removed, from 2000 to 120000. |
settings.allowed-worlds | World name patterns that count as the same map across servers. Up to 64 entries. |
messages.* | MiniMessage texts used by /ghost. |
command.permission | Permission required by /ghost. |
Worlds. A ghost is visible when the viewer and the ghost are in worlds with the same name, or when both world names contain the same pattern from allowed-worlds (for example dungeon_ matches dungeon_1 and dungeon_2). Distance can only be measured inside one world, so with a view-distance above 0, ghosts from a differently named world are hidden. Set view-distance to 0 if you rely on pattern matching.
Redis connection, server name and group id are read from the MySQL configuration. The file is read once at startup: restart the server after editing it.
Commands
| Command | Description |
|---|---|
/ghost toggle | Turn ghost visibility on or off for yourself. |
Aliases: /ghosts, /ghostplayer. Players only. The permission is command.permission, typewriter.ghost.use by default.
Permissions
| Permission | Grants |
|---|---|
typewriter.ghost.use | /ghost toggle. The name can be changed with command.permission. The permission is not registered with a default value, so it needs to be granted. |
The Ghost Toggle Visibility action does not check this permission.
Placeholders
MiniPlaceholders tags for MiniMessage text. PlaceholderAPI is not supported.
| Placeholder | Returns |
|---|---|
<ghost_visibility> | true when the player sees ghosts, false otherwise. |
<ghost_visible> | 1 when the player sees ghosts, 0 otherwise. |
<ghost_count> | Number of remote ghosts currently known to this server. |
Entries
2 entries, generated from the extension source.
Actions1
- Ghost Toggle VisibilityToggle Ghost Visibility
Facts1
- Ghost Visibility FactGhost Visibility
Troubleshooting
No ghost appears. Check, in this order: both servers use the same non-empty redis.group-id and different redis.server-name values, Redis is connected (the log says "BTC RedisService is not connected; Ghost updates will remain inactive" otherwise), the viewer has ghosts enabled, and the two players are in compatible worlds (same world name, or a shared allowed-worlds pattern with view-distance: 0).
Players of the same server never show as ghosts. This is expected: a server ignores messages it published itself.
The extension fails to start with "Unsupported Ghost custom config schema". schema-version in ghost_player/config.yml is outside 1..2. Set it to 2.
The extension fails with a server name error. redis.server-name in the MySQL configuration must contain only letters, digits, ., _ or -, up to 64 characters.
/ghost says you do not have permission. The permission has no default. Grant typewriter.ghost.use (or the permission you set in command.permission) with your permissions plugin.
Ghosts vanish after a few seconds. No update arrived within timeout-millis. Check Redis latency, and that the origin server is running Ghost with the same redis.group-id.
Ghosts have no nameplate. Nameplates come from UiCore. Install it on the viewing server. Without it only the server suffix in the name is shown.