diff --git a/src/client/java/dev/spiritstudios/snapper/Snapper.java b/src/client/java/dev/spiritstudios/snapper/Snapper.java index 6f1e3a3..aaba5cb 100644 --- a/src/client/java/dev/spiritstudios/snapper/Snapper.java +++ b/src/client/java/dev/spiritstudios/snapper/Snapper.java @@ -61,7 +61,12 @@ public void onInitializeClient() { client.player.sendMessage(Text.translatable("text.snapper.panorama_success", SCREENSHOT_MENU_KEY.getBoundKeyLocalizedText()), true); } while (RECENT_SCREENSHOT_KEY.wasPressed()) { - File latestScreenshot = ScreenshotActions.getScreenshots(client).getFirst(); + List screenshots = ScreenshotActions.getScreenshots(client); + if (screenshots.size() == 0) { + if (client.player != null) client.player.sendMessage(Text.translatable("text.snapper.screenshot_failure_open"), true); + continue; + } + File latestScreenshot = screenshots.getFirst(); client.setScreen(new ScreenshotViewerScreen( ScreenshotImage.of(latestScreenshot, client.getTextureManager()), diff --git a/src/client/java/dev/spiritstudios/snapper/gui/PanoramaViewerScreen.java b/src/client/java/dev/spiritstudios/snapper/gui/PanoramaViewerScreen.java index 48276f0..b6d3241 100644 --- a/src/client/java/dev/spiritstudios/snapper/gui/PanoramaViewerScreen.java +++ b/src/client/java/dev/spiritstudios/snapper/gui/PanoramaViewerScreen.java @@ -43,18 +43,17 @@ protected PanoramaViewerScreen(String title, Screen parent) { super(Text.translatable("menu.snapper.viewermenu")); this.title = title; this.parent = parent; - this.load(); } private void load() { List panorama = this.loadPanorama(); - if (panorama == null || client == null) return; + if (panorama == null) return; panorama.parallelStream().map(face -> { ScreenshotImage icon = ScreenshotImage.forPanoramaFace(this.client.getTextureManager(), face.getName()); this.loadIcon(icon, face.getName(), Path.of(face.getPath())); return icon; - }).forEach(ScreenshotImage::joinLoad); + }).toList().forEach(ScreenshotImage::joinLoad); // MUST be joined & called on render thread! this.loaded = true; } @@ -74,8 +73,6 @@ private void loadIcon(ScreenshotImage icon, String fileName, Path filePath) { @Nullable private List loadPanorama() { - if (client == null) return null; - File panoramaDir = new File(this.client.runDirectory, "screenshots/panorama"); List panoramaFaces; if (!Files.exists(panoramaDir.toPath())) return null; @@ -103,15 +100,13 @@ private List loadPanorama() { @Override public void close() { - if (client == null) return; client.setScreen(this.parent); } @SuppressWarnings("ResultOfMethodCallIgnored") @Override protected void init() { - if (client == null) return; - + if (client == null) throw new RuntimeException("Attempted loading panorama screen without client set."); File panoramaDirectory = new File(client.runDirectory, "screenshots/panorama"); addDrawableChild(ButtonWidget.builder(Text.translatable("button.snapper.folder"), button -> { if (!panoramaDirectory.exists()) new File(String.valueOf(panoramaDirectory)).mkdirs(); @@ -119,6 +114,7 @@ protected void init() { }).dimensions(width / 2 - 150 - 4, height - 32, 150, 20).build()); addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).dimensions(width / 2 + 4, height - 32, 150, 20).build()); + this.load(); } @Override diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java new file mode 100644 index 0000000..94d0995 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/CameraMixin.java @@ -0,0 +1,20 @@ +package dev.spiritstudios.snapper.mixin; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Camera; +import net.minecraft.entity.Entity; +import net.minecraft.world.BlockView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Camera.class) +public class CameraMixin { + @Inject(method = "update", at = @At("HEAD"), cancellable = true) + private void blockUpdateDuringPanoRender(BlockView area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo ci) { + if (MinecraftClient.getInstance().gameRenderer.isRenderingPanorama() && thirdPerson) { + ci.cancel(); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java index 7ba9e55..6f2dbea 100644 --- a/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java +++ b/src/client/java/dev/spiritstudios/snapper/mixin/MinecraftClientMixin.java @@ -2,9 +2,15 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef; +import dev.spiritstudios.snapper.mixin.accessor.CameraAccessor; import net.minecraft.client.MinecraftClient; import net.minecraft.client.RunArgs; import net.minecraft.client.gl.Framebuffer; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.render.GameRenderer; import net.minecraft.text.Text; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; @@ -25,6 +31,14 @@ public class MinecraftClientMixin { @Shadow public static boolean IS_SYSTEM_MAC; + @Final + @Shadow + public GameOptions options; + + @Final + @Shadow + public GameRenderer gameRenderer; + @WrapOperation( method = "takePanorama", at = @At( @@ -46,4 +60,28 @@ private void init(RunArgs args, CallbackInfo ci) { } } + @WrapOperation( + method = "takePanorama", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;setYaw(F)V") + ) + private void captureSetYaw(ClientPlayerEntity player, float value, Operation op, @Share("yaw")LocalFloatRef yaw) { + if (!this.options.getPerspective().isFirstPerson()) { + yaw.set(value); + } else { + op.call(player, value); + } + } + + @WrapOperation( + method = "takePanorama", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;setPitch(F)V") + ) + private void applyThirdPersonCameraRotation(ClientPlayerEntity player, float value, Operation op, @Share("yaw")LocalFloatRef yaw) { + if (!this.options.getPerspective().isFirstPerson()) { + ((CameraAccessor)this.gameRenderer.getCamera()).invokeSetRotation(yaw.get(), value); + } else { + op.call(player, value); + } + } + } diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java new file mode 100644 index 0000000..437e45d --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/ScreenshotRecorderMixin.java @@ -0,0 +1,44 @@ +package dev.spiritstudios.snapper.mixin; + +import dev.spiritstudios.snapper.mixinsupport.ImageTransferable; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.util.ScreenshotRecorder; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +@Mixin(ScreenshotRecorder.class) +public class ScreenshotRecorderMixin { + /** + * @author hama + * @reason check if pano file exists before writing to it + */ + @Inject( + method = "method_1661", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/NativeImage;writeTo(Ljava/io/File;)V") + ) + private static void lookBeforeYouLeap(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { + screenshotFile.getParentFile().mkdirs(); + screenshotFile.createNewFile(); + } + + @Inject( + method = "method_1661", + at = @At(value = "INVOKE", target = "Lnet/minecraft/text/Text;literal(Ljava/lang/String;)Lnet/minecraft/text/MutableText;", shift = At.Shift.AFTER) + ) + private static void saveWrittenFileToClipboard(NativeImage nativeImage, File screenshotFile, Consumer messageReceiver, CallbackInfo ci) throws IOException { + if (!screenshotFile.getAbsolutePath().contains("/panorama/")) { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new ImageTransferable(ImageIO.read(screenshotFile)), null); + } + } +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java new file mode 100644 index 0000000..222e6e3 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixin/accessor/CameraAccessor.java @@ -0,0 +1,11 @@ +package dev.spiritstudios.snapper.mixin.accessor; + +import net.minecraft.client.render.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Camera.class) +public interface CameraAccessor { + @Invoker + void invokeSetRotation(float yaw, float pitch); +} diff --git a/src/client/java/dev/spiritstudios/snapper/mixinsupport/ImageTransferable.java b/src/client/java/dev/spiritstudios/snapper/mixinsupport/ImageTransferable.java new file mode 100644 index 0000000..dfb9d02 --- /dev/null +++ b/src/client/java/dev/spiritstudios/snapper/mixinsupport/ImageTransferable.java @@ -0,0 +1,36 @@ +package dev.spiritstudios.snapper.mixinsupport; + +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +public class ImageTransferable implements Transferable { + private static final DataFlavor[] flavors = {DataFlavor.imageFlavor}; + + private final Image image; + + public ImageTransferable(Image im) { + this.image = im; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return flavors; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor.equals(DataFlavor.imageFlavor); + } + + @NotNull + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor); + return image; + } +} \ No newline at end of file diff --git a/src/client/resources/assets/snapper/lang/en_us.json b/src/client/resources/assets/snapper/lang/en_us.json index 29a0110..c6e8d57 100644 --- a/src/client/resources/assets/snapper/lang/en_us.json +++ b/src/client/resources/assets/snapper/lang/en_us.json @@ -27,6 +27,7 @@ "text.snapper.rename_invalid": "New name for screenshot invalid", "text.snapper.rename_invalid_png": "Name must end with '.png'", "text.snapper.screenshot_instructions": "%s. View by pressing %s", + "text.snapper.screenshot_failure_open": "Take a screenshot in order to view it", "panorama.snapper.failure": "Couldn't save panorama: %s", "panorama.snapper.success": "Saved screenshot as %s" } \ No newline at end of file diff --git a/src/client/resources/snapper.mixins.json b/src/client/resources/snapper.mixins.json index 5f8cc41..cac04ef 100644 --- a/src/client/resources/snapper.mixins.json +++ b/src/client/resources/snapper.mixins.json @@ -3,10 +3,13 @@ "package": "dev.spiritstudios.snapper.mixin", "compatibilityLevel": "JAVA_21", "client": [ + "accessor.CameraAccessor", + "CameraMixin", "GameMenuMixin", "KeyboardMixin", "MinecraftClientMixin", - "TitleScreenMixin" + "TitleScreenMixin", + "ScreenshotRecorderMixin" ], "injectors": { "defaultRequire": 1