@file:UseSerializers(DashlessUUIDSerializer::class) 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 moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FeaturesInitializedEvent 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.GuiRenderLayers import moe.nea.firmament.util.render.drawLine import moe.nea.firmament.util.skyblock.DungeonUtil 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: BoundSlots = BoundSlots() ) @Serializable data class BoundSlot( val hotbar: Int, val inventory: Int, ) @Serializable(with = BoundSlots.Serializer::class) data class BoundSlots( val pairs: MutableSet = mutableSetOf() ) { fun findMatchingSlots(index: Int): List { 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 { override val descriptor: SerialDescriptor get() = serializer().descriptor override fun serialize( encoder: Encoder, value: BoundSlots ) { serializer>() .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>(), json)) } } } 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 } val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES } val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } enum class SlotRenderLinesMode : StringIdentifiable { EVERYTHING, ONLY_BOXES, NOTHING; override fun asString(): String { return name } } 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, IsSlotProtectedEvent.MoveOrigin.SALVAGE, 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 onEvent(event: FeaturesInitializedEvent) { 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 ?: BoundSlots() 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 matchingSlots = boundSlots.findMatchingSlots(slot.index) if (matchingSlots.isEmpty()) return it.protectSilent() val boundSlot = matchingSlots.singleOrNull() ?: return val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @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.removeDuplicateForInventory(invSlot.index) boundSlots.insert(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 val boundSlots = DConfig.data?.boundSlots ?: return if (slot != null) boundSlots.removeAllInvolving(slot.index) } } @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 val highlitSlots = mutableSetOf() 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 if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) continue 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(anyHovered) ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, 16, 16, color(hotbarSlot in highlitSlots).color) event.context.drawBorder(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 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 ?: BoundSlots() if (slot.inventory is PlayerInventory) { if (boundSlots.removeAllInvolving(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) { it.context.drawGuiTexture( GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, when { isSlotLocked -> (Identifier.of("firmament:slot_locked")) isUUIDLocked -> (Identifier.of("firmament:uuid_locked")) else -> error("unreachable") }, it.slot.x, it.slot.y, 16, 16, -1 ) } } }