Defer per-node speed calculation until end of tick

This commit is contained in:
reidbhuntley 2021-12-29 18:45:53 -05:00
parent d58d303d28
commit 5c5e535551
4 changed files with 132 additions and 86 deletions

View file

@ -2,8 +2,6 @@ package com.simibubi.create.content.contraptions.solver;
import com.simibubi.create.foundation.utility.Pair; import com.simibubi.create.foundation.utility.Pair;
import net.minecraft.util.Mth;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.HashSet; import java.util.HashSet;
@ -17,32 +15,37 @@ public class KineticNetwork {
private final Set<KineticNode> generators = new HashSet<>(); private final Set<KineticNode> generators = new HashSet<>();
private final Set<Pair<KineticNode, KineticNode>> conflictingCycles = new HashSet<>(); private final Set<Pair<KineticNode, KineticNode>> conflictingCycles = new HashSet<>();
private float rootSpeed; private float rootSpeed;
private @Nullable KineticNode mainGenerator;
private boolean speedDirty;
public KineticNetwork(KineticNode root) { public KineticNetwork(KineticNode root) {
addMember(root); addMember(root);
rootSpeed = root.getGeneratedSpeed();
} }
public void addMember(KineticNode node) { public void addMember(KineticNode node) {
members.add(node); members.add(node);
if (node.isGenerator()) { if (node.isGenerator() && !generators.contains(node)) {
generators.add(node); generators.add(node);
speedDirty = true;
} }
} }
public void updateMember(KineticNode node) { public void updateMember(KineticNode node) {
if (!members.contains(node)) throw new IllegalArgumentException(); if (!members.contains(node)) throw new IllegalArgumentException();
if (node.isGenerator()) { if (node.isGenerator()) {
generators.add(node); generators.add(node);
} else { } else {
generators.remove(node); generators.remove(node);
} }
speedDirty = true;
} }
public void removeMember(KineticNode node) { public void removeMember(KineticNode node) {
members.remove(node); members.remove(node);
if (node.isGenerator()) { if (node.isGenerator() && generators.contains(node)) {
generators.remove(node); generators.remove(node);
speedDirty = true;
} }
conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node); conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node);
} }
@ -56,70 +59,88 @@ public class KineticNetwork {
return rootSpeed; 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 * Recalculates the speed at the root node of this network.
* KineticNodes in the network and pops any nodes whose speed has increased above the speed limit. * @return CONTRADICTION if the network has generators turning against each other, and OK otherwise
* @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.
*/ */
public SolveResult recalculateSpeed(@Nullable KineticNode checkRoot, boolean forced) { public SolveResult recalculateSpeed() {
if (!conflictingCycles.isEmpty() && !generators.isEmpty()) { if (!speedDirty) return SolveResult.OK;
// cycle with conflicting speed ratios is present
return SolveResult.CONTRADICTION; float newSpeed = 0;
} KineticNode newGenerator = null;
float sign = 0;
// search over all generators to maximize the root speed // search over all generators to maximize the root speed
float newSpeed = 0;
float sign = 0;
for (KineticNode generator : generators) { for (KineticNode generator : generators) {
float speedAtRoot = generator.getGeneratedSpeedAtRoot(); float speedAtRoot = generator.getGeneratedSpeedAtRoot();
if (newSpeed == 0) { if (newSpeed == 0) {
sign = Math.signum(speedAtRoot); sign = Math.signum(speedAtRoot);
} else if (Math.signum(speedAtRoot) != sign) { }
if (Math.signum(speedAtRoot) != sign) {
// generators are turning against each other // generators are turning against each other
return SolveResult.CONTRADICTION; return SolveResult.CONTRADICTION;
} }
newSpeed = Math.max(newSpeed, sign * speedAtRoot);
}
newSpeed *= sign;
if (!Mth.equal(rootSpeed, newSpeed)) { if (newSpeed < speedAtRoot * sign) {
rootSpeed = newSpeed; newSpeed = speedAtRoot * sign;
newGenerator = generator;
if (checkRoot == null) {
members.forEach(KineticNode::tryUpdateSpeed);
} else {
updateNodeSpeeds(checkRoot, false);
} }
} else if (forced) {
updateNodeSpeeds(checkRoot, true);
} }
rootSpeed = newSpeed * sign;
mainGenerator = newGenerator;
speedDirty = false;
return SolveResult.OK; return SolveResult.OK;
} }
private void updateNodeSpeeds(KineticNode root, boolean followSources) { /**
* @return a List of new networks created during this function call
*/
public List<KineticNetwork> tick() {
List<KineticNetwork> newNetworks = updateMemberSpeeds();
members.forEach(KineticNode::flushChangedSpeed);
return newNetworks;
}
private List<KineticNetwork> 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<KineticNetwork> newNetworks = new LinkedList<>();
Set<KineticNode> visited = new HashSet<>(); Set<KineticNode> visited = new HashSet<>();
List<KineticNode> frontier = new LinkedList<>(); List<KineticNode> frontier = new LinkedList<>();
frontier.add(root); frontier.add(mainGenerator);
// update node speeds in a breadth-first order, starting at root
while (!frontier.isEmpty()) { while (!frontier.isEmpty()) {
KineticNode cur = frontier.remove(0); KineticNode cur = frontier.remove(0);
visited.add(cur); visited.add(cur);
if (cur.tryUpdateSpeed().isOk()) { if (cur.tryUpdateSpeed().isOk()) {
for (KineticNode next : cur.getActiveConnections().keySet()) { cur.getActiveConnections()
if (!(visited.contains(next) || (followSources && !cur.isSourceOf(next)))) .map(Pair::getFirst)
frontier.add(next); .filter(n -> !visited.contains(n))
} .forEach(frontier::add);
} else { } else {
// stop searching on this branch once a speeding node is found // stop searching on this branch once a speeding node is found
cur.onPopBlock(); cur.onPopBlock();
newNetworks.add(cur.getNetwork());
} }
} }
return newNetworks;
} }
} }

