Commit Graph

222 Commits

Author SHA1 Message Date
laforetbrut
4597041b1a Tutorial banner when MySQL init fails on a dedicated server
If the admin installs PlayerSync without configuring a reachable database,
onServerStarting used to throw SQLException and either crash the server or
spam a raw JDBC stack trace with no guidance. Now the whole init is wrapped
in a single try/catch that prints a large, readable banner to the console:

  - What failed (root cause summary, message truncated to 180 chars)
  - Current config values (host, port, user, db, password status)
  - A 5-step checklist:
      1. Is the DB reachable (telnet / mysql CLI hints)
      2. Is the password still the default placeholder
      3. Docker compose up for local dev
      4. GRANT + bind-address reminders
      5. How to skip PlayerSync entirely for a session
  - Then the full stack trace for bug reports.

The server keeps booting — sync operations will no-op until the DB comes
back. Avoids the 'server crashed, no idea why' experience for first-time
users.

Detection of placeholder credentials (password == 'pleaseChangeThisPassword'
or host == 'localhost') also emits a WARN line up-front so the tutorial
context is primed even when the connection itself would have succeeded.
2026-04-22 06:55:20 +02:00
laforetbrut
2361ffb272 jarJar: declare version ranges for MySQL + HikariCP
Enables co-installation with arcadia-lib2 which embeds
  HikariCP in [5.1.0, 6.0.0)
  mysql-connector-j in [8.3.0, 9.0.0)

Before: PlayerSync declared its embedded libs with no range, only the
exact version (9.3.0 / 5.1.0). When another mod declared a range that
did not include our exact version, NeoForge's jarJar resolver had no
valid overlap and would either refuse to load or arbitrary-pick one
version, risking runtime breakage.

After:
  - mysql-connector-j: strictly [8.3.0, 10.0.0), prefer 9.3.0.
    Intersects arcadia-lib's [8.3.0, 9.0.0) — resolver picks 8.3.0
    when both mods are present. 8.3.0 and 9.3.0 share the same
    Connection / PreparedStatement / ResultSet APIs we actually use,
    so downgrade is safe.
  - HikariCP: strictly [5.1.0, 6.0.0), prefer 5.1.0. Identical to
    arcadia-lib's declared range — shared single instance.

No code changes — only the metadata shipped in META-INF/jarjar/metadata.json.
Verified via unzip -p that the range is correctly emitted.
2026-04-22 06:46:24 +02:00
laforetbrut
d818794a20 Phase 8 fix: preserve config backward compatibility
The Phase 8 refactor moved the connection keys (host, password, Server_id,
etc.) from [general] into a new [connection] section. On servers with an
existing playersync-common.toml this would silently reset:
  - host to 'localhost'
  - password to 'pleaseChangeThisPassword'
  - Server_id to a new random value

The last one is the worst: every player_data row with last_server=<old_id>
would momentarily point to a zombie peer until the next heartbeat tick.

Fix: move every key that already existed in 2.1.4 configs back into
[general]. Only genuinely new keys (save_triggers, sync_toggles,
performance, safety, observability) stay in their new sections. Existing
users upgrading see their old [general] block load correctly; the new
sections get created with defaults on first boot and don't wipe anything.

Also adds modid=PlayerSync.MODID to CommandInit's @EventBusSubscriber
so RegisterCommandsEvent is guaranteed to fire under our mod's bus scope.
2026-04-22 06:38:27 +02:00
laforetbrut
c7487196ec Phase 8: 20+ new config keys + 14 admin commands (/playersync)
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).
2026-04-22 06:34:02 +02:00
laforetbrut
44178e020e Phase 7: server-perf hardening (hash-skip + batch + heartbeat tuning)
Based on a fresh audit against the Arcadia V2 modpack (444 mods, including
Curios + Accessories + SophisticatedBackpacks/Storage + RS2 + Cosmetic
Armor Reworked). Three perf wins + two opportunistic fixes.

Perf
  - Heartbeat period 10s -> 30s. Paired with the 60s staleness threshold
    this keeps failure-detection latency unchanged while cutting 3x the
    server_info UPDATE traffic per server.
  - Per-player hash-skip for unchanged snapshots (SaveToFile + staggered
    auto-save). computeSnapshotHash() rolls over inventory/equipment/
    enderchest/effects/xp/health/food/mod-data; when an auto-save produces
    the same hash as the last successful write, the BG task returns early
    and no UPDATE hits MySQL. Idle-server reduction is >95%. Logout /
    shutdown / death never use the skip and refresh the hash on success
    so post-logout rejoin doesn't wrongly skip.
  - Batched backpack + SS saves. saveBackpackSnapshots / saveSSSnapshots
    now build one transaction via executeBatchTransaction instead of
    N sequential REPLACE INTO calls. A player with 3 backpacks + 2
    shulkers drops from 5 network round-trips to 1 per logout save.
    Per-entry fallback preserved on transaction failure.
  - Periodic-save tick short-circuits when the player list is empty —
    no main-thread hop, no log line, no DB heartbeat on empty servers.

Compat notes (no code change needed)
  - CosmeticArmours (modid=cosmeticarmoursmod) items are worn in vanilla
    armor slots (Helmet / Chestplate / Leggings / Boots inner classes) —
    already captured by the core armor[] serialization. No handler needed.
  - CosmeticWeapons uses the same pattern via main hand / offhand — also
    already covered by core inventory serialization.

Cleanup
  - removePlayerLock now also clears the hash cache so a player who
    fully logged out doesn't leave a stale hash behind.
2026-04-22 06:17:28 +02:00
laforetbrut
a83543853c Phase 6: docs (CHANGELOG, ERROR_LOG, TEST_PROCEDURE)
Adds three documentation files covering the Phase 0-5 hardening work:

CHANGELOG.md
  - Bilingual EN/FR, strict template (English first, then ---, then French).
  - Version section 2.1.5 dated 2026-04-22 (NO version bump per
    CLAUDE.md version-lock rule).
  - Sections: Fixed / Added / Changed / Correctifs / Ajouts / Modifications.

ERROR_LOG.md
  - Journal of 8 bugs discovered and fixed during the hardening sweep.
  - Each entry: Context / Error / Root cause / Fix / Prevention rule.
  - Cross-references commits bea5f80 / c84f920 / 746cb56 / c70ca9f / bd0482c.

