Description
IllegalStateException: Recursive update thrown in ConcurrentHashMap.computeIfAbsent inside RapierVoxelColliderBakery.getPhysicsDataForBlock() during chunk post-processing generation. Sables physics handler re-enters itself: a block change triggers physics computation, which calls getCollisionShape(), which loads a chunk, which triggers postProcessGeneration, which calls setBlock(), which fires back into Sables block change handler.
Stack Trace (top)
java.lang.IllegalStateException: Recursive update
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1742)
at net.minecraft.Util$8.apply(Util.java:795)
at dev.ryanhcode.sable.physics.impl.rapier.collider.RapierVoxelColliderBakery.getPhysicsDataForBlock(RapierVoxelColliderBakery.java:98)
at dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.handleBlockChange(RapierPhysicsPipeline.java:513)
at dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem.handleBlockChange(SubLevelPhysicsSystem.java:451)
at dev.ryanhcode.sable.SableCommonEvents.handleBlockChange(SableCommonEvents.java:91)
at net.minecraft.world.level.chunk.LevelChunk.wrapOperation$geo000$sable$setBlockState(LevelChunk.java:4802)
at net.minecraft.world.level.chunk.LevelChunk.setBlockState(LevelChunk.java:249)
at net.minecraft.world.level.Level.setBlock(Level.java:236)
at net.minecraft.world.level.Level.setBlock(Level.java:212)
at net.minecraft.world.level.chunk.LevelChunk.postProcessGeneration(LevelChunk.java:547)
at net.minecraft.server.level.ChunkMap.lambda$prepareTickingChunk$27(ChunkMap.java:678)
at net.minecraft.server.level.ChunkResult$Success.ifSuccess(ChunkResult.java:60)
at net.minecraft.server.level.ChunkMap.lambda$prepareTickingChunk$28(ChunkMap.java:677)
Recursive chain
ChunkMap.prepareTickingChunk -> LevelChunk.postProcessGeneration -> Level.setBlock()
- Sable mixin intercepts via
wrapOperation$geo000$sable$setBlockState -> SableCommonEvents.handleBlockChange()
RapierPhysicsPipeline.handleBlockChange() -> RapierVoxelColliderBakery.getPhysicsDataForBlock() -> ConcurrentHashMap.computeIfAbsent()
- Inside the lambda:
buildPhysicsDataForBlock() -> BlockStateBase.getCollisionShape()
- ShetiPhianCore handler ->
LevelAccelerator.getBlockState() -> Level.getChunk() -> loads chunk
- Chunk load fires
prepareTickingChunk again -> back to step 1
ConcurrentHashMap.computeIfAbsent in step 3 detects recursion -> throws IllegalStateException
Environment
- Sable version: 1.2.2 (neoforge-1.21.1)
- Minecraft: 1.21.1
- Modloader: NeoForge 21.1.230
- Dimension: twilightforest:twilight_forest
- Crash UUID: 79f9e0a9-bdf4-47c7-a1e9-68cd3b0cc7b1
Impact
- Server crash with world ticking exception (no graceful recovery)
- Chunks [947, 384], [947, 385], [947, 386] failed to save during shutdown
- Server process hung in shutdown state indefinitely
Reproduction
A player logged into the Twilight Forest at coordinates (14886, 23, 5922). The crash occurred approximately 8 seconds after login during chunk post-processing, likely as new chunks were generated or loaded around the player. Corrupted Sable UDP snapshot packets were also seen from the players client around the same time.
Suggested fix
RapierVoxelColliderBakery.getPhysicsDataForBlock() at line 98 needs a reentrancy guard - either:
- Cache the currently-computing block and return a default/fallback physics shape instead of recursing
- Defer the physics computation outside the
computeIfAbsent lock (e.g., compute ahead of time, or use a separate lock that allows reentrancy)
Description
IllegalStateException: Recursive updatethrown inConcurrentHashMap.computeIfAbsentinsideRapierVoxelColliderBakery.getPhysicsDataForBlock()during chunk post-processing generation. Sables physics handler re-enters itself: a block change triggers physics computation, which callsgetCollisionShape(), which loads a chunk, which triggerspostProcessGeneration, which callssetBlock(), which fires back into Sables block change handler.Stack Trace (top)
Recursive chain
ChunkMap.prepareTickingChunk->LevelChunk.postProcessGeneration->Level.setBlock()wrapOperation$geo000$sable$setBlockState->SableCommonEvents.handleBlockChange()RapierPhysicsPipeline.handleBlockChange()->RapierVoxelColliderBakery.getPhysicsDataForBlock()->ConcurrentHashMap.computeIfAbsent()buildPhysicsDataForBlock()->BlockStateBase.getCollisionShape()LevelAccelerator.getBlockState()->Level.getChunk()-> loads chunkprepareTickingChunkagain -> back to step 1ConcurrentHashMap.computeIfAbsentin step 3 detects recursion -> throwsIllegalStateExceptionEnvironment
Impact
Reproduction
A player logged into the Twilight Forest at coordinates (14886, 23, 5922). The crash occurred approximately 8 seconds after login during chunk post-processing, likely as new chunks were generated or loaded around the player. Corrupted Sable UDP snapshot packets were also seen from the players client around the same time.
Suggested fix
RapierVoxelColliderBakery.getPhysicsDataForBlock()at line 98 needs a reentrancy guard - either:computeIfAbsentlock (e.g., compute ahead of time, or use a separate lock that allows reentrancy)