completely rework coolant logic
This commit is contained in:
parent
8089051c00
commit
f942596b7f
|
@ -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) { }
|
||||
}
|
||||
|
|
|
@ -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<Block> 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<Transition> getValidTransitionsFrom(NetworkState state, Level level) {
|
||||
Queue<Transition> 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<StateTransitions> 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<StateTransitions> 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<Transition> 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<BlockPos> visited;
|
||||
Direction lastTransition;
|
||||
BlockPos currentPos;
|
||||
|
||||
public NetworkState(boolean metering, boolean compressor, boolean spreader, HashSet<BlockPos> 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<Transition> transitions) {}
|
||||
}
|
||||
|
|
|
@ -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<BlockPos> positions, Set<Transition> 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<NetworkBlockNode> nodes;
|
||||
|
||||
public Graph(List<NetworkBlockNode> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<CoolantNetworkGraph.Graph> networks;
|
||||
|
||||
public CoolantNetworksSavedData(List<CoolantNetworkGraph.Graph> 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<CoolantNetworkGraph.Graph> graphs = new ArrayList<>();
|
||||
nets.iterator().forEachRemaining(network -> {
|
||||
List<CoolantNetworkGraph.NetworkBlockNode> nodes = new ArrayList<>();
|
||||
((ListTag) network).iterator().forEachRemaining(node -> {
|
||||
Block type = CoolantNetworkGraph.ValidBlocks.getBlockFor(((CompoundTag) node).getString("type"));
|
||||
Set<BlockPos> positions = new HashSet<>();
|
||||
ListTag poses = ((CompoundTag) node).getList("positions", ListTag.TAG_COMPOUND);
|
||||
poses.iterator().forEachRemaining(pos -> positions.add(NbtUtils.readBlockPos((CompoundTag) pos)));
|
||||
Set<CoolantNetworkGraph.Transition> 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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user