Now with 100% less solver

This commit is contained in:
reidbhuntley 2021-12-28 13:20:31 -05:00
parent 8f5a885bb0
commit 1c8f9232b7
25 changed files with 579 additions and 517 deletions

View file

@ -18,8 +18,9 @@ import com.simibubi.create.content.contraptions.goggles.IHaveGoggleInformation;
import com.simibubi.create.content.contraptions.goggles.IHaveHoveringInformation;
import com.simibubi.create.content.contraptions.relays.elementary.ICogWheel;
import com.simibubi.create.content.contraptions.relays.gearbox.GearboxBlock;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import com.simibubi.create.content.contraptions.solver.KineticNodeState;
import com.simibubi.create.content.contraptions.solver.KineticSolver;
import com.simibubi.create.content.contraptions.solver.SolverBlock;
import com.simibubi.create.foundation.block.BlockStressValues;
import com.simibubi.create.foundation.config.AllConfigs;
import com.simibubi.create.foundation.item.TooltipHelper;
@ -72,6 +73,26 @@ public class KineticTileEntity extends SmartTileEntity
protected float lastStressApplied;
protected float lastCapacityProvided;
private KineticNodeState kineticNodeState;
public KineticNodeState getKineticNodeState() {
return kineticNodeState;
}
public KineticNodeState getInitialKineticNodeState() {
return new KineticNodeState(new KineticConnections(), 0);
}
private void addToSolver() {
kineticNodeState = getInitialKineticNodeState();
KineticSolver.getSolver(level).addNode(this);
}
private void removeFromSolver() {
KineticSolver.getSolver(level).removeNode(this);
}
public KineticTileEntity(BlockEntityType<?> typeIn, BlockPos pos, BlockState state) {
super(typeIn, pos, state);
effects = new KineticEffectHandler(this);
@ -87,6 +108,10 @@ public class KineticTileEntity extends SmartTileEntity
network.addSilently(this, lastCapacityProvided, lastStressApplied);
}
if (!level.isClientSide) {
addToSolver();
}
super.initialize();
}
@ -98,6 +123,10 @@ public class KineticTileEntity extends SmartTileEntity
super.tick();
effects.tick();
if (!level.isClientSide && !isRemoved()) {
KineticSolver.getSolver(level).updateNode(this);
}
if (level.isClientSide) {
cachedBoundingBox = null; // cache the bounding box for every frame between ticks
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> this.tickAudio());
@ -196,6 +225,7 @@ public class KineticTileEntity extends SmartTileEntity
if (hasNetwork())
getOrCreateNetwork().remove(this);
detachKinetics();
removeFromSolver();
}
super.setRemovedNotDueToChunkUnload();
}
@ -346,24 +376,10 @@ public class KineticTileEntity extends SmartTileEntity
public void attachKinetics() {
updateSpeed = false;
KineticSolver solver = KineticSolver.getSolver(level);
BlockState state = getBlockState();
if (state.getBlock() instanceof SolverBlock sb) {
solver.removeAllRules(worldPosition);
sb.created(solver, level, worldPosition);
}
RotationPropagator.handleAdded(level, worldPosition, this);
}
public void detachKinetics() {
KineticSolver solver = KineticSolver.getSolver(level);
BlockState state = getBlockState();
if (state.getBlock() instanceof SolverBlock) {
solver.removeAllRules(worldPosition);
}
RotationPropagator.handleRemoved(level, worldPosition, this);
}

View file

@ -3,10 +3,7 @@ package com.simibubi.create.content.contraptions.components.motor;
import com.simibubi.create.AllShapes;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.contraptions.base.DirectionalKineticBlock;
import com.simibubi.create.content.contraptions.solver.ConstantSpeedRule;
import com.simibubi.create.content.contraptions.solver.HalfShaftConnectionRule;
import com.simibubi.create.content.contraptions.solver.KineticSolver;
import com.simibubi.create.content.contraptions.solver.SolverBlock;
import com.simibubi.create.foundation.block.ITE;
import net.minecraft.core.BlockPos;
@ -22,7 +19,7 @@ import net.minecraft.world.level.pathfinder.PathComputationType;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
public class CreativeMotorBlock extends DirectionalKineticBlock implements ITE<CreativeMotorTileEntity>, SolverBlock {
public class CreativeMotorBlock extends DirectionalKineticBlock implements ITE<CreativeMotorTileEntity> {
public CreativeMotorBlock(Properties properties) {
super(properties);
@ -75,13 +72,4 @@ public class CreativeMotorBlock extends DirectionalKineticBlock implements ITE<C
return AllTileEntities.MOTOR.get();
}
@Override
public void created(KineticSolver solver, Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
Direction to = state.getValue(FACING);
int speed = getTileEntityOptional(level, pos).map(te -> te.generatedSpeed.getValue()).orElse(0);
solver.addRule(pos, new HalfShaftConnectionRule(to));
solver.addRule(pos, new ConstantSpeedRule(speed));
}
}

View file

@ -4,6 +4,8 @@ import java.util.List;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.content.contraptions.base.GeneratingKineticTileEntity;
import com.simibubi.create.content.contraptions.solver.AllConnections;
import com.simibubi.create.content.contraptions.solver.KineticNodeState;
import com.simibubi.create.foundation.config.AllConfigs;
import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
import com.simibubi.create.foundation.tileEntity.behaviour.CenteredSideValueBoxTransform;
@ -37,11 +39,19 @@ public class CreativeMotorTileEntity extends GeneratingKineticTileEntity {
generatedSpeed.value = DEFAULT_SPEED;
generatedSpeed.scrollableValue = DEFAULT_SPEED;
generatedSpeed.withUnit(i -> Lang.translate("generic.unit.rpm"));
generatedSpeed.withCallback(i -> this.updateGeneratedRotation());
generatedSpeed.withCallback(i -> this.getKineticNodeState().setGeneratedSpeed(getGeneratedSpeed()));
generatedSpeed.withStepFunction(CreativeMotorTileEntity::step);
behaviours.add(generatedSpeed);
}
@Override
public KineticNodeState getInitialKineticNodeState() {
return new KineticNodeState(
AllConnections.HALF_SHAFT.apply(getBlockState().getValue(CreativeMotorBlock.FACING)),
getGeneratedSpeed()
);
}
@Override
public void initialize() {
super.initialize();

View file

@ -8,6 +8,8 @@ import com.simibubi.create.content.contraptions.base.IRotate;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.content.contraptions.relays.advanced.SpeedControllerBlock;
import com.simibubi.create.content.contraptions.relays.encased.EncasedCogwheelBlock;
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.MethodsReturnNonnullByDefault;
@ -33,7 +35,7 @@ import net.minecraft.world.phys.shapes.VoxelShape;
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class CogWheelBlock extends AbstractShaftBlock implements ICogWheel {
public class CogWheelBlock extends AbstractShaftBlock implements ICogWheel, ISimpleConnectable {
boolean isLarge;
@ -60,6 +62,12 @@ public class CogWheelBlock extends AbstractShaftBlock implements ICogWheel {
return !isLarge;
}
@Override
public KineticConnections getConnections(BlockState state) {
return (isLargeCog() ? AllConnections.LARGE_COG_FULL_SHAFT : AllConnections.SMALL_COG_FULL_SHAFT)
.apply(state.getValue(AXIS));
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) {
return (isLarge ? AllShapes.LARGE_GEAR : AllShapes.SMALL_GEAR).get(state.getValue(AXIS));
@ -105,7 +113,7 @@ public class CogWheelBlock extends AbstractShaftBlock implements ICogWheel {
encasedState.cycle(d.getAxisDirection() == AxisDirection.POSITIVE ? EncasedCogwheelBlock.TOP_SHAFT
: EncasedCogwheelBlock.BOTTOM_SHAFT);
}
KineticTileEntity.switchToBlockState(world, pos, encasedState);
return InteractionResult.SUCCESS;
}

View file

@ -0,0 +1,9 @@
package com.simibubi.create.content.contraptions.relays.elementary;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import net.minecraft.world.level.block.state.BlockState;
public interface ISimpleConnectable {
KineticConnections getConnections(BlockState state);
}

View file

@ -4,12 +4,10 @@ import java.util.function.Predicate;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllShapes;
import com.simibubi.create.content.contraptions.solver.KineticSolver;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.content.contraptions.relays.encased.EncasedShaftBlock;
import com.simibubi.create.content.contraptions.solver.ShaftConnectionRule;
import com.simibubi.create.content.contraptions.solver.ShaftEqualSpeedRule;
import com.simibubi.create.content.contraptions.solver.SolverBlock;
import com.simibubi.create.content.contraptions.solver.AllConnections;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import com.simibubi.create.foundation.advancement.AllTriggers;
import com.simibubi.create.foundation.utility.placement.IPlacementHelper;
import com.simibubi.create.foundation.utility.placement.PlacementHelpers;
@ -30,7 +28,7 @@ import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
public class ShaftBlock extends AbstractShaftBlock implements SolverBlock {
public class ShaftBlock extends AbstractShaftBlock implements ISimpleConnectable {
private static final int placementHelperId = PlacementHelpers.register(new PlacementHelper());
@ -42,6 +40,11 @@ public class ShaftBlock extends AbstractShaftBlock implements SolverBlock {
return AllBlocks.SHAFT.has(state);
}
@Override
public KineticConnections getConnections(BlockState state) {
return AllConnections.FULL_SHAFT.apply(state.getValue(AXIS));
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter worldIn, BlockPos pos, CollisionContext context) {
return AllShapes.SIX_VOXEL_POLE.get(state.getValue(AXIS));
@ -87,18 +90,6 @@ public class ShaftBlock extends AbstractShaftBlock implements SolverBlock {
return InteractionResult.PASS;
}
@Override
public void created(KineticSolver solver, Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
Direction.Axis axis = state.getValue(AXIS);
Direction positive = Direction.fromAxisAndDirection(axis, Direction.AxisDirection.POSITIVE);
Direction negative = positive.getOpposite();
solver.addRule(pos, new ShaftConnectionRule(axis));
solver.addRule(pos, new ShaftEqualSpeedRule(positive));
solver.addRule(pos, new ShaftEqualSpeedRule(negative));
}
@MethodsReturnNonnullByDefault
private static class PlacementHelper extends PoleHelper<Direction.Axis> {
//used for extending a shaft in its axis, like the piston poles. works with shafts and cogs

View file

@ -1,10 +1,12 @@
package com.simibubi.create.content.contraptions.relays.elementary;
import java.util.List;
import com.simibubi.create.content.contraptions.base.IRotate;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.content.contraptions.base.RotatedPillarKineticBlock;
import com.simibubi.create.content.contraptions.solver.AllConnections;
import com.simibubi.create.content.contraptions.solver.KineticConnections;
import com.simibubi.create.content.contraptions.solver.KineticNodeState;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
@ -21,22 +23,19 @@ public class SimpleKineticTileEntity extends KineticTileEntity {
return new AABB(worldPosition).inflate(1);
}
@Override
public List<BlockPos> addPropagationLocations(IRotate block, BlockState state, List<BlockPos> neighbours) {
if (!ICogWheel.isLargeCog(state))
return super.addPropagationLocations(block, state, neighbours);
BlockPos.betweenClosedStream(new BlockPos(-1, -1, -1), new BlockPos(1, 1, 1))
.forEach(offset -> {
if (offset.distSqr(0, 0, 0, false) == BlockPos.ZERO.distSqr(1, 1, 0, false))
neighbours.add(worldPosition.offset(offset));
});
return neighbours;
}
@Override
protected boolean isNoisy() {
return false;
}
@Override
public KineticNodeState getInitialKineticNodeState() {
KineticConnections connections = AllConnections.EMPTY;
BlockState state = getBlockState();
if (state.getBlock() instanceof ISimpleConnectable connectable) {
connections = connectable.getConnections(state);
}
return new KineticNodeState(connections, 0);
}
}

View file

@ -0,0 +1,65 @@
package com.simibubi.create.content.contraptions.solver;
import com.simibubi.create.foundation.utility.DirectionHelper;
import com.simibubi.create.foundation.utility.LazyMap;
import com.simibubi.create.content.contraptions.solver.KineticConnections.Type;
import com.simibubi.create.content.contraptions.solver.KineticConnections.Entry;
import net.minecraft.core.Direction;
import java.util.LinkedList;
import java.util.List;
public class AllConnections {
private static Direction pos(Direction.Axis axis) {
return Direction.get(Direction.AxisDirection.POSITIVE, axis);
}
private static Direction neg(Direction.Axis axis) {
return Direction.get(Direction.AxisDirection.NEGATIVE, axis);
}
public static final KineticConnections EMPTY = new KineticConnections();
public static final LazyMap<Direction, KineticConnections> HALF_SHAFT
= new LazyMap<>(dir -> new KineticConnections(new Entry(dir.getNormal(), Type.SHAFT)));
public static final LazyMap<Direction.Axis, KineticConnections> FULL_SHAFT
= new LazyMap<>(axis -> HALF_SHAFT.apply(pos(axis)).merge(HALF_SHAFT.apply(neg(axis))));
public static final LazyMap<Direction.Axis, KineticConnections> LARGE_COG
= new LazyMap<>(axis -> {
List<Entry> out = new LinkedList<>();
Direction cur = DirectionHelper.getPositivePerpendicular(axis);
for (int i = 0; i < 4; i++) {
Direction next = DirectionHelper.rotateAround(cur, axis);
out.add(new Entry(cur.getNormal().multiply(2), Type.LARGE_COG, -1));
out.add(new Entry(cur.getNormal().relative(pos(axis)), Type.LARGE_COG, -1));
out.add(new Entry(cur.getNormal().relative(neg(axis)), Type.LARGE_COG, -1));
out.add(new Entry(cur.getNormal().relative(next), Type.LARGE_COG, Type.SMALL_COG, -2));
cur = next;
}
return new KineticConnections(out);
});
public static final LazyMap<Direction.Axis, KineticConnections> SMALL_COG
= new LazyMap<>(axis -> {
List<Entry> out = new LinkedList<>();
Direction cur = DirectionHelper.getPositivePerpendicular(axis);
for (int i = 0; i < 4; i++) {
Direction next = DirectionHelper.rotateAround(cur, axis);
out.add(new Entry(cur.getNormal(), Type.SMALL_COG, -1));
out.add(new Entry(cur.getNormal().relative(next), Type.SMALL_COG, Type.LARGE_COG, -0.5f));
cur = next;
}
return new KineticConnections(out);
});
public static final LazyMap<Direction.Axis, KineticConnections> LARGE_COG_FULL_SHAFT
= new LazyMap<>(axis -> LARGE_COG.apply(axis).merge(FULL_SHAFT.apply(axis)));
public static final LazyMap<Direction.Axis, KineticConnections> SMALL_COG_FULL_SHAFT
= new LazyMap<>(axis -> SMALL_COG.apply(axis).merge(FULL_SHAFT.apply(axis)));
}

View file

@ -1,10 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.Vec3i;
import java.util.Set;
public class AllPropertyTypes {
public static final Property.Type<Float> SPEED = new Property.Type<>();
public static final Property.Type<Set<Vec3i>> SHAFT_CONNECTIONS = new Property.Type<>();
}

View file

@ -1,21 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import java.util.Optional;
public class ConstantSpeedRule implements RewriteRule.Descriptor<Float> {
private final float speed;
public ConstantSpeedRule(float speed) {
this.speed = speed;
}
@Override
public Property.Type<Float> getWrittenProperty() {
return AllPropertyTypes.SPEED;
}
@Override
public Optional<Float> getRewrittenValue(RewriteRule.PropertyReader reader) {
return Optional.of(speed);
}
}

View file

@ -1,25 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import java.util.Optional;
import java.util.Set;
public class HalfShaftConnectionRule implements RewriteRule.Descriptor<Set<Vec3i>> {
private final Set<Vec3i> connections;
public HalfShaftConnectionRule(Direction dir) {
connections = Set.of(dir.getNormal());
}
@Override
public Property.Type<Set<Vec3i>> getWrittenProperty() {
return AllPropertyTypes.SHAFT_CONNECTIONS;
}
@Override
public Optional<Set<Vec3i>> getRewrittenValue(RewriteRule.PropertyReader reader) {
return Optional.of(connections);
}
}

View file

@ -0,0 +1,90 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.Vec3i;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class KineticConnections {
public enum Type {
SHAFT, SMALL_COG, LARGE_COG, BELT
}
public static record Entry(Vec3i offset, Value value) {
public Entry(Vec3i offset, Type from, Type to, float ratio) {
this(offset, new Value(from, to, ratio));
}
public Entry(Vec3i offset, Type type, float ratio) {
this(offset, type, type, ratio);
}
public Entry(Vec3i offset, Type type) {
this(offset, type, 1);
}
}
private static record Value(Type from, Type to, float ratio) { }
private final Map<Vec3i, Value> connections;
private KineticConnections(Map<Vec3i, Value> connections) {
this.connections = connections;
}
public KineticConnections(Stream<Entry> entries) {
this(entries.collect(Collectors.toMap(Entry::offset, Entry::value)));
}
public KineticConnections(Collection<Entry> entries) {
this(entries.stream());
}
public KineticConnections(Entry... entries) {
this(Arrays.stream(entries));
}
public Set<Vec3i> getDirections() {
return connections.keySet();
}
public Optional<Float> checkConnection(KineticConnections to, Vec3i offset) {
Value fromValue = connections.get(offset);
if (fromValue == null) return Optional.empty();
Value toValue = to.connections.get(offset.multiply(-1));
if (toValue == null) return Optional.empty();
if (fromValue.from.equals(toValue.to) && fromValue.to.equals(toValue.from))
return Optional.of(fromValue.ratio);
return Optional.empty();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KineticConnections that = (KineticConnections) o;
return Objects.equals(connections, that.connections);
}
@Override
public int hashCode() {
return Objects.hash(connections);
}
public KineticConnections merge(KineticConnections other) {
Map<Vec3i, Value> out = new HashMap<>();
out.putAll(other.connections);
out.putAll(connections);
return new KineticConnections(out);
}
}

View file

@ -0,0 +1,88 @@
package com.simibubi.create.content.contraptions.solver;
import com.simibubi.create.foundation.utility.Pair;
import java.util.HashSet;
import java.util.Set;
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 rootSpeed;
private boolean speedDirty;
public KineticNetwork(KineticNode root) {
addMember(root);
rootSpeed = root.getGeneratedSpeed();
speedDirty = false;
}
public void addMember(KineticNode node) {
members.add(node);
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() && generators.contains(node)) {
generators.remove(node);
speedDirty = true;
}
conflictingCycles.removeIf(p -> p.getFirst() == node || p.getSecond() == node);
}
public void markConflictingCycle(KineticNode from, KineticNode to) {
if (!members.contains(from) || !members.contains(to)) throw new IllegalArgumentException();
conflictingCycles.add(Pair.of(from, to));
}
public float getRootSpeed() {
return rootSpeed;
}
public SolveResult recalculateSpeed() {
if (!conflictingCycles.isEmpty() && !generators.isEmpty()) return SolveResult.CONTRADICTION;
if (!speedDirty) return SolveResult.OK;
float newSpeed = 0;
float sign = 0;
for (KineticNode generator : generators) {
float speedAtRoot = generator.getGeneratedSpeedAtRoot();
if (newSpeed == 0) {
sign = Math.signum(speedAtRoot);
newSpeed = sign * speedAtRoot;
} else {
if (Math.signum(speedAtRoot) != sign)
return SolveResult.CONTRADICTION;
newSpeed = Math.max(newSpeed, sign * speedAtRoot);
}
}
newSpeed *= sign;
if (rootSpeed != newSpeed) {
rootSpeed = newSpeed;
for (KineticNode member : members) {
member.onSpeedUpdated();
}
}
speedDirty = false;
return SolveResult.OK;
}
}

View file

@ -0,0 +1,158 @@
package com.simibubi.create.content.contraptions.solver;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.foundation.utility.Pair;
import net.minecraft.core.BlockPos;
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;
public class KineticNode {
private final Function<BlockPos, Optional<KineticNode>> nodeAccessor;
private final KineticTileEntity entity;
private @Nullable KineticNode source;
private KineticNetwork network;
private float speedRatio = 1;
private final KineticConnections connections;
private float generatedSpeed;
private float speedCur;
private float speedNext;
public KineticNode(KineticTileEntity entity, Function<BlockPos, Optional<KineticNode>> nodeAccessor) {
this.nodeAccessor = nodeAccessor;
this.entity = entity;
KineticNodeState state = entity.getKineticNodeState();
this.connections = state.getConnections();
this.generatedSpeed = state.getGeneratedSpeed();
this.network = new KineticNetwork(this);
onSpeedUpdated();
}
public KineticConnections getConnections() {
return connections;
}
public Map<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));
}
public float getGeneratedSpeed() {
return generatedSpeed;
}
public float getGeneratedSpeedAtRoot() {
return generatedSpeed / speedRatio;
}
public boolean isGenerator() {
return generatedSpeed != 0;
}
public void setGeneratedSpeed(float newSpeed) {
generatedSpeed = newSpeed;
network.updateMember(this);
if (network.recalculateSpeed().isContradiction())
onPopBlock();
}
private void setNetwork(KineticNetwork network) {
this.network.removeMember(this);
this.network = network;
network.addMember(this);
onSpeedUpdated();
}
private void setSource(KineticNode from, float ratio) {
source = from;
speedRatio = from.speedRatio * ratio;
setNetwork(from.network);
}
public void onAdded() {
getActiveConnections()
.keySet()
.stream()
.findAny()
.ifPresent(n -> {
if (n.propagateSource().isContradiction())
onPopBlock();
});
}
public void onRemoved() {
network.removeMember(this);
for (KineticNode neighbor : getActiveConnections().keySet()) {
if (neighbor.source != this) continue;
neighbor.rerootHere();
}
network.recalculateSpeed();
}
private SolveResult 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();
if (next == cur.source) continue;
if (next.network == network) {
if (next.speedRatio != cur.speedRatio * ratio) {
network.markConflictingCycle(cur, next);
}
continue;
}
next.setSource(cur, ratio);
frontier.add(next);
}
}
return network.recalculateSpeed();
}
private void rerootHere() {
source = null;
speedRatio = 1;
setNetwork(new KineticNetwork(this));
propagateSource();
}
public void onSpeedUpdated() {
speedNext = network.getRootSpeed() * speedRatio;
}
public void flushChangedSpeed() {
if (speedCur != speedNext) {
speedCur = speedNext;
// TODO: update entity's speed
System.out.printf("Set speed of %s to %f\n", this, speedNext);
}
}
public void onPopBlock() {
// this should cause the node to get removed from the solver and lead to onRemoved() being called
entity.getLevel().destroyBlock(entity.getBlockPos(), true);
}
}

