Skip to content
Draft
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
179 changes: 179 additions & 0 deletions src/main/java/dev/murad/shipping/block/rail/PortalRail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package dev.murad.shipping.block.rail;

import com.mojang.serialization.MapCodec;
import dev.murad.shipping.util.RailHelper;
import dev.murad.shipping.util.RailShapeUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.entity.vehicle.AbstractMinecart;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.BaseRailBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.block.state.properties.RailShape;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.portal.PortalForcer;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jetbrains.annotations.Nullable;

import java.util.Optional;

public class PortalRail extends BaseRailBlock {

public static final MapCodec<PortalRail> CODEC = simpleCodec(PortalRail::new);
public static final EnumProperty<RailShape> RAIL_SHAPE = RailShapeUtil.RAIL_SHAPE_STRAIGHT_FLAT;
// Direction toward the adjacent nether portal; determines the model variant and is read by destroyLinkedRail to locate the paired rail in the other dimension.
public static final DirectionProperty PORTAL_FACING = BlockStateProperties.HORIZONTAL_FACING;

@Override
protected MapCodec<? extends BaseRailBlock> codec() {
return CODEC;
}

public PortalRail(Properties properties) {
super(true, properties);
registerDefaultState(stateDefinition.any()
.setValue(RAIL_SHAPE, RailShape.NORTH_SOUTH)
.setValue(PORTAL_FACING, Direction.NORTH));
}

@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction facing = context.getHorizontalDirection();
RailShape shape = (facing == Direction.EAST || facing == Direction.WEST)
? RailShape.EAST_WEST : RailShape.NORTH_SOUTH;
return defaultBlockState().setValue(RAIL_SHAPE, shape).setValue(PORTAL_FACING, facing);
}

@Override
protected BlockState updateState(BlockState state, Level level, BlockPos pos, boolean moving) {
return state;
}

@Override
protected BlockState updateShape(BlockState state, Direction dir, BlockState neighborState,
LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
if (neighborState.getFluidState().is(FluidTags.WATER)) {
return Blocks.AIR.defaultBlockState();
}
return state;
}

@Override
public void neighborChanged(BlockState state, Level level, BlockPos pos,
Block neighborBlock, BlockPos fromPos, boolean isMoving) {
super.neighborChanged(state, level, pos, neighborBlock, fromPos, isMoving);
if (level.isClientSide()) return;
if (!level.getBlockState(pos.relative(state.getValue(PORTAL_FACING))).is(Blocks.NETHER_PORTAL)) {
level.destroyBlock(pos, false);
}
}

// Returns empty so creative pick-block doesn't hand the player a PortalRail; it's placed automatically, not by hand.
@Override
public ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state) {
return ItemStack.EMPTY;
}

// Destroys the paired PortalRail in the other dimension so both sides always disappear together.
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
super.onRemove(state, level, pos, newState, isMoving);
if (newState.is(this) || level.isClientSide() || !(level instanceof ServerLevel serverLevel)) return;

destroyLinkedRail(serverLevel, state, pos);
}

private static void destroyLinkedRail(ServerLevel level, BlockState state, BlockPos pos) {
Direction portalFacing = state.getValue(PORTAL_FACING);
if (!level.getBlockState(pos.relative(portalFacing)).is(Blocks.NETHER_PORTAL)) return;

ResourceKey<Level> thisDim = level.dimension();
ResourceKey<Level> otherDim = thisDim == Level.NETHER ? Level.OVERWORLD : Level.NETHER;
ServerLevel otherLevel = level.getServer().getLevel(otherDim);
if (otherLevel == null) return;

double scale = thisDim == Level.NETHER ? 8.0 : 0.125;
BlockPos scaledPos = BlockPos.containing(pos.getX() * scale, pos.getY(), pos.getZ() * scale);

Optional<BlockPos> exitPortal = otherLevel.getPortalForcer()
.findClosestPortalPosition(scaledPos, otherDim == Level.NETHER, otherLevel.getWorldBorder());

exitPortal.ifPresent(exitPos -> {
BlockPos floor = exitPos;
while (otherLevel.getBlockState(floor.below()).is(Blocks.NETHER_PORTAL)) {
floor = floor.below();
}
for (Direction dir : Direction.Plane.HORIZONTAL) {
BlockPos candidate = floor.relative(dir);
if (otherLevel.getBlockState(candidate).getBlock() instanceof PortalRail) {
otherLevel.destroyBlock(candidate, false);
break;
}
}
});
}