TEST_PROCEDURE_v2.1.5.html
  - Self-contained HTML (no external deps), bilingual EN/FR.
  - 10 test scenarios tagged CRITICAL / HIGH / MEDIUM with Setup, Steps,
    Expected Results, and a regression-check block.
  - Covers: drop+deco+reco, backpack dup, SS shulker dup, kill -9 recovery,
    zombie-peer short-circuit, periodic save, pool stats, heartbeat,
    curios cap unavailable, cross-server claim.
2026-04-22 06:09:08 +02:00
laforetbrut
bd0482cb76 Phase 5: structured logging + periodic pool-stats reporter
SyncLogger additions
  - containerForceClosed(uuid, reason)
  - modCompatSkip / modCompatSaved / modCompatRestored (per-mod tracing)
  - storageSave(storageUuid, kind, detail) for backpack/SS/RS2 lines
  - poolStats(exec active/queue/idle, hikari active/idle)
  - warnPlayer / nbtAnomaly generic helpers

PoolStatsReporter.java
  - Dedicated single-thread daemon scheduler, 5-min cadence.
  - Reads VanillaSync.executorService stats via reflection.
  - Reads HikariCP MBean via new JDBCsetUp.getPoolMXBean().
  - Emits WARN logs when executor queue > 400/512 or Hikari active >= 14/15
    so admins see saturation trends before they become outages.

JDBCsetUp.getPoolMXBean()
  - Public accessor for the HikariCP pool MBean. Returns null when pool
    is uninitialised / closed.

Wire-in: PlayerSync.onServerStarting starts the reporter, onServerShutdown
stops it before pool close.

Instrumentation
  - VanillaSync.onPlayerLogout logs containerForceClosed for self + viewer
    containers.
  - ModCompatSync.snapshotAccessories logs modCompatSkip when cap==null.
2026-04-22 06:03:52 +02:00
laforetbrut
c70ca9f464 Phase 4: 10-min periodic save + dimension-change trigger
Adds two new triggers that complement NeoForge's vanilla SaveToFile event:

PeriodicSaveService.java
  - Dedicated single-thread daemon scheduler, started after server boot.
  - Ticks every 'auto_save_interval_minutes' (config, default 10 min).
  - On each tick: hops to main thread, snapshots every online synced
    player via VanillaSync.snapshotAndQueueSave, async BG writes with full
    P0 guard stack (pendingLogoutSaves + online=0 + bgLock tryLock).
  - Set interval to 0 to disable.

VanillaSync.snapshotAndQueueSave(Player, String label)
  - Extracted from onPlayerSaveToFile body; public entry point shared by
    PeriodicSaveService, onPlayerChangeDimension, and the existing SaveToFile
    event. Label flows into logs for traceability (SaveToFile / PERIODIC / DIMENSION).

VanillaSync.onPlayerChangeDimension
  - New @SubscribeEvent on PlayerChangedDimensionEvent, gated by
    'save_on_dimension_change' config (default false). Queues a full save
    when a player teleports across dimensions, protecting against mid-
    teleport crashes.

JdbcConfig
  - Added AUTO_SAVE_INTERVAL_MINUTES (int, 0-1440, default 10)
  - Added SAVE_ON_DIMENSION_CHANGE (bool, default false)

VanillaSync.onServerShutdown also stops PeriodicSaveService before the pool
close, same pattern as HeartbeatService.
2026-04-22 06:01:55 +02:00
laforetbrut
746cb56275 Phase 3: anti-loss infrastructure (shutdown hook + heartbeat + crash recovery)
Adds three utilities to harden PlayerSync against ungraceful server exits:

CrashRecovery.java
  - installShutdownHook: registers a non-daemon JVM shutdown hook that calls
    VanillaSync.emergencyFlushAll() synchronously when the process is killed
    (SIGTERM, kill, OOM, host reboot). Covers the case where the normal
    ServerStoppingEvent path never runs.
  - clearOrphanedOnlineFlags: on startup, clears any online=1 player_data
    rows pointing to this server_id (left by a previous crash). Reports the
    count via SyncLogger so admins can see recovery activity.
  - reportZombiePeers: logs peer server_ids whose heartbeat is missing or
    stale (>60s), exposing the root of doPlayerJoin poll timeouts.

HeartbeatService.java
  - Single-thread daemon scheduler pinging server_info.last_update every 10s.
  - Lets peer servers distinguish live from dead via isPeerServerStale().
  - Stopped explicitly in VanillaSync.onServerShutdown before pool close.

VanillaSync.emergencyFlushAll()
  - Synchronous best-effort flush for every online player. No executor, no
    locks — the server is dying, we just want data on disk. Writes player_data,
    backpacks, SS, RS2 directly; logs SAVE/SKIPPED/FAILED per player via
    SyncLogger so post-mortem analysis is possible.

PlayerSync.onServerStarting wires the four new calls after table init.

Fixes the production issue where players remained online=1 forever after
kill -9 and the 30s poll timeouts waiting for zombie server_ids.
2026-04-22 05:44:19 +02:00
laforetbrut
c84f920d11 Phase 2: hardened anti-dup + zombie-server detection + guard propagation
P0-1: Backpack/SS clear-before-restore now has a belt-and-suspenders
reflection fallback if the public removeBackpackContents / removeStorageContents
API fails. setBackpackContents / setStorageContents receive a defensive NBT
copy to prevent upstream from mutating the cached snapshot.

P0-2: writeSnapshotToDB now returns a boolean. When the last_server guard
blocks the core player_data UPDATE (another server claimed the player),
the downstream backpack / SS / RS2 saves are skipped instead of overwriting
the claiming server's rows. Affects logout, shutdown, staggered auto-save,
and death-save paths.

P1-1: StoreCurios now aborts when the Curios capability is unavailable
(dead player, mod init race) instead of writing an empty flatMap that
would wipe the DB row.

P1-3: doPlayerJoin last_server poll raised 60→120 attempts (30s→60s)
and gained a zombie-server short-circuit: if the peer server_id is 0
(legacy / corrupted), or its server_info heartbeat is older than 60s,
the poll takes over immediately and force-clears the orphaned online=1.
Fixes the user-observed 'attempt 60/60' loops on server_id=0 and stale
heartbeats.