View file

@ -0,0 +1,27 @@
package com.simibubi.create.content.contraptions.solver;
public class KineticNodeState {
private KineticConnections connections;
private float generatedSpeed;
public KineticNodeState(KineticConnections connections, float generatedSpeed) {
this.connections = connections;
this.generatedSpeed = generatedSpeed;
}
public KineticConnections getConnections() {
return connections;
}
public float getGeneratedSpeed() {
return generatedSpeed;
}
public void setConnections(KineticConnections connections) {
this.connections = connections;
}
public void setGeneratedSpeed(float generatedSpeed) {
this.generatedSpeed = generatedSpeed;
}
}

View file

@ -1,12 +1,10 @@
package com.simibubi.create.content.contraptions.solver;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Optional;
import com.simibubi.create.content.contraptions.base.KineticTileEntity;
import com.simibubi.create.foundation.utility.WorldAttached;
import net.minecraft.core.BlockPos;
@ -20,71 +18,39 @@ public class KineticSolver {
return SOLVERS.get(level);
}
private final PropertyMap properties = new PropertyMap();
private final Map<BlockPos, Set<RewriteRule.Tracker<?>>> rules = new HashMap<>();
private final HashSet<RewriteRule.Tracker<?>> allRules = new HashSet<>();
private final Map<BlockPos, KineticNode> nodes = new HashMap<>();
private Set<RewriteRule.Tracker<?>> rulesFrontier = new HashSet<>();
public <T> RewriteRule<T> addRule(BlockPos pos, RewriteRule.Descriptor<T> ruleDesc) {
RewriteRule<T> rule = new RewriteRule<>(ruleDesc);
RewriteRule.Tracker<?> tracker = new RewriteRule.Tracker<>(rule, pos, properties::trackReader);
rules.computeIfAbsent(pos, $ -> new HashSet<>()).add(tracker);
allRules.add(tracker);
rulesFrontier.add(tracker);
return rule;
public void addNode(KineticTileEntity entity) {
removeNode(entity);
KineticNode node = new KineticNode(entity, this::getNode);
nodes.put(entity.getBlockPos(), node);
node.onAdded();
}
public void removeRule(BlockPos pos, RewriteRule<?> rule) {
Set<RewriteRule.Tracker<?>> trackers = rules.get(pos);
if (trackers == null) return;
trackers.stream()
.filter(t -> t.rule == rule)
.findAny()
.ifPresent(tracker -> {
allRules.remove(tracker);
trackers.remove(tracker);
if (trackers.isEmpty()) {
rules.remove(pos);
}
properties.untrackReader(tracker);
rulesFrontier.addAll(properties.unwrite(tracker.writes));
});
}
public void updateNode(KineticTileEntity entity) {
KineticNode node = nodes.get(entity.getBlockPos());
KineticNodeState state = entity.getKineticNodeState();
public void removeAllRules(BlockPos pos) {
Set<RewriteRule.Tracker<?>> trackers = rules.remove(pos);
if (trackers == null) return;
for (RewriteRule.Tracker<?> tracker: trackers) {
allRules.remove(tracker);
properties.untrackReader(tracker);
}
for (RewriteRule.Tracker<?> tracker: trackers) {
rulesFrontier.addAll(properties.unwrite(tracker.writes));
if (!node.getConnections().equals(state.getConnections())) {
// connections changed, so things could've been disconnected
removeNode(entity);
addNode(entity);
} else {
// connections are the same, so just set speed in case it changed
node.setGeneratedSpeed(state.getGeneratedSpeed());
}
}
public Set<BlockPos> solve() {
Set<BlockPos> contradictions = new HashSet<>();
protected Optional<KineticNode> getNode(BlockPos pos) {
return Optional.ofNullable(nodes.get(pos));
}
while (!rulesFrontier.isEmpty()) {
Set<RewriteRule.Tracker<?>> next = new HashSet<>();
public void removeNode(KineticTileEntity entity) {
KineticNode node = nodes.remove(entity.getBlockPos());
if (node != null) node.onRemoved();
}
for (RewriteRule.Tracker<?> rule : rulesFrontier) {
if (!allRules.contains(rule) || !rule.canRewrite()) continue;
PropertyMap.WriteResult res = rule.rewrite(properties);
if (res instanceof PropertyMap.WriteResult.Ok ok) {
next.addAll(ok.readyToRewrite);
} else if (res instanceof PropertyMap.WriteResult.Contradiction) {
removeAllRules(rule.pos);
contradictions.add(rule.pos);
}
}
rulesFrontier = next;
}
return contradictions;
public void flushChangedSpeeds() {
nodes.values().forEach(KineticNode::flushChangedSpeed);
}
}

View file

@ -1,46 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import java.util.Objects;
public record Property<T>(BlockPos pos, Type<T> type) {
public static class Type<T> { }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Property<?> property = (Property<?>) o;
return Objects.equals(pos, property.pos) && Objects.equals(type, property.type);
}
@Override
public int hashCode() {
return Objects.hash(pos, type);
}
public static record Relative<T>(Vec3i offset, Type<T> type) {
public Relative(Type<T> type) {
this(Vec3i.ZERO, type);
}
public Property<T> toAbsolute(BlockPos pos) {
return new Property<>(pos.offset(offset), type);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Relative<?> relative = (Relative<?>) o;
return Objects.equals(offset, relative.offset) && Objects.equals(type, relative.type);
}
@Override
public int hashCode() {
return Objects.hash(offset, type);
}
}
}

View file

@ -1,139 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.BlockPos;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class PropertyMap {
public static sealed class WriteResult {
public static final class Ok extends WriteResult {
public static final Ok V = new Ok(Set.of());
public final Set<RewriteRule.Tracker<?>> readyToRewrite;
public Ok(Set<RewriteRule.Tracker<?>> readyToRewrite) { this.readyToRewrite = readyToRewrite; }
}
public static final class Contradiction extends WriteResult {
public static final Contradiction V = new Contradiction();
}
}
private static class Counter<T> {
public final Property<T> key;
private T value;
private final Set<RewriteRule.Tracker<?>> readers = new HashSet<>();
public Counter(Property<T> key) {
this.key = key;
}
public boolean isEmpty() {
return value == null;
}
public boolean canDrop() {
return isEmpty() && readers.isEmpty();
}
public Optional<T> read() {
return Optional.ofNullable(value);
}
public WriteResult write(@Nonnull T newValue) {
WriteResult result = WriteResult.Ok.V;
if (isEmpty()) {
value = newValue;
// notify readers
readers.forEach(RewriteRule.Tracker::dependencyRemoved);
result = new WriteResult.Ok(readers.stream()
.filter(RewriteRule.Tracker::canRewrite)
.collect(Collectors.toSet()));
} else if (!value.equals(newValue)) {
return WriteResult.Contradiction.V;
}
return result;
}
public void unwrite() {
if (isEmpty()) throw new IllegalStateException();
value = null;
// notify readers
readers.forEach(RewriteRule.Tracker::dependencyAdded);
}
public boolean trackReader(RewriteRule.Tracker<?> reader) {
readers.add(reader);
return isEmpty();
}
public void untrackReader(RewriteRule.Tracker<?> reader) {
readers.remove(reader);
}
}
private final Map<Property<?>, Counter<?>> properties = new HashMap<>();
public <T> Optional<T> read(Property<T> property) {
Counter<T> counter = (Counter<T>) properties.get(property);
if (counter == null) return Optional.empty();
return counter.read();
}
public <T> WriteResult write(Property<T> property, T value) {
Counter<T> counter = (Counter<T>) properties.computeIfAbsent(property, $ -> new Counter<>(property));
return counter.write(value);
}
public Set<RewriteRule.Tracker<?>> unwrite(Property<?> property) {
Counter<?> init = properties.get(property);
if (init == null) return Set.of();
Set<Counter<?>> toVisit = new HashSet<>();
toVisit.add(init);
Set<Counter<?>> visited = new HashSet<>();
while (!toVisit.isEmpty()) {
Set<Counter<?>> next = new HashSet<>();
for (Counter<?> c : toVisit) {
if (c.isEmpty() || visited.contains(c)) continue;
visited.add(c);
c.unwrite();
if (c.canDrop()) {
properties.put(c.key, null);
}
c.readers.stream()
.map(r -> properties.get(r.writes))
.filter(Objects::nonNull)
.forEachOrdered(toVisit::add);
}
toVisit = next;
}
return visited.stream().flatMap(c -> c.readers.stream()).collect(Collectors.toSet());
}
public int trackReader(RewriteRule.Tracker<?> reader) {
int dependencies = 0;
for (Property<?> p : reader.reads) {
dependencies += properties.computeIfAbsent(p, $ -> new Counter<>(p)).trackReader(reader) ? 1 : 0;
}
return dependencies;
}
public void untrackReader(RewriteRule.Tracker<?> reader) {
for (Property<?> property : reader.reads) {
properties.get(property).untrackReader(reader);
}
}
}

View file

@ -1,76 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.BlockPos;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RewriteRule<T> {
private final Property.Type<T> writes;
private final Set<Property.Relative<?>> reads;
private final Descriptor<T> descriptor;
public interface Descriptor<T> {
Property.Type<T> getWrittenProperty();
default Set<Property.Relative<?>> getReadProperties() { return Set.of(); }
Optional<T> getRewrittenValue(PropertyReader reader);
}
public RewriteRule(Descriptor<T> descriptor) {
this.descriptor = descriptor;
this.writes = descriptor.getWrittenProperty();
this.reads = descriptor.getReadProperties();
}
record PropertyReader(BlockPos pos, PropertyMap map) {
public <U> U read(Property.Relative<U> property) {
return map.read(property.toAbsolute(pos)).get();
}
}
private PropertyMap.WriteResult rewrite(BlockPos pos, PropertyMap properties) {
return descriptor
.getRewrittenValue(new PropertyReader(pos, properties))
.map(v -> properties.write(new Property<>(pos, writes), v))
.orElse(PropertyMap.WriteResult.Ok.V);
}
public static class Tracker<T> {
public final RewriteRule<T> rule;
public final BlockPos pos;
public final Property<T> writes;
public final Set<Property<?>> reads;
private int dependencies;
public Tracker(RewriteRule<T> rule, BlockPos pos, Function<Tracker<?>, Integer> dependencies) {
this.rule = rule;
this.pos = pos;
this.writes = new Property<>(pos, rule.writes);
this.reads = rule.reads.stream().map(p -> p.toAbsolute(pos)).collect(Collectors.toSet());
this.dependencies = dependencies.apply(this);
}
public PropertyMap.WriteResult rewrite(PropertyMap properties) {
if (!canRewrite()) throw new IllegalStateException();
return rule.rewrite(pos, properties);
}
public void dependencyAdded() {
dependencies += 1;
}
public void dependencyRemoved() {
if (dependencies == 0) throw new IllegalStateException();
dependencies -= 1;
}
public boolean canRewrite() {
return dependencies == 0;
}
}
}

View file

@ -1,26 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import java.util.Optional;
import java.util.Set;
public class ShaftConnectionRule implements RewriteRule.Descriptor<Set<Vec3i>> {
private final Set<Vec3i> connections;
public ShaftConnectionRule(Direction.Axis axis) {
Direction positive = Direction.fromAxisAndDirection(axis, Direction.AxisDirection.POSITIVE);
connections = Set.of(positive.getNormal(), positive.getOpposite().getNormal());
}
@Override
public Property.Type<Set<Vec3i>> getWrittenProperty() {
return AllPropertyTypes.SHAFT_CONNECTIONS;
}
@Override
public Optional<Set<Vec3i>> getRewrittenValue(RewriteRule.PropertyReader reader) {
return Optional.of(connections);
}
}

View file

@ -1,40 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import java.util.Optional;
import java.util.Set;
public class ShaftEqualSpeedRule implements RewriteRule.Descriptor<Float> {
private final Vec3i to, from;
private final Property.Relative<Set<Vec3i>> otherConnections;
private final Property.Relative<Float> otherSpeed;
public ShaftEqualSpeedRule(Direction dir) {
to = dir.getNormal();
from = dir.getOpposite().getNormal();
otherConnections = new Property.Relative<>(to, AllPropertyTypes.SHAFT_CONNECTIONS);
otherSpeed = new Property.Relative<>(to, AllPropertyTypes.SPEED);
}
@Override
public Property.Type<Float> getWrittenProperty() {
return AllPropertyTypes.SPEED;
}
@Override
public Set<Property.Relative<?>> getReadProperties() {
return Set.of(otherConnections, otherSpeed);
}
@Override
public Optional<Float> getRewrittenValue(RewriteRule.PropertyReader reader) {
Set<Vec3i> otherConnections = reader.read(this.otherConnections);
float otherSpeed = reader.read(this.otherSpeed);
if (otherConnections.contains(from))
return Optional.of(otherSpeed);
return Optional.empty();
}
}

View file

@ -0,0 +1,8 @@
package com.simibubi.create.content.contraptions.solver;
public enum SolveResult {
OK, CONTRADICTION;
public boolean isOk() { return this == OK; }
public boolean isContradiction() { return this == CONTRADICTION; }
}

View file

@ -1,8 +0,0 @@
package com.simibubi.create.content.contraptions.solver;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
public interface SolverBlock {
void created(KineticSolver solver, Level level, BlockPos pos);
}

View file

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

View file

@ -0,0 +1,30 @@
package com.simibubi.create.foundation.utility;
import net.minecraftforge.common.util.NonNullFunction;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class LazyMap<K, V> implements Function<K, V> {
private final NonNullFunction<K, V> function;
private final Map<K, V> map;
public LazyMap(NonNullFunction<K, V> function, Map<K, V> map) {
this.function = function;
this.map = map;
}
public LazyMap(NonNullFunction<K, V> function) {
this(function, new HashMap<>());
}
@Override
public V apply(K k) {
return map.computeIfAbsent(k, function::apply);
}
public static <K, V> LazyMap<K, V> of(NonNullFunction<K, V> function) {
return new LazyMap<>(function);
}
}