Bulk commit

This commit is contained in:
nea
2023-07-11 21:01:58 +02:00
parent 4444fcca44
commit 4d93f475aa
39 changed files with 951 additions and 40 deletions

View File

@@ -26,8 +26,6 @@ object CraftingOverlay : FirmamentFeature {
this.recipe = recipe
}
override val name: String
get() = "Crafting Overlay"
override val identifier: String
get() = "crafting-overlay"

View File

@@ -0,0 +1,67 @@
package moe.nea.firmament.features.inventory
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import net.minecraft.client.util.InputUtil
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.assertNotNullOr
object SaveCursorPosition : FirmamentFeature {
override val identifier: String
get() = "save-cursor-position"
object TConfig : ManagedConfig(identifier) {
val enable by toggle("enable") { true }
val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
}
override val config: TConfig
get() = TConfig
override fun onLoad() {
}
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>? {
val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < 1.seconds }
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) {
val cursorPos = assertNotNullOr(savedPositionedP1) { return }
savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
}
}

View File

@@ -33,8 +33,6 @@ import moe.nea.firmament.util.MC
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object SlotLocking : FirmamentFeature {
override val name: String
get() = "Slot Locking"
override val identifier: String
get() = "slot-locking"

View File

@@ -0,0 +1,56 @@
package moe.nea.firmament.features.inventory.storageoverlay
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.screen.GenericContainerScreenHandler
import moe.nea.firmament.util.ifMatches
import moe.nea.firmament.util.unformattedString
/**
* A handle representing the state of the "server side" screens.
*/
sealed interface StorageBackingHandle {
sealed interface HasBackingScreen {
val handler: GenericContainerScreenHandler
}
/**
* No open "server side" screen.
*/
object None : StorageBackingHandle
/**
* The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage`
*/
data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen
/**
* An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via
* the [Overview] or via `/ec <index + 1>` for enderchest pages.
*/
data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) :
StorageBackingHandle, HasBackingScreen
companion object {
private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex()
private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex()
/**
* Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not
* representable as a [StorageBackingHandle], meaning another screen is open, for example the enderchest icon
* selection screen.
*/
fun fromScreen(screen: Screen?): StorageBackingHandle? {
if (screen == null) return None
if (screen !is GenericContainerScreen) return null
val title = screen.title.unformattedString
if (title == "Storage") return Overview(screen.screenHandler)
return title.ifMatches(enderChestName) {
Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt()))
} ?: title.ifMatches(backPackName) {
Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt()))
}
}
}
}

View File

@@ -0,0 +1,16 @@
package moe.nea.firmament.features.inventory.storageoverlay
import java.util.SortedMap
import kotlinx.serialization.Serializable
@Serializable
data class StorageData(
val storageInventories: SortedMap<StoragePageSlot, StorageInventory> = sortedMapOf()
) {
@Serializable
data class StorageInventory(
var title: String,
val slot: StoragePageSlot,
var inventory: VirtualInventory?,
)
}

View File

@@ -0,0 +1,94 @@
package moe.nea.firmament.features.inventory.storageoverlay
import java.util.*
import kotlinx.serialization.serializer
import moe.nea.firmament.events.ScreenOpenEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object StorageOverlay : FirmamentFeature {
object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData)
override val identifier: String
get() = "storage-overlay"
object TConfig : ManagedConfig(identifier) {
val rows by integer("rows", 1, 5) { 3 }
val scrollSpeed by integer("scroll-speed", 1, 50) { 10 }
val inverseScroll by toggle("inverse-scroll") { false }
val padding by integer("padding", 1, 20) { 5 }
val margin by integer("margin", 1, 60) { 20 }
}
override val config: TConfig
get() = TConfig
var currentHandler: StorageBackingHandle? = StorageBackingHandle.None
override fun onLoad() {
ScreenOpenEvent.subscribe { event ->
currentHandler = StorageBackingHandle.fromScreen(event.new)
if (event.old is StorageOverlayScreen && !event.old.isClosing) {
event.old.setHandler(currentHandler)
if (currentHandler != null)
// TODO: Consider instead only replacing rendering? might make a lot of stack handling easier
event.cancel()
}
}
TickEvent.subscribe {
rememberContent(currentHandler ?: return@subscribe)
}
}
private fun rememberContent(handler: StorageBackingHandle) {
// TODO: Make all of these functions work on deltas / updates instead of the entire contents
val data = Data.data?.storageInventories ?: return
when (handler) {
StorageBackingHandle.None -> {}
is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data)
is StorageBackingHandle.Page -> rememberPage(handler, data)
}
}
private fun rememberStorageOverview(
handler: StorageBackingHandle.Overview,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
for ((index, stack) in handler.handler.stacks.withIndex()) {
// Ignore unloaded item stacks
if (stack.isEmpty) continue
val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue
val isEmpty = stack.item in StorageOverlayScreen.emptyStorageSlotItems
if (slot in data) {
if (isEmpty)
data.remove(slot)
continue
}
if (!isEmpty) {
data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null)
}
}
}
private fun rememberPage(
handler: StorageBackingHandle.Page,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
// TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence
val newStacks =
VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() })
data.compute(handler.storagePageSlot) { slot, existingInventory ->
(existingInventory ?: StorageData.StorageInventory(
slot.defaultName(),
slot,
null
)).also {
it.inventory = newStacks
}
}
}
}

