diff --git a/PhyConDebugLogging.xml b/PhyConDebugLogging.xml new file mode 100644 index 0000000..62f2153 --- /dev/null +++ b/PhyConDebugLogging.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build.gradle b/build.gradle index 0561ae3..dbb768d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id "fabric-loom" version "0.5-SNAPSHOT" + id "fabric-loom" version "0.6.49" id "maven-publish" id "org.jetbrains.kotlin.jvm" version "1.4.30" id "com.github.johnrengelman.shadow" version "4.0.4" @@ -13,6 +13,7 @@ version = project.mod_version group = project.maven_group minecraft { + log4jConfigs.from "PhyConDebugLogging.xml" } repositories { diff --git a/src/main/java/net/shadowfacts/phycon/api/util/IPAddress.java b/src/main/java/net/shadowfacts/phycon/api/util/IPAddress.java index 737b239..e15ed87 100644 --- a/src/main/java/net/shadowfacts/phycon/api/util/IPAddress.java +++ b/src/main/java/net/shadowfacts/phycon/api/util/IPAddress.java @@ -1,8 +1,11 @@ package net.shadowfacts.phycon.api.util; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author shadowfacts @@ -25,6 +28,20 @@ public final class IPAddress { return random(ipAddressRandom); } + private static final Pattern IP_PATTERN = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$"); + @Nullable + public static IPAddress parse(String s) { + Matcher matcher = IP_PATTERN.matcher(s); + if (!matcher.matches()) { + return null; + } + int a = Integer.parseInt(matcher.group(1)); + int b = Integer.parseInt(matcher.group(2)); + int c = Integer.parseInt(matcher.group(3)); + int d = Integer.parseInt(matcher.group(4)); + return new IPAddress(a, b, c, d); + } + public static final IPAddress BROADCAST = new IPAddress(0xff_ff_ff_ff); public final int address; diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt index 1a1459b..d43573f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt @@ -6,8 +6,8 @@ import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyItems import net.shadowfacts.phycon.init.PhyScreens -import net.shadowfacts.phycon.networking.C2STerminalRequestItem -import net.shadowfacts.phycon.networking.ServerReceiver +import net.shadowfacts.phycon.networking.* +import org.apache.logging.log4j.LogManager /** * @author shadowfacts @@ -16,6 +16,8 @@ object PhysicalConnectivity: ModInitializer { val MODID = "phycon" + val NETWORK_LOGGER = LogManager.getLogger("PhyNet") + override fun onInitialize() { PhyBlocks.init() PhyBlockEntities.init() @@ -23,6 +25,9 @@ object PhysicalConnectivity: ModInitializer { PhyScreens.init() registerGlobalReceiver(C2STerminalRequestItem) + registerGlobalReceiver(C2STerminalUpdateDisplayedItems) + registerGlobalReceiver(C2SConfigureActivationMode) + registerGlobalReceiver(C2SConfigureRedstoneController) } private fun registerGlobalReceiver(receiver: ServerReceiver) { diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt index 4575fbb..c32c7b4 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt @@ -2,19 +2,30 @@ package net.shadowfacts.phycon import net.fabricmc.api.ClientModInitializer import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry import net.minecraft.client.render.RenderLayer import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.network.block.terminal.TerminalScreen +import net.shadowfacts.phycon.networking.ClientReceiver +import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems /** * @author shadowfacts */ object PhysicalConnectivityClient: ClientModInitializer { + override fun onInitializeClient() { BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent()) ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) + + registerGlobalReceiver(S2CTerminalUpdateDisplayedItems) } + + private fun registerGlobalReceiver(receiver: ClientReceiver) { + ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver) + } + } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt index 8f9bd00..a092601 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt @@ -13,6 +13,8 @@ import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlockEntity import net.shadowfacts.phycon.network.block.netswitch.SwitchBlock import net.shadowfacts.phycon.network.block.netswitch.SwitchBlockEntity +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlock +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity import net.shadowfacts.phycon.network.block.terminal.TerminalBlock import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity import net.shadowfacts.phycon.network.block.test.DestBlock @@ -30,6 +32,7 @@ object PhyBlockEntities { val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH) val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR) val MINER = create(::MinerBlockEntity, PhyBlocks.MINER) + val REDSTONE_CONTROLLER = create(::RedstoneControllerBlockEntity, PhyBlocks.REDSTONE_CONTROLLER) val SOURCE = create(::SourceBlockEntity, PhyBlocks.SOURCE) val DEST = create(::DestBlockEntity, PhyBlocks.DEST) @@ -44,6 +47,7 @@ object PhyBlockEntities { register(SwitchBlock.ID, SWITCH) register(ExtractorBlock.ID, EXTRACTOR) register(MinerBlock.ID, MINER) + register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) register(SourceBlock.ID, SOURCE) register(DestBlock.ID, DEST) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt index 9a90e4f..caabd63 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt @@ -8,6 +8,7 @@ import net.shadowfacts.phycon.network.block.extractor.ExtractorBlock import net.shadowfacts.phycon.network.block.miner.MinerBlock import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.network.block.netswitch.SwitchBlock +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlock import net.shadowfacts.phycon.network.block.terminal.TerminalBlock import net.shadowfacts.phycon.network.block.test.DestBlock import net.shadowfacts.phycon.network.block.test.SourceBlock @@ -23,6 +24,7 @@ object PhyBlocks { val CABLE = CableBlock() val EXTRACTOR = ExtractorBlock() val MINER = MinerBlock() + val REDSTONE_CONTROLLER = RedstoneControllerBlock() val SOURCE = SourceBlock() val DEST = DestBlock() @@ -34,6 +36,7 @@ object PhyBlocks { register(CableBlock.ID, CABLE) register(ExtractorBlock.ID, EXTRACTOR) register(MinerBlock.ID, MINER) + register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) register(SourceBlock.ID, SOURCE) register(DestBlock.ID, DEST) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt index a25f779..ff75216 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt @@ -11,6 +11,7 @@ import net.shadowfacts.phycon.network.block.extractor.ExtractorBlock import net.shadowfacts.phycon.network.block.miner.MinerBlock import net.shadowfacts.phycon.network.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.network.block.netswitch.SwitchBlock +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlock import net.shadowfacts.phycon.network.block.terminal.TerminalBlock import net.shadowfacts.phycon.network.block.test.DestBlock import net.shadowfacts.phycon.network.block.test.SourceBlock @@ -26,6 +27,7 @@ object PhyItems { val CABLE = BlockItem(PhyBlocks.CABLE, Item.Settings()) val EXTRACTOR = BlockItem(PhyBlocks.EXTRACTOR, Item.Settings()) val MINER = BlockItem(PhyBlocks.MINER, Item.Settings()) + val REDSTONE_CONTROLLER = BlockItem(PhyBlocks.REDSTONE_CONTROLLER, Item.Settings()) val SOURCE = BlockItem(PhyBlocks.SOURCE, Item.Settings()) val DEST = BlockItem(PhyBlocks.DEST , Item.Settings()) @@ -40,6 +42,7 @@ object PhyItems { register(CableBlock.ID, CABLE) register(ExtractorBlock.ID, EXTRACTOR) register(MinerBlock.ID, MINER) + register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) register(SourceBlock.ID, SOURCE) register(DestBlock.ID, DEST) diff --git a/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt b/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt index f5cb03a..8df0680 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/item/ConsoleItem.kt @@ -8,7 +8,11 @@ import net.minecraft.util.Identifier import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.network.DeviceBlock import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity +import net.shadowfacts.phycon.network.component.ActivationController +import net.shadowfacts.phycon.screen.ActivatableDeviceConsoleScreen import net.shadowfacts.phycon.screen.DeviceConsoleScreen +import net.shadowfacts.phycon.screen.RedstoneControllerConsoleScreen import net.shadowfacts.phycon.screen.TestCacaoScreen /** @@ -37,9 +41,13 @@ class ConsoleItem: Item(Settings()) { } private fun openScreen(be: DeviceBlockEntity) { -// val screen = DeviceConsoleScreen(be) val screen = TestCacaoScreen() +// val screen = when (be) { +// is ActivationController.ActivatableDevice -> ActivatableDeviceConsoleScreen(be) +// is RedstoneControllerBlockEntity -> RedstoneControllerConsoleScreen(be) +// else -> DeviceConsoleScreen(be) +// } MinecraftClient.getInstance().openScreen(screen) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/DeviceBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/DeviceBlockEntity.kt index 42a68b9..b30553b 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/DeviceBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/DeviceBlockEntity.kt @@ -6,6 +6,7 @@ import net.minecraft.block.entity.BlockEntity import net.minecraft.block.entity.BlockEntityType import net.minecraft.nbt.CompoundTag import net.minecraft.util.Tickable +import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.api.PacketSink import net.shadowfacts.phycon.api.PacketSource import net.shadowfacts.phycon.api.Interface @@ -18,8 +19,7 @@ import net.shadowfacts.phycon.api.util.MACAddress import net.shadowfacts.phycon.network.frame.ARPQueryFrame import net.shadowfacts.phycon.network.frame.ARPResponseFrame import net.shadowfacts.phycon.network.frame.BasePacketFrame -import net.shadowfacts.phycon.network.packet.DeviceRemovedPacket -import java.lang.RuntimeException +import net.shadowfacts.phycon.network.packet.* import java.util.* /** @@ -52,36 +52,45 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type), abstract override fun handle(packet: Packet) + private fun doHandlePacket(packet: Packet) { + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) received packet: {}", this, ipAddress, packet) + when (packet) { + is DeviceRemovedPacket -> { + arpTable.remove(packet.source) + } + } + handle(packet) + } + override fun send(frame: EthernetFrame) { findDestination()?.receive(frame) } override fun receive(frame: EthernetFrame) { - println("$this ($ipAddress, ${macAddress}) received frame from ${frame.source}: $frame") + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) received frame from {}: {}", this, ipAddress, macAddress, frame.source, frame) when (frame) { is ARPQueryFrame -> handleARPQuery(frame) is ARPResponseFrame -> handleARPResponse(frame) is PacketFrame -> { if (frame.packet.destination.isBroadcast || frame.packet.destination == ipAddress) { - println("$this ($ipAddress) received packet: ${frame.packet}") - handle(frame.packet) + doHandlePacket(frame.packet) } } } } private fun handleARPQuery(frame: ARPQueryFrame) { - println("$this ($ipAddress) received ARP query for ${frame.queryIP}") + PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}), received ARP query for {}", this, ipAddress, frame.queryIP) arpTable[frame.sourceIP] = frame.source if (frame.queryIP == ipAddress) { - println("$this ($ipAddress) sending ARP response to ${frame.source} with $macAddress") + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) sending ARP response to {} with {}", this, ipAddress, frame.sourceIP, macAddress) send(ARPResponseFrame(ipAddress, macAddress, frame.source)) } } private fun handleARPResponse(frame: ARPResponseFrame) { arpTable[frame.query] = frame.source - println("$this ($ipAddress) received ARP response for ${frame.query} with ${frame.source}") + PhysicalConnectivity.NETWORK_LOGGER.debug("{}, ({}) received ARP response for {} with {}", this, ipAddress, frame.query, frame.source) val toRemove = packetQueue.filter { (packet, _) -> if (packet.destination == frame.query) { @@ -102,7 +111,7 @@ abstract class DeviceBlockEntity(type: BlockEntityType<*>): BlockEntity(type), } else { packetQueue.add(PendingPacket(packet, counter)) - println("$this ($ipAddress) sending ARP query for ${packet.destination}") + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}) sending ARP query for {}", this, ipAddress, packet.destination) send(ARPQueryFrame(packet.destination, ipAddress, macAddress)) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/FaceDeviceBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/network/FaceDeviceBlock.kt new file mode 100644 index 0000000..b787008 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/FaceDeviceBlock.kt @@ -0,0 +1,107 @@ +package net.shadowfacts.phycon.network + +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.ShapeContext +import net.minecraft.item.ItemPlacementContext +import net.minecraft.state.StateManager +import net.minecraft.state.property.EnumProperty +import net.minecraft.state.property.Properties +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.shape.VoxelShape +import net.minecraft.util.shape.VoxelShapes +import net.minecraft.world.BlockView +import net.minecraft.world.World +import net.minecraft.world.WorldAccess +import net.shadowfacts.phycon.api.Interface +import net.shadowfacts.phycon.api.NetworkComponentBlock +import net.shadowfacts.phycon.network.block.cable.CableBlock +import java.util.* + + +/** + * @author shadowfacts + */ +abstract class FaceDeviceBlock(settings: Settings): DeviceBlock(settings) { + companion object { + val FACING = Properties.FACING + val CABLE_CONNECTION = EnumProperty.of("cable_connection", Direction::class.java) + } + + protected abstract val faceThickness: Double + protected abstract val faceShapes: Map + private val centerShapes: Map by lazy { + mapOf( + Direction.DOWN to createCuboidShape(6.0, faceThickness, 6.0, 10.0, 10.0, 10.0), + Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 16.0 - faceThickness, 10.0), + Direction.NORTH to createCuboidShape(6.0, 6.0, faceThickness, 10.0, 10.0, 10.0), + Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 16.0 - faceThickness), + Direction.WEST to createCuboidShape(faceThickness, 6.0, 6.0, 10.0, 10.0, 10.0), + Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 16.0 - faceThickness, 10.0, 10.0) + ) + } + private val shapeCache = mutableMapOf, VoxelShape>() + + fun getShape(facing: Direction, cableConnection: Direction): VoxelShape { + return shapeCache.getOrPut(facing to cableConnection) { + VoxelShapes.union( + faceShapes[facing], + centerShapes[facing], + CableBlock.SIDE_SHAPES[cableConnection] + ) + } + } + + override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection { + return EnumSet.of(state[CABLE_CONNECTION]) + } + + override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { + return if (side == state[FACING]) { + null + } else { + getBlockEntity(world, pos) + } + } + + override fun appendProperties(builder: StateManager.Builder) { + super.appendProperties(builder) + builder.add(FACING) + builder.add(CABLE_CONNECTION) + } + + override fun getPlacementState(context: ItemPlacementContext): BlockState { + val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite + val cableConnection = getCableConnectedSide(context.world, context.blockPos, facing) ?: facing.opposite + return defaultState + .with(FACING, facing) + .with(CABLE_CONNECTION, cableConnection) + } + + protected fun getCableConnectedSide(world: World, pos: BlockPos, facing: Direction): Direction? { + for (side in Direction.values()) { + if (side == facing) { + continue + } + val offsetPos = pos.offset(side) + val state = world.getBlockState(offsetPos) + val block = state.block + if (block is NetworkComponentBlock && block.getNetworkConnectedSides(state, world, offsetPos).contains(side.opposite)) { + return side + } + } + return null + } + + override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState { + if (neighborState.block is NetworkComponentBlock && world.getBlockState(pos.offset(state[CABLE_CONNECTION])).block !is NetworkComponentBlock) { + return state.with(CABLE_CONNECTION, side) + } + return state + } + + override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape { + return getShape(state[FACING], state[CABLE_CONNECTION]) + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/extractor/ExtractorBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/extractor/ExtractorBlockEntity.kt index 90f0de9..7d83833 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/extractor/ExtractorBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/extractor/ExtractorBlockEntity.kt @@ -1,59 +1,126 @@ package net.shadowfacts.phycon.network.block.extractor import alexiil.mc.lib.attributes.SearchOptions +import alexiil.mc.lib.attributes.Simulation import alexiil.mc.lib.attributes.item.FixedItemInv -import alexiil.mc.lib.attributes.item.GroupedItemInv import alexiil.mc.lib.attributes.item.ItemAttributes +import alexiil.mc.lib.attributes.item.filter.ExactItemStackFilter +import net.minecraft.block.BlockState +import net.minecraft.item.ItemStack +import net.minecraft.nbt.CompoundTag import net.minecraft.util.math.Direction import net.shadowfacts.phycon.api.packet.Packet -import net.shadowfacts.phycon.api.util.IPAddress import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.component.ActivationController +import net.shadowfacts.phycon.network.component.NetworkStackDispatcher +import net.shadowfacts.phycon.network.component.handleItemStack import net.shadowfacts.phycon.network.packet.CapacityPacket -import net.shadowfacts.phycon.network.packet.CheckCapacityPacket import net.shadowfacts.phycon.network.packet.ItemStackPacket +import net.shadowfacts.phycon.network.packet.RemoteActivationPacket +import net.shadowfacts.phycon.util.ActivationMode +import kotlin.properties.Delegates /** * @author shadowfacts */ -class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR) { +class ExtractorBlockEntity: DeviceBlockEntity(PhyBlockEntities.EXTRACTOR), + NetworkStackDispatcher, + ActivationController.ActivatableDevice { + + companion object { + val SLEEP_TIME = 40L + } private val facing: Direction get() = cachedState[ExtractorBlock.FACING] - private var inventory: GroupedItemInv? = null - private var shouldExtract = false + private var inventory: FixedItemInv? = null + override val pendingInsertions = mutableListOf() + override val dispatchStackTimeout = 40L + override val controller = ActivationController(SLEEP_TIME, this) fun updateInventory() { val offsetPos = pos.offset(facing) val option = SearchOptions.inDirection(facing) - inventory = ItemAttributes.GROUPED_INV.getFirstOrNull(world, offsetPos, option) + inventory = ItemAttributes.FIXED_INV.getFirstOrNull(world, offsetPos, option) } - private fun getInventory(): GroupedItemInv? { + private fun getInventory(): FixedItemInv? { if (inventory == null) updateInventory() return inventory } override fun handle(packet: Packet) { - if (packet is CapacityPacket && shouldExtract) { - getInventory()?.also { inv -> - shouldExtract = false - val extracted = inv.extract(packet.stack, packet.capacity) - sendPacket(ItemStackPacket(extracted, ipAddress, packet.stackReceiver.ipAddress)) - } + when (packet) { + is CapacityPacket -> handleCapacity(packet) + is ItemStackPacket -> handleItemStack(packet) + is RemoteActivationPacket -> controller.handleRemoteActivation(packet) } } + override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { + // we can't insert things back into the inventory, so just let them spawn + return packet.stack + } + + override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) + + override fun finishInsertion(insertion: PendingInsertion): ItemStack { + val inventory = getInventory() ?: return insertion.stack + // if the inventory has changed, the old slot index is meaningless + if (inventory !== insertion.inventory) return insertion.stack + val extracted = inventory.extractStack(insertion.inventorySlot, ExactItemStackFilter(insertion.stack), ItemStack.EMPTY, insertion.totalCapacity, Simulation.ACTION) + if (extracted.isEmpty) return insertion.stack + // if we extracted less than expected, make sure super.finishInsertion doesn't send more than we actually have + insertion.stack = extracted + return super.finishInsertion(insertion) + } + override fun tick() { super.tick() - if (!world!!.isClient && counter % 40 == 0L) { - getInventory()?.also { - val stack = it.storedStacks.firstOrNull() ?: return - shouldExtract = true - sendPacket(CheckCapacityPacket(stack, ipAddress, IPAddress.BROADCAST)) - } + if (!world!!.isClient) { + controller.tick() } } -} \ No newline at end of file + + override fun activate(): Boolean { + val inventory = getInventory() ?: return false + for (slot in 0 until inventory.slotCount) { + val slotStack = inventory.getInvStack(slot) + if (slotStack.isEmpty) continue + dispatchItemStack(slotStack) { insertion -> + insertion.inventory = inventory + insertion.inventorySlot = slot + } + return true + } + return false + } + + override fun toTag(tag: CompoundTag): CompoundTag { + tag.putString("ActivationMode", controller.activationMode.name) + return super.toTag(tag) + } + + override fun fromTag(state: BlockState, tag: CompoundTag) { + super.fromTag(state, tag) + controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) + } + + override fun toClientTag(tag: CompoundTag): CompoundTag { + tag.putString("ActivationMode", controller.activationMode.name) + return super.toClientTag(tag) + } + + override fun fromClientTag(tag: CompoundTag) { + super.fromClientTag(tag) + controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) + } + + class PendingInsertion(stack: ItemStack, timestamp: Long): NetworkStackDispatcher.PendingInsertion(stack, timestamp) { + lateinit var inventory: FixedItemInv + var inventorySlot by Delegates.notNull() + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlock.kt index cab6826..0a091ff 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlock.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlock.kt @@ -4,103 +4,39 @@ import alexiil.mc.lib.attributes.AttributeList import alexiil.mc.lib.attributes.AttributeProvider import net.minecraft.block.* import net.minecraft.entity.LivingEntity -import net.minecraft.item.ItemPlacementContext import net.minecraft.item.ItemStack -import net.minecraft.state.StateManager -import net.minecraft.state.property.EnumProperty -import net.minecraft.state.property.Properties import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Direction -import net.minecraft.util.shape.VoxelShape -import net.minecraft.util.shape.VoxelShapes import net.minecraft.world.BlockView import net.minecraft.world.World -import net.minecraft.world.WorldAccess import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.api.NetworkComponentBlock -import net.shadowfacts.phycon.api.Interface -import net.shadowfacts.phycon.network.DeviceBlock -import net.shadowfacts.phycon.network.block.cable.CableBlock -import java.util.* +import net.shadowfacts.phycon.network.FaceDeviceBlock /** * @author shadowfacts */ -class InterfaceBlock: DeviceBlock(Settings.of(Material.METAL)), +class InterfaceBlock: FaceDeviceBlock(Settings.of(Material.METAL)), NetworkComponentBlock, AttributeProvider { companion object { val ID = Identifier(PhysicalConnectivity.MODID, "network_interface") - val FACING = Properties.FACING - val CABLE_CONNECTION = EnumProperty.of("cable_connection", Direction::class.java) - private val SIDE_SHAPES = mapOf( - Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0), - Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0), - Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0), - Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0), - Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0), - Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0) - ) - private val CENTER_SHAPES = mapOf( - Direction.DOWN to createCuboidShape(6.0, 2.0, 6.0, 10.0, 10.0, 10.0), - Direction.UP to createCuboidShape(6.0, 6.0, 6.0, 10.0, 14.0, 10.0), - Direction.NORTH to createCuboidShape(6.0, 6.0, 2.0, 10.0, 10.0, 10.0), - Direction.SOUTH to createCuboidShape(6.0, 6.0, 6.0, 10.0, 10.0, 14.0), - Direction.WEST to createCuboidShape(2.0, 6.0, 6.0, 10.0, 10.0, 10.0), - Direction.EAST to createCuboidShape(6.0, 6.0, 6.0, 14.0, 10.0, 10.0) - ) - - private val shapeCache = mutableMapOf, VoxelShape>() - fun getShape(facing: Direction, cableConnection: Direction): VoxelShape { - return shapeCache.getOrPut(facing to cableConnection) { - VoxelShapes.union( - VoxelShapes.union(SIDE_SHAPES[facing], CENTER_SHAPES[facing]), - CableBlock.SIDE_SHAPES[cableConnection] - ) - } - } } - override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection { - val set = EnumSet.of(state[CABLE_CONNECTION]) - set.remove(state[FACING]) - return set - } - - override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { - return if (side == state[FACING]) { - null - } else { - getBlockEntity(world, pos) - } - } - - override fun appendProperties(builder: StateManager.Builder) { - super.appendProperties(builder) - builder.add(FACING) - builder.add(CABLE_CONNECTION) - } + override val faceThickness = 2.0 + override val faceShapes = mapOf( + Direction.DOWN to createCuboidShape(2.0, 0.0, 2.0, 14.0, 2.0, 14.0), + Direction.UP to createCuboidShape(2.0, 14.0, 2.0, 14.0, 16.0, 14.0), + Direction.NORTH to createCuboidShape(2.0, 2.0, 0.0, 14.0, 14.0, 2.0), + Direction.SOUTH to createCuboidShape(2.0, 2.0, 14.0, 14.0, 14.0, 16.0), + Direction.WEST to createCuboidShape(0.0, 2.0, 2.0, 2.0, 14.0, 14.0), + Direction.EAST to createCuboidShape(14.0, 2.0, 2.0, 16.0, 14.0, 14.0) + ) override fun createBlockEntity(world: BlockView) = InterfaceBlockEntity() - override fun getPlacementState(context: ItemPlacementContext): BlockState { - val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite - val cableConnection = getCableConnectionSide(context.world, context.blockPos) ?: facing.opposite - return defaultState.with(FACING, facing).with(CABLE_CONNECTION, cableConnection) - } - - private fun getCableConnectionSide(world: World, pos: BlockPos): Direction? { - for (side in Direction.values()) { - val offsetPos = pos.offset(side) - if (world.getBlockState(offsetPos).block is NetworkComponentBlock) { - return side - } - } - return null - } - override fun onPlaced(world: World, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) { if (!world.isClient) { getBlockEntity(world, pos)!!.updateInventory() @@ -113,19 +49,8 @@ class InterfaceBlock: DeviceBlock(Settings.of(Material.MET } } - override fun getStateForNeighborUpdate(state: BlockState, side: Direction, neighborState: BlockState, world: WorldAccess, pos: BlockPos, neighborPos: BlockPos): BlockState { - if (neighborState.block is NetworkComponentBlock && world.getBlockState(pos.offset(state[CABLE_CONNECTION])).block !is NetworkComponentBlock) { - return state.with(CABLE_CONNECTION, side) - } - return state - } - override fun addAllAttributes(world: World, pos: BlockPos, state: BlockState, to: AttributeList<*>) { to.offer(getBlockEntity(world, pos)) } - override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape { - return getShape(state[FACING], state[CABLE_CONNECTION]) - } - } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt index 85500f6..9cb58ae 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/netinterface/InterfaceBlockEntity.kt @@ -9,11 +9,13 @@ import net.minecraft.util.math.Direction import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.FaceDeviceBlock import net.shadowfacts.phycon.network.component.ItemStackPacketHandler import net.shadowfacts.phycon.network.component.NetworkStackProvider import net.shadowfacts.phycon.network.component.NetworkStackReceiver import net.shadowfacts.phycon.network.component.handleItemStack import net.shadowfacts.phycon.network.packet.* +import kotlin.math.min /** * @author shadowfacts @@ -24,7 +26,7 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE), NetworkStackReceiver { private val facing: Direction - get() = cachedState[InterfaceBlock.FACING] + get() = cachedState[FaceDeviceBlock.FACING] // todo: should this be a weak ref? private var inventory: GroupedItemInv? = null @@ -67,8 +69,16 @@ class InterfaceBlockEntity: DeviceBlockEntity(PhyBlockEntities.INTERFACE), private fun handleExtractStack(packet: ExtractStackPacket) { getInventory()?.also { inv -> - val extracted = inv.extract(packet.stack, packet.amount) - sendPacket(ItemStackPacket(extracted, ipAddress, packet.source)) + var amount = packet.amount + while (amount > 0) { + val extracted = inv.extract(packet.stack, min(amount, packet.stack.maxCount)) + if (extracted.isEmpty) { + break + } else { + amount -= extracted.count + sendPacket(ItemStackPacket(extracted, ipAddress, packet.source)) + } + } } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/netswitch/SwitchBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/netswitch/SwitchBlockEntity.kt index 51d0d51..23c54b8 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/netswitch/SwitchBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/netswitch/SwitchBlockEntity.kt @@ -7,6 +7,7 @@ import net.minecraft.entity.ItemEntity import net.minecraft.nbt.CompoundTag import net.minecraft.util.Tickable import net.minecraft.util.math.Direction +import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.api.Interface import net.shadowfacts.phycon.api.frame.EthernetFrame import net.shadowfacts.phycon.api.frame.PacketFrame @@ -53,10 +54,10 @@ class SwitchBlockEntity: BlockEntity(PhyBlockEntities.SWITCH), if (frame.destination.type != MACAddress.Type.BROADCAST && macTable.containsKey(frame.destination)) { val dir = macTable[frame.destination]!! - println("$this (${fromItf.side}, ${fromItf.macAddress}) forwarding $frame to side $dir") + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) forwarding {} to side {}", this, fromItf.side, fromItf.macAddress, frame, dir) interfaceForSide(dir).send(frame) } else { - println("$this (${fromItf.side}, ${fromItf.macAddress}) flooding $frame") + PhysicalConnectivity.NETWORK_LOGGER.debug("{} ({}, {}) flooding {}", this, fromItf.side, fromItf.macAddress, frame) flood(frame, fromItf) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlock.kt new file mode 100644 index 0000000..51d4773 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlock.kt @@ -0,0 +1,74 @@ +package net.shadowfacts.phycon.network.block.redstone + +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.Material +import net.minecraft.item.ItemPlacementContext +import net.minecraft.server.world.ServerWorld +import net.minecraft.state.StateManager +import net.minecraft.state.property.Properties +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.world.BlockView +import net.minecraft.world.World +import net.minecraft.world.WorldAccess +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.network.FaceDeviceBlock +import java.util.* + +/** + * @author shadowfacts + */ +class RedstoneControllerBlock: FaceDeviceBlock(Settings.of(Material.METAL)) { + + companion object { + val ID = Identifier(PhysicalConnectivity.MODID, "redstone_controller") + val LIT = Properties.LIT + } + + // todo: don't just copy this from the Interface block + override val faceThickness = 3.0 + override val faceShapes = mapOf( + Direction.DOWN to createCuboidShape(0.0, 0.0, 0.0, 16.0, 3.0, 16.0), + Direction.UP to createCuboidShape(0.0, 13.0, 0.0, 16.0, 16.0, 16.0), + Direction.NORTH to createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 3.0), + Direction.SOUTH to createCuboidShape(0.0, 0.0, 13.0, 16.0, 16.0, 16.0), + Direction.WEST to createCuboidShape(0.0, 0.0, 0.0, 3.0, 16.0, 16.0), + Direction.EAST to createCuboidShape(13.0, 0.0, 0.0, 16.0, 16.0, 16.0) + ) + + override fun appendProperties(builder: StateManager.Builder) { + super.appendProperties(builder) + builder.add(LIT) + } + + override fun createBlockEntity(world: BlockView) = RedstoneControllerBlockEntity() + + override fun getPlacementState(context: ItemPlacementContext): BlockState { + val state = super.getPlacementState(context) + return state.with(LIT, isPowered(context.world, context.blockPos, state[FACING])) + } + + override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, bl: Boolean) { + // this can't be done in getStateForNeighborUpdate because getEmittedRedstonePower is defined in World not WorldAccess + if (!world.isClient) { + val wasLit = state[LIT] + val isLit = isPowered(world, pos, state[FACING]) + if (wasLit != isLit) { + toggleLit(state, world, pos) + } + } + } + + private fun isPowered(world: World, pos: BlockPos, facing: Direction): Boolean { + val offset = pos.offset(facing) + return world.getEmittedRedstonePower(offset, facing) > 0 + } + + private fun toggleLit(state: BlockState, world: World, pos: BlockPos) { + world.setBlockState(pos, state.cycle(LIT), 2) + getBlockEntity(world, pos)!!.redstoneStateChanged() + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlockEntity.kt new file mode 100644 index 0000000..d9bbfbb --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/redstone/RedstoneControllerBlockEntity.kt @@ -0,0 +1,75 @@ +package net.shadowfacts.phycon.network.block.redstone + +import net.minecraft.block.BlockState +import net.minecraft.nbt.CompoundTag +import net.shadowfacts.phycon.api.packet.Packet +import net.shadowfacts.phycon.api.util.IPAddress +import net.shadowfacts.phycon.init.PhyBlockEntities +import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.packet.RemoteActivationPacket +import net.shadowfacts.phycon.util.RedstoneMode + +/** + * @author shadowfacts + */ +class RedstoneControllerBlockEntity: DeviceBlockEntity(PhyBlockEntities.REDSTONE_CONTROLLER) { + + var managedDevices = Array(5) { null } + var redstoneMode = RedstoneMode.HIGH + + private var redstonePowered = false + + override fun handle(packet: Packet) { + } + + fun redstoneStateChanged() { + val oldPowered = redstonePowered + redstonePowered = cachedState[RedstoneControllerBlock.LIT] + + val mode: RemoteActivationPacket.Mode? = when (redstoneMode) { + RedstoneMode.TOGGLE -> if (oldPowered != redstonePowered) RemoteActivationPacket.Mode.SINGLE else null + RedstoneMode.RISING_EDGE -> if (!oldPowered && redstonePowered) RemoteActivationPacket.Mode.SINGLE else null + RedstoneMode.FALLING_EDGE -> if (oldPowered && !redstonePowered) RemoteActivationPacket.Mode.SINGLE else null + RedstoneMode.HIGH -> if (redstonePowered) RemoteActivationPacket.Mode.ENABLE else RemoteActivationPacket.Mode.DISABLE + RedstoneMode.LOW -> if (redstonePowered) RemoteActivationPacket.Mode.DISABLE else RemoteActivationPacket.Mode.ENABLE + } + + if (mode != null) { + sendActivatePacket(mode) + } + } + + private fun sendActivatePacket(mode: RemoteActivationPacket.Mode) { + for (ip in managedDevices) { + if (ip == null) continue + sendPacket(RemoteActivationPacket(mode, ipAddress, ip)) + } + } + + override fun toTag(tag: CompoundTag): CompoundTag { + tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address }) + tag.putString("RedstoneMode", redstoneMode.name) + return super.toTag(tag) + } + + override fun fromTag(state: BlockState, tag: CompoundTag) { + super.fromTag(state, tag) + val addresses = tag.getIntArray("ManagedDevices") + managedDevices = (0..4).map { if (it >= addresses.size) null else IPAddress(addresses[it]) }.toTypedArray() + redstoneMode = RedstoneMode.valueOf(tag.getString("RedstoneMode")) + } + + override fun toClientTag(tag: CompoundTag): CompoundTag { + tag.putIntArray("ManagedDevices", managedDevices.mapNotNull { it?.address }) + tag.putString("RedstoneMode", redstoneMode.name) + return super.toClientTag(tag) + } + + override fun fromClientTag(tag: CompoundTag) { + super.fromClientTag(tag) + val addresses = tag.getIntArray("ManagedDevices") + managedDevices = (0..4).map { if (it >= addresses.size) null else IPAddress(addresses[it]) }.toTypedArray() + redstoneMode = RedstoneMode.valueOf(tag.getString("RedstoneMode")) + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt index eab52cf..e17af71 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalBlockEntity.kt @@ -56,7 +56,6 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), private var observers = 0 val cachedNetItems = ItemStackCollections.intMap() - var cachedSortedNetItems = listOf() var netItemObserver: WeakReference? = null @@ -86,14 +85,12 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), private fun handleReadInventory(packet: ReadInventoryPacket) { inventoryCache[packet.source] = packet.inventory - updateNetItems() - sync() + updateAndSync() } private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { inventoryCache.remove(packet.source) - updateNetItems() - sync() + updateAndSync() } private fun handleStackLocation(packet: StackLocationPacket) { @@ -113,12 +110,17 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), // this happens outside the normal update loop because by receiving the item stack packet // we "know" how much the count in the source inventory has changed - updateNetItems() - sync() + updateAndSync() return remaining } + private fun updateAndSync() { + updateNetItems() + sync() + netItemObserver?.get()?.netItemsChanged() + } + private fun updateNetItems() { cachedNetItems.clear() for (inventory in inventoryCache.values) { @@ -127,12 +129,6 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), cachedNetItems.mergeInt(stack, amount) { a, b -> a + b } } } - // todo: is the map necessary or is just the sorted list enough? - cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { - val stack = it.key.copy() - stack.count = it.intValue - stack - } } private fun beginInsertions() { @@ -175,21 +171,15 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), finishTimedOutPendingInsertions() } - if (observers > 0) { - if (world!!.isClient) { - println(cachedNetItems) - } else { - updateNetItems() - sync() - } + if (observers > 0 && !world!!.isClient) { + updateAndSync() } } } fun onActivate(player: PlayerEntity) { if (!world!!.isClient) { - updateNetItems() - sync() + updateAndSync() inventoryCache.clear() sendPacket(RequestInventoryPacket(ipAddress)) @@ -238,8 +228,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), internalBuffer.setStack(insertion.bufferSlot, remaining) // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets - updateNetItems() - sync() + updateAndSync() return remaining } @@ -263,31 +252,11 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), override fun toClientTag(tag: CompoundTag): CompoundTag { tag.put("InternalBuffer", internalBuffer.toTag()) - val list = ListTag() - tag.put("CachedNetItems", list) - for ((stack, amount) in cachedNetItems) { - val entryTag = stack.toTag(CompoundTag()) - entryTag.putInt("NetAmount", amount) - list.add(entryTag) - } return tag } override fun fromClientTag(tag: CompoundTag) { internalBuffer.fromTag(tag.getCompound("InternalBuffer")) - val list = tag.getList("CachedNetItems", 10) - cachedNetItems.clear() - for (entryTag in list) { - val stack = ItemStack.fromTag(entryTag as CompoundTag) - val netAmount = entryTag.getInt("NetAmount") - cachedNetItems[stack] = netAmount - } - netItemObserver?.get()?.netItemsChanged() - cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { - val stack = it.key.copy() - stack.count = it.intValue - stack - } } interface NetItemObserver { diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt index 372a17e..762c0e9 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt @@ -1,18 +1,33 @@ package net.shadowfacts.phycon.network.block.terminal -import com.mojang.blaze3d.platform.GlStateManager +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.widget.AbstractButtonWidget +import net.minecraft.client.gui.widget.AbstractPressableButtonWidget +import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.ItemStack import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.text.LiteralText import net.minecraft.text.Text +import net.minecraft.text.TranslatableText import net.minecraft.util.Identifier import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.networking.C2STerminalRequestItem +import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems +import net.shadowfacts.phycon.util.SortMode +import net.shadowfacts.phycon.util.next +import net.shadowfacts.phycon.util.prev import org.lwjgl.glfw.GLFW +import java.lang.NumberFormatException +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.min /** * @author shadowfacts @@ -20,10 +35,32 @@ import org.lwjgl.glfw.GLFW // todo: translate title class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): HandledScreen(handler, playerInv, title) { companion object { - val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") + private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") + private val DIALOG = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal_amount.png") } private lateinit var searchBox: TextFieldWidget + private lateinit var sortButton: SortButton + private lateinit var amountBox: TextFieldWidget + private var dialogStack = ItemStack.EMPTY + private var showingAmountDialog = false + set(value) { + val oldValue = field + field = value + for (e in dialogChildren) { + e.visible = value + } + amountBox.isVisible + searchBox.setSelected(!value) + amountBox.setSelected(value) + if (value && !oldValue) { + amountBox.text = "1" + } + } + private var dialogChildren = mutableListOf() + + private val dialogWidth = 158 + private val dialogHeight = 62 init { backgroundWidth = 252 @@ -33,6 +70,9 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, override fun init() { super.init() + children.clear() + dialogChildren.clear() + client!!.keyboard.setRepeatEvents(true) searchBox = TextFieldWidget(textRenderer, x + 138, y + 6, 80, 9, LiteralText("Search")) @@ -42,37 +82,134 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, searchBox.isVisible = true searchBox.setSelected(true) searchBox.setEditableColor(0xffffff) - children.add(searchBox) + addChild(searchBox) + + sortButton = SortButton(x + 256, y + 0, handler.sortMode, { + requestUpdatedItems() + }, ::renderTooltip) + addButton(sortButton) + + val dialogMinX = width / 2 - dialogWidth / 2 + val dialogMinY = height / 2 - dialogHeight / 2 + amountBox = TextFieldWidget(textRenderer, dialogMinX + 8, dialogMinY + 27, 80, 9, LiteralText("Amount")) + amountBox.setHasBorder(false) + amountBox.isVisible = false + amountBox.setSelected(false) + amountBox.setEditableColor(0xffffff) + amountBox.setTextPredicate { + if (it.isEmpty()) { + true + } else { + try { + Integer.parseInt(it) > 0 + } catch (e: NumberFormatException) { + false + } + } + } + dialogChildren.add(amountBox) + + val plusOne = SmallButton(dialogMinX + 7, dialogMinY + 7, 28, LiteralText("+1")) { + amountBox.intValue += 1 + } + dialogChildren.add(plusOne) + + val plusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 7, 28, LiteralText("+10")) { + amountBox.intValue = ceil((amountBox.intValue + 1) / 10.0).toInt() * 10 + } + dialogChildren.add(plusTen) + + val plusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 7, 28, LiteralText("+100")) { + amountBox.intValue = ceil((amountBox.intValue + 1) / 100.0).toInt() * 100 + } + dialogChildren.add(plusHundred) + + val minusOne = SmallButton(dialogMinX + 7, dialogMinY + 39, 28, LiteralText("-1")) { + amountBox.intValue -= 1 + } + dialogChildren.add(minusOne) + + val minusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 39, 28, LiteralText("-10")) { + amountBox.intValue = floor((amountBox.intValue - 1) / 10.0).toInt() * 10 + } + dialogChildren.add(minusTen) + + val minusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 39, 28, LiteralText("-100")) { + amountBox.intValue = floor((amountBox.intValue - 1) / 100.0).toInt() * 100 + } + dialogChildren.add(minusHundred) + + // 101,25 + val request = ButtonWidget(dialogMinX + 101, dialogMinY + 21, 50, 20, LiteralText("Request")) { + doDialogRequest() + } + dialogChildren.add(request) + + requestUpdatedItems() + } + + private fun requestUpdatedItems() { + val player = MinecraftClient.getInstance().player!! + player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode)) } override fun tick() { super.tick() searchBox.tick() + amountBox.tick() } override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { textRenderer.draw(matrixStack, title, 65f, 6f, 0x404040) textRenderer.draw(matrixStack, playerInventory.displayName, 65f, backgroundHeight - 94f, 0x404040) - // todo: translate this - textRenderer.draw(matrixStack, "Buffer", 7f, 6f, 0x404040) + textRenderer.draw(matrixStack, TranslatableText("gui.phycon.terminal_buffer"), 7f, 6f, 0x404040) } override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { - renderBackground(matrixStack) + // if the dialog is open, the background gradient will be drawn in front of the main terminal gui + if (!showingAmountDialog) { + renderBackground(matrixStack) + } - GlStateManager.color4f(1f, 1f, 1f, 1f) + RenderSystem.color4f(1f, 1f, 1f, 1f) client!!.textureManager.bindTexture(BACKGROUND) val x = (width - backgroundWidth) / 2 val y = (height - backgroundHeight) / 2 drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight) } + @ExperimentalUnsignedTypes override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { - super.render(matrixStack, mouseX, mouseY, delta) + if (showingAmountDialog) { + RenderSystem.pushMatrix() + // items are rendered at some stupidly high z offset. item amounts at an even higher one + RenderSystem.translatef(0f, 0f, -350f) + + // fake the mouse x/y while showing a dialog so slot mouseover highlights aren't drawn + super.render(matrixStack, -1, -1, delta) + + RenderSystem.popMatrix() + } else { + super.render(matrixStack, mouseX, mouseY, delta) + } searchBox.render(matrixStack, mouseX, mouseY, delta) - drawMouseoverTooltip(matrixStack, mouseX, mouseY) + if (showingAmountDialog) { + renderBackground(matrixStack) + + RenderSystem.color4f(1f, 1f, 1f, 1f) + client!!.textureManager.bindTexture(DIALOG) + val dialogMinX = width / 2 - dialogWidth / 2 + val dialogMinY = height / 2 - dialogHeight / 2 + drawTexture(matrixStack, dialogMinX, dialogMinY, 0, 0, dialogWidth, dialogHeight) + + for (e in dialogChildren) { + e.render(matrixStack, mouseX, mouseY, delta) + } + } else { + drawMouseoverTooltip(matrixStack, mouseX, mouseY) + } } @ExperimentalUnsignedTypes @@ -90,42 +227,208 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt()) } - override fun onMouseClick(slot: Slot?, i: Int, j: Int, slotActionType: SlotActionType?) { - super.onMouseClick(slot, i, j, slotActionType) + override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) { + super.onMouseClick(slot, invSlot, clickData, type) // don't unfocus the search box on mouse click searchBox.setSelected(true) + + if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) { + val stack = slot.stack + + if (type == SlotActionType.QUICK_MOVE) { + // shift click, request full stack + requestItem(stack, min(stack.count, stack.maxCount)) + } else if (type == SlotActionType.PICKUP) { + if (clickData == 1) { + // right click, request half stack + requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt()) + } else { + dialogStack = stack + showingAmountDialog = true + searchBox.setSelected(false) + } + } + } + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (showingAmountDialog) { + for (e in dialogChildren) { + if (e.mouseClicked(mouseX, mouseY, button)) { + return true + } + } + return false + } else { + return super.mouseClicked(mouseX, mouseY, button) + } + } + + override fun mouseDragged(d: Double, e: Double, i: Int, f: Double, g: Double): Boolean { + if (showingAmountDialog) { + return false + } else { + return super.mouseDragged(d, e, i, f, g) + } + } + + override fun mouseMoved(d: Double, e: Double) { + if (showingAmountDialog) { + } else { + super.mouseMoved(d, e) + } + } + + override fun mouseReleased(d: Double, e: Double, i: Int): Boolean { + if (showingAmountDialog) { + return false + } else { + return super.mouseReleased(d, e, i) + } + } + + override fun mouseScrolled(d: Double, e: Double, f: Double): Boolean { + if (showingAmountDialog) { + return false + } else { + return super.mouseScrolled(d, e, f) + } } override fun charTyped(c: Char, i: Int): Boolean { - val oldText = searchBox.text - if (searchBox.charTyped(c, i)) { - if (searchBox.text != oldText) { - search() + if (showingAmountDialog) { + return amountBox.charTyped(c, i) + } else { + val oldText = searchBox.text + if (searchBox.charTyped(c, i)) { + if (searchBox.text != oldText) { + requestUpdatedItems() + } + return true } - return true - } - return super.charTyped(c, i) + return super.charTyped(c, i) + } } override fun keyPressed(key: Int, j: Int, k: Int): Boolean { - val oldText = searchBox.text - if (searchBox.keyPressed(key, j, k)) { - if (searchBox.text != oldText) { - search() + if (showingAmountDialog) { + return when (key) { + GLFW.GLFW_KEY_ESCAPE -> { + showingAmountDialog = false + true + } + GLFW.GLFW_KEY_ENTER -> { + doDialogRequest() + true + } + else -> { + amountBox.keyPressed(key, j, k) + } } - return true - } - return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { - true } else { - super.keyPressed(key, j, k) + val oldText = searchBox.text + if (searchBox.keyPressed(key, j, k)) { + if (searchBox.text != oldText) { + requestUpdatedItems() + } + return true + } + return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { + true + } else { + super.keyPressed(key, j, k) + } } } - private fun search() { - screenHandler.search(searchBox.text) + private fun doDialogRequest() { + showingAmountDialog = false + requestItem(dialogStack, amountBox.intValue) + } + + private fun requestItem(stack: ItemStack, amount: Int) { + val netHandler = MinecraftClient.getInstance().player!!.networkHandler + val packet = C2STerminalRequestItem(handler.terminal, stack, amount) + netHandler.sendPacket(packet) + } + + private var TextFieldWidget.intValue: Int + get() = if (text.isEmpty()) 0 else Integer.parseInt(text) + set(value) { + text = value.toString() + setSelected(true) + } + + class SmallButton(x: Int, y: Int, width: Int, title: Text, action: PressAction): ButtonWidget(x, y, width, 14, title, action) { + @ExperimentalUnsignedTypes + override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { + val client = MinecraftClient.getInstance() + client.textureManager.bindTexture(DIALOG) + RenderSystem.color4f(1f, 1f, 1f, 1f) + val v = if (isHovered) 142 else 128 + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + RenderSystem.enableDepthTest() + drawTexture(matrixStack, x, y, 0, v, width / 2, height) + drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, v, width / 2, height) + drawCenteredText(matrixStack, client.textRenderer, message, x + width / 2, y + (height - 8) / 2, 0xffffffffu.toInt()) + } + } + + class SortButton( + x: Int, + y: Int, + var mode: SortMode, + val onChange: (SortMode) -> Unit, + val doRenderTooltip: (MatrixStack, Text, Int, Int) -> Unit + ): AbstractPressableButtonWidget(x, y, 20, 20, LiteralText("")) { + override fun onPress() {} + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if ((button == 0 || button == 1) && clicked(mouseX, mouseY)) { + val newVal = if (button == 0) mode.next else mode.prev + mode = newVal + onChange(mode) + + playDownSound(MinecraftClient.getInstance().soundManager) + return true + } + return false + } + + override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { + val client = MinecraftClient.getInstance() + RenderSystem.color4f(1f, 1f, 1f, 1f) + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + RenderSystem.enableDepthTest() + + client.textureManager.bindTexture(WIDGETS_LOCATION) + val k = getYImage(isHovered) + drawTexture(matrixStack, x, y, 0, 46 + k * 20, width / 2, height) + drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, 46 + k * 20, width / 2, height) + + client.textureManager.bindTexture(BACKGROUND) + val u: Int = when (mode) { + SortMode.COUNT_HIGH_FIRST -> 0 + SortMode.COUNT_LOW_FIRST -> 16 + SortMode.ALPHABETICAL -> 32 + } + drawTexture(matrixStack, x + 2, y + 2, u, 230, 16, 16) + + if (isHovered) { + renderToolTip(matrixStack, mouseX, mouseY) + } + } + + override fun renderToolTip(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { + val text = LiteralText("") + text.append("Sort by: ") + text.append(mode.tooltip) + doRenderTooltip(matrixStack, text, mouseX, mouseY) + } } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt index e648f07..e361c85 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt @@ -8,12 +8,15 @@ import net.minecraft.entity.player.PlayerInventory import net.minecraft.item.ItemStack import net.minecraft.network.PacketByteBuf import net.minecraft.screen.ScreenHandler +import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.util.Identifier import net.minecraft.util.registry.Registry import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.networking.C2STerminalRequestItem +import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems +import net.shadowfacts.phycon.util.SortMode import java.lang.ref.WeakReference import kotlin.math.ceil import kotlin.math.min @@ -21,7 +24,7 @@ import kotlin.math.min /** * @author shadowfacts */ -class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId), +class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId), TerminalBlockEntity.NetItemObserver { companion object { @@ -30,6 +33,19 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina private val fakeInv = FakeInventory(this) private var searchQuery: String = "" + var sortMode = SortMode.COUNT_HIGH_FIRST + private set + private var itemEntries = listOf() + set(value) { + field = value + if (terminal.world!!.isClient) { + itemsForDisplay = value.map { + val stack = it.stack.copy() + stack.count = it.amount + stack + } + } + } var itemsForDisplay = listOf() private set @@ -37,8 +53,10 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!) init { - terminal.netItemObserver = WeakReference(this) - netItemsChanged() + if (!terminal.world!!.isClient) { + terminal.netItemObserver = WeakReference(this) + netItemsChanged() + } // network for (y in 0 until 6) { @@ -67,7 +85,10 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina } override fun netItemsChanged() { - itemsForDisplay = terminal.cachedNetItems.object2IntEntrySet().filter { + val player = playerInv.player + assert(player is ServerPlayerEntity) + + val filtered = terminal.cachedNetItems.object2IntEntrySet().filter { if (searchQuery.isBlank()) return@filter true if (searchQuery.startsWith('@')) { val unprefixed = searchQuery.drop(1) @@ -77,20 +98,34 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina } } it.key.name.string.contains(searchQuery, true) - }.sortedByDescending { - it.intValue - }.map { - val stack = it.key.copy() - stack.count = it.intValue - stack } + + val sorted = + when (sortMode) { + SortMode.COUNT_HIGH_FIRST -> filtered.sortedByDescending { it.intValue } + SortMode.COUNT_LOW_FIRST -> filtered.sortedBy { it.intValue } + SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string } + } + + itemEntries = sorted.map { Entry(it.key, it.intValue) } + + (player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, sortMode)) } - fun search(query: String) { - searchQuery = query + fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, sortMode: SortMode) { + this.searchQuery = query + this.sortMode = sortMode netItemsChanged() } + fun receivedUpdatedItemsFromServer(entries: List, query: String, sortMode: SortMode) { + assert(playerInv.player.world.isClient) + + this.searchQuery = query + this.sortMode = sortMode + itemEntries = entries + } + override fun canUse(player: PlayerEntity): Boolean { return true } @@ -102,28 +137,7 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina } override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack { - if (isNetworkSlot(slotId)) { - // the slot clicked was one of the network stacks - if (actionType == SlotActionType.QUICK_MOVE) { - val stack = slots[slotId].stack - if (!stack.isEmpty && player.world.isClient) { - requestItem(player, stack, min(stack.count, stack.maxCount)) - - } - } else if (actionType == SlotActionType.PICKUP && clickData == 1) { - if (clickData == 1) { - // right click, request half stack - val stack = slots[slotId].stack - if (!stack.isEmpty && player.world.isClient) { - requestItem(player, stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt()) - } - } else { - // todo: left click, show amount dialog - } - } - return ItemStack.EMPTY - } else if (isBufferSlot(slotId)) { - // internal buffer + if (isBufferSlot(slotId)) { // todo: why does this think it's quick_craft sometimes? if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) && !player.inventory.cursorStack.isEmpty) { // placing cursor stack into buffer @@ -134,13 +148,6 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina return super.onSlotClick(slotId, clickData, actionType, player) } - private fun requestItem(player: PlayerEntity, stack: ItemStack, amount: Int) { - if (!player.world.isClient) return - val handler = (player as ClientPlayerEntity).networkHandler - val packet = C2STerminalRequestItem(terminal, stack, amount) - handler.sendPacket(packet) - } - override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { if (isNetworkSlot(slotId)) { return ItemStack.EMPTY; @@ -154,7 +161,8 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina val result = slot.stack.copy() if (isBufferSlot(slotId)) { - if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, false)) { + // last boolean param is fromLast + if (!insertItem(slot.stack, playerSlotsStart, playerSlotsEnd, true)) { return ItemStack.EMPTY } if (slot.stack.isEmpty) { @@ -204,4 +212,6 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart fun isPlayerSlot(id: Int) = id >= playerSlotsStart + + data class Entry(val stack: ItemStack, val amount: Int) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/component/ActivationController.kt b/src/main/kotlin/net/shadowfacts/phycon/network/component/ActivationController.kt new file mode 100644 index 0000000..5ec6b2c --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/component/ActivationController.kt @@ -0,0 +1,60 @@ +package net.shadowfacts.phycon.network.component + +import net.minecraft.block.entity.BlockEntity +import net.shadowfacts.phycon.network.packet.RemoteActivationPacket +import net.shadowfacts.phycon.util.ActivationMode + +/** + * @author shadowfacts + */ +class ActivationController( + private val sleepInterval: Long, + private val device: T +) where T: ActivationController.ActivatableDevice, T: BlockEntity { + + var activationMode = ActivationMode.AUTOMATIC + set(value) { + field = value + // when the activation mode changes, reset the remote enabled status + remotelyEnabled = false + } + + private var lastActivation = -sleepInterval + private var remotelyEnabled = false + + fun tick() { + if (activationMode == ActivationMode.AUTOMATIC || remotelyEnabled) { + tryActivate() + } + } + + fun handleRemoteActivation(packet: RemoteActivationPacket) { + if (activationMode != ActivationMode.MANAGED) { + return + } + + when (packet.mode) { + RemoteActivationPacket.Mode.SINGLE -> tryActivate() + RemoteActivationPacket.Mode.ENABLE -> remotelyEnabled = true + RemoteActivationPacket.Mode.DISABLE -> remotelyEnabled = false + } + } + + private fun tryActivate() { + assert(!device.world!!.isClient) + if ((device.counter - lastActivation) < sleepInterval) { + return + } + if (device.activate()) { + lastActivation = device.counter + } + } + + interface ActivatableDevice { + val controller: ActivationController<*> + + val counter: Long + + fun activate(): Boolean + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/component/NetworkStackDispatcher.kt b/src/main/kotlin/net/shadowfacts/phycon/network/component/NetworkStackDispatcher.kt index 1b6d7b8..6d2f3f6 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/component/NetworkStackDispatcher.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/component/NetworkStackDispatcher.kt @@ -33,7 +33,8 @@ interface NetworkStackDispatcher>( - val stack: ItemStack, + var stack: ItemStack, val timestamp: Long ) { val results = mutableSetOf>() @@ -77,5 +79,6 @@ fun > Self.f val finishable = pendingInsertions.filter { it.isFinishable(this) } // finishInsertion removes the object from pendingInsertions finishable.forEach(::finishInsertion) + // todo: do something with remaining? // todo: if a timed-out insertion can't be finished, we should probably retry after some time (exponential backoff?) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/packet/RemoteActivationPacket.kt b/src/main/kotlin/net/shadowfacts/phycon/network/packet/RemoteActivationPacket.kt new file mode 100644 index 0000000..4a92ba8 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/network/packet/RemoteActivationPacket.kt @@ -0,0 +1,14 @@ +package net.shadowfacts.phycon.network.packet + +import net.shadowfacts.phycon.api.util.IPAddress + +/** + * @author shadowfacts + */ +class RemoteActivationPacket(val mode: Mode, source: IPAddress, destination: IPAddress): BasePacket(source, destination) { + enum class Mode { + SINGLE, + ENABLE, + DISABLE + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureActivationMode.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureActivationMode.kt new file mode 100644 index 0000000..3abd003 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureActivationMode.kt @@ -0,0 +1,49 @@ +package net.shadowfacts.phycon.networking + +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.minecraft.network.Packet +import net.minecraft.network.PacketByteBuf +import net.minecraft.server.MinecraftServer +import net.minecraft.server.network.ServerPlayNetworkHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Identifier +import net.minecraft.util.registry.Registry +import net.minecraft.util.registry.RegistryKey +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.component.ActivationController +import net.shadowfacts.phycon.util.ActivationMode + +/** + * @author shadowfacts + */ +object C2SConfigureActivationMode: ServerReceiver { + override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "configure_activation_mode") + + operator fun invoke(be: T): Packet<*> where T: DeviceBlockEntity, T: ActivationController.ActivatableDevice { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(be.world!!.registryKey.value) + buf.writeBlockPos(be.pos) + buf.writeString(be.controller.activationMode.name) + + return createPacket(buf) + } + + override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) { + val dimID = buf.readIdentifier() + val pos = buf.readBlockPos() + val mode = ActivationMode.valueOf(buf.readString()) + + server.execute { + // todo: check the player is close enough + val key = RegistryKey.of(Registry.DIMENSION, dimID) + val world = server.getWorld(key) ?: return@execute + val device = world.getBlockEntity(pos) ?: return@execute + if (device !is ActivationController.ActivatableDevice) return@execute + device.controller.activationMode = mode + device.markDirty() + } + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureRedstoneController.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureRedstoneController.kt new file mode 100644 index 0000000..f51ec0c --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureRedstoneController.kt @@ -0,0 +1,59 @@ +package net.shadowfacts.phycon.networking + +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.minecraft.network.Packet +import net.minecraft.network.PacketByteBuf +import net.minecraft.server.MinecraftServer +import net.minecraft.server.network.ServerPlayNetworkHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Identifier +import net.minecraft.util.registry.Registry +import net.minecraft.util.registry.RegistryKey +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.api.util.IPAddress +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity +import net.shadowfacts.phycon.util.RedstoneMode + +/** + * @author shadowfacts + */ +object C2SConfigureRedstoneController: ServerReceiver { + // todo: it would be nice if there wasn't so much duplication with C2SConfigureActivationMode + + override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "configure_redstone_controller") + + operator fun invoke(be: RedstoneControllerBlockEntity): Packet<*> { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(be.world!!.registryKey.value) + buf.writeBlockPos(be.pos) + buf.writeString(be.redstoneMode.name) + be.managedDevices.forEach { + buf.writeInt(it?.address ?: 0) + } + + return createPacket(buf) + } + + override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) { + val dimID = buf.readIdentifier() + val pos = buf.readBlockPos() + val mode = RedstoneMode.valueOf(buf.readString()) + val managedDevices = Array(5) { null } + (0..4).map { + val v = buf.readInt() + managedDevices[it] = if (v == 0) null else IPAddress(v) + } + + server.execute { + // todo: check if the player is close enough + val key = RegistryKey.of(Registry.DIMENSION, dimID) + val world = server.getWorld(key) ?: return@execute + val device = world.getBlockEntity(pos) as? RedstoneControllerBlockEntity ?: return@execute + device.redstoneMode = mode + device.managedDevices = managedDevices + device.markDirty() + } + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt new file mode 100644 index 0000000..eed71b9 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalUpdateDisplayedItems.kt @@ -0,0 +1,50 @@ +package net.shadowfacts.phycon.networking + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.minecraft.network.Packet +import net.minecraft.network.PacketByteBuf +import net.minecraft.server.MinecraftServer +import net.minecraft.server.network.ServerPlayNetworkHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Identifier +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity +import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler +import net.shadowfacts.phycon.util.SortMode + +/** + * @author shadowfacts + */ +object C2STerminalUpdateDisplayedItems: ServerReceiver { + + override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed") + + operator fun invoke(terminal: TerminalBlockEntity, query: String, sortMode: SortMode): Packet<*> { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(terminal.world!!.registryKey.value) + buf.writeBlockPos(terminal.pos) + + buf.writeString(query) + buf.writeVarInt(sortMode.ordinal) + + return ClientPlayNetworking.createC2SPacket(CHANNEL, buf) + } + + override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) { + val dimID = buf.readIdentifier() + val pos = buf.readBlockPos() + val query = buf.readString() + val sortMode = SortMode.values()[buf.readVarInt()] + + server.execute { + if (player.world.registryKey.value != dimID) return@execute + val screenHandler = player.currentScreenHandler + if (screenHandler !is TerminalScreenHandler) return@execute + if (screenHandler.terminal.pos != pos) return@execute + screenHandler.sendUpdatedItemsToClient(player, query, sortMode) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/ClientReceiver.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/ClientReceiver.kt new file mode 100644 index 0000000..9fa0cf6 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/ClientReceiver.kt @@ -0,0 +1,11 @@ +package net.shadowfacts.phycon.networking + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.minecraft.util.Identifier + +/** + * @author shadowfacts + */ +interface ClientReceiver: ClientPlayNetworking.PlayChannelHandler { + val CHANNEL: Identifier +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt new file mode 100644 index 0000000..db0efe9 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/S2CTerminalUpdateDisplayedItems.kt @@ -0,0 +1,57 @@ +package net.shadowfacts.phycon.networking + +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.ClientPlayNetworkHandler +import net.minecraft.network.Packet +import net.minecraft.network.PacketByteBuf +import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity +import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler +import net.shadowfacts.phycon.util.SortMode + +/** + * @author shadowfacts + */ +object S2CTerminalUpdateDisplayedItems: ClientReceiver { + override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL + + operator fun invoke(terminal: TerminalBlockEntity, entries: List, query: String, sortMode: SortMode): Packet<*> { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(terminal.world!!.registryKey.value) + buf.writeBlockPos(terminal.pos) + + buf.writeVarInt(entries.size) + for (e in entries) { + buf.writeItemStack(e.stack) + buf.writeVarInt(e.amount) + } + + buf.writeString(query) + buf.writeVarInt(sortMode.ordinal) + + return ServerPlayNetworking.createS2CPacket(CHANNEL, buf) + } + + override fun receive(client: MinecraftClient, handler: ClientPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) { + val dimID = buf.readIdentifier() + val pos = buf.readBlockPos() + val entryCount = buf.readVarInt() + val entries = ArrayList(entryCount) + for (i in 0 until entryCount) { + entries.add(TerminalScreenHandler.Entry(buf.readItemStack(), buf.readVarInt())) + } + val query = buf.readString() + val sortMode = SortMode.values()[buf.readVarInt()] + + client.execute { + if (client.player!!.world.registryKey.value != dimID) return@execute + val screenHandler = client.player!!.currentScreenHandler + if (screenHandler !is TerminalScreenHandler) return@execute + if (screenHandler.terminal.pos != pos) return@execute + screenHandler.receivedUpdatedItemsFromServer(entries, query, sortMode) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/ServerReceiver.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/ServerReceiver.kt index 0ea41e9..f940b49 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/networking/ServerReceiver.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/ServerReceiver.kt @@ -1,6 +1,9 @@ package net.shadowfacts.phycon.networking +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking +import net.minecraft.network.Packet +import net.minecraft.network.PacketByteBuf import net.minecraft.util.Identifier /** @@ -8,4 +11,8 @@ import net.minecraft.util.Identifier */ interface ServerReceiver: ServerPlayNetworking.PlayChannelHandler { val CHANNEL: Identifier -} \ No newline at end of file + + fun createPacket(buf: PacketByteBuf): Packet<*> { + return ClientPlayNetworking.createC2SPacket(CHANNEL, buf) + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/ActivatableDeviceConsoleScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/ActivatableDeviceConsoleScreen.kt new file mode 100644 index 0000000..b1ffa81 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/ActivatableDeviceConsoleScreen.kt @@ -0,0 +1,64 @@ +package net.shadowfacts.phycon.screen + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.math.MatrixStack +import net.shadowfacts.phycon.network.DeviceBlockEntity +import net.shadowfacts.phycon.network.component.ActivationController +import net.shadowfacts.phycon.networking.C2SConfigureActivationMode +import net.shadowfacts.phycon.util.ActivationMode +import net.shadowfacts.phycon.util.next +import org.lwjgl.glfw.GLFW + +/** + * @author shadowfacts + */ +class ActivatableDeviceConsoleScreen( + val device: T +): Screen(device.cachedState.block.name) where T: DeviceBlockEntity, T: ActivationController.ActivatableDevice { + + private val backgroundWidth = 252 + private val backgroundHeight = 222 + + override fun init() { + super.init() + + buttons.clear() + + val minX = (width - backgroundWidth) / 2 + val minY = (height - backgroundHeight) / 2 + + val mode = EnumButton(device.controller::activationMode, minX + 5, minY + 25, 55, 20) { + client!!.player!!.networkHandler.sendPacket(C2SConfigureActivationMode(device)) + } + addButton(mode) + } + + override fun isPauseScreen() = false + + override fun keyPressed(key: Int, j: Int, k: Int): Boolean { + if (key == GLFW.GLFW_KEY_E) { + onClose() + return true + } + return super.keyPressed(key, j, k) + } + + override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { + renderBackground(matrixStack) + + val minX = (width - backgroundWidth) / 2 + val minY = (height - backgroundHeight) / 2 + + RenderSystem.color4f(1f, 1f, 1f, 1f) + client!!.textureManager.bindTexture(DeviceConsoleScreen.BACKGROUND) + drawTexture(matrixStack, minX, minY, 0, 0, backgroundWidth, backgroundHeight) + + super.render(matrixStack, mouseX, mouseY, delta) + + textRenderer.draw(matrixStack, "IP Address: ${device.ipAddress}", minX + 5f, minY + 5f, 0x404040) + textRenderer.draw(matrixStack, "MAC Address: ${device.macAddress}", minX + 5f, minY + 15f, 0x404040) + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/DeviceConsoleScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/DeviceConsoleScreen.kt index 37f9751..2336157 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/screen/DeviceConsoleScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/DeviceConsoleScreen.kt @@ -3,6 +3,8 @@ package net.shadowfacts.phycon.screen import net.minecraft.client.gui.screen.Screen import net.minecraft.client.util.math.MatrixStack import net.minecraft.text.TranslatableText +import net.minecraft.util.Identifier +import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.network.DeviceBlockEntity import org.lwjgl.glfw.GLFW @@ -11,18 +13,18 @@ import org.lwjgl.glfw.GLFW */ class DeviceConsoleScreen( val device: DeviceBlockEntity, -): Screen(TranslatableText("item.phycon.onsole")) { +): Screen(TranslatableText("item.phycon.console")) { - override fun init() { - super.init() + companion object { + val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/console.png") } override fun isPauseScreen() = false override fun keyPressed(key: Int, j: Int, k: Int): Boolean { if (key == GLFW.GLFW_KEY_E) { - onClose(); - return true; + onClose() + return true } return super.keyPressed(key, j, k) } @@ -35,4 +37,4 @@ class DeviceConsoleScreen( drawCenteredString(matrixStack, textRenderer, device.macAddress.toString(), width / 2, height / 2 - 5, 0xffffff) drawCenteredString(matrixStack, textRenderer, device.ipAddress.toString(), width / 2, height / 2 + 5, 0xffffff) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/EnumButton.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/EnumButton.kt new file mode 100644 index 0000000..79e2608 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/EnumButton.kt @@ -0,0 +1,49 @@ +package net.shadowfacts.phycon.screen + +import net.minecraft.client.gui.widget.AbstractPressableButtonWidget +import net.minecraft.text.Text +import net.shadowfacts.phycon.util.FriendlyNameable +import net.shadowfacts.phycon.util.RotatableEnum +import net.shadowfacts.phycon.util.next +import net.shadowfacts.phycon.util.prev +import kotlin.reflect.KMutableProperty + +/** + * @author shadowfacts + */ +class EnumButton( + val prop: KMutableProperty, + x: Int, + y: Int, + width: Int, + height: Int, + val onChange: () -> Unit +): AbstractPressableButtonWidget( + x, + y, + width, + height, + prop.getter.call().friendlyName +) where E: Enum, E: RotatableEnum, E: FriendlyNameable { + + private var currentButton: Int? = null + + override fun mouseClicked(d: Double, e: Double, button: Int): Boolean { + currentButton = button + val res = super.mouseClicked(d, e, button) + currentButton = null + return res + } + + override fun isValidClickButton(i: Int): Boolean { + return i == 0 || i == 1 + } + + override fun onPress() { + val newVal = if ((currentButton ?: 0) == 0) prop.getter.call().next else prop.getter.call().prev + prop.setter.call(newVal) + message = newVal.friendlyName + onChange() + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/RedstoneControllerConsoleScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/RedstoneControllerConsoleScreen.kt new file mode 100644 index 0000000..9f30544 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/RedstoneControllerConsoleScreen.kt @@ -0,0 +1,96 @@ +package net.shadowfacts.phycon.screen + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.TextFieldWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.LiteralText +import net.shadowfacts.phycon.api.util.IPAddress +import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity +import net.shadowfacts.phycon.networking.C2SConfigureRedstoneController +import net.shadowfacts.phycon.util.next +import org.lwjgl.glfw.GLFW + +/** + * @author shadowfacts + */ +class RedstoneControllerConsoleScreen( + val device: RedstoneControllerBlockEntity +): Screen(device.cachedState.block.name) { + + private val backgroundWidth = 252 + private val backgroundHeight = 222 + + private val ipAddressTextFields = mutableListOf() + + override fun init() { + super.init() + + buttons.clear() + ipAddressTextFields.clear() + + val minX = (width - backgroundWidth) / 2 + val minY = (height - backgroundHeight) / 2 + + val mode = EnumButton(device::redstoneMode, minX + 5, minY + 25, 75, 20) { + client!!.player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device)) + } + addButton(mode) + + for (i in 0 until 5) { + // todo: field name + val field = TextFieldWidget(textRenderer, minX + 5, minY + 50 + 22 * i, backgroundWidth / 2, 20, LiteralText("")) + field.setMaxLength(15) + field.setHasBorder(true) + field.isVisible = true + field.setEditableColor(0xffffff) + field.text = device.managedDevices[i]?.toString() + field.setChangedListener { newVal -> + device.managedDevices[i] = IPAddress.parse(newVal) + client!!.player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device)) + } + addChild(field) + ipAddressTextFields.add(field) + } + } + + override fun isPauseScreen() = false + + override fun keyPressed(key: Int, j: Int, k: Int): Boolean { + if (key == GLFW.GLFW_KEY_E) { + onClose() + return true + } + return super.keyPressed(key, j, k) + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + val clickedField = ipAddressTextFields.find { it.x <= mouseX && it.x + it.width >= mouseX && it.y <= mouseY && it.y + it.height >= mouseY } + if (clickedField != null) { + ipAddressTextFields.forEach { + if (it !== clickedField) it.setSelected(false) + } + } + return super.mouseClicked(mouseX, mouseY, button) + } + + override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { + renderBackground(matrixStack) + + val minX = (width - backgroundWidth) / 2 + val minY = (height - backgroundHeight) / 2 + + RenderSystem.color4f(1f, 1f, 1f, 1f) + client!!.textureManager.bindTexture(DeviceConsoleScreen.BACKGROUND) + drawTexture(matrixStack, minX, minY, 0, 0, backgroundWidth, backgroundHeight) + + super.render(matrixStack, mouseX, mouseY, delta) + + ipAddressTextFields.forEach { it.render(matrixStack, mouseX, mouseY, delta) } + + textRenderer.draw(matrixStack, "IP Address: ${device.ipAddress}", minX + 5f, minY + 5f, 0x404040) + textRenderer.draw(matrixStack, "MAC Address: ${device.macAddress}", minX + 5f, minY + 15f, 0x404040) + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/ActivationMode.kt b/src/main/kotlin/net/shadowfacts/phycon/util/ActivationMode.kt new file mode 100644 index 0000000..2db1810 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/ActivationMode.kt @@ -0,0 +1,15 @@ +package net.shadowfacts.phycon.util + +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText + +/** + * @author shadowfacts + */ +enum class ActivationMode: RotatableEnum, FriendlyNameable { + AUTOMATIC, + MANAGED; + + override val friendlyName: Text + get() = TranslatableText("gui.phycon.activation_mode.${name.toLowerCase()}") +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/FriendlyNameable.kt b/src/main/kotlin/net/shadowfacts/phycon/util/FriendlyNameable.kt new file mode 100644 index 0000000..978d21a --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/FriendlyNameable.kt @@ -0,0 +1,10 @@ +package net.shadowfacts.phycon.util + +import net.minecraft.text.Text + +/** + * @author shadowfacts + */ +interface FriendlyNameable { + val friendlyName: Text +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/RedstoneMode.kt b/src/main/kotlin/net/shadowfacts/phycon/util/RedstoneMode.kt new file mode 100644 index 0000000..0fe895a --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/RedstoneMode.kt @@ -0,0 +1,30 @@ +package net.shadowfacts.phycon.util + +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText + +/** + * @author shadowfacts + */ +enum class RedstoneMode: RotatableEnum, FriendlyNameable { + HIGH, + LOW, + TOGGLE, + RISING_EDGE, + FALLING_EDGE; + + val isDiscrete: Boolean + get() = when (this) { + TOGGLE, RISING_EDGE, FALLING_EDGE -> true + else -> false + } + + val isContinuous: Boolean + get() = when (this) { + HIGH, LOW -> true + else -> false + } + + override val friendlyName: Text + get() = TranslatableText("gui.phycon.redstone_mode.${name.toLowerCase()}") +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/RotatableEnum.kt b/src/main/kotlin/net/shadowfacts/phycon/util/RotatableEnum.kt new file mode 100644 index 0000000..75d40a3 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/RotatableEnum.kt @@ -0,0 +1,13 @@ +package net.shadowfacts.phycon.util + +/** + * @author shadowfacts + */ +interface RotatableEnum { +} + +val E.prev: E where E: Enum, E: RotatableEnum + get() = javaClass.enumConstants[(ordinal - 1 + javaClass.enumConstants.size) % javaClass.enumConstants.size] + +val E.next: E where E: Enum, E: RotatableEnum + get() = javaClass.enumConstants[(ordinal + 1) % javaClass.enumConstants.size] diff --git a/src/main/kotlin/net/shadowfacts/phycon/util/SortMode.kt b/src/main/kotlin/net/shadowfacts/phycon/util/SortMode.kt new file mode 100644 index 0000000..fbcde94 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/util/SortMode.kt @@ -0,0 +1,21 @@ +package net.shadowfacts.phycon.util + +import net.minecraft.text.LiteralText +import net.minecraft.text.Text + +/** + * @author shadowfacts + */ +enum class SortMode: RotatableEnum { + COUNT_HIGH_FIRST, + COUNT_LOW_FIRST, + ALPHABETICAL; + + val tooltip: Text + get() = when (this) { + COUNT_HIGH_FIRST -> LiteralText("Count, highest first") + COUNT_LOW_FIRST -> LiteralText("Count, lowest first") + ALPHABETICAL -> LiteralText("Name") + } + +} diff --git a/src/main/resources/assets/phycon/blockstates/network_interface.json b/src/main/resources/assets/phycon/blockstates/network_interface.json index b1166ed..6d851e4 100644 --- a/src/main/resources/assets/phycon/blockstates/network_interface.json +++ b/src/main/resources/assets/phycon/blockstates/network_interface.json @@ -140,7 +140,6 @@ }, { - "when": {"cable_connection": "down", "facing": "east"}, "apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 270 } }, diff --git a/src/main/resources/assets/phycon/blockstates/redstone_controller.json b/src/main/resources/assets/phycon/blockstates/redstone_controller.json new file mode 100644 index 0000000..196adf8 --- /dev/null +++ b/src/main/resources/assets/phycon/blockstates/redstone_controller.json @@ -0,0 +1,183 @@ +{ + "multipart": [ + { + "apply": { "model": "phycon:block/cable_center" } + }, + { + "when": { "facing": "down", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off" } + }, + { + "when": { "facing": "up", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off", "x": 180 } + }, + { + "when": { "facing": "north", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off", "x": 270 } + }, + { + "when": { "facing": "south", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off", "x": 90 } + }, + { + "when": { "facing": "west", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off", "x": 90, "y": 90 } + }, + { + "when": { "facing": "east", "lit": "false" }, + "apply": { "model": "phycon:block/redstone_controller_side_off", "x": 90, "y": 270 } + }, + { + "when": { "facing": "down", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on" } + }, + { + "when": { "facing": "up", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on", "x": 180 } + }, + { + "when": { "facing": "north", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on", "x": 270 } + }, + { + "when": { "facing": "south", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on", "x": 90 } + }, + { + "when": { "facing": "west", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on", "x": 90, "y": 90 } + }, + { + "when": { "facing": "east", "lit": "true" }, + "apply": { "model": "phycon:block/redstone_controller_side_on", "x": 90, "y": 270 } + }, + + { + "when": { "cable_connection": "up", "facing": "down" }, + "apply": { "model": "phycon:block/interface_cable_straight" } + }, + { + "when": { "cable_connection": "down", "facing": "up" }, + "apply": { "model": "phycon:block/interface_cable_straight", "x": 180 } + }, + { + "when": { "cable_connection": "north", "facing": "south" }, + "apply": { "model": "phycon:block/interface_cable_straight", "x": 90 } + }, + { + "when": { "cable_connection": "south", "facing": "north" }, + "apply": { "model": "phycon:block/interface_cable_straight", "x": 270 } + }, + { + "when": { "cable_connection": "west", "facing": "east" }, + "apply": { "model": "phycon:block/interface_cable_straight", "x": 90, "y": 270 } + }, + { + "when": { "cable_connection": "east", "facing": "west" }, + "apply": { "model": "phycon:block/interface_cable_straight", "x": 90, "y": 90 } + }, + + { + "when": {"cable_connection": "north", "facing": "down"}, + "apply": { "model": "phycon:block/interface_cable_corner" } + }, + { + "when": {"cable_connection": "east", "facing": "down"}, + "apply": { "model": "phycon:block/interface_cable_corner", "y": 90 } + }, + { + "when": {"cable_connection": "south", "facing": "down"}, + "apply": { "model": "phycon:block/interface_cable_corner", "y": 180 } + }, + { + "when": {"cable_connection": "west", "facing": "down"}, + "apply": { "model": "phycon:block/interface_cable_corner", "y": 270 } + }, + + { + "when": {"cable_connection": "north", "facing": "up"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 180, "y": 180 } + }, + { + "when": {"cable_connection": "east", "facing": "up"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 180, "y": 270 } + }, + { + "when": {"cable_connection": "south", "facing": "up"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 180 } + }, + { + "when": {"cable_connection": "west", "facing": "up"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 180, "y": 90 } + }, + + { + "when": {"cable_connection": "down", "facing": "north"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 180 } + }, + { + "when": {"cable_connection": "up", "facing": "north"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 270 } + }, + { + "when": {"cable_connection": "west", "facing": "north"}, + "apply": { "model": "phycon:block/interface_cable_corner_2" } + }, + { + "when": {"cable_connection": "east", "facing": "north"}, + "apply": { "model": "phycon:block/interface_cable_corner_2", "x": 180, "y": 180 } + }, + + { + "when": {"cable_connection": "down", "facing": "south"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 90 } + }, + { + "when": {"cable_connection": "up", "facing": "south"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 270, "y": 180 } + }, + { + "when": {"cable_connection": "west", "facing": "south"}, + "apply": { "model": "phycon:block/interface_cable_corner_3" } + }, + { + "when": {"cable_connection": "east", "facing": "south"}, + "apply": { "model": "phycon:block/interface_cable_corner_3", "x": 180, "y": 180 } + }, + + { + + "when": {"cable_connection": "down", "facing": "west"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 90 } + }, + { + "when": {"cable_connection": "up", "facing": "west"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 270, "y": 270 } + }, + { + "when": {"cable_connection": "north", "facing": "west"}, + "apply": { "model": "phycon:block/interface_cable_corner_3", "y": 90 } + }, + { + "when": {"cable_connection": "south", "facing": "west"}, + "apply": { "model": "phycon:block/interface_cable_corner_3", "x": 180, "y": 270 } + }, + + { + "when": {"cable_connection": "down", "facing": "east"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 90, "y": 270 } + }, + { + "when": {"cable_connection": "up", "facing": "east"}, + "apply": { "model": "phycon:block/interface_cable_corner", "x": 270, "y": 90 } + }, + { + "when": {"cable_connection": "north", "facing": "east"}, + "apply": { "model": "phycon:block/interface_cable_corner_2", "y": 90 } + }, + { + "when": {"cable_connection": "south", "facing": "east"}, + "apply": { "model": "phycon:block/interface_cable_corner_2", "x": 180, "y": 270 } + } + ] +} diff --git a/src/main/resources/assets/phycon/lang/en_us.json b/src/main/resources/assets/phycon/lang/en_us.json index 0ae03a5..ea4ce28 100644 --- a/src/main/resources/assets/phycon/lang/en_us.json +++ b/src/main/resources/assets/phycon/lang/en_us.json @@ -5,7 +5,17 @@ "block.phycon.cable": "Cable", "block.phycon.extractor": "Inventory Extractor", "block.phycon.miner": "Block Miner", + "block.phycon.redstone_controller": "Redstone Controller", "item.phycon.screwdriver": "Screwdriver", - "item.phycon.console": "Console" -} \ No newline at end of file + "item.phycon.console": "Console", + + "gui.phycon.terminal_buffer": "Buffer", + "gui.phycon.redstone_mode.high": "High", + "gui.phycon.redstone_mode.low": "Low", + "gui.phycon.redstone_mode.toggle": "Toggle", + "gui.phycon.redstone_mode.rising_edge": "Rising Edge", + "gui.phycon.redstone_mode.falling_edge": "Falling Edge", + "gui.phycon.activation_mode.automatic": "Automatic", + "gui.phycon.activation_mode.managed": "Managed" +} diff --git a/src/main/resources/assets/phycon/models/block/cable_center.json b/src/main/resources/assets/phycon/models/block/cable_center.json index 05ecfd8..cfda031 100644 --- a/src/main/resources/assets/phycon/models/block/cable_center.json +++ b/src/main/resources/assets/phycon/models/block/cable_center.json @@ -1,6 +1,7 @@ { "textures": { - "center": "phycon:block/cable_cap_end" + "center": "phycon:block/cable_cap_end", + "particle": "#center" }, "elements": [ { @@ -39,4 +40,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/main/resources/assets/phycon/models/block/redstone_controller_side_off.json b/src/main/resources/assets/phycon/models/block/redstone_controller_side_off.json new file mode 100644 index 0000000..2c26a3b --- /dev/null +++ b/src/main/resources/assets/phycon/models/block/redstone_controller_side_off.json @@ -0,0 +1,33 @@ +{ + "parent": "block/block", + "textures": { + "cable": "phycon:block/cable_straight", + "front": "phycon:block/redstone_controller_front_off", + "back": "phycon:block/redstone_controller_back", + "side": "phycon:block/redstone_controller_back" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 3, 16], + "faces": { + "down": { "texture": "#front" }, + "up": { "texture": "#back" }, + "north": { "texture": "#side" }, + "south": { "texture": "#side" }, + "west": { "texture": "#side" }, + "east": { "texture": "#side" } + } + }, + { + "from": [6, 3, 6], + "to": [10, 6, 10], + "faces": { + "north": { "texture": "#cable" }, + "south": { "texture": "#cable" }, + "west": { "texture": "#cable" }, + "east": { "texture": "#cable" } + } + } + ] +} diff --git a/src/main/resources/assets/phycon/models/block/redstone_controller_side_on.json b/src/main/resources/assets/phycon/models/block/redstone_controller_side_on.json new file mode 100644 index 0000000..65b44a4 --- /dev/null +++ b/src/main/resources/assets/phycon/models/block/redstone_controller_side_on.json @@ -0,0 +1,33 @@ +{ + "parent": "block/block", + "textures": { + "cable": "phycon:block/cable_straight", + "front": "phycon:block/redstone_controller_front_on", + "back": "phycon:block/redstone_controller_back", + "side": "phycon:block/redstone_controller_back" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 3, 16], + "faces": { + "down": { "texture": "#front" }, + "up": { "texture": "#back" }, + "north": { "texture": "#side" }, + "south": { "texture": "#side" }, + "west": { "texture": "#side" }, + "east": { "texture": "#side" } + } + }, + { + "from": [6, 3, 6], + "to": [10, 6, 10], + "faces": { + "north": { "texture": "#cable" }, + "south": { "texture": "#cable" }, + "west": { "texture": "#cable" }, + "east": { "texture": "#cable" } + } + } + ] +} diff --git a/src/main/resources/assets/phycon/models/item/redstone_controller.json b/src/main/resources/assets/phycon/models/item/redstone_controller.json new file mode 100644 index 0000000..e4bd117 --- /dev/null +++ b/src/main/resources/assets/phycon/models/item/redstone_controller.json @@ -0,0 +1,51 @@ +{ + "parent": "block/block", + "textures": { + "front": "phycon:block/redstone_controller_front_off", + "back": "phycon:block/redstone_controller_back", + "side": "phycon:block/redstone_controller_back", + "cable_end": "phycon:block/cable_cap_end", + "cable_side": "phycon:block/cable_straight" + }, + "elements": [ + { + "_comment": "cable center", + "from": [6, 6, 3], + "to": [10, 10, 16], + "faces": { + "down": { "texture": "#cable_side" }, + "up": { "texture": "#cable_side" }, + "south": { "texture": "#cable_end" }, + "west": { "texture": "#cable_side", "rotation": 90, "uv": [6, 0, 10, 16] }, + "east": { "texture": "#cable_side", "rotation": 90, "uv": [6, 0, 10, 16] } + } + }, + { + "_comment": "redstone controller side", + "from": [0, 0, 0], + "to": [16, 16, 3], + "faces": { + "down": { "texture": "#side" }, + "up": { "texture": "#side" }, + "north": { "texture": "#front" }, + "south": { "texture": "#back" }, + "west": { "texture": "#side" }, + "east": { "texture": "#side" } + } + } + ], + "_test": [ + { + "_comment": "cable center", + "from": [6, 6, 3], + "to": [10, 10, 16], + "faces": { + "down": { "texture": "#cable_side", "rotation": 270, "uv": [4, 6, 10, 10] }, + "up": { "texture": "#cable_side", "rotation": 90, "uv": [4, 6, 10, 10] }, + "south": { "texture": "#cable_end" }, + "west": { "texture": "#cable_side", "uv": [4, 6, 10, 10] }, + "east": { "texture": "#cable_side", "rotation": 180, "uv": [4, 6, 10, 10] } + } + } + ] +} diff --git a/src/main/resources/assets/phycon/textures/block/redstone_controller_back.png b/src/main/resources/assets/phycon/textures/block/redstone_controller_back.png new file mode 100644 index 0000000..14643d1 Binary files /dev/null and b/src/main/resources/assets/phycon/textures/block/redstone_controller_back.png differ diff --git a/src/main/resources/assets/phycon/textures/block/redstone_controller_front_off.png b/src/main/resources/assets/phycon/textures/block/redstone_controller_front_off.png new file mode 100644 index 0000000..7099eb9 Binary files /dev/null and b/src/main/resources/assets/phycon/textures/block/redstone_controller_front_off.png differ diff --git a/src/main/resources/assets/phycon/textures/block/redstone_controller_front_on.png b/src/main/resources/assets/phycon/textures/block/redstone_controller_front_on.png new file mode 100644 index 0000000..227353c Binary files /dev/null and b/src/main/resources/assets/phycon/textures/block/redstone_controller_front_on.png differ diff --git a/src/main/resources/assets/phycon/textures/gui/console.png b/src/main/resources/assets/phycon/textures/gui/console.png new file mode 100644 index 0000000..b92218c Binary files /dev/null and b/src/main/resources/assets/phycon/textures/gui/console.png differ diff --git a/src/main/resources/assets/phycon/textures/gui/terminal.png b/src/main/resources/assets/phycon/textures/gui/terminal.png index 7fd4d13..c2d10a7 100644 Binary files a/src/main/resources/assets/phycon/textures/gui/terminal.png and b/src/main/resources/assets/phycon/textures/gui/terminal.png differ diff --git a/src/main/resources/assets/phycon/textures/gui/terminal_amount.png b/src/main/resources/assets/phycon/textures/gui/terminal_amount.png new file mode 100644 index 0000000..3999831 Binary files /dev/null and b/src/main/resources/assets/phycon/textures/gui/terminal_amount.png differ