aboutsummaryrefslogtreecommitdiff
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
parent47fbb25ab280b6af9496780672780db78fe36f27 (diff)
downloadfirmament-e15406e22ef65b4933274df791632b6c17f594be.tar.gz
firmament-e15406e22ef65b4933274df791632b6c17f594be.tar.bz2
firmament-e15406e22ef65b4933274df791632b6c17f594be.zip
Add Inventory Buttons
-rw-r--r--TODO.txt3
-rw-r--r--build.gradle.kts3
-rw-r--r--gradle/libs.versions.toml2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java11
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java15
-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
-rw-r--r--src/main/resources/assets/firmament/gui/button_editor_fragment.xml32
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json4
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.pngbin0 -> 635 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.license3
24 files changed, 683 insertions, 9 deletions
diff --git a/TODO.txt b/TODO.txt
index b2d735c..e29f518 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -3,7 +3,8 @@
# SPDX-License-Identifier: CC0-1.0
Prio 0 (Bugs):
-- WorldReadyEvent buggy? -> out of date locraw
+- Figure out how to do translations in moulconfig
+
Priority 1:
- recipes
diff --git a/build.gradle.kts b/build.gradle.kts
index 14a6900..1e8eeb6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -66,6 +66,7 @@ repositories {
}
maven("https://server.bbkr.space/artifactory/libs-release")
maven("https://repo.nea.moe/releases")
+ maven("https://maven.notenoughupdates.org/releases")
mavenLocal()
}
@@ -104,8 +105,10 @@ dependencies {
modImplementation(libs.fabric.kotlin)
modImplementation(libs.modmenu)
modImplementation(libs.libgui)
+ modImplementation(libs.moulconfig)
modCompileOnly(libs.explosiveenhancement)
include(libs.libgui)
+ include(libs.moulconfig)
annotationProcessor(libs.mixinextras)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index de0f5de..79e3609 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -26,6 +26,7 @@ mixinextras = "0.2.0-rc.5"
jarvis = "1.1.1"
nealisp = "1.0.0"
explosiveenhancement = "1.2.1-1.20.x"
+moulconfig = "2.4.0"
[libraries]
@@ -35,6 +36,7 @@ fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fab
fabric_kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_kotlin" }
architectury = { module = "dev.architectury:architectury", version.ref = "architectury" }
rei_api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
+moulconfig = { module = "org.notenoughupdates.moulconfig:modern", version.ref = "moulconfig" }
repoparser = { module = "moe.nea:neurepoparser", version.ref = "neurepoparser" }
dbus_java_core = { module = "com.github.hypfvieh:dbus-java-core", version.ref = "dbus_java" }
dbus_java_unixsocket = { module = "com.github.hypfvieh:dbus-java-transport-native-unixsocket", version.ref = "dbus_java" }
diff --git a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
index 3972ffc..35856cb 100644
--- a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
+++ b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
@@ -35,6 +35,10 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> {
@Shadow
public abstract T getScreenHandler();
+ @Shadow
+ protected int y;
+ @Shadow
+ protected int x;
@Unique
PlayerInventory playerInventory;
@@ -52,14 +56,17 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> {
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
public void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
- if (ScreenClickEvent.Companion.publish(new ScreenClickEvent((HandledScreen<?>) (Object) this, mouseX, mouseY, button)).getCancelled()) {
+ if (HandledScreenClickEvent.Companion.publish(new HandledScreenClickEvent((HandledScreen<?>) (Object) this, mouseX, mouseY, button)).getCancelled()) {
cir.setReturnValue(true);
}
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawForeground(Lnet/minecraft/client/gui/DrawContext;II)V", shift = At.Shift.AFTER))
public void onAfterRenderForeground(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
- HandledScreenForegroundEvent.Companion.publish(new HandledScreenForegroundEvent((HandledScreen<?>) (Object) this, mouseX, mouseY, delta));
+ context.getMatrices().push();
+ context.getMatrices().translate(-x, -y, 0);
+ HandledScreenForegroundEvent.Companion.publish(new HandledScreenForegroundEvent((HandledScreen<?>) (Object) this, context, mouseX, mouseY, delta));
+ context.getMatrices().pop();
}
@Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
index c42f819..b14717d 100644
--- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
@@ -6,6 +6,7 @@
package moe.nea.firmament.mixins.accessor;
+import me.shedaniel.math.Rectangle;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.screen.slot.Slot;
import org.jetbrains.annotations.Nullable;
@@ -17,4 +18,18 @@ public interface AccessorHandledScreen {
@Accessor("focusedSlot")
@Nullable
Slot getFocusedSlot_Firmament();
+
+ @Accessor("backgroundWidth")
+ int getBackgroundWidth_Firmament();
+
+ @Accessor("backgroundHeight")
+ int getBackgroundHeight_Firmament();
+
+ @Accessor("x")
+ int getX_Firmament();
+
+ @Accessor("y")
+ int getY_Firmament();
+
+
}
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
+ }
+ }
+
+}
diff --git a/src/main/resources/assets/firmament/gui/button_editor_fragment.xml b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml
new file mode 100644
index 0000000..2992825
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://notenoughupdates.org/moulconfig https://raw.githubusercontent.com/NotEnoughUpdates/MoulConfig/master/MoulConfig.xsd">
+ <Panel>
+ <Column>
+ <Row>
+ <ItemStack value="@getItemIcon"/>
+ <Text text="Icon"/>
+ </Row>
+ <Hover lines="Put your display item in here.;Can be any SkyBlock item id.">
+ <TextField value="@icon" width="180"/>
+ </Hover>
+ <Text text="Command"/>
+ <Hover lines="Put the command in here.;The text box should not start with a /">
+ <Row>
+ <Text text="/"/>
+ <TextField value="@command" width="180"/>
+ </Row>
+ </Hover>
+ <Button onClick="@delete">
+ <Text text="Delete"/>
+ </Button>
+ </Column>
+ </Panel>
+</Root>
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index 4e68efe..4d00d4d 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -61,6 +61,10 @@
"firmament.config.fishing-warning.display-warning": "Display a warning when you are about to hook a fish",
"firmament.config.fishing-warning.highlight-wake-chain": "Highlight fishing particles",
"firmament.protectitem": "Firmament protected your item: ",
+ "firmament.inventory-buttons.save-preset": "Save Preset",
+ "firmament.inventory-buttons.load-preset": "Load Preset",
+ "firmament.inventory-buttons.import-failed": "One of your buttons could only be imported partially",
+ "firmament.config.inventory-buttons": "One of your buttons could only be imported partially",
"firmament.recipe.forge.time": "Forging Time: %s",
"firmament.pv.skills": "Skills",
"firmament.pv.skills.farming": "Farming",
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png
new file mode 100644
index 0000000..3e1e769
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.license
new file mode 100644
index 0000000..c01d463
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: CC0-1.0