View file

@ -11,10 +11,10 @@ import javax.annotation.Nullable;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class KineticNode { public class KineticNode {
@ -46,18 +46,25 @@ public class KineticNode {
return connections; 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 * @return a Stream containing a pair for each compatible connection with this node, where the first value is
* speed ratios of those connections * the connecting node and the second value is the speed ratio of the connection
*/ */
public Map<KineticNode, Float> getActiveConnections() { public Stream<Pair<KineticNode, Float>> getActiveConnections() {
return connections.getDirections().stream() return connections.getDirections().stream()
.map(d -> nodeAccessor.apply(entity.getBlockPos().offset(d)) .map(d -> nodeAccessor.apply(entity.getBlockPos().offset(d))
.map(n -> connections.checkConnection(n.connections, d) .map(n -> connections.checkConnection(n.connections, d)
.map(r -> Pair.of(n, r)))) .map(r -> Pair.of(n, r))))
.flatMap(Optional::stream) .flatMap(Optional::stream)
.flatMap(Optional::stream) .flatMap(Optional::stream);
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); }
public Iterable<Pair<KineticNode, Float>> getActiveConnectionsList() {
return getActiveConnections().collect(Collectors.toList());
} }
public float getGeneratedSpeed() { public float getGeneratedSpeed() {
@ -73,87 +80,92 @@ public class KineticNode {
} }
public void setGeneratedSpeed(float newSpeed) { public void setGeneratedSpeed(float newSpeed) {
if (Mth.equal(generatedSpeed, newSpeed)) return; if (generatedSpeed == newSpeed) return;
generatedSpeed = newSpeed; generatedSpeed = newSpeed;
network.updateMember(this); network.updateMember(this);
if (network.recalculateSpeed(this, false).isContradiction()) if (network.recalculateSpeed().isContradiction()) {
onPopBlock(); onPopBlock();
}
} }
private void setNetwork(KineticNetwork network) { private SolveResult setNetwork(KineticNetwork network) {
this.network.removeMember(this); this.network.removeMember(this);
this.network = network; this.network = network;
network.addMember(this); network.addMember(this);
return network.recalculateSpeed();
} }
private void setSource(KineticNode from, float ratio) { private SolveResult setSource(KineticNode from, float ratio) {
source = from; source = from;
speedRatio = from.speedRatio * ratio; speedRatio = from.speedRatio * ratio;
setNetwork(from.network); return setNetwork(from.network);
} }
public void onAdded() { public void onAdded() {
getActiveConnections() getActiveConnections()
.keySet()
.stream()
.findAny() .findAny()
.ifPresent(n -> { .ifPresent(e -> {
if (n.propagateSource(this).isContradiction()) if (setSource(e.getFirst(), 1/e.getSecond()).isOk()) {
propagateSource();
} else {
onPopBlock(); onPopBlock();
}
}); });
} }
/** /**
* Propagates this node's source and network to any connected nodes that aren't yet part of the same network, then * 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. * 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<KineticNode> frontier = new LinkedList<>(); List<KineticNode> frontier = new LinkedList<>();
frontier.add(this); frontier.add(this);
while (!frontier.isEmpty()) { while (!frontier.isEmpty()) {
KineticNode cur = frontier.remove(0); KineticNode cur = frontier.remove(0);
for (Map.Entry<KineticNode, Float> entry : cur.getActiveConnections().entrySet()) { for (Pair<KineticNode, Float> pair : cur.getActiveConnectionsList()) {
KineticNode next = entry.getKey(); KineticNode next = pair.getFirst();
float ratio = entry.getValue(); float ratio = pair.getSecond();
if (next == cur.source) continue; if (next == cur.source) continue;
if (next.network == network) { if (next.network == network) {
if (!Mth.equal(next.speedRatio, cur.speedRatio * ratio)) { if (!Mth.equal(next.speedRatio, cur.speedRatio * ratio)) {
// we found a cycle with conflicting speed ratios // this node will cause a cycle with conflicting speed ratios
network.markConflictingCycle(cur, next); if (network.isStopped()) {
network.markConflictingCycle(cur, next);
} else {
onPopBlock();
return;
}
} }
continue; continue;
} }
next.setSource(cur, ratio); if (next.setSource(cur, ratio).isOk()) {
frontier.add(next); frontier.add(next);
} else {
// this node will run against the network
onPopBlock();
return;
}
} }
} }
return network.recalculateSpeed(checkRoot, true);
} }
public void onRemoved() { public void onRemoved() {
network.removeMember(this); network.removeMember(this);
for (KineticNode neighbor : getActiveConnections().keySet()) { getActiveConnections()
if (neighbor.source != this) continue; .map(Pair::getFirst)
neighbor.rerootHere(); .filter(n -> n.source == this)
} .forEach(KineticNode::rerootHere);
network.recalculateSpeed(null, false);
} }
private void rerootHere() { private void rerootHere() {
source = null; source = null;
speedRatio = 1; speedRatio = 1;
setNetwork(new KineticNetwork(this)); setNetwork(new KineticNetwork(this));
if (tryUpdateSpeed().isOk()) { propagateSource();
propagateSource(this);
} else {
onPopBlock();
}
} }
/** /**
@ -182,5 +194,4 @@ public class KineticNode {
public boolean isSourceOf(KineticNode other) { public boolean isSourceOf(KineticNode other) {
return other.source == this; return other.source == this;
} }
} }

View file

@ -1,8 +1,13 @@
package com.simibubi.create.content.contraptions.solver; package com.simibubi.create.content.contraptions.solver;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; 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.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.foundation.utility.WorldAttached; import com.simibubi.create.foundation.utility.WorldAttached;
@ -24,11 +29,7 @@ public class KineticSolver {
removeNode(entity); removeNode(entity);
KineticNode node = new KineticNode(entity, this::getNode); KineticNode node = new KineticNode(entity, this::getNode);
nodes.put(entity.getBlockPos(), node); nodes.put(entity.getBlockPos(), node);
if (node.tryUpdateSpeed().isOk()) { node.onAdded();
node.onAdded();
} else {
node.onPopBlock();
}
} }
public void updateNode(KineticTileEntity entity) { public void updateNode(KineticTileEntity entity) {
@ -54,7 +55,20 @@ public class KineticSolver {
if (node != null) node.onRemoved(); if (node != null) node.onRemoved();
} }
public void flushChangedSpeeds() { public void tick() {
nodes.values().forEach(KineticNode::flushChangedSpeed); Set<KineticNetwork> visited = new HashSet<>();
List<KineticNetwork> frontier = new LinkedList<>();
Set<KineticNetwork> 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());
}
}
} }
} }

View file

@ -112,7 +112,7 @@ public class CommonEvents {
CouplingPhysics.tick(world); CouplingPhysics.tick(world);
LinkedControllerServerHandler.tick(world); LinkedControllerServerHandler.tick(world);
KineticSolver.getSolver(world).flushChangedSpeeds(); KineticSolver.getSolver(world).tick();
} }
@SubscribeEvent @SubscribeEvent