aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/inventory
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/features/inventory
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadFirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/features/inventory')
-rw-r--r--src/main/kotlin/features/inventory/CraftingOverlay.kt66
-rw-r--r--src/main/kotlin/features/inventory/ItemRarityCosmetics.kt85
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt51
-rw-r--r--src/main/kotlin/features/inventory/SaveCursorPosition.kt66
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt203
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt85
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt184
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt35
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt88
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt53
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageData.kt21
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt154
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt98
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt296
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt123
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt66
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt65
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
+