@file:UseSerializers(DashlessUUIDSerializer::class) package moe.nea.firmament.features.inventory import com.mojang.blaze3d.systems.RenderSystem import java.util.UUID import org.lwjgl.glfw.GLFW import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlinx.serialization.serializer import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.entity.player.PlayerInventory import net.minecraft.screen.GenericContainerScreenHandler import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.util.Identifier import moe.nea.firmament.annotations.Subscribe 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.SavedKeyBinding import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.CommonSoundEffects import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SkyBlockIsland import moe.nea.firmament.util.data.ProfileSpecificDataHolder import moe.nea.firmament.util.json.DashlessUUIDSerializer 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.drawLine import moe.nea.firmament.util.skyblockUUID import moe.nea.firmament.util.unformattedString object SlotLocking : FirmamentFeature { override val identifier: String get() = "slot-locking" @Serializable data class Data( val lockedSlots: MutableSet = mutableSetOf(), val lockedSlotsRift: MutableSet = mutableSetOf(), val lockedUUIDs: MutableSet = mutableSetOf(), val boundSlots: MutableMap = mutableMapOf() ) 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) } val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L } val slotBindRequireShift by toggle("require-quick-move") { true } } override val config: TConfig get() = TConfig object DConfig : ProfileSpecificDataHolder(serializer(), "locked-slots", ::Data) val lockedUUIDs get() = DConfig.data?.lockedUUIDs val lockedSlots get() = when (SBData.skyblockLocation) { SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift null -> null else -> DConfig.data?.lockedSlots } fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { if (screen == null) return false return screen.title.unformattedString.contains("Salvage Item") } fun isTradeScreen(screen: HandledScreen<*>?): Boolean { if (screen == null) return false val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false if (handler.inventory.size() < 9) return false val middlePane = handler.inventory.getStack(handler.inventory.size() - 5) if (middlePane == null) return false return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" } fun isNpcShop(screen: HandledScreen<*>?): Boolean { if (screen == null) return false val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false if (handler.inventory.size() < 9) return false val sellItem = handler.inventory.getStack(handler.inventory.size() - 5) if (sellItem == null) return false if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true val lore = sellItem.loreAccordingToNbt return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!" } @Subscribe fun onSalvageProtect(event: IsSlotProtectedEvent) { if (event.slot == null) return if (!event.slot.hasStack()) return if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return val inv = event.slot.inventory var anyBlocked = false for (i in 0 until event.slot.index) { val stack = inv.getStack(i) if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) anyBlocked = true } if (anyBlocked) { event.protectSilent() } } @Subscribe fun onProtectUuidItems(event: IsSlotProtectedEvent) { val doesNotDeleteItem = event.actionType == SlotActionType.SWAP || event.actionType == SlotActionType.PICKUP || event.actionType == SlotActionType.QUICK_MOVE || event.actionType == SlotActionType.QUICK_CRAFT || event.actionType == SlotActionType.CLONE || event.actionType == SlotActionType.PICKUP_ALL val isSellOrTradeScreen = isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) && doesNotDeleteItem ) return val stack = event.itemStack ?: return val uuid = stack.skyblockUUID ?: return if (uuid in (lockedUUIDs ?: return)) { event.protect() } } @Subscribe fun onProtectSlot(it: IsSlotProtectedEvent) { if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) { it.protect() } } @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { val boundSlots = DConfig.data?.boundSlots ?: mapOf() val isValidAction = it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return val handler = MC.handledScreen?.screenHandler ?: 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 it.protectSilent() val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) inventorySlot?.swapWithHotBar(handler, boundSlot.key) } } @Subscribe fun onLockUUID(it: HandledScreenKeyPressedEvent) { if (!it.matches(TConfig.lockUUID)) return val inventory = MC.handledScreen ?: return inventory as AccessorHandledScreen val slot = inventory.focusedSlot_Firmament ?: return val stack = slot.stack ?: return val uuid = stack.skyblockUUID ?: return val lockedUUIDs = lockedUUIDs ?: return if (uuid in lockedUUIDs) { lockedUUIDs.remove(uuid) } else { lockedUUIDs.add(uuid) } DConfig.markDirty() CommonSoundEffects.playSuccess() it.cancel() } @Subscribe fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) { val inventory = MC.handledScreen ?: return inventory as AccessorHandledScreen val slot = inventory.focusedSlot_Firmament val storedSlot = storedLockingSlot ?: return if (it.matches(TConfig.slotBind) && slot != storedSlot && slot != null && slot.isHotbar() != storedSlot.isHotbar()) { 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 DConfig.markDirty() CommonSoundEffects.playSuccess() return } if (it.matches(TConfig.lockSlot) && slot == storedSlot) { storedLockingSlot = null toggleSlotLock(slot) return } if (it.matches(TConfig.slotBind)) { storedLockingSlot = null } } @Subscribe fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) { val boundSlots = DConfig.data?.boundSlots ?: return fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true) val accScreen = event.screen as AccessorHandledScreen val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament boundSlots.entries.forEach { val hotbarSlot = findByIndex(it.key) ?: return@forEach val inventorySlot = findByIndex(it.value) ?: return@forEach val (hotX, hotY) = hotbarSlot.lineCenter() val (invX, invY) = inventorySlot.lineCenter() event.context.drawLine( invX + sx, invY + sy, hotX + sx, hotY + sy, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) event.context.drawBorder(inventorySlot.x + sx, inventorySlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) } } @Subscribe fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) { val draggingSlot = storedLockingSlot ?: return val accScreen = event.screen as AccessorHandledScreen val hoveredSlot = accScreen.focusedSlot_Firmament ?.takeIf { it.inventory is PlayerInventory } ?.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()) if (hoveredSlot == null) { event.context.drawLine( borderX + sx, borderY + sy, event.mouseX, event.mouseY, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) } else if (hoveredSlot != draggingSlot) { val (hovX, hovY) = hoveredSlot.lineCenter() event.context.drawLine( borderX + sx, borderY + sy, hovX + sx, hovY + sy, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) event.context.drawBorder(hoveredSlot.x + sx, hoveredSlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) } } fun Slot.lineCenter(): Pair { return if (isHotbar()) { x + 9 to y } else { x + 9 to y + 17 } } fun Slot.isHotbar(): Boolean { return index < 9 } @Subscribe fun onScreenChange(event: ScreenChangeEvent) { storedLockingSlot = null } var storedLockingSlot: Slot? = null 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 }) { // intentionally do nothing } else if (slot.index in lockedSlots) { lockedSlots.remove(slot.index) } else { lockedSlots.add(slot.index) } DConfig.markDirty() CommonSoundEffects.playSuccess() } } @Subscribe fun onLockSlot(it: HandledScreenKeyPressedEvent) { val inventory = MC.handledScreen ?: return inventory as AccessorHandledScreen val slot = inventory.focusedSlot_Firmament ?: return if (slot.inventory !is PlayerInventory) return if (it.matches(TConfig.slotBind)) { storedLockingSlot = storedLockingSlot ?: slot return } if (!it.matches(TConfig.lockSlot)) { return } toggleSlotLock(slot) } @Subscribe fun onRenderSlotOverlay(it: SlotRenderEvents.After) { val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf()) val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { RenderSystem.disableDepthTest() it.context.drawSprite( it.slot.x, it.slot.y, 0, 16, 16, MC.guiAtlasManager.getSprite( when { isSlotLocked -> (Identifier.of("firmament:slot_locked")) isUUIDLocked -> (Identifier.of("firmament:uuid_locked")) else -> error("unreachable") } ) ) RenderSystem.enableDepthTest() } } }