diff --git a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNetwork.java b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNetwork.java index 3b8a4e6ff..d5dacf453 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNetwork.java +++ b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNetwork.java @@ -2,8 +2,6 @@ package com.simibubi.create.content.contraptions.solver; import com.simibubi.create.foundation.utility.Pair; -import net.minecraft.util.Mth; - import javax.annotation.Nullable; import java.util.HashSet; @@ -17,32 +15,37 @@ public class KineticNetwork { private final Set generators = new HashSet<>(); private final Set> conflictingCycles = new HashSet<>(); private float rootSpeed; + private @Nullable KineticNode mainGenerator; + private boolean speedDirty; public KineticNetwork(KineticNode root) { addMember(root); - rootSpeed = root.getGeneratedSpeed(); } public void addMember(KineticNode node) { members.add(node); - if (node.isGenerator()) { + if (node.isGenerator() && !generators.contains(node)) { generators.add(node); + speedDirty = true; } } public void updateMember(KineticNode node) { if (!members.contains(node)) throw new IllegalArgumentException(); + if (node.isGenerator()) { generators.add(node); } else { generators.remove(node); } + speedDirty = true; } public void removeMember(KineticNode node) { members.remove(node); - if (node.isGenerator()) { + if (node.isGenerator() && generators.contains(node)) { generators.remove(node); + speedDirty = true; } conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node); } @@ -56,70 +59,88 @@ public class KineticNetwork { return rootSpeed; } + public boolean isStopped() { return generators.isEmpty(); } + /** - * Recalculates the speed at the root of this network, and if it has changed, recalculates the speed of all - * KineticNodes in the network and pops any nodes whose speed has increased above the speed limit. - * @param checkRoot Node to start performing a breadth-first from in order to find and pop speeding nodes. If null, - * speeding nodes are ignored. - * @param forced If true, will check for speeding nodes from checkRoot even if the root speed has not changed. - * @return CONTRADICTION if the network currently has a cycle with conflicting speed ratios or - * has generators turning against each other, and OK otherwise. + * Recalculates the speed at the root node of this network. + * @return CONTRADICTION if the network has generators turning against each other, and OK otherwise */ - public SolveResult recalculateSpeed(@Nullable KineticNode checkRoot, boolean forced) { - if (!conflictingCycles.isEmpty() && !generators.isEmpty()) { - // cycle with conflicting speed ratios is present - return SolveResult.CONTRADICTION; - } + public SolveResult recalculateSpeed() { + if (!speedDirty) return SolveResult.OK; + + float newSpeed = 0; + KineticNode newGenerator = null; + float sign = 0; // search over all generators to maximize the root speed - float newSpeed = 0; - float sign = 0; for (KineticNode generator : generators) { float speedAtRoot = generator.getGeneratedSpeedAtRoot(); + if (newSpeed == 0) { sign = Math.signum(speedAtRoot); - } else if (Math.signum(speedAtRoot) != sign) { + } + + if (Math.signum(speedAtRoot) != sign) { // generators are turning against each other return SolveResult.CONTRADICTION; } - newSpeed = Math.max(newSpeed, sign * speedAtRoot); - } - newSpeed *= sign; - if (!Mth.equal(rootSpeed, newSpeed)) { - rootSpeed = newSpeed; - - if (checkRoot == null) { - members.forEach(KineticNode::tryUpdateSpeed); - } else { - updateNodeSpeeds(checkRoot, false); + if (newSpeed < speedAtRoot * sign) { + newSpeed = speedAtRoot * sign; + newGenerator = generator; } - } else if (forced) { - updateNodeSpeeds(checkRoot, true); } - + rootSpeed = newSpeed * sign; + mainGenerator = newGenerator; + speedDirty = false; return SolveResult.OK; } - private void updateNodeSpeeds(KineticNode root, boolean followSources) { + /** + * @return a List of new networks created during this function call + */ + public List tick() { + List newNetworks = updateMemberSpeeds(); + members.forEach(KineticNode::flushChangedSpeed); + return newNetworks; + } + + private List updateMemberSpeeds() { + SolveResult recalculateSpeedResult = recalculateSpeed(); + // generators should not be turning against each other by now + assert(recalculateSpeedResult.isOk()); + + // if we're stopped then all members' speeds will be 0, so no need to check for speeding nodes + if (isStopped()) { + members.forEach(KineticNode::tryUpdateSpeed); + return List.of(); + } + + // there should be no cycles with conflicting speed ratios by now + assert(conflictingCycles.isEmpty()); + + // update node speeds in a breadth-first order, checking for speeding nodes along the way + List newNetworks = new LinkedList<>(); Set visited = new HashSet<>(); List frontier = new LinkedList<>(); - frontier.add(root); + frontier.add(mainGenerator); - // update node speeds in a breadth-first order, starting at root while (!frontier.isEmpty()) { KineticNode cur = frontier.remove(0); visited.add(cur); if (cur.tryUpdateSpeed().isOk()) { - for (KineticNode next : cur.getActiveConnections().keySet()) { - if (!(visited.contains(next) || (followSources && !cur.isSourceOf(next)))) - frontier.add(next); - } + cur.getActiveConnections() + .map(Pair::getFirst) + .filter(n -> !visited.contains(n)) + .forEach(frontier::add); } else { // stop searching on this branch once a speeding node is found cur.onPopBlock(); + newNetworks.add(cur.getNetwork()); } } + + return newNetworks; } } diff --git a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNode.java b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNode.java index fb6e1a035..0471b38f1 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNode.java +++ b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticNode.java @@ -11,10 +11,10 @@ import javax.annotation.Nullable; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; public class KineticNode { @@ -46,18 +46,25 @@ public class KineticNode { return connections; } + public KineticNetwork getNetwork() { + return network; + } + /** - * @return a map where the keys are every node with a compatible connection to this node, and the values are the - * speed ratios of those connections + * @return a Stream containing a pair for each compatible connection with this node, where the first value is + * the connecting node and the second value is the speed ratio of the connection */ - public Map getActiveConnections() { + public Stream> getActiveConnections() { return connections.getDirections().stream() .map(d -> nodeAccessor.apply(entity.getBlockPos().offset(d)) .map(n -> connections.checkConnection(n.connections, d) .map(r -> Pair.of(n, r)))) .flatMap(Optional::stream) - .flatMap(Optional::stream) - .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); + .flatMap(Optional::stream); + } + + public Iterable> getActiveConnectionsList() { + return getActiveConnections().collect(Collectors.toList()); } public float getGeneratedSpeed() { @@ -73,87 +80,92 @@ public class KineticNode { } public void setGeneratedSpeed(float newSpeed) { - if (Mth.equal(generatedSpeed, newSpeed)) return; + if (generatedSpeed == newSpeed) return; generatedSpeed = newSpeed; network.updateMember(this); - if (network.recalculateSpeed(this, false).isContradiction()) + if (network.recalculateSpeed().isContradiction()) { onPopBlock(); + } } - private void setNetwork(KineticNetwork network) { + private SolveResult setNetwork(KineticNetwork network) { this.network.removeMember(this); this.network = network; network.addMember(this); + return network.recalculateSpeed(); } - private void setSource(KineticNode from, float ratio) { + private SolveResult setSource(KineticNode from, float ratio) { source = from; speedRatio = from.speedRatio * ratio; - setNetwork(from.network); + return setNetwork(from.network); } public void onAdded() { getActiveConnections() - .keySet() - .stream() .findAny() - .ifPresent(n -> { - if (n.propagateSource(this).isContradiction()) + .ifPresent(e -> { + if (setSource(e.getFirst(), 1/e.getSecond()).isOk()) { + propagateSource(); + } else { onPopBlock(); + } }); } /** * Propagates this node's source and network to any connected nodes that aren't yet part of the same network, then * repeats this recursively with the connected nodes in a breadth-first order. - * @param checkRoot Node to start searching from when looking for nodes that started speeding because of this call - * @return whether or not this propagation caused a contradiction in the kinetic network */ - private SolveResult propagateSource(KineticNode checkRoot) { + private void propagateSource() { List frontier = new LinkedList<>(); frontier.add(this); while (!frontier.isEmpty()) { KineticNode cur = frontier.remove(0); - for (Map.Entry entry : cur.getActiveConnections().entrySet()) { - KineticNode next = entry.getKey(); - float ratio = entry.getValue(); + for (Pair pair : cur.getActiveConnectionsList()) { + KineticNode next = pair.getFirst(); + float ratio = pair.getSecond(); if (next == cur.source) continue; + if (next.network == network) { if (!Mth.equal(next.speedRatio, cur.speedRatio * ratio)) { - // we found a cycle with conflicting speed ratios - network.markConflictingCycle(cur, next); + // this node will cause a cycle with conflicting speed ratios + if (network.isStopped()) { + network.markConflictingCycle(cur, next); + } else { + onPopBlock(); + return; + } } continue; } - next.setSource(cur, ratio); - frontier.add(next); + if (next.setSource(cur, ratio).isOk()) { + frontier.add(next); + } else { + // this node will run against the network + onPopBlock(); + return; + } } } - - return network.recalculateSpeed(checkRoot, true); } public void onRemoved() { network.removeMember(this); - for (KineticNode neighbor : getActiveConnections().keySet()) { - if (neighbor.source != this) continue; - neighbor.rerootHere(); - } - network.recalculateSpeed(null, false); + getActiveConnections() + .map(Pair::getFirst) + .filter(n -> n.source == this) + .forEach(KineticNode::rerootHere); } private void rerootHere() { source = null; speedRatio = 1; setNetwork(new KineticNetwork(this)); - if (tryUpdateSpeed().isOk()) { - propagateSource(this); - } else { - onPopBlock(); - } + propagateSource(); } /** @@ -182,5 +194,4 @@ public class KineticNode { public boolean isSourceOf(KineticNode other) { return other.source == this; } - } diff --git a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticSolver.java b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticSolver.java index fa16bef31..bd9f0ed8e 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/solver/KineticSolver.java +++ b/src/main/java/com/simibubi/create/content/contraptions/solver/KineticSolver.java @@ -1,8 +1,13 @@ package com.simibubi.create.content.contraptions.solver; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import com.simibubi.create.content.contraptions.base.KineticTileEntity; import com.simibubi.create.foundation.utility.WorldAttached; @@ -24,11 +29,7 @@ public class KineticSolver { removeNode(entity); KineticNode node = new KineticNode(entity, this::getNode); nodes.put(entity.getBlockPos(), node); - if (node.tryUpdateSpeed().isOk()) { - node.onAdded(); - } else { - node.onPopBlock(); - } + node.onAdded(); } public void updateNode(KineticTileEntity entity) { @@ -54,7 +55,20 @@ public class KineticSolver { if (node != null) node.onRemoved(); } - public void flushChangedSpeeds() { - nodes.values().forEach(KineticNode::flushChangedSpeed); + public void tick() { + Set visited = new HashSet<>(); + List frontier = new LinkedList<>(); + + Set networks = nodes.values().stream().map(KineticNode::getNetwork).collect(Collectors.toSet()); + for (KineticNetwork network : networks) { + frontier.add(network); + while (!frontier.isEmpty()) { + KineticNetwork cur = frontier.remove(0); + if (visited.contains(cur)) continue; + visited.add(cur); + frontier.addAll(cur.tick()); + } + } } + } diff --git a/src/main/java/com/simibubi/create/events/CommonEvents.java b/src/main/java/com/simibubi/create/events/CommonEvents.java index ff1562c96..ec5c9e431 100644 --- a/src/main/java/com/simibubi/create/events/CommonEvents.java +++ b/src/main/java/com/simibubi/create/events/CommonEvents.java @@ -112,7 +112,7 @@ public class CommonEvents { CouplingPhysics.tick(world); LinkedControllerServerHandler.tick(world); - KineticSolver.getSolver(world).flushChangedSpeeds(); + KineticSolver.getSolver(world).tick(); } @SubscribeEvent