Repository: FabricMC/fabric (Java)

Issue:
Vanilla Jukebox bug with Fabric transfer API v1
### Triggering the bug

Extract the disc from the Jukebox using Fabric's transfer API v1.

The disc is extracted from the Jukebox but the music restarts. The music is now playing and the Jukebox's state is `hasRecord:true`, but it stores no item (the disc will be wherever you extracted it to and breaking the Jukebox will not drop a disc). 

The music will keep on playing for its normal duration and can not be stopped, breaking the Jukebox will not affect the music either (it will keep on playing). The Jukebox's state will remain as `hasRecord:true` and can be (as far as I found out) not altered anymore, putting a new disc into the Jukebox will not work.

### Reproducing the bug

To reproduce, any Fabric mod that has pipes (or something similar) that use the transfer API will work. I tested it with [Modern Industrialization](https://github.com/AztechMC/Modern-Industrialization)'s Item Pipes.

Alternatively, if you are in a dev environment, you can apply the following patch to make Vanilla hoppers use the transfer API.

```diff
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java
index cff062a3..7e2a15ef 100644
--- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java
@@ -83,7 +83,7 @@ public class HopperBlockEntityMixin {
        )
        private static void hookExtract(World world, Hopper hopper, CallbackInfoReturnable<Boolean> cir, Inventory inputInventory) {
                // Let vanilla handle the transfer if it found an inventory.
-               if (inputInventory != null) return;
+//             if (inputInventory != null) return;

                // Otherwise inject our transfer logic.
                BlockPos sourcePos = BlockPos.ofFloored(hopper.getHopperX(), hopper.getHopperY() + 1.0D, hopper.getHopperZ());
```

You will have to wait until the music finishes once, because the Jukebox's redstone signal blocks the hopper while playing. I recommend using the melohi disc in that case, as it is the shortest (1:36) that is actually pleasant to listen to (if you don't care, use 11).

### Fixing the bug

Don't know. But it happens because the `JukeboxBlockEntity` (which implements `Container` and is therefore discoverable via the transfer API) starts playing the music every time it's `setStack` method is called. Apparently, if you call `extract` on a `Storage` it will end up calling this method. This causes the music to start playing every time an item is extracted. For more information see the stacktraces below. Note that in one extraction the `setStack` method is called multiple times.

<details><summary><h4>Stacktraces</h4></summary>
<p>
I obtained those by breakpointing `JukeboxBlockEntity#startPlaying` and getting a thread dump for every hit. They are truncated to start at hopper's `extract` method which then calls into the hopper mixin and then into the transfer API.

<h5>first hit</h5>

This is the simulated extraction.

<pre>
java.lang.Thread.State: RUNNABLE
at net.minecraft.block.entity.JukeboxBlockEntity.startPlaying(JukeboxBlockEntity.java:84)
at net.minecraft.block.entity.JukeboxBlockEntity.setStack(JukeboxBlockEntity.java:144)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.setStack(InventorySlotWrapper.java:63)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.createSnapshot(SingleStackStorage.java:159)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.createSnapshot(SingleStackStorage.java:42)
at net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant.updateSnapshots(SnapshotParticipant.java:102)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.updateSnapshots(InventorySlotWrapper.java:128)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.extract(SingleStackStorage.java:144)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.extract(InventorySlotWrapper.java:97)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.extract(InventorySlotWrapper.java:40)
at net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil.simulateExtract(StorageUtil.java:162)
at net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil.move(StorageUtil.java:98)
at net.minecraft.block.entity.HopperBlockEntity.handler$zbd000$fabric-transfer-api-v1$hookExtract(HopperBlockEntityMixin.java:93)
at net.minecraft.block.entity.HopperBlockEntity.extract(HopperBlockEntity.java:191)
</pre>

<h5>second hit</h5>

This is the rollback of the simulated transaction.

