Skip to content

Schematic And Quill captures the entire parent sub-level when selecting a nearby child sub-level #1604

Open

Description

@juanbaiing
Sable 2.0.5 (sable-neoforge-1.21.1-2.0.5.jar)
Create 6.0.10
Create Aeronautics 1.3.2 (bundled)
Minecraft 1.21.1
NeoForge 21.1.248
Java 21.0.11
Side Both (reproduced by a player using Schematic And Quill on a server)
Other mods involved sablecleanup 0.3.0, create_connected 1.3.3, copycats 3.0.9

Summary

When using Create's Schematic And Quill to select a child sub-level that has been detached from a parent sub-level, the parent sub-level is also captured in the saved schematic — as long as the child is within some distance of the parent.

This happens regardless of whether the structures are physics-paused or physics-active.

Impact

  • A schematic of a single small ship came out at ~730 KB because a neighbouring 169-chunk floating island was included.
  • The resulting schematic cannot be used as intended (it re-places the whole island).

Steps to reproduce

  1. Build a large structure and assemble it as a Sable sub-level (the "parent" — in our case a floating island, ~169 chunks).
  2. Detach / assemble a smaller structure as a separate sub-level nearby (the "child" — a ship).
  3. Take a Schematic And Quill and select an area that only covers the child.
  4. Save the schematic.
  5. Inspect the resulting .nbt — it contains both the child and the parent.

Notes:

  • The two structures are separate sub-levels (they appear separately in sablecleanup's sub-level list, with different UUIDs).
  • Pausing physics (/sable physics paused) does not change the outcome.
  • Picking the two corners very precisely does not help.

Evidence from the saved schematic NBT

The saved file (tajian.nbt, 747,845 bytes gzipped, 9.7 MB decompressed) begins with:

size
  └── sub_levels          <-- embedded sub-level data
        ├── size
        ├── uuid
        └── blocks
              ├── state
              └── pos   (repeated for a very large number of blocks)
      ...
palette
entities
DataVersion

So the schematic carries an embedded sub_levels payload far larger than the selected area.

Code path (decompiled from sable-neoforge-1.21.1-2.0.5.jar)

StructureTemplateMixin.fillEntityList is where the entity + sub-level list is pruned to the schematic's bounds:

private void fillEntityList(Level, BlockPos, BlockPos, CallbackInfo, List<Entity>);
   0: getstatic     Sable.HELPER : ActiveSableCompanion
   5: aload_1                                  // level
   7: aload_2                                  // block pos
   9: invokevirtual ActiveSableCompanion.getContaining(Level, Vec3i) : SubLevel
  12: astore        7                           // <-- the sub-level "owning" that position
  14: aload         5                           // List<Entity>
  16: aload         6
  18: aload         7
  20: invokedynamic  test:(ActiveSableCompanion, SubLevel)Predicate
  25: invokeinterface List.removeIf(Predicate)

and the predicate:

private static boolean lambda$fillEntityList$0(ActiveSableCompanion, SubLevel, Entity);
   0: aload_0
   2: aload_2
   4: invokevirtual ActiveSableCompanion.getContaining(Entity) : SubLevel
   7: astore_3
   8: aload_3
  10: aload_1                                  // the sub-level from the block pos
  12: if_acmpeq     26                         // same -> keep
  14: getstatic     Sable.HELPER
  17: aload_2
  18: invokevirtual ActiveSableCompanion.getTrackingSubLevel(Entity) : SubLevel
  21: aload_1
  23: if_acmpeq     26
  26: iconst_0 / iconst_1

The problem: the schematic's sub-level set is resolved by getContaining on a single position (the selection corner / block pos). There is no bounding-box intersection test between the schematic's from/to corners and each sub-level's own bounds.

So when a child sub-level sits on or overlaps the parent's geometry, getContaining resolves the position to the parent, and the parent's whole template is then serialized into the schematic.

This also explains why pausing physics has no effect: getContaining is a pure spatial-lookup on the position, independent of physics state.

Related API surface (for reference):

StructureTemplateExtension.sable$getSubLevels() : List<SubLevelTemplate>
StructureTemplateExtension$SubLevelTemplate(uuid, position, orientation, template)
SchematicLevelExtension.sable$getSubLevels() : List<SchematicSubLevel>

Suggested fix directions

  1. Use bounding-box intersection between the schematic corners and each candidate sub-level, instead of getContaining on one position. Only serialize sub-levels whose bounds actually intersect the selection.
  2. If a sub-level partially intersects the selection, decide explicitly whether to clip it or to reject the export with a clear message — but do not silently include the entire structure.
  3. Consider making this configurable (e.g. "only include sub-levels fully inside the selection" vs "include intersecting ones").

Related information

Workarounds we are using

  • Exporting sub-levels through sablecleanup 0.3.0's own schematic action, which operates per sub-level rather than by a free-form selection.
  • Physically separating the child structure from the parent before using the Schematic And Quill.

Activity

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions