GroupSaveState
GroupSaveState is a Paper plugin for accounts that should play as one character. You list a group of accounts in the config. Only one of them can be online at a time. When a member quits, the plugin saves their inventory, ender chest, XP, health, location and a few other things to the group. The next member who joins gets exactly that state.
It targets Paper 26.2 and needs Java 25. It depends on paper-api and nothing else.
Build on a fresh machine
Install mise, then build:
curl https://mise.run | sh
mise install && mise run buildThe jar ends up in build/libs/GroupSaveState-1.0.0.jar. mise pins Java and Gradle in mise.toml. If you already have Java 25, ./gradlew build works too.
Run the end-to-end tests
mise run e2eThis builds the plugin and downloads Paper 26.2 build 129 plus ViaVersion and ViaBackwards 5.12.0 into e2e/server/. It checks each download against a pinned hash. It also accepts the Minecraft EULA for that local test server, so read the EULA before you run it. The script starts an offline-mode server and connects mineflayer bots. The bots speak protocol 26.1, and ViaBackwards bridges the gap because mineflayer doesn't support 26.2 yet. The run covers handover, kicks, deaths, crashes, reconnects, the admin commands, bad config and corrupt data files. The full log goes to e2e/last-run.log.
Bots can't log in to an online-mode server without real Microsoft accounts. So the tests only check that online mode is detected at startup. The login path itself is the same code in both modes, but online-mode logins still need a manual test with two real accounts.
How it works
Members are matched at login. An entry matches if it equals the player's UUID, or the player's username ignoring case. The plugin never resolves names to UUIDs and never calls Mojang.
While a member is online the plugin autosaves their state on a timer. It saves again when they quit or get kicked, and when the server stops. Another member who tries to join while the first one is online gets turned away with a message. If the quit save is still being written to disk, they get a "still saving" message and can retry a second later.
If a group has no saved data yet, the first member to join seeds it with their current state. Nobody's inventory gets wiped.
A member who quits on the death screen is saved as they'd be after respawning, with full health at their respawn point and without the items they dropped. A member who joins while dead gets the group state once they respawn.
Configuration
The config lives in plugins/GroupSaveState/config.yml. Every key in the shipped file has a comment with its type, default and range. The plugin checks every value on load. A bad value gets a console warning naming the key and the expected type, and the plugin uses the default instead. If the YAML itself is broken, /gss reload keeps the settings that were already loaded.
groups:
# matched by username
family:
- Notch
- Steve
# one member by UUID, one by name
alts:
- 069a79f4-44e9-4726-a5be-fca90e38aaf5
- Alex
autosave-interval-seconds: 300
# MiniMessage strings. The prefix only goes in front of command replies.
messages:
prefix: "<dark_gray>[<gold>GroupSaveState</gold>]</dark_gray> "
# placeholders <group> and <member>
member-online: "<red>Another member of group <yellow><group></yellow> (<yellow><member></yellow>) is already online.</red>"
# placeholder <group>
still-saving: "<red>Data for group <yellow><group></yellow> is still saving. Try again in a few seconds.</red>"
# placeholder <group>
data-error: "<red>Your group's saved data could not be loaded. Please contact an administrator.</red>"
shared:
inventory: true
armor: true
offhand: true
enderchest: true
location: true
xp: true
health: true
hunger: true
effects: true
gamemode: true
respawn-point: trueGroup names can use letters, digits, _ and -, up to 32 characters. A player who shows up in two groups is a config error. The plugin logs it and uses the first group.
If you set a shared type to false, the plugin stops saving and applying it, and each player keeps their own. The group's last saved copy stays on disk, so turning it back on later picks up where it left off.
Storage
Each group gets one file, plugins/GroupSaveState/groups/<group>.yml. Items are stored with Paper's ItemStack.serializeItemsAsBytes, so enchantments, names, lore, container contents and other components come back intact. The plugin writes each save to a temp file and then moves it over the old one. It copies the old file to <group>.yml.bak first.
Files are named after the group, not a player UUID, so a member's UUID changing between online and offline mode doesn't lose anything.
If a group file can't be read, members of that group can't log in and the console says why. The plugin won't write to that file until it reads cleanly again, so a broken file never gets overwritten. It doesn't restore the .bak for you. Copy it back yourself, then restart the server.
Commands
/groupsavestate, or /gss for short. Every subcommand needs groupsavestate.admin, which ops have by default.
| Command | What it does |
|---|---|
/gss reload | Re-reads config.yml. Players already online stay in their current group until they quit. |
/gss info <player> | Shows the player's group, its members, who is online and when it was last saved. |
/gss save <group> | Saves the online member's state right now. |
/gss reset <group> | Deletes the group's saved data. Run it again with confirm within 30 seconds. It refuses while a member is online. |
Online and offline mode
The plugin reads online-mode at startup and logs what it found.
In online mode, UUIDs come from Mojang and stay the same when a player renames. UUID entries are the safest choice there.
In offline mode, the server derives each UUID from the username. A renamed account is a new player to the server. It drops out of any group that listed the old name or its UUID, and it matches a group that lists the new name. Name entries work fine in offline mode.
Offline mode has a real security hole. Anyone who can connect with a member's username gets that group's inventory and everything else, unless you run an authentication plugin. The plugin prints a warning about this at startup.
Behind a BungeeCord or Velocity proxy with forwarding turned on, the backend runs with online-mode=false but receives real UUIDs from the proxy. The plugin detects this from spigot.yml and config/paper-global.yml and says so in the log. UUID and name matching both work. That setup is only safe if the proxy runs in online mode and players can't reach the backend directly.
Known limits
- Each account still has its own vanilla playerdata file. The plugin overwrites the shared parts on join.
- It doesn't share statistics, advancements, recipes, attribute modifiers, vehicles, passengers, fire or air ticks, or the persistent data container.
- Items dropped on death stay in the world, just like vanilla.
- If a player gets through the login check but never finishes joining, their group is blocked for up to 60 seconds, unless the server reports the disconnect sooner.
- Folia isn't supported.