diff --git a/src/main/java/appeng/core/AEConfig.java b/src/main/java/appeng/core/AEConfig.java index 53421b9621b..6cabf4e4e35 100644 --- a/src/main/java/appeng/core/AEConfig.java +++ b/src/main/java/appeng/core/AEConfig.java @@ -332,10 +332,6 @@ public void setChannelModel(ChannelMode mode) { } } - public int getPathfindingStepsPerTick() { - return common.pathfindingStepsPerTick.get(); - } - /** * @return True if an in-world preview of parts and facade placement should be shown when holding one in hand. */ @@ -531,7 +527,6 @@ private static class CommonConfig { public final BooleanValue matterCannonBlockDamage; public final BooleanValue tinyTntBlockDamage; public final EnumValue channels; - public final IntValue pathfindingStepsPerTick; public final BooleanValue spatialAnchorEnableRandomTicks; public final IntValue growthAcceleratorSpeed; @@ -601,9 +596,6 @@ public CommonConfig() { "Enables the ability of Tiny TNT to break blocks."); channels = defineEnum(builder, "channels", ChannelMode.DEFAULT, "Changes the channel capacity that cables provide in AE2."); - pathfindingStepsPerTick = define(builder, "pathfindingStepsPerTick", 4, - 1, 1024, - "The number of pathfinding steps that are taken per tick and per grid that is booting. Lower numbers will mean booting takes longer, but less work is done per tick."); spatialAnchorEnableRandomTicks = define(builder, "spatialAnchorEnableRandomTicks", true, "Whether Spatial Anchors should force random chunk ticks and entity spawning."); builder.pop(); diff --git a/src/main/java/appeng/debug/DebugCardItem.java b/src/main/java/appeng/debug/DebugCardItem.java index 0ddec606a4d..611ec2117b5 100644 --- a/src/main/java/appeng/debug/DebugCardItem.java +++ b/src/main/java/appeng/debug/DebugCardItem.java @@ -213,7 +213,7 @@ public InteractionResult onItemUseFirst(ItemStack stack, UseOnContext context) { partHost.markForUpdate(); if (center != null) { final GridNode n = (GridNode) center.getGridNode(); - this.outputSecondaryMessage(player, "Node Channels", Integer.toString(n.usedChannels())); + this.outputSecondaryMessage(player, "Node Channels", Integer.toString(n.getUsedChannels())); for (var entry : n.getInWorldConnections().entrySet()) { this.outputSecondaryMessage(player, "Channels " + entry.getKey().getName(), Integer.toString(entry.getValue().getUsedChannels())); diff --git a/src/main/java/appeng/me/GridConnection.java b/src/main/java/appeng/me/GridConnection.java index 0741d29dc41..cd513c3c3df 100644 --- a/src/main/java/appeng/me/GridConnection.java +++ b/src/main/java/appeng/me/GridConnection.java @@ -36,7 +36,13 @@ public class GridConnection implements IGridConnection, IPathItem { - private int usedChannels = 0; + /** + * Will be modified during pathing and should not be exposed outside of that purpose. + */ + int usedChannels = 0; + /** + * Finalized version of {@link #usedChannels} once pathing is done. + */ private int lastUsedChannels = 0; private Object visitorIterationNumber = null; /** @@ -93,12 +99,12 @@ public void destroy() { } @Override - public IGridNode a() { + public GridNode a() { return this.sideA; } @Override - public IGridNode b() { + public GridNode b() { return this.sideB; } @@ -109,20 +115,22 @@ public boolean isInWorld() { @Override public int getUsedChannels() { - return usedChannels; + return lastUsedChannels; } @Override - public IPathItem getControllerRoute() { - if (this.sideA.hasFlag(GridFlags.CANNOT_CARRY)) { - return null; - } + public void setAdHocChannels(int channels) { + this.usedChannels = channels; + } + + @Override + public GridNode getControllerRoute() { return this.sideA; } @Override public void setControllerRoute(IPathItem fast) { - this.lastUsedChannels = 0; + this.usedChannels = 0; // If the shortest route to the controller is via side B, we need to flip the // connections sides because side A should be the closest route to the controller. @@ -136,11 +144,6 @@ public void setControllerRoute(IPathItem fast) { } } - @Override - public boolean canSupportMoreChannels() { - return this.getLastUsedChannels() < getMaxChannels(); - } - @Override public int getMaxChannels() { var mode = sideB.getGrid().getPathingService().getChannelMode(); @@ -152,12 +155,7 @@ public int getMaxChannels() { @Override public Iterable getPossibleOptions() { - return ImmutableList.of((IPathItem) this.a(), (IPathItem) this.b()); - } - - @Override - public void incrementChannelCount(int usedChannels) { - this.lastUsedChannels += usedChannels; + return ImmutableList.of(this.a(), this.b()); } @Override @@ -165,10 +163,19 @@ public boolean hasFlag(GridFlags flag) { return false; } + public int propagateChannelsUpwards() { + if (this.sideB.getControllerRoute() == this) { // Check that we are in B's route + this.usedChannels = this.sideB.usedChannels; + } else { + this.usedChannels = 0; + } + return this.usedChannels; + } + @Override public void finalizeChannels() { - if (this.getUsedChannels() != this.getLastUsedChannels()) { - this.usedChannels = this.lastUsedChannels; + if (this.lastUsedChannels != this.usedChannels) { + this.lastUsedChannels = this.usedChannels; if (this.sideA.getInternalGrid() != null) { this.sideA.notifyStatusChange(IGridNodeListener.State.CHANNEL); @@ -180,10 +187,6 @@ public void finalizeChannels() { } } - private int getLastUsedChannels() { - return this.lastUsedChannels; - } - Object getVisitorIterationNumber() { return this.visitorIterationNumber; } diff --git a/src/main/java/appeng/me/GridNode.java b/src/main/java/appeng/me/GridNode.java index 885da602266..9a03525809e 100644 --- a/src/main/java/appeng/me/GridNode.java +++ b/src/main/java/appeng/me/GridNode.java @@ -35,8 +35,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.MutableClassToInstanceMap; import com.google.gson.stream.JsonWriter; +import com.mojang.logging.LogUtils; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; import net.minecraft.CrashReportCategory; import net.minecraft.core.Direction; @@ -64,12 +66,15 @@ import appeng.api.parts.IPart; import appeng.api.stacks.AEItemKey; import appeng.api.util.AEColor; +import appeng.blockentity.networking.ControllerBlockEntity; import appeng.core.AELog; import appeng.me.pathfinding.IPathItem; import appeng.util.IDebugExportable; import appeng.util.JsonStreamUtil; public class GridNode implements IGridNode, IPathItem, IDebugExportable { + private static final Logger LOGGER = LogUtils.getLogger(); + private final ServerLevel level; /** * This is the logical host of the node, which could be any object. In many cases this will be a block entity or @@ -96,9 +101,33 @@ public class GridNode implements IGridNode, IPathItem, IDebugExportable { private int owningPlayerId = -1; private Grid myGrid; private Object visitorIterationNumber = null; - // connection criteria - private int usedChannels = 0; + /** + * Will be modified during pathing and should not be exposed outside of that purpose. + */ + int usedChannels = 0; + /** + * Finalized version of {@link #usedChannels} once pathing is done. + */ private int lastUsedChannels = 0; + /** + * The nearest ancestor of this node which restricts the number of maximum available channels for its subtree. It is + * {@code null} if the next node is a controller. + *

+ * Used to quickly walk the path to the controller when checking channel assignability, based on the observation + * that the max channel count increases as we get to the controller, and that we only need to check the highest node + * of each max channel count. + *

+ * For example, on the following path: + * {@code controller - dense cable 1 - dense cable 2 - dense cable 3 - cable 1 - cable 2 - cable 3 - device}, we + * need to check that {@code dense cable 1} can accept the additional channel. If this is true then dense cables + * {@code 2} and {@code 3} can always accept it. Same for regular cables, so it is enough to check that + * {@code dense cable 1} and {@code cable 1} can accept it, massively speeding up the assignment for large trees. + */ + @Nullable + private GridNode highestSimilarAncestor = null; + private int subtreeMaxChannels; + private boolean subtreeAllowsCompressedChannels; + private final EnumSet flags; private ClassToInstanceMap services; @@ -124,10 +153,6 @@ Grid getMyGrid() { return this.myGrid; } - public int usedChannels() { - return this.lastUsedChannels; - } - @FunctionalInterface public interface ListenerCallback { void call(IGridNodeListener listener, T owner, IGridNode node); @@ -560,19 +585,54 @@ private void visitorNode(Object tracker, IGridVisitor g, Deque nextRun } } + @Override + public void setAdHocChannels(int channels) { + this.usedChannels = channels; + } + @Override public IPathItem getControllerRoute() { - if (this.connections.isEmpty() || this.hasFlag(GridFlags.CANNOT_CARRY)) { - return null; + if (this.connections.isEmpty()) { + throw new IllegalStateException( + "Node %s has no connections, cannot have a controller route!".formatted(this)); } - return this.connections.get(0); + return this.connections.getFirst(); + } + + public @Nullable GridNode getHighestSimilarAncestor() { + return highestSimilarAncestor; + } + + public boolean getSubtreeAllowsCompressedChannels() { + return subtreeAllowsCompressedChannels; } @Override public void setControllerRoute(IPathItem fast) { this.usedChannels = 0; + var nodeParent = (GridNode) fast.getControllerRoute(); + if (nodeParent.getOwner() instanceof ControllerBlockEntity) { + this.highestSimilarAncestor = null; + this.subtreeMaxChannels = getMaxChannels(); + this.subtreeAllowsCompressedChannels = !hasFlag(GridFlags.CANNOT_CARRY_COMPRESSED); + } else { + if (nodeParent.highestSimilarAncestor == null) { + // Parent is connected to a controller, it is the bottleneck. + this.highestSimilarAncestor = nodeParent; + } else if (nodeParent.subtreeMaxChannels == nodeParent.highestSimilarAncestor.subtreeMaxChannels) { + // Parent is not restricting the number of channels, go as high as possible. + this.highestSimilarAncestor = nodeParent.highestSimilarAncestor; + } else { + // Parent is restricting the number of channels, link to it directly. + this.highestSimilarAncestor = nodeParent; + } + this.subtreeMaxChannels = Math.min(nodeParent.subtreeMaxChannels, getMaxChannels()); + this.subtreeAllowsCompressedChannels = nodeParent.subtreeAllowsCompressedChannels + && !hasFlag(GridFlags.CANNOT_CARRY_COMPRESSED); + } + GridConnection connection = (GridConnection) fast; final int idx = this.connections.indexOf(connection); @@ -582,14 +642,9 @@ public void setControllerRoute(IPathItem fast) { } } - @Override - public boolean canSupportMoreChannels() { - return this.getUsedChannels() < this.getMaxChannels(); - } - @Override public int getUsedChannels() { - return this.usedChannels; + return this.lastUsedChannels; } @Override @@ -615,18 +670,39 @@ public Iterable getPossibleOptions() { return ImmutableList.copyOf(this.connections); } - @Override + public int propagateChannelsUpwards(boolean consumesChannel) { + this.usedChannels = 0; + for (var connection : connections) { + if (connection.getControllerRoute() == this) { + this.usedChannels += connection.usedChannels; + } + } + if (consumesChannel) { + this.usedChannels++; + } + + if (this.usedChannels > getMaxChannels()) { + LOGGER.error( + "Internal channel assignment error. Grid node {} has {} channels passing through it but it only supports up to {}. Please open an issue on the AE2 repository.", + this, this.usedChannels, getMaxChannels()); + } + + return this.usedChannels; + } + public void incrementChannelCount(int usedChannels) { this.usedChannels += usedChannels; } @Override public void finalizeChannels() { + this.highestSimilarAncestor = null; + if (hasFlag(GridFlags.CANNOT_CARRY)) { return; } - if (this.lastUsedChannels != this.getUsedChannels()) { + if (this.lastUsedChannels != this.usedChannels) { this.lastUsedChannels = this.usedChannels; if (this.getInternalGrid() != null) { diff --git a/src/main/java/appeng/me/pathfinding/AdHocChannelUpdater.java b/src/main/java/appeng/me/pathfinding/AdHocChannelUpdater.java index 6e7e06f7529..83656ae1dd6 100644 --- a/src/main/java/appeng/me/pathfinding/AdHocChannelUpdater.java +++ b/src/main/java/appeng/me/pathfinding/AdHocChannelUpdater.java @@ -35,15 +35,13 @@ public AdHocChannelUpdater(int used) { @Override public boolean visitNode(IGridNode n) { final GridNode gn = (GridNode) n; - gn.setControllerRoute(null); - gn.incrementChannelCount(this.usedChannels); + gn.setAdHocChannels(this.usedChannels); return true; } @Override public void visitConnection(IGridConnection gcc) { final GridConnection gc = (GridConnection) gcc; - gc.setControllerRoute(null); - gc.incrementChannelCount(this.usedChannels); + gc.setAdHocChannels(this.usedChannels); } } diff --git a/src/main/java/appeng/me/pathfinding/IPathItem.java b/src/main/java/appeng/me/pathfinding/IPathItem.java index f1ddc40e9b1..8272749fd76 100644 --- a/src/main/java/appeng/me/pathfinding/IPathItem.java +++ b/src/main/java/appeng/me/pathfinding/IPathItem.java @@ -22,14 +22,18 @@ public interface IPathItem { - IPathItem getControllerRoute(); + /* USED BY AD HOC PATHING */ - void setControllerRoute(IPathItem fast); + void setAdHocChannels(int channels); + + /* USED BY CONTROLLER PATHING */ + + IPathItem getControllerRoute(); /** - * used to determine if the finder can continue. + * Sets route to controller. */ - boolean canSupportMoreChannels(); + void setControllerRoute(IPathItem fast); /** * The maximum number of channels connections to this path item can carry. @@ -37,20 +41,17 @@ public interface IPathItem { int getMaxChannels(); /** - * find possible choices for other pathing. + * Find possible choices for other pathing. */ Iterable getPossibleOptions(); - /** - * add one to the channel count, this is mostly for cables. - */ - void incrementChannelCount(int usedChannels); - /** * Tests if this path item has the specific grid flag set. */ boolean hasFlag(GridFlags flag); + /* USED BY BOTH */ + /** * channels are done, wrap it up. */ diff --git a/src/main/java/appeng/me/pathfinding/PathingCalculation.java b/src/main/java/appeng/me/pathfinding/PathingCalculation.java index d46bc4197cb..f2b49ec8ecd 100644 --- a/src/main/java/appeng/me/pathfinding/PathingCalculation.java +++ b/src/main/java/appeng/me/pathfinding/PathingCalculation.java @@ -18,11 +18,15 @@ package appeng.me.pathfinding; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Set; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; + import appeng.api.networking.GridFlags; import appeng.api.networking.IGrid; import appeng.api.networking.IGridMultiblock; @@ -32,27 +36,43 @@ import appeng.me.GridNode; /** - * Calculation to assign channels starting from the controllers. Basically a BFS, with one step each tick. + * Calculation to assign channels starting from the controllers. The full computation is split in two steps, each linear + * time. + *

+ * 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. + *

+ * Second, a DFS is performed to propagate the channel count upwards. */ public class PathingCalculation { - + private final IGrid grid; /** * Path items that are part of a multiblock that was already granted a channel. */ - private final Set multiblocksWithChannel = new HashSet<>(); + private final Set multiblocksWithChannel = new HashSet<>(); /** * The BFS queues: all the path items that need to be visited on the next tick. Dense queue is prioritized to have * the behavior of dense cables extending the controller faces, then cables, then normal devices. */ - private List[] queues = new List[] { - new ArrayList<>(), // 0: dense cable queue - new ArrayList<>(), // 1: normal cable queue - new ArrayList<>() // 2: non-cable queue + private final Queue[] queues = new Queue[] { + new ArrayDeque<>(), // 0: dense cable queue + new ArrayDeque<>(), // 1: normal cable queue + new ArrayDeque<>() // 2: non-cable queue }; /** - * Path items that are either in the queue, or have been processed already. + * Path items that are either in a queue, or have been processed already. */ private final Set visited = new HashSet<>(); + /** + * Tracks the number of channels assigned to each path item during the BFS pass. Only a few key nodes along any path + * are checked and updated. + */ + private final Reference2IntOpenHashMap channelBottlenecks = new Reference2IntOpenHashMap<>(); + /** + * Nodes that have been granted a channel during the BFS pass. + */ + private final Set channelNodes = new HashSet<>(); /** * Tracks the total number of used channels. */ @@ -66,6 +86,8 @@ public class PathingCalculation { * Create a new pathing calculation from the passed grid. */ public PathingCalculation(IGrid grid) { + this.grid = grid; + // Add every outgoing connection of the controllers (that doesn't point to another controller) to the list. for (var node : grid.getMachineNodes(ControllerBlockEntity.class)) { visited.add((IPathItem) node); @@ -101,34 +123,28 @@ private void enqueue(IPathItem pathItem, int queueIndex) { queues[index].add(pathItem); } - public void step() { - // Keep processing dense queue as long as it's not empty. + public void compute() { + // BFS pass for (int i = 0; i < 3; ++i) { - if (!queues[i].isEmpty()) { - List oldOpen = queues[i]; - queues[i] = new ArrayList<>(); - processQueue(oldOpen, i); - break; - } + processQueue(queues[i], i); } + + // DFS pass + propagateAssignments(); } - private void processQueue(List oldOpen, int queueIndex) { - for (IPathItem i : oldOpen) { + private void processQueue(Queue oldOpen, int queueIndex) { + while (!oldOpen.isEmpty()) { + IPathItem i = oldOpen.poll(); for (IPathItem pi : i.getPossibleOptions()) { if (!this.visited.contains(pi)) { // Set BFS parent. pi.setControllerRoute(i); if (pi.hasFlag(GridFlags.REQUIRE_CHANNEL)) { - if (this.multiblocksWithChannel.contains(pi)) { - // If this is part of a multiblock that was given a channel before, just give a channel to - // the node. - pi.incrementChannelCount(1); - this.multiblocksWithChannel.remove(pi); - } else { - // Otherwise try to use the channel along the path. - boolean worked = tryUseChannel(pi); + if (!this.multiblocksWithChannel.contains(pi)) { + // Try to use the channel along the path. + boolean worked = tryUseChannel((GridNode) pi); if (worked && pi.hasFlag(GridFlags.MULTIBLOCK)) { var multiblock = ((IGridNode) pi).getService(IGridMultiblock.class); @@ -137,7 +153,7 @@ private void processQueue(List oldOpen, int queueIndex) { while (oni.hasNext()) { final IGridNode otherNodes = oni.next(); if (otherNodes != pi) { - this.multiblocksWithChannel.add((IPathItem) otherNodes); + this.multiblocksWithChannel.add((GridNode) otherNodes); } } } @@ -156,42 +172,84 @@ private void processQueue(List oldOpen, int queueIndex) { * * @return true if allocation was successful */ - private boolean tryUseChannel(IPathItem start) { - boolean isCompressed = start.hasFlag(GridFlags.COMPRESSED_CHANNEL); + private boolean tryUseChannel(GridNode start) { + if (start.hasFlag(GridFlags.COMPRESSED_CHANNEL) && !start.getSubtreeAllowsCompressedChannels()) { + // Don't send a compressed channel through this item. + return false; + } // Check that the allocation is possible. - IPathItem pi = start; + GridNode pi = start; while (pi != null) { - if (!pi.canSupportMoreChannels()) { - return false; - } - if (isCompressed && pi.hasFlag(GridFlags.CANNOT_CARRY_COMPRESSED)) { - // Don't send a compressed channel through this item. + if (channelBottlenecks.getOrDefault(pi, 0) >= pi.getMaxChannels()) { return false; } - pi = pi.getControllerRoute(); + pi = pi.getHighestSimilarAncestor(); } // Allocate the channel along the path. pi = start; while (pi != null) { - channelsByBlocks++; - pi.incrementChannelCount(1); - pi = pi.getControllerRoute(); + channelBottlenecks.addTo(pi, 1); + pi = pi.getHighestSimilarAncestor(); } - channelsInUse++; + channelNodes.add(start); return true; } - public boolean isFinished() { - for (List queue : queues) { - if (!queue.isEmpty()) { - return false; + private static final Object SUBTREE_END = new Object(); + + /** + * Propagates assignment to all nodes by performing a DFS. The implementation is iterative to avoid stack overflow. + */ + private void propagateAssignments() { + List stack = new ArrayList<>(); + Set controllerNodes = new HashSet<>(); + + for (var node : grid.getMachineNodes(ControllerBlockEntity.class)) { + controllerNodes.add((IPathItem) node); + for (var gcc : node.getConnections()) { + var gc = (GridConnection) gcc; + if (!(gc.getOtherSide(node).getOwner() instanceof ControllerBlockEntity)) { + stack.add(gc); + } } } - return true; + + while (!stack.isEmpty()) { + Object current = stack.getLast(); + if (current == SUBTREE_END) { + stack.removeLast(); + IPathItem item = (IPathItem) stack.removeLast(); + // We have visited the entire subtree and can now propagate channels upwards. + if (item instanceof GridNode node) { + boolean hasChannel = channelNodes.contains(item); + channelsByBlocks += node.propagateChannelsUpwards(hasChannel); + if (hasChannel) { + channelsInUse++; + } + } else { + channelsByBlocks += ((GridConnection) item).propagateChannelsUpwards(); + } + } else { + stack.add(SUBTREE_END); + for (var pi : ((IPathItem) current).getPossibleOptions()) { + // The neighbor could either be: a child, the parent, or in a different tree if it is closer to + // another controller. It is a child if we are its parent. + // We need to exclude controller nodes because their getControllerRoute() is nonsense. + if (!controllerNodes.contains(pi) && pi.getControllerRoute() == current) { + stack.add(pi); + } + } + } + } + + // Give a channel to all nodes that are a part of a multiblock that was given a channel before. + for (var multiblockNode : multiblocksWithChannel) { + multiblockNode.incrementChannelCount(1); + } } public int getChannelsInUse() { diff --git a/src/main/java/appeng/me/service/PathingService.java b/src/main/java/appeng/me/service/PathingService.java index 3514ab7571f..fdc34c71395 100644 --- a/src/main/java/appeng/me/service/PathingService.java +++ b/src/main/java/appeng/me/service/PathingService.java @@ -62,7 +62,6 @@ public class PathingService implements IPathingService, IGridServiceProvider { }); } - private PathingCalculation ongoingCalculation = null; private final Set controllers = new HashSet<>(); private final Set nodesNeedingChannels = new HashSet<>(); private final Set cannotCarryCompressedNodes = new HashSet<>(); @@ -74,7 +73,6 @@ public class PathingService implements IPathingService, IGridServiceProvider { // Flag to indicate a reboot should occur next tick private boolean reboot = true; private boolean booting = false; - private int bootingTicks = 0; @Nullable private AdHocNetworkError adHocNetworkError; private ControllerState controllerState = ControllerState.NO_CONTROLLER; @@ -99,11 +97,9 @@ public void onServerEndTick() { if (this.reboot) { this.reboot = false; - if (!this.booting) { - this.booting = true; - this.bootingTicks = 0; - this.postBootingStatusChange(); - } + // Preserve the illusion that the network is booting for a while before channel assignment completes. + this.booting = true; + this.postBootingStatusChange(); this.channelsInUse = 0; this.adHocNetworkError = null; @@ -125,41 +121,24 @@ public void onServerEndTick() { this.grid.getPivot().beginVisit(new AdHocChannelUpdater(this.channelsInUse)); } else if (this.controllerState == ControllerState.CONTROLLER_CONFLICT) { this.grid.getPivot().beginVisit(new AdHocChannelUpdater(0)); + this.channelsInUse = 0; + this.channelsByBlocks = 0; } else { - this.ongoingCalculation = new PathingCalculation(grid); - } - } - - if (this.booting) { - // Work on remaining pathfinding work - if (ongoingCalculation != null) { // can be null for ad-hoc or invalid controller state - for (var i = 0; i < AEConfig.instance().getPathfindingStepsPerTick(); i++) { - ongoingCalculation.step(); - if (ongoingCalculation.isFinished()) { - this.channelsByBlocks = ongoingCalculation.getChannelsByBlocks(); - this.channelsInUse = ongoingCalculation.getChannelsInUse(); - ongoingCalculation = null; - break; - } - } + var calculation = new PathingCalculation(grid); + calculation.compute(); + this.channelsInUse = calculation.getChannelsInUse(); + this.channelsByBlocks = calculation.getChannelsByBlocks(); } - bootingTicks++; - - // Booting completes when both pathfinding completes, and the minimum boot time has elapsed - if (ongoingCalculation == null) { - // check for achievements - this.achievementPost(); + // check for achievements + this.achievementPost(); - this.booting = false; - this.setChannelPowerUsage(this.channelsByBlocks / 128.0); - // Notify of channel changes AFTER we set booting to false, this ensures that any activeness check will - // properly return true. - this.grid.getPivot().beginVisit(new ChannelFinalizer()); - this.postBootingStatusChange(); - } else if (bootingTicks == 2000) { - AELog.warn("Booting has still not completed after %d ticks for %s", bootingTicks, grid); - } + this.booting = false; + this.setChannelPowerUsage(this.channelsByBlocks / 128.0); + // Notify of channel changes AFTER we set booting to false, this ensures that any activeness check will + // properly return true. + this.grid.getPivot().beginVisit(new ChannelFinalizer()); + this.postBootingStatusChange(); } } @@ -345,9 +324,6 @@ public void repath() { this.channelMode = AEConfig.instance().getChannelMode(); } - // clean up... - this.ongoingCalculation = null; - this.channelsByBlocks = 0; this.reboot = true; } diff --git a/src/main/java/appeng/server/testplots/ChannelTests.java b/src/main/java/appeng/server/testplots/ChannelTests.java new file mode 100644 index 00000000000..073981e6436 --- /dev/null +++ b/src/main/java/appeng/server/testplots/ChannelTests.java @@ -0,0 +1,200 @@ +package appeng.server.testplots; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import net.minecraft.core.BlockPos; + +import appeng.api.networking.IGridConnection; +import appeng.api.networking.IGridConnectionVisitor; +import appeng.api.networking.IGridNode; +import appeng.core.definitions.AEBlocks; +import appeng.server.testworld.PlotBuilder; +import appeng.server.testworld.PlotTestHelper; + +@TestPlotClass +public class ChannelTests { + @TestPlot("channel_assignment_test") + public static void channelAssignmentTest(PlotBuilder plot) { + plot.block("[-1,1] 0 0", AEBlocks.CONTROLLER); + plot.creativeEnergyCell("0 -1 0"); + plot.block("0 1 0", AEBlocks.ME_CHEST); + plot.denseCable("0 0 [1,3]"); + plot.block("0 0 4", AEBlocks.PATTERN_PROVIDER); + plot.cable("[1,2] 0 3"); + plot.block("3 [0,1] [3,4]", AEBlocks.CRAFTING_STORAGE_4K); + plot.cable("[4,9] 0 3"); + plot.block("5 1 3", AEBlocks.INTERFACE); + plot.block("5 -1 3", AEBlocks.INTERFACE); + plot.block("5 0 2", AEBlocks.INTERFACE); + plot.block("5 0 4", AEBlocks.INTERFACE); + plot.block("7 -1 3", AEBlocks.INTERFACE); + plot.block("7 0 2", AEBlocks.INTERFACE); + plot.block("7 0 4", AEBlocks.INTERFACE); + plot.block("10 0 3", AEBlocks.CRAFTING_STORAGE_256K); + + plot.test(helper -> { + helper.startSequence() + .thenWaitUntil(() -> { + var grid = helper.getGrid(BlockPos.ZERO); + helper.check(!grid.getPathingService().isNetworkBooting(), "Network is still booting"); + }) + .thenExecute(() -> { + var checker = new ChannelChecker(plot, helper); + + // No channels through controllers + checker.node("[-1,1] 0 0", 0); + checker.connection("-1 0 0", "0 0 0", 0); + checker.connection("0 0 0", "1 0 0", 0); + + checker.leafNode("0 -1 0", 0); + checker.leafNode("0 1 0", 1); + + checker.node("0 0 [1,3]", 9); + checker.connection("0 0 0", "0 0 1", 9); + checker.connection("0 0 1", "0 0 2", 9); + checker.connection("0 0 2", "0 0 3", 9); + + checker.leafNode("0 0 4", 1); + + checker.node("[1,2] 0 3", 8); + checker.connection("0 0 3", "1 0 3", 8); + checker.connection("1 0 3", "2 0 3", 8); + checker.connection("2 0 3", "3 0 3", 8); + + // Multiblocks are a bit special: each node gets +1 channel + checker.node("3 0 3", 8); + checker.node("3 1 3", 1); + checker.node("3 [0,1] 4", 1); + checker.connection("3 0 [3,4]", "3 1 [3,4]", 0); + checker.connection("3 [0,1] 3", "3 [0,1] 4", 0); + + checker.connection("3 0 3", "4 0 3", 7); + checker.node("4 0 3", 7); + checker.connection("4 0 3", "5 0 3", 7); + checker.node("5 0 3", 7); + checker.connection("5 0 3", "6 0 3", 3); + checker.node("6 0 3", 3); + checker.connection("6 0 3", "7 0 3", 3); + checker.node("7 0 3", 3); + checker.connection("7 0 3", "8 0 3", 0); + checker.node("8 0 3", 0); + checker.connection("8 0 3", "9 0 3", 0); + checker.node("9 0 3", 0); + + checker.leafNode("5 1 3", 1); + checker.leafNode("5 -1 3", 1); + checker.leafNode("5 0 2", 1); + checker.leafNode("5 0 4", 1); + checker.leafNode("7 -1 3", 1); + checker.leafNode("7 0 2", 1); + checker.leafNode("7 0 4", 1); + + checker.leafNode("10 0 3", 0); + + checker.ensureEverythingWasChecked(); + }) + .thenSucceed(); + }); + } + + private static class ChannelChecker { + private final PlotBuilder plot; + private final PlotTestHelper helper; + private final Set nodes = new HashSet<>(); + private final Set connections = new HashSet<>(); + + private ChannelChecker(PlotBuilder plot, PlotTestHelper helper) { + this.plot = plot; + this.helper = helper; + helper.getGrid(BlockPos.ZERO).getPivot().beginVisit(new IGridConnectionVisitor() { + @Override + public void visitConnection(IGridConnection n) { + connections.add(n); + } + + @Override + public boolean visitNode(IGridNode n) { + nodes.add(n); + return true; + } + }); + } + + private void forEachInBb(String bb, Consumer action) { + BlockPos.betweenClosedStream(plot.bb(bb)).forEach(action); + } + + private void checkNode(BlockPos pos, IGridNode node, int expectedChannelCount) { + if (nodes.contains(node)) { + if (node.getUsedChannels() != expectedChannelCount) { + helper.fail("Node has wrong channel count. Expected %d. Got %d.".formatted(expectedChannelCount, + node.getUsedChannels()), pos); + } + nodes.remove(node); + } else { + helper.fail("Node is not in the grid or it was already checked", pos); + } + } + + private void checkConnection(BlockPos pos, IGridConnection connection, int expectedChannelCount) { + if (connections.contains(connection)) { + if (connection.getUsedChannels() != expectedChannelCount) { + helper.fail("Connection has wrong channel count. Expected %d. Got %d." + .formatted(expectedChannelCount, connection.getUsedChannels()), pos); + } + connections.remove(connection); + } else { + helper.fail("Connection is not in the grid or it was already checked", pos); + } + } + + public void node(String bb, int expectedChannelCount) { + forEachInBb(bb, pos -> { + var node = helper.getGridNode(pos); + checkNode(pos, node, expectedChannelCount); + }); + } + + /** + * Checks both the node and its only connection. + */ + public void leafNode(String bb, int expectedChannelCount) { + forEachInBb(bb, pos -> { + var node = helper.getGridNode(pos); + checkNode(pos, node, expectedChannelCount); + + helper.check(node.getConnections().size() == 1, "Node does not have exactly one connection", pos); + checkConnection(pos, node.getConnections().getFirst(), expectedChannelCount); + }); + } + + public void connection(String bb, String bb2, int expectedChannelCount) { + AtomicBoolean foundAny = new AtomicBoolean(); + + forEachInBb(bb, pos -> { + var node = helper.getGridNode(pos); + forEachInBb(bb2, pos2 -> { + var node2 = helper.getGridNode(pos2); + for (var connection : node.getConnections()) { + if (connection.getOtherSide(node) == node2) { + checkConnection(pos, connection, expectedChannelCount); + foundAny.setPlain(true); + } + } + }); + }); + + if (!foundAny.getPlain()) { + helper.fail("Connection spec " + bb + " and " + bb2 + " did not find any connections."); + } + } + + public void ensureEverythingWasChecked() { + helper.check(nodes.isEmpty(), "Not all nodes were checked: " + nodes); + helper.check(connections.isEmpty(), "Not all connections were checked: " + connections); + } + } +} diff --git a/src/main/java/appeng/server/testworld/PlotBuilder.java b/src/main/java/appeng/server/testworld/PlotBuilder.java index b700c85cbfa..e42cc86b498 100644 --- a/src/main/java/appeng/server/testworld/PlotBuilder.java +++ b/src/main/java/appeng/server/testworld/PlotBuilder.java @@ -84,7 +84,11 @@ default CableBuilder cable(String bb, IPartItem what) { } default CableBuilder denseCable(BlockPos pos) { - return cable(posToBb(pos), AEParts.SMART_DENSE_CABLE.item(AEColor.TRANSPARENT)); + return denseCable(posToBb(pos)); + } + + default CableBuilder denseCable(String bb) { + return cable(bb, AEParts.SMART_DENSE_CABLE.item(AEColor.TRANSPARENT)); } default void part(String bb, Direction side, ItemDefinition> part) { diff --git a/src/main/java/appeng/server/testworld/PlotTestHelper.java b/src/main/java/appeng/server/testworld/PlotTestHelper.java index 457d669c63b..c528cf4b0bf 100644 --- a/src/main/java/appeng/server/testworld/PlotTestHelper.java +++ b/src/main/java/appeng/server/testworld/PlotTestHelper.java @@ -18,6 +18,7 @@ import appeng.api.config.Actionable; import appeng.api.networking.GridHelper; import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; import appeng.api.parts.IPartHost; import appeng.api.stacks.AEItemKey; import appeng.api.stacks.AEKey; @@ -80,18 +81,15 @@ public T getPart(BlockPos pos, @Nullable Direction side, return partClass.cast(part); } - /** - * Find all grids in the area and return the biggest one. - */ @NotNull - public IGrid getGrid(BlockPos pos) { + public IGridNode getGridNode(BlockPos pos) { checkAllInitialized(); var be = getLevel().getBlockEntity(absolutePos(pos)); if (be instanceof IGridConnectedBlockEntity gridConnectedBlockEntity) { - IGrid grid = gridConnectedBlockEntity.getMainNode().getGrid(); - check(grid != null, "no grid", pos); - return grid; + var node = gridConnectedBlockEntity.getMainNode().getNode(); + check(node != null, "no node", pos); + return node; } var nodeHost = GridHelper.getNodeHost(getLevel(), this.absolutePos(pos)); @@ -99,14 +97,23 @@ public IGrid getGrid(BlockPos pos) { for (var side : Direction.values()) { var node = nodeHost.getGridNode(side); if (node != null) { - return node.getGrid(); + return node; } } } - fail("no grid", pos); + fail("no node", pos); return null; } + /** + * Find some grid at the given position. + */ + @NotNull + public IGrid getGrid(BlockPos pos) { + var node = getGridNode(pos); + return node.getGrid(); + } + /** * Checks that everything is initialized */