aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-10-28 03:36:11 +0200
committerLinnea Gräf <nea@nea.moe>2023-11-12 09:35:33 +0100
commite15406e22ef65b4933274df791632b6c17f594be (patch)
treee1625c713bf95e9f0cb65dfe9a7c9b9ceaa46402 /src/main/kotlin/moe/nea/firmament
parent47fbb25ab280b6af9496780672780db78fe36f27 (diff)
downloadFirmament-e15406e22ef65b4933274df791632b6c17f594be.tar.gz
Firmament-e15406e22ef65b4933274df791632b6c17f594be.tar.bz2
Firmament-e15406e22ef65b4933274df791632b6c17f594be.zip
Add Inventory Buttons
Diffstat (limited to 'src/main/kotlin/moe/nea/firmament')
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt6
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/HandledScreenClickEvent.kt (renamed from src/main/kotlin/moe/nea/firmament/events/ScreenClickEvent.kt)4
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/HandledScreenForegroundEvent.kt6
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt9
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButton.kt66
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonEditor.kt128
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonTemplates.kt39
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtons.kt82
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt3
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/FragmentGuiScreen.kt97
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/GetRectangle.kt21
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/MoulConfigFragment.kt49
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt18
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/TemplateUtil.kt89
15 files changed, 613 insertions, 6 deletions
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index 3d62408..f4c20bd 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -21,6 +21,7 @@ import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.*
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
+import moe.nea.firmament.features.inventory.buttons.InventoryButtons
fun firmamentCommand() = literal("firmament") {
@@ -83,6 +84,11 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
+ thenLiteral("buttons") {
+ thenExecute {
+ InventoryButtons.openEditor()
+ }
+ }
thenLiteral("storage") {
thenExecute {
ScreenUtil.setScreenLater(StorageOverlayScreen())
diff --git a/src/main/kotlin/moe/nea/firmament/events/ScreenClickEvent.kt b/src/main/kotlin/moe/nea/firmament/events/HandledScreenClickEvent.kt
index 0977971..0c4d4f6 100644
--- a/src/main/kotlin/moe/nea/firmament/events/ScreenClickEvent.kt
+++ b/src/main/kotlin/moe/nea/firmament/events/HandledScreenClickEvent.kt
@@ -8,7 +8,7 @@ package moe.nea.firmament.events
import net.minecraft.client.gui.screen.ingame.HandledScreen
-data class ScreenClickEvent(val screen: HandledScreen<*>, val mouseX: Double, val mouseY: Double, val button: Int) :
+data class HandledScreenClickEvent(val screen: HandledScreen<*>, val mouseX: Double, val mouseY: Double, val button: Int) :
FirmamentEvent.Cancellable() {
- companion object : FirmamentEventBus<ScreenClickEvent>()
+ companion object : FirmamentEventBus<HandledScreenClickEvent>()
}
diff --git a/src/main/kotlin/moe/nea/firmament/events/HandledScreenForegroundEvent.kt b/src/main/kotlin/moe/nea/firmament/events/HandledScreenForegroundEvent.kt
index d45b484..95f9b70 100644
--- a/src/main/kotlin/moe/nea/firmament/events/HandledScreenForegroundEvent.kt
+++ b/src/main/kotlin/moe/nea/firmament/events/HandledScreenForegroundEvent.kt
@@ -6,11 +6,15 @@
package moe.nea.firmament.events
+import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.ingame.HandledScreen
data class HandledScreenForegroundEvent(
val screen: HandledScreen<*>,
- val mouseX: Int, val mouseY: Int, val delta: Float
+ val context: DrawContext,
+ val mouseX: Int,
+ val mouseY: Int,
+ val delta: Float
) : FirmamentEvent() {
companion object : FirmamentEventBus<HandledScreenForegroundEvent>()
}
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
index 2683775..4888f54 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
@@ -23,6 +23,7 @@ import moe.nea.firmament.features.inventory.ItemRarityCosmetics
import moe.nea.firmament.features.inventory.PriceData
import moe.nea.firmament.features.inventory.SaveCursorPosition
import moe.nea.firmament.features.inventory.SlotLocking
+import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay
import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
import moe.nea.firmament.features.world.FairySouls
@@ -56,6 +57,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(CraftingOverlay)
loadFeature(PowerUserTools)
loadFeature(ChatLinks)
+ loadFeature(InventoryButtons)
loadFeature(CompatibliltyFeatures)
loadFeature(QuickCommands)
loadFeature(SaveCursorPosition)
diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt b/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt
index e045fa8..7e3203b 100644
--- a/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt
@@ -64,6 +64,7 @@ object DeveloperFeatures : FirmamentFeature {
return reloadFuture.thenCompose { client.reloadResources() }
}
+
override fun onLoad() {
HandledScreenKeyPressedEvent.subscribe {
if (it.matches(IKeyBinding.ofKeyCode(GLFW.GLFW_KEY_K))) {
@@ -78,10 +79,16 @@ object DeveloperFeatures : FirmamentFeature {
).setStyle(
Style.EMPTY.withColor(Formatting.AQUA)
.withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, ident))
- .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("firmament.debug.skyblockid.copy")))
+ .withHoverEvent(
+ HoverEvent(
+ HoverEvent.Action.SHOW_TEXT,
+ Text.translatable("firmament.debug.skyblockid.copy")
+ )
+ )
)
)
}
}
}
}
+
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButton.kt
new file mode 100644
index 0000000..0383258
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButton.kt
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.buttons
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import kotlinx.serialization.Serializable
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+
+@Serializable
+data class InventoryButton(
+ val x: Int,
+ val y: Int,
+ val anchorRight: Boolean,
+ val anchorBottom: Boolean,
+ var icon: String? = "",
+ var command: String? = "",
+) {
+ companion object {
+ val dimensions = Dimension(18, 18)
+ fun getItemForName(icon: String): ItemStack {
+ return RepoManager.getNEUItem(SkyblockId(icon)).asItemStack(idHint = SkyblockId(icon))
+ }
+ }
+
+ fun render(context: DrawContext) {
+ context.drawSprite(
+ 0,
+ 0,
+ 0,
+ dimensions.width,
+ dimensions.height,
+ MC.guiAtlasManager.getSprite(Identifier("firmament:inventory_button_background"))
+ )
+ context.drawItem(getItem(), 1, 1)
+ }
+
+ fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
+
+ fun getPosition(guiRect: Rectangle): Point {
+ return Point(
+ (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
+ (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
+ )
+ }
+
+ fun getBounds(guiRect: Rectangle): Rectangle {
+ return Rectangle(getPosition(guiRect), dimensions)
+ }
+
+ fun getItem(): ItemStack {
+ return getItemForName(icon ?: "")
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonEditor.kt
new file mode 100644
index 0000000..66be212
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonEditor.kt
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.buttons
+
+import io.github.moulberry.moulconfig.common.IItemStack
+import io.github.moulberry.moulconfig.xml.Bind
+import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import org.lwjgl.glfw.GLFW
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.widget.ButtonWidget
+import net.minecraft.text.Text
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.FragmentGuiScreen
+import moe.nea.firmament.util.MoulConfigUtils
+
+class InventoryButtonEditor(
+ val lastGuiRect: Rectangle,
+) : FragmentGuiScreen() {
+ inner class Editor(val originalButton: InventoryButton) {
+ @field:Bind
+ var command: String = originalButton.command ?: ""
+
+ @field:Bind
+ var icon: String = originalButton.icon ?: ""
+
+ @Bind
+ fun getItemIcon(): IItemStack {
+ save()
+ return ModernItemStack.of(InventoryButton.getItemForName(icon))
+ }
+
+ @Bind
+ fun delete() {
+ buttons.removeIf { it === originalButton }
+ popup = null
+ }
+
+ fun save() {
+ originalButton.icon = icon
+ originalButton.command = command
+ }
+ }
+
+ var buttons: MutableList<InventoryButton> =
+ InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList()
+
+ override fun close() {
+ InventoryButtons.DConfig.data.buttons = buttons
+ InventoryButtons.DConfig.markDirty()
+ super.close()
+ }
+
+ override fun init() {
+ super.init()
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) {
+ val t = ClipboardUtils.getTextContents()
+ val newButtons = InventoryButtonTemplates.loadTemplate(t)
+ if (newButtons != null)
+ buttons = newButtons.toMutableList()
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) {
+ ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons))
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ }
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1)
+ context.setShaderColor(1f, 1f, 1f, 1f)
+ super.render(context, mouseX, mouseY, delta)
+ for (button in buttons) {
+ val buttonPosition = button.getBounds(lastGuiRect)
+ context.matrices.push()
+ context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F)
+ button.render(context)
+ context.matrices.pop()
+ }
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ if (super.keyPressed(keyCode, scanCode, modifiers)) return true
+ if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
+ close()
+ return true
+ }
+ return false
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ if (super.mouseClicked(mouseX, mouseY, button)) return true
+ val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
+ if (clickedButton != null) {
+ createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
+ return true
+ }
+ if (lastGuiRect.contains(mouseX, mouseY) || lastGuiRect.contains(
+ Point(
+ mouseX + InventoryButton.dimensions.width,
+ mouseY + InventoryButton.dimensions.height,
+ )
+ )
+ ) return true
+ val mx = mouseX.toInt()
+ val my = mouseY.toInt()
+ val anchorRight = mx > lastGuiRect.maxX
+ val anchorBottom = my > lastGuiRect.maxY
+ val offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX
+ val offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY
+ buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null))
+ return true
+ }
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonTemplates.kt
new file mode 100644
index 0000000..6b551b0
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtonTemplates.kt
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.buttons
+
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TemplateUtil
+
+object InventoryButtonTemplates {
+
+ val legacyPrefix = "NEUBUTTONS/"
+ val modernPrefix = "MAYBEONEDAYIWILLHAVEMYOWNFORMAT"
+
+ fun loadTemplate(t: String): List<InventoryButton>? {
+ val buttons = TemplateUtil.maybeDecodeTemplate<List<String>>(legacyPrefix, t) ?: return null
+ return buttons.mapNotNull {
+ try {
+ Firmament.json.decodeFromString<InventoryButton>(it).also {
+ if (it.icon?.startsWith("extra:") == true || it.command?.any { it.isLowerCase() } == true) {
+ MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed"))
+ }
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+ }
+
+ fun saveTemplate(buttons: List<InventoryButton>): String {
+ return TemplateUtil.encodeTemplate(legacyPrefix, buttons.map { Firmament.json.encodeToString(it) })
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtons.kt
new file mode 100644
index 0000000..85e1d88
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/buttons/InventoryButtons.kt
@@ -0,0 +1,82 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.buttons
+
+import me.shedaniel.math.Rectangle
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import moe.nea.firmament.events.HandledScreenClickEvent
+import moe.nea.firmament.events.HandledScreenForegroundEvent
+import moe.nea.firmament.events.HandledScreenPushREIEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.data.DataHolder
+import moe.nea.firmament.util.getRectangle
+
+object InventoryButtons : FirmamentFeature {
+ override val identifier: String
+ get() = "inventory-buttons"
+
+ object TConfig : ManagedConfig(identifier) {}
+ object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+
+ @Serializable
+ data class Data(
+ var buttons: MutableList<InventoryButton> = mutableListOf()
+ )
+
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
+ override fun onLoad() {
+ HandledScreenForegroundEvent.subscribe {
+ val bounds = it.screen.getRectangle()
+ for (button in getValidButtons()) {
+ val buttonBounds = button.getBounds(bounds)
+ it.context.matrices.push()
+ it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
+ button.render(it.context)
+ it.context.matrices.pop()
+ }
+ lastRectangle = bounds
+ }
+ HandledScreenClickEvent.subscribe {
+ val bounds = it.screen.getRectangle()
+ for (button in getValidButtons()) {
+ val buttonBounds = button.getBounds(bounds)
+ if (buttonBounds.contains(it.mouseX, it.mouseY)) {
+ MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
+ break
+ }
+ }
+ }
+ HandledScreenPushREIEvent.subscribe {
+ val bounds = it.screen.getRectangle()
+ for (button in getValidButtons()) {
+ val buttonBounds = button.getBounds(bounds)
+ it.block(buttonBounds)
+ }
+ }
+ }
+
+ var lastRectangle: Rectangle? = null
+ fun openEditor() {
+ ScreenUtil.setScreenLater(
+ InventoryButtonEditor(
+ lastRectangle ?: Rectangle(
+ MC.window.scaledWidth / 2 - 100,
+ MC.window.scaledHeight / 2 - 100,
+ 200, 200,
+ )
+ )
+ )
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
index a91cd71..d643cbf 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
@@ -42,7 +42,6 @@ class FirmamentReiPlugin : REIClientPlugin {
val SKYBLOCK_ITEM_TYPE_ID = Identifier("firmament", "skyblockitems")
}
-
override fun registerTransferHandlers(registry: TransferHandlerRegistry) {
registry.register(TransferHandler { context ->
val screen = context.containerScreen
@@ -66,7 +65,7 @@ class FirmamentReiPlugin : REIClientPlugin {
}
override fun registerExclusionZones(zones: ExclusionZones) {
- zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent(it).rectangles }
+ zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles }
}
override fun registerDisplays(registry: DisplayRegistry) {
diff --git a/src/main/kotlin/moe/nea/firmament/util/FragmentGuiScreen.kt b/src/main/kotlin/moe/nea/firmament/util/FragmentGuiScreen.kt
new file mode 100644
index 0000000..4927d65
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/FragmentGuiScreen.kt
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import io.github.moulberry.moulconfig.gui.GuiContext
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+
+abstract class FragmentGuiScreen(
+ val dismissOnOutOfBounds: Boolean = true
+) : Screen(Text.literal("")) {
+ var popup: MoulConfigFragment? = null
+
+ fun createPopup(context: GuiContext, position: Point) {
+ popup = MoulConfigFragment(context, position) { popup = null }
+ }
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ super.render(context, mouseX, mouseY, delta)
+ context.matrices.push()
+ context.matrices.translate(0f, 0f, 1000f)
+ popup?.render(context, mouseX, mouseY, delta)
+ context.matrices.pop()
+ }
+
+ private inline fun ifPopup(ifYes: (MoulConfigFragment) -> Unit): Boolean {
+ val p = popup ?: return false
+ ifYes(p)
+ return true
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return ifPopup {
+ it.keyPressed(keyCode, scanCode, modifiers)
+ }
+ }
+
+ override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return ifPopup {
+ it.keyReleased(keyCode, scanCode, modifiers)
+ }
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ ifPopup { it.mouseMoved(mouseX, mouseY) }
+ }
+
+ override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return ifPopup {
+ it.mouseReleased(mouseX, mouseY, button)
+ }
+ }
+
+ override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
+ return ifPopup {
+ it.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
+ }
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return ifPopup {
+ if (!Rectangle(
+ it.position,
+ Dimension(it.context.root.width, it.context.root.height)
+ ).contains(Point(mouseX, mouseY))
+ && dismissOnOutOfBounds
+ ) {
+ popup = null
+ } else {
+ it.mouseClicked(mouseX, mouseY, button)
+ }
+ }|| super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ override fun charTyped(chr: Char, modifiers: Int): Boolean {
+ return ifPopup { it.charTyped(chr, modifiers) }
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ return ifPopup {
+ it.mouseScrolled(mouseX, mouseY, verticalAmount)
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/GetRectangle.kt b/src/main/kotlin/moe/nea/firmament/util/GetRectangle.kt
new file mode 100644
index 0000000..261c97a
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/GetRectangle.kt
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import me.shedaniel.math.Rectangle
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+
+fun HandledScreen<*>.getRectangle(): Rectangle {
+ this as AccessorHandledScreen
+ return Rectangle(
+ getX_Firmament(),
+ getY_Firmament(),
+ getBackgroundWidth_Firmament(),
+ getBackgroundHeight_Firmament()
+ )
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/MoulConfigFragment.kt b/src/main/kotlin/moe/nea/firmament/util/MoulConfigFragment.kt
new file mode 100644
index 0000000..bb4a860
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/MoulConfigFragment.kt
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import io.github.moulberry.moulconfig.gui.GuiContext
+import io.github.moulberry.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.DrawContext
+import moe.nea.firmament.util.MC
+
+class MoulConfigFragment(
+ context: GuiContext,
+ val position: Point,
+ val dismiss: () -> Unit
+) : GuiComponentWrapper(context) {
+ init {
+ this.init(MC.instance, MC.screen!!.width, MC.screen!!.height)
+ }
+
+ override fun createContext(drawContext: DrawContext?): GuiImmediateContext {
+ val oldContext = super.createContext(drawContext)
+ return oldContext.translated(
+ position.x,
+ position.y,
+ context.root.width,
+ context.root.height,
+ )
+ }
+
+
+ override fun render(drawContext: DrawContext?, i: Int, j: Int, f: Float) {
+ val ctx = createContext(drawContext)
+ val m = drawContext!!.matrices
+ m.push()
+ m.translate(position.x.toFloat(), position.y.toFloat(), 0F)
+ context.root.render(ctx)
+ m.pop()
+ ctx.renderContext.doDrawTooltip()
+ }
+
+ override fun close() {
+ dismiss()
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt b/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt
new file mode 100644
index 0000000..bea3bc6
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/MoulConfigUtils.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import io.github.moulberry.moulconfig.common.MyResourceLocation
+import io.github.moulberry.moulconfig.gui.GuiContext
+import io.github.moulberry.moulconfig.xml.XMLUniverse
+
+object MoulConfigUtils {
+ val universe = XMLUniverse.getDefaultUniverse()
+ fun loadGui(name: String, bindTo: Any): GuiContext {
+ return GuiContext(universe.load(bindTo, MyResourceLocation("firmament", "gui/$name.xml")))
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/TemplateUtil.kt b/src/main/kotlin/moe/nea/firmament/util/TemplateUtil.kt
new file mode 100644
index 0000000..f5e3466
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/TemplateUtil.kt
@@ -0,0 +1,89 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util
+
+import java.util.*
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.serializer
+import moe.nea.firmament.Firmament
+
+object TemplateUtil {
+
+ @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)
+ }
+
+ inline fun <reified T> encodeTemplate(sharePrefix: String, data: T): String =
+ encodeTemplate(sharePrefix, data, serializer())
+
+ fun <T> encodeTemplate(sharePrefix: String, data: T, serializer: SerializationStrategy<T>): String {
+ require(sharePrefix.endsWith("/"))
+ return intoBase64Encoded(sharePrefix + Firmament.json.encodeToString(serializer, data))
+ }
+
+ inline fun <reified T : Any> maybeDecodeTemplate(sharePrefix: String, data: String): T? =
+ maybeDecodeTemplate(sharePrefix, data, serializer())
+
+ fun <T : Any> maybeDecodeTemplate(sharePrefix: String, data: String, serializer: DeserializationStrategy<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 {
+ Firmament.json.decodeFromString<T>(serializer, decoded.substring(sharePrefix.length))
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+}