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 eacc9d523..42577fe17 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 @@ -15,11 +15,15 @@ public class KineticNetwork { private final Set members = new HashSet<>(); private final Set generators = new HashSet<>(); private final Set> conflictingCycles = new HashSet<>(); - private float rootSpeed; + private float rootTheoreticalSpeed; private @Nullable KineticNode mainGenerator; - private boolean speedDirty; + private boolean rootSpeedDirty; private boolean overstressed; + private float rootSpeedCur; + private float rootSpeedPrev; + private final Set potentialNewBranches = new HashSet<>(); + public KineticNetwork(KineticNode root) { addMember(root); } @@ -28,8 +32,10 @@ public class KineticNetwork { members.add(node); if (node.isGenerator() && !generators.contains(node)) { generators.add(node); - speedDirty = true; + rootSpeedDirty = true; } + + potentialNewBranches.add(node); } public void updateMember(KineticNode node) { @@ -40,14 +46,14 @@ public class KineticNetwork { } else { generators.remove(node); } - speedDirty = true; + rootSpeedDirty = true; } public void removeMember(KineticNode node) { members.remove(node); if (node.isGenerator() && generators.contains(node)) { generators.remove(node); - speedDirty = true; + rootSpeedDirty = true; } conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node); } @@ -65,10 +71,14 @@ public class KineticNetwork { * each other, and OK otherwise */ public SolveResult tryRecalculateSpeed() { - if (!conflictingCycles.isEmpty() && !isStopped()) return SolveResult.CONTRADICTION; - if (!speedDirty) return SolveResult.OK; + SolveResult result = tryRecalculateTheoreticalSpeed(); + if (isStopped()) return SolveResult.OK; + return result; + } - SolveResult result = SolveResult.OK; + private SolveResult tryRecalculateTheoreticalSpeed() { + SolveResult result = conflictingCycles.isEmpty() ? SolveResult.OK : SolveResult.CONTRADICTION; + if (!rootSpeedDirty) return result; float newSpeed = 0; KineticNode newGenerator = null; @@ -93,11 +103,15 @@ public class KineticNetwork { newGenerator = generator; } } - rootSpeed = newSpeed * sign; - mainGenerator = newGenerator; - speedDirty = false; - if (overstressed) return SolveResult.OK; + rootTheoreticalSpeed = newSpeed * sign; + if (!overstressed) { + rootSpeedCur = rootTheoreticalSpeed; + } + + mainGenerator = newGenerator; + rootSpeedDirty = false; + return result; } @@ -109,20 +123,24 @@ public class KineticNetwork { if (generators.isEmpty()) { overstressed = false; + members.forEach(KineticNode::stop); + members.forEach(KineticNode::flushChangedSpeed); return newNetworks; } - float stressImpact = (float) members.stream().mapToDouble(n -> n.getTotalStressImpact(rootSpeed)).sum(); + float stressImpact = (float) members.stream().mapToDouble(n -> n.getTotalStressImpact(rootTheoreticalSpeed)).sum(); float stressCapacity = (float) members.stream().mapToDouble(KineticNode::getStressCapacity).sum(); if (stressImpact > stressCapacity) { if (!overstressed) { overstressed = true; + rootSpeedCur = 0; members.forEach(KineticNode::stop); } } else { if (overstressed) { overstressed = false; + rootSpeedCur = rootTheoreticalSpeed; newNetworks.addAll(bulldozeContradictingMembers()); newNetworks.addAll(updateMemberSpeeds()); } @@ -132,7 +150,14 @@ public class KineticNetwork { return newNetworks; } + /** + * Update the speed of every member, starting from the main generator and popping off speeding nodes along the way + * @return a List of new networks created during this function call + */ private List updateMemberSpeeds() { + boolean rootSpeedChanged = rootSpeedPrev != rootSpeedCur; + rootSpeedPrev = rootSpeedCur; + // if we're stopped, then all members' speeds will be 0, so no need to check for speeding nodes if (isStopped()) { members.forEach(KineticNode::stop); @@ -143,45 +168,68 @@ public class KineticNetwork { // generators should not be turning against each other or have conflicting cycles by now assert(recalculateSpeedResult.isOk()); - // 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(mainGenerator); - while (!frontier.isEmpty()) { - KineticNode cur = frontier.remove(0); - visited.add(cur); - if (cur.tryUpdateSpeed(rootSpeed).isOk()) { - 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()); - } + if (rootSpeedChanged) { + // root speed changed, update all nodes starting from the main generator + bfs(mainGenerator, newNetworks, false); + } else if (!potentialNewBranches.isEmpty()) { + // new nodes added, update only the new network branches + potentialNewBranches.stream() + .filter(n -> !potentialNewBranches.contains(n.getSource())) + .forEach(n -> bfs(n, newNetworks, true)); + potentialNewBranches.clear(); } return newNetworks; } + private void bfs(KineticNode root, List newNetworks, boolean followSource) { + // update node speeds in a breadth-first order, checking for speeding nodes along the way + Set visited = new HashSet<>(); + List frontier = new LinkedList<>(); + frontier.add(root); + + while (!frontier.isEmpty()) { + KineticNode cur = frontier.remove(0); + if (!members.contains(cur) || visited.contains(cur)) continue; + visited.add(cur); + + if (cur.tryUpdateSpeed(rootSpeedCur).isOk()) { + cur.getActiveConnections() + .map(Pair::getFirst) + .filter(n -> !followSource || n.getSource() == cur) + .forEach(frontier::add); + } else { + // stop searching on this branch once a speeding node is found + cur.popBlock(); + newNetworks.add(cur.getNetwork()); + } + } + } + private List bulldozeContradictingMembers() { + /* + This method is necessary to handle the edge case where contradicting nodes have been added to the network while + it was overstressed and now that it's moving again we need to pop them. Here we can't just stop following a + branch after popping a block though since there may be more contradictions further down that branch, so we'll + just pop all potentially contradicting nodes off and hope no one cares + */ + List newNetworks = new LinkedList<>(); // generators running against network - float sign = Math.signum(rootSpeed); + float sign = Math.signum(rootSpeedCur); List runningAgainst = generators.stream() .filter(n -> Math.signum(n.getGeneratedSpeedAtRoot()) != sign) .collect(Collectors.toList()); - runningAgainst.forEach(n -> { n.onPopBlock(); newNetworks.add(n.getNetwork()); }); + runningAgainst.forEach(n -> { n.popBlock(); newNetworks.add(n.getNetwork()); }); // conflicting cycles List cycles = conflictingCycles.stream() .map(Pair::getFirst) .collect(Collectors.toList()); - cycles.forEach(n -> { n.onPopBlock(); newNetworks.add(n.getNetwork()); }); + cycles.forEach(n -> { n.popBlock(); newNetworks.add(n.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 780a31fad..0803dd3e2 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 @@ -84,7 +84,7 @@ public class KineticNode { generatedSpeed = newSpeed; network.updateMember(this); if (network.tryRecalculateSpeed().isContradiction()) { - onPopBlock(); + popBlock(); } } @@ -111,6 +111,10 @@ public class KineticNode { return network.tryRecalculateSpeed(); } + public KineticNode getSource() { + return source; + } + private SolveResult setSource(KineticNode from, float ratio) { source = from; speedRatio = from.speedRatio * ratio; @@ -124,7 +128,7 @@ public class KineticNode { if (setSource(e.getFirst(), 1/e.getSecond()).isOk()) { propagateSource(); } else { - onPopBlock(); + popBlock(); } }); } @@ -151,7 +155,7 @@ public class KineticNode { if (network.isStopped()) { network.markConflictingCycle(cur, next); } else { - onPopBlock(); + popBlock(); return; } } @@ -162,7 +166,7 @@ public class KineticNode { frontier.add(next); } else { // this node will run against the network or activate a conflicting cycle - onPopBlock(); + popBlock(); return; } } @@ -208,12 +212,9 @@ public class KineticNode { } } - public void onPopBlock() { + public void popBlock() { // this should cause the node to get removed from the solver and lead to onRemoved() being called entity.getLevel().destroyBlock(entity.getBlockPos(), true); } - public boolean isSourceOf(KineticNode other) { - return other.source == this; - } }