@Override
protected FluidState getFluidState(BlockState state) {
return Fluids.EMPTY.defaultFluidState();
}

@Deprecated
@Override
public Property<RailShape> getShapeProperty() {
return RAIL_SHAPE;
}

@Override
public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return FLAT_AABB;
}

@Override
public boolean canMakeSlopes(BlockState state, BlockGetter world, BlockPos pos) {
return false;
}

@Override
public RailShape getRailDirection(BlockState state, BlockGetter world, BlockPos pos,
@Nullable AbstractMinecart cart) {
if (cart == null) return state.getValue(RAIL_SHAPE);
return RailHelper.directionFromVelocity(cart.getDeltaMovement()).getAxis() == Direction.Axis.X
? RailShape.EAST_WEST : RailShape.NORTH_SOUTH;
}

@Override
public BlockState rotate(BlockState state, Rotation rot) {
return state;
}

@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return state;
}

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(RAIL_SHAPE, PORTAL_FACING);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ protected void addTags(HolderLookup.@NotNull Provider lookupProvider) {
tag(BlockTags.RAILS).add(ModBlocks.TEE_JUNCTION_RAIL.get());
tag(BlockTags.RAILS).add(ModBlocks.AUTOMATIC_TEE_JUNCTION_RAIL.get());
tag(BlockTags.RAILS).add(ModBlocks.JUNCTION_RAIL.get());
tag(BlockTags.RAILS).add(ModBlocks.PORTAL_RAIL.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ protected void generate() {
dropSelf(ModBlocks.AUTOMATIC_TEE_JUNCTION_RAIL.get());
dropSelf(ModBlocks.JUNCTION_RAIL.get());
dropSelf(ModBlocks.DOCKING_STATION.get());
add(ModBlocks.PORTAL_RAIL.get(), LootTable.lootTable());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.murad.shipping.block.dockingstation.DockingStationBlock;
import dev.murad.shipping.block.dockingstation.DockingStationPart;
import dev.murad.shipping.block.guiderail.CornerGuideRailBlock;
import dev.murad.shipping.block.rail.PortalRail;
import dev.murad.shipping.block.rail.SwitchRail;
import dev.murad.shipping.block.vesseldetector.VesselDetectorBlock;
import dev.murad.shipping.setup.ModBlocks;
Expand Down Expand Up @@ -131,6 +132,21 @@ protected void registerStatesAndModels() {
.texture("rail", getBlTx("junction_rail")))
.build());

ModelFile portalRailModel = models().getExistingFile(modLoc("block/portal_rail"));
getVariantBuilder(ModBlocks.PORTAL_RAIL.get()).forAllStates(state -> {
Direction facing = state.getValue(PortalRail.PORTAL_FACING);
int yRot = switch (facing) {
case NORTH -> 0;
case EAST -> 90;
case SOUTH -> 180;
default -> 270; // WEST
};
return ConfiguredModel.builder()
.modelFile(portalRailModel)
.rotationY(yRot)
.build();
});

// --- Docking Station ---
// Models are hand-crafted in src/main/resources; just reference them.
ModelFile portModel = models().getExistingFile(modLoc("block/docking_station_port"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.Level;
import dev.murad.shipping.block.rail.PortalRail;
import net.minecraft.world.level.block.BaseRailBlock;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.RailShape;
import net.minecraft.world.phys.AABB;
Expand Down Expand Up @@ -80,6 +82,16 @@ public void setFrozen(boolean frozen) {
this.frozen = frozen;
}

public void clearFollowerWait() {
linkingHandler.clearWaitForDominated();
}

/* Non-null while this wagon must keep moving toward a portal. Applied every tick, overriding
chain math and friction. Cleared when the entity crosses; the new instance starts null. */
@Getter @Setter
private Vec3 forcedPortalVelocity = null;
private int forcedPortalVelocityTicks = 0;

private static final Map<RailShape, Pair<Vec3i, Vec3i>> EXITS = Util.make(Maps.newEnumMap(RailShape.class), (enumMap) -> {
Vec3i west = Direction.WEST.getNormal();
Vec3i east = Direction.EAST.getNormal();
Expand Down Expand Up @@ -132,9 +144,20 @@ private void resetAttributes() {
protected Optional<RailShape> getRailShape() {
for (var pos : Arrays.asList(getOnPos().above(), getOnPos())) {
var state = level().getBlockState(pos);
if (state.getBlock() instanceof BaseRailBlock railBlock) {
if (state.getBlock() instanceof BaseRailBlock) {
return Optional.of(railHelper.getShape(pos));
}
/* No rail exists inside the portal block. The adjacent PortalRail defines the axis;
returning its shape prevents accelerate() from stalling and stops the ascending-slope
check from resetting position every tick. */
if (state.is(Blocks.NETHER_PORTAL)) {
for (Direction dir : Direction.Plane.HORIZONTAL) {
BlockPos adj = pos.relative(dir);
if (level().getBlockState(adj).getBlock() instanceof PortalRail) {
return Optional.of(railHelper.getShape(adj));
}
}
}
}
return Optional.empty();
}
Expand Down Expand Up @@ -233,9 +256,33 @@ public void tick() {
this.setYRot(yrot);
if (!level().isClientSide) {
doChainMath();
applyForcedPortalVelocity();
}
}

/* Refreshes the forced velocity and resets the timeout. Called every tick by the
locomotive while it's alive in the other dimension. */
public void updateForcedPortalVelocity(Vec3 velocity) {
this.forcedPortalVelocity = velocity;
this.forcedPortalVelocityTicks = 0;
}

private void applyForcedPortalVelocity() {
if (forcedPortalVelocity == null) return;
setDeltaMovement(forcedPortalVelocity);
// Self-clear after 600 ticks if the loco disappears without calling releasePortalWagons (e.g. server crash).
if (++forcedPortalVelocityTicks > 600) {
forcedPortalVelocity = null;
forcedPortalVelocityTicks = 0;
}
}

@Override
public int getDimensionChangingDelay() {
// 40 ticks (2 s) prevents an immediate return trip; without this the entity spawns inside the portal block and re-enters on the next tick.
return 40;
}

@Override
public float getMaxCartSpeedOnRail() {
return (float) (ShippingConfig.Server.TRAIN_MAX_SPEED.get() * 1f);
Expand All @@ -256,6 +303,7 @@ protected void enforceMaxVelocity(double maxSpeed) {
@Override
public void push(Entity pEntity) {
if (!this.level().isClientSide) {
if (forcedPortalVelocity != null) return;
// not perfect, doesn't work when a mob stand in the way without moving, but works well enough underwater to keep this
if (pEntity instanceof LivingEntity l && l.getVehicle() == null){
if (this instanceof StallingCapability s) {
Expand Down Expand Up @@ -488,8 +536,7 @@ protected void tickVanilla(){

@Override
public void remove(RemovalReason r) {
// Only sever chain links on permanent removal. Chunk unloads are temporary —
// the head entity's consist list will reconnect when the chunk reloads.
// Only sever links on permanent removal; UNLOADED_TO_CHUNK is temporary and tickReconnect will relink when the chunk reloads.
if (r != RemovalReason.UNLOADED_TO_CHUNK) {
handleLinkableKill();
}
Expand Down
Loading