Skip to content

java.lang.RuntimeException: Sub-level assembly attempted inside plot of already removed sub-level #1376

Open

Description

@createmeow

Context

When it happened, I was trying to use Create: Aeronautics to turn the ship my friend and I built into a sub-level. The conversion process went pretty smoothly, but since the ship was originally built on the water and partly underwater, the underwater blocks got pulled into the sub-level too. Sponges can absorb these water blocks, which I actually did… then I saw the client reported the connection was terminated, and the server output a crash message...

From my experience, I think it's because:

  • Sable will try to remove any sub-level that only contains fluids (I'll call this α below)
  • It will split a sub-level that has no connecting blocks in the middle into two sub-levels (call this β below)
  • The sub-level’s physics simulation (call this γ below)
    conflicted with each other.

When I tried to drain our ship, the sub-level looked roughly like this:
boat
waterA <-HERE
waterB

I placed a sponge at '<-HERE>', absorbing waterA, so there were no connecting blocks between waterB and boat. β split waterB and the boat into two sub-levels (let’s call the separated waterB 'waterC', and the separated boat 'boat1'), and γ tried to simulate the physical state of waterC, but it had already been removed by α!

This text comes from Bing Translate. If you can't understand it, you can copy the untranslated original text to AI.

事情发生的时候,我正试着用 Create: Aeronautics 把我和朋友建的船改造成一个sub-level。转换过程进行得很顺利,但由于飞船最初建在海上且部分在水下,转换后船下水块也被一起带入sub-level了。海绵可以吸收这些水块,我实际上也这么做了……然后我看到客户端报告连接终止,服务器输出崩溃内容……

依照我的经验来推测,这是因为

  • sable会尝试移除所有仅包含流体的sub-level(在下文中称为α)
  • 将中间没有任何方块连接的sub-level拆分为两个sub-level(在下文中称为β)
  • sub-level的物理模拟(在下文中称为γ)
    功能冲突了,
    在我尝试为我们的船排水时,船的sub-level大致如下
    boat
    waterA <-HERE
    waterB
    我在“<-HERE”处放置了海绵,waterA被吸收了,所以waterB与boat之间没有任何方块连接,β将waterB和boat拆分成了两个sub-level(用waterC代指分离的waterB,boat1代指分离后的boat),γ试图模拟waterC的物理状态,但是其已经被α移除了!
Image

Crash Report

https://mclo.gs/K7Y37OX https://mclo.gs/iYAPkki

Activity

  1. createmeow commented on Jul 24, 2026

    @createmeow
    Author

    Similarly, when a player shoots a flame arrow at a sub-level that only has one TNT block, the game will also crash, because Sable tries to track a TNT sub-level that has already exploded.
    All of this is because the damn physics simulation didn't check if the sub-level still exists in the world!
    同样,当玩家用火焰箭射击一个只有一个TNT方块的物理结构时,游戏也会崩溃,因为Sable会尝试追踪一个已经爆炸的TNT物理结构。
    这一切都是该死的物理模拟没有效验物理结构是否依然存在于世界中

  2. JamesXNelson commented on Aug 30, 2026

    @JamesXNelson

    Additional Sable 2.0.5 case: split/removed-plot crash with a surviving airship later stranded in storage

    I have a real-world case with the exact exception in this issue, plus enough retained storage/log data to show what happened to the surviving airship afterward.

    Environment

    • Minecraft 1.21.1
    • NeoForge 21.1.249
    • Sable 2.0.5
    • Sable Companion 1.6.0
    • Create 6.0.10
    • Create Aeronautics 1.3.1
    • Create Gadgets & Gizmos 1.1.3
    • Create Propulsion: Simulated 1.1.5
    • Waystones 21.1.41
    • WaystonesSable 1.0.7
    • Single-player integrated server

    Structure setup

    The main structure was a large flying castle, eventually named castle.

    Separately, my kid found a random vanilla village hut, assembled the hut into its own Sable sublevel at the village, and later moved/placed that already-assembled hut sublevel inside the castle.

    The hut was not assembled from blocks while inside the castle.

    User-visible action around the later failure

    We remember breaking/deleting a Waystone immediately before one of the visible failures (the game crashed).

    I do not want to over-attribute that to Waystones. Minecraft does not log an ordinary block break, and the retained crash stack contains no Waystones frame. Breaking the Waystone may simply have changed block connectivity and caused Sable's heat-map/splitting logic to run against state that was already inconsistent.

    Important pre-existing-state caveat

    The world appears to have already had Sable trouble before the later hut/Waystone incident.

    In the earliest retained Aug 30 session, Sable logs:

    [02:49:56.464] [Render thread/ERROR] [dev.ryanhcode.sable.Sable/]:
    Received a sub-level movement packet for a non-existent sub-level
    

    Later in that same session, Sable repeatedly fails to restore the eventual castle UUID:

    [03:16:12.713] [Server thread/ERROR] [dev.ryanhcode.sable.Sable/]:
    Attempted to snatch sub-level with ID
    global->[chunkPos=[-41, 11], storageIndex=0, subLevelIndex=0]
    stored at 845625ba-98c8-461a-93b5-da56312c9109,
    but the requested sub-level wasn't present in the holding chunk
    

    WaystonesSable then reports that the same stored sublevel cannot be restored for a Waystone target when we tried teleporting back via a bound scroll.

    There was a separate Immersive-Aeronautics/portal experiment the previous night that crashed the world; that mod was removed afterward and is already absent from the retained 02:48+ mod lists. This is a plausible source of earlier bad state, but I do not have an earlier log proving that attribution.

    So the strongest claim here is: the world already contained at least one stale/non-live Sable state before the later crash.

    Exact Sable crash

    On Aug 30 at 09:34:21 the integrated server crashes with the same exception reported here:

    java.lang.RuntimeException: Sub-level assembly attempted inside plot of already removed sub-level
        at dev.ryanhcode.sable.api.SubLevelAssemblyHelper.assembleBlocks(SubLevelAssemblyHelper.java:85)
        at dev.ryanhcode.sable.sublevel.plot.heat.SubLevelHeatMapManager.split(SubLevelHeatMapManager.java:242)
        at dev.ryanhcode.sable.sublevel.plot.heat.SubLevelHeatMapManager.step(SubLevelHeatMapManager.java:118)
        at dev.ryanhcode.sable.sublevel.plot.heat.SubLevelHeatMapManager.tick(SubLevelHeatMapManager.java:85)
        at dev.ryanhcode.sable.sublevel.ServerSubLevel.tick(ServerSubLevel.java:235)
    

    A second crash at 10:03:05 has the same Sable split stack.

    Immediately before the 09:34 crash, the live/storage mismatch is already visible:

    [09:31:17.168] [Sable/ERROR]:
    Attempted to snatch sub-level with ID
    global->[chunkPos=[-13, 46], storageIndex=0, subLevelIndex=0]
    stored at 845625ba-98c8-461a-93b5-da56312c9109,
    but the requested sub-level wasn't present in the holding chunk
    
    [09:31:17.168] [WaystonesSable/WARN]:
    Sable did not load SubLevel 845625ba-98c8-461a-93b5-da56312c9109
    for target Waystone a41cd7eb-c0f1-49e0-ae1b-945f68376857 ...
    
    [09:33:34.873] [Sable/ERROR]:
    Received a sub-level movement packet for a non-existent sub-level
    

    Aftermath: the surviving castle becomes storage-only

    After restarting, the castle remained visible to /sable storage find_all_sub_levels, but normal live-sublevel commands/selectors could not target it.

    A later restart logs:

    [10:04:17.189] [Sable/ERROR]:
    Couldn't find sub-level at index 0 in storage file for chunk [9, 65]
    
    [10:04:17.189] [Sable/ERROR]:
    Due to a failed storage sub-level data load, we can't add a holding sub-level
    for pointer local->[storageIndex=0, subLevelIndex=0].
    This will cause issues later down the line.
    
    [10:04:17.191] [Sable/ERROR]:
    Sub-level dependency does not exist in chunk. Something has gone terribly wrong.
    

    That appears to connect this runtime split/remove failure directly to the storage/bootstrap failure tracked in #1387.

    Offline storage inspection and verified recovery

    I investigated the damaged save collaboratively with ChatGPT (GPT-5.6 Sol), which decoded the Sable .slvlr / .slvls storage; I verified the repair by loading the castle successfully in-game.

    Castle UUID:

    845625ba-98c8-461a-93b5-da56312c9109

    In the damaged storage we found:

    • the castle's actual .slvls block payload was intact and parsed cleanly;
    • the castle holding-chunk pointer list contained [0, 1];
    • slot 1 pointed to the intact castle;
    • slot 0 was empty/dead;
    • the castle's loading_dependencies referenced UUID 89ff0293-8292-4e32-8b0c-dbcd19584fad;
    • that UUID's payload still existed in another storage file and decoded as the small village-hut-like sublevel;
    • the two had mutual loading-dependency state.

    A minimal offline repair:

    1. removed the dead holding-chunk pointer,
    2. removed the stale/missing dependency from the castle, and
    3. zeroed the castle's saved velocity.

    The castle then instantiated normally, with its block payload intact.

    Why this seems useful for this issue

    This case suggests the already removed sub-level crash can have a persistent consequence:

    1. a sublevel/plot becomes removed or invalidated;
    2. SubLevelHeatMapManager.split() still attempts to assemble against that removed plot;
    3. the server crashes;
    4. pointer/dependency cleanup is not left in a self-consistent state;
    5. a surviving large sublevel later exists on disk but cannot bootstrap into the live world.

    That makes this look related to #1387 rather than only a transient crash.

    Evidence

    I have attached one ZIP containing:

    • retained logs 2026-08-30-1.log.gz through 2026-08-30-6.log.gz;
    • the two relevant server crash reports;
    • a concise evidence/timeline summary.

    sable-issue-evidence-bundle.zip

    I can also provide the damaged Sable sublevels/ directory and the successfully repaired version if that is useful for a regression fixture.

    For a future test, I plan to focus first on Sable alone: trigger connectivity splitting while a related sublevel/plot is removed, assert that the split path never assembles against an already-removed plot, then unload/reload and verify no dead pointer or stale dependency survives.

  3. Petroid2470 commented on Oct 5, 2026

    @Petroid2470

    I think i hit the same crash, happened on dedicated server then i was able to reproduce it in singleplayer

    Minecraft 1.21.1
    Neoforge 21.1.230
    Sable 2.0.5

    Crash Report:
    https://mclo.gs/zANiDCR

    Video of how i caused the crash:
    https://youtu.be/IYgkKwOptVg

  4. anenigmabeyondwords commented on Oct 12, 2026

    @anenigmabeyondwords

    A deterministic reproduction on Sable 2.0.6 (NeoForge 21.1.248, dedicated server), and the cause.

    Cause. SubLevelHeatMapManager.split() assembles the cut-off pieces one after another. Moving a piece's blocks out of the parent is a block change on the parent. If what is left of the parent has no mass (only blocks without a collision shape), SubLevelPhysicsSystem destroys the parent's remaining blocks and marks it removed, inside that loop. The next iteration's assembleBlocks then finds its containing sub-level removed and throws. On our server the shutdown save then failed as well (Body has been removed from SubLevelSerializer.serialize), so the level was not saved and the JVM stayed up without a server thread until the watchdog killed it.

    Reproduction (any world, area force-loaded; minecraft:light has no mass but is not air):

    setblock 0 150 60 minecraft:light
    setblock 1 150 60 minecraft:light
    setblock 2 150 60 minecraft:stone
    setblock 3 150 60 minecraft:light
    setblock 1 150 61 minecraft:stone
    setblock 1 150 62 minecraft:stone
    sable assemble area 0 150 60 3 150 62
    # first sub-level of a fresh world: the anchor (0,150,60) lands on the plot centre block (20481032, 128, 20481032)
    fill 20481033 128 20481032 20481034 128 20481033 minecraft:air replace minecraft:stone
    

    The /fill removes both stone connectors in one command, leaving two pieces: a light block and a stone block. In this order the stone piece is assembled first, the parent keeps only light blocks, is removed, and the light piece's assembly crashes (split:242 <- step:187). With the two pieces swapped (the light one assembled first, dropped as zero mass) there is no crash.

    Fix we run (as a bytecode patch; the source equivalent):

    for (final List<BlockPos> blocks : newSubLevelBlocks.values()) {
        if (this.subLevel.isRemoved()) break;
        ...

    Nothing is lost by stopping: a parent is only marked removed here after its plot's blocks were destroyed. With it, the crash order above ends with one sub-level (the stone piece) at 20 TPS, and a plain three-stone bar cut in the middle still splits into two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: crashSomething crashes my game

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions