diff --git a/src/main/java/com/chattriggers/ctjs/internal/mixins/PlayerEntityMixin.java b/src/main/java/com/chattriggers/ctjs/internal/mixins/PlayerEntityMixin.java index 4c8d1d70..f2a91b7b 100644 --- a/src/main/java/com/chattriggers/ctjs/internal/mixins/PlayerEntityMixin.java +++ b/src/main/java/com/chattriggers/ctjs/internal/mixins/PlayerEntityMixin.java @@ -18,7 +18,7 @@ public class PlayerEntityMixin implements NameTagOverridable { @ModifyVariable(method = "getDisplayName", at = @At(value = "STORE", ordinal = 0)) private MutableText injectGetName(MutableText original) { if (overriddenNametagName != null) - return overriddenNametagName.getComponent(); + return overriddenNametagName.toMutableText$ctjs(); return original; } diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/commands/DynamicCommands.kt b/src/main/kotlin/com/chattriggers/ctjs/api/commands/DynamicCommands.kt index 3e88ec61..1e83b934 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/commands/DynamicCommands.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/commands/DynamicCommands.kt @@ -853,23 +853,23 @@ object DynamicCommands : CommandCollection() { if (impl.selectors.isEmpty()) return TextComponent(text) - val component = TextComponent(text.substring(0, impl.selectors[0].start)) + var component = TextComponent(text.substring(0, impl.selectors[0].start)) var i = impl.selectors[0].start for (selector in impl.selectors) { val entities = EntitySelectorWrapper(selector.selector).getEntities() val nameComponent = EntitySelector.getNames(entities.map(Entity::toMC)) if (i < selector.start) - component.append(TextComponent(text.substring(i, selector.start))) + component = component.withText(text.substring(i, selector.start)) if (nameComponent != null) - component.append(nameComponent) + component = component.withText(nameComponent) i = selector.end } if (i < text.length) - component.append(TextComponent(text.drop(i))) + component = component.withText(text.drop(i)) return component } diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/entity/PlayerMP.kt b/src/main/kotlin/com/chattriggers/ctjs/api/entity/PlayerMP.kt index 66021fc1..b395ac8b 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/entity/PlayerMP.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/entity/PlayerMP.kt @@ -55,7 +55,7 @@ class PlayerMP(override val mcValue: PlayerEntity) : LivingEntity(mcValue) { } private fun getPlayerName(playerListEntry: PlayerListEntry?): TextComponent { - return playerListEntry?.displayName?.let(::TextComponent) + return playerListEntry?.displayName?.let { TextComponent(it) } ?: TextComponent( MCTeam.decorateName( playerListEntry?.scoreboardTeam, diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/inventory/Item.kt b/src/main/kotlin/com/chattriggers/ctjs/api/inventory/Item.kt index a796f2e7..a467a6f7 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/inventory/Item.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/inventory/Item.kt @@ -84,7 +84,7 @@ class Item(override val mcValue: ItemStack) : CTWrapper { fun getLore(advanced: Boolean = false): List { mcValue.asMixin().ctjs_setShouldSkip(true) val tooltip = mcValue.getTooltip(Player.toMC(), if (advanced) TooltipContext.ADVANCED else TooltipContext.BASIC) - .map(::TextComponent) + .map { TextComponent(it) } mcValue.asMixin().ctjs_setShouldSkip(false) return tooltip diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/message/ChatLib.kt b/src/main/kotlin/com/chattriggers/ctjs/api/message/ChatLib.kt index 88b627c8..862b64ac 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/message/ChatLib.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/message/ChatLib.kt @@ -1,9 +1,7 @@ package com.chattriggers.ctjs.api.message import com.chattriggers.ctjs.api.client.Client -import com.chattriggers.ctjs.api.client.Player import com.chattriggers.ctjs.api.render.Renderer -import com.chattriggers.ctjs.engine.printToConsole import com.chattriggers.ctjs.internal.listeners.ClientListener import com.chattriggers.ctjs.internal.mixins.ChatHudAccessor import com.chattriggers.ctjs.internal.utils.asMixin @@ -22,50 +20,47 @@ object ChatLib { /** * Prints text in the chat. - * The text can be a String, a [Message] or a [TextComponent] + * The text can be a String or a [TextComponent] * * @param text the text to be printed */ @JvmStatic fun chat(text: Any?) { when (text) { - is String -> Message(text).chat() - is Message -> text.chat() - is TextComponent -> text.chat() - else -> Message(text.toString()).chat() - } + is TextComponent -> text + is String -> TextComponent(text) + else -> TextComponent(text.toString()) + }.chat() } /** * Shows text in the action bar. - * The text can be a String, a [Message] or a [TextComponent] + * The text can be a String or a [TextComponent] * * @param text the text to show */ @JvmStatic fun actionBar(text: Any?) { when (text) { - is String -> Message(text).actionBar() - is Message -> text.actionBar() - is TextComponent -> text.actionBar() - else -> Message(text.toString()).actionBar() - } + is TextComponent -> text + is String -> TextComponent(text) + else -> TextComponent(text.toString()) + }.actionBar() } /** * Simulates a chat message to be caught by other triggers for testing. - * The text can be a String, a [Message] or a [TextComponent] + * The text can be a String or a [TextComponent] * * @param text The message to simulate */ @JvmStatic fun simulateChat(text: Any?) { when (text) { - is String -> Message(text).setRecursive(true).chat() - is Message -> text.setRecursive(true).chat() - is TextComponent -> Message(text).setRecursive(true).chat() - else -> Message(text.toString()).setRecursive(true).chat() - } + is TextComponent -> text + is String -> TextComponent(text) + else -> TextComponent(text.toString()) + }.withRecursive().chat() } @@ -229,15 +224,15 @@ object ChatLib { } /** - * Edits an already sent chat message by the [Message] + * Edits an already sent chat message by the [TextComponent] * * @param toReplace the message to be replaced * @param replacements the new message(s) to be put in place of the old one */ @JvmStatic - fun editChat(toReplace: Message, vararg replacements: Any) { + fun editChat(toReplace: TextComponent, vararg replacements: Any) { editLines(replacements) { - toReplace.chatMessage.formattedText == TextComponent(it.content).formattedText + toReplace.formattedText == TextComponent(it.content).formattedText } } @@ -254,6 +249,18 @@ object ChatLib { } } + /** + * Edits an already sent chat message by given a callback that receives + * [TextComponent] instances + * + * @param matcher a function that accepts a [TextComponent] and returns a boolean + * @param replacements the new message(s) to be put in place of the old one + */ + @JvmStatic + fun editChat(matcher: (TextComponent) -> Boolean, vararg replacements: Any) { + editLines(replacements) { matcher(TextComponent(it.content)) } + } + private fun editLines(replacements: Array, matcher: (ChatHudLine) -> Boolean) { val mc = Client.getMinecraft() val indicator = if (mc.isConnectedToLocalServer) MessageIndicator.singlePlayer() else MessageIndicator.system() @@ -267,11 +274,8 @@ object ChatLib { it.remove() chatLineIds.remove(next) for (replacement in replacements) { - val message = if (replacement !is Message) { - TextComponent.from(replacement)?.let(::Message) ?: continue - } else replacement - - val line = ChatHudLine(next.creationTick, message.chatMessage, null, indicator) + val message = replacement as? TextComponent ?: TextComponent(replacement) + val line = ChatHudLine(next.creationTick, message, null, indicator) if (message.getChatLineId() != -1) chatLineIds[line] = message.getChatLineId() @@ -317,14 +321,14 @@ object ChatLib { } /** - * Deletes an already sent chat message by the [Message] + * Deletes an already sent chat message by the [TextComponent] * * @param toDelete the message to be deleted */ @JvmStatic - fun deleteChat(toDelete: Message) { + fun deleteChat(toDelete: TextComponent) { removeLines { - toDelete.chatMessage.formattedText == TextComponent(it.content).formattedText + toDelete.formattedText == TextComponent(it.content).formattedText } } @@ -338,6 +342,17 @@ object ChatLib { removeLines { chatLineIds[it] == chatLineId } } + /** + * Deletes an already sent chat message given a callback that receives + * [TextComponent] instances + * + * @param chatLineId the chat line id of the message to be deleted + */ + @JvmStatic + fun deleteChat(matcher: (TextComponent) -> Boolean) { + removeLines { matcher(TextComponent(it.content)) } + } + private fun removeLines(matcher: (ChatHudLine) -> Boolean) { var removed = false val it = chatHudAccessor?.messages?.listIterator() ?: return @@ -383,15 +398,14 @@ object ChatLib { } } - internal fun sendMessageWithId(message: Message) { + internal fun sendMessageWithId(message: TextComponent) { require(message.getChatLineId() != -1) val chatGui = Client.getChatGui() ?: return - val chatMessage = message.chatMessage - chatGui.addMessage(chatMessage) - val newChatLine: ChatHudLine = chatHudAccessor!!.messages[0] + chatGui.addMessage(message) + val newChatLine = chatHudAccessor!!.messages[0] - check(chatMessage == newChatLine.content()) { + check(message == newChatLine.content()) { "Expected new chat message to be at index 0" } diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/message/Message.kt b/src/main/kotlin/com/chattriggers/ctjs/api/message/Message.kt deleted file mode 100644 index f8334cfe..00000000 --- a/src/main/kotlin/com/chattriggers/ctjs/api/message/Message.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.chattriggers.ctjs.api.message - -import com.chattriggers.ctjs.api.client.Client -import com.chattriggers.ctjs.api.client.Player -import net.minecraft.network.packet.s2c.play.GameMessageS2CPacket -import net.minecraft.text.MutableText -import java.util.concurrent.ThreadLocalRandom - -class Message { - private var chatLineId: Int = -1 - private var isRecursive: Boolean = false - private var isFormatted: Boolean = true - - /** - * The individual components in the message - */ - val messageParts: MutableList = mutableListOf() - - /** - * The underlying [TextComponent] - */ - lateinit var chatMessage: TextComponent - private set - - val formattedText: String - get() = chatMessage.formattedText - - val unformattedText: String - get() = chatMessage.unformattedText - - constructor(component: TextComponent) { - if (component.siblings.isEmpty()) { - messageParts.add(component) - } else { - component.siblings - .filterIsInstance() - .map { TextComponent(it) } - .forEach { messageParts.add(it) } - } - parseMessage() - } - - constructor(vararg parts: Any) { - parts.forEach(::addPart) - parseMessage() - } - - /** - * @return the chat line ID of the message - */ - fun getChatLineId(): Int = chatLineId - - /** - * Sets the chat line ID of the message. Useful for updating an already sent chat message. - */ - fun setChatLineId(id: Int) = apply { chatLineId = id } - - /** - * @return true if the message can trip other triggers. - */ - fun isRecursive(): Boolean = isRecursive - - /** - * Sets whether the message can trip other triggers. - * @param recursive true if message can trip other triggers. - */ - fun setRecursive(recursive: Boolean) = apply { this.isRecursive = recursive } - - /** - * @return true if the message is formatted - */ - fun isFormatted(): Boolean = isFormatted - - /** - * Sets if the message is to be formatted - * @param formatted true if formatted - */ - fun setFormatted(formatted: Boolean) = apply { this.isFormatted = formatted } - - /** - * Sets the TextComponent or String in the Message at index. - * - * @param index the index of the TextComponent or String to change - * @param component the new TextComponent or String to replace with - * @return the Message for method chaining - */ - fun setTextComponent(index: Int, component: Any) = apply { - if (component is String) { - messageParts[index] = TextComponent(component) - } else { - TextComponent.from(component)?.also { messageParts[index] = it } - } - parseMessage() - } - - /** - * Adds a TextComponent or String at index of the Message. - * - * @param index the index to insert the new TextComponent or String - * @param component the new TextComponent or String to insert - * @return the Message for method chaining - */ - fun addTextComponent(index: Int, component: Any) = apply { - if (component is String) { - messageParts.add(index, TextComponent(component)) - } else { - TextComponent.from(component)?.also { messageParts.add(index, it) } - } - parseMessage() - } - - /** - * Adds a TextComponent or String to the end of the Message. - * - * @param component the new TextComponent or String to add - * @return the Message for method chaining - */ - fun addTextComponent(component: Any): Message = addTextComponent(messageParts.size, component) - - /** - * Must be called to be able to edit later - */ - fun mutable() = apply { - chatLineId = ThreadLocalRandom.current().nextInt() - } - - fun edit(vararg replacements: Any) = apply { - require(chatLineId != -1) { "This Message is not mutable" } - messageParts.clear() - replacements.forEach(::addPart) - parseMessage() - ChatLib.editChat(chatLineId, *replacements) - } - - fun chat() = apply { - if (Player.toMC() == null) - return@apply - - if (chatLineId != -1) { - ChatLib.sendMessageWithId(this) - return@apply - } - - if (isRecursive) { - Client.scheduleTask { - Client.getMinecraft().networkHandler?.onGameMessage(GameMessageS2CPacket(chatMessage, false)) - } - } else { - Player.toMC()?.sendMessage(chatMessage) - } - } - - fun actionBar() = apply { - if (Player.toMC() == null) - return@apply - - if (isRecursive) { - Client.scheduleTask { - Client.getMinecraft().networkHandler?.onGameMessage(GameMessageS2CPacket(chatMessage, true)) - } - } else { - Player.toMC()?.sendMessage(chatMessage, true) - } - } - - private fun addPart(part: Any) { - if (part is TextComponent) { - messageParts.add(part) - } else TextComponent.from(part)?.also(messageParts::add) - parseMessage() - } - - private fun parseMessage() { - chatMessage = TextComponent("") - messageParts.forEach { chatMessage.appendSibling(it) } - } -} diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/message/TextComponent.kt b/src/main/kotlin/com/chattriggers/ctjs/api/message/TextComponent.kt index bf28dee2..95f5871d 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/message/TextComponent.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/message/TextComponent.kt @@ -1,420 +1,540 @@ package com.chattriggers.ctjs.api.message +import com.chattriggers.ctjs.CTJS import com.chattriggers.ctjs.MCEntity +import com.chattriggers.ctjs.api.client.Client +import com.chattriggers.ctjs.api.client.Player import com.chattriggers.ctjs.api.entity.Entity import com.chattriggers.ctjs.api.inventory.Item import com.chattriggers.ctjs.api.inventory.ItemType -import com.chattriggers.ctjs.api.render.Renderer import com.chattriggers.ctjs.internal.utils.hoverEventActionByName +import com.chattriggers.ctjs.internal.utils.toIdentifier +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder import gg.essential.universal.UChat +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject import net.minecraft.item.ItemStack +import net.minecraft.network.packet.s2c.play.GameMessageS2CPacket import net.minecraft.text.* import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.ScriptRuntime import java.util.* - -@Suppress("MemberVisibilityCanBePrivate") -class TextComponent : Text { - lateinit var component: MutableText - private set - - private var text: String - private var formatted = true - private var clickAction: ClickEvent.Action? = null - private var clickValue: String? = null - private var hoverAction: HoverEvent.Action<*>? = null - private var hoverValue: Any? = null - +import java.util.concurrent.ThreadLocalRandom +import kotlin.streams.toList + +/** + * A wrapper around the Minecraft Text class and it's various inheritors. + * + * This class acts as a container for "pairs". A pair is a string of text + * that is associated with a particular [Style]. Unlike Minecraft's Text + * class, this container is a list, not a tree, which makes them much easier + * to work with. It implements [List]<[NativeObject]>, so it can be iterated + * over. + * + * Importantly, instances of [TextComponent] are immutable. Methods for + * "mutation" exist, but they return new instances of [TextComponent]. See + * [withText] for an example. + * + * @see Text + */ +// Note: For the sake of the Text implementations, parts[0] is the "self" part, +// and parts[1..] are the siblings, but the exposed CT API treats this as +// a container of individual parts +class TextComponent private constructor( + private val parts: MutableList, + private val chatLineId: Int = -1, + private val isRecursive: Boolean = false, +) : Text, List { /** - * Creates a [TextComponent] from a string. - * - * @param text the text string in the component. + * Creates an empty [TextComponent] with a single, unstyled, empty part. */ - constructor(text: String) { - this.text = text - reInstance() - } + constructor() : this(listOf(Part("", Style.EMPTY))) /** - * Creates a [TextComponent] from an existing [Text] instance. + * Creates a [TextComponent] from a variable number of objects. These + * objects can be: + * - A plain string possibly containing formatting codes. If the string has + * formatting codes, it will be split into different parts accordingly + * - A [TextComponent], whose parts will be appended in sequence to this + * [TextComponent]'s list of parts + * - A [Text] object, which acts as a single part + * - A JS object, which must contain a "text" key, and can optionally contain + * any of the [Style] keys * - * @param component the [Text] to convert + * @see Style */ - constructor(component: Text) : this(component.copy()) + constructor(vararg parts: Any) : this(parts.flatMap(Part::of).toMutableList()) /** - * Creates a [TextComponent] from an existing [Text] instance. - * - * @param component the [Text] to convert + * Returns the text of all parts concatenated without formatting codes. */ - constructor(component: MutableText) { - this.component = component - text = formattedText - - val clickEvent = component.style.clickEvent - if (clickEvent != null) { - clickAction = clickEvent.action - clickValue = clickEvent.value - } - - val hoverEvent = component.style.hoverEvent - if (hoverEvent != null) { - hoverAction = hoverEvent.action - hoverValue = hoverEvent.getValue(hoverAction) - } + val unformattedText by lazy { + parts.fold("") { prev, curr -> prev + curr.text } } /** - * Gets the component text - */ - fun getText() = text - - /** - * Sets the component text + * Returns the text of all parts concatenated with formatting codes. */ - fun setText(value: String) = apply { - text = value - reInstance() + val formattedText by lazy { + parts.fold("") { prev, curr -> prev + curr.style_.formatCodes() + curr.text } } /** - * Whether this component is formatted. A formatted component interprets - * color codes (using both & and §) and applies them as style. - */ - fun isFormatted() = formatted - - /** - * Sets whether this component is formatted. A formatted component interprets - * color codes (using both & and §) and applies them as style. - */ - fun setFormatted(value: Boolean) = apply { - formatted = value - reInstance() - } - - /** - * Gets the action to be performed when the component is clicked on. See [setClickAction] - * for possible values. - */ - fun getClickAction() = clickAction - - /** - * Sets the action to be performed when the component is clicked on. Possible actions include: - * - open_url - * - open_file - * - run_command - * - suggest_command - * - change_page + * Get the chat line ID of this message, if it exists. The chat line can be used + * to easily edit or delete a message later via [ChatLib.editChat] and + * [ChatLib.deleteChat]. * - * @param value The new click action, can be a [ClickEvent.Action], [String], or null + * @return the chat line ID of the message, or -1 if this [TextComponent] does + * not have an associated chat line ID. */ - fun setClickAction(value: Any?) = apply { - clickAction = when (value) { - is ClickEvent.Action -> value - is String -> ClickEvent.Action.valueOf(value.uppercase()) - null -> null - else -> error( - "TextComponent.setClickAction() expects a String, ClickEvent.Action, or null, but got " + - value::class - ) - } - - reInstanceClick() - } + fun getChatLineId() = chatLineId /** - * The value to be used by the click action. The value is interpreted according to [clickAction] + * @return a new [TextComponent] with the given chat line id */ - fun getClickValue() = clickValue + @JvmOverloads + fun withChatLineId(id: Int = ThreadLocalRandom.current().nextInt()) = copy(chatLineId = id) /** - * Sets the value to be used by the click action. The value is interpreted according to [clickAction]. - */ - fun setClickValue(value: String?) = apply { - clickValue = value - reInstanceClick() - } - - /** - * Sets the click action and value of the component.See [clickAction] for - * possible click actions. + * If this [TextComponent] is recursive, sending this instance (via [chat] or + * [actionBar]) may trigger other `chat` triggers as if it had been received by + * the server. [TextComponent]s are non-recursive by default. * - * @param action the click action - * @param value the click value + * @return true if the message can trigger other triggers. */ - fun setClick(action: ClickEvent.Action?, value: String?) = apply { - clickAction = action - clickValue = value - reInstanceClick() - } + fun isRecursive(): Boolean = isRecursive /** - * Sets the click action and value of the component.See [clickAction] for - * possible click actions. + * Sets whether the message can trigger other triggers. * - * @param action the click action as a [String] - * @param value the click value + * @param recursive true if message can trigger other triggers. */ - fun setClick(action: String, value: String?) = apply { - setClick(ClickEvent.Action.valueOf(action.uppercase()), value) - } + @JvmOverloads + fun withRecursive(recursive: Boolean = true) = copy(isRecursive = recursive) /** - * Gets the action to be performed when the component is hovered. See [setHoverAction] - * for possible values + * @return a new [TextComponent] with the specified [value] appended to the end. + * This accepts all types of objects that the vararg constructor does. */ - fun getHoverAction() = hoverAction + fun withText(value: Any) = copy(parts = (parts + Part.of(value)).toMutableList()) /** - * Sets the action to be performed when the component is hovered. Possible actions include: - * - show_text - * - show_item - * - show_entity - * - * @param value The new hover action, can be a [HoverEvent.Action], [String], or null + * @return a new [TextComponent] with the specified [value] inserted at [index]. + * This accepts all types of objects that the vararg constructor does. */ - fun setHoverAction(value: Any?) = apply { - hoverAction = when (value) { - is HoverEvent.Action<*> -> value - is String -> hoverEventActionByName(value.lowercase()) - null -> null - else -> error( - "TextComponent.setHoverAction() expects a String, HoverEvent.Action, or null, but got " + - value::class - ) - } - - // Trigger re-wrapping if necessary - setHoverValue(hoverValue) - } + fun withTextAt(index: Int, value: Any) = + copy(parts = (parts.take(index) + Part.of(value) + parts.drop(index)).toMutableList()) /** - * Gets the value to be used by the hover action. The value is interpreted according to [hoverAction] + * @return a new [TextComponent] without the part at [index] */ - fun getHoverValue() = hoverValue + fun withoutTextAt(index: Int) = + copy(parts = (parts.take(index) + parts.drop(index + 1)).toMutableList()) /** - * Sets the value to be used by the hover action. The value is interpreted according to [hoverAction] + * Edits this text component, replacing it with the given [newText]. Note that + * this compares [TextComponent]s based on [formattedText]; if an exact match + * is needed, use [ChatLib.editChat] in conjunction with a chat line ID. */ - fun setHoverValue(value: Any?) = apply { - hoverValue = value?.let { - when (hoverAction) { - HoverEvent.Action.SHOW_TEXT -> from(it) - HoverEvent.Action.SHOW_ITEM -> parseItemContent(it) - HoverEvent.Action.SHOW_ENTITY -> parseEntityContent(it) - else -> value - } - } - - reInstanceHover() + fun edit(newText: TextComponent) = apply { + ChatLib.editChat(this, newText) } /** - * Sets the hover action and value of the component. See [hoverAction] for possible hover actions. - * - * @param action the hover action - * @param value the hover value + * Edits this text component, replacing it with a new [TextComponent] from the + * given [parts]. Note that this compares [TextComponent]s based on + * [formattedText]; if an exact match is needed, use [ChatLib.editChat] in + * conjunction with a chat line ID. */ - fun setHover(action: HoverEvent.Action<*>?, value: Any?) = apply { - hoverAction = action - setHoverValue(value) - } + fun edit(vararg parts: Any) = edit(TextComponent(*parts)) /** - * Sets the hover action and value of the component. See [hoverAction] for possible hover actions. - * - * @param action the hover action as a [String] - * @param value the hover value + * Deletes this text component. Note that this compares [TextComponent]s based on + * [formattedText]; if an exact match is needed, use [ChatLib.editChat] in conjunction + * with a chat line ID. */ - fun setHover(action: String, value: Any?) = apply { - setHover(hoverEventActionByName(action.lowercase()), value) + fun delete() = apply { + ChatLib.deleteChat(this) } /** - * Sets the color of this [TextComponent] - * This won't override your color codes unless &r is explicitly used. + * Sends this [TextComponent] to the players chat. * - * @param color RGB value acquired using [Renderer.getColor]. Alpha values will be ignored - */ - fun setColor(color: Long) = apply { - component.setStyle(component.style.withColor(color.toInt())); - } - - /** - * Sets the color of this [TextComponent] - * This won't override your color codes unless &r is explicitly used. + * Note that this is purely client-side, and will not be sent to the server. If [isRecursive], + * will trigger any matching `chat` triggers * - * @param red value between 0 and 255 - * @param green value between 0 and 255 - * @param blue value between 0 and 255 - */ - fun setColor(red: Int, green: Int, blue: Int) = setColor(Renderer.getColor(red, green, blue)) - - /** - * Shows the component in chat as a new [Message] + * @see ChatLib.chat + * @see ChatLib.say */ fun chat() = apply { - Message(this).chat() + if (Player.toMC() == null) + return@apply + + if (chatLineId != -1) { + ChatLib.sendMessageWithId(this) + return@apply + } + + if (isRecursive) { + Client.scheduleTask { + Client.getMinecraft().networkHandler?.onGameMessage(GameMessageS2CPacket(this, false)) + } + } else { + Player.toMC()?.sendMessage(this) + } } /** - * Shows the component on the actionbar as a new [Message] + * Sends this [TextComponent] to the players action bar. + * + * If [isRecursive], will trigger any matching `actionBar` triggers + * + * @see ChatLib.actionBar */ fun actionBar() = apply { - Message(this).actionBar() - } + if (Player.toMC() == null) + return@apply - override fun toString() = "TextComponent(${if (formatted) formattedText else unformattedText})" - - private fun parseItemContent(obj: Any): HoverEvent.ItemStackContent { - return when (obj) { - is ItemStack -> obj - is Item -> obj.toMC() - is String -> ItemType(obj).asItem().toMC() - is HoverEvent.ItemStackContent -> return obj - else -> error("${obj::class} cannot be parsed as an item HoverEvent") - }.let(HoverEvent::ItemStackContent) - } - - private fun parseEntityContent(obj: Any): HoverEvent.EntityContent? { - return when (obj) { - is MCEntity -> obj - is Entity -> obj.toMC() - //#if MC>=12004 - is String -> return HoverEvent.EntityContent.legacySerializer(from(obj)).getOrThrow(false) {} - //#else - //$$ is String -> return HoverEvent.EntityContent.parse(from(obj)) - //#endif - is HoverEvent.EntityContent -> return obj - else -> error("${obj::class} cannot be parsed as an entity HoverEvent") - }.let { HoverEvent.EntityContent(it.type, it.uuid, it.name) } + if (isRecursive) { + Client.scheduleTask { + Client.getMinecraft().networkHandler?.onGameMessage(GameMessageS2CPacket(this, true)) + } + } else { + Player.toMC()?.sendMessage(this, true) + } } - private fun reInstance() { - component = Text.literal(text.formatIf(formatted)) + override fun toString() = formattedText - reInstanceClick() - reInstanceHover() + internal fun toMutableText() = Text.empty().apply { + parts.forEach(::append) } - private fun reInstanceClick() { - if (clickAction == null || clickValue == null) - return + // Make this method manually to avoid exposing it as a public API + private fun copy( + parts: MutableList = this.parts, + chatLineId: Int = this.chatLineId, + isRecursive: Boolean = this.isRecursive + ) = TextComponent(parts, chatLineId, isRecursive) - val event = ClickEvent(clickAction, clickValue!!.formatIf(formatted)) - component.style = component.style.withClickEvent(event) - } + ////////// + // Text // + ////////// - private fun reInstanceHover() { - if (hoverAction == null || hoverValue == null) - return + override fun getContent(): TextContent = parts[0] - @Suppress("UNCHECKED_CAST") - val event = HoverEvent(hoverAction as HoverEvent.Action, hoverValue!!) - component.style = component.style.withHoverEvent(event) - } + override fun getString(): String = parts[0].text - private fun String.formatIf(predicate: Boolean) = if (predicate) UChat.addColor(this) else this + override fun getStyle(): Style = parts[0].style_ - private class TextBuilder(private val isFormatted: Boolean) : CharacterVisitor { - private val builder = StringBuilder() - private var cachedStyle: Style? = null + override fun getSiblings(): MutableList = parts.drop(1).toMutableList() - override fun accept(index: Int, style: Style, codePoint: Int): Boolean { - if (isFormatted && style != cachedStyle) { - cachedStyle = style - builder.append(formatString(style)) + override fun asOrderedText(): OrderedText = OrderedText { visitor -> + var i = 0 + parts.all { + it.text.codePoints().toList().all { cp -> + visitor.accept(i++, style, cp) } - - builder.append(codePoint.toChar()) - return true } + } - fun getString() = builder.toString() - - private fun formatString(style: Style): String { - val builder = StringBuilder("§r") - - when { - style.isBold -> builder.append("§l") - style.isItalic -> builder.append("§o") - style.isUnderlined -> builder.append("§n") - style.isStrikethrough -> builder.append("§m") - style.isObfuscated -> builder.append("§k") + ///////////// + // List // + ///////////// + + override val size by parts::size + + override fun contains(element: NativeObject) = parts.any { ScriptRuntime.eq(element, it.nativeObject) } + + override fun containsAll(elements: Collection) = elements.all(::contains) + + override fun get(index: Int) = parts[index].nativeObject + + override fun indexOf(element: NativeObject) = parts.indexOfFirst { it.nativeObject == element } + + override fun isEmpty() = parts.isEmpty() + + override fun iterator() = parts.map(Part::nativeObject).iterator() + + override fun listIterator() = parts.map(Part::nativeObject).listIterator() + + override fun listIterator(index: Int) = parts.map(Part::nativeObject).listIterator(index) + + override fun lastIndexOf(element: NativeObject) = parts.indexOfLast { it.nativeObject == element } + + override fun spliterator() = parts.map(Part::nativeObject).spliterator() + + override fun subList(fromIndex: Int, toIndex: Int): List = + parts.subList(fromIndex, toIndex).map(Part::nativeObject) + + private class Part(val text: String, val style_: Style) : Text, TextContent { + val nativeObject: NativeObject by lazy { + val cx = Context.getContext() + cx.newObject(cx.topCallScope).also { + it.put("text", it, text) + if (style_.color != null) + it.put("color", it, style_.color) + if (style_.isBold) + it.put("bold", it, true) + if (style_.isItalic) + it.put("italic", it, true) + if (style_.isUnderlined) + it.put("underline", it, true) + if (style_.isStrikethrough) + it.put("strikethrough", it, true) + if (style_.isObfuscated) + it.put("obfuscated", it, true) + style_.clickEvent?.let { event -> + if (event.action != null) { + //#if MC>=12004 + it.put("clickAction", it, event.action.asString()) + //#else + //$$ it.put("clickAction", it, event.action.name) + //#endif + if (event.value != null) + it.put("clickValue", it, event.value) + } + } + style_.hoverEvent?.let { event -> + if (event.action != null) { + //#if MC>=12004 + it.put("hoverAction", it, event.action.asString()) + //#else + //$$ it.put("hoverAction", it, event.action.name) + //#endif + event.getValue(event.action!!)?.let { value -> + it.put("hoverValue", it, value) + } + } + } + if (style_.insertion != null) + it.put("insertion", it, style_.insertion) + if (style_.font != null && style_.font.toString() != "minecraft:default") + it.put("font", it, style_.font) } + } - style.color?.let(colorToFormatChar::get)?.run(builder::append) + override fun getContent(): TextContent = this - return builder.toString() - } + override fun getString(): String = text - companion object { - private val colorToFormatChar = Formatting.values().mapNotNull { format -> - TextColor.fromFormatting(format)?.let { it to format } - }.toMap() - } - } + override fun getStyle(): Style = style_ - // ********************** - // * METHOD DELEGATIONS * - // ********************** + override fun getSiblings(): MutableList = mutableListOf() - override fun getContent(): TextContent = component.content + override fun visit(visitor: StringVisitable.Visitor): Optional = visitor.accept(text) - val unformattedText: String - get() { - val builder = TextBuilder(false) - component.asOrderedText().accept(builder) - return builder.getString() + override fun visit(visitor: StringVisitable.StyledVisitor, style: Style): Optional { + return visitor.accept(this.style_, text) } - val formattedText: String - get() { - val builder = TextBuilder(true) - component.asOrderedText().accept(builder) - return builder.getString() - } + override fun asTruncatedString(length: Int): String = text.take(length) - fun appendSibling(text: Text): MutableText = component.append(text) + override fun asOrderedText(): OrderedText = OrderedText { visitor -> + text.codePoints().toList().withIndex().all { (index, cp) -> + visitor.accept(index, style_, cp) + } + } - fun append(text: Text) = appendSibling(text) + //#if MC>=12004 + override fun getType(): TextContent.Type<*> = TextContent.Type(CODEC, "ctjs_part") + //#endif - override fun getString(): String = component.string + companion object { + //#if MC>=12004 + private val CODEC: MapCodec = RecordCodecBuilder.mapCodec { builder -> + builder.group( + Codec.STRING.fieldOf("text").forGetter(Part::text), + net.minecraft.text.Style.Codecs.CODEC.fieldOf("style").forGetter { it.style_ }, + ).apply(builder) { text, style -> Part(text, style) } + } + //#endif - override fun getStyle(): Style = component.style + fun of(obj: Any) = when (obj) { + is NativeObject -> { + val text = obj["text"] + ?: throw IllegalArgumentException("Expected TextComponent part to have a \"text\" key") + require(text is String) { "TextComponent part's \"text\" key must be a string" } + listOf(Part(UChat.addColor(text), jsObjectToStyle(obj))) + } + is Part -> listOf(obj) + is TextComponent -> obj.parts + is Text -> listOf(Part(obj.string, obj.style)) + is String -> { + val parts = mutableListOf() + val builder = StringBuilder() + var lastStyle = Style.EMPTY + + TextVisitFactory.visitFormatted(UChat.addColor(obj), 0, Style.EMPTY) { _, style, cp -> + if (style != lastStyle) { + parts.add(Part(builder.toString(), lastStyle)) + lastStyle = style + builder.clear() + } + builder.appendCodePoint(cp) + true + } + + if (builder.isNotEmpty()) + parts.add(Part(builder.toString(), lastStyle)) + + parts + } + else -> throw IllegalArgumentException("Cannot convert ${obj::class.simpleName} to TextComponent part") + } + } + } - override fun getSiblings(): MutableList = component.siblings + companion object { + private val colorToFormatChar = Formatting.values().mapNotNull { format -> + TextColor.fromFormatting(format)?.let { it to format } + }.toMap() + + fun jsObjectToStyle(obj: NativeObject): Style { + return Style.EMPTY + .withColor(obj["color"]?.let { color -> + when (color) { + is TextColor -> color + is Formatting -> TextColor.fromFormatting(color) + is Int -> TextColor.fromRgb(color) + //#if MC>=12004 + is String -> TextColor.parse(color).result().orElseThrow { + IllegalArgumentException("Could not parse \"$color\" as a text color") + } + //#else + //$$ is String -> TextColor.parse(color) ?: throw IllegalArgumentException("Could not parse \"$color\" as a text color") + //#endif + else -> throw IllegalArgumentException("Could not convert type ${color::class.simpleName} to a text color") + } + }) + .withBold( + obj.getOrDefault("bold", false) as? Boolean + ?: error("Expected \"bold\" key to be a boolean") + ) + .withItalic( + obj.getOrDefault("italic", false) as? Boolean + ?: error("Expected \"italic\" key to be a boolean") + ) + .withUnderline( + obj.getOrDefault("underline", false) as? Boolean + ?: error("Expected \"underline\" key to be a boolean") + ) + .withStrikethrough( + obj.getOrDefault("strikethrough", false) as? Boolean + ?: error("Expected \"strikethrough\" key to be a boolean") + ) + .withObfuscated( + obj.getOrDefault("obfuscated", false) as? Boolean + ?: error("Expected \"obfuscated\" key to be a boolean") + ) + .withClickEvent( + makeClickEvent( + obj["clickAction"], + when (val clickValue = obj["clickValue"]) { + null -> null + is String -> clickValue + else -> error("Expected \"clickValue\" key to be a string") + } + ) + ) + .withHoverEvent(makeHoverEvent(obj["hoverAction"], obj["hoverValue"])) + .withInsertion( + when (val insertion = obj["insertion"]) { + null -> null + is String -> insertion + else -> error("Expected \"insertion\" key to be a String") + } + ) + .withFont( + when (val font = obj["font"]) { + null -> null + is String -> font.toIdentifier() + else -> error("Expected \"font\" key to be a String") + } + ) + } - override fun visit(styledVisitor: StringVisitable.StyledVisitor?, style: Style?): Optional = - component.visit(styledVisitor, style) + private fun Style.formatCodes() = buildString { + append("§r") - override fun visit(visitor: StringVisitable.Visitor?): Optional = component.visit(visitor) + when { + isBold -> append("§l") + isItalic -> append("§o") + isUnderlined -> append("§n") + isStrikethrough -> append("§m") + isObfuscated -> append("§k") + } - override fun asTruncatedString(length: Int): String = component.asTruncatedString(length) + color?.let(colorToFormatChar::get)?.run(::append) + } - override fun copyContentOnly(): MutableText = component.copyContentOnly() + private fun makeClickEvent(action: Any?, value: String?): ClickEvent? { + val clickAction = when (action) { + is ClickEvent.Action -> action + is String -> ClickEvent.Action.valueOf(action.uppercase()) + null -> if (value != null) { + error("Cannot set Style's click value without a click action") + } else return null + else -> error("Style.withClickAction() expects a String, ClickEvent.Action, or null, but got ${action::class.simpleName}") + } - override fun copy(): MutableText = component.copy() + return ClickEvent(clickAction, value.orEmpty()) + } - override fun asOrderedText(): OrderedText = component.asOrderedText() + private fun makeHoverEvent(action: Any?, value: Any?): HoverEvent? { + val hoverAction = when (action) { + is HoverEvent.Action<*> -> action + is String -> hoverEventActionByName(action) + null -> if (value != null) { + error("Cannot set Style's hover value without a hover action") + } else return null + else -> error("Style.withHoverAction() expects a String, HoverEvent.Action, or null, but got ${action::class.simpleName}") + } - override fun withoutStyle(): MutableList = component.withoutStyle() + if (value == null) + return HoverEvent(hoverAction, null) - override fun getWithStyle(style: Style?): MutableList = component.getWithStyle(style) + val hoverValue: Any? = when (hoverAction) { + HoverEvent.Action.SHOW_TEXT -> TextComponent(value) + HoverEvent.Action.SHOW_ITEM -> parseItemContent(value) + HoverEvent.Action.SHOW_ENTITY -> parseEntityContent(value) + else -> error("unreachable") + } - override fun contains(text: Text?): Boolean = component.contains(text) + @Suppress("UNCHECKED_CAST") + return HoverEvent(hoverAction as HoverEvent.Action, hoverValue) + } - companion object { - fun from(obj: Any): TextComponent? { + private fun parseItemContent(obj: Any): HoverEvent.ItemStackContent { return when (obj) { - is TextComponent -> obj - is String -> TextComponent(obj) - is Text -> TextComponent(obj) - else -> null - } + is ItemStack -> obj + is Item -> obj.toMC() + is String -> ItemType(obj).asItem().toMC() + is HoverEvent.ItemStackContent -> return obj + else -> error("${obj::class} cannot be parsed as an item HoverEvent") + }.let(HoverEvent::ItemStackContent) } - fun stripFormatting(string: String): String { - return Formatting.strip(string)!! + private fun parseEntityContent(obj: Any): HoverEvent.EntityContent? { + return when (obj) { + is MCEntity -> obj + is Entity -> obj.toMC() + //#if MC>=12004 + is String -> return HoverEvent.EntityContent.legacySerializer(TextComponent(obj)) + .getOrThrow(false) {} + //#else + //$$ is String -> return HoverEvent.EntityContent.parse(TextComponent(obj)) + //#endif + is HoverEvent.EntityContent -> return obj + else -> error("${obj::class} cannot be parsed as an entity HoverEvent") + }.let { HoverEvent.EntityContent(it.type, it.uuid, it.name) } } } } diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/world/Server.kt b/src/main/kotlin/com/chattriggers/ctjs/api/world/Server.kt index 7f0d6b22..cc80ae9a 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/world/Server.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/world/Server.kt @@ -50,7 +50,7 @@ object Server { if (isSingleplayer()) return "SinglePlayer" - return toMC()?.label?.let(::TextComponent)?.formattedText ?: "" + return toMC()?.label?.let { TextComponent(it) }?.formattedText ?: "" } // TODO(breaking): Return -1 if not in a world diff --git a/src/main/kotlin/com/chattriggers/ctjs/api/world/TabList.kt b/src/main/kotlin/com/chattriggers/ctjs/api/world/TabList.kt index 8719f0c7..b37e41f8 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/api/world/TabList.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/api/world/TabList.kt @@ -75,7 +75,7 @@ object TabList { } @JvmStatic - fun getHeaderComponent() = toMC()?.asMixin()?.header?.let(::TextComponent) + fun getHeaderComponent() = toMC()?.asMixin()?.header?.let { TextComponent(it) } @JvmStatic fun getHeader() = getHeaderComponent()?.formattedText @@ -100,7 +100,7 @@ object TabList { fun clearHeader() = setHeader(null) @JvmStatic - fun getFooterComponent() = toMC()?.asMixin()?.footer?.let(::TextComponent) + fun getFooterComponent() = toMC()?.asMixin()?.footer?.let { TextComponent(it) } @JvmStatic fun getFooter() = getFooterComponent()?.formattedText diff --git a/src/main/kotlin/com/chattriggers/ctjs/internal/commands/CTCommand.kt b/src/main/kotlin/com/chattriggers/ctjs/internal/commands/CTCommand.kt index 25f9de17..c3d660b1 100644 --- a/src/main/kotlin/com/chattriggers/ctjs/internal/commands/CTCommand.kt +++ b/src/main/kotlin/com/chattriggers/ctjs/internal/commands/CTCommand.kt @@ -4,12 +4,10 @@ import com.chattriggers.ctjs.CTJS import com.chattriggers.ctjs.api.Config import com.chattriggers.ctjs.api.client.Client import com.chattriggers.ctjs.api.message.ChatLib -import com.chattriggers.ctjs.api.message.Message import com.chattriggers.ctjs.api.message.TextComponent import com.chattriggers.ctjs.internal.commands.StaticCommand.Companion.onExecute import com.chattriggers.ctjs.engine.Console import com.chattriggers.ctjs.engine.printTraceToConsole -import com.chattriggers.ctjs.internal.engine.JSLoader import com.chattriggers.ctjs.internal.engine.module.ModuleManager import com.chattriggers.ctjs.internal.engine.module.ModulesGui import com.chattriggers.ctjs.internal.listeners.ClientListener @@ -29,9 +27,11 @@ import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallba import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.minecraft.text.ClickEvent import net.minecraft.text.HoverEvent +import net.minecraft.text.Text import net.minecraft.util.Util import java.io.File import java.io.IOException +import java.lang.IllegalArgumentException import kotlin.concurrent.thread internal object CTCommand : Initializer { @@ -177,21 +177,19 @@ internal object CTCommand : Initializer { val messages = type.messageList() val toDump = lines.coerceAtMost(messages.size) - Message("&6&m${ChatLib.getChatBreak()}").setChatLineId(idFixed).chat() + TextComponent("&6&m${ChatLib.getChatBreak()}").withChatLineId(idFixed).chat() for (i in 0 until toDump) { val msg = ChatLib.replaceFormatting(messages[messages.size - toDump + i].formattedText) - Message( - TextComponent(msg) - .setClick(ClickEvent.Action.COPY_TO_CLIPBOARD, msg) - .setHover(HoverEvent.Action.SHOW_TEXT, TextComponent("&eClick here to copy this message.")) - .setFormatted(true) - ).setFormatted(false) - .setChatLineId(idFixed + i + 1) + TextComponent(Text.literal(msg).styled { + it.withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, msg)) + .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent("&eClick here to copy this message."))) + }) + .withChatLineId(idFixed + i + 1) .chat() } - Message("&6&m${ChatLib.getChatBreak()}").setChatLineId(idFixed + lines + 1).chat() + TextComponent("&6&m${ChatLib.getChatBreak()}").withChatLineId(idFixed + lines + 1).chat() idFixedOffset = idFixed + lines + 1 } diff --git a/src/main/resources/assets/chattriggers/js/moduleProvidedLibs.js b/src/main/resources/assets/chattriggers/js/moduleProvidedLibs.js index 2ecbbaf6..ff453ab8 100644 --- a/src/main/resources/assets/chattriggers/js/moduleProvidedLibs.js +++ b/src/main/resources/assets/chattriggers/js/moduleProvidedLibs.js @@ -77,7 +77,6 @@ loadClass("com.chattriggers.ctjs.api.inventory.Slot"); loadClass("com.chattriggers.ctjs.api.message.ChatLib"); - loadClass("com.chattriggers.ctjs.api.message.Message"); loadClass("com.chattriggers.ctjs.api.message.TextComponent"); loadClass("com.chattriggers.ctjs.api.render.Book");