Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
*.iws
out/

# VS Code
.vscode/

# Kotlin
.kotlin/

Expand Down Expand Up @@ -44,3 +47,7 @@ Thumbs.db
# Claude Code
.claude/
CLAUDE.md

# Local temp / decompile scratch
tmp/
tmp*
4 changes: 4 additions & 0 deletions src/main/java/de/devin/pipesnphysics/PipesNPhysics.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.simibubi.create.foundation.item.ItemDescription;
import com.simibubi.create.foundation.item.KineticStats;
import com.simibubi.create.foundation.item.TooltipModifier;
import de.devin.pipesnphysics.compat.CreateFluidCompat;
import de.devin.pipesnphysics.compat.SableCompat;
import de.devin.pipesnphysics.engine.EngineTickHandler;
import de.devin.pipesnphysics.engine.OpenEndPipes;
Expand Down Expand Up @@ -68,6 +69,9 @@ public static ResourceLocation asResource(String path) {
}

private void onCommonSetup(FMLCommonSetupEvent event) {
if (CreateFluidCompat.isLoaded()) {
LOGGER.info("Create: Fluid compatibility enabled");
}
LOGGER.info("Common setup...");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.mojang.math.Axis;
import com.simibubi.create.content.equipment.goggles.GogglesItem;
import com.simibubi.create.content.fluids.pump.PumpBlock;
import de.devin.pipesnphysics.compat.CreateFluidCompat;
import de.devin.pipesnphysics.PipesNPhysics;
import de.devin.pipesnphysics.PipesNPhysicsConfig;
import de.devin.pipesnphysics.engine.net.PumpRangePayload;
Expand All @@ -17,6 +18,7 @@
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
Expand Down Expand Up @@ -63,7 +65,7 @@ public static void onRenderLevel(RenderLevelStageEvent event) {
long now = mc.level.getGameTime();
if (mc.hitResult instanceof BlockHitResult blockHit
&& mc.hitResult.getType() == HitResult.Type.BLOCK
&& mc.level.getBlockState(blockHit.getBlockPos()).getBlock() instanceof PumpBlock) {
&& isPumpBlock(mc.level.getBlockState(blockHit.getBlockPos()))) {
PumpRangeClient.looking(blockHit.getBlockPos(), now);
}

Expand Down Expand Up @@ -144,6 +146,10 @@ private static void emitArrow(PoseStack poseStack, Minecraft mc, VertexConsumer
poseStack.popPose();
}

private static boolean isPumpBlock(BlockState state) {
return state.getBlock() instanceof PumpBlock || CreateFluidCompat.isCentrifugalPump(state);
}

private static void applyDirectionRotation(PoseStack poseStack, Direction dir) {
switch (dir) {
case NORTH -> poseStack.mulPose(Axis.YP.rotationDegrees(180));
Expand Down
119 changes: 119 additions & 0 deletions src/main/java/de/devin/pipesnphysics/compat/CreateFluidCompat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package de.devin.pipesnphysics.compat;

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;

/**
* Optional compatibility with Create: Fluid's centrifugal pump ({@code fluid:centrifugal_pump}).
* Uses reflection so the mod is not a hard dependency.
*/
public final class CreateFluidCompat {
public static final ResourceLocation CENTRIFUGAL_PUMP_ID =
ResourceLocation.fromNamespaceAndPath("fluid", "centrifugal_pump");

/** Create: Fluid applies 2× pressure vs Create's mechanical pump at the same RPM. */
public static final double PERFORMANCE_MULTIPLIER = 2.0;

private static final Provider PROVIDER;

static {
Provider provider;
try {
Class.forName("com.adonis.fluid.block.CentrifugalPump.CentrifugalPumpBlock", false,
CreateFluidCompat.class.getClassLoader());
provider = new ReflectiveProvider();
} catch (ReflectiveOperationException | LinkageError e) {
provider = NoOpProvider.INSTANCE;
}
PROVIDER = provider;
}

private CreateFluidCompat() {}

public static boolean isLoaded() {
return PROVIDER instanceof ReflectiveProvider;
}

public static boolean isCentrifugalPump(BlockState state) {
return PROVIDER.isCentrifugalPump(state);
}

public static boolean isCentrifugalPump(Level level, BlockPos pos) {
return PROVIDER.isCentrifugalPump(level.getBlockState(pos));
}

/** Push and pull ports; null when the block is not a centrifugal pump or ports cannot resolve. */
public static PumpPorts getPumpPorts(Level level, BlockPos pos, BlockState state) {
return PROVIDER.getPumpPorts(level, pos, state);
}

public static Direction getPushSide(Level level, BlockPos pos, BlockState state) {
PumpPorts ports = getPumpPorts(level, pos, state);
return ports != null ? ports.push() : null;
}

public record PumpPorts(Direction push, Direction pull) {}

private interface Provider {
boolean isCentrifugalPump(BlockState state);

PumpPorts getPumpPorts(Level level, BlockPos pos, BlockState state);
}

private static final class NoOpProvider implements Provider {
static final NoOpProvider INSTANCE = new NoOpProvider();

@Override
public boolean isCentrifugalPump(BlockState state) {
return false;
}

@Override
public PumpPorts getPumpPorts(Level level, BlockPos pos, BlockState state) {
return null;
}
}

private static final class ReflectiveProvider implements Provider {
private final Class<?> blockClass;
private final Class<?> beClass;

ReflectiveProvider() throws ReflectiveOperationException {
blockClass = Class.forName("com.adonis.fluid.block.CentrifugalPump.CentrifugalPumpBlock");
beClass = Class.forName("com.adonis.fluid.block.CentrifugalPump.CentrifugalPumpBlockEntity");
}

@Override
public boolean isCentrifugalPump(BlockState state) {
return blockClass.isInstance(state.getBlock());
}

@Override
public PumpPorts getPumpPorts(Level level, BlockPos pos, BlockState state) {
if (!isCentrifugalPump(state)) return null;
try {
Direction primary = (Direction) blockClass
.getMethod("getPrimaryFluidDirection", BlockState.class)
.invoke(null, state);
Direction secondary = (Direction) blockClass
.getMethod("getSecondaryFluidDirection", BlockState.class)
.invoke(null, state);
if (primary == null || secondary == null) return null;

var be = level.getBlockEntity(pos);
if (!beClass.isInstance(be)) return null;
boolean pullPrimary = (boolean) beClass
.getMethod("isPullingOnSide", boolean.class)
.invoke(be, true);
Direction push = pullPrimary ? secondary : primary;
Direction pull = pullPrimary ? primary : secondary;
return new PumpPorts(push, pull);
} catch (ReflectiveOperationException e) {
return null;
}
}
}
}
7 changes: 5 additions & 2 deletions src/main/java/de/devin/pipesnphysics/engine/FlowSolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.simibubi.create.content.fluids.FluidPropagator;
import com.simibubi.create.content.kinetics.base.KineticBlockEntity;
import de.devin.pipesnphysics.PipesNPhysicsConfig;
import de.devin.pipesnphysics.compat.CreateFluidCompat;
import de.devin.pipesnphysics.compat.SableCompat;
import de.devin.pipesnphysics.engine.solve.Apportion;
import de.devin.pipesnphysics.engine.solve.NetworkSolver;
Expand Down Expand Up @@ -255,7 +256,9 @@ private static Map<Integer, PumpState> collectPumps(Level level, Graph graph) {
for (Node pump : graph.pumps()) {
float speed = level.getBlockEntity(pump.pos()) instanceof KineticBlockEntity kinetic
? kinetic.getSpeed() : 0;
double head = Math.abs(speed) * headPerRpm;
double mult = CreateFluidCompat.isCentrifugalPump(level, pump.pos())
? CreateFluidCompat.PERFORMANCE_MULTIPLIER : 1.0;
double head = Math.abs(speed) * headPerRpm * mult;
pumps.put(pump.index(), new PumpState(isPumpRunning(level, pump), head, pump.pumpFacing(),
flowPerRpm / headPerRpm));
}
Expand Down Expand Up @@ -693,7 +696,7 @@ private static void assembleBranch(Level level, Graph graph, Columns columns,
driveNode = driveNode < 0 ? nodeIndex : -2;
driveHead = pump.head();
driveInternalG = pump.internalConductance();
} else if (toward.equals(pumpNode.pos().relative(pump.pushSide().getOpposite()))) {
} else if (toward.equals(pumpNode.pos().relative(pumpNode.effectivePullSide()))) {
allowedSign = combineSign(allowedSign, -outSign);
} else {
blockedEdges.add(edge.index());
Expand Down
95 changes: 89 additions & 6 deletions src/main/java/de/devin/pipesnphysics/engine/GraphBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import com.simibubi.create.content.fluids.pipes.VanillaFluidTargets;
import com.simibubi.create.content.fluids.pump.PumpBlock;
import com.simibubi.create.content.fluids.tank.FluidTankBlockEntity;
import de.devin.pipesnphysics.mixin.FluidTankAccessor;
import de.devin.pipesnphysics.compat.CreateFluidCompat;
import de.devin.pipesnphysics.compat.SableCompat;
import de.devin.pipesnphysics.mixin.FluidTankAccessor;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
Expand Down Expand Up @@ -104,6 +105,12 @@ public static Graph build(Level level, BlockPos startPos) {
BlockState bs = level.getBlockState(pos);
if (bs.getBlock() instanceof PumpBlock) {
facing = bs.getValue(PumpBlock.FACING);
} else if (CreateFluidCompat.isCentrifugalPump(bs)) {
CreateFluidCompat.PumpPorts ports = CreateFluidCompat.getPumpPorts(level, pos, bs);
if (ports != null) {
facing = ports.push();
accessFace = ports.pull();
}
}
} else if (d.handlers.contains(pos)) {
kind = Node.Kind.HANDLER;
Expand Down Expand Up @@ -177,7 +184,9 @@ public static BlockPos findSeed(Level level, BlockPos startPos) {

private static boolean isPipeLike(Level level, BlockPos pos) {
if (!level.isLoaded(pos)) return false;
return FluidPropagator.getPipe(level, pos) != null || isConduit(level, pos);
BlockState state = level.getBlockState(pos);
return FluidPropagator.getPipe(level, pos) != null || isConduit(level, pos)
|| isPumpBlock(state);
}

/** BFS state. */
Expand All @@ -203,12 +212,14 @@ private static Discovery discover(Level level, BlockPos start) {

FluidTransportBehaviour pipe = FluidPropagator.getPipe(level, cur);
if (pipe == null) {
if (isConduit(level, cur)) discoverConduit(level, cur, d, frontier);
BlockState curState = level.getBlockState(cur);
if (isPumpBlock(curState)) discoverPump(level, cur, d, frontier);
else if (isConduit(level, cur)) discoverConduit(level, cur, d, frontier);
continue;
}

BlockState curState = level.getBlockState(cur);
boolean isPump = curState.getBlock() instanceof PumpBlock;
boolean isPump = isPumpBlock(curState);
if (isPump) d.pumps.add(cur);
else d.pipes.add(cur);

Expand All @@ -218,7 +229,8 @@ private static Discovery discover(Level level, BlockPos start) {
if (!level.isLoaded(neighbor)) continue;
BlockState nState = level.getBlockState(neighbor);

if (nState.getBlock() instanceof PumpBlock) {
if (isPumpBlock(nState)) {
if (!isPumpPortFace(level, neighbor, nState, face.getOpposite())) continue;
d.pumps.add(neighbor.immutable());
conns.add(neighbor.immutable());
frontier.add(neighbor.immutable());
Expand Down Expand Up @@ -364,7 +376,9 @@ private static void discoverConduit(Level level, BlockPos cur, Discovery d, Queu
BlockPos neighbor = cur.relative(face);
if (!level.isLoaded(neighbor)) continue;

if (level.getBlockState(neighbor).getBlock() instanceof PumpBlock) {
BlockState nState = level.getBlockState(neighbor);
if (isPumpBlock(nState)) {
if (!isPumpPortFace(level, neighbor, nState, face.getOpposite())) continue;
d.pumps.add(neighbor.immutable());
conns.add(neighbor.immutable());
frontier.add(neighbor.immutable());
Expand All @@ -385,6 +399,75 @@ private static void discoverConduit(Level level, BlockPos cur, Discovery d, Queu
d.connections.put(cur, conns);
}

/**
* Explore a pump that has no Create {@link FluidTransportBehaviour} (e.g. Create: Fluid's
* centrifugal pump). Without this, the pump can be queued from an adjacent pipe but is
* dropped on visit before its other port is scanned.
*/
private static void discoverPump(Level level, BlockPos cur, Discovery d, Queue<BlockPos> frontier) {
d.pumps.add(cur);
BlockState curState = level.getBlockState(cur);
List<BlockPos> conns = new ArrayList<>();
for (Direction face : pumpPortFaces(level, cur, curState)) {
BlockPos neighbor = cur.relative(face);
if (!level.isLoaded(neighbor)) continue;
BlockState nState = level.getBlockState(neighbor);

if (isPumpBlock(nState)) {
if (!isPumpPortFace(level, neighbor, nState, face.getOpposite())) continue;
d.pumps.add(neighbor.immutable());
conns.add(neighbor.immutable());
frontier.add(neighbor.immutable());
continue;
}

var handler = level.getCapability(
Capabilities.FluidHandler.BLOCK, neighbor, face.getOpposite());
if (handler != null && !VanillaFluidTargets.canProvideFluidWithoutCapability(nState)
&& !HandlerRoles.isIgnored(level, neighbor)) {
d.handlers.add(neighbor.immutable());
conns.add(neighbor.immutable());
if (isConduit(level, neighbor)) frontier.add(neighbor.immutable());
continue;
}

var neighborPipe = FluidPropagator.getPipe(level, neighbor);
if (neighborPipe != null) {
if (neighborPipe.canHaveFlowToward(nState, face.getOpposite())) {
conns.add(neighbor.immutable());
frontier.add(neighbor.immutable());
}
continue;
}

if (FluidPropagator.isOpenEnd(level, cur, face)) {
d.openEnds.putIfAbsent(neighbor.immutable(), face.getOpposite());
conns.add(neighbor.immutable());
}
}
d.connections.put(cur, conns);
}

private static boolean isPumpBlock(BlockState state) {
return state.getBlock() instanceof PumpBlock || CreateFluidCompat.isCentrifugalPump(state);
}

private static List<Direction> pumpPortFaces(Level level, BlockPos pos, BlockState state) {
if (state.getBlock() instanceof PumpBlock) {
Direction facing = state.getValue(PumpBlock.FACING);
return List.of(facing, facing.getOpposite());
}
if (CreateFluidCompat.isCentrifugalPump(state)) {
CreateFluidCompat.PumpPorts ports = CreateFluidCompat.getPumpPorts(level, pos, state);
if (ports != null) return List.of(ports.push(), ports.pull());
}
return List.of();
}

private static boolean isPumpPortFace(Level level, BlockPos pos, BlockState state, Direction face) {
return pumpPortFaces(level, pos, state).contains(face);
}

/**
* Add an edge unless this exact run was already added from its other end. The
* key includes the run's terminal pipe cells so PARALLEL runs between the same
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/de/devin/pipesnphysics/engine/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
*
* {@code accessFace} is the face a HANDLER is reached through when it is SIDE-SPECIFIC (exposes no
* side-agnostic {@code null} capability): the network resolves and transfers the handler through that
* exact face, so a block with a different tank per side serves each side its own fluid. It is null for
* an ordinary side-agnostic handler (resolved through {@code null}) and for every non-handler node.
* exact face, so a block with a different tank per side serves each side its own fluid. For pumps whose
* suction port is not opposite push (e.g. Create: Fluid's centrifugal pump), it stores the pull face.
* It is null for an ordinary side-agnostic handler and for pumps with inline push/pull.
*/
public record Node(int index, BlockPos pos, Kind kind, double worldY,
Direction pumpFacing, Direction openFace, Direction accessFace) {
Expand All @@ -35,4 +36,11 @@ public enum Kind { HANDLER, PUMP, JUNCTION, OPEN_END, CLOSED_GATE }
public boolean isJunction() { return kind == Kind.JUNCTION; }
public boolean isOpenEnd() { return kind == Kind.OPEN_END; }
public boolean isClosedGate() { return kind == Kind.CLOSED_GATE; }

/** Suction port: explicit for orthogonal pumps, otherwise opposite of {@link #pumpFacing}. */
public Direction effectivePullSide() {
if (!isPump() || pumpFacing == null) return null;
if (accessFace != null) return accessFace;
return pumpFacing.getOpposite();
}
}
Loading
Loading