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

@@ -1,13 +1,12 @@
package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.events.ChestInventoryUpdateEvent;
import moe.nea.firmament.events.PlayerInventoryUpdate;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommonNetworkHandler;
import net.minecraft.client.network.ClientConnectionState;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.s2c.play.InventoryS2CPacket;
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
@@ -18,36 +17,40 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public abstract class SlotUpdateListener extends ClientCommonNetworkHandler {
protected SlotUpdateListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
super(client, connection, connectionState);
}
protected SlotUpdateListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
super(client, connection, connectionState);
}
@Inject(
method = "onScreenHandlerSlotUpdate",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/tutorial/TutorialManager;onSlotUpdate(Lnet/minecraft/item/ItemStack;)V"))
private void onSingleSlotUpdate(
ScreenHandlerSlotUpdateS2CPacket packet,
CallbackInfo ci) {
var player = this.client.player;
assert player != null;
if (packet.getSyncId() == ScreenHandlerSlotUpdateS2CPacket.UPDATE_PLAYER_INVENTORY_SYNC_ID
|| packet.getSyncId() == 0) {
PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Single(packet.getSlot(), packet.getStack()));
} else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
// TODO: dispatch single chest slot
}
}
@Inject(
method = "onScreenHandlerSlotUpdate",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/tutorial/TutorialManager;onSlotUpdate(Lnet/minecraft/item/ItemStack;)V"))
private void onSingleSlotUpdate(
ScreenHandlerSlotUpdateS2CPacket packet,
CallbackInfo ci) {
var player = this.client.player;
assert player != null;
if (packet.getSyncId() == ScreenHandlerSlotUpdateS2CPacket.UPDATE_PLAYER_INVENTORY_SYNC_ID
|| packet.getSyncId() == 0) {
PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Single(packet.getSlot(), packet.getStack()));
} else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
ChestInventoryUpdateEvent.Companion.publish(
new ChestInventoryUpdateEvent.Single(packet.getSlot(), packet.getStack())
);
}
}
@Inject(method = "onInventory",
at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V",
shift = At.Shift.AFTER))
private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) {
var player = this.client.player;
assert player != null;
if (packet.getSyncId() == 0) {
PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents()));
} else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
// TODO: dispatch multi chest
}
}
@Inject(method = "onInventory",
at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V",
shift = At.Shift.AFTER))
private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) {
var player = this.client.player;
assert player != null;
if (packet.getSyncId() == 0) {
PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents()));
} else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
ChestInventoryUpdateEvent.Companion.publish(
new ChestInventoryUpdateEvent.Multi(packet.getContents())
);
}
}
}

View File

@@ -0,0 +1,14 @@
package moe.nea.firmament.mixins.accessor;
import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.ChatHudLine;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.List;
@Mixin(ChatHud.class)
public interface AccessorChatHud {
@Accessor("messages")
List<ChatHudLine> getMessages_firmament();
}

View File

@@ -4,7 +4,9 @@ import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType.string
import io.ktor.client.statement.bodyAsText
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.nbt.NbtOps
import net.minecraft.text.Text
import net.minecraft.text.TextCodecs
import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.FirmamentEventBus
@@ -24,6 +26,7 @@ import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.accessors.messages
import moe.nea.firmament.util.collections.InstanceList
import moe.nea.firmament.util.collections.WeakCache
@@ -205,6 +208,14 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
thenLiteral("dumpchat") {
thenExecute {
MC.inGameHud.chatHud.messages.forEach {
val nbt = TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow
println(nbt)
}
}
}
thenLiteral("sbdata") {
thenExecute {
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.profile", SBData.profileId))

View File

@@ -0,0 +1,11 @@
package moe.nea.firmament.events
import net.minecraft.item.ItemStack
import moe.nea.firmament.util.MC
sealed class ChestInventoryUpdateEvent : FirmamentEvent() {
companion object : FirmamentEventBus<ChestInventoryUpdateEvent>()
data class Single(val slot: Int, val stack: ItemStack) : ChestInventoryUpdateEvent()
data class Multi(val contents: List<ItemStack>) : ChestInventoryUpdateEvent()
val inventory = MC.screen
}

View File

@@ -27,7 +27,7 @@ import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.item.createSkullItem
import moe.nea.firmament.util.mc.createSkullItem
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.setSkyBlockFirmamentUiId
import moe.nea.firmament.util.skyBlockId

View File

@@ -13,7 +13,7 @@ import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.collections.lastNotNullOfOrNull
import moe.nea.firmament.util.collections.memoizeIdentity
import moe.nea.firmament.util.unformattedString

View File

@@ -28,8 +28,8 @@ 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.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.skyblockUUID
import moe.nea.firmament.util.unformattedString

View File

@@ -21,8 +21,8 @@ import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.TIME_PATTERN
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
import moe.nea.firmament.util.render.RenderCircleProgress

View File

@@ -19,7 +19,7 @@ import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.item.decodeProfileTextureProperty
import moe.nea.firmament.util.mc.decodeProfileTextureProperty
import moe.nea.firmament.util.skyBlockId
object CustomSkyBlockTextures : FirmamentFeature {

View File

@@ -5,8 +5,8 @@ import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {

View File

@@ -3,7 +3,7 @@ package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import moe.nea.firmament.util.item.loreAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser {

View File

@@ -12,8 +12,8 @@ import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import moe.nea.firmament.rei.SBItemStack
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.item.setEncodedSkullOwner
import moe.nea.firmament.util.item.zeroUUID
import moe.nea.firmament.util.mc.setEncodedSkullOwner
import moe.nea.firmament.util.mc.zeroUUID
object ModifyEquipment : EntityModifier {
val names = mapOf(

View File

@@ -28,8 +28,8 @@ import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.HypixelPetInfo
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.appendLore
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.withColor

View File

@@ -33,10 +33,10 @@ import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.appendLore
import moe.nea.firmament.util.item.setCustomName
import moe.nea.firmament.util.item.setSkullOwner
import moe.nea.firmament.util.modifyLore
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.setCustomName
import moe.nea.firmament.util.mc.setSkullOwner
import moe.nea.firmament.util.mc.modifyLore
import moe.nea.firmament.util.skyblockId
object ItemCache : IReloadable {

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))
}
}
}