A Minecraft Forge mod that synchronizes player data across multiple servers using a MySQL backend.
Go to file
laforetbrut ea54596d8c Phase 15: 2-phase commit protocol — no more ambiguity, no more waits, no more dups
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.
2026-04-22 09:57:07 +02:00
.github Bump gradle/actions from 4 to 5 2025-10-14 16:55:21 +08:00
compat-mods Add compat-mods staging folder for mod compatibility analysis 2026-04-22 03:33:11 +02:00
gradle/wrapper migrate from ForgeGradle to ModDevGradle legacy 2025-05-02 22:40:39 +00:00
src/main Phase 15: 2-phase commit protocol — no more ambiguity, no more waits, no more dups 2026-04-22 09:57:07 +02:00
.gitattributes Initial commit 2022-12-08 16:59:20 +08:00
.gitignore Fix backpack/curios dup, perf overhaul, drop chat+cobblemon 2026-04-22 02:50:26 +02:00
build.gradle jarJar: declare version ranges for MySQL + HikariCP 2026-04-22 06:46:24 +02:00
CHANGELOG.md Phase 8: 20+ new config keys + 14 admin commands (/playersync) 2026-04-22 06:34:02 +02:00
docker-compose.yml use volume for docker-compose db to persist data 2025-05-01 18:42:58 +00:00
ERROR_LOG.md Phase 6: docs (CHANGELOG, ERROR_LOG, TEST_PROCEDURE) 2026-04-22 06:09:08 +02:00
gradle.properties perf: zero JDBC on server thread + HikariCP + parallel shutdown + audit fixes 2026-03-29 18:58:27 +02:00
gradlew migrate from ForgeGradle to ModDevGradle legacy 2025-05-02 22:40:39 +00:00
gradlew.bat migrate from ForgeGradle to ModDevGradle legacy 2025-05-02 22:40:39 +00:00
LICENSE Create LICENSE 2022-12-08 17:11:47 +08:00
README.md readme: add section on how to setup a dev env 2025-05-01 16:59:05 +00:00
settings.gradle half done, noticed about advancement can't store normally 2025-06-07 00:55:30 +08:00
TEST_PROCEDURE_v2.1.5.html Phase 6: docs (CHANGELOG, ERROR_LOG, TEST_PROCEDURE) 2026-04-22 06:09:08 +02:00

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.

  1. Make sure Docker is installed.
  2. Inside your work directory run:
    docker compose up -d
    
    This will download the MariaDB image (if not already present) and start a database container in the background.
  3. 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.

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).

  1. Make sure that the MySQL database you configured is running.
  2. Run the Server
    ./gradlew runServer
    
    or on Windows:
    .\gradlew.bat runServer
    
    This task compiles the mod and starts a dedicated Minecraft server instance with the mod loaded in the run directory.
  3. Run the Client
    ./gradlew runClient
    
    or on Windows:
    .\gradlew.bat runClient