package com.simibubi.create.content.logistics.trains.entity; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import javax.annotation.Nullable; import com.simibubi.create.content.logistics.trains.track.AbstractBogeyTileEntity; import net.minecraft.world.level.block.entity.BlockEntity; import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.lang3.mutable.MutableObject; import com.simibubi.create.AllMovementBehaviours; import com.simibubi.create.Create; import com.simibubi.create.content.contraptions.components.structureMovement.MovementBehaviour; import com.simibubi.create.content.logistics.item.filter.FilterItem; import com.simibubi.create.content.logistics.trains.DimensionPalette; import com.simibubi.create.content.logistics.trains.GraphLocation; import com.simibubi.create.content.logistics.trains.TrackEdge; import com.simibubi.create.content.logistics.trains.TrackGraph; import com.simibubi.create.content.logistics.trains.TrackNode; import com.simibubi.create.content.logistics.trains.entity.Carriage.DimensionalCarriageEntity; import com.simibubi.create.content.logistics.trains.entity.TravellingPoint.IEdgePointListener; import com.simibubi.create.content.logistics.trains.entity.TravellingPoint.SteerDirection; import com.simibubi.create.content.logistics.trains.management.edgePoint.EdgeData; import com.simibubi.create.content.logistics.trains.management.edgePoint.EdgePointType; import com.simibubi.create.content.logistics.trains.management.edgePoint.observer.TrackObserver; import com.simibubi.create.content.logistics.trains.management.edgePoint.signal.SignalBlock.SignalType; import com.simibubi.create.content.logistics.trains.management.edgePoint.signal.SignalBoundary; import com.simibubi.create.content.logistics.trains.management.edgePoint.signal.SignalEdgeGroup; import com.simibubi.create.content.logistics.trains.management.edgePoint.station.GlobalStation; import com.simibubi.create.content.logistics.trains.management.edgePoint.station.StationTileEntity; import com.simibubi.create.content.logistics.trains.management.schedule.ScheduleRuntime; import com.simibubi.create.content.logistics.trains.management.schedule.ScheduleRuntime.State; import com.simibubi.create.foundation.advancement.AllAdvancements; import com.simibubi.create.foundation.config.AllConfigs; import com.simibubi.create.foundation.networking.AllPackets; import com.simibubi.create.foundation.utility.Couple; import com.simibubi.create.foundation.utility.Iterate; import com.simibubi.create.foundation.utility.Lang; import com.simibubi.create.foundation.utility.NBTHelper; import com.simibubi.create.foundation.utility.Pair; import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Direction.Axis; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Explosion.BlockInteraction; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; import net.minecraftforge.items.IItemHandlerModifiable; import net.minecraftforge.items.ItemHandlerHelper; import net.minecraftforge.network.PacketDistributor; public class Train { public double speed = 0; public double targetSpeed = 0; public Double speedBeforeStall = null; public int carriageWaitingForChunks = -1; public double throttle = 1; public boolean honk = false; public UUID id; public UUID owner; public TrackGraph graph; public Navigation navigation; public ScheduleRuntime runtime; public TrainIconType icon; public Component name; public TrainStatus status; public boolean invalid; public SteerDirection manualSteer; public boolean manualTick; public UUID currentStation; public boolean currentlyBackwards; public boolean doubleEnded; public List carriages; public List carriageSpacing; public boolean updateSignalBlocks; public Map occupiedSignalBlocks; public Set reservedSignalBlocks; public Set occupiedObservers; public Map> cachedObserverFiltering; List migratingPoints; public int migrationCooldown; public boolean derailed; public int fuelTicks; public int honkTicks; public Boolean lowHonk; public int honkPitch; public float accumulatedSteamRelease; int tickOffset; double[] stress; // advancements public Player backwardsDriver; public Train(UUID id, UUID owner, TrackGraph graph, List carriages, List carriageSpacing, boolean doubleEnded) { this.id = id; this.owner = owner; this.graph = graph; this.carriages = carriages; this.carriageSpacing = carriageSpacing; this.icon = TrainIconType.getDefault(); this.stress = new double[carriageSpacing.size()]; this.name = Lang.translateDirect("train.unnamed"); this.status = new TrainStatus(this); this.doubleEnded = doubleEnded; carriages.forEach(c -> c.setTrain(this)); navigation = new Navigation(this); runtime = new ScheduleRuntime(this); migratingPoints = new ArrayList<>(); currentStation = null; manualSteer = SteerDirection.NONE; occupiedSignalBlocks = new HashMap<>(); reservedSignalBlocks = new HashSet<>(); occupiedObservers = new HashSet<>(); cachedObserverFiltering = new HashMap<>(); tickOffset = Create.RANDOM.nextInt(100); } public void earlyTick(Level level) { status.tick(level); if (graph == null && !migratingPoints.isEmpty()) reattachToTracks(level); if (graph == null) { addToSignalGroups(occupiedSignalBlocks.keySet()); return; } if (updateSignalBlocks) { updateSignalBlocks = false; collectInitiallyOccupiedSignalBlocks(); } addToSignalGroups(occupiedSignalBlocks.keySet()); addToSignalGroups(reservedSignalBlocks); if (occupiedObservers.isEmpty()) return; tickOccupiedObservers(level); } private void tickOccupiedObservers(Level level) { int storageVersion = 0; for (Carriage carriage : carriages) storageVersion += carriage.storage.getVersion(); for (UUID uuid : occupiedObservers) { TrackObserver observer = graph.getPoint(EdgePointType.OBSERVER, uuid); if (observer == null) continue; ItemStack filter = observer.getFilter(); if (filter.isEmpty()) { observer.keepAlive(this); continue; } Pair cachedMatch = cachedObserverFiltering.computeIfAbsent(uuid, $ -> Pair.of(-1, false)); boolean shouldActivate = cachedMatch.getSecond(); if (cachedMatch.getFirst() == storageVersion) { if (shouldActivate) observer.keepAlive(this); continue; } shouldActivate = false; for (Carriage carriage : carriages) { if (shouldActivate) break; IItemHandlerModifiable inv = carriage.storage.getItems(); if (inv != null) { for (int slot = 0; slot < inv.getSlots(); slot++) { if (shouldActivate) break; ItemStack extractItem = inv.extractItem(slot, 1, true); if (extractItem.isEmpty()) continue; shouldActivate |= FilterItem.test(level, extractItem, filter); } } IFluidHandler tank = carriage.storage.getFluids(); if (tank != null) { for (int slot = 0; slot < tank.getTanks(); slot++) { if (shouldActivate) break; FluidStack drain = tank.drain(1, FluidAction.SIMULATE); if (drain.isEmpty()) continue; shouldActivate |= FilterItem.test(level, drain, filter); } } } cachedObserverFiltering.put(uuid, Pair.of(storageVersion, shouldActivate)); if (shouldActivate) observer.keepAlive(this); } } private void addToSignalGroups(Collection groups) { Map groupMap = Create.RAILWAYS.signalEdgeGroups; for (Iterator iterator = groups.iterator(); iterator.hasNext();) { SignalEdgeGroup signalEdgeGroup = groupMap.get(iterator.next()); if (signalEdgeGroup == null) iterator.remove(); else signalEdgeGroup.trains.add(this); } } public void tick(Level level) { Create.RAILWAYS.markTracksDirty(); if (graph == null) { carriages.forEach(c -> c.manageEntities(level)); updateConductors(); return; } updateConductors(); runtime.tick(level); navigation.tick(level); tickPassiveSlowdown(); if (derailed) tickDerailedSlowdown(); double distance = speed; Carriage previousCarriage = null; int carriageCount = carriages.size(); boolean stalled = false; double maxStress = 0; if (carriageWaitingForChunks != -1) distance = 0; for (int i = 0; i < carriageCount; i++) { Carriage carriage = carriages.get(i); if (previousCarriage != null) { int target = carriageSpacing.get(i - 1); double actual = target; TravellingPoint leadingPoint = carriage.getLeadingPoint(); TravellingPoint trailingPoint = previousCarriage.getTrailingPoint(); int entries = 0; double total = 0; if (leadingPoint.node1 != null && trailingPoint.node1 != null) { ResourceKey d1 = leadingPoint.node1.getLocation().dimension; ResourceKey d2 = trailingPoint.node1.getLocation().dimension; for (boolean b : Iterate.trueAndFalse) { ResourceKey d = b ? d1 : d2; if (!b && d1.equals(d2)) continue; if (!d1.equals(d2)) continue; DimensionalCarriageEntity dimensional = carriage.getDimensionalIfPresent(d); DimensionalCarriageEntity dimensional2 = previousCarriage.getDimensionalIfPresent(d); if (dimensional == null || dimensional2 == null) continue; Vec3 leadingAnchor = dimensional.leadingAnchor(); Vec3 trailingAnchor = dimensional2.trailingAnchor(); if (leadingAnchor == null || trailingAnchor == null) continue; double distanceTo = leadingAnchor.distanceToSqr(trailingAnchor); if (carriage.leadingBogey().isUpsideDown() != previousCarriage.trailingBogey().isUpsideDown()) { distanceTo = Math.sqrt(distanceTo - 4); } else { distanceTo = Math.sqrt(distanceTo); } total += distanceTo; entries++; } } if (entries > 0) actual = total / entries; stress[i - 1] = target - actual; maxStress = Math.max(maxStress, Math.abs(target - actual)); } previousCarriage = carriage; if (carriage.stalled) { if (speedBeforeStall == null) speedBeforeStall = speed; distance = 0; speed = 0; stalled = true; } } if (!stalled && speedBeforeStall != null) { speed = Mth.clamp(speedBeforeStall, -1, 1); speedBeforeStall = null; } // positive stress: carriages should move apart // negative stress: carriages should move closer boolean approachingStation = navigation.distanceToDestination < 5; double leadingModifier = approachingStation ? 0.75d : 0.5d; double trailingModifier = approachingStation ? 0d : 0.125d; boolean blocked = false; boolean iterateFromBack = speed < 0; for (int index = 0; index < carriageCount; index++) { int i = iterateFromBack ? carriageCount - 1 - index : index; double leadingStress = i == 0 ? 0 : stress[i - 1] * -(iterateFromBack ? trailingModifier : leadingModifier); double trailingStress = i == stress.length ? 0 : stress[i] * (iterateFromBack ? leadingModifier : trailingModifier); Carriage carriage = carriages.get(i); TravellingPoint toFollowForward = i == 0 ? null : carriages.get(i - 1) .getTrailingPoint(); TravellingPoint toFollowBackward = i == carriageCount - 1 ? null : carriages.get(i + 1) .getLeadingPoint(); double totalStress = derailed ? 0 : leadingStress + trailingStress; boolean first = i == 0; boolean last = i == carriageCount - 1; int carriageType = first ? last ? Carriage.BOTH : Carriage.FIRST : last ? Carriage.LAST : Carriage.MIDDLE; double actualDistance = carriage.travel(level, graph, distance + totalStress, toFollowForward, toFollowBackward, carriageType); blocked |= carriage.blocked || carriage.isOnIncompatibleTrack(); boolean onTwoBogeys = carriage.isOnTwoBogeys(); maxStress = Math.max(maxStress, onTwoBogeys ? carriage.bogeySpacing - carriage.getAnchorDiff() : 0); maxStress = Math.max(maxStress, carriage.leadingBogey() .getStress()); if (onTwoBogeys) maxStress = Math.max(maxStress, carriage.trailingBogey() .getStress()); if (index == 0) { distance = actualDistance; collideWithOtherTrains(level, carriage); backwardsDriver = null; if (graph == null) return; } } if (blocked) { speed = 0; navigation.cancelNavigation(); runtime.tick(level); status.endOfTrack(); } else if (maxStress > 4) { speed = 0; navigation.cancelNavigation(); runtime.tick(level); derailed = true; status.highStress(); } else if (speed != 0) status.trackOK(); updateNavigationTarget(distance); } public IEdgePointListener frontSignalListener() { return (distance, couple) -> { if (couple.getFirst()instanceof GlobalStation station) { if (!station.canApproachFrom(couple.getSecond() .getSecond()) || navigation.destination != station) return false; speed = 0; navigation.distanceToDestination = 0; navigation.currentPath.clear(); arriveAt(navigation.destination); navigation.destination = null; return true; } if (couple.getFirst()instanceof TrackObserver observer) { occupiedObservers.add(observer.getId()); return false; } if (!(couple.getFirst()instanceof SignalBoundary signal)) return false; if (navigation.waitingForSignal != null && navigation.waitingForSignal.getFirst() .equals(signal.getId())) { speed = 0; navigation.distanceToSignal = 0; return true; } UUID groupId = signal.getGroup(couple.getSecond() .getSecond()); SignalEdgeGroup signalEdgeGroup = Create.RAILWAYS.signalEdgeGroups.get(groupId); if (signalEdgeGroup == null) return false; if ((runtime.getSchedule() == null || runtime.paused) && signalEdgeGroup.isOccupiedUnless(this)) carriages.forEach(c -> c.forEachPresentEntity(cce -> cce.getControllingPlayer() .ifPresent(uuid -> AllAdvancements.RED_SIGNAL.awardTo(cce.level.getPlayerByUUID(uuid))))); signalEdgeGroup.reserved = signal; occupy(groupId, signal.id); return false; }; } public void cancelStall() { speedBeforeStall = null; carriages.forEach(c -> { c.stalled = false; c.forEachPresentEntity(cce -> cce.getContraption() .getActors() .forEach(pair -> { MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(pair.getKey().state); if (behaviour != null) behaviour.cancelStall(pair.getValue()); })); }); } private boolean occupy(UUID groupId, @Nullable UUID boundaryId) { reservedSignalBlocks.remove(groupId); if (boundaryId != null && occupiedSignalBlocks.containsKey(groupId)) if (boundaryId.equals(occupiedSignalBlocks.get(groupId))) return false; return occupiedSignalBlocks.put(groupId, boundaryId) == null; } public IEdgePointListener backSignalListener() { return (distance, couple) -> { if (couple.getFirst()instanceof TrackObserver observer) { occupiedObservers.remove(observer.getId()); cachedObserverFiltering.remove(observer.getId()); return false; } if (!(couple.getFirst()instanceof SignalBoundary signal)) return false; UUID groupId = signal.getGroup(couple.getSecond() .getFirst()); occupiedSignalBlocks.remove(groupId); return false; }; } private void updateNavigationTarget(double distance) { if (navigation.destination == null) return; Pair blockingSignal = navigation.waitingForSignal; boolean fullRefresh = navigation.distanceToDestination > 100 && navigation.distanceToDestination % 100 > 20; boolean signalRefresh = blockingSignal != null && navigation.distanceToSignal % 50 > 5; boolean partialRefresh = navigation.distanceToDestination < 100 && navigation.distanceToDestination % 50 > 5; double toSubstract = navigation.destinationBehindTrain ? -distance : distance; boolean navigatingManually = runtime.paused; navigation.distanceToDestination -= toSubstract; if (blockingSignal != null) { navigation.distanceToSignal -= toSubstract; signalRefresh &= navigation.distanceToSignal % 50 < 5; } fullRefresh &= navigation.distanceToDestination % 100 <= 20; partialRefresh &= navigation.distanceToDestination % 50 <= 5; if (blockingSignal != null && navigation.ticksWaitingForSignal % 100 == 50) { SignalBoundary signal = graph.getPoint(EdgePointType.SIGNAL, blockingSignal.getFirst()); fullRefresh |= signal != null && signal.types.get(blockingSignal.getSecond()) == SignalType.CROSS_SIGNAL; } if (signalRefresh) navigation.waitingForSignal = null; if (!fullRefresh && !partialRefresh) return; if (!reservedSignalBlocks.isEmpty()) return; GlobalStation destination = navigation.destination; if (!navigatingManually && fullRefresh) { GlobalStation preferredDestination = runtime.startCurrentInstruction(); if (preferredDestination != null) destination = preferredDestination; } navigation.startNavigation(destination, navigatingManually ? -1 : Double.MAX_VALUE, false); } private void tickDerailedSlowdown() { speed /= 3f; if (Mth.equal(speed, 0)) speed = 0; } private void tickPassiveSlowdown() { if (!manualTick && navigation.destination == null && speed != 0) { double acceleration = acceleration(); if (speed > 0) { speed = Math.max(speed - acceleration, 0); } else speed = Math.min(speed + acceleration, 0); } manualTick = false; } private void updateConductors() { for (Carriage carriage : carriages) carriage.updateConductors(); } public boolean hasForwardConductor() { for (Carriage carriage : carriages) if (carriage.presentConductors.getFirst()) return true; return false; } public boolean hasBackwardConductor() { for (Carriage carriage : carriages) if (carriage.presentConductors.getSecond()) return true; return false; } private void collideWithOtherTrains(Level level, Carriage carriage) { if (derailed) return; TravellingPoint trailingPoint = carriage.getTrailingPoint(); TravellingPoint leadingPoint = carriage.getLeadingPoint(); if (leadingPoint.node1 == null || trailingPoint.node1 == null) return; ResourceKey dimension = leadingPoint.node1.getLocation().dimension; if (!dimension.equals(trailingPoint.node1.getLocation().dimension)) return; Vec3 start = (speed < 0 ? trailingPoint : leadingPoint).getPosition(); Vec3 end = (speed < 0 ? leadingPoint : trailingPoint).getPosition(); Pair collision = findCollidingTrain(level, start, end, this, dimension); if (collision == null) return; Train train = collision.getFirst(); double combinedSpeed = Math.abs(speed) + Math.abs(train.speed); if (combinedSpeed > .2f) { Vec3 v = collision.getSecond(); level.explode(null, v.x, v.y, v.z, (float) Math.min(3 * combinedSpeed, 5), BlockInteraction.NONE); } crash(); train.crash(); } public static Pair findCollidingTrain(Level level, Vec3 start, Vec3 end, Train ignore, ResourceKey dimension) { for (Train train : Create.RAILWAYS.sided(level).trains.values()) { if (train == ignore) continue; Vec3 diff = end.subtract(start); Vec3 lastPoint = null; for (Carriage otherCarriage : train.carriages) { for (boolean betweenBits : Iterate.trueAndFalse) { if (betweenBits && lastPoint == null) continue; TravellingPoint otherLeading = otherCarriage.getLeadingPoint(); TravellingPoint otherTrailing = otherCarriage.getTrailingPoint(); if (otherLeading.edge == null || otherTrailing.edge == null) continue; ResourceKey otherDimension = otherLeading.node1.getLocation().dimension; if (!otherDimension.equals(otherTrailing.node1.getLocation().dimension)) continue; if (!otherDimension.equals(dimension)) continue; Vec3 start2 = otherLeading.getPosition(); Vec3 end2 = otherTrailing.getPosition(); if (betweenBits) { end2 = start2; start2 = lastPoint; } lastPoint = end2; if ((end.y < end2.y - 3 || end2.y < end.y - 3) && (start.y < start2.y - 3 || start2.y < start.y - 3)) continue; Vec3 diff2 = end2.subtract(start2); Vec3 normedDiff = diff.normalize(); Vec3 normedDiff2 = diff2.normalize(); double[] intersect = VecHelper.intersect(start, start2, normedDiff, normedDiff2, Axis.Y); if (intersect == null) { Vec3 intersectSphere = VecHelper.intersectSphere(start2, normedDiff2, start, .125f); if (intersectSphere == null) continue; if (!Mth.equal(normedDiff2.dot(intersectSphere.subtract(start2) .normalize()), 1)) continue; intersect = new double[2]; intersect[0] = intersectSphere.distanceTo(start) - .125; intersect[1] = intersectSphere.distanceTo(start2) - .125; } if (intersect[0] > diff.length()) continue; if (intersect[1] > diff2.length()) continue; if (intersect[0] < 0) continue; if (intersect[1] < 0) continue; return Pair.of(train, start.add(normedDiff.scale(intersect[0]))); } } } return null; } public void crash() { navigation.cancelNavigation(); if (derailed) return; speed = -Mth.clamp(speed, -.5, .5); derailed = true; graph = null; status.crash(); for (Carriage carriage : carriages) carriage.forEachPresentEntity(e -> e.getIndirectPassengers() .forEach(entity -> { if (!(entity instanceof Player p)) return; Optional controllingPlayer = e.getControllingPlayer(); if (controllingPlayer.isPresent() && controllingPlayer.get() .equals(p.getUUID())) return; AllAdvancements.TRAIN_CRASH.awardTo(p); })); if (backwardsDriver != null) AllAdvancements.TRAIN_CRASH_BACKWARDS.awardTo(backwardsDriver); } public boolean disassemble(Direction assemblyDirection, BlockPos pos) { if (!canDisassemble()) return false; int offset = 1; boolean backwards = currentlyBackwards; Level level = null; for (int i = 0; i < carriages.size(); i++) { Carriage carriage = carriages.get(backwards ? carriages.size() - i - 1 : i); CarriageContraptionEntity entity = carriage.anyAvailableEntity(); if (entity == null) return false; level = entity.level; if (entity.getContraption()instanceof CarriageContraption cc) cc.returnStorageForDisassembly(carriage.storage); entity.setPos(Vec3 .atLowerCornerOf(pos.relative(assemblyDirection, backwards ? offset + carriage.bogeySpacing : offset).below(carriage.leadingBogey().isUpsideDown() ? 2 : 0))); entity.disassemble(); for (CarriageBogey bogey : carriage.bogeys) { if (bogey == null) continue; Vec3 bogeyPosition = bogey.getAnchorPosition(); if (bogeyPosition == null) continue; BlockEntity be = level.getBlockEntity(new BlockPos(bogeyPosition)); if (!(be instanceof AbstractBogeyTileEntity sbte)) continue; sbte.setBogeyData(bogey.bogeyData); } offset += carriage.bogeySpacing; if (i < carriageSpacing.size()) offset += carriageSpacing.get(backwards ? carriageSpacing.size() - i - 1 : i); } GlobalStation currentStation = getCurrentStation(); if (currentStation != null) { currentStation.cancelReservation(this); BlockPos tilePos = currentStation.getTilePos(); if (level.getBlockEntity(tilePos)instanceof StationTileEntity ste) ste.lastDisassembledTrainName = name.copy(); } Create.RAILWAYS.removeTrain(id); AllPackets.channel.send(PacketDistributor.ALL.noArg(), new TrainPacket(this, false)); return true; } public boolean canDisassemble() { for (Carriage carriage : carriages) { if (carriage.presentInMultipleDimensions()) return false; CarriageContraptionEntity entity = carriage.anyAvailableEntity(); if (entity == null) return false; if (!Mth.equal(entity.pitch, 0)) return false; if (!Mth.equal(((entity.yaw % 90) + 360) % 90, 0)) return false; } return true; } public boolean isTravellingOn(TrackNode node) { MutableBoolean affected = new MutableBoolean(false); forEachTravellingPoint(tp -> { if (tp.node1 == node || tp.node2 == node) affected.setTrue(); }); return affected.booleanValue(); } public void detachFromTracks() { migratingPoints.clear(); navigation.cancelNavigation(); forEachTravellingPoint(tp -> migratingPoints.add(new TrainMigration(tp))); graph = null; } public void forEachTravellingPoint(Consumer callback) { for (Carriage c : carriages) { c.leadingBogey().points.forEach(callback::accept); if (c.isOnTwoBogeys()) c.trailingBogey().points.forEach(callback::accept); } } public void forEachTravellingPointBackwards(BiConsumer callback) { double lastWheelOffset = 0; for (int i = 0; i < carriages.size(); i++) { int index = carriages.size() - i - 1; Carriage carriage = carriages.get(index); CarriageBogey trailingBogey = carriage.trailingBogey(); double trailSpacing = trailingBogey.type.getWheelPointSpacing(); // trailing point callback.accept(trailingBogey.trailing(), i == 0 ? 0 : carriageSpacing.get(index) - lastWheelOffset - trailSpacing / 2); // inside 1st bogey callback.accept(trailingBogey.leading(), trailSpacing); lastWheelOffset = trailSpacing / 2; if (!carriage.isOnTwoBogeys()) continue; CarriageBogey leadingBogey = carriage.leadingBogey(); double leadSpacing = carriage.leadingBogey().type.getWheelPointSpacing(); // between bogeys callback.accept(leadingBogey.trailing(), carriage.bogeySpacing - lastWheelOffset - leadSpacing / 2); // inside 2nd bogey callback.accept(trailingBogey.leading(), leadSpacing); lastWheelOffset = leadSpacing / 2; } } public void reattachToTracks(Level level) { if (migrationCooldown > 0) { migrationCooldown--; return; } Set> entrySet = new HashSet<>(Create.RAILWAYS.trackNetworks.entrySet()); Map> successfulMigrations = new HashMap<>(); for (TrainMigration md : migratingPoints) { for (Iterator> iterator = entrySet.iterator(); iterator.hasNext();) { Entry entry = iterator.next(); GraphLocation gl = md.tryMigratingTo(entry.getValue()); if (gl == null) { iterator.remove(); continue; } successfulMigrations.computeIfAbsent(entry.getKey(), uuid -> new ArrayList<>()) .add(gl); } } if (entrySet.isEmpty()) { migrationCooldown = 40; status.failedMigration(); derailed = true; return; } for (Entry entry : entrySet) { graph = entry.getValue(); List locations = successfulMigrations.get(entry.getKey()); forEachTravellingPoint(tp -> tp.migrateTo(locations)); migratingPoints.clear(); if (derailed) status.successfulMigration(); derailed = false; if (runtime.getSchedule() != null && runtime.state == State.IN_TRANSIT) runtime.state = State.PRE_TRANSIT; GlobalStation currentStation = getCurrentStation(); if (currentStation != null) currentStation.reserveFor(this); updateSignalBlocks = true; migrationCooldown = 0; return; } } public int getTotalLength() { int length = 0; for (int i = 0; i < carriages.size(); i++) { Carriage carriage = carriages.get(i); if (i == 0) length += carriage.leadingBogey().type.getWheelPointSpacing() / 2; if (i == carriages.size() - 1) length += carriage.trailingBogey().type.getWheelPointSpacing() / 2; length += carriage.bogeySpacing; if (i < carriageSpacing.size()) length += carriageSpacing.get(i); } return length; } public void leaveStation() { GlobalStation currentStation = getCurrentStation(); if (currentStation != null) currentStation.trainDeparted(this); this.currentStation = null; } public void arriveAt(GlobalStation station) { setCurrentStation(station); reservedSignalBlocks.clear(); runtime.destinationReached(); } public void setCurrentStation(GlobalStation station) { currentStation = station.id; } public GlobalStation getCurrentStation() { if (currentStation == null) return null; if (graph == null) return null; return graph.getPoint(EdgePointType.STATION, currentStation); } @Nullable public LivingEntity getOwner(Level level) { try { UUID uuid = owner; return uuid == null ? null : level.getServer() .getPlayerList() .getPlayer(uuid); } catch (IllegalArgumentException illegalargumentexception) { return null; } } public void approachTargetSpeed(float accelerationMod) { double actualTarget = targetSpeed; if (Mth.equal(actualTarget, speed)) return; if (manualTick) leaveStation(); double acceleration = acceleration(); if (speed < actualTarget) speed = Math.min(speed + acceleration * accelerationMod, actualTarget); else if (speed > actualTarget) speed = Math.max(speed - acceleration * accelerationMod, actualTarget); } public void collectInitiallyOccupiedSignalBlocks() { TravellingPoint trailingPoint = carriages.get(carriages.size() - 1) .getTrailingPoint(); TrackNode node1 = trailingPoint.node1; TrackNode node2 = trailingPoint.node2; TrackEdge edge = trailingPoint.edge; double position = trailingPoint.position; EdgeData signalData = edge.getEdgeData(); occupiedSignalBlocks.clear(); reservedSignalBlocks.clear(); occupiedObservers.clear(); cachedObserverFiltering.clear(); TravellingPoint signalScout = new TravellingPoint(node1, node2, edge, position, false); Map allGroups = Create.RAILWAYS.signalEdgeGroups; MutableObject prevGroup = new MutableObject<>(null); if (signalData.hasSignalBoundaries()) { SignalBoundary nextBoundary = signalData.next(EdgePointType.SIGNAL, position); if (nextBoundary == null) { double d = 0; SignalBoundary prev = null; SignalBoundary current = signalData.next(EdgePointType.SIGNAL, 0); while (current != null) { prev = current; d = current.getLocationOn(edge); current = signalData.next(EdgePointType.SIGNAL, d); } if (prev != null) { UUID group = prev.getGroup(node2); if (Create.RAILWAYS.signalEdgeGroups.containsKey(group)) { occupy(group, null); prevGroup.setValue(group); } } } else { UUID group = nextBoundary.getGroup(node1); if (Create.RAILWAYS.signalEdgeGroups.containsKey(group)) { occupy(group, null); prevGroup.setValue(group); } } } else { UUID groupId = signalData.getEffectiveEdgeGroupId(graph); if (allGroups.containsKey(groupId)) { occupy(groupId, null); prevGroup.setValue(groupId); } } forEachTravellingPointBackwards((tp, d) -> { signalScout.travel(graph, d, signalScout.follow(tp), (distance, couple) -> { if (couple.getFirst()instanceof TrackObserver observer) { occupiedObservers.add(observer.getId()); return false; } if (!(couple.getFirst()instanceof SignalBoundary signal)) return false; couple.getSecond() .map(signal::getGroup) .forEach(id -> { if (!Create.RAILWAYS.signalEdgeGroups.containsKey(id)) return; if (id.equals(prevGroup.getValue())) return; occupy(id, null); prevGroup.setValue(id); }); return false; }, signalScout.ignoreTurns()); }); } public boolean shouldCarriageSyncThisTick(long gameTicks, int updateInterval) { return (gameTicks + tickOffset) % updateInterval == 0; } public Couple> getEndpointEdges() { return Couple.create(carriages.get(0) .getLeadingPoint(), carriages.get(carriages.size() - 1) .getTrailingPoint()) .map(tp -> Couple.create(tp.node1, tp.node2)); } public static class Penalties { static final int STATION = 200, STATION_WITH_TRAIN = 300; static final int MANUAL_TRAIN = 200, IDLE_TRAIN = 700, ARRIVING_TRAIN = 50, WAITING_TRAIN = 50, ANY_TRAIN = 25, RED_SIGNAL = 25, REDSTONE_RED_SIGNAL = 400; } public int getNavigationPenalty() { if (manualTick) return Penalties.MANUAL_TRAIN; if (runtime.getSchedule() == null || runtime.paused) return Penalties.IDLE_TRAIN; if (navigation.waitingForSignal != null && navigation.ticksWaitingForSignal > 0) return Penalties.WAITING_TRAIN + Math.min(navigation.ticksWaitingForSignal / 20, 1000); if (navigation.destination != null && navigation.distanceToDestination < 50 || navigation.distanceToSignal < 20) return Penalties.ARRIVING_TRAIN; return Penalties.ANY_TRAIN; } public void burnFuel() { if (fuelTicks > 0) { fuelTicks--; return; } boolean iterateFromBack = speed < 0; int carriageCount = carriages.size(); for (int index = 0; index < carriageCount; index++) { int i = iterateFromBack ? carriageCount - 1 - index : index; Carriage carriage = carriages.get(i); IItemHandlerModifiable fuelItems = carriage.storage.getFuelItems(); if (fuelItems == null) continue; for (int slot = 0; slot < fuelItems.getSlots(); slot++) { ItemStack stack = fuelItems.extractItem(slot, 1, true); int burnTime = ForgeHooks.getBurnTime(stack, null); if (burnTime <= 0) continue; stack = fuelItems.extractItem(slot, 1, false); fuelTicks += burnTime * stack.getCount(); ItemStack containerItem = stack.getContainerItem(); if (!containerItem.isEmpty()) ItemHandlerHelper.insertItemStacked(fuelItems, containerItem, false); return; } } } public float maxSpeed() { return (fuelTicks > 0 ? AllConfigs.SERVER.trains.poweredTrainTopSpeed.getF() : AllConfigs.SERVER.trains.trainTopSpeed.getF()) / 20; } public float maxTurnSpeed() { return (fuelTicks > 0 ? AllConfigs.SERVER.trains.poweredTrainTurningTopSpeed.getF() : AllConfigs.SERVER.trains.trainTurningTopSpeed.getF()) / 20; } public float acceleration() { return (fuelTicks > 0 ? AllConfigs.SERVER.trains.poweredTrainAcceleration.getF() : AllConfigs.SERVER.trains.trainAcceleration.getF()) / 400; } public CompoundTag write(DimensionPalette dimensions) { CompoundTag tag = new CompoundTag(); tag.putUUID("Id", id); tag.putUUID("Owner", owner); if (graph != null) tag.putUUID("Graph", graph.id); tag.put("Carriages", NBTHelper.writeCompoundList(carriages, c -> c.write(dimensions))); tag.putIntArray("CarriageSpacing", carriageSpacing); tag.putBoolean("DoubleEnded", doubleEnded); tag.putDouble("Speed", speed); tag.putDouble("Throttle", throttle); if (speedBeforeStall != null) tag.putDouble("SpeedBeforeStall", speedBeforeStall); tag.putInt("Fuel", fuelTicks); tag.putDouble("TargetSpeed", targetSpeed); tag.putString("IconType", icon.id.toString()); tag.putString("Name", Component.Serializer.toJson(name)); if (currentStation != null) tag.putUUID("Station", currentStation); tag.putBoolean("Backwards", currentlyBackwards); tag.putBoolean("Derailed", derailed); tag.putBoolean("UpdateSignals", updateSignalBlocks); tag.put("SignalBlocks", NBTHelper.writeCompoundList(occupiedSignalBlocks.entrySet(), e -> { CompoundTag compoundTag = new CompoundTag(); compoundTag.putUUID("Id", e.getKey()); if (e.getValue() != null) compoundTag.putUUID("Boundary", e.getValue()); return compoundTag; })); tag.put("ReservedSignalBlocks", NBTHelper.writeCompoundList(reservedSignalBlocks, uid -> { CompoundTag compoundTag = new CompoundTag(); compoundTag.putUUID("Id", uid); return compoundTag; })); tag.put("OccupiedObservers", NBTHelper.writeCompoundList(occupiedObservers, uid -> { CompoundTag compoundTag = new CompoundTag(); compoundTag.putUUID("Id", uid); return compoundTag; })); tag.put("MigratingPoints", NBTHelper.writeCompoundList(migratingPoints, tm -> tm.write(dimensions))); tag.put("Runtime", runtime.write()); tag.put("Navigation", navigation.write(dimensions)); return tag; } public static Train read(CompoundTag tag, Map trackNetworks, DimensionPalette dimensions) { UUID id = tag.getUUID("Id"); UUID owner = tag.getUUID("Owner"); UUID graphId = tag.contains("Graph") ? tag.getUUID("Graph") : null; TrackGraph graph = graphId == null ? null : trackNetworks.get(graphId); List carriages = new ArrayList<>(); NBTHelper.iterateCompoundList(tag.getList("Carriages", Tag.TAG_COMPOUND), c -> carriages.add(Carriage.read(c, graph, dimensions))); List carriageSpacing = new ArrayList<>(); for (int i : tag.getIntArray("CarriageSpacing")) carriageSpacing.add(i); boolean doubleEnded = tag.getBoolean("DoubleEnded"); Train train = new Train(id, owner, graph, carriages, carriageSpacing, doubleEnded); train.speed = tag.getDouble("Speed"); train.throttle = tag.getDouble("Throttle"); if (tag.contains("SpeedBeforeStall")) train.speedBeforeStall = tag.getDouble("SpeedBeforeStall"); train.targetSpeed = tag.getDouble("TargetSpeed"); train.icon = TrainIconType.byId(new ResourceLocation(tag.getString("IconType"))); train.name = Component.Serializer.fromJson(tag.getString("Name")); train.currentStation = tag.contains("Station") ? tag.getUUID("Station") : null; train.currentlyBackwards = tag.getBoolean("Backwards"); train.derailed = tag.getBoolean("Derailed"); train.updateSignalBlocks = tag.getBoolean("UpdateSignals"); train.fuelTicks = tag.getInt("Fuel"); NBTHelper.iterateCompoundList(tag.getList("SignalBlocks", Tag.TAG_COMPOUND), c -> train.occupiedSignalBlocks .put(c.getUUID("Id"), c.contains("Boundary") ? c.getUUID("Boundary") : null)); NBTHelper.iterateCompoundList(tag.getList("ReservedSignalBlocks", Tag.TAG_COMPOUND), c -> train.reservedSignalBlocks.add(c.getUUID("Id"))); NBTHelper.iterateCompoundList(tag.getList("OccupiedObservers", Tag.TAG_COMPOUND), c -> train.occupiedObservers.add(c.getUUID("Id"))); NBTHelper.iterateCompoundList(tag.getList("MigratingPoints", Tag.TAG_COMPOUND), c -> train.migratingPoints.add(TrainMigration.read(c, dimensions))); train.runtime.read(tag.getCompound("Runtime")); train.navigation.read(tag.getCompound("Navigation"), graph, dimensions); if (train.getCurrentStation() != null) train.getCurrentStation() .reserveFor(train); return train; } public int countPlayerPassengers() { AtomicInteger count = new AtomicInteger(); for (Carriage carriage : carriages) carriage.forEachPresentEntity(e -> e.getIndirectPassengers() .forEach(p -> { if (p instanceof Player) count.incrementAndGet(); })); return count.intValue(); } public void determineHonk(Level level) { if (lowHonk != null) return; for (int index = 0; index < carriages.size(); index++) { Carriage carriage = carriages.get(index); DimensionalCarriageEntity dimensional = carriage.getDimensionalIfPresent(level.dimension()); if (dimensional == null) return; CarriageContraptionEntity entity = dimensional.entity.get(); if (entity == null || !(entity.getContraption()instanceof CarriageContraption otherCC)) break; Pair first = otherCC.soundQueue.getFirstWhistle(entity); if (first != null) { lowHonk = first.getFirst(); honkPitch = first.getSecond(); } } } public float distanceToLocationSqr(Level level, Vec3 location) { float distance = Float.MAX_VALUE; for (Carriage carriage : carriages) { DimensionalCarriageEntity dce = carriage.getDimensionalIfPresent(level.dimension()); if (dce == null || dce.positionAnchor == null) continue; distance = Math.min(distance, (float) dce.positionAnchor.distanceToSqr(location)); } return distance; } }