diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/StackView.kt b/src/main/kotlin/net/shadowfacts/cacao/view/StackView.kt index a24f317..024a250 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/StackView.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/StackView.kt @@ -6,6 +6,7 @@ import net.shadowfacts.cacao.geometry.Axis import net.shadowfacts.cacao.geometry.AxisPosition import net.shadowfacts.cacao.geometry.AxisPosition.* import no.birkett.kiwi.Constraint +import java.lang.RuntimeException import java.util.* /** @@ -18,6 +19,7 @@ import java.util.* * @param axis The primary axis that this stack lays out its children along. * @param distribution The mode by which this stack lays out its children along the axis perpendicular to the * primary [axis]. + * @param spacing The distance between arranged subviews along the primary axis. */ open class StackView( val axis: Axis, @@ -25,7 +27,7 @@ open class StackView( val spacing: Double = 0.0 ): View() { - // the internal mutable, list of arranged subviews + // the internal, mutable list of arranged subviews private val _arrangedSubviews = LinkedList() /** * The list of arranged subviews belonging to this stack view. @@ -57,6 +59,64 @@ open class StackView( return view } + /** + * Removes the given arranged subview from this stack view's arranged subviews. + */ + fun removeArrangedSubview(view: View) { + val index = arrangedSubviews.indexOf(view) + if (index < 0) { + throw RuntimeException("Cannot remove view that is not arranged subview") + } + + if (index == 0) { + solver.removeConstraint(leadingConnection) + val next = arrangedSubviews.getOrNull(1) + if (next != null) { + solver.dsl { + leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next) + } + } else { + leadingConnection = null + } + } + if (index == arrangedSubviews.size - 1) { + solver.removeConstraint(trailingConnection) + val prev = arrangedSubviews.getOrNull(arrangedSubviews.size - 2) + if (prev != null) { + solver.dsl { + trailingConnection = anchor(TRAILING) equalTo anchor(TRAILING, prev) + } + } else { + trailingConnection = null + } + } + + // if the removed view is in the middle + if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) { + val prev = arrangedSubviews[index - 1] + val next = arrangedSubviews[index + 1] + solver.dsl { + solver.removeConstraint(arrangedSubviewConnections[index - 1]) + solver.removeConstraint(arrangedSubviewConnections[index]) + + // todo: double check me + arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next) + arrangedSubviewConnections.removeAt(index) + } + } + + _arrangedSubviews.remove(view) + removeSubview(view) + } + + override fun removeSubview(view: View) { + if (arrangedSubviews.contains(view)) { + removeArrangedSubview(view) + } else { + super.removeSubview(view) + } + } + private fun addConstraintsForArrangedView(view: View, index: Int) { if (index == 0) { if (leadingConnection != null) { @@ -209,4 +269,4 @@ open class StackView( */ FILL } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt index 81c2e70..b58ce42 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt @@ -197,10 +197,13 @@ open class View(): Responder { * its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's * hierarchy (e.g., one between the subview and its child) will be left in place. * + * This method may be overridden by layout-providing views (such as [StackView]) to update its layout when a managed + * subview is removed. + * * @param view The view to removed as a child of this view. * @throws RuntimeException If the given [view] is not a subview of this view. */ - fun removeSubview(view: View) { + open fun removeSubview(view: View) { if (view.superview !== this) { throw RuntimeException("Cannot remove subview whose superview is not this view") } diff --git a/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt index fceeb1e..c6282f6 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt @@ -61,6 +61,13 @@ class TabViewController( * may be reused or created from scratch each time. */ val controller: ViewController + + /** + * Used by the tab view controller to determine whether the button for this tab should be displayed. + * If the conditions that control this change, call [TabViewController.visibleTabsChanged]. + */ + val isVisible: Boolean + get() = true } /** @@ -68,12 +75,17 @@ class TabViewController( * @param tabView The view to display on the tab's button. * @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none). * @param controller The content view controller for this tab. + * @param visible A function that determines if the tab should currently be visible. */ class SimpleTab( override val tabView: View, override val tooltip: Text? = null, override val controller: ViewController, - ): Tab + private val visible: (() -> Boolean)? = null + ): Tab { + override val isVisible: Boolean + get() = visible?.invoke() ?: true + } /** * The currently selected tab. @@ -100,20 +112,7 @@ class TabViewController( tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL) tabStack.zIndex = 1.0 outerStack.addArrangedSubview(tabStack) - - tabButtons = tabs.mapIndexed { index, tab -> - val btn = TabButton(tab) - btn.handler = { selectTab(it.tab) } - if (tab == currentTab) { - btn.setSelected(true) - } - btn - } - // todo: batch calls to addArrangedSubview - tabButtons.forEach(tabStack::addArrangedSubview) - - // spacer - tabStack.addArrangedSubview(View()) + updateTabButtons() val background = NinePatchView(NinePatchTexture.PANEL_BG) outerStack.addArrangedSubview(background) @@ -137,6 +136,37 @@ class TabViewController( } } + private fun updateTabButtons() { + while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first()) + + tabButtons = tabs.mapNotNull { tab -> + if (!tab.isVisible) { + return@mapNotNull null + } + + val btn = TabButton(tab) + btn.handler = { selectTab(it.tab) } + if (tab == currentTab) { + btn.setSelected(true) + } + btn + } + // todo: batch calls to addArrangedSubview + tabButtons.forEach(tabStack::addArrangedSubview) + + // spacer + tabStack.addArrangedSubview(View()) + + window!!.layout() + } + + /** + * Call this method when the conditions that make the configured tabs visible change. + */ + fun visibleTabsChanged() { + updateTabButtons() + } + /** * Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and * swaps the content view controller. diff --git a/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt b/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt index c4b8fc4..f49e41b 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/block/miner/MinerBlockEntity.kt @@ -6,7 +6,10 @@ import alexiil.mc.lib.attributes.item.filter.ItemFilter import net.minecraft.block.Block import net.minecraft.block.BlockState import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.nbt.CompoundTag import net.minecraft.server.world.ServerWorld +import net.minecraft.text.TranslatableText import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Direction import net.minecraft.world.World @@ -14,10 +17,13 @@ import net.shadowfacts.phycon.api.packet.Packet import net.shadowfacts.phycon.init.PhyBlockEntities import net.shadowfacts.phycon.block.DeviceBlockEntity import net.shadowfacts.phycon.block.terminal.TerminalBlockEntity +import net.shadowfacts.phycon.component.ActivationController import net.shadowfacts.phycon.component.NetworkStackDispatcher import net.shadowfacts.phycon.component.NetworkStackProvider import net.shadowfacts.phycon.component.handleItemStack import net.shadowfacts.phycon.packet.* +import net.shadowfacts.phycon.util.ActivationMode +import net.shadowfacts.phycon.util.ClientConfigurableDevice import net.shadowfacts.phycon.util.copyWithCount import kotlin.math.min @@ -26,7 +32,9 @@ import kotlin.math.min */ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), NetworkStackProvider, - NetworkStackDispatcher { + NetworkStackDispatcher, + ActivationController.ActivatableDevice, + ClientConfigurableDevice { private val facing: Direction get() = cachedState[MinerBlock.FACING] @@ -35,6 +43,9 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), override val pendingInsertions = mutableListOf() override val dispatchStackTimeout = TerminalBlockEntity.INSERTION_TIMEOUT + override val controller = ActivationController(40L, this) + + var minerMode = MinerMode.ON_DEMAND override fun handle(packet: Packet) { when (packet) { @@ -47,10 +58,16 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), } private fun handleRequestInventory(packet: RequestInventoryPacket) { + if (minerMode != MinerMode.ON_DEMAND) { + return + } sendPacket(ReadInventoryPacket(invProxy, ipAddress, packet.source)) } private fun handleLocateStack(packet: LocateStackPacket) { + if (minerMode != MinerMode.ON_DEMAND) { + return + } val amount = invProxy.getAmount(packet.stack) if (amount > 0) { sendPacket(StackLocationPacket(packet.stack, amount, this, ipAddress, packet.source)) @@ -58,6 +75,10 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), } private fun handleExtractStack(packet: ExtractStackPacket) { + if (minerMode != MinerMode.ON_DEMAND) { + return + } + // always recalculate immediately before breaking val drops = invProxy.getDrops(recalculate = true) if (invProxy.getAmount(packet.stack) > 0) { @@ -97,7 +118,70 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), override fun createPendingInsertion(stack: ItemStack) = PendingInsertion(stack, counter) + override fun tick() { + super.tick() + + if (!world!!.isClient && minerMode == MinerMode.AUTOMATIC) { + + controller.tick() + } + } + + override fun activate(): Boolean { + if (minerMode == MinerMode.ON_DEMAND) { + return false + } + + val drops = invProxy.getDrops(recalculate = true) + if (!world!!.getBlockState(pos.offset(facing)).isAir) { + world!!.breakBlock(pos.offset(facing), false) + + for (stack in drops) { + if (stack.isEmpty) continue + dispatchItemStack(stack) + } + + return true + } else { + return false + } + } + + override fun canConfigureActivationController(): Boolean { + return minerMode == MinerMode.AUTOMATIC + } + + override fun toCommonTag(tag: CompoundTag) { + super.toCommonTag(tag) + writeDeviceConfiguration(tag) + } + + override fun fromCommonTag(tag: CompoundTag) { + super.fromCommonTag(tag) + loadDeviceConfiguration(tag) + } + + override fun writeDeviceConfiguration(tag: CompoundTag) { + tag.putString("MinerMode", minerMode.name) + tag.putString("ActivationMode", controller.activationMode.name) + } + + override fun loadDeviceConfiguration(tag: CompoundTag) { + minerMode = MinerMode.valueOf(tag.getString("MinerMode")) + controller.activationMode = ActivationMode.valueOf(tag.getString("ActivationMode")) + } + + enum class MinerMode { + ON_DEMAND, AUTOMATIC; + + val friendlyName = TranslatableText("gui.phycon.miner_mode.${name.toLowerCase()}") + } + class MinerInvProxy(val miner: MinerBlockEntity): GroupedItemInvView { + companion object { + val TOOL = ItemStack(Items.DIAMOND_PICKAXE) + } + private var cachedState: BlockState? = null private var cachedDrops: List? = null @@ -115,12 +199,15 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), if (cachedDrops == null || realState != cachedState || recalculate) { cachedState = realState val be = if (realState.block.hasBlockEntity()) world.getBlockEntity(targetPos) else null - cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be) + cachedDrops = Block.getDroppedStacks(realState, world as ServerWorld, targetPos, be, null, TOOL) } return cachedDrops!! } override fun getStoredStacks(): Set { + if (miner.minerMode != MinerMode.ON_DEMAND) { + return setOf() + } return getDrops().toSet() } @@ -130,7 +217,7 @@ class MinerBlockEntity: DeviceBlockEntity(PhyBlockEntities.MINER), override fun getStatistics(filter: ItemFilter): GroupedItemInvView.ItemInvStatistic { var totalCount = 0 - for (s in getDrops()) { + for (s in storedStacks) { if (filter.matches(s)) { totalCount += s.count } diff --git a/src/main/kotlin/net/shadowfacts/phycon/component/ActivationController.kt b/src/main/kotlin/net/shadowfacts/phycon/component/ActivationController.kt index 8dcd141..38a4628 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/component/ActivationController.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/component/ActivationController.kt @@ -57,5 +57,9 @@ class ActivationController( val counter: Long fun activate(): Boolean + + fun canConfigureActivationController(): Boolean { + return true + } } } diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/console/DeviceConsoleScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/console/DeviceConsoleScreen.kt index 8456087..fc279b8 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/screen/console/DeviceConsoleScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/console/DeviceConsoleScreen.kt @@ -13,6 +13,7 @@ import net.shadowfacts.cacao.viewcontroller.ViewController import net.shadowfacts.cacao.window.Window import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.phycon.block.DeviceBlockEntity +import net.shadowfacts.phycon.block.miner.MinerBlockEntity import net.shadowfacts.phycon.block.redstone_controller.RedstoneControllerBlockEntity import net.shadowfacts.phycon.component.ActivationController import org.lwjgl.glfw.GLFW @@ -40,12 +41,12 @@ class DeviceConsoleScreen( intrinsicContentSize = Size(16.0, 16.0) }, TranslatableText("gui.phycon.console.remote"), - ActivatableDeviceViewController(device) + ActivatableDeviceViewController(device), + device::canConfigureActivationController )) } if (device is RedstoneControllerBlockEntity) { - tabs.add( - TabViewController.SimpleTab( + tabs.add(TabViewController.SimpleTab( TextureView(Texture(Identifier("textures/block/redstone_torch.png"), 0, 0, 16, 16)).apply { intrinsicContentSize = Size(16.0, 16.0) }, @@ -53,6 +54,15 @@ class DeviceConsoleScreen( RedstoneControllerViewController(device) )) } + if (device is MinerBlockEntity) { + tabs.add(TabViewController.SimpleTab( + TextureView(Texture(Identifier("textures/item/diamond_pickaxe.png"), 0, 0, 16, 16)).apply { + intrinsicContentSize = Size(16.0, 16.0) + }, + TranslatableText("block.phycon.miner"), + MinerViewController(device) + )) + } tabController = TabViewController(tabs) diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/console/MinerViewController.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/console/MinerViewController.kt new file mode 100644 index 0000000..337051c --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/console/MinerViewController.kt @@ -0,0 +1,49 @@ +package net.shadowfacts.phycon.screen.console + +import net.minecraft.client.MinecraftClient +import net.minecraft.text.TranslatableText +import net.shadowfacts.cacao.util.Color +import net.shadowfacts.cacao.view.Label +import net.shadowfacts.cacao.view.button.EnumButton +import net.shadowfacts.cacao.viewcontroller.TabViewController +import net.shadowfacts.cacao.viewcontroller.ViewController +import net.shadowfacts.kiwidsl.dsl +import net.shadowfacts.phycon.block.miner.MinerBlockEntity +import net.shadowfacts.phycon.networking.C2SConfigureDevice + +/** + * @author shadowfacts + */ +class MinerViewController( + val device: MinerBlockEntity, +): ViewController() { + + override fun viewDidLoad() { + super.viewDidLoad() + + val label = Label(TranslatableText("gui.phycon.console.miner.mode")).apply { + textColor = Color.TEXT + } + view.addSubview(label) + + val mode = EnumButton(device.minerMode, MinerBlockEntity.MinerMode::friendlyName) + mode.handler = { + device.minerMode = it.value + MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureDevice(device)) + + (parent as TabViewController).visibleTabsChanged() + } + view.addSubview(mode) + + view.solver.dsl { + mode.widthAnchor equalTo 100 + mode.heightAnchor equalTo 20 + mode.topAnchor equalTo view.topAnchor + mode.rightAnchor equalTo view.rightAnchor + + label.centerYAnchor equalTo mode.centerYAnchor + label.rightAnchor equalTo (mode.leftAnchor - 4) + } + } + +} diff --git a/src/main/resources/assets/phycon/lang/en_us.json b/src/main/resources/assets/phycon/lang/en_us.json index 937d34f..afdd81a 100644 --- a/src/main/resources/assets/phycon/lang/en_us.json +++ b/src/main/resources/assets/phycon/lang/en_us.json @@ -20,6 +20,7 @@ "gui.phycon.console.redstone.devices": "Managed Devices", "gui.phycon.console.remote": "Remote Management", "gui.phycon.console.remote.mode": "Activation Mode", + "gui.phycon.console.miner.mode": "Miner Mode", "gui.phycon.redstone_mode.high": "High", "gui.phycon.redstone_mode.low": "Low", "gui.phycon.redstone_mode.toggle": "Toggle", @@ -27,5 +28,7 @@ "gui.phycon.redstone_mode.falling_edge": "Falling Edge", "gui.phycon.activation_mode.automatic": "Automatic", "gui.phycon.activation_mode.managed": "Managed", - "gui.phycon.emitter.count": "%d Item(s)" + "gui.phycon.emitter.count": "%d Item(s)", + "gui.phycon.miner_mode.automatic": "Automatic", + "gui.phycon.miner_mode.on_demand": "On Demand" }