Repository navigation
Schematic And Quill captures the entire parent sub-level when selecting a nearby child sub-level #1604
Description
| 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
- Build a large structure and assemble it as a Sable sub-level (the "parent" — in our case a floating island, ~169 chunks).
- Detach / assemble a smaller structure as a separate sub-level nearby (the "child" — a ship).
- Take a Schematic And Quill and select an area that only covers the child.
- Save the schematic.
- 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
- Use bounding-box intersection between the schematic corners and each candidate sub-level, instead of
getContainingon one position. Only serialize sub-levels whose bounds actually intersect the selection. - 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.
- Consider making this configurable (e.g. "only include sub-levels fully inside the selection" vs "include intersecting ones").
Related information
- Reddit discussion "do schematics save sable sublevels?" — the accepted answer notes that super-glued parts behave differently and suggests disassembling and re-gluing with honey glue to get correct saving:
https://www.reddit.com/r/CreateMod/comments/1uqfacj/do_schematics_save_sable_sublevels/ - Similar family of issues around sub-level identity/bounds:
Schematic Printing doesn't work at all— Schematic Printing doesn't work at all #910Mutually-dependent sub-levels can never load again once the whole group is unloaded— Mutually-dependent sub-levels can never load again once the whole group is unloaded (jointed assembly permanently lost) #1387
Workarounds we are using
- Exporting sub-levels through
sablecleanup0.3.0's ownschematicaction, 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