Skip to content

Failed to load saved sub-level: Plot already exists at 0, 36 #1626

Open

Description

@wannholtemil-png

Context

I'm getting a persistent warning from Sable when loading my Minecraft 1.21.1 world. The world loads successfully, and my moving structures appear to work normally, but Sable reports that one saved sub-level could not be loaded.

The affected sub-level is my tank, which uses Create Tracks+. I added Create Tracks+ after the world was originally created.

The relevant error in latest.log is:

[Server thread/ERROR] [dev.ryanhcode.sable.Sable/]: Failed to load sub-level HalfLoadedSublevel

java.lang.IllegalArgumentException: Plot already exists at 0, 36

Sable then reports that it failed to load the holding sub-level and skips it.

Sub-level UUID: d3972b5e-5fdb-432c-ab25-4e4ddc6e69e9

Position: 1650.894, 84.967, 6556.689

Environment

Minecraft 1.21.1
NeoForge 21.1.251
Sable 2.0.6
Create 6.0.10
Create Aeronautics 1.3.2
Create Tracks+ 1.0.5
Distant Horizons 3.3.3
Chunky 1.4.23

Disabling Chunky resolved a separate crash during world loading, but the Sable warning persisted. The tank and my other moving structures still appear to work normally despite the warning.

Could this be a duplicate plot allocation or a problem with loading saved sub-level data? Could adding Create Tracks+ after world creation be related?

I have kept a backup of the world and have not manually modified or deleted any Sable storage files.

Crash Report

[12okt.2026 21:44:50.129] [Server thread/ERROR] [dev.ryanhcode.sable.Sable/]: Failed to load sub-level HalfLoadedSublevel[uuid=d3972b5e-5fdb-432c-ab25-4e4ddc6e69e9, bounds=dev.ryanhcode.sable.companion.math.BoundingBox3d@4b03b4db, pose=Pose3d{position=(1 650,894 84,967 6 556,689), orientation=(−0,015 0,16 0,03 0,987), rotationPoint=(20 481 034,25 224,671 20 554 760,549), scale=(1 1 1)}, relations=[]], skipping java.lang.IllegalArgumentException: Plot already exists at 0, 36 at TRANSFORMER/sable@2.0.6/dev.ryanhcode.sable.api.sublevel.SubLevelContainer.allocateSubLevel(SubLevelContainer.java:249) ~[sable-neoforge-1.21.1-2.0.6.jar%23251!/:2.0.6] at TRANSFORMER/sable@2.0.6/dev.ryanhcode.sable.sublevel.storage.serialization.SubLevelSerializer.fullyLoad(SubLevelSerializer.java:157) ~[sable-neoforge-1.21.1-2.0.6.jar%23251!/:2.0.6] at TRANSFORMER/sable@2.0.6/dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap.loadHoldingSubLevel(SubLevelHoldingChunkMap.java:565) ~[sable-neoforge-1.21.1-2.0.6.jar%23251!/:2.0.6] at TRANSFORMER/sable@2.0.6/dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap.processChanges(SubLevelHoldingChunkMap.java:559) ~[sable-neoforge-1.21.1-2.0.6.jar%23251!/:2.0.6] [12okt.2026 21:44:50.133] [Server thread/INFO] [dev.ryanhcode.sable.Sable/]: Failed to load holding sub-level HoldingSubLevel[data=HalfLoadedSublevel[uuid=d3972b5e-5fdb-432c-ab25-4e4ddc6e69e9, bounds=dev.ryanhcode.sable.companion.math.BoundingBox3d@4b03b4db, pose=Pose3d{position=(1 650,894 84,967 6 556,689), orientation=(−0,015 0,16 0,03 0,987), rotationPoint=(20 481 034,25 224,671 20 554 760,549), scale=(1 1 1)}, relations=[]], pointer=global->[chunkPos=[105, 400], storageIndex=0, subLevelIndex=3]] with pointer global->[chunkPos=[105, 400], storageIndex=0, subLevelIndex=3]. This is a problem.

Activity

  1. anenigmabeyondwords commented on Oct 13, 2026

    @anenigmabeyondwords

    We hit the same Plot already exists on Sable 2.0.6 (NeoForge 21.1.248, MC 1.21.1, Create Aeronautics 1.3.2) and traced one cause that produces it. (We run one local bytecode patch, the #1627 split fix in SubLevelHeatMapManager; every storage and plot class is stock.)

    What we see

    • 688 saved overworld sub-levels sit in 654 plots; 10 plots are claimed by 2 to 10 saved records each. No UUID appears twice.
    • Each of those 10 plots contains exactly one sub-level built only from blocks of a mod we later removed (Domum Ornamentum), and every such sub-level is in one of those plots.
    • Every boot logs Recoverable errors when loading plot section ... Unknown registry key ... using default, then Failed to load sub-level, invalid plot bounds: EMPTY for those records. IllegalArgumentException: Plot already exists at x, z follows whenever two of the other claimants load together, and one of them never loads.

    Cause

    1. When a record loads with no non-air blocks, fullyLoad calls removeSubLevel(subLevel, REMOVED) (
      if (plot.getBoundingBox() == BoundingBox3i.EMPTY || plot.getBoundingBox().volume() <= 0) {
      Sable.LOGGER.error("Failed to load sub-level, invalid plot bounds: {}", plot.getBoundingBox() == BoundingBox3i.EMPTY ? "EMPTY" : plot.getBoundingBox());
      plotContainer.removeSubLevel(subLevel, SubLevelRemovalReason.REMOVED);
      return null;
      }
      ).
    2. REMOVED clears the plot's occupancy bit and marks it dirty (
      if (reason == SubLevelRemovalReason.REMOVED) {
      this.getOccupancy().clear(index);
      }
      ). The record itself is never deleted: lastSerializationPointer is still null at that point (it is only set after fullyLoad succeeds,
      public void loadHoldingSubLevel(final HoldingSubLevel holdingSubLevel) {
      final ServerSubLevel subLevel = SubLevelSerializer.fullyLoad(this.level, holdingSubLevel.data());
      if (subLevel != null) {
      subLevel.setLastSerializationPointer(holdingSubLevel.pointer());
      } else {
      final MinecraftServer server = this.level.getServer();
      if (server instanceof final SableToastableServer toastable) {
      toastable.sable$reportSubLevelLoadFailure(holdingSubLevel.pointer());
      }
      Sable.LOGGER.info("Failed to load holding sub-level {} with pointer {}. This is a problem.", holdingSubLevel, holdingSubLevel.pointer());
      }
      this.allHoldingSubLevels.remove(holdingSubLevel.data().uuid());
      }
      ), so queueDeletion does nothing.
    3. The record keeps its plot on disk while the occupancy file says the plot is free, and the next allocateNewSubLevel (first free plot,
      private Vector2i getFirstEmptyPlot() {
      for (int x = 0; x < (1 << this.logSideLength); x++) {
      for (int z = 0; z < (1 << this.logSideLength); z++) {
      if (!this.occupancy.get(this.getIndex(x, z))) {
      return new Vector2i(x, z);
      }
      }
      }
      return null;
      }
      ) hands it out.
    4. On every later restart the record loads again. allocateSubLevel only rejects a plot that has a loaded sub-level in it and never checks occupancy (
      public SubLevel allocateSubLevel(final UUID uuid, final int x, final int z, final Pose3d pose) {
      if (this.getLocalPlot(x, z) != null) {
      throw new IllegalArgumentException("Plot already exists at " + x + ", " + z);
      }
      ), so the record takes the plot from an unloaded owner, fails again, and frees it again.
    5. Each cycle adds one more claimant; our worst plot has 10.

    Repro

    1. Assemble a one-block sub-level from a mod block near spawn, stop, remove the mod, start. The log shows invalid plot bounds: EMPTY.
    2. Assemble a vanilla sub-level C: it gets the same plot. Let C unload and restart.
    3. Assemble D: it also gets that plot. Load C and D together: Plot already exists.

    Possible fix

    • On a failed load, release the sub-level without clearing occupancy (or also delete the record through the holding pointer), so the disk and the occupancy data agree.
    • Longer term, store the owning UUID per plot so a load can tell its own plot from another sub-level's and move the newcomer to a free plot instead of refusing to load.

    Unchanged on main as of d501fb3. For the original report: it may be worth checking the log for invalid plot bounds: EMPTY earlier in the same boot.

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