Network Sync & Storage
EdenEffects keeps each player's look the same on every backend. This page explains how, what is stored, and what happens when parts of the network are unavailable.
MySQL is the source of truth
Every backend reads and writes the same MySQL database. Plugin messages through the proxy never carry cosmetic data: they only tell other backends to re-read a player from MySQL.
player clicks a cosmetic on the hub
→ hub updates its cache, so the player sees it straight away
→ hub writes the change to MySQL (off the main thread)
→ hub sends "cosmetic-changed <uuid>" to the proxy on channel edeneffects:sync
→ proxy forwards it to every other backend
→ each backend re-reads that player from MySQL and re-applies their cosmetics if they are online
The Velocity plugin is only a relay built on StoneLib's ProxyMessageRelay. It never reads the message body, so it needs no configuration and no updates when the backend plugin changes.
Dropped messages are harmless
Plugin messages travel over a player's connection, so a backend with nobody online cannot receive one. That is safe: the change is already in MySQL, and a player joining that server loads their cosmetics fresh from the database. A missed message can only mean a stale cache until the next read, never lost data.
What triggers a sync
| Action | Synced |
|---|---|
| Clicking a cosmetic, or removing one | Yes |
| Clear everything | Yes |
| Loading a preset | Yes |
| Saving pet settings | Yes |
| Saving or deleting a preset | No message needed. Presets are read from MySQL whenever the presets menu opens. |
The player cache
Each backend keeps an in-memory copy of its online players' cosmetics so the render loop and menus never wait on a query.
- On join, the player is loaded from MySQL off the main thread, then their cosmetics are applied.
- On quit, their leave effect is played, then their entry is dropped, their pet is removed, and their cosmetics stop drawing.
- On a change message, the player is re-read from MySQL.
If loading a player fails, EdenEffects logs Failed to load cosmetics for <uuid> and gives them an empty look for that session instead of failing the join. Their saved data is untouched.
When cosmetics are re-applied
A player's worn cosmetics are taken off and put back on, using the cached selections and their current permissions, when they:
- join the server,
- change world,
- load a preset,
- have a change arrive from another server,
- come out of vanish or spectator mode.
Each time, a cosmetic is only shown if the player still holds its node and its type is allowed in their current world. This is also when an expired temporary unlock stops showing.
Join, leave, and death effects are not applied this way. They are read at the moment they fire, and checked against the same rules then.
Database schema
The schema is created and migrated automatically on first start by StoneLib's SchemaMigrator.
cosmetic_selections
One row per player per cosmetic type, so adding a new type never needs a schema change.
| Column | Type | Notes |
|---|---|---|
uuid | CHAR(36) | Player UUID. Primary key with type. |
type | VARCHAR(32) | TRAIL, HAT, WINGS, PET, GADGET, JOIN, LEAVE, or DEATH. |
cosmetic_id | VARCHAR(64) | The cosmetic's ID, e.g. flame. |
updated_at | TIMESTAMP | Set automatically on insert and update. |
pet_settings
One row per player. Settings apply to whichever pet they have selected.
| Column | Type | Default |
|---|---|---|
uuid | CHAR(36) | Primary key. |
custom_name | VARCHAR(48) | '' (use the mob's own name) |
scale | DOUBLE | 1.0 |
visible_to_others | BOOLEAN | TRUE |
glowing | BOOLEAN | FALSE |
presets
One row per cosmetic in each saved look.
| Column | Type | Notes |
|---|---|---|
uuid | CHAR(36) | Player UUID. |
name | VARCHAR(32) | Preset name. |
type | VARCHAR(32) | Cosmetic type. |
cosmetic_id | VARCHAR(64) | Cosmetic ID. |
The primary key is (uuid, name, type). Saving a preset replaces every row for that name in one transaction, and loading one replaces all of the player's selections in one transaction.
Useful queries
Who is wearing a particular cosmetic:
SELECT uuid FROM cosmetic_selections WHERE type = 'PET' AND cosmetic_id = 'wither';
Remove a cosmetic from everyone, for example after retiring it:
DELETE FROM cosmetic_selections WHERE type = 'HAT' AND cosmetic_id = 'storm';
DELETE FROM presets WHERE type = 'HAT' AND cosmetic_id = 'storm';
Changes made directly in the database are picked up the next time each player joins or switches server. Changing world re-applies from the cache, so it does not re-read the database.
All player data lives in these three tables, so a normal MySQL backup of the database covers EdenEffects completely. The plugin folder only holds config.yml.
Vanish and spectator mode
A vanished or spectating player shows nothing: no trail, hat, wings, or pet, no join, leave, or death effect, and gadgets are refused. A cosmetic following a vanished moderator would give away where they are. The selection is untouched, and everything comes back when they reappear or leave spectator mode.
Vanish is read from the vanished metadata key, which SuperVanish, PremiumVanish, EssentialsX, CMI, and VanishNoPacket all set, so no vanish plugin is a dependency. Those plugins share the key but not an event, so EdenEffects checks it every other tick. Game mode changes are picked up on the next tick.
The join effect waits a second before playing, so a staff member who joins vanished has already been hidden by their vanish plugin and is not announced. That delay also gives their cosmetics time to load from MySQL.