diff --git a/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt b/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt index d08956759..565e4ce71 100644 --- a/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt +++ b/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt @@ -1,7 +1,7 @@ package io.github.homchom.recode import io.github.homchom.recode.ui.sendSystemToast -import io.github.homchom.recode.ui.translateText +import io.github.homchom.recode.ui.text.translatedText import kotlinx.coroutines.* import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Executor @@ -42,8 +42,8 @@ object RecodeDispatcher : CoroutineContext { private val delegate = dispatcher + CoroutineExceptionHandler { _, exception -> mc.sendSystemToast( - translateText("recode.uncaught_exception.toast.title"), - translateText("recode.uncaught_exception.toast") + translatedText("recode.uncaught_exception.toast.title"), + translatedText("recode.uncaught_exception.toast") ) runOnMinecraftThread { val thread = Thread.currentThread() diff --git a/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt b/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt index cbcd7334b..34ff92da4 100644 --- a/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt +++ b/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt @@ -6,7 +6,7 @@ import io.github.homchom.recode.event.Listenable import io.github.homchom.recode.event.Requester import io.github.homchom.recode.event.createEvent import io.github.homchom.recode.ui.sendSystemToast -import io.github.homchom.recode.ui.translateText +import io.github.homchom.recode.ui.text.translatedText import io.github.homchom.recode.util.computeNullable import io.github.homchom.recode.util.coroutines.cancelAndLog import kotlinx.coroutines.* @@ -200,8 +200,8 @@ private class TrialRequester( response } catch (timeout: TimeoutCancellationException) { mc.sendSystemToast( - translateText("multiplayer.recode.request_timeout.toast.title"), - translateText("multiplayer.recode.request_timeout.toast") + translatedText("multiplayer.recode.request_timeout.toast.title"), + translatedText("multiplayer.recode.request_timeout.toast") ) logError("${debugString(input, hidden)} timed out after $timeoutDuration") throw timeout diff --git a/src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt b/src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt index 31107ebc4..9a0950523 100644 --- a/src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt +++ b/src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt @@ -182,7 +182,7 @@ class TrialScope @DelicateCoroutinesApi constructor( } /** - * Returns a non-null [TestResult.value] or fails the trial. + * @return a non-null [TestResult.value] or fails the trial. */ operator fun TestResult.unaryPlus() = value ?: fail() @@ -194,12 +194,12 @@ class TrialScope @DelicateCoroutinesApi constructor( fun fail(): Nothing = nullableScope.fail() /** - * Returns an instant [TrialResult] with [value]. Use this when a trial does not end asynchronously. + * @return an instant [TrialResult] with [value]. Use this when a trial does not end asynchronously. */ fun instant(value: R?) = TrialResult(value) /** - * Returns the asynchronous [TrialResult] of [block] ran in its own [TrialScope]. + * @return the asynchronous [TrialResult] of [block] ran in its own [TrialScope]. */ fun suspending(block: suspend TrialScope.() -> R?) = TrialResult(block, coroutineScope, hidden) diff --git a/src/main/java/io/github/homchom/recode/feature/social/MessageStacking.kt b/src/main/java/io/github/homchom/recode/feature/social/MessageStacking.kt index d59d22397..b353d2c02 100644 --- a/src/main/java/io/github/homchom/recode/feature/social/MessageStacking.kt +++ b/src/main/java/io/github/homchom/recode/feature/social/MessageStacking.kt @@ -4,7 +4,10 @@ package io.github.homchom.recode.feature.social import io.github.homchom.recode.MOD_NAME import io.github.homchom.recode.render.ColorPalette -import io.github.homchom.recode.ui.text +import io.github.homchom.recode.ui.text.literalText +import io.github.homchom.recode.ui.text.style +import io.github.homchom.recode.ui.text.toVanilla +import io.github.homchom.recode.ui.text.translatedText import io.github.homchom.recode.util.regex.regex import net.minecraft.client.GuiMessageTag @@ -18,9 +21,11 @@ private val stackRegex = regex { fun stackedMessageTag(amount: Int) = GuiMessageTag( ColorPalette.AQUA.hex, GuiMessageTag.Icon.CHAT_MODIFIED, - text { - color(aqua) { translate("chat.tag.recode.stacked", amount) } - }, + translatedText( + "chat.tag.recode.stacked", + style().aqua(), + arrayOf(literalText(amount)) + ).toVanilla(), "$stackTagPrefix$amount" ) diff --git a/src/main/java/io/github/homchom/recode/feature/social/SideChat.kt b/src/main/java/io/github/homchom/recode/feature/social/SideChat.kt index 71d0f1e5d..afd432e36 100644 --- a/src/main/java/io/github/homchom/recode/feature/social/SideChat.kt +++ b/src/main/java/io/github/homchom/recode/feature/social/SideChat.kt @@ -42,7 +42,10 @@ class SideChat(private val mc: Minecraft) : ChatComponent(mc) { private fun tailWidth(scale: Double) = (12 * scale).toInt() } +/** + * A duck interface applied to [net.minecraft.client.gui.Gui]. + */ @Suppress("FunctionName") -interface MCGuiWithSideChat { +interface DGuiWithSideChat { fun `recode$getSideChat`(): SideChat } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/feature/visual/BuiltInResourcePacks.kt b/src/main/java/io/github/homchom/recode/feature/visual/BuiltInResourcePacks.kt index b3ecd26ff..3919eec10 100644 --- a/src/main/java/io/github/homchom/recode/feature/visual/BuiltInResourcePacks.kt +++ b/src/main/java/io/github/homchom/recode/feature/visual/BuiltInResourcePacks.kt @@ -6,7 +6,9 @@ import io.github.homchom.recode.feature.feature import io.github.homchom.recode.id import io.github.homchom.recode.render.IntegralColor import io.github.homchom.recode.render.toColor -import io.github.homchom.recode.ui.text +import io.github.homchom.recode.ui.text.style +import io.github.homchom.recode.ui.text.text +import io.github.homchom.recode.ui.text.toVanilla import net.fabricmc.fabric.api.resource.ResourceManagerHelper import net.fabricmc.fabric.api.resource.ResourcePackActivationType @@ -24,8 +26,8 @@ private fun registerBuiltInResourcePack( Recode, text { literal("[$MOD_ID] ") - color(displayColor) { translate("resourcePack.recode.$id") } - }, + translate("resourcePack.recode.$id", style().color(displayColor)) + }.toVanilla(), activationType ) } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/feature/visual/ExpressionHighlighting.kt b/src/main/java/io/github/homchom/recode/feature/visual/ExpressionHighlighting.kt new file mode 100644 index 000000000..19ba71d99 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/feature/visual/ExpressionHighlighting.kt @@ -0,0 +1,220 @@ +package io.github.homchom.recode.feature.visual + +import io.github.homchom.recode.hypercube.CommandAliasGroup +import io.github.homchom.recode.hypercube.DFValueMeta +import io.github.homchom.recode.hypercube.dfMiniMessage +import io.github.homchom.recode.hypercube.dfValueMeta +import io.github.homchom.recode.ui.text.* +import io.github.homchom.recode.util.regex.regex +import net.kyori.adventure.text.BuildableComponent +import net.kyori.adventure.text.TextComponent +import net.minecraft.util.FormattedCharSequence +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack + +data class HighlightedExpression(val text: FormattedCharSequence, val preview: FormattedCharSequence?) + +/** + * An object that highlights and caches DF value and MiniMessage expressions. + * + * @see runHighlighting + */ +class ExpressionHighlighter { + private val codes = setOf( + "default", + "selected", + "uuid", + "var", + "math", + "damager", + "killer", + "shooter", + "victim", + "projectile", + "random", + "round", + "index", + "entry" + ) + + private val highlightedCommands = buildList { + fun addGroup( + group: CommandAliasGroup, + highlightedArgumentIndex: Int = 0, + hasCount: Boolean = false, + parseMiniMessage: Boolean = true + ) { + addAll(group.map { prefix -> + CommandInfo(prefix, highlightedArgumentIndex, hasCount, parseMiniMessage) + }) + } + + addGroup(CommandAliasGroup.NUMBER, hasCount = true, parseMiniMessage = false) + addGroup(CommandAliasGroup.STRING, hasCount = true, parseMiniMessage = false) + addGroup(CommandAliasGroup.TEXT, hasCount = true) + addGroup(CommandAliasGroup.VARIABLE, hasCount = true, parseMiniMessage = false) + addGroup(CommandAliasGroup.ITEM_NAME) + addGroup(CommandAliasGroup.ITEM_LORE_ADD) + addGroup(CommandAliasGroup.ITEM_LORE_SET, highlightedArgumentIndex = 1) + addGroup(CommandAliasGroup.PLOT_NAME) + addGroup(CommandAliasGroup.RELORE) + } + + private data class CommandInfo( + val prefix: String, + val highlightedArgumentIndex: Int, + val hasCount: Boolean, + val parseMiniMessage: Boolean + ) + + // TODO: new color scheme? + private val colors = listOf( + 0xffd600, + 0x33ff00, + 0x00ffe0, + 0x5e77f7, + 0xca64fa, + 0xff4242 + ) + + private val codeRegex = regex { + group { + str("%") + any("a-zA-Z").oneOrMore() + str("(").optional() + } + or; str(")") + or; end + } + + private var cachedInput = "" + private var cachedHighlight = HighlightedExpression(FormattedCharSequence.EMPTY, null) + + private val countRegex = regex { + space + digit.oneOrMore() + end + } + + private fun leadingArgumentsRegex(highlightIndex: Int) = regex { + group { + none(" ").oneOrMore() + space + } * (highlightIndex - 1) + none(" ").oneOrMore() + space.optional() + } + + fun runHighlighting( + chatInput: String, + formatted: FormattedCharSequence, + player: Player + ): HighlightedExpression? { + if (cachedInput == chatInput) return cachedHighlight + + val highlight = highlight(chatInput, formatted, player.mainHandItem) + if (highlight != null) { + cachedInput = chatInput + cachedHighlight = highlight + } + return highlight + } + + private fun highlight( + chatInput: String, + formatted: FormattedCharSequence, + mainHandItem: ItemStack + ): HighlightedExpression? { + // highlight commands + if (chatInput.startsWith('/')) { + val command = highlightedCommands.firstNotNullOfOrNull { info -> + info.takeIf { chatInput.startsWith(info.prefix, 1) } + } ?: return null + return highlightCommand(chatInput, formatted, command) + } + + // highlight values + val valueMeta = mainHandItem.dfValueMeta() + if (valueMeta is DFValueMeta.Primitive || valueMeta is DFValueMeta.Variable) { + return highlightString(chatInput, valueMeta.type == "comp") + } + + return null + } + + private fun highlightString(string: String, parseMiniMessage: Boolean = true): HighlightedExpression { + val builder = TextBuilder() + var sliceStart = 0 + var depth = 0 + + for (match in codeRegex.findAll(string)) { + builder.literal(string.substring(sliceStart, match.range.first), styleAt(depth)) + + val code = match.value + if (code == ")") { + if (depth > 0) depth-- + } else depth++ + + val style = if (code.length > 1 && code.drop(1).removeSuffix("(") !in codes) { + style().red() + } else { + styleAt(depth) + } + builder.literal(string.substring(match.range), style) + + if (code.endsWith('(')) depth++ else { + if (depth > 0) depth-- + } + + sliceStart = match.range.last + 1 + } + + if (parseMiniMessage) builder.raw.mapChildren { text -> + if (text is TextComponent && text.style().isEmpty) { + MiniMessageHighlighter.highlight(text.content()) as BuildableComponent<*, *> + } else { + text + } + } + + val text = builder.build().toFormattedCharSequence(false) + val preview = if (parseMiniMessage) { + dfMiniMessage.deserialize(string).toFormattedCharSequence() + } else null + return HighlightedExpression(text, preview) + } + + private fun highlightCommand( + input: String, + formatted: FormattedCharSequence, + info: CommandInfo + ): HighlightedExpression? { + var startIndex = info.prefix.length + 2 + var endIndex = input.length + if (startIndex > input.lastIndex) return null + + if (info.highlightedArgumentIndex > 0) { + val regex = leadingArgumentsRegex(info.highlightedArgumentIndex) + val match = regex.find(input, startIndex) + if (match != null) { + startIndex = match.range.last + 1 + if (startIndex > input.lastIndex) return null + } + + } + if (info.hasCount) { + val match = countRegex.find(input, startIndex) + if (match != null) endIndex = match.range.first + } + + val highlighted = highlightString(input.substring(startIndex, endIndex), info.parseMiniMessage) + val combined = formatted.replaceRange(startIndex.. { + val loreTag = tag + ?.getCompoundOrNull("display") + ?.getListOrNull("Lore", Tag.TAG_STRING) + ?: return emptyList() + + return buildList(loreTag.size) { + for (index in 0.. CompoundTag.getNullable(key: String, getter: (String) -> R) = + if (contains(key)) getter(key) else null \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/hypercube/CommandAliasGroup.kt b/src/main/java/io/github/homchom/recode/hypercube/CommandAliasGroup.kt index 4fa2536df..fd79b99cf 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/CommandAliasGroup.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/CommandAliasGroup.kt @@ -3,14 +3,19 @@ package io.github.homchom.recode.hypercube /** * A group of server command aliases. Add here as needed. */ -// TODO: does this suck? remove if so -enum class CommandAliasGroup(vararg aliases: String) { - RENAME("rename", "i name", "item name"), - ITEM_LORE_ADD("addlore", "ila", "lore add", "i lore add", "item lore add"), - ITEM_LORE_SET("ils", "sll", "lore set", "i lore set", "item lore set", "setloreline"), - PLOT_NAME("p name", "plot name"); +enum class CommandAliasGroup(vararg aliases: String) : Set by aliases.toSet() { + // value items + NUMBER("number", "num"), + STRING("string", "str"), + TEXT("text", "txt", "styledtext", "stxt"), + VARIABLE("variable", "var"), - val aliases = aliases.toList() + // renaming + ITEM_NAME("item name", "i name", "rename"), + ITEM_LORE_ADD("item lore add", "i lore add", "i l add", "ila", "lore add", "addlore"), + ITEM_LORE_SET("item lore set", "i lore set", "i l set", "ils", "lore set", "setlore"), + PLOT_NAME("plot name", "p name"), - operator fun unaryPlus() = aliases + // legacy + RELORE("relore"); } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/hypercube/DFValueMeta.kt b/src/main/java/io/github/homchom/recode/hypercube/DFValueMeta.kt new file mode 100644 index 000000000..1d1adea14 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/hypercube/DFValueMeta.kt @@ -0,0 +1,63 @@ +package io.github.homchom.recode.hypercube + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonSyntaxException +import io.github.homchom.recode.game.getCompoundOrNull +import io.github.homchom.recode.game.getStringOrNull +import io.github.homchom.recode.game.lore +import io.github.homchom.recode.logError +import net.kyori.adventure.text.Component +import net.minecraft.world.item.ItemStack + +fun ItemStack.dfValueMeta(): DFValueMeta? { + val raw = tag + ?.getCompoundOrNull("PublicBukkitValues") + ?.getStringOrNull("hypercube:varitem") + ?: return null + + fun invalid(): Nothing? { + logError("Invalid or unrecognized DF value item metadata: $raw") + return null + } + + // TODO: improve the following by using kotlinx.serialization + // (if we decide to switch to FLK as an external dependency) + + val type: String + val data: JsonObject + try { + val json = JsonParser.parseString(raw).asJsonObject + type = json["id"]?.asString ?: return invalid() + data = json["data"]?.asJsonObject ?: return invalid() + } catch (e: JsonSyntaxException) { + return invalid() + } catch (e: IllegalStateException) { + return invalid() + } + + return when { + data.has("name") && data.size() == 1 -> { + val name = data["name"]?.asString ?: return invalid() + DFValueMeta.Primitive(type, name) + } + type == "var" -> { + val name = data["name"]?.asString ?: return invalid() + if (!data.has("scope")) return invalid() + DFValueMeta.Variable(name, lore().firstOrNull()) + } + else -> DFValueMeta.Compound(type, data) + } + } + +sealed interface DFValueMeta { + val type: String + + data class Primitive(override val type: String, val expression: String) : DFValueMeta + + data class Variable(val expression: String, val scope: Component?) : DFValueMeta { + override val type get() = "var" + } + + data class Compound(override val type: String, val data: JsonObject) : DFValueMeta +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/hypercube/HypercubeConstants.kt b/src/main/java/io/github/homchom/recode/hypercube/HypercubeConstants.kt index 55b44636d..0cff9390d 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/HypercubeConstants.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/HypercubeConstants.kt @@ -1,7 +1,9 @@ -@file:JvmName("ServerConstants") +@file:JvmName("HypercubeConstants") package io.github.homchom.recode.hypercube +import net.kyori.adventure.text.minimessage.MiniMessage + const val SERVER_ADDRESS = "mcdiamondfire.com" const val DIAMOND = '◆' @@ -12,4 +14,9 @@ const val BOOSTER_ARROW = '⏵' const val TOKEN_NOTCH_CHAR = '□' -const val LAGSLAYER_PREFIX = """[LagSlayer]""" \ No newline at end of file +const val LAGSLAYER_PREFIX = """[LagSlayer]""" + +/** + * A [MiniMessage] instance to match DiamondFire's MiniMessage behavior. + */ +val dfMiniMessage = MiniMessage.miniMessage() \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/hypercube/HypercubeEvents.kt b/src/main/java/io/github/homchom/recode/hypercube/HypercubeEvents.kt index 2ae99f093..9beb889fd 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/HypercubeEvents.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/HypercubeEvents.kt @@ -12,7 +12,7 @@ import io.github.homchom.recode.multiplayer.DisconnectFromServerEvent import io.github.homchom.recode.multiplayer.JoinServerEvent import io.github.homchom.recode.multiplayer.ReceiveChatMessageEvent import io.github.homchom.recode.multiplayer.username -import io.github.homchom.recode.ui.matchEntireUnstyled +import io.github.homchom.recode.ui.text.matchEntirePlain import io.github.homchom.recode.util.Case import io.github.homchom.recode.util.regex.regex import kotlinx.coroutines.flow.map @@ -36,7 +36,7 @@ val JoinDFDetector = detector("DF join", failOn(disconnect) val patch = +test(messages, unlimited) { (text) -> - patchRegex.matchEntireUnstyled(text)?.groupValues?.get(1) + patchRegex.matchEntirePlain(text)?.groupValues?.get(1) } val locateMessage = StateMessages.Locate.request(mc.player!!.username, true) diff --git a/src/main/java/io/github/homchom/recode/hypercube/SettingRequesters.kt b/src/main/java/io/github/homchom/recode/hypercube/SettingRequesters.kt index f5df70157..d4d7a8189 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/SettingRequesters.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/SettingRequesters.kt @@ -6,9 +6,9 @@ import io.github.homchom.recode.event.trial.trial import io.github.homchom.recode.hypercube.state.DFStateDetectors import io.github.homchom.recode.multiplayer.ReceiveChatMessageEvent import io.github.homchom.recode.multiplayer.sendCommand -import io.github.homchom.recode.ui.equalsUnstyled -import io.github.homchom.recode.ui.matchesUnstyled -import io.github.homchom.recode.ui.unstyledString +import io.github.homchom.recode.ui.text.equalsPlain +import io.github.homchom.recode.ui.text.matchesPlain +import io.github.homchom.recode.ui.text.plainText import io.github.homchom.recode.util.regex.cachedRegex import io.github.homchom.recode.util.regex.regex @@ -19,7 +19,7 @@ val ChatLocalRequester = requester("/chat local", DFStateDetectors.ChangeMode, t tests = { (text), _, _ -> val message = "$MAIN_ARROW Chat is now set to Local. You will only see messages from players on " + "your plot. Use /chat to change it again." - text.equalsUnstyled(message).instantUnitOrNull() + text.equalsPlain(message).instantUnitOrNull() } )) @@ -35,7 +35,7 @@ val ClientTimeRequester = requester("/time", DFStateDetectors.ChangeMode, trial( null as Long?, start = { time -> sendCommand("time $time") }, tests = { context, time, _: Boolean -> - timeRegex(time).matchesUnstyled(context.value).instantUnitOrNull() + timeRegex(time).matchesPlain(context.value).instantUnitOrNull() } )) @@ -44,7 +44,7 @@ val FlightRequesters = toggleRequesterGroup("/fly", DFStateDetectors, trial( Unit, start = { sendCommand("fly") }, tests = { message, _, _ -> - val enabled = when (message().unstyledString) { + val enabled = when (message().plainText) { "$MAIN_ARROW Flight enabled." -> true "$MAIN_ARROW Flight disabled." -> false else -> fail() @@ -70,8 +70,8 @@ val LagSlayerRequesters = toggleRequesterGroup("/lagslayer", DFStateDetectors.Ch start = { sendCommand("lagslayer") }, tests = { (message), _, _ -> val enabled = when { - lsEnabledRegex.matchesUnstyled(message) -> true - lsDisabledRegex.matchesUnstyled(message) -> false + lsEnabledRegex.matchesPlain(message) -> true + lsDisabledRegex.matchesPlain(message) -> false else -> fail() } instant(enabled) @@ -83,7 +83,7 @@ val NightVisionRequesters = toggleRequesterGroup("/nightvis", DFStateDetectors.C Unit, start = { sendCommand("nightvis") }, tests = { (message), _, _ -> - val enabled = when (message.unstyledString) { + val enabled = when (message.plainText) { "$MAIN_ARROW Enabled night vision." -> true "$MAIN_ARROW Disabled night vision." -> false else -> fail() diff --git a/src/main/java/io/github/homchom/recode/hypercube/message/CodeMessages.kt b/src/main/java/io/github/homchom/recode/hypercube/message/CodeMessages.kt index 9618d8c06..1e220ab20 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/message/CodeMessages.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/message/CodeMessages.kt @@ -3,11 +3,11 @@ package io.github.homchom.recode.hypercube.message import io.github.homchom.recode.event.Requester import io.github.homchom.recode.hypercube.state.DFStateDetectors import io.github.homchom.recode.multiplayer.sendCommand -import io.github.homchom.recode.ui.equalsUnstyled -import io.github.homchom.recode.ui.matchEntireUnstyled +import io.github.homchom.recode.ui.text.equalsPlain +import io.github.homchom.recode.ui.text.matchEntirePlain import io.github.homchom.recode.util.regex.namedGroupValues import io.github.homchom.recode.util.regex.regex -import net.minecraft.network.chat.Component +import net.kyori.adventure.text.Component import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -25,7 +25,7 @@ object CodeMessages { ) { override fun match(input: Component): SupportTime? { - if (input.equalsUnstyled("Error: You are not in a session.")) { + if (input.equalsPlain("Error: You are not in a session.")) { return SupportTime(null) } @@ -35,7 +35,7 @@ object CodeMessages { val minutes by digit * 2 val seconds by digit * 2 } - val values = regex.matchEntireUnstyled(input)?.namedGroupValues ?: return null + val values = regex.matchEntirePlain(input)?.namedGroupValues ?: return null val hours = values["hours"].toIntOrNull()?.hours ?: return null val minutes = values["minutes"].toIntOrNull()?.minutes ?: return null diff --git a/src/main/java/io/github/homchom/recode/hypercube/message/MessageParser.kt b/src/main/java/io/github/homchom/recode/hypercube/message/MessageParser.kt index c5fc3f098..eacfa3724 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/message/MessageParser.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/message/MessageParser.kt @@ -11,7 +11,7 @@ import io.github.homchom.recode.multiplayer.ReceiveChatMessageEvent import io.github.homchom.recode.util.Matcher import io.github.homchom.recode.util.matcherOf import io.github.homchom.recode.util.splitByHumps -import net.minecraft.network.chat.Component +import net.kyori.adventure.text.Component sealed interface MessageParser : Matcher, Detector diff --git a/src/main/java/io/github/homchom/recode/hypercube/message/ShopMessages.kt b/src/main/java/io/github/homchom/recode/hypercube/message/ShopMessages.kt index 6334ef3cb..cb1b1dc88 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/message/ShopMessages.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/message/ShopMessages.kt @@ -7,11 +7,11 @@ import io.github.homchom.recode.hypercube.BOOSTER_ARROW import io.github.homchom.recode.hypercube.TOKEN_NOTCH_CHAR import io.github.homchom.recode.multiplayer.ReceiveChatMessageEvent import io.github.homchom.recode.multiplayer.username -import io.github.homchom.recode.ui.matchEntireUnstyled -import io.github.homchom.recode.ui.matchesUnstyled +import io.github.homchom.recode.ui.text.matchEntirePlain +import io.github.homchom.recode.ui.text.matchesPlain import io.github.homchom.recode.util.regex.RegexPatternBuilder import io.github.homchom.recode.util.regex.regex -import net.minecraft.network.chat.Component +import net.kyori.adventure.text.Component object ShopMessages { val parsers get() = arrayOf(BoosterActive) @@ -29,7 +29,7 @@ object ShopMessages { } override fun match(input: Component): BoosterActive? { - val match = regex.matchEntireUnstyled(input) ?: return null + val match = regex.matchEntirePlain(input) ?: return null return BoosterActive(match.groupValues[1]) } } @@ -45,11 +45,11 @@ data class ActiveBoosterInfo(val player: String, val canTip: Boolean) { suspending { val (first) = subsequent.receive() - val canTip = ActiveBoosterInfo.commandRegex.matchesUnstyled(first) + val canTip = ActiveBoosterInfo.commandRegex.matchesPlain(first) - if (!ActiveBoosterInfo.timeRegex.matchesUnstyled(first)) { + if (!ActiveBoosterInfo.timeRegex.matchesPlain(first)) { +testBoolean(subsequent) { (second) -> - ActiveBoosterInfo.timeRegex.matchesUnstyled(second) + ActiveBoosterInfo.timeRegex.matchesPlain(second) } } diff --git a/src/main/java/io/github/homchom/recode/hypercube/message/StateMessages.kt b/src/main/java/io/github/homchom/recode/hypercube/message/StateMessages.kt index b0199ebc9..7c693b7a7 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/message/StateMessages.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/message/StateMessages.kt @@ -6,12 +6,12 @@ import io.github.homchom.recode.hypercube.state.* import io.github.homchom.recode.mc import io.github.homchom.recode.multiplayer.sendCommand import io.github.homchom.recode.multiplayer.username -import io.github.homchom.recode.ui.matchEntireUnstyled +import io.github.homchom.recode.ui.text.matchEntirePlain import io.github.homchom.recode.util.regex.RegexModifier import io.github.homchom.recode.util.regex.RegexPatternBuilder import io.github.homchom.recode.util.regex.namedGroupValues import io.github.homchom.recode.util.regex.regex -import net.minecraft.network.chat.Component +import net.kyori.adventure.text.Component import org.intellij.lang.annotations.RegExp object StateMessages { @@ -67,7 +67,7 @@ object StateMessages { override fun match(input: Component): Locate? { val values = locateRegex - .matchEntireUnstyled(input) + .matchEntirePlain(input) ?.namedGroupValues ?: return null val player = values["player"].takeUnless(String::isEmpty) ?: mc.player?.username ?: return null @@ -120,7 +120,7 @@ object StateMessages { override fun match(input: Component): Profile? { val values = profileRegex - .matchEntireUnstyled(input) + .matchEntirePlain(input) ?.namedGroupValues ?: return null diff --git a/src/main/java/io/github/homchom/recode/hypercube/state/DFState.kt b/src/main/java/io/github/homchom/recode/hypercube/state/DFState.kt index 87ac78b84..04034d7e9 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/state/DFState.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/state/DFState.kt @@ -7,16 +7,16 @@ import io.github.homchom.recode.hypercube.MAIN_ARROW import io.github.homchom.recode.hypercube.SUPPORT_ARROW import io.github.homchom.recode.mc import io.github.homchom.recode.multiplayer.username -import io.github.homchom.recode.ui.equalsUnstyled -import io.github.homchom.recode.ui.matchesUnstyled +import io.github.homchom.recode.ui.text.equalsPlain +import io.github.homchom.recode.ui.text.matchesPlain import io.github.homchom.recode.util.Matcher import io.github.homchom.recode.util.matcherOf import io.github.homchom.recode.util.regex.RegexModifier import io.github.homchom.recode.util.regex.regex import kotlinx.coroutines.Deferred +import net.kyori.adventure.text.Component import net.minecraft.client.multiplayer.ServerData import net.minecraft.core.BlockPos -import net.minecraft.network.chat.Component import net.minecraft.world.item.ItemStack val ServerData?.ipMatchesDF get(): Boolean { @@ -50,7 +50,7 @@ sealed interface DFState { suspend fun permissions() = permissions.await() /** - * Returns a new [DFState] derived from this one and [state], including calculated [PlotMode] state. + * @return a new [DFState] derived from this one and [state], including calculated [PlotMode] state. */ fun withState(state: LocateState) = when (state) { is LocateState.AtSpawn -> AtSpawn(state.node, permissions, session) @@ -160,7 +160,7 @@ sealed interface PlotMode { override val descriptor = "playing" - override fun match(input: Component) = takeIf { playModeRegex.matchesUnstyled(input) } + override fun match(input: Component) = takeIf { playModeRegex.matchesPlain(input) } } data object Build : PlotMode, ID { @@ -169,7 +169,7 @@ sealed interface PlotMode { override val descriptor = "building" override fun match(input: Component) = - takeIf { input.equalsUnstyled("$MAIN_ARROW You are now in build mode.") } + takeIf { input.equalsPlain("$MAIN_ARROW You are now in build mode.") } } data class Dev(val buildCorner: BlockPos, val referenceBookCopy: ItemStack) : PlotMode { @@ -179,14 +179,14 @@ sealed interface PlotMode { override val descriptor = "coding" override fun match(input: Component) = - takeIf { input.equalsUnstyled("$MAIN_ARROW You are now in dev mode.") } + takeIf { input.equalsPlain("$MAIN_ARROW You are now in dev mode.") } } } } enum class SupportSession : Matcher { Requested { - override fun match(input: Component) = takeIf { input.equalsUnstyled( + override fun match(input: Component) = takeIf { input.equalsPlain( "You have requested code support.\nIf you wish to leave the queue, use /support cancel." ) } }, @@ -197,7 +197,7 @@ enum class SupportSession : Matcher { username() str(". $SUPPORT_ARROW Queue cleared!") } - return takeIf { regex.matchesUnstyled(input) } + return takeIf { regex.matchesPlain(input) } } }; diff --git a/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt b/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt index f8cbdcfbe..f1af77417 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt @@ -18,8 +18,8 @@ import io.github.homchom.recode.multiplayer.DisconnectFromServerEvent import io.github.homchom.recode.multiplayer.ReceiveChatMessageEvent import io.github.homchom.recode.multiplayer.ReceiveGamePacketEvent import io.github.homchom.recode.multiplayer.username -import io.github.homchom.recode.ui.matchesUnstyled -import io.github.homchom.recode.ui.removeLegacyCodes +import io.github.homchom.recode.ui.text.matchesPlain +import io.github.homchom.recode.ui.text.removeLegacyCodes import io.github.homchom.recode.util.Case import io.github.homchom.recode.util.encase import io.github.homchom.recode.util.regex.namedGroupValues @@ -114,7 +114,7 @@ object DFStateDetectors : StateListenable> by eventGroup { } // this is safe because of the previous enforce call; only one can run at a time testBoolean(subsequent, unlimited, Duration.INFINITE) { (text) -> - regex.matchesUnstyled(text) + regex.matchesPlain(text) } val supportTime = CodeMessages.SupportTime.request(Unit, true).duration @@ -137,7 +137,7 @@ object DFStateDetectors : StateListenable> by eventGroup { str(" has ended.") } - requireTrue(regex.matchesUnstyled(message)) + requireTrue(regex.matchesPlain(message)) instant(Case(currentDFState!!.withSession(null))) } )) diff --git a/src/main/java/io/github/homchom/recode/mixin/game/ItemStackAccessor.java b/src/main/java/io/github/homchom/recode/mixin/game/ItemStackAccessor.java new file mode 100644 index 000000000..ab6a8a839 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/mixin/game/ItemStackAccessor.java @@ -0,0 +1,14 @@ +package io.github.homchom.recode.mixin.game; + +import net.minecraft.network.chat.Style; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ItemStack.class) +public interface ItemStackAccessor { + @Accessor("LORE_STYLE") + static Style getLoreVanillaStyle() { + throw new AssertionError(); + } +} diff --git a/src/main/java/io/github/homchom/recode/mixin/multiplayer/MChatListener.java b/src/main/java/io/github/homchom/recode/mixin/multiplayer/MChatListener.java index 31d92bf06..c2eda870d 100644 --- a/src/main/java/io/github/homchom/recode/mixin/multiplayer/MChatListener.java +++ b/src/main/java/io/github/homchom/recode/mixin/multiplayer/MChatListener.java @@ -2,7 +2,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import io.github.homchom.recode.feature.social.MCGuiWithSideChat; +import io.github.homchom.recode.feature.social.DGuiWithSideChat; import io.github.homchom.recode.feature.social.SideChat; import io.github.homchom.recode.sys.sidedchat.ChatRule; import io.github.homchom.recode.sys.util.SoundUtil; @@ -17,7 +17,7 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(ChatListener.class) -public class MChatListener { +public abstract class MChatListener { @WrapOperation(method = "showMessageToPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent;addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;Lnet/minecraft/client/GuiMessageTag;)V" )) @@ -65,7 +65,7 @@ private ChatRule.ChatSide matchToChatSide(Component message) { @Unique private SideChat getSideChat() { - var gui = (MCGuiWithSideChat) Minecraft.getInstance().gui; + var gui = (DGuiWithSideChat) Minecraft.getInstance().gui; return gui.recode$getSideChat(); } } diff --git a/src/main/java/io/github/homchom/recode/mixin/multiplayer/MJoinMultiplayerScreen.java b/src/main/java/io/github/homchom/recode/mixin/multiplayer/MJoinMultiplayerScreen.java index ff8affa4f..3b547c6f3 100644 --- a/src/main/java/io/github/homchom/recode/mixin/multiplayer/MJoinMultiplayerScreen.java +++ b/src/main/java/io/github/homchom/recode/mixin/multiplayer/MJoinMultiplayerScreen.java @@ -1,13 +1,13 @@ package io.github.homchom.recode.mixin.multiplayer; -import io.github.homchom.recode.hypercube.ServerConstants; +import io.github.homchom.recode.hypercube.HypercubeConstants; import io.github.homchom.recode.ui.Toasts; +import net.kyori.adventure.text.Component; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; -import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -47,7 +47,7 @@ private void replaceUnofficialDFAddresses(ServerData serverData, CallbackInfo ci if (prefix == null) prefix = ""; var suffix = matcher.group("suffix"); if (suffix == null) suffix = ""; - serverData.ip = prefix + ServerConstants.SERVER_ADDRESS + suffix; + serverData.ip = prefix + HypercubeConstants.SERVER_ADDRESS + suffix; servers.save(); } diff --git a/src/main/java/io/github/homchom/recode/mixin/render/MGui.java b/src/main/java/io/github/homchom/recode/mixin/render/MGui.java index 7dfcfaac9..ef7070fa2 100644 --- a/src/main/java/io/github/homchom/recode/mixin/render/MGui.java +++ b/src/main/java/io/github/homchom/recode/mixin/render/MGui.java @@ -2,7 +2,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import io.github.homchom.recode.feature.social.MCGuiWithSideChat; +import io.github.homchom.recode.feature.social.DGuiWithSideChat; import io.github.homchom.recode.feature.social.SideChat; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphics; @@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(Gui.class) -public abstract class MGui implements MCGuiWithSideChat { +public abstract class MGui implements DGuiWithSideChat { @Unique private final SideChat sideChat = new SideChat(); @@ -34,6 +34,7 @@ private void renderSideChat( @Unique @NotNull + @Override public SideChat recode$getSideChat() { return sideChat; } diff --git a/src/main/java/io/github/homchom/recode/mixin/render/MWindow.java b/src/main/java/io/github/homchom/recode/mixin/render/MWindow.java index 4a40cd2e2..3c8fae036 100644 --- a/src/main/java/io/github/homchom/recode/mixin/render/MWindow.java +++ b/src/main/java/io/github/homchom/recode/mixin/render/MWindow.java @@ -1,7 +1,7 @@ package io.github.homchom.recode.mixin.render; import com.mojang.blaze3d.platform.Window; -import io.github.homchom.recode.feature.social.MCGuiWithSideChat; +import io.github.homchom.recode.feature.social.DGuiWithSideChat; import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -9,10 +9,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(Window.class) -public class MWindow { +public abstract class MWindow { @Inject(method = "setGuiScale", at = @At("TAIL")) private void rescaleSideChat(CallbackInfo ci) { - var gui = (MCGuiWithSideChat) Minecraft.getInstance().gui; + var gui = (DGuiWithSideChat) Minecraft.getInstance().gui; gui.recode$getSideChat().rescaleChat(); } } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatComponent.java b/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatComponent.java index ed89b53ab..8e89cc777 100644 --- a/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatComponent.java +++ b/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatComponent.java @@ -2,13 +2,12 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import io.github.homchom.recode.feature.social.MCGuiWithSideChat; +import io.github.homchom.recode.feature.social.DGuiWithSideChat; import io.github.homchom.recode.feature.social.MessageStacking; import io.github.homchom.recode.feature.social.SideChat; import io.github.homchom.recode.mod.config.Config; import io.github.homchom.recode.ui.ChatUI; -import io.github.homchom.recode.ui.TextFunctions; -import io.github.homchom.recode.util.mixin.MixinCustomField; +import io.github.homchom.recode.ui.text.TextFunctions; import net.minecraft.client.GuiMessage; import net.minecraft.client.GuiMessageTag; import net.minecraft.client.Minecraft; @@ -73,7 +72,7 @@ private void clearSideChat(boolean refresh, CallbackInfo ci) { @Unique private SideChat getSideChat() { - var gui = (MCGuiWithSideChat) Minecraft.getInstance().gui; + var gui = (DGuiWithSideChat) Minecraft.getInstance().gui; return gui.recode$getSideChat(); } @@ -98,14 +97,14 @@ private boolean isSideChat() { // message stacking @Unique - private final MixinCustomField trimmedMessageCount = new MixinCustomField<>(() -> 0); + private int trimmedMessageCount = 0; @Inject( method = "addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;ILnet/minecraft/client/GuiMessageTag;Z)V", at = @At("HEAD") ) private void countTrimmedMessagesBeforeMessageStacking(CallbackInfo ci) { - trimmedMessageCount.set(thisChatComponent(), trimmedMessages.size()); + trimmedMessageCount = trimmedMessages.size(); } @Inject( @@ -121,13 +120,12 @@ private void stackMessages( CallbackInfo ci ) { if (!Config.getBoolean("stackDuplicateMsgs")) return; - int trimmedCount = trimmedMessageCount.get(thisChatComponent()); - if (trimmedCount == 0) return; + if (trimmedMessageCount == 0) return; // trimmedMessages[0] is the most recent message - var lineCount = trimmedMessages.size() - trimmedCount; - if (trimmedCount < lineCount) return; - if (trimmedCount > lineCount) { + var lineCount = trimmedMessages.size() - trimmedMessageCount; + if (trimmedMessageCount < lineCount) return; + if (trimmedMessageCount > lineCount) { if (!trimmedMessages.get(lineCount * 2).endOfEntry()) return; } @@ -157,9 +155,4 @@ private void stackMessages( trimmedMessages.set(index, newLine); } } - - @Unique - private ChatComponent thisChatComponent() { - return (ChatComponent) (Object) this; - } } diff --git a/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatScreen.java b/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatScreen.java index f9043a6dd..2ad1f8938 100644 --- a/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatScreen.java +++ b/src/main/java/io/github/homchom/recode/mixin/render/chat/MChatScreen.java @@ -2,7 +2,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import io.github.homchom.recode.feature.social.MCGuiWithSideChat; +import io.github.homchom.recode.feature.social.DGuiWithSideChat; import net.minecraft.client.GuiMessageTag; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.ChatComponent; @@ -11,7 +11,7 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(ChatScreen.class) -public class MChatScreen { +public abstract class MChatScreen { @WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/components/ChatComponent;getMessageTagAt(DD)Lnet/minecraft/client/GuiMessageTag;" )) @@ -23,7 +23,7 @@ private GuiMessageTag recognizeSideChatTags( ) { var mainTag = operation.call(mainChat, screenX, screenY); if (mainTag != null) return mainTag; - var gui = (MCGuiWithSideChat) Minecraft.getInstance().gui; + var gui = (DGuiWithSideChat) Minecraft.getInstance().gui; return gui.recode$getSideChat().getMessageTagAt(screenX, screenY); } } diff --git a/src/main/java/io/github/homchom/recode/mixin/render/chat/MCommandSuggestions.java b/src/main/java/io/github/homchom/recode/mixin/render/chat/MCommandSuggestions.java new file mode 100644 index 000000000..5e49bbbd8 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/mixin/render/chat/MCommandSuggestions.java @@ -0,0 +1,50 @@ +package io.github.homchom.recode.mixin.render.chat; + +import io.github.homchom.recode.feature.visual.ExpressionHighlighter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.CommandSuggestions; +import net.minecraft.util.FormattedCharSequence; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Objects; + +@Mixin(CommandSuggestions.class) +public abstract class MCommandSuggestions { + @Unique + private final ExpressionHighlighter highlighter = new ExpressionHighlighter(); + + @Unique + private @Nullable FormattedCharSequence preview = null; + + @Inject(method = "formatChat", at = @At("RETURN"), cancellable = true) + private void highlightAndPreview( + String partialInput, + int position, + CallbackInfoReturnable cir + ) { + var formatted = cir.getReturnValue(); + var player = Objects.requireNonNull(Minecraft.getInstance().player); + var highlighted = highlighter.runHighlighting(partialInput, formatted, player); + if (highlighted == null) return; + + if (highlighted.getPreview() != null) preview = highlighted.getPreview(); + cir.setReturnValue(highlighted.getText()); + } + + @Inject(method = "render", at = @At("HEAD")) + private void renderPreview(GuiGraphics guiGraphics, int i, int j, CallbackInfo ci) { + if (preview != null) { + var font = Minecraft.getInstance().font; + var y = Objects.requireNonNull(Minecraft.getInstance().screen).height - 25; + guiGraphics.drawString(font, preview, 4, y, 0xffffff, true); + } + preview = null; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/mod/events/impl/LegacyReceiveChatMessageEvent.java b/src/main/java/io/github/homchom/recode/mod/events/impl/LegacyReceiveChatMessageEvent.java index 1f6635daf..50cafdbed 100644 --- a/src/main/java/io/github/homchom/recode/mod/events/impl/LegacyReceiveChatMessageEvent.java +++ b/src/main/java/io/github/homchom/recode/mod/events/impl/LegacyReceiveChatMessageEvent.java @@ -8,10 +8,12 @@ import io.github.homchom.recode.sys.player.chat.ChatType; import io.github.homchom.recode.sys.player.chat.ChatUtil; import io.github.homchom.recode.sys.util.TextUtil; +import io.github.homchom.recode.ui.text.TextInterop; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.network.chat.ClickEvent.Action; -import net.minecraft.network.chat.Component; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,9 +34,9 @@ public void run(SimpleValidated context) { boolean cancel = false; // TODO: temporary, migrate all code here - String msgToString = message.getString(); + String msgToString = PlainTextComponentSerializer.plainText().serialize(message); - String msgWithColor = TextUtil.toLegacyCodes(message); + String msgWithColor = TextUtil.toLegacyCodes(TextInterop.toVanilla(message)); String msgWithoutColor = msgWithColor.replaceAll("§.", ""); //PJoin command @@ -143,8 +145,8 @@ public void run(SimpleValidated context) { } if (Config.getBoolean("autoClickEditMsgs") && msgToString.startsWith("⏵ Click to edit variable: ")) { - if (message.getStyle().getClickEvent().getAction() == Action.SUGGEST_COMMAND) { - String toOpen = message.getStyle().getClickEvent().getValue(); + if (message.style().clickEvent().action() == ClickEvent.Action.SUGGEST_COMMAND) { + String toOpen = message.style().clickEvent().value(); Minecraft.getInstance().tell(() -> Minecraft.getInstance().setScreen(new ChatScreen(toOpen))); } } diff --git a/src/main/java/io/github/homchom/recode/mod/features/VarSyntaxHighlighter.kt b/src/main/java/io/github/homchom/recode/mod/features/VarSyntaxHighlighter.kt deleted file mode 100644 index 17d8818b0..000000000 --- a/src/main/java/io/github/homchom/recode/mod/features/VarSyntaxHighlighter.kt +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.homchom.recode.mod.features - -import com.google.gson.JsonParser -import io.github.homchom.recode.hypercube.CommandAliasGroup -import io.github.homchom.recode.logError -import io.github.homchom.recode.mc -import io.github.homchom.recode.ui.ExpressionHighlighter -import io.github.homchom.recode.ui.text -import io.github.homchom.recode.util.Computation -import io.github.homchom.recode.util.collections.verticalFlatten -import net.minecraft.network.chat.Component -import net.minecraft.world.item.Items - -// TODO: move, refactor and make better/faster, apply kotlin idioms -object VarSyntaxHighlighter { - @JvmStatic - val textPreviews = listOf( - +CommandAliasGroup.RENAME, - +CommandAliasGroup.ITEM_LORE_ADD, - +CommandAliasGroup.ITEM_LORE_SET, - listOf("relore"), - +CommandAliasGroup.PLOT_NAME - ).verticalFlatten().map { "/$it " } - - // TODO: fix timing issue - @JvmStatic - fun highlight(msg: String): Component? { - var mutableMsg = msg - val item = mc.player!!.mainHandItem - var type = "" - try { - if (item.item !== Items.AIR) { - val vals = item.getOrCreateTagElement("PublicBukkitValues") - if (vals.contains("hypercube:varitem")) { - val `var` = vals.getString("hypercube:varitem") - val json = JsonParser.parseString(`var`).asJsonObject - type = json["id"].asString - } - } - } catch (e: Exception) { - logError("Unknown variable syntax highlighting error") - e.printStackTrace() - } - var doTagsAndCount = true - if (mutableMsg.startsWith("/variable ")) { - mutableMsg = mutableMsg.replaceFirst("/variable", "/var") - } else if (mutableMsg.startsWith("/number ")) { - mutableMsg = mutableMsg.replaceFirst("/number", "/num") - } else if (mutableMsg.startsWith("/text ")) { - mutableMsg = mutableMsg.replaceFirst("/text", "/txt") - } else { - for (preview in textPreviews) { - var mutablePreview = preview - val num = if (mutablePreview.endsWith("N")) { - mutablePreview = mutablePreview.replace("N", "") - true - } else false - if (mutableMsg.startsWith(mutablePreview)) { - doTagsAndCount = false - mutableMsg = mutableMsg.substring(mutablePreview.length) - mutableMsg = if (num) { - if (!mutableMsg.contains(" ")) return null - mutableMsg.substring(mutableMsg.indexOf(" ")) - } else " $mutableMsg" - mutableMsg = "/txt$mutableMsg" - break - } - } - } - if (mutableMsg.startsWith("/") && doTagsAndCount) { - if (mutableMsg.endsWith(" -l") || mutableMsg.endsWith(" -s") || mutableMsg.endsWith(" -g")) { - mutableMsg = mutableMsg.substring(0, mutableMsg.length - 3) - } - val matchResult = Regex(".+( \\d+)").matchEntire(mutableMsg) - if (matchResult != null) { - mutableMsg = mutableMsg.removeRange(matchResult.groups[1]!!.range) - } - } - - val parseMiniMessage = when { - mutableMsg.startsWith("/var ") || mutableMsg.startsWith("/num ") - || type == "var" || type == "num" -> false - mutableMsg.startsWith("/txt ") || type == "txt" -> true - else -> return null - } - val string = mutableMsg.substring(5) - return when (val comp = ExpressionHighlighter.highlightString(string, parseMiniMessage)) { - is Computation.Success -> comp() - is Computation.Failure -> text { - color(red) { literal(comp()) } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/mod/mixin/inventory/MHeldItemTooltip.java b/src/main/java/io/github/homchom/recode/mod/mixin/inventory/MHeldItemTooltip.java index 9b94a1af3..c30492022 100644 --- a/src/main/java/io/github/homchom/recode/mod/mixin/inventory/MHeldItemTooltip.java +++ b/src/main/java/io/github/homchom/recode/mod/mixin/inventory/MHeldItemTooltip.java @@ -3,13 +3,12 @@ import com.google.gson.JsonParser; import io.github.homchom.recode.RecodeKt; +import io.github.homchom.recode.game.ItemExtensions; import io.github.homchom.recode.mod.config.Config; -import io.github.homchom.recode.mod.features.VarSyntaxHighlighter; -import io.github.homchom.recode.sys.util.TextUtil; +import io.github.homchom.recode.ui.text.TextInterop; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; @@ -18,8 +17,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.Objects; - @Mixin(Gui.class) public class MHeldItemTooltip { @Shadow @@ -61,19 +58,17 @@ public void renderSelectedItemName(GuiGraphics guiGraphics, CallbackInfo callbac var y1 = scaledHeight - 45; guiGraphics.drawString(font, nameText, x1, y1, 0xffffff, true); - var lore = tag.getCompound("display").getList("Lore", Tag.TAG_STRING); - if (lore.isEmpty()) return; - var scopeJson = tag.getCompound("display") - .getList("Lore", Tag.TAG_STRING) - .getString(0); - var scope = Objects.requireNonNull(Component.Serializer.fromJson(scopeJson)); - var x2 = (scaledWidth - font.width(scope.getVisualOrderText())) / 2; - var y2 = scaledHeight - 35; - guiGraphics.drawString(font, scope, x2, y2, 0xffffff, true); + var lore = ItemExtensions.lore(lastToolHighlight); + if (!lore.isEmpty()) { + var scope = TextInterop.toVanilla(lore.get(0)); + var x2 = (scaledWidth - font.width(scope)) / 2; + var y2 = scaledHeight - 35; + guiGraphics.drawString(font, scope, x2, y2, 0xffffff, true); + } } // render highlighting - if (highlightVarSyntax) { + /*if (highlightVarSyntax) { var formatted = VarSyntaxHighlighter.highlight(name.getAsString()); if (formatted != null) { @@ -98,7 +93,7 @@ public void renderSelectedItemName(GuiGraphics guiGraphics, CallbackInfo callbac guiGraphics.drawString(font, formatted, x2, y2, 0xffffff); } } - } + }*/ } catch (Exception e) { RecodeKt.logError("Unrecognized DF value item data: " + varJson); throw e; diff --git a/src/main/java/io/github/homchom/recode/mod/mixin/message/MMessageListener.java b/src/main/java/io/github/homchom/recode/mod/mixin/message/MMessageListener.java index aacac0be0..8576b1c88 100644 --- a/src/main/java/io/github/homchom/recode/mod/mixin/message/MMessageListener.java +++ b/src/main/java/io/github/homchom/recode/mod/mixin/message/MMessageListener.java @@ -32,7 +32,7 @@ private void handleChat(ClientboundSystemChatPacket packet, CallbackInfo ci) { // this method is also called on an IO thread if (!Minecraft.getInstance().isSameThread()) return; - var context = new SimpleValidated<>(packet.content()); + var context = new SimpleValidated<>(packet.content().asComponent()); if (!MultiplayerEvents.getReceiveChatMessageEvent().run(context)) ci.cancel(); if (DF.isOnDF()) { diff --git a/src/main/java/io/github/homchom/recode/mod/mixin/render/MChatScreen.java b/src/main/java/io/github/homchom/recode/mod/mixin/render/MChatScreen.java index 6a356a328..7a8541807 100644 --- a/src/main/java/io/github/homchom/recode/mod/mixin/render/MChatScreen.java +++ b/src/main/java/io/github/homchom/recode/mod/mixin/render/MChatScreen.java @@ -1,60 +1,13 @@ package io.github.homchom.recode.mod.mixin.render; -import io.github.homchom.recode.mod.config.Config; -import io.github.homchom.recode.mod.features.VarSyntaxHighlighter; import io.github.homchom.recode.sys.sidedchat.ChatShortcut; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.network.chat.Component; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ChatScreen.class) public class MChatScreen { - @Shadow - protected EditBox input; - - @Inject(method = "render", at = @At("TAIL")) - private void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { - if (Config.getBoolean("highlightVarSyntax")) { - Minecraft mc = Minecraft.getInstance(); - - String text = input.getValue(); - - if (text.startsWith("/") && !( - text.startsWith("/var") || - text.startsWith("/variable") || - text.startsWith("/num") || - text.startsWith("/number") || - text.startsWith("/txt") || - text.startsWith("/text") - )) { - boolean r = true; - for (String o : VarSyntaxHighlighter.getTextPreviews()) { - if (o.endsWith(" N")) o = o.replace(" N",""); - if (text.startsWith(o)) { - r = false; - break; - } - } - - if (r) return; - } - - Component formatted = VarSyntaxHighlighter.highlight(text); - - if (formatted != null) { - guiGraphics.drawString(mc.font, formatted, 4, mc.screen.height - 25, 0xffffff, true); - } - } - } - @ModifyArg(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;fill(IIIII)V"), index = 4) private int getTextboxColor(int defaultColour) { ChatShortcut currentChatShortcut = ChatShortcut.getCurrentChatShortcut(); diff --git a/src/main/java/io/github/homchom/recode/multiplayer/LocalPlayerFunctions.kt b/src/main/java/io/github/homchom/recode/multiplayer/LocalPlayerFunctions.kt index 71e9bf9ff..dfe15df42 100644 --- a/src/main/java/io/github/homchom/recode/multiplayer/LocalPlayerFunctions.kt +++ b/src/main/java/io/github/homchom/recode/multiplayer/LocalPlayerFunctions.kt @@ -9,14 +9,14 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import net.kyori.adventure.text.Component import net.minecraft.client.multiplayer.ClientPacketListener import net.minecraft.client.player.LocalPlayer -import net.minecraft.network.chat.Component /** * @throws IllegalStateException if there is no current player */ -fun displayMessage(message: Component) = asPlayer { displayClientMessage(message, false) } +fun displayMessage(message: Component) = asPlayer { sendMessage(message) } /** * @param command The command to send, without the leading slash. diff --git a/src/main/java/io/github/homchom/recode/multiplayer/MultiplayerEvents.kt b/src/main/java/io/github/homchom/recode/multiplayer/MultiplayerEvents.kt index fd251c456..89f888a6b 100644 --- a/src/main/java/io/github/homchom/recode/multiplayer/MultiplayerEvents.kt +++ b/src/main/java/io/github/homchom/recode/multiplayer/MultiplayerEvents.kt @@ -9,9 +9,9 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Disconnect import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Join import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.kyori.adventure.text.Component import net.minecraft.client.Minecraft import net.minecraft.client.multiplayer.ClientPacketListener -import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet val JoinServerEvent = wrapFabricEvent(ClientPlayConnectionEvents.JOIN) { listener -> diff --git a/src/main/java/io/github/homchom/recode/render/Color.kt b/src/main/java/io/github/homchom/recode/render/Color.kt index d8e0888be..bbf9d4fb5 100644 --- a/src/main/java/io/github/homchom/recode/render/Color.kt +++ b/src/main/java/io/github/homchom/recode/render/Color.kt @@ -17,26 +17,22 @@ sealed interface IntegralColor { fun toInt(): Int } -private sealed interface RGBIntegralColor : IntegralColor { - val red: Int - val green: Int +data class RGBColor( + val red: Int, + val green: Int, val blue: Int - +) : IntegralColor { override fun toInt() = (red shl 16) + (green shl 8) + blue } -data class RGBColor( - override val red: Int, - override val green: Int, - override val blue: Int -) : RGBIntegralColor - data class RGBAColor( - override val red: Int, - override val green: Int, - override val blue: Int, + val red: Int, + val green: Int, + val blue: Int, val alpha: Int -) : RGBIntegralColor +) : IntegralColor { + override fun toInt() = (alpha shl 24) + (red shl 16) + (green shl 8) + blue +} @JvmInline value class HexColor(val hex: Int) : IntegralColor { diff --git a/src/main/java/io/github/homchom/recode/ui/ChatUI.kt b/src/main/java/io/github/homchom/recode/ui/ChatUI.kt index f1eddd842..e981a7a77 100644 --- a/src/main/java/io/github/homchom/recode/ui/ChatUI.kt +++ b/src/main/java/io/github/homchom/recode/ui/ChatUI.kt @@ -2,6 +2,7 @@ package io.github.homchom.recode.ui +import io.github.homchom.recode.ui.text.VanillaComponent import net.minecraft.client.GuiMessageTag /** @@ -24,7 +25,9 @@ operator fun GuiMessageTag.plus(other: GuiMessageTag): GuiMessageTag { if (first.ordinal > second.ordinal) first else second } val newText = combineIfNotNull(text, other.text) { first, second -> - text { append(first); space(); append(second) } + first.copy() + .append(VanillaComponent.literal(" ")) + .append(second) } val newLogTag = combineIfNotNull(logTag, other.logTag) { first, second -> "$first, $second" diff --git a/src/main/java/io/github/homchom/recode/ui/ExpressionHighlighter.kt b/src/main/java/io/github/homchom/recode/ui/ExpressionHighlighter.kt deleted file mode 100644 index 9c011eab0..000000000 --- a/src/main/java/io/github/homchom/recode/ui/ExpressionHighlighter.kt +++ /dev/null @@ -1,112 +0,0 @@ -package io.github.homchom.recode.ui - -import io.github.homchom.recode.render.HexColor -import io.github.homchom.recode.util.Computation -import io.github.homchom.recode.util.regex.regex -import net.kyori.adventure.text.minimessage.MiniMessage -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.Style - -object ExpressionHighlighter { - private val codes = setOf( - "default", - "selected", - "uuid", - "var", - "math", - "damager", - "killer", - "shooter", - "victim", - "projectile", - "random", - "round", - "index", - "entry" - ) - - private val codeRegex = regex { - group { - str("%") - any("a-zA-Z").oneOrMore() - str("(").optional() - } - or; str(")") - or; end - } - - // TODO: new color scheme? - private val colors = listOf( - 0xffffff, - 0xffd600, - 0x33ff00, - 0x00ffe0, - 0x5e77f7, - 0xca64fa, - 0xff4242 - ) - - fun highlightString(string: String, parseMiniMessage: Boolean = false): Computation { - val result = if (parseMiniMessage) { - object : HighlightBuilder { - private val builder = StringBuilder() - - override fun append(text: String, depth: Int, depthIncreased: Boolean) { - val tag = if (depthIncreased) "" else "" - builder.append("$tag$text") - } - - override fun build() = MiniMessage.miniMessage().deserializeToNative(builder.toString()) - } - } else { - object : HighlightBuilder { - private val builder = Component.empty() - - override fun append(text: String, depth: Int, depthIncreased: Boolean) { - val style = Style.EMPTY.withColor(colors[depth % colors.size]) - builder.append(Component.literal(text).withStyle(style)) - } - - override fun build(): Component = builder - } - } - - var sliceStart = 0 - var depth = 0 - var code = "" - for (match in codeRegex.findAll(string)) { - val depthIncreased = code.endsWith('(') || sliceStart == 0 - result.append(string.substring(sliceStart, match.range.first), depth, depthIncreased) - - code = match.value - if (code.length > 1) { - val codeName = if (code.endsWith('(')) { - code.substring(1, code.lastIndex) - } else { - code.drop(1) - } - if (codeName !in codes) return Computation.Failure("Invalid text code: %$codeName") - } - - if (code == ")") { - if (depth > 0) depth-- - } else depth++ - result.append(string.substring(match.range), depth, code != ")") - if (code.endsWith('(')) depth++ else { - if (depth > 0) depth-- - } - - sliceStart = match.range.last + 1 - } - - return Computation.Success(result.build()) - } - - private fun colorAt(depth: Int) = HexColor(colors[depth % colors.size]) - - private interface HighlightBuilder { - fun append(text: String, depth: Int, depthIncreased: Boolean) - - fun build(): Component - } -} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/MiniMessage.kt b/src/main/java/io/github/homchom/recode/ui/MiniMessage.kt deleted file mode 100644 index 209321874..000000000 --- a/src/main/java/io/github/homchom/recode/ui/MiniMessage.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.homchom.recode.ui - -import net.kyori.adventure.platform.fabric.FabricClientAudiences -import net.kyori.adventure.text.minimessage.MiniMessage -import net.minecraft.network.chat.Component - -fun MiniMessage.deserializeToNative(input: String): Component { - val adventureComponent = deserialize(input) - return FabricClientAudiences.of().toNative(adventureComponent) -} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/TextBuilder.kt b/src/main/java/io/github/homchom/recode/ui/TextBuilder.kt deleted file mode 100644 index 4d92be89d..000000000 --- a/src/main/java/io/github/homchom/recode/ui/TextBuilder.kt +++ /dev/null @@ -1,98 +0,0 @@ -package io.github.homchom.recode.ui - -import io.github.homchom.recode.render.ColorPalette -import io.github.homchom.recode.render.IntegralColor -import net.minecraft.network.chat.ClickEvent -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.HoverEvent -import net.minecraft.network.chat.HoverEvent.EntityTooltipInfo -import net.minecraft.network.chat.HoverEvent.ItemStackInfo -import net.minecraft.network.chat.Style -import net.minecraft.resources.ResourceLocation - -typealias TextScope = TextBuilder.() -> Unit - -inline fun text(style: Style = Style.EMPTY, builder: TextScope) = - TextBuilder(style).apply(builder).builtText - -fun translateText(key: String, vararg args: Any): Component = Component.translatable(key, *args) -fun literalText(string: String): Component = Component.literal(string) - -@Suppress("unused") -class TextBuilder(style: Style = Style.EMPTY) { - val builtText: Component get() = _text - private val _text = Component.empty().withStyle(style) - - inline val black get() = ColorPalette.BLACK - inline val darkBlue get() = ColorPalette.DARK_BLUE - inline val darkGreen get() = ColorPalette.DARK_GREEN - inline val darkAqua get() = ColorPalette.DARK_AQUA - inline val darkRed get() = ColorPalette.DARK_RED - inline val darkPurple get() = ColorPalette.DARK_PURPLE - inline val gold get() = ColorPalette.GOLD - inline val gray get() = ColorPalette.GRAY - inline val darkGray get() = ColorPalette.DARK_GRAY - inline val blue get() = ColorPalette.BLUE - inline val green get() = ColorPalette.GREEN - inline val aqua get() = ColorPalette.AQUA - inline val red get() = ColorPalette.RED - inline val lightPurple get() = ColorPalette.LIGHT_PURPLE - inline val yellow get() = ColorPalette.YELLOW - inline val white get() = ColorPalette.WHITE - - val openUrl get() = ClickEvent.Action.OPEN_URL - val openFile get() = ClickEvent.Action.OPEN_FILE - val runCommand get() = ClickEvent.Action.RUN_COMMAND - val suggestCommand get() = ClickEvent.Action.SUGGEST_COMMAND - val changePage get() = ClickEvent.Action.CHANGE_PAGE - val copyToClipboard get() = ClickEvent.Action.COPY_TO_CLIPBOARD - - val showText: HoverEvent.Action get() = HoverEvent.Action.SHOW_TEXT - val showItem: HoverEvent.Action get() = HoverEvent.Action.SHOW_ITEM - val showEntity: HoverEvent.Action get() = HoverEvent.Action.SHOW_ENTITY - - fun append(component: Component) { - _text += component - } - - inline fun appendBlock(style: Style, scope: TextScope) = append(text(style, scope)) - - fun translate(key: String, vararg args: Any) = append(translateText(key, *args)) - fun literal(string: String) = append(literalText(string)) - fun keybind(key: String) = append(Component.keybind(key)) - - fun space() = literal(" ") - - inline fun color(color: IntegralColor, scope: TextScope) = - appendBlock(Style.EMPTY.withColor(color.toInt()), scope) - - inline fun color(hex: Int, scope: TextScope) = - appendBlock(Style.EMPTY.withColor(hex), scope) - - inline fun bold(scope: TextScope) = - appendBlock(Style.EMPTY.withBold(true), scope) - - inline fun underline(scope: TextScope) = - appendBlock(Style.EMPTY.withUnderlined(true), scope) - - inline fun italic(scope: TextScope) = - appendBlock(Style.EMPTY.withItalic(true), scope) - - inline fun strikethrough(scope: TextScope) = - appendBlock(Style.EMPTY.withStrikethrough(true), scope) - - inline fun obfuscated(scope: TextScope) = - appendBlock(Style.EMPTY.withObfuscated(true), scope) - - inline fun onClick(action: ClickEvent.Action, value: String, scope: TextScope) = - appendBlock(Style.EMPTY.withClickEvent(ClickEvent(action, value)), scope) - - inline fun onHover(action: HoverEvent.Action, value: T & Any, scope: TextScope) = - appendBlock(Style.EMPTY.withHoverEvent(HoverEvent(action, value)), scope) - - inline fun insert(string: String, scope: TextScope) = - appendBlock(Style.EMPTY.withInsertion(string), scope) - - inline fun font(id: ResourceLocation, scope: TextScope) = - appendBlock(Style.EMPTY.withFont(id), scope) -} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/TextFunctions.kt b/src/main/java/io/github/homchom/recode/ui/TextFunctions.kt deleted file mode 100644 index af59535e8..000000000 --- a/src/main/java/io/github/homchom/recode/ui/TextFunctions.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:JvmName("TextFunctions") - -package io.github.homchom.recode.ui - -import io.github.homchom.recode.util.regex.RegexModifier -import io.github.homchom.recode.util.regex.regex -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.MutableComponent -import net.minecraft.network.chat.Style -import net.minecraft.util.FormattedCharSequence - -val FORMATTING_CODE_REGEX = regex { - str("§") - group(RegexModifier.IgnoreCase) { - any("0-9a-fk-o") - or - str("x") - group { - str("§") - any("0-9a-f") - } * 6 - } -} - -operator fun MutableComponent.plusAssign(component: Component) { - append(component) -} - -/** - * Removes all § formatting codes from [componentString]. - */ -fun removeLegacyCodes(componentString: String) = componentString.replace(FORMATTING_CODE_REGEX, "") - -val Component.unstyledString get() = removeLegacyCodes(string) - -infix fun Component.looksLike(other: Component) = - toFlatList(Style.EMPTY) == other.toFlatList(Style.EMPTY) - -infix fun FormattedCharSequence.looksLike(other: FormattedCharSequence): Boolean { - val list = mutableListOf() // even indices are styles; odd indices are code points - accept { _, style, codePoint -> - list += style - list += codePoint - true - } - var index = 0 - val result = other.accept { _, style, codePoint -> - if (index == list.size) return@accept false - style == list[index++] && codePoint == list[index++] - } - return result && index == list.size -} - -fun Component.equalsUnstyled(string: String) = unstyledString == string - -fun Regex.matchEntireUnstyled(text: Component) = matchEntire(text.unstyledString) - -fun Regex.matchesUnstyled(text: Component) = matches(text.unstyledString) \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/Toasts.kt b/src/main/java/io/github/homchom/recode/ui/Toasts.kt index fba95016c..3e093bc63 100644 --- a/src/main/java/io/github/homchom/recode/ui/Toasts.kt +++ b/src/main/java/io/github/homchom/recode/ui/Toasts.kt @@ -2,16 +2,20 @@ package io.github.homchom.recode.ui +import io.github.homchom.recode.ui.text.toVanilla +import net.kyori.adventure.text.Component import net.minecraft.client.Minecraft import net.minecraft.client.gui.components.toasts.SystemToast import net.minecraft.client.gui.components.toasts.SystemToast.SystemToastIds -import net.minecraft.network.chat.Component +/** + * Sends a system toast notification of [type] with [title] and [body]. + */ @JvmOverloads fun Minecraft.sendSystemToast( title: Component, body: Component, type: SystemToastIds = SystemToastIds.PERIODIC_NOTIFICATION ) { - toasts.addToast(SystemToast.multiline(this, type, title, body)) + toasts.addToast(SystemToast.multiline(this, type, title.toVanilla(), body.toVanilla())) } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceBuilder.kt b/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceBuilder.kt new file mode 100644 index 000000000..a2f0597c1 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceBuilder.kt @@ -0,0 +1,161 @@ +package io.github.homchom.recode.ui.text + +import io.github.homchom.recode.render.ColorPalette +import io.github.homchom.recode.render.IntegralColor +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration +import net.minecraft.util.FormattedCharSequence + +/** + * Builds a composite [FormattedCharSequence] by applying [builder]. + * + * Use [text] for higher-level [Component] creation, which supports more than literals. + * + * @see FormattedCharSequenceBuilder + */ +inline fun formattedCharSequence(builder: FormattedCharSequenceBuilder.() -> Unit) = + FormattedCharSequenceBuilder().apply(builder).build() + +/** + * Creates a [StyleWrapper]. + */ +fun style(initial: Style = Style.empty()) = StyleWrapper(initial.toBuilder()) + +/** + * @see forward + * @see backward + * + * @see formattedCharSequence + */ +@JvmInline +value class FormattedCharSequenceBuilder private constructor(private val list: MutableList) { + constructor() : this(mutableListOf()) + + fun build(): FormattedCharSequence = FormattedCharSequence.composite(list) + + /** + * Appends [string] in forward order with [style]. + */ + fun forward(string: String, style: StyleWrapper) { + list += FormattedCharSequence.forward(string, style.build().toVanilla()) + } + + /** + * Appends [string] in backward order with [style]. + */ + fun backward(string: String, style: StyleWrapper) { + list += FormattedCharSequence.backward(string, style.build().toVanilla()) + } + + /** + * Appends the character with code point [code], with [style]. + */ + fun codepoint(code: Int, style: StyleWrapper) { + list += FormattedCharSequence.codepoint(code, style.build().toVanilla()) + } +} + +/** + * A wrapper class for idiomatic [Style] creation. + */ +@Suppress("unused") +@JvmInline +value class StyleWrapper(private val builder: Style.Builder) { + fun build() = builder.build() + + fun black() = color(ColorPalette.BLACK) + + fun darkBlue() = color(ColorPalette.DARK_BLUE) + + fun darkGreen() = color(ColorPalette.DARK_GREEN) + + fun darkAqua() = color(ColorPalette.DARK_AQUA) + + fun darkRed() = color(ColorPalette.DARK_RED) + + fun darkPurple() = color(ColorPalette.DARK_PURPLE) + + fun gold() = color(ColorPalette.GOLD) + + fun gray() = color(ColorPalette.GRAY) + + fun darkGray() = color(ColorPalette.DARK_GRAY) + + fun blue() = color(ColorPalette.BLUE) + + fun green() = color(ColorPalette.GREEN) + + fun aqua() = color(ColorPalette.AQUA) + + fun red() = color(ColorPalette.RED) + + fun lightPurple() = color(ColorPalette.LIGHT_PURPLE) + + fun yellow() = color(ColorPalette.YELLOW) + + fun white() = color(ColorPalette.WHITE) + + fun color(color: IntegralColor) = color(color.toInt()) + + fun color(color: Int) = apply { + builder.color { color } + } + + fun bold() = apply { + builder.decorate(TextDecoration.BOLD) + } + + fun underlined() = apply { + builder.decorate(TextDecoration.UNDERLINED) + } + + fun italic() = apply { + builder.decorate(TextDecoration.ITALIC) + } + + fun strikethrough() = apply { + builder.decorate(TextDecoration.STRIKETHROUGH) + } + + fun obfuscated() = apply { + builder.decorate(TextDecoration.OBFUSCATED) + } + + fun onClick(action: ClickEvent.Action, value: String) = apply { + builder.clickEvent(ClickEvent.clickEvent(action, value)) + } + + val openUrl get() = ClickEvent.Action.OPEN_URL + + val openFile get() = ClickEvent.Action.OPEN_FILE + + val runCommand get() = ClickEvent.Action.RUN_COMMAND + + val suggestCommand get() = ClickEvent.Action.SUGGEST_COMMAND + + val changePage get() = ClickEvent.Action.CHANGE_PAGE + + val copyToClipboard get() = ClickEvent.Action.COPY_TO_CLIPBOARD + + fun onHover(action: HoverEvent.Action, value: T) = apply { + builder.hoverEvent(HoverEvent.hoverEvent(action, value)) + } + + val showText: HoverEvent.Action get() = HoverEvent.Action.SHOW_TEXT + + val showItem: HoverEvent.Action get() = HoverEvent.Action.SHOW_ITEM + + val showEntity: HoverEvent.Action get() = HoverEvent.Action.SHOW_ENTITY + + fun insert(string: String) = apply { + builder.insertion(string) + } + + fun font(key: Key) = apply { + builder.font(key) + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceTransformations.kt b/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceTransformations.kt new file mode 100644 index 000000000..aab0f24a1 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/FormattedCharSequenceTransformations.kt @@ -0,0 +1,25 @@ +package io.github.homchom.recode.ui.text + +import net.minecraft.util.FormattedCharSequence + +/** + * @see CharSequence.replaceRange + */ +fun FormattedCharSequence.replaceRange( + range: IntRange, + replacement: FormattedCharSequence +): FormattedCharSequence { + return FormattedCharSequence { sink -> + var index = 0 + var adjustedIndex = 0 + accept { _, style, codePoint -> + when (index++) { + range.first -> replacement.accept { _, style2, codePoint2 -> + sink.accept(adjustedIndex++, style2, codePoint2) + } + in range -> true + else -> sink.accept(adjustedIndex++, style, codePoint) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/MiniMessageFunctions.kt b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageFunctions.kt new file mode 100644 index 000000000..2b9ffb0a3 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageFunctions.kt @@ -0,0 +1,30 @@ +package io.github.homchom.recode.ui.text + +import net.kyori.adventure.text.minimessage.Context +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver + +// https://github.com/KyoriPowered/adventure/issues/998 +/** + * Escapes [MiniMessage] tags and escape characters in [input]. + * + * @return The escaped string. + */ +fun MiniMessage.escapeAll(input: String) = serialize(literalText(input)) + +/** + * @return A new [TagResolver] that resolves like this TagResolver but excludes [disabledTags]. + */ +fun TagResolver.without(vararg disabledTags: TagResolver) = object : TagResolver { + override fun resolve(name: String, arguments: ArgumentQueue, ctx: Context): Tag? { + if (disabledTags.any { it.has(name) }) return null + return this@without.resolve(name, arguments, ctx) + } + + override fun has(name: String): Boolean { + if (disabledTags.any { it.has(name) }) return false + return this@without.has(name) + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt new file mode 100644 index 000000000..7d0ea4cd5 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt @@ -0,0 +1,130 @@ +package io.github.homchom.recode.ui.text + +import io.github.homchom.recode.ui.text.MiniMessageHighlighter.highlight +import io.github.homchom.recode.util.interpolate +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.Context +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.internal.parser.node.TagNode +import net.kyori.adventure.text.minimessage.internal.parser.node.ValueNode +import net.kyori.adventure.text.minimessage.tag.Inserting +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.minimessage.tag.standard.StandardTags +import net.kyori.adventure.text.minimessage.tree.Node + +/** + * A parser that performs syntax highlighting on [MiniMessage] expressions. + * + * @see highlight + */ +object MiniMessageHighlighter { + // a resolver for tags that determine their own style + private val nonLiteralTags = TagResolver.resolver( + StandardTags.color(), + StandardTags.decorations().without( + StandardTags.decorations(TextDecoration.OBFUSCATED) + ), + StandardTags.reset(), + StandardTags.font(), + StandardTags.gradient(), + StandardTags.rainbow() + ) + + private val standardTags = TagResolver.standard() + + private val instance = MiniMessage.builder().run { + tags(object : TagResolver { + override fun resolve(name: String, arguments: ArgumentQueue, ctx: Context): Tag? { + nonLiteralTags.resolve(name, arguments, ctx)?.let { return it } + if (standardTags.has(name)) return LiteralTag + return null + } + + override fun has(name: String) = standardTags.has(name) || nonLiteralTags.has(name) + }) + build() + } + + private fun overrideStyle(tagName: String, tag: Tag) = when { + StandardTags.gradient().has(tagName) || StandardTags.rainbow().has(tagName) -> "white" + tag == LiteralTag -> "gray" + else -> null + } + + /** + * Performs [MiniMessage] syntax highlighting on [input]. + * + * - Color, decoration, and font tags are styled with their respective style insertions. + * `` is **not** supported. + * - All other valid tags are treated as "literals" and colored gray. + */ + @Suppress("UnstableApiUsage") // TODO: open issue at adventure github + fun highlight(input: String): Component { + // handle the special case of inputs with parser directives + // TODO: determine if there is a better way to do this + val splitByResets = input.split("") + if (splitByResets.size > 1) return text { + for (index in 0..") + } + append(highlight(splitByResets.last())) + } + + fun TagNode.input() = with(token()) { input.substring(startIndex(), endIndex()) } + fun ValueNode.input() = with(token()) { input.substring(startIndex(), endIndex()) } + + // we highlight by building a second MiniMessage expression and deserializing it + // this is easier than reimplementing all tag behavior, e.g. Modifying + val newInput = StringBuilder(input.length) + var inputIndex = 0 + + fun buildNewInput(node: Node) { + val style = when (node) { + is TagNode -> { + val tagString = node.input() + inputIndex += tagString.length + val styleOverride = overrideStyle(node.name(), node.tag()) + ?: node.parts().joinToString(":") + newInput.appendEscapedTag(tagString, styleOverride) + newInput.append(tagString) + styleOverride + } + is ValueNode -> { + val value = node.input() + inputIndex += value.length + newInput.append(instance.escapeAll(value)) + null + } + else -> null + } + + for (child in node.children()) buildNewInput(child) + + // handle closing tags (which are optional) + if (node is TagNode) { + val closing = "" + if (input.startsWith(closing, inputIndex)) { + inputIndex += closing.length + newInput.append(closing) + newInput.appendEscapedTag(closing, style!!) + } + } + } + + buildNewInput(instance.deserializeToTree(input)) + return instance.deserialize(newInput.toString()) + } + + private fun StringBuilder.appendEscapedTag(tagString: String, style: String) { + interpolate("<", style, ">", instance.escapeAll(tagString), "") + } + + // an empty tag and a marker used to identify literals + private data object LiteralTag : Inserting { + override fun value() = emptyText() + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/TextBuilders.kt b/src/main/java/io/github/homchom/recode/ui/text/TextBuilders.kt new file mode 100644 index 000000000..c254bf8f3 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/TextBuilders.kt @@ -0,0 +1,91 @@ +package io.github.homchom.recode.ui.text + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.ComponentBuilder +import net.kyori.adventure.text.ComponentLike + +typealias TextScope = TextBuilder.() -> Unit + +/** + * Creates a translated [Component] with [key], [args], and [style]. + */ +fun translatedText( + key: String, + style: StyleWrapper = style(), + args: Array = emptyArray() +): Component { + return Component.translatable(key, style.build(), *args) +} + +/** + * Creates a [Component] with literal [contents] and [style]. + */ +fun literalText(contents: Any, style: StyleWrapper = style()) = + Component.text(contents.toString(), style.build()) + +/** + * Creates an empty [Component] with [style]. + */ +fun emptyText(style: StyleWrapper = style()) = + Component.empty().style(style.build()) + +/** + * Builds a [Component] by adding [style] to [root] and applying [builder]. + * + * Use [translatedText] and [literalText] when applicable, as their output is more optimized. + * Use [formattedCharSequence] if all text is literal and a [Component] representation is not necessary, as it + * is the most optimized. + * + * @see TextBuilder + */ +inline fun text( + style: StyleWrapper = style(), + root: ComponentBuilder<*, *> = Component.text(), + builder: TextScope +): Component { + return root.style(style.build()) + .let(::TextBuilder) + .apply(builder) + .build() +} + +/** + * @see translate + * @see literal + * + * @see text + */ +@JvmInline +value class TextBuilder(val raw: ComponentBuilder<*, *> = Component.text()) { + fun build(): Component = raw.build() + + /** + * Appends [translatedText] with [key], [args], and [style] and applies [builder] to it. + */ + inline fun translate( + key: String, + style: StyleWrapper = style(), + args: Array = emptyArray(), + builder: TextScope = {} + ) { + raw.append(text(style, Component.translatable(key, *args).toBuilder(), builder)) + } + + /** + * Appends [literalText] with [string] and [style] and applies [builder] to it. + */ + inline fun literal( + string: String, + style: StyleWrapper = style(), + builder: TextScope = {} + ) { + raw.append(text(style, Component.text(string).toBuilder(), builder)) + } + + /** + * Appends a pre-existing [component]. + */ + fun append(component: Component) { + raw.append(component) + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt b/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt new file mode 100644 index 000000000..5f0b2d3a1 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt @@ -0,0 +1,81 @@ +@file:JvmName("TextFunctions") + +package io.github.homchom.recode.ui.text + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.minecraft.util.FormattedCharSequence + +/** + * @return A new [Component] created by merging this Component's style with [style], using [strategy] and [merges]. + */ +fun Component.mergeStyle( + style: Style, + strategy: Style.Merge.Strategy = Style.Merge.Strategy.ALWAYS, + merges: Set = Style.Merge.all() +): Component { + return style(style().merge(style, strategy, merges)) +} + +/** + * Returns a flattened [Sequence] of this [Component]'s nodes, where parent and child styles are recursively merged. + */ +fun Component.asFlatSequence(): Sequence = sequence { + yield(this@asFlatSequence) + for (child in children()) { + val merged = mergeStyle(child) + yieldAll(merged.asFlatSequence()) + } +} + +/** + * @return Whether this [Component] equals [other] when flattened. + */ +infix fun Component.looksLike(other: Component) = asFlatSequence() == other.asFlatSequence() + +/** + * @return Whether this [FormattedCharSequence] and [other] yield the same styles and code points. + */ +infix fun FormattedCharSequence.looksLike(other: FormattedCharSequence): Boolean { + val list = mutableListOf() // even indices are styles; odd indices are code points + accept { _, style, codePoint -> + list += style + list += codePoint + true + } + var index = 0 + val result = other.accept { _, style, codePoint -> + if (index == list.size) return@accept false + style == list[index++] && codePoint == list[index++] + } + return result && index == list.size +} + +/** + * @return A plain text representation of this [Component]. + * + * @see PlainTextComponentSerializer + */ +val Component.plainText get() = + removeLegacyCodes(PlainTextComponentSerializer.plainText().serialize(this)) + +/** + * @return Whether this [Component]'s [plainText] equals [string]. + */ +fun Component.equalsPlain(string: String) = plainText == string + +/** + * @return Whether this [Component]'s [plainText] equals [other]'s plain text. + */ +fun Component.equalsPlain(other: Component) = equalsPlain(other.plainText) + +/** + * Attempts to match [text]'s entire [plainText] against this [Regex] pattern. + */ +fun Regex.matchEntirePlain(text: Component) = matchEntire(text.plainText) + +/** + * @return Whether [text]'s entire [plainText] matches this [Regex] pattern. + */ +fun Regex.matchesPlain(text: Component) = matches(text.plainText) \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/ui/text/TextInterop.kt b/src/main/java/io/github/homchom/recode/ui/text/TextInterop.kt new file mode 100644 index 000000000..5a9fd14ec --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/text/TextInterop.kt @@ -0,0 +1,63 @@ +@file:JvmName("TextInterop") + +package io.github.homchom.recode.ui.text + +import io.github.homchom.recode.util.regex.RegexModifier +import io.github.homchom.recode.util.regex.regex +import net.kyori.adventure.platform.fabric.FabricClientAudiences +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.Style +import net.minecraft.network.chat.FormattedText.StyledContentConsumer +import net.minecraft.util.FormattedCharSequence +import net.minecraft.util.StringDecomposer +import java.util.* + +typealias VanillaComponent = net.minecraft.network.chat.Component +typealias VanillaStyle = net.minecraft.network.chat.Style + +fun Component.toVanilla() = FabricClientAudiences.of().toNative(this) + +fun VanillaStyle.toAdventure() = VanillaComponent.empty() + .withStyle(this) + .asComponent() + .style() + +fun Style.toVanilla(): VanillaStyle = Component.text() + .style(this) + .build() + .toVanilla() + .style + +fun Component.toFormattedCharSequence(inLanguageOrder: Boolean = true): FormattedCharSequence { + if (inLanguageOrder) return toVanilla().visualOrderText + + return FormattedCharSequence { sink -> + val consumer = StyledContentConsumer { style, string -> + if (StringDecomposer.iterateFormatted(string, style, sink)) { + Optional.empty() + } else { + Optional.of(Unit) + } + } + // isEmpty, not isPresent; the Minecraft source is bugged + toVanilla().visit(consumer, VanillaStyle.EMPTY).isEmpty + } +} + +private val legacyCodeRegex = regex { + str("§") + group(RegexModifier.IgnoreCase) { + any("0-9a-fk-o") + or + str("x") + group { + str("§") + any("0-9a-f") + } * 6 + } +} + +/** + * Removes all § formatting codes from [componentString]. + */ +fun removeLegacyCodes(componentString: String) = componentString.replace(legacyCodeRegex, "") \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/BasicTypeExtensions.kt b/src/main/java/io/github/homchom/recode/util/BasicTypeExtensions.kt index fed6d6eb5..f2a312fad 100644 --- a/src/main/java/io/github/homchom/recode/util/BasicTypeExtensions.kt +++ b/src/main/java/io/github/homchom/recode/util/BasicTypeExtensions.kt @@ -2,4 +2,14 @@ package io.github.homchom.recode.util -fun Boolean.unitOrNull() = if (this) Unit else null \ No newline at end of file +/** + * @return [Unit] if this boolean is true, or `null` otherwise. + */ +fun Boolean.unitOrNull() = if (this) Unit else null + +/** + * Appends multiple [substrings] to this [StringBuilder]. + */ +fun StringBuilder.interpolate(vararg substrings: String) = apply { + for (substring in substrings) append(substring) +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/CommonOptIns.kt b/src/main/java/io/github/homchom/recode/util/CommonOptIns.kt index 64a2a0acf..d0c8d2ca2 100644 --- a/src/main/java/io/github/homchom/recode/util/CommonOptIns.kt +++ b/src/main/java/io/github/homchom/recode/util/CommonOptIns.kt @@ -1,5 +1,5 @@ package io.github.homchom.recode.util -@RequiresOptIn("This inline class constructor is public for inlining but is essentially private; " + - "use the appropriate factory function instead") -annotation class ExposedInline \ No newline at end of file +@Target(AnnotationTarget.CONSTRUCTOR) +@RequiresOptIn("This exception type should generally only be caught, not thrown") +annotation class ThrownInternally \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/Computations.kt b/src/main/java/io/github/homchom/recode/util/Computations.kt index 5ae083d77..d71a12f14 100644 --- a/src/main/java/io/github/homchom/recode/util/Computations.kt +++ b/src/main/java/io/github/homchom/recode/util/Computations.kt @@ -3,6 +3,34 @@ package io.github.homchom.recode.util import io.github.homchom.recode.util.Computation.Failure import io.github.homchom.recode.util.Computation.Success +// TODO: probably remove most of this + +/** + * A computed result that can either be a [Success] or [Failure], like the + * [Either](https://docs.rs/either/latest/either/enum.Either.html) type found in many functional languages. + */ +sealed interface Computation { + fun successOrNull() = this as? Success + fun failureOrNull() = this as? Failure + + class Success(override val value: T) : Computation, InvokableWrapper + class Failure(override val value: T) : Computation, InvokableWrapper +} + +inline fun Computation.map(transform: (S) -> R): Computation = + when (this) { + is Success -> Success(transform(value)) + is Failure -> Failure(value) + } + +/** + * Computes the result of [block] with type [S], with the ability to short-circuit across functions with + * [ComputeScope.fail]. + * + * **[ComputeScope] should not be leaked.** See [FailScope] for more details. + */ +inline fun compute(block: ComputeScope.() -> S) = computeIn(ComputeScope(), block) + /** * Computes the nullable result of [block] with the ability to short-circuit across functions with * both [NullableScope.fail] and [NullPointerException]. @@ -19,28 +47,20 @@ inline fun computeNullable(block: NullableScope.() -> T?): T? { } /** - * @see computeNullable + * @see compute */ -sealed interface NullableScope : FailScope { - fun fail(): Nothing = fail(null) - - companion object Instance : NullableScope +@JvmInline +value class ComputeScope private constructor(private val unit: NullableScope) : FailScope { + constructor() : this(NullableScope) } /** - * Computes the result of [block] with type [S], with the ability to short-circuit across functions with - * [ComputeScope.fail]. - * - * **[ComputeScope] should not be leaked.** See [FailScope] for more details. + * @see computeNullable */ -inline fun compute(block: ComputeScope.() -> S) = computeIn(ComputeScope(), block) +sealed interface NullableScope : FailScope { + fun fail(): Nothing = fail(null) -/** - * @see compute - */ -@JvmInline -value class ComputeScope private constructor(private val unit: NullableScope) : FailScope { - constructor() : this(NullableScope) + companion object Instance : NullableScope } /** @@ -57,15 +77,6 @@ inline fun > computeIn(scope: C, block: C.() -> S): Compu } } -/** - * A computed result that can either be a [Success] or [Failure], like the - * [Either](https://docs.rs/either/latest/either/enum.Either.html) type found in many functional languages. - */ -sealed interface Computation { - class Success(override val value: T) : Computation, InvokableWrapper - class Failure(override val value: T) : Computation, InvokableWrapper -} - /** * A computation scope that can [fail], throwing a [FailureException]. Because such exceptions are designed to * be automatically caught by computation functions, derived types with supertype [FailScope] should **never** be diff --git a/src/main/java/io/github/homchom/recode/util/GenericWrappers.kt b/src/main/java/io/github/homchom/recode/util/GenericWrappers.kt index 86c49f46a..c20173b8d 100644 --- a/src/main/java/io/github/homchom/recode/util/GenericWrappers.kt +++ b/src/main/java/io/github/homchom/recode/util/GenericWrappers.kt @@ -7,11 +7,21 @@ data class Case(val content: T) { companion object { val ofNull = Case(null) } + + /** + * Unwraps and returns [content]. + */ + operator fun invoke() = content } /** * @see Case */ -data class MutableCase(var content: T) +data class MutableCase(var content: T) { + /** + * Unwraps and returns [content]. + */ + operator fun invoke() = content +} inline fun T.encase(block: (T) -> R) = Case(block(this)) \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/Matcher.kt b/src/main/java/io/github/homchom/recode/util/Matcher.kt index b8169009d..d4add1a01 100644 --- a/src/main/java/io/github/homchom/recode/util/Matcher.kt +++ b/src/main/java/io/github/homchom/recode/util/Matcher.kt @@ -13,13 +13,13 @@ fun interface Matcher { } /** - * Creates and returns a [MatcherList] with [initialPredicates]. + * Creates a [MatcherList] with [initialPredicates]. */ fun matcherOf(vararg initialPredicates: Matcher) = MatcherList().apply { addAll(initialPredicates) } /** - * Creates and returns a [MatcherList] with [initialPredicates]. + * Creates a [MatcherList] with [initialPredicates]. */ fun > matcherOf(initialPredicates: Collection) = MatcherList().apply { addAll(initialPredicates) } diff --git a/src/main/java/io/github/homchom/recode/util/Math.kt b/src/main/java/io/github/homchom/recode/util/Math.kt index 9d55f8678..d5e45b37d 100644 --- a/src/main/java/io/github/homchom/recode/util/Math.kt +++ b/src/main/java/io/github/homchom/recode/util/Math.kt @@ -1,26 +1,23 @@ package io.github.homchom.recode.util /** - * Returns the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) + * @return the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) * of this Int and [other]. */ infix fun Int.mod(other: Int) = Math.floorMod(this, other) /** - * Returns the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) + * @return the [floor modulo](https://en.wikipedia.org/wiki/Modulo#Variants_of_the_definition) * of this Long and [other]. */ infix fun Long.mod(other: Long) = Math.floorMod(this, other) /** - * Returns the greatest common factor of [a] and [b] using the + * @return the greatest common factor of [a] and [b] using the * [Euclidean algorithm](https://en.wikipedia.org/wiki/Euclidean_algorithm). */ -tailrec fun greatestCommonFactor(a: Int, b: Int): Int = when { - a == 0 -> b - b == 0 -> a - else -> greatestCommonFactor(b, a % b) -} +tailrec fun greatestCommonFactor(a: Int, b: Int): Int = + if (b == 0) a else greatestCommonFactor(b, a % b) /** * A [mixed number](https://en.wikipeedia.org/wiki/Fraction#Forms_of_fractions) with [Int] components. diff --git a/src/main/java/io/github/homchom/recode/util/PrevCached.kt b/src/main/java/io/github/homchom/recode/util/PrevCached.kt index 0305aa50d..afc09b250 100644 --- a/src/main/java/io/github/homchom/recode/util/PrevCached.kt +++ b/src/main/java/io/github/homchom/recode/util/PrevCached.kt @@ -1,7 +1,7 @@ package io.github.homchom.recode.util /** - * Returns a [PrevCached] that returns results produced by [builder]. + * @return a [PrevCached] that returns results produced by [builder]. */ fun cachePrevious(builder: (T) -> R): PrevCached = PrevCachedBuilder(builder) diff --git a/src/main/java/io/github/homchom/recode/util/TraitInterfaces.kt b/src/main/java/io/github/homchom/recode/util/TraitInterfaces.kt index 53aecf953..8a672d175 100644 --- a/src/main/java/io/github/homchom/recode/util/TraitInterfaces.kt +++ b/src/main/java/io/github/homchom/recode/util/TraitInterfaces.kt @@ -11,7 +11,7 @@ interface KeyHashable { /** * A wrapper for a [value] of type [T] that can be unboxed with [invoke]. */ -interface InvokableWrapper { +interface InvokableWrapper { val value: T /** diff --git a/src/main/java/io/github/homchom/recode/util/collections/CollectionExtensions.kt b/src/main/java/io/github/homchom/recode/util/collections/CollectionExtensions.kt index 69d3fc2c4..a67388e31 100644 --- a/src/main/java/io/github/homchom/recode/util/collections/CollectionExtensions.kt +++ b/src/main/java/io/github/homchom/recode/util/collections/CollectionExtensions.kt @@ -1,32 +1,11 @@ package io.github.homchom.recode.util.collections -/** - * Adds and returns [element] to this collection. - */ -fun MutableCollection.with(element: S) = element.also(::add) - /** * Maps this list into an [Array]. * * @see map */ -inline fun Collection.mapToArray(transform: (T) -> R) = - with(iterator()) { - Array(size) { transform(next()) } - } - -/** - * Flattens an [Iterable] of [Iterable]s vertically. For each index n starting at 0, the nth element of each - * collection is added if it exists, and a new [List] is returned. - */ -fun Iterable>.verticalFlatten() = map { it.iterator() }.let { outer -> - buildList { - do { - val columnNotEmpty = outer.any { - val hasNext = it.hasNext() - if (hasNext) add(it.next()) - hasNext - } - } while (columnNotEmpty) - } +inline fun Collection.mapToArray(transform: (T) -> R): Array { + val iterator = iterator() + return Array(size) { transform(iterator.next()) } } \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/mixin/MixinCustomField.kt b/src/main/java/io/github/homchom/recode/util/mixin/MixinCustomField.kt deleted file mode 100644 index 8dfdd1668..000000000 --- a/src/main/java/io/github/homchom/recode/util/mixin/MixinCustomField.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.homchom.recode.util.mixin - -import com.google.common.collect.MapMaker - -// TODO: test this -/** - * An optimized wrapper for a weak identity hash map, used to augment custom fields into Minecraft classes - * with mixins. - * 1. For best performance, this should only be used at most once per mixin; use a class type when multiple - * fields are desired. - * 2. This is not necessary to use when the target class is reasonably assumed to be a singleton. - */ -class MixinCustomField(private val default: () -> T) { - private lateinit var valueMap: MutableMap - - private var singletonInstance: V? = null // invalidated iff valueMap is initialized - private var singletonValue: T? = default() // invalidated iff valueMap is initialized - - fun get(instance: V) = if (::valueMap.isInitialized) { - valueMap.getOrPut(instance, default) - } else { - singletonValue!! - } - - @Suppress("UNCHECKED_CAST") - fun set(instance: V, value: T) { - when { - ::valueMap.isInitialized -> valueMap[instance] = value - singletonInstance != null && instance != singletonInstance -> { - valueMap = MapMaker().weakKeys().makeMap() // identity-based - valueMap[singletonInstance!!] = singletonValue as T - valueMap[instance] = value - singletonInstance = null - singletonValue = null - } - else -> { - singletonInstance = instance - singletonValue = value - } - } - } -} \ No newline at end of file diff --git a/src/main/resources/recode.mixins.json b/src/main/resources/recode.mixins.json index 71e75709d..7085c9879 100644 --- a/src/main/resources/recode.mixins.json +++ b/src/main/resources/recode.mixins.json @@ -5,6 +5,7 @@ "mixins": [], "client": [ "MMinecraft", + "game.ItemStackAccessor", "multiplayer.MChatListener", "multiplayer.MClientPacketListener", "multiplayer.MConnection", @@ -15,7 +16,8 @@ "render.MLevelRenderer", "render.MWindow", "render.chat.MChatComponent", - "render.chat.MChatScreen" + "render.chat.MChatScreen", + "render.chat.MCommandSuggestions" ], "server": [], "plugin": "io.github.homchom.recode.mixin.MixinPluginRecode",