From 9d98481ba5226a6f24d1265adfbdf90f47167e1e Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 28 Mar 2021 13:50:11 -0400 Subject: [PATCH] Add Crafting Terminal --- .../phycon/PhysicalConnectivity.kt | 3 +- .../phycon/PhysicalConnectivityClient.kt | 2 + .../terminal/AbstractTerminalBlockEntity.kt | 11 +- .../terminal/AbstractTerminalScreenHandler.kt | 12 +- .../AbstractTerminalViewController.kt | 8 +- .../block/terminal/CraftingTerminalBlock.kt | 18 +++ .../terminal/CraftingTerminalBlockEntity.kt | 141 ++++++++++++++++++ .../block/terminal/CraftingTerminalScreen.kt | 48 ++++++ .../terminal/CraftingTerminalScreenHandler.kt | 130 ++++++++++++++++ .../CraftingTerminalViewController.kt | 100 +++++++++++++ .../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 | 5 + .../networking/C2STerminalCraftingButton.kt | 56 +++++++ .../resources/assets/phycon/lang/en_us.json | 4 + .../textures/gui/crafting_terminal_1.png | Bin 0 -> 1696 bytes .../textures/gui/crafting_terminal_2.png | Bin 0 -> 1142 bytes .../assets/phycon/textures/gui/icons.png | Bin 2272 -> 3579 bytes 19 files changed, 534 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalBlock.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalBlockEntity.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreen.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreenHandler.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalViewController.kt create mode 100644 src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalCraftingButton.kt create mode 100644 src/main/resources/assets/phycon/textures/gui/crafting_terminal_1.png create mode 100644 src/main/resources/assets/phycon/textures/gui/crafting_terminal_2.png diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt index 62e55aa..e784c5b 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivity.kt @@ -26,9 +26,10 @@ object PhysicalConnectivity: ModInitializer { PhyItems.init() PhyScreens.init() + registerGlobalReceiver(C2SConfigureDevice) + registerGlobalReceiver(C2STerminalCraftingButton) registerGlobalReceiver(C2STerminalRequestItem) registerGlobalReceiver(C2STerminalUpdateDisplayedItems) - registerGlobalReceiver(C2SConfigureDevice) for (it in FabricLoader.getInstance().getEntrypoints("phycon", PhyConPlugin::class.java)) { it.initializePhyCon(PhyConAPIImpl) diff --git a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt index 5ed45f9..1afd68d 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/PhysicalConnectivityClient.kt @@ -8,6 +8,7 @@ import net.fabricmc.fabric.api.renderer.v1.RendererAccess import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial import net.shadowfacts.phycon.block.inserter.InserterScreen import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreen +import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreen import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.block.terminal.TerminalScreen import net.shadowfacts.phycon.client.PhyExtendedModelProvider @@ -40,6 +41,7 @@ object PhysicalConnectivityClient: ClientModInitializer { } ScreenRegistry.register(PhyScreens.TERMINAL, ::TerminalScreen) + ScreenRegistry.register(PhyScreens.CRAFTING_TERMINAL, ::CraftingTerminalScreen) ScreenRegistry.register(PhyScreens.INSERTER, ::InserterScreen) ScreenRegistry.register(PhyScreens.REDSTONE_EMITTER, ::RedstoneEmitterScreen) diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt index deee792..2de4207 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalBlockEntity.kt @@ -52,7 +52,7 @@ abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBloc protected val inventoryCache = mutableMapOf() val internalBuffer = TerminalBufferInventory(18) - private val pendingRequests = LinkedList() + protected val pendingRequests = LinkedList() override val pendingInsertions = mutableListOf() override val dispatchStackTimeout = INSERTION_TIMEOUT @@ -190,7 +190,7 @@ abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBloc sendPacket(LocateStackPacket(stack, ipAddress)) } - private fun stackLocateRequestCompleted(request: StackLocateRequest) { + protected open fun stackLocateRequestCompleted(request: StackLocateRequest) { pendingRequests.remove(request) val sortedResults = request.results.toMutableList() @@ -252,19 +252,20 @@ abstract class AbstractTerminalBlockEntity(type: BlockEntityType<*>): DeviceBloc var bufferSlot by Delegates.notNull() } - data class StackLocateRequest( + open class StackLocateRequest( val stack: ItemStack, val amount: Int, val timestamp: Long, - var results: MutableSet> = mutableSetOf() ) { + var results: MutableSet> = mutableSetOf() + val totalResultAmount: Int get() = results.fold(0) { acc, (amount, _) -> acc + amount } fun isFinishable(currentTimestamp: Long): Boolean { // we can't check totalResultAmount >= amount because we need to hear back from all network stack providers to // correctly sort by priority - return currentTimestamp - timestamp >= AbstractTerminalBlockEntity.LOCATE_REQUEST_TIMEOUT + return currentTimestamp - timestamp >= LOCATE_REQUEST_TIMEOUT } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt index 41743d2..e52621a 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalScreenHandler.kt @@ -56,6 +56,8 @@ abstract class AbstractTerminalScreenHandler( var itemsForDisplay = listOf() private set + open val xOffset: Int = 0 + init { if (!terminal.world!!.isClient) { assert(terminal.netItemObserver?.get() === null) @@ -63,29 +65,31 @@ abstract class AbstractTerminalScreenHandler( // intentionally don't call netItemsChanged immediately, we need to wait for the client to send us its settings } + val xOffset = xOffset + // network for (y in 0 until 6) { for (x in 0 until 9) { - addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, 66 + x * 18, 18 + y * 18)) + addSlot(TerminalFakeSlot(fakeInv, y * 9 + x, xOffset + 66 + x * 18, 18 + y * 18)) } } // internal buffer for (y in 0 until 6) { for (x in 0 until 3) { - addSlot(Slot(terminal.internalBuffer, y * 3 + x, 8 + x * 18, 18 + y * 18)) + addSlot(Slot(terminal.internalBuffer, y * 3 + x, xOffset + 8 + x * 18, 18 + y * 18)) } } // player inv for (y in 0 until 3) { for (x in 0 until 9) { - addSlot(Slot(playerInv, x + y * 9 + 9, 66 + x * 18, 140 + y * 18)) + addSlot(Slot(playerInv, x + y * 9 + 9, xOffset + 66 + x * 18, 140 + y * 18)) } } // hotbar for (x in 0 until 9) { - addSlot(Slot(playerInv, x, 66 + x * 18, 198)) + addSlot(Slot(playerInv, x, xOffset + 66 + x * 18, 198)) } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt index e2189cb..91a111f 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/AbstractTerminalViewController.kt @@ -64,7 +64,7 @@ abstract class AbstractTerminalViewController() { + + companion object { + val ID = Identifier(PhysicalConnectivity.MODID, "crafting_terminal") + } + + override fun createBlockEntity(world: BlockView) = CraftingTerminalBlockEntity() + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalBlockEntity.kt new file mode 100644 index 0000000..c71aabf --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalBlockEntity.kt @@ -0,0 +1,141 @@ +package net.shadowfacts.phycon.block.terminal + +import alexiil.mc.lib.attributes.item.ItemStackCollections +import alexiil.mc.lib.attributes.item.ItemStackUtil +import it.unimi.dsi.fastutil.objects.Object2IntMap +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.inventory.CraftingInventory +import net.minecraft.inventory.SimpleInventory +import net.minecraft.item.ItemStack +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.PacketByteBuf +import net.minecraft.screen.ScreenHandler +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.minecraft.text.TranslatableText +import net.shadowfacts.phycon.init.PhyBlockEntities +import net.shadowfacts.phycon.packet.ItemStackPacket +import net.shadowfacts.phycon.packet.LocateStackPacket +import net.shadowfacts.phycon.packet.RequestInventoryPacket +import net.shadowfacts.phycon.util.fromTag +import net.shadowfacts.phycon.util.toTag +import java.util.LinkedList +import kotlin.math.min + +/** + * @author shadowfacts + */ +class CraftingTerminalBlockEntity: AbstractTerminalBlockEntity(PhyBlockEntities.CRAFTING_TERMINAL) { + + val craftingInv = SimpleInventory(9) + + private val completedCraftingStackRequests = LinkedList() + + override fun onActivate(player: PlayerEntity) { + if (!world!!.isClient) { + updateAndSync() + + inventoryCache.clear() + sendPacket(RequestInventoryPacket(ipAddress)) + val factory = object: ExtendedScreenHandlerFactory { + override fun createMenu(syncId: Int, playerInv: PlayerInventory, player: PlayerEntity): ScreenHandler? { + return CraftingTerminalScreenHandler(syncId, playerInv, this@CraftingTerminalBlockEntity) + } + + override fun getDisplayName() = TranslatableText("block.phycon.crafting_terminal") + + override fun writeScreenOpeningData(player: ServerPlayerEntity, buf: PacketByteBuf) { + buf.writeBlockPos(this@CraftingTerminalBlockEntity.pos) + } + } + player.openHandledScreen(factory) + } + } + + fun requestItemsForCrafting(maxAmount: Int) { + val amounts = ItemStackCollections.map() + + for (i in 0 until craftingInv.size()) { + val craftingInvStack = craftingInv.getStack(i) + if (craftingInvStack.isEmpty) continue + if (craftingInvStack.count >= craftingInvStack.maxCount) continue + + if (craftingInvStack !in amounts) amounts[craftingInvStack] = IntArray(9) { 0 } + amounts[craftingInvStack]!![i] = min(maxAmount, craftingInvStack.maxCount - craftingInvStack.count) + } + + for ((stack, amountPerSlot) in amounts) { + val total = amountPerSlot.sum() + val request = CraftingStackLocateRequest(stack, total, counter, amountPerSlot) + pendingRequests.add(request) + sendPacket(LocateStackPacket(stack, ipAddress)) + } + } + + override fun stackLocateRequestCompleted(request: StackLocateRequest) { + if (request is CraftingStackLocateRequest) { + completedCraftingStackRequests.add(request) + } + + super.stackLocateRequestCompleted(request) + } + + override fun doHandleItemStack(packet: ItemStackPacket): ItemStack { + val craftingReq = completedCraftingStackRequests.find { ItemStackUtil.areEqualIgnoreAmounts(it.stack, packet.stack) } + if (craftingReq != null) { + var remaining = packet.stack.copy() + + for (i in 0 until craftingInv.size()) { + val currentStack = craftingInv.getStack(i) + if (currentStack.count >= currentStack.maxCount) continue + if (!ItemStackUtil.areEqualIgnoreAmounts(currentStack, remaining)) continue + + val toInsert = minOf(remaining.count, currentStack.maxCount - currentStack.count, craftingReq.amountPerSlot[i]) + currentStack.count += toInsert + remaining.count -= toInsert + craftingReq.amountPerSlot[i] -= toInsert + craftingReq.received += toInsert + + if (remaining.isEmpty) { + break + } + } + + if (craftingReq.amountPerSlot.sum() == 0 || craftingReq.received >= craftingReq.totalResultAmount) { + completedCraftingStackRequests.remove(craftingReq) + } + + if (!remaining.isEmpty) { + remaining = internalBuffer.insert(remaining, TerminalBufferInventory.Mode.FROM_NETWORK) + } + + updateAndSync() + + return remaining + } else { + return super.doHandleItemStack(packet) + } + } + + override fun toCommonTag(tag: CompoundTag) { + super.toCommonTag(tag) + tag.put("CraftingInv", craftingInv.toTag()) + } + + override fun fromCommonTag(tag: CompoundTag) { + super.fromCommonTag(tag) + craftingInv.fromTag(tag.getList("CraftingInv", 10)) + } + + class CraftingStackLocateRequest( + stack: ItemStack, + amount: Int, + timestamp: Long, + val amountPerSlot: IntArray, + ): StackLocateRequest(stack, amount, timestamp) { + var received: Int = 0 + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreen.kt new file mode 100644 index 0000000..106a375 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreen.kt @@ -0,0 +1,48 @@ +package net.shadowfacts.phycon.block.terminal + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.shadowfacts.phycon.PhysicalConnectivity + +/** + * @author shadowfacts + */ +class CraftingTerminalScreen( + handler: CraftingTerminalScreenHandler, + playerInv: PlayerInventory, + title: Text, +): AbstractTerminalScreen( + handler, + playerInv, + title, + 259, + 252, +) { + + companion object { + private val BACKGROUND_1 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_1.png") + private val BACKGROUND_2 = Identifier(PhysicalConnectivity.MODID, "textures/gui/crafting_terminal_2.png") + } + + override val backgroundTexture = BACKGROUND_1 + + override fun createViewController(): AbstractTerminalViewController<*, *, *> { + return CraftingTerminalViewController(this, handler) + } + + override fun drawBackgroundTexture(matrixStack: MatrixStack) { + RenderSystem.color4f(1f, 1f, 1f, 1f) + client!!.textureManager.bindTexture(BACKGROUND_1) + val x = (width - backgroundWidth) / 2 + val y = (height - backgroundHeight) / 2 + drawTexture(matrixStack, x, y, 0, 0, 256, 252) + + client!!.textureManager.bindTexture(BACKGROUND_2) + drawTexture(matrixStack, x + 256, y, 0, 0, 3, 252) + } + + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreenHandler.kt new file mode 100644 index 0000000..6998c63 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalScreenHandler.kt @@ -0,0 +1,130 @@ +package net.shadowfacts.phycon.block.terminal + +import alexiil.mc.lib.attributes.item.ItemStackCollections +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.inventory.CraftingInventory +import net.minecraft.inventory.CraftingResultInventory +import net.minecraft.inventory.Inventory +import net.minecraft.item.ItemStack +import net.minecraft.network.PacketByteBuf +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket +import net.minecraft.recipe.RecipeFinder +import net.minecraft.recipe.RecipeType +import net.minecraft.screen.slot.CraftingResultSlot +import net.minecraft.screen.slot.Slot +import net.minecraft.server.network.ServerPlayerEntity +import net.shadowfacts.phycon.init.PhyBlocks +import net.shadowfacts.phycon.init.PhyScreens + +/** + * @author shadowfacts + */ +class CraftingTerminalScreenHandler( + syncId: Int, + playerInv: PlayerInventory, + terminal: CraftingTerminalBlockEntity, +): AbstractTerminalScreenHandler(PhyScreens.CRAFTING_TERMINAL, syncId, playerInv, terminal) { + + val craftingInv = CraftingInv(this) + val result = CraftingResultInventory() + val resultSlot: CraftingResultSlot + + override val xOffset: Int + get() = 5 + + constructor(syncId: Int, playerInv: PlayerInventory, buf: PacketByteBuf): + this( + syncId, + playerInv, + PhyBlocks.CRAFTING_TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!! + ) + + init { + for (y in 0 until 3) { + for (x in 0 until 3) { + this.addSlot(Slot(craftingInv, x + y * 3, 13 + x * 18, 140 + y * 18)) + } + } + + resultSlot = CraftingResultSlot(playerInv.player, craftingInv, result, 0, 31, 224) + addSlot(resultSlot) + + updateCraftingResult() + } + + override fun onContentChanged(inventory: Inventory?) { + updateCraftingResult() + } + + private fun updateCraftingResult() { + val world = playerInv.player.world + if (!world.isClient) { + val player = playerInv.player as ServerPlayerEntity + val recipe = world.server!!.recipeManager.getFirstMatch(RecipeType.CRAFTING, craftingInv, world) + val resultStack = + if (recipe.isPresent && result.shouldCraftRecipe(world, player, recipe.get())) { + recipe.get().craft(craftingInv) + } else { + ItemStack.EMPTY + } + result.setStack(0, resultStack) + player.networkHandler.sendPacket(ScreenHandlerSlotUpdateS2CPacket(syncId, resultSlot.id, resultStack)) + } + } + + fun clearCraftingGrid() { + assert(!playerInv.player.world.isClient) + for (i in 0 until terminal.craftingInv.size()) { + val craftingInvStack = terminal.craftingInv.getStack(i) + if (craftingInvStack.isEmpty) continue + val remainder = terminal.internalBuffer.insert(craftingInvStack, TerminalBufferInventory.Mode.TO_NETWORK) + terminal.craftingInv.setStack(i, remainder) + } + updateCraftingResult() + sendContentUpdates() + } + + fun requestMoreCraftingIngredients(maxAmount: Int) { + assert(!playerInv.player.world.isClient) + terminal.requestItemsForCrafting(maxAmount) + } + + // RecipeType.CRAFTING wants a CraftingInventory, but we can't store a CraftingInventory on the BE without a screen handler, so... + class CraftingInv(val handler: CraftingTerminalScreenHandler): CraftingInventory(handler, 3, 3) { + private val backing = handler.terminal.craftingInv + + override fun isEmpty(): Boolean { + return backing.isEmpty + } + + override fun getStack(i: Int): ItemStack { + return backing.getStack(i) + } + + override fun removeStack(i: Int): ItemStack { + return backing.removeStack(i) + } + + override fun removeStack(i: Int, j: Int): ItemStack { + val res = backing.removeStack(i, j) + if (!res.isEmpty) { + handler.onContentChanged(this) + } + return res + } + + override fun setStack(i: Int, itemStack: ItemStack?) { + backing.setStack(i, itemStack) + handler.onContentChanged(this) + } + + override fun clear() { + backing.clear() + } + + override fun provideRecipeInputs(finder: RecipeFinder) { + TODO() + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalViewController.kt new file mode 100644 index 0000000..4489dd7 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/block/terminal/CraftingTerminalViewController.kt @@ -0,0 +1,100 @@ +package net.shadowfacts.phycon.block.terminal + +import net.minecraft.client.MinecraftClient +import net.minecraft.client.util.InputUtil +import net.minecraft.text.TranslatableText +import net.minecraft.util.Identifier +import net.shadowfacts.cacao.geometry.Size +import net.shadowfacts.cacao.util.Color +import net.shadowfacts.cacao.util.LayoutGuide +import net.shadowfacts.cacao.util.texture.Texture +import net.shadowfacts.cacao.view.Label +import net.shadowfacts.cacao.view.TextureView +import net.shadowfacts.cacao.view.button.Button +import net.shadowfacts.kiwidsl.dsl +import net.shadowfacts.phycon.PhysicalConnectivity +import net.shadowfacts.phycon.networking.C2STerminalCraftingButton +import org.lwjgl.glfw.GLFW + +/** + * @author shadowfacts + */ +class CraftingTerminalViewController( + screen: CraftingTerminalScreen, + handler: CraftingTerminalScreenHandler, +): AbstractTerminalViewController( + screen, + handler, +) { + + companion object { + val SMALL_BUTTON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 0, 48) + val SMALL_BUTTON_HOVERED = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 16, 48) + val CLEAR_ICON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 32, 48) + val PLUS_ICON = Texture(Identifier(PhysicalConnectivity.MODID, "textures/gui/icons.png"), 48, 48) + } + + lateinit var craftingInv: LayoutGuide + + override fun viewDidLoad() { + super.viewDidLoad() + + craftingInv = view.addLayoutGuide() + view.solver.dsl { + craftingInv.leftAnchor equalTo buffer.leftAnchor + craftingInv.topAnchor equalTo playerInv.topAnchor + craftingInv.widthAnchor equalTo buffer.widthAnchor + craftingInv.heightAnchor equalTo 54 + } + + val craftingLabel = view.addSubview(Label(TranslatableText("gui.phycon.terminal_crafting"))).apply { + textColor = Color.TEXT + } + view.solver.dsl { + craftingLabel.leftAnchor equalTo craftingInv.leftAnchor + craftingLabel.topAnchor equalTo playerInvLabel.topAnchor + } + + val clearIcon = TextureView(CLEAR_ICON).apply { + intrinsicContentSize = Size(3.0,3.0) + } + val clearButton = view.addSubview(Button(clearIcon, padding = 2.0, handler = ::clearPressed)).apply { + background = TextureView(SMALL_BUTTON) + hoveredBackground = TextureView(SMALL_BUTTON_HOVERED) + tooltip = TranslatableText("gui.phycon.terminal.clear_crafting") + } + view.solver.dsl { + clearButton.topAnchor equalTo craftingInv.topAnchor + clearButton.leftAnchor equalTo (pane.leftAnchor + 4) + } + + val plusIcon = TextureView(PLUS_ICON).apply { + intrinsicContentSize = Size(3.0, 3.0) + } + val plusButton = view.addSubview(Button(plusIcon, padding = 2.0, handler = ::plusPressed)).apply { + background= TextureView(SMALL_BUTTON) + hoveredBackground = TextureView(SMALL_BUTTON_HOVERED) + tooltip = TranslatableText("gui.phycon.terminal.more_crafting") + } + view.solver.dsl { + plusButton.topAnchor equalTo (clearButton.bottomAnchor + 2) + plusButton.leftAnchor equalTo clearButton.leftAnchor + } + } + + private fun clearPressed(button: Button) { + MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2STerminalCraftingButton(terminal, C2STerminalCraftingButton.Action.CLEAR_GRID)) + } + + private fun plusPressed(button: Button) { + val client = MinecraftClient.getInstance() + val action = + if (InputUtil.isKeyPressed(client.window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) || InputUtil.isKeyPressed(client.window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT)) { + C2STerminalCraftingButton.Action.REQUEST_MAX_MORE + } else { + C2STerminalCraftingButton.Action.REQUEST_ONE_MORE + } + client.player!!.networkHandler.sendPacket(C2STerminalCraftingButton(terminal, action)) + } + +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt index 1bb1e11..9261c26 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlockEntities.kt @@ -19,6 +19,8 @@ import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlockEntity import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlockEntity +import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock +import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlockEntity import net.shadowfacts.phycon.block.terminal.TerminalBlock import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity @@ -29,6 +31,7 @@ object PhyBlockEntities { val INTERFACE = create(::InterfaceBlockEntity, PhyBlocks.INTERFACE) val TERMINAL = create(::TerminalBlockEntity, PhyBlocks.TERMINAL) + val CRAFTING_TERMINAL = create(::CraftingTerminalBlockEntity, PhyBlocks.CRAFTING_TERMINAL) val SWITCH = create(::SwitchBlockEntity, PhyBlocks.SWITCH) val EXTRACTOR = create(::ExtractorBlockEntity, PhyBlocks.EXTRACTOR) val INSERTER = create(::InserterBlockEntity, PhyBlocks.INSERTER) @@ -43,6 +46,7 @@ object PhyBlockEntities { fun init() { register(InterfaceBlock.ID, INTERFACE) register(TerminalBlock.ID, TERMINAL) + register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL) register(SwitchBlock.ID, SWITCH) register(ExtractorBlock.ID, EXTRACTOR) register(InserterBlock.ID, INSERTER) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt index c59d5d4..1a0cefa 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyBlocks.kt @@ -13,6 +13,7 @@ import net.shadowfacts.phycon.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.block.netswitch.SwitchBlock import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock +import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock import net.shadowfacts.phycon.block.terminal.TerminalBlock /** @@ -24,6 +25,7 @@ object PhyBlocks { val INTERFACE = InterfaceBlock() val TERMINAL = TerminalBlock() + val CRAFTING_TERMINAL = CraftingTerminalBlock() val SWITCH = SwitchBlock() val EXTRACTOR = ExtractorBlock() val INSERTER = InserterBlock() @@ -38,6 +40,7 @@ object PhyBlocks { register(InterfaceBlock.ID, INTERFACE) register(TerminalBlock.ID, TERMINAL) + register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL) register(SwitchBlock.ID, SWITCH) register(ExtractorBlock.ID, EXTRACTOR) register(InserterBlock.ID, INSERTER) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt index 87598dd..dad46de 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyItems.kt @@ -14,6 +14,7 @@ import net.shadowfacts.phycon.block.netinterface.InterfaceBlock import net.shadowfacts.phycon.block.netswitch.SwitchBlock import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlock import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterBlock +import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock import net.shadowfacts.phycon.block.terminal.TerminalBlock import net.shadowfacts.phycon.item.DeviceBlockItem import net.shadowfacts.phycon.item.FaceDeviceBlockItem @@ -29,6 +30,7 @@ object PhyItems { val INTERFACE = FaceDeviceBlockItem(PhyBlocks.INTERFACE, Item.Settings()) val TERMINAL = DeviceBlockItem(PhyBlocks.TERMINAL, Item.Settings()) + val CRAFTING_TERMINAL = DeviceBlockItem(PhyBlocks.CRAFTING_TERMINAL, Item.Settings()) val SWITCH = BlockItem(PhyBlocks.SWITCH, Item.Settings()) val EXTRACTOR = FaceDeviceBlockItem(PhyBlocks.EXTRACTOR, Item.Settings()) val INSERTER = FaceDeviceBlockItem(PhyBlocks.INSERTER, Item.Settings()) @@ -52,6 +54,7 @@ object PhyItems { register(InterfaceBlock.ID, INTERFACE) register(TerminalBlock.ID, TERMINAL) + register(CraftingTerminalBlock.ID, CRAFTING_TERMINAL) register(SwitchBlock.ID, SWITCH) register(ExtractorBlock.ID, EXTRACTOR) register(InserterBlock.ID, INSERTER) diff --git a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt index dc228cc..91a84f4 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/init/PhyScreens.kt @@ -4,6 +4,8 @@ import net.fabricmc.fabric.api.screenhandler.v1.ScreenHandlerRegistry import net.minecraft.screen.ScreenHandlerType import net.shadowfacts.phycon.block.inserter.InserterScreenHandler import net.shadowfacts.phycon.block.redstone_emitter.RedstoneEmitterScreenHandler +import net.shadowfacts.phycon.block.terminal.CraftingTerminalBlock +import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler import net.shadowfacts.phycon.block.terminal.TerminalBlock import net.shadowfacts.phycon.block.terminal.TerminalScreenHandler @@ -11,6 +13,8 @@ object PhyScreens { lateinit var TERMINAL: ScreenHandlerType private set + lateinit var CRAFTING_TERMINAL: ScreenHandlerType + private set lateinit var INSERTER: ScreenHandlerType private set lateinit var REDSTONE_EMITTER: ScreenHandlerType @@ -18,6 +22,7 @@ object PhyScreens { fun init() { TERMINAL = ScreenHandlerRegistry.registerExtended(TerminalBlock.ID, ::TerminalScreenHandler) + CRAFTING_TERMINAL = ScreenHandlerRegistry.registerExtended(CraftingTerminalBlock.ID, ::CraftingTerminalScreenHandler) INSERTER = ScreenHandlerRegistry.registerExtended(InserterScreenHandler.ID, ::InserterScreenHandler) REDSTONE_EMITTER = ScreenHandlerRegistry.registerExtended(RedstoneEmitterScreenHandler.ID, ::RedstoneEmitterScreenHandler) } diff --git a/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalCraftingButton.kt b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalCraftingButton.kt new file mode 100644 index 0000000..978d649 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/networking/C2STerminalCraftingButton.kt @@ -0,0 +1,56 @@ +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.terminal.CraftingTerminalBlockEntity +import net.shadowfacts.phycon.block.terminal.CraftingTerminalScreenHandler + +/** + * @author shadowfacts + */ +object C2STerminalCraftingButton: ServerReceiver { + override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_crafting_button") + + enum class Action { + CLEAR_GRID, + REQUEST_ONE_MORE, + REQUEST_MAX_MORE, + } + + operator fun invoke(terminal: CraftingTerminalBlockEntity, action: Action): Packet<*> { + val buf = PacketByteBufs.create() + + buf.writeIdentifier(terminal.world!!.registryKey.value) + buf.writeBlockPos(terminal.pos) + buf.writeByte(action.ordinal) + + 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 action = Action.values()[buf.readByte().toInt()] + + server.execute { + val key = RegistryKey.of(Registry.DIMENSION, dimID) + val screenHandler = player.currentScreenHandler as? CraftingTerminalScreenHandler ?: return@execute + if (screenHandler.terminal.pos != pos || screenHandler.terminal.world!!.registryKey != key) return@execute + + when (action) { + Action.CLEAR_GRID -> screenHandler.clearCraftingGrid() + Action.REQUEST_ONE_MORE -> screenHandler.requestMoreCraftingIngredients(1) + Action.REQUEST_MAX_MORE -> screenHandler.requestMoreCraftingIngredients(64) + } + } + } +} diff --git a/src/main/resources/assets/phycon/lang/en_us.json b/src/main/resources/assets/phycon/lang/en_us.json index 3cf0cf9..6f84be5 100644 --- a/src/main/resources/assets/phycon/lang/en_us.json +++ b/src/main/resources/assets/phycon/lang/en_us.json @@ -2,6 +2,7 @@ "block.phycon.switch": "Network Switch", "block.phycon.network_interface": "Inventory Interface", "block.phycon.terminal": "Terminal", + "block.phycon.crafting_terminal": "Crafting Terminal", "block.phycon.cable_white": "White Cable", "block.phycon.cable_orange": "Orange Cable", "block.phycon.cable_magenta": "Magenta Cable", @@ -35,6 +36,9 @@ "gui.phycon.terminal_buffer": "Buffer", "gui.phycon.terminal_network": "Network", + "gui.phycon.terminal_crafting": "Crafting", + "gui.phycon.terminal.clear_crafting": "Clear crafting table", + "gui.phycon.terminal.more_crafting": "Request more ingredients", "gui.phycon.console.details": "Device Details", "gui.phycon.console.details.ip": "IP Address: %s", "gui.phycon.console.details.mac": "MAC Address: %s", diff --git a/src/main/resources/assets/phycon/textures/gui/crafting_terminal_1.png b/src/main/resources/assets/phycon/textures/gui/crafting_terminal_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7e15698a03d5fd8a729f3d2b802839c5e3ce2da5 GIT binary patch literal 1696 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$5xX)7d$|)7e>}peR2rGbfdS zL1SX=L|c!;4l+mMgO@6WwwFzKHDRGuq=0DQ6)lm4PAf#CN)2{?;htrpqN&%{lzs4E z|It;=-J93(t!q;F!T9Levn4N-RR68l+|u#!!4CQN?`+@QV|3bk#$qwKdQtZN8Wq>Po_5OTvCI-%DmLJOyPV1|NL8Q z&ubqUXrI#hX!&u5r?^VplUuMEwd))TV#AWeH z)1B4{Nmz;nG_vh{$^O6j{@tjbCppbECD?bx%REnJ;dsN;JL_?cytUtBmJjyK;Z?We zS27#VUvm9c)E5qk8*F;(ZqM4p9`lZ&*}pC4li( zP=vFq+xb0zY#3#Ewg89s8k@MPH{wNq7+>yI0_PNbdxA3WkS@nz}U21>L)D@;Y zEBkzY^KW@2wgYGR9c%ah@9ppB@8@#iX_!4fz4!48#~Ts1x4o>fd;gn5K=A8}z@{?*GOA zc9)*-<&Z0Le)Gr5e6>2;S*|C4Hu9^`g4er3#m#t}XDwT~!m@wfUzv|!x&6NaCYm!U z8ZZKrEenGHg99l2Vluq8SI9eDFO3XyoNA_is&rQH_nl{QKYAHRH@@2ESNzI;x}|v2 z4UQ=}EJn-M6~DB_Wenp70jMtontB}$uDd*E*ZI^uGbUB(#xpBl$K2xBa!>}y+$hMw z;fl))h~pYA-Q*B?V|8}r@0e$4$^n}MPt4hMZfUQBG*I&ipcxGTELgmOF1m2PCv#}^9?b9Mh&6x{#5`u~NfeeHicu4nq)eg5e!Qa`ZRDq8k3)@7J0yKTnuN3Ru|Ht#- zTl3mPRa&(8uf3>ePexsz9SV)c$%hUcGuKAc@wegDt4tFwUC ztQFlYdc)6wGDZ|O=VkWmu6m$~ zBe>A>GTfO!2KSq9dl|OYL~FS`+Rb>-8}peR2rGY2T7 zF|l@{t;b;pnWOQ+OO-;~%O<>G=3yhy43@w(ssSI_*7UvN~JlLdk6()kjHc zyS3Qj9v&C?CFtmpHqB7o)3c&)ef5tYRpO5$?>+w~Q=4@zsli`mUTRyWaK3Gu1BAwpF2%vohNB_d1d7zgN+d{5|%bcUcAz=ykqgIWx@L< z6h=PXb2L&rJo5FtKdlGn?Avsvbk<>y1VQf!3Qmrqs#6k`-B$Z`wTiqwZu@8AvUsKG zPV0muEX4vE*>=8U|KEK7Zq(0{oaUMm?7QM+o+q<#ykY8{^|(gf+V3&T2Ycr5s@w4^ znT_W!xqd6^3x~uFHobMXXKiASdB@P~-qjejlE5!l6Ab^Xr0sNWW~ z441Bieqyt*U~%J~viIG`ck|BO`)7Ok)q8et$D-->Y&E}u(ZZJG?e4_`b0%kLAG5y%3#$fkc#*rba`hB#iap-B450}ytm>hT*3>l6v z2r$!+K|h<2d_{L(3NR0VmB65-QCv3s?>u-WM7Q|B?pR=21cLoGpZD-O=fC=Q4aDbP0l+XkKtW-?8 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phycon/textures/gui/icons.png b/src/main/resources/assets/phycon/textures/gui/icons.png index 7e7d627c5ac99b9ea37731cfbae6e5ceb70f1317..541afe79d425e6d4f145e317cf2b656b2815ab78 100644 GIT binary patch delta 3334 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 SmA^~?0000