Commit Graph

191 Commits

Author SHA1 Message Date
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
laforetbrut
5576d7f7e2 Add anti-duplication locks, shutdown save, and security hardening
- Per-player ReentrantLock prevents concurrent save/restore operations,
  eliminating race conditions that could cause item duplication
- Save ALL online players on ServerStoppingEvent (before disconnect) to
  prevent data loss from server shutdowns/restarts
- Lock acquired before restore on join, released in finally block
- Lock acquired before save on logout, cleaned up after completion
- Verified compatibility with 430-mod Arcadia V2 modpack:
  - All item DataComponents from all mods preserved via BNBT serialization
  - Curios items (Artifacts, Elytra Slot, Charm of Undying, etc.) synced
  - Accessories items (Aether, Deep Aether) synced
  - Server-specific data (FTB Quests/Chunks, Waystones, Lootr) correctly
    NOT synced as intended

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:39:44 +01:00
laforetbrut
c63d5849a3 Add mod compatibility: Accessories (Aether), Cosmetic Armor, Apotheosis
- Add Accessories API sync for Aether mod accessory slots (pendant, cape,
  gloves, rings, shield, misc). Uses same pattern as Curios: validate data
  before clearing slots, PreparedStatements for DB operations
- Add Cosmetic Armor Reworked sync for 4 cosmetic armor slots via
  InventoryManager/CosArmorAPI
- Add Apotheosis + Placebo as compileOnly deps. Apotheosis item data
  (affixes, gems, sockets, rarity) travels with items via DataComponents
  and is already synced by the inventory sync
- New generic mod_player_data DB table with composite key (uuid, mod_id)
  for extensible mod-specific data storage
- Integrated save/restore in join, logout, and auto-save pipelines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:21:09 +01:00
laforetbrut
03b57c3e6b Fix critical sync bugs, security, and add Sophisticated Storage support
- Fix advancements disappearing: use PreparedStatements for all SQL with
  user data (advancement JSON contains chars that broke string-concat SQL),
  add null safety for advancement file
- Fix multi-server kick: run doPlayerConnect synchronously instead of async
  (players could join before the duplicate check completed)
- Fix Curios disappearing: clear slots AFTER validating data exists (not
  before), use CuriosCache for dead players on logout instead of empty API
- Fix Sophisticated Storage items: add storeSophisticatedStorageItems() and
  restoreSophisticatedStorageItems() to sync packed barrels/shulkers/chests
- Anti-duplication: clear all inventories before restoring from DB on join
- Fix tick counter: remove LevelTickEvent (fired per dimension = 3x too
  fast), merge heartbeat into ServerTickEvent
- Fix connection leaks: use try-with-resources for all QueryResult
- Fix logout order: save data BEFORE marking player offline
- Skip auto-save for dead/unsynced players to prevent saving empty data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:04:00 +01:00
mlus
148ac4db9b Revert "fallback BNBT, Compat with codec"
This reverts commit ce7004dba0.
2026-02-24 10:24:13 +08:00
mlus
ce7004dba0 fallback BNBT, Compat with codec 2026-02-24 10:20:27 +08:00
mlus
201e63a322 Fix SQL syntax by adding backticks around database and table names 2026-02-24 00:11:06 +08:00
mlus
4b39d52c12 Add binary NBT serialization and deserialization support to improve data handling 2026-02-23 23:53:41 +08:00
mlus
b6da709393 curios snbt clean 2026-02-23 22:33:13 +08:00
mlus
bc71c59a45
Change pokedex column type to MEDIUMBLOB 2026-02-21 16:14:56 +08:00
mlus
d0044fa824 snbt structure clean 2026-02-17 19:08:34 +08:00
mlus
d8c3bac31f new nbt serializer 2026-02-17 15:41:45 +08:00
mlus
0c51ad8eb7 try to fix component parse error 2026-02-17 14:27:45 +08:00
mlus
d4a2568f16 fix crash with too many cobblemon in pc 2026-02-08 22:21:37 +08:00
mlus
38c88a95f9 fix #151
fix #160
2026-01-14 20:26:26 +08:00
mlus
e373b8c226 add cobblemon support(in test) 2025-12-15 22:47:47 +08:00
mlus
72d0255d48 fix advancements sync 2025-11-30 01:57:26 +08:00
mlus
90742aafa0 fix backpack didn't sync 2025-11-30 00:50:55 +08:00
mlus
733f37cbb3 supplement 2025-11-28 22:15:29 +08:00
mlus
29da0f28ad recently commit port 2025-11-28 21:00:25 +08:00
mlus
b5e5f9fb65 line 155 make method return so curios data won't save at the first time 2025-10-14 16:56:47 +08:00
mlus
260b24975c heartbeat 2025-10-14 16:56:25 +08:00
mlus
62b0cf15bc chat sync reconnect system 2025-10-14 16:55:24 +08:00
dependabot[bot]
f06e6a6632 Bump gradle/actions from 4 to 5
Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-14 16:55:21 +08:00
mlus
07709ad792 may fix #111 2025-09-25 23:03:17 +08:00
mlus
fc043e1e03 half done, noticed about advancement can't store normally 2025-06-07 00:55:30 +08:00
mlus
3b442fbee1 not done yet 2025-06-06 21:17:22 +08:00
mlus
79e0e2cbe4 Fix error when no player in server 2025-06-06 13:09:32 +08:00
mlus
0562b01138 Full tested ChatSync Feature 2025-06-05 12:43:41 +08:00
mlus
74451eecff
Merge pull request #65 from EoD/1.20.4-neoforge
Add support for 1.20.4 using NeoForge 20.4.248
2025-05-05 02:53:30 +08:00
EoD
22b628bdcb fix sophisticated backpack integration for 1.20.4 2025-05-04 18:43:11 +00:00
EoD
9eb8bdc4a0 add support for Minecraft 1.20.4 with ModDevGradle 2025-05-04 18:43:11 +00:00