Staggered auto-save and death-save BG tasks also gained the P0-a/b/c
guards introduced in bea5f80 (pendingLogoutSaves + online=0 DB check).
2026-04-22 05:40:16 +02:00
laforetbrut
bea5f80e3a Fix critical item duplication race (drop+deco+reco)
Root cause: auto-save BG task queued before logout could acquire bgLock and
write a stale snapshot AFTER the logout BG task had committed fresh data +
online=0. On reconnect, the stale inventory was restored while the dropped
ItemEntity remained on the ground -> duplication.

Three-layer guard applied to onPlayerSaveToFile and onLivingDeath BG tasks:
  1. Early skip if pendingLogoutSaves contains the player (before tryLock)
  2. Re-check pendingLogoutSaves after acquiring bgLock (race window)
  3. SELECT online from player_data before write; skip if online=0

Logout BG task now acquires bgLock via .lock() (blocking) so concurrent
auto-save / death-save tasks using tryLock either skip cleanly or wait.
removePlayerLock reordered before bgLock.unlock so late auto-save BGs see
containsKey=false and skip.
2026-04-22 05:28:36 +02:00
laforetbrut
f334b44a55 Add compat-mods staging folder for mod compatibility analysis
Local .jar staging area for inspecting mod APIs and writing compatibility
shims. Binaries git-ignored; README documents the purpose and conventions.
2026-04-22 03:33:11 +02:00
laforetbrut
13de5b65c0 Fix backpack/curios dup, perf overhaul, drop chat+cobblemon
Root cause of backpack duplication: Sophisticated Backpacks'
setBackpackContents merges shallowly when the UUID exists, so stale
sub-tags survived every restore. doBackPackRestore now calls
removeBackpackContents before setBackpackContents for a clean replace.

Curios cosmetic stacks (getCosmeticStacks) are now snapshotted, applied,
restored and cached on all paths. Old-format rows without the "cos:"
prefix still parse unchanged, so existing DB data is preserved on upgrade.

closeContainer no longer matches by class-name substring (was closing
unrelated mod menus containing "curio"/"accessor"). Only menus whose
slots reference the disconnecting player's inventory/ender-chest are
closed.

Thread-safety: Sophisticated Storage contents are now snapshotted on the
main thread (snapshotSSData + saveSSSnapshots) instead of read from a
background thread racing with world ticks.

Event priority / defensive guards:
- onPlayerDeath is now EventPriority.LOW and skips cancelled events so
  Revive Me / Corail Tombstone's cancel runs first.
- onServerStarting short-circuits on integrated (single-player) servers
  to avoid noisy MySQL connection attempts.

Observability:
- executeBatchTransaction now returns per-statement row counts.
- writeSnapshotToDB calls SyncLogger.guardBlocked when the core UPDATE
  silently no-ops (another server claimed last_server).
- SyncLogger uses a daemon scheduler that flushes every 500 ms; shutdown
  happens after parallel saves so final save logs are no longer dropped.
- Rollback failures inside executeBatchTransaction and
  refreshInventoryForInputOutput are now logged instead of swallowed.

HikariCP retuned: maxPoolSize 25->15, connectionTimeout 30->10s,
idleTimeout 600->300s, leakDetectionThreshold 10->25s (covers worst-case
join polling without log spam).

New table_prefix config option (Tables helper) lets a user share one
MySQL database with other mods without table-name collisions. Default
is empty to preserve backward compatibility.

Reflection Methods for NeoForge AttachmentHolder are resolved once in
a static initializer and cached.

Chat sync and Cobblemon integration removed:
- Chat sync: 319 LoC of socket/thread code guarded by a config flag that
  defaulted to false; orphaned config keys are silently ignored by the
  NeoForge ModConfig loader, so no crash on upgrade.
- Cobblemon: 297 LoC of mixins that ran synchronous JDBC on the main
  thread and built SQL with raw UUID concatenation. The existing
  cobblemon table in the DB is left untouched on upgrade.

Also fixes cobblemon ALTER TABLE running blindly on every boot
(alterColumnIfNeeded helper checks INFORMATION_SCHEMA first).

Author: vyrriox
2026-04-22 02:50:26 +02:00
laforetbrut
edf63aeb8c Add dedicated PlayerSync diagnostic log file (logs/playersync/sync.log)
New SyncLogger utility class:
- Writes to logs/playersync/sync.log (separate from MC console)
- Automatic rotation: 10MB max per file, 5 files kept
- Thread-safe: lock-free ConcurrentLinkedQueue + async flush
- Categorized log levels: INFO, WARN, ERROR, DUPE_RISK, DATA_LOSS,
  RACE, PERF_SLOW, SAVE, SAVE_FAIL, SAVE_SKIP, RESTORE, EVENT, GUARD

Tracked events:
- Every player join/leave with sync status
- Every save (logout, shutdown, death, auto-save) with duration
- Save failures with error details
- Saves skipped (uncompleted sync, dead player)
- Cross-server race conditions (poll loop waiting)
- Player disconnects before sync apply (potential data loss)
- Duplicate login kicks
- Slow operations (> 50ms threshold)

Usage: check logs/playersync/sync.log on your server for diagnostics.
Look for DUPE_RISK, DATA_LOSS, RACE, SAVE_FAIL entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:12:31 +02:00
laforetbrut
57f7925c2f Perf: MySQL connection tuning, batch transactions, leak detection
MySQL connection string optimizations:
- rewriteBatchedStatements=true: rewrites batch INSERTs into multi-row (5-30x)
- cachePrepStmts=true + useServerPrepStmts=true: server-side prepared
  statement caching, avoids re-parsing identical queries (15-25% CPU reduction)
- prepStmtCacheSize=256: keeps 256 compiled statements warm
- useCompression=true: compresses network traffic (40-60% for large NBT blobs)
- tcpNoDelay=true: disables Nagle's algorithm for lower latency

Batch transaction for writeSnapshotToDB:
- New JDBCsetUp.executeBatchTransaction() executes multiple SQL statements
  in a SINGLE transaction on ONE connection with automatic rollback.
