Add basic sack util

[no changelog]
This commit is contained in:
Linnea Gräf
2024-10-01 18:00:43 +02:00
parent beb14d73bd
commit a4eac70118
22 changed files with 350 additions and 177 deletions

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.util.accessors
import net.minecraft.client.gui.hud.ChatHud
import net.minecraft.client.gui.hud.ChatHudLine
import moe.nea.firmament.mixins.accessor.AccessorChatHud
val ChatHud.messages: MutableList<ChatHudLine>
get() = (this as AccessorChatHud).messages_firmament

View File

@@ -0,0 +1,28 @@
package moe.nea.firmament.util.mc
import java.util.Spliterator
import java.util.Spliterators
import net.minecraft.inventory.Inventory
import net.minecraft.item.ItemStack
val Inventory.indices get() = 0 until size()
val Inventory.iterableView
get() = object : Iterable<ItemStack> {
override fun spliterator(): Spliterator<ItemStack> {
return Spliterators.spliterator(iterator(), size().toLong(), 0)
}
override fun iterator(): Iterator<ItemStack> {
return object : Iterator<ItemStack> {
var i = 0
override fun hasNext(): Boolean {
return i < size()
}
override fun next(): ItemStack {
if (!hasNext()) throw NoSuchElementException()
return getStack(i++)
}
}
}
}

View File

@@ -1,13 +1,7 @@
package moe.nea.firmament.util
package moe.nea.firmament.util.mc
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtList
import net.minecraft.text.Text
import moe.nea.firmament.util.item.loreAccordingToNbt
fun ItemStack.appendLore(args: List<Text>) {
if (args.isEmpty()) return

View File

@@ -1,6 +1,4 @@
package moe.nea.firmament.util.item
package moe.nea.firmament.util.mc
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.LoreComponent

View File

@@ -1,8 +1,6 @@
@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class)
package moe.nea.firmament.util.item
package moe.nea.firmament.util.mc
import com.mojang.authlib.GameProfile
import com.mojang.authlib.minecraft.MinecraftProfileTexture

View File

@@ -1,5 +1,3 @@
package moe.nea.firmament.util
import java.util.regex.Matcher
@@ -10,12 +8,12 @@ import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? =
regex.matchEntire(this)?.let(block)
regex.matchEntire(this)?.let(block)
inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? =
matcher(string)
.takeIf(Matcher::matches)
?.let(block)
matcher(string)
.takeIf(Matcher::matches)
?.let(block)
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"
@@ -25,31 +23,33 @@ val SHORT_NUMBER_FORMAT = "[0-9]+(?:,[0-9]+)*(?:\\.[0-9]+)?[kKmMbB]?"
val siScalars = mapOf(
'k' to 1_000.0,
'K' to 1_000.0,
'm' to 1_000_000.0,
'M' to 1_000_000.0,
'b' to 1_000_000_000.0,
'B' to 1_000_000_000.0,
'k' to 1_000.0,
'K' to 1_000.0,
'm' to 1_000_000.0,
'M' to 1_000_000.0,
'b' to 1_000_000_000.0,
'B' to 1_000_000_000.0,
)
fun parseTimePattern(text: String): Duration {
val length = text.dropLast(1).toInt()
return when (text.last()) {
'm' -> length.minutes
's' -> length.seconds
else -> error("Invalid pattern for time $text")
}
val length = text.dropLast(1).toInt()
return when (text.last()) {
'm' -> length.minutes
's' -> length.seconds
else -> error("Invalid pattern for time $text")
}
}
fun parseShortNumber(string: String): Double {
var k = string.replace(",", "")
val scalar = k.last()
var scalarMultiplier = siScalars[scalar]
if (scalarMultiplier == null) {
scalarMultiplier = 1.0
} else {
k = k.dropLast(1)
}
return k.toDouble() * scalarMultiplier
if (string.startsWith("-")) return -parseShortNumber(string.substring(1))
if (string.startsWith("+")) return parseShortNumber(string.substring(1))
var k = string.replace(",", "")
val scalar = k.last()
var scalarMultiplier = siScalars[scalar]
if (scalarMultiplier == null) {
scalarMultiplier = 1.0
} else {
k = k.dropLast(1)
}
return k.toDouble() * scalarMultiplier
}

View File

