diff options
Diffstat (limited to 'src/main/kotlin')
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) + } + +} |