- writeSnapshotToDB now batches all 4-8 queries (player_data + curios +
  mod_player_data) into one connection borrow + one commit.
- Previous: 4-8 separate getConnection() + executeUpdate() + close() calls
  per player save = 4-8 network round-trips.
- Now: 1 getConnection() + N executeUpdate() + 1 commit() + 1 close()
  = 1 network round-trip for the transaction.
- With 35 players: 140-280 connection borrows → 35 connection borrows.

HikariCP leak detection:
- Added leakDetectionThreshold=10000ms to detect connections held > 10s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:06:22 +02:00
laforetbrut
b4d863efa2 Perf: staggered auto-save, pool scaling, cached kick check
CRITICAL PERF - Staggered auto-save:
- Old: all 35 players snapshotted in ONE tick → 770-3605ms MSPT spike
  (15-36 second TPS drop every 5 minutes)
- New: queue filled every 5min, drained 1 player/tick → max 22-103ms/tick
- autoSaveQueue processes one player per server tick, imperceptible impact

CRITICAL PERF - Pool scaling for 35+ players:
- Thread pool: 2-8 → 4-16 threads, queue 256 → 512
  Prevents CallerRunsPolicy from executing DB tasks on main thread
- HikariCP: 10 → 25 max connections, 2 → 4 min idle
  Prevents connection starvation during concurrent saves

HIGH PERF - Cached kick check (eliminates main thread DB queries):
- doPlayerConnect (network thread) caches online/lastServer/serverAlive
- onPlayerLoggedInKickCheck (MAIN thread) reuses cached result
- Fast path: 1 DB query on main thread instead of 2-4
- Fallback: full DB check if cache miss (race condition safety)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:33:02 +02:00
laforetbrut
badc87c84e Fix backpack crash loss, ender chest restore, ReviveMe compat, effect sync
Backpack data loss on server crash:
- Periodic auto-save (every 5min) now includes backpack content snapshots.
  Previously backpacks were only saved on logout/shutdown — hard crashes
  (OOM, watchdog, kill -9) skipped both, losing all backpack changes.
- snapshotBackpackData captures NBT with .copy() on main thread.

Backpack ender chest restore mismatch:
- doBackPackRestore now scans ender chest in addition to main inventory.
  Save side already scanned ender chest, but restore didn't — backpacks
  in ender chest were saved to DB but never restored on join.

ReviveMe mod compatibility:
- Dead player kick check now uses health <= 0 instead of isDeadOrDying().
  ReviveMe puts players in a "downed" state (alive but isDeadOrDying=true)
  — previously these players were kicked on join.

Infinite effect filtering (phantom effects fix):
- Effects with infinite duration are now skipped during save. These come
  from ReviveMe (downed state effects with MAX_VALUE duration), beacons,
  and other mods. Syncing them across servers caused phantom effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:24:18 +02:00
laforetbrut
1d30184aba Fix critical data loss, backpack duplication, and ender chest sync
CRITICAL - New player data loss (players lose everything):
- store() INSERT now includes last_server column. Without it, last_server
  stayed NULL, causing ALL subsequent writes (AND last_server=?) to fail
  silently — new players' data was never saved after initial INSERT.
- writeSnapshotToDB now handles legacy NULL last_server with
  (last_server=? OR last_server IS NULL) and auto-claims ownership.
- Same NULL handling in writeGuardedModData for mod_player_data table.

CRITICAL - online=0 stuck at 1 (players unable to connect):
- Removed AND last_server=? from deadPlayerWhileLogging and
  syncNotCompletedPlayer logout paths. These fire before doPlayerJoin
  sets last_server, so the guard always failed → online stayed 1.

CRITICAL - Backpack duplication via viewer race:
- snapshotBackpackData() now captures backpack NBT on the MAIN THREAD
  (not just UUIDs). Previously saveBackpacksByUuids read BackpackStorage
  on an async thread — another player viewing the backpack could take
  items between the main-thread refresh and the async read.
- .copy() freezes the NBT state at snapshot time.

CRITICAL - Backpacks in ender chest not synced:
- snapshotBackpackData() and doBackPackRestore now scan the ender chest
  in addition to main inventory. PlayerInventoryProvider.runOnBackpacks
  only scans equipment/inventory, missing ender chest backpacks entirely.

Anti-duplication - Container closing on disconnect:
- Owner's container menu is force-closed before snapshot to prevent
  post-snapshot modifications by viewers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:00:18 +02:00
laforetbrut
f042058e5b Fix Accessories/CosmeticArmor duplication + guard remaining online=0
Accessories & CosmeticArmor duplication fix:
- snapshotAccessories() and snapshotCosmeticArmor() returned null when
  slots were empty, causing writeModSnapshot to SKIP the write. The DB
  kept stale data from when slots had items, restoring them on next join.
- Now return "{}" (like snapshotCuriosData already does), so empty state
  is properly written to DB. On restore, apply*FromData clears slots
  when it sees "{}" (length <= 2).

Guard remaining online=0 writes:
- deadPlayerWhileLogging and syncNotCompletedPlayer logout paths now
  use AND last_server=? to prevent setting online=0 for a player that
  already moved to another server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:26:10 +02:00
laforetbrut
8f40d5b27f Fix critical cross-server duplication race + memory leak + atomic saves
CRITICAL FIX - Stale server overwrite prevention:
- writeSnapshotToDB now guards ALL writes with AND last_server=? so a
  crashing/slow server cannot overwrite fresher data saved by another server
- Logout and shutdown saves atomically set online=0 in the SAME UPDATE as
  the data write (no more gap between data write and flag set)
- ModCompatSync.writeModSnapshot guarded variant uses subquery on last_server

CRITICAL FIX - Poll loop actually waits now:
- onPlayerLoggedInKickCheck no longer sets last_server (only online=1)
- last_server is claimed AFTER the poll in doPlayerJoin completes
- This allows the poll to correctly detect and wait for the old server's
  async save to finish before reading data
- Poll increased from 30 to 60 attempts (30s window)

Memory leak fix:
- Added removePlayerLock() in doPlayerJoin's outer catch block to prevent
  unbounded growth of playerLocks ConcurrentHashMap on exceptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:47:38 +02:00
