diff --git a/cambios.md b/cambios.md
index 712c18b..04dc1b4 100644
--- a/cambios.md
+++ b/cambios.md
@@ -1,4 +1,4 @@
-# v5.4.1
+# v5.5
## novedades
@@ -6,9 +6,13 @@
#### módulos nuevos:
+#### comandos nuevos:
+
+- `$panorama`: construye un pack de texturas con una panorámica basada en tu posición actual.
+
### arreglos / mejoras
-- actualizado todos los links a GitHub para reflejar la migración a Codeberg.
+- arreglado ScreenshotMixin, es decir, que ya no crasheará al tomar capturas
## problemas reconocidos
diff --git a/credits.txt b/credits.txt
index d9ebebe..43ffc49 100644
--- a/credits.txt
+++ b/credits.txt
@@ -10,6 +10,8 @@ elytra-recast: https://github.com/InLieuOfLuna/elytra-recast/
SpearHax: https://github.com/etianl/spearHax/
ProjectileTrajectoryPreview: https://github.com/maDU59/ProjectilesTrajectoryPreview
DupersUnited public addon: https://discord.gg/dupes | https://dupers.wtf
+Stardust: https://github.com/0xTas/stardust/
probablemente más pero como tengo demencia se me olvida
-si quieres que te acredite o quite tu código de mi mod, mándame un mensaje en discord (username: retucio) y sin problemas :)
\ No newline at end of file
+si quieres que te acredite o quite tu código de mi mod, mándame un mensaje en discord (username: retucio) y sin problemas :)
+también es probable que haya créditos sueltos en forma de comentarios donde haya usado código ajeno
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index bab18dd..3514ceb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,6 +10,6 @@ discord_rpc_version=1.0.3
neutrino_version=1.1.0
jdk_version=25
-mod_version=5.4
+mod_version=5.5
maven_group=me.retucio
archives_base_name=sputnik
\ No newline at end of file
diff --git a/src/main/java/me/retucio/sputnik/command/CommandManager.java b/src/main/java/me/retucio/sputnik/command/CommandManager.java
index cb8c10c..38c6e36 100644
--- a/src/main/java/me/retucio/sputnik/command/CommandManager.java
+++ b/src/main/java/me/retucio/sputnik/command/CommandManager.java
@@ -38,6 +38,7 @@ public class CommandManager {
addCommand(new EnderChestCommand());
addCommand(new FriendCommand());
addCommand(new GarbageCleanerCommand());
+ addCommand(new PanoramaCommand());
addCommand(new PeekCommand());
addCommand(new PluginsCommand());
addCommand(new PrefixCommand());
diff --git a/src/main/java/me/retucio/sputnik/command/commands/PanoramaCommand.java b/src/main/java/me/retucio/sputnik/command/commands/PanoramaCommand.java
new file mode 100644
index 0000000..72cad26
--- /dev/null
+++ b/src/main/java/me/retucio/sputnik/command/commands/PanoramaCommand.java
@@ -0,0 +1,265 @@
+package me.retucio.sputnik.command.commands;
+
+
+import com.github.retucio.neutrino.EventListener;
+import me.retucio.sputnik.Sputnik;
+import me.retucio.sputnik.command.Command;
+import me.retucio.sputnik.event.TickEvent;
+import me.retucio.sputnik.util.ChatUtil;
+import me.retucio.sputnik.util.MiscUtil;
+
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.SharedConstants;
+import net.minecraft.WorldVersion;
+import net.minecraft.client.Screenshot;
+import net.minecraft.commands.SharedSuggestionProvider;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.packs.PackType;
+import net.minecraft.server.packs.metadata.pack.PackFormat;
+import net.minecraft.sounds.SoundEvents;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.arguments.StringArgumentType;
+
+import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.NonNull;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.concurrent.ThreadLocalRandom;
+
+
+/**
+ * literalmente robado de aquí
+ */
+
+public class PanoramaCommand extends Command {
+
+ private int i = -1;
+ int timer = 10;
+
+ boolean ready = false;
+ boolean warming = false;
+
+ private float prevYaw;
+ private float prevPitch;
+
+ private int prevWidth;
+ private int prevHeight;
+
+ private boolean prevHudHidden;
+
+ @Nullable Path dir = null;
+
+ public PanoramaCommand() {
+ super("panorama", Component.translatable("sputnik.commands.panorama.desc").getString());
+ }
+
+ private void takeScreenshot() {
+ if (dir == null) return;
+
+ Screenshot.grab(
+ dir.toFile(),
+ "panorama_" + i + ".png",
+ mc.gameRenderer.mainRenderTarget(),
+ 1, msg -> Sputnik.LOGGER.info(msg.getString())
+ );
+
+ i++;
+ warming = false;
+ }
+
+ private void begin(String name) {
+ if (mc.player == null) return;
+
+ dir = FabricLoader.getInstance().getGameDir().resolve(Sputnik.MOD_ID + "/panoramas/" + name);
+ dir.toFile().mkdirs();
+
+ i = 0;
+ warming = false;
+
+ prevYaw = mc.player.getYRot();
+ prevPitch = mc.player.getXRot();
+ prevWidth = mc.getWindow().getWidth();
+ prevHeight = mc.getWindow().getHeight();
+ prevHudHidden = mc.gui.hud.isHidden();
+
+ mc.getWindow().setWidth(4096);
+ mc.getWindow().setHeight(4096);
+ mc.gameRenderer.mainRenderTarget().resize(4096, 4096);
+ }
+
+ private void assembleResourcePack() {
+ if (dir == null) return;
+
+ Path resourcePacks = FabricLoader.getInstance().getGameDir().resolve("resourcepacks");
+ Path screenshotsFolder = FabricLoader.getInstance().getGameDir().resolve(Sputnik.MOD_ID + "/panoramas/" + dir.getFileName() + "/screenshots");
+ Path customBaseFolder = resourcePacks.resolve(dir.getFileName());
+ Path customPackFolder = customBaseFolder.resolve("assets/minecraft/textures/gui/title/background/");
+ customPackFolder.toFile().mkdirs();
+
+ String mcMeta =
+ """
+ {
+ "pack":
+ {
+ "min_format": [65, 0],
+ "max_format": [%d, 0],
+ "description": "%s"
+ }
+ }
+ """.formatted(
+ getPackVersion(),
+ Component.translatable("sputnik.commands.panorama.packdesc").getString()
+ );
+
+ try {
+ int luckyNumber = ThreadLocalRandom.current().nextInt(6);
+ Files.walkFileTree(screenshotsFolder, new SimpleFileVisitor<>() {
+ public @NonNull FileVisitResult visitFile(@NonNull Path file, @NonNull BasicFileAttributes attrs) {
+ MiscUtil.copy(file.toFile(), customPackFolder.resolve(file.getFileName()).toFile());
+
+ // pack.png random de alguna de los screenshots
+ if (file.getFileName().toString().contains(String.valueOf(luckyNumber))) {
+ MiscUtil.copy(file.toFile(), customBaseFolder.resolve("pack.png").toFile());
+ }
+
+ return FileVisitResult.CONTINUE;
+ }
+ });
+
+ File packMeta = customBaseFolder.resolve("pack.mcmeta").toFile();
+ if (!packMeta.createNewFile()) {
+ ChatUtil.error(Component.translatable("sputnik.commands.panorama.errorassemble").getString());
+ return;
+ }
+ Files.write(packMeta.toPath(), mcMeta.getBytes());
+
+ // eliminar el efecto vignette feo de cojones que usa Minecraft por defecto
+ BufferedImage transparentOverlay = new BufferedImage(16, 128, BufferedImage.TYPE_INT_ARGB);
+ File overlayImage = customPackFolder.resolve("panorama_overlay.png").toFile();
+ ImageIO.write(transparentOverlay, "PNG", overlayImage);
+ } catch (Exception e) {
+ ChatUtil.error("error de i/o");
+ Sputnik.LOGGER.error(e);
+ }
+
+ ready = false;
+ if (mc.player != null) {
+ mc.player.playSound(SoundEvents.AMETHYST_BLOCK_CHIME, 1f, 1f);
+ ChatUtil.info(Component.translatable("sputnik.commands.panorama.ready").getString());
+ }
+ }
+
+ @EventListener
+ private void onTick(TickEvent.Post event) {
+ if (mc.player == null || dir == null) return;
+
+ if (timer > 0) {
+ --timer;
+ return;
+ } else {
+ timer = 10;
+ }
+
+ if (ready) {
+ assembleResourcePack();
+ }
+ if (i < 0) return;
+
+ switch (i) {
+ case 0 -> {
+ if (!warming) {
+ mc.gameRenderer.mainCamera().enablePanoramicMode();
+ mc.gameRenderer.setRenderBlockOutline(false);
+ mc.execute(() -> {
+ synchronized (mc.levelRenderer) {
+ mc.levelRenderer.notifyAll();
+ }
+ });
+ if (!mc.gui.hud.isHidden()) mc.gui.hud.toggle();
+ mc.player.setYRot(prevYaw);
+ mc.player.setXRot(0f);
+ warming = true;
+ } else takeScreenshot();
+ }
+ case 1 -> {
+ if (!warming) {
+ mc.player.setYRot((prevYaw + 90f) % 360f);
+ mc.player.setXRot(0f);
+ warming = true;
+ } else takeScreenshot();
+ }
+ case 2 -> {
+ if (!warming) {
+ mc.player.setYRot((prevYaw + 180f) % 360f);
+ mc.player.setXRot(0f);
+ warming = true;
+ } else takeScreenshot();
+ }
+ case 3 -> {
+ if (!warming) {
+ mc.player.setYRot((prevYaw - 90f) % 360f);
+ mc.player.setXRot(0f);
+ warming = true;
+ } else takeScreenshot();
+ }
+ case 4 -> {
+ if (!warming) {
+ mc.player.setYRot(prevYaw);
+ mc.player.setXRot(-90f);
+ warming = true;
+ } else takeScreenshot();
+ }
+ default -> {
+ if (!warming) {
+ mc.player.setYRot(prevYaw);
+ mc.player.setXRot(90f);
+ warming = true;
+ } else {
+ takeScreenshot();
+ i = -1;
+ mc.player.setYRot(prevYaw);
+ mc.player.setXRot(prevPitch);
+ mc.gameRenderer.mainCamera().disablePanoramicMode();
+ mc.gameRenderer.setRenderBlockOutline(true);
+ mc.getWindow().setWidth(prevWidth);
+ mc.getWindow().setHeight(prevHeight);
+ if (mc.gui.hud.isHidden() != prevHudHidden) mc.gui.hud.toggle();
+ mc.gameRenderer.mainRenderTarget().resize(prevWidth, prevHeight);
+ mc.execute(() -> {
+ synchronized (mc.levelRenderer) {
+ mc.levelRenderer.notifyAll();
+ }
+ });
+
+ timer = 100; // esperar a que las capturas se escriban
+ ready = true;
+ ChatUtil.info(Component.translatable("sputnik.commands.panorama.finalizing").getString());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void build(LiteralArgumentBuilder builder) {
+ builder.then(
+ argument("name", StringArgumentType.word()).executes(ctx -> {
+ String name = ctx.getArgument("name", String.class);
+ begin(name);
+ return SUCCESS;
+ })
+ );
+ }
+
+ public static int getPackVersion() {
+ WorldVersion version = SharedConstants.getCurrentVersion();
+ PackFormat format = version.packVersion(PackType.CLIENT_RESOURCES);
+ return format.major();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/retucio/sputnik/mixin/mixins/io/ScreenshotMixin.java b/src/main/java/me/retucio/sputnik/mixin/mixins/io/ScreenshotMixin.java
index 8ed8d5a..a520914 100644
--- a/src/main/java/me/retucio/sputnik/mixin/mixins/io/ScreenshotMixin.java
+++ b/src/main/java/me/retucio/sputnik/mixin/mixins/io/ScreenshotMixin.java
@@ -5,6 +5,7 @@ import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.misc.ScreenshotPlus;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
+import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -19,14 +20,14 @@ import java.util.function.Consumer;
@Mixin(Screenshot.class)
public abstract class ScreenshotMixin {
- @Inject(method = "lambda$grab$1", at = @At(value = "INVOKE", target = "Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V"), cancellable = true)
- private static void sendActionsMessage(NativeImage image, File file, Consumer> callback, CallbackInfo ci) {
+ @Inject(method = "lambda$grab$3", at = @At(value = "INVOKE", target = "Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V"), cancellable = true)
+ private static void sendActionsMessage(NativeImage image, File file, Consumer callback, CallbackInfo ci) {
ScreenshotPlus screenshotPlus = getScreenshotPlus();
if (screenshotPlus.isEnabled()) ci.cancel();
}
- @Inject(method = "lambda$grab$1", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;writeToFile(Ljava/io/File;)V"), cancellable = true)
- private static void interceptScreenshotFile(NativeImage image, File file, Consumer> callback, CallbackInfo ci) throws IOException {
+ @Inject(method = "lambda$grab$3", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/NativeImage;writeToFile(Ljava/io/File;)V"), cancellable = true)
+ private static void interceptScreenshotFile(NativeImage image, File file, Consumer callback, CallbackInfo ci) throws IOException {
ScreenshotPlus screenshotPlus = getScreenshotPlus();
// aunque el módulo esté apagado, pasar las referencias para poder usar los comandos para las capturas
diff --git a/src/main/java/me/retucio/sputnik/util/MiscUtil.java b/src/main/java/me/retucio/sputnik/util/MiscUtil.java
index 25dcb1f..4096e48 100644
--- a/src/main/java/me/retucio/sputnik/util/MiscUtil.java
+++ b/src/main/java/me/retucio/sputnik/util/MiscUtil.java
@@ -1,6 +1,7 @@
package me.retucio.sputnik.util;
import com.github.retucio.neutrino.EventListener;
+import me.retucio.sputnik.Sputnik;
import me.retucio.sputnik.event.TickEvent;
import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud;
@@ -8,10 +9,12 @@ import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.core.Vec3i;
import net.minecraft.world.phys.Vec3;
+import org.apache.commons.io.IOUtils;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.lwjgl.glfw.GLFW;
+import java.io.*;
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneOffset;
@@ -110,6 +113,26 @@ public class MiscUtil {
}
+ public static void copy(File from, File to) {
+ try (InputStream in = new FileInputStream(from);
+ OutputStream out = new FileOutputStream(to)) {
+ in.transferTo(out);
+ } catch (IOException e) {
+ Sputnik.LOGGER.error("error copying from file \"{}\" to file \"{}\".", from.getName(), to.getName(), e);
+ }
+ }
+
+ public static void copy(InputStream in, File to) {
+ try (OutputStream out = new FileOutputStream(to)) {
+ in.transferTo(out);
+ } catch (IOException e) {
+ Sputnik.LOGGER.error("error writing to file \"{}\".", to.getName(), e);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+
public static Vec3 vec3dOf(Vector3f v) {
return new Vec3(v.x, v.y, v.z);
}
diff --git a/src/main/resources/assets/sputnik/lang/en_us.json b/src/main/resources/assets/sputnik/lang/en_us.json
index faa4f0f..f74c38f 100644
--- a/src/main/resources/assets/sputnik/lang/en_us.json
+++ b/src/main/resources/assets/sputnik/lang/en_us.json
@@ -29,8 +29,13 @@
"sputnik.commands.friend.removed": "%s is not your friend anymore :(",
"sputnik.commands.gc.collected": "garbage collected",
"sputnik.commands.gc.desc": "frees up ram using the garbage collector (gc)",
+ "sputnik.commands.panorama.desc": "creates a panorama resource pack based on your current position",
+ "sputnik.commands.panorama.errorassemble": "failed to assemble custom resource pack! does a pack of that name already exist?",
+ "sputnik.commands.panorama.packdesc": "generated by Sputnik",
+ "sputnik.commands.panorama.ready": "resource pack ready!",
+ "sputnik.commands.panorama.finalizing": "finalizing resource pack...",
"sputnik.commands.peek.desc": "peeks at another player's inventory",
- "sputnik.commands.plugins.calmdown": "calm dooown",
+ "sputnik.commands.plugins.calmdown": "calm dooowwnnn",
"sputnik.commands.plugins.desc": "looks for the plugins the server has",
"sputnik.commands.plugins.erroranalyzingcommands": "error analyzing commands: %s",
"sputnik.commands.plugins.namespacesfound": "%s command namespace(s) found:",
diff --git a/src/main/resources/assets/sputnik/lang/es_es.json b/src/main/resources/assets/sputnik/lang/es_es.json
index 3799f9b..3871f41 100644
--- a/src/main/resources/assets/sputnik/lang/es_es.json
+++ b/src/main/resources/assets/sputnik/lang/es_es.json
@@ -29,6 +29,11 @@
"sputnik.commands.friend.removed": "%s ya no es tu amigo :(",
"sputnik.commands.gc.collected": "basura recolectada",
"sputnik.commands.gc.desc": "limpia la ram mediante el recolector de basura (gc)",
+ "sputnik.commands.panorama.desc": "crea un pack de textures con la panorámica basada en tu posición actual",
+ "sputnik.commands.panorama.errorassemble": "no se pudo crear el pack; existe uno ya con ese nombre?",
+ "sputnik.commands.panorama.packdesc": "generado por Sputnik",
+ "sputnik.commands.panorama.ready": "pack listo!",
+ "sputnik.commands.panorama.finalizing": "finalizando pack...",
"sputnik.commands.peek.desc": "cotillea el inventario de otro jugador",
"sputnik.commands.plugins.calmdown": "calmaaa",
"sputnik.commands.plugins.desc": "busca los plugins que tenga el server",