Description
Environment
|
|
| Sable |
2.0.5+mc1.21.1 (also reproduced on 2.0.3) |
| Simulated |
1.3.0 / 1.3.2 (Physics Assembler) |
| Create Aeronautics |
1.3.2 |
| Loader |
NeoForge 21.1.235 |
| Minecraft |
1.21.1 |
Summary
Disassembling a sub-level can duplicate any collision-less (zero-mass) block that happens to be cleared after the last mass-bearing block in the plot-sweep order.
The block is destroyed with drops even though a copy of it has already been placed at the destination, so the player receives an extra copy of the item.
This is reproducible with vanilla blocks only (e.g. wall signs and torches) and is still reproducible on Sable 2.0.5.
Reproduction
- Build a small structure from solid blocks, then attach vanilla wall signs / torches to it.
- Assemble and then disassemble it using the Physics Assembler.
- Signs/torches that come after the last solid block in the plot-sweep order are dropped as items while also being correctly placed at the destination.
Which blocks are duplicated depends on their position relative to the last mass-bearing block in the clear order, so the affected blocks vary with the structure's orientation and placement.
A sunflower is a handy east-facing indicator when re-testing. 🙂
Root Cause
-
PhysicsBlockPropertyHelper.getMass short-circuits to 0.0 for any block whose collision shape is empty (VoxelNeighborhoodState.isSolid).
The source itself contains a TODO explicitly acknowledging this case:
// TODO: Doing this means that sub-levels can end up with an existent bounding box but without any mass, invalidating them
PhysicsBlockPropertyHelper.java:28
-
During SubLevelAssemblyHelper.moveBlocks, each source-position clear is routed through SubLevelPhysicsSystem.updateMassDataFromBlockChange, which subtracts the block's mass from the tracker.
When the last mass-bearing block is cleared, the total mass reaches 0, causing MassData.isInvalid() to become true even though zero-mass blocks may still remain in the source plot.
-
This triggers ServerLevelPlot.destroyAllBlocks(), which destroys the remaining plot blocks with drops.
However, those blocks have already been placed at their destination by moveBlocks, so the destruction drops create duplicate items.
In short:
moveBlocks places all blocks at the destination
↓
source positions are cleared one by one
↓
last mass-bearing block is cleared
↓
mass reaches 0
↓
MassData.isInvalid()
↓
destroyAllBlocks()
↓
remaining zero-mass blocks are destroyed with drops
↓
destination copies + dropped source items = duplication
This also explains why the issue can appear to be random: the result depends on the source-position clear order and the structure's orientation/placement.
Suggested Fixes
Any of the following could prevent the duplication:
- Do not trigger the
isInvalid() → destroyAllBlocks() path while moveBlocks is still performing its source-position clearing.
- Make
destroyAllBlocks silent (destroyBlock(pos, false) or removeBlock) when it is being used as part of this cleanup, since the blocks in the remaining plot have already been moved to their destination.
- More generally, do not use
mass == 0 alone to determine that a sub-level contains no remaining blocks; only invalidate/destroy the sub-level once all of its blocks have actually been processed.
Workaround
For our own decorative blocks, we currently work around this by giving them a tiny 0.01³ collision box so Sable treats them as mass-bearing.
Collision-less vanilla blocks such as signs and torches remain affected.
Attached log: reproduction on Sable 2.0.3. The issue was also tested and reproduced on Sable 2.0.5.
Game Log
https://mclo.gs/miDxgEv
Description
Environment
Summary
Disassembling a sub-level can duplicate any collision-less (zero-mass) block that happens to be cleared after the last mass-bearing block in the plot-sweep order.
The block is destroyed with drops even though a copy of it has already been placed at the destination, so the player receives an extra copy of the item.
This is reproducible with vanilla blocks only (e.g. wall signs and torches) and is still reproducible on Sable 2.0.5.
Reproduction
Which blocks are duplicated depends on their position relative to the last mass-bearing block in the clear order, so the affected blocks vary with the structure's orientation and placement.
A sunflower is a handy east-facing indicator when re-testing. 🙂
Root Cause
PhysicsBlockPropertyHelper.getMassshort-circuits to0.0for any block whose collision shape is empty (VoxelNeighborhoodState.isSolid).The source itself contains a TODO explicitly acknowledging this case:
PhysicsBlockPropertyHelper.java:28
During
SubLevelAssemblyHelper.moveBlocks, each source-position clear is routed throughSubLevelPhysicsSystem.updateMassDataFromBlockChange, which subtracts the block's mass from the tracker.When the last mass-bearing block is cleared, the total mass reaches
0, causingMassData.isInvalid()to become true even though zero-mass blocks may still remain in the source plot.This triggers
ServerLevelPlot.destroyAllBlocks(), which destroys the remaining plot blocks with drops.However, those blocks have already been placed at their destination by
moveBlocks, so the destruction drops create duplicate items.In short:
This also explains why the issue can appear to be random: the result depends on the source-position clear order and the structure's orientation/placement.
Suggested Fixes
Any of the following could prevent the duplication:
isInvalid()→destroyAllBlocks()path whilemoveBlocksis still performing its source-position clearing.destroyAllBlockssilent (destroyBlock(pos, false)orremoveBlock) when it is being used as part of this cleanup, since the blocks in the remaining plot have already been moved to their destination.mass == 0alone to determine that a sub-level contains no remaining blocks; only invalidate/destroy the sub-level once all of its blocks have actually been processed.Workaround
For our own decorative blocks, we currently work around this by giving them a tiny
0.01³collision box so Sable treats them as mass-bearing.Collision-less vanilla blocks such as signs and torches remain affected.
Attached log: reproduction on Sable 2.0.3. The issue was also tested and reproduced on Sable 2.0.5.
Game Log
https://mclo.gs/miDxgEv