From f942596b7fc5da55c0c59f93657ee54c7d0079a9 Mon Sep 17 00:00:00 2001 From: Curle Date: Sat, 1 Jul 2023 02:14:39 +0100 Subject: [PATCH] completely rework coolant logic --- src/main/java/uk/gemwire/engage/Engage.java | 4 +- .../block/coolant/CoolantBaseBlock.java | 167 +------------- .../systems/coolant/CoolantNetworkGraph.java | 215 ++++++++++++++++++ .../coolant/CoolantNetworksSavedData.java | 94 ++++++++ 4 files changed, 312 insertions(+), 168 deletions(-) create mode 100644 src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworkGraph.java create mode 100644 src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworksSavedData.java diff --git a/src/main/java/uk/gemwire/engage/Engage.java b/src/main/java/uk/gemwire/engage/Engage.java index 6041692..98f36ff 100644 --- a/src/main/java/uk/gemwire/engage/Engage.java +++ b/src/main/java/uk/gemwire/engage/Engage.java @@ -71,7 +71,5 @@ public class Engage { bus.addListener(this::commonSetup); } - public void commonSetup(FMLCommonSetupEvent e) { - e.enqueueWork(() -> CoolantBaseBlock.VALID_COOLANT_BLOCKS = Set.of(Blocks.COMPRESSOR_BLOCK.block().get(), Blocks.COPPER_TUBE_BLOCK.block().get(), Blocks.COOLANT_METERING_BLOCK.block().get(), Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get())); - } + public void commonSetup(FMLCommonSetupEvent e) { } } diff --git a/src/main/java/uk/gemwire/engage/block/coolant/CoolantBaseBlock.java b/src/main/java/uk/gemwire/engage/block/coolant/CoolantBaseBlock.java index f0cfbf6..ea6af64 100644 --- a/src/main/java/uk/gemwire/engage/block/coolant/CoolantBaseBlock.java +++ b/src/main/java/uk/gemwire/engage/block/coolant/CoolantBaseBlock.java @@ -1,5 +1,6 @@ package uk.gemwire.engage.block.coolant; +import com.google.common.graph.Network; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.level.Level; @@ -7,6 +8,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import uk.gemwire.engage.registries.Blocks; import java.util.*; @@ -17,8 +19,6 @@ import java.util.*; public class CoolantBaseBlock extends Block { static Logger LOGGER = LogManager.getLogger(); - public static Set VALID_COOLANT_BLOCKS; - public CoolantBaseBlock(Properties p_49795_) { super(p_49795_); } @@ -27,168 +27,5 @@ public class CoolantBaseBlock extends Block { public void onPlace(BlockState prior, Level level, BlockPos pos, BlockState newState, boolean p_60570_) { if (level.isClientSide) return; - long startTime = System.currentTimeMillis(); - - //searchForValidNetwork(new BlockPos.MutableBlockPos(pos.getX(), pos.getY(), pos.getZ()), level); - - long endTime = System.currentTimeMillis(); - LOGGER.info("Discovery took " + (endTime - startTime) + "ms"); } - - private Queue getValidTransitionsFrom(NetworkState state, Level level) { - Queue dirs = new ArrayDeque<>(); - for (Direction dir : Direction.values()) { - if (state.lastTransition != null && dir == state.lastTransition) continue; - BlockPos newPos = state.currentPos.relative(dir); - if (state.visited.contains(newPos)) continue; - if (VALID_COOLANT_BLOCKS.contains(level.getBlockState(newPos).getBlock())) dirs.add(new Transition(dir, newPos, level.getBlockState(newPos))); - } - - return dirs; - } - - - // TODO: Revert the search to a known good state - private boolean searchForValidNetwork(BlockPos.MutableBlockPos pos, Level level) { - NetworkState state = new NetworkState(false, false, false, new HashSet<>(), false, null, pos); - Queue queue = new ArrayDeque<>(); - - LOGGER.info("Beginning network discovery"); - - // Get the valid directions we can go. - var dirs = getValidTransitionsFrom(state, level); - - LOGGER.info("First step: " + dirs.size() + " valid moves."); - // If we aren't connected to at least 2 other valid blocks, it is impossible to have a completed network. - if (dirs.size() < 2) { - return false; - } - - state.visited.add(pos); - - queue.add(new StateTransitions(state.copy(), dirs)); - - // Check the block we're at, first. - if (this == Blocks.COMPRESSOR_BLOCK.block().get()) state.compressor = true; - else if (this == Blocks.COOLANT_METERING_BLOCK.block().get()) { state.metering = true; state.premature = true; } - else if (this == Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get()) state.spreader = true; - - while(!queue.isEmpty()) { - // The search loop - search(level, queue); - - // Try to finalise stuff - we gotta have everything in our network. - // First, check that the last remaining direction takes us into a block we've already visited (the closed loop) - if (state.visited.contains(dirs.iterator().next().pos())) { - // Check for premature mode - if (state.premature) { - if (!(state.compressor && state.spreader)) { - LOGGER.info("Two heat spreaders not found in valid configuration!"); - queue.poll(); - } - } else { - if (!(state.compressor && state.metering && state.spreader)) { - LOGGER.info("Not encountered all components of the coolant system!"); - queue.poll(); - } - } - } - queue.poll(); - } - - LOGGER.info("Valid coolant loop!"); - return true; - } - - void search(Level level, Queue queue) { - StateTransitions currentState = queue.peek(); - while (currentState.transitions.size() > 1) { - // Traverse the new block - Transition transition = currentState.transitions.iterator().next(); - currentState.state.lastTransition = transition.dir(); - currentState.state.currentPos = transition.pos(); - - Block reachedBlock = transition.state().getBlock(); - - LOGGER.info("Traversing through " + currentState.state.lastTransition + " to " + currentState.state.currentPos + ", which is a " + getBlockName(reachedBlock)); - currentState.state.visited.add(currentState.state.currentPos); - - // If we reached a metering block for the first time - if (reachedBlock == Blocks.COOLANT_METERING_BLOCK.block().get()) { - currentState.state.metering(); - // The compressor has its own things to check.. - } else if (reachedBlock == Blocks.COMPRESSOR_BLOCK.block().get()) { - currentState.state.compressor(); - // Heat spreaders may have many, so we just flip the flag to be true if we're just getting to one now. - } else if (reachedBlock == Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get()) { - if (!currentState.state.spreader) currentState.state.spreader = true; - } - - Queue newTransitions = getValidTransitionsFrom(currentState.state, level); - queue.add(new StateTransitions(currentState.state.copy(), newTransitions)); - currentState = queue.peek(); - } - } - - private String getBlockName(Block block) { - return block.getName().getContents().toString(); - } - - record Transition(Direction dir, BlockPos pos, BlockState state) {} - - static class NetworkState { - boolean metering, compressor, spreader, premature; - Set visited; - Direction lastTransition; - BlockPos currentPos; - - public NetworkState(boolean metering, boolean compressor, boolean spreader, HashSet visited, boolean premature, Direction lastTransition, BlockPos pos) { - this.metering = metering; - this.compressor = compressor; - this.spreader = spreader; - this.premature = premature; - this.visited = visited; - this.lastTransition = lastTransition; - this.currentPos = pos; - } - - void metering() { - if (metering) - LOGGER.info("Two metering devices in the same network! Not valid."); - - // And we have already reached a heat spreader: - if (spreader) { - // Tell the system to check for another - spreader = false; - // Set the metering flag true - } else { - // We're searching prematurely! - // We'll search for it at the compressor instead. - premature = true; - } - metering = true; - } - - void compressor() { - if (compressor) - LOGGER.info("Two compressors in the same network! Not valid."); - - // Premature metering means we should check that a heat spreader is between the metering device and the compressor. - if (premature) { - if (!spreader) { - LOGGER.info("Metering Device and Compressor are too close together! Not valid."); - } else { - compressor = true; - } - } else { - compressor = true; - } - } - - NetworkState copy() { - return new NetworkState(metering, compressor, spreader, new HashSet<>(visited), premature, lastTransition, currentPos); - } - } - - record StateTransitions(NetworkState state, Queue transitions) {} } diff --git a/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworkGraph.java b/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworkGraph.java new file mode 100644 index 0000000..2bfbf7d --- /dev/null +++ b/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworkGraph.java @@ -0,0 +1,215 @@ +package uk.gemwire.engage.systems.coolant; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import uk.gemwire.engage.block.coolant.CoolantBaseBlock; +import uk.gemwire.engage.registries.Blocks; + +import java.util.List; +import java.util.Set; + +public class CoolantNetworkGraph { + + public enum ValidBlocks { + COPPER_PIPE("pipe", Blocks.COPPER_TUBE_BLOCK.block().get()), + COMPRESSOR("compressor", Blocks.COMPRESSOR_BLOCK.block().get()), + METERING("metering", Blocks.COMPRESSOR_BLOCK.block().get()), + SPREADER("spreader", Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get()); + + public final String name; + public final CoolantBaseBlock type; + + ValidBlocks(String name, Block block) { + this.name = name; + this.type = (CoolantBaseBlock) block; + } + + public static String getNameFor(Block b) { + for (ValidBlocks v : values()) { + if (v.type == b) return v.name; + } + return "invalid"; + } + + public static Block getBlockFor(String name) { + for (ValidBlocks v : values()) { + if (v.name.equals(name)) return v.type; + } + return null; + } + } + + // A Transition leads to another valid Network block. + public record Transition(BlockPos start, Direction dir, BlockPos dest, BlockState state) {} + + // A Node in the network - a set of BlockPositions representing a Coolant Network Block. + public record NetworkBlockNode(CoolantBaseBlock blockType, Set positions, Set transitions) { + boolean hasTransitionTo(CoolantBaseBlock type) { + for (Transition t : transitions) { + if (t.state.getBlock() == type) return true; + } + return false; + } + } + + /** + * A network consists of at least one Network Block Node (@see CoolantBaseBlock#VALID_COOLANT_BLOCKS) + */ + public static class Graph { + List nodes; + + public Graph(List nodes) { + this.nodes = nodes; + } + + Direction distance(BlockPos bp1, BlockPos bp2) { + for (Direction dir : Direction.values()) { + if (bp1.relative(dir).equals(bp2)) return dir; + } + return null; + } + + @Nullable + NetworkBlockNode getConnectedNodeOfType(NetworkBlockNode start, CoolantBaseBlock type) { + if (!start.hasTransitionTo(type)) return null; + + BlockPos transitionedBlock = null; + for (Transition t : start.transitions) { + if (t.state.getBlock() == type) { + transitionedBlock = t.dest; + } + } + + if (transitionedBlock == null) return null; + + for (NetworkBlockNode node : nodes) { + if (node.positions.contains(transitionedBlock)) return node; + } + + return null; + } + + @Nullable + NetworkBlockNode getConnectedNodeOfTypeExcludingNode(NetworkBlockNode start, CoolantBaseBlock type, NetworkBlockNode blacklist) { + if (!start.hasTransitionTo(type)) return null; + + BlockPos transitionedBlock = null; + for (Transition t : start.transitions) { + if (t.state.getBlock() == type && !blacklist.positions.contains(t.dest)) { + transitionedBlock = t.dest; + } + } + + if (transitionedBlock == null) return null; + + for (NetworkBlockNode node : nodes) { + if (node.positions.contains(transitionedBlock)) return node; + } + + return null; + } + + boolean addBlock(BlockPos position, Level level) { + boolean foundNode = false; + // Get Block Type + if (level.getBlockState(position).getBlock() instanceof CoolantBaseBlock coolant) { + // Find node to add it to + for (NetworkBlockNode node : nodes) { + // Find the neighbors + for (BlockPos pos : node.positions) { + // Cache the direction + Direction dir = distance(pos, position); + if (dir != null) { + // Add the position to the node if of the same type + if (node.blockType == coolant) { + node.positions.add(position); + foundNode = true; + } + // Add the transition to the node, even if it will lead to a new Node. + node.transitions.add(new Transition(pos, dir, position, level.getBlockState(position))); + } + } + } + + if (foundNode) return true; + + // Couldn't add to any existing Node, so create a new one + NetworkBlockNode node = new NetworkBlockNode(coolant, Set.of(position), Set.of()); + nodes.add(node); + + // Find transitions into neighboring nodes + for (Direction dir : Direction.values()) { + BlockState directionBlock = level.getBlockState(position.relative(dir)); + if (directionBlock.getBlock() instanceof CoolantBaseBlock) { + node.transitions.add(new Transition(position, dir, position.relative(dir), level.getBlockState(position))); + } + } + + return true; + } + return false; + } + + boolean tryVerifyNetwork() { + // Find one of the two cables connected to the Compressor. + NetworkBlockNode cableNode = null; + for (NetworkBlockNode node : nodes) { + if (node.blockType == Blocks.COPPER_TUBE_BLOCK.block().get() && node.hasTransitionTo((CoolantBaseBlock) Blocks.COMPRESSOR_BLOCK.block().get())) { + cableNode = node; + break; + } + } + if (cableNode == null) return false; + + if (cableNode.hasTransitionTo((CoolantBaseBlock) Blocks.COOLANT_METERING_BLOCK.block().get())) return false; + + // Start at Copper pipe. + + // Move to Compressor. + + NetworkBlockNode compressorNode = getConnectedNodeOfType(cableNode, (CoolantBaseBlock) Blocks.COMPRESSOR_BLOCK.block().get()); + if (compressorNode == null) return false; + + // Move to Copper pipe. + NetworkBlockNode cable2Node = getConnectedNodeOfTypeExcludingNode(compressorNode, (CoolantBaseBlock) Blocks.COPPER_TUBE_BLOCK.block().get(), cableNode); + if (cable2Node == null) return false; + + // Move to Heat Spreader 1 (evaporator, "before" the metering device) + NetworkBlockNode evaporatorNode = getConnectedNodeOfType(cable2Node, (CoolantBaseBlock) Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get()); + if (evaporatorNode == null) return false; + + // Move to Copper pipe. + cable2Node = getConnectedNodeOfTypeExcludingNode(evaporatorNode, (CoolantBaseBlock) Blocks.COPPER_TUBE_BLOCK.block().get(), cable2Node); + if (cable2Node == null) return false; + + // Move to metering device. + NetworkBlockNode meteringNode = getConnectedNodeOfType(cable2Node, (CoolantBaseBlock) Blocks.COOLANT_METERING_BLOCK.block().get()); + if (meteringNode == null) return false; + + // Move to Copper pipe. + cable2Node = getConnectedNodeOfTypeExcludingNode(meteringNode, (CoolantBaseBlock) Blocks.COPPER_TUBE_BLOCK.block().get(), cable2Node); + if (cable2Node == null) return false; + + // Move to Heat Spreader 2 (condenser, "after" the metering device) + NetworkBlockNode condenserNode = getConnectedNodeOfType(cable2Node, (CoolantBaseBlock) Blocks.COOLANT_HEAT_SPREADER_BLOCK.block().get()); + if (condenserNode == null) return false; + + // Move to Copper pipe. + cable2Node = getConnectedNodeOfTypeExcludingNode(meteringNode, (CoolantBaseBlock) Blocks.COPPER_TUBE_BLOCK.block().get(), cable2Node); + if (cable2Node == null) return false; + + return cableNode == cable2Node; + } + + boolean containsBlock(BlockPos position) { + for (NetworkBlockNode node : nodes) { + if (node.positions.contains(position)) return true; + } + return false; + } + } +} diff --git a/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworksSavedData.java b/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworksSavedData.java new file mode 100644 index 0000000..a6527e0 --- /dev/null +++ b/src/main/java/uk/gemwire/engage/systems/coolant/CoolantNetworksSavedData.java @@ -0,0 +1,94 @@ +package uk.gemwire.engage.systems.coolant; + + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraftforge.server.ServerLifecycleHooks; +import org.jetbrains.annotations.NotNull; +import uk.gemwire.engage.block.coolant.CoolantBaseBlock; + +import java.util.*; + +public class CoolantNetworksSavedData extends SavedData { + + // The list of all Coolant Networks currently loaded + public List networks; + + public CoolantNetworksSavedData(List networks) { this.networks = networks; } + public CoolantNetworksSavedData() { networks = new ArrayList<>(); } + + @Override + public @NotNull CompoundTag save(final @NotNull CompoundTag pCompoundTag) { + + ListTag net = new ListTag(); + for (var acc : networks) { + ListTag nodes = new ListTag(); + for (var node : acc.nodes) { + CompoundTag n = new CompoundTag(); + n.putString("type", CoolantNetworkGraph.ValidBlocks.getNameFor(node.blockType())); + ListTag pos = new ListTag(); + for (var p : node.positions()) { + pos.add(NbtUtils.writeBlockPos(p)); + } + n.put("positions", pos); + ListTag transitions = new ListTag(); + for (var t : node.transitions()) { + CompoundTag transition = new CompoundTag(); + transition.put("start", NbtUtils.writeBlockPos(t.start())); + transition.put("dest", NbtUtils.writeBlockPos(t.start())); + transition.put("dir", IntTag.valueOf(t.dir().get3DDataValue())); + transition.put("state", NbtUtils.writeBlockState(t.state())); + transitions.add(transition); + } + n.put("transitions", transitions); + nodes.add(n); + } + net.add(nodes); + } + + pCompoundTag.put("networks", net); + return pCompoundTag; + } + + public static CoolantNetworksSavedData load(CompoundTag tag) { + ListTag nets = tag.getList("networks", ListTag.TAG_COMPOUND); + + List graphs = new ArrayList<>(); + nets.iterator().forEachRemaining(network -> { + List nodes = new ArrayList<>(); + ((ListTag) network).iterator().forEachRemaining(node -> { + Block type = CoolantNetworkGraph.ValidBlocks.getBlockFor(((CompoundTag) node).getString("type")); + Set positions = new HashSet<>(); + ListTag poses = ((CompoundTag) node).getList("positions", ListTag.TAG_COMPOUND); + poses.iterator().forEachRemaining(pos -> positions.add(NbtUtils.readBlockPos((CompoundTag) pos))); + Set transitions = new HashSet<>(); + ListTag transes = ((CompoundTag) node).getList("transitions", ListTag.TAG_COMPOUND); + transes.iterator().forEachRemaining(trans -> + transitions.add( + new CoolantNetworkGraph.Transition( + NbtUtils.readBlockPos(((CompoundTag) trans).getCompound("start")), + Direction.from3DDataValue(((CompoundTag) trans).getInt("dir")), + NbtUtils.readBlockPos(((CompoundTag) trans).getCompound("dest")), + NbtUtils.readBlockState(ServerLifecycleHooks.getCurrentServer().overworld().holderLookup(Registries.BLOCK), ((CompoundTag) trans).getCompound("state")) + ) + ) + ); + nodes.add(new CoolantNetworkGraph.NetworkBlockNode((CoolantBaseBlock) type, positions, transitions)); + }); + + graphs.add(new CoolantNetworkGraph.Graph(nodes)); + }); + + return new CoolantNetworksSavedData(graphs); + } + + + public static CoolantNetworksSavedData getOrCreate(ServerLevel level) { + return level.getDataStorage().computeIfAbsent(CoolantNetworksSavedData::load, CoolantNetworksSavedData::new, "graphs"); + } +}