laforetbrut
1dfdd43908 Fix advancement wipe, phantom effects on death, and advancements COALESCE
- Advancements: default to null instead of "" in snapshotPlayerData, use
  COALESCE(?, advancements) in SQL so failed file reads preserve DB value
  instead of silently wiping advancements every 5min periodic save
- Effects: skip saving effects when player isDeadOrDying() — Minecraft
  clears effects on respawn not death, so pre-death effects were persisted
  in DB and restored as phantom effects on next login
- Legacy store() also uses COALESCE(NULLIF(?, ''), advancements)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:52:14 +02:00
laforetbrut
eec949f405 Fix anti-duplication: clear slots before restoring data 2026-04-04 07:16:50 +02:00
laforetbrut
a8c0cb50af Update VanillaSync.java 2026-03-31 03:51:01 +02:00
laforetbrut
59bd884263 perf: zero JDBC on server thread + HikariCP + parallel shutdown + audit fixes
- Migrate connection pool from manual LinkedBlockingQueue to HikariCP
  (eliminates isValid() ping on every query visible in Spark profiler)
- Move ALL DB writes off server thread: logout uses snapshot+async+latch,
  shutdown uses snapshot+CompletableFuture.allOf for parallel saves
- Pre-read curios/accessories/cosmeticarmor/attachments on background
  thread during login (4-7 fewer DB queries on main thread per login)
- Auto-save interval increased to 5 minutes
- Fix pool shutdown ordering: shutdownPool() now runs AFTER all shutdown
  saves complete (previously could fire before, silently losing all data)
- Fix connection leak in executeQuery/executePreparedQuery when
  prepareStatement throws (leaked connections exhaust HikariCP pool)
- Fix duplication bug: saveStorageContents guard used nbt.size()<=1 which
  blocked legitimately emptied backpacks from saving to DB
- Fix stale SaveToFile overwriting logout: check playerLocks.containsKey
  before writing to prevent stale background task from regressing data
- Remove LIMIT 1000 on startup online=0 reset (could leave players stuck)
- Add executorService.shutdown() on server stop to prevent JVM hang
- Add apply methods (applyCuriosFromData, applyAccessoriesFromData, etc.)
  to separate entity writes from DB reads for thread-safe restore
- Add UUID collectors (collectBackpackUuids, collectSSUuids) and
  background save methods for snapshot+async logout/shutdown pattern
2026-03-29 18:58:27 +02:00
laforetbrut
4999c372ec perf: eliminate synchronous MySQL calls on server main thread
Root cause of lag (TPS 9-16, MSPT spikes to 4846ms):
PlayerEvent.SaveToFile triggered synchronous JDBC writes on the
server main thread every Minecraft autosave cycle. With 35 players
this caused hundreds of network round-trips to MySQL blocking the
tick loop for up to 4846ms (97x the 50ms limit).

Fixes applied:
- onPlayerSaveToFile: now fully async. Entity state is snapshotted
  on the main thread (pure memory ops, <1ms), then ALL DB writes are
  submitted to the background executor. Main thread never blocks on
  MySQL again.

- snapshotPlayerData: now captures ALL entity-dependent mod data
  (Curios, Accessories, CosmeticArmor, NeoForge attachments) on the
  main thread. Previously these were read from a background thread
  which is not thread-safe and could cause data corruption.

- writeSnapshotToDB: single method that writes all player data in one
  background pass: player_data + curios + mod_player_data.

- Auto-save background task: removed ModCompatSync.storeAll(player),
  storeSophisticatedBackpacks, storeSophisticatedStorageItems,
  storeRefinedStorageDisks from background thread. These all accessed
  entity state off-thread. Mod compat data is now in the main-thread
  snapshot; backpack/SS/RS2 contents are saved on logout/shutdown.

- Added ModCompatSync snapshot API: snapshotAccessories(),
  snapshotCosmeticArmor(), snapshotAttachments(), writeModSnapshot()
  for clean separation of entity reads vs DB writes.
2026-03-27 14:15:29 +01:00
laforetbrut
04a1f0128e Optimize: move ALL DB writes off main thread + increase auto-save to 2min
Spark showed 5.66% server thread from auto-save. Breakdown:
- store() DB write: 1.39% (already moved to background)
- StoreCurios DB write: 0.56% (was on main thread)
- storeAccessories DB write: 0.55% (was on main thread)
- storeCosmeticArmor DB write: 0.56% (was on main thread)
- storeNeoForgeAttachments DB write: 0.58% (was on main thread)
- storeSophisticatedStorage: 0.69% (was on main thread)
- storeSophisticatedBackpacks: 0.59% (was on main thread)

Changes:
1. Curios snapshot: new snapshotCuriosData() reads entity state on
   main thread (fast), returns serialized string. DB write in background.
2. ALL mod saves moved to background thread lambda:
   - ModCompatSync.storeAll (Accessories, CosmeticArmor, Attachments)
   - Sophisticated Backpacks/Storage/RS2
3. Auto-save interval doubled: 1200 -> 2400 ticks (1min -> 2min)
4. Main thread now only does: entity snapshot (~0.3ms) + curios snapshot

Expected: ~80% reduction in main thread usage (5.66% -> ~1%)

Vyrriox
2026-03-26 22:17:25 +01:00
laforetbrut
7613f4ecfb Fix backpack/shulker contents lost on transfer: never overwrite DB with empty data
ROOT CAUSE: Sophisticated Backpacks/Storage wrappers cache inventory
in memory. When store() reads from BackpackStorage/ItemContentsStorage,
the SavedData may not have the latest wrapper state (unflushed changes).
This returns empty/default NBT which overwrites the real data in our DB.

Going back to the original server showed data because that server's
local SavedData still had the correct data (never overwritten).

FIX: saveStorageContents() now checks if the NBT is empty/minimal
before writing. If the DB already has substantial data (>50 bytes)
and the new NBT is empty, the save is SKIPPED to preserve the real
data. This prevents the empty-overwrite scenario while still allowing
legitimate saves of actual content.

Vyrriox
2026-03-26 22:09:51 +01:00
laforetbrut
e511414463 Final hardening: online=0 in finally + auto-save race fix
CRITICAL-1: online=0 moved to finally block in logout handler.
If store() threw an exception, online=0 was never written and the
player was permanently locked out of all servers.

