aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-07-10 01:34:37 +0200
committerLinnea Gräf <nea@nea.moe>2024-07-10 03:14:55 +0200
commit986ce538f123cdec7e0da12ed89ba7225539df0a (patch)
treedd4a3448878c2af26be3bca81bbc8aa710bdc112 /src/main
parent4e1cda1d64ce60719f6ff6e60c6af4881d81fdd2 (diff)
downloadFirmament-986ce538f123cdec7e0da12ed89ba7225539df0a.tar.gz
Firmament-986ce538f123cdec7e0da12ed89ba7225539df0a.tar.bz2
Firmament-986ce538f123cdec7e0da12ed89ba7225539df0a.zip
Add interactive storage overlay
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java77
-rw-r--r--src/main/java/moe/nea/firmament/init/EarlyRiser.java67
-rw-r--r--src/main/java/moe/nea/firmament/init/HandledScreenRiser.java86
-rw-r--r--src/main/java/moe/nea/firmament/init/RiserUtils.java32
-rw-r--r--src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java7
-rw-r--r--src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java48
-rw-r--r--src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java175
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt4
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/ScreenChangeEvent.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/subscription/Subscription.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt6
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt8
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlay.kt49
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayCustom.kt97
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayScreen.kt369
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverviewScreen.kt126
-rw-r--r--src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt18
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/customgui/CoordRememberingSlot.kt19
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/customgui/CustomGui.kt77
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/customgui/HasCustomGui.kt22
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.pngbin0 -> 1019 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.pngbin0 -> 4348 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta10
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.pngbin0 -> 4583 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png.license2
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.pngbin0 -> 649 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta9
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.pngbin0 -> 1396 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.license3
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta10
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta.license3
-rw-r--r--src/main/resources/firmament.accesswidener4
37 files changed, 1130 insertions, 217 deletions
diff --git a/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java b/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java
new file mode 100644
index 0000000..1d80a9e
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.init;
+
+import me.shedaniel.mm.api.ClassTinkerers;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.VarInsnNode;
+
+import java.lang.reflect.Modifier;
+import java.util.Objects;
+
+public class ClientPlayerRiser extends RiserUtils {
+ String PlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_1657");
+ String World = remapper.mapClassName("intermediary", "net.minecraft.class_1937");
+ String GameProfile = "com.mojang.authlib.GameProfile";
+ String BlockPos = remapper.mapClassName("intermediary", "net.minecraft.class_2338");
+ String AbstractClientPlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_742");
+ String GuiPlayer = "moe.nea.firmament.gui.entity.GuiPlayer";
+ // World world, BlockPos pos, float yaw, GameProfile gameProfile
+ Type constructorDescriptor = Type.getMethodType(Type.VOID_TYPE, getTypeForClassName(World), getTypeForClassName(BlockPos), Type.FLOAT_TYPE, getTypeForClassName(GameProfile));
+
+
+ private void mapClassNode(ClassNode classNode, Type superClass) {
+ for (MethodNode method : classNode.methods) {
+ if (Objects.equals(method.name, "<init>") && Type.getMethodType(method.desc).equals(constructorDescriptor)) {
+ modifyConstructor(method, superClass);
+ return;
+ }
+ }
+ var node = new MethodNode(Opcodes.ASM9, "<init>", constructorDescriptor.getDescriptor(), null, null);
+ classNode.methods.add(node);
+ modifyConstructor(node, superClass);
+ }
+
+
+ private void modifyConstructor(MethodNode method, Type superClass) {
+ method.access = (method.access | Modifier.PUBLIC) & ~Modifier.PRIVATE & ~Modifier.PROTECTED;
+ if (method.instructions.size() != 0) return; // Some other mod has already made a constructor here
+
+ // World world, BlockPos pos, float yaw, GameProfile gameProfile
+ // ALOAD this
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
+
+ // ALOAD World
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1));
+
+ // ALOAD BlockPos
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 2));
+
+ // ALOAD yaw
+ method.instructions.add(new VarInsnNode(Opcodes.FLOAD, 3));
+
+ // ALOAD gameProfile
+ method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 4));
+
+ // Call super
+ method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, superClass.getInternalName(), "<init>", constructorDescriptor.getDescriptor(), false));
+
+ // Return
+ method.instructions.add(new InsnNode(Opcodes.RETURN));
+ }
+
+ @Override
+ public void addTinkerers() {
+ ClassTinkerers.addTransformation(AbstractClientPlayerEntity, it -> mapClassNode(it, getTypeForClassName(PlayerEntity)));
+ ClassTinkerers.addTransformation(GuiPlayer, it -> mapClassNode(it, getTypeForClassName(AbstractClientPlayerEntity)));
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/init/EarlyRiser.java b/src/main/java/moe/nea/firmament/init/EarlyRiser.java
index 268e3f3..6ea0cd4 100644
--- a/src/main/java/moe/nea/firmament/init/EarlyRiser.java
+++ b/src/main/java/moe/nea/firmament/init/EarlyRiser.java
@@ -6,73 +6,10 @@
package moe.nea.firmament.init;
-import me.shedaniel.mm.api.ClassTinkerers;
-import net.fabricmc.loader.api.FabricLoader;
-import net.fabricmc.loader.api.MappingResolver;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.Type;
-import org.objectweb.asm.tree.*;
-
-import java.lang.reflect.Modifier;
-import java.util.Objects;
-
public class EarlyRiser implements Runnable {
- MappingResolver remapper = FabricLoader.getInstance().getMappingResolver();
- String PlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_1657");
- String World = remapper.mapClassName("intermediary", "net.minecraft.class_1937");
- String GameProfile = "com.mojang.authlib.GameProfile";
- String BlockPos = remapper.mapClassName("intermediary", "net.minecraft.class_2338");
- String AbstractClientPlayerEntity = remapper.mapClassName("intermediary", "net.minecraft.class_742");
- String GuiPlayer = "moe.nea.firmament.gui.entity.GuiPlayer";
- // World world, BlockPos pos, float yaw, GameProfile gameProfile
- Type constructorDescriptor = Type.getMethodType(Type.VOID_TYPE, getTypeForClassName(World), getTypeForClassName(BlockPos), Type.FLOAT_TYPE, getTypeForClassName(GameProfile));
-
@Override
public void run() {
- ClassTinkerers.addTransformation(AbstractClientPlayerEntity, it -> mapClassNode(it, getTypeForClassName(PlayerEntity)));
- ClassTinkerers.addTransformation(GuiPlayer, it -> mapClassNode(it, getTypeForClassName(AbstractClientPlayerEntity)));
- }
-
- private void mapClassNode(ClassNode classNode, Type superClass) {
- for (MethodNode method : classNode.methods) {
- if (Objects.equals(method.name, "<init>") && Type.getMethodType(method.desc).equals(constructorDescriptor)) {
- modifyConstructor(method, superClass);
- return;
- }
- }
- var node = new MethodNode(Opcodes.ASM9, "<init>", constructorDescriptor.getDescriptor(), null, null);
- classNode.methods.add(node);
- modifyConstructor(node, superClass);
- }
-
- private Type getTypeForClassName(String className) {
- return Type.getObjectType(className.replace('.', '/'));
- }
-
- private void modifyConstructor(MethodNode method, Type superClass) {
- method.access = (method.access | Modifier.PUBLIC) & ~Modifier.PRIVATE & ~Modifier.PROTECTED;
- if (method.instructions.size() != 0) return; // Some other mod has already made a constructor here
-
- // World world, BlockPos pos, float yaw, GameProfile gameProfile
- // ALOAD this
- method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
-
- // ALOAD World
- method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1));
-
- // ALOAD BlockPos
- method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 2));
-
- // ALOAD yaw
- method.instructions.add(new VarInsnNode(Opcodes.FLOAD, 3));
-
- // ALOAD gameProfile
- method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 4));
-
- // Call super
- method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, superClass.getInternalName(), "<init>", constructorDescriptor.getDescriptor(), false));
-
- // Return
- method.instructions.add(new InsnNode(Opcodes.RETURN));
+ new ClientPlayerRiser().addTinkerers();
+ new HandledScreenRiser().addTinkerers();
}
}
diff --git a/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java
new file mode 100644
index 0000000..3222a91
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java
@@ -0,0 +1,86 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.init;
+
+import me.shedaniel.mm.api.ClassTinkerers;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.InsnNode;
+import org.objectweb.asm.tree.JumpInsnNode;
+import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.VarInsnNode;
+
+import java.lang.reflect.Modifier;
+
+public class HandledScreenRiser extends RiserUtils {
+ String Screen = remapper.mapClassName("intermediary", "net.minecraft.class_437");
+ String HandledScreen = remapper.mapClassName("intermediary", "net.minecraft.class_465");
+ Type mouseScrolledDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE);
+ String mouseScrolled = remapper.mapMethodName("intermediary", "net.minecraft.class_364", "method_25401",
+ mouseScrolledDesc.getDescriptor());
+
+ @Override
+ public void addTinkerers() {
+ ClassTinkerers.addTransformation(HandledScreen, this::handle);
+ }
+
+ void handle(ClassNode classNode) {
+ MethodNode mouseScrolledNode = findMethod(classNode, mouseScrolled, mouseScrolledDesc);
+ if (mouseScrolledNode == null) {
+ mouseScrolledNode = new MethodNode(
+ Modifier.PUBLIC,
+ mouseScrolled,
+ mouseScrolledDesc.getDescriptor(),
+ null,
+ new String[0]
+ );
+ var insns = mouseScrolledNode.instructions;
+ // ALOAD 0, load this
+ insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
+ // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time.
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 1));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 3));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 5));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 7));
+ // INVOKESPECIAL call super method
+ insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(), mouseScrolled, mouseScrolledDesc.getDescriptor()));
+ // IRETURN return int on stack (booleans are int at runtime)
+ insns.add(new InsnNode(Opcodes.IRETURN));
+ classNode.methods.add(mouseScrolledNode);
+ }
+
+ var insns = new InsnList();
+ // ALOAD 0, load this
+ insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
+ // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time.
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 1));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 3));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 5));
+ insns.add(new VarInsnNode(Opcodes.DLOAD, 7));
+ // INVOKEVIRTUAL call custom handler
+ insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,
+ getTypeForClassName(HandledScreen).getInternalName(),
+ "mouseScrolled_firmament",
+ mouseScrolledDesc.getDescriptor()));
+ // Create jump target (but not insert it yet)
+ var jumpIfFalse = new LabelNode();
+ // IFEQ (if returned boolean == 0), jump to jumpIfFalse
+ insns.add(new JumpInsnNode(Opcodes.IFEQ, jumpIfFalse));
+ // LDC 1 (as int, which is what booleans are at runtime)
+ insns.add(new LdcInsnNode(1));
+ // IRETURN return int on stack (booleans are int at runtime)
+ insns.add(new InsnNode(Opcodes.IRETURN));
+ insns.add(jumpIfFalse);
+ mouseScrolledNode.instructions.insert(insns);
+ }
+
+}
diff --git a/src/main/java/moe/nea/firmament/init/RiserUtils.java b/src/main/java/moe/nea/firmament/init/RiserUtils.java
new file mode 100644
index 0000000..4c68dd4
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/init/RiserUtils.java
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.init;
+
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.MappingResolver;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodNode;
+
+public abstract class RiserUtils {
+ protected Type getTypeForClassName(String className) {
+ return Type.getObjectType(className.replace('.', '/'));
+ }
+
+ protected MappingResolver remapper = FabricLoader.getInstance().getMappingResolver();
+
+ public abstract void addTinkerers();
+
+ protected MethodNode findMethod(ClassNode classNode, String name, Type desc) {
+ for (MethodNode method : classNode.methods) {
+ if (method.name.equals(name) && desc.getDescriptor().equals(method.desc))
+ return method;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java b/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java
index 1f46f25..6fa950b 100644
--- a/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java
@@ -1,11 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.sugar.Local;
+import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import moe.nea.firmament.events.ScreenChangeEvent;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
@@ -23,10 +26,12 @@ public abstract class ScreenChangeEventPatch {
public Screen currentScreen;
@Inject(method = "setScreen", at = @At("HEAD"), cancellable = true)
- public void onScreenChange(Screen screen, CallbackInfo ci) {
+ public void onScreenChange(Screen screen, CallbackInfo ci, @Local(argsOnly = true) LocalRef<Screen> screenLocalRef) {
var event = new ScreenChangeEvent(currentScreen, screen);
if (ScreenChangeEvent.Companion.publish(event).getCancelled()) {
ci.cancel();
+ } else if (event.getOverrideScreen() != null) {
+ screenLocalRef.set(event.getOverrideScreen());
}
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java b/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java
new file mode 100644
index 0000000..f5330bf
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins.customgui;
+
+import moe.nea.firmament.util.customgui.CoordRememberingSlot;
+import net.minecraft.screen.slot.Slot;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(Slot.class)
+public class OriginalSlotCoords implements CoordRememberingSlot {
+
+ @Shadow
+ public int x;
+ @Shadow
+ public int y;
+ @Unique
+ public int originalX;
+ @Unique
+ public int originalY;
+
+ @Override
+ public void rememberCoords_firmament() {
+ this.originalX = this.x;
+ this.originalY = this.y;
+ }
+
+ @Override
+ public void restoreCoords_firmament() {
+ this.x = this.originalX;
+ this.y = this.originalY;
+ }
+
+ @Override
+ public int getOriginalX_firmament() {
+ return originalX;
+ }
+
+ @Override
+ public int getOriginalY_firmament() {
+ return originalY;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java
new file mode 100644
index 0000000..59dc259
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java
@@ -0,0 +1,175 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins.customgui;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.util.customgui.CoordRememberingSlot;
+import moe.nea.firmament.util.customgui.CustomGui;
+import moe.nea.firmament.util.customgui.HasCustomGui;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.collection.DefaultedList;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(HandledScreen.class)
+public class PatchHandledScreen<T extends ScreenHandler> extends Screen implements HasCustomGui {
+ @Shadow
+ @Final
+ protected T handler;
+ @Shadow
+ protected int x;
+ @Shadow
+ protected int y;
+ @Unique
+ public CustomGui override;
+ @Unique
+ public boolean hasRememberedSlots = false;
+
+ protected PatchHandledScreen(Text title) {
+ super(title);
+ }
+
+ @Nullable
+ @Override
+ public CustomGui getCustomGui_Firmament() {
+ return override;
+ }
+
+ @Override
+ public void setCustomGui_Firmament(@Nullable CustomGui gui) {
+ this.override = gui;
+ }
+
+ public boolean mouseScrolled_firmament(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
+ return override != null && override.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
+ }
+
+ @Inject(method = "init", at = @At("TAIL"))
+ private void onInit(CallbackInfo ci) {
+ if (override != null) {
+ override.onInit();
+ }
+ }
+
+ @Inject(method = "drawForeground", at = @At("HEAD"), cancellable = true)
+ private void onDrawForeground(DrawContext context, int mouseX, int mouseY, CallbackInfo ci) {
+ if (override != null && !override.shouldDrawForeground())
+ ci.cancel();
+ }
+
+
+ @Unique
+ private Slot didBeforeSlotRender;
+
+ @WrapOperation(
+ method = "render",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/util/collection/DefaultedList;get(I)Ljava/lang/Object;"))
+ private Object beforeSlotRender(DefaultedList instance, int index, Operation<Object> original, @Local(argsOnly = true) DrawContext context) {
+ var slot = (Slot) original.call(instance, index);
+ if (override != null) {
+ didBeforeSlotRender = slot;
+ override.beforeSlotRender(context, slot);
+ }
+ return slot;
+ }
+
+ @Inject(method = "render",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/DefaultedList;size()I"))
+ private void afterSlotRender(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (override != null && didBeforeSlotRender != null) {
+ override.afterSlotRender(context, didBeforeSlotRender);
+ didBeforeSlotRender = null;
+ }
+ }
+
+ @Inject(method = "isClickOutsideBounds", at = @At("HEAD"), cancellable = true)
+ public void onIsClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button, CallbackInfoReturnable<Boolean> cir) {
+ if (override != null) {
+ cir.setReturnValue(override.isClickOutsideBounds(mouseX, mouseY));
+ }
+ }
+
+ @Inject(method = "isPointWithinBounds", at = @At("HEAD"), cancellable = true)
+ public void onIsPointWithinBounds(int x, int y, int width, int height, double pointX, double pointY, CallbackInfoReturnable<Boolean> cir) {
+ if (override != null) {
+ cir.setReturnValue(override.isPointWithinBounds(x + this.x, y + this.y, width, height, pointX, pointY));
+ }
+ }
+
+ @Inject(method = "isPointOverSlot", at = @At("HEAD"), cancellable = true)
+ public void onIsPointOverSlot(Slot slot, double pointX, double pointY, CallbackInfoReturnable<Boolean> cir) {
+ if (override != null) {
+ cir.setReturnValue(override.isPointOverSlot(slot, this.x, this.y, pointX, pointY));
+ }
+ }
+
+ @Inject(method = "render", at = @At("HEAD"))
+ public void moveSlots(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (override != null) {
+ for (Slot slot : handler.slots) {
+ if (!hasRememberedSlots) {
+ ((CoordRememberingSlot) slot).rememberCoords_firmament();
+ }
+ override.moveSlot(slot);
+ }
+ hasRememberedSlots = true;
+ } else {
+ if (hasRememberedSlots) {
+ for (Slot slot : handler.slots) {
+ ((CoordRememberingSlot) slot).restoreCoords_firmament();
+ }
+ hasRememberedSlots = false;
+ }
+ }
+ }
+
+ @Inject(at = @At("HEAD"), method = "close", cancellable = true)
+ private void onVoluntaryExit(CallbackInfo ci) {
+ if (override != null) {
+ if (!override.onVoluntaryExit())
+ ci.cancel();
+ }
+ }
+
+ @WrapWithCondition(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V"))
+ public boolean preventDrawingBackground(HandledScreen instance, DrawContext drawContext, float delta, int mouseX, int mouseY) {
+ if (override != null) {
+ override.render(drawContext, delta, mouseX, mouseY);
+ }
+ return override == null;
+ }
+
+ @WrapOperation(
+ method = "mouseClicked",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;mouseClicked(DDI)Z"))
+ public boolean overrideMouseClicks(HandledScreen instance, double mouseX, double mouseY, int button,
+ Operation<Boolean> original) {
+ if (override != null) {
+ if (override.mouseClick(mouseX, mouseY, button))
+ return true;
+ }
+ return original.call(instance, mouseX, mouseY, button);
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index 6ec09f2..4211963 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -15,7 +15,7 @@ import net.minecraft.text.Text
import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
-import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
+import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.gui.config.BooleanHandler
@@ -107,7 +107,7 @@ fun firmamentCommand() = literal("firmament") {
}
thenLiteral("storage") {
thenExecute {
- ScreenUtil.setScreenLater(StorageOverlayScreen())
+ ScreenUtil.setScreenLater(StorageOverviewScreen())
MC.player?.networkHandler?.sendChatCommand("storage")
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/events/ScreenChangeEvent.kt b/src/main/kotlin/moe/nea/firmament/events/ScreenChangeEvent.kt
index c6b0a7f..4683fac 100644
--- a/src/main/kotlin/moe/nea/firmament/events/ScreenChangeEvent.kt
+++ b/src/main/kotlin/moe/nea/firmament/events/ScreenChangeEvent.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -9,5 +10,6 @@ package moe.nea.firmament.events
import net.minecraft.client.gui.screen.Screen
data class ScreenChangeEvent(val old: Screen?, val new: Screen?) : FirmamentEvent.Cancellable() {
+ var overrideScreen: Screen? = null
companion object : FirmamentEventBus<ScreenChangeEvent>()
}
diff --git a/src/main/kotlin/moe/nea/firmament/events/subscription/Subscription.kt b/src/main/kotlin/moe/nea/firmament/events/subscription/Subscription.kt
index a4542e7..bf29543 100644
--- a/src/main/kotlin/moe/nea/firmament/events/subscription/Subscription.kt
+++ b/src/main/kotlin/moe/nea/firmament/events/subscription/Subscription.kt
@@ -15,7 +15,7 @@ interface SubscriptionOwner {
}
data class Subscription<T : FirmamentEvent>(
- val owner: SubscriptionOwner,
+ val owner: Any,
val invoke: (T) -> Unit,
val eventBus: FirmamentEventBus<T>,
)
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
index f2b2d25..f047ad3 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
@@ -98,11 +98,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
}
private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
- if (it.owner.delegateFeature in features.values) { // TODO: better check here, somehow. probably implement some interface method
- it.eventBus.subscribe(false, it.invoke) // TODO: pass through receivesCancelled from the annotation
- } else {
- Firmament.logger.error("Ignoring event listener for ${it.eventBus} in ${it.owner}")
- }
+ it.eventBus.subscribe(false, it.invoke)
}
fun loadFeature(feature: FirmamentFeature) {
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt
index 006ad54..a38896f 100644
--- a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -22,11 +23,6 @@ sealed interface StorageBackingHandle {
}
/**
- * No open "server side" screen.
- */
- object None : StorageBackingHandle
-
- /**
* The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage`
*/
data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen
@@ -48,7 +44,7 @@ sealed interface StorageBackingHandle {
* selection screen.
*/
fun fromScreen(screen: Screen?): StorageBackingHandle? {
- if (screen == null) return None
+ if (screen == null) return null
if (screen !is GenericContainerScreen) return null
val title = screen.title.unformattedString
if (title == "Storage") return Overview(screen.screenHandler)
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlay.kt
index 0426e34..f3a0f47 100644
--- a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlay.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlay.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -9,12 +10,14 @@ package moe.nea.firmament.features.inventory.storageoverlay
import java.util.SortedMap
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
+import net.minecraft.client.gui.screen.ingame.HandledScreen
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.ScreenUtil.setScreenLater
+import moe.nea.firmament.util.customgui.customGui
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object StorageOverlay : FirmamentFeature {
@@ -33,13 +36,15 @@ object StorageOverlay : FirmamentFeature {
val margin by integer("margin", 1, 60) { 20 }
}
+ fun adjustScrollSpeed(amount: Double): Double {
+ return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1)
+ }
+
override val config: TConfig
get() = TConfig
var lastStorageOverlay: Screen? = null
- var shouldReturnToStorageOverlayFrom: Screen? = null
- var shouldReturnToStorageOverlay: Screen? = null
- var currentHandler: StorageBackingHandle? = StorageBackingHandle.None
+ var currentHandler: StorageBackingHandle? = null
@Subscribe
fun onTick(event: TickEvent) {
@@ -47,35 +52,25 @@ object StorageOverlay : FirmamentFeature {
}
@Subscribe
- fun onScreenChangeLegacy(event: ScreenChangeEvent) {
- currentHandler = StorageBackingHandle.fromScreen(event.new)
- if (event.old is StorageOverlayScreen && !event.old.isClosing) {
- event.old.setHandler(currentHandler)
- if (currentHandler != null)
- // TODO: Consider instead only replacing rendering? might make a lot of stack handling easier
- event.cancel()
- }
- }
-
- @Subscribe
fun onScreenChange(it: ScreenChangeEvent) {
- if (lastStorageOverlay != null && it.new != null) {
- shouldReturnToStorageOverlay = lastStorageOverlay
- shouldReturnToStorageOverlayFrom = it.new
- lastStorageOverlay = null
- } else if (it.old === shouldReturnToStorageOverlayFrom) {
- if (shouldReturnToStorageOverlay != null && it.new == null)
- setScreenLater(shouldReturnToStorageOverlay)
- shouldReturnToStorageOverlay = null
- shouldReturnToStorageOverlayFrom = null
+ val storageOverlayScreen = it.old as? StorageOverlayScreen
+ ?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview
+ if (it.new == null && storageOverlayScreen != null && !storageOverlayScreen.isExiting) {
+ it.overrideScreen = storageOverlayScreen
+ return
}
+ val screen = it.new as? GenericContainerScreen ?: return
+ currentHandler = StorageBackingHandle.fromScreen(screen)
+ screen.customGui = StorageOverlayCustom(
+ currentHandler as? StorageBackingHandle.Page ?: return,
+ screen,
+ storageOverlayScreen ?: return)
}
- private fun rememberContent(handler: StorageBackingHandle) {
+ fun rememberContent(handler: StorageBackingHandle) {
// TODO: Make all of these functions work on deltas / updates instead of the entire contents
val data = Data.data?.storageInventories ?: return
when (handler) {
- StorageBackingHandle.None -> {}
is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data)
is StorageBackingHandle.Page -> rememberPage(handler, data)
}
@@ -90,7 +85,7 @@ object StorageOverlay : FirmamentFeature {
// Ignore unloaded item stacks
if (stack.isEmpty) continue
val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue
- val isEmpty = stack.item in StorageOverlayScreen.emptyStorageSlotItems
+ val isEmpty = stack.item in StorageOverviewScreen.emptyStorageSlotItems
if (slot in data) {
if (isEmpty)
data.remove(slot)
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayCustom.kt
new file mode 100644
index 0000000..d1cdef2
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayCustom.kt
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.storageoverlay
+
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
+import net.minecraft.entity.player.PlayerInventory
+import net.minecraft.screen.slot.Slot
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+import moe.nea.firmament.util.customgui.CustomGui
+
+class StorageOverlayCustom(
+ val handler: StorageBackingHandle.Page,
+ val screen: GenericContainerScreen,
+ val overview: StorageOverlayScreen,
+) : CustomGui() {
+ override fun onVoluntaryExit(): Boolean {
+ overview.isExiting = true
+ return super.onVoluntaryExit()
+ }
+
+ override fun getBounds(): List<Rectangle> {
+ return overview.getBounds()
+ }
+
+ override fun afterSlotRender(context: DrawContext, slot: Slot) {
+ if (slot.inventory !is PlayerInventory)
+ context.disableScissor()
+ }
+
+ override fun beforeSlotRender(context: DrawContext, slot: Slot) {
+ if (slot.inventory !is PlayerInventory)
+ overview.createScissors(context)
+ }
+
+ override fun onInit() {
+ overview.init(MinecraftClient.getInstance(), screen.width, screen.height)
+ }
+
+ override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
+ if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY))
+ return false
+ if (slot.inventory !is PlayerInventory) {
+ if (!overview.getScrollPanelInner().contains(pointX, pointY))
+ return false
+ }
+ return true
+ }
+
+ override fun shouldDrawForeground(): Boolean {
+ return false
+ }
+
+ override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return overview.mouseClicked(mouseX, mouseY, button, handler.storagePageSlot)
+ }
+
+ override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) {
+ overview.drawBackgrounds(drawContext)
+ overview.drawPages(drawContext,
+ mouseX,
+ mouseY,
+ delta,
+ handler.storagePageSlot,
+ screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9),
+ Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament))
+ overview.drawScrollBar(drawContext)
+ }
+
+ override fun moveSlot(slot: Slot) {
+ val index = slot.index
+ if (index in 0..<36) {
+ val (x, y) = overview.getPlayerInventorySlotPosition(index)
+ slot.x = x - (screen as AccessorHandledScreen).x_Firmament
+ slot.y = y - screen.y_Firmament
+ } else {
+ slot.x = -100000
+ slot.y = -100000
+ }
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayScreen.kt
index e0cd3c8..ac89f8c 100644
--- a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -1,147 +1,310 @@
/*
- * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.features.inventory.storageoverlay
-import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.assertNotNullOr
-import moe.nea.firmament.util.toShedaniel
-import net.minecraft.block.Blocks
+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.item.Item
-import net.minecraft.item.Items
-import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
+import net.minecraft.screen.slot.Slot
import net.minecraft.text.Text
-import net.minecraft.util.DyeColor
-import kotlin.math.max
+import net.minecraft.util.Identifier
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.assertTrueOr
-class StorageOverlayScreen() : Screen(Text.empty()) {
- companion object {
- val emptyStorageSlotItems = listOf<Item>(
- Blocks.RED_STAINED_GLASS_PANE.asItem(),
- Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
- Items.GRAY_DYE
- )
- val pageWidth get() = 19 * 9
- }
+class StorageOverlayScreen : Screen(Text.literal("")) {
- private var handler: StorageBackingHandle = StorageBackingHandle.None
- val content = StorageOverlay.Data.data ?: StorageData()
- var isClosing = false
+ companion object {
+ val PLAYER_WIDTH = 184
+ val PLAYER_HEIGHT = 91
+ val PLAYER_Y_INSET = 3
+ val SLOT_SIZE = 18
+ val PADDING = 10
+ val PAGE_WIDTH = SLOT_SIZE * 9
+ val HOTBAR_X = 12
+ val HOTBAR_Y = 67
+ val MAIN_INVENTORY_Y = 9
+ val SCROLL_BAR_WIDTH = 8
+ val SCROLL_BAR_HEIGHT = 16
- private fun discardOldHandle() {
- val player = assertNotNullOr(MC.player) { return }
- val handle = this.handler
- if (handle is StorageBackingHandle.HasBackingScreen) {
- player.networkHandler.sendPacket(CloseHandledScreenC2SPacket(handle.handler.syncId))
- if (player.currentScreenHandler === handle.handler) {
- player.currentScreenHandler = player.playerScreenHandler
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("teststorage") {
+ executes {
+ ScreenUtil.setScreenLater(StorageOverlayScreen())
+ MC.sendCommand("ec")
+ 0
+ }
}
}
}
- fun setHandler(handler: StorageBackingHandle?) {
- discardOldHandle()
- if (handler != null)
- this.handler = handler
+ var isExiting: Boolean = false
+ var scroll: Float = 0F
+ var pageWidthCount = StorageOverlay.TConfig.rows
+
+ inner class Measurements {
+ val innerScrollPanelWidth = PAGE_WIDTH * pageWidthCount + (pageWidthCount - 1) * PADDING
+ val overviewWidth = innerScrollPanelWidth + 3 * PADDING + SCROLL_BAR_WIDTH
+ val x = width / 2 - overviewWidth / 2
+ val overviewHeight = minOf(3 * 18 * 6, height - PLAYER_HEIGHT - minOf(80, height / 10))
+ val innerScrollPanelHeight = overviewHeight - PADDING * 2
+ val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
+ val playerX = width / 2 - PLAYER_WIDTH / 2
+ val playerY = y + overviewHeight - PLAYER_Y_INSET
}
- var scroll = 0
- var lastRenderedHeight = 0
+ var measurements = Measurements()
+
+ var lastRenderedInnerHeight = 0
+ override fun init() {
+ super.init()
+ pageWidthCount = StorageOverlay.TConfig.rows
+ .coerceAtMost((width - PADDING) / (PAGE_WIDTH + PADDING))
+ .coerceAtLeast(1)
+ measurements = Measurements()
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toFloat()
+ .coerceAtMost(getMaxScroll())
+ .coerceAtLeast(0F)
+ return true
+ }
+
+ fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height
+
+ val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory")
+ val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background")
+ val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row")
+ val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background")
+ val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob")
+
+ override fun close() {
+ isExiting = true
+ super.close()
+ }
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
- context.fill(0, 0, width, height, 0x90000000.toInt())
- layoutedForEach { (key, value), offsetX, offsetY ->
- context.matrices.push()
- context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
- renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
- context.matrices.pop()
+ drawBackgrounds(context)
+ drawPages(context, mouseX, mouseY, delta, null, null, Point())
+ drawScrollBar(context)
+ drawPlayerInventory(context, mouseX, mouseY, delta)
+ }
+
+ fun getScrollbarPercentage(): Float {
+ return scroll / getMaxScroll()
+ }
+
+ fun drawScrollBar(context: DrawContext) {
+ val sbRect = getScrollBarRect()
+ context.drawGuiTexture(
+ scrollbarBackground,
+ sbRect.minX, sbRect.minY,
+ sbRect.width, sbRect.height,
+ )
+ context.drawGuiTexture(
+ scrollbarKnob,
+ sbRect.minX, sbRect.minY + (getScrollbarPercentage() * (sbRect.height - SCROLL_BAR_HEIGHT)).toInt(),
+ SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT
+ )
+ }
+
+ fun drawBackgrounds(context: DrawContext) {
+ context.drawGuiTexture(upperBackgroundSprite,
+ measurements.x,
+ measurements.y,
+ 0,
+ measurements.overviewWidth,
+ measurements.overviewHeight)
+ context.drawGuiTexture(playerInventorySprite,
+ measurements.playerX,
+ measurements.playerY,
+ 0,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT)
+ }
+
+ fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
+ if (int < 9) {
+ return Pair(measurements.playerX + int * SLOT_SIZE + HOTBAR_X, HOTBAR_Y + measurements.playerY)
}
+ return Pair(
+ measurements.playerX + (int % 9) * SLOT_SIZE + HOTBAR_X,
+ measurements.playerY + (int / 9 - 1) * SLOT_SIZE + MAIN_INVENTORY_Y
+ )
}
- inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
- var offsetY = 0
- var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
- var totalHeight = -currentMaxHeight
- content.storageInventories.onEachIndexed { index, (key, value) ->
- val pageX = (index % StorageOverlay.config.rows)
- if (pageX == 0) {
- currentMaxHeight += StorageOverlay.config.padding
- offsetY += currentMaxHeight
- totalHeight += currentMaxHeight
- currentMaxHeight = 0
- }
- val xPosition =
- width / 2 - (StorageOverlay.config.rows * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
- onEach(Pair(key, value), xPosition, offsetY)
- val height = getStorePageHeight(value)
- currentMaxHeight = max(currentMaxHeight, height)
+ fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ val items = MC.player?.inventory?.main ?: return
+ items.withIndex().forEach { (index, item) ->
+ val (x, y) = getPlayerInventorySlotPosition(index)
+ context.drawItem(item, x, y, 0)
+ context.drawItemInSlot(textRenderer, item, x, y)
}
- lastRenderedHeight = totalHeight + currentMaxHeight
}
- override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
- layoutedForEach { (k, p), x, y ->
- val rx = mouseX - x
- val ry = mouseY - y
- if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
- close()
- StorageOverlay.lastStorageOverlay = this
- k.navigateTo()
- return true
- }
+ fun getScrollBarRect(): Rectangle {
+ return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
+ measurements.y + PADDING,
+ SCROLL_BAR_WIDTH,
+ measurements.innerScrollPanelHeight)
+ }
+
+ fun getScrollPanelInner(): Rectangle {
+ return Rectangle(measurements.x + PADDING,
+ measurements.y + PADDING,
+ measurements.innerScrollPanelWidth,
+ measurements.innerScrollPanelHeight)
+ }
+
+ fun createScissors(context: DrawContext) {
+ val rect = getScrollPanelInner()
+ context.enableScissor(
+ rect.minX, rect.minY,
+ rect.maxX, rect.maxY
+ )
+ }
+
+ fun drawPages(
+ context: DrawContext, mouseX: Int, mouseY: Int, delta: Float,
+ excluding: StoragePageSlot?,
+ slots: List<Slot>?,
+ slotOffset: Point
+ ) {
+ createScissors(context)
+ val data = StorageOverlay.Data.data ?: StorageData()
+ layoutedForEach(data) { rect, page, inventory ->
+ drawPage(context,
+ rect.x,
+ rect.y,
+ page, inventory,
+ if (excluding == page) slots else null,
+ slotOffset
+ )
}
- return super.mouseClicked(mouseX, mouseY, button)
+ context.disableScissor()
}
- fun getStorePageHeight(page: StorageData.StorageInventory): Int {
- return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return mouseClicked(mouseX, mouseY, button, null)
}
- override fun mouseScrolled(
- mouseX: Double,
- mouseY: Double,
- horizontalAmount: Double,
- verticalAmount: Double
- ): Boolean {
- scroll =
- (scroll + verticalAmount * StorageOverlay.config.scrollSpeed *
- (if (StorageOverlay.config.inverseScroll) 1 else -1)).toInt()
- .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
- return true
+ fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean {
+ if (getScrollPanelInner().contains(mouseX, mouseY)) {
+ val data = StorageOverlay.Data.data ?: StorageData()
+ layoutedForEach(data) { rect, page, _ ->
+ if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) {
+ page.navigateTo()
+ return true
+ }
+ }
+ return false
+ }
+ val sbRect = getScrollBarRect()
+ if (sbRect.contains(mouseX, mouseY)) {
+ // TODO: support dragging of the mouse and such
+ val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight()
+ scroll = (getMaxScroll() * percentage).toFloat()
+ mouseScrolled(0.0, 0.0, 0.0, 0.0)
+ return true
+ }
+ return false
}
- private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
- context.drawText(MC.font, page.title, 2, 2, -1, true)
- val inventory = page.inventory
- if (inventory == null) {
- // TODO: Missing texture
- context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
- context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
- return
+ private inline fun layoutedForEach(
+ data: StorageData,
+ func: (
+ rectangle: Rectangle,
+ page: StoragePageSlot, inventory: StorageData.StorageInventory,
+ ) -> Unit
+ ) {
+ var yOffset = -scroll.toInt()
+ var xOffset = 0
+ var maxHeight = 0
+ for ((page, inventory) in data.storageInventories.entries) {
+ val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
+ ?: 18
+ maxHeight = maxOf(maxHeight, currentHeight)
+ val rect = Rectangle(
+ measurements.x + PADDING + (PAGE_WIDTH + PADDING) * xOffset,
+ yOffset + measurements.y + PADDING,
+ PAGE_WIDTH,
+ currentHeight
+ )
+ func(rect, page, inventory)
+ xOffset++
+ if (xOffset >= pageWidthCount) {
+ yOffset += maxHeight
+ xOffset = 0
+ maxHeight = 0
+ }
}
+ lastRenderedInnerHeight = maxHeight + yOffset + scroll.toInt()
+ }
- for ((index, stack) in inventory.stacks.withIndex()) {
- val x = (index % 9) * 19
- val y = (index / 9) * 19 + MC.font.fontHeight + 2
- if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
- context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
+ fun drawPage(
+ context: DrawContext,
+ x: Int,
+ y: Int,
+ page: StoragePageSlot,
+ inventory: StorageData.StorageInventory,
+ slots: List<Slot>?,
+ slotOffset: Point,
+ ): Int {
+ val inv = inventory.inventory
+ if (inv == null) {
+ context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
+ context.drawText(textRenderer,
+ Text.literal("TODO: open this page"),
+ x + 4,
+ y + 4,
+ -1,
+ true)
+ return 18
+ }
+ assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
+ val name = page.defaultName()
+ context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
+ if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
+ context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
+ inv.stacks.forEachIndexed { index, stack ->
+ val slotX = (index % 9) * SLOT_SIZE + x + 1
+ val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
+ if (slots == null) {
+ context.drawItem(stack, slotX, slotY)
+ context.drawItemInSlot(textRenderer, stack, slotX, slotY)
} else {
- context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
+ val slot = slots[index]
+ slot.x = slotX - slotOffset.x
+ slot.y = slotY - slotOffset.y
}
- context.drawItem(stack, x + 1, y + 1)
- context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
}
+ return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
}
- override fun close() {
- discardOldHandle()
- isClosing = true
- super.close()
+ fun getBounds(): List<Rectangle> {
+ return listOf(
+ Rectangle(measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight),
+ Rectangle(measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT))
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverviewScreen.kt
new file mode 100644
index 0000000..22b612b
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageOverviewScreen.kt
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.inventory.storageoverlay
+
+import kotlin.math.max
+import net.minecraft.block.Blocks
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.item.Item
+import net.minecraft.item.Items
+import net.minecraft.text.Text
+import net.minecraft.util.DyeColor
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.toShedaniel
+
+class StorageOverviewScreen() : Screen(Text.empty()) {
+ companion object {
+ val emptyStorageSlotItems = listOf<Item>(
+ Blocks.RED_STAINED_GLASS_PANE.asItem(),
+ Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
+ Items.GRAY_DYE
+ )
+ val pageWidth get() = 19 * 9
+ }
+
+ val content = StorageOverlay.Data.data ?: StorageData()
+ var isClosing = false
+
+ var scroll = 0
+ var lastRenderedHeight = 0
+
+ override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ super.render(context, mouseX, mouseY, delta)
+ context.fill(0, 0, width, height, 0x90000000.toInt())
+ layoutedForEach { (key, value), offsetX, offsetY ->
+ context.matrices.push()
+ context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
+ renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
+ context.matrices.pop()
+ }
+ }
+
+ inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
+ var offsetY = 0
+ var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
+ var totalHeight = -currentMaxHeight
+ content.storageInventories.onEachIndexed { index, (key, value) ->
+ val pageX = (index % StorageOverlay.config.rows)
+ if (pageX == 0) {
+ currentMaxHeight += StorageOverlay.config.padding
+ offsetY += currentMaxHeight
+ totalHeight += currentMaxHeight
+ currentMaxHeight = 0
+ }
+ val xPosition =
+ width / 2 - (StorageOverlay.config.rows * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
+ onEach(Pair(key, value), xPosition, offsetY)
+ val height = getStorePageHeight(value)
+ currentMaxHeight = max(currentMaxHeight, height)
+ }
+ lastRenderedHeight = totalHeight + currentMaxHeight
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ layoutedForEach { (k, p), x, y ->
+ val rx = mouseX - x
+ val ry = mouseY - y
+ if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
+ close()
+ StorageOverlay.lastStorageOverlay = this
+ k.navigateTo()
+ return true
+ }
+ }
+ return super.mouseClicked(mouseX, mouseY, button)
+ }
+
+ fun getStorePageHeight(page: StorageData.StorageInventory): Int {
+ return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ scroll =
+ (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt()
+ .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
+ return true
+ }
+
+ private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
+ context.drawText(MC.font, page.title, 2, 2, -1, true)
+ val inventory = page.inventory
+ if (inventory == null) {
+ // TODO: Missing texture
+ context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
+ context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
+ return
+ }
+
+ for ((index, stack) in inventory.stacks.withIndex()) {
+ val x = (index % 9) * 19
+ val y = (index / 9) * 19 + MC.font.fontHeight + 2
+ if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
+ context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
+ } else {
+ context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
+ }
+ context.drawItem(stack, x + 1, y + 1)
+ context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
+ }
+ }
+
+ override fun close() {
+ isClosing = true
+ super.close()
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
index a916b3e..85efd15 100644
--- a/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
+++ b/src/main/kotlin/moe/nea/firmament/rei/FirmamentReiPlugin.kt
@@ -19,20 +19,21 @@ import me.shedaniel.rei.api.client.registry.transfer.TransferHandlerRegistry
import me.shedaniel.rei.api.common.entry.EntryStack
import me.shedaniel.rei.api.common.entry.type.EntryTypeRegistry
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
+import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
import moe.nea.firmament.rei.recipes.SBCraftingRecipe
import moe.nea.firmament.rei.recipes.SBForgeRecipe
+import moe.nea.firmament.rei.recipes.SBMobDropRecipe
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.unformattedString
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
-import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.item.ItemStack
-import net.minecraft.text.Text
-import net.minecraft.util.Identifier
-import moe.nea.firmament.rei.recipes.SBMobDropRecipe
class FirmamentReiPlugin : REIClientPlugin {
@@ -44,6 +45,7 @@ class FirmamentReiPlugin : REIClientPlugin {
val SKYBLOCK_ITEM_TYPE_ID = Identifier.of("firmament", "skyblockitems")
}
+
override fun registerTransferHandlers(registry: TransferHandlerRegistry) {
registry.register(TransferHandler { context ->
val screen = context.containerScreen
@@ -69,6 +71,7 @@ class FirmamentReiPlugin : REIClientPlugin {
override fun registerExclusionZones(zones: ExclusionZones) {
zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles }
+ zones.register(StorageOverlayScreen::class.java) { it.getBounds() }
}
override fun registerDisplays(registry: DisplayRegistry) {
@@ -80,7 +83,8 @@ class FirmamentReiPlugin : REIClientPlugin {
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator
)
- registry.registerDisplayGenerator(SBMobDropRecipe.Category.categoryIdentifier, SkyblockMobDropRecipeDynamicGenerator)
+ registry.registerDisplayGenerator(SBMobDropRecipe.Category.categoryIdentifier,
+ SkyblockMobDropRecipeDynamicGenerator)
}
override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) {
diff --git a/src/main/kotlin/moe/nea/firmament/util/customgui/CoordRememberingSlot.kt b/src/main/kotlin/moe/nea/firmament/util/customgui/CoordRememberingSlot.kt
new file mode 100644
index 0000000..cf290af
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/customgui/CoordRememberingSlot.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util.customgui
+
+import net.minecraft.screen.slot.Slot
+
+interface CoordRememberingSlot {
+ fun rememberCoords_firmament()
+ fun restoreCoords_firmament()
+ fun getOriginalX_firmament(): Int
+ fun getOriginalY_firmament(): Int
+}
+
+val Slot.originalX get() = (this as CoordRememberingSlot).getOriginalX_firmament()
+val Slot.originalY get() = (this as CoordRememberingSlot).getOriginalY_firmament()
diff --git a/src/main/kotlin/moe/nea/firmament/util/customgui/CustomGui.kt b/src/main/kotlin/moe/nea/firmament/util/customgui/CustomGui.kt
new file mode 100644
index 0000000..40c9ade
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/customgui/CustomGui.kt
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util.customgui
+
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.screen.slot.Slot
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenPushREIEvent
+
+abstract class CustomGui {
+
+ abstract fun getBounds(): List<Rectangle>
+
+ open fun moveSlot(slot: Slot) {
+ // TODO: return a Pair maybe? worth an investigation
+ }
+
+ companion object {
+ @Subscribe
+ fun onExclusionZone(event: HandledScreenPushREIEvent) {
+ val customGui = event.screen.customGui ?: return
+ event.rectangles.addAll(customGui.getBounds())
+ }
+ }
+
+ open fun render(
+ drawContext: DrawContext,
+ delta: Float,
+ mouseX: Int,
+ mouseY: Int
+ ) {
+ }
+
+ open fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ return false
+ }
+
+ open fun afterSlotRender(context: DrawContext, slot: Slot) {}
+ open fun beforeSlotRender(context: DrawContext, slot: Slot) {}
+ open fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
+ return false
+ }
+
+ open fun isClickOutsideBounds(mouseX: Double, mouseY: Double): Boolean {
+ return getBounds().none { it.contains(mouseX, mouseY) }
+ }
+
+ open fun isPointWithinBounds(
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ pointX: Double,
+ pointY: Double,
+ ): Boolean {
+ return getBounds().any { it.contains(pointX, pointY) } &&
+ Rectangle(x, y, width, height).contains(pointX, pointY)
+ }
+
+ open fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
+ return isPointWithinBounds(slot.x + xOffset, slot.y + yOffset, 16, 16, pointX, pointY)
+ }
+
+ open fun onInit() {}
+ open fun shouldDrawForeground(): Boolean {
+ return true
+ }
+
+ open fun onVoluntaryExit(): Boolean {
+ return true
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/customgui/HasCustomGui.kt b/src/main/kotlin/moe/nea/firmament/util/customgui/HasCustomGui.kt
new file mode 100644
index 0000000..54eb50d
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/util/customgui/HasCustomGui.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.util.customgui
+
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+
+@Suppress("FunctionName")
+interface HasCustomGui {
+ fun getCustomGui_Firmament(): CustomGui?
+ fun setCustomGui_Firmament(gui: CustomGui?)
+}
+
+var <T : HandledScreen<*>> T.customGui: CustomGui?
+ get() = (this as HasCustomGui).getCustomGui_Firmament()
+ set(value) {
+ (this as HasCustomGui).setCustomGui_Firmament(value)
+ }
+
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
new file mode 100644
index 0000000..8dccb7f
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
new file mode 100644
index 0000000..10a3d83
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
new file mode 100644
index 0000000..94b9a1d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
@@ -0,0 +1,10 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "nine_slice",
+ "width": 17,
+ "height": 18,
+ "border": 2
+ }
+ }
+}
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png
new file mode 100644
index 0000000..8ced28a
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png.license
new file mode 100644
index 0000000..41cf35d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
new file mode 100644
index 0000000..5ffc990
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
new file mode 100644
index 0000000..cd2857e
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
@@ -0,0 +1,9 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "tile",
+ "width": 162,
+ "height": 18
+ }
+ }
+}
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
new file mode 100644
index 0000000..8362bb6
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
new file mode 100644
index 0000000..a29299d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
@@ -0,0 +1,10 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "nine_slice",
+ "width": 176,
+ "height": 222,
+ "border": 10
+ }
+ }
+}
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta.license b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta.license
new file mode 100644
index 0000000..c5a7a6d
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+SPDX-FileCopyrightText: This texture is a derivative of textures from Minecraft by Mojang.
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index 5009a61..40e8dda 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -13,3 +13,7 @@ accessible field net/minecraft/entity/passive/AbstractHorseEntity SADDLED_FLAG I
accessible field net/minecraft/entity/passive/AbstractHorseEntity HORSE_FLAGS Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata;
accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Lnet/minecraft/util/Identifier;IIIIIFFFFFFFF)V
+
+mutable field net/minecraft/screen/slot/Slot x I
+mutable field net/minecraft/screen/slot/Slot y I
+