diff options
Diffstat (limited to 'src/main/kotlin/features/inventory')
21 files changed, 1974 insertions, 770 deletions
diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt index d2c79fd..5241f54 100644 --- a/src/main/kotlin/features/inventory/CraftingOverlay.kt +++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt @@ -1,20 +1,20 @@ package moe.nea.firmament.features.inventory import io.github.moulberry.repo.data.NEUCraftingRecipe -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.item.ItemStack -import net.minecraft.util.Formatting +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.item.ItemStack +import net.minecraft.ChatFormatting import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.MC import moe.nea.firmament.util.skyblockId -object CraftingOverlay : FirmamentFeature { +object CraftingOverlay { - private var screen: GenericContainerScreen? = null + private var screen: ContainerScreen? = null private var recipe: NEUCraftingRecipe? = null private var useNextScreen = false private val craftingOverlayIndices = listOf( @@ -24,7 +24,7 @@ object CraftingOverlay : FirmamentFeature { ) val CRAFTING_SCREEN_NAME = "Craft Item" - fun setOverlay(screen: GenericContainerScreen?, recipe: NEUCraftingRecipe) { + fun setOverlay(screen: ContainerScreen?, recipe: NEUCraftingRecipe) { this.screen = screen if (screen == null) { useNextScreen = true @@ -34,7 +34,7 @@ object CraftingOverlay : FirmamentFeature { @Subscribe fun onScreenChange(event: ScreenChangeEvent) { - if (useNextScreen && event.new is GenericContainerScreen + if (useNextScreen && event.new is ContainerScreen && event.new.title?.string == "Craft Item" ) { useNextScreen = false @@ -42,18 +42,19 @@ object CraftingOverlay : FirmamentFeature { } } - override val identifier: String + val identifier: String get() = "crafting-overlay" + @OptIn(ExpensiveItemCacheApi::class) @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 (slot.container != screen?.menu?.container) return + val recipeIndex = craftingOverlayIndices.indexOf(slot.containerSlot) if (recipeIndex < 0) return val expectedItem = recipe.inputs[recipeIndex] - val actualStack = slot.stack ?: ItemStack.EMPTY!! + val actualStack = slot.item ?: ItemStack.EMPTY!! val actualEntry = SBItemStack(actualStack) if ((actualEntry.skyblockId != expectedItem.skyblockId || actualEntry.getStackSize() < expectedItem.amount) && expectedItem.amount.toInt() != 0 @@ -66,15 +67,15 @@ object CraftingOverlay : FirmamentFeature { 0x80FF0000.toInt() ) } - if (!slot.hasStack()) { + if (!slot.hasItem()) { val itemStack = SBItemStack(expectedItem)?.asImmutableItemStack() ?: return - event.context.drawItem(itemStack, event.slot.x, event.slot.y) - event.context.drawStackOverlay( + event.context.renderItem(itemStack, event.slot.x, event.slot.y) + event.context.renderItemDecorations( MC.font, itemStack, event.slot.x, event.slot.y, - "${Formatting.RED}${expectedItem.amount.toInt()}" + "${ChatFormatting.RED}${expectedItem.amount.toInt()}" ) } } diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt index 4aa8202..e9d0631 100644 --- a/src/main/kotlin/features/inventory/ItemHotkeys.kt +++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt @@ -2,22 +2,26 @@ package moe.nea.firmament.features.inventory import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HandledScreenKeyPressedEvent -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.HypixelStaticData -import moe.nea.firmament.repo.ItemCache import moe.nea.firmament.repo.ItemCache.asItemStack import moe.nea.firmament.repo.ItemCache.isBroken import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName object ItemHotkeys { + @Config object TConfig : ManagedConfig("item-hotkeys", Category.INVENTORY) { val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface") } + @OptIn(ExpensiveItemCacheApi::class) @Subscribe fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) { if (!event.matches(TConfig.openGlobalTradeInterface)) { @@ -26,7 +30,7 @@ object ItemHotkeys { var item = event.screen.focusedItemStack ?: return val skyblockId = item.skyBlockId ?: return item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item - if (HypixelStaticData.hasBazaarStock(skyblockId)) { + if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) { MC.sendCommand("bz ${item.getSearchName()}") } else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) { MC.sendCommand("ahs ${item.getSearchName()}") diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index fdc378a..9712067 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -1,45 +1,38 @@ package moe.nea.firmament.features.inventory import java.awt.Color -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderLayer -import net.minecraft.item.ItemStack -import net.minecraft.util.Formatting -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation 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.collections.lastNotNullOfOrNull -import moe.nea.firmament.util.collections.memoizeIdentity -import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.skyblock.Rarity -import moe.nea.firmament.util.unformattedString -object ItemRarityCosmetics : FirmamentFeature { - override val identifier: String +object ItemRarityCosmetics { + val identifier: String get() = "item-rarity-cosmetics" + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val showItemRarityBackground by toggle("background") { false } val showItemRarityInHotbar by toggle("background-hotbar") { false } } - override val config: ManagedConfig - get() = TConfig - private val rarityToColor = Rarity.colourMap.mapValues { - val c = Color(it.value.colorValue!!) + val c = Color(it.value.color!!) c.rgb } - fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) { + fun drawItemStackRarity(drawContext: GuiGraphics, x: Int, y: Int, item: ItemStack) { val rarity = Rarity.fromItem(item) ?: return val rgb = rarityToColor[rarity] ?: 0xFF00FF80.toInt() - drawContext.drawGuiTexture( - RenderLayer::getGuiTextured, - Identifier.of("firmament:item_rarity_background"), + drawContext.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:item_rarity_background"), x, y, 16, 16, rgb @@ -50,7 +43,7 @@ object ItemRarityCosmetics : FirmamentFeature { @Subscribe fun onRenderSlot(it: SlotRenderEvents.Before) { if (!TConfig.showItemRarityBackground) return - val stack = it.slot.stack ?: return + val stack = it.slot.item ?: return drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack) } diff --git a/src/main/kotlin/features/inventory/JunkHighlighter.kt b/src/main/kotlin/features/inventory/JunkHighlighter.kt new file mode 100644 index 0000000..15bdcfa --- /dev/null +++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName +import moe.nea.firmament.util.useMatch + +object JunkHighlighter { + val identifier: String + get() = "junk-highlighter" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val junkRegex by string("regex") { "" } + val highlightBind by keyBinding("highlight") { GLFW.GLFW_KEY_LEFT_CONTROL } + } + + @Subscribe + fun onDrawSlot(event: SlotRenderEvents.After) { + if (!TConfig.highlightBind.isPressed() || TConfig.junkRegex.isEmpty()) return + val junkRegex = TConfig.junkRegex.toPattern() + val slot = event.slot + junkRegex.useMatch(slot.item.getSearchName()) { + event.context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0xffff0000.toInt()) + } + } +} diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt index 5ca10f7..e0bb4b1 100644 --- a/src/main/kotlin/features/inventory/PetFeatures.kt +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -1,40 +1,561 @@ package moe.nea.firmament.features.inventory -import net.minecraft.util.Identifier +import java.util.regex.Matcher +import org.joml.Vector2i +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.util.StringRepresentable +import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.ProfileSwitchEvent +import moe.nea.firmament.events.SlotClickEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.jarvis.JarvisIntegration +import moe.nea.firmament.repo.ExpLadders +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.FirmFormatters.formatPercent +import moe.nea.firmament.util.FirmFormatters.shortFormat import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.formattedString +import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.petData import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblock.TabListAPI +import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.titleCase +import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor -object PetFeatures : FirmamentFeature { - override val identifier: String +object PetFeatures { + val identifier: String get() = "pets" - override val config: ManagedConfig? - get() = TConfig - + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val highlightEquippedPet by toggle("highlight-pet") { true } + val petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { + Vector2i() + } + val petOverlayHudStyle by choice("pet-overlay-hud-style") { PetOverlayHudStyles.PLAIN_NO_BACKGROUND } } - val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + enum class PetOverlayHudStyles : StringRepresentable { + PLAIN_NO_BACKGROUND, + COLOUR_NO_BACKGROUND, + PLAIN_BACKGROUND, + COLOUR_BACKGROUND, + ICON_ONLY; + + override fun getSerializedName() : String { + return name + } + } + + private val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + private val autopetPattern = + "§cAutopet §eequipped your §7\\[Lvl (\\d{1,3})\\] §([fa956d])([\\w\\s]+)§e! §aVIEW RULE".toPattern() + private val petItemPattern = "§aYour pet is now holding (§[fa956d][\\w\\s]+)§a.".toPattern() + private val petLevelUpPattern = "§aYour §([fa956d])([\\w\\s]+) §aleveled up to level §9(\\d+)§a!".toPattern() + private val petMap = HashMap<String, ParsedPet>() + private var currentPetUUID: String = "" + private var tempTabPet: ParsedPet? = null + private var tempChatPet: ParsedPet? = null + + @Subscribe + fun onProfileSwitch(event: ProfileSwitchEvent) { + petMap.clear() + currentPetUUID = "" + tempTabPet = null + tempChatPet = null + } @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { - if (!TConfig.highlightEquippedPet) return - val stack = event.slot.stack - if (stack.petData?.active == true) - petMenuTitle.useMatch(MC.screenName ?: return) { + // Cache pets + petMenuTitle.useMatch(MC.screenName ?: return) { + val stack = event.slot.item + if (!stack.isEmpty) cachePet(stack) + if (stack.petData?.active == true) { + if (currentPetUUID == "") currentPetUUID = stack.skyblockUUID.toString() + // Highlight active pet feature + if (!TConfig.highlightEquippedPet) return event.context.drawGuiTexture( - event.slot.x, event.slot.y, 0, 16, 16, - Identifier.of("firmament:selected_pet_background") + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, ) } + } + } + + private fun cachePet(stack: ItemStack) { + // Cache information about a pet + if (stack.skyblockUUID == null) return + if (petMap.containsKey(stack.skyblockUUID.toString()) && + petMap[stack.skyblockUUID.toString()]?.isComplete == true) return + + val pet = PetParser.parsePetMenuSlot(stack) ?: return + petMap[stack.skyblockUUID.toString()] = pet + } + + @Subscribe + fun onSlotClick(event: SlotClickEvent) { + // Check for switching/removing pet manually + petMenuTitle.useMatch(MC.screenName ?: return) { + if (event.slot.container is Inventory) return + if (event.button != 0 && event.button != 1) return + val petData = event.stack.petData ?: return + if (petData.active == true) { + currentPetUUID = "None" + return + } + if (event.button != 0) return + if (!petMap.containsKey(event.stack.skyblockUUID.toString())) cachePet(event.stack) + currentPetUUID = event.stack.skyblockUUID.toString() + } } + @Subscribe + fun onChatEvent(event: ProcessChatEvent) { + // Handle AutoPet + var matcher = autopetPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == matcher.group(3) && + pet.rarity == PetParser.reversePetColourMap[matcher.group(2)] && + pet.level <= matcher.group(1).toInt() + } + if (tempMap.isNotEmpty()) { + currentPetUUID = tempMap.keys.first() + } else { + tempChatPet = PetParser.parsePetChatMessage(matcher.group(3), matcher.group(2), matcher.group(1).toInt()) + currentPetUUID = "" + } + tempTabPet = null + return + } + // Handle changing pet item + // This is needed for when pet item can't be found in tab list + matcher = petItemPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + petMap[currentPetUUID]?.petItem = matcher.group(1) + tempTabPet?.petItem = matcher.group(1) + tempChatPet?.petItem = matcher.group(1) + // TODO: Handle tier boost pet items if required + // I'm not rich enough to be able to test tier boosts + } + // Handle pet levelling up + // This is needed for when pet level can't be found in tab list + matcher = petLevelUpPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempPet = + PetParser.parsePetChatMessage(matcher.group(2), matcher.group(1), matcher.group(3).toInt()) ?: return + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == tempPet.name && + pet.rarity == tempPet.rarity && + pet.level <= tempPet.level + } + if (tempMap.isNotEmpty()) petMap[tempMap.keys.first()]?.update(tempPet) + if (tempTabPet?.name == tempPet.name && tempTabPet?.rarity == tempPet.rarity) { + tempTabPet?.update(tempPet) + } + if (tempChatPet?.name == tempPet.name && tempChatPet?.rarity == tempPet.rarity) tempChatPet?.update(tempPet) + } + } + private fun renderLinesAndBackground(it: HudRenderEvent, lines: List<Component>) { + // Render background for the hud + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + var maxWidth = 0 + lines.forEach { if (MC.font.width(it) > maxWidth) maxWidth = MC.font.width(it.unformattedString) } + val height = if (MC.font.lineHeight * lines.size > 32) MC.font.lineHeight * lines.size else 32 + it.context.fill(0, -3, 40 + maxWidth, height + 2, 0x80000000.toInt()) + } + + // Render text for the hud + lines.forEachIndexed { index, line -> + it.context.drawString( + MC.font, + line.copy().withColor(ChatFormatting.GRAY), + 36, + MC.font.lineHeight * index, + -1, + true + ) + } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay || !SBData.isOnSkyblock) return + + // Possibly handle Montezuma as a future feature? Could track how many pieces have been found etc + // Would likely need to be a separate config toggle though since that has + // very different usefulness/purpose to the pet hud outside of rift + if (SBData.skyblockLocation == SkyBlockIsland.RIFT) return + + // Initial data + var pet: ParsedPet? = null + // Do not render the HUD if there is no pet active + if (currentPetUUID == "None") return + // Get active pet from cache + if (currentPetUUID != "") pet = petMap[currentPetUUID] + // Parse tab widget for pet data + val tabPet = PetParser.parseTabWidget(TabListAPI.getWidgetLines(TabListAPI.WidgetName.PET)) + if (pet == null && tabPet == null && tempTabPet == null && tempChatPet == null) { + // No data on current pet + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + val lines = mutableListOf<Component>() + lines.add(Component.literal("" + ChatFormatting.WHITE + "Unknown Pet")) + lines.add(Component.literal("Open Pets Menu To Fix")) + renderLinesAndBackground(it, lines) + it.context.pose().popMatrix() + return + } + if (pet == null) { + // Pet is only known through tab widget or chat message, potentially saved from tab widget elsewhere + // (e.g. another server or before removing the widget from the tab list) + pet = tabPet ?: tempTabPet ?: tempChatPet ?: return + if (tempTabPet == null) tempTabPet = tabPet + } + + // Update pet based on tab widget if needed + if (tabPet != null && pet.name == tabPet.name && pet.rarity == tabPet.rarity) { + if (tabPet.level > pet.level) { + // Level has increased since caching + pet.level = tabPet.level + pet.currentExp = tabPet.currentExp + pet.expForNextLevel = tabPet.expForNextLevel + pet.totalExp = tabPet.totalExp + } else if (tabPet.currentExp > pet.currentExp) { + // Exp has increased since caching, level has not + pet.currentExp = tabPet.currentExp + pet.totalExp = tabPet.totalExp + } + if (tabPet.petItem != pet.petItem && tabPet.petItem != "Unknown") { + // Pet item has changed since caching + pet.petItem = tabPet.petItem + pet.petItemStack = tabPet.petItemStack + } + } + + // Set the text for the HUD + + val lines = mutableListOf<Component>() + + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + // Colour Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForNextLevel)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L100: ${shortFormat(pet.totalExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForMax)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "" + ChatFormatting.AQUA + ChatFormatting.BOLD + "MAX LEVEL" + )) + lines.add(Component.literal( + "" + ChatFormatting.GOLD + "+" + ChatFormatting.YELLOW + "${shortFormat(pet.overflowExp)} XP" + )) + } + } else if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND) { + // Plain Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(if (pet.petItem != "None" && pet.petItem != "Unknown") + pet.petItem.substring(2) else pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}/" + + "${shortFormat(pet.expForNextLevel)} " + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "Required L100: ${shortFormat(pet.totalExp)}/${shortFormat(pet.expForMax)} " + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "MAX LEVEL" + )) + lines.add(Component.literal( + "+${shortFormat(pet.overflowExp)} XP" + )) + } + } + + // Render HUD + + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + + renderLinesAndBackground(it, lines) + + // Draw the ItemStack + it.context.pose().pushMatrix() + it.context.pose().translate(-0.5F, -0.5F) + it.context.pose().scale(2f, 2f) + it.context.renderItem(pet.petItemStack.value, 0, 0) + it.context.pose().popMatrix() + + it.context.pose().popMatrix() + } +} + +object PetParser { + private val petNamePattern = " §7\\[Lvl (\\d{1,3})] §([fa956d])([\\w\\s]+)".toPattern() + private val petItemPattern = " (§[fa956dbc4][\\s\\w]+)".toPattern() + private val petExpPattern = " §e((?:\\d{1,3}[,.]?)+\\d*[kM]?)§6\\/§e((?:\\d{1,3}[,.]?)+\\d*[kM]?) XP §6\\(\\d+(?:.\\d+)?%\\)".toPattern() + private val petOverflowExpPattern = " §6\\+§e((?:\\d{1,3}[,.])+\\d*[kM]?) XP".toPattern() + private val katPattern = " Kat:.*".toPattern() + + val reversePetColourMap = mapOf( + "f" to Rarity.COMMON, + "a" to Rarity.UNCOMMON, + "9" to Rarity.RARE, + "5" to Rarity.EPIC, + "6" to Rarity.LEGENDARY, + "d" to Rarity.MYTHIC + ) + + val found = HashMap<String, Matcher>() + + @OptIn(ExpensiveItemCacheApi::class) + fun parsePetChatMessage(name: String, rarityCode: String, level: Int) : ParsedPet? { + val petId = name.uppercase().replace(" ", "_") + val petRarity = reversePetColourMap[rarityCode] ?: Rarity.COMMON + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(level + 1).toDouble() - + expLadder.getPetExpForLevel(level).toDouble() + } + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(level).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + return ParsedPet( + name, + petRarity, + level, + -1, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + 0.0, + "Unknown", + petItemStack, + false + ) + } + + @OptIn(ExpensiveItemCacheApi::class) + fun parseTabWidget(lines: List<Component>): ParsedPet? { + found.clear() + for (line in lines.reversed()) { + if (!found.containsKey("kat")) { + val matcher = katPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["kat"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["exp"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petOverflowExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["overflow"] = matcher + continue + } + } + if (!found.containsKey("item")) { + val matcher = petItemPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["item"] = matcher + continue + } + } + if (!found.containsKey("name")) { + val matcher = petNamePattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["name"] = matcher + continue + } + } + } + if (!found.containsKey("name")) return null + + val petName = titleCase(found.getValue("name").group(3)) + val petRarity = reversePetColourMap.getValue(found.getValue("name").group(2)) + val petId = petName.uppercase().replace(" ", "_") + + val petLevel = found.getValue("name").group(1).toInt() + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(petLevel + 1).toDouble() - + expLadder.getPetExpForLevel(petLevel).toDouble() + } + + val overflowExp: Double = if (found.containsKey("overflow")) + parseShortNumber(found.getValue("overflow").group(1)) else 0.0 + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(petLevel).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + + var petItem = "Unknown" + if (found.containsKey("item")) { + petItem = found.getValue("item").group(1) + } + + return ParsedPet( + petName, + petRarity, + petLevel, + maxLevel, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + overflowExp, + petItem, + petItemStack, + false + ) + } + + fun parsePetMenuSlot(stack: ItemStack) : ParsedPet? { + val petData = stack.petData ?: return null + val expData = petData.level + val overflow = if (expData.expTotal - expData.expRequiredForMaxLevel > 0) + (expData.expTotal - expData.expRequiredForMaxLevel).toDouble() else 0.0 + val petItem = if (stack.petData?.heldItem != null) + RepoManager.neuRepo.items.items.getValue(stack.petData?.heldItem).displayName else "None" + return ParsedPet( + titleCase(petData.type), + Rarity.fromNeuRepo(petData.tier) ?: Rarity.COMMON, + expData.currentLevel, + expData.maxLevel, + ExpLadders.getExpLadder(petData.skyblockId.toString(), petData.tier), + expData.expInCurrentLevel.toDouble(), + expData.expRequiredForNextLevel.toDouble(), + expData.expTotal.toDouble(), + expData.expTotal.toDouble() - expData.expInCurrentLevel.toDouble(), + expData.expRequiredForMaxLevel.toDouble(), + overflow, + petItem, + lazy { stack }, + true + ) + } +} + +data class ParsedPet( + val name: String, + val rarity: Rarity, + var level: Int, + val maxLevel: Int, + val expLadder: ExpLadders.ExpLadder?, + var currentExp: Double, + var expForNextLevel: Double, + var totalExp: Double, + var totalExpBeforeLevel: Double, + val expForMax: Double, + var overflowExp: Double, + var petItem: String, + var petItemStack: Lazy<ItemStack>, + var isComplete: Boolean +) { + fun update(other: ParsedPet) { + // Update the pet data to reflect another instance (of itself) + if (other.level > level) { + level = other.level + currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + totalExp = other.totalExp + totalExpBeforeLevel = other.totalExpBeforeLevel + overflowExp = other.overflowExp + } else { + if (other.currentExp > currentExp) currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + if (other.totalExp > totalExp) totalExp = other.totalExp + if (other.totalExpBeforeLevel > totalExpBeforeLevel) totalExpBeforeLevel = other.totalExpBeforeLevel + if (other.overflowExp > overflowExp) overflowExp = other.overflowExp + } + if (other.petItem != "Unknown") petItem = other.petItem + isComplete = false + } } diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt index 4477203..54802db 100644 --- a/src/main/kotlin/features/inventory/PriceData.kt +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -1,51 +1,120 @@ - - package moe.nea.firmament.features.inventory -import net.minecraft.text.Text +import org.lwjgl.glfw.GLFW +import net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable 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.FirmFormatters.formatCommas +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.getLogicalStackSize +import moe.nea.firmament.util.gold import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow + +object PriceData { + val identifier: String + get() = "price-data" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT } + val avgLowestBin by choice( + "avg-lowest-bin-days", + ) { + AvgLowestBin.THREEDAYAVGLOWESTBIN + } + } -object PriceData : FirmamentFeature { - override val identifier: String - get() = "price-data" + enum class AvgLowestBin : StringRepresentable { + OFF, + ONEDAYAVGLOWESTBIN, + THREEDAYAVGLOWESTBIN, + SEVENDAYAVGLOWESTBIN; - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val tooltipEnabled by toggle("enable-always") { true } - val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") - } + override fun getSerializedName(): String { + return name + } + } - override val config get() = TConfig + fun formatPrice(label: Component, price: Double): Component { + return Component.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Component.literal(formatCommas(price, fractionalDigits = 1)) + .append(if (price != 1.0) " coins" else " coin") + .gold() + .bold() + ) + } - @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)) - ) - } - } + @Subscribe + fun onItemTooltip(it: ItemTooltipEvent) { + if (!TConfig.tooltipEnabled) return + if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return + val sbId = it.stack.skyBlockId + val stackSize = it.stack.getLogicalStackSize() + val isShowingStack = TConfig.stackSizeKey.isPressed() + val multiplier = if (isShowingStack) stackSize else 1 + val multiplierText = + if (isShowingStack) + tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey() + else + tr( + "firmament.tooltip.multiply.hint", + "[${TConfig.stackSizeKey.format()}] to show x${stackSize}" + ).darkGrey() + val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock] + val lowestBin = HypixelStaticData.lowestBin[sbId] + val avgBinValue: Double? = when (TConfig.avgLowestBin) { + AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId] + AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId] + AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId] + AvgLowestBin.OFF -> null + } + if (bazaarData != null) { + it.lines.add(Component.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"), + bazaarData.quickStatus.sellPrice * multiplier + ) + ) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"), + bazaarData.quickStatus.buyPrice * multiplier + ) + ) + } else if (lowestBin != null) { + it.lines.add(Component.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"), + lowestBin * multiplier + ) + ) + if (avgBinValue != null) { + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"), + avgBinValue * multiplier + ) + ) + } + } + } } diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt index 1e9b1b8..e508016 100644 --- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -1,12 +1,13 @@ package moe.nea.firmament.features.inventory +import java.net.URI import net.fabricmc.loader.api.FabricLoader import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds import net.minecraft.SharedConstants -import net.minecraft.text.ClickEvent -import net.minecraft.text.Text +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute @@ -30,27 +31,28 @@ object REIDependencyWarner { var sentWarning = false fun modrinthLink(slug: String) = - "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric" + "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getCurrentVersion().name()}&l=fabric" - fun downloadButton(modName: String, modId: String, slug: String): Text { + fun downloadButton(modName: String, modId: String, slug: String): Component { val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId) - return Text.literal(" - ") + return Component.literal(" - ") .white() - .append(Text.literal("[").aqua()) - .append(Text.translatable("firmament.download", modName) - .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) } + .append(Component.literal("[").aqua()) + .append(Component.translatable("firmament.download", modName) + .withStyle { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } .yellow() .also { if (alreadyDownloaded) - it.append(Text.translatable("firmament.download.already", modName) + it.append(Component.translatable("firmament.download.already", modName) .lime()) }) - .append(Text.literal("]").aqua()) + .append(Component.literal("]").aqua()) } @Subscribe fun checkREIDependency(event: SkyblockServerUpdateEvent) { if (!SBData.isOnSkyblock) return + if (!RepoManager.TConfig.warnForMissingItemListMod) return if (hasREI) return if (sentWarning) return sentWarning = true @@ -58,11 +60,11 @@ object REIDependencyWarner { delay(2.seconds) // TODO: should we offer an automatic install that actually downloads the JARs and places them into the mod folder? MC.sendChat( - Text.translatable("firmament.reiwarning").red().bold().append("\n") + Component.translatable("firmament.reiwarning").red().bold().append("\n") .append(downloadButton("RoughlyEnoughItems", reiModId, "rei")).append("\n") .append(downloadButton("Architectury API", "architectury", "architectury-api")).append("\n") .append(downloadButton("Cloth Config API", "cloth-config", "cloth-config")).append("\n") - .append(Text.translatable("firmament.reiwarning.disable") + .append(Component.translatable("firmament.reiwarning.disable") .clickCommand("/firm disablereiwarning") .grey()) ) @@ -74,9 +76,9 @@ object REIDependencyWarner { if (hasREI) return event.subcommand("disablereiwarning") { thenExecute { - RepoManager.Config.warnForMissingItemListMod = false - RepoManager.Config.save() - MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow()) + RepoManager.TConfig.warnForMissingItemListMod = false + RepoManager.TConfig.markDirty() + MC.sendChat(Component.translatable("firmament.reiwarning.disabled").yellow()) } } } diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt index c47867b..c492a75 100644 --- a/src/main/kotlin/features/inventory/SaveCursorPosition.kt +++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt @@ -1,66 +1,63 @@ - - package moe.nea.firmament.features.inventory +import org.lwjgl.glfw.GLFW 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 com.mojang.blaze3d.platform.InputConstants 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, Category.INVENTORY) { - 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) - } +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object SaveCursorPosition { + val identifier: String + get() = "save-cursor-position" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + 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 + ) { + InputConstants.grabOrReleaseMouse( + MC.window, + InputConstants.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 index 99130d5..fca40c8 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,107 +4,197 @@ package moe.nea.firmament.features.inventory import java.util.UUID import org.lwjgl.glfw.GLFW +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.int 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.Slot -import net.minecraft.screen.slot.SlotActionType -import net.minecraft.util.Identifier -import net.minecraft.util.StringIdentifiable +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.ChestMenu +import net.minecraft.world.inventory.InventoryMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientInitEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyReleasedEvent import moe.nea.firmament.events.IsSlotProtectedEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.keybindings.InputModifiers 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.accessors.castAccessor +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.lime import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt -import moe.nea.firmament.util.render.GuiRenderLayers +import moe.nea.firmament.util.red import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.DungeonUtil +import moe.nea.firmament.util.skyblock.SkyBlockItems import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString -object SlotLocking : FirmamentFeature { - override val identifier: String +object SlotLocking { + val identifier: String get() = "slot-locking" @Serializable - data class Data( + data class DimensionData( val lockedSlots: MutableSet<Int> = mutableSetOf(), - val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), + val boundSlots: BoundSlots = BoundSlots(), + ) + + @Serializable + data class Data( val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val rift: DimensionData = DimensionData(), + val overworld: DimensionData = DimensionData(), + ) + + + val currentWorldData + get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT) + DConfig.data.rift + else + DConfig.data.overworld + + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, ) + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet<BoundSlot> = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List<BoundSlot> { + return pairs.filter { it.hotbar == index || it.inventory == index } + } + + fun removeDuplicateForInventory(index: Int) { + pairs.removeIf { it.inventory == index } + } + + fun removeAllInvolving(index: Int): Boolean { + return pairs.removeIf { it.inventory == index || it.hotbar == index } + } + + fun insert(hotbar: Int, inventory: Int) { + if (!TConfig.allowMultiBinding) { + removeAllInvolving(hotbar) + removeAllInvolving(inventory) + } + pairs.add(BoundSlot(hotbar, inventory)) + } + + object Serializer : KSerializer<BoundSlots> { + override val descriptor: SerialDescriptor + get() = serializer<JsonElement>().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer<MutableSet<BoundSlot>>() + .serialize(encoder, value.pairs) + } + + override fun deserialize(decoder: Decoder): BoundSlots { + decoder as JsonDecoder + val json = decoder.decodeJsonElement() + if (json is JsonObject) { + return BoundSlots(json.entries.map { + BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int) + }.toMutableSet()) + } + return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json)) + + } + } + } + + + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { - SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true) + SavedKeyBinding.keyWithMods(GLFW.GLFW_KEY_L, InputModifiers.of(shift = true)) } val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L } val slotBindRequireShift by toggle("require-quick-move") { true } val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES } + val slotBindOnlyInInv by toggle("bind-only-in-inv") { false } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option + val protectAllHuntingBoxes by toggle("hunting-box") { false } + val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } - enum class SlotRenderLinesMode : StringIdentifiable { + enum class SlotRenderLinesMode : StringRepresentable { EVERYTHING, ONLY_BOXES, NOTHING; - override fun asString(): String { + override fun getSerializedName(): String { return name } } - override val config: TConfig - get() = TConfig - + @Config 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 - } + get() = currentWorldData?.lockedSlots - fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { + fun isSalvageScreen(screen: AbstractContainerScreen<*>?): Boolean { if (screen == null) return false return screen.title.unformattedString.contains("Salvage Item") } - fun isTradeScreen(screen: HandledScreen<*>?): Boolean { + fun isTradeScreen(screen: AbstractContainerScreen<*>?): 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) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val middlePane = handler.container.getItem(handler.container.containerSize - 5) if (middlePane == null) return false return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" } - fun isNpcShop(screen: HandledScreen<*>?): Boolean { + fun isNpcShop(screen: AbstractContainerScreen<*>?): 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) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val sellItem = handler.container.getItem(handler.container.containerSize - 5) if (sellItem == null) return false if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true val lore = sellItem.loreAccordingToNbt @@ -114,13 +204,19 @@ object SlotLocking : FirmamentFeature { @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 + if (!event.slot.hasItem()) return + if (event.slot.item.displayNameAccordingToNbt.unformattedString != "Salvage Items") return + val inv = event.slot.container var anyBlocked = false - for (i in 0 until event.slot.index) { - val stack = inv.getStack(i) - if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + for (i in 0 until event.slot.containerSlot) { + val stack = inv.getItem(i) + if (IsSlotProtectedEvent.shouldBlockInteraction( + null, + ClickType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack + ) + ) anyBlocked = true } if (anyBlocked) { @@ -130,46 +226,69 @@ object SlotLocking : FirmamentFeature { @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 doesNotDeleteItem = event.actionType == ClickType.SWAP + || event.actionType == ClickType.PICKUP + || event.actionType == ClickType.QUICK_MOVE + || event.actionType == ClickType.QUICK_CRAFT + || event.actionType == ClickType.CLONE + || event.actionType == ClickType.PICKUP_ALL val isSellOrTradeScreen = isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) - if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) + if ((!isSellOrTradeScreen || event.slot?.container !is Inventory) && doesNotDeleteItem ) return val stack = event.itemStack ?: return + if (TConfig.protectAllHuntingBoxes && (stack.isHuntingBox())) { + event.protect() + return + } val uuid = stack.skyblockUUID ?: return if (uuid in (lockedUUIDs ?: return)) { event.protect() } } + fun ItemStack.isHuntingBox(): Boolean { + return skyBlockId == SkyBlockItems.HUNTING_TOOLKIT || extraAttributes.get("tool_kit") != null + } + @Subscribe fun onProtectSlot(it: IsSlotProtectedEvent) { - if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) { + if (it.slot != null && it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf())) { it.protect() } } @Subscribe + fun onEvent(event: ClientInitEvent) { + IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") { + if (it.isProtected + && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR + && DungeonUtil.isInActiveDungeon + && TConfig.allowDroppingInDungeons + ) { + it.isProtected = false + } + } + } + + @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { - val boundSlots = DConfig.data?.boundSlots ?: mapOf() + val boundSlots = currentWorldData?.boundSlots ?: BoundSlots() val isValidAction = - it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) + it.actionType == ClickType.QUICK_MOVE || (it.actionType == ClickType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return - val handler = MC.handledScreen?.screenHandler ?: return + val handler = MC.handledScreen?.menu ?: return + if (TConfig.slotBindOnlyInInv && handler !is InventoryMenu) + return val slot = it.slot - if (slot != null && it.slot.inventory is PlayerInventory) { - val boundSlot = boundSlots.entries.find { - it.value == slot.index || it.key == slot.index - } ?: return + if (slot != null && it.slot.container is Inventory) { + val matchingSlots = boundSlots.findMatchingSlots(slot.containerSlot) + if (matchingSlots.isEmpty()) return it.protectSilent() - val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) - inventorySlot?.swapWithHotBar(handler, boundSlot.key) + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @@ -177,10 +296,25 @@ object SlotLocking : FirmamentFeature { fun onLockUUID(it: HandledScreenKeyPressedEvent) { if (!it.matches(TConfig.lockUUID)) return val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - val stack = slot.stack ?: return + val stack = slot.item ?: return + if (stack.isHuntingBox()) { + MC.sendChat( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint", + "The hunting box cannot be UUID bound reliably. It changes its own UUID frequently when switching tools. " + ).red().append( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint.solution", + "Use the Firmament config option for locking all hunting boxes instead." + ).lime() + ) + ) + CommonSoundEffects.playFailure() + return + } val uuid = stack.skyblockUUID ?: return val lockedUUIDs = lockedUUIDs ?: return if (uuid in lockedUUIDs) { @@ -197,7 +331,7 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament val storedSlot = storedLockingSlot ?: return @@ -205,13 +339,11 @@ object SlotLocking : FirmamentFeature { storedLockingSlot = null val hotBarSlot = if (slot.isHotbar()) slot else storedSlot val invSlot = if (slot.isHotbar()) storedSlot else slot - val boundSlots = DConfig.data?.boundSlots ?: return - lockedSlots?.remove(hotBarSlot.index) - lockedSlots?.remove(invSlot.index) - boundSlots.entries.removeIf { - it.value == invSlot.index - } - boundSlots[hotBarSlot.index] = invSlot.index + val boundSlots = currentWorldData?.boundSlots ?: return + lockedSlots?.remove(hotBarSlot.containerSlot) + lockedSlots?.remove(invSlot.containerSlot) + boundSlots.removeDuplicateForInventory(invSlot.containerSlot) + boundSlots.insert(hotBarSlot.containerSlot, invSlot.containerSlot) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -223,61 +355,75 @@ object SlotLocking : FirmamentFeature { } if (it.matches(TConfig.slotBind)) { storedLockingSlot = null - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.containerSlot) } } @Subscribe fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) { - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true) - val accScreen = event.screen as AccessorHandledScreen + val accScreen = event.screen.castAccessor() val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - for (it in boundSlots.entries) { - val hotbarSlot = findByIndex(it.key) ?: continue - val inventorySlot = findByIndex(it.value) ?: continue + val highlitSlots = mutableSetOf<Slot>() + for (it in boundSlots.pairs) { + val hotbarSlot = findByIndex(it.hotbar) ?: continue + val inventorySlot = findByIndex(it.inventory) ?: continue val (hotX, hotY) = hotbarSlot.lineCenter() val (invX, invY) = inventorySlot.lineCenter() val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot - || accScreen.focusedSlot_Firmament === inventorySlot + || accScreen.focusedSlot_Firmament === inventorySlot if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) continue - val color = if (anyHovered) - me.shedaniel.math.Color.ofOpaque(0x00FF00) - else - me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) + if (anyHovered) { + highlitSlots.add(hotbarSlot) + highlitSlots.add(inventorySlot) + } + fun color(highlit: Boolean) = + if (highlit) + me.shedaniel.math.Color.ofOpaque(0x00FF00) + else + me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered) event.context.drawLine( invX + sx, invY + sy, hotX + sx, hotY + sy, - color + color(anyHovered) ) - event.context.drawBorder(hotbarSlot.x + sx, - hotbarSlot.y + sy, - 16, 16, color.color) - event.context.drawBorder(inventorySlot.x + sx, - inventorySlot.y + sy, - 16, 16, color.color) + event.context.submitOutline( + hotbarSlot.x + sx, + hotbarSlot.y + sy, + 16, 16, color(hotbarSlot in highlitSlots).color + ) + event.context.submitOutline( // TODO: 1.21.10 + inventorySlot.x + sx, + inventorySlot.y + sy, + 16, 16, color(inventorySlot in highlitSlots).color + ) } } @Subscribe fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) { val draggingSlot = storedLockingSlot ?: return - val accScreen = event.screen as AccessorHandledScreen + val accScreen = event.screen.castAccessor() val hoveredSlot = accScreen.focusedSlot_Firmament - ?.takeIf { it.inventory is PlayerInventory } + ?.takeIf { it.container is Inventory } ?.takeIf { it == draggingSlot || it.isHotbar() != draggingSlot.isHotbar() } val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament val (borderX, borderY) = draggingSlot.lineCenter() - event.context.drawBorder(draggingSlot.x + sx, draggingSlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) + event.context.submitOutline( + draggingSlot.x + sx, + draggingSlot.y + sy, + 16, + 16, + 0xFF00FF00u.toInt() + ) // TODO: 1.21.10 if (hoveredSlot == null) { event.context.drawLine( borderX + sx, borderY + sy, @@ -291,9 +437,11 @@ object SlotLocking : FirmamentFeature { hovX + sx, hovY + sy, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) - event.context.drawBorder(hoveredSlot.x + sx, - hoveredSlot.y + sy, - 16, 16, 0xFF00FF00u.toInt()) + event.context.submitOutline( + hoveredSlot.x + sx, + hoveredSlot.y + sy, + 16, 16, 0xFF00FF00u.toInt() + ) } } @@ -301,13 +449,13 @@ object SlotLocking : FirmamentFeature { return if (isHotbar()) { x + 9 to y } else { - x + 9 to y + 17 + x + 9 to y + 16 } } fun Slot.isHotbar(): Boolean { - return index < 9 + return containerSlot < 9 } @Subscribe @@ -319,16 +467,14 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() - if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + val boundSlots = currentWorldData?.boundSlots ?: BoundSlots() + if (slot.container is Inventory) { + if (boundSlots.removeAllInvolving(slot.containerSlot)) { // intentionally do nothing - } else if (slot.index in lockedSlots) { - lockedSlots.remove(slot.index) + } else if (slot.containerSlot in lockedSlots) { + lockedSlots.remove(slot.containerSlot) } else { - lockedSlots.add(slot.index) + lockedSlots.add(slot.containerSlot) } DConfig.markDirty() CommonSoundEffects.playSuccess() @@ -338,10 +484,10 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlot(it: HandledScreenKeyPressedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - if (slot.inventory !is PlayerInventory) return + if (slot.container !is Inventory) return if (it.matches(TConfig.slotBind)) { storedLockingSlot = storedLockingSlot ?: slot return @@ -354,17 +500,17 @@ object SlotLocking : FirmamentFeature { @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()) + val isSlotLocked = it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf()) + val isUUIDLocked = (it.slot.item?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { - it.context.drawGuiTexture( - GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, + it.context.blitSprite( + RenderPipelines.GUI_TEXTURED, when { isSlotLocked -> - (Identifier.of("firmament:slot_locked")) + (ResourceLocation.parse("firmament:slot_locked")) isUUIDLocked -> - (Identifier.of("firmament:uuid_locked")) + (ResourceLocation.parse("firmament:uuid_locked")) else -> error("unreachable") diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..9bb78c9 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -7,25 +7,29 @@ import java.time.format.DateTimeFormatterBuilder import java.time.format.FormatStyle import java.time.format.TextStyle import java.time.temporal.ChronoField -import net.minecraft.text.Text -import net.minecraft.util.StringIdentifiable +import net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ItemTooltipEvent -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.SBData import moe.nea.firmament.util.aqua +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.grey import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.timestamp import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString object TimerInLore { + @Config object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) { val showTimers by toggle("show") { true } + val showCreationTimestamp by toggle("show-creation") { true } val timerFormat by choice("format") { TimerFormat.SOCIALIST } } - enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable { + enum class TimerFormat(val formatter: DateTimeFormatter) : StringRepresentable { RFC(DateTimeFormatter.RFC_1123_DATE_TIME), LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), SOCIALIST( @@ -45,6 +49,7 @@ object TimerInLore { appendValue(ChronoField.SECOND_OF_MINUTE, 2) }), AMERICAN("EEEE, MMM d h:mm a yyyy"), + RFCPrecise(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss.SSS Z")), ; constructor(block: DateTimeFormatterBuilder.() -> Unit) @@ -52,7 +57,7 @@ object TimerInLore { constructor(format: String) : this(DateTimeFormatter.ofPattern(format)) - override fun asString(): String { + override fun getSerializedName(): String { return name } } @@ -80,13 +85,25 @@ object TimerInLore { COMMUNITYPROJECTS("Contribute again", "Come back at"), CHOCOLATEFACTORY("Next Charge", "Available at"), STONKSAUCTION("Auction ends in", "Ends at"), - LIZSTONKREDEMPTION("Resets in:", "Resets at"); + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + TIMEREMAININGS("Time Remaining:", "Ends at"), + COOLDOWN("Cooldown:", "Come back at"), + ONCOOLDOWN("On cooldown:", "Available at"), + EVENTENDING("Event ends in:", "Ends at"); } val regex = "(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex() @Subscribe + fun creationInLore(event: ItemTooltipEvent) { + if (!TConfig.showCreationTimestamp) return + val timestamp = event.stack.timestamp ?: return + val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault())) + event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey()) + } + + @Subscribe fun modifyLore(event: ItemTooltipEvent) { if (!TConfig.showTimers) return var lastTimer: ZonedDateTime? = null @@ -107,9 +124,13 @@ object TimerInLore { var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone) if (countdownType.isRelative) { if (lastTimer == null) { - event.lines.add(i + 1, - tr("firmament.loretimer.missingrelative", - "Found a relative countdown with no baseline (Firmament)").grey()) + event.lines.add( + i + 1, + tr( + "firmament.loretimer.missingrelative", + "Found a relative countdown with no baseline (Firmament)" + ).grey() + ) continue } baseLine = lastTimer @@ -119,10 +140,11 @@ object TimerInLore { lastTimer = timer val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault()) // TODO: install approximate time stabilization algorithm - event.lines.add(i + 1, - Text.literal("${countdownType.label}: ") - .grey() - .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua()) + event.lines.add( + i + 1, + Component.literal("${countdownType.label}: ") + .grey() + .append(Component.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua()) ) } } diff --git a/src/main/kotlin/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt new file mode 100644 index 0000000..8d4760b --- /dev/null +++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton + +object WardrobeKeybinds { + @Config + object TConfig : ManagedConfig("wardrobe-keybinds", Category.INVENTORY) { + val wardrobeKeybinds by toggle("wardrobe-keybinds") { false } + val changePageKeybind by keyBinding("change-page") { GLFW.GLFW_KEY_ENTER } + val nextPage by keyBinding("next-page") { GLFW.GLFW_KEY_D } + val previousPage by keyBinding("previous-page") { GLFW.GLFW_KEY_A } + val slotKeybinds = (1..9).map { + keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it } + } + val allowUnequipping by toggle("allow-unequipping") { true } + } + + val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) -> + index + 36 to keybinding + } + + @Subscribe + fun switchSlot(event: HandledScreenKeyPressedEvent) { + if (MC.player == null || MC.world == null || MC.interactionManager == null) return + if (event.screen !is AbstractContainerScreen<*>) return + + val regex = Regex("Wardrobe \\([12]/2\\)") + if (!regex.matches(event.screen.title.string)) return + if (!TConfig.wardrobeKeybinds) return + + if ( + event.matches(TConfig.changePageKeybind) || + event.matches(TConfig.previousPage) || + event.matches(TConfig.nextPage) + ) { + event.cancel() + + val handler = event.screen.menu + val previousSlot = handler.getSlot(45) + val nextSlot = handler.getSlot(53) + + val backPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.previousPage) + val nextPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.nextPage) + + if (backPressed && previousSlot.item.item == Items.ARROW) { + previousSlot.clickLeftMouseButton(handler) + } else if (nextPressed && nextSlot.item.item == Items.ARROW) { + nextSlot.clickLeftMouseButton(handler) + } + } + + + val slot = + slotKeybindsWithSlot + .find { event.matches(it.second.get()) } + ?.first ?: return + + event.cancel() + + val handler = event.screen.menu + val invSlot = handler.getSlot(slot) + + val itemStack = invSlot.item + val isSelected = itemStack.item == Items.LIME_DYE + val isSelectable = itemStack.item == Items.PINK_DYE + if (!isSelectable && !isSelected) return + if (!TConfig.allowUnequipping && isSelected) return + + invSlot.clickLeftMouseButton(handler) + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt index a46bd76..0cb51ca 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory.buttons import com.mojang.brigadier.StringReader @@ -7,80 +5,120 @@ 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 net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.commands.CommandBuildContext +import net.minecraft.commands.arguments.item.ItemArgument +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.flag.FeatureFlags +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.ItemCache.isBroken import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.collections.memoize +import moe.nea.firmament.util.mc.arbitraryUUID +import moe.nea.firmament.util.mc.createSkullItem import moe.nea.firmament.util.render.drawGuiTexture @Serializable data class InventoryButton( - var x: Int, - var y: Int, - var anchorRight: Boolean, - var anchorBottom: Boolean, - var icon: String? = "", - var command: String? = "", + var x: Int, + var y: Int, + var anchorRight: Boolean, + var anchorBottom: Boolean, + var icon: String? = "", + var command: String? = "", + var isGigantic: Boolean = false, ) { - 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.drawGuiTexture( - 0, - 0, - 0, - dimensions.width, - dimensions.height, - Identifier.of("firmament:inventory_button_background") - ) - context.drawItem(getItem(), 1, 1) - } + val myDimension get() = if (isGigantic) bigDimension else dimensions + + companion object { + val itemStackParser by lazy { + ItemArgument.item( + CommandBuildContext.simple( + MC.defaultRegistries, + FeatureFlags.VANILLA_SET + ) + ) + } + val dimensions = Dimension(18, 18) + val gap = 2 + val bigDimension = Dimension(dimensions.width * 2 + gap, dimensions.height * 2 + gap) + val getItemForName = ::getItemForName0.memoize(1024) + + @OptIn(ExpensiveItemCacheApi::class) + fun getItemForName0(icon: String): ItemStack { + val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) + var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) + if (repoItem == null) { + when { + icon.startsWith("skull:") -> { + itemStack = createSkullItem( + arbitraryUUID, + "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}" + ) + } + + else -> { + 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)).createItemStack(1, false) + }.getOrNull() + if (componentItem != null) + itemStack = componentItem + } + } + } + if (itemStack.item == Items.PAINTING) + ErrorUtil.logError("created broken itemstack for inventory button $icon: $itemStack") + return itemStack + } + } + + fun render(context: GuiGraphics) { + context.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:inventory_button_background"), + 0, + 0, + myDimension.width, + myDimension.height, + ) + if (isGigantic) { + context.pose().pushMatrix() + context.pose().translate(myDimension.width / 2F, myDimension.height / 2F) + context.pose().scale(2F) + context.renderItem(getItem(), -8, -8) + context.pose().popMatrix() + } else { + context.renderItem(getItem(), 1, 1) + } + } - fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() + 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 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 getBounds(guiRect: Rectangle): Rectangle { + return Rectangle(getPosition(guiRect), myDimension) + } - fun getItem(): ItemStack { - return getItemForName(icon ?: "") - } + 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 index ee3ae8b..6b6a2d6 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -1,17 +1,24 @@ 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.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext 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 net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.MultiLineTextWidget +import net.minecraft.client.gui.components.StringWidget +import net.minecraft.client.input.KeyEvent +import com.mojang.blaze3d.platform.InputConstants +import net.minecraft.network.chat.Component +import net.minecraft.util.Mth +import net.minecraft.world.phys.Vec2 import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.FragmentGuiScreen import moe.nea.firmament.util.MC @@ -28,10 +35,13 @@ class InventoryButtonEditor( @field:Bind var icon: String = originalButton.icon ?: "" + @field:Bind + var isGigantic: Boolean = originalButton.isGigantic + @Bind fun getItemIcon(): IItemStack { save() - return ModernItemStack.of(InventoryButton.getItemForName(icon)) + return MoulConfigPlatform.wrap(InventoryButton.getItemForName(icon)) } @Bind @@ -42,6 +52,7 @@ class InventoryButtonEditor( fun save() { originalButton.icon = icon + originalButton.isGigantic = isGigantic originalButton.command = command } } @@ -49,30 +60,92 @@ class InventoryButtonEditor( var buttons: MutableList<InventoryButton> = InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList() - override fun close() { + override fun onClose() { InventoryButtons.DConfig.data.buttons = buttons InventoryButtons.DConfig.markDirty() - super.close() + super.onClose() + } + + override fun resize(client: Minecraft, width: Int, height: Int) { + lastGuiRect.move( + MC.window.guiScaledWidth / 2 - lastGuiRect.width / 2, + MC.window.guiScaledHeight / 2 - lastGuiRect.height / 2 + ) + super.resize(client, width, height) } override fun init() { super.init() - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) { + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 25, + Component.translatable("firmament.inventory-buttons.delete"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 40, + Component.translatable("firmament.inventory-buttons.info"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.reset")) { + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 10) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.load-preset")) { val t = ClipboardUtils.getTextContents() val newButtons = InventoryButtonTemplates.loadTemplate(t) if (newButtons != null) buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) } - .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 35) .width(lastGuiRect.width - 20) .build() ) - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) { + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.save-preset")) { ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons)) } - .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.simple-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348 + val newButtons = InventoryButtonTemplates.loadTemplate( + "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0=" + ) + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 85) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.all-warps-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276 + val newButtons = InventoryButtonTemplates.loadTemplate( + "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd" + ) + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 110) .width(lastGuiRect.width - 20) .build() ) @@ -83,14 +156,20 @@ class InventoryButtonEditor( val movedButtons = mutableListOf<InventoryButton>() for (button in buttons) { if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) { - MC.sendChat(tr("firmament.inventory-buttons.button-moved", - "One of your imported buttons intersects with the inventory and has been moved to the top left.")) - movedButtons.add(button.copy( - x = 0, - y = -InventoryButton.dimensions.width, - anchorRight = false, - anchorBottom = false - )) + MC.sendChat( + tr( + "firmament.inventory-buttons.button-moved", + "One of your imported buttons intersects with the inventory and has been moved to the top left." + ) + ) + movedButtons.add( + button.copy( + x = 0, + y = -InventoryButton.dimensions.width, + anchorRight = false, + anchorBottom = false + ) + ) } else { newButtons.add(button) } @@ -99,9 +178,11 @@ class InventoryButtonEditor( val zeroRect = Rectangle(0, 0, 1, 1) for (movedButton in movedButtons) { fun getPosition(button: InventoryButton, index: Int) = - button.copy(x = (index % 10) * InventoryButton.dimensions.width, - y = (index / 10) * -InventoryButton.dimensions.height, - anchorRight = false, anchorBottom = false) + button.copy( + x = (index % 10) * InventoryButton.dimensions.width, + y = (index / 10) * -InventoryButton.dimensions.height, + anchorRight = false, anchorBottom = false + ) while (true) { val newPos = getPosition(movedButton, i++) val newBounds = newPos.getBounds(zeroRect) @@ -114,35 +195,48 @@ class InventoryButtonEditor( return newButtons } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + context.pose().pushMatrix() + PanelComponent.DefaultBackgroundRenderer.VANILLA + .render( + MoulConfigRenderContext(context), + lastGuiRect.minX, lastGuiRect.minY, + lastGuiRect.width, lastGuiRect.height, + ) + context.pose().popMatrix() 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.matrices.pop() for (button in buttons) { val buttonPosition = button.getBounds(lastGuiRect) - context.matrices.push() - context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F) + context.pose().pushMatrix() + context.pose().translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat()) button.render(context) - context.matrices.pop() + context.pose().popMatrix() } + renderPopup(context, mouseX, mouseY, delta) } - 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() + override fun keyPressed(input: KeyEvent): Boolean { + if (super.keyPressed(input)) return true + if (input.input() == GLFW.GLFW_KEY_ESCAPE) { + onClose() 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)) } + override fun mouseReleased(click: MouseButtonEvent): Boolean { + if (super.mouseReleased(click)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(click.x, click.y)) } if (clickedButton != null && !justPerformedAClickAction) { - createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY)) + if (InputConstants.isKeyDown( + MC.window, + InputConstants.KEY_LCONTROL + ) + ) Editor(clickedButton).delete() + else createPopup( + MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), + Point(click.x, click.y) + ) return true } justPerformedAClickAction = false @@ -150,19 +244,27 @@ class InventoryButtonEditor( 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 + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + if (super.mouseDragged(click, offsetX, offsetY)) return true - if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) { - initialDragMousePosition = Vec2f(-10F, -10F) + if (initialDragMousePosition.distanceToSqr(Vec2(click.x.toFloat(), click.y.toFloat())) >= 4 * 4) { + initialDragMousePosition = Vec2(-10F, -10F) lastDraggedButton?.let { dragging -> justPerformedAClickAction = true - val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt()) + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(click.x.toInt(), click.y.toInt()) ?: return true dragging.x = offsetX dragging.y = offsetY dragging.anchorRight = anchorRight dragging.anchorBottom = anchorBottom + if (!anchorRight && offsetX > -dragging.myDimension.width + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.x = -dragging.myDimension.width + if (!anchorRight && offsetY > -dragging.myDimension.height + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.y = -dragging.myDimension.height } } return false @@ -170,7 +272,7 @@ class InventoryButtonEditor( var lastDraggedButton: InventoryButton? = null var justPerformedAClickAction = false - var initialDragMousePosition = Vec2f(-10F, -10F) + var initialDragMousePosition = Vec2(-10F, -10F) data class AnchoredCoords( val anchorRight: Boolean, @@ -180,35 +282,30 @@ class InventoryButtonEditor( ) 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 + if (InputConstants.isKeyDown(MC.window, InputConstants.KEY_LSHIFT)) { + offsetX = Mth.floor(offsetX / 20F) * 20 + offsetY = Mth.floor(offsetY / 20F) * 20 } - return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect) + if (rect.intersects(lastGuiRect)) return null + val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + return anchoredCoords } - 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)) } + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + if (super.mouseClicked(click, doubled)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(click.x, click.y) } if (clickedButton != null) { lastDraggedButton = clickedButton - initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat()) + initialDragMousePosition = Vec2(click.y.toFloat(), click.y.toFloat()) return true } - val mx = mouseX.toInt() - val my = mouseY.toInt() + val mx = click.x.toInt() + val my = click.y.toInt() val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null)) justPerformedAClickAction = true diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt index d282157..c6ad14d 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt @@ -1,7 +1,6 @@ package moe.nea.firmament.features.inventory.buttons -import kotlinx.serialization.encodeToString -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC @@ -18,7 +17,7 @@ object InventoryButtonTemplates { ErrorUtil.catch<InventoryButton?>("Could not import button") { Firmament.json.decodeFromString<InventoryButton>(it).also { if (it.icon?.startsWith("extra:") == true) { - MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed")) + MC.sendChat(Component.translatable("firmament.inventory-buttons.import-failed")) } } }.or { diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index d5b5417..eaa6138 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -1,88 +1,118 @@ - - package moe.nea.firmament.features.inventory.buttons import me.shedaniel.math.Rectangle import kotlinx.serialization.Serializable import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.network.chat.Component 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.impl.v1.FirmamentAPIImpl import moe.nea.firmament.util.MC import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.accessors.getProperRectangle +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder -import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.gold + +object InventoryButtons { + + @Config + object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) { + val _openEditor by button("open-editor") { + openEditor() + } + val hoverText by toggle("hover-text") { true } + val onlyInv by toggle("only-inv") { false } + } -object InventoryButtons : FirmamentFeature { - override val identifier: String - get() = "inventory-buttons" + @Config + object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data) - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val _openEditor by button("open-editor") { - openEditor() - } - } + @Serializable + data class Data( + var buttons: MutableList<InventoryButton> = mutableListOf() + ) - object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) + fun getValidButtons(screen: AbstractContainerScreen<*>): Sequence<InventoryButton> { + if (TConfig.onlyInv && screen !is InventoryScreen) return emptySequence() + if (FirmamentAPIImpl.extensions.any { it.shouldHideInventoryButtons(screen) }) { + return emptySequence() + } + return DConfig.data.buttons.asSequence().filter(InventoryButton::isValid) + } - @Serializable - data class Data( - var buttons: MutableList<InventoryButton> = mutableListOf() - ) + @Subscribe + fun onRectangles(it: HandledScreenPushREIEvent) { + val bounds = it.screen.getProperRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.block(buttonBounds) + } + } - override val config: ManagedConfig - get() = TConfig + @Subscribe + fun onClickScreen(it: HandledScreenClickEvent) { + val bounds = it.screen.getProperRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + if (buttonBounds.contains(it.mouseX, it.mouseY)) { + MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) + break + } + } + } - fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() } + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() - @Subscribe - fun onRectangles(it: HandledScreenPushREIEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - val buttonBounds = button.getBounds(bounds) - it.block(buttonBounds) - } - } + @Subscribe + fun onRenderForeground(it: HandledScreenForegroundEvent) { + val bounds = it.screen.getProperRectangle() - @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 - } - } - } + var hoveredComponent: InventoryButton? = null + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.context.pose().pushMatrix() + it.context.pose().translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat()) + button.render(it.context) + it.context.pose().popMatrix() - @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 - } + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.setComponentTooltipForNextFrame( + MC.font, + listOf(Component.literal(button.command).gold()), + buttonBounds.minX - 15, + buttonBounds.maxY + 20, + ) + } + } + } + if (hoveredComponent !== lastHoveredComponent) + lastMouseMove = TimeMark.now() + lastHoveredComponent = hoveredComponent + 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, - ) - ) - ) - } + var lastRectangle: Rectangle? = null + fun openEditor() { + ScreenUtil.setScreenLater( + InventoryButtonEditor( + lastRectangle ?: Rectangle( + MC.window.guiScaledWidth / 2 - 88, + MC.window.guiScaledHeight / 2 - 83, + 176, 166, + ) + ) + ) + } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt index 8fad4df..964f415 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -4,9 +4,9 @@ package moe.nea.firmament.features.inventory.storageoverlay import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.inventory.ChestMenu import moe.nea.firmament.util.ifMatches import moe.nea.firmament.util.unformattedString @@ -16,24 +16,24 @@ import moe.nea.firmament.util.unformattedString sealed interface StorageBackingHandle { sealed interface HasBackingScreen { - val handler: GenericContainerScreenHandler + val handler: ChestMenu } /** * 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 + data class Overview(override val handler: ChestMenu) : 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) : + data class Page(override val handler: ChestMenu, 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() + 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 @@ -46,13 +46,13 @@ sealed interface StorageBackingHandle { returnsNotNull() implies (screen != null) } if (screen == null) return null - if (screen !is GenericContainerScreen) return null + if (screen !is ContainerScreen) return null val title = screen.title.unformattedString - if (title == "Storage") return Overview(screen.screenHandler) + if (title == "Storage") return Overview(screen.menu) return title.ifMatches(enderChestName) { - Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) } ?: title.ifMatches(backPackName) { - Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) } } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt index 2e807de..7f96637 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -1,59 +1,106 @@ package moe.nea.firmament.features.inventory.storageoverlay +import io.github.notenoughupdates.moulconfig.ChromaColour 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 net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Items +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ChestInventoryUpdateEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.SlotRenderEvents 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.async.discard import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder -object StorageOverlay : FirmamentFeature { - +object StorageOverlay { + @Config object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData) - override val identifier: String + val identifier: String get() = "storage-overlay" + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val alwaysReplace by toggle("always-replace") { true } + val outlineActiveStoragePage by toggle("outline-active-page") { false } + val outlineActiveStoragePageColour by colour("outline-active-page-colour") { + ChromaColour.fromRGB( + 255, + 255, + 0, + 0, + 255 + ) + } + val showInactivePageTooltips by toggle("inactive-page-tooltips") { false } val columns by integer("rows", 1, 10) { 3 } val height by integer("height", 80, 3000) { 3 * 18 * 6 } + val retainScroll by toggle("retain-scroll") { true } 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 } + val itemsBlockScrolling by toggle("block-item-scrolling") { true } + val highlightSearchResults by toggle("highlight-search-results") { true } + val highlightSearchResultsColour by colour("highlight-search-results-colour") { + ChromaColour.fromRGB( + 0, + 176, + 0, + 0, + 255 + ) + } + } + + @Subscribe + fun highlightSlots(event: SlotRenderEvents.Before) { + if (!TConfig.highlightSearchResults) return + val storageOverlayScreen = + (MC.screen as? StorageOverlayScreen) + ?: (MC.handledScreen?.customGui as? StorageOverlayCustom)?.overview + ?: return + val stack = event.slot.item ?: return + val search = storageOverlayScreen.searchText.get().takeIf { it.isNotBlank() } ?: return + if (storageOverlayScreen.matchesSearch(stack, search)) { + event.context.fill( + event.slot.x, + event.slot.y, + event.slot.x + 16, + event.slot.y + 16, + TConfig.highlightSearchResultsColour.getEffectiveColourRGB() + ) + } } + 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) + fun onChestContentUpdate(event: ChestInventoryUpdateEvent) { + rememberContent(currentHandler) } @Subscribe fun onClick(event: SlotClickEvent) { - if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9 + if (lastStorageOverlay != null && event.slot.container !is Inventory && event.slot.containerSlot < 9 && event.stack.item != Items.BLACK_STAINED_GLASS_PANE ) { skipNextStorageOverlayBackflip = true @@ -64,18 +111,18 @@ object StorageOverlay : FirmamentFeature { 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 + ?: ((it.old as? AbstractContainerScreen<*>)?.customGui as? StorageOverlayCustom)?.overview var storageOverviewScreen = it.old as? StorageOverviewScreen - val screen = it.new as? GenericContainerScreen + val screen = it.new as? ContainerScreen + rememberContent(currentHandler) 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 + player?.connection?.send(ServerboundContainerClosePacket(oldHandler.handler.containerId)) + if (player?.containerMenu === oldHandler.handler) { + player.containerMenu = player.inventoryMenu } } storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay @@ -100,25 +147,24 @@ object StorageOverlay : FirmamentFeature { screen.customGui = StorageOverlayCustom( currentHandler ?: return, screen, - storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)) + 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 + val data = Data.data.storageInventories 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()) { + for ((index, stack) in handler.handler.items.withIndex()) { // TODO: replace with slot iteration // Ignore unloaded item stacks if (stack.isEmpty) continue val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue @@ -132,15 +178,15 @@ object StorageOverlay : FirmamentFeature { data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null) } } + Data.markDirty() } 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() }) + VirtualInventory(handler.handler.items.take(handler.handler.rowCount * 9).drop(9).map { it.copy() }) data.compute(handler.storagePageSlot) { slot, existingInventory -> (existingInventory ?: StorageData.StorageInventory( slot.defaultName(), @@ -150,5 +196,6 @@ object StorageOverlay : FirmamentFeature { it.inventory = newStacks } } + Data.markDirty(newStacks.serializationCache.discard()) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 6092e26..98e8085 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -2,21 +2,27 @@ 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 net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.inventory.Slot import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.accessors.castAccessor import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack class StorageOverlayCustom( - val handler: StorageBackingHandle, - val screen: GenericContainerScreen, - val overview: StorageOverlayScreen, + val handler: StorageBackingHandle, + val screen: ContainerScreen, + val overview: StorageOverlayScreen, ) : CustomGui() { override fun onVoluntaryExit(): Boolean { overview.isExiting = true + StorageOverlayScreen.resetScroll() return super.onVoluntaryExit() } @@ -24,20 +30,20 @@ class StorageOverlayCustom( return overview.getBounds() } - override fun afterSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun afterSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) context.disableScissor() } - override fun beforeSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun beforeSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) overview.createScissors(context) } override fun onInit() { - overview.init(MinecraftClient.getInstance(), screen.width, screen.height) + overview.init(Minecraft.getInstance(), screen.width, screen.height) overview.init() - screen as AccessorHandledScreen + screen.castAccessor() screen.x_Firmament = overview.measurements.x screen.y_Firmament = overview.measurements.y screen.backgroundWidth_Firmament = overview.measurements.totalWidth @@ -47,7 +53,7 @@ class StorageOverlayCustom( 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 (slot.container !is Inventory) { if (!overview.getScrollPanelInner().contains(pointX, pointY)) return false } @@ -58,48 +64,50 @@ class StorageOverlayCustom( return false } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseReleased(mouseX, mouseY, button) + override fun mouseReleased(click: MouseButtonEvent): Boolean { + return overview.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { - return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + return overview.mouseDragged(click, offsetX, offsetY) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyReleased(keyCode, scanCode, modifiers) + override fun keyReleased(input: KeyEvent): Boolean { + return overview.keyReleased(input) } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyPressed(keyCode, scanCode, modifiers) + override fun keyPressed(input: KeyEvent): Boolean { + return overview.keyPressed(input) } - override fun charTyped(chr: Char, modifiers: Int): Boolean { - return overview.charTyped(chr, modifiers) + override fun charTyped(input: CharacterEvent): Boolean { + return overview.charTyped(input) } - override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) + override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { + return overview.mouseClicked(click, doubled, (handler as? StorageBackingHandle.Page)?.storagePageSlot) } - override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + override fun render(drawContext: GuiGraphics, 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.drawPages( + drawContext, + mouseX, + mouseY, + delta, + (handler as? StorageBackingHandle.Page)?.storagePageSlot, + screen.menu.slots.take(screen.menu.rowCount * 9).drop(9), + Point((screen.castAccessor()).x_Firmament, screen.y_Firmament) + ) overview.drawScrollBar(drawContext) overview.drawControls(drawContext, mouseX, mouseY) } override fun moveSlot(slot: Slot) { - val index = slot.index + val index = slot.containerSlot if (index in 0..<36) { val (x, y) = overview.getPlayerInventorySlotPosition(index) - slot.x = x - (screen as AccessorHandledScreen).x_Firmament + slot.x = x - (screen.castAccessor()).x_Firmament slot.y = y - screen.y_Firmament } else { slot.x = -100000 @@ -113,6 +121,8 @@ class StorageOverlayCustom( horizontalAmount: Double, verticalAmount: Double ): Boolean { + if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling) + return false 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 index cf1cf1d..3e0bb4b 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -13,13 +13,17 @@ import io.github.notenoughupdates.moulconfig.observer.Property import java.util.TreeSet 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.client.gui.screen.ingame.HandledScreen -import net.minecraft.item.ItemStack -import net.minecraft.screen.slot.Slot -import net.minecraft.text.Text -import net.minecraft.util.Identifier +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.gui.EmptyComponent import moe.nea.firmament.gui.FirmButtonComponent @@ -35,10 +39,11 @@ import moe.nea.firmament.util.mc.FakeSlot import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.render.enableScissorWithoutTranslation import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString -class StorageOverlayScreen : Screen(Text.literal("")) { +class StorageOverlayScreen : Screen(Component.literal("")) { companion object { val PLAYER_WIDTH = 184 @@ -46,19 +51,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val PLAYER_Y_INSET = 3 val SLOT_SIZE = 18 val PADDING = 10 - val PAGE_WIDTH = SLOT_SIZE * 9 + val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9 + val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4 val HOTBAR_X = 12 val HOTBAR_Y = 67 val MAIN_INVENTORY_Y = 9 val SCROLL_BAR_WIDTH = 8 val SCROLL_BAR_HEIGHT = 16 + val CONTROL_X_INSET = 3 + val CONTROL_Y_INSET = 5 val CONTROL_WIDTH = 70 - val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + PLAYER_Y_INSET - val CONTROL_HEIGHT = 100 + val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1 + val CONTROL_HEIGHT = 50 + + var scroll: Float = 0F + var lastRenderedInnerHeight = 0 + + fun resetScroll() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0F + } } var isExiting: Boolean = false - var scroll: Float = 0F var pageWidthCount = StorageOverlay.TConfig.columns inner class Measurements { @@ -67,20 +81,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val x = width / 2 - overviewWidth / 2 val overviewHeight = minOf( height - PLAYER_HEIGHT - minOf(80, height / 10), - StorageOverlay.TConfig.height) + StorageOverlay.TConfig.height + ) 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 controlX = x - CONTROL_WIDTH - val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2 + val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET + val controlY = playerY - CONTROL_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 @@ -99,6 +113,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat()) return true } + fun coerceScroll(offset: Float) { scroll = (scroll + offset) .coerceAtMost(getMaxScroll()) @@ -107,19 +122,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { 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") - val controllerBackground = Identifier.of("firmament:storageoverlay/storage_controls") + val playerInventorySprite = ResourceLocation.parse("firmament:storageoverlay/player_inventory") + val upperBackgroundSprite = ResourceLocation.parse("firmament:storageoverlay/upper_background") + val slotRowSprite = ResourceLocation.parse("firmament:storageoverlay/storage_row") + val scrollbarBackground = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_background") + val scrollbarKnob = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_knob") + val controllerBackground = ResourceLocation.parse("firmament:storageoverlay/storage_controls") - override fun close() { + override fun onClose() { isExiting = true - super.close() + resetScroll() + super.onClose() } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) drawBackgrounds(context) drawPages(context, mouseX, mouseY, delta, null, null, Point()) @@ -132,7 +148,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return scroll / getMaxScroll() } - fun drawScrollBar(context: DrawContext) { + fun drawScrollBar(context: GuiGraphics) { val sbRect = getScrollBarRect() context.drawGuiTexture( scrollbarBackground, @@ -148,21 +164,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun editPages() { isExiting = true - val hs = MC.screen as? HandledScreen<*> - if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { - hs.customGui = null - } else { - MC.sendCommand("storage") + MC.instance.schedule { + val hs = MC.screen as? AbstractContainerScreen<*> + if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { + hs.customGui = null + hs.init(MC.instance, width, height) + } else { + MC.sendCommand("storage") + } } } val guiContext = GuiContext(EmptyComponent()) private val knobStub = EmptyComponent() - val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages) + val editButton = FirmButtonComponent( + TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), + action = ::editPages + ) val searchText = Property.of("") // TODO: sync with REI - val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true), - tr("firmament.storage-overlay.search.suggestion", "Search...").string, - IMinecraft.instance.defaultFontRenderer) + val searchField = TextFieldComponent( + searchText, 100, GetSetter.constant(true), + tr("firmament.storage-overlay.search.suggestion", "Search...").string, + IMinecraft.INSTANCE.defaultFontRenderer + ) val controlComponent = PanelComponent( ColumnComponent( searchField, @@ -180,30 +204,36 @@ class StorageOverlayScreen : Screen(Text.literal("")) { guiContext.adopt(controlComponent) } - fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) { + fun drawControls(context: GuiGraphics, mouseX: Int, mouseY: Int) { context.drawGuiTexture( controllerBackground, measurements.controlX, measurements.controlY, - CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT) + CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT + ) context.drawMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX, mouseY) + mouseX, mouseY + ) } - fun drawBackgrounds(context: DrawContext) { - context.drawGuiTexture(upperBackgroundSprite, - measurements.x, - measurements.y, - measurements.overviewWidth, - measurements.overviewHeight) - context.drawGuiTexture(playerInventorySprite, - measurements.playerX, - measurements.playerY, - PLAYER_WIDTH, - PLAYER_HEIGHT) + fun drawBackgrounds(context: GuiGraphics) { + context.drawGuiTexture( + upperBackgroundSprite, + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ) + context.drawGuiTexture( + playerInventorySprite, + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ) } fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> { @@ -216,55 +246,63 @@ class StorageOverlayScreen : Screen(Text.literal("")) { ) } - fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - val items = MC.player?.inventory?.main ?: return + fun drawPlayerInventory(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + val items = MC.player?.inventory?.nonEquipmentItems ?: return items.withIndex().forEach { (index, item) -> val (x, y) = getPlayerInventorySlotPosition(index) - context.drawItem(item, x, y, 0) - context.drawStackOverlay(textRenderer, item, x, y) + context.renderItem(item, x, y, 0) + context.renderItemDecorations(font, item, x, y) } } fun getScrollBarRect(): Rectangle { - return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, - measurements.y + PADDING, - SCROLL_BAR_WIDTH, - measurements.innerScrollPanelHeight) + 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) + return Rectangle( + measurements.x + PADDING, + measurements.y + PADDING, + measurements.innerScrollPanelWidth, + measurements.innerScrollPanelHeight + ) } - fun createScissors(context: DrawContext) { + fun createScissors(context: GuiGraphics) { val rect = getScrollPanelInner() - context.enableScissor( - rect.minX, rect.minY, - rect.maxX, rect.maxY + context.enableScissorWithoutTranslation( + rect.minX.toFloat(), rect.minY.toFloat(), + rect.maxX.toFloat(), rect.maxY.toFloat(), ) } fun drawPages( - context: DrawContext, mouseX: Int, mouseY: Int, delta: Float, - excluding: StoragePageSlot?, - slots: List<Slot>?, - slotOffset: Point + context: GuiGraphics, 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 + drawPage( + context, + rect.x, + rect.y, + page, inventory, + if (excluding == page) slots else null, + slotOffset, + mouseX, + mouseY ) } context.disableScissor() + } @@ -272,40 +310,45 @@ class StorageOverlayScreen : Screen(Text.literal("")) { get() = guiContext.focusedElement == knobStub set(value) = knobStub.setFocus(value) - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - return mouseClicked(mouseX, mouseY, button, null) + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + return mouseClicked(click, doubled, null) } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseReleased(click: MouseButtonEvent): Boolean { if (knobGrabbed) { knobGrabbed = false return true } - if (clickMCComponentInPlace(controlComponent, - measurements.controlX, measurements.controlY, - CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX.toInt(), mouseY.toInt(), - MouseEvent.Click(button, false)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + click.x.toInt(), click.y.toInt(), + MouseEvent.Click(click.button(), false) + ) ) return true - return super.mouseReleased(mouseX, mouseY, button) + return super.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { if (knobGrabbed) { val sbRect = getScrollBarRect() - val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + val percentage = (click.x - sbRect.getY()) / sbRect.getHeight() scroll = (getMaxScroll() * percentage).toFloat() mouseScrolled(0.0, 0.0, 0.0, 0.0) return true } - return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + return super.mouseDragged(click, offsetX, offsetY) } - fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + fun mouseClicked(click: MouseButtonEvent, doubled: Boolean, activePage: StoragePageSlot?): Boolean { + guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components. + val mouseX = click.x + val mouseY = click.y if (getScrollPanelInner().contains(mouseX, mouseY)) { - val data = StorageOverlay.Data.data ?: StorageData() + val data = StorageOverlay.Data.data layoutedForEach(data) { rect, page, _ -> - if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) { + if (rect.contains(mouseX, mouseY) && activePage != page && click.button() == 0) { page.navigateTo() return true } @@ -320,52 +363,58 @@ class StorageOverlayScreen : Screen(Text.literal("")) { knobGrabbed = true return true } - if (clickMCComponentInPlace(controlComponent, - measurements.controlX, measurements.controlY, - CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX.toInt(), mouseY.toInt(), - MouseEvent.Click(button, true)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX.toInt(), mouseY.toInt(), + MouseEvent.Click(click.button(), true) + ) ) return true return false } - override fun charTyped(chr: Char, modifiers: Int): Boolean { + override fun charTyped(input: CharacterEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.CharTyped(chr) + KeyboardEvent.CharTyped(input.codepointAsString().first()) // TODO: i dont like this .first() ) ) { return true } - return super.charTyped(chr, modifiers) + return super.charTyped(input) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyReleased(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, false) + KeyboardEvent.KeyPressed(input.input(), input.scancode, false) ) ) { return true } - return super.keyReleased(keyCode, scanCode, modifiers) + return super.keyReleased(input) + } + + override fun shouldCloseOnEsc(): Boolean { + return this === MC.screen // Fixes this UI closing the handled screen on Escape press. } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyPressed(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, true) + KeyboardEvent.KeyPressed(input.input(), input.scancode, true) ) ) { return true } - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } @@ -414,7 +463,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val filter = getFilteredPages() for ((page, inventory) in data.storageInventories.entries) { if (page !in filter) continue - val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight } + val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + font.lineHeight } ?: 18 maxHeight = maxOf(maxHeight, currentHeight) val rect = Rectangle( @@ -435,61 +484,102 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun drawPage( - context: DrawContext, - x: Int, - y: Int, - page: StoragePageSlot, - inventory: StorageData.StorageInventory, - slots: List<Slot>?, - slotOffset: Point, + context: GuiGraphics, + x: Int, + y: Int, + page: StoragePageSlot, + inventory: StorageData.StorageInventory, + slots: List<Slot>?, + slotOffset: Point, + mouseX: Int, + mouseY: Int, ): 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) + context.drawString( + font, + Component.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) + val name = inventory.title + val pageHeight = inv.rows * SLOT_SIZE + 8 + font.lineHeight + if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage) + context.submitOutline( + x, + y + 3 + font.lineHeight, + PAGE_WIDTH, + inv.rows * SLOT_SIZE + 4, + StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB() + ) + context.drawString( + font, Component.literal(name), x + 6, y + 3, + if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true + ) + context.drawGuiTexture( + slotRowSprite, + x + 2, + y + 5 + font.lineHeight, + PAGE_SLOTS_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 - val fakeSlot = FakeSlot(stack, slotX, slotY) + val slotX = (index % 9) * SLOT_SIZE + x + 3 + val slotY = (index / 9) * SLOT_SIZE + y + 5 + font.lineHeight + 1 if (slots == null) { + val fakeSlot = FakeSlot(stack, slotX, slotY) SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot)) - context.drawItem(stack, slotX, slotY) - context.drawStackOverlay(textRenderer, stack, slotX, slotY) + context.renderItem(stack, slotX, slotY) + context.renderItemDecorations(font, stack, slotX, slotY) SlotRenderEvents.After.publish(SlotRenderEvents.After(context, fakeSlot)) + val rect = getScrollPanelInner() + if (StorageOverlay.TConfig.showInactivePageTooltips && !stack.isEmpty && + mouseX >= slotX && mouseY >= slotY && + mouseX <= slotX + 16 && mouseY <= slotY + 16 && + mouseY >= rect.minY && mouseY <= rect.maxY) { + try { + context.setTooltipForNextFrame(font, stack, mouseX, mouseY) + } catch (e: IllegalStateException) { + context.setComponentTooltipForNextFrame(font, listOf(Component.nullToEmpty(ChatFormatting.RED.toString() + + "Error Getting Tooltip!"), Component.nullToEmpty(ChatFormatting.YELLOW.toString() + + "Open page to fix" + ChatFormatting.RESET)), mouseX, mouseY) + } + } } else { val slot = slots[index] slot.x = slotX - slotOffset.x slot.y = slotY - slotOffset.y } } - return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight + return pageHeight + 6 } fun getBounds(): List<Rectangle> { return listOf( - Rectangle(measurements.x, - measurements.y, - measurements.overviewWidth, - measurements.overviewHeight), - Rectangle(measurements.playerX, - measurements.playerY, - PLAYER_WIDTH, - PLAYER_HEIGHT), - Rectangle(measurements.controlX, - measurements.controlY, - CONTROL_WIDTH, - CONTROL_HEIGHT)) + Rectangle( + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ), + Rectangle( + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ), + Rectangle( + measurements.controlX, + measurements.controlY, + CONTROL_WIDTH, + CONTROL_HEIGHT + ) + ) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt index 9112fab..3c40fc6 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt @@ -4,17 +4,19 @@ 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 net.minecraft.world.level.block.Blocks +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.Item +import net.minecraft.world.item.Items +import net.minecraft.network.chat.Component +import net.minecraft.world.item.DyeColor import moe.nea.firmament.util.MC import moe.nea.firmament.util.toShedaniel -class StorageOverviewScreen() : Screen(Text.empty()) { +class StorageOverviewScreen() : Screen(Component.empty()) { companion object { val emptyStorageSlotItems = listOf<Item>( Blocks.RED_STAINED_GLASS_PANE.asItem(), @@ -22,39 +24,49 @@ class StorageOverviewScreen() : Screen(Text.empty()) { Items.GRAY_DYE ) val pageWidth get() = 19 * 9 + + var scroll = 0 + var lastRenderedHeight = 0 } val content = StorageOverlay.Data.data ?: StorageData() var isClosing = false - var scroll = 0 - var lastRenderedHeight = 0 + override fun init() { + super.init() + scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0) + } + + override fun onClose() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0 + super.onClose() + } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, 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) + context.pose().pushMatrix() + context.pose().translate(offsetX.toFloat(), offsetY.toFloat()) renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY) - context.matrices.pop() + context.pose().popMatrix() } } 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 currentMaxHeight = StorageOverlay.TConfig.margin - StorageOverlay.TConfig.padding - scroll var totalHeight = -currentMaxHeight content.storageInventories.onEachIndexed { index, (key, value) -> - val pageX = (index % StorageOverlay.config.columns) + val pageX = (index % StorageOverlay.TConfig.columns) if (pageX == 0) { - currentMaxHeight += StorageOverlay.config.padding + currentMaxHeight += StorageOverlay.TConfig.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) + width / 2 - (StorageOverlay.TConfig.columns * (pageWidth + StorageOverlay.TConfig.padding) - StorageOverlay.TConfig.padding) / 2 + pageX * (pageWidth + StorageOverlay.TConfig.padding) onEach(Pair(key, value), xPosition, offsetY) val height = getStorePageHeight(value) currentMaxHeight = max(currentMaxHeight, height) @@ -62,22 +74,22 @@ class StorageOverviewScreen() : Screen(Text.empty()) { lastRenderedHeight = totalHeight + currentMaxHeight } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { layoutedForEach { (k, p), x, y -> - val rx = mouseX - x - val ry = mouseY - y + val rx = click.x - x + val ry = click.y - y if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) { - close() + onClose() StorageOverlay.lastStorageOverlay = this k.navigateTo() return true } } - return super.mouseClicked(mouseX, mouseY, button) + return super.mouseClicked(click, doubled) } fun getStorePageHeight(page: StorageData.StorageInventory): Int { - return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60 + return page.inventory?.rows?.let { it * 19 + MC.font.lineHeight + 2 } ?: 60 } override fun mouseScrolled( @@ -88,36 +100,38 @@ class StorageOverviewScreen() : Screen(Text.empty()) { ): Boolean { scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt() - .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0) + .coerceAtMost(getMaxScroll()).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) + private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.TConfig.margin + + private fun renderStoragePage(context: GuiGraphics, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { + context.drawString(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) + context.drawCenteredString(MC.font, Component.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 + val y = (index / 9) * 19 + MC.font.lineHeight + 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.drawStackOverlay(MC.font, stack, x + 1, y + 1) + context.renderItem(stack, x + 1, y + 1) + context.renderItemDecorations(MC.font, stack, x + 1, y + 1) } } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) + override fun keyPressed(input: KeyEvent): Boolean { + if (input.input() == GLFW.GLFW_KEY_ESCAPE) isClosing = true - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index 3b86184..69d686f 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -1,9 +1,10 @@ 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 java.util.Base64 +import java.util.concurrent.CompletableFuture +import kotlinx.coroutines.async import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -11,13 +12,16 @@ 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 kotlin.jvm.optionals.getOrNull +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtIo -import net.minecraft.nbt.NbtList +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtSizeTracker -import net.minecraft.registry.RegistryOps +import net.minecraft.nbt.NbtAccounter +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.inventory.storageoverlay.VirtualInventory.Serializer.writeToByteArray +import moe.nea.firmament.util.Base64Util import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.mc.TolerantRegistriesOps @@ -28,6 +32,10 @@ data class VirtualInventory( ) { val rows = stacks.size / 9 + val serializationCache = CompletableFuture.supplyAsync { + writeToByteArray(this) + } + init { assert(stacks.size % 9 == 0) assert(stacks.size / 9 in 1..5) @@ -35,41 +43,47 @@ data class VirtualInventory( object Serializer : KSerializer<VirtualInventory> { + fun writeToByteArray(value: VirtualInventory): ByteArray { + val list = ListTag() + val ops = getOps() + value.stacks.forEach { + if (it.isEmpty) list.add(CompoundTag()) + else list.add(ErrorUtil.catch("Could not serialize item") { + ItemStack.CODEC.encode( + it, + ops, + CompoundTag() + ).orThrow + } + .or { CompoundTag() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(CompoundTag().also { it.put(INVENTORY, list) }, baos) + return baos.toByteArray() + } + 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()) + val n = NbtIo.readCompressed(ByteArrayInputStream(Base64Util.decodeBytes(s)), NbtAccounter.create(100_000_000)) + val items = n.getList(INVENTORY).getOrNull() val ops = getOps() - return VirtualInventory(items.map { - it as NbtCompound + return VirtualInventory(items?.map { + it as CompoundTag if (it.isEmpty) ItemStack.EMPTY else ErrorUtil.catch("Could not deserialize item") { ItemStack.CODEC.parse(ops, it).orThrow }.or { ItemStack.EMPTY } - }) + } ?: listOf()) } - fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) + fun getOps() = MC.currentOrDefaultRegistryNbtOps override fun serialize(encoder: Encoder, value: VirtualInventory) { - val list = NbtList() - val ops = getOps() - value.stacks.forEach { - if (it.isEmpty) list.add(NbtCompound()) - else list.add(ErrorUtil.catch("Could not serialize item") { - ItemStack.CODEC.encode(it, - ops, - NbtCompound()).orThrow - } - .or { NbtCompound() }) - } - val baos = ByteArrayOutputStream() - NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos) - encoder.encodeString(baos.toByteArray().encodeBase64()) + encoder.encodeString(Base64Util.encodeToString(value.serializationCache.get())) } } } |
