Real-time survival-to-creative phantom mirror plugin
- Java 100%
v1.4.0 posted a webhook embed for EVERY connection, drowning the audit
channel in regular-player noise. The point of the audit is hostile
attempts (banned UUIDs, banned IPs, non-whitelisted UUIDs) — the
PresenceManager already broadcasts "X joined the game" for legitimate
joins via a different webhook.
New AccessGate reads the ekaii backend's banned-players.json,
banned-ips.json, and whitelist.json (mtime-poll for hot reload) and
returns a Decision { OK | BANNED_UUID | BANNED_IP | NOT_WHITELISTED }
on every PostLogin. SecurityAuditor only emits a webhook when the
decision is not OK; the local audit-history.json still records the
attempt either way so history context stays correct.
Default gate source-dir = /ekaii-data (read-only mount of
/opt/mc-stack/ekaii on the host). The mc-stack compose change goes
in a separate commit on the mc-stack repo.
Reason: noisy webhook channel made hostile attempts invisible — every
player was generating an entry, indistinguishable at a glance from a
banned UUID retry. Restrict reporting to actionable attempts.
|
||
|---|---|---|
| .forgejo/workflows | ||
| backend-presence | ||
| gradle/wrapper | ||
| src | ||
| velocity-plugin | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
EkaiiMirror
Real-time survival → creative server mirroring plugin for Minecraft 26.1.2 (Luminol/Folia).
Players on the creative server see survival players as phantom entities with full skin, armor, items, sneaking, elytra flying, and vehicle riding — all in real time.
Features
Phantom Players (NMS)
- Full player model with skin (via NMS ServerPlayer + reflection)
- Real-time position + head rotation sync (100ms updates)
- Sneaking (Pose.CROUCHING + shared flags)
- Arm swing animation
- Full equipment sync: main hand, off hand, helmet, chestplate, leggings, boots
- Elytra flying (Pose.FALL_FLYING + flag 0x80)
- Vehicle riding: boats, horses, minecarts, camels, etc.
- Glow outline (scoreboard team color, default AQUA)
- Soul flame particles
- Periodic respawn (5s) handles: late joins, dimension changes, reconnections
Ghost Blocks
- Block place/break events mirrored as client-side fake blocks
- Uses
player.sendBlockChange()— no server-side modification - Batched for performance (configurable flush interval)
Cross-Server (Velocity Plugin)
- Chat bridge: messages forwarded between all servers
/world survie|crea|plotcommand to switch servers- Tab list: cross-server player entries via Velocity
Architecture
Sender (Survie) → Redis pub/sub → Receiver (CreaClone)
channel: ekaii:mirror
Velocity Plugin: chat bridge + /world command + tab sync
Message Protocol
Compact JSON over Redis pub/sub. Message types:
bpBlock Place,bbBlock BreakpjPlayer Join,pqPlayer QuitpmPlayer Move,psPlayer Sneak,paPlayer SwingpePlayer Equipment (6 slots)elPlayer Elytra,vmVehicle Mount,vdVehicle DismounthbHeartbeat,baBatch
NMS Approach
PacketEvents was found to silently drop all entity spawn packets on Luminol/Folia. The plugin uses pure Java reflection to access NMS classes at runtime — zero compile-time NMS dependencies.
Key NMS packets used:
ClientboundPlayerInfoUpdatePacket(player profile + skin)ClientboundAddEntityPacket(spawn entity, raw values constructor)ClientboundSetEntityDataPacket(metadata: glow, sneak, elytra flags)ClientboundTeleportEntityPacket(position sync via PositionMoveRotation)ClientboundRotateHeadPacket(head rotation)ClientboundSetEquipmentPacket(all 6 equipment slots)ClientboundSetPassengersPacket(vehicle riding, via Unsafe field injection)ClientboundRemoveEntitiesPacket(despawn)ClientboundAnimatePacket(arm swing)
Installation
Requirements
- Minecraft 26.1.2 server (Luminol, Folia, or Paper)
- Redis server
- Velocity proxy (for cross-server features)
Backend Plugin
- Drop
EkaiiMirror-1.0.0.jarinplugins/on both servers - Configure
plugins/EkaiiMirror/config.yml:
Sender (survival server):
mode: SENDER
redis:
host: "<redis-host>"
port: 6379
password: ""
channel: "ekaii:mirror"
sender:
worlds: [] # empty = all worlds (overworld, nether, end)
move-interval-ms: 100
batch-interval-ms: 50
receiver:
glow-color: AQUA
block-display: true
particles: true
Receiver (creative server):
mode: RECEIVER
redis:
host: "<redis-host>"
port: 6379
password: ""
channel: "ekaii:mirror"
sender:
worlds: []
move-interval-ms: 100
batch-interval-ms: 50
receiver:
glow-color: AQUA
block-display: true
particles: true
Velocity Plugin
- Drop
EkaiiMirrorVelocity-1.0.0.jarin Velocity'splugins/ - Configure
plugins/ekaii-mirror-velocity/config.yml:
servers:
ekaii:
display: "Survie"
color: "gray"
creaclone:
display: "Créa"
color: "green"
plot:
display: "Plot"
color: "gold"
shared-chat: true
shared-tab: true
Redis
docker run -d --name ekaii-redis --restart unless-stopped --network host redis:7-alpine
Commands
| Command | Description |
|---|---|
/mirror toggle |
Toggle phantom visibility (receiver only) |
/mirror status |
Show plugin status |
/world survie |
Switch to survival server |
/world crea |
Switch to creative server |
/world plot |
Switch to plot server |
Building
./gradlew :shadowJar # Backend plugin
./gradlew :velocity-plugin:shadowJar # Velocity plugin
Output:
build/libs/EkaiiMirror-1.0.0.jarvelocity-plugin/build/libs/EkaiiMirrorVelocity-1.0.0.jar
Known Limitations
- PacketEvents
sendPacketdoes not work on Luminol/Folia for entity spawning — NMS reflection is used instead - Vehicle position syncs via teleport (may appear slightly jerky at high latency)
- Equipment updates every ~2 seconds (not instant on item pickup)
- Phantom players are purely visual — no collision, no interaction