aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/io/github
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/io/github')
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt6
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/events/SidebarChangeEvent.kt25
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/events/TabListChangeEvent.kt26
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodo.kt96
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoEditor.kt257
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoHud.kt116
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoList.kt99
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/MoulConfig.kt39
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/TemplateUtil.kt99
9 files changed, 761 insertions, 2 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt
index eb5d51b1..40919a8e 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/help/SettingsCommand.kt
@@ -21,6 +21,7 @@ package io.github.moulberry.notenoughupdates.commands.help
import io.github.moulberry.moulconfig.GuiTextures
import io.github.moulberry.moulconfig.annotations.ConfigOption
+import io.github.moulberry.moulconfig.common.MyResourceLocation
import io.github.moulberry.moulconfig.gui.GuiOptionEditor
import io.github.moulberry.moulconfig.gui.GuiScreenElementWrapper
import io.github.moulberry.moulconfig.gui.MoulConfigEditor
@@ -34,7 +35,6 @@ import io.github.moulberry.notenoughupdates.miscfeatures.IQTest
import io.github.moulberry.notenoughupdates.options.NEUConfig
import io.github.moulberry.notenoughupdates.util.brigadier.*
import net.minecraft.client.gui.GuiScreen
-import net.minecraft.util.ResourceLocation
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import java.lang.reflect.Field
@@ -118,6 +118,7 @@ object SettingsCommand {
return object : GuiScreenElementWrapper(createConfigElement(search)) {
}
}
+
fun createConfigElement(search: String): MoulConfigEditor<NEUConfig> {
val processor = BlockingMoulConfigProcessor()
BuiltinMoulConfigGuis.addProcessors(processor)
@@ -131,7 +132,8 @@ object SettingsCommand {
lastEditor = editor
return editor
}
+
init {
- GuiTextures.setTextureRoot(ResourceLocation("notenoughupdates:core"))
+ GuiTextures.setTextureRoot(MyResourceLocation("notenoughupdates", "core"))
}
}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/events/SidebarChangeEvent.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/SidebarChangeEvent.kt
new file mode 100644
index 00000000..0dc4b9f6
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/SidebarChangeEvent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.events
+
+class SidebarChangeEvent(
+ val lines: List<String>,
+ val lastLines: List<String>,
+) : NEUEvent()
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/events/TabListChangeEvent.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/TabListChangeEvent.kt
new file mode 100644
index 00000000..b5677598
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/events/TabListChangeEvent.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.events
+
+class TabListChangeEvent(
+ val lastLines: List<String>,
+ val newLines: List<String>,
+) : NEUEvent() {
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodo.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodo.kt
new file mode 100644
index 00000000..7f8c6d1a
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodo.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.miscgui.customtodos
+
+import com.google.gson.annotations.Expose
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.TemplateUtil
+import io.github.moulberry.notenoughupdates.util.kotlin.KSerializable
+
+@KSerializable
+data class CustomTodo(
+ @Expose var label: String,
+ @Expose var timer: Int,
+ @Expose var trigger: String,
+ @Expose var icon: String,
+ @Expose var isResetOffset: Boolean,
+ @Expose var triggerTarget: TriggerTarget = TriggerTarget.CHAT,
+ @Expose var triggerMatcher: TriggerMatcher = TriggerMatcher.CONTAINS,
+ @Expose var readyAt: MutableMap<String, Long> = mutableMapOf(),
+ @Expose var enabled: MutableMap<String, Boolean> = mutableMapOf(),
+) {
+ enum class TriggerMatcher {
+ REGEX, STARTS_WITH, CONTAINS, EQUALS
+ }
+
+ enum class TriggerTarget {
+ CHAT, ACTIONBAR, TAB_LIST, SIDEBAR
+ }
+
+ fun isValid(): Boolean {
+ return timer >= 0 && !trigger.isBlank()
+ }
+
+ fun setDoneNow() {
+ val t = System.currentTimeMillis()
+ readyAt[SBInfo.getInstance().currentProfile ?: return] =
+ if (isResetOffset) {
+ t + DAY - t % DAY + timer * 1000L
+ } else {
+ t + timer * 1000L
+ }
+ }
+
+ var readyAtOnCurrentProfile: Long?
+ get() {
+ return readyAt[SBInfo.getInstance().currentProfile ?: return null]
+ }
+ set(value) {
+ readyAt[SBInfo.getInstance().currentProfile ?: return] = value ?: return
+ }
+
+ var isEnabledOnCurrentProfile: Boolean
+ get() {
+ return enabled[SBInfo.getInstance().currentProfile ?: return true] ?: true
+ }
+ set(value) {
+ enabled[SBInfo.getInstance().currentProfile ?: return] = value
+ }
+
+
+ companion object {
+ val templatePrefix = "NEU:CUSTOMTODO/"
+ val DAY = (24 * 60 * 60 * 100)
+ fun fromTemplate(data: String): CustomTodo? {
+ return TemplateUtil.maybeDecodeTemplate(templatePrefix, data, CustomTodo::class.java)
+ ?.also {
+ it.enabled.clear()
+ it.readyAt.clear()
+ }
+ }
+ }
+
+ fun toTemplate(): String {
+ return TemplateUtil.encodeTemplate(
+ templatePrefix,
+ this.copy(enabled = mutableMapOf(), readyAt = mutableMapOf())
+ )
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoEditor.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoEditor.kt
new file mode 100644
index 00000000..1c5c16ed
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoEditor.kt
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.miscgui.customtodos
+
+import io.github.moulberry.moulconfig.common.IItemStack
+import io.github.moulberry.moulconfig.forge.ForgeItemStack
+import io.github.moulberry.moulconfig.internal.ClipboardUtils
+import io.github.moulberry.moulconfig.observer.ObservableList
+import io.github.moulberry.moulconfig.xml.Bind
+import io.github.moulberry.moulconfig.xml.XMLUniverse
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.loadResourceLocation
+import net.minecraft.client.Minecraft
+import net.minecraft.init.Items
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.util.ResourceLocation
+
+class CustomTodoEditor(
+ val from: CustomTodo,
+ val todos: ObservableList<CustomTodoEditor>,
+ val xmlUniverse: XMLUniverse
+) {
+ @field:Bind
+ var label: String = from.label
+
+ @field:Bind
+ var enabled: Boolean = from.isEnabledOnCurrentProfile
+
+ @field:Bind
+ var timer: String = from.timer.toString()
+
+ @field:Bind
+ var trigger: String = from.trigger
+
+ @field:Bind
+ var icon: String = from.icon
+
+ @field:Bind
+ var isResetOffset: Boolean = from.isResetOffset
+
+ var target = from.triggerTarget
+ var matchMode = from.triggerMatcher
+ var lastCustomTodo: CustomTodo? = null
+
+ fun into(): CustomTodo {
+ val nextCustomTodo = CustomTodo(
+ label,
+ timer.toIntOrNull() ?: 0,
+ trigger,
+ icon,
+ isResetOffset,
+ target, matchMode,
+ from.readyAt,
+ from.enabled.toMutableMap().also { it[SBInfo.getInstance().currentProfile ?: return@also] = enabled }
+ )
+ if (nextCustomTodo != lastCustomTodo) {
+ lastCustomTodo = nextCustomTodo
+ CustomTodoList(todos, xmlUniverse).save()
+ }
+ return nextCustomTodo
+ }
+
+ @Bind
+ fun setChat() {
+ target = CustomTodo.TriggerTarget.CHAT
+ }
+
+ @Bind
+ fun setActionbar() {
+ target = CustomTodo.TriggerTarget.ACTIONBAR
+ }
+
+ @Bind
+ fun setSidebar() {
+ target = CustomTodo.TriggerTarget.SIDEBAR
+ }
+
+ @Bind
+ fun setTablist() {
+ target = CustomTodo.TriggerTarget.TAB_LIST
+ }
+
+ private fun colorFromBool(b: Boolean): String {
+ return if (b) "§a" else "§c"
+ }
+
+ @Bind
+ fun getChat(): String {
+ return colorFromBool(target == CustomTodo.TriggerTarget.CHAT) + "Chat"
+ }
+
+ @Bind
+ fun getActionbar(): String {
+ return colorFromBool(target == CustomTodo.TriggerTarget.ACTIONBAR) + "Actionbar"
+ }
+
+ @Bind
+ fun getSidebar(): String {
+ return colorFromBool(target == CustomTodo.TriggerTarget.SIDEBAR) + "Sidebar"
+ }
+
+ @Bind
+ fun getTablist(): String {
+ return colorFromBool(target == CustomTodo.TriggerTarget.TAB_LIST) + "Tablist"
+ }
+
+ @Bind
+ fun setRegex() {
+ matchMode = CustomTodo.TriggerMatcher.REGEX
+ }
+
+ @Bind
+ fun setStartsWith() {
+ matchMode = CustomTodo.TriggerMatcher.STARTS_WITH
+ }
+
+ @Bind
+ fun setContains() {
+ matchMode = CustomTodo.TriggerMatcher.CONTAINS
+ }
+
+ @Bind
+ fun setEquals() {
+ matchMode = CustomTodo.TriggerMatcher.EQUALS
+ }
+
+ @Bind
+ fun getRegex(): String {
+ return colorFromBool(matchMode == CustomTodo.TriggerMatcher.REGEX) + "Regex"
+ }
+
+ @Bind
+ fun getStartsWith(): String {
+ return colorFromBool(matchMode == CustomTodo.TriggerMatcher.STARTS_WITH) + "Starts With"
+ }
+
+ @Bind
+ fun getContains(): String {
+ return colorFromBool(matchMode == CustomTodo.TriggerMatcher.CONTAINS) + "Contains"
+ }
+
+ @Bind
+ fun getEquals(): String {
+ return colorFromBool(matchMode == CustomTodo.TriggerMatcher.EQUALS) + "Equals"
+ }
+
+ @Bind
+ fun getItemStack(): IItemStack {
+ val item = Item.getByNameOrId(icon) ?: (Items.paper)
+ return ForgeItemStack.of(ItemStack(item))
+ }
+
+ @Bind
+ fun copyTemplate() {
+ ClipboardUtils.copyToClipboard(into().toTemplate())
+ }
+
+ @Bind
+ fun markAsReady() {
+ from.readyAtOnCurrentProfile = System.currentTimeMillis()
+ }
+
+ @Bind
+ fun markAsCompleted() {
+ from.setDoneNow()
+ }
+
+ @Bind
+ fun getFancyTime(): String {
+ val tint = timer.toIntOrNull() ?: return "§3Invalid Time"
+ val timeFormat = Utils.prettyTime(tint * 1000L)
+ if (isResetOffset) {
+ return "Reset $timeFormat after 00:00 GMT"
+ }
+ return "Reset $timeFormat after completion"
+ }
+
+ fun changeTimer(value: Int) {
+ timer = ((timer.toIntOrNull() ?: 0) + value).coerceAtLeast(0).toString()
+ }
+
+ @Bind
+ fun plusDay() {
+ changeTimer(60 * 60 * 24)
+ }
+
+ @Bind
+ fun minusDay() {
+ changeTimer(-60 * 60 * 24)
+ }
+
+ @Bind
+ fun minusHour() {
+ changeTimer(-60 * 60)
+ }
+
+ @Bind
+ fun plusHour() {
+ changeTimer(60 * 60)
+ }
+
+ @Bind
+ fun plusMinute() {
+ changeTimer(60)
+ }
+
+ @Bind
+ fun minusMinute() {
+ changeTimer(-60)
+ }
+
+ @Bind
+ fun delete() {
+ todos.remove(this)
+ CustomTodoList(todos, xmlUniverse).save()
+ }
+
+ @Bind
+ fun getTitle(): String {
+ return "Editing ${into().label}"
+ }
+
+ @Bind
+ fun close() {
+ Minecraft.getMinecraft().displayGuiScreen(
+ CustomTodoList(
+ todos, xmlUniverse
+ ).open()
+ )
+ }
+
+ @Bind
+ fun edit() {
+ Minecraft.getMinecraft().displayGuiScreen(
+ xmlUniverse.loadResourceLocation(this, ResourceLocation("notenoughupdates:gui/customtodos/edit.xml"))
+ )
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoHud.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoHud.kt
new file mode 100644
index 00000000..591bcedb
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoHud.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.miscgui.customtodos
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.core.util.StringUtils
+import io.github.moulberry.notenoughupdates.events.SidebarChangeEvent
+import io.github.moulberry.notenoughupdates.events.TabListChangeEvent
+import io.github.moulberry.notenoughupdates.util.Utils
+import net.minecraft.util.EnumChatFormatting
+import net.minecraftforge.client.event.ClientChatReceivedEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+@NEUAutoSubscribe
+object CustomTodoHud {
+
+ private fun matchString(todo: CustomTodo, text: String): Boolean {
+ return when (todo.triggerMatcher) {
+ CustomTodo.TriggerMatcher.REGEX -> text.matches(todo.trigger.toRegex())
+ CustomTodo.TriggerMatcher.STARTS_WITH -> text.startsWith(todo.trigger)
+ CustomTodo.TriggerMatcher.CONTAINS -> text.contains(todo.trigger)
+ CustomTodo.TriggerMatcher.EQUALS -> text == todo.trigger
+ }
+ }
+
+ @SubscribeEvent
+ fun onTabList(event: TabListChangeEvent) {
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos
+ .forEach { todo ->
+ if (todo.triggerTarget != CustomTodo.TriggerTarget.TAB_LIST) return@forEach
+ event.newLines.forEach { text ->
+ val doesMatch = matchString(todo, text)
+ if (doesMatch) {
+ todo.setDoneNow()
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onSidebar(event: SidebarChangeEvent) {
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos
+ .forEach { todo ->
+ if (todo.triggerTarget != CustomTodo.TriggerTarget.SIDEBAR) return@forEach
+ event.lines.forEach { text ->
+ val doesMatch = matchString(todo, text)
+ if (doesMatch) {
+ todo.setDoneNow()
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onChat(event: ClientChatReceivedEvent) {
+ val text = StringUtils.cleanColour(event.message.unformattedText)
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos
+ .forEach {
+ val isCorrectTrigger = when (it.triggerTarget) {
+ CustomTodo.TriggerTarget.CHAT -> event.type != 2.toByte()
+ CustomTodo.TriggerTarget.ACTIONBAR -> event.type == 2.toByte()
+ CustomTodo.TriggerTarget.TAB_LIST -> false
+ CustomTodo.TriggerTarget.SIDEBAR -> false
+ }
+ val doesMatch = matchString(it, text)
+ if (isCorrectTrigger && doesMatch)
+ it.setDoneNow()
+ }
+ }
+
+ @JvmStatic
+ fun processInto(strings: MutableList<String>) {
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos
+ .filter { it.isEnabledOnCurrentProfile }
+ .forEach {
+ val readyAt = it.readyAtOnCurrentProfile ?: (System.currentTimeMillis() - 1000L)
+ val until = readyAt - System.currentTimeMillis()
+ strings.add(
+ "CUSTOM" + it.icon + ":§3" + it.label + ": " +
+ if (until <= 0)
+ EnumChatFormatting.values()[NotEnoughUpdates.INSTANCE.config.miscOverlays.readyColour].toString() + "Ready"
+ else if (until < 60 * 30 * 1000L)
+ EnumChatFormatting.values()[NotEnoughUpdates.INSTANCE.config.miscOverlays.verySoonColour].toString()
+ + Utils.prettyTime(until)
+ else if (until < 60 * 60 * 1000L)
+ EnumChatFormatting.values()[NotEnoughUpdates.INSTANCE.config.miscOverlays.soonColour].toString()
+ + Utils.prettyTime(until)
+ else if (until < 3 * 60 * 60 * 1000L)
+ EnumChatFormatting.values()[NotEnoughUpdates.INSTANCE.config.miscOverlays.kindaSoonColour].toString()
+ + Utils.prettyTime(until)
+ else
+ EnumChatFormatting.values()[NotEnoughUpdates.INSTANCE.config.miscOverlays.defaultColour].toString()
+ + Utils.prettyTime(until)
+ )
+ }
+ }
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoList.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoList.kt
new file mode 100644
index 00000000..1b278990
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscgui/customtodos/CustomTodoList.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.miscgui.customtodos
+
+import io.github.moulberry.moulconfig.internal.ClipboardUtils
+import io.github.moulberry.moulconfig.observer.ObservableList
+import io.github.moulberry.moulconfig.xml.Bind
+import io.github.moulberry.moulconfig.xml.XMLUniverse
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.RegisterBrigadierCommandEvent
+import io.github.moulberry.notenoughupdates.util.brigadier.thenExecute
+import io.github.moulberry.notenoughupdates.util.brigadier.withHelp
+import io.github.moulberry.notenoughupdates.util.loadResourceLocation
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.util.ResourceLocation
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class CustomTodoList(
+ @field:Bind
+ val todos: ObservableList<CustomTodoEditor>,
+ val xmlUniverse: XMLUniverse,
+) {
+ @NEUAutoSubscribe
+ companion object {
+ @SubscribeEvent
+ fun onCommand(event: RegisterBrigadierCommandEvent) {
+ event.command("neutodos", "neucustomtodos") {
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.openGui = create().open()
+ }
+ }.withHelp("Edit NEUs custom TODOs")
+ }
+
+ fun create(): CustomTodoList {
+ val universe = XMLUniverse.getDefaultUniverse()
+ val list = ObservableList<CustomTodoEditor>(mutableListOf())
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos.forEach {
+ list.add(CustomTodoEditor(it, list, universe))
+ }
+ return CustomTodoList(
+ list,
+ universe
+ )
+ }
+ }
+
+ fun open(): GuiScreen {
+ return xmlUniverse.loadResourceLocation(this, ResourceLocation("notenoughupdates:gui/customtodos/overview.xml"))
+ }
+
+ @Bind
+ fun pasteTodo() {
+ val customTodo = CustomTodo.fromTemplate(ClipboardUtils.getClipboardContent())
+ ?: return
+ todos.add(CustomTodoEditor(customTodo, todos, xmlUniverse))
+ save()
+ }
+
+ fun save() {
+ NotEnoughUpdates.INSTANCE.config.hidden.customTodos = todos.map { it.into() }.toMutableList()
+ NotEnoughUpdates.INSTANCE.saveConfig()
+ }
+
+ @Bind
+ fun addTodo() {
+ todos.add(
+ CustomTodoEditor(
+ CustomTodo(
+ "Custom Todo # ${todos.size + 1}",
+ 0,
+ "",
+ "",
+ false,
+ ),
+ todos,
+ xmlUniverse,
+ )
+ )
+ save()
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MoulConfig.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MoulConfig.kt
new file mode 100644
index 00000000..9c626627
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/MoulConfig.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+import io.github.moulberry.moulconfig.gui.GuiContext
+import io.github.moulberry.moulconfig.gui.GuiScreenElementWrapperNew
+import io.github.moulberry.moulconfig.xml.XMLUniverse
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.util.ResourceLocation
+
+
+fun XMLUniverse.loadResourceLocation(obj: Any, resourceLocation: ResourceLocation): GuiScreen {
+ return GuiScreenElementWrapperNew(
+ GuiContext(
+ load(
+ obj,
+ Minecraft.getMinecraft().resourceManager.getResource(resourceLocation).inputStream
+ )
+ )
+ )
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/TemplateUtil.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/TemplateUtil.kt
new file mode 100644
index 00000000..2cd90acc
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/TemplateUtil.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+import com.google.gson.GsonBuilder
+import io.github.moulberry.notenoughupdates.util.kotlin.KotlinTypeAdapterFactory
+import java.util.*
+
+object TemplateUtil {
+ val gson = GsonBuilder()
+ .registerTypeAdapterFactory(KotlinTypeAdapterFactory)
+ .create()
+
+ @JvmStatic
+ fun getTemplatePrefix(data: String): String? {
+ val decoded = maybeFromBase64Encoded(data) ?: return null
+ return decoded.replaceAfter("/", "", "").ifBlank { null }
+ }
+
+ @JvmStatic
+ fun intoBase64Encoded(raw: String): String {
+ return Base64.getEncoder().encodeToString(raw.encodeToByteArray())
+ }
+
+ private val base64Alphabet = charArrayOf(
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '='
+ )
+
+ @JvmStatic
+ fun maybeFromBase64Encoded(raw: String): String? {
+ val raw = raw.trim()
+ if (raw.any { it !in base64Alphabet }) {
+ return null
+ }
+ return try {
+ Base64.getDecoder().decode(raw).decodeToString()
+ } catch (ex: Exception) {
+ null
+ }
+ }
+
+
+ /**
+ * Returns a base64 encoded string, truncated such that for all `x`, `x.startsWith(prefix)` implies
+ * `base64Encoded(x).startsWith(getPrefixComparisonSafeBase64Encoding(prefix))`
+ * (however, the inverse may not always be true).
+ */
+ @JvmStatic
+ fun getPrefixComparisonSafeBase64Encoding(prefix: String): String {
+ val rawEncoded =
+ Base64.getEncoder().encodeToString(prefix.encodeToByteArray())
+ .replace("=", "")
+ return rawEncoded.substring(0, rawEncoded.length - rawEncoded.length % 4)
+ }
+
+ @JvmStatic
+ fun encodeTemplate(sharePrefix: String, data: Any): String {
+ require(sharePrefix.endsWith("/"))
+ return intoBase64Encoded(sharePrefix + gson.toJson(data))
+ }
+
+ @JvmStatic
+ fun <T : Any> maybeDecodeTemplate(sharePrefix: String, data: String, type: Class<T>): T? {
+ require(sharePrefix.endsWith("/"))
+ val data = data.trim()
+ if (!data.startsWith(getPrefixComparisonSafeBase64Encoding(sharePrefix)))
+ return null
+ val decoded = maybeFromBase64Encoded(data) ?: return null
+ if (!decoded.startsWith(sharePrefix))
+ return null
+ return try {
+ gson.fromJson(decoded.substring(sharePrefix.length), type)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+}