View File

@@ -0,0 +1,135 @@
package moe.nea.firmament.features.inventory.storageoverlay
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.assertNotNullOr
import moe.nea.firmament.util.toShedaniel
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.network.packet.c2s.play.CloseHandledScreenC2SPacket
import net.minecraft.text.Text
import net.minecraft.util.DyeColor
import kotlin.math.max
class StorageOverlayScreen() : Screen(Text.empty()) {
companion object {
val emptyStorageSlotItems = listOf<Item>(
Blocks.RED_STAINED_GLASS_PANE.asItem(),
Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
}
private var handler: StorageBackingHandle = StorageBackingHandle.None
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
private fun discardOldHandle() {
val player = assertNotNullOr(MC.player) { return }
val handle = this.handler
if (handle is StorageBackingHandle.HasBackingScreen) {
player.networkHandler.sendPacket(CloseHandledScreenC2SPacket(handle.handler.syncId))
if (player.currentScreenHandler === handle.handler) {
player.currentScreenHandler = player.playerScreenHandler
}
}
}
fun setHandler(handler: StorageBackingHandle?) {
discardOldHandle()
if (handler != null)
this.handler = handler
}
var scroll = 0
var lastRenderedHeight = 0
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
context.matrices.push()
context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
context.matrices.pop()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
val pageX = (index % StorageOverlay.config.rows)
if (pageX == 0) {
currentMaxHeight += StorageOverlay.config.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
width / 2 - (StorageOverlay.config.rows * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
}
lastRenderedHeight = totalHeight + currentMaxHeight
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
layoutedForEach { (k, p), x, y ->
val rx = mouseX - x
val ry = mouseY - y
if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
close()
k.navigateTo()
return true
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
fun getStorePageHeight(page: StorageData.StorageInventory): Int {
return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean {
scroll =
(scroll + amount * StorageOverlay.config.scrollSpeed *
(if (StorageOverlay.config.inverseScroll) 1 else -1)).toInt()
.coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
return true
}
private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
context.drawText(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
if (inventory == null) {
// TODO: Missing texture
context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
return
}
for ((index, stack) in inventory.stacks.withIndex()) {
val x = (index % 9) * 19
val y = (index / 9) * 19 + MC.font.fontHeight + 2
if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
} else {
context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
}
context.drawItem(stack, x + 1, y + 1)
context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
}
}
override fun close() {
discardOldHandle()
isClosing = true
super.close()
}
}

View File

@@ -0,0 +1,64 @@
package moe.nea.firmament.features.inventory.storageoverlay
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.nea.firmament.util.MC
@Serializable(with = StoragePageSlot.Serializer::class)
data class StoragePageSlot(val index: Int) : Comparable<StoragePageSlot> {
object Serializer : KSerializer<StoragePageSlot> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StoragePageSlot", PrimitiveKind.INT)
override fun deserialize(decoder: Decoder): StoragePageSlot {
return StoragePageSlot(decoder.decodeInt())
}
override fun serialize(encoder: Encoder, value: StoragePageSlot) {
encoder.encodeInt(value.index)
}
}
init {
assert(index in 0 until (3 * 9))
}
val isEnderChest get() = index < 9
val isBackPack get() = !isEnderChest
val slotIndexInOverviewPage get() = if (isEnderChest) index + 9 else index + 18
fun defaultName(): String = if (isEnderChest) "Ender Chest #${index + 1}" else "Backpack #${index - 9 + 1}"
fun navigateTo() {
if (isBackPack) {
MC.sendCommand("backpack ${index - 9 + 1}")
} else {
MC.sendCommand("enderchest ${index + 1}")
}
}
companion object {
fun fromOverviewSlotIndex(slot: Int): StoragePageSlot? {
if (slot in 9 until 18) return StoragePageSlot(slot - 9)
if (slot in 27 until 45) return StoragePageSlot(slot - 27 + 9)
return null
}
fun ofEnderChestPage(slot: Int): StoragePageSlot {
assert(slot in 1..9)
return StoragePageSlot(slot - 1)
}
fun ofBackPackPage(slot: Int): StoragePageSlot {
assert(slot in 1..18)
return StoragePageSlot(slot - 1 + 9)
}
}
override fun compareTo(other: StoragePageSlot): Int {
return this.index - other.index
}
}

View File

@@ -0,0 +1,53 @@
package moe.nea.firmament.features.inventory.storageoverlay
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.encodeBase64
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
val stacks: List<ItemStack>
) {
val rows = stacks.size / 9
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
return VirtualInventory(items.map { ItemStack.fromNbt(it as NbtCompound) })
}
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
value.stacks.forEach {
list.add(NbtCompound().also(it::writeNbt))
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
}