diff options
author | Linnea Gräf <nea@nea.moe> | 2024-07-10 01:34:37 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-07-10 03:14:55 +0200 |
commit | 986ce538f123cdec7e0da12ed89ba7225539df0a (patch) | |
tree | dd4a3448878c2af26be3bca81bbc8aa710bdc112 /src/main/java | |
parent | 4e1cda1d64ce60719f6ff6e60c6af4881d81fdd2 (diff) | |
download | Firmament-986ce538f123cdec7e0da12ed89ba7225539df0a.tar.gz Firmament-986ce538f123cdec7e0da12ed89ba7225539df0a.tar.bz2 Firmament-986ce538f123cdec7e0da12ed89ba7225539df0a.zip |
Add interactive storage overlay
Diffstat (limited to 'src/main/java')
7 files changed, 426 insertions, 66 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); + } +} |