@@ -0,0 +1,110 @@
package moe.nea.firmament.util.skyblock
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.text.HoverEvent
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ChestInventoryUpdateEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.iterableView
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
object SackUtil {
@Serializable
data class SackContents(
// TODO: store the certainty of knowledge for each item.
val contents: MutableMap<SkyblockId, Long> = mutableMapOf(),
// val sackTypes:
)
object Store : ProfileSpecificDataHolder<SackContents>(serializer(), "Sacks", ::SackContents)
val items get() = Store.data?.contents ?: mutableMapOf()
val storedRegex = "^Stored: (?<stored>$SHORT_NUMBER_FORMAT)/(?<max>$SHORT_NUMBER_FORMAT)$".toPattern()
@Subscribe
fun storeDataFromInventory(event: ChestInventoryUpdateEvent) {
val screen = event.inventory as? GenericContainerScreen ?: return
if (!screen.title.unformattedString.endsWith(" Sack")) return
val inv = screen.screenHandler?.inventory ?: return
if (inv.size() < 18) return
val backSlot = inv.getStack(inv.size() - 5)
if (backSlot.displayNameAccordingToNbt.unformattedString != "Go Back") return
if (backSlot.loreAccordingToNbt.map { it.unformattedString } != listOf("To Sack of Sacks")) return
for (itemStack in inv.iterableView) {
// TODO: handle runes and gemstones
val stored = itemStack.loreAccordingToNbt.firstNotNullOfOrNull {
storedRegex.useMatch(it.unformattedString) {
val stored = parseShortNumber(group("stored")).toLong()
val max = parseShortNumber(group("max")).toLong()
stored
}
} ?: continue
val itemId = itemStack.skyBlockId ?: continue
items[itemId] = stored
}
Store.markDirty()
}
@Subscribe
fun updateFromChat(event: ProcessChatEvent) {
if (!event.unformattedString.startsWith("[Sacks]")) return
val update = ChatUpdate()
event.text.siblings.forEach(update::updateFromHoverText)
}
data class SackUpdate(
val itemId: SkyblockId?,
val itemName: String,
val changeAmount: Long,
)
private class ChatUpdate {
val updates = mutableListOf<SackUpdate>()
var foundAdded = false
var foundRemoved = false
fun updateFromCleanText(cleanedText: String) {
cleanedText.split("\n").forEach { line ->
changePattern.useMatch(line) {
val amount = parseShortNumber(group("amount")).toLong()
val itemName = group("itemName")
val itemId = ItemNameLookup.guessItemByName(itemName, false)
updates.add(SackUpdate(itemId, itemName, amount))
}
}
}
fun updateFromHoverText(text: Text) {
text.siblings.forEach(::updateFromHoverText)
val hoverText = text.style.hoverEvent?.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
val cleanedText = hoverText.unformattedString
if (cleanedText.startsWith("Added items:\n")) {
if (!foundAdded) {
updateFromCleanText(cleanedText)
foundAdded = true
}
}
if (cleanedText.startsWith("Removed items:\n")) {
if (!foundRemoved) {
updateFromCleanText(cleanedText)
foundRemoved = true
}
}
}
}
val changePattern = " (?<amount>[+\\-]$SHORT_NUMBER_FORMAT) (?<itemName>[^(]+) \\(.*\\)".toPattern()
}

View File

