Add ComputerCraft integration to Train Signals

- Makes computer-controlled signals unable to be changed by external
  factors
- Emit CC event train_signal_state_change with the new state as
  parameter whenever the signal changes
- Add CC getState() function to get the current signal state
- Add CC isForcedRed()/setForcedRed(forced) functions to force the
  signal to be red, replacing redstone control
- Add CC getSignalType()/cycleSignalType() function to change the
  signal signal type in the same manner as using the wrench
- Add CC listBlockingTrainNames() function to list names of trains
  blocking the track group ahead of the signal
This commit is contained in:
Céleste Wouters 2024-08-23 18:56:25 +02:00
parent 75081320ac
commit 7c385510d4
Failed to generate hash of commit
13 changed files with 208 additions and 6 deletions

View file

@ -3052,6 +3052,7 @@
"create.track_signal.cannot_change_mode": "Unable to switch mode of this Signal",
"create.track_signal.mode_change.cross_signal": "-> Allow passage if section fully traversable",
"create.track_signal.mode_change.entry_signal": "-> Allow passage if section unoccupied",
"create.track_signal.mode_controlled_by_computer": "Signal mode is controlled by computer",
"create.track_target.clear": "Cleared track selection",
"create.track_target.invalid": "Cannot target this track here",
"create.track_target.missing": "Right-click the targeted train track first",

View file

@ -1,5 +1,9 @@
package com.simibubi.create.compat.computercraft;
import org.jetbrains.annotations.NotNull;
import com.simibubi.create.compat.computercraft.events.ComputerEvent;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import com.simibubi.create.foundation.blockEntity.behaviour.BehaviourType;
import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour;
@ -49,6 +53,8 @@ public class AbstractComputerBehaviour extends BlockEntityBehaviour {
return hasAttachedComputer;
}
public void prepareComputerEvent(@NotNull ComputerEvent event) {}
@Override
public BehaviourType<?> getType() {
return TYPE;

View file

@ -0,0 +1,3 @@
package com.simibubi.create.compat.computercraft.events;
public interface ComputerEvent {}

View file

@ -0,0 +1,13 @@
package com.simibubi.create.compat.computercraft.events;
import com.simibubi.create.content.trains.signal.SignalBlockEntity;
public class SignalStateChangeEvent implements ComputerEvent {
public SignalBlockEntity.SignalState state;
public SignalStateChangeEvent(SignalBlockEntity.SignalState state) {
this.state = state;
}
}

View file

@ -1,19 +1,26 @@
package com.simibubi.create.compat.computercraft.implementation;
import org.jetbrains.annotations.NotNull;
import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour;
import com.simibubi.create.compat.computercraft.events.ComputerEvent;
import com.simibubi.create.compat.computercraft.implementation.peripherals.DisplayLinkPeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.NixieTubePeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.SequencedGearshiftPeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.SignalPeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.SpeedControllerPeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.SpeedGaugePeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.StationPeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.StressGaugePeripheral;
import com.simibubi.create.compat.computercraft.implementation.peripherals.SyncedPeripheral;
import com.simibubi.create.content.kinetics.gauge.SpeedGaugeBlockEntity;
import com.simibubi.create.content.kinetics.gauge.StressGaugeBlockEntity;
import com.simibubi.create.content.kinetics.speedController.SpeedControllerBlockEntity;
import com.simibubi.create.content.kinetics.transmission.sequencer.SequencedGearshiftBlockEntity;
import com.simibubi.create.content.redstone.displayLink.DisplayLinkBlockEntity;
import com.simibubi.create.content.redstone.nixieTube.NixieTubeBlockEntity;
import com.simibubi.create.content.trains.signal.SignalBlockEntity;
import com.simibubi.create.content.trains.station.StationBlockEntity;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
@ -30,15 +37,15 @@ public class ComputerBehaviour extends AbstractComputerBehaviour {
protected static final Capability<IPeripheral> PERIPHERAL_CAPABILITY =
CapabilityManager.get(new CapabilityToken<>() {
});
LazyOptional<IPeripheral> peripheral;
NonNullSupplier<IPeripheral> peripheralSupplier;
LazyOptional<SyncedPeripheral<?>> peripheral;
NonNullSupplier<SyncedPeripheral<?>> peripheralSupplier;
public ComputerBehaviour(SmartBlockEntity te) {
super(te);
this.peripheralSupplier = getPeripheralFor(te);
}
public static NonNullSupplier<IPeripheral> getPeripheralFor(SmartBlockEntity be) {
public static NonNullSupplier<SyncedPeripheral<?>> getPeripheralFor(SmartBlockEntity be) {
if (be instanceof SpeedControllerBlockEntity scbe)
return () -> new SpeedControllerPeripheral(scbe, scbe.targetSpeed);
if (be instanceof DisplayLinkBlockEntity dlbe)
@ -47,6 +54,8 @@ public class ComputerBehaviour extends AbstractComputerBehaviour {
return () -> new NixieTubePeripheral(ntbe);
if (be instanceof SequencedGearshiftBlockEntity sgbe)
return () -> new SequencedGearshiftPeripheral(sgbe);
if (be instanceof SignalBlockEntity sbe)
return () -> new SignalPeripheral(sbe);
if (be instanceof SpeedGaugeBlockEntity sgbe)
return () -> new SpeedGaugePeripheral(sgbe);
if (be instanceof StressGaugeBlockEntity sgbe)
@ -76,4 +85,10 @@ public class ComputerBehaviour extends AbstractComputerBehaviour {
peripheral.invalidate();
}
@Override
public void prepareComputerEvent(@NotNull ComputerEvent event) {
if (peripheral != null)
peripheral.ifPresent(p -> p.prepareComputerEvent(event));
}
}

View file

@ -0,0 +1,104 @@
package com.simibubi.create.compat.computercraft.implementation.peripherals;
import java.util.Map;
import java.util.UUID;
import org.jetbrains.annotations.NotNull;
import com.simibubi.create.Create;
import com.simibubi.create.compat.computercraft.events.ComputerEvent;
import com.simibubi.create.compat.computercraft.events.SignalStateChangeEvent;
import com.simibubi.create.compat.computercraft.implementation.CreateLuaTable;
import com.simibubi.create.content.trains.entity.Train;
import com.simibubi.create.content.trains.signal.SignalBlock;
import com.simibubi.create.content.trains.signal.SignalBlockEntity;
import com.simibubi.create.content.trains.signal.SignalBoundary;
import com.simibubi.create.content.trains.signal.SignalEdgeGroup;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import net.createmod.catnip.data.Iterate;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
public class SignalPeripheral extends SyncedPeripheral<SignalBlockEntity> {
public SignalPeripheral(SignalBlockEntity blockEntity) {
super(blockEntity);
}
@LuaFunction
public final String getState() {
return blockEntity.getState().toString();
}
@LuaFunction
public final boolean isForcedRed() {
return blockEntity.getBlockState().getValue(SignalBlock.POWERED);
}
@LuaFunction(mainThread = true)
public final void setForcedRed(boolean powered) {
Level level = blockEntity.getLevel();
if (level != null)
level.setBlock(blockEntity.getBlockPos(),
blockEntity.getBlockState().setValue(SignalBlock.POWERED, powered), 2);
}
@LuaFunction
public final CreateLuaTable listBlockingTrainNames() throws LuaException {
SignalBoundary signal = blockEntity.getSignal();
if (signal == null)
throw new LuaException("no signal");
CreateLuaTable trainList = new CreateLuaTable();
int trainCounter = 1;
for (boolean current : Iterate.trueAndFalse) {
Map<BlockPos, Boolean> set = signal.blockEntities.get(current);
if (!set.containsKey(blockEntity.getBlockPos()))
continue;
UUID group = signal.groups.get(current);
Map<UUID, SignalEdgeGroup> signalEdgeGroups = Create.RAILWAYS.signalEdgeGroups;
SignalEdgeGroup signalEdgeGroup = signalEdgeGroups.get(group);
for (Train train : signalEdgeGroup.trains) {
trainList.put(trainCounter, train.name.getString());
trainCounter += 1;
}
}
return trainList;
}
@LuaFunction
public final String getSignalType() throws LuaException {
SignalBoundary signal = blockEntity.getSignal();
if (signal != null) {
return signal.getTypeFor(blockEntity.getBlockPos()).toString();
} else {
throw new LuaException("no signal");
}
}
@LuaFunction(mainThread = true)
public final void cycleSignalType() throws LuaException {
SignalBoundary signal = blockEntity.getSignal();
if (signal != null) {
signal.cycleSignalType(blockEntity.getBlockPos());
} else {
throw new LuaException("no signal");
}
}
@Override
public void prepareComputerEvent(@NotNull ComputerEvent event) {
if (event instanceof SignalStateChangeEvent ssce) {
queueEvent("train_signal_state_change", ssce.state.toString());
}
}
@NotNull
@Override
public String getType() {
return "Create_Signal";
}
}

View file

@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable;
import com.simibubi.create.AllPackets;
import com.simibubi.create.compat.computercraft.AttachedComputerPacket;
import com.simibubi.create.compat.computercraft.events.ComputerEvent;
import com.simibubi.create.compat.computercraft.implementation.ComputerBehaviour;
import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
@ -61,4 +62,21 @@ public abstract class SyncedPeripheral<T extends SmartBlockEntity> implements IP
return this == other;
}
public void prepareComputerEvent(@NotNull ComputerEvent event) {}
/**
* Queue an event to all attached computers. Adds the peripheral attachment name as 1st event argument, followed by
* any optional arguments passed to this method.
*/
protected void queueEvent(@NotNull String event, @Nullable Object... arguments) {
Object[] sourceAndArgs = new Object[arguments.length + 1];
System.arraycopy(arguments, 0, sourceAndArgs, 1, arguments.length);
synchronized (computers) {
for (IComputerAccess computer : computers) {
sourceAndArgs[0] = computer.getAttachmentName();
computer.queueEvent(event, sourceAndArgs);
}
}
}
}

View file

@ -1,5 +1,8 @@
package com.simibubi.create.content.trains.signal;
import java.util.Optional;
import java.util.Random;
import javax.annotation.Nullable;
import com.simibubi.create.AllBlockEntityTypes;
@ -76,7 +79,12 @@ public class SignalBlock extends Block implements IBE<SignalBlockEntity>, IWrenc
if (pLevel.isClientSide)
return;
boolean powered = pState.getValue(POWERED);
if (powered == pLevel.hasNeighborSignal(pPos))
Optional<SignalBlockEntity> ste = getBlockEntityOptional(pLevel, pPos);
boolean neighborPowered = false;
if (ste.isEmpty() || !ste.get().computerBehaviour.hasAttachedComputer()) {
powered = pLevel.hasNeighborSignal(pPos);
}
if (powered == neighborPowered)
return;
if (powered) {
pLevel.scheduleTick(pPos, this, 4);
@ -87,7 +95,8 @@ public class SignalBlock extends Block implements IBE<SignalBlockEntity>, IWrenc
@Override
public void tick(BlockState pState, ServerLevel pLevel, BlockPos pPos, RandomSource pRand) {
if (pState.getValue(POWERED) && !pLevel.hasNeighborSignal(pPos))
Optional<SignalBlockEntity> ste = getBlockEntityOptional(pLevel, pPos);
if ((ste.isEmpty() || !ste.get().computerBehaviour.hasAttachedComputer()) && pState.getValue(POWERED) && !pLevel.hasNeighborSignal(pPos))
pLevel.setBlock(pPos, pState.cycle(POWERED), 2);
}
@ -108,8 +117,13 @@ public class SignalBlock extends Block implements IBE<SignalBlockEntity>, IWrenc
if (level.isClientSide)
return InteractionResult.SUCCESS;
withBlockEntityDo(level, pos, ste -> {
SignalBoundary signal = ste.getSignal();
Player player = context.getPlayer();
if (ste.computerBehaviour.hasAttachedComputer()) {
if (player != null)
player.displayClientMessage(CreateLang.translateDirect("track_signal.mode_controlled_by_computer"), true);
return;
}
SignalBoundary signal = ste.getSignal();
if (signal != null) {
signal.cycleSignalType(pos);
if (player != null)

View file

@ -3,8 +3,12 @@ package com.simibubi.create.content.trains.signal;
import java.util.List;
import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import com.simibubi.create.api.contraption.transformable.TransformableBlockEntity;
import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour;
import com.simibubi.create.compat.computercraft.ComputerCraftProxy;
import com.simibubi.create.compat.computercraft.events.SignalStateChangeEvent;
import com.simibubi.create.content.contraptions.StructureTransform;
import com.simibubi.create.content.trains.graph.EdgePointType;
import com.simibubi.create.content.trains.signal.SignalBlock.SignalType;
@ -14,11 +18,14 @@ import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour
import net.createmod.catnip.nbt.NBTHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.LazyOptional;
public class SignalBlockEntity extends SmartBlockEntity implements TransformableBlockEntity {
@ -48,6 +55,7 @@ public class SignalBlockEntity extends SmartBlockEntity implements Transformable
private OverlayState overlay;
private int switchToRedAfterTrainEntered;
private boolean lastReportedPower;
public AbstractComputerBehaviour computerBehaviour;
public SignalBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
@ -86,6 +94,7 @@ public class SignalBlockEntity extends SmartBlockEntity implements Transformable
public void addBehaviours(List<BlockEntityBehaviour> behaviours) {
edgePoint = new TrackTargetingBehaviour<>(this, EdgePointType.SIGNAL);
behaviours.add(edgePoint);
behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this));
}
@Override
@ -152,9 +161,24 @@ public class SignalBlockEntity extends SmartBlockEntity implements Transformable
return;
this.state = state;
switchToRedAfterTrainEntered = state == SignalState.GREEN || state == SignalState.YELLOW ? 15 : 0;
if (computerBehaviour.hasAttachedComputer())
computerBehaviour.prepareComputerEvent(new SignalStateChangeEvent(state));
notifyUpdate();
}
@Override
public <T> @NotNull LazyOptional<T> getCapability(@NotNull Capability<T> cap, Direction side) {
if (computerBehaviour.isPeripheralCap(cap))
return computerBehaviour.getPeripheralCapability();
return super.getCapability(cap, side);
}
@Override
public void invalidateCaps() {
super.invalidateCaps();
computerBehaviour.removePeripheral();
}
@Override
protected AABB createRenderBoundingBox() {
return new AABB(worldPosition, edgePoint.getGlobalPosition()).inflate(2);

View file

@ -997,6 +997,7 @@
"create.track_signal.cannot_change_mode": "Unable to switch mode of this Signal",
"create.track_signal.mode_change.entry_signal": "-> Allow passage if section unoccupied",
"create.track_signal.mode_change.cross_signal": "-> Allow passage if section fully traversable",
"create.track_signal.mode_controlled_by_computer": "Signal mode is controlled by computer",
"create.contraption.controls.start_controlling": "Now controlling: %1$s",
"create.contraption.controls.stop_controlling": "Stopped controlling contraption",

View file

@ -2597,6 +2597,7 @@
"create.track_signal.cannot_change_mode": "No se puede cambiar el modo de esta señal",
"create.track_signal.mode_change.cross_signal": "-> Permitir entrada si la sección se puede atravesar por completo",
"create.track_signal.mode_change.entry_signal": "-> Permitir entrada si la sección está desocupada",
"create.track_signal.mode_controlled_by_computer": "El modo de esta señal está siendo controlado por un ordenador",
"create.track_target.clear": "Selección de vía borrada",
"create.track_target.invalid": "No se puede marcar esta vía como objetivo",
"create.track_target.missing": "Primero haz clic derecho en la vía objetivo",

View file

@ -2594,6 +2594,7 @@
"create.track_signal.cannot_change_mode": "No se puede cambiar el modo de esta señal",
"create.track_signal.mode_change.cross_signal": "-> Permitir entrada si la sección se puede atravesar por completo",
"create.track_signal.mode_change.entry_signal": "-> Permitir entrada si la sección está desocupada",
"create.track_signal.mode_controlled_by_computer": "El modo de esta señal está siendo controlado por un ordenador",
"create.track_target.clear": "Selección de vía borrada",
"create.track_target.invalid": "No se puede marcar esta vía como objetivo",
"create.track_target.missing": "Primero haz clic derecho en la vía objetivo",

View file

@ -2597,6 +2597,7 @@
"create.track_signal.cannot_change_mode": "Incapable de changer le mode de ce signal",
"create.track_signal.mode_change.cross_signal": "-> Autoriser le passage si la section est entièrement traversable",
"create.track_signal.mode_change.entry_signal": "-> Autoriser le passage si la section est inoccupée",
"create.track_signal.mode_controlled_by_computer": "Le mode de ce signal est contrôlé par un ordinateur",
"create.track_target.clear": "Sélection du rail effacée",
"create.track_target.invalid": "Ne peut pas cibler ce rail ici",
"create.track_target.missing": "Faites clic droit sur le rail ciblé d'abord",