Crafting CPU only drops items when broken (#1713)

Co-authored-by: Martin Robertz <dream-master@gmx.net>
This commit is contained in:
Ruling-0andMartin Robertz authored and GitHub committed 2026-10-20 22:49:13 +02:00
1 parent 2444966e6f
commit db9e4f579b
5 files changed
+194 -75

No files matched your search

@@ -2,6 +2,7 @@ package appeng.test.me.crafting;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -202,6 +203,24 @@ public class CraftingCPUClusterFunctionalTest {
assertTrue(cpu.completed());
}
@Test
void breakingClusterWithInvalidatedCoreKeepsJob() {
cpu.prepare(pattern, 64, 128);
cpu.core.updateStatus(cpu);
cpu.core.invalidate();
cpu.breakCluster();
assertAll(
() -> assertTrue(cpu.isDestroyed()),
() -> assertFalse(cpu.completed(), "The job must not be cancelled"),
() -> assertEquals(512, cpu.storedInputs(), "The inventory must not be spilled"),
() -> assertEquals(
cpu.getInventory().writeInventory(),
cpu.core.getPreviousState().getTag("inventory"),
"The core must keep the job"));
}
private void tick() {
cpu.updateCraftingLogic(grid, energy, cache);
}
@@ -0,0 +1,35 @@
package appeng.test.tile.crafting;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import org.junit.jupiter.api.Test;
import appeng.tile.crafting.TileCraftingTile;
public class TileCraftingTileFunctionalTest {
@Test
void unformedCoreKeepsPendingJobAcrossSaves() {
final NBTTagList inventory = new NBTTagList();
inventory.appendTag(new ItemStack(Blocks.cobblestone, 64).writeToNBT(new NBTTagCompound()));
final NBTTagCompound saved = new NBTTagCompound();
saved.setBoolean("core", true);
saved.setTag("inventory", inventory);
final TileCraftingTile unformed = new TileCraftingTile();
unformed.readFromNBT(saved);
final NBTTagCompound resaved = new NBTTagCompound();
unformed.writeToNBT(resaved);
final TileCraftingTile reloaded = new TileCraftingTile();
reloaded.readFromNBT(resaved);
assertNotNull(reloaded.getPreviousState());
assertEquals(inventory, reloaded.getPreviousState().getTag("inventory"));
}
}
@@ -102,6 +102,7 @@ public class BlockCraftingUnit extends AEBaseTileBlock {
final TileCraftingTile cp = this.getTileEntity(w, x, y, z);
if (cp != null) {
cp.breakCluster();
cp.spillPreviousState();
}
super.breakBlock(w, x, y, z, a, b);
@@ -341,7 +341,7 @@ public class CraftingCPUCluster implements IAECluster, ICraftingCPU {
}
protected void addTile(final TileCraftingTile te) {
if (this.machineSrc == null || te.isCoreBlock()) {
if (this.machineSrc == null || te.isCoreBlock() && this.getCore().getPreviousState() == null) {
this.machineSrc = new MachineSource(te);
}
@@ -621,6 +621,18 @@ public class CraftingCPUCluster implements IAECluster, ICraftingCPU {
return (TileCraftingTile) this.machineSrc.via;
}
/// Whether the core tile, which persists this CPU's job, has been unloaded or replaced.
public boolean isCoreInvalid() {
return this.getCore().isInvalid();
}
/// Saves this CPU's job to its core, to be resumed once the core forms a cluster again.
public void storeJobInCore() {
final NBTTagCompound job = new NBTTagCompound();
this.writeToNBT(job);
this.getCore().setPreviousState(job);
}
public IGrid getGrid() {
IGrid node;
for (final TileCraftingTile r : this.tiles) {
@@ -1649,6 +1661,10 @@ public class CraftingCPUCluster implements IAECluster, ICraftingCPU {
core.setPreviousState(null);
}
for (final TileCraftingTile te : this.tiles) {
te.spillPreviousState();
}
this.updateCPU();
this.updateName();
@@ -17,10 +17,12 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import net.minecraft.block.Block;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraftforge.common.util.ForgeDirection;
@@ -184,8 +186,16 @@ public class TileCraftingTile extends AENetworkTile implements IAEMultiBlock, IP
@TileEvent(TileEventType.WORLD_NBT_WRITE)
public void writeToNBT_TileCraftingTile(final NBTTagCompound data) {
data.setBoolean("core", this.isCoreBlock());
if (this.isCoreBlock() && this.cluster != null) {
this.cluster.writeToNBT(data);
if (this.isCoreBlock()) {
if (this.cluster != null) {
this.cluster.writeToNBT(data);
} else if (this.previousState != null) {
for (final String key : (Set<String>) this.previousState.func_150296_c()) {
if (!data.hasKey(key)) {
data.setTag(key, this.previousState.getTag(key).copy());
}
}
}
}
}
@@ -249,80 +259,118 @@ public class TileCraftingTile extends AENetworkTile implements IAEMultiBlock, IP
}
public void breakCluster() {
if (this.cluster != null) {
this.cluster.cancel();
final MECraftingInventory inv = this.cluster.getInventory();
final LinkedList<WorldCoord> places = new LinkedList<>();
final Iterator<IGridHost> i = this.cluster.getTiles();
while (i.hasNext()) {
final IGridHost h = i.next();
if (h == this) {
places.add(new WorldCoord(this));
} else {
final TileEntity te = (TileEntity) h;
for (final ForgeDirection d : ForgeDirection.VALID_DIRECTIONS) {
final WorldCoord wc = new WorldCoord(te);
wc.add(d, 1);
if (this.worldObj.isAirBlock(wc.x, wc.y, wc.z)) {
places.add(wc);
}
}
}
}
Collections.shuffle(places);
if (places.isEmpty()) {
throw new IllegalStateException(
this.cluster + " does not contain any kind of blocks, which were destroyed.");
}
for (IAEItemStack ais : (IItemList<IAEItemStack>) inv
.getAvailableItems(AEApi.instance().storage().createItemList())) {
ais = ais.copy();
ais.setStackSize(ais.getItemStack().getMaxStackSize());
while (!places.isEmpty()) {
final IAEItemStack g = inv.extractItems(ais.copy(), Actionable.MODULATE);
if (g == null) {
break;
}
final WorldCoord wc = places.removeFirst();
if (!AEConfig.instance.limitCraftingCPUSpill) {
places.add(wc);
}
Platform.spawnDrops(this.worldObj, wc.x, wc.y, wc.z, Collections.singletonList(g.getItemStack()));
}
if (places.isEmpty()) {
break;
}
}
for (IAEFluidStack ifs : (IItemList<IAEFluidStack>) inv
.getAvailableItems(AEApi.instance().storage().createFluidList())) {
while (!places.isEmpty()) {
final IAEItemStack g = stackConvertPacket(inv.extractItems(ifs, Actionable.MODULATE));
if (g == null) {
break;
}
final WorldCoord wc = places.removeFirst();
if (!AEConfig.instance.limitCraftingCPUSpill) {
places.add(wc);
}
Platform.spawnDrops(this.worldObj, wc.x, wc.y, wc.z, Collections.singletonList(g.getItemStack()));
}
if (places.isEmpty()) {
break;
}
}
if (this.cluster == null) {
return;
}
/*
* An invalidated core was unloaded or replaced (e.g. by a block mover) rather than broken, so the NBT it was
* saved to still holds the job and restores it once the core forms a cluster again. Cancelling and spilling the
* job here as well would duplicate it. The job is also kept on the core itself, for when the same tile is
* revalidated instead of being loaded from NBT.
*/
if (this.cluster.isCoreInvalid()) {
this.cluster.storeJobInCore();
this.cluster.destroy();
return;
}
this.cluster.cancel();
final LinkedList<WorldCoord> places = new LinkedList<>();
final Iterator<IGridHost> i = this.cluster.getTiles();
while (i.hasNext()) {
final IGridHost h = i.next();
if (h == this) {
places.add(new WorldCoord(this));
} else {
places.addAll(this.getAdjacentAir((TileEntity) h));
}
}
if (places.isEmpty()) {
throw new IllegalStateException(
this.cluster + " does not contain any kind of blocks, which were destroyed.");
}
this.spillInventory(this.cluster.getInventory(), places);
this.cluster.destroy();
}
/// Drops the items of the job loaded from NBT, for when no cluster resumes it.
public void spillPreviousState() {
if (this.previousState == null) {
return;
}
final MECraftingInventory inv = new MECraftingInventory();
inv.readInventory((NBTTagList) this.previousState.getTag("inventory"));
this.previousState = null;
final LinkedList<WorldCoord> places = this.getAdjacentAir(this);
if (places.isEmpty()) {
places.add(new WorldCoord(this));
}
this.spillInventory(inv, places);
}
private LinkedList<WorldCoord> getAdjacentAir(final TileEntity te) {
final LinkedList<WorldCoord> places = new LinkedList<>();
for (final ForgeDirection d : ForgeDirection.VALID_DIRECTIONS) {
final WorldCoord wc = new WorldCoord(te);
wc.add(d, 1);
if (this.worldObj.isAirBlock(wc.x, wc.y, wc.z)) {
places.add(wc);
}
}
return places;
}
private void spillInventory(final MECraftingInventory inv, final LinkedList<WorldCoord> places) {
Collections.shuffle(places);
for (IAEItemStack ais : (IItemList<IAEItemStack>) inv
.getAvailableItems(AEApi.instance().storage().createItemList())) {
ais = ais.copy();
ais.setStackSize(ais.getItemStack().getMaxStackSize());
while (!places.isEmpty()) {
final IAEItemStack g = inv.extractItems(ais.copy(), Actionable.MODULATE);
if (g == null) {
break;
}
final WorldCoord wc = places.removeFirst();
if (!AEConfig.instance.limitCraftingCPUSpill) {
places.add(wc);
}
Platform.spawnDrops(this.worldObj, wc.x, wc.y, wc.z, Collections.singletonList(g.getItemStack()));
}
if (places.isEmpty()) {
break;
}
}
for (IAEFluidStack ifs : (IItemList<IAEFluidStack>) inv
.getAvailableItems(AEApi.instance().storage().createFluidList())) {
while (!places.isEmpty()) {
final IAEItemStack g = stackConvertPacket(inv.extractItems(ifs, Actionable.MODULATE));
if (g == null) {
break;
}
final WorldCoord wc = places.removeFirst();
if (!AEConfig.instance.limitCraftingCPUSpill) {
places.add(wc);
}
Platform.spawnDrops(this.worldObj, wc.x, wc.y, wc.z, Collections.singletonList(g.getItemStack()));
}
if (places.isEmpty()) {
break;
}
}
}