@@ -1,10 +1,7 @@
package moe.nea.firmament.util
import net.minecraft.text.MutableText
import net.minecraft.text.PlainTextContent
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.text.TranslatableTextContent
import net.minecraft.util.Formatting
@@ -12,106 +9,107 @@ import moe.nea.firmament.Firmament
class TextMatcher(text: Text) {
data class State(
var iterator: MutableList<Text>,
var currentText: Text?,
var offset: Int,
var textContent: String,
)
data class State(
var iterator: MutableList<Text>,
var currentText: Text?,
var offset: Int,
var textContent: String,
)
var state = State(
mutableListOf(text),
null,
0,
""
)
var state = State(
mutableListOf(text),
null,
0,
""
)
fun pollChunk(): Boolean {
val firstOrNull = state.iterator.removeFirstOrNull() ?: return false
state.offset = 0
state.currentText = firstOrNull
state.textContent = when (val content = firstOrNull.content) {
is PlainTextContent.Literal -> content.string
else -> {
Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.")
return false
}
}
state.iterator.addAll(0, firstOrNull.siblings)
return true
}
fun pollChunk(): Boolean {
val firstOrNull = state.iterator.removeFirstOrNull() ?: return false
state.offset = 0
state.currentText = firstOrNull
state.textContent = when (val content = firstOrNull.content) {
is PlainTextContent.Literal -> content.string
else -> {
Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.")
return false
}
}
state.iterator.addAll(0, firstOrNull.siblings)
return true
}
fun pollChunks(): Boolean {
while (state.offset !in state.textContent.indices) {
if (!pollChunk()) {
return false
}
}
return true
}
fun pollChunks(): Boolean {
while (state.offset !in state.textContent.indices) {
if (!pollChunk()) {
return false
}
}
return true
}
fun pollChar(): Char? {
if (!pollChunks()) return null
return state.textContent[state.offset++]
}
fun pollChar(): Char? {
if (!pollChunks()) return null
return state.textContent[state.offset++]
}
fun expectString(string: String): Boolean {
var found = ""
while (found.length < string.length) {
if (!pollChunks()) return false
val takeable = state.textContent.drop(state.offset).take(string.length - found.length)
state.offset += takeable.length
found += takeable
}
return found == string
}
fun expectString(string: String): Boolean {
var found = ""
while (found.length < string.length) {
if (!pollChunks()) return false
val takeable = state.textContent.drop(state.offset).take(string.length - found.length)
state.offset += takeable.length
found += takeable
}
return found == string
}
}
val formattingChars = "kmolnrKMOLNR".toSet()
fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
var nextParagraph = indexOf('§')
if (nextParagraph < 0) return this.toString()
val stringBuffer = StringBuilder(this.length)
var readIndex = 0
while (nextParagraph >= 0) {
stringBuffer.append(this, readIndex, nextParagraph)
if (keepNonColorCodes && nextParagraph + 1 < length && this[nextParagraph + 1] in formattingChars) {
readIndex = nextParagraph
nextParagraph = indexOf('§', startIndex = readIndex + 1)
} else {
readIndex = nextParagraph + 2
nextParagraph = indexOf('§', startIndex = readIndex)
}
if (readIndex > this.length)
readIndex = this.length
}
stringBuffer.append(this, readIndex, this.length)
return stringBuffer.toString()
var nextParagraph = indexOf('§')
if (nextParagraph < 0) return this.toString()
val stringBuffer = StringBuilder(this.length)
var readIndex = 0
while (nextParagraph >= 0) {
stringBuffer.append(this, readIndex, nextParagraph)
if (keepNonColorCodes && nextParagraph + 1 < length && this[nextParagraph + 1] in formattingChars) {
readIndex = nextParagraph
nextParagraph = indexOf('§', startIndex = readIndex + 1)
} else {
readIndex = nextParagraph + 2
nextParagraph = indexOf('§', startIndex = readIndex)
}
if (readIndex > this.length)
readIndex = this.length
}
stringBuffer.append(this, readIndex, this.length)
return stringBuffer.toString()
}
val Text.unformattedString: String
get() = string.removeColorCodes()
get() = string.removeColorCodes()
fun Text.allSiblings(): List<Text> = listOf(this) + siblings.flatMap { it.allSiblings() }
fun MutableText.withColor(formatting: Formatting) = this.styled { it.withColor(formatting).withItalic(false) }
fun Text.transformEachRecursively(function: (Text) -> Text): Text {
val c = this.content
if (c is TranslatableTextContent) {
return Text.translatableWithFallback(c.key, c.fallback, *c.args.map {
(if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function)
}.toTypedArray()).also { new ->
new.style = this.style
new.siblings.clear()
this.siblings.forEach { child ->
new.siblings.add(child.transformEachRecursively(function))
}
}
}
return function(this.copy().also { it.siblings.clear() }).also { tt ->
this.siblings.forEach {
tt.siblings.add(it.transformEachRecursively(function))
}
}
val c = this.content
if (c is TranslatableTextContent) {
return Text.translatableWithFallback(c.key, c.fallback, *c.args.map {
(if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function)
}.toTypedArray()).also { new ->
new.style = this.style
new.siblings.clear()
this.siblings.forEach { child ->
new.siblings.add(child.transformEachRecursively(function))
}
}
}
return function(this.copy().also { it.siblings.clear() }).also { tt ->
this.siblings.forEach {
tt.siblings.add(it.transformEachRecursively(function))
}
}
}