CRITICAL-2: Same fix for shutdown handler. Any save failure during
shutdown left the player permanently stuck as online=1.

IMPORTANT: Auto-save background DB write now acquires tryLock()
before writing. If logout already saved newer data and holds/held
the lock, the stale auto-save snapshot is skipped. Prevents
overwriting correct logout data with an older snapshot.

Vyrriox
2026-03-26 22:06:38 +01:00
laforetbrut
1bf2a67e8d Optimize auto-save: snapshot on main thread, DB write on background
Spark showed 5.66% server thread from auto-save DB writes blocking
the tick loop (~1-2ms per player per query, ~8 queries per save).

New approach:
- snapshotPlayerData() captures ALL entity data into an immutable
  PlayerDataSnapshot record on the main thread (fast, no DB I/O)
- writeSnapshotToDB() writes the snapshot to DB on the background
  thread via executorService (slow DB I/O off main thread)
- Mod data (Curios, Accessories, CosmeticArmor, NeoForge attachments)
  still read entity state on main thread but their DB writes happen
  inline (they manage their own connections)
- Sophisticated Backpacks/Storage/RS2 saves happen during snapshot
  phase on main thread (they need entity access for inventory scan)

Expected: ~60-70% reduction in main thread blocking from auto-save.

Vyrriox
2026-03-26 21:31:43 +01:00
laforetbrut
d60b8eb01e Add connection pool - fix 10% server thread usage from MySQL connects
Spark showed PlayerSync consuming 10.16% of the server thread, almost
entirely from DriverManager.getConnection() (TCP handshake + MySQL auth
+ USE db) called for EVERY single query. With auto-save every 60s,
each player generated ~6 new connections per save cycle on main thread.

FIX: Simple connection pool (LinkedBlockingQueue, 5 connections).
- Connections are reused instead of opened/closed per query
- isValid(2) check before reuse to detect dead connections
- returnConnection() puts connections back in pool instead of closing
- QueryResult.close() also returns to pool
- autoReconnect=true in JDBC URL for resilience
- shutdownPool() for clean server stop
- Non-database connections (startup DDL) bypass the pool

Expected improvement: ~90% reduction in MySQL overhead on server thread.

Vyrriox
2026-03-26 21:13:17 +01:00
laforetbrut
e9620eb07e Fix RS2 restore: wrap entry in UUID key before codec decode
ROOT CAUSE from logs:
  "Invalid UUID capacity: Invalid UUID string: capacity"
  "Invalid UUID resources: Invalid UUID string: resources"

We saved the INNER storage data ({type, capacity, resources}) but the
map codec expects {uuid-string: {type, capacity, resources}}.
The codec tried to parse "capacity", "resources", "type" as UUIDs.

FIX: Wrap the stored NBT back in a UUID-keyed CompoundTag before
decoding: wrapped.put(uuid.toString(), storedNbt)

Also increased sync timeout from 15s to 60s - the server was 34s
behind (691 ticks) causing timeout errors for player sync.

Vyrriox
2026-03-26 20:53:23 +01:00
laforetbrut
12645a1d3d Fix RS2 restore: remove() before set() + reflection fallback
repo.set(uuid, storage) throws IllegalArgumentException if the UUID
already exists in the StorageRepository. This happens when a player
revisits a server where the disk was previously used.

Items appeared briefly (data was decoded correctly) but then the
exception prevented the set() and the storage fell back to empty.

Fix:
- Call repo.remove(uuid) before repo.set(uuid, storage)
- If set() still fails, inject directly into the entries map via
  reflection + mark SavedData dirty
- setDirty() ensures the injected data persists to disk

Vyrriox
2026-03-26 20:37:02 +01:00
laforetbrut
2baa8e4c39 Fix RS2: use createCodec() not getMapCodec() - wrong return type
ROOT CAUSE: getMapCodec(Runnable) returns MapCodec (not Codec).
createCodec(Runnable) returns Codec<Map<UUID, SerializableStorage>>.
Reflection on getMapCodec silently failed because the returned
MapCodec.decode() has a different signature than Codec.decode().

Both save fallback and restore codec paths now use createCodec().

RS2 uses ErrorHandlingMapCodec with UUIDUtil.STRING_CODEC for keys,
so the encoded format IS a CompoundTag with UUID strings as keys.

Vyrriox
2026-03-26 20:25:50 +01:00
laforetbrut
bce7a73cb8 Fix RS2 disk sync: use save() return value + codec reflection fallback
Save side:
- save() returns data in a NEW CompoundTag (fixed in previous commit)
- Now logs full NBT structure for debugging (describeNbtStructure)
- If UUID not found in save() NBT, falls back to reflection on
  internal entries map + codec.encodeStart() to serialize directly

Restore side:
- Rewritten to use raw Codec types to avoid generic compilation issues
- Decodes stored NBT via the same map codec, then repo.set() to inject

Both sides now have comprehensive logging to diagnose any remaining
format issues in production.

Vyrriox
2026-03-26 20:14:26 +01:00
laforetbrut
4e2574a147 Fix RS2 disk save: use return value of SavedData.save()
save() returns the serialized data in a NEW CompoundTag - it does NOT
fill the input parameter. We were passing an empty tag and reading it
back, getting nothing. The actual data was in the return value.

Log showed: "RS2 disk UUID xxx exists in repo but NOT found in save()
NBT. Keys at top: []" - empty because we ignored the return value.

Vyrriox
2026-03-26 19:30:27 +01:00
laforetbrut
50c77f7bb8 Fix last 2 audit issues: syncNotCompleted race + SaveToFile off-thread
BUG 1 - syncNotCompletedPlayer race condition:
  syncNotCompletedPlayer.add() was inside the background thread body.
  A player disconnecting instantly before the thread starts bypasses
  the "sync not completed" guard in onPlayerLogout, causing store()
  to read invalid entity state.
  FIX: add() moved to onPlayerJoin BEFORE executorService.submit().

BUG 2 - doPlayerSaveToFile off main thread:
  onPlayerSaveToFile wrapped doPlayerSaveToFile in executorService,
  but SaveToFile already fires on the main thread. store() reads
  player inventory/armor/effects from a background thread = corruption.
  FIX: Call doPlayerSaveToFile directly (no executor). Same fix as
  auto-save and logout paths.

