Config (JdbcConfig.java completely restructured into sections):
connection
host, port, use_ssl, user_name, password, db_name, table_prefix, Server_id
general
sync_world, sync_advancements, kick_when_already_online,
kick_message, kick_grace_period_ms, use_legacy_serialization,
item_placeholder_title_override, item_placeholder_description_override
save_triggers
auto_save_interval_minutes (0-1440, default 10)
save_on_dimension_change (default false)
save_on_death (default true)
save_on_respawn (default true)
sync_toggles
sync_inventory, sync_ender_chest, sync_xp, sync_effects,
sync_health_food, sync_curios, sync_accessories, sync_backpacks,
sync_cosmetic_armor, sync_refined_storage (all default true)
performance
heartbeat_interval_seconds (5-600, default 30)
peer_stale_threshold_seconds (10-3600, default 60)
join_poll_max_attempts (10-600, default 120)
join_poll_interval_ms (100-5000, default 500)
pool_stats_interval_minutes (0-1440, default 5)
hikari_pool_max_size (1-200, default 15)
hikari_leak_threshold_ms (2000-600000, default 25000)
safety
refuse_empty_inventory_write (default true) — enforced in writeSnapshotToDB
max_inventory_size_bytes (default 10 MB)
skip_saves_when_tps_below (0-20, default 0 = never)
observability
log_structured_json (future use)
log_rotation_size_mb (default 10)
log_rotation_max_files (default 5)
Wiring
- HeartbeatService reads heartbeat_interval_seconds at start.
- PoolStatsReporter reads pool_stats_interval_minutes (0 disables).
- doPlayerJoin poll uses join_poll_max_attempts + join_poll_interval_ms +
peer_stale_threshold_seconds.
- writeSnapshotToDB: refuse_empty guard + max_inventory_size_bytes guard
before core UPDATE. Both log via SyncLogger.dataLoss / .nbtAnomaly.
- Restore-side toggles: applyCuriosFromData, applyAccessoriesFromData,
applyCosmeticArmorFromData, doBackPackRestore, restoreRefinedStorageDisks
all short-circuit when their toggle is false.
Commands — new /playersync tree (perm level 2 required):
status — server id + heartbeat age + exec/Hikari stats + online
poolstats — log current stats immediately
flush [player] — force save all / one
info <player> — DB row metadata
dump <player> — dump full DB row to server log
resync <player> — clear synced tag + kick to force re-restore
wipe <player> confirm — DELETE all rows (DANGER, double-keyword required)
orphans — list stuck online=1 rows on dead peers
clearorphans [id] — clear orphans (global or by server_id)
peers — list peer servers with ALIVE/STALE/STOPPED tag
peerkill <id> — force-disable a zombie peer
cleanup — orphans + stale peers in one shot
reload — note about runtime reload scope
help — in-chat command reference
Every command logs to SyncLogger as ADMIN_<OP> for audit trail.
Infrastructure
- JDBCsetUp.executePreparedUpdateRet(String, Object...) returns rows-affected
for commands that need meaningful counts.
- VanillaSync.getExecutor() exposes the thread pool for read-only stats access
from admin commands (replaces reflection use in PoolStatsReporter eventually).
13 KiB
13 KiB
Changelog
All notable changes to PlayerSync are documented here.
[2.1.5] - 2026-04-22 (cont.)
Added (Phase 8: configs + admin commands)
- Structured config sections —
connection,general,save_triggers,sync_toggles,performance,safety,observability. Old keys still accepted thanks to NeoForge's lenient loader. - Sync toggles —
sync_inventory,sync_ender_chest,sync_xp,sync_effects,sync_health_food,sync_curios,sync_accessories,sync_backpacks,sync_cosmetic_armor,sync_refined_storage. All default true. Wired as restore-side guards in each mod-compat path. - Save triggers —
save_on_death(default true),save_on_respawn(default true).save_on_dimension_changekept from Phase 4. - Perf configs —
heartbeat_interval_seconds(default 30),peer_stale_threshold_seconds(default 60),join_poll_max_attempts(default 120),join_poll_interval_ms(default 500),pool_stats_interval_minutes(default 5, 0 to disable),hikari_pool_max_size(default 15),hikari_leak_threshold_ms(default 25000). - Safety configs —
refuse_empty_inventory_write(default true) now enforced insidewriteSnapshotToDB: if the snapshot inventory is empty/tiny AND the DB row currently has real data, the write is refused and logged asDATA_LOSS.max_inventory_size_bytes(default 10 MB) rejects oversized snapshots.skip_saves_when_tps_belowplaceholder for future use.kick_message,kick_grace_period_ms. - Observability configs —
log_structured_json(future),log_rotation_size_mb,log_rotation_max_files. - Admin commands —
/playersync— full toolkit for diagnosis and maintenance:status— server id, heartbeat age, executor + Hikari pool snapshot, online countpoolstats— immediate log of current pool statsflush [player]— force save of all online players or a specific oneinfo <player>— DB row metadata (last_server, online flag, data sizes)dump <player>— full DB row dump into server logresync <player>— clear player_synced tag and kick to force fresh restorewipe <player> confirm— DANGER: DELETE all rows for a playerorphans— list online=1 rows whose peer is dead/staleclearorphans [server_id]— clear orphaned online flagspeers— list all peer servers with their heartbeat age and ALIVE/STALE/STOPPED tagpeerkill <server_id>— force-disable a zombie peercleanup— one-shot orphans + stale peers cleanupreload— status note about runtime config reloadhelp— in-chat command reference
- All commands require permission level 2 (op) and log to
SyncLoggerasADMIN_*events for audit trail.
Changed
JDBCsetUp.executePreparedUpdatenow delegates toexecutePreparedUpdateRetwhich returns rows affected. Existing callers unchanged; admin commands use the ret version for meaningful counts.HeartbeatService+PoolStatsReporter+doPlayerJoinpoll all read their interval/threshold from the new config keys instead of hardcoded constants.
Ajouts (French mirror — Phase 8)
- Sections config structurées —
connection,general,save_triggers,sync_toggles,performance,safety,observability. - Toggles de sync — 10 clés pour activer/désactiver la sync par catégorie.
- Triggers de sauvegarde —
save_on_death,save_on_respawn,save_on_dimension_change. - Configs perf — intervalles heartbeat/poll/pool-stats/hikari, seuils peer-stale.
- Configs sécurité —
refuse_empty_inventory_write(enforce-wipe protection),max_inventory_size_bytes(anti-bloat),kick_message,kick_grace_period_ms. - Commandes admin
/playersync— 14 commandes pour diagnostic et maintenance (status, flush, info, dump, resync, wipe, orphans, clearorphans, peers, peerkill, cleanup, poolstats, reload, help). - Toutes les commandes requièrent permission op (niveau 2) et logguent dans
SyncLoggerpour traçabilité.
[2.1.5] - 2026-04-22
Fixed (English first)
- Critical item duplication on drop + quick disconnect + reconnect — Race condition between the auto-save background task and the logout background task could commit a stale snapshot AFTER the logout save, resurrecting dropped items. Triple guard now applied:
pendingLogoutSavescheck (early + under lock) andSELECT online FROM player_dataskip if logout already committed. Logout BG now acquiresbgLockwith blocking.lock()for proper serialization. - Backpack / Sophisticated Storage merge-on-restore duplication —
setBackpackContents/setStorageContentsupstream are shallow merges, not replaces. Restore now callsremoveBackpackContents/removeStorageContents(with reflection fallback if absent) AND passes a defensive NBT copy. Fixes mass-duplication of items in backpacks/shulkers on every cross-server transfer. - Cross-server save overwrite — When
writeSnapshotToDB'slast_serverguard blocked the core player_data UPDATE, the downstream backpack/SS/RS2 saves still executed and overwrote the claiming server's data. The function now returns a boolean; all 5 callers short-circuit downstream writes on guard block. - 30-second join delay on zombie peer servers —
doPlayerJoinpoll waited the full 60 attempts (30s) for server_ids that no longer existed (legacyserver_id=0rows, or peers that crashed without clearingonline=0). NewisPeerServerStalecheck (peer_id=0 OR heartbeat >60s) takes over immediately and force-clears the orphaned flag. Poll max raised from 60 to 120 attempts (60s) for legitimate slow shutdowns. - Curios wipe on dead player — Legacy
StoreCurioswrote an empty flatMap when the Curios capability was unavailable, wiping DB data. Now early-returns with a WARN log.
Added
- JVM shutdown hook (kill -9 / OOM / SIGTERM recovery) — New
CrashRecovery.installShutdownHookregisters a non-daemon hook that callsVanillaSync.emergencyFlushAllsynchronously to snapshot and write every online player before process exit. Marksserver_info.enable=0so peers detect the shutdown. - Startup orphan-flag recovery —
CrashRecovery.clearOrphanedOnlineFlagsruns atonServerStartingto clear anyplayer_data.online=1rows left by a previous ungraceful exit. Logs the count viaSyncLogger. - Zombie-peer reporter —
CrashRecovery.reportZombiePeerslogs peerserver_ids whose heartbeat is stale or missing at boot time. - Server heartbeat service —
HeartbeatServicepingsserver_info.last_updateevery 10 seconds so peer servers can distinguish live from dead via the newisPeerServerStalecheck. - Periodic full-save scheduler —
PeriodicSaveServicetriggers a complete save (player data + backpacks + SS + RS2) for every online synced player everyauto_save_interval_minutes(new config, default 10, range 0-1440). Independent of NeoForge's vanillaPlayerEvent.SaveToFilecadence. - Dimension-change save trigger — New
onPlayerChangeDimensionhandler, gated bysave_on_dimension_changeconfig (default false). Protects against mid-teleport crashes. - Executor + HikariCP pool stats reporter —
PoolStatsReporterlogs[POOL] executor active/queue/idle, hikari active/idleevery 5 minutes. WARN thresholds trigger when queue >400/512 or Hikari active >=14/15. - Structured logging events —
SyncLoggergainedcontainerForceClosed,modCompatSkip,modCompatSaved,modCompatRestored,storageSave,poolStats,warnPlayer,nbtAnomalyfor finer-grained diagnostics.
Changed
writeSnapshotToDBsignature — Now returnsbooleaninstead ofvoid.truemeans the core UPDATE persisted,falsemeans thelast_serverguard blocked. All callers MUST check the return before firing downstream backpack/SS/RS2 writes.- Default
auto_save_interval_minutes— 10 min (new config key). Trades data-loss window on crash for DB load. Set to 0 to disable. - Backpack / SS restore — Now uses two-step clear (public API + reflection fallback) and defensive NBT copy before upstream setter. Full log line per restore with
cleared_via=api|reflectionandnbt_keys=N.
Correctifs (French mirror)
- Duplication d'items critique lors d'un drop + déconnexion rapide + reconnexion — Race condition entre la task auto-save background et la task logout background pouvait commiter un snapshot périmé APRÈS le save logout, ressuscitant les items drop. Triple garde maintenant appliquée : check
pendingLogoutSaves(early + sous lock) et skip viaSELECT online FROM player_datasi le logout a déjà commité. La task logout BG acquiert maintenantbgLocken blocking.lock()pour sérialiser proprement. - Duplication Backpack / Sophisticated Storage par merge au restore —
setBackpackContents/setStorageContentsen amont sont des merges shallow, pas des replaces. Le restore appelle maintenantremoveBackpackContents/removeStorageContents(avec fallback reflection si absent) ET passe une copie défensive du NBT. Corrige la duplication massive d'items dans les backpacks/shulkers à chaque transfert cross-server. - Écrasement cross-server des saves — Quand le guard
last_serverdewriteSnapshotToDBbloquait l'UPDATE core player_data, les saves downstream backpack/SS/RS2 s'exécutaient quand même et écrasaient les données du serveur ayant claim. La fonction retourne maintenant un boolean ; les 5 callers court-circuitent les writes downstream en cas de guard block. - Délai de 30 secondes à la connexion sur serveurs zombies — Le poll
doPlayerJoinattendait les 60 tentatives (30s) pour desserver_idn'existant plus (lignes legacyserver_id=0, ou peers ayant crashé sans clearonline=0). Nouveau checkisPeerServerStale(peer_id=0 OU heartbeat >60s) prend la main immédiatement et force-clear le flag orphelin. Poll max passé de 60 à 120 tentatives (60s) pour couvrir les shutdowns lents légitimes. - Wipe Curios sur joueur mort — La méthode legacy
StoreCuriosécrivait un flatMap vide quand la capability Curios était absente, wipant les données DB. Elle early-return maintenant avec un log WARN.
Ajouts (French mirror)
- Hook JVM shutdown (kill -9 / OOM / SIGTERM recovery) — Nouveau
CrashRecovery.installShutdownHookenregistre un hook non-daemon qui appelleVanillaSync.emergencyFlushAllsynchronement pour snapshot et écrire chaque joueur online avant la fin du process. Marqueserver_info.enable=0pour que les peers détectent le shutdown. - Recovery des flags orphelins au boot —
CrashRecovery.clearOrphanedOnlineFlagstourne auonServerStartingpour clear les rowsplayer_data.online=1laissées par une sortie ungracieuse précédente. Log le compte viaSyncLogger. - Reporter de peers zombies —
CrashRecovery.reportZombiePeerslog lesserver_idpeers dont le heartbeat est stale ou absent au boot. - Service heartbeat —
HeartbeatServicepingserver_info.last_updatetoutes les 10 secondes pour que les peers distinguent live vs dead via le nouveau checkisPeerServerStale. - Scheduler de sauvegarde périodique —
PeriodicSaveServicedéclenche une save complète (player data + backpacks + SS + RS2) pour chaque joueur online synced toutes lesauto_save_interval_minutes(nouvelle config, défaut 10, plage 0-1440). Indépendant de la cadence vanillaPlayerEvent.SaveToFilede NeoForge. - Trigger save sur changement de dimension — Nouveau handler
onPlayerChangeDimension, gated par la configsave_on_dimension_change(défaut false). Protège contre les crashes en plein téléport. - Reporter stats executor + HikariCP —
PoolStatsReporterlog[POOL] executor active/queue/idle, hikari active/idletoutes les 5 min. Seuils WARN quand queue >400/512 ou Hikari active >=14/15. - Événements structurés —
SyncLoggera gagnécontainerForceClosed,modCompatSkip,modCompatSaved,modCompatRestored,storageSave,poolStats,warnPlayer,nbtAnomalypour un diagnostic plus fin.
Modifications
- Signature
writeSnapshotToDB— Retourne maintenantbooleanau lieu devoid.true= l'UPDATE core a persisté,false= le guardlast_servera bloqué. Tous les callers DOIVENT vérifier le retour avant de déclencher les writes downstream backpack/SS/RS2. - Défaut
auto_save_interval_minutes— 10 min (nouvelle clé config). Trade-off entre fenêtre de perte de données sur crash et charge DB. 0 pour désactiver. - Restore Backpack / SS — Utilise maintenant un clear en deux étapes (API publique + fallback reflection) et une copie défensive NBT avant le setter upstream. Log complet par restore avec
cleared_via=api|reflectionetnbt_keys=N.