Repository: MinecraftForge/MinecraftForge (language: java)

Issue:
1.20.1 Forge Redstone differs from Vanilla and Fabric

**Minecraft Version:** {1.20.1}

**Forge Version:** {Forge version. *47.2*}

**Steps to Reproduce:**
 1. download the linked world (Orbital Strike 4.0)
 2. use the cannon
 3. set the command blocks to use @ A instead of the username present
 4. go to spectator and watch the bottom TNT duper (first one to activate)

**Description of issue:**
It seems that both the timing as well as the normal game features differ in forge as whenever I use the cannon the timing for pushing the TNT is off as well as whatever TNT does get put on the cannon never makes it to target.

**Link to Files**
https://www.mediafire.com/folder/88qrgise22lax/Orbital_Strike_Cannon


Discussion / hints:
To debug this I need something far simpler then this massive contraption. At the end of the day its the TNT duplicator that is the issue, so find a isolated case to test that.
I will try to create a simpler version of the mechanism used in the cannon later. I also want to ask whether the main mechanisms behind the cannon such as building momentum on the TNT stuck in stasis would still work on forge and whether it would make its way to target or whether the main mechanisms behind it are broken by forge. if the mechanisms work on forge what I could do is fine tune the cannon in order to make it work within forge.
Chunk loading is modified by Forge, however in theory it should use the same chunk ticketing system as vanilla. Just with the ability for mods to add extra ticket types. 
So.. in theory it should be fine. 
The issue is that this entire process exploits bugs in Minecraft. Sometimes bugs are fixed by Forge. But typically with intentional fixes and links to the bug reports on Mojang's tracker. I can't say if any of those changes interact with anything in this specific contraption. Which is why smaller reproduction cases are needed.

Also for future note, a bit more detailed reproduction steps is also helpful. 
We had no idea that `use the cannon` meant that we needed to grab at least a trim x, trim z, and payload type from the chest, add it to the minecart right click one note block, then right click another. I only found that out by googeling the name that was in the command blocks and seeing what I could find.

`set the command blocks to use @ A instead of the username present` also was slightly better because we only found 3 command blocks, but what we actually had to do was edit the two touching each other and then add a button to the pair. and then press that button after starting the firing sequence {the 2nd note block}

