From e13154943eddf0acf01c41fe77fdc563d72258c9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 28 Feb 2021 17:56:25 -0500 Subject: [PATCH] Add Inserter --- .../shadowfacts/cacao/CacaoHandledScreen.kt | 4 +- .../phycon/PhysicalConnectivity.kt | 1 + .../phycon/PhysicalConnectivityClient.kt | 4 +- .../phycon/block/extractor/ExtractorBlock.kt | 2 +- .../phycon/block/inserter/InserterBlock.kt | 143 +++++++++++++++ .../block/inserter/InserterBlockEntity.kt | 168 ++++++++++++++++++ .../phycon/block/inserter/InserterScreen.kt | 123 +++++++++++++ .../block/inserter/InserterScreenHandler.kt | 122 +++++++++++++ .../block/terminal/TerminalBlockEntity.kt | 2 +- .../phycon/init/PhyBlockEntities.kt | 4 + .../net/shadowfacts/phycon/init/PhyBlocks.kt | 3 + .../net/shadowfacts/phycon/init/PhyItems.kt | 3 + .../net/shadowfacts/phycon/init/PhyScreens.kt | 6 +- .../networking/C2SConfigureInserterAmount.kt | 45 +++++ .../assets/phycon/blockstates/inserter.json | 29 +++ .../resources/assets/phycon/lang/en_us.json | 1 + .../assets/phycon/models/block/extractor.json | 18 +- .../assets/phycon/models/block/inserter.json | 54 ++++++ .../assets/phycon/textures/gui/inserter.png | Bin 0 -> 6250 bytes 19 files changed, 718 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlock.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlockEntity.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreen.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreenHandler.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureInserterAmount.kt create mode 100644 src/main/resources/assets/phycon/blockstates/inserter.json create mode 100644 src/main/resources/assets/phycon/models/block/inserter.json create mode 100644 src/main/resources/assets/phycon/textures/gui/inserter.png diff --git a/src/main/kotlin/net/shadowfacts/cacao/CacaoHandledScreen.kt b/src/main/kotlin/net/shadowfacts/cacao/CacaoHandledScreen.kt index ef10a71..18352f8 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/CacaoHandledScreen.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/CacaoHandledScreen.kt @@ -16,7 +16,7 @@ import java.util.* /** * @author shadowfacts */ -class CacaoHandledScreen( +open class CacaoHandledScreen( handler: Handler, playerInv: PlayerInventory, title: Text, @@ -90,4 +90,4 @@ class CacaoHandledScreen( } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt index d43573f..653ef3c 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt @@ -28,6 +28,7 @@ object PhysicalConnectivity: ModInitializer { registerGlobalReceiver(C2STerminalUpdateDisplayedItems) registerGlobalReceiver(C2SConfigureActivationMode) registerGlobalReceiver(C2SConfigureRedstoneController) + registerGlobalReceiver(C2SConfigureInserterAmount) } 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 145a624..c92be8b 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt @@ -5,6 +5,7 @@ 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.block.inserter.InserterScreen import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.block.terminal.TerminalScreen @@ -17,9 +18,8 @@ import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems object PhysicalConnectivityClient: ClientModInitializer { override fun onInitializeClient() { - BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent()) - ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) + ScreenRegistry.register(PhyScreens.INSERTER_SCREEN_HANDLER, ::InserterScreen) registerGlobalReceiver(S2CTerminalUpdateDisplayedItems) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/extractor/ExtractorBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/block/extractor/ExtractorBlock.kt index 4e3747a..21d3871 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/extractor/ExtractorBlock.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/extractor/ExtractorBlock.kt @@ -88,7 +88,7 @@ class ExtractorBlock: DeviceBlock(Settings.of(Material.MET override fun createBlockEntity(world: BlockView) = ExtractorBlockEntity() override fun getPlacementState(context: ItemPlacementContext): BlockState { - val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerFacing.opposite + val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite return defaultState.with(FACING, facing) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlock.kt b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlock.kt new file mode 100644 index 0000000..8fd6690 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlock.kt @@ -0,0 +1,143 @@ +package net.shadowfacts.phycon.block.inserter + +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.Material +import net.minecraft.block.ShapeContext +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.ItemPlacementContext +import net.minecraft.item.ItemStack +import net.minecraft.network.PacketByteBuf +import net.minecraft.screen.ScreenHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.state.StateManager +import net.minecraft.state.property.Properties +import net.minecraft.text.Text +import net.minecraft.util.ActionResult +import net.minecraft.util.Hand +import net.minecraft.util.Identifier +import net.minecraft.util.hit.BlockHitResult +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.Interface +import net.shadowfacts.phycon.block.DeviceBlock +import net.shadowfacts.phycon.block.extractor.ExtractorBlock +import java.util.* + +/** + * @author shadowfacts + */ +class InserterBlock: DeviceBlock(Settings.of(Material.METAL)) { + companion object { + val ID = Identifier(PhysicalConnectivity.MODID, "inserter") + val FACING = Properties.FACING + private val INSERTER_SHAPES = mutableMapOf() + + init { + val components = arrayOf( + doubleArrayOf(4.0, 0.0, 4.0, 12.0, 2.0, 12.0), + doubleArrayOf(2.0, 2.0, 2.0, 14.0, 4.0, 14.0), + doubleArrayOf(0.0, 4.0, 0.0, 16.0, 6.0, 16.0), + doubleArrayOf(6.0, 6.0, 6.0, 10.0, 16.0, 10.0) + ) + val directions = arrayOf( + Triple(Direction.DOWN, null, false), + Triple(Direction.UP, null, true), + Triple(Direction.NORTH, 2, false), + Triple(Direction.SOUTH, 2, true), + Triple(Direction.WEST, 1, false), + Triple(Direction.EAST, 1, true), + ) + for ((dir, rotate, flip) in directions) { + val shapes = components.map { it -> + val arr = it.copyOf() + if (rotate != null) { + for (i in 0 until 3) { + arr[i] = it[(i + rotate) % 3] + arr[3 + i] = it[3 + ((i + rotate) % 3)] + } + } + if (flip) { + for (i in arr.indices) { + arr[i] = 16.0 - arr[i] + } + } + createCuboidShape(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]) + } + INSERTER_SHAPES[dir] = shapes.reduce { a, b -> VoxelShapes.union(a, b) } + } + } + } + + override fun getNetworkConnectedSides(state: BlockState, world: WorldAccess, pos: BlockPos): Collection { + return EnumSet.of(state[FACING].opposite) + } + + override fun getNetworkInterfaceForSide(side: Direction, state: BlockState, world: WorldAccess, pos: BlockPos): Interface? { + return if (side == state[FACING].opposite) { + getBlockEntity(world, pos) + } else { + null + } + } + + override fun appendProperties(builder: StateManager.Builder) { + super.appendProperties(builder) + builder.add(FACING) + } + + override fun createBlockEntity(world: BlockView) = InserterBlockEntity() + + override fun getPlacementState(context: ItemPlacementContext): BlockState { + val facing = if (context.player?.isSneaking == true) context.side.opposite else context.playerLookDirection.opposite + return defaultState.with(FACING, facing) + } + + override fun getOutlineShape(state: BlockState, world: BlockView, pos: BlockPos, context: ShapeContext): VoxelShape { + return INSERTER_SHAPES[state[FACING]]!! + } + + override fun onPlaced(world: World, pos: BlockPos, state: BlockState, entity: LivingEntity?, stack: ItemStack) { + if (!world.isClient) { + getBlockEntity(world, pos)!!.updateInventory() + } + } + + override fun neighborUpdate(state: BlockState, world: World, pos: BlockPos, neighborBlock: Block, neighborPos: BlockPos, bl: Boolean) { + if (!world.isClient) { + getBlockEntity(world, pos)!!.updateInventory() + } + } + + override fun onUse(state: BlockState, world: World, pos: BlockPos, player: PlayerEntity, hand: Hand, hitResult: BlockHitResult): ActionResult { + if (!world.isClient) { + val be = getBlockEntity(world, pos)!! + + be.sync() + + val factory = object: ExtendedScreenHandlerFactory { + override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler { + return InserterScreenHandler(syncId, playerInv, be) + } + + override fun getDisplayName() = this@InserterBlock.name + + override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { + buf.writeBlockPos(be.pos) + } + } + player.openHandledScreen(factory) + } + return ActionResult.SUCCESS + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlockEntity.kt new file mode 100644 index 0000000..f6e9030 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterBlockEntity.kt @@ -0,0 +1,168 @@ +package net.shadowfacts.phycon.block.inserter + +import alexiil.mc.lib.attributes.SearchOptions +import alexiil.mc.lib.attributes.Simulation +import alexiil.mc.lib.attributes.item.ItemAttributes +import alexiil.mc.lib.attributes.item.ItemInsertable +import alexiil.mc.lib.attributes.item.ItemStackUtil +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.block.DeviceBlockEntity +import net.shadowfacts.phycon.component.ActivationController +import net.shadowfacts.phycon.component.ItemStackPacketHandler +import net.shadowfacts.phycon.component.NetworkStackProvider +import net.shadowfacts.phycon.component.handleItemStack +import net.shadowfacts.phycon.init.PhyBlockEntities +import net.shadowfacts.phycon.packet.* +import net.shadowfacts.phycon.util.ActivationMode +import kotlin.math.min + +/** + * @author shadowfacts + */ +class InserterBlockEntity: DeviceBlockEntity(PhyBlockEntities.INSERTER), + ItemStackPacketHandler, + ActivationController.ActivatableDevice { + + companion object { + val SLEEP_TIME = 40L + val REQUEST_TIMEOUT = 40 + } + + private val facing: Direction + get() = cachedState[InserterBlock.FACING] + + private var inventory: ItemInsertable? = null + private var currentRequest: PendingExtractRequest? = null + var stackToExtract = ItemStack.EMPTY + var amountToExtract = 1 + override val controller = ActivationController(SLEEP_TIME, this) + + fun updateInventory() { + val offsetPos = pos.offset(facing) + val option = SearchOptions.inDirection(facing) + inventory = ItemAttributes.INSERTABLE.getFirstOrNull(world, offsetPos, option) + } + + private fun getInventory(): ItemInsertable? { + if (inventory == null) updateInventory() + return inventory + } + + override fun handle(packet: Packet) { + when (packet) { + is RemoteActivationPacket -> controller.handleRemoteActivation(packet) + is StackLocationPacket -> handleStackLocation(packet) + is ItemStackPacket -> handleItemStack(packet) + } + } + + override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { + val inventory = getInventory() + return if (inventory != null) { + inventory.attemptInsertion(packet.stack, Simulation.ACTION) + } else { + // no inventory, entire stack remains + packet.stack + } + } + + private fun handleStackLocation(packet: StackLocationPacket) { + val request = currentRequest + if (request != null && ItemStackUtil.areEqualIgnoreAmounts(request.stack, packet.stack)) { + request.results.add(packet.amount to packet.stackProvider) + if (request.isFinishable(counter)) { + finishRequest() + } + } + } + + override fun tick() { + super.tick() + + if (!world!!.isClient) { + controller.tick() + + val request = currentRequest + if (request != null) { + if (request.isFinishable(counter)) { + finishRequest() + } else if (counter - request.timestamp >= REQUEST_TIMEOUT && request.totalAmount == 0) { + currentRequest = null + } + } + } + } + + override fun activate(): Boolean { + if (currentRequest != null || stackToExtract.isEmpty) { + return false + } + + // todo: configure me + currentRequest = PendingExtractRequest(stackToExtract, counter) + sendPacket(LocateStackPacket(stackToExtract, ipAddress)) + return true + } + + private fun finishRequest() { + val request = currentRequest ?: return + + // todo: dedup with TerminalBlockEntity.stackLocateRequestCompleted + val actualAmount = min(min(request.stack.maxCount, request.totalAmount), amountToExtract) + val sortedResults = request.results.sortedByDescending { it.first }.toMutableList() + var amountRequested = 0 + while (amountRequested < actualAmount && sortedResults.isNotEmpty()) { + val (sourceAmount, source) = sortedResults.removeAt(0) + val amountToRequest = min(sourceAmount, actualAmount - amountRequested) + amountRequested += amountToRequest + sendPacket(ExtractStackPacket(request.stack, amountToRequest, ipAddress, source.ipAddress)) + } + + currentRequest = null + } + + override fun toTag(tag: CompoundTag): CompoundTag { + tag.putString("ActivationMode", controller.activationMode.name) + tag.put("StackToExtract", stackToExtract.toTag(CompoundTag())) + tag.putInt("AmountToExtract", amountToExtract) + return super.toTag(tag) + } + + override fun fromTag(state: BlockState, tag: CompoundTag) { + super.fromTag(state, tag) + controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) + stackToExtract = ItemStack.fromTag(tag.getCompound("StackToExtract")) + amountToExtract = tag.getInt("AmountToExtract") + } + + override fun toClientTag(tag: CompoundTag): CompoundTag { + tag.putString("ActivationMode", controller.activationMode.name) + tag.put("StackToExtract", stackToExtract.toTag(CompoundTag())) + tag.putInt("AmountToExtract", amountToExtract) + return super.toClientTag(tag) + } + + override fun fromClientTag(tag: CompoundTag) { + super.fromClientTag(tag) + controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) + stackToExtract = ItemStack.fromTag(tag.getCompound("StackToExtract")) + amountToExtract = tag.getInt("AmountToExtract") + } + + class PendingExtractRequest( + val stack: ItemStack, + val timestamp: Long, + var results: MutableSet> = mutableSetOf() + ) { + val totalAmount: Int + get() = results.fold(0) { acc, (amount, _) -> acc + amount } + + fun isFinishable(currentTimestamp: Long): Boolean { + return totalAmount >= stack.maxCount || (currentTimestamp - timestamp >= REQUEST_TIMEOUT && totalAmount > 0) + } + } +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreen.kt new file mode 100644 index 0000000..699b704 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreen.kt @@ -0,0 +1,123 @@ +package net.shadowfacts.phycon.block.inserter + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.widget.TextFieldWidget +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerInventory +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.C2SConfigureInserterAmount +import java.lang.NumberFormatException + +/** + * @author shadowfacts + */ +class InserterScreen( + handler: InserterScreenHandler, + playerInv: PlayerInventory, + title: Text, +): HandledScreen( + handler, + playerInv, + title +) { + + companion object { + val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/inserter.png") + } + + private lateinit var amountField: TextFieldWidget + + init { + backgroundWidth = 176 + backgroundHeight = 133 + playerInventoryTitleY = backgroundHeight - 94 + } + + override fun init() { + super.init() + + amountField = TextFieldWidget(textRenderer, x + 57, y + 24, 80, 9, LiteralText("Amount")) + amountField.text = handler.inserter.amountToExtract.toString() + amountField.setHasBorder(false) + amountField.isVisible = true + amountField.setSelected(true) + amountField.setEditableColor(0xffffff) + amountField.setTextPredicate { + if (it.isEmpty()) { + true + } else { + try { + val value = Integer.parseInt(it) + value in 1..64 + } catch (e: NumberFormatException) { + false + } + } + } + addChild(amountField) + } + + fun amountUpdated() { + if (amountField.text.isNotEmpty()) { + handler.inserter.amountToExtract = Integer.parseInt(amountField.text) + client!!.player!!.networkHandler.sendPacket(C2SConfigureInserterAmount(handler.inserter)) + } + } + + override fun tick() { + super.tick() + amountField.tick() + } + + override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { + 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) + } + + override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { + super.render(matrixStack, mouseX, mouseY, delta) + + amountField.render(matrixStack, mouseX, mouseY, delta) + + drawMouseoverTooltip(matrixStack, mouseX, mouseY) + } + + override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, slotActionType: SlotActionType?) { + super.onMouseClick(slot, invSlot, clickData, slotActionType) + + amountField.setSelected(true) + } + + override fun charTyped(c: Char, i: Int): Boolean { + val oldText = amountField.text + if (amountField.charTyped(c, i)) { + if (oldText != amountField.text) { + amountUpdated() + } + return true + } + return super.charTyped(c, i) + } + + override fun keyPressed(i: Int, j: Int, k: Int): Boolean { + val oldText = amountField.text + if (amountField.keyPressed(i, j, k)) { + if (oldText != amountField.text) { + amountUpdated() + } + return true + } + return super.keyPressed(i, j, k) + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreenHandler.kt new file mode 100644 index 0000000..5a8ffdf --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/inserter/InserterScreenHandler.kt @@ -0,0 +1,122 @@ +package net.shadowfacts.phycon.block.inserter + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.inventory.Inventory +import net.minecraft.item.ItemStack +import net.minecraft.network.PacketByteBuf +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType +import net.minecraft.util.Identifier +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.init.PhyBlocks +import net.shadowfacts.phycon.init.PhyScreens +import kotlin.math.min + +/** + * @author shadowfacts + */ +class InserterScreenHandler( + syncId: Int, + playerInv: PlayerInventory, + val inserter: InserterBlockEntity, +): ScreenHandler(PhyScreens.INSERTER_SCREEN_HANDLER, syncId) { + + companion object { + val ID = Identifier(PhysicalConnectivity.MODID, "inserter") + } + + private val fakeInv = FakeInventory(inserter) + + constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): + this( + syncId, + playerInv, + PhyBlocks.INSERTER.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! + ) + + init { + // fake slot + addSlot(FakeSlot(fakeInv, 31, 20)) + + // player inv + for (y in 0 until 3) { + for (x in 0 until 9) { + addSlot(Slot(playerInv, x + y * 9 + 9, 8 + x * 18, 51 + y * 18)) + } + } + + // hotbar + for (x in 0 until 9) { + addSlot(Slot(playerInv, x, 8 + x * 18, 109)) + } + } + + private fun stackToExtractChanged() { + inserter.amountToExtract = min(inserter.stackToExtract.maxCount, inserter.amountToExtract) + } + + override fun canUse(player: PlayerEntity): Boolean { + return true + } + + override fun onSlotClick(slotId: Int, clickData: Int, actionType: SlotActionType, player: PlayerEntity): ItemStack { + // fake slot + if (slotId == 0) { + if (player.inventory.cursorStack.isEmpty) { + inserter.stackToExtract = ItemStack.EMPTY + } else { + val copy = player.inventory.cursorStack.copy() + copy.count = 1 + inserter.stackToExtract = copy + } + stackToExtractChanged() + } + return super.onSlotClick(slotId, clickData, actionType, player) + } + + override fun transferSlot(player: PlayerEntity, slotId: Int): ItemStack { + val slot = slots[slotId] + val copy = slot.stack.copy() + copy.count = 1 + inserter.stackToExtract = copy + stackToExtractChanged() + return ItemStack.EMPTY + } + + class FakeSlot(inv: FakeInventory, x: Int, y: Int): Slot(inv, 0, x, y) { + override fun canInsert(stack: ItemStack) = false + + override fun setStack(stack: ItemStack) { + } + + override fun canTakeItems(player: PlayerEntity) = false + } + + class FakeInventory(val inserter: InserterBlockEntity): Inventory { + override fun clear() { + inserter.stackToExtract = ItemStack.EMPTY + } + + override fun size() = 1 + + override fun isEmpty() = inserter.stackToExtract.isEmpty + + override fun getStack(i: Int): ItemStack { + return if (i == 0) inserter.stackToExtract else ItemStack.EMPTY + } + + override fun removeStack(i: Int, j: Int) = ItemStack.EMPTY + + override fun removeStack(i: Int) = ItemStack.EMPTY + + override fun setStack(i: Int, itemStack: ItemStack?) {} + + override fun markDirty() {} + + override fun canPlayerUse(playerEntity: PlayerEntity?) = true + + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt index de7fc8c..986dd0f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/TerminalBlockEntity.kt @@ -183,7 +183,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL), inventoryCache.clear() sendPacket(RequestInventoryPacket(ipAddress)) val factory = object: ExtendedScreenHandlerFactory { - override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { + override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler { return TerminalScreenHandler(syncId, playerInv, this@TerminalBlockEntity) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt index 227c344..ad659a5 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt @@ -7,6 +7,8 @@ import net.minecraft.util.Identifier import net.minecraft.util.registry.Registry import net.shadowfacts.phycon.block.extractor.ExtractorBlock import net.shadowfacts.phycon.block.extractor.ExtractorBlockEntity +import net.shadowfacts.phycon.block.inserter.InserterBlock +import net.shadowfacts.phycon.block.inserter.InserterBlockEntity import net.shadowfacts.phycon.block.miner.MinerBlock import net.shadowfacts.phycon.block.miner.MinerBlockEntity import net.shadowfacts.phycon.block.netinterface.InterfaceBlock @@ -27,6 +29,7 @@ object PhyBlockEntities { val TERMINAL = create(::TerminalBlockEntity, PhyBlocks.TERMINAL) val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH) val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR) + val INSERTER = create(::InserterBlockEntity, PhyBlocks.INSERTER) val MINER = create(::MinerBlockEntity, PhyBlocks.MINER) val REDSTONE_CONTROLLER = create(::RedstoneControllerBlockEntity, PhyBlocks.REDSTONE_CONTROLLER) @@ -39,6 +42,7 @@ object PhyBlockEntities { register(TerminalBlock.ID, TERMINAL) register(SwitchBlock.ID, SWITCH) register(ExtractorBlock.ID, EXTRACTOR) + register(InserterBlock.ID, INSERTER) register(MinerBlock.ID, MINER) register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt index 41cd8df..48cd5b1 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt @@ -5,6 +5,7 @@ import net.minecraft.util.Identifier import net.minecraft.util.registry.Registry import net.shadowfacts.phycon.block.cable.CableBlock import net.shadowfacts.phycon.block.extractor.ExtractorBlock +import net.shadowfacts.phycon.block.inserter.InserterBlock import net.shadowfacts.phycon.block.miner.MinerBlock import net.shadowfacts.phycon.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.block.netswitch.SwitchBlock @@ -21,6 +22,7 @@ object PhyBlocks { val SWITCH = SwitchBlock() val CABLE = CableBlock() val EXTRACTOR = ExtractorBlock() + val INSERTER = InserterBlock() val MINER = MinerBlock() val REDSTONE_CONTROLLER = RedstoneControllerBlock() @@ -30,6 +32,7 @@ object PhyBlocks { register(SwitchBlock.ID, SWITCH) register(CableBlock.ID, CABLE) register(ExtractorBlock.ID, EXTRACTOR) + register(InserterBlock.ID, INSERTER) register(MinerBlock.ID, MINER) register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt index de01063..4d0e15f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt @@ -8,6 +8,7 @@ import net.shadowfacts.phycon.item.ConsoleItem import net.shadowfacts.phycon.item.ScrewdriverItem import net.shadowfacts.phycon.block.cable.CableBlock import net.shadowfacts.phycon.block.extractor.ExtractorBlock +import net.shadowfacts.phycon.block.inserter.InserterBlock import net.shadowfacts.phycon.block.miner.MinerBlock import net.shadowfacts.phycon.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.block.netswitch.SwitchBlock @@ -24,6 +25,7 @@ object PhyItems { val SWITCH = BlockItem(PhyBlocks.SWITCH, Item.Settings()) val CABLE = BlockItem(PhyBlocks.CABLE, Item.Settings()) val EXTRACTOR = BlockItem(PhyBlocks.EXTRACTOR, Item.Settings()) + val INSERTER = BlockItem(PhyBlocks.INSERTER, Item.Settings()) val MINER = BlockItem(PhyBlocks.MINER, Item.Settings()) val REDSTONE_CONTROLLER = BlockItem(PhyBlocks.REDSTONE_CONTROLLER, Item.Settings()) @@ -36,6 +38,7 @@ object PhyItems { register(SwitchBlock.ID, SWITCH) register(CableBlock.ID, CABLE) register(ExtractorBlock.ID, EXTRACTOR) + register(InserterBlock.ID, INSERTER) register(MinerBlock.ID, MINER) register(RedstoneControllerBlock.ID, REDSTONE_CONTROLLER) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt index 75bfd8d..f8f36e5 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt @@ -4,15 +4,19 @@ import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry import net.minecraft.screen.ScreenHandlerType import net.minecraft.util.Identifier import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.block.inserter.InserterScreenHandler import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler object PhyScreens { lateinit var TERMINAL_SCREEN_HANDLER: ScreenHandlerType private set + lateinit var INSERTER_SCREEN_HANDLER: ScreenHandlerType + private set fun init() { - TERMINAL_SCREEN_HANDLER = ScreenHandlerRegistry.registerExtended(Identifier(PhysicalConnectivity.MODID, "terminal"), ::TerminalScreenHandler) + TERMINAL_SCREEN_HANDLER = ScreenHandlerRegistry.registerExtended(TerminalScreenHandler.ID, ::TerminalScreenHandler) + INSERTER_SCREEN_HANDLER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureInserterAmount.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureInserterAmount.kt new file mode 100644 index 0000000..86c23a0 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2SConfigureInserterAmount.kt @@ -0,0 +1,45 @@ +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.block.inserter.InserterBlockEntity + +/** + * @author shadowfacts + */ +object C2SConfigureInserterAmount: ServerReceiver { + override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "configure_inserter_amount") + + operator fun invoke(be: InserterBlockEntity): Packet<*> { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(be.world!!.registryKey.value) + buf.writeBlockPos(be.pos) + buf.writeVarInt(be.amountToExtract) + + 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 amount = buf.readVarInt() + + server.execute { + val key = RegistryKey.of(Registry.DIMENSION, dimID) + val world = server.getWorld(key) ?: return@execute + val be = world.getBlockEntity(pos) as? InserterBlockEntity ?: return@execute + be.amountToExtract = amount + be.markDirty() + } + } +} diff --git a/src/main/resources/assets/phycon/blockstates/inserter.json b/src/main/resources/assets/phycon/blockstates/inserter.json new file mode 100644 index 0000000..ef844a4 --- /dev/null +++ b/src/main/resources/assets/phycon/blockstates/inserter.json @@ -0,0 +1,29 @@ +{ + "variants": { + "facing=down": { + "model": "phycon:block/inserter" + }, + "facing=up": { + "model": "phycon:block/inserter", + "x": 180 + }, + "facing=north": { + "model": "phycon:block/inserter", + "x": 270 + }, + "facing=south": { + "model": "phycon:block/inserter", + "x": 90 + }, + "facing=west": { + "model": "phycon:block/inserter", + "x": 90, + "y": 90 + }, + "facing=east": { + "model": "phycon:block/inserter", + "x": 90, + "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 389a51c..e558bfb 100644 --- a/src/main/resources/assets/phycon/lang/en_us.json +++ b/src/main/resources/assets/phycon/lang/en_us.json @@ -4,6 +4,7 @@ "block.phycon.terminal": "Terminal", "block.phycon.cable": "Cable", "block.phycon.extractor": "Inventory Extractor", + "block.phycon.inserter": "Inventory Inserter", "block.phycon.miner": "Block Miner", "block.phycon.redstone_controller": "Redstone Controller", diff --git a/src/main/resources/assets/phycon/models/block/extractor.json b/src/main/resources/assets/phycon/models/block/extractor.json index fbbba21..c0ad572 100644 --- a/src/main/resources/assets/phycon/models/block/extractor.json +++ b/src/main/resources/assets/phycon/models/block/extractor.json @@ -1,11 +1,15 @@ { "parent": "block/block", + "textures": { + "cable_side": "phycon:block/cable_straight", + "cable_end": "phycon:block/cable_cap_end" + }, "elements": [ { "from": [0, 0, 0], "to": [16, 2, 16], "faces": { - "down": {"texture": "phycon:block/extractor_front"}, + "down": {"texture": "phycon:block/extractor_front", "cullface": "down"}, "up": {"texture": "phycon:block/extractor_back"}, "north": {"texture": "phycon:block/extractor_side"}, "south": {"texture": "phycon:block/extractor_side"}, @@ -39,12 +43,12 @@ "from": [6, 6, 6], "to": [10, 16, 10], "faces": { - "up": {"texture": "phycon:block/cable_side"}, - "north": {"texture": "phycon:block/cable_side"}, - "south": {"texture": "phycon:block/cable_side"}, - "west": {"texture": "phycon:block/cable_side"}, - "east": {"texture": "phycon:block/cable_side"} + "up": {"texture": "#cable_end", "cullface": "up"}, + "north": {"texture": "#cable_side"}, + "south": {"texture": "#cable_side"}, + "west": {"texture": "#cable_side"}, + "east": {"texture": "#cable_side"} } } ] -} \ No newline at end of file +} diff --git a/src/main/resources/assets/phycon/models/block/inserter.json b/src/main/resources/assets/phycon/models/block/inserter.json new file mode 100644 index 0000000..e11da66 --- /dev/null +++ b/src/main/resources/assets/phycon/models/block/inserter.json @@ -0,0 +1,54 @@ +{ + "parent": "block/block", + "textures": { + "cable_side": "phycon:block/cable_straight", + "cable_end": "phycon:block/cable_cap_end" + }, + "elements": [ + { + "from": [4, 0, 4], + "to": [12, 2, 12], + "faces": { + "down": {"texture": "phycon:block/extractor_front", "cullface": "down"}, + "north": {"texture": "phycon:block/extractor_side"}, + "south": {"texture": "phycon:block/extractor_side"}, + "west": {"texture": "phycon:block/extractor_side"}, + "east": {"texture": "phycon:block/extractor_side"} + } + }, + { + "from": [2, 2, 2], + "to": [14, 4, 14], + "faces": { + "down": {"texture": "phycon:block/extractor_front"}, + "north": {"texture": "phycon:block/extractor_side"}, + "south": {"texture": "phycon:block/extractor_side"}, + "west": {"texture": "phycon:block/extractor_side"}, + "east": {"texture": "phycon:block/extractor_side"} + } + }, + { + "from": [0, 4, 0], + "to": [16, 6, 16], + "faces": { + "down": {"texture": "phycon:block/extractor_front"}, + "up": {"texture": "phycon:block/extractor_back"}, + "north": {"texture": "phycon:block/extractor_side"}, + "south": {"texture": "phycon:block/extractor_side"}, + "west": {"texture": "phycon:block/extractor_side"}, + "east": {"texture": "phycon:block/extractor_side"} + } + }, + { + "from": [6, 6, 6], + "to": [10, 16, 10], + "faces": { + "up": {"texture": "#cable_end", "cullface": "up"}, + "north": {"texture": "#cable_side"}, + "south": {"texture": "#cable_side"}, + "west": {"texture": "#cable_side"}, + "east": {"texture": "#cable_side"} + } + } + ] +} diff --git a/src/main/resources/assets/phycon/textures/gui/inserter.png b/src/main/resources/assets/phycon/textures/gui/inserter.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe950f7aa3bb89ded0986a97da50902953f2a17 GIT binary patch literal 6250 zcmeHr^-~1W1rLFI(`N^Hy(b>Vm+RmKX)zi_O+T6q10s!z>I7&Bgdky{?yiy(c5xMAB6)1Y= z@}(EzkzPHK8FG1Lf3x9Wn5Rvb(9g_;Okex7)6t0Fc%pa6k4RK1V>Wo|0hT*Fs2@J+ zjk#NHAfC>~L{>W9^XG_pWuuI(wZ|`Bl=eaqX90MxVSa6&2QMNy#w4jy9=~AO82DvZ zW^6Wh%zf}7EjxIMw;8B}9F0kCcy7!OUXTpEVjMf!9tu%TDl%sB=m^BV_K9=Hkmz`C zwF5uX@yNlB$oYWZ{T223t*m%^(+zEBCDI+=?e@-Y2X?!|*wv}l+bOx($r}y<#GcnJ z&xR3h*cb((lH>R0ImfZEk@r~5P4jYnYPgjb%YJR_C|7GW(Yd2&iicTyaL$FNHf0YV zSRPT=?MB(HvokQr$KJe`zW6z7XAK<^?TZ+$y$O=ut0%0f^hF)fnhskO4Fg&*Z93eV zORm;D|A_*-9aQtb|9S4(Is+zk@!34;emL&&yWsT3!%e8&;?_MJvNf;99m(|F`9SyU zh|$3r`Qhx=&hS3*w>bJ%Fnrax_K|&}=dq;1@f_rlbLs=>PZQ2TdqL0;GSVcD&J;w= zE|}3aHcHSZ83+X5ytz|F&kWD8o9x3+q9$y&c`O^e(j|p6h(}`5RJmobHk>~;+z&6s4L@ouM<9v8f1=3je($EWOD{dN+Usri5mEYv z#d#>ITF5Wox!Q?XRUds_7IS~&vYM#N7H~;QDK*NgXtK=K|oCo<=~{=J78@L z>3|5TFE-BAIM5=2?XTipP`E7dOc^K1*GphtV&*81%iAJub|_}^BNs{oBCxiM`HIVp z@-R_2=CW*6>9)Mkp)tIdt}-~UPmx7$S3e=y+hp-`i0d_7_jD`k_kQOTQ$B#^omT3e z0OuhbFuum%l=kGLM1x0m!pIp}MI5+^ z^0{1_Jh5}-yarAaOea4@Ry3`9vnp&`5tfhWC{(KwdAmtDXwX1(8x8f$PNtux#4I`* z;`r2dD5yrRE^Mh{u2iVf{3i6N?4iw(9zK^ddu2rV-+E2du^~p>uEfL@b+&Yug|V6P&JAxR&wDz}(p`fniXoQ$ zaoTAD$2#6H_E_ZFz}P1Jcx-zd9%+Ij_weBw_!Jb0Y+S$}X~7Mu%ZB04mgD6pGQV;2 za>-Ugab8V=P+*WIrgp5V=9-MMxfo!Q9^zraJ?jy9npa7>*2r)%g+I4#dKSD+TrqDH zBg#;Z+w8rt+P!Ofq5sg6?fLE&gMEA4RuokX6tZ%>*D3HLg~`y!n>hJ2kwW@>I%1Zf zjbG18(QL@>V(CD3Mf}}iyumK)>fW-E{$9|e*l3rc6HW?xmk+*WXo0@mYkVKEc1hMI z8M|#~xWMO05%#*v4LMU+tvNvv%&I;-sp8n0pTuXp0v&Zn*pyXcbgpf#!8HRXY6Gb`bPd_s_2%VFh z2g_M~p~l6bqOiGDAl`U7(~CnrWUxP!>lc!R&=rp2!~$uhz;OV$K?M>9-kZO8-FHY4 z-&dWRFr0st9RxgpiJz1j|4QVThZYs+wi&iKz13k{oPHcNk%TZRR%fR@d|8k;MEvfr z#Vz>ud6S0#&=yT9T1XswQ4YreucF|b+3bKa%G9lufz-I-Gme`oNv;*lgYXh6W%F)J z-t#o!J-TAW&+oUV?-s`CMRy}w-b#nP7KxdhxiaCmla@NjKnJ50pi!XkQjeN2J{ zVc!n_m>k;B90m$wanW{7t754F3!HZ~Q&MlqNQ$G=@7O(Zs{~Ijw#-L{dT{^U z=QRUCN^!znTo;;?2MT~W3osyUrXqDw8MU!+L%UW3T(fIH3jRF_O~u>oqbIY#EyY7b z zWOjsuOeP0bNf6hV8Auuqh^U>nEbr`Js;Kem=V1z07%!0mJrsnagL2ng-1;pV>VSb1 zI@#R?$wT{+lLUQ-z)dxT!f_hjlW2uKZ%OP=S~J-+ zdF=)DBvg>`EF4?av#+`9T(BeNI(n+!sq1NwMtiqnT1i@y#4rlE%do)5d5z`#`zL!H zM#$@L_#Wq0?-@juzPF{R^|~3rggkB;ez-LC_lLn`QaS2zUw>vAqyk*O>=@2LAwwYt zK)hy)E#0F~U0>F zA~hn}Bv90H0d=sbpwOxCqm+L za6{BQ+_Kx*O_=m0re~XoOrnx@okujnb?OBr;LR*?GnVJqw@_Dn;HWZdU)=Vz1PIzt z8@6!N!Sf<_?zU*PzhXU4%Y{^- z2ZznZpEn*;OmjU0ARnW!fL%<{3v$YgugKNIEwuY2x%aF>rC!HpFw3wk#j-v2roB>q zOx85Yun5c5WC{3y&xT9P{kybk_alPhjB<6u*aT5xtneG@t5M`ktM&m$H1?2~fL?>g z-W3juwL!umsy|b5;g3JlLU~pD@DCZ(p*j>T318OBY|f>1H@pl>4-4$Do{LDqb<0tL z+LO-=d?fR5AsZ`vsNp<`(n~o(3@U(p;Y=43I=BL}L?0F_8fKhCa=Dqk|8r(kLH6l! zAlFlH!0MI-YIG^t^*!qDh4W`~WUBE1z7$HIk2QNX?GyFsY8$C}3im*r_v&xlY(jtD z{*3HLzf_Y9@0|MCl>|PcP#J6F>AP2sf{DNUq$hr$BGuG_o%s2YK?BT#Lq!-OFenx1 zTn}W$g|7{vt$y7LhSL@iR|fe?q^rvM)?aJ4sgb6BqPFxixQmj ztsi;N>nG*qnKYl}Bm800ra?hVOi1D0sCoMfrxYyK8*Y(PsN2Ie1hHy)Nb2Rk%)0IcrjDtiSa^A{?0b~QkVPZWqyBdjE4|-UamD99rl>`c-B*e$-r8{{aqp3Mv z{4pbuNQVZ$lyPF@kFYHy81m71HS4`r=5E%JTG`sARt}PQ5vk^{u33hz>uj5ibdKWZ z$f3P#FUea-o~>v$QV~(<9l6yW zt!jMh{Q*ILM>$jjpA0)pv2SpM=FUozr$AV}5wd4@Nnl#Z)Q-Tb#(}Hd6J>!QhHy;V zHYLh&8l@CTV}aj(%UPbK=W{gAhUEy4#iwV|_2E_;dkR{v@kz@E18fC?b-mYM=5=K~Zcsn<+#x<}@X|2e0(+LLyPYLj~M@y<&gXyvR*$NH8_J;T!S7(rbcB2ea-% zCGWtyE65VpgP(?qEE+Wl;>Fg%l9d8gusw1}M9sO#?$SrG zT=QjPB}66|H}m)j-BhDoGu8fr0YbE;$rBVjGZjAQF&Qz|G`|1A9{Yo><{PUJCYedLB09CDAu@jr5j&RhrvF8Vb3vta3+g3M>B=y zkn3Ce#2gHLSW+fhAMd_AHIqjDir>lfLd-dx+A>K1nd;ZFE1s$5SgATT`FFxZA?)1i zQlE>QF}uc^4?J6Ot_dIx+5<^LPxHs%_2MtC1rzVDEnUSfIzPeecOM%wYu`? z%v#%Wz&ctlKaA4PP$y$|bvv*jh?TT|r9f|JXr)O`yy#2_o{{;-DTQ ztwI_S0_;d3vl3NOX&F#GmP`r0O^7aqGW#QbIu9K)du*WX&!2HUWkFH3u$Qnv>RY7s z?d^hH8u}*==DP5+)h4QwW0B)!B*}_jJmvN{ik=)>9UZN3$2|E0$XT6( zuCA^h000z#y0W&+U$^dW@bmNQ=;%a6MfLXfK0iPIckw^?9|iu`D`1jzVgL7&NUoZy zS^)9w-+!%X0FsH4oHQUDjY9r!g6Sx4-~s?(6a8x;0n#(c{uVJ@6;xy}wva(2c)V|t zXukiwBds7Uq5ap@UXsA-;=~u9AkWx*lADd8K2kgsH(7>Qn2VAwU=|W29E4*D&L(7> z_+9?M9M^LK84%=RQ~LvuU;G28C@t+90Y_#ps5|c)iLJ{~tz<&~FOCa_71VN-G88qH zQT%=z&xVfSpB<_U{SO3h?O11!*zulfEB@L0-%q(?G-U^kHJH$B65@*kkWl{#%E8LP z?~(tXnf{eVjN>7-@-z7ZmO;EEZdCu6`QP-SuA(lBK;##kb5GdF(z9Rqeuzu>_mA8> jvsA_Y74Clsf4?i#HH}F3^r;1!f6)|VRHb2(#)1C@dGdB$ literal 0 HcmV?d00001