Limit channels per controller face (#1638)

This commit is contained in:
Worive authored and GitHub committed 2026-09-20 15:57:21 +02:00
1 parent 59c002bc5a
commit 3640653615
11 files changed
+493 -1

No files matched your search

+2
View File
@@ -37,6 +37,7 @@ dependencies {
}
compileOnly('curse.maven:rotarycraft-235596:4721191')
compileOnly('com.github.GTNewHorizons:Baubles-Expanded:2.2.23-GTNH:dev')
compileOnly('com.github.GTNewHorizons:RemoteIO:2.7.10:dev') { transitive = false }
compileOnly("org.jetbrains:annotations:26.0.2")
compileOnly("com.github.GTNewHorizons:Postea:1.2.6:dev")
compileOnly("com.github.GTNewHorizons:InventoryBogoSorter:1.3.50-GTNH:dev")
@@ -58,6 +59,7 @@ dependencies {
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Postea:1.2.6:dev")
runtimeOnlyNonPublishable('com.github.GTNewHorizons:waila:1.19.34:dev') {transitive = false}
runtimeOnlyNonPublishable("com.github.GTNewHorizons:DuraDisplay:1.4.6:dev")
runtimeOnlyNonPublishable('com.github.GTNewHorizons:RemoteIO:2.7.10:dev') { transitive = false }
runtimeOnlyNonPublishable('com.github.GTNewHorizons:Baubles-Expanded:2.2.23-GTNH:dev')
runtimeOnlyNonPublishable('thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev') {
exclude module: "Baubles"
@@ -0,0 +1,80 @@
package appeng.test.me;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import net.minecraftforge.common.util.ForgeDirection;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import appeng.api.networking.GridFlags;
import appeng.api.networking.IGridHost;
import appeng.me.GridConnection;
import appeng.me.GridNode;
import appeng.me.pathfinding.PathingCalculation;
import appeng.test.mockme.MockGridBlock;
import appeng.test.mockme.MockGridMachine;
import appeng.tile.networking.TileController;
public class ControllerFaceChannelsFunctionalTest {
private final List<GridNode> nodes = new ArrayList<>();
@AfterEach
public void destroyGrid() {
for (final GridNode node : this.nodes) {
node.destroy();
}
}
@Test
public void connectionsOnTheSameControllerFaceShareItsChannelCapacity() throws Exception {
final GridNode controller = this.node(new TileController(), GridFlags.CANNOT_CARRY, GridFlags.DENSE_CAPACITY);
for (int i = 0; i < 33; i++) {
new GridConnection(
controller,
this.node(new MockGridMachine(), GridFlags.REQUIRE_CHANNEL),
ForgeDirection.NORTH);
}
new GridConnection(
controller,
this.node(new MockGridMachine(), GridFlags.REQUIRE_CHANNEL),
ForgeDirection.SOUTH);
final PathingCalculation calculation = new PathingCalculation(controller.getGrid());
calculation.compute();
assertEquals(33, calculation.getChannelsInUse());
}
private GridNode node(final IGridHost machine, final GridFlags... flags) {
final EnumSet<GridFlags> nodeFlags = flags.length == 0 ? EnumSet.noneOf(GridFlags.class)
: EnumSet.copyOf(Arrays.asList(flags));
final GridNode node = new GridNode(new MockGridBlock() {
@Override
public IGridHost getMachine() {
return machine;
}
@Override
public EnumSet<GridFlags> getFlags() {
return nodeFlags;
}
@Override
public boolean hasFlag(final GridFlags flag) {
return nodeFlags.contains(flag);
}
});
node.updateState();
this.nodes.add(node);
return node;
}
}
@@ -0,0 +1,119 @@
package appeng.gametests.compatibility.appliedenergistics2_remoteio;
import static appeng.gametests.AEGameTestHelpers.assertActive;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.item.ItemStack;
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
import com.gtnewhorizons.horizonqa.api.TestPos;
import com.gtnewhorizons.horizonqa.api.TickCallbackHandle;
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
import appeng.api.AEApi;
import appeng.api.networking.IGridNode;
import appeng.core.AppEng;
import appeng.tile.networking.TileController;
import appeng.tile.storage.TileDrive;
import remoteio.common.core.TransferType;
import remoteio.common.lib.DimensionalCoords;
import remoteio.common.lib.ModBlocks;
import remoteio.common.lib.ModItems;
import remoteio.common.tile.TileRemoteInterface;
@GameTestHolder(value = AppEng.MOD_ID, requiredMods = "RIO")
public class RemoteIOChannelCompatibilityTests {
private static final int CHANNEL_DEVICE_COUNT = 33;
@GameTest(template = "compatibility/remoteio/dense_cable", timeoutTicks = 100)
public static void denseCableCarriesThirtyTwoChannels(GameTestHelper helper) {
TileController controller = helper.assertTileEntityPresent(TileController.class, "controller");
List<TileDrive> devices = channelDevices(helper, "channel_device_", CHANNEL_DEVICE_COUNT);
connectRemoteInterface(helper, "remote_interface");
assertActiveDevices(helper, controller, devices, 32);
}
@GameTest(template = "compatibility/remoteio/same_controller_face", timeoutTicks = 100)
public static void sameControllerFaceSharesThirtyTwoChannels(GameTestHelper helper) {
TileController controller = helper.assertTileEntityPresent(TileController.class, "controller");
List<TileDrive> devices = dualChannelDevices(helper);
connectRemoteInterface(helper, "remote_interface_a");
connectRemoteInterface(helper, "remote_interface_b");
assertActiveDevices(helper, controller, devices, 32);
}
@GameTest(template = "compatibility/remoteio/different_controller_faces", timeoutTicks = 100)
public static void differentControllerFacesHaveIndependentChannels(GameTestHelper helper) {
TileController controller = helper.assertTileEntityPresent(TileController.class, "controller");
moveSecondRemoteInterfaceToNorthSide(helper);
List<TileDrive> devices = dualChannelDevices(helper);
connectRemoteInterface(helper, "remote_interface_a");
connectRemoteInterface(helper, "remote_interface_b_north");
assertActiveDevices(helper, controller, devices, 33);
}
private static List<TileDrive> dualChannelDevices(GameTestHelper helper) {
List<TileDrive> devices = channelDevices(helper, "channel_device_a_", 16);
devices.addAll(channelDevices(helper, "channel_device_b_", 17));
return devices;
}
private static List<TileDrive> channelDevices(GameTestHelper helper, String labelPrefix, int count) {
List<TileDrive> devices = new ArrayList<>();
for (int i = 1; i <= count; i++) {
devices.add(helper.assertTileEntityPresent(TileDrive.class, labelPrefix + i));
}
return devices;
}
private static void connectRemoteInterface(GameTestHelper helper, String label) {
TileRemoteInterface remoteInterface = helper.assertTileEntityPresent(TileRemoteInterface.class, label);
TestPos controller = helper.absolute("controller");
remoteInterface.transferChips
.setInventorySlotContents(0, new ItemStack(ModItems.transferChip, 1, TransferType.NETWORK_AE));
remoteInterface.setRemotePosition(
new DimensionalCoords(helper.getWorld(), controller.x(), controller.y(), controller.z()));
}
private static void moveSecondRemoteInterfaceToNorthSide(GameTestHelper helper) {
helper.destroyBlock("remote_interface_b");
helper.destroyBlock("remote_interface_b_north");
helper.setBlock("channel_device_b_4", AEApi.instance().definitions().blocks().drive().maybeBlock().get());
helper.setBlock("remote_interface_b_north", ModBlocks.remoteInterface);
}
private static void assertActiveDevices(GameTestHelper helper, TileController controller, List<TileDrive> devices,
int expectedActive) {
Runnable assertAllocation = () -> {
assertActive(helper, controller.getProxy(), "Controller should become active");
IGridNode controllerNode = controller.getProxy().getNode();
int active = 0;
for (TileDrive device : devices) {
IGridNode deviceNode = device.getProxy().getNode();
helper.assertNotNull(deviceNode, "Every ME drive should have a grid node");
helper.assertSame(
controllerNode.getGrid(),
deviceNode.getGrid(),
"Every ME drive should join the controller grid through RemoteIO");
if (deviceNode.isActive()) {
active++;
}
}
helper.assertEquals(expectedActive, active, "Unexpected number of active devices");
};
TickCallbackHandle stableAllocation = helper
.onEachTickDisabled("RemoteIO channel allocation remains stable", assertAllocation);
helper.startSequence().thenWaitUntil("wait for RemoteIO channel allocation", 60, assertAllocation)
.thenExecute("begin stable channel-allocation observation", stableAllocation::enable).thenIdle(20)
.thenExecute("finish stable channel-allocation observation", stableAllocation::disable).thenSucceed();
}
}
@@ -12,16 +12,23 @@ package appeng.me.pathfinding;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import net.minecraftforge.common.util.ForgeDirection;
import appeng.api.networking.GridFlags;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridMultiblock;
import appeng.api.networking.IGridNode;
import appeng.core.AEConfig;
import appeng.core.AELog;
import appeng.core.features.AEFeature;
import appeng.me.GridConnection;
import appeng.me.GridNode;
import appeng.tile.networking.TileController;
@@ -34,13 +41,16 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
* <p>
* First, a BFS is performed starting from the controllers. This establishes a tree that connects all path items to a
* controller. As nodes that require channels are visited, they are assigned a channel if possible. This is done by
* checking the channel count of a few key nodes (max 3) along the path.
* checking the controller face and a few key nodes along the path.
* <p>
* Second, a DFS is performed to propagate the channel count upwards.
*/
public class PathingCalculation {
private static final int CONTROLLER_FACE_CHANNELS = 32;
private final IGrid grid;
private final int controllerFaceCapacity;
/**
* Path items that are part of a multiblock that was already granted a channel.
*/
@@ -62,6 +72,10 @@ public class PathingCalculation {
* are checked and updated.
*/
private final Reference2IntOpenHashMap<GridNode> channelBottlenecks = new Reference2IntOpenHashMap<>();
/**
* Associates every path item with the controller face that supplies its channels.
*/
private final Map<IPathItem, ControllerFace> controllerFaces = new IdentityHashMap<>();
/**
* Nodes that have been granted a channel during the BFS pass.
*/
@@ -80,13 +94,21 @@ public class PathingCalculation {
*/
public PathingCalculation(IGrid grid) {
this.grid = grid;
this.controllerFaceCapacity = AEConfig.instance.isFeatureEnabled(AEFeature.Channels) ? CONTROLLER_FACE_CHANNELS
: Integer.MAX_VALUE;
// Add every outgoing connection of the controllers (that doesn't point to another controller) to the list.
for (IGridNode node : grid.getMachines(TileController.class)) {
visited.add((IPathItem) node);
final Map<ForgeDirection, ControllerFace> faces = new EnumMap<>(ForgeDirection.class);
for (var gcc : node.getConnections()) {
var gc = (GridConnection) gcc;
if (!(gc.getOtherSide(node).getMachine() instanceof TileController)) {
controllerFaces.put(
gc,
faces.computeIfAbsent(
gc.getDirection(node),
ignored -> new ControllerFace(this.controllerFaceCapacity)));
enqueue(gc, 0);
gc.setControllerRoute((GridNode) node);
}
@@ -94,9 +116,15 @@ public class PathingCalculation {
}
for (IGridNode node : grid.getMachines(TileCreativeEnergyController.class)) {
visited.add((IPathItem) node);
final Map<ForgeDirection, ControllerFace> faces = new EnumMap<>(ForgeDirection.class);
for (var gcc : node.getConnections()) {
var gc = (GridConnection) gcc;
if (!(gc.getOtherSide(node).getMachine() instanceof TileController)) {
controllerFaces.put(
gc,
faces.computeIfAbsent(
gc.getDirection(node),
ignored -> new ControllerFace(this.controllerFaceCapacity)));
enqueue(gc, 0);
gc.setControllerRoute((GridNode) node);
}
@@ -143,6 +171,7 @@ public class PathingCalculation {
if (!this.visited.contains(pi)) {
// Set BFS parent.
pi.setControllerRoute(i);
this.controllerFaces.put(pi, this.controllerFaces.get(i));
if (pi.hasFlag(GridFlags.REQUIRE_CHANNEL)) {
if (!this.multiblocksWithChannel.contains(pi)) {
@@ -188,6 +217,11 @@ public class PathingCalculation {
return false;
}
final ControllerFace controllerFace = this.controllerFaces.get(start);
if (!controllerFace.canUseChannel()) {
return false;
}
// Check that the allocation is possible.
GridNode pi = start;
while (pi != null) {
@@ -205,10 +239,29 @@ public class PathingCalculation {
pi = pi.getHighestSimilarAncestor();
}
controllerFace.useChannel();
channelNodes.add(start);
return true;
}
static final class ControllerFace {
private final int capacity;
private int usedChannels;
ControllerFace(final int capacity) {
this.capacity = capacity;
}
boolean canUseChannel() {
return this.usedChannels < this.capacity;
}
void useChannel() {
this.usedChannels++;
}
}
private static final Object SUBTREE_END = new Object();
/**
@@ -0,0 +1,71 @@
{
"format_version": 2,
"size": [9, 1, 8],
"palette": {
"A": {"name": "RIO:tile.remote_interface", "meta": 0, "label": "Remote Interface"},
"B": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
"C": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
"D": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"}
},
"layers": [
[
"....C....",
".........",
"....A....",
"BBBBBBBBB",
"DDDDDDDDD",
"DDDDDDDDD",
"DDDDDDDDD",
"DDDDDD..."
]
],
"annotations": {
"labels": {
"channel_device_1": [0, 0, 4],
"channel_device_10": [0, 0, 5],
"channel_device_11": [1, 0, 5],
"channel_device_12": [2, 0, 5],
"channel_device_13": [3, 0, 5],
"channel_device_14": [4, 0, 5],
"channel_device_15": [5, 0, 5],
"channel_device_16": [6, 0, 5],
"channel_device_17": [7, 0, 5],
"channel_device_18": [8, 0, 5],
"channel_device_19": [0, 0, 6],
"channel_device_2": [1, 0, 4],
"channel_device_20": [1, 0, 6],
"channel_device_21": [2, 0, 6],
"channel_device_22": [3, 0, 6],
"channel_device_23": [4, 0, 6],
"channel_device_24": [5, 0, 6],
"channel_device_25": [6, 0, 6],
"channel_device_26": [7, 0, 6],
"channel_device_27": [8, 0, 6],
"channel_device_28": [0, 0, 7],
"channel_device_29": [1, 0, 7],
"channel_device_3": [2, 0, 4],
"channel_device_30": [2, 0, 7],
"channel_device_31": [3, 0, 7],
"channel_device_32": [4, 0, 7],
"channel_device_33": [5, 0, 7],
"channel_device_4": [3, 0, 4],
"channel_device_5": [4, 0, 4],
"channel_device_6": [5, 0, 4],
"channel_device_7": [6, 0, 4],
"channel_device_8": [7, 0, 4],
"channel_device_9": [8, 0, 4],
"controller": [4, 0, 0],
"controller_face": [4, 0, 1],
"dense_cable_1": [0, 0, 3],
"dense_cable_2": [1, 0, 3],
"dense_cable_3": [2, 0, 3],
"dense_cable_4": [3, 0, 3],
"dense_cable_5": [4, 0, 3],
"dense_cable_6": [5, 0, 3],
"dense_cable_7": [6, 0, 3],
"dense_cable_8": [7, 0, 3],
"dense_cable_9": [8, 0, 3],
"remote_interface": [4, 0, 2]
}
}
}
@@ -0,0 +1,74 @@
{
"format_version": 2,
"size": [15, 1, 9],
"palette": {
"A": {"name": "RIO:tile.remote_interface", "meta": 0, "label": "Remote Interface"},
"B": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
"C": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
"D": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"}
},
"layers": [
[
".......C.......",
"...............",
".A...........A.",
"BBBB.......BBBB",
"DDDD.......DDDD",
"DDDD.......DDDD",
"DDDD.......DDDD",
"DDDD.......DDDD",
"...........D..."
]
],
"annotations": {
"labels": {
"channel_device_a_1": [0, 0, 4],
"channel_device_a_10": [1, 0, 6],
"channel_device_a_11": [2, 0, 6],
"channel_device_a_12": [3, 0, 6],
"channel_device_a_13": [0, 0, 7],
"channel_device_a_14": [1, 0, 7],
"channel_device_a_15": [2, 0, 7],
"channel_device_a_16": [3, 0, 7],
"channel_device_a_2": [1, 0, 4],
"channel_device_a_3": [2, 0, 4],
"channel_device_a_4": [3, 0, 4],
"channel_device_a_5": [0, 0, 5],
"channel_device_a_6": [1, 0, 5],
"channel_device_a_7": [2, 0, 5],
"channel_device_a_8": [3, 0, 5],
"channel_device_a_9": [0, 0, 6],
"channel_device_b_1": [11, 0, 4],
"channel_device_b_10": [12, 0, 6],
"channel_device_b_11": [13, 0, 6],
"channel_device_b_12": [14, 0, 6],
"channel_device_b_13": [11, 0, 7],
"channel_device_b_14": [12, 0, 7],
"channel_device_b_15": [13, 0, 7],
"channel_device_b_16": [14, 0, 7],
"channel_device_b_17": [11, 0, 8],
"channel_device_b_2": [12, 0, 4],
"channel_device_b_3": [13, 0, 4],
"channel_device_b_4": [13, 0, 2],
"channel_device_b_5": [11, 0, 5],
"channel_device_b_6": [12, 0, 5],
"channel_device_b_7": [13, 0, 5],
"channel_device_b_8": [14, 0, 5],
"channel_device_b_9": [11, 0, 6],
"controller": [7, 0, 0],
"controller_face_a": [6, 0, 0],
"controller_face_b": [8, 0, 0],
"dense_cable_a_1": [0, 0, 3],
"dense_cable_a_2": [1, 0, 3],
"dense_cable_a_3": [2, 0, 3],
"dense_cable_a_4": [3, 0, 3],
"dense_cable_b_1": [11, 0, 3],
"dense_cable_b_2": [12, 0, 3],
"dense_cable_b_3": [13, 0, 3],
"dense_cable_b_4": [14, 0, 3],
"remote_interface_a": [1, 0, 2],
"remote_interface_b": [13, 0, 2],
"remote_interface_b_north": [14, 0, 4]
}
}
}
@@ -0,0 +1,72 @@
{
"format_version": 2,
"size": [15, 1, 9],
"palette": {
"A": {"name": "RIO:tile.remote_interface", "meta": 0, "label": "Remote Interface"},
"B": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
"C": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
"D": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"}
},
"layers": [
[
".......C.......",
"...............",
".A...........A.",
"BBBB.......BBBB",
"DDDD.......DDDD",
"DDDD.......DDDD",
"DDDD.......DDDD",
"DDDD.......DDDD",
"...........D..."
]
],
"annotations": {
"labels": {
"channel_device_a_1": [0, 0, 4],
"channel_device_a_10": [1, 0, 6],
"channel_device_a_11": [2, 0, 6],
"channel_device_a_12": [3, 0, 6],
"channel_device_a_13": [0, 0, 7],
"channel_device_a_14": [1, 0, 7],
"channel_device_a_15": [2, 0, 7],
"channel_device_a_16": [3, 0, 7],
"channel_device_a_2": [1, 0, 4],
"channel_device_a_3": [2, 0, 4],
"channel_device_a_4": [3, 0, 4],
"channel_device_a_5": [0, 0, 5],
"channel_device_a_6": [1, 0, 5],
"channel_device_a_7": [2, 0, 5],
"channel_device_a_8": [3, 0, 5],
"channel_device_a_9": [0, 0, 6],
"channel_device_b_1": [11, 0, 4],
"channel_device_b_10": [12, 0, 6],
"channel_device_b_11": [13, 0, 6],
"channel_device_b_12": [14, 0, 6],
"channel_device_b_13": [11, 0, 7],
"channel_device_b_14": [12, 0, 7],
"channel_device_b_15": [13, 0, 7],
"channel_device_b_16": [14, 0, 7],
"channel_device_b_17": [11, 0, 8],
"channel_device_b_2": [12, 0, 4],
"channel_device_b_3": [13, 0, 4],
"channel_device_b_4": [14, 0, 4],
"channel_device_b_5": [11, 0, 5],
"channel_device_b_6": [12, 0, 5],
"channel_device_b_7": [13, 0, 5],
"channel_device_b_8": [14, 0, 5],
"channel_device_b_9": [11, 0, 6],
"controller": [7, 0, 0],
"controller_face_b": [7, 0, 1],
"dense_cable_a_1": [0, 0, 3],
"dense_cable_a_2": [1, 0, 3],
"dense_cable_a_3": [2, 0, 3],
"dense_cable_a_4": [3, 0, 3],
"dense_cable_b_1": [11, 0, 3],
"dense_cable_b_2": [12, 0, 3],
"dense_cable_b_3": [13, 0, 3],
"dense_cable_b_4": [14, 0, 3],
"remote_interface_a": [1, 0, 2],
"remote_interface_b": [13, 0, 2]
}
}
}
@@ -0,0 +1,21 @@
package appeng.me.pathfinding;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class PathingCalculationTest {
@Test
public void controllerFaceEnforcesItsChannelCapacity() {
final PathingCalculation.ControllerFace face = new PathingCalculation.ControllerFace(32);
for (int i = 0; i < 32; i++) {
assertTrue(face.canUseChannel());
face.useChannel();
}
assertFalse(face.canUseChannel());
}
}