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 net.minecraft.util.Mth;
import javax.annotation.Nullable;
import java.util.HashSet;
@ -17,32 +15,37 @@ public class KineticNetwork {
private final Set<KineticNode> generators = new HashSet<>();
private final Set<Pair<KineticNode, KineticNode>> 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<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<>();
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()) {
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;
}
}

View file

@ -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<KineticNode, Float> getActiveConnections() {
public Stream<Pair<KineticNode, Float>> 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<Pair<KineticNode, Float>> 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<KineticNode> frontier = new LinkedList<>();
frontier.add(this);
while (!frontier.isEmpty()) {
KineticNode cur = frontier.remove(0);
for (Map.Entry<KineticNode, Float> entry : cur.getActiveConnections().entrySet()) {
KineticNode next = entry.getKey();
float ratio = entry.getValue();
for (Pair<KineticNode, Float> 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;
}
}

View file

@ -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<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);
LinkedControllerServerHandler.tick(world);
KineticSolver.getSolver(world).flushChangedSpeeds();
KineticSolver.getSolver(world).tick();
}
@SubscribeEvent