File Details
Locks Reforged 1.6.4 (Forge 1.20.1)
- R
- Jun 29, 2026
- 668.38 KB
- 437
- 1.20.1
- Forge
File Name
locks_reforged-1.6.4.jar
Supported Versions
- 1.20.1
Curse Maven Snippet
1.6.4
C2ME / Async Chunk — the world-load HANG (re-entrant blocking chunk fetch)
- Fixed a hard hang (no crash, no exception — the game freezes during initial world generation) under C2ME. The Server thread, while blocked awaiting a spawn chunk, is made by C2ME's
beforeAwaitChunkredirect to drain the chunk executor (managedBlock); a chunk-load completion firesChunkEvent.Loadre-entrantly on that same thread. OuronChunkLoadhandler ran inline (it was already "on the server thread") and called a blockingLevel#getChunk(x, z)to re-fetch the chunk — which re-parks on the very chunk future the thread is mid-completing. Permanent self-deadlock. onChunkLoadnow uses the liveLevelChunkthe event already provides (the blocking re-fetch was redundant) and keeps only a non-blockinghasChunkstaleness guard.registerChunkStorageis a leaf-mutexmap put with no chunk fetch, so running it inline during the drain is safe.- Hardened
LocksThreadUtil.runOnServerThreadwith a hard contract that deferred work must never perform a blocking chunk fetch, and made theserver == nullbranch degrade safely (run inline only on the owning thread, otherwise drop + warn-once) instead of mutating the global handler off-thread. - Replaced every
hasChunk-guarded blockinggetChunk/getChunkAtinLockableHandler(add,addDirect,getInChunk,markDirty,update) with the non-blockinggetChunkSource().getChunkNow(x, z). Behaviour-preserving on the main thread; removes the same latent re-entrant-hang surface (including the path reached byStructureTemplate#placeInWorld's deferredadd).
C2ME correctness hardening
- Border-spanning generated locks no longer silently dropped. When
LockChestsFeatureadds a lock to a neighbour the worldgen region returns as an already-finishedImposterProtoChunk, the lock previously landed in an inheritedProtoChunklist that nothing drains. It is now written through to the wrappedLevelChunk's storage and registered with the world handler on the main thread. - Per-chunk
LockableStorageis now thread-safe. Its fastutil map is written on C2ME worker threads (deserializeNBT, theLevelChunkdrain) and read on the main thread; all mutations and a newsnapshot()copy are guarded by the storage monitor, giving an explicit happens-before edge (used byregisterChunkStorageandChunkMapMixin). - Added a one-time diagnostic warning if
nextId()allocates off-thread before the persisted counter is bootstrapped (initIds), so any future ordering regression is observable; the existingadvanceLastIdbackstop still reconciles ids on chunk load.
Build
- Fixed the recurring mislabeled jar bug:
processResourcesdid not trackmod_versionas an input, so it stayed UP-TO-DATE and baked a stale version intomods.tomleven as the jar filename updated (e.g. a "1.6.3" jar internally reporting 1.6.2). Addedinputs.property 'mod_version'so the embedded version always matches the release.
1.6.3
C2ME / Async Chunk — the remaining crash (off-thread read of the handler map)
- Fixed an
ArrayIndexOutOfBoundsException(sameIndex -1signature as the 1.6.2 crash, now on the read side) that could still occur under C2ME.StructureTemplate#fillFromWorld(our mixin) runs on a C2ME worker thread and was copying the world-globalLockableHandler.lockablesmap (new ArrayList<>(handler.getLoaded().values())). The 1.6.x fixes serialized all writes to that map onto the main thread, but building a snapshot of a fastutil open-addressing map while the main thread rehashes it still reads a torn backing array — the copy itself crashes. A plain snapshot does not make a concurrent read safe. LockableHandlernow guards every structural mutation of its map with an internal monitor and exposessnapshotLoaded(), a synchronized copy used byfillFromWorld(and the Respawning Structures compat). No behavior change on the main thread or without C2ME; we only serialize the rare worker-thread read against main-thread writes.- Also closed a lower-probability worldgen race: a border-spanning generated lock is appended to neighbouring
ProtoChunklock lists fromLockChestsFeatureon worldgen worker threads. That list is now synchronized and is drained under its monitor when the chunk converts to aLevelChunk.
Ghost-locked doors — a door could stay blocked with no visible, pickable lock
- Root cause: two stores must mirror each other — the per-chunk
LockableStorage(read by door-blocking viagetInChunk) and the world-globalLockableHandlerindex (read by rendering and the lock-picking minigame, including the client). When they diverged (e.g. a missed sync under async chunk loading), a door blocked authoritatively while its lock was invisible and could not be picked — and force-loading the chunk didn't help because the chunk was already loaded. - Self-healing reconcile at interaction time (server-authoritative). When a player interacts with a block that has lockables, the server now re-registers that chunk's storage into the world index (idempotent, no extra packets) so every lock that can block a door is the canonical, observed instance — guaranteeing its lock/unlock changes persist and sync. When a lock actually blocks the interaction, the server also pushes it to the interacting player so a desynced or missing client copy re-renders and becomes pickable. A door can no longer be permanently blocked by a lock the player cannot see or pick.
- Lock-picking no longer dead-ends on a sync race. The open-screen packet now carries the full lockable instead of just its id, so the client reconstructs it directly instead of failing with "Lockable not found" when its loaded map hasn't caught up. The pin order stays server-authoritative (the network form is lossy on it). The client still prefers its already-loaded instance when present.
No more resurrected / duplicate locks across chunk reloads
LockableHandler.removepreviously skipped chunks of a multi-chunk lockable that were unloaded, leaving a stale copy on disk thatregisterChunkStorageresurrected as canonical on reload (a removed lock reappearing, or a duplicate after a structure respawn). On the server it now force-loads each chunk the lockable spans (they already exist on disk — this loads, it does not generate) and clears the lock from all of them. The client path stays best-effort and never force-loads.
Respawning Structures — preserved and hardened
- No behavioral change to the respawn re-lock flow; it still runs on the server thread and reuses the exact world-generation rules (
LocksUtil.createChestLockable). It now benefits directly from the fixes above: the stale-lock clear uses the thread-safesnapshotLoaded(), andremoveclearing every chunk copy means repeated respawns can't accumulate duplicate locks.
Tests
- Added a JUnit source set (
src/test) covering the bootstrap-free pure logic:Cuboid6igeometry and chunk-span math (including negative coordinates and border spanning),Lockcombo/locked NBT round-trips plus the seed-regeneration backward-compat path, andLockableHandler's id-counter monotonicity (the guarantee that prevents id-reuse resurrection). Run with./gradlew test.