Curse of the Speed Controller

This commit is contained in:
reidbhuntley 2021-12-31 02:53:35 -05:00
parent 1fed398e8f
commit f4514b3e5e
9 changed files with 216 additions and 76 deletions

View file

@ -1,5 +1,7 @@
package com.simibubi.create.content.contraptions.base;
import com.simibubi.create.content.contraptions.solver.AllConnections;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import com.simibubi.create.foundation.utility.Iterate;
import net.minecraft.core.BlockPos;
@ -61,6 +63,11 @@ public abstract class HorizontalAxisKineticBlock extends KineticBlock {
return state.getValue(HORIZONTAL_AXIS);
}
@Override
public KineticConnections getInitialConnections(BlockState state) {
return AllConnections.FULL_SHAFT.apply(state.getValue(HORIZONTAL_AXIS));
}
@Override
public boolean hasShaftTowards(LevelReader world, BlockPos pos, BlockState state, Direction face) {
return face.getAxis() == state.getValue(HORIZONTAL_AXIS);

View file

@ -10,6 +10,8 @@ import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.contraptions.base.HorizontalAxisKineticBlock;
import com.simibubi.create.content.contraptions.relays.elementary.CogWheelBlock;
import com.simibubi.create.content.contraptions.relays.elementary.ICogWheel;
import com.simibubi.create.content.contraptions.solver.AllConnections;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import com.simibubi.create.foundation.block.ITE;
import com.simibubi.create.foundation.utility.placement.IPlacementHelper;
import com.simibubi.create.foundation.utility.placement.PlacementHelpers;
@ -43,6 +45,11 @@ public class SpeedControllerBlock extends HorizontalAxisKineticBlock implements
super(properties);
}
@Override
public KineticConnections getInitialConnections(BlockState state) {
return AllConnections.SPEED_CONTROLLER.apply(state.getValue(HORIZONTAL_AXIS));
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
BlockState above = context.getLevel()
@ -111,7 +118,7 @@ public class SpeedControllerBlock extends HorizontalAxisKineticBlock implements
public Class<SpeedControllerTileEntity> getTileEntityClass() {
return SpeedControllerTileEntity.class;
}
@Override
public BlockEntityType<? extends SpeedControllerTileEntity> getTileEntityType() {
return AllTileEntities.ROTATION_SPEED_CONTROLLER.get();

View file

@ -49,11 +49,14 @@ public class SpeedControllerTileEntity extends KineticTileEntity {
targetSpeed.value = DEFAULT_SPEED;
targetSpeed.moveText(new Vec3(9, 0, 10));
targetSpeed.withUnit(i -> Lang.translate("generic.unit.rpm"));
targetSpeed.withCallback(i -> this.updateTargetRotation());
targetSpeed.withStepFunction(CreativeMotorTileEntity::step);
behaviours.add(targetSpeed);
}
public float getTargetSpeed() {
return targetSpeed.getValue();
}
private void updateTargetRotation() {
if (hasNetwork())
getOrCreateNetwork().remove(this);
@ -123,7 +126,7 @@ public class SpeedControllerTileEntity extends KineticTileEntity {
return true;
}
private class ControllerValueBoxTransform extends ValueBoxTransform.Sided {
private static class ControllerValueBoxTransform extends ValueBoxTransform.Sided {
@Override
protected Vec3 getSouthLocation() {

View file

@ -2,7 +2,11 @@ package com.simibubi.create.content.contraptions.relays.elementary;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.content.contraptions.relays.advanced.SpeedControllerTileEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
@ -23,4 +27,13 @@ public class SimpleKineticTileEntity extends KineticTileEntity {
return false;
}
@Override
public float getGeneratedSpeed() {
Block block = getBlockState().getBlock();
BlockEntity below = level.getBlockEntity(getBlockPos().below());
if (block instanceof ICogWheel cog && cog.isLargeCog()
&& below instanceof SpeedControllerTileEntity controller && controller.getSpeed() != 0)
return controller.getTargetSpeed();
return 0;
}
}

View file

@ -11,6 +11,7 @@ import net.minecraft.core.Vec3i;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static net.minecraft.world.level.block.state.properties.BlockStateProperties.AXIS;
@ -30,7 +31,8 @@ public class AllConnections {
public static final LazyMap<Axis, Type>
TYPE_SHAFT = ValueType.map(),
TYPE_LARGE_COG = ValueType.map(),
TYPE_SMALL_COG = ValueType.map();
TYPE_SMALL_COG = ValueType.map(),
TYPE_SPEED_CONTROLLER_TOP = ValueType.map();
private static Direction pos(Axis axis) {
@ -48,6 +50,14 @@ public class AllConnections {
return new Entry(diff, TYPE_LARGE_COG.apply(from), TYPE_LARGE_COG.apply(to), ratio);
}
private static Optional<Axis> oppAxis(Axis axis) {
return switch (axis) {
case X -> Optional.of(Axis.Z);
case Z -> Optional.of(Axis.X);
default -> Optional.empty();
};
}
public static final KineticConnections EMPTY = new KineticConnections();
@ -61,6 +71,7 @@ public class AllConnections {
LARGE_COG = new LazyMap<>(axis -> {
Type large = TYPE_LARGE_COG.apply(axis);
Type small = TYPE_SMALL_COG.apply(axis);
List<Entry> out = new LinkedList<>();
Direction cur = DirectionHelper.getPositivePerpendicular(axis);
for (int i = 0; i < 4; i++) {
@ -70,12 +81,18 @@ public class AllConnections {
out.add(new Entry(cur.getNormal().relative(next), large, small, -2));
cur = next;
}
oppAxis(axis).ifPresent(opp -> {
Type sc = TYPE_SPEED_CONTROLLER_TOP.apply(opp);
out.add(new Entry(Direction.DOWN.getNormal(), large, sc).stressOnly());
});
return new KineticConnections(out);
}),
SMALL_COG = new LazyMap<>(axis -> {
Type large = TYPE_LARGE_COG.apply(axis);
Type small = TYPE_SMALL_COG.apply(axis);
List<Entry> out = new LinkedList<>();
Direction cur = DirectionHelper.getPositivePerpendicular(axis);
for (int i = 0; i < 4; i++) {
@ -84,11 +101,19 @@ public class AllConnections {
out.add(new Entry(cur.getNormal().relative(next), small, large, -0.5f));
cur = next;
}
return new KineticConnections(out);
}),
LARGE_COG_FULL_SHAFT = new LazyMap<>(axis -> LARGE_COG.apply(axis).merge(FULL_SHAFT.apply(axis))),
SMALL_COG_FULL_SHAFT = new LazyMap<>(axis -> SMALL_COG.apply(axis).merge(FULL_SHAFT.apply(axis)));
SMALL_COG_FULL_SHAFT = new LazyMap<>(axis -> SMALL_COG.apply(axis).merge(FULL_SHAFT.apply(axis))),
SPEED_CONTROLLER = new LazyMap<>(axis -> {
Type sc = TYPE_SPEED_CONTROLLER_TOP.apply(axis);
Type large = TYPE_LARGE_COG.apply(oppAxis(axis).get());
Vec3i up = Direction.UP.getNormal();
return new KineticConnections(new Entry(up, sc, large).stressOnly()).merge(FULL_SHAFT.apply(axis));
});
}

View file

@ -32,9 +32,16 @@ public class KineticConnections {
public Entry(Vec3i offset, Type type) {
this(offset, type, type, 1);
}
public Entry stressOnly() {
return new Entry(offset, new Value(value.from, value.to, 0));
}
}
private static record Value(Type from, Type to, float ratio) { }
private static record Value(Type from, Type to, float ratio) {
public boolean isStressOnly() {
return ratio == 0;
}
}
private final Map<Vec3i, Value> connections;
@ -65,12 +72,26 @@ public class KineticConnections {
Value toValue = to.connections.get(offset.multiply(-1));
if (toValue == null) return Optional.empty();
if (fromValue.isStressOnly() || toValue.isStressOnly()) return Optional.empty();
if (fromValue.from.compatible(toValue.to) && fromValue.to.compatible(toValue.from)
&& (Mth.equal(fromValue.ratio, 1/toValue.ratio) || (Mth.equal(toValue.ratio, 1/fromValue.ratio))))
return Optional.of(fromValue.ratio);
return Optional.empty();
}
public boolean checkStressOnlyConnection(KineticConnections to, Vec3i offset) {
Value fromValue = connections.get(offset);
if (fromValue == null) return false;
Value toValue = to.connections.get(offset.multiply(-1));
if (toValue == null) return false;
if (!fromValue.isStressOnly() || !toValue.isStressOnly()) return false;
return fromValue.from.compatible(toValue.to) && fromValue.to.compatible(toValue.from);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -91,5 +112,9 @@ public class KineticConnections {
return new KineticConnections(out);
}
public boolean hasStressOnlyConnections() {
return connections.values().stream().anyMatch(Value::isStressOnly);
}
}

View file

@ -2,6 +2,8 @@ package com.simibubi.create.content.contraptions.solver;
import com.simibubi.create.foundation.utility.Pair;
import com.simibubi.create.foundation.utility.ResetableLazy;
import javax.annotation.Nullable;
import java.util.HashSet;
@ -15,46 +17,71 @@ public class KineticNetwork {
private final Set<KineticNode> members = new HashSet<>();
private final Set<KineticNode> generators = new HashSet<>();
private final Set<Pair<KineticNode, KineticNode>> conflictingCycles = new HashSet<>();
private float rootTheoreticalSpeed;
private @Nullable KineticNode mainGenerator;
private boolean rootSpeedDirty;
private @Nullable KineticNode mainGenerator;
private boolean overstressed;
private float rootSpeedCur;
private float rootSpeedPrev;
private boolean rootSpeedChanged;
private final Set<KineticNode> potentialNewBranches = new HashSet<>();
private final ResetableLazy<Float> totalStressImpact = ResetableLazy.of(() ->
(float) members.stream().mapToDouble(n -> n.getTotalStressImpact(rootTheoreticalSpeed)).sum());
private final ResetableLazy<Float> totalStressCapacity = ResetableLazy.of(() ->
(float) members.stream().mapToDouble(KineticNode::getStressCapacity).sum());
private boolean ticked;
private final Set<KineticNode> stressConnectors = new HashSet<>();
public KineticNetwork(KineticNode root) {
addMember(root);
}
public void addMember(KineticNode node) {
members.add(node);
potentialNewBranches.add(node);
if (node.getConnections().hasStressOnlyConnections()) stressConnectors.add(node);
if (node.isGenerator() && !generators.contains(node)) {
generators.add(node);
rootSpeedDirty = true;
rootSpeedChanged = true;
}
potentialNewBranches.add(node);
if (node.hasStressImpact()) onMemberStressImpactUpdated();
if (node.hasStressCapacity()) onMemberStressCapacityUpdated();
}
public void updateMember(KineticNode node) {
if (!members.contains(node)) throw new IllegalArgumentException();
public void onMemberGeneratedSpeedUpdated(KineticNode node) {
if (node.isGenerator()) {
generators.add(node);
} else {
generators.remove(node);
}
rootSpeedDirty = true;
rootSpeedChanged = true;
}
public void onMemberStressImpactUpdated() {
totalStressImpact.reset();
}
public void onMemberStressCapacityUpdated() {
totalStressCapacity.reset();
}
public void removeMember(KineticNode node) {
members.remove(node);
if (node.isGenerator() && generators.contains(node)) {
generators.remove(node);
rootSpeedDirty = true;
rootSpeedChanged = true;
}
if (node.hasStressImpact()) onMemberStressImpactUpdated();
if (node.hasStressCapacity()) onMemberStressCapacityUpdated();
members.remove(node);
stressConnectors.remove(node);
conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node);
}
@ -105,73 +132,88 @@ public class KineticNetwork {
}
rootTheoreticalSpeed = newSpeed * sign;
if (!overstressed) {
rootSpeedCur = rootTheoreticalSpeed;
}
mainGenerator = newGenerator;
rootSpeedDirty = false;
return result;
}
/**
* @return a List of new networks created during this function call
*/
public List<KineticNetwork> tick() {
List<KineticNetwork> newNetworks = updateMemberSpeeds();
public float getTotalStressImpact() {
return totalStressImpact.get();
}
if (generators.isEmpty()) {
overstressed = false;
members.forEach(KineticNode::stop);
members.forEach(KineticNode::flushChangedSpeed);
return newNetworks;
public float getTotalStressCapacity() {
return totalStressCapacity.get();
}
private float getRootSpeed() {
return isStopped() ? 0 : rootTheoreticalSpeed;
}
public void untick() {
ticked = false;
}
public void tick(List<KineticNetwork> newNetworks) {
if (ticked) return;
Set<KineticNetwork> stressConnected = stressConnectors.stream()
.flatMap(KineticNode::getActiveStressOnlyConnections)
.collect(Collectors.toSet());
stressConnected.add(this);
float stressImpact = 0;
float stressCapacity = 0;
for (KineticNetwork cur : stressConnected) {
cur.ticked = true;
cur.updateMemberSpeeds(newNetworks);
stressImpact += cur.getTotalStressImpact();
stressCapacity += cur.getTotalStressCapacity();
}
float stressImpact = (float) members.stream().mapToDouble(n -> n.getTotalStressImpact(rootTheoreticalSpeed)).sum();
float stressCapacity = (float) members.stream().mapToDouble(KineticNode::getStressCapacity).sum();
boolean nowOverstressed = stressImpact > stressCapacity;
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());
for (KineticNetwork cur : stressConnected) {
if (cur.generators.isEmpty()) {
cur.overstressed = false;
} else if (nowOverstressed) {
if (!cur.overstressed) {
cur.overstressed = true;
rootSpeedChanged = true;
cur.members.forEach(KineticNode::stop);
}
} else {
if (cur.overstressed) {
cur.overstressed = false;
rootSpeedChanged = true;
cur.bulldozeContradictingMembers(newNetworks);
cur.updateMemberSpeeds(newNetworks);
}
}
cur.members.forEach(KineticNode::flushChangedSpeed);
}
members.forEach(KineticNode::flushChangedSpeed);
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
* @param newNetworks a List that any new networks created during this call will be added to
*/
private List<KineticNetwork> updateMemberSpeeds() {
boolean rootSpeedChanged = rootSpeedPrev != rootSpeedCur;
rootSpeedPrev = rootSpeedCur;
private void updateMemberSpeeds(List<KineticNetwork> newNetworks) {
// 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);
return new LinkedList<>();
return;
}
SolveResult recalculateSpeedResult = tryRecalculateSpeed();
// generators should not be turning against each other or have conflicting cycles by now
assert(recalculateSpeedResult.isOk());
List<KineticNetwork> newNetworks = new LinkedList<>();
if (rootSpeedChanged) {
// root speed changed, update all nodes starting from the main generator
rootSpeedChanged = false;
bfs(mainGenerator, newNetworks, false);
} else if (!potentialNewBranches.isEmpty()) {
// new nodes added, update only the new network branches
@ -180,8 +222,6 @@ public class KineticNetwork {
.forEach(n -> bfs(n, newNetworks, true));
potentialNewBranches.clear();
}
return newNetworks;
}
private void bfs(KineticNode root, List<KineticNetwork> newNetworks, boolean followSource) {
@ -195,7 +235,7 @@ public class KineticNetwork {
if (!members.contains(cur) || visited.contains(cur)) continue;
visited.add(cur);
if (cur.tryUpdateSpeed(rootSpeedCur).isOk()) {
if (cur.tryUpdateSpeed(getRootSpeed()).isOk()) {
cur.getActiveConnections()
.map(Pair::getFirst)
.filter(n -> !followSource || n.getSource() == cur)
@ -208,7 +248,7 @@ public class KineticNetwork {
}
}
private List<KineticNetwork> bulldozeContradictingMembers() {
private void bulldozeContradictingMembers(List<KineticNetwork> newNetworks) {
/*
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
@ -216,10 +256,8 @@ public class KineticNetwork {
just pop all potentially contradicting nodes off and hope no one cares
*/
List<KineticNetwork> newNetworks = new LinkedList<>();
// generators running against network
float sign = Math.signum(rootSpeedCur);
float sign = Math.signum(rootTheoreticalSpeed);
List<KineticNode> runningAgainst = generators.stream()
.filter(n -> Math.signum(n.getGeneratedSpeedAtRoot()) != sign)
.collect(Collectors.toList());
@ -230,8 +268,6 @@ public class KineticNetwork {
.map(Pair::getFirst)
.collect(Collectors.toList());
cycles.forEach(n -> { n.popBlock(); newNetworks.add(n.getNetwork()); });
return newNetworks;
}
}

View file

@ -39,8 +39,8 @@ public class KineticNode {
this.connections = entity.getConnections();
this.generatedSpeed = entity.getGeneratedSpeed();
this.stressCapacity = entity.getStressCapacity();
this.stressImpact = entity.getStressImpact();
this.stressCapacity = entity.getStressCapacity();
this.network = new KineticNetwork(this);
}
@ -70,6 +70,14 @@ public class KineticNode {
return getActiveConnections().collect(Collectors.toList());
}
public Stream<KineticNetwork> getActiveStressOnlyConnections() {
return connections.getDirections().stream()
.map(d -> nodeAccessor.apply(entity.getBlockPos().offset(d))
.filter(n -> connections.checkStressOnlyConnection(n.connections, d)))
.flatMap(Optional::stream)
.map(KineticNode::getNetwork);
}
public float getGeneratedSpeedAtRoot() {
return generatedSpeed / speedRatio;
}
@ -79,17 +87,34 @@ public class KineticNode {
}
public void onUpdated() {
float newSpeed = entity.getGeneratedSpeed();
if (generatedSpeed != newSpeed) {
generatedSpeed = newSpeed;
network.updateMember(this);
float generatedSpeedNew = entity.getGeneratedSpeed();
if (this.generatedSpeed != generatedSpeedNew) {
this.generatedSpeed = generatedSpeedNew;
network.onMemberGeneratedSpeedUpdated(this);
if (network.tryRecalculateSpeed().isContradiction()) {
popBlock();
}
}
stressImpact = entity.getStressImpact();
stressCapacity = entity.getStressCapacity();
float stressImpactNew = entity.getStressImpact();
if (this.stressImpact != stressImpactNew) {
this.stressImpact = stressImpactNew;
network.onMemberStressImpactUpdated();
}
float stressCapacityNew = entity.getStressCapacity();
if (this.stressCapacity != stressCapacityNew) {
this.stressCapacity = stressCapacityNew;
network.onMemberStressCapacityUpdated();
}
}
public boolean hasStressCapacity() {
return stressCapacity != 0;
}
public boolean hasStressImpact() {
return stressImpact != 0;
}
public float getTheoreticalSpeed(float speedAtRoot) {
@ -111,7 +136,7 @@ public class KineticNode {
return network.tryRecalculateSpeed();
}
public KineticNode getSource() {
public @Nullable KineticNode getSource() {
return source;
}

View file

@ -55,17 +55,16 @@ public class KineticSolver {
}
public void tick() {
Set<KineticNetwork> visited = new HashSet<>();
Set<KineticNetwork> networks = nodes.values().stream().map(KineticNode::getNetwork).collect(Collectors.toSet());
networks.forEach(KineticNetwork::untick);
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());
cur.tick(frontier);
}
}
}