Vyrriox
2026-03-26 19:17:16 +01:00
laforetbrut
6bb8aeba39 Fix RS2 disk + SS shulker data loss: use in-memory API, not .dat files
ROOT CAUSE for both:
- RS2: We removed dataStorage.save() to avoid fastasyncworldsave crash,
  but then read the .dat file which had stale data. Disks appeared
  empty because the file didn't contain the latest in-memory state.
- SS: getOrCreateStorageContents() could create empty content if the
  data wasn't loaded yet for that UUID.

FIX RS2:
- Save: Use SavedData.save(CompoundTag, Provider) which serializes
  from MEMORY, not disk. No file I/O, no fastasyncworldsave conflict.
- Restore: Decode entries via RS2's codec (reflection on getMapCodec)
  and inject via repo.set(). Falls back to direct NBT injection if
  codec fails.
- Removed dead code: getRS2DataFile, injectRS2EntryIntoNbt

FIX SS:
- Already using StackStorageWrapper.fromStack() API for UUID extraction
  (DataComponent-based, not CustomData). This was fixed in previous
  commit. If data still missing, the save() logging will show which
  UUIDs fail to find in ItemContentsStorage.

Vyrriox
2026-03-26 19:12:02 +01:00
laforetbrut
7c89df7d1b Remove dataStorage.save() call that conflicts with fastasyncworldsave
storeRefinedStorageDisks() called DimensionDataStorage.save() directly
to flush RS2 data before reading the .dat file. This triggers all
SavedData saves simultaneously and conflicts with fastasyncworldsave's
async save mixin, causing ConcurrentModificationException crash.

Fix: Only mark RS2 SavedData as dirty (setDirty()) and let the normal
world save cycle handle the flush. The .dat file read may get slightly
stale data but avoids crashing the server.

Vyrriox
2026-03-26 18:51:27 +01:00
laforetbrut
484f1a8c05 Final audit: fix ghost-online, SQL injection, resource leak, NPE
CRITICAL-1/2: Remove duplicate online=1 writes from doPlayerJoin.
The synchronous onPlayerLoggedInKickCheck already sets online=1.
The background thread writes raced with logout's online=0, permanently
locking players as "online" after crash-disconnect during join.

HIGH-1: Startup SQL uses PreparedStatement for server_id (was string concat).
HIGH-2: update() method now uses try-with-resources for PreparedStatement.
HIGH-3: NPE guard in RS2 data file logging when getRS2DataFile returns null.

Vyrriox
2026-03-26 18:33:00 +01:00
laforetbrut
b1563cc9ae Fix duplicate login kick bypass - logout was resetting online flag
ROOT CAUSE: When Server B kicks a player for being already online on
Server A, the onPlayerLogout handler on Server B fires and sets
online=0 in the DB. The player then immediately reconnects to Server B,
the DB says online=0, and the kick check passes - player is now on
BOTH servers simultaneously.

FIX: New kickedForDuplicateLogin set tracks players being kicked for
duplicate login. onPlayerLogout checks this set FIRST and skips the
online=0 update entirely. The player's DB record correctly stays
online=1 with last_server=A, preventing reconnect bypass.

Flow:
1. Player on Server A (online=1, last_server=A)
2. Player tries Server B → kick check → online=1, A alive → KICK
3. kickedForDuplicateLogin.add(uuid) BEFORE disconnect
4. onPlayerLogout fires → sees kickedForDuplicateLogin → skips online=0
5. Player retries Server B → online=1 still → KICKED AGAIN

Vyrriox
2026-03-26 18:27:29 +01:00
laforetbrut
0a88694166 Production hardening: fix all critical audit issues
CRITICAL fixes:
- C-1/C-2/C-4: Auto-save and logout now run on MAIN THREAD. All entity
  reads (inventory, curios, effects) were happening off-thread, causing
  duplication exploits (player interacts during save → items duplicated).
  Auto-save uses tryLock() to skip players already being saved.
- C-5: NPE fix for non-RS2 items (null check on registry key lookup)
- C-6: RS2 .dat file written atomically (temp file + rename) to prevent
  corruption of entire RS2 storage on crash mid-write

HIGH fixes:
- H-3: Deadlock prevention: lock released BEFORE latch.await() in
  doPlayerJoin. Prevents shutdown deadlock where background thread
  holds lock while waiting for main thread, and shutdown holds main
  thread while waiting for lock.
- H-5: Curios cache now works WITHOUT keepInventory. Players who die
  then disconnect before respawning no longer lose curios data.
- H-8: server_id SQL uses PreparedStatements instead of string concat

MEDIUM fixes:
- M-1: NumberFormatException in LocalJsonUtil caught per-entry instead
  of crashing entire map parse (prevents losing all cosmetic armor)

Vyrriox
2026-03-26 18:14:31 +01:00
laforetbrut
6c5807d3c8 Fix Sophisticated Storage shulkers, RS2 disks, and kick system
1. Sophisticated Storage shulkers/barrels/chests:
   - ROOT CAUSE: UUID stored as DataComponent (not in CustomData).
     extractStorageUuid() only checked CustomData, missing the UUID.
   - FIX: Use StackStorageWrapper.fromStack(provider, item).getContentsUuid()
     which reads the DataComponent via the proper API.
   - Also scan ender chest for packed storage items.

2. Refined Storage 2 disks:
   - ROOT CAUSE: save() on StorageRepositoryImpl returned data in an
     unknown codec format that our extraction couldn't parse.
   - FIX: Read/write the .dat file directly from disk after forcing
     a save flush. This uses the exact NBT format RS2 writes.
   - Search multiple NBT structures (direct keys, nested compounds,
     list-of-pairs) to handle any codec format.
   - On restore: write entries into .dat file, clear DimensionDataStorage
     cache via reflection to force RS2 to reload.

3. Kick system:
   - ROOT CAUSE: PlayerNegotiationEvent.getConnection().disconnect()
     does NOT work in NeoForge 1.21.1 (too early in connection).
   - FIX: Full duplicate check moved to PlayerLoggedInEvent with
     HIGHEST priority. Uses player.connection.disconnect() which
     is reliable on the server thread.
   - Marks online=1 synchronously to close race condition.

