summaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/AllModules.kt17
-rw-r--r--src/main/kotlin/ChatStore.kt53
-rw-r--r--src/main/kotlin/Constants.kt12
-rw-r--r--src/main/kotlin/UltraNotifier.kt16
-rw-r--r--src/main/kotlin/UltraNotifierEntryPoint.kt4
-rw-r--r--src/main/kotlin/commands/BrigadierPatchbay.kt119
-rw-r--r--src/main/kotlin/commands/Commands.kt51
-rw-r--r--src/main/kotlin/datamodel/ChatType.kt175
-rw-r--r--src/main/kotlin/event/ChatGuiLineEvent.kt8
-rw-r--r--src/main/kotlin/event/CommandRegistrationEvent.kt10
-rw-r--r--src/main/kotlin/event/RegistrationFinishedEvent.kt6
-rw-r--r--src/main/kotlin/event/SubscriptionTarget.kt5
-rw-r--r--src/main/kotlin/event/TickEvent.kt26
-rw-r--r--src/main/kotlin/event/UltraEvent.kt6
-rw-r--r--src/main/kotlin/event/UltraNotifierEvents.kt7
-rw-r--r--src/main/kotlin/event/VisibleChatMessageAddedEvent.kt17
-rw-r--r--src/main/kotlin/gui/ChatUi.kt91
-rw-r--r--src/main/kotlin/gui/MessageUi.kt28
-rw-r--r--src/main/kotlin/gui/ScreenUtil.kt19
-rw-r--r--src/main/kotlin/util/GsonUtil.kt55
-rw-r--r--src/main/kotlin/util/IdentityCharacteristics.kt15
-rw-r--r--src/main/kotlin/util/KSerializable.kt112
-rw-r--r--src/main/kotlin/util/identifierutil.kt11
-rw-r--r--src/main/kotlin/util/iterutil.kt36
-rw-r--r--src/main/kotlin/util/minecrat/MC.kt11
-rw-r--r--src/main/kotlin/util/minecrat/TextUtil.kt70
-rw-r--r--src/main/kotlin/util/minecrat/infer.kt22
-rw-r--r--src/main/kotlin/util/render/ScreenRenderUtils.kt135
28 files changed, 1111 insertions, 26 deletions
diff --git a/src/main/kotlin/AllModules.kt b/src/main/kotlin/AllModules.kt
new file mode 100644
index 0000000..01cfb36
--- /dev/null
+++ b/src/main/kotlin/AllModules.kt
@@ -0,0 +1,17 @@
+package moe.nea.ultranotifier
+
+import moe.nea.ultranotifier.commands.Commands
+import moe.nea.ultranotifier.datamodel.ChatCategoryArbiter
+import moe.nea.ultranotifier.event.SubscriptionTarget
+import moe.nea.ultranotifier.event.TickEvent
+import moe.nea.ultranotifier.gui.ScreenUtil
+
+object AllModules {
+ val allModules: List<SubscriptionTarget> = listOf(
+ ChatStore,
+ Commands,
+ ScreenUtil,
+ TickEvent,
+ ChatCategoryArbiter,
+ )
+}
diff --git a/src/main/kotlin/ChatStore.kt b/src/main/kotlin/ChatStore.kt
new file mode 100644
index 0000000..4cc6ee1
--- /dev/null
+++ b/src/main/kotlin/ChatStore.kt
@@ -0,0 +1,53 @@
+package moe.nea.ultranotifier
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import moe.nea.ultranotifier.commands.UltraCommandSource
+import moe.nea.ultranotifier.commands.literalText
+import moe.nea.ultranotifier.event.ChatGuiLineEvent
+import moe.nea.ultranotifier.event.CommandRegistrationEvent
+import moe.nea.ultranotifier.event.PacketChatLineEvent
+import moe.nea.ultranotifier.event.SubscriptionTarget
+import moe.nea.ultranotifier.event.UltraSubscribe
+import moe.nea.ultranotifier.gui.MessageUi
+import moe.nea.ultranotifier.gui.ScreenUtil
+import moe.nea.ultranotifier.util.IdentityCharacteristics
+import net.minecraft.text.Text
+
+object ChatStore : SubscriptionTarget {
+
+ data class ChatLine(
+ val text: Text,
+ var fromPacket: Boolean = false,
+ var isDisplayed: Boolean = false,
+ )
+
+ val allLines = object : LinkedHashMap<IdentityCharacteristics<Text>, ChatLine>() {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry<IdentityCharacteristics<Text>, ChatLine>?): Boolean {
+ return size > 500 // TODO: config
+ }
+ }
+
+ fun insertChatLine(text: Text): ChatLine {
+ return allLines.getOrPut(IdentityCharacteristics(text)) { ChatLine(text) }
+ }
+
+ @UltraSubscribe
+ fun onMessageDisplayed(event: ChatGuiLineEvent) {
+ insertChatLine(event.component).isDisplayed = true
+ }
+
+ @UltraSubscribe
+ fun registerCommands(event: CommandRegistrationEvent) {
+ event.dispatcher.register(LiteralArgumentBuilder.literal<UltraCommandSource?>("ultranotifier")
+ .executes {
+ it.source.sendFeedback(literalText("Opening screen"))
+ ScreenUtil.openScreen = (MessageUi())
+ 0
+ })
+ }
+
+ @UltraSubscribe
+ fun onMessageReceived(event: PacketChatLineEvent) {
+ insertChatLine(event.component).fromPacket = true
+ }
+}
diff --git a/src/main/kotlin/Constants.kt b/src/main/kotlin/Constants.kt
index a1903ce..46d4236 100644
--- a/src/main/kotlin/Constants.kt
+++ b/src/main/kotlin/Constants.kt
@@ -1,6 +1,6 @@
package moe.nea.ultranotifier
-
+// TODO: blossom this shit
object Constants {
const val MOD_ID = "ultranotifier"
const val VERSION = "1.0.0"
@@ -23,5 +23,15 @@ object Constants {
"1.20.6"
//#elseif MC == 11404
//$$ "1.14.4"
+//#elseif MC == 11202
+//$$ "1.12.2"
+//#elseif MC == 11605
+//$$ "1.16.5"
+//#elseif MC == 11602
+//$$ "1.16.2"
+//#elseif MC == 12100
+//$$ "1.21"
+//#elseif MC == 12101
+//$$ "1.21.1"
//#endif
}
diff --git a/src/main/kotlin/UltraNotifier.kt b/src/main/kotlin/UltraNotifier.kt
index dce1fda..ddf8e87 100644
--- a/src/main/kotlin/UltraNotifier.kt
+++ b/src/main/kotlin/UltraNotifier.kt
@@ -1,12 +1,14 @@
package moe.nea.ultranotifier
-import moe.nea.ultranotifier.commands.Commands
+import moe.nea.ultranotifier.event.RegistrationFinishedEvent
+import moe.nea.ultranotifier.event.UltraEvent
+import moe.nea.ultranotifier.event.UltraNotifierEvents
import moe.nea.ultranotifier.init.NeaMixinConfig
import java.io.File
object UltraNotifier {
val logger =
-//#if MC <= 11404
+//#if MC <= 1.17
//$$ org.apache.logging.log4j.LogManager.getLogger("UltraNotifier")!!
//#else
org.slf4j.LoggerFactory.getLogger("UltraNotifier")!!
@@ -17,7 +19,15 @@ object UltraNotifier {
for (mixinPlugin in NeaMixinConfig.getMixinPlugins()) {
logger.info("Loaded ${mixinPlugin.mixins.size} mixins for ${mixinPlugin.mixinPackage}.")
}
- Commands.init()
+ logger.info("All modules: ${AllModules.allModules}")
+ AllModules.allModules.forEach {
+ logger.info("Registering $it")
+ UltraNotifierEvents.register(it)
+ it.init()
+ }
+
+ RegistrationFinishedEvent().post()
+
}
val configFolder = File("config/ultra-notifier").also {
diff --git a/src/main/kotlin/UltraNotifierEntryPoint.kt b/src/main/kotlin/UltraNotifierEntryPoint.kt
index 42ae064..aa84dc5 100644
--- a/src/main/kotlin/UltraNotifierEntryPoint.kt
+++ b/src/main/kotlin/UltraNotifierEntryPoint.kt
@@ -3,14 +3,14 @@ package moe.nea.ultranotifier
//#if FORGE
//$$import net.minecraftforge.fml.common.Mod
//$$
-//#if MC == 10809
+//#if MC < 1.13
//$$import net.minecraftforge.fml.common.event.FMLInitializationEvent
//$$@Mod(modid = Constants.MOD_ID, version = Constants.VERSION, useMetadata = true)
//#else
//$$@Mod(Constants.MOD_ID)
//#endif
//$$class UltraNotifierEntryPoint {
-//#if MC == 10809
+//#if MC < 1.13
//$$ @Mod.EventHandler
//$$ fun onInit(@Suppress("UNUSED_PARAMETER") event: FMLInitializationEvent) {
//#else
diff --git a/src/main/kotlin/commands/BrigadierPatchbay.kt b/src/main/kotlin/commands/BrigadierPatchbay.kt
new file mode 100644
index 0000000..b88d0e7
--- /dev/null
+++ b/src/main/kotlin/commands/BrigadierPatchbay.kt
@@ -0,0 +1,119 @@
+package moe.nea.ultranotifier.commands
+
+//#if MC < 1.16
+//$$import com.mojang.brigadier.CommandDispatcher
+//$$import com.mojang.brigadier.builder.LiteralArgumentBuilder
+//$$import com.mojang.brigadier.tree.CommandNode
+//$$import moe.nea.ultranotifier.event.CommandRegistrationEvent
+//$$import moe.nea.ultranotifier.event.RegistrationFinishedEvent
+//$$import moe.nea.ultranotifier.event.UltraNotifierEvents
+//$$import moe.nea.ultranotifier.event.UltraSubscribe
+//$$import moe.nea.ultranotifier.event.SubscriptionTarget
+//$$import moe.nea.ultranotifier.mixin.AccessorCommandHandler
+//$$import net.minecraft.command.CommandBase
+//$$import net.minecraft.command.CommandHandler
+//$$import net.minecraft.command.ICommandSender
+//$$import net.minecraft.server.MinecraftServer
+//$$import net.minecraft.util.text.ITextComponent
+//$$import net.minecraftforge.client.ClientCommandHandler
+//$$
+//$$fun CommandHandler.getCommandSet() = (this as AccessorCommandHandler).commandSet_ultraNotifier
+//$$
+//$$class BridgedCommandSource(
+//$$ val sender: ICommandSender
+//$$) : UltraCommandSource {
+//$$ override fun sendFeedback(text: ITextComponent) {
+//$$ sender.sendMessage(text)
+//$$ }
+//$$}
+//$$
+//$$class BrigadierCommand(
+//$$ val dispatcher: CommandDispatcher<UltraCommandSource>,
+//$$ val node: CommandNode<UltraCommandSource>
+//$$) : CommandBase() {
+//#if MC >= 1.12
+//$$ override fun checkPermission(server: MinecraftServer, sender: ICommandSender): Boolean {
+//$$ return true
+//$$ }
+//#else
+//$$ override fun canCommandSenderUseCommand(sender: ICommandSender): Boolean {
+//$$ return true
+//$$ }
+//#endif
+//$$
+//$$ override fun getName(): String {
+//$$ return node.name
+//$$ }
+//$$
+//$$ override fun getUsage(sender: ICommandSender): String {
+//$$ return ""
+//$$ }
+//$$
+//$$ private fun getCommandLineText(args: Array<out String>) = "${node.name} ${args.joinToString(" ")}".trim()
+//$$
+//$$
+//#if MC < 1.12
+//$$ override fun processCommand(sender: ICommandSender, args: Array<out String>) {
+//#else
+//$$ override fun execute(server: MinecraftServer, sender: ICommandSender, args: Array<out String>) {
+//#endif
+//$$ val source = BridgedCommandSource(sender)
+//$$ val results = dispatcher.parse(getCommandLineText(args), source)
+//$$ kotlin.runCatching {
+//$$ dispatcher.execute(results)
+//$$ Unit
+//$$ }.recoverCatching {
+//$$ source.sendFeedback(literalText("Could not execute ultra command: ${it.message}"))
+//$$ }
+//$$ }
+//$$}
+//$$
+//$$object BrigadierPatchbay : SubscriptionTarget {
+//$$
+//$$ @UltraSubscribe
+//$$ fun onAfterRegistration(event: RegistrationFinishedEvent) {
+//$$ fullReload()
+//$$ }
+//$$
+//$$ @UltraSubscribe
+//$$ fun onCommands(event: CommandRegistrationEvent) {
+//$$ event.dispatcher
+//$$ .register(LiteralArgumentBuilder.literal<UltraCommandSource>("reloadcommands")
+//$$ .executes {
+//$$ it.source.sendFeedback(literalText("Reloading commands"))
+//$$ fullReload()
+//$$ it.source.sendFeedback(literalText("Reload completed"))
+//$$ 0
+//$$ })
+//$$ }
+//$$
+//$$ fun fullReload() {
+//$$ val handler = ClientCommandHandler.instance
+//$$ unpatch(handler)
+//$$ val dispatcher = createDispatcher()
+//$$ UltraNotifierEvents.post(CommandRegistrationEvent(dispatcher))
+//$$ patch(handler, dispatcher)
+//$$ }
+//$$
+//$$ fun createDispatcher() = CommandDispatcher<UltraCommandSource>()
+//$$
+//$$ fun unpatch(handler: CommandHandler) {
+//$$ handler.getCommandSet()
+//$$ .removeIf {
+//$$ it is BrigadierCommand
+//$$ }
+//$$ handler.commands.entries
+//$$ .removeIf {
+//$$ it.value is BrigadierCommand
+//$$ }
+//$$ }
+//$$
+//$$ fun patch(handler: CommandHandler, dispatcher: CommandDispatcher<UltraCommandSource>) {
+//$$ dispatcher.root.children
+//$$ .map { BrigadierCommand(dispatcher, it) }
+//$$ .forEach(handler::registerCommand)
+//$$ }
+//$$}
+//#endif
+
+
diff --git a/src/main/kotlin/commands/Commands.kt b/src/main/kotlin/commands/Commands.kt
index 75047cd..5975bb2 100644
--- a/src/main/kotlin/commands/Commands.kt
+++ b/src/main/kotlin/commands/Commands.kt
@@ -1,9 +1,11 @@
package moe.nea.ultranotifier.commands
-import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import moe.nea.ultranotifier.UltraNotifier
+import moe.nea.ultranotifier.event.CommandRegistrationEvent
+import moe.nea.ultranotifier.event.SubscriptionTarget
import moe.nea.ultranotifier.event.UltraNotifierEvents
+import moe.nea.ultranotifier.event.UltraSubscribe
import net.minecraft.text.Text
interface CustomSource {
@@ -14,35 +16,50 @@ interface CustomSource {
typealias UltraCommandSource =
//#if FORGE
//$$ CustomSource
-//#else
+//#elseif MC > 1.18
net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
-
+//#else
+//$$ net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource
//#endif
+fun translatableText(key: String, vararg args: String) =
+//#if MC > 1.17
+ Text.translatable(key, *args)
+//#else
+//$$ net.minecraft.text.TranslatableText(key, *args)
+//#endif
+
fun literalText(string: String): Text =
-//#if MC >= 11400
+//#if MC > 1.17
Text.literal(string)
//#else
-//$$ net.minecraft.util.ChatComponentText(string)
+//$$ net.minecraft.text.LiteralText(string)
//#endif
-object Commands {
- fun registerAll(dispatcher: CommandDispatcher<UltraCommandSource>) {
- dispatcher.register(LiteralArgumentBuilder.literal<UltraCommandSource>("hello")
- .executes {
- it.source.sendFeedback(literalText("Hello World"))
- 0
- })
+object Commands : SubscriptionTarget {
+ @UltraSubscribe
+ fun registerTestCommand(event: CommandRegistrationEvent) {
+ event.dispatcher.register(LiteralArgumentBuilder.literal<UltraCommandSource>("hello")
+ .executes {
+ it.source.sendFeedback(literalText("Hello World"))
+ 0
+ })
}
- fun init() {
- UltraNotifierEvents.register(this)
+//#if MC <= 1.18 && FABRIC
+//$$ @UltraSubscribe
+//$$ fun registerEverythingOnce(event: moe.nea.ultranotifier.event.RegistrationFinishedEvent) {
+//$$ CommandRegistrationEvent(net.fabricmc.fabric.api.client.command.v1.ClientCommandManager.DISPATCHER).post()
+//$$ }
+//#endif
+
+ override fun init() {
//#if FORGE
-//$$
-//#else
+//$$ UltraNotifierEvents.register(BrigadierPatchbay)
+//#elseif MC > 1.18
net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback.EVENT.register { dispatcher, registryAccess ->
- registerAll(dispatcher)
+ UltraNotifierEvents.post(CommandRegistrationEvent(dispatcher))
}
//#endif
UltraNotifier.logger.info("Initialized command subsystem")
diff --git a/src/main/kotlin/datamodel/ChatType.kt b/src/main/kotlin/datamodel/ChatType.kt
new file mode 100644
index 0000000..47abe2a
--- /dev/null
+++ b/src/main/kotlin/datamodel/ChatType.kt
@@ -0,0 +1,175 @@
+package moe.nea.ultranotifier.datamodel
+
+import moe.nea.ultranotifier.UltraNotifier
+import moe.nea.ultranotifier.event.SubscriptionTarget
+import moe.nea.ultranotifier.event.TickEvent
+import moe.nea.ultranotifier.event.UltraSubscribe
+import moe.nea.ultranotifier.event.VisibleChatMessageAddedEvent
+import moe.nea.ultranotifier.util.GsonUtil
+import moe.nea.ultranotifier.util.duplicatesBy
+import moe.nea.ultranotifier.util.minecrat.MC
+import moe.nea.ultranotifier.util.minecrat.category
+import moe.nea.ultranotifier.util.minecrat.getFormattedTextCompat
+import moe.nea.ultranotifier.util.minecrat.removeFormattingCodes
+import net.minecraft.text.Text
+import util.KSerializable
+import java.util.function.Predicate
+import java.util.regex.Pattern
+
+data class ChatTypeId(
+ val id: String
+)
+
+@KSerializable
+data class ChatType(
+ val name: String,
+ val patterns: List<ChatPattern>,
+)
+
+data class ChatPattern(
+ val text: String
+) {
+ val pattern = Pattern.compile(text)
+ val predicate: Predicate<String> =
+//#if JAVA > 11
+ pattern.asMatchPredicate()
+//#else
+//$$ Predicate { it: String -> pattern.matcher(it).matches() }
+//#endif
+}
+
+data class CategoryId(val id: String)
+
+@KSerializable
+data class ChatCategory(
+ val id: CategoryId,
+ val label: String,
+ val chatTypes: Set<ChatTypeId>,
+)
+
+data class ChatUniverse(
+ val name: String,
+ val types: Map<ChatTypeId, ChatType>,
+ val categories: List<ChatCategory>,
+) {
+ fun categorize(
+ text: String
+ ): CategorizedChatLine {
+ val types = this.types
+ .asSequence()
+ .filter {
+ it.value.patterns.any {
+ it.predicate.test(text)
+ }
+ }
+ .map {
+ it.key
+ }
+ .toSet()
+ return CategorizedChatLine(
+ text, types
+ )
+ }
+}
+
+data class CategorizedChatLine(
+ val text: String,
+ val types: Set<ChatTypeId>,
+// val categories: Set<ChatCategory>,
+)
+
+@KSerializable
+data class UniverseMeta(
+ // TODO: implement the ip filter
+ val ipFilter: List<ChatPattern> = listOf(),
+ val name: String,
+)
+
+interface HasCategorizedChatLine {
+ val categorizedChatLine_ultraNotifier: CategorizedChatLine
+}
+
+data class UniverseId(
+ val id: String
+)
+
+private fun loadAllUniverses(): Map<UniverseId, ChatUniverse> = buildMap {
+ for (file in UltraNotifier.configFolder
+ .resolve("universes/")
+ .listFiles() ?: emptyArray()) {
+ runCatching {
+ val meta = GsonUtil.read<UniverseMeta>(file.resolve("meta.json"))
+ val types = GsonUtil.read<Map<ChatTypeId, ChatType>>(file.resolve("chattypes.json"))
+ val categories = GsonUtil.read<List<ChatCategory>>(file.resolve("categories.json"))
+ // Validate categories linking properly
+ for (category in categories) {
+ for (chatType in category.chatTypes) {
+ if (chatType in types.keys) {
+ UltraNotifier.logger.warn("Missing definition for $chatType required by ${category.id} in $file")
+ }
+ }
+ }
+ for (category in categories.asSequence().duplicatesBy { it.id }) {
+ UltraNotifier.logger.warn("Found duplicate category ${category.id} in $file")
+ }
+
+ put(
+ UniverseId(file.name),
+ ChatUniverse(
+ meta.name,
+ types,
+ categories,
+ ))
+ }.getOrElse {
+ UltraNotifier.logger.warn("Could not load universe at $file", it)
+ }
+ }
+}
+
+object ChatCategoryArbiter : SubscriptionTarget {
+ val specialAll = CategoryId("special-all")
+
+ var allUniverses = loadAllUniverses()
+
+ var activeUniverse: ChatUniverse? = allUniverses.values.single()
+ private val allCategoryList = listOf(
+ ChatCategory(specialAll, "All", setOf())
+ )
+
+ val categories // TODO: memoize
+ get() = (activeUniverse?.categories ?: listOf()) + allCategoryList
+
+ var selectedCategoryId = specialAll
+ set(value) {
+ field = value
+ selectedCategory = findCategory(value)
+ }
+ private var lastSelectedId = selectedCategoryId
+ var selectedCategory: ChatCategory = findCategory(selectedCategoryId)
+ private set
+
+ @UltraSubscribe
+ fun onTick(event: TickEvent) {
+ if (lastSelectedId != selectedCategoryId) {
+ MC.chatHud.reset()
+ lastSelectedId = selectedCategoryId
+ }
+ }
+
+ @UltraSubscribe
+ fun onVisibleChatMessage(event: VisibleChatMessageAddedEvent) {
+ val cl = event.chatLine.category
+ if (selectedCategory.id == specialAll)
+ return
+ if (cl.types.none { it in selectedCategory.chatTypes })
+ event.cancel()
+ }
+
+ fun findCategory(id: CategoryId) = categories.find { it.id == id }!!
+
+ fun categorize(content: Text): CategorizedChatLine {
+ val stringContent = content.getFormattedTextCompat().removeFormattingCodes()
+ return activeUniverse?.categorize(stringContent) ?: CategorizedChatLine(stringContent, setOf())
+ }
+}
+
diff --git a/src/main/kotlin/event/ChatGuiLineEvent.kt b/src/main/kotlin/event/ChatGuiLineEvent.kt
index e37d31f..eb585dd 100644
--- a/src/main/kotlin/event/ChatGuiLineEvent.kt
+++ b/src/main/kotlin/event/ChatGuiLineEvent.kt
@@ -3,7 +3,13 @@ package moe.nea.ultranotifier.event
import net.minecraft.text.Text
class ChatGuiLineEvent(val component: Text) : UltraEvent() {
- val string = component.string
+ val string =
+//#if MC < 1.16
+//$$ component.unformattedText // Why does remap not do this automatically? hello?
+//#else
+ component.string
+//#endif
+
override fun toString(): String {
return "ChatLineAddedEvent($string)"
}
diff --git a/src/main/kotlin/event/CommandRegistrationEvent.kt b/src/main/kotlin/event/CommandRegistrationEvent.kt
new file mode 100644
index 0000000..49bc637
--- /dev/null
+++ b/src/main/kotlin/event/CommandRegistrationEvent.kt
@@ -0,0 +1,10 @@
+package moe.nea.ultranotifier.event
+
+import com.mojang.brigadier.CommandDispatcher
+import moe.nea.ultranotifier.commands.UltraCommandSource
+
+/**
+ * Fired whenever commands need to be registered. This may be multiple times during each launch. Old commands will be
+ * automatically unregistered first.
+ */
+class CommandRegistrationEvent(val dispatcher: CommandDispatcher<UltraCommandSource>) : UltraEvent()
diff --git a/src/main/kotlin/event/RegistrationFinishedEvent.kt b/src/main/kotlin/event/RegistrationFinishedEvent.kt
new file mode 100644
index 0000000..325e1d8
--- /dev/null
+++ b/src/main/kotlin/event/RegistrationFinishedEvent.kt
@@ -0,0 +1,6 @@
+package moe.nea.ultranotifier.event
+
+/**
+ * Indicates that the registration of all ultra event handlers is done
+ */
+class RegistrationFinishedEvent : UltraEvent()
diff --git a/src/main/kotlin/event/SubscriptionTarget.kt b/src/main/kotlin/event/SubscriptionTarget.kt
new file mode 100644
index 0000000..e66eb94
--- /dev/null
+++ b/src/main/kotlin/event/SubscriptionTarget.kt
@@ -0,0 +1,5 @@
+package moe.nea.ultranotifier.event
+
+interface SubscriptionTarget {
+ fun init() = Unit
+}
diff --git a/src/main/kotlin/event/TickEvent.kt b/src/main/kotlin/event/TickEvent.kt
new file mode 100644
index 0000000..4bd2c6c
--- /dev/null
+++ b/src/main/kotlin/event/TickEvent.kt
@@ -0,0 +1,26 @@
+package moe.nea.ultranotifier.event
+
+//#if FABRIC
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
+//#endif
+
+class TickEvent : UltraEvent(), UltraEvent.Silent<TickEvent> {
+
+ companion object : SubscriptionTarget {
+ override fun init() {
+//#if FABRIC
+ ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick {
+ TickEvent().post()
+ })
+//#else
+//$$ net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(object {
+//$$ @UltraSubscribe
+//$$ fun onForgeEvent(event: net.minecraftforge.event.TickEvent.ClientTickEvent) {
+//$$ if (event.phase == net.minecraftforge.event.TickEvent.Phase.END)
+//$$ TickEvent().post()
+//$$ }
+//$$ })
+//#endif
+ }
+ }
+}
diff --git a/src/main/kotlin/event/UltraEvent.kt b/src/main/kotlin/event/UltraEvent.kt
index 42fa4f2..80f63fc 100644
--- a/src/main/kotlin/event/UltraEvent.kt
+++ b/src/main/kotlin/event/UltraEvent.kt
@@ -7,6 +7,8 @@ abstract class UltraEvent :
me.bush.eventbus.event.Event()
//#endif
{
+ interface Silent<T> where T : Silent<T>, T : UltraEvent
+
//#if FORGE
//$$ override fun isCancelable(): Boolean {
//$$ return this.isCancellable()
@@ -32,6 +34,10 @@ abstract class UltraEvent :
setCancelled(true)
}
+ fun post() {
+ UltraNotifierEvents.post(this)
+ }
+
}
diff --git a/src/main/kotlin/event/UltraNotifierEvents.kt b/src/main/kotlin/event/UltraNotifierEvents.kt
index 34d1769..9c1e1ee 100644
--- a/src/main/kotlin/event/UltraNotifierEvents.kt
+++ b/src/main/kotlin/event/UltraNotifierEvents.kt
@@ -9,14 +9,17 @@ object UltraNotifierEvents {
//#else
me.bush.eventbus.bus.EventBus { UltraNotifier.logger.warn("EventBus: $it") }
//#endif
+
@JvmStatic
fun <T : UltraEvent> post(event: T): T {
- UltraNotifier.logger.info("Posting $event")
+ if (event !is UltraEvent.Silent<*>) {
+ UltraNotifier.logger.info("Posting $event")
+ }
eventBus.post(event)
return event
}
- fun register(obj: Any) {
+ fun register(obj: SubscriptionTarget) {
//#if FORGE
//$$ eventBus.register(obj)
//#else
diff --git a/src/main/kotlin/event/VisibleChatMessageAddedEvent.kt b/src/main/kotlin/event/VisibleChatMessageAddedEvent.kt
new file mode 100644
index 0000000..97d919b
--- /dev/null
+++ b/src/main/kotlin/event/VisibleChatMessageAddedEvent.kt
@@ -0,0 +1,17 @@
+package moe.nea.ultranotifier.event
+
+import net.minecraft.client.gui.hud.ChatHudLine
+import net.minecraft.text.Text
+
+typealias ChattyHudLine =
+ ChatHudLine
+//#if MC < 1.20
+//#if MC > 1.16
+//$$ <Text>
+//#endif
+//#endif
+
+
+data class VisibleChatMessageAddedEvent(
+ val chatLine: ChattyHudLine,
+) : UltraEvent()
diff --git a/src/main/kotlin/gui/ChatUi.kt b/src/main/kotlin/gui/ChatUi.kt
new file mode 100644
index 0000000..b812b73
--- /dev/null
+++ b/src/main/kotlin/gui/ChatUi.kt
@@ -0,0 +1,91 @@
+package moe.nea.ultranotifier.gui
+
+import gg.essential.universal.UGraphics
+import gg.essential.universal.UMatrixStack
+import moe.nea.ultranotifier.datamodel.ChatCategory
+import moe.nea.ultranotifier.datamodel.ChatCategoryArbiter
+import moe.nea.ultranotifier.util.minecrat.MC
+import moe.nea.ultranotifier.util.minecrat.accessor
+import moe.nea.ultranotifier.util.render.ScreenRenderUtils
+import net.minecraft.client.gui.screen.ChatScreen
+import java.awt.Color
+
+class ChatUi(val chatScreen: ChatScreen) {
+
+ val Double.value get() = this
+ val Float.value get() = this
+ fun getChatBgOpacity(opacityMultiplier: Double = 1.0): Color {
+ return Color((MC.instance.options.textBackgroundOpacity.value * opacityMultiplier * 255).toInt() shl 24, true)
+ }
+
+ fun calculateChatTop(): Double {
+ val ch = MC.chatHud
+ ch.accessor()
+ val chatOffset =
+ 40
+ val chatTop =
+ (chatScreen.height - chatOffset) / ch.chatScale - ch.visibleLineCount * ch.lineHeight_ultranotifier
+ return chatTop.toDouble()
+ }
+
+ fun iterateButtons(
+ onEach: (
+ label: ChatCategory,
+ isSelected: Boolean,
+ pos: Rect,
+ ) -> Unit
+ ) {
+ val chatTop = calculateChatTop()
+ var xOffset = 5
+ val top = chatTop - 16.0
+ for (button in ChatCategoryArbiter.categories) {
+ val w = ScreenRenderUtils.getTextWidth(button.label) + 3
+ val isSelected = button == ChatCategoryArbiter.selectedCategory
+ onEach(button, isSelected, Rect(xOffset.toDouble(), top, w.toDouble(), 16.0))
+ xOffset += w + 5
+ }
+ }
+
+ data class Rect(
+ val x: Double, val y: Double,
+ val w: Double, val h: Double,
+ ) {
+ fun contains(mouseX: Double, mouseY: Double): Boolean {
+ return mouseX in (l..<r) && mouseY in (t..<b)
+ }
+
+ val l get() = x
+ val t get() = y
+ val r get() = x + w
+ val b get() = y + h
+ val cy get() = y + h / 2
+ val cx get() = x + w / 2
+ }
+
+ fun renderButtons(
+ matrixStack: UMatrixStack,
+ mouseX: Double, mouseY: Double,
+ ) {
+ iterateButtons { button, isSelected, pos ->
+ UGraphics.enableBlend()
+ ScreenRenderUtils.fillRect(matrixStack,
+ pos.l, pos.t, pos.r, pos.b,
+ if (isSelected) getChatBgOpacity() else getChatBgOpacity(0.75))
+ UGraphics.disableBlend()
+ ScreenRenderUtils.renderText(matrixStack,
+ pos.l + 2, pos.cy - 9 / 2,
+ if (isSelected) "§a${button.label}" else "§f${button.label}")
+ }
+ }
+
+ fun clickMouse(mouseX: Double, mouseY: Double, button: Int) {
+ iterateButtons { label, isSelected, pos ->
+ if (pos.contains(mouseX, mouseY)) {
+ if (button == 0) {
+ ChatCategoryArbiter.selectedCategoryId = label.id
+ }
+ // TODO: right click options or something
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/MessageUi.kt b/src/main/kotlin/gui/MessageUi.kt
new file mode 100644
index 0000000..5f337f3
--- /dev/null
+++ b/src/main/kotlin/gui/MessageUi.kt
@@ -0,0 +1,28 @@
+package moe.nea.ultranotifier.gui
+
+import gg.essential.universal.UMatrixStack
+import gg.essential.universal.UScreen
+import juuxel.libninepatch.NinePatch
+import moe.nea.ultranotifier.util.render.ScreenRenderUtils
+import moe.nea.ultranotifier.util.ultraIdentifier
+import java.awt.Color
+
+class MessageUi : UScreen() {
+ override fun onDrawScreen(matrixStack: UMatrixStack, mouseX: Int, mouseY: Int, partialTicks: Float) {
+ super.onDrawScreen(matrixStack, mouseX, mouseY, partialTicks)
+ ScreenRenderUtils.fillRect(matrixStack, 0.0, 0.0, width.toDouble(), height.toDouble(), Color.RED)
+ ScreenRenderUtils.renderTexture(
+ ultraIdentifier("textures/gui/square_panel.png"),
+ matrixStack,
+ 200.0, 0.0, 300.0, 100.0
+ )
+ ScreenRenderUtils.renderNineSlice(
+ NinePatch.builder(ultraIdentifier("textures/gui/square_panel.png"))
+ .cornerSize(10)
+ .mode(NinePatch.Mode.STRETCHING)
+ .cornerUv(0.1F, 0.1F).build(),
+ matrixStack,
+ 225.0, 25.0, 275.0, 75.0
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/ScreenUtil.kt b/src/main/kotlin/gui/ScreenUtil.kt
new file mode 100644
index 0000000..bb3dfc3
--- /dev/null
+++ b/src/main/kotlin/gui/ScreenUtil.kt
@@ -0,0 +1,19 @@
+package moe.nea.ultranotifier.gui
+
+import gg.essential.universal.UScreen
+import moe.nea.ultranotifier.event.SubscriptionTarget
+import moe.nea.ultranotifier.event.TickEvent
+import moe.nea.ultranotifier.event.UltraSubscribe
+import net.minecraft.client.gui.screen.Screen
+
+object ScreenUtil : SubscriptionTarget {
+ var openScreen: Screen? = null
+
+ @UltraSubscribe
+ fun onTick(event: TickEvent) {
+ openScreen?.let {
+ UScreen.displayScreen(it)
+ openScreen = null
+ }
+ }
+}
diff --git a/src/main/kotlin/util/GsonUtil.kt b/src/main/kotlin/util/GsonUtil.kt
new file mode 100644
index 0000000..d689330
--- /dev/null
+++ b/src/main/kotlin/util/GsonUtil.kt
@@ -0,0 +1,55 @@
+package moe.nea.ultranotifier.util
+
+import com.google.gson.GsonBuilder
+import com.google.gson.TypeAdapter
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+import moe.nea.ultranotifier.datamodel.CategoryId
+import moe.nea.ultranotifier.datamodel.ChatPattern
+import moe.nea.ultranotifier.datamodel.ChatTypeId
+import net.minecraft.util.Identifier
+import util.KotlinTypeAdapterFactory
+import java.io.File
+
+object GsonUtil {
+ val sharedGsonBuilder = GsonBuilder()
+ .registerTypeAdapterFactory(KotlinTypeAdapterFactory())
+ .registerTypeHierarchyAdapter(Identifier::class.java, object : TypeAdapter<Identifier>() {
+ override fun write(out: JsonWriter, value: Identifier) {
+ out.value(value.namespace + ":" + value.path)
+ }
+
+ override fun read(`in`: JsonReader): Identifier {
+ val identifierName = `in`.nextString()
+ val parts = identifierName.split(":")
+ require(parts.size != 2) { "$identifierName is not a valid identifier" }
+ return identifier(parts[0], parts[1])
+ }
+ }.nullSafe())
+ .registerTypeHierarchyAdapter(ChatPattern::class.java, stringWrapperAdapter(ChatPattern::text, ::ChatPattern))
+ .registerTypeHierarchyAdapter(CategoryId::class.java, stringWrapperAdapter(CategoryId::id, ::CategoryId))
+ .registerTypeHierarchyAdapter(ChatTypeId::class.java, stringWrapperAdapter(ChatTypeId::id, ::ChatTypeId))
+
+ private fun <T> stringWrapperAdapter(from: (T) -> String, to: (String) -> T): TypeAdapter<T> {
+ return object : TypeAdapter<T>() {
+ override fun write(out: JsonWriter, value: T) {
+ out.value(from(value))
+ }
+
+ override fun read(`in`: JsonReader): T {
+ return to(`in`.nextString())
+ }
+ }.nullSafe()
+ }
+
+ inline fun <reified T : Any> read(meta: File): T {
+ // TODO: add exception
+ meta.reader().use { reader ->
+ return gson.fromJson(reader, object : TypeToken<T>() {}.type)
+ }
+ }
+
+ val gson = sharedGsonBuilder.create()
+ val prettyGson = sharedGsonBuilder.setPrettyPrinting().create()
+}
diff --git a/src/main/kotlin/util/IdentityCharacteristics.kt b/src/main/kotlin/util/IdentityCharacteristics.kt
new file mode 100644
index 0000000..d3f5294
--- /dev/null
+++ b/src/main/kotlin/util/IdentityCharacteristics.kt
@@ -0,0 +1,15 @@
+package moe.nea.ultranotifier.util
+
+class IdentityCharacteristics<T>(val value: T) {
+ override fun hashCode(): Int {
+ return System.identityHashCode(value)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return value === other
+ }
+
+ override fun toString(): String {
+ return "IdentityCharacteristics($value)"
+ }
+}
diff --git a/src/main/kotlin/util/KSerializable.kt b/src/main/kotlin/util/KSerializable.kt
new file mode 100644
index 0000000..ef4c953
--- /dev/null
+++ b/src/main/kotlin/util/KSerializable.kt
@@ -0,0 +1,112 @@
+package util
+import com.google.gson.*
+import com.google.gson.annotations.SerializedName
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonToken
+import com.google.gson.stream.JsonWriter
+import kotlin.reflect.*
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.isSubtypeOf
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.primaryConstructor
+import kotlin.reflect.jvm.javaType
+import com.google.gson.internal.`$Gson$Types` as InternalGsonTypes
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class KSerializable(
+)
+
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.PROPERTY)
+annotation class ExtraData
+
+
+class KotlinTypeAdapterFactory : TypeAdapterFactory {
+
+ internal data class ParameterInfo(
+ val param: KParameter,
+ val adapter: TypeAdapter<Any?>,
+ val name: String,
+ val field: KProperty1<Any, Any?>
+ )
+
+ override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
+ val kotlinClass = type.rawType.kotlin as KClass<T>
+ if (kotlinClass.findAnnotation<KSerializable>() == null) return null
+ if (!kotlinClass.isData) return null
+ val primaryConstructor = kotlinClass.primaryConstructor ?: return null
+ val params = primaryConstructor.parameters.filter { it.findAnnotation<ExtraData>() == null }
+ val extraDataParam = primaryConstructor.parameters
+ .find { it.findAnnotation<ExtraData>() != null }
+ ?.let { param ->
+ require(typeOf<MutableMap<String, JsonElement>>().isSubtypeOf(param.type))
+ param to kotlinClass.memberProperties.find { it.name == param.name && it.returnType.isSubtypeOf(typeOf<Map<String, JsonElement>>()) } as KProperty1<Any, Map<String, JsonElement>>
+ }
+ val parameterInfos = params.map { param ->
+ ParameterInfo(
+ param,
+ gson.getAdapter(
+ TypeToken.get(InternalGsonTypes.resolve(type.type, type.rawType, param.type.javaType))
+ ) as TypeAdapter<Any?>,
+ param.findAnnotation<SerializedName>()?.value ?: param.name!!,
+ kotlinClass.memberProperties.find { it.name == param.name }!! as KProperty1<Any, Any?>
+ )
+ }.associateBy { it.name }
+ val jsonElementAdapter = gson.getAdapter(JsonElement::class.java)
+
+ return object : TypeAdapter<T>() {
+ override fun write(out: JsonWriter, value: T?) {
+ if (value == null) {
+ out.nullValue()
+ return
+ }
+ out.beginObject()
+ parameterInfos.forEach { (name, paramInfo) ->
+ out.name(name)
+ paramInfo.adapter.write(out, paramInfo.field.get(value))
+ }
+ if (extraDataParam != null) {
+ val extraData = extraDataParam.second.get(value)
+ extraData.forEach { (extraName, extraValue) ->
+ out.name(extraName)
+ jsonElementAdapter.write(out, extraValue)
+ }
+ }
+ out.endObject()
+ }
+
+ override fun read(reader: JsonReader): T? {
+ if (reader.peek() == JsonToken.NULL) {
+ reader.nextNull()
+ return null
+ }
+ reader.beginObject()
+ val args = mutableMapOf<KParameter, Any?>()
+ val extraData = mutableMapOf<String, JsonElement>()
+ while (reader.peek() != JsonToken.END_OBJECT) {
+ val name = reader.nextName()
+ val paramData = parameterInfos[name]
+ if (paramData == null) {
+ extraData[name] = jsonElementAdapter.read(reader)
+ continue
+ }
+ val value = paramData.adapter.read(reader)
+ args[paramData.param] = value
+ }
+ reader.endObject()
+ if (extraDataParam == null) {
+ if (extraData.isNotEmpty()) {
+ throw JsonParseException("Encountered unknown keys ${extraData.keys} while parsing $type")
+ }
+ } else {
+ args[extraDataParam.first] = extraData
+ }
+ return primaryConstructor.callBy(args)
+ }
+ }
+ }
+}
+
diff --git a/src/main/kotlin/util/identifierutil.kt b/src/main/kotlin/util/identifierutil.kt
new file mode 100644
index 0000000..e94e15c
--- /dev/null
+++ b/src/main/kotlin/util/identifierutil.kt
@@ -0,0 +1,11 @@
+package moe.nea.ultranotifier.util
+
+import moe.nea.ultranotifier.Constants
+import net.minecraft.util.Identifier
+
+fun identifier(namespace: String, path: String): Identifier {
+ return Identifier(namespace, path)
+}
+
+fun vanillaIdentifier(path: String) = identifier("minecraft", path)
+fun ultraIdentifier(path: String) = identifier(Constants.MOD_ID, path)
diff --git a/src/main/kotlin/util/iterutil.kt b/src/main/kotlin/util/iterutil.kt
new file mode 100644
index 0000000..7845b05
--- /dev/null
+++ b/src/main/kotlin/util/iterutil.kt
@@ -0,0 +1,36 @@
+package moe.nea.ultranotifier.util
+
+
+fun <T, K : Any> Sequence<T>.duplicatesBy(keyFunc: (T) -> K): Sequence<T> {
+ return object : Sequence<T> {
+ override fun iterator(): Iterator<T> {
+ val observed = HashSet<K>()
+ val oldIterator = this@duplicatesBy.iterator()
+
+ return object : Iterator<T> {
+ var next: T? = null
+ var hasNext = false
+ override fun hasNext(): Boolean {
+ if (hasNext) return true
+ while (oldIterator.hasNext()) {
+ val elem = oldIterator.next()
+ val key = keyFunc(elem)
+ if (observed.add(key))
+ continue
+ hasNext = true
+ next = elem
+ }
+ return hasNext
+ }
+
+ override fun next(): T {
+ if (!hasNext()) throw NoSuchElementException()
+ hasNext = false
+ val elem = next as T
+ next = null
+ return elem
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/util/minecrat/MC.kt b/src/main/kotlin/util/minecrat/MC.kt
new file mode 100644
index 0000000..d942982
--- /dev/null
+++ b/src/main/kotlin/util/minecrat/MC.kt
@@ -0,0 +1,11 @@
+package moe.nea.ultranotifier.util.minecrat
+
+import net.minecraft.client.MinecraftClient
+
+object MC {
+ val font get() = instance.textRenderer
+ val instance get() = MinecraftClient.getInstance()
+ val inGameHud get() = instance.inGameHud!!
+ val chatHud get() = inGameHud.chatHud!!
+
+}
diff --git a/src/main/kotlin/util/minecrat/TextUtil.kt b/src/main/kotlin/util/minecrat/TextUtil.kt
new file mode 100644
index 0000000..0640021
--- /dev/null
+++ b/src/main/kotlin/util/minecrat/TextUtil.kt
@@ -0,0 +1,70 @@
+package moe.nea.ultranotifier.util.minecrat
+import net.minecraft.text.Text
+
+//#if MC > 1.16
+import net.minecraft.text.TextColor
+import net.minecraft.util.Formatting
+//#endif
+//#if MC > 1.20
+import net.minecraft.text.MutableText
+import net.minecraft.text.PlainTextContent
+//#endif
+
+fun Text.getDirectlyContainedText() =
+//#if MC < 1.16
+//$$ this.unformattedComponentText
+//#elseif MC < 1.20
+//$$ this.asString()
+//#else
+ (this.content as? PlainTextContent)?.string().orEmpty()
+//#endif
+
+fun Text?.getFormattedTextCompat(): String =
+//#if MC < 1.16
+//$$ this?.formattedText.orEmpty()
+//#else
+run {
+ this ?: return@run ""
+ val sb = StringBuilder()
+ for (component in iterator()) {
+ sb.append(component.style.color?.toChatFormatting()?.toString() ?: "§r")
+ sb.append(component.getDirectlyContainedText())
+ sb.append("§r")
+ }
+ sb.toString()
+}
+
+private val textColorLUT = Formatting.entries
+ .mapNotNull { formatting -> formatting.colorValue?.let { it to formatting } }
+ .toMap()
+
+fun TextColor.toChatFormatting(): Formatting? {
+ return textColorLUT[this.rgb]
+}
+
+fun Text.iterator(): Sequence<Text> {
+ return sequenceOf(this) + siblings.asSequence().flatMap { it.iterator() } // TODO: in theory we want to properly inherit styles here
+}
+//#endif
+
+//#if MC > 1.20
+fun MutableText.withColor(formatting: Formatting): Text {
+ return this.styled { it.withColor(formatting) }
+}
+//#endif
+
+fun CharSequence.removeFormattingCodes(): String {
+ var nextParagraph = indexOf('§')
+ if (nextParagraph < 0) return this.toString()
+ val stringBuffer = StringBuilder(this.length)
+ var readIndex = 0
+ while (nextParagraph >= 0) {
+ stringBuffer.append(this, readIndex, nextParagraph)
+ readIndex = nextParagraph + 2
+ nextParagraph = indexOf('§', startIndex = readIndex)
+ if (readIndex > this.length)
+ readIndex = this.length
+ }
+ stringBuffer.append(this, readIndex, this.length)
+ return stringBuffer.toString()
+}
diff --git a/src/main/kotlin/util/minecrat/infer.kt b/src/main/kotlin/util/minecrat/infer.kt
new file mode 100644
index 0000000..dc89392
--- /dev/null
+++ b/src/main/kotlin/util/minecrat/infer.kt
@@ -0,0 +1,22 @@
+@file:OptIn(ExperimentalContracts::class)
+
+package moe.nea.ultranotifier.util.minecrat
+
+import moe.nea.ultranotifier.datamodel.HasCategorizedChatLine
+import moe.nea.ultranotifier.event.ChattyHudLine
+import net.minecraft.client.gui.hud.ChatHud
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+interface AccessorChatHud {
+ val lineHeight_ultranotifier: Int
+}
+
+fun ChatHud.accessor(): AccessorChatHud {
+ contract {
+ returns() implies (this@accessor is AccessorChatHud)
+ }
+ return this as AccessorChatHud
+}
+
+val ChattyHudLine.category get() = (this as HasCategorizedChatLine).categorizedChatLine_ultraNotifier
diff --git a/src/main/kotlin/util/render/ScreenRenderUtils.kt b/src/main/kotlin/util/render/ScreenRenderUtils.kt
new file mode 100644
index 0000000..959d0a8
--- /dev/null
+++ b/src/main/kotlin/util/render/ScreenRenderUtils.kt
@@ -0,0 +1,135 @@
+package moe.nea.ultranotifier.util.render
+
+import gg.essential.universal.UGraphics
+import gg.essential.universal.UMatrixStack
+import juuxel.libninepatch.NinePatch
+import juuxel.libninepatch.TextureRenderer
+import moe.nea.ultranotifier.util.minecrat.MC
+import net.minecraft.util.Identifier
+import java.awt.Color
+//#if MC > 1.16
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.util.math.MatrixStack
+
+//#endif
+
+object ScreenRenderUtils {
+ //#if MC > 1.16
+ @JvmStatic
+ fun umatrix(
+ matrixStack: MatrixStack
+ ) = UMatrixStack(matrixStack)
+
+ //#endif
+ //#if MC >= 1.20
+ @JvmStatic
+ fun umatrix(
+ context: DrawContext
+ ) = UMatrixStack(context.matrices)
+ //#endif
+
+ @JvmStatic
+ fun umatrix() = UMatrixStack()
+
+ fun fillRect(
+ matrixStack: UMatrixStack,
+ left: Double, top: Double,
+ right: Double, bottom: Double,
+ color: Color,
+ ) {
+ val buffer = UGraphics.getFromTessellator()
+ buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_COLOR)
+ buffer.pos(matrixStack, left, top, 0.0).color(color).endVertex()
+ buffer.pos(matrixStack, left, bottom, 0.0).color(color).endVertex()
+ buffer.pos(matrixStack, right, bottom, 0.0).color(color).endVertex()
+ buffer.pos(matrixStack, right, top, 0.0).color(color).endVertex()
+ buffer.drawDirect()
+ }
+
+ fun renderTexture(
+ identifier: Identifier,
+ matrixStack: UMatrixStack,
+ left: Double, top: Double,
+ right: Double, bottom: Double,
+ ) {
+ UGraphics.bindTexture(0, identifier)
+ val graphics = UGraphics.getFromTessellator()
+ graphics.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE)
+ graphics.pos(matrixStack, left, top, 0.0).tex(0.0, 0.0).endVertex()
+ graphics.pos(matrixStack, left, bottom, 0.0).tex(0.0, 1.0).endVertex()
+ graphics.pos(matrixStack, right, bottom, 0.0).tex(1.0, 1.0).endVertex()
+ graphics.pos(matrixStack, right, top, 0.0).tex(1.0, 0.0).endVertex()
+ graphics.drawDirect()
+ }
+
+ fun renderNineSlice(
+ ninePatch: NinePatch<Identifier>,
+ matrixStack: UMatrixStack,
+ left: Double, top: Double,
+ right: Double, bottom: Double,
+ ) {
+ class Saver : TextureRenderer<Identifier> {
+ override fun draw(
+ texture: Identifier?,
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ u1: Float,
+ v1: Float,
+ u2: Float,
+ v2: Float
+ ) {
+ this.texture = texture
+ }
+
+ var texture: Identifier? = null
+ }
+
+ val saver = Saver()
+ ninePatch.draw(saver, 1, 1)
+ UGraphics.bindTexture(0, saver.texture!!)
+ val graphics = UGraphics.getFromTessellator()
+ graphics.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE)
+ ninePatch.draw(object : TextureRenderer<Identifier> {
+ override fun draw(
+ texture: Identifier,
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ u1: Float,
+ v1: Float,
+ u2: Float,
+ v2: Float
+ ) {
+ val x1 = left + x.toDouble()
+ val y1 = top + y.toDouble()
+ val x2 = x1 + width
+ val y2 = y1 + height
+ graphics.pos(matrixStack, x1, y1, 0.0)
+ .tex(u1.toDouble(), v1.toDouble())
+ .endVertex()
+ graphics.pos(matrixStack, x1, y2, 0.0)
+ .tex(u1.toDouble(), v2.toDouble())
+ .endVertex()
+ graphics.pos(matrixStack, x2, y2, 0.0)
+ .tex(u2.toDouble(), v2.toDouble())
+ .endVertex()
+ graphics.pos(matrixStack, x2, y1, 0.0)
+ .tex(u2.toDouble(), v1.toDouble())
+ .endVertex()
+ }
+ }, (right - left).toInt(), (bottom - top).toInt())
+ graphics.drawDirect()
+ }
+
+ fun getTextWidth(text: String): Int {
+ return MC.font.getWidth(text)
+ }
+
+ fun renderText(matrixStack: UMatrixStack, x: Double, y: Double, text: String) {
+ UGraphics.drawString(matrixStack, text, x.toFloat(), y.toFloat(), -1, false)
+ }
+
+}