And then `go to spectator and watch the bottom TNT duper (first one to activate)`
Took us feakin ages to figure out where the tnt duper even was, as the contraption is quite large, and it does literally nothing unless you do the first step correctly {which again we had no way of knowing how to do}
I was able to find a TNT duper explanation [here ](https://youtu.be/65OwMFbnHnE) and [here](https://youtu.be/mM0uO_BGyEo?t=240).  But none that looked anything like what your setup is.

Overall, my main point is that when reporting issues, its nice to have a example of the issue but it needs to be something A LOT simpler and better explained.  So even tho we tried, and it was kinda fun digging into it, we had no idea how to isolate the issue and verify it. Which is needed to be done before we can fix it.

This issue is becoming stale. If you're still interested, please send us a reproducible test case so we can try and fix whatever's causing this.
Here's two detailed Neoforge issues on comparator behaviour differing that might be relevant as the analyzed code (seems to) originates(s) from before they forked from Forge:
https://github.com/neoforged/NeoForge/issues/1750 (missing updates)
https://github.com/neoforged/NeoForge/issues/1863 (different update order)
The write-ups are useful, but [like mentioned earlier](https://github.com/MinecraftForge/MinecraftForge/issues/9973#issuecomment-2113108713), having an isolated, reproducible test case is going to help us actually understand what the issue is with Forge and how we can actively tinker with it without causing much trouble.

Fix patch:
diff --git a/patches/minecraft/net/minecraft/core/Direction.java.patch b/patches/minecraft/net/minecraft/core/Direction.java.patch
index d0dabc7787d..1213925f351 100644
--- a/patches/minecraft/net/minecraft/core/Direction.java.patch
+++ b/patches/minecraft/net/minecraft/core/Direction.java.patch
@@ -1,12 +1,17 @@
 --- a/net/minecraft/core/Direction.java
 +++ b/net/minecraft/core/Direction.java
-@@ -49,6 +_,10 @@
+@@ -49,6 +_,15 @@
      private final Vec3i normal;
      private final Vec3 normalVec3;
      private static final Direction[] VALUES = values();
 +    private static final List<Direction> VALUES_VIEW = java.util.Collections.unmodifiableList(Arrays.asList(VALUES));
 +    public static final List<Direction> valuesView() {
 +        return VALUES_VIEW;
++    }
++    // In vanilla updates are notified only on the horizontal plane, in Forge we also notify up and down
++    private static final List<Direction> UPDATE_ORDER = java.util.Collections.unmodifiableList(Stream.concat(Plane.HORIZONTAL.stream(), Plane.VERTICAL.stream()).toList());
++    public static final List<Direction> getUpdateOrder() {
++        return UPDATE_ORDER;
 +    }
      private static final Direction[] BY_3D_DATA = Arrays.stream(VALUES)
          .sorted(Comparator.comparingInt(p_235687_ -> p_235687_.data3d))
diff --git a/patches/minecraft/net/minecraft/world/level/Level.java.patch b/patches/minecraft/net/minecraft/world/level/Level.java.patch
index 2b967f294aa..7acf8f6e9af 100644
--- a/patches/minecraft/net/minecraft/world/level/Level.java.patch
+++ b/patches/minecraft/net/minecraft/world/level/Level.java.patch
@@ -157,12 +157,12 @@
                      T t = p_261885_.tryCast(enderdragonpart);
                      if (t != null && p_261688_.test(t)) {
                          p_262071_.add(t);
-@@ -919,16 +_,15 @@
+@@ -919,17 +_,16 @@
      public abstract Scoreboard getScoreboard();
  
      public void updateNeighbourForOutputSignal(BlockPos p_46718_, Block p_46719_) {
 -        for (Direction direction : Direction.Plane.HORIZONTAL) {
-+        for (Direction direction : Direction.valuesView()) {
++        for (Direction direction : Direction.getUpdateOrder()) {
              BlockPos blockpos = p_46718_.relative(direction);
              if (this.hasChunkAt(blockpos)) {
                  BlockState blockstate = this.getBlockState(blockpos);
@@ -174,10 +174,12 @@
                      blockpos = blockpos.relative(direction);
                      blockstate = this.getBlockState(blockpos);
 -                    if (blockstate.is(Blocks.COMPARATOR)) {
+-                        this.neighborChanged(blockstate, blockpos, p_46719_, null, false);
 +                    if (blockstate.getWeakChanges(this, blockpos)) {
-                         this.neighborChanged(blockstate, blockpos, p_46719_, null, false);
++                        blockstate.onNeighborChange(this, blockpos, p_46718_);
                      }
                  }
+             }
 @@ -1031,6 +_,20 @@
      }
  


Test patch:
diff --git a/patches/minecraft/net/minecraft/gametest/framework/GameTestInfo.java.patch b/patches/minecraft/net/minecraft/gametest/framework/GameTestInfo.java.patch
new file mode 100644
index 00000000000..3ba28ba53f4
--- /dev/null
+++ b/patches/minecraft/net/minecraft/gametest/framework/GameTestInfo.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/gametest/framework/GameTestInfo.java
++++ b/net/minecraft/gametest/framework/GameTestInfo.java
+@@ -265,7 +_,7 @@
+         BlockPos blockpos = this.getOrCalculateNorthwestCorner();
+         this.structureBlockEntity = StructureUtils.prepareTestStructure(this, blockpos, this.getRotation(), this.level);
+         this.structureBlockPos = this.structureBlockEntity.getBlockPos();
+-        StructureUtils.addCommandBlockAndButtonToStartTest(this.structureBlockPos, new BlockPos(1, 0, -1), this.getRotation(), this.level);
++        StructureUtils.addCommandBlockAndButtonToStartTest(this.structureBlockPos, new BlockPos(1, 0, -1), Rotation.NONE, this.level); // Forge: The control blocks are always in the North West Corner
+         StructureUtils.encaseStructure(this.getStructureBounds(), this.level, !this.testFunction.skyAccess());
+         this.listeners.forEach(p_127630_ -> p_127630_.testStructureLoaded(this));
+         return this;
diff --git a/patches/minecraft/net/minecraft/gametest/framework/ReportGameListener.java.patch b/patches/minecraft/net/minecraft/gametest/framework/ReportGameListener.java.patch
new file mode 100644
index 00000000000..9cc37009ef9
--- /dev/null
+++ b/patches/minecraft/net/minecraft/gametest/framework/ReportGameListener.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/gametest/framework/ReportGameListener.java
++++ b/net/minecraft/gametest/framework/ReportGameListener.java
+@@ -151,7 +_,7 @@
+     private static BlockPos getBeaconPos(GameTestInfo p_344999_) {
+         BlockPos blockpos = p_344999_.getStructureBlockPos();
+         BlockPos blockpos1 = new BlockPos(-1, -2, -1);
+-        return StructureTemplate.transform(blockpos.offset(blockpos1), Mirror.NONE, p_344999_.getRotation(), blockpos);
++        return blockpos.offset(blockpos1); // Forge: The control blocks are always in the North West Corner
+     }
+ 
+     private static void updateBeaconGlass(GameTestInfo p_343978_, Block p_344076_) {
+@@ -167,8 +_,8 @@
+         ServerLevel serverlevel = p_177739_.getLevel();
+         BlockPos blockpos = p_177739_.getStructureBlockPos();
+         BlockPos blockpos1 = new BlockPos(-1, 0, -1);
+-        BlockPos blockpos2 = StructureTemplate.transform(blockpos.offset(blockpos1), Mirror.NONE, p_177739_.getRotation(), blockpos);
+-        serverlevel.setBlockAndUpdate(blockpos2, Blocks.LECTERN.defaultBlockState().rotate(p_177739_.getRotation()));
++        BlockPos blockpos2 = blockpos.offset(blockpos1); // Forge: The control blocks are always in the North West Corner
++        serverlevel.setBlockAndUpdate(blockpos2, Blocks.LECTERN.defaultBlockState());
+         BlockState blockstate = serverlevel.getBlockState(blockpos2);
+         ItemStack itemstack = createBook(p_177739_.getTestName(), p_177739_.isRequired(), p_177740_);
+         LecternBlock.tryPlaceBook(null, serverlevel, blockpos2, blockstate, itemstack);
diff --git a/patches/minecraft/net/minecraft/gametest/framework/StructureUtils.java.patch b/patches/minecraft/net/minecraft/gametest/framework/StructureUtils.java.patch
new file mode 100644
index 00000000000..b1757f2e2b5
--- /dev/null
+++ b/patches/minecraft/net/minecraft/gametest/framework/StructureUtils.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/gametest/framework/StructureUtils.java
++++ b/net/minecraft/gametest/framework/StructureUtils.java
+@@ -132,10 +_,16 @@
+             .orElseThrow(() -> new IllegalStateException("Missing test structure: " + p_311701_.getStructureName()))
+             .getSize();
+         BoundingBox boundingbox = getStructureBoundingBox(p_311042_, vec3i, p_310584_);
+-        BlockPos blockpos = getStartCorner(p_311701_, p_311042_, p_310584_, p_312330_);
++        BlockPos blockpos = getStartCorner(p_311701_, p_311042_, Rotation.NONE, p_312330_); // Forge: The control blocks are always in the North West Corner
+         forceLoadChunks(boundingbox, p_312330_);
+         clearSpaceForStructure(boundingbox, p_312330_);
+-        return createStructureBlock(p_311701_, blockpos.below(), p_310584_, p_312330_);
++        var entity = createStructureBlock(p_311701_, blockpos.below(), p_310584_, p_312330_);
++        //Forge:  We need to offset the structure so that it will load within bounds.
++        if (p_310584_ != Rotation.NONE) {
++            var rotated = StructureTemplate.getZeroPositionWithTransform(BlockPos.ZERO, Mirror.NONE, p_310584_, entity.getStructureSize().getX(), entity.getStructureSize().getZ());
++            entity.setStructurePos(rotated.offset(new BlockPos(0, 1, 0)));
++        }
++        return entity;
+     }
+ 
+     public static void encaseStructure(AABB p_330422_, ServerLevel p_331249_, boolean p_328180_) {
diff --git a/patches/minecraft/net/minecraft/gametest/framework/TestCommand.java.patch b/patches/minecraft/net/minecraft/gametest/framework/TestCommand.java.patch
new file mode 100644
index 00000000000..a4a6ffc7f75
--- /dev/null
+++ b/patches/minecraft/net/minecraft/gametest/framework/TestCommand.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/gametest/framework/TestCommand.java
++++ b/net/minecraft/gametest/framework/TestCommand.java
+@@ -313,7 +_,11 @@
+                 return Optional.empty();
+             } else {
+                 TestFunction testfunction = optional.get();
+-                GameTestInfo gametestinfo = new GameTestInfo(testfunction, structureblockentity.getRotation(), p_328153_, p_330368_);
++                // Forge: The rotation is stored in the structure block, and added in the GameTestInfo constructor.
++                // So reverse it to find the manually specified rotation so the test runs the same every time.
++                var steps = StructureUtils.getRotationStepsForRotation(structureblockentity.getRotation()) - StructureUtils.getRotationStepsForRotation(testfunction.rotation());
++                if (steps < 0) steps += 4;
++                GameTestInfo gametestinfo = new GameTestInfo(testfunction, StructureUtils.getRotationForRotationSteps(steps), p_328153_, p_330368_);
+                 gametestinfo.setStructureBlockPos(p_332856_);
+                 return !verifyStructureExists(p_328153_, gametestinfo.getStructureName()) ? Optional.empty() : Optional.of(gametestinfo);
+             }
diff --git a/src/test/java/net/minecraftforge/debug/gameplay/redstone/UpdateOrder.java b/src/test/java/net/minecraftforge/debug/gameplay/redstone/UpdateOrder.java
new file mode 100644
index 00000000000..65e66ec4317
--- /dev/null
+++ b/src/test/java/net/minecraftforge/debug/gameplay/redstone/UpdateOrder.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.debug.gameplay.redstone;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.gametest.framework.GameTest;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.entity.BarrelBlockEntity;
+import net.minecraft.world.level.block.entity.ChestBlockEntity;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraftforge.test.BaseTestMod;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.gametest.GameTestHolder;
+
+/**
+ * Redstone update orders are important in many contraptions as there are pistons and other things that could conflict with each other.
+ * These tests verify just that, two pistons pushing the same block.
+ *
+ * Partial verification/fix for https://github.com/MinecraftForge/MinecraftForge/issues/9973
+ */
+@Mod(UpdateOrder.MODID)
+@GameTestHolder("forge." + UpdateOrder.MODID)
+public class UpdateOrder extends BaseTestMod {
+    public static final String MODID = "update_order";
+    private static final int PISTON_DELAY = 4;
+
+    public UpdateOrder(FMLJavaModLoadingContext context) {
+        super(context);
+    }
+
+    @GameTest(template = "update_order:update_order", rotationSteps = 0)
+    public static void south(GameTestHelper helper) {
+        var chestPos = new BlockPos(0, 1, 0);
+        helper.assertBlockPresent(Blocks.CHEST, chestPos);
+        var chest = helper.<ChestBlockEntity>getBlockEntity(chestPos);
+        chest.setItem(0, new ItemStack(Items.DIRT));
+
+        helper.runAfterDelay(PISTON_DELAY, () -> {
+            var expectedPos = new BlockPos(2, 1, 3);
+            helper.assertBlockPresent(Blocks.WHITE_WOOL, expectedPos);
+            helper.succeed();
+        });
+    }
+
+    @GameTest(template = "update_order:update_order", rotationSteps = 1)
+    public static void west(GameTestHelper helper) {
+        var chestPos = new BlockPos(0, 1, 0);
+        helper.assertBlockPresent(Blocks.CHEST, chestPos);
+        var chest = helper.<ChestBlockEntity>getBlockEntity(chestPos);
+        chest.setItem(0, new ItemStack(Items.DIRT));
+
+        helper.runAfterDelay(PISTON_DELAY, () -> {
+            var expectedPos = new BlockPos(2, 1, 3);
+            helper.assertBlockPresent(Blocks.WHITE_WOOL, expectedPos);
+            helper.succeed();
+        });
+    }
+
+    @GameTest(template = "update_order:update_order", rotationSteps = 2)
+    public static void north(GameTestHelper helper) {
+        var chestPos = new BlockPos(0, 1, 0);
+        helper.assertBlockPresent(Blocks.CHEST, chestPos);
+        var chest = helper.<ChestBlockEntity>getBlockEntity(chestPos);
+        chest.setItem(0, new ItemStack(Items.DIRT));
+
+        helper.runAfterDelay(4, () -> {
+            var expectedPos = new BlockPos(3, 1, 2);
+            helper.assertBlockPresent(Blocks.WHITE_WOOL, expectedPos);
+            helper.succeed();
+        });
+    }
+
+    @GameTest(template = "update_order:update_order", rotationSteps = 3)
+    public static void east(GameTestHelper helper) {
+        var chestPos = new BlockPos(0, 1, 0);
+        helper.assertBlockPresent(Blocks.CHEST, chestPos);
+        var chest = helper.<ChestBlockEntity>getBlockEntity(chestPos);
+        chest.setItem(0, new ItemStack(Items.DIRT));
+
+        helper.runAfterDelay(PISTON_DELAY, () -> {
+            var expectedPos = new BlockPos(2, 1, 3);
+            helper.assertBlockPresent(Blocks.WHITE_WOOL, expectedPos);
+            helper.succeed();
+        });
+    }
+
+    @GameTest(template = "update_order:upward_update")
+    public static void up(GameTestHelper helper) {
+        var chestPos = new BlockPos(0, 1, 2);
+        var expectedPos = new BlockPos(2, 1, 0);
+        var unexpectedPos = new BlockPos(0, 4, 0);
+        helper.assertBlockPresent(Blocks.BARREL, chestPos);
+        helper.assertBlockPresent(Blocks.AIR, expectedPos);
+        helper.assertBlockPresent(Blocks.AIR, unexpectedPos);
+        var chest = helper.<BarrelBlockEntity>getBlockEntity(chestPos);
+        chest.setItem(0, new ItemStack(Items.DIRT));
+
+        helper.runAfterDelay(10, () -> { // There are a lot of things happening, give it a few ticks
+            helper.assertBlockNotPresent(Blocks.WHITE_WOOL, unexpectedPos);
+            helper.assertBlockPresent(Blocks.WHITE_WOOL, expectedPos);
+            helper.succeed();
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/update_order/data/update_order/structure/update_order.nbt b/src/test/resources/update_order/data/update_order/structure/update_order.nbt
new file mode 100644
index 00000000000..7a10f54b941
Binary files /dev/null and b/src/test/resources/update_order/data/update_order/structure/update_order.nbt differ
diff --git a/src/test/resources/update_order/data/update_order/structure/upward_update.nbt b/src/test/resources/update_order/data/update_order/structure/upward_update.nbt
new file mode 100644
index 00000000000..f192fa00846
Binary files /dev/null and b/src/test/resources/update_order/data/update_order/structure/upward_update.nbt differ