Vyrriox
2026-03-26 18:05:12 +01:00
laforetbrut
e907bcbfb0 Security audit: fix 7 critical/high issues from code review
1. CRITICAL - Anti-dupe: Player inventory mutations now run on the main
   server thread via server.execute(). DB reads stay async, but all
   setItem/setHealth/addEffect calls happen on the tick thread.
   CountDownLatch ensures the lock is held until apply completes.

2. CRITICAL - Resource leaks: 3 QueryResults in PlayerSync.java startup
   now use try-with-resources + PreparedStatements instead of raw
   String.format SQL.

3. HIGH - Curios save: UPDATE changed to REPLACE INTO to prevent silent
   no-ops when the curios row doesn't exist yet (new player who died
   before first init save).

4. HIGH - RS2 restore: Removed skip-if-exists check. DB is always the
   source of truth - stale local data was persisting permanently.

5. HIGH - Race conditions: Shutdown save now acquires per-player lock.
   All logout saves (curios, mod-compat, inventory) moved inside
   doPlayerLogout under a single lock acquisition.

6. HIGH - SQL injection: DATABASE_NAME validated against [A-Za-z0-9_]+
   regex on startup to prevent injection via config.

Vyrriox
2026-03-26 17:34:36 +01:00
laforetbrut
46689a360c Fix advancements disappearing on server transfer
Minecraft only flushes PlayerAdvancements to disk during auto-save
(~every 5 min). If a player earns an advancement and switches servers
before the next auto-save, store() reads the stale file and the
advancement is lost in the DB.

Fix: call sp.getAdvancements().save() to force flush to disk before
reading the advancement file in store().

Vyrriox
2026-03-26 17:24:18 +01:00
laforetbrut
a85131708f Fix NeoForge attachment sync, kick system, and backpack upgrades
1. NeoForge attachments (SOL Onion, Ars Nouveau, etc.):
   - deserializeAttachments signature is (Provider, CompoundTag) not
     (CompoundTag) - reflection was failing silently, nothing restored
   - Use serializeAttachments(Provider) directly for saving instead of
     saveWithoutId() for cleaner approach
   - This fixes SOL Onion food diversity, Ars Nouveau mana/glyphs,
     Iron's Spellbooks, Pehkui scale, and all other NeoForge attachments

2. Multi-server kick:
   - Add secondary kick check in PlayerLoggedInEvent as fallback
   - Mark online=1 SYNCHRONOUSLY on login to close race condition
     where async doPlayerJoin hasn't set online=1 yet

3. Backpack upgrades:
   - Call refreshInventoryForInputOutput() before reading from
     BackpackStorage to flush pending wrapper changes

Vyrriox
2026-03-26 17:22:21 +01:00
laforetbrut
fc7d81f914 Fix Sophisticated Storage shulkers/chests/barrels losing contents on transfer
Root cause: Sophisticated Storage uses its own ItemContentsStorage
(SavedData) for packed items, NOT BackpackStorage from Sophisticated
Backpacks. The code was calling BackpackStorage which returned empty
data for storage items.

Fixes:
- Use ItemContentsStorage.get().getOrCreateStorageContents() for save
- Use ItemContentsStorage.get().setStorageContents() for restore
- Add extractStorageUuid() for "storageUuid" key (SS uses this, not
  "contentsUuid" which is for backpacks only)
- Try both UUID keys when scanning inventory items
- Add sophisticatedstorage as compileOnly dependency

Vyrriox
2026-03-26 17:12:29 +01:00
laforetbrut
0c7026aa65 Fix stale effects persisting on server transfer
Effects from the local server .dat file persisted when the player had
no effects saved in the DB. removeAllEffects() was only called inside
the if-block that checks for saved effect data, so it was skipped when
effectData was null/empty. Now effects are ALWAYS cleared before
restoring from DB.

SOL Onion food diversity is already synced via the generic NeoForge
attachment system (FoodPlayerData is a NeoForge attachment).

Vyrriox
2026-03-26 15:11:38 +01:00
laforetbrut
2e0269ee62 Add Refined Storage 2 disk sync + Extra Disks support
- Sync RS2 disk storage contents between servers (storageReference UUID)
- Support both refinedstorage and extradisks namespaces
- Save: extract individual entries from StorageRepository SavedData
- Restore: decode via RS2 codec and inject into target server repository
- Skip restore if storage already exists on target server (no overwrite)
- Scan inventory + ender chest for disks

Vyrriox
2026-03-26 15:07:28 +01:00
laforetbrut
87d320c1f4 Fix excessive thread creation (issue #169) - bounded thread pool
Replace unbounded CachedThreadPool with bounded ThreadPoolExecutor.

Problem: CachedThreadPool creates unlimited threads. With many players
online and slow DB queries, thread count explodes (25000+ threads
observed in issue #169), causing memory leaks and server crashes.

Fix: ThreadPoolExecutor with 2 core / 8 max threads, 30s keepalive,
256-task bounded queue, and CallerRunsPolicy for backpressure.
When the queue is full, tasks execute on the calling thread instead
of creating more threads, providing natural flow control.

Closes mlus-asuka/PlayerSync#169

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:51:09 +01:00
laforetbrut
f37e58be53 Add generic NeoForge attachment sync for full mod compatibility
Adds a generic system that syncs ALL NeoForge player attachments,
covering per-player data from every mod in the modpack:

- Ars Nouveau: mana, glyph/spell knowledge
- Iron's Spellbooks: mana, learned spells, cooldowns
- Pehkui: player scale
- Spice of Life: Onion: food diversity history
- And ANY other mod using NeoForge's attachment system

Implementation:
- Save: extracts neoforge:attachments tag from player.saveWithoutId()
- Restore: uses reflection to call NeoForge's deserializeAttachments()
  which ensures exact same deserialization path as normal player load
- Stored as BNBT in mod_player_data table (mod_id=neoforge_attachments)

Also verified CosmeticArmours (mod id: cosmeticarmoursmod) and
CosmeticWeapons (mod id: cosmeticweaponsmod) are content-only mods
that add craftable items - no custom player storage, fully handled
by existing inventory sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:43:42 +01:00