<pre>
at net.minecraft.block.entity.JukeboxBlockEntity.startPlaying(JukeboxBlockEntity.java:84)
at net.minecraft.block.entity.JukeboxBlockEntity.setStack(JukeboxBlockEntity.java:144)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.setStack(InventorySlotWrapper.java:63)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.readSnapshot(SingleStackStorage.java:165)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.readSnapshot(SingleStackStorage.java:42)
at net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant.onClose(SnapshotParticipant.java:117)
at net.fabricmc.fabric.impl.transfer.transaction.TransactionManagerImpl$TransactionImpl.close(TransactionManagerImpl.java:138)
at net.fabricmc.fabric.impl.transfer.transaction.TransactionManagerImpl$TransactionImpl.abort(TransactionManagerImpl.java:181)
at net.fabricmc.fabric.impl.transfer.transaction.TransactionManagerImpl$TransactionImpl.close(TransactionManagerImpl.java:192)
at net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil.simulateExtract(StorageUtil.java:163)
at net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil.move(StorageUtil.java:98)
at net.minecraft.block.entity.HopperBlockEntity.handler$zbd000$fabric-transfer-api-v1$hookExtract(HopperBlockEntityMixin.java:93)
at net.minecraft.block.entity.HopperBlockEntity.extract(HopperBlockEntity.java:191)
</pre>

<h5>third hit</h5>

This is the actual transaction.

<pre>
at net.minecraft.block.entity.JukeboxBlockEntity.startPlaying(JukeboxBlockEntity.java:84)
at net.minecraft.block.entity.JukeboxBlockEntity.setStack(JukeboxBlockEntity.java:144)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.setStack(InventorySlotWrapper.java:63)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.createSnapshot(SingleStackStorage.java:159)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.createSnapshot(SingleStackStorage.java:42)
at net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant.updateSnapshots(SnapshotParticipant.java:102)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.updateSnapshots(InventorySlotWrapper.java:128)
at net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage.extract(SingleStackStorage.java:144)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.extract(InventorySlotWrapper.java:97)
at net.fabricmc.fabric.impl.transfer.item.InventorySlotWrapper.extract(InventorySlotWrapper.java:40)
at net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil.move(StorageUtil.java:105)
at net.minecraft.block.entity.HopperBlockEntity.handler$zbd000$fabric-transfer-api-v1$hookExtract(HopperBlockEntityMixin.java:93)
at net.minecraft.block.entity.HopperBlockEntity.extract(HopperBlockEntity.java:191)
</pre>
</p>
</details>

I guess the real problem here is that the transfer API assumes `setStack` has no side effects. I am not familiar with Fabric at all tbh, so I will leave finding a solution to this problem to others.



Fix (patch):
diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/JukeboxBlockEntityMixin.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/JukeboxBlockEntityMixin.java
new file mode 100644
index 0000000000..eb5d1da8d6
--- /dev/null
+++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/JukeboxBlockEntityMixin.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.transfer;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.block.entity.JukeboxBlockEntity;
+import net.minecraft.item.ItemStack;
+
+import net.fabricmc.fabric.impl.transfer.item.SpecialLogicInventory;
+
+@Mixin(JukeboxBlockEntity.class)
+public abstract class JukeboxBlockEntityMixin implements SpecialLogicInventory {
+	@Shadow
+	private ItemStack recordStack;
+
+	@Shadow
+	public abstract void setStack(ItemStack stack);
+
+	@Unique
+	private boolean fabric_suppressSpecialLogic = false;
+
+	@Override
+	public void fabric_setSuppress(boolean suppress) {
+		fabric_suppressSpecialLogic = suppress;
+	}
+
+	@Inject(method = "setStack", at = @At("HEAD"), cancellable = true)
+	private void setStackBypass(ItemStack stack, CallbackInfo ci) {
+		if (fabric_suppressSpecialLogic) {
+			recordStack = stack;
+			ci.cancel();
+		}
+	}
+
+	@Override
+	public void fabric_onFinalCommit(int slot, ItemStack oldStack, ItemStack newStack) {
+		// Call setStack again without suppressing vanilla logic,
+		// where now the record will actually getting played/stopped.
+		setStack(newStack);
+	}
+}
diff --git a/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json b/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json
index 626673dab4..f74b8199b2 100644
--- a/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json
+++ b/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json
@@ -12,6 +12,7 @@
     "FluidMixin",
     "HopperBlockEntityMixin",
     "ItemMixin",
+    "JukeboxBlockEntityMixin",
     "LootableContainerBlockEntityMixin",
     "SimpleInventoryMixin"
   ]