diff --git a/gradle.properties b/gradle.properties index a59fa37..cfa4da6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ mod_name=Create: Maintenance Control # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=GPL-3.0 License # The mod version. See https://semver.org/ -mod_version=1.1.3-mc1.21.1 +mod_version=1.2.0-mc1.21.1 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/net/dantemc/create_maintenance_control/Config.java b/src/main/java/net/dantemc/create_maintenance_control/Config.java index 6e3a015..e6f22b7 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/Config.java +++ b/src/main/java/net/dantemc/create_maintenance_control/Config.java @@ -1,7 +1,5 @@ package net.dantemc.create_maintenance_control; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; import net.neoforged.neoforge.common.ModConfigSpec; public class Config { @@ -12,8 +10,4 @@ public class Config { .define("debugLogs", false); static final ModConfigSpec SPEC = BUILDER.build(); - - private static boolean validateItemName(final Object obj) { - return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName)); - } } diff --git a/src/main/java/net/dantemc/create_maintenance_control/CreateMaintenance.java b/src/main/java/net/dantemc/create_maintenance_control/CreateMaintenance.java index 57468ca..bdfdae3 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/CreateMaintenance.java +++ b/src/main/java/net/dantemc/create_maintenance_control/CreateMaintenance.java @@ -9,6 +9,7 @@ import net.dantemc.create_maintenance_control.foundation.IgnorePlacingRulesItem; import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceBoxBlock; import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceBoxBlockEntity; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.SoundType; import net.neoforged.bus.api.IEventBus; @@ -54,9 +55,15 @@ public CreateMaintenance(IEventBus modEventBus, ModContainer modContainer) { NeoForge.EVENT_BUS.register(this); + ModPackets.register(); + modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); } + public static ResourceLocation asResource(String path) { + return ResourceLocation.fromNamespaceAndPath(MODID, path); + } + //helper method for loggers public static void debug(String message, Object... args) { if (Config.WRITE_DEBUG_LOGS.get()) { diff --git a/src/main/java/net/dantemc/create_maintenance_control/ModPackets.java b/src/main/java/net/dantemc/create_maintenance_control/ModPackets.java new file mode 100644 index 0000000..44bb471 --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/ModPackets.java @@ -0,0 +1,42 @@ +package net.dantemc.create_maintenance_control; + +import java.util.Locale; + +import net.createmod.catnip.net.base.BasePacketPayload; +import net.createmod.catnip.net.base.CatnipPacketRegistry; +import net.dantemc.create_maintenance_control.content.maintenance_box.gui.MaintenanceBoxConfigurationPacket; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +public enum ModPackets implements BasePacketPayload.PacketTypeProvider { + CONFIGURE_MAINTENANCE_BOX( + MaintenanceBoxConfigurationPacket.class, + MaintenanceBoxConfigurationPacket.STREAM_CODEC + ); + + private final CatnipPacketRegistry.PacketType type; + + ModPackets(Class clazz, StreamCodec codec) { + String name = this.name().toLowerCase(Locale.ROOT); + this.type = new CatnipPacketRegistry.PacketType<>( + new CustomPacketPayload.Type<>(CreateMaintenance.asResource(name)), + clazz, codec + ); + } + + @Override + @SuppressWarnings("unchecked") + public CustomPacketPayload.Type getType() { + return (CustomPacketPayload.Type) this.type.type(); + } + + public static void register() { + CatnipPacketRegistry packetRegistry = + new CatnipPacketRegistry(CreateMaintenance.MODID, "1.0.0"); + for (ModPackets packet : ModPackets.values()) { + packetRegistry.registerPacket(packet.type); + } + packetRegistry.registerAllPackets(); + } +} \ No newline at end of file diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlock.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlock.java index e6a6ba6..ee0f432 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlock.java +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlock.java @@ -1,14 +1,18 @@ package net.dantemc.create_maintenance_control.content.maintenance_box; - import com.simibubi.create.content.trains.station.GlobalStation; import com.simibubi.create.foundation.block.IBE; import com.simibubi.create.foundation.block.WrenchableDirectionalBlock; +import net.createmod.catnip.gui.ScreenOpener; import net.dantemc.create_maintenance_control.CreateMaintenance; +import net.dantemc.create_maintenance_control.content.maintenance_box.gui.MaintenanceBoxScreen; import net.dantemc.create_maintenance_control.foundation.CreateMaintenanceShapes; import net.dantemc.create_maintenance_control.railway.OfflineStationManager; +import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; @@ -19,8 +23,11 @@ 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.BooleanProperty; +import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; public class MaintenanceBoxBlock extends WrenchableDirectionalBlock implements IBE { @@ -31,6 +38,14 @@ public MaintenanceBoxBlock(Properties properties) { registerDefaultState(defaultBlockState().setValue(POWERED, false)); } + private void refreshAttachedStationBox(Level level, BlockPos pos, boolean powered, boolean skipDownstream) { + GlobalStation station = StationUtils.findNearbyStation(level, pos); + if (station == null) + return; + + OfflineStationManager.refreshBox(level, pos, powered, skipDownstream); + } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { builder.add(POWERED); @@ -50,10 +65,7 @@ public void neighborChanged(BlockState state, Level level, BlockPos pos, Block b level.setBlock(pos, state.setValue(POWERED, powered), Block.UPDATE_ALL); - GlobalStation station = StationUtils.findNearbyStation(level, pos); - if (station == null) - return; - OfflineStationManager.registerBox(level, pos, station.name, !powered); + refreshAttachedStationBox(level, pos, powered, false); } @Override @@ -73,10 +85,7 @@ public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldS level.setBlock(pos, state, Block.UPDATE_ALL); } - GlobalStation station = StationUtils.findNearbyStation(level, pos); - if (station == null) - return; - OfflineStationManager.registerBox(level, pos, station.name, !powered); + refreshAttachedStationBox(level, pos, powered, false); } @Override @@ -94,6 +103,13 @@ public void onRemove(BlockState state, Level level, BlockPos pos, BlockState new OfflineStationManager.unregisterBox(level, pos); } + @OnlyIn(value = Dist.CLIENT) + protected void displayScreen(MaintenanceBoxBlockEntity be, Player player) { + if (!(player instanceof LocalPlayer)) + return; + ScreenOpener.open(new MaintenanceBoxScreen(be)); + } + @Override public Class getBlockEntityClass() { return MaintenanceBoxBlockEntity.class; @@ -115,6 +131,15 @@ public VoxelShape getShape(BlockState State, BlockGetter Level, BlockPos Pos, Co return CreateMaintenanceShapes.MAINTENANCE_BOX.get(State.getValue(FACING)); } + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + if (level.isClientSide) { + withBlockEntityDo(level, pos, be -> displayScreen(be, player)); + } + + return InteractionResult.SUCCESS; + } + @Override protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlockEntity.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlockEntity.java index ba9edc7..b1dad30 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlockEntity.java +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceBoxBlockEntity.java @@ -1,20 +1,39 @@ package net.dantemc.create_maintenance_control.content.maintenance_box; import com.simibubi.create.content.trains.station.GlobalStation; +import com.simibubi.create.foundation.blockEntity.SyncedBlockEntity; import net.dantemc.create_maintenance_control.CreateMaintenance; +import net.dantemc.create_maintenance_control.railway.MaintenanceEntry; import net.dantemc.create_maintenance_control.railway.OfflineStationManager; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -public class MaintenanceBoxBlockEntity extends BlockEntity { +public class MaintenanceBoxBlockEntity extends SyncedBlockEntity { public MaintenanceBoxBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); } + private String stationFilter = ""; + private MaintenanceRedstoneMode redstoneMode = MaintenanceRedstoneMode.UNPOWERED_ACTIVE; + private boolean skipDownstream = false; + + public String getStationFilter() { + return stationFilter; + } + + public MaintenanceRedstoneMode getRedstoneMode() { + return redstoneMode; + } + + public boolean shouldSkipDownstream() { + return skipDownstream; + } + public void registerStation() { Level level = getLevel(); @@ -22,13 +41,38 @@ public void registerStation() { return; GlobalStation station = StationUtils.findNearbyStation(level, getBlockPos()); - if (station == null) return; boolean powered = getBlockState().getValue(MaintenanceBoxBlock.POWERED); - OfflineStationManager.registerBox(level, getBlockPos(), station.name, !powered); + MaintenanceEntry entry = OfflineStationManager.ensureBoxEntry(level, getBlockPos(), station.name); + + this.stationFilter = entry.stationFilter(); + this.redstoneMode = entry.redstoneMode(); + + OfflineStationManager.refreshBox(level, getBlockPos(), powered, skipDownstream); + + setChanged(); + notifyUpdate(); + } + + public void applySettings(String stationFilter, MaintenanceRedstoneMode redstoneMode, boolean skipDownstream) { + Level level = getLevel(); + if (level == null || level.isClientSide) + return; + + this.stationFilter = stationFilter; + this.redstoneMode = redstoneMode; + this.skipDownstream = skipDownstream; + + OfflineStationManager.updateBoxSettings(level, getBlockPos(), stationFilter, redstoneMode, skipDownstream); + + boolean powered = getBlockState().getValue(MaintenanceBoxBlock.POWERED); + OfflineStationManager.refreshBox(level, getBlockPos(), powered, skipDownstream); + + setChanged(); + notifyUpdate(); } @Override @@ -42,10 +86,32 @@ public void onLoad() { CreateMaintenance.debug("Scheduling station registration"); - level.scheduleTick( - getBlockPos(), - getBlockState().getBlock(), - 100 - ); + level.scheduleTick(getBlockPos(), getBlockState().getBlock(), 100); + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + + tag.putString("StationFilter", stationFilter); + tag.putString("RedstoneMode", redstoneMode.name()); + tag.putBoolean("SkipDownstream", skipDownstream); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + + stationFilter = tag.getString("StationFilter"); + + try { + redstoneMode = MaintenanceRedstoneMode.valueOf(tag.getString("RedstoneMode")); + } catch (IllegalArgumentException e) { + redstoneMode = MaintenanceRedstoneMode.UNPOWERED_ACTIVE; + } + + if (tag.contains("SkipDownstream")) { + skipDownstream = tag.getBoolean("SkipDownstream"); + } } } diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceRedstoneMode.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceRedstoneMode.java new file mode 100644 index 0000000..0da280a --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/MaintenanceRedstoneMode.java @@ -0,0 +1,18 @@ +package net.dantemc.create_maintenance_control.content.maintenance_box; + +public enum MaintenanceRedstoneMode { + UNPOWERED_ACTIVE { + @Override + public boolean isMaintenanceActive(boolean powered) { + return !powered; + } + }, + POWERED_ACTIVE { + @Override + public boolean isMaintenanceActive(boolean powered) { + return powered; + } + }; + + public abstract boolean isMaintenanceActive(boolean powered); +} diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/StationUtils.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/StationUtils.java index 32622ff..eb466be 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/StationUtils.java +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/StationUtils.java @@ -19,4 +19,4 @@ public static GlobalStation findNearbyStation(Level level, BlockPos pos) { } return null; } -} +} \ No newline at end of file diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxConfigurationPacket.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxConfigurationPacket.java new file mode 100644 index 0000000..42db196 --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxConfigurationPacket.java @@ -0,0 +1,44 @@ +package net.dantemc.create_maintenance_control.content.maintenance_box.gui; + +import com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket; +import io.netty.buffer.ByteBuf; +import net.dantemc.create_maintenance_control.ModPackets; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceBoxBlockEntity; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceRedstoneMode; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; + +public class MaintenanceBoxConfigurationPacket extends BlockEntityConfigurationPacket { + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, packet -> packet.pos, + ByteBufCodecs.STRING_UTF8, packet -> packet.stationFilter, + ByteBufCodecs.VAR_INT, packet -> packet.redstoneMode.ordinal(), + ByteBufCodecs.BOOL, packet -> packet.skipDownstream, + (pos, stationFilter, redstoneModeOrdinal, skipDownstream) -> + new MaintenanceBoxConfigurationPacket(pos, stationFilter, MaintenanceRedstoneMode.values()[redstoneModeOrdinal], skipDownstream) + ); + + private final String stationFilter; + private final MaintenanceRedstoneMode redstoneMode; + private final boolean skipDownstream; + + public MaintenanceBoxConfigurationPacket(BlockPos pos, String stationFilter, MaintenanceRedstoneMode redstoneMode, boolean skipDownstream) { + super(pos); + this.stationFilter = stationFilter; + this.redstoneMode = redstoneMode; + this.skipDownstream = skipDownstream; + } + + @Override + protected void applySettings(ServerPlayer player, MaintenanceBoxBlockEntity be) { + be.applySettings(stationFilter, redstoneMode, skipDownstream); + } + + @Override + public PacketTypeProvider getTypeProvider() { + return ModPackets.CONFIGURE_MAINTENANCE_BOX; + } +} diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxGuiTexture.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxGuiTexture.java new file mode 100644 index 0000000..454f23c --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxGuiTexture.java @@ -0,0 +1,66 @@ +package net.dantemc.create_maintenance_control.content.maintenance_box.gui; + +import com.simibubi.create.Create; +import net.createmod.catnip.gui.TextureSheetSegment; +import net.createmod.catnip.gui.element.ScreenElement; +import net.dantemc.create_maintenance_control.CreateMaintenance; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +public enum MaintenanceBoxGuiTexture implements ScreenElement, TextureSheetSegment { + MAINTENANCE_BOX_INTERFACE("maintenance_box", 235, 162); + + public final ResourceLocation location; + private final int width; + private final int height; + private final int startX; + private final int startY; + + MaintenanceBoxGuiTexture(String location, int width, int height) { + this(location, 0, 0, width, height); + } + + MaintenanceBoxGuiTexture(String location, int startX, int startY, int width, int height) { + this(CreateMaintenance.MODID, location, startX, startY, width, height); + } + + MaintenanceBoxGuiTexture(String namespace, String location, int startX, int startY, int width, int height) { + this.location = ResourceLocation.fromNamespaceAndPath(namespace, "textures/gui/" + location + ".png"); + this.width = width; + this.height = height; + this.startX = startX; + this.startY = startY; + } + + @Override + public ResourceLocation getLocation() { + return location; + } + + @OnlyIn(Dist.CLIENT) + public void render(GuiGraphics graphics, int x, int y) { + graphics.blit(location, x, y, startX, startY, width, height); + } + + @Override + public int getStartX() { + return startX; + } + + @Override + public int getStartY() { + return startY; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } +} diff --git a/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxScreen.java b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxScreen.java new file mode 100644 index 0000000..9772d82 --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/content/maintenance_box/gui/MaintenanceBoxScreen.java @@ -0,0 +1,182 @@ +package net.dantemc.create_maintenance_control.content.maintenance_box.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.AllItems; +import com.simibubi.create.Create; +import com.simibubi.create.content.redstone.displayLink.DisplayLinkBlock; +import com.simibubi.create.foundation.gui.AllIcons; +import com.simibubi.create.foundation.gui.ModularGuiLine; +import com.simibubi.create.foundation.gui.ModularGuiLineBuilder; +import com.simibubi.create.foundation.gui.widget.IconButton; +import com.simibubi.create.foundation.gui.widget.Label; +import com.simibubi.create.foundation.gui.widget.SelectionScrollInput; +import com.simibubi.create.foundation.utility.CreateLang; +import dev.engine_room.flywheel.lib.transform.TransformStack; +import net.createmod.catnip.animation.Force; +import net.createmod.catnip.animation.PhysicalFloat; +import net.createmod.catnip.gui.AbstractSimiScreen; +import net.createmod.catnip.gui.element.GuiGameElement; +import net.createmod.catnip.platform.CatnipServices; +import net.dantemc.create_maintenance_control.CreateMaintenance; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceBoxBlockEntity; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceRedstoneMode; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.util.List; + +import static net.createmod.catnip.config.ui.ConfigScreen.shadowState; + +public class MaintenanceBoxScreen extends AbstractSimiScreen { + + private final MaintenanceBoxGuiTexture background = MaintenanceBoxGuiTexture.MAINTENANCE_BOX_INTERFACE; + private final MaintenanceBoxBlockEntity blockEntity; + + private IconButton confirmButton; + + private ModularGuiLine stationFilterLine; + + private SelectionScrollInput redstoneModeSelector; + private Label redstoneModeLabel; + + private SelectionScrollInput skipDownstreamSelector; + private Label skipDownstreamLabel; + + private CompoundTag configData; + + public MaintenanceBoxScreen(MaintenanceBoxBlockEntity blockEntity) { + this.blockEntity = blockEntity; + } + + @Override + protected void init() { + setWindowSize(background.getWidth(), background.getHeight()); + super.init(); + + int x = guiLeft; + int y = guiTop; + + configData = new CompoundTag(); + configData.putString("StationFilter", blockEntity.getStationFilter()); + + // station filter textbox + stationFilterLine = new ModularGuiLine(); + new ModularGuiLineBuilder(font, stationFilterLine, x + 24, y + 60) + .addTextInput(0, 160, (editBox, tooltip) -> { + editBox.setMaxLength(64); + tooltip.withTooltip(ImmutableList.of(Component.translatable("gui.maintenance_box.station_filter.tooltip") + .withStyle(s -> s.withColor(0x5391E1)), + CreateLang.translateDirect("gui.schedule.lmb_edit") + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC))); + }, "StationFilter"); + + // redstone mode selector + redstoneModeLabel = new Label(x + 29, y + 85, Component.empty()).withShadow(); + + redstoneModeSelector = (SelectionScrollInput) new SelectionScrollInput(x + 24, y + 85, 160, 18) + .forOptions(List.of( + Component.translatable("gui.maintenance_box.redstone_mode.default"), + Component.translatable("gui.maintenance_box.redstone_mode.inverted") + )) + .writingTo(redstoneModeLabel) + .titled(Component.translatable("gui.maintenance_box.redstone_mode.tooltip")) + .setState(blockEntity.getRedstoneMode().ordinal()); + + addRenderableWidget(redstoneModeSelector); + addRenderableWidget(redstoneModeLabel); + + // Skip downstream stations selector + skipDownstreamLabel = new Label(x + 29, y + 110, Component.empty()).withShadow(); + + skipDownstreamSelector = (SelectionScrollInput) new SelectionScrollInput(x + 24, y + 110, 160, 18) + .forOptions(List.of( + Component.translatable("gui.maintenance_box.skip_downstream_stations.false"), + Component.translatable("gui.maintenance_box.skip_downstream_stations.true") + )) + .writingTo(skipDownstreamLabel) + .titled(Component.translatable("gui.maintenance_box.skip_downstream_stations.tooltip")) + .addHint(Component.translatable("gui.maintenance_box.skip_downstream_stations.explanation")) + .setState(blockEntity.shouldSkipDownstream() ? 1 : 0); + + addRenderableWidget(skipDownstreamSelector); + addRenderableWidget(skipDownstreamLabel); + + // push initial value from configData into the widget + stationFilterLine.loadValues(configData, this::addRenderableWidget, this::addRenderableOnly); + + // Confirm button + confirmButton = new IconButton(x + background.getWidth() - 33, y + background.getHeight() - 24, AllIcons.I_CONFIRM); + confirmButton.withCallback(this::onConfirm); + addRenderableWidget(confirmButton); + } + + public static final PhysicalFloat cogSpin = PhysicalFloat.create().withLimit(10f).withDrag(0.3).addForce(new Force.Static(.2f)); + + @Override + protected void renderWindow(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + background.render(graphics, guiLeft, guiTop); + + int x = guiLeft; + int y = guiTop; + + MutableComponent header = Component.translatable("gui.maintenance_box.title"); + graphics.drawString(font, header, (x + background.getWidth() / 2 - font.width(header) / 2) - 2, y + 4, 0x592424, false); + + //cog + partialTicks = Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(false); + PoseStack poseStack = graphics.pose(); + poseStack.pushPose(); + + poseStack.translate(x + background.getWidth() / 2f - 18, y + 50, 0); + poseStack.scale(28, 28, 1); + GuiGameElement.of(shadowState) + .rotateBlock(22.5, cogSpin.getValue(partialTicks), 22.5) + .render(graphics); + + poseStack.popPose(); + + //block model + PoseStack ms2 = graphics.pose(); + ms2.pushPose(); + TransformStack.of(ms2) + .pushPose() + .translate(x + background.getWidth() + 4, y + background.getHeight() + 4, 100) + .scale(40) + .rotateXDegrees(-22) + .rotateYDegrees(63); + GuiGameElement.of(blockEntity.getBlockState() + .setValue(DisplayLinkBlock.FACING, Direction.UP)) + .render(graphics); + ms2.popPose(); + } + + @Override + public void tick() { + cogSpin.tick(); + } + + private void onConfirm() { + + CompoundTag tag = new CompoundTag(); + stationFilterLine.saveValues(tag); + + String stationFilter = tag.getString("StationFilter"); + MaintenanceRedstoneMode redstoneMode = MaintenanceRedstoneMode.values()[redstoneModeSelector.getState()]; + boolean skipDownstream = skipDownstreamSelector.getState() == 1; + + CatnipServices.NETWORK.sendToServer(new MaintenanceBoxConfigurationPacket(blockEntity.getBlockPos(), stationFilter, + redstoneMode, skipDownstream)); + + CreateMaintenance.debug("Station filter: " + stationFilter); + CreateMaintenance.debug("Redstone mode: " + redstoneMode); + CreateMaintenance.debug("Skip downstream: " + skipDownstream); + + onClose(); + } +} \ No newline at end of file diff --git a/src/main/java/net/dantemc/create_maintenance_control/mixin/DestinationInstructionMixin.java b/src/main/java/net/dantemc/create_maintenance_control/mixin/DestinationInstructionMixin.java index e41296f..2c3fcaa 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/mixin/DestinationInstructionMixin.java +++ b/src/main/java/net/dantemc/create_maintenance_control/mixin/DestinationInstructionMixin.java @@ -6,11 +6,13 @@ import com.simibubi.create.content.trains.station.GlobalStation; import net.dantemc.create_maintenance_control.CreateMaintenance; -import net.dantemc.create_maintenance_control.content.maintenance_box.StationUtils; +import net.dantemc.create_maintenance_control.railway.MaintenanceEntry; +import net.dantemc.create_maintenance_control.railway.MaintenanceSkipState; import net.dantemc.create_maintenance_control.railway.OfflineStationManager; import net.minecraft.world.level.Level; import com.simibubi.create.content.trains.graph.EdgePointType; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -62,38 +64,111 @@ public abstract class DestinationInstructionMixin { CallbackInfoReturnable cir) { String regex = getFilterForRegex(); + MaintenanceSkipState state = maintenance$analyzeMatchingStations(runtime, level, regex); - boolean foundMatch = false; - boolean allOffline = true; + if (!state.anyMatch()) + return; + + if (!state.allSkipped()) + return; + + int scheduleSize = runtime.schedule.entries.size(); + + CreateMaintenance.debug("All matching stations for '{}' are under maintenance", getFilter()); + + // skip 1 entry (default behavior) + if (!state.skipDownstream()) { + runtime.currentEntry = (runtime.currentEntry + 1) % scheduleSize; + cir.setReturnValue(null); + cir.cancel(); + return; + } + + // skipDownstream = true: + // search for next destination entry that train can pathfind to + int nextReachableEntry = maintenance$findNextReachableDestinationEntry(runtime, level); + + if (nextReachableEntry != -1) { + CreateMaintenance.debug("Skipping downstream to schedule entry {}", nextReachableEntry); + runtime.currentEntry = nextReachableEntry; + } else { + // fallback: if no stations work, only skip one + runtime.currentEntry = (runtime.currentEntry + 1) % scheduleSize; + } + + cir.setReturnValue(null); + cir.cancel(); + } + + @Unique + private MaintenanceSkipState maintenance$analyzeMatchingStations(ScheduleRuntime runtime, Level level, String regex) { + boolean anyMatch = false; + boolean allSkipped = true; + boolean skipDownstream = false; for (GlobalStation station : runtime.train.graph.getPoints(EdgePointType.STATION)) { if (!station.name.matches(regex)) continue; - foundMatch = true; + anyMatch = true; - if (!OfflineStationManager.isStationSkipped(level, station)) { - allOffline = false; - break; + MaintenanceEntry entry = OfflineStationManager.getMatchingEntry(level, station); + if (entry == null) { + allSkipped = false; + continue; + } + + if (entry.skipDownstream()) { + skipDownstream = true; } } - if (!foundMatch || !allOffline) { - return; + return new MaintenanceSkipState(anyMatch, allSkipped, skipDownstream); + } + + @Unique + private ArrayList maintenance$getValidStations(ScheduleRuntime runtime, Level level, String regex) { + ArrayList validStations = new ArrayList<>(); + + for (GlobalStation station : runtime.train.graph.getPoints(EdgePointType.STATION)) { + if (!station.name.matches(regex)) + continue; + + if (OfflineStationManager.isStationSkipped(level, station)) + continue; + + validStations.add(station); } - CreateMaintenance.debug( - "Skipping destination entry {} with filter '{}' for {} because matching stations are under maintenance", - runtime.currentEntry, - getFilterForRegex(), - runtime.train - ); + return validStations; + } + + @Unique + private boolean maintenance$destinationEntryHasPath(ScheduleRuntime runtime, Level level, DestinationInstruction instruction) { + ArrayList validStations = maintenance$getValidStations(runtime, level, instruction.getFilterForRegex()); - runtime.currentEntry = - (runtime.currentEntry + 1) - % runtime.schedule.entries.size(); + if (validStations.isEmpty()) + return false; - cir.setReturnValue(null); - cir.cancel(); + DiscoveredPath path = runtime.train.navigation.findPathTo(validStations, Double.MAX_VALUE); + return path != null; + } + + @Unique + private int maintenance$findNextReachableDestinationEntry(ScheduleRuntime runtime, Level level) { + int scheduleSize = runtime.schedule.entries.size(); + int originalEntry = runtime.currentEntry; + + for (int offset = 1; offset < scheduleSize; offset++) { + int candidateIndex = (originalEntry + offset) % scheduleSize; + + var scheduleEntry = runtime.schedule.entries.get(candidateIndex); + if (scheduleEntry.instruction instanceof DestinationInstruction destinationInstruction) { + if (maintenance$destinationEntryHasPath(runtime, level, destinationInstruction)) { + return candidateIndex; + } + } + } + return -1; } } \ No newline at end of file diff --git a/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceEntry.java b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceEntry.java index 6487eb6..cb4bb32 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceEntry.java +++ b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceEntry.java @@ -1,4 +1,6 @@ package net.dantemc.create_maintenance_control.railway; -public record MaintenanceEntry(String stationFilter, boolean shouldSkip) { +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceRedstoneMode; + +public record MaintenanceEntry(String stationFilter, boolean shouldSkip, MaintenanceRedstoneMode redstoneMode, boolean skipDownstream) { } \ No newline at end of file diff --git a/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSavedData.java b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSavedData.java index 607e361..2e24b17 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSavedData.java +++ b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSavedData.java @@ -1,5 +1,6 @@ package net.dantemc.create_maintenance_control.railway; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceRedstoneMode; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -57,8 +58,17 @@ public static MaintenanceSavedData load(CompoundTag tag, HolderLookup.Provider r String stationFilter = entryTag.getString("StationFilter"); boolean shouldSkip = entryTag.getBoolean("ShouldSkip"); + MaintenanceRedstoneMode redstoneMode; - MaintenanceEntry entry = new MaintenanceEntry(stationFilter, shouldSkip); + try { + redstoneMode = MaintenanceRedstoneMode.valueOf(entryTag.getString("RedstoneMode")); + } catch (IllegalArgumentException e) { + redstoneMode = MaintenanceRedstoneMode.UNPOWERED_ACTIVE; + } + + boolean skipDownstream = entryTag.getBoolean("SkipDownstream"); + + MaintenanceEntry entry = new MaintenanceEntry(stationFilter, shouldSkip, redstoneMode, skipDownstream); data.entries.put(boxPos, entry); } return data; @@ -79,6 +89,9 @@ public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { entryTag.putString("StationFilter", entry.stationFilter()); entryTag.putBoolean("ShouldSkip", entry.shouldSkip()); + entryTag.putString("RedstoneMode", entry.redstoneMode().name()); + entryTag.putBoolean("SkipDownstream", entry.skipDownstream()); + entryList.add(entryTag); } diff --git a/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSkipState.java b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSkipState.java new file mode 100644 index 0000000..75b725b --- /dev/null +++ b/src/main/java/net/dantemc/create_maintenance_control/railway/MaintenanceSkipState.java @@ -0,0 +1,4 @@ +package net.dantemc.create_maintenance_control.railway; + +public record MaintenanceSkipState(boolean anyMatch, boolean allSkipped, boolean skipDownstream) { +} diff --git a/src/main/java/net/dantemc/create_maintenance_control/railway/OfflineStationManager.java b/src/main/java/net/dantemc/create_maintenance_control/railway/OfflineStationManager.java index a0b679c..6415d4a 100644 --- a/src/main/java/net/dantemc/create_maintenance_control/railway/OfflineStationManager.java +++ b/src/main/java/net/dantemc/create_maintenance_control/railway/OfflineStationManager.java @@ -1,11 +1,12 @@ package net.dantemc.create_maintenance_control.railway; import com.simibubi.create.content.trains.station.GlobalStation; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceBoxBlock; +import net.dantemc.create_maintenance_control.content.maintenance_box.MaintenanceRedstoneMode; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; -import java.util.Map; import java.util.Objects; @@ -20,31 +21,77 @@ private static MaintenanceSavedData getData(Level level) { return overworld.getDataStorage().computeIfAbsent(MaintenanceSavedData.factory(), MaintenanceSavedData.dataName()); } - public static void registerBox(Level level, BlockPos boxPos, String stationFilter, boolean shouldSkip) { - MaintenanceEntry entry = new MaintenanceEntry(stationFilter, shouldSkip); + public static MaintenanceEntry getEntry(Level level, BlockPos boxPos) { + return getData(level).getEntries().get(boxPos); + } + public static void setEntry(Level level, BlockPos boxPos, MaintenanceEntry entry) { getData(level).setEntry(boxPos, entry); } - public static void unregisterBox(Level level, BlockPos boxPos) { - getData(level).removeEntry(boxPos); + public static MaintenanceEntry ensureBoxEntry(Level level, BlockPos boxPos, String defaultStationFilter) { + MaintenanceEntry entry = getEntry(level, boxPos); + + if (entry == null) { + entry = new MaintenanceEntry( + defaultStationFilter, + true, // shouldSkip is recalculated immediately by refreshBox so value here doesn't really matter + MaintenanceRedstoneMode.UNPOWERED_ACTIVE, + false + ); + setEntry(level, boxPos, entry); + } + + return entry; } - public static boolean isStationSkipped(Level level, GlobalStation station) { + public static void refreshBox(Level level, BlockPos boxPos, boolean powered, boolean skipDownstream) { + MaintenanceEntry entry = getEntry(level, boxPos); + if (entry == null) + return; - String stationName = station.name; + boolean shouldSkip = entry.redstoneMode().isMaintenanceActive(powered); + + MaintenanceEntry updated = new MaintenanceEntry( + entry.stationFilter(), + shouldSkip, + entry.redstoneMode(), + entry.skipDownstream() + ); - Map allData = getData(level).getEntries(); + setEntry(level, boxPos, updated); + } + + public static void updateBoxSettings(Level level, BlockPos boxPos, String stationFilter, MaintenanceRedstoneMode redstoneMode, boolean skipDownstream) { + MaintenanceEntry current = getEntry(level, boxPos); + if (current == null) + return; - for (BlockPos boxPos : allData.keySet()) { - MaintenanceEntry entry = allData.get(boxPos); + boolean powered = level.getBlockState(boxPos).getValue(MaintenanceBoxBlock.POWERED); + boolean shouldSkip = redstoneMode.isMaintenanceActive(powered); - if (entry.shouldSkip()) { + MaintenanceEntry updated = new MaintenanceEntry(stationFilter, shouldSkip, redstoneMode, skipDownstream); - if (Objects.equals(entry.stationFilter(), stationName)) { - return true; - } - } + setEntry(level, boxPos, updated); + } + + public static void unregisterBox(Level level, BlockPos boxPos) { + getData(level).removeEntry(boxPos); + } + + public static MaintenanceEntry getMatchingEntry(Level level, GlobalStation station) { + String stationName = station.name; + + for (MaintenanceEntry entry : getData(level).getEntries().values()) { + if (!entry.shouldSkip()) + continue; + + if (Objects.equals(entry.stationFilter(), stationName)) + return entry; } - return false; + return null; + } + + public static boolean isStationSkipped(Level level, GlobalStation station) { + return getMatchingEntry(level, station) != null; } } diff --git a/src/main/resources/assets/create_maintenance_control/lang/en_us.json b/src/main/resources/assets/create_maintenance_control/lang/en_us.json index 5018f4d..ac1231b 100644 --- a/src/main/resources/assets/create_maintenance_control/lang/en_us.json +++ b/src/main/resources/assets/create_maintenance_control/lang/en_us.json @@ -8,6 +8,18 @@ "block.create_maintenance_control.maintenance_box.tooltip.behaviour1": "Trains with this station _in their schedules_ will _skip it_ while maintenance is active.", "block.create_maintenance_control.maintenance_box.tooltip.behaviour2": "When powered, maintenance is _temporarily disabled_ and trains will _stop at the station_ as usual.", + "gui.maintenance_box.title": "Maintenance Box", + "gui.maintenance_box.station_filter.tooltip": "Station name filter", + + "gui.maintenance_box.redstone_mode.tooltip": "Redstone mode", + "gui.maintenance_box.redstone_mode.default": "Unpowered = Maintenance", + "gui.maintenance_box.redstone_mode.inverted": "Powered = Maintenance", + + "gui.maintenance_box.skip_downstream_stations.tooltip": "Skip downstream stations", + "gui.maintenance_box.skip_downstream_stations.explanation": "Note: When enabled, trains may skip downstream schedule entries until a reachable station is found.", + "gui.maintenance_box.skip_downstream_stations.false": "Off", + "gui.maintenance_box.skip_downstream_stations.true": "On", + "create_maintenance_control.configuration.title": "Create: Maintenance Control Configs", "create_maintenance_control.configuration.section.create_maintenance_control.common.toml": "Create: Maintenance Control Configs", "create_maintenance_control.configuration.section.create_maintenance_control.common.toml.title": "Create: Maintenance Control Configs", diff --git a/src/main/resources/assets/create_maintenance_control/lang/sv_se.json b/src/main/resources/assets/create_maintenance_control/lang/sv_se.json index 28a5f1e..8c25e57 100644 --- a/src/main/resources/assets/create_maintenance_control/lang/sv_se.json +++ b/src/main/resources/assets/create_maintenance_control/lang/sv_se.json @@ -8,8 +8,20 @@ "block.create_maintenance_control.maintenance_box.tooltip.behaviour1": "Tåg som har denna station _i sina scheman_ kommer att _hoppa över den_ medan underhållet pågår.", "block.create_maintenance_control.maintenance_box.tooltip.behaviour2": "När blocket _får en redstonesignal_ kommer tåg att _stanna vid stationen_ som vanligt.", + "gui.maintenance_box.title": "Underhållsblock", + "gui.maintenance_box.station_filter.tooltip": "Stationsfilter", + + "gui.maintenance_box.redstone_mode.tooltip": "Redstone-läge", + "gui.maintenance_box.redstone_mode.default": "Odriven = Underhåll", + "gui.maintenance_box.redstone_mode.inverted": "Driven = Underhåll", + + "gui.maintenance_box.skip_downstream_stations.tooltip": "Hoppa över efterföljande stationer", + "gui.maintenance_box.skip_downstream_stations.explanation": "Obs: När detta är aktiverat kan tåg hoppa över efterföljande schemaposter tills en nåbar station hittas.", + "gui.maintenance_box.skip_downstream_stations.false": "Av", + "gui.maintenance_box.skip_downstream_stations.true": "På", + "create_maintenance_control.configuration.title": "Create: Maintenance Control-konfigurationer", "create_maintenance_control.configuration.section.create_maintenance_control.common.toml": "Create: Maintenance Control-konfigurationer", "create_maintenance_control.configuration.section.create_maintenance_control.common.toml.title": "Create: Maintenance Control-konfigurationer", "create_maintenance_control.configuration.debugLogs": "Aktivera debug-loggar" -} +} \ No newline at end of file diff --git a/src/main/resources/assets/create_maintenance_control/textures/gui/maintenance_box.png b/src/main/resources/assets/create_maintenance_control/textures/gui/maintenance_box.png new file mode 100644 index 0000000..b1af8fb Binary files /dev/null and b/src/main/resources/assets/create_maintenance_control/textures/gui/maintenance_box.png differ