I'm building a mod on top of Sable's sub-level API and ran into a reproducible hard crash. I've narrowed it down pretty far and I'm fairly sure the failing path is inside Sable's native collider handling rather than my code — reasoning below, and there are already several open issues that look like the same thing reproduced with no custom mod at all.
Env: Sable 1.2.2 · MC 1.21.1 · NeoForge 21.1.x · Create + Create Aeronautics + Create Simulated (rope)
What happens
My mod programmatically changes a block inside a sub-level (an ordinary setBlock-style edit — it goes through RapierPhysicsPipeline.handleBlockChange → Rapier3D.changeBlock, which rebakes that sub-level's voxel collider). If that sub-level is joined to another one by a Create Simulated rope — e.g. a hot-air balloon, base sub-level + envelope sub-level tied together by a rope — the next Rapier3D.step() hard-aborts the JVM with a non-unwinding Rust panic:
thread '<unnamed>' panicked at rapier3d/.../src/geometry/narrow_phase.rs:1115:33:
No element at index
thread '<unnamed>' panicked at library/core/src/panicking.rs:225:5:
panic in a function that cannot unwind
Sometimes it surfaces (same cause, different timing) as:
panicked at rapier/src/hooks.rs:30:17: No collider B!
panicked at rapier/src/hooks.rs:240:14: called `Result::unwrap()` on an `Err` value: JavaException
It crosses the JNI boundary as a non-unwinding panic → process::abort(), so it can't be caught or logged from Java.
The isolating fact
This is not a physics/collision problem. Without my mod installed, you can ram that same balloon together as hard as you want and it never crashes — Sable handles the rope plus the collision perfectly fine. The crash only appears when a block in a rope-attached sub-level is changed and its collider gets rebaked. The exact same block change on a sub-level with no rope attached also doesn't crash. So the trigger isn't the rope, and isn't the collision — it's specifically rebaking a sub-level's voxel collider while a rope is keeping that sub-level in a live contact pair.
Minimal repro
- Two sub-levels joined by a Create Simulated rope (hot-air balloon base + envelope).
- From anything — a mod, a command, a Create drill — change or remove a block in one of the rope-attached sub-levels (anything that reaches
Rapier3D.changeBlock / forces the voxel rebake).
- Next physics step →
narrow_phase.rs:1115 abort. Deterministic, within one or two changes.
What I think is going on
The voxel-collider rebuild in the changeBlock path (and Rapier3D.removeSubLevel) frees/replaces the sub-level's collider but doesn't remove it from Rapier's narrow-phase contact graph — the Coarena that narrow_phase.rs:1115 indexes into. Normally a freed collider would just fall out of contact on the next step, but the rope (setRopeAttachment) holds that sub-level in an active contact pair, so a stale ColliderHandle survives into the next step() and the lookup panics. It looks like the same family as the existing "sub-level removed mid-substep → narrow_phase.rs:1115" reports; the rope just makes it trivially deterministic. I can't see the native source so I can't confirm the precise internal mechanism, but every isolating fact above points at the rebake not purging the contact graph.
Why I don't think it's fixable downstream
Rapier3D.removeSubLevel is the only collider/body removal primitive exposed, and Sable's own SubLevelPhysicsSystem.recoverSubLevel goes through it — so even forcing a full clean rebuild in the safe pre-step window inherits the same gap and still crashes. Nothing on the public API purges the stale narrow-phase contact pair, so I don't think a mod can paper over this; the cleanup has to happen where the collider is freed.
Suggested direction
When a sub-level collider is removed or rebuilt — both in removeSubLevel and in the changeBlock rebake path — also drop the affected collider(s) from the NarrowPhase (and from any rope/joint attachment) before the next step, e.g. collider_set.remove(handle, &mut islands, &mut bodies, &mut joints, true) / narrow_phase.remove_collider(...), so no stale handle survives into narrow_phase.rs:1115. You'll obviously know the native layout better than I can infer it from stack traces.
This already reproduces with no custom mod
These open issues look like exactly this path, and several of them involve no custom mod — just vanilla Create changing a block on a rope-pulley contraption:
#641 and #914 in particular are this bug with zero custom code involved — a Create drill mining/touching a block on a rope-attached contraption rebakes its collider and hits the same crash. Cross-referencing so this can be deduped/linked rather than treated as five separate reports.
Game log
It's a native process::abort(), so the JVM-side latest.log contains nothing beyond the panic lines quoted above. I have full per-run logs and crash reports and can put a specific run on mclo.gs if you want one for a particular trace.
I'm building a mod on top of Sable's sub-level API and ran into a reproducible hard crash. I've narrowed it down pretty far and I'm fairly sure the failing path is inside Sable's native collider handling rather than my code — reasoning below, and there are already several open issues that look like the same thing reproduced with no custom mod at all.
Env: Sable 1.2.2 · MC 1.21.1 · NeoForge 21.1.x · Create + Create Aeronautics + Create Simulated (rope)
What happens
My mod programmatically changes a block inside a sub-level (an ordinary
setBlock-style edit — it goes throughRapierPhysicsPipeline.handleBlockChange→Rapier3D.changeBlock, which rebakes that sub-level's voxel collider). If that sub-level is joined to another one by a Create Simulated rope — e.g. a hot-air balloon, base sub-level + envelope sub-level tied together by a rope — the nextRapier3D.step()hard-aborts the JVM with a non-unwinding Rust panic:Sometimes it surfaces (same cause, different timing) as:
It crosses the JNI boundary as a non-unwinding panic →
process::abort(), so it can't be caught or logged from Java.The isolating fact
This is not a physics/collision problem. Without my mod installed, you can ram that same balloon together as hard as you want and it never crashes — Sable handles the rope plus the collision perfectly fine. The crash only appears when a block in a rope-attached sub-level is changed and its collider gets rebaked. The exact same block change on a sub-level with no rope attached also doesn't crash. So the trigger isn't the rope, and isn't the collision — it's specifically rebaking a sub-level's voxel collider while a rope is keeping that sub-level in a live contact pair.
Minimal repro
Rapier3D.changeBlock/ forces the voxel rebake).narrow_phase.rs:1115abort. Deterministic, within one or two changes.What I think is going on
The voxel-collider rebuild in the
changeBlockpath (andRapier3D.removeSubLevel) frees/replaces the sub-level's collider but doesn't remove it from Rapier's narrow-phase contact graph — theCoarenathatnarrow_phase.rs:1115indexes into. Normally a freed collider would just fall out of contact on the next step, but the rope (setRopeAttachment) holds that sub-level in an active contact pair, so a staleColliderHandlesurvives into the nextstep()and the lookup panics. It looks like the same family as the existing "sub-level removed mid-substep → narrow_phase.rs:1115" reports; the rope just makes it trivially deterministic. I can't see the native source so I can't confirm the precise internal mechanism, but every isolating fact above points at the rebake not purging the contact graph.Why I don't think it's fixable downstream
Rapier3D.removeSubLevelis the only collider/body removal primitive exposed, and Sable's ownSubLevelPhysicsSystem.recoverSubLevelgoes through it — so even forcing a full clean rebuild in the safe pre-step window inherits the same gap and still crashes. Nothing on the public API purges the stale narrow-phase contact pair, so I don't think a mod can paper over this; the cleanup has to happen where the collider is freed.Suggested direction
When a sub-level collider is removed or rebuilt — both in
removeSubLeveland in thechangeBlockrebake path — also drop the affected collider(s) from theNarrowPhase(and from any rope/joint attachment) before the nextstep, e.g.collider_set.remove(handle, &mut islands, &mut bodies, &mut joints, true)/narrow_phase.remove_collider(...), so no stale handle survives intonarrow_phase.rs:1115. You'll obviously know the native layout better than I can infer it from stack traces.This already reproduces with no custom mod
These open issues look like exactly this path, and several of them involve no custom mod — just vanilla Create changing a block on a rope-pulley contraption:
#641 and #914 in particular are this bug with zero custom code involved — a Create drill mining/touching a block on a rope-attached contraption rebakes its collider and hits the same crash. Cross-referencing so this can be deduped/linked rather than treated as five separate reports.
Game log
It's a native
process::abort(), so the JVM-sidelatest.logcontains nothing beyond the panic lines quoted above. I have full per-run logs and crash reports and can put a specific run on mclo.gs if you want one for a particular trace.