diff options
Diffstat (limited to 'src/main/kotlin/features/inventory')
17 files changed, 1739 insertions, 0 deletions
diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt new file mode 100644 index 0000000..031ef78 --- /dev/null +++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt @@ -0,0 +1,66 @@ + + +package moe.nea.firmament.features.inventory + +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.item.ItemStack +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.rei.FirmamentReiPlugin.Companion.asItemEntry +import moe.nea.firmament.rei.SBItemEntryDefinition +import moe.nea.firmament.rei.recipes.SBCraftingRecipe +import moe.nea.firmament.util.MC + +object CraftingOverlay : FirmamentFeature { + + private var screen: GenericContainerScreen? = null + private var recipe: SBCraftingRecipe? = null + private val craftingOverlayIndices = listOf( + 10, 11, 12, + 19, 20, 21, + 28, 29, 30, + ) + + + fun setOverlay(screen: GenericContainerScreen, recipe: SBCraftingRecipe) { + this.screen = screen + this.recipe = recipe + } + + override val identifier: String + get() = "crafting-overlay" + + @Subscribe + fun onSlotRender(event: SlotRenderEvents.After) { + val slot = event.slot + val recipe = this.recipe ?: return + if (slot.inventory != screen?.screenHandler?.inventory) return + val recipeIndex = craftingOverlayIndices.indexOf(slot.index) + if (recipeIndex < 0) return + val expectedItem = recipe.neuRecipe.inputs[recipeIndex] + val actualStack = slot.stack ?: ItemStack.EMPTY!! + val actualEntry = SBItemEntryDefinition.getEntry(actualStack).value + if ((actualEntry.skyblockId.neuItem != expectedItem.itemId || actualEntry.getStackSize() < expectedItem.amount) && expectedItem.amount.toInt() != 0) { + event.context.fill( + event.slot.x, + event.slot.y, + event.slot.x + 16, + event.slot.y + 16, + 0x80FF0000.toInt() + ) + } + if (!slot.hasStack()) { + val itemStack = SBItemEntryDefinition.getEntry(expectedItem).asItemEntry().value + event.context.drawItem(itemStack, event.slot.x, event.slot.y) + event.context.drawItemInSlot( + MC.font, + itemStack, + event.slot.x, + event.slot.y, + "${Formatting.RED}${expectedItem.amount.toInt()}" + ) + } + } +} diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt new file mode 100644 index 0000000..566a813 --- /dev/null +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -0,0 +1,85 @@ + + +package moe.nea.firmament.features.inventory + +import java.awt.Color +import net.minecraft.client.gui.DrawContext +import net.minecraft.item.ItemStack +import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HotbarItemRenderEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.item.loreAccordingToNbt +import moe.nea.firmament.util.lastNotNullOfOrNull +import moe.nea.firmament.util.memoize +import moe.nea.firmament.util.memoizeIdentity +import moe.nea.firmament.util.unformattedString + +object ItemRarityCosmetics : FirmamentFeature { + override val identifier: String + get() = "item-rarity-cosmetics" + + object TConfig : ManagedConfig(identifier) { + val showItemRarityBackground by toggle("background") { false } + val showItemRarityInHotbar by toggle("background-hotbar") { false } + } + + override val config: ManagedConfig + get() = TConfig + + private val rarityToColor = mapOf( + "UNCOMMON" to Formatting.GREEN, + "COMMON" to Formatting.WHITE, + "RARE" to Formatting.DARK_BLUE, + "EPIC" to Formatting.DARK_PURPLE, + "LEGENDARY" to Formatting.GOLD, + "LEGENJERRY" to Formatting.GOLD, + "MYTHIC" to Formatting.LIGHT_PURPLE, + "DIVINE" to Formatting.BLUE, + "SPECIAL" to Formatting.DARK_RED, + "SUPREME" to Formatting.DARK_RED, + ).mapValues { + val c = Color(it.value.colorValue!!) + Triple(c.red / 255F, c.green / 255F, c.blue / 255F) + } + + private fun getSkyblockRarity0(itemStack: ItemStack): Triple<Float, Float, Float>? { + return itemStack.loreAccordingToNbt.lastNotNullOfOrNull { + val entry = it.unformattedString + rarityToColor.entries.find { (k, v) -> k in entry }?.value + } + } + + val getSkyblockRarity = ::getSkyblockRarity0.memoizeIdentity(100) + + + fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) { + val (r, g, b) = getSkyblockRarity(item) ?: return + drawContext.drawSprite( + x, y, + 0, + 16, 16, + MC.guiAtlasManager.getSprite(Identifier.of("firmament:item_rarity_background")), + r, g, b, 1F + ) + } + + + @Subscribe + fun onRenderSlot(it: SlotRenderEvents.Before) { + if (!TConfig.showItemRarityBackground) return + val stack = it.slot.stack ?: return + drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack) + } + + @Subscribe + fun onRenderHotbarItem(it: HotbarItemRenderEvent) { + if (!TConfig.showItemRarityInHotbar) return + val stack = it.item + drawItemStackRarity(it.context, it.x, it.y, stack) + } +} diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt new file mode 100644 index 0000000..c61f8e8 --- /dev/null +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -0,0 +1,51 @@ + + +package moe.nea.firmament.features.inventory + +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.HypixelStaticData +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.skyBlockId + +object PriceData : FirmamentFeature { + override val identifier: String + get() = "price-data" + + object TConfig : ManagedConfig(identifier) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + } + + override val config get() = TConfig + + @Subscribe + fun onItemTooltip(it: ItemTooltipEvent) { + if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { + return + } + val sbId = it.stack.skyBlockId + val bazaarData = HypixelStaticData.bazaarData[sbId] + val lowestBin = HypixelStaticData.lowestBin[sbId] + if (bazaarData != null) { + it.lines.add(Text.literal("")) + it.lines.add( + Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order", + FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1)) + ) + it.lines.add( + Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order", + FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1)) + ) + } else if (lowestBin != null) { + it.lines.add(Text.literal("")) + it.lines.add( + Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin", + FirmFormatters.formatCommas(lowestBin, 1)) + ) + } + } +} diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt new file mode 100644 index 0000000..1c55753 --- /dev/null +++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt @@ -0,0 +1,66 @@ + + +package moe.nea.firmament.features.inventory + +import kotlin.math.absoluteValue +import kotlin.time.Duration.Companion.milliseconds +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.assertNotNullOr + +object SaveCursorPosition : FirmamentFeature { + override val identifier: String + get() = "save-cursor-position" + + object TConfig : ManagedConfig(identifier) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + override val config: TConfig + get() = TConfig + + var savedPositionedP1: Pair<Double, Double>? = null + var savedPosition: SavedPosition? = null + + data class SavedPosition( + val middle: Pair<Double, Double>, + val cursor: Pair<Double, Double>, + val savedAt: TimeMark = TimeMark.now() + ) + + @JvmStatic + fun saveCursorOriginal(positionedX: Double, positionedY: Double) { + savedPositionedP1 = Pair(positionedX, positionedY) + } + + @JvmStatic + fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { + if (!TConfig.enable) return null + val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance } + savedPosition = null + if (lastPosition != null && + (lastPosition.middle.first - middleX).absoluteValue < 1 && + (lastPosition.middle.second - middleY).absoluteValue < 1 + ) { + InputUtil.setCursorParameters( + MC.window.handle, + InputUtil.GLFW_CURSOR_NORMAL, + lastPosition.cursor.first, + lastPosition.cursor.second + ) + return lastPosition.cursor + } + return null + } + + @JvmStatic + fun saveCursorMiddle(middleX: Double, middleY: Double) { + if (!TConfig.enable) return + val cursorPos = assertNotNullOr(savedPositionedP1) { return } + savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos) + } +} diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt new file mode 100644 index 0000000..a50d8fb --- /dev/null +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -0,0 +1,203 @@ + + +@file:UseSerializers(DashlessUUIDSerializer::class) + +package moe.nea.firmament.features.inventory + +import com.mojang.blaze3d.systems.RenderSystem +import java.util.UUID +import org.lwjgl.glfw.GLFW +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.serializer +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.screen.slot.SlotActionType +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.IsSlotProtectedEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.CommonSoundEffects +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.item.displayNameAccordingToNbt +import moe.nea.firmament.util.item.loreAccordingToNbt +import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.unformattedString + +object SlotLocking : FirmamentFeature { + override val identifier: String + get() = "slot-locking" + + @Serializable + data class Data( + val lockedSlots: MutableSet<Int> = mutableSetOf(), + val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), + + val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), + ) + + object TConfig : ManagedConfig(identifier) { + val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } + val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { + SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true) + } + } + + override val config: TConfig + get() = TConfig + + object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data) + + val lockedUUIDs get() = DConfig.data?.lockedUUIDs + + val lockedSlots + get() = when (SBData.skyblockLocation) { + SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift + null -> null + else -> DConfig.data?.lockedSlots + } + + fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + return screen.title.unformattedString.contains("Salvage Item") + } + + fun isTradeScreen(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false + if (handler.inventory.size() < 9) return false + val middlePane = handler.inventory.getStack(handler.inventory.size() - 5) + if (middlePane == null) return false + return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" + } + + fun isNpcShop(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false + if (handler.inventory.size() < 9) return false + val sellItem = handler.inventory.getStack(handler.inventory.size() - 5) + if (sellItem == null) return false + if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true + val lore = sellItem.loreAccordingToNbt + return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!" + } + + @Subscribe + fun onSalvageProtect(event: IsSlotProtectedEvent) { + if (event.slot == null) return + if (!event.slot.hasStack()) return + if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return + val inv = event.slot.inventory + var anyBlocked = false + for (i in 0 until event.slot.index) { + val stack = inv.getStack(i) + if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + anyBlocked = true + } + if (anyBlocked) { + event.protectSilent() + } + } + + @Subscribe + fun onProtectUuidItems(event: IsSlotProtectedEvent) { + val doesNotDeleteItem = event.actionType == SlotActionType.SWAP + || event.actionType == SlotActionType.PICKUP + || event.actionType == SlotActionType.QUICK_MOVE + || event.actionType == SlotActionType.QUICK_CRAFT + || event.actionType == SlotActionType.CLONE + || event.actionType == SlotActionType.PICKUP_ALL + val isSellOrTradeScreen = + isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) + if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) + && doesNotDeleteItem + ) return + val stack = event.itemStack ?: return + val uuid = stack.skyblockUUID ?: return + if (uuid in (lockedUUIDs ?: return)) { + event.protect() + } + } + + @Subscribe + fun onProtectSlot(it: IsSlotProtectedEvent) { + if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) { + it.protect() + } + } + + @Subscribe + fun onLockUUID(it: HandledScreenKeyPressedEvent) { + if (!it.matches(TConfig.lockUUID)) return + val inventory = MC.handledScreen ?: return + inventory as AccessorHandledScreen + + val slot = inventory.focusedSlot_Firmament ?: return + val stack = slot.stack ?: return + val uuid = stack.skyblockUUID ?: return + val lockedUUIDs = lockedUUIDs ?: return + if (uuid in lockedUUIDs) { + lockedUUIDs.remove(uuid) + } else { + lockedUUIDs.add(uuid) + } + DConfig.markDirty() + CommonSoundEffects.playSuccess() + it.cancel() + } + + @Subscribe + fun onLockSlot(it: HandledScreenKeyPressedEvent) { + if (!it.matches(TConfig.lockSlot)) return + val inventory = MC.handledScreen ?: return + inventory as AccessorHandledScreen + + val slot = inventory.focusedSlot_Firmament ?: return + val lockedSlots = lockedSlots ?: return + if (slot.inventory is PlayerInventory) { + if (slot.index in lockedSlots) { + lockedSlots.remove(slot.index) + } else { + lockedSlots.add(slot.index) + } + DConfig.markDirty() + CommonSoundEffects.playSuccess() + } + } + + @Subscribe + fun onRenderSlotOverlay(it: SlotRenderEvents.After) { + val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf()) + val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) + if (isSlotLocked || isUUIDLocked) { + RenderSystem.disableDepthTest() + it.context.drawSprite( + it.slot.x, it.slot.y, 0, + 16, 16, + MC.guiAtlasManager.getSprite( + when { + isSlotLocked -> + (Identifier.of("firmament:slot_locked")) + + isUUIDLocked -> + (Identifier.of("firmament:uuid_locked")) + + else -> + error("unreachable") + } + ) + ) + RenderSystem.enableDepthTest() + } + } +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt new file mode 100644 index 0000000..539edf2 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt @@ -0,0 +1,85 @@ + + +package moe.nea.firmament.features.inventory.buttons + +import com.mojang.brigadier.StringReader +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import kotlinx.serialization.Serializable +import net.minecraft.client.gui.DrawContext +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.command.argument.ItemStackArgumentType +import net.minecraft.item.ItemStack +import net.minecraft.resource.featuretoggle.FeatureFlags +import net.minecraft.util.Identifier +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.memoize + +@Serializable +data class InventoryButton( + var x: Int, + var y: Int, + var anchorRight: Boolean, + var anchorBottom: Boolean, + var icon: String? = "", + var command: String? = "", +) { + companion object { + val itemStackParser by lazy { + ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries, + FeatureFlags.VANILLA_FEATURES)) + } + val dimensions = Dimension(18, 18) + val getItemForName = ::getItemForName0.memoize(1024) + fun getItemForName0(icon: String): ItemStack { + val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) + var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) + if (repoItem == null) { + val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give")) + icon.split(" ", limit = 3).getOrNull(2) ?: icon + else icon + val componentItem = + runCatching { + itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false) + }.getOrNull() + if (componentItem != null) + itemStack = componentItem + } + return itemStack + } + } + + fun render(context: DrawContext) { + context.drawSprite( + 0, + 0, + 0, + dimensions.width, + dimensions.height, + MC.guiAtlasManager.getSprite(Identifier.of("firmament:inventory_button_background")) + ) + context.drawItem(getItem(), 1, 1) + } + + fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() + + fun getPosition(guiRect: Rectangle): Point { + return Point( + (if (anchorRight) guiRect.maxX else guiRect.minX) + x, + (if (anchorBottom) guiRect.maxY else guiRect.minY) + y, + ) + } + + fun getBounds(guiRect: Rectangle): Rectangle { + return Rectangle(getPosition(guiRect), dimensions) + } + + fun getItem(): ItemStack { + return getItemForName(icon ?: "") + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt new file mode 100644 index 0000000..c57563e --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -0,0 +1,184 @@ + + +package moe.nea.firmament.features.inventory.buttons + +import io.github.notenoughupdates.moulconfig.common.IItemStack +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import org.lwjgl.glfw.GLFW +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.FragmentGuiScreen +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils + +class InventoryButtonEditor( + val lastGuiRect: Rectangle, +) : FragmentGuiScreen() { + inner class Editor(val originalButton: InventoryButton) { + @field:Bind + var command: String = originalButton.command ?: "" + + @field:Bind + var icon: String = originalButton.icon ?: "" + + @Bind + fun getItemIcon(): IItemStack { + save() + return ModernItemStack.of(InventoryButton.getItemForName(icon)) + } + + @Bind + fun delete() { + buttons.removeIf { it === originalButton } + popup = null + } + + fun save() { + originalButton.icon = icon + originalButton.command = command + } + } + + var buttons: MutableList<InventoryButton> = + InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList() + + override fun close() { + InventoryButtons.DConfig.data.buttons = buttons + InventoryButtons.DConfig.markDirty() + super.close() + } + + override fun init() { + super.init() + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) { + val t = ClipboardUtils.getTextContents() + val newButtons = InventoryButtonTemplates.loadTemplate(t) + if (newButtons != null) + buttons = newButtons.toMutableList() + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35) + .width(lastGuiRect.width - 20) + .build() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) { + ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons)) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .width(lastGuiRect.width - 20) + .build() + ) + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + context.matrices.push() + context.matrices.translate(0f, 0f, -10f) + context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1) + context.setShaderColor(1f, 1f, 1f, 1f) + context.matrices.pop() + for (button in buttons) { + val buttonPosition = button.getBounds(lastGuiRect) + context.matrices.push() + context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F) + button.render(context) + context.matrices.pop() + } + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (super.keyPressed(keyCode, scanCode, modifiers)) return true + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + close() + return true + } + return false + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (super.mouseReleased(mouseX, mouseY, button)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + if (clickedButton != null && !justPerformedAClickAction) { + createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY)) + return true + } + justPerformedAClickAction = false + lastDraggedButton = null + return false + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true + + if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) { + initialDragMousePosition = Vec2f(-10F, -10F) + lastDraggedButton?.let { dragging -> + justPerformedAClickAction = true + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt()) + ?: return true + dragging.x = offsetX + dragging.y = offsetY + dragging.anchorRight = anchorRight + dragging.anchorBottom = anchorBottom + } + } + return false + } + + var lastDraggedButton: InventoryButton? = null + var justPerformedAClickAction = false + var initialDragMousePosition = Vec2f(-10F, -10F) + + data class AnchoredCoords( + val anchorRight: Boolean, + val anchorBottom: Boolean, + val offsetX: Int, + val offsetY: Int, + ) + + fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? { + if (lastGuiRect.contains(mx, my) || lastGuiRect.contains( + Point( + mx + InventoryButton.dimensions.width, + my + InventoryButton.dimensions.height, + ) + ) + ) return null + + val anchorRight = mx > lastGuiRect.maxX + val anchorBottom = my > lastGuiRect.maxY + var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX + var offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY + if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_SHIFT)) { + offsetX = MathHelper.floor(offsetX / 20F) * 20 + offsetY = MathHelper.floor(offsetY / 20F) * 20 + } + return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (super.mouseClicked(mouseX, mouseY, button)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + if (clickedButton != null) { + lastDraggedButton = clickedButton + initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat()) + return true + } + val mx = mouseX.toInt() + val my = mouseY.toInt() + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true + buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null)) + justPerformedAClickAction = true + return true + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt new file mode 100644 index 0000000..99b544b --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt @@ -0,0 +1,35 @@ + + +package moe.nea.firmament.features.inventory.buttons + +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil + +object InventoryButtonTemplates { + + val legacyPrefix = "NEUBUTTONS/" + val modernPrefix = "MAYBEONEDAYIWILLHAVEMYOWNFORMAT" + + fun loadTemplate(t: String): List<InventoryButton>? { + val buttons = TemplateUtil.maybeDecodeTemplate<List<String>>(legacyPrefix, t) ?: return null + return buttons.mapNotNull { + try { + Firmament.json.decodeFromString<InventoryButton>(it).also { + if (it.icon?.startsWith("extra:") == true || it.command?.any { it.isLowerCase() } == true) { + MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed")) + } + } + } catch (e: Exception) { + null + } + } + } + + fun saveTemplate(buttons: List<InventoryButton>): String { + return TemplateUtil.encodeTemplate(legacyPrefix, buttons.map { Firmament.json.encodeToString(it) }) + } +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt new file mode 100644 index 0000000..fa90d21 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -0,0 +1,88 @@ + + +package moe.nea.firmament.features.inventory.buttons + +import me.shedaniel.math.Rectangle +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenClickEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.HandledScreenPushREIEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.data.DataHolder +import moe.nea.firmament.util.getRectangle + +object InventoryButtons : FirmamentFeature { + override val identifier: String + get() = "inventory-buttons" + + object TConfig : ManagedConfig(identifier) { + val _openEditor by button("open-editor") { + openEditor() + } + } + + object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) + + @Serializable + data class Data( + var buttons: MutableList<InventoryButton> = mutableListOf() + ) + + + override val config: ManagedConfig + get() = TConfig + + fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() } + + @Subscribe + fun onRectangles(it: HandledScreenPushREIEvent) { + val bounds = it.screen.getRectangle() + for (button in getValidButtons()) { + val buttonBounds = button.getBounds(bounds) + it.block(buttonBounds) + } + } + + @Subscribe + fun onClickScreen(it: HandledScreenClickEvent) { + val bounds = it.screen.getRectangle() + for (button in getValidButtons()) { + val buttonBounds = button.getBounds(bounds) + if (buttonBounds.contains(it.mouseX, it.mouseY)) { + MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) + break + } + } + } + + @Subscribe + fun onRenderForeground(it: HandledScreenForegroundEvent) { + val bounds = it.screen.getRectangle() + for (button in getValidButtons()) { + val buttonBounds = button.getBounds(bounds) + it.context.matrices.push() + it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F) + button.render(it.context) + it.context.matrices.pop() + } + lastRectangle = bounds + } + + var lastRectangle: Rectangle? = null + fun openEditor() { + ScreenUtil.setScreenLater( + InventoryButtonEditor( + lastRectangle ?: Rectangle( + MC.window.scaledWidth / 2 - 100, + MC.window.scaledHeight / 2 - 100, + 200, 200, + ) + ) + ) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt new file mode 100644 index 0000000..1015578 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -0,0 +1,53 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.screen.GenericContainerScreenHandler +import moe.nea.firmament.util.ifMatches +import moe.nea.firmament.util.unformattedString + +/** + * A handle representing the state of the "server side" screens. + */ +sealed interface StorageBackingHandle { + + sealed interface HasBackingScreen { + val handler: GenericContainerScreenHandler + } + + /** + * The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage` + */ + data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen + + /** + * An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via + * the [Overview] or via `/ec <index + 1>` for enderchest pages. + */ + data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) : + StorageBackingHandle, HasBackingScreen + + companion object { + private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex() + private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex() + + /** + * Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not + * representable as a [StorageBackingHandle], meaning another screen is open, for example the enderchest icon + * selection screen. + */ + fun fromScreen(screen: Screen?): StorageBackingHandle? { + if (screen == null) return null + if (screen !is GenericContainerScreen) return null + val title = screen.title.unformattedString + if (title == "Storage") return Overview(screen.screenHandler) + return title.ifMatches(enderChestName) { + Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) + } ?: title.ifMatches(backPackName) { + Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) + } + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt new file mode 100644 index 0000000..7555c56 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt @@ -0,0 +1,21 @@ + + +@file:UseSerializers(SortedMapSerializer::class) +package moe.nea.firmament.features.inventory.storageoverlay + +import java.util.SortedMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import moe.nea.firmament.util.SortedMapSerializer + +@Serializable +data class StorageData( + val storageInventories: SortedMap<StoragePageSlot, StorageInventory> = sortedMapOf() +) { + @Serializable + data class StorageInventory( + var title: String, + val slot: StoragePageSlot, + var inventory: VirtualInventory?, + ) +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt new file mode 100644 index 0000000..b615c73 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -0,0 +1,154 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import java.util.SortedMap +import kotlinx.serialization.serializer +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.Items +import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.data.ProfileSpecificDataHolder + +object StorageOverlay : FirmamentFeature { + + + object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData) + + override val identifier: String + get() = "storage-overlay" + + object TConfig : ManagedConfig(identifier) { + val alwaysReplace by toggle("always-replace") { true } + val columns by integer("rows", 1, 10) { 3 } + val scrollSpeed by integer("scroll-speed", 1, 50) { 10 } + val inverseScroll by toggle("inverse-scroll") { false } + val padding by integer("padding", 1, 20) { 5 } + val margin by integer("margin", 1, 60) { 20 } + } + + fun adjustScrollSpeed(amount: Double): Double { + return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1) + } + + override val config: TConfig + get() = TConfig + + var lastStorageOverlay: StorageOverviewScreen? = null + var skipNextStorageOverlayBackflip = false + var currentHandler: StorageBackingHandle? = null + + @Subscribe + fun onTick(event: TickEvent) { + rememberContent(currentHandler ?: return) + } + + @Subscribe + fun onClick(event: SlotClickEvent) { + if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9 + && event.stack.item != Items.BLACK_STAINED_GLASS_PANE + ) { + skipNextStorageOverlayBackflip = true + } + } + + @Subscribe + fun onScreenChange(it: ScreenChangeEvent) { + if (it.old == null && it.new == null) return + val storageOverlayScreen = it.old as? StorageOverlayScreen + ?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview + var storageOverviewScreen = it.old as? StorageOverviewScreen + val screen = it.new as? GenericContainerScreen + val oldHandler = currentHandler + currentHandler = StorageBackingHandle.fromScreen(screen) + rememberContent(currentHandler) + if (storageOverviewScreen != null && oldHandler is StorageBackingHandle.HasBackingScreen) { + val player = MC.player + assert(player != null) + player?.networkHandler?.sendPacket(CloseHandledScreenC2SPacket(oldHandler.handler.syncId)) + if (player?.currentScreenHandler === oldHandler.handler) { + player.currentScreenHandler = player.playerScreenHandler + } + } + storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay + if (it.new == null && storageOverlayScreen != null && !storageOverlayScreen.isExiting) { + it.overrideScreen = storageOverlayScreen + return + } + if (storageOverviewScreen != null + && !storageOverviewScreen.isClosing + && (currentHandler is StorageBackingHandle.Overview || currentHandler == null) + ) { + if (skipNextStorageOverlayBackflip) { + skipNextStorageOverlayBackflip = false + } else { + it.overrideScreen = storageOverviewScreen + lastStorageOverlay = null + } + return + } + screen ?: return + screen.customGui = StorageOverlayCustom( + currentHandler ?: return, + screen, + storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)) + } + + fun rememberContent(handler: StorageBackingHandle?) { + handler ?: return + // TODO: Make all of these functions work on deltas / updates instead of the entire contents + val data = Data.data?.storageInventories ?: return + when (handler) { + is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data) + is StorageBackingHandle.Page -> rememberPage(handler, data) + } + Data.markDirty() + } + + private fun rememberStorageOverview( + handler: StorageBackingHandle.Overview, + data: SortedMap<StoragePageSlot, StorageData.StorageInventory> + ) { + for ((index, stack) in handler.handler.stacks.withIndex()) { + // Ignore unloaded item stacks + if (stack.isEmpty) continue + val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue + val isEmpty = stack.item in StorageOverviewScreen.emptyStorageSlotItems + if (slot in data) { + if (isEmpty) + data.remove(slot) + continue + } + if (!isEmpty) { + data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null) + } + } + } + + private fun rememberPage( + handler: StorageBackingHandle.Page, + data: SortedMap<StoragePageSlot, StorageData.StorageInventory> + ) { + // TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence + val newStacks = + VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() }) + data.compute(handler.storagePageSlot) { slot, existingInventory -> + (existingInventory ?: StorageData.StorageInventory( + slot.defaultName(), + slot, + null + )).also { + it.inventory = newStacks + } + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt new file mode 100644 index 0000000..d0d9114 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -0,0 +1,98 @@ + +package moe.nea.firmament.features.inventory.storageoverlay + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.screen.slot.Slot +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.customgui.CustomGui + +class StorageOverlayCustom( + val handler: StorageBackingHandle, + val screen: GenericContainerScreen, + val overview: StorageOverlayScreen, +) : CustomGui() { + override fun onVoluntaryExit(): Boolean { + overview.isExiting = true + return super.onVoluntaryExit() + } + + override fun getBounds(): List<Rectangle> { + return overview.getBounds() + } + + override fun afterSlotRender(context: DrawContext, slot: Slot) { + if (slot.inventory !is PlayerInventory) + context.disableScissor() + } + + override fun beforeSlotRender(context: DrawContext, slot: Slot) { + if (slot.inventory !is PlayerInventory) + overview.createScissors(context) + } + + override fun onInit() { + overview.init(MinecraftClient.getInstance(), screen.width, screen.height) + overview.init() + screen as AccessorHandledScreen + screen.x_Firmament = overview.measurements.x + screen.y_Firmament = overview.measurements.y + screen.backgroundWidth_Firmament = overview.measurements.totalWidth + screen.backgroundHeight_Firmament = overview.measurements.totalHeight + } + + override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean { + if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY)) + return false + if (slot.inventory !is PlayerInventory) { + if (!overview.getScrollPanelInner().contains(pointX, pointY)) + return false + } + return true + } + + override fun shouldDrawForeground(): Boolean { + return false + } + + override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) + } + + override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + overview.drawBackgrounds(drawContext) + overview.drawPages(drawContext, + mouseX, + mouseY, + delta, + (handler as? StorageBackingHandle.Page)?.storagePageSlot, + screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9), + Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament)) + overview.drawScrollBar(drawContext) + } + + override fun moveSlot(slot: Slot) { + val index = slot.index + if (index in 0..<36) { + val (x, y) = overview.getPlayerInventorySlotPosition(index) + slot.x = x - (screen as AccessorHandledScreen).x_Firmament + slot.y = y - screen.y_Firmament + } else { + slot.x = -100000 + slot.y = -100000 + } + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt new file mode 100644 index 0000000..13c6974 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -0,0 +1,296 @@ + +package moe.nea.firmament.features.inventory.storageoverlay + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.screen.slot.Slot +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.assertTrueOr + +class StorageOverlayScreen : Screen(Text.literal("")) { + + companion object { + val PLAYER_WIDTH = 184 + val PLAYER_HEIGHT = 91 + val PLAYER_Y_INSET = 3 + val SLOT_SIZE = 18 + val PADDING = 10 + val PAGE_WIDTH = SLOT_SIZE * 9 + val HOTBAR_X = 12 + val HOTBAR_Y = 67 + val MAIN_INVENTORY_Y = 9 + val SCROLL_BAR_WIDTH = 8 + val SCROLL_BAR_HEIGHT = 16 + } + + var isExiting: Boolean = false + var scroll: Float = 0F + var pageWidthCount = StorageOverlay.TConfig.columns + + inner class Measurements { + val innerScrollPanelWidth = PAGE_WIDTH * pageWidthCount + (pageWidthCount - 1) * PADDING + val overviewWidth = innerScrollPanelWidth + 3 * PADDING + SCROLL_BAR_WIDTH + val x = width / 2 - overviewWidth / 2 + val overviewHeight = minOf(3 * 18 * 6, height - PLAYER_HEIGHT - minOf(80, height / 10)) + val innerScrollPanelHeight = overviewHeight - PADDING * 2 + val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2 + val playerX = width / 2 - PLAYER_WIDTH / 2 + val playerY = y + overviewHeight - PLAYER_Y_INSET + val totalWidth = overviewWidth + val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT + } + + var measurements = Measurements() + + var lastRenderedInnerHeight = 0 + public override fun init() { + super.init() + pageWidthCount = StorageOverlay.TConfig.columns + .coerceAtMost((width - PADDING) / (PAGE_WIDTH + PADDING)) + .coerceAtLeast(1) + measurements = Measurements() + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toFloat() + .coerceAtMost(getMaxScroll()) + .coerceAtLeast(0F) + return true + } + + fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height + + val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory") + val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background") + val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row") + val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background") + val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob") + + override fun close() { + isExiting = true + super.close() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + drawBackgrounds(context) + drawPages(context, mouseX, mouseY, delta, null, null, Point()) + drawScrollBar(context) + drawPlayerInventory(context, mouseX, mouseY, delta) + } + + fun getScrollbarPercentage(): Float { + return scroll / getMaxScroll() + } + + fun drawScrollBar(context: DrawContext) { + val sbRect = getScrollBarRect() + context.drawGuiTexture( + scrollbarBackground, + sbRect.minX, sbRect.minY, + sbRect.width, sbRect.height, + ) + context.drawGuiTexture( + scrollbarKnob, + sbRect.minX, sbRect.minY + (getScrollbarPercentage() * (sbRect.height - SCROLL_BAR_HEIGHT)).toInt(), + SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT + ) + } + + fun drawBackgrounds(context: DrawContext) { + context.drawGuiTexture(upperBackgroundSprite, + measurements.x, + measurements.y, + 0, + measurements.overviewWidth, + measurements.overviewHeight) + context.drawGuiTexture(playerInventorySprite, + measurements.playerX, + measurements.playerY, + 0, + PLAYER_WIDTH, + PLAYER_HEIGHT) + } + + fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> { + if (int < 9) { + return Pair(measurements.playerX + int * SLOT_SIZE + HOTBAR_X, HOTBAR_Y + measurements.playerY) + } + return Pair( + measurements.playerX + (int % 9) * SLOT_SIZE + HOTBAR_X, + measurements.playerY + (int / 9 - 1) * SLOT_SIZE + MAIN_INVENTORY_Y + ) + } + + fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + val items = MC.player?.inventory?.main ?: return + items.withIndex().forEach { (index, item) -> + val (x, y) = getPlayerInventorySlotPosition(index) + context.drawItem(item, x, y, 0) + context.drawItemInSlot(textRenderer, item, x, y) + } + } + + fun getScrollBarRect(): Rectangle { + return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, + measurements.y + PADDING, + SCROLL_BAR_WIDTH, + measurements.innerScrollPanelHeight) + } + + fun getScrollPanelInner(): Rectangle { + return Rectangle(measurements.x + PADDING, + measurements.y + PADDING, + measurements.innerScrollPanelWidth, + measurements.innerScrollPanelHeight) + } + + fun createScissors(context: DrawContext) { + val rect = getScrollPanelInner() + context.enableScissor( + rect.minX, rect.minY, + rect.maxX, rect.maxY + ) + } + + fun drawPages( + context: DrawContext, mouseX: Int, mouseY: Int, delta: Float, + excluding: StoragePageSlot?, + slots: List<Slot>?, + slotOffset: Point + ) { + createScissors(context) + val data = StorageOverlay.Data.data ?: StorageData() + layoutedForEach(data) { rect, page, inventory -> + drawPage(context, + rect.x, + rect.y, + page, inventory, + if (excluding == page) slots else null, + slotOffset + ) + } + context.disableScissor() + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + return mouseClicked(mouseX, mouseY, button, null) + } + + fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + if (getScrollPanelInner().contains(mouseX, mouseY)) { + val data = StorageOverlay.Data.data ?: StorageData() + layoutedForEach(data) { rect, page, _ -> + if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) { + page.navigateTo() + return true + } + } + return false + } + val sbRect = getScrollBarRect() + if (sbRect.contains(mouseX, mouseY)) { + // TODO: support dragging of the mouse and such + val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + scroll = (getMaxScroll() * percentage).toFloat() + mouseScrolled(0.0, 0.0, 0.0, 0.0) + return true + } + return false + } + + private inline fun layoutedForEach( + data: StorageData, + func: ( + rectangle: Rectangle, + page: StoragePageSlot, inventory: StorageData.StorageInventory, + ) -> Unit + ) { + var yOffset = -scroll.toInt() + var xOffset = 0 + var maxHeight = 0 + for ((page, inventory) in data.storageInventories.entries) { + val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight } + ?: 18 + maxHeight = maxOf(maxHeight, currentHeight) + val rect = Rectangle( + measurements.x + PADDING + (PAGE_WIDTH + PADDING) * xOffset, + yOffset + measurements.y + PADDING, + PAGE_WIDTH, + currentHeight + ) + func(rect, page, inventory) + xOffset++ + if (xOffset >= pageWidthCount) { + yOffset += maxHeight + xOffset = 0 + maxHeight = 0 + } + } + lastRenderedInnerHeight = maxHeight + yOffset + scroll.toInt() + } + + fun drawPage( + context: DrawContext, + x: Int, + y: Int, + page: StoragePageSlot, + inventory: StorageData.StorageInventory, + slots: List<Slot>?, + slotOffset: Point, + ): Int { + val inv = inventory.inventory + if (inv == null) { + context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18) + context.drawText(textRenderer, + Text.literal("TODO: open this page"), + x + 4, + y + 4, + -1, + true) + return 18 + } + assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 } + val name = page.defaultName() + context.drawText(textRenderer, Text.literal(name), x + 4, y + 2, + if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true) + context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE) + inv.stacks.forEachIndexed { index, stack -> + val slotX = (index % 9) * SLOT_SIZE + x + 1 + val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1 + if (slots == null) { + context.drawItem(stack, slotX, slotY) + context.drawItemInSlot(textRenderer, stack, slotX, slotY) + } else { + val slot = slots[index] + slot.x = slotX - slotOffset.x + slot.y = slotY - slotOffset.y + } + } + return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight + } + + fun getBounds(): List<Rectangle> { + return listOf( + Rectangle(measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight), + Rectangle(measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT)) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt new file mode 100644 index 0000000..2cbd54e --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt @@ -0,0 +1,123 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import org.lwjgl.glfw.GLFW +import kotlin.math.max +import net.minecraft.block.Blocks +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.Item +import net.minecraft.item.Items +import net.minecraft.text.Text +import net.minecraft.util.DyeColor +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.toShedaniel + +class StorageOverviewScreen() : Screen(Text.empty()) { + companion object { + val emptyStorageSlotItems = listOf<Item>( + Blocks.RED_STAINED_GLASS_PANE.asItem(), + Blocks.BROWN_STAINED_GLASS_PANE.asItem(), + Items.GRAY_DYE + ) + val pageWidth get() = 19 * 9 + } + + val content = StorageOverlay.Data.data ?: StorageData() + var isClosing = false + + var scroll = 0 + var lastRenderedHeight = 0 + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + context.fill(0, 0, width, height, 0x90000000.toInt()) + layoutedForEach { (key, value), offsetX, offsetY -> + context.matrices.push() + context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F) + renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY) + context.matrices.pop() + } + } + + inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) { + var offsetY = 0 + var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll + var totalHeight = -currentMaxHeight + content.storageInventories.onEachIndexed { index, (key, value) -> + val pageX = (index % StorageOverlay.config.columns) + if (pageX == 0) { + currentMaxHeight += StorageOverlay.config.padding + offsetY += currentMaxHeight + totalHeight += currentMaxHeight + currentMaxHeight = 0 + } + val xPosition = + width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding) + onEach(Pair(key, value), xPosition, offsetY) + val height = getStorePageHeight(value) + currentMaxHeight = max(currentMaxHeight, height) + } + lastRenderedHeight = totalHeight + currentMaxHeight + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + layoutedForEach { (k, p), x, y -> + val rx = mouseX - x + val ry = mouseY - y + if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) { + close() + StorageOverlay.lastStorageOverlay = this + k.navigateTo() + return true + } + } + return super.mouseClicked(mouseX, mouseY, button) + } + + fun getStorePageHeight(page: StorageData.StorageInventory): Int { + return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60 + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + scroll = + (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt() + .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0) + return true + } + + private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { + context.drawText(MC.font, page.title, 2, 2, -1, true) + val inventory = page.inventory + if (inventory == null) { + // TODO: Missing texture + context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color) + context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1) + return + } + + for ((index, stack) in inventory.stacks.withIndex()) { + val x = (index % 9) * 19 + val y = (index / 9) * 19 + MC.font.fontHeight + 2 + if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) { + context.fill(x, y, x + 18, y + 18, 0x80808080.toInt()) + } else { + context.fill(x, y, x + 18, y + 18, 0x40808080.toInt()) + } + context.drawItem(stack, x + 1, y + 1) + context.drawItemInSlot(MC.font, stack, x + 1, y + 1) + } + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) + isClosing = true + return super.keyPressed(keyCode, scanCode, modifiers) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt b/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt new file mode 100644 index 0000000..9259415 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt @@ -0,0 +1,66 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.util.MC + +@Serializable(with = StoragePageSlot.Serializer::class) +data class StoragePageSlot(val index: Int) : Comparable<StoragePageSlot> { + object Serializer : KSerializer<StoragePageSlot> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StoragePageSlot", PrimitiveKind.INT) + + override fun deserialize(decoder: Decoder): StoragePageSlot { + return StoragePageSlot(decoder.decodeInt()) + } + + override fun serialize(encoder: Encoder, value: StoragePageSlot) { + encoder.encodeInt(value.index) + } + } + + init { + assert(index in 0 until (3 * 9)) + } + + val isEnderChest get() = index < 9 + val isBackPack get() = !isEnderChest + val slotIndexInOverviewPage get() = if (isEnderChest) index + 9 else index + 18 + fun defaultName(): String = if (isEnderChest) "Ender Chest #${index + 1}" else "Backpack #${index - 9 + 1}" + + fun navigateTo() { + if (isBackPack) { + MC.sendCommand("backpack ${index - 9 + 1}") + } else { + MC.sendCommand("enderchest ${index + 1}") + } + } + + companion object { + fun fromOverviewSlotIndex(slot: Int): StoragePageSlot? { + if (slot in 9 until 18) return StoragePageSlot(slot - 9) + if (slot in 27 until 45) return StoragePageSlot(slot - 27 + 9) + return null + } + + fun ofEnderChestPage(slot: Int): StoragePageSlot { + assert(slot in 1..9) + return StoragePageSlot(slot - 1) + } + + fun ofBackPackPage(slot: Int): StoragePageSlot { + assert(slot in 1..18) + return StoragePageSlot(slot - 1 + 9) + } + } + + override fun compareTo(other: StoragePageSlot): Int { + return this.index - other.index + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt new file mode 100644 index 0000000..e07df8a --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -0,0 +1,65 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import io.ktor.util.decodeBase64Bytes +import io.ktor.util.encodeBase64 +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtIo +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtSizeTracker + +@Serializable(with = VirtualInventory.Serializer::class) +data class VirtualInventory( + val stacks: List<ItemStack> +) { + val rows = stacks.size / 9 + + init { + assert(stacks.size % 9 == 0) + assert(stacks.size / 9 in 1..5) + } + + + object Serializer : KSerializer<VirtualInventory> { + const val INVENTORY = "INVENTORY" + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): VirtualInventory { + val s = decoder.decodeString() + val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000)) + val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt()) + return VirtualInventory(items.map { + it as NbtCompound + if (it.isEmpty) ItemStack.EMPTY + else runCatching { + ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow + }.getOrElse { ItemStack.EMPTY } + }) + } + + override fun serialize(encoder: Encoder, value: VirtualInventory) { + val list = NbtList() + value.stacks.forEach { + if (it.isEmpty) list.add(NbtCompound()) + else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow } + .getOrElse { NbtCompound() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos) + encoder.encodeString(baos.toByteArray().encodeBase64()) + } + } +} |