The definitive fix. Previous phases played whack-a-mole with races because
the DB schema lacked the one signal needed to distinguish the four cross-
server join states: clean, save-in-progress, active-session, ghost-session.
New column: player_data.logout_started_at BIGINT NULL
- Set to System.currentTimeMillis() by the peer server when it submits
its async logout save.
- Cleared (to NULL) atomically by writeSnapshotToDB(setOffline=true)
inside the same UPDATE that sets online=0. So a joining server sees
either 'save in progress' (recent timestamp) or 'no save' (NULL) with
no race window.
Auto-migration: PlayerSync.onServerStarting ADD COLUMN IF NOT EXISTS at
boot. Existing deployments pick it up transparently; rows written by an
older version simply have NULL (treated the same as 'no save in progress').
doPlayerJoin poll rewritten as a decision matrix:
online=0 -> CLEAN. Claim instantly.
online=1 AND last_server=self -> already ours. Proceed.
peer heartbeat stale -> peer process dead. Force-claim.
logout_started_at recent (< 10s) -> peer saving, wait briefly.
logout_started_at stale (> 10s) -> save thread died. Force-claim.
online=1 AND logout_started_at NULL -> active session OR rare ghost.
Brief 2s grace, then force-claim.
Claim is an atomic CAS:
UPDATE SET last_server=?, online=1, logout_started_at=NULL
WHERE uuid=? AND (online=0 OR last_server=?)
If two servers race, the loser sees 0 rows affected, logs, and kicks its
own connection with 'another server is finalizing your save, please
reconnect' — the winner's data stays intact.
Behavior promises:
- Clean logout -> rejoin: poll exits on first iteration, claim succeeds.
Restore starts immediately. Typical latency < 200ms end-to-end.
- Logout mid-save: peer commits within ~1s. Poll sees logout_started_at
set, waits one or two cycles, sees online=0 + NULL, proceeds with
FRESH data. Zero duplication.
- Ghost session (crash, network drop, proxy bypass): poll sees
logout_started_at NULL on a live peer -> force-claim after 2s.
Peer can never overwrite us later thanks to the last_server guard.
- Truly dead peer: stale heartbeat short-circuit, instant force-claim.
- Two servers joining the same UUID: CAS ensures only one claim sticks.
Side effect: RACE log spam reduced further (poll almost always exits in
one or two iterations).
Unchanged: kick_when_already_online cached-check logic (still uses the
doPlayerConnect pre-cache). RS2 batching (Phase 13). Heartbeat, pool
stats, admin commands, inventory viewer.
|
||
|---|---|---|
| .github | ||
| compat-mods | ||
| gradle/wrapper | ||
| src/main | ||
| .gitattributes | ||
| .gitignore | ||
| build.gradle | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| ERROR_LOG.md | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle | ||
| TEST_PROCEDURE_v2.1.5.html | ||
PlayerSync
PlayerSync is a Minecraft Forge mod that synchronizes player data across multiple servers using a MySQL backend. It allows players to maintain their inventory, equipment, experience, advancements, and more when moving between servers in a network.
Mod Support
Any other mods support is also possible.
Development Setup
Database Setup (Docker)
A docker-compose.yml file is provided for easily setting up a MariaDB database instance for development testing.
- Make sure Docker is installed.
- Inside your work directory run:
This will download the MariaDB image (if not already present) and start a database container in the background.docker compose up -d - Stoppinng the Database
docker compose down
Data Persistence: The database uses a Docker volume, ensuring your data persists even if you stop and restart the containers.
Database Management Tool
The docker-compose.yml also includes an Adminer service, a lightweight database management tool.
- Access Adminer in your web browser at http://localhost:8080.
- Log in using the server with
- username:
playersync - database:
playersync - password: see docker-compose.yml
- username:
For debugging purposes, you can enable use_legacy_serialization to have readable database fields. This can cause crashes and unintended side-effects. Do not enable this on a production server if not absolutely necessary!
Running the Mod
The project uses Gradle for building and running. Use the provided Gradle wrapper (gradlew for Linux/macOS, gradlew.bat for Windows).
- Make sure that the MySQL database you configured is running.
- Run the Server
or on Windows:./gradlew runServer
This task compiles the mod and starts a dedicated Minecraft server instance with the mod loaded in the.\gradlew.bat runServerrundirectory. - Run the Client
or on Windows:./gradlew runClient.\gradlew.bat runClient