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 96a325c03b
commit cbf886e69b
Failed to generate hash of commit
15 changed files with 210 additions and 9 deletions

View file

@ -582,8 +582,8 @@ bf2b0310500213ff853c748c236eb5d01f61658e assets/create/blockstates/yellow_toolbo
5616dda664dd106d576848124fc0fc1de18d0fd3 assets/create/blockstates/yellow_valve_handle.json
7f39521b211441f5c3e06d60c5978cebe16cacfb assets/create/blockstates/zinc_block.json
b7181bcd8182b2f17088e5aa881f374c9c65470c assets/create/blockstates/zinc_ore.json
3c04d30f0521554ade532f1662cdaf4ae2958ab3 assets/create/lang/en_ud.json
6a0df005c7594667d9c7582a877984d25382df7b assets/create/lang/en_us.json
82ed4a747f58fc0435f52fff39b7ea1579147d04 assets/create/lang/en_ud.json
f8a82ca4b8aee16b3ba754016e1994766251fc56 assets/create/lang/en_us.json
487a511a01b2a4531fb672f917922312db78f958 assets/create/models/block/acacia_window.json
b48060cba1a382f373a05bf0039054053eccf076 assets/create/models/block/acacia_window_pane_noside.json
3066db1bf03cffa1a9c7fbacf47ae586632f4eb3 assets/create/models/block/acacia_window_pane_noside_alt.json

View file

@ -2606,6 +2606,7 @@
"create.track_signal.cannot_change_mode": "\u05DF\u0250ub\u0131S s\u0131\u0265\u0287 \u025Fo \u01DDpo\u026F \u0265\u0254\u0287\u0131\u028Ds o\u0287 \u01DD\u05DFq\u0250u\u2229",
"create.track_signal.mode_change.cross_signal": "\u01DD\u05DFq\u0250s\u0279\u01DD\u028C\u0250\u0279\u0287 \u028E\u05DF\u05DFn\u025F uo\u0131\u0287\u0254\u01DDs \u025F\u0131 \u01DDb\u0250ss\u0250d \u028Do\u05DF\u05DF\u2C6F >-",
"create.track_signal.mode_change.entry_signal": "p\u01DD\u0131dn\u0254\u0254oun uo\u0131\u0287\u0254\u01DDs \u025F\u0131 \u01DDb\u0250ss\u0250d \u028Do\u05DF\u05DF\u2C6F >-",
"create.track_signal.mode_controlled_by_computer": "\u0279\u01DD\u0287nd\u026Fo\u0254 \u028Eq p\u01DD\u05DF\u05DFo\u0279\u0287uo\u0254 s\u0131 \u01DDpo\u026F \u05DF\u0250ub\u0131S",
"create.track_target.clear": "uo\u0131\u0287\u0254\u01DD\u05DF\u01DDs \u029E\u0254\u0250\u0279\u0287 p\u01DD\u0279\u0250\u01DD\u05DF\u0186",
"create.track_target.invalid": "\u01DD\u0279\u01DD\u0265 \u029E\u0254\u0250\u0279\u0287 s\u0131\u0265\u0287 \u0287\u01DDb\u0279\u0250\u0287 \u0287ouu\u0250\u0186",
"create.track_target.missing": "\u0287s\u0279\u0131\u025F \u029E\u0254\u0250\u0279\u0287 u\u0131\u0250\u0279\u0287 p\u01DD\u0287\u01DDb\u0279\u0250\u0287 \u01DD\u0265\u0287 \u029E\u0254\u0131\u05DF\u0254-\u0287\u0265b\u0131\u1D1A",

View file

@ -2606,6 +2606,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;
@ -29,15 +36,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)
@ -46,6 +53,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)
@ -75,4 +84,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 com.simibubi.create.foundation.utility.Iterate;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
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

@ -63,4 +63,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,6 @@
package com.simibubi.create.content.trains.signal;
import java.util.Optional;
import java.util.Random;
import javax.annotation.Nullable;
@ -76,7 +77,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 +93,8 @@ public class SignalBlock extends Block implements IBE<SignalBlockEntity>, IWrenc
@Override
public void tick(BlockState pState, ServerLevel pLevel, BlockPos pPos, Random 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 +115,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(Lang.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,7 +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.compat.computercraft.AbstractComputerBehaviour;
import com.simibubi.create.compat.computercraft.ComputerCraftProxy;
import com.simibubi.create.compat.computercraft.events.SignalStateChangeEvent;
import com.simibubi.create.content.contraptions.ITransformableBlockEntity;
import com.simibubi.create.content.contraptions.StructureTransform;
import com.simibubi.create.content.trains.graph.EdgePointType;
@ -14,10 +19,13 @@ import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour
import com.simibubi.create.foundation.utility.NBTHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
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 ITransformableBlockEntity {
@ -47,6 +55,7 @@ public class SignalBlockEntity extends SmartBlockEntity implements ITransformabl
private OverlayState overlay;
private int switchToRedAfterTrainEntered;
private boolean lastReportedPower;
public AbstractComputerBehaviour computerBehaviour;
public SignalBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
@ -85,6 +94,7 @@ public class SignalBlockEntity extends SmartBlockEntity implements ITransformabl
public void addBehaviours(List<BlockEntityBehaviour> behaviours) {
edgePoint = new TrackTargetingBehaviour<>(this, EdgePointType.SIGNAL);
behaviours.add(edgePoint);
behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this));
}
@Override
@ -151,9 +161,24 @@ public class SignalBlockEntity extends SmartBlockEntity implements ITransformabl
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

@ -863,6 +863,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

@ -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

@ -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

@ -2594,6 +2594,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",