Compare commits

...
3 Commits
Author SHA1 Message Date
retucio 272c1059ef v5.7 2026-08-19 22:57:41 +02:00
retucio e0f8523809 v5.6 2026-08-18 13:35:24 +02:00
retucio 81fc25c33d v5.5 2026-07-20 22:17:44 +02:00
87 changed files with 3058 additions and 445 deletions

No files matched your search

+19 -2
View File
@@ -1,14 +1,31 @@
# v5.4.1 # v5.7
## novedades ## novedades
actualización a gradle 9.7.0
interfaz reorganizada en pestañas: módulos, hud, perfiles y macros
perfiles de configuración: guarda varias configs con nombre y cambia entre ellas sin reiniciar
macros: asigna una tecla a una lista de comandos o mensajes
### adiciones ### adiciones
#### módulos nuevos: #### módulos nuevos:
- cancelador de paquetes: evita que se envíen o reciban ciertos paquetes
#### comandos nuevos:
- `$?`: muestra los comandos disponibles y los describe
- `$perfil`: gestiona los perfiles de configuración
- `$macro`: gestiona las macros
### arreglos / mejoras ### arreglos / mejoras
- actualizado todos los links a GitHub para reflejar la migración a Codeberg. - las rotaciones de los módulos se reparten por prioridad en vez de pisarse entre ellas
- el editor del hud ya es una pestaña más de la interfaz, además de su propia pantalla
## problemas reconocidos ## problemas reconocidos
+2
View File
@@ -10,6 +10,8 @@ elytra-recast: https://github.com/InLieuOfLuna/elytra-recast/
SpearHax: https://github.com/etianl/spearHax/ SpearHax: https://github.com/etianl/spearHax/
ProjectileTrajectoryPreview: https://github.com/maDU59/ProjectilesTrajectoryPreview ProjectileTrajectoryPreview: https://github.com/maDU59/ProjectilesTrajectoryPreview
DupersUnited public addon: https://discord.gg/dupes | https://dupers.wtf 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 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 :) 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
+1 -1
View File
@@ -10,6 +10,6 @@ discord_rpc_version=1.0.3
neutrino_version=1.1.0 neutrino_version=1.1.0
jdk_version=25 jdk_version=25
mod_version=5.4 mod_version=5.7
maven_group=me.retucio maven_group=me.retucio
archives_base_name=sputnik archives_base_name=sputnik
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
View File
Whitespace-only changes.
+15 -9
View File
@@ -7,6 +7,8 @@ import me.retucio.sputnik.command.CommandManager;
import me.retucio.sputnik.command.commands.BindCommand; import me.retucio.sputnik.command.commands.BindCommand;
import me.retucio.sputnik.config.ConfigManager; import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.config.ProfileManager;
import me.retucio.sputnik.macro.MacroManager;
import me.retucio.sputnik.event.ShutdownEvent; import me.retucio.sputnik.event.ShutdownEvent;
import me.retucio.sputnik.event.interact.OpenScreenEvent; import me.retucio.sputnik.event.interact.OpenScreenEvent;
@@ -17,6 +19,7 @@ import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud; import me.retucio.sputnik.module.modules.client.Hud;
import me.retucio.sputnik.ui.hud.HudEditor;
import me.retucio.sputnik.ui.hud.HudRenderer; import me.retucio.sputnik.ui.hud.HudRenderer;
import me.retucio.sputnik.ui.screen.ClickGui; import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.hud.HudEditorScreen; import me.retucio.sputnik.ui.hud.HudEditorScreen;
@@ -99,12 +102,17 @@ public class Sputnik implements ClientModInitializer {
EVENT_BUS.subscribe(MiscUtil.class); EVENT_BUS.subscribe(MiscUtil.class);
EVENT_BUS.subscribe(NetworkUtil.class); EVENT_BUS.subscribe(NetworkUtil.class);
EVENT_BUS.subscribe(RenderUtil.class); EVENT_BUS.subscribe(RenderUtil.class);
EVENT_BUS.subscribe(RotationUtil.class);
EVENT_BUS.subscribe(BlockIterator.class); EVENT_BUS.subscribe(BlockIterator.class);
Lists.init(); Lists.init();
HudRenderer.INSTANCE = new HudRenderer(); HudRenderer.INSTANCE = new HudRenderer();
HudEditor.INSTANCE = new HudEditor();
MacroManager.INSTANCE = new MacroManager();
ProfileManager.INSTANCE = new ProfileManager();
CapeManager.INSTANCE = new CapeManager(); CapeManager.INSTANCE = new CapeManager();
EVENT_BUS.post(new LoadCapeManagerEvent()); EVENT_BUS.post(new LoadCapeManagerEvent());
@@ -135,11 +143,8 @@ public class Sputnik implements ClientModInitializer {
// se ejecuta cada tick, es decir, 20 veces por segundo // se ejecuta cada tick, es decir, 20 veces por segundo
public void onTick() { public void onTick() {
if (!settingsApplied if (!settingsApplied && !ConfigManager.INSTANCE.hasLoaded()) {
&& ConfigManager.INSTANCE.getConfig() != null settingsApplied = ConfigManager.INSTANCE.applyConfig();
&& !ConfigManager.INSTANCE.hasLoaded()) {
ConfigManager.INSTANCE.applyConfig();
settingsApplied = true;
} }
ModuleManager.INSTANCE.getEnabledModules().forEach(Module::onTick); ModuleManager.INSTANCE.getEnabledModules().forEach(Module::onTick);
@@ -154,14 +159,15 @@ public class Sputnik implements ClientModInitializer {
// prevenir interrumpir combos de F3 // prevenir interrumpir combos de F3
if (key == GLFW.GLFW_KEY_F3) return; if (key == GLFW.GLFW_KEY_F3) return;
boolean anyFocused = isAnySettingButtonFocused() || ClickGui.INSTANCE.getSearchBar().isFocused(); boolean anyFocused = isAnySettingButtonFocused() || ClickGui.INSTANCE.isCapturingInput();
ClickGui.INSTANCE.setAnyFocused(anyFocused); ClickGui.INSTANCE.setAnyFocused(anyFocused);
if (action != GLFW.GLFW_RELEASE) { if (action != GLFW.GLFW_RELEASE) {
if (BindCommand.onKeyPress(key)) return; if (BindCommand.onKeyPress(key)) return;
if (action == GLFW.GLFW_PRESS && MacroManager.INSTANCE.onKeyPress(key)) return;
if (action == GLFW.GLFW_PRESS) { if (action == GLFW.GLFW_PRESS) {
handleModuleToggle(key, anyFocused); handleModuleToggle(key, anyFocused);
handleClickGUIKey(key, anyFocused); handleClickGuiKey(key, anyFocused);
handleHudEditorKey(key, anyFocused); handleHudEditorKey(key, anyFocused);
} }
// solo reenviar teclas del teclado // solo reenviar teclas del teclado
@@ -208,7 +214,7 @@ public class Sputnik implements ClientModInitializer {
} }
// maneja la lógica de apertura de la interfaz // maneja la lógica de apertura de la interfaz
private void handleClickGUIKey(int key, boolean anyFocused) { private void handleClickGuiKey(int key, boolean anyFocused) {
if (key != ClientSettingsPanel.clientSettings.getKey() || anyFocused || isOnTypingScreen()) if (key != ClientSettingsPanel.clientSettings.getKey() || anyFocused || isOnTypingScreen())
return; return;
@@ -221,7 +227,7 @@ public class Sputnik implements ClientModInitializer {
} }
} }
// manejar la lógica de apertura del editor de elementos del hud, con la misma lógica que handleClickGUIKey() // manejar la lógica de apertura del editor de elementos del hud, con la misma lógica que handleClickGuiKey()
private void handleHudEditorKey(int key, boolean anyFocused) { private void handleHudEditorKey(int key, boolean anyFocused) {
Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class); Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class);
if (key != hud.editorKey.getValue() || anyFocused || isOnTypingScreen()) return; if (key != hud.editorKey.getValue() || anyFocused || isOnTypingScreen()) return;
@@ -9,6 +9,7 @@ import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.data.registries.VanillaRegistries; import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.network.chat.Component;
import java.util.List; import java.util.List;
@@ -20,12 +21,14 @@ public abstract class Command {
protected static final Minecraft mc = Minecraft.getInstance(); protected static final Minecraft mc = Minecraft.getInstance();
private final String name, description; private final String name, descriptionKey;
private final List<String> aliases; private final List<String> aliases;
public Command(String name, String description, String... aliases) { // la descripción se guarda como clave de traducción, no como texto ya resuelto: los comandos
// se construyen antes de que se cargue el idioma, así que hay que traducirla al mostrarla
public Command(String name, String descriptionKey, String... aliases) {
this.name = name; this.name = name;
this.description = description; this.descriptionKey = descriptionKey;
this.aliases = List.of(aliases); this.aliases = List.of(aliases);
} }
@@ -56,8 +59,12 @@ public abstract class Command {
return name; return name;
} }
public String getDescription() { public String getDescriptionKey() {
return description; return descriptionKey;
}
public Component getDescription() {
return Component.translatable(descriptionKey);
} }
public List<String> getAliases() { public List<String> getAliases() {
@@ -38,9 +38,13 @@ public class CommandManager {
addCommand(new EnderChestCommand()); addCommand(new EnderChestCommand());
addCommand(new FriendCommand()); addCommand(new FriendCommand());
addCommand(new GarbageCleanerCommand()); addCommand(new GarbageCleanerCommand());
addCommand(new HelpCommand());
addCommand(new MacroCommand());
addCommand(new PanoramaCommand());
addCommand(new PeekCommand()); addCommand(new PeekCommand());
addCommand(new PluginsCommand()); addCommand(new PluginsCommand());
addCommand(new PrefixCommand()); addCommand(new PrefixCommand());
addCommand(new ProfileCommand());
addCommand(new PurgeCommand()); addCommand(new PurgeCommand());
addCommand(new RepeatCommand()); addCommand(new RepeatCommand());
addCommand(new RepeatWaitCommand()); addCommand(new RepeatWaitCommand());
@@ -24,7 +24,7 @@ public class BindCommand extends Command {
private static Module listeningModule = null; private static Module listeningModule = null;
public BindCommand() { public BindCommand() {
super("bind", Component.translatable("sputnik.commands.bind.desc").getString(), "keybind", "kb", "tecla"); super("bind", "sputnik.commands.bind.desc", "keybind", "kb", "tecla");
} }
@Override @Override
@@ -7,14 +7,13 @@ import me.retucio.sputnik.command.args.EnumArgumentType;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.ContainerScreen; import net.minecraft.client.gui.screens.inventory.ContainerScreen;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.inventory.ContainerInput;
// https://cdn.discordapp.com/attachments/1456079799045980400/1457154232737206375/dupersunited-public-addon-1.21.11-3.1.3.jar?ex=69735be1&is=69720a61&hm=77fb6faee49762e0a812661f0d98931e89cb725db711c45722249d0efe3b91e4& // https://cdn.discordapp.com/attachments/1456079799045980400/1457154232737206375/dupersunited-public-addon-1.21.11-3.1.3.jar?ex=69735be1&is=69720a61&hm=77fb6faee49762e0a812661f0d98931e89cb725db711c45722249d0efe3b91e4&
public class ClickSlotCommand extends Command { public class ClickSlotCommand extends Command {
public ClickSlotCommand() { public ClickSlotCommand() {
super("clickslot", Component.translatable("sputnik.commands.clickslot.desc").getString(), "cs"); super("clickslot", "sputnik.commands.clickslot.desc", "cs");
} }
public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) { public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) {
@@ -9,12 +9,11 @@ import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import net.minecraft.network.chat.Component;
public class ConfigCommand extends Command { public class ConfigCommand extends Command {
public ConfigCommand() { public ConfigCommand() {
super("config", Component.translatable("sputnik.commands.config.desc").getString(), "ajustes", "settings"); super("config", "sputnik.commands.config.desc", "ajustes", "settings");
} }
@Override @Override
@@ -5,13 +5,12 @@ import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.misc.ScreenshotPlus; import me.retucio.sputnik.module.modules.misc.ScreenshotPlus;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
// se usa principalmente para el módulo ScreenshotPlus, pero también se puede usar sin él supongo // se usa principalmente para el módulo ScreenshotPlus, pero también se puede usar sin él supongo
public class CopyScreenshotCommand extends Command { public class CopyScreenshotCommand extends Command {
public CopyScreenshotCommand() { public CopyScreenshotCommand() {
super("copiarcaptura", Component.translatable("sputnik.commands.copyscreenshot.desc").getString(), "copyscreenshot"); super("copiarcaptura", "sputnik.commands.copyscreenshot.desc", "copyscreenshot");
} }
@Override @Override
@@ -16,7 +16,7 @@ public class EnderChestCommand extends Command {
private boolean shouldOpenEchest = false; private boolean shouldOpenEchest = false;
public EnderChestCommand() { public EnderChestCommand() {
super("echest", Component.translatable("sputnik.commands.echest.desc").getString(), "ec", "enderchest"); super("echest", "sputnik.commands.echest.desc", "ec", "enderchest");
} }
@Override @Override
@@ -15,7 +15,7 @@ import net.minecraft.world.entity.player.Player;
public class FriendCommand extends Command { public class FriendCommand extends Command {
public FriendCommand() { public FriendCommand() {
super("friend", Component.translatable("sputnik.commands.friend.desc").getString(), "amigo", "f"); super("friend", "sputnik.commands.friend.desc", "amigo", "f");
} }
@Override @Override
@@ -9,12 +9,12 @@ import net.minecraft.network.chat.Component;
public class GarbageCleanerCommand extends Command { public class GarbageCleanerCommand extends Command {
public GarbageCleanerCommand() { public GarbageCleanerCommand() {
super("gc", Component.translatable("sputnik.commands.gc.desc").getString(), "garbagecollect"); super("gc", "sputnik.commands.gc.desc", "garbagecollect");
} }
@Override @Override
public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) { public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) {
builder.executes(context -> { builder.executes(ctx -> {
System.gc(); System.gc();
ChatUtil.info(Component.translatable("sputnik.commands.gc.collected")); ChatUtil.info(Component.translatable("sputnik.commands.gc.collected"));
return SUCCESS; return SUCCESS;
@@ -0,0 +1,28 @@
package me.retucio.sputnik.command.commands;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.command.CommandManager;
import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
public class HelpCommand extends Command {
public HelpCommand() {
super("help", Component.translatable("sputnik.commands.help.desc").getString(), "?", "ayuda", "h");
}
@Override
public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) {
builder.executes(ctx -> {
for (Command command : CommandManager.commands) {
ChatUtil.info(CommandManager.INSTANCE.getPrefix()
+ ChatFormatting.BOLD + command.getName() + ": "
+ ChatFormatting.RESET + ChatFormatting.GRAY + command.getDescription().getString());
}
return SUCCESS;
});
}
}
@@ -0,0 +1,114 @@
package me.retucio.sputnik.command.commands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.macro.Macro;
import me.retucio.sputnik.macro.MacroManager;
import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.KeyUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.util.List;
public class MacroCommand extends Command {
private static final SuggestionProvider<SharedSuggestionProvider> MACROS = (ctx, builder) ->
SharedSuggestionProvider.suggest(
MacroManager.INSTANCE.getMacros().stream().map(Macro::getName), builder);
public MacroCommand() {
super("macro", "sputnik.commands.macro.desc", "macros");
}
@Override
public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) {
builder
.executes(ctx -> {
list();
return SUCCESS;
})
.then(literal("list").executes(ctx -> {
list();
return SUCCESS;
}))
.then(literal("add")
.then(argument("nombre", StringArgumentType.word()).executes(ctx -> {
String name = ctx.getArgument("nombre", String.class);
if (MacroManager.INSTANCE.add(new Macro(name)))
ChatUtil.info(Component.translatable("sputnik.commands.macro.added",
ChatFormatting.AQUA + name));
else
ChatUtil.error(Component.translatable("sputnik.commands.macro.exists", name));
return SUCCESS;
})))
.then(literal("remove")
.then(argument("nombre", StringArgumentType.word()).suggests(MACROS).executes(ctx -> {
Macro macro = get(ctx.getArgument("nombre", String.class));
if (macro == null) return SUCCESS;
MacroManager.INSTANCE.remove(macro);
ChatUtil.info(Component.translatable("sputnik.commands.macro.removed",
ChatFormatting.AQUA + macro.getName()));
return SUCCESS;
})))
.then(literal("line")
.then(argument("nombre", StringArgumentType.word()).suggests(MACROS)
.then(argument("línea", StringArgumentType.greedyString()).executes(ctx -> {
Macro macro = get(ctx.getArgument("nombre", String.class));
if (macro == null) return SUCCESS;
String line = ctx.getArgument("línea", String.class);
macro.getLines().add(line);
MacroManager.INSTANCE.save();
ChatUtil.info(Component.translatable("sputnik.commands.macro.lineadded",
ChatFormatting.AQUA + macro.getName()));
return SUCCESS;
}))))
.then(literal("clear")
.then(argument("nombre", StringArgumentType.word()).suggests(MACROS).executes(ctx -> {
Macro macro = get(ctx.getArgument("nombre", String.class));
if (macro == null) return SUCCESS;
macro.getLines().clear();
MacroManager.INSTANCE.save();
ChatUtil.info(Component.translatable("sputnik.commands.macro.cleared",
ChatFormatting.AQUA + macro.getName()));
return SUCCESS;
})))
.then(literal("run")
.then(argument("nombre", StringArgumentType.word()).suggests(MACROS).executes(ctx -> {
Macro macro = get(ctx.getArgument("nombre", String.class));
if (macro != null) macro.run();
return SUCCESS;
})));
}
private static Macro get(String name) {
Macro macro = MacroManager.INSTANCE.getByName(name);
if (macro == null) ChatUtil.error(Component.translatable("sputnik.commands.macro.notfound", name));
return macro;
}
private static void list() {
List<Macro> macros = MacroManager.INSTANCE.getMacros();
if (macros.isEmpty()) {
ChatUtil.info(Component.translatable("sputnik.commands.macro.none"));
return;
}
ChatUtil.info(Component.translatable("sputnik.commands.macro.list", macros.size()));
for (Macro macro : macros)
ChatUtil.addMessage(ChatFormatting.GRAY + " - " + macro.getName()
+ ChatFormatting.DARK_GRAY + " [" + KeyUtil.getKeyName(macro.getKey()) + "] "
+ macro.getLines().size());
}
}
@@ -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 <a href="https://github.com/0xTas/stardust/blob/main/src/main/java/dev/stardust/commands/Panorama.java">aquí</a>
*/
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", "sputnik.commands.panorama.desc");
}
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<SharedSuggestionProvider> 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();
}
}
@@ -6,7 +6,6 @@ import me.retucio.sputnik.command.args.PlayerArgumentType;
import me.retucio.sputnik.util.MiscUtil; import me.retucio.sputnik.util.MiscUtil;
import net.minecraft.client.gui.screens.inventory.InventoryScreen; import net.minecraft.client.gui.screens.inventory.InventoryScreen;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.awt.*; import java.awt.*;
@@ -14,7 +13,7 @@ import java.awt.*;
public class PeekCommand extends Command { public class PeekCommand extends Command {
public PeekCommand() { public PeekCommand() {
super("inventario", Component.translatable("sputnik.commands.peek.desc").getString(), "invsee", "peek", "inv"); super("inventario", "sputnik.commands.peek.desc", "invsee", "peek", "inv");
} }
@Override @Override
@@ -24,7 +24,7 @@ public class PluginsCommand extends Command {
private boolean scanning = false; private boolean scanning = false;
public PluginsCommand() { public PluginsCommand() {
super("plugins", Component.translatable("sputnik.commands.plugins.desc").getString(), "pl", "serverplugins"); super("plugins", "sputnik.commands.plugins.desc", "pl", "serverplugins");
Sputnik.EVENT_BUS.subscribe(this); Sputnik.EVENT_BUS.subscribe(this);
} }
@@ -13,7 +13,7 @@ import net.minecraft.network.chat.Component;
public class PrefixCommand extends Command { public class PrefixCommand extends Command {
public PrefixCommand() { public PrefixCommand() {
super("prefijo", Component.translatable("sputnik.commands.prefix.desc").getString(), "prefix"); super("prefijo", "sputnik.commands.prefix.desc", "prefix");
} }
@Override @Override
@@ -0,0 +1,92 @@
package me.retucio.sputnik.command.commands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.config.ProfileManager;
import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.util.List;
public class ProfileCommand extends Command {
private static final SuggestionProvider<SharedSuggestionProvider> PROFILES = (ctx, builder) ->
SharedSuggestionProvider.suggest(ProfileManager.INSTANCE.getProfileNames(), builder);
public ProfileCommand() {
super("perfil", "sputnik.commands.profile.desc", "profile", "perfiles", "profiles");
}
@Override
public void build(LiteralArgumentBuilder<SharedSuggestionProvider> builder) {
builder
.executes(ctx -> {
list();
return SUCCESS;
})
.then(literal("list").executes(ctx -> {
list();
return SUCCESS;
}))
.then(literal("save")
.then(argument("nombre", StringArgumentType.word()).executes(ctx -> {
String name = ctx.getArgument("nombre", String.class);
if (!ProfileManager.isValidName(name)) {
ChatUtil.error(Component.translatable("sputnik.commands.profile.invalidname", name));
return SUCCESS;
}
if (ProfileManager.INSTANCE.save(name)) {
ConfigManager.INSTANCE.save();
ChatUtil.info(Component.translatable("sputnik.commands.profile.saved",
ChatFormatting.AQUA + name));
} else {
ChatUtil.error(Component.translatable("sputnik.commands.profile.savefailed", name));
}
return SUCCESS;
})))
.then(literal("load")
.then(argument("nombre", StringArgumentType.word()).suggests(PROFILES).executes(ctx -> {
String name = ctx.getArgument("nombre", String.class);
if (ProfileManager.INSTANCE.load(name))
ChatUtil.info(Component.translatable("sputnik.commands.profile.loaded",
ChatFormatting.AQUA + name));
else
ChatUtil.error(Component.translatable("sputnik.commands.profile.notfound", name));
return SUCCESS;
})))
.then(literal("delete")
.then(argument("nombre", StringArgumentType.word()).suggests(PROFILES).executes(ctx -> {
String name = ctx.getArgument("nombre", String.class);
if (ProfileManager.INSTANCE.delete(name))
ChatUtil.info(Component.translatable("sputnik.commands.profile.deleted",
ChatFormatting.AQUA + name));
else
ChatUtil.error(Component.translatable("sputnik.commands.profile.notfound", name));
return SUCCESS;
})));
}
private static void list() {
List<String> profiles = ProfileManager.INSTANCE.getProfileNames();
if (profiles.isEmpty()) {
ChatUtil.info(Component.translatable("sputnik.commands.profile.none"));
return;
}
String current = ProfileManager.INSTANCE.getCurrent();
ChatUtil.info(Component.translatable("sputnik.commands.profile.list", profiles.size()));
for (String profile : profiles)
ChatUtil.addMessage((profile.equals(current) ? ChatFormatting.GREEN : ChatFormatting.GRAY) + " - " + profile);
}
}
@@ -7,7 +7,6 @@ import me.retucio.sputnik.util.interfaces.IChatComponent;
import net.minecraft.client.gui.components.ChatComponent; import net.minecraft.client.gui.components.ChatComponent;
import net.minecraft.client.multiplayer.chat.GuiMessage; import net.minecraft.client.multiplayer.chat.GuiMessage;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.awt.*; import java.awt.*;
import java.util.List; import java.util.List;
@@ -15,7 +14,7 @@ import java.util.List;
public class PurgeCommand extends Command { public class PurgeCommand extends Command {
public PurgeCommand() { public PurgeCommand() {
super("purgar", Component.translatable("sputnik.commands.purge.desc").getString(), "purge"); super("purgar", "sputnik.commands.purge.desc", "purge");
} }
@Override @Override
@@ -6,12 +6,11 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import me.retucio.sputnik.command.Command; import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
public class RepeatCommand extends Command { public class RepeatCommand extends Command {
public RepeatCommand() { public RepeatCommand() {
super("repetir", Component.translatable("sputnik.commands.repeat.desc").getString(), "repeat"); super("repetir", "sputnik.commands.repeat.desc", "repeat");
} }
@Override @Override
@@ -6,7 +6,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import me.retucio.sputnik.command.Command; import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -14,7 +13,7 @@ import java.util.concurrent.TimeUnit;
public class RepeatWaitCommand extends Command { public class RepeatWaitCommand extends Command {
public RepeatWaitCommand() { public RepeatWaitCommand() {
super("repesperar", Component.translatable("sputnik.commands.repeatwait.desc").getString(), "repeatwait"); super("repesperar", "sputnik.commands.repeatwait.desc", "repeatwait");
} }
@Override @Override
@@ -14,7 +14,7 @@ import java.io.IOException;
public class SaveScreenshotCommand extends Command { public class SaveScreenshotCommand extends Command {
public SaveScreenshotCommand() { public SaveScreenshotCommand() {
super("guardarcaptura", Component.translatable("sputnik.commands.savescreenshot.desc").getString(), "savescreenshot"); super("guardarcaptura", "sputnik.commands.savescreenshot.desc", "savescreenshot");
} }
@Override @Override
@@ -6,7 +6,6 @@ import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.mixin.accessors.ClientPacketListenerAccessor; import me.retucio.sputnik.mixin.accessors.ClientPacketListenerAccessor;
import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.LastSeenMessagesTracker; import net.minecraft.network.chat.LastSeenMessagesTracker;
import net.minecraft.network.chat.MessageSignature; import net.minecraft.network.chat.MessageSignature;
import net.minecraft.network.chat.SignedMessageBody; import net.minecraft.network.chat.SignedMessageBody;
@@ -19,7 +18,7 @@ import java.time.Instant;
public class SendCommand extends Command { public class SendCommand extends Command {
public SendCommand() { public SendCommand() {
super("enviar", Component.translatable("sputnik.commands.send.desc").getString(), "send"); super("enviar", "sputnik.commands.send.desc", "send");
} }
@Override @Override
@@ -7,7 +7,6 @@ import me.retucio.sputnik.command.args.EntityArgumentType;
import me.retucio.sputnik.event.input.KeyEvent; import me.retucio.sputnik.event.input.KeyEvent;
import me.retucio.sputnik.util.EntityUtil; import me.retucio.sputnik.util.EntityUtil;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
@@ -16,7 +15,7 @@ import java.awt.*;
public class SpectateCommand extends Command { public class SpectateCommand extends Command {
public SpectateCommand() { public SpectateCommand() {
super("espectar", Component.translatable("sputnik.commands.spectate.desc").getString(), "spectate", "spec"); super("espectar", "sputnik.commands.spectate.desc", "spectate", "spec");
} }
@Override @Override
@@ -6,7 +6,6 @@ import me.retucio.sputnik.command.args.ModuleArgumentType;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.awt.*; import java.awt.*;
@@ -14,7 +13,7 @@ import java.awt.*;
public class ToggleCommand extends Command { public class ToggleCommand extends Command {
public ToggleCommand() { public ToggleCommand() {
super("alternar", Component.translatable("sputnik.commands.toggle.desc").getString(), "toggle"); super("alternar", "sputnik.commands.toggle.desc", "toggle");
} }
@Override @Override
@@ -6,7 +6,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import me.retucio.sputnik.command.Command; import me.retucio.sputnik.command.Command;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -17,7 +16,7 @@ public class WaitCommand extends Command {
private static final ScheduledExecutorService DELAYED_EXECUTOR = new ScheduledThreadPoolExecutor(2); private static final ScheduledExecutorService DELAYED_EXECUTOR = new ScheduledThreadPoolExecutor(2);
public WaitCommand() { public WaitCommand() {
super("wait", Component.translatable("sputnik.commands.wait.desc").getString(), "esperar", "sleep", "delay"); super("wait", "sputnik.commands.wait.desc", "esperar", "sleep", "delay");
} }
@Override @Override
@@ -1,6 +1,6 @@
package me.retucio.sputnik.config; package me.retucio.sputnik.config;
import net.minecraft.network.chat.Component; import me.retucio.sputnik.util.I18nUtil;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -14,7 +14,8 @@ import java.util.Map;
public class BinarySerializer { public class BinarySerializer {
private static final int MAGIC_NUMBER = 0x53505554; // "SPUT" private static final int MAGIC_NUMBER = 0x53505554; // "SPUT"
private static final int VERSION = 2; private static final int VERSION = 3;
private static final int MIN_VERSION = 2;
private static final byte TYPE_NULL = 0; private static final byte TYPE_NULL = 0;
private static final byte TYPE_BOOLEAN = 1; private static final byte TYPE_BOOLEAN = 1;
@@ -72,6 +73,8 @@ public class BinarySerializer {
writeStringBooleanMap(out, config.hudVisibilities); writeStringBooleanMap(out, config.hudVisibilities);
writeStringStringMap(out, config.hudImagePaths); writeStringStringMap(out, config.hudImagePaths);
writeStringStringMap(out, config.friends); writeStringStringMap(out, config.friends);
writeMacros(out, config.macros);
writeString(out, config.currentProfile);
if (config.searchBarPosition != null) { if (config.searchBarPosition != null) {
out.writeBoolean(true); out.writeBoolean(true);
@@ -85,44 +88,63 @@ public class BinarySerializer {
public static ClientConfig readConfig(File file) public static ClientConfig readConfig(File file)
throws IOException { throws IOException {
if (!file.exists() || file.length() == 0) { if (!file.exists() || file.length() == 0) {
throw new IOException(Component.translatable("sputnik.config.error.empty").getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.empty",
"archivo de config. vacío o inexistente"));
} }
try (DataInputStream in = new DataInputStream( try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(file)))) { new BufferedInputStream(new FileInputStream(file)))) {
if (file.length() < 8) { if (file.length() < 8) {
throw new IOException(Component.translatable("sputnik.config.error.toosmall").getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.toosmall",
"archivo demasiado pequeño para ser válido"));
} }
// verificar cabezal // verificar cabezal
int magic = in.readInt(); int magic = in.readInt();
if (magic != MAGIC_NUMBER) { if (magic != MAGIC_NUMBER) {
throw new IOException(Component.translatable("sputnik.config.error.invalidformat", magic).getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.invalidformat",
"formato de archivo inválido: %s", magic));
} }
// todo: sustituir por lógica de migración en vez de simplemente no aceptarlo
int version = in.readInt(); int version = in.readInt();
if (version != VERSION) { if (version < MIN_VERSION || version > VERSION) {
throw new IOException(Component.translatable("sputnik.config.error.invalidversion", version).getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.invalidversion",
"versión inválida: %s", version));
} }
ClientConfig config = new ClientConfig(); // leer mapas. se leen a locales y la ClientConfig se construye al final: su
// constructor la suscribe al bus de eventos, así que crearla antes de terminar
// de leer dejaba una config. huérfana escuchando si el archivo estaba corrupto
Map<String, Boolean> moduleStates = readStringBooleanMap(in);
Map<String, Object> settings = readSettingsMap(in);
Map<String, int[]> settingsPanels = readStringIntArrayMap(in);
Map<String, ClientConfig.PanelData> extendablePanels = readExtendablePanels(in);
Map<String, int[]> hudPositions = readStringIntArrayMap(in);
Map<String, Boolean> hudVisibilities = readStringBooleanMap(in);
Map<String, String> hudImagePaths = readStringStringMap(in);
Map<String, String> friends = readStringStringMap(in);
// leer mapas List<ClientConfig.MacroData> macros = version >= 3 ? readMacros(in) : new ArrayList<>();
config.moduleStates = readStringBooleanMap(in); String currentProfile = version >= 3 ? readString(in) : "";
config.settings = readSettingsMap(in);
config.settingsPanels = readStringIntArrayMap(in);
config.extendablePanels = readExtendablePanels(in);
config.hudPositions = readStringIntArrayMap(in);
config.hudVisibilities = readStringBooleanMap(in);
config.hudImagePaths = readStringStringMap(in);
config.friends = readStringStringMap(in);
// leer pos. de la barra de búsqueda // leer pos. de la barra de búsqueda
if (in.readBoolean()) { int[] searchBarPosition = in.readBoolean()
config.searchBarPosition = new int[]{in.readInt(), in.readInt()}; ? new int[]{in.readInt(), in.readInt()}
} : null;
ClientConfig config = new ClientConfig();
config.moduleStates = moduleStates;
config.settings = settings;
config.settingsPanels = settingsPanels;
config.extendablePanels = extendablePanels;
config.hudPositions = hudPositions;
config.hudVisibilities = hudVisibilities;
config.hudImagePaths = hudImagePaths;
config.friends = friends;
config.macros = macros;
config.currentProfile = currentProfile == null ? "" : currentProfile;
if (searchBarPosition != null) config.searchBarPosition = searchBarPosition;
return config; return config;
} }
@@ -371,6 +393,35 @@ public class BinarySerializer {
return map; return map;
} }
private static void writeMacros(DataOutputStream out, List<ClientConfig.MacroData> macros)
throws IOException {
if (macros == null) {
out.writeInt(0);
return;
}
out.writeInt(macros.size());
for (ClientConfig.MacroData macro : macros) {
writeString(out, macro.name());
out.writeInt(macro.key());
out.writeInt(macro.lines().size());
for (String line : macro.lines()) writeString(out, line);
}
}
private static List<ClientConfig.MacroData> readMacros(DataInputStream in) throws IOException {
int size = in.readInt();
List<ClientConfig.MacroData> macros = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
String name = readString(in);
int key = in.readInt();
int lineCount = in.readInt();
List<String> lines = new ArrayList<>(lineCount);
for (int j = 0; j < lineCount; j++) lines.add(readString(in));
macros.add(new ClientConfig.MacroData(name, key, lines));
}
return macros;
}
private static void writeString(DataOutputStream out, String str) throws IOException { private static void writeString(DataOutputStream out, String str) throws IOException {
if (str == null) { if (str == null) {
out.writeInt(-1); out.writeInt(-1);
@@ -47,19 +47,31 @@ public class ClientConfig {
// amigos: uuid -> nombre // amigos: uuid -> nombre
public Map<String, String> friends = new HashMap<>(); public Map<String, String> friends = new HashMap<>();
public List<MacroData> macros = new ArrayList<>();
// perfil cargado actualmente, vacío si es la config. suelta
public String currentProfile = "";
public ClientConfig() { public ClientConfig() {
EVENT_BUS.subscribe(this); EVENT_BUS.subscribe(this);
} }
// mientras se aplica una config. los cambios vienen de ella, así que reescribirla es
// como poco redundante, y al cambiar de perfil pisaba lo que aún no se había aplicado
private static boolean isApplying() {
return ConfigManager.INSTANCE != null && ConfigManager.INSTANCE.isApplying();
}
@EventListener @EventListener
public void onToggleModule(ToggleModuleEvent event) { public void onToggleModule(ToggleModuleEvent event) {
if (isApplying()) return;
ConfigManager.INSTANCE.setModuleState(event.getModule()); ConfigManager.INSTANCE.setModuleState(event.getModule());
} }
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@EventListener @EventListener
public void onUpdateSetting(UpdateSettingEvent event) { public void onUpdateSetting(UpdateSettingEvent event) {
if (mc == null || !event.shouldSave()) return; if (mc == null || !event.shouldSave() || isApplying()) return;
// guardar ajustes con su respectivo tipo de valor, en formato "nombreMódulo:nombreAjuste" // guardar ajustes con su respectivo tipo de valor, en formato "nombreMódulo:nombreAjuste"
Object value = null; Object value = null;
@@ -71,7 +83,7 @@ public class ClientConfig {
case StringSetting s: value = s.getValue(); break; case StringSetting s: value = s.getValue(); break;
case ListSetting l: value = l.getConfigValues(); break; case ListSetting l: value = l.getConfigValues(); break;
case ColorSetting c: value = c.getConfigValue(); break; case ColorSetting c: value = c.getConfigValue(); break;
case OptionSetting o: value = o.getDisplayName(); break; case OptionSetting o: value = o.getConfigName(); break;
default: break; default: break;
} }
@@ -153,6 +165,7 @@ public class ClientConfig {
@EventListener @EventListener
public void onAddFriend(FriendEvent.Add event) { public void onAddFriend(FriendEvent.Add event) {
if (isApplying()) return;
String uuid = event.getFriend().getUuid().toString(); String uuid = event.getFriend().getUuid().toString();
String name = event.getFriend().getName(); String name = event.getFriend().getName();
if (!friends.containsKey(uuid)) { if (!friends.containsKey(uuid)) {
@@ -163,6 +176,7 @@ public class ClientConfig {
@EventListener @EventListener
public void onRemoveFriend(FriendEvent.Remove event) { public void onRemoveFriend(FriendEvent.Remove event) {
if (isApplying()) return;
friends.remove(event.getFriend().getUuid().toString()); friends.remove(event.getFriend().getUuid().toString());
ConfigManager.INSTANCE.markDirty(); ConfigManager.INSTANCE.markDirty();
} }
@@ -170,4 +184,8 @@ public class ClientConfig {
public record PanelData(int x, int y, boolean extended) implements Serializable { public record PanelData(int x, int y, boolean extended) implements Serializable {
@Serial private static final long serialVersionUID = 1L; @Serial private static final long serialVersionUID = 1L;
} }
public record MacroData(String name, int key, List<String> lines) implements Serializable {
@Serial private static final long serialVersionUID = 1L;
}
} }
@@ -8,8 +8,11 @@ import me.retucio.sputnik.friend.FriendManager;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.ClientSettingsModule; import me.retucio.sputnik.module.modules.client.ClientSettingsModule;
import me.retucio.sputnik.macro.Macro;
import me.retucio.sputnik.macro.MacroManager;
import me.retucio.sputnik.module.setting.*; import me.retucio.sputnik.module.setting.*;
import me.retucio.sputnik.module.setting.settings.*; import me.retucio.sputnik.module.setting.settings.*;
import me.retucio.sputnik.ui.hud.HudElement;
import me.retucio.sputnik.ui.hud.HudRenderer; import me.retucio.sputnik.ui.hud.HudRenderer;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import me.retucio.sputnik.ui.widgets.panels.ExtendablePanel; import me.retucio.sputnik.ui.widgets.panels.ExtendablePanel;
@@ -19,18 +22,21 @@ import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel; import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.io.File; import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
// se ocupa de guardar, cargar y aplicar ajustes // se ocupa de guardar, cargar y aplicar ajustes
@@ -42,8 +48,9 @@ public class ConfigManager {
public static ConfigManager INSTANCE; public static ConfigManager INSTANCE;
private boolean loaded = false; private boolean loaded = false;
private boolean applying = false;
private boolean dirty = false; private boolean dirty = false;
private long lastDirtyTime = 0L; private long lastSaveTime = System.currentTimeMillis();
private ClientConfig config = null; private ClientConfig config = null;
private final ExecutorService saveExecutor = Executors.newSingleThreadExecutor(r -> { private final ExecutorService saveExecutor = Executors.newSingleThreadExecutor(r -> {
@@ -52,7 +59,10 @@ public class ConfigManager {
return t; return t;
}); });
private final AtomicBoolean saveQueued = new AtomicBoolean(false); // instantánea pendiente de escribir. si llega otra antes de que el hilo de fondo escriba,
// sustituye a la anterior en vez de descartarse (que era lo que pasaba con un simple
// "ya hay uno en cola": se tiraba justo la más reciente y se perdían esos cambios)
private final AtomicReference<byte[]> pending = new AtomicReference<>();
// ------------------ MÉTODOS PRINCIPALES ------------------ // ------------------ MÉTODOS PRINCIPALES ------------------
@@ -60,58 +70,81 @@ public class ConfigManager {
// guardar configuraciones // guardar configuraciones
public void save() { public void save() {
if (!loaded) return; if (!loaded) return;
ensureConfig(); writeSnapshot(false);
// serializar a bytes en el hilo principal (snapshot inmediato, evita race conditions)
final byte[] snapshot;
try {
snapshot = BinarySerializer.toBytes(config);
} catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.serialization").getString(), e);
return;
}
// si ya hay un guardado en cola, no añadir otro
if (!saveQueued.compareAndSet(false, true)) return;
saveExecutor.execute(() -> {
try {
BinarySerializer.writeBytes(snapshot, CONFIG_FILE);
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.saved").getString());
} catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.saving").getString(), e);
} finally {
saveQueued.set(false);
}
});
} }
public void saveOnShutdown() { public void saveOnShutdown() {
// esperar a que termine cualquier guardado en curso, luego guardar síncronamente // no aceptar más tareas y esperar a que termine el guardado en curso
saveExecutor.shutdown(); saveExecutor.shutdown();
try { try {
// dar hasta 5 segundos para que termine el guardado en curso // dar hasta 5 segundos para que termine el guardado en curso
if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) { if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.thread").getString()); Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.thread",
"el hilo de guardado no terminó a tiempo"));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
// guardado final sincrónico por si había cambios pendientes no guardados // el guardado final va en este mismo hilo: el executor ya está cerrado, así que
if (dirty) save(); // encolar la escritura lanzaba RejectedExecutionException y no se guardaba NADA al
// cerrar el juego. o sea que se perdía todo lo hecho desde el último autoguardado
if (!loaded) return;
writeSnapshot(true);
}
// serializa en el hilo actual (instantánea inmediata, evita race conditions) y escribe
// en el de fondo, salvo que se pida síncrono (al cerrar)
private void writeSnapshot(boolean sync) {
ensureConfig();
final byte[] snapshot;
try {
snapshot = BinarySerializer.toBytes(config);
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.serialization",
"error al serializar ajustes"), e);
return;
}
dirty = false;
lastSaveTime = System.currentTimeMillis();
if (sync) {
writeToDisk(snapshot);
return;
}
// si ya había una instantánea esperando, la tarea encolada recogerá ésta
if (pending.getAndSet(snapshot) != null) return;
saveExecutor.execute(() -> {
byte[] bytes = pending.getAndSet(null);
if (bytes != null) writeToDisk(bytes);
});
}
private static void writeToDisk(byte[] bytes) {
try {
BinarySerializer.writeBytes(bytes, CONFIG_FILE);
Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.saved", "ajustes guardados"));
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.saving",
"error al guardar ajustes"), e);
}
} }
public void load() { public void load() {
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.loading").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.loading", "cargando ajustes..."));
if (CONFIG_FILE.exists()) { if (CONFIG_FILE.exists()) {
try { try {
config = BinarySerializer.readConfig(CONFIG_FILE); setConfig(BinarySerializer.readConfig(CONFIG_FILE));
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.loaded").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.loaded", "ajustes cargados"));
return; return;
} catch (IOException e) { } catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.loading").getString(), e); Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.loading",
"error cargando ajustes, intentando json"), e);
} }
} }
@@ -119,35 +152,96 @@ public class ConfigManager {
if (LEGACY_JSON_FILE.exists()) { if (LEGACY_JSON_FILE.exists()) {
try (FileReader reader = new FileReader(LEGACY_JSON_FILE)) { try (FileReader reader = new FileReader(LEGACY_JSON_FILE)) {
Gson gson = new GsonBuilder().setPrettyPrinting().create(); Gson gson = new GsonBuilder().setPrettyPrinting().create();
config = gson.fromJson(reader, ClientConfig.class); setConfig(gson.fromJson(reader, ClientConfig.class));
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.json").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.json",
"ajustes json cargados (migración)"));
save(); // aquí `loaded` todavía es false (se pone al aplicar), así que save() no
// haría nada: la migración se escribe directamente
writeSnapshot(true);
return; return;
} catch (IOException e) { } catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.json").getString(), e); Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.json",
"error cargando json, usando defaults"), e);
} }
} }
// crear nuevo config // crear nuevo config
ensureConfig(); ensureConfig();
save(); writeSnapshot(true);
} }
// aplicar configuraciones cargadas // aplicar configuraciones cargadas. devuelve false si todavía no se puede (falta algo por
public void applyConfig() { // construir), para que se reintente en el siguiente tick en vez de quedarse sin aplicar
if (!shouldApply()) return; public boolean applyConfig() {
if (!shouldApply()) return false;
applying = true;
try {
// los del mod primero: entre ellos está "teclas multimódulo", que decide si al
// asignar una tecla se le quita a los demás módulos. aplicándolos los últimos,
// las teclas de los módulos se cargaban con el valor por defecto de ese ajuste
applyClientSettings();
applyModuleStates(); applyModuleStates();
applyModuleSettings(); applyModuleSettings();
applySettingsPanels(); applySettingsPanels();
applySearchBarPosition(); applySearchBarPosition();
applyClientSettings();
applyExtendablePanels(); applyExtendablePanels();
applyFriends(); applyFriends();
applyMacros();
applyHudElements();
Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.applied", "ajustes aplicados"));
} catch (Exception e) {
// si algo peta a medio aplicar no se puede dejar `loaded` en false: save() no
// haría nada en toda la sesión y se perdería la config. entera al cerrar
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.applying",
"error aplicando ajustes"), e);
} finally {
applying = false;
loaded = true; loaded = true;
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.applied").getString()); }
return true;
}
// true mientras se está cargando la config. sobre los ajustes. lo mira KeySetting para no
// tratar las teclas guardadas como si el usuario las acabara de asignar, y ClientConfig
// para no reescribir en la config. lo que se le está aplicando
public boolean isApplying() {
return applying;
}
// sustituye la config. viva por otra y la vuelca sobre el estado actual del cliente
public void replaceConfig(ClientConfig newConfig) {
applying = true;
try {
resetLiveState();
} finally {
applying = false;
}
setConfig(newConfig);
loaded = false;
applyConfig();
save();
}
// lo que no venga en la config. nueva tiene que quedarse en su valor por defecto, no en
// el que tuviera el perfil anterior
private void resetLiveState() {
if (ClickGui.INSTANCE != null) ClickGui.INSTANCE.closeAllSettingsPanels();
for (Module module : ModuleManager.INSTANCE.getModules()) {
module.setEnabled(false);
module.getSettings().forEach(Setting::reset);
}
ClientSettingsPanel.clientSettings.getSettings().forEach(Setting::reset);
for (Friend friend : new ArrayList<>(FriendManager.INSTANCE.getFriends()))
FriendManager.INSTANCE.remove(friend);
MacroManager.INSTANCE.setMacros(List.of());
} }
// obtener configuración actual // obtener configuración actual
@@ -204,6 +298,20 @@ public class ConfigManager {
markDirty(); markDirty();
} }
public void setMacros(List<Macro> macros) {
ensureConfig();
config.macros = macros.stream()
.map(macro -> new ClientConfig.MacroData(macro.getName(), macro.getKey(), List.copyOf(macro.getLines())))
.toList();
markDirty();
}
public void setCurrentProfile(String name) {
ensureConfig();
config.currentProfile = name;
markDirty();
}
// ------------------ APLICAR AJUSTES ------------------ // ------------------ APLICAR AJUSTES ------------------
@@ -213,7 +321,28 @@ public class ConfigManager {
friend.setName(name); friend.setName(name);
FriendManager.INSTANCE.add(friend); FriendManager.INSTANCE.add(friend);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.friends").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.friends",
"amigos aplicados"));
}
public void applyMacros() {
MacroManager.INSTANCE.setMacros(config.macros.stream()
.map(data -> new Macro(data.name(), data.key(), data.lines()))
.toList());
}
// el HudRenderer coloca sus elementos al construirse, pero al cambiar de perfil ya existen
public void applyHudElements() {
if (HudRenderer.INSTANCE == null) return;
for (HudElement element : HudRenderer.INSTANCE.getElements()) {
int[] position = config.hudPositions.get(element.getId());
if (position != null && position.length == 2) element.setPosition(position[0], position[1]);
else element.resetPosition();
Boolean visible = config.hudVisibilities.get(element.getId());
element.setVisible(visible == null || visible);
}
} }
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@@ -233,8 +362,8 @@ public class ConfigManager {
case ListSetting listSetting -> applyListSetting(listSetting, value); case ListSetting listSetting -> applyListSetting(listSetting, value);
case ColorSetting colorSetting -> applyColorSetting(colorSetting, value); case ColorSetting colorSetting -> applyColorSetting(colorSetting, value);
case OptionSetting optionSetting -> applyOptionSetting(optionSetting, value); case OptionSetting optionSetting -> applyOptionSetting(optionSetting, value);
default -> Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.unknownsetting", default -> Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.unknownsetting",
setting.getClass().getSimpleName()).getString()); "watafac queseso: %s", setting.getClass().getSimpleName()));
} }
} }
@@ -244,14 +373,16 @@ public class ConfigManager {
module.setEnabled(config.moduleStates.get(module.getName())); module.setEnabled(config.moduleStates.get(module.getName()));
} }
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.modulestates").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.modulestates",
"estados de módulos aplicados"));
} }
private void applyModuleSettings() { private void applyModuleSettings() {
ModuleManager.INSTANCE.getModules().forEach(module -> { ModuleManager.INSTANCE.getModules().forEach(module -> {
module.getSettings().forEach(this::applySetting); module.getSettings().forEach(this::applySetting);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.modulesettings").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.modulesettings",
"ajustes de módulos aplicados"));
} }
private void applySettingsPanels() { private void applySettingsPanels() {
@@ -259,14 +390,16 @@ public class ConfigManager {
Module module = ModuleManager.INSTANCE.getModuleByName(moduleName); Module module = ModuleManager.INSTANCE.getModuleByName(moduleName);
if (module == null) { if (module == null) {
Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.nullmodule", moduleName).getString()); Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.nullmodule",
"\"null\" como módulo (\"%s\")", moduleName));
return; return;
} }
ClickGui.INSTANCE.openSettingsPanel(module, position[0], position[1]); ClickGui.INSTANCE.openSettingsPanel(module, position[0], position[1]);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.settingspanels").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.settingspanels",
"estados de marcos de ajustes aplicados"));
} }
private void applySearchBarPosition() { private void applySearchBarPosition() {
@@ -274,13 +407,15 @@ public class ConfigManager {
ClickGui.INSTANCE.getSearchBar().setX(config.searchBarPosition[0]); ClickGui.INSTANCE.getSearchBar().setX(config.searchBarPosition[0]);
ClickGui.INSTANCE.getSearchBar().setY(config.searchBarPosition[1]); ClickGui.INSTANCE.getSearchBar().setY(config.searchBarPosition[1]);
} }
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.searchbar").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.searchbar",
"posición de la barra de búsqueda aplicada"));
} }
private void applyClientSettings() { private void applyClientSettings() {
ClientSettingsPanel.clientSettings.setEnabled(true); ClientSettingsPanel.clientSettings.setEnabled(true);
ClientSettingsPanel.clientSettings.getSettings().forEach(this::applySetting); ClientSettingsPanel.clientSettings.getSettings().forEach(this::applySetting);
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.clientsettings").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.clientsettings",
"ajustes del cliente aplicados"));
} }
private void applyExtendablePanels() { private void applyExtendablePanels() {
@@ -290,7 +425,8 @@ public class ConfigManager {
applyExtendablePanel(ClickGui.INSTANCE.getClientSettingsPanel()); applyExtendablePanel(ClickGui.INSTANCE.getClientSettingsPanel());
applyExtendablePanel(ClickGui.INSTANCE.getFriendsPanel()); applyExtendablePanel(ClickGui.INSTANCE.getFriendsPanel());
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.extendablepanels").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.extendablepanels",
"posiciones de marcos extendibles aplicadas"));
ClickGui.INSTANCE.refreshListButtons(); ClickGui.INSTANCE.refreshListButtons();
} }
@@ -359,8 +495,14 @@ public class ConfigManager {
Map<T, Boolean> convertedValues = new HashMap<>(); Map<T, Boolean> convertedValues = new HashMap<>();
for (T option : setting.getOptions()) { for (T option : setting.getOptions()) {
String key = setting.getDisplayName(option); String key = setting.getConfigName(option);
Object booleanValue = map.get(key); Object booleanValue = map.get(key);
// compatibilidad con configs. de antes, cuando la clave era el nombre ya
// traducido (el de vanilla en inglés). sin esto la lista entera se quedaba
// en false, o sea que al usuario se le borraba lo que tuviera marcado
if (booleanValue == null) booleanValue = map.get(I18nUtil.vanillaName(key));
convertedValues.put(option, booleanValue instanceof Boolean && (Boolean) booleanValue); convertedValues.put(option, booleanValue instanceof Boolean && (Boolean) booleanValue);
} }
@@ -411,12 +553,24 @@ public class ConfigManager {
return setting.getSg().getModule().getName() + ":" + setting.getTrueName(); return setting.getSg().getModule().getName() + ":" + setting.getTrueName();
} }
// ClickGui incluida: los marcos y la barra de búsqueda se aplican sobre ella, y se
// construye en una tarea aparte. si todavía no está, mejor esperar al siguiente tick
private boolean shouldApply() { private boolean shouldApply() {
return Sputnik.mc != null && ModuleManager.INSTANCE != null && config != null; return Sputnik.mc != null && ModuleManager.INSTANCE != null
&& ClickGui.INSTANCE != null && config != null;
} }
private void ensureConfig() { private void ensureConfig() {
if (config == null) config = new ClientConfig(); if (config == null) setConfig(new ClientConfig());
}
// ClientConfig se suscribe al bus en su constructor, así que al reemplazarla hay que
// dar de baja a la anterior: si no, la vieja seguía escuchando y escribiendo en un
// mapa que ya no era el que se guarda
private void setConfig(ClientConfig newConfig) {
if (config == newConfig) return;
if (config != null) Sputnik.EVENT_BUS.unsubscribe(config);
config = newConfig;
} }
public boolean hasLoaded() { public boolean hasLoaded() {
@@ -425,7 +579,6 @@ public class ConfigManager {
public void markDirty() { public void markDirty() {
this.dirty = true; this.dirty = true;
this.lastDirtyTime = System.currentTimeMillis();
} }
public boolean isDirty() { public boolean isDirty() {
@@ -438,13 +591,14 @@ public class ConfigManager {
ClientSettingsModule settings = ClientSettingsPanel.clientSettings; ClientSettingsModule settings = ClientSettingsPanel.clientSettings;
if (!settings.autosave.getValue()) return; if (!settings.autosave.getValue()) return;
// desde el último guardado, no desde el último cambio: contando desde el cambio,
// tocando ajustes cada menos del intervalo no se autoguardaba nunca
long intervalMs = (long) (settings.autosaveInterval.getValue() * 1000); long intervalMs = (long) (settings.autosaveInterval.getValue() * 1000);
if (System.currentTimeMillis() - lastDirtyTime < intervalMs) return; if (System.currentTimeMillis() - lastSaveTime < intervalMs) return;
HudRenderer.INSTANCE.pushNotification( HudRenderer.INSTANCE.pushNotification(
Component.translatable("sputnik.config.autosaving.title").getString(), Component.translatable("sputnik.config.autosaving.title").getString(),
Component.translatable("sputnik.config.autosaving.desc").getString()); Component.translatable("sputnik.config.autosaving.desc").getString());
save(); save();
dirty = false;
} }
} }
@@ -0,0 +1,99 @@
package me.retucio.sputnik.config;
import me.retucio.sputnik.Sputnik;
import me.retucio.sputnik.util.I18nUtil;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ProfileManager {
public static final int MAX_NAME_LENGTH = 32;
private static final File DIR = new File("sputnik/profiles");
private static final String EXTENSION = ".dat";
public static ProfileManager INSTANCE;
public List<String> getProfileNames() {
File[] files = DIR.listFiles((dir, name) -> name.endsWith(EXTENSION));
if (files == null) return List.of();
List<String> names = new ArrayList<>();
for (File file : files) {
String name = file.getName();
names.add(name.substring(0, name.length() - EXTENSION.length()));
}
names.sort(Comparator.naturalOrder());
return names;
}
public boolean exists(String name) {
return fileOf(name).exists();
}
public String getCurrent() {
return ConfigManager.INSTANCE.getConfig().currentProfile;
}
// vuelca la config. viva en el perfil y lo deja como actual
public boolean save(String name) {
if (!isValidName(name)) return false;
if (!DIR.exists() && !DIR.mkdirs()) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.profile.error.dir",
"no se pudo crear el directorio de perfiles"));
return false;
}
ConfigManager.INSTANCE.setCurrentProfile(name);
try {
BinarySerializer.writeConfig(ConfigManager.INSTANCE.getConfig(), fileOf(name));
return true;
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.profile.error.saving",
"error al guardar el perfil \"%s\"", name), e);
return false;
}
}
public boolean load(String name) {
if (!exists(name)) return false;
ClientConfig profile;
try {
profile = BinarySerializer.readConfig(fileOf(name));
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.profile.error.loading",
"error al cargar el perfil \"%s\"", name), e);
return false;
}
profile.currentProfile = name;
ConfigManager.INSTANCE.replaceConfig(profile);
return true;
}
public boolean delete(String name) {
if (!exists(name)) return false;
if (!fileOf(name).delete()) return false;
if (name.equals(getCurrent())) ConfigManager.INSTANCE.setCurrentProfile("");
return true;
}
public static boolean isValidName(String name) {
return name != null
&& !name.isBlank()
&& name.length() <= MAX_NAME_LENGTH
&& name.matches("[a-zA-Z0-9 _-]+");
}
private static File fileOf(String name) {
return new File(DIR, name + EXTENSION);
}
}
@@ -0,0 +1,53 @@
package me.retucio.sputnik.macro;
import me.retucio.sputnik.util.ChatUtil;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public class Macro {
private String name;
private int key;
private final List<String> lines = new ArrayList<>();
private boolean searchMatch = true;
public Macro(String name) {
this(name, GLFW.GLFW_KEY_UNKNOWN, List.of());
}
public Macro(String name, int key, List<String> lines) {
this.name = name;
this.key = key;
this.lines.addAll(lines);
}
// cada línea se manda como si el usuario la hubiera escrito en el chat, así que valen
// comandos del mod, comandos del servidor y mensajes normales
public void run() {
if (Minecraft.getInstance().player == null) return;
for (String line : lines) {
if (!line.isBlank()) ChatUtil.simulateChatMessage(line);
}
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getKey() { return key; }
public void setKey(int key) { this.key = key; }
public List<String> getLines() { return lines; }
public void setLines(List<String> lines) {
this.lines.clear();
this.lines.addAll(lines);
}
public boolean isSearchMatch() { return searchMatch; }
public void setSearchMatch(boolean searchMatch) { this.searchMatch = searchMatch; }
}
@@ -0,0 +1,60 @@
package me.retucio.sputnik.macro;
import me.retucio.sputnik.config.ConfigManager;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public class MacroManager {
public static MacroManager INSTANCE;
private final List<Macro> macros = new ArrayList<>();
public boolean add(Macro macro) {
if (getByName(macro.getName()) != null) return false;
macros.add(macro);
save();
return true;
}
public void remove(Macro macro) {
macros.remove(macro);
save();
}
public Macro getByName(String name) {
for (Macro macro : macros) {
if (macro.getName().equalsIgnoreCase(name)) return macro;
}
return null;
}
public boolean onKeyPress(int key) {
if (key == GLFW.GLFW_KEY_UNKNOWN || Minecraft.getInstance().gui.screen() != null) return false;
boolean ran = false;
for (Macro macro : new ArrayList<>(macros)) {
if (macro.getKey() != key) continue;
macro.run();
ran = true;
}
return ran;
}
public void setMacros(List<Macro> macros) {
this.macros.clear();
this.macros.addAll(macros);
}
public void save() {
if (ConfigManager.INSTANCE != null) ConfigManager.INSTANCE.setMacros(macros);
}
public List<Macro> getMacros() {
return macros;
}
}
@@ -5,6 +5,7 @@ import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.misc.ScreenshotPlus; import me.retucio.sputnik.module.modules.misc.ScreenshotPlus;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot; import net.minecraft.client.Screenshot;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@@ -19,14 +20,14 @@ import java.util.function.Consumer;
@Mixin(Screenshot.class) @Mixin(Screenshot.class)
public abstract class ScreenshotMixin { 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) @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) { private static void sendActionsMessage(NativeImage image, File file, Consumer<Component> callback, CallbackInfo ci) {
ScreenshotPlus screenshotPlus = getScreenshotPlus(); ScreenshotPlus screenshotPlus = getScreenshotPlus();
if (screenshotPlus.isEnabled()) ci.cancel(); 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) @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 { private static void interceptScreenshotFile(NativeImage image, File file, Consumer<Component> callback, CallbackInfo ci) throws IOException {
ScreenshotPlus screenshotPlus = getScreenshotPlus(); ScreenshotPlus screenshotPlus = getScreenshotPlus();
// aunque el módulo esté apagado, pasar las referencias para poder usar los comandos para las capturas // aunque el módulo esté apagado, pasar las referencias para poder usar los comandos para las capturas
@@ -10,6 +10,7 @@ import me.retucio.sputnik.event.render.Render3DEvent;
import me.retucio.sputnik.event.render.RenderBlockOutlineEvent; import me.retucio.sputnik.event.render.RenderBlockOutlineEvent;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.camera.Freecam; import me.retucio.sputnik.module.modules.camera.Freecam;
import me.retucio.sputnik.util.render.SputnikBuffers;
import net.minecraft.client.DeltaTracker; import net.minecraft.client.DeltaTracker;
import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.SubmitNodeCollector;
@@ -42,6 +43,7 @@ public abstract class LevelRendererMixin {
profiler.push(Sputnik.MOD_ID + "-3d"); profiler.push(Sputnik.MOD_ID + "-3d");
EVENT_BUS.post(new Render3DEvent(matrices, dt, cameraState)); EVENT_BUS.post(new Render3DEvent(matrices, dt, cameraState));
SputnikBuffers.flush(); // antes de que empiece el hud
profiler.pop(); profiler.pop();
matrices.popPose(); matrices.popPose();
@@ -83,10 +83,6 @@ public class Module {
this.bind.setDefaultValue(key); this.bind.setDefaultValue(key);
// puto marcos // puto marcos
// la clave tiene que salir igual que en ConfigManager#getSettingKey, o sea con los
// nombres internos: Module#getName ya lo es, pero en los ajustes el traducido es
// getName() y el interno getTrueName(). si no cuadran no se encuentra la tecla
// guardada y se le pisa al usuario con la de por defecto
if (ConfigManager.INSTANCE.getConfig().settings if (ConfigManager.INSTANCE.getConfig().settings
.get(this.getName() + ":" + bind.getTrueName()) == null) .get(this.getName() + ":" + bind.getTrueName()) == null)
this.bind.setValue(key); this.bind.setValue(key);
@@ -162,6 +158,7 @@ public class Module {
// onDisable no miraba notify.getValue(), así que avisaba aunque estuviera desactivado // onDisable no miraba notify.getValue(), así que avisaba aunque estuviera desactivado
private void notifyToggle(boolean on) { private void notifyToggle(boolean on) {
if (!notify.getValue()) return; if (!notify.getValue()) return;
if (ConfigManager.INSTANCE != null && ConfigManager.INSTANCE.isApplying()) return;
String key = on ? "sputnik.module.toggled.on" : "sputnik.module.toggled.off"; String key = on ? "sputnik.module.toggled.on" : "sputnik.module.toggled.off";
Component message = Component.translatable(key, getDisplayName()); Component message = Component.translatable(key, getDisplayName());
@@ -89,6 +89,7 @@ public class ModuleManager {
add(new AttributeSwapper()); add(new AttributeSwapper());
add(new ChestplateSwitch()); add(new ChestplateSwitch());
add(new CrystalAura()); add(new CrystalAura());
add(new HoldAttack());
add(new MaceKill()); add(new MaceKill());
add(new ProjectileTrajectories()); add(new ProjectileTrajectories());
add(new SpearKill()); add(new SpearKill());
@@ -141,6 +142,7 @@ public class ModuleManager {
private void addNetwork() { private void addNetwork() {
add(new BungeecordSpoofer()); add(new BungeecordSpoofer());
add(new LogoutSpots()); add(new LogoutSpots());
add(new PacketCanceller());
add(new PacketDelay()); add(new PacketDelay());
add(new PacketMine()); add(new PacketMine());
add(new Reconnect()); add(new Reconnect());
@@ -1,12 +1,10 @@
package me.retucio.sputnik.module.modules.camera; package me.retucio.sputnik.module.modules.camera;
import com.github.retucio.neutrino.EventListener;
import me.retucio.sputnik.event.network.PacketEvent;
import me.retucio.sputnik.module.Category; import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.EnumSetting; import me.retucio.sputnik.module.setting.settings.EnumSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting; import me.retucio.sputnik.module.setting.settings.NumberSetting;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import me.retucio.sputnik.util.RotationUtil;
/** /**
@@ -47,18 +45,7 @@ public class Autism extends Module {
if (direction.is(RotationDirection.COUNTERCLOCKWISE)) if (direction.is(RotationDirection.COUNTERCLOCKWISE))
yaw -= interval.getFloatValue(); yaw -= interval.getFloatValue();
mc.getConnection().send(new ServerboundMovePlayerPacket.Rot( RotationUtil.requestYaw(RotationUtil.HIGHEST, yaw, RotationUtil.Mode.SILENT);
yaw,
mc.player.getXRot(),
mc.player.onGround(),
mc.player.horizontalCollision
));
}
@EventListener
private void onPacketSend(PacketEvent.Send event) {
if (event.getPacket() instanceof ServerboundMovePlayerPacket.Rot packet
&& packet.getYRot(yaw) != yaw) event.cancel();
} }
private enum RotationDirection { private enum RotationDirection {
@@ -177,15 +177,14 @@ public class Freecam extends Module {
super.onEnable(); super.onEnable();
} }
// allChanged() ya no existe; ahora hay que pasarle el estado a mano
private static void invalidateChunks() { private static void invalidateChunks() {
if (mc.level == null) return; if (mc.level == null) return;
mc.levelRenderer.invalidateCompiledGeometry(mc.level, mc.options, mc.gameRenderer.mainCamera(), mc.getBlockColors()); mc.levelExtractor.allChanged();
} }
@Override @Override
public void onDisable() { public void onDisable() {
if (reloadChunks.getValue()) mc.execute(Freecam::invalidateChunks); if (reloadChunks.getValue()) invalidateChunks();
mc.options.setCameraType(perspective); mc.options.setCameraType(perspective);
crouching = false; crouching = false;
@@ -6,6 +6,7 @@ import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.BooleanSetting; import me.retucio.sputnik.module.setting.settings.BooleanSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting; import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.RotationUtil;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
/** /**
@@ -30,8 +31,7 @@ public class Rotations extends Module {
public void onTick() { public void onTick() {
if (mc.player == null) return; if (mc.player == null) return;
mc.player.setYRot(yaw.getFloatValue()); RotationUtil.request(RotationUtil.HIGHEST, yaw.getFloatValue(), pitch.getFloatValue(), RotationUtil.Mode.CLIENT);
mc.player.setXRot(pitch.getFloatValue());
if (serverSide.getValue()) if (serverSide.getValue())
mc.player.connection.send( mc.player.connection.send(
@@ -234,11 +234,22 @@ public class CrystalAura extends Module {
private void doBreak() { private void doBreak() {
if (breakTimer > 0) return; if (breakTimer > 0) return;
List<Entity> crystals = new ArrayList<>();
for (Entity entity : mc.level.getEntities().getAll()) { for (Entity entity : mc.level.getEntities().getAll()) {
if (!(entity instanceof EndCrystal) || entity.distanceTo(mc.player) > mc.player.entityInteractionRange()) continue; if (!(entity instanceof EndCrystal) || entity.distanceTo(mc.player) > mc.player.entityInteractionRange()) continue;
if (rotateToBreak.getValue()) EntityUtil.lookAt(entity.getEyePosition()); crystals.add(entity);
mc.gameMode.attack(mc.player, entity);
} }
if (!crystals.isEmpty()) {
Runnable attack = () -> crystals.forEach(crystal -> mc.gameMode.attack(mc.player, crystal));
// solo se puede apuntar a uno, así que se rompen todos rotando solo al primero
if (rotateToBreak.getValue())
RotationUtil.lookAt(RotationUtil.HIGH, crystals.getFirst().getEyePosition(), attack);
else attack.run();
}
breakTimer = breakDelay.getIntValue(); breakTimer = breakDelay.getIntValue();
} }
@@ -254,8 +265,11 @@ public class CrystalAura extends Module {
return; return;
} }
if (rotateToPlace.getValue()) EntityUtil.lookAt(Vec3.atCenterOf(result.getBlockPos())); Runnable place = () -> mc.gameMode.startPrediction(mc.level, (sequence) -> new ServerboundUseItemOnPacket(hand, result, sequence));
mc.gameMode.startPrediction(mc.level, (sequence) -> new ServerboundUseItemOnPacket(hand, result, sequence));
if (rotateToPlace.getValue())
RotationUtil.lookAt(RotationUtil.HIGH, Vec3.atCenterOf(result.getBlockPos()), place);
else place.run();
} }
private void placeObby(BlockPos pos) { private void placeObby(BlockPos pos) {
@@ -273,8 +287,11 @@ public class CrystalAura extends Module {
BlockPos supportPos = pos.below(); BlockPos supportPos = pos.below();
BlockHitResult result = new BlockHitResult(Vec3.atCenterOf(supportPos), Direction.UP, supportPos, false); BlockHitResult result = new BlockHitResult(Vec3.atCenterOf(supportPos), Direction.UP, supportPos, false);
if (rotateToPlace.getValue()) EntityUtil.lookAt(Vec3.atCenterOf(pos)); Runnable place = () -> mc.gameMode.startPrediction(mc.level, (sequence) -> new ServerboundUseItemOnPacket(hand, result, sequence));
mc.gameMode.startPrediction(mc.level, (sequence) -> new ServerboundUseItemOnPacket(hand, result, sequence));
if (rotateToPlace.getValue())
RotationUtil.lookAt(RotationUtil.HIGH, Vec3.atCenterOf(pos), place);
else place.run();
} }
private void doAutoSelect(Item item) { private void doAutoSelect(Item item) {
@@ -0,0 +1,95 @@
package me.retucio.sputnik.module.modules.combat;
import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.BooleanSetting;
import me.retucio.sputnik.module.setting.settings.EnumSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting;
import net.minecraft.world.phys.EntityHitResult;
/**
* @author retucio
*/
public class HoldAttack extends Module {
private final EnumSetting<AttackMode> mode = sgGeneral.add(new EnumSetting<>(
"modo de ataque",
"cómo atacar",
AttackMode.class,
AttackMode.TIME
));
private final NumberSetting timeCooldown = sgGeneral.add(new NumberSetting(
"cooldown",
"cómo de llena tiene que estar la barra de ataque antes de atacar",
84.8f, 0.0f, 100.0f, 0.1f
)).visibility(() -> mode.is(AttackMode.TIME));
private final NumberSetting spamCooldown = sgGeneral.add(new NumberSetting(
"cooldown ",
"cada cuánto atacar, en ticks",
0, 0, 40, 1
)).visibility(() -> mode.is(AttackMode.SPAM));
private final BooleanSetting attackInScreens = sgGeneral.add(new BooleanSetting(
"atacar en pantallas",
"atacar aun estando en una pantalla",
false
));
private int counter = 0;
public HoldAttack() {
super("autoataque", "mantén pulsado el clic para atacar automáticamente", Category.COMBAT);
mode.onUpdate(v -> counter = 0);
}
@Override
public void onEnable() {
counter = 0;
super.onEnable();
}
@Override
public void onTick() {
if (mc.player == null || mc.gameMode == null || mc.getCameraEntity() == null) return;
counter++;
if (mc.gui.screen() != null && !attackInScreens.getValue()) {
return;
}
if (!mc.options.keyAttack.isDown()) {
return;
}
if (!(mc.hitResult instanceof EntityHitResult ehr)) {
return;
}
if (mode.is(AttackMode.TIME)) {
if (mc.player.getAttackStrengthScale(0) < timeCooldown.getNormalizedValue()) {
return;
}
} else if (mode.is(AttackMode.SPAM)) {
if (counter < spamCooldown.getIntValue()) {
return;
}
}
mc.gameMode.attack(mc.player, ehr.getEntity());
counter = 0;
}
private enum AttackMode {
SPAM("spamear"),
TIME("timear");
private final String name;
AttackMode(String name) { this.name = name; }
@Override public String toString() { return name; }
}
}
@@ -5,6 +5,7 @@ import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.BooleanSetting; import me.retucio.sputnik.module.setting.settings.BooleanSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting; import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.RotationUtil;
import net.minecraft.tags.ItemTags; import net.minecraft.tags.ItemTags;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
@@ -73,13 +74,11 @@ public class SpearKill extends Module {
float yaw = (float) (Math.toDegrees(Math.atan2(toTarget.z, toTarget.x)) - 90.0); float yaw = (float) (Math.toDegrees(Math.atan2(toTarget.z, toTarget.x)) - 90.0);
float pitch = (float) -Math.toDegrees(Math.asin(toTarget.y)); float pitch = (float) -Math.toDegrees(Math.asin(toTarget.y));
mc.player.setYRot(yaw); RotationUtil.request(RotationUtil.HIGH, yaw, pitch, RotationUtil.Mode.CLIENT);
mc.player.setYHeadRot(yaw);
mc.player.setXRot(pitch);
if (heldTicks >= 10) { if (heldTicks >= 10) {
double lungeSpeed = speed.getValue(); double lungeSpeed = speed.getValue();
Vec3 viewDir = Vec3.directionFromRotation(mc.player.getXRot(), mc.player.getYRot()); Vec3 viewDir = Vec3.directionFromRotation(pitch, yaw);
mc.player.setSprinting(true); mc.player.setSprinting(true);
mc.player.setDeltaMovement(viewDir.scale(lungeSpeed)); mc.player.setDeltaMovement(viewDir.scale(lungeSpeed));
} }
@@ -5,6 +5,7 @@ import me.retucio.sputnik.event.interact.UseItemEvent;
import me.retucio.sputnik.module.Category; import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.NumberSetting; import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.RotationUtil;
import net.minecraft.world.item.Items; import net.minecraft.world.item.Items;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@@ -99,7 +100,7 @@ public class InfiniteElytra extends Module {
} }
} }
mc.player.setXRot(pitch); RotationUtil.requestPitch(RotationUtil.NORMAL, pitch, RotationUtil.Mode.CLIENT);
} }
@EventListener @EventListener
@@ -107,7 +108,7 @@ public class InfiniteElytra extends Module {
if (mc.player == null) return; if (mc.player == null) return;
if (mc.player.getItemInHand(event.getHand()).getItem() == Items.FIREWORK_ROCKET && !mc.player.isSpectator() && mc.player.isFallFlying()) { if (mc.player.getItemInHand(event.getHand()).getItem() == Items.FIREWORK_ROCKET && !mc.player.isSpectator() && mc.player.isFallFlying()) {
pitch = pitchUp.getFloatValue(); pitch = pitchUp.getFloatValue();
mc.player.setXRot(pitch); RotationUtil.requestPitch(RotationUtil.NORMAL, pitch, RotationUtil.Mode.CLIENT);
state = FlyState.PITCHING_DOWN; state = FlyState.PITCHING_DOWN;
lowestY = mc.player.getY(); lowestY = mc.player.getY();
} }
@@ -334,8 +334,8 @@ public class Scaffold extends Module {
if (NetworkUtil.placeBlock( if (NetworkUtil.placeBlock(
pos, InteractionHand.MAIN_HAND, pos, InteractionHand.MAIN_HAND,
slot, slot,
rotate.getValue(), rotate.getValue() ? RotationUtil.Mode.CLIENT : null,
rotate.getValue(), RotationUtil.NORMAL,
swingHand.getValue(), swingHand.getValue(),
true, true,
true true
@@ -0,0 +1,44 @@
package me.retucio.sputnik.module.modules.network;
import com.github.retucio.neutrino.EventListener;
import me.retucio.sputnik.event.network.PacketEvent;
import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.ListSetting;
import me.retucio.sputnik.util.Lists;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.PacketType;
import org.jspecify.annotations.NonNull;
public class PacketCanceller extends Module {
private final ListSetting<PacketType<? extends @NonNull Packet<?>>> c2s = sgGeneral.add(new ListSetting(
"cliente > servidor", "paquetes a cancelar, dirección servidor",
Lists.c2sPacketList, Lists.allFalse(Lists.c2sPacketList), Lists.c2sPacketNames
));
private final ListSetting<PacketType<? extends @NonNull Packet<?>>> s2c = sgGeneral.add(new ListSetting(
"servidor > cliente", "paquetes a cancelar, dirección cliente",
Lists.s2cPacketList, Lists.allFalse(Lists.s2cPacketList), Lists.s2cPacketNames
));
public PacketCanceller() {
super("cancelador de paquetes", "cancela los paquetes seleccionados", Category.NETWORK);
}
@EventListener
private void onPacketSend(PacketEvent.Send event) {
if (c2s.isEnabled(event.getPacket().type())) {
event.cancel();
}
}
@EventListener
private void onPacketReceive(PacketEvent.Receive event) {
if (s2c.isEnabled(event.getPacket().type())) {
event.cancel();
}
}
}
@@ -120,8 +120,6 @@ public class PacketDelay extends Module {
@EventListener @EventListener
private void onJoinWorld(JoinWorldEvent event) { private void onJoinWorld(JoinWorldEvent event) {
// el aviso iba sin condición, así que el módulo se apagaba solo al entrar
// a cualquier mundo, servidores incluidos
if (!MiscUtil.isSingleplayer()) return; if (!MiscUtil.isSingleplayer()) return;
ChatUtil.error(Component.translatable("sputnik.module.packetdelay.singleplayer")); ChatUtil.error(Component.translatable("sputnik.module.packetdelay.singleplayer"));
@@ -11,7 +11,7 @@ import me.retucio.sputnik.module.setting.settings.ColorSetting;
import me.retucio.sputnik.module.setting.settings.EnumSetting; import me.retucio.sputnik.module.setting.settings.EnumSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting; import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.EntityUtil; import me.retucio.sputnik.util.RotationUtil;
import me.retucio.sputnik.util.render.RenderUtil; import me.retucio.sputnik.util.render.RenderUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
@@ -278,14 +278,18 @@ public class PacketMine extends Module {
return false; return false;
} }
rotate(); Runnable mine = () -> {
mc.gameMode.startPrediction(mc.level, sequence -> mc.gameMode.startPrediction(mc.level, sequence ->
new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, pos, dir, sequence)); new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, pos, dir, sequence));
mining = true;
mc.gameMode.startPrediction(mc.level, sequence -> mc.gameMode.startPrediction(mc.level, sequence ->
new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, pos, dir, sequence)); new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, pos, dir, sequence));
};
RotationUtil.Mode rotation = getRotationMode();
if (rotation == null) mine.run();
else RotationUtil.lookAt(RotationUtil.NORMAL, Vec3.atLowerCornerOf(pos), 0, rotation, mine);
mining = true;
return true; return true;
} }
@@ -303,13 +307,12 @@ public class PacketMine extends Module {
); );
} }
private void rotate() { private RotationUtil.Mode getRotationMode() {
Vec3 pos = Vec3.atLowerCornerOf(this.pos); return switch (rotate.getValue()) {
switch (rotate.getValue()) { case CLIENT, BOTH -> RotationUtil.Mode.CLIENT;
case CLIENT -> EntityUtil.lookAtClient(pos); case SERVER -> RotationUtil.Mode.SILENT;
case SERVER -> EntityUtil.lookAtServer(pos); case NONE -> null;
case BOTH -> EntityUtil.lookAt(pos); };
}
} }
public boolean shouldRetry() { public boolean shouldRetry() {
@@ -11,6 +11,7 @@ import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.InventoryUtil; import me.retucio.sputnik.util.InventoryUtil;
import me.retucio.sputnik.util.Lists; import me.retucio.sputnik.util.Lists;
import me.retucio.sputnik.util.RotationUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
@@ -136,23 +137,23 @@ public class NoFall extends Module {
return; return;
} }
// mirar abajo
mc.player.setXRot(180);
int slot = mc.player.getInventory().findSlotMatchingItem(mlgItem); int slot = mc.player.getInventory().findSlotMatchingItem(mlgItem);
mc.player.getInventory().setSelectedSlot(slot); mc.player.getInventory().setSelectedSlot(slot);
// usar bloque // usar bloque
if (mlgItem.getItem() instanceof BlockItem) { if (mlgItem.getItem() instanceof BlockItem) {
RotationUtil.requestPitch(RotationUtil.HIGH, 90, RotationUtil.Mode.CLIENT);
shouldUse = true; shouldUse = true;
placed = true; placed = true;
return; return;
} }
// usar ítem // mirar abajo y usar el ítem cuando el servidor lo vea
RotationUtil.request(RotationUtil.HIGH, Float.NaN, 90, 0, RotationUtil.Mode.CLIENT, () -> {
InteractionResult didPlace = mc.gameMode.useItem(mc.player, InteractionHand.MAIN_HAND); InteractionResult didPlace = mc.gameMode.useItem(mc.player, InteractionHand.MAIN_HAND);
if (didPlace.consumesAction()) if (didPlace.consumesAction())
placed = true; placed = true;
});
} }
} }
} }
@@ -8,7 +8,6 @@ import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.Lists; import me.retucio.sputnik.util.Lists;
import net.minecraft.core.particles.SimpleParticleType; import net.minecraft.core.particles.SimpleParticleType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import java.util.List; import java.util.List;
@@ -26,9 +25,10 @@ public class Particles extends Module {
.map(particleType -> (SimpleParticleType) particleType) .map(particleType -> (SimpleParticleType) particleType)
.toList(); .toList();
// claves de traducción, no texto traducido (ver Lists#init)
Map<SimpleParticleType, String> particleNames = Lists.getMapOfLists(particleList, Map<SimpleParticleType, String> particleNames = Lists.getMapOfLists(particleList,
particleList.stream().map(particle -> Component.translatable( particleList.stream().map(particle ->
BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toShortLanguageKey()).getString()) BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toShortLanguageKey())
.toList()); .toList());
private final ListSetting<SimpleParticleType> particles = sgGeneral.add(new ListSetting<>( private final ListSetting<SimpleParticleType> particles = sgGeneral.add(new ListSetting<>(
@@ -3,6 +3,7 @@ package me.retucio.sputnik.module.modules.world;
import me.retucio.sputnik.module.Category; import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.util.EntityUtil; import me.retucio.sputnik.util.EntityUtil;
import me.retucio.sputnik.util.RotationUtil;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.monster.EnderMan; import net.minecraft.world.entity.monster.EnderMan;
@@ -52,10 +53,6 @@ public class Racist extends Module {
} }
private void lookAtEnderman(EnderMan enderman) { private void lookAtEnderman(EnderMan enderman) {
double yaw = EntityUtil.getYaw(enderman); RotationUtil.lookAt(RotationUtil.LOW, enderman, EntityUtil.Target.HEAD, 0, RotationUtil.Mode.CLIENT);
double pitch = EntityUtil.getPitch(enderman, EntityUtil.Target.HEAD);
mc.player.setYRot((float) yaw);
mc.player.setXRot((float) pitch);
} }
} }
@@ -1,5 +1,6 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
@@ -32,8 +33,17 @@ public class KeySetting extends Setting<Integer> {
// con "teclas multimódulo" puesto se permite repetir tecla, que es justo lo que // con "teclas multimódulo" puesto se permite repetir tecla, que es justo lo que
// dice el ajuste. antes se le quitaba la tecla al otro módulo igualmente, así que // dice el ajuste. antes se le quitaba la tecla al otro módulo igualmente, así que
// el ajuste sólo servía para las teclas de vanilla // el ajuste sólo servía para las teclas de vanilla.
//
// mientras se carga la config. no se toca nada: son las teclas que el usuario ya
// tenía, y quitárselas aquí se las machacaba (y encima guardaba el -1).
//
// clientSettings puede ser null: el módulo de ajustes se construye en el
// inicializador estático de ClientSettingsPanel, y su propia tecla por defecto pasa
// por aquí antes de que el campo esté asignado
if (ModuleManager.INSTANCE != null if (ModuleManager.INSTANCE != null
&& ClientSettingsPanel.clientSettings != null
&& !ConfigManager.INSTANCE.isApplying()
&& !ClientSettingsPanel.clientSettings.multipleKeybinds.getValue()) { && !ClientSettingsPanel.clientSettings.multipleKeybinds.getValue()) {
for (Module module : ModuleManager.INSTANCE.getModules()) for (Module module : ModuleManager.INSTANCE.getModules())
if (module.getKey() == value && value != -1) { if (module.getKey() == value && value != -1) {
@@ -1,6 +1,7 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
import me.retucio.sputnik.util.I18nUtil;
import java.util.*; import java.util.*;
@@ -62,7 +63,7 @@ public class ListSetting<T> extends Setting<Map<T, Boolean>> {
public Map<String, Boolean> getConfigValues() { public Map<String, Boolean> getConfigValues() {
Map<String, Boolean> configValues = new HashMap<>(); Map<String, Boolean> configValues = new HashMap<>();
for (T option : options) { for (T option : options) {
configValues.put(getDisplayName(option), value.getOrDefault(option, false)); configValues.put(getConfigName(option), value.getOrDefault(option, false));
} }
return configValues; return configValues;
} }
@@ -103,10 +104,20 @@ public class ListSetting<T> extends Setting<Map<T, Boolean>> {
return displayNames != null ? new HashMap<>(displayNames) : null; return displayNames != null ? new HashMap<>(displayNames) : null;
} }
public String getDisplayName(T key) { // OJO: nombre interno de la opción. es la clave de la config. (ver
// ConfigManager#applyListSetting) y para las listas del juego es la clave de
// traducción ("entity.minecraft.zombie"), que no depende del idioma. no traducir
public String getConfigName(T key) {
if (displayNames != null && displayNames.containsKey(key)) { if (displayNames != null && displayNames.containsKey(key)) {
return displayNames.get(key); return displayNames.get(key);
} }
return key != null ? key.toString() : "?"; return key != null ? key.toString() : "?";
} }
// nombre visible: se traduce aquí. antes se mostraba el nombre interno tal cual, o sea
// los nombres de vanilla en inglés aunque el juego estuviera en otro idioma
public String getDisplayName(T key) {
String name = getConfigName(key);
return I18nUtil.orElse(name, name);
}
} }
@@ -57,6 +57,10 @@ public class NumberSetting extends Setting<Double> {
return value.byteValue(); return value.byteValue();
} }
public double getNormalizedValue() {
return (value - min ) / (max - min);
}
public void setValue(double value) { public void setValue(double value) {
if (this.value == value || this.locked) return; if (this.value == value || this.locked) return;
double clamped = clamp(value, min, max); double clamped = clamp(value, min, max);
@@ -1,6 +1,7 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
import me.retucio.sputnik.util.I18nUtil;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -26,14 +27,19 @@ public class OptionSetting<T> extends Setting<T> {
if (options.contains(option)) super.setValue(option); if (options.contains(option)) super.setValue(option);
} }
// por nombre interno, no por el traducido: es lo que se guarda en la config.
public void setValueByName(String name) { public void setValueByName(String name) {
for (T opt : options) { for (T opt : options) {
if (displayNames != null && !displayNames.isEmpty()) { if (getConfigName(opt).equals(name)) {
if (displayNames.get(opt).equals(name)) {
setValue(opt); setValue(opt);
return; return;
} }
} else if (opt.toString().equals(name)) { }
// compatibilidad con configs. de antes, cuando se guardaba el nombre ya traducido
// (el de vanilla en inglés). si no, la opción guardada se perdía y volvía a la de serie
for (T opt : options) {
if (I18nUtil.vanillaName(getConfigName(opt)).equals(name)) {
setValue(opt); setValue(opt);
return; return;
} }
@@ -82,9 +88,22 @@ public class OptionSetting<T> extends Setting<T> {
return displayNames; return displayNames;
} }
// OJO: nombre interno de la opción, o sea la clave de la config. (ver ClientConfig
// #onUpdateSetting). igual que en ListSetting, para las listas del juego es la clave
// de traducción. no traducir
public String getConfigName(T key) {
if (displayNames != null && displayNames.containsKey(key)) return displayNames.get(key);
return key != null ? key.toString() : "?";
}
public String getConfigName() {
return getConfigName(value);
}
// nombre visible, traducido
public String getDisplayName(T key) { public String getDisplayName(T key) {
if (displayNames != null) return displayNames.get(key); String name = getConfigName(key);
return key.toString(); return I18nUtil.orElse(name, name);
} }
public String getDisplayName() { public String getDisplayName() {
@@ -0,0 +1,155 @@
package me.retucio.sputnik.ui.hud;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.KeyUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
// editor de elementos del hud, compartido entre su propia pantalla y la pestaña de la interfaz
public class HudEditor {
public static HudEditor INSTANCE;
private static final Minecraft mc = Minecraft.getInstance();
private final List<HudElement> elements = new ArrayList<>();
@Nullable private HudElement selected = null;
private boolean dragging = false;
private int dragX, dragY;
public void setElements(List<HudElement> elements) {
this.elements.clear();
this.elements.addAll(elements);
}
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY) {
Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class);
if (hud.axialMovement.getValue() && dragging && selected != null) {
int centerX = mc.getWindow().getGuiScaledWidth() / 2;
if (Math.abs(centerX - mouseX) < hud.axisOffset.getValue()) {
dragX = mouseX - centerX + selected.getW() / 2;
gui.verticalLine(centerX, 0, mc.getWindow().getGuiScaledHeight(), Colors.CELESTE.getRGB());
}
int centerY = mc.getWindow().getGuiScaledHeight() / 2;
if (Math.abs(centerY - mouseY) < hud.axisOffset.getValue()) {
dragY = mouseY - centerY + selected.getH() / 2;
gui.horizontalLine(0, mc.getWindow().getGuiScaledWidth(), centerY, Colors.CELESTE.getRGB());
}
}
for (HudElement element : elements) element.renderInEditor(gui, hud);
if (dragging) return;
for (HudElement element : elements) {
if (!element.isVisible() || !element.isHovered(mouseX, mouseY)) continue;
List<Component> tooltip = element.getTooltip();
if (!tooltip.isEmpty())
gui.setComponentTooltipForNextFrame(mc.font, tooltip, mouseX + 7, mouseY + 20);
break;
}
}
public boolean mouseClicked(int mouseX, int mouseY, int button) {
for (HudElement element : elements) {
if (!element.isHovered(mouseX, mouseY)) continue;
selected = element;
if (button == 0) {
dragging = true;
dragX = mouseX - element.getX();
dragY = mouseY - element.getY();
elements.remove(element);
elements.add(element);
return true;
}
if (button == 1) {
if (KeyUtil.isShiftDown()) selected.resetPosition();
else selected.setVisible(!selected.isVisible());
saveElement(selected);
return true;
}
}
return false;
}
public boolean mouseDragged(int mouseX, int mouseY, int button) {
if (!dragging || selected == null || button != 0) return false;
int width = mc.getWindow().getGuiScaledWidth();
int height = mc.getWindow().getGuiScaledHeight();
selected.setPosition(
Math.clamp(mouseX - dragX, 1, width - selected.getW() - 1),
Math.clamp(mouseY - dragY, 1, height - selected.getH() - 1));
return true;
}
public boolean mouseReleased(int button) {
if (dragging && button == 0) {
dragging = false;
if (selected != null) saveElement(selected);
return true;
}
selected = null;
return false;
}
public void onKey(int key) {
Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class);
if (!hud.arrowMovement.getValue() || selected == null) return;
int offset = hud.arrowOffset.getIntValue();
switch (key) {
case GLFW.GLFW_KEY_UP -> selected.setY(Math.max(1, selected.getY() - offset));
case GLFW.GLFW_KEY_DOWN -> selected.setY(Math.min(
mc.getWindow().getGuiScaledHeight() - selected.getH() + 1, selected.getY() + offset));
case GLFW.GLFW_KEY_LEFT -> selected.setX(Math.max(1, selected.getX() - offset));
case GLFW.GLFW_KEY_RIGHT -> selected.setX(Math.min(
mc.getWindow().getGuiScaledWidth() - selected.getW() + 1, selected.getX() + offset));
}
}
public void saveAll() {
for (HudElement element : elements) writeElement(element);
ConfigManager.INSTANCE.save();
}
private void saveElement(HudElement element) {
writeElement(element);
ConfigManager.INSTANCE.save();
}
private static void writeElement(HudElement element) {
ConfigManager.INSTANCE.setHudPosition(element.getId(), element.getX(), element.getY());
ConfigManager.INSTANCE.setHudVisibility(element.getId(), element.isVisible());
}
public boolean isSelected(HudElement element) {
return selected == element;
}
public void unselect() {
selected = null;
dragging = false;
}
}
@@ -1,200 +1,77 @@
package me.retucio.sputnik.ui.hud; package me.retucio.sputnik.ui.hud;
import com.github.retucio.neutrino.EventListener;
import me.retucio.sputnik.Sputnik; import me.retucio.sputnik.Sputnik;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.event.input.KeyEvent; import me.retucio.sputnik.event.input.KeyEvent;
import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud;
import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel; import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel;
import me.retucio.sputnik.util.Colors; import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.KeyUtil;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public class HudEditorScreen extends Screen { public class HudEditorScreen extends Screen {
public static HudEditorScreen INSTANCE; public static HudEditorScreen INSTANCE;
private final Minecraft mc = Minecraft.getInstance(); private final Minecraft mc = Minecraft.getInstance();
private final Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class);
private final List<HudElement> elements = new ArrayList<>();
@Nullable private HudElement selected = null;
private boolean dragging = false;
private int dragX, dragY;
public HudEditorScreen() { public HudEditorScreen() {
super(Component.translatable("sputnik.hud.editor.title")); super(Component.translatable("sputnik.hud.editor.title"));
Sputnik.EVENT_BUS.subscribe(this); Sputnik.EVENT_BUS.subscribe(this);
} }
public void setElements(List<HudElement> elements) {
this.elements.clear();
this.elements.addAll(elements);
}
@Override @Override
public void extractRenderState(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) { public void extractRenderState(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
gui.fill(0, 0, this.width, this.height, Colors.hudEditorScreenBackgroundColor.getRGB()); gui.fill(0, 0, this.width, this.height, Colors.hudEditorScreenBackgroundColor.getRGB());
// instrucciones
gui.centeredText(mc.font, title, this.width / 2, this.height / 2 - mc.font.lineHeight, -1); gui.centeredText(mc.font, title, this.width / 2, this.height / 2 - mc.font.lineHeight, -1);
gui.centeredText(mc.font, gui.centeredText(mc.font,
Component.translatable("sputnik.hud.editor.instructions"), Component.translatable("sputnik.hud.editor.instructions"),
this.width / 2, this.height / 2 + mc.font.lineHeight, Colors.instructionsTextColor.getRGB()); this.width / 2, this.height / 2 + mc.font.lineHeight, Colors.instructionsTextColor.getRGB());
// movimiento axial HudEditor.INSTANCE.render(gui, mouseX, mouseY);
if (hud.axialMovement.getValue()) {
int centerX = mc.getWindow().getGuiScaledWidth() / 2;
if (dragging && selected != null && Math.abs(centerX - mouseX) < hud.axisOffset.getValue()) {
dragX = mouseX - centerX + selected.getW() / 2;
gui.verticalLine(centerX, 0, mc.getWindow().getGuiScaledHeight(), Colors.CELESTE.getRGB());
}
int centerY = mc.getWindow().getGuiScaledHeight() / 2;
if (dragging && selected != null && Math.abs(centerY - mouseY) < hud.axisOffset.getValue()) {
dragY = mouseY - centerY + selected.getH() / 2;
gui.horizontalLine(0, mc.getWindow().getGuiScaledWidth(), centerY, Colors.CELESTE.getRGB());
}
}
// elementos
for (HudElement element : elements) {
element.renderInEditor(gui, hud);
}
// tooltips
if (!dragging) {
for (HudElement element : elements) {
if (!element.isVisible()) continue;
if (element.isHovered(mouseX, mouseY)) {
List<Component> tooltip = element.getTooltip();
if (!tooltip.isEmpty())
gui.setComponentTooltipForNextFrame(mc.font, tooltip, mouseX + 7, mouseY + 20);
break;
}
}
}
super.extractRenderState(gui, mouseX, mouseY, delta); super.extractRenderState(gui, mouseX, mouseY, delta);
} }
@Override @Override
public boolean mouseClicked(@NonNull MouseButtonEvent click, boolean doubled) { public boolean mouseClicked(@NonNull MouseButtonEvent click, boolean doubled) {
int mouseX = (int) click.x(); if (HudEditor.INSTANCE.mouseClicked((int) click.x(), (int) click.y(), click.button())) return true;
int mouseY = (int) click.y();
for (HudElement element : elements) {
if (element.isHovered(mouseX, mouseY)) {
selected = element;
if (click.button() == 0) {
dragging = true;
dragX = mouseX - element.getX();
dragY = mouseY - element.getY();
elements.remove(element);
elements.add(element);
return true;
} else if (click.button() == 1) {
if (KeyUtil.isShiftDown()) {
selected.resetPosition();
} else {
selected.setVisible(!selected.isVisible());
}
saveElementToConfig(selected);
return true;
}
}
}
return super.mouseClicked(click, doubled); return super.mouseClicked(click, doubled);
} }
@Override @Override
public boolean mouseDragged(@NonNull MouseButtonEvent click, double deltaX, double deltaY) { public boolean mouseDragged(@NonNull MouseButtonEvent click, double deltaX, double deltaY) {
if (!dragging || selected == null || click.button() != 0) if (HudEditor.INSTANCE.mouseDragged((int) click.x(), (int) click.y(), click.button())) return true;
return super.mouseDragged(click, deltaX, deltaY); return super.mouseDragged(click, deltaX, deltaY);
int mouseX = (int) click.x();
int mouseY = (int) click.y();
int newX = mouseX - dragX;
int newY = mouseY - dragY;
newX = Math.clamp(newX, 1, width - selected.getW() - 1);
newY = Math.clamp(newY, 1, height - selected.getH() - 1);
selected.setPosition(newX, newY);
return true;
} }
@Override @Override
public boolean mouseReleased(@NonNull MouseButtonEvent click) { public boolean mouseReleased(@NonNull MouseButtonEvent click) {
if (dragging && click.button() == 0) { if (HudEditor.INSTANCE.mouseReleased(click.button())) return true;
dragging = false;
if (selected != null)
saveElementToConfig(selected);
return true;
}
selected = null;
return super.mouseReleased(click); return super.mouseReleased(click);
} }
// he sustituído el método del súper por un evento para ver si así se podían // he sustituído el método del súper por un evento para ver si así se podían
// registrar dos teclas a la vez, pero resulta que no :P // registrar dos teclas a la vez, pero resulta que no :P
@EventListener
public void onKey(KeyEvent event) { public void onKey(KeyEvent event) {
if (!(mc.gui.screen() == INSTANCE)) return; if (mc.gui.screen() != INSTANCE) return;
if (event.getAction() == GLFW.GLFW_RELEASE) return; if (event.getAction() == GLFW.GLFW_RELEASE) return;
event.cancel(); event.cancel();
// guardar y salir
if (event.getKey() == GLFW.GLFW_KEY_ESCAPE) { if (event.getKey() == GLFW.GLFW_KEY_ESCAPE) {
saveAllElementsToConfig(); HudEditor.INSTANCE.saveAll();
selected = null; HudEditor.INSTANCE.unselect();
mc.gui.setScreen(null); mc.gui.setScreen(null);
} return;
// movimiento de los elementos con las teclas de las flechas
else if (hud.arrowMovement.getValue() && selected != null) {
int offset = hud.arrowOffset.getIntValue();
if (event.getKey() == GLFW.GLFW_KEY_UP) {
selected.setY(Math.max(1, selected.getY() - offset));
}
if (event.getKey() == GLFW.GLFW_KEY_DOWN) {
selected.setY(Math.min(mc.getWindow().getGuiScaledHeight() - selected.getH() + 1, selected.getY() + offset));
}
if (event.getKey() == GLFW.GLFW_KEY_LEFT) {
selected.setX(Math.max(1, selected.getX() - offset));
}
if (event.getKey() == GLFW.GLFW_KEY_RIGHT) {
selected.setX(Math.min(mc.getWindow().getGuiScaledWidth() - selected.getW() + 1, selected.getX() + offset));
}
}
} }
private void saveElementToConfig(HudElement element) { HudEditor.INSTANCE.onKey(event.getKey());
ConfigManager.INSTANCE.setHudPosition(element.getId(), element.getX(), element.getY());
ConfigManager.INSTANCE.setHudVisibility(element.getId(), element.isVisible());
ConfigManager.INSTANCE.save();
}
private void saveAllElementsToConfig() {
for (HudElement element : elements) {
saveElementToConfig(element);
}
} }
@Override @Override
@@ -203,8 +80,4 @@ public class HudEditorScreen extends Screen {
super.extractBlurredBackground(gui); super.extractBlurredBackground(gui);
} }
} }
public boolean isSelected(HudElement element) {
return selected == element;
}
} }
@@ -4,6 +4,7 @@ import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud; import me.retucio.sputnik.module.modules.client.Hud;
import me.retucio.sputnik.ui.hud.elements.*; import me.retucio.sputnik.ui.hud.elements.*;
import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.misc.NotificationWidget; import me.retucio.sputnik.ui.widgets.misc.NotificationWidget;
import net.minecraft.client.DeltaTracker; import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@@ -49,7 +50,7 @@ public class HudRenderer {
addElement(new TotemsElement(), positions, visibilities); addElement(new TotemsElement(), positions, visibilities);
addElement(new ArmorElement(), positions, visibilities); addElement(new ArmorElement(), positions, visibilities);
HudEditorScreen.INSTANCE.setElements(elements); HudEditor.INSTANCE.setElements(elements);
} }
private void addElement(HudElement element, Map<String, int[]> positions, Map<String, Boolean> visibilities) { private void addElement(HudElement element, Map<String, int[]> positions, Map<String, Boolean> visibilities) {
@@ -130,7 +131,8 @@ public class HudRenderer {
|| !hud.isEnabled() || !hud.isEnabled()
|| (mc.debugEntries.isOverlayVisible() && !hud.showOnF3.getValue()) || (mc.debugEntries.isOverlayVisible() && !hud.showOnF3.getValue())
|| (mc.gui.screen() instanceof ChatScreen && !hud.showOnChat.getValue()) || (mc.gui.screen() instanceof ChatScreen && !hud.showOnChat.getValue())
|| mc.gui.screen() instanceof HudEditorScreen; || mc.gui.screen() instanceof HudEditorScreen
|| (ClickGui.INSTANCE != null && mc.gui.screen() == ClickGui.INSTANCE && ClickGui.INSTANCE.isOnHudTab());
} }
public void pushNotification(String title, String desc) { public void pushNotification(String title, String desc) {
@@ -109,7 +109,7 @@ public abstract class ImageHudElement extends HudElement {
protected void drawEditorBackground(GuiGraphicsExtractor gui) { protected void drawEditorBackground(GuiGraphicsExtractor gui) {
int bgColor = visible ? Colors.visibleHudElementColor.getRGB() : Colors.disabledHudElementColor.getRGB(); int bgColor = visible ? Colors.visibleHudElementColor.getRGB() : Colors.disabledHudElementColor.getRGB();
int outlineColor = HudEditorScreen.INSTANCE != null && HudEditorScreen.INSTANCE.isSelected(this) int outlineColor = HudEditor.INSTANCE != null && HudEditor.INSTANCE.isSelected(this)
? Colors.selectedHudElementOutlineColor.getRGB() ? Colors.selectedHudElementOutlineColor.getRGB()
: Colors.unselectedHudElementOutlineColor.getRGB(); : Colors.unselectedHudElementOutlineColor.getRGB();
@@ -37,7 +37,7 @@ public abstract class TextHudElement extends HudElement {
protected void drawEditorBackground(GuiGraphicsExtractor gui) { protected void drawEditorBackground(GuiGraphicsExtractor gui) {
int bgColor = visible ? Colors.visibleHudElementColor.getRGB() : Colors.disabledHudElementColor.getRGB(); int bgColor = visible ? Colors.visibleHudElementColor.getRGB() : Colors.disabledHudElementColor.getRGB();
int outlineColor = HudEditorScreen.INSTANCE != null && HudEditorScreen.INSTANCE.isSelected(this) int outlineColor = HudEditor.INSTANCE != null && HudEditor.INSTANCE.isSelected(this)
? Colors.selectedHudElementOutlineColor.getRGB() ? Colors.selectedHudElementOutlineColor.getRGB()
: Colors.unselectedHudElementOutlineColor.getRGB(); : Colors.unselectedHudElementOutlineColor.getRGB();
@@ -24,8 +24,13 @@ import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.ui.widgets.Button; import me.retucio.sputnik.ui.widgets.Button;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import me.retucio.sputnik.ui.widgets.Widget; import me.retucio.sputnik.ui.widgets.Widget;
import me.retucio.sputnik.ui.hud.HudEditor;
import me.retucio.sputnik.ui.screen.tabs.MacrosTab;
import me.retucio.sputnik.ui.screen.tabs.ProfilesTab;
import me.retucio.sputnik.ui.widgets.misc.ScrollBarWidget; import me.retucio.sputnik.ui.widgets.misc.ScrollBarWidget;
import me.retucio.sputnik.ui.widgets.misc.SearchBarWidget; import me.retucio.sputnik.ui.widgets.misc.SearchBarWidget;
import me.retucio.sputnik.ui.widgets.misc.TabBarWidget;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.ui.widgets.panels.settings.FriendSettingsPanel; import me.retucio.sputnik.ui.widgets.panels.settings.FriendSettingsPanel;
import me.retucio.sputnik.util.KeyUtil; import me.retucio.sputnik.util.KeyUtil;
@@ -70,6 +75,10 @@ public class ClickGui extends Screen {
private final SearchBarWidget searchBar = new SearchBarWidget(320, 10, 320, 20); private final SearchBarWidget searchBar = new SearchBarWidget(320, 10, 320, 20);
private final List<Widget> miscWidgets = List.of(scrollBar, searchBar); private final List<Widget> miscWidgets = List.of(scrollBar, searchBar);
private final TabBarWidget tabBar = new TabBarWidget(this::onTabLeft);
private final ProfilesTab profilesTab = new ProfilesTab();
private final MacrosTab macrosTab = new MacrosTab();
public ClickGui() { public ClickGui() {
super(Component.translatable("sputnik.ui.clickgui.title")); super(Component.translatable("sputnik.ui.clickgui.title"));
Sputnik.EVENT_BUS.subscribe(this); Sputnik.EVENT_BUS.subscribe(this);
@@ -83,6 +92,27 @@ public class ClickGui extends Screen {
@Override @Override
public void extractRenderState(@NonNull GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) { public void extractRenderState(@NonNull GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
switch (tabBar.getSelected()) {
case MODULES -> renderModules(gui, mouseX, mouseY, delta);
case HUD -> renderHud(gui, mouseX, mouseY);
case PROFILES -> profilesTab.render(gui, mouseX, mouseY, delta);
case MACROS -> macrosTab.render(gui, mouseX, mouseY, delta);
}
tabBar.render(gui, mouseX, mouseY, delta);
super.extractRenderState(gui, mouseX, mouseY, delta);
}
private void renderHud(GuiGraphicsExtractor gui, int mouseX, int mouseY) {
gui.fill(0, 0, this.width, this.height, Colors.hudEditorScreenBackgroundColor.getRGB());
gui.centeredText(mc.font, Component.translatable("sputnik.hud.editor.instructions"),
this.width / 2, this.height / 2, Colors.instructionsTextColor.getRGB());
HudEditor.INSTANCE.render(gui, mouseX, mouseY);
}
private void renderModules(@NonNull GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
scrollBar.setWindowHeight(mc.getWindow().getGuiScaledHeight()); scrollBar.setWindowHeight(mc.getWindow().getGuiScaledHeight());
scrollBar.setContentHeight(calculateContentHeight()); scrollBar.setContentHeight(calculateContentHeight());
scrollBar.render(gui, mouseX, mouseY, delta); scrollBar.render(gui, mouseX, mouseY, delta);
@@ -128,8 +158,6 @@ public class ClickGui extends Screen {
filterSearchResults(); filterSearchResults();
renderBottomGradient(gui, scrollOffset); renderBottomGradient(gui, scrollOffset);
super.extractRenderState(gui, mouseX, mouseY, delta);
} }
// renderizar un gradiente negro leve en la parte inferior de la pantalla si el contenido excede el límite inferior de la pantalla, para indicarlo visualmente // renderizar un gradiente negro leve en la parte inferior de la pantalla si el contenido excede el límite inferior de la pantalla, para indicarlo visualmente
@@ -179,11 +207,12 @@ public class ClickGui extends Screen {
int x = (int) click.x(); int x = (int) click.x();
int y = (int) click.y(); int y = (int) click.y();
miscWidgets.forEach(w -> w.mouseClicked( if (tabBar.handleClick(x, y, click.button())) return super.mouseClicked(click, doubled);
x, y, click.button()
)); switch (tabBar.getSelected()) {
case MODULES -> {
miscWidgets.forEach(w -> w.mouseClicked(x, y, click.button()));
// detectar clics sobre los paneles
friendsPanel.mouseClicked(x, y, click.button()); friendsPanel.mouseClicked(x, y, click.button());
clientSettingsPanel.mouseClicked(x, y, click.button()); clientSettingsPanel.mouseClicked(x, y, click.button());
for (ModulePanel mp : modulePanels.reversed()) { for (ModulePanel mp : modulePanels.reversed()) {
@@ -192,6 +221,11 @@ public class ClickGui extends Screen {
for (SettingsPanel sp : new ArrayList<>(settingsPanels)) { for (SettingsPanel sp : new ArrayList<>(settingsPanels)) {
sp.mouseClicked(x, y, click.button()); sp.mouseClicked(x, y, click.button());
} }
}
case HUD -> HudEditor.INSTANCE.mouseClicked(x, y, click.button());
case PROFILES -> profilesTab.mouseClicked(x, y, click.button());
case MACROS -> macrosTab.mouseClicked(x, y, click.button());
}
return super.mouseClicked(click, doubled); return super.mouseClicked(click, doubled);
} }
@@ -201,6 +235,13 @@ public class ClickGui extends Screen {
int x = (int) click.x(); int x = (int) click.x();
int y = (int) click.y(); int y = (int) click.y();
if (tabBar.getSelected() == Tab.HUD) {
HudEditor.INSTANCE.mouseReleased(click.button());
return super.mouseReleased(click);
}
if (tabBar.getSelected() != Tab.MODULES) return super.mouseReleased(click);
miscWidgets.forEach(w -> w.mouseReleased( miscWidgets.forEach(w -> w.mouseReleased(
x, y, click.button())); x, y, click.button()));
@@ -219,6 +260,13 @@ public class ClickGui extends Screen {
@Override @Override
public boolean mouseDragged(@NonNull MouseButtonEvent click, double deltaX, double deltaY) { public boolean mouseDragged(@NonNull MouseButtonEvent click, double deltaX, double deltaY) {
if (tabBar.getSelected() == Tab.HUD) {
HudEditor.INSTANCE.mouseDragged((int) click.x(), (int) click.y(), click.button());
return super.mouseDragged(click, deltaX, deltaY);
}
if (tabBar.getSelected() != Tab.MODULES) return super.mouseDragged(click, deltaX, deltaY);
miscWidgets.forEach(w -> w.mouseDragged( miscWidgets.forEach(w -> w.mouseDragged(
(int) click.x(), (int) click.x(),
(int) click.y())); (int) click.y()));
@@ -233,11 +281,21 @@ public class ClickGui extends Screen {
@EventListener @EventListener
public void onKey(KeyEvent event) { public void onKey(KeyEvent event) {
searchBar.onKey(event.getKey(), event.getAction()); if (mc.gui.screen() != this) return;
switch (tabBar.getSelected()) {
case MODULES -> searchBar.onKey(event.getKey(), event.getAction());
case HUD -> {
if (event.getAction() != GLFW.GLFW_RELEASE) HudEditor.INSTANCE.onKey(event.getKey());
}
case PROFILES -> profilesTab.onKey(event.getKey(), event.getAction());
case MACROS -> macrosTab.onKey(event.getKey(), event.getAction());
}
} }
@EventListener @EventListener
public void onMouseScroll(MouseScrollEvent event) { public void onMouseScroll(MouseScrollEvent event) {
if (tabBar.getSelected() != Tab.MODULES) return;
miscWidgets.forEach(w -> w.mouseScrolled( miscWidgets.forEach(w -> w.mouseScrolled(
event.getVertical() * clientSettings.scrollSens.getValue())); event.getVertical() * clientSettings.scrollSens.getValue()));
} }
@@ -477,6 +535,32 @@ public class ClickGui extends Screen {
// métodos del súper // métodos del súper
private void onTabLeft(Tab previous) {
if (previous == Tab.HUD) {
HudEditor.INSTANCE.saveAll();
HudEditor.INSTANCE.unselect();
}
unselect(selected);
}
// lo miran Sputnik#onInput y el editor del hud para no tratar las teclas que se están
// escribiendo como atajos de módulos
public boolean isCapturingInput() {
return searchBar.isFocused() || profilesTab.isCapturingInput() || macrosTab.isCapturingInput();
}
public boolean isOnHudTab() {
return tabBar.getSelected() == Tab.HUD;
}
public void closeAllSettingsPanels() {
for (SettingsPanel sp : new ArrayList<>(settingsPanels)) {
Sputnik.EVENT_BUS.post(new SettingsPanelEvent.Close(sp));
unselect(sp);
}
settingsPanels.clear();
}
@Override @Override
public void onClose() { // evitar que al reabrir la interfaz sin previamente haber soltado el clic, se sigan arrastrando objetos public void onClose() { // evitar que al reabrir la interfaz sin previamente haber soltado el clic, se sigan arrastrando objetos
modulePanels.forEach(mp -> mp.mouseReleased(0, 0, 0)); modulePanels.forEach(mp -> mp.mouseReleased(0, 0, 0));
@@ -484,6 +568,11 @@ public class ClickGui extends Screen {
scrollBar.mouseReleased(0, 0, 0); scrollBar.mouseReleased(0, 0, 0);
searchBar.mouseReleased(0, 0, 0); searchBar.mouseReleased(0, 0, 0);
if (isOnHudTab()) {
HudEditor.INSTANCE.saveAll();
HudEditor.INSTANCE.unselect();
}
unselect(selected); unselect(selected);
super.onClose(); super.onClose();
} }
@@ -512,6 +601,7 @@ public class ClickGui extends Screen {
public ClientSettingsPanel getClientSettingsPanel() { return clientSettingsPanel; } public ClientSettingsPanel getClientSettingsPanel() { return clientSettingsPanel; }
public FriendsPanel getFriendsPanel() { return friendsPanel; } public FriendsPanel getFriendsPanel() { return friendsPanel; }
public SearchBarWidget getSearchBar() { return searchBar; } public SearchBarWidget getSearchBar() { return searchBar; }
public TabBarWidget getTabBar() { return tabBar; }
public ScrollBarWidget getScrollBar() { return scrollBar; } public ScrollBarWidget getScrollBar() { return scrollBar; }
public void setAnyFocused(boolean anyFocused) { public void setAnyFocused(boolean anyFocused) {
@@ -0,0 +1,27 @@
package me.retucio.sputnik.ui.screen;
import me.retucio.sputnik.util.I18nUtil;
public enum Tab {
MODULES("módulos"),
HUD("hud"),
PROFILES("perfiles"),
MACROS("macros");
private final String name;
Tab(String name) {
this.name = name;
}
public String getDisplayName() {
return I18nUtil.orElse("sputnik.ui.tab." + name().toLowerCase(), name);
}
@Override
public String toString() {
return name;
}
}
@@ -0,0 +1,247 @@
package me.retucio.sputnik.ui.screen.tabs;
import me.retucio.sputnik.macro.Macro;
import me.retucio.sputnik.macro.MacroManager;
import me.retucio.sputnik.ui.widgets.misc.TextFieldWidget;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.KeyUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public class MacrosTab {
private static final Minecraft mc = Minecraft.getInstance();
private static final int LIST_WIDTH = 180;
private static final int LINES_WIDTH = 280;
private static final int GAP = 12;
private static final int ROW_HEIGHT = 18;
private static final int TOP = 46;
private final TextFieldWidget nameField = new TextFieldWidget(
() -> "sputnik.ui.macros.nameplaceholder", 0, 0, LIST_WIDTH - 24, ROW_HEIGHT)
.maxLength(32)
.onSubmit(this::create);
private final TextFieldWidget lineField = new TextFieldWidget(
() -> "sputnik.ui.macros.lineplaceholder", 0, 0, LINES_WIDTH - 24, ROW_HEIGHT)
.maxLength(256)
.onSubmit(this::addLine);
@Nullable private Macro selected = null;
@Nullable private Macro listening = null;
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
List<Macro> macros = MacroManager.INSTANCE.getMacros();
if (selected != null && !macros.contains(selected)) selected = null;
int listX = listX();
gui.text(mc.font, Component.translatable("sputnik.ui.macros.title"),
listX, TOP - 14, Colors.mainColor.getRGB(), true);
nameField.setX(listX);
nameField.setY(TOP);
nameField.render(gui, mouseX, mouseY, delta);
int addX = listX + LIST_WIDTH - 20;
gui.fill(addX, TOP, addX + 20, TOP + ROW_HEIGHT,
isAddHovered(mouseX, mouseY) ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.text(mc.font, "+", addX + 8, TOP + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
drawHint(gui, Component.translatable("sputnik.ui.macros.instructions"), listX, LIST_WIDTH);
int rowY = listTop();
if (macros.isEmpty())
gui.text(mc.font, Component.translatable("sputnik.ui.macros.empty"),
listX, rowY + 4, Colors.instructionsTextColor.getRGB(), true);
for (Macro macro : macros) {
boolean hovered = isHovered(listX, rowY, LIST_WIDTH, mouseX, mouseY);
int color = macro == selected ? Colors.mainColor.getRGB()
: (hovered ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.fill(listX, rowY, listX + LIST_WIDTH, rowY + ROW_HEIGHT, color);
String key = macro == listening
? Component.translatable("sputnik.ui.macros.presskey").getString()
: KeyUtil.getKeyName(macro.getKey());
gui.text(mc.font, trim(macro.getName(), LIST_WIDTH - mc.font.width(key) - 15), listX + 5,
rowY + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
gui.text(mc.font, key, listX + LIST_WIDTH - mc.font.width(key) - 5,
rowY + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, Colors.instructionsTextColor.getRGB(), true);
rowY += ROW_HEIGHT + 2;
}
renderLines(gui, mouseX, mouseY, delta);
}
private void renderLines(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
if (selected == null) return;
int x = linesX();
gui.text(mc.font, selected.getName(), x, TOP - 14, Colors.mainColor.getRGB(), true);
lineField.setX(x);
lineField.setY(TOP);
lineField.render(gui, mouseX, mouseY, delta);
int addX = x + LINES_WIDTH - 20;
gui.fill(addX, TOP, addX + 20, TOP + ROW_HEIGHT,
isAddLineHovered(mouseX, mouseY) ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.text(mc.font, "+", addX + 8, TOP + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
drawHint(gui, Component.translatable("sputnik.ui.macros.lineinstructions"), x, LINES_WIDTH);
int rowY = listTop();
for (String line : selected.getLines()) {
boolean hovered = isHovered(x, rowY, LINES_WIDTH, mouseX, mouseY);
gui.fill(x, rowY, x + LINES_WIDTH, rowY + ROW_HEIGHT,
hovered ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.text(mc.font, trim(line, LINES_WIDTH - 10), x + 5,
rowY + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
rowY += ROW_HEIGHT + 2;
}
}
public void mouseClicked(int mouseX, int mouseY, int button) {
nameField.mouseClicked(mouseX, mouseY, button);
lineField.mouseClicked(mouseX, mouseY, button);
if (button == 0 && isAddHovered(mouseX, mouseY)) {
create(nameField.getText().trim());
nameField.clear();
return;
}
if (button == 0 && selected != null && isAddLineHovered(mouseX, mouseY)) {
addLine(lineField.getText().trim());
lineField.clear();
return;
}
int rowY = listTop();
for (Macro macro : new ArrayList<>(MacroManager.INSTANCE.getMacros())) {
if (isHovered(listX(), rowY, LIST_WIDTH, mouseX, mouseY)) {
onMacroClicked(macro, button);
return;
}
rowY += ROW_HEIGHT + 2;
}
if (selected == null) return;
rowY = listTop();
for (String line : new ArrayList<>(selected.getLines())) {
if (isHovered(linesX(), rowY, LINES_WIDTH, mouseX, mouseY)) {
if (button == 1 && KeyUtil.isShiftDown()) {
selected.getLines().remove(line);
MacroManager.INSTANCE.save();
}
return;
}
rowY += ROW_HEIGHT + 2;
}
}
private void onMacroClicked(Macro macro, int button) {
switch (button) {
case 0 -> selected = macro;
case 1 -> {
if (KeyUtil.isShiftDown()) {
MacroManager.INSTANCE.remove(macro);
if (selected == macro) selected = null;
if (listening == macro) listening = null;
} else {
listening = macro;
}
}
}
}
private void create(String name) {
if (name.isEmpty()) return;
Macro macro = new Macro(name);
if (MacroManager.INSTANCE.add(macro)) selected = macro;
}
private void addLine(String line) {
if (selected == null || line.isEmpty()) return;
selected.getLines().add(line);
MacroManager.INSTANCE.save();
}
public void onKey(int key, int action) {
if (listening != null && action != GLFW.GLFW_RELEASE) {
listening.setKey(key == GLFW.GLFW_KEY_ESCAPE ? GLFW.GLFW_KEY_UNKNOWN : key);
listening = null;
MacroManager.INSTANCE.save();
return;
}
nameField.onKey(key, action);
lineField.onKey(key, action);
}
public boolean isCapturingInput() {
return listening != null || nameField.isFocused() || lineField.isFocused();
}
private static int listX() {
return (mc.getWindow().getGuiScaledWidth() - (LIST_WIDTH + GAP + LINES_WIDTH)) / 2;
}
private static int linesX() {
return listX() + LIST_WIDTH + GAP;
}
private static void drawHint(GuiGraphicsExtractor gui, Component hint, int x, int width) {
int y = hintTop();
for (FormattedCharSequence line : mc.font.split(hint, width)) {
gui.text(mc.font, line, x, y, Colors.instructionsTextColor.getRGB(), true);
y += mc.font.lineHeight;
}
}
private static String trim(String text, int maxWidth) {
if (mc.font.width(text) <= maxWidth) return text;
return mc.font.plainSubstrByWidth(text, maxWidth - mc.font.width("...")) + "...";
}
private static int hintTop() {
return TOP + ROW_HEIGHT + 6;
}
// las dos columnas arrancan bajo la pista más alta, para que ninguna se le meta encima
private static int listTop() {
int lines = Math.max(
mc.font.split(Component.translatable("sputnik.ui.macros.instructions"), LIST_WIDTH).size(),
mc.font.split(Component.translatable("sputnik.ui.macros.lineinstructions"), LINES_WIDTH).size());
return hintTop() + lines * mc.font.lineHeight + 6;
}
private static boolean isHovered(int x, int rowY, int width, int mouseX, int mouseY) {
return mouseX >= x && mouseX < x + width && mouseY >= rowY && mouseY < rowY + ROW_HEIGHT;
}
private static boolean isAddHovered(int mouseX, int mouseY) {
int addX = listX() + LIST_WIDTH - 20;
return mouseX >= addX && mouseX < addX + 20 && mouseY >= TOP && mouseY < TOP + ROW_HEIGHT;
}
private static boolean isAddLineHovered(int mouseX, int mouseY) {
int addX = linesX() + LINES_WIDTH - 20;
return mouseX >= addX && mouseX < addX + 20 && mouseY >= TOP && mouseY < TOP + ROW_HEIGHT;
}
}
@@ -0,0 +1,158 @@
package me.retucio.sputnik.ui.screen.tabs;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.config.ProfileManager;
import me.retucio.sputnik.ui.hud.HudRenderer;
import me.retucio.sputnik.ui.widgets.misc.TextFieldWidget;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.I18nUtil;
import me.retucio.sputnik.util.KeyUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import java.util.List;
public class ProfilesTab {
private static final Minecraft mc = Minecraft.getInstance();
private static final int WIDTH = 280;
private static final int ROW_HEIGHT = 18;
private static final int TOP = 46;
private final TextFieldWidget nameField = new TextFieldWidget(
() -> "sputnik.ui.profiles.placeholder", 0, 0, WIDTH - 24, ROW_HEIGHT)
.maxLength(ProfileManager.MAX_NAME_LENGTH)
.onSubmit(this::create);
private List<String> profiles = List.of();
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
profiles = ProfileManager.INSTANCE.getProfileNames();
int x = (mc.getWindow().getGuiScaledWidth() - WIDTH) / 2;
gui.text(mc.font, Component.translatable("sputnik.ui.profiles.title"),
x, TOP - 14, Colors.mainColor.getRGB(), true);
nameField.setX(x);
nameField.setY(TOP);
nameField.render(gui, mouseX, mouseY, delta);
int addX = x + WIDTH - 20;
gui.fill(addX, TOP, addX + 20, TOP + ROW_HEIGHT,
isAddHovered(mouseX, mouseY) ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.text(mc.font, "+", addX + 8, TOP + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
int hintY = TOP + ROW_HEIGHT + 6;
for (FormattedCharSequence line : mc.font.split(
Component.translatable("sputnik.ui.profiles.instructions"), WIDTH)) {
gui.text(mc.font, line, x, hintY, Colors.instructionsTextColor.getRGB(), true);
hintY += mc.font.lineHeight;
}
int rowY = listTop();
if (profiles.isEmpty()) {
gui.text(mc.font, Component.translatable("sputnik.ui.profiles.empty"),
x, rowY + 4, Colors.instructionsTextColor.getRGB(), true);
return;
}
String current = ProfileManager.INSTANCE.getCurrent();
for (String profile : profiles) {
boolean hovered = isRowHovered(x, rowY, mouseX, mouseY);
gui.fill(x, rowY, x + WIDTH, rowY + ROW_HEIGHT,
hovered ? Colors.buttonColor.brighter().getRGB() : Colors.buttonColor.getRGB());
gui.text(mc.font, profile, x + 5, rowY + ROW_HEIGHT / 2 - mc.font.lineHeight / 2, -1, true);
if (profile.equals(current))
gui.text(mc.font, "●", x + WIDTH - 12, rowY + ROW_HEIGHT / 2 - mc.font.lineHeight / 2,
Colors.mainColor.getRGB(), true);
rowY += ROW_HEIGHT + 2;
}
}
public void mouseClicked(int mouseX, int mouseY, int button) {
nameField.mouseClicked(mouseX, mouseY, button);
if (button == 0 && isAddHovered(mouseX, mouseY)) {
create(nameField.getText().trim());
nameField.clear();
return;
}
int x = (mc.getWindow().getGuiScaledWidth() - WIDTH) / 2;
int rowY = listTop();
for (String profile : profiles) {
if (isRowHovered(x, rowY, mouseX, mouseY)) {
onRowClicked(profile, button);
return;
}
rowY += ROW_HEIGHT + 2;
}
}
private void onRowClicked(String profile, int button) {
if (button == 0) {
if (ProfileManager.INSTANCE.load(profile))
notify("sputnik.ui.profiles.loaded", profile);
return;
}
if (button != 1) return;
if (KeyUtil.isShiftDown()) {
if (ProfileManager.INSTANCE.delete(profile))
notify("sputnik.ui.profiles.deleted", profile);
} else if (ProfileManager.INSTANCE.save(profile)) {
ConfigManager.INSTANCE.save();
notify("sputnik.ui.profiles.saved", profile);
}
}
private void create(String name) {
if (!ProfileManager.isValidName(name)) {
notify("sputnik.ui.profiles.invalidname", name);
return;
}
if (ProfileManager.INSTANCE.save(name)) {
ConfigManager.INSTANCE.save();
notify("sputnik.ui.profiles.saved", name);
}
}
private static void notify(String key, String profile) {
HudRenderer.INSTANCE.pushNotification(
I18nUtil.orElse("sputnik.ui.profiles.title", "perfiles"),
I18nUtil.orElse(key, key, profile));
}
public void onKey(int key, int action) {
nameField.onKey(key, action);
}
public boolean isCapturingInput() {
return nameField.isFocused();
}
private static int listTop() {
int lines = mc.font.split(Component.translatable("sputnik.ui.profiles.instructions"), WIDTH).size();
return TOP + ROW_HEIGHT + 6 + lines * mc.font.lineHeight + 6;
}
private static boolean isRowHovered(int x, int rowY, int mouseX, int mouseY) {
return mouseX >= x && mouseX < x + WIDTH && mouseY >= rowY && mouseY < rowY + ROW_HEIGHT;
}
private static boolean isAddHovered(int mouseX, int mouseY) {
int addX = (mc.getWindow().getGuiScaledWidth() - WIDTH) / 2 + WIDTH - 20;
return mouseX >= addX && mouseX < addX + 20 && mouseY >= TOP && mouseY < TOP + ROW_HEIGHT;
}
}
@@ -4,11 +4,16 @@ import net.minecraft.client.gui.GuiGraphicsExtractor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
public abstract class Panel<B extends Button> extends Widget { public abstract class Panel<B extends Button> extends Widget {
protected String title; // el título se resuelve al pintarlo, no al construir el marco: los marcos se crean en
// la inicialización del mod, cuando el idioma todavía no ha cargado, así que guardar el
// texto ya traducido dejaba los títulos en español para siempre (y no cambiaban aunque
// se cambiara el idioma del juego en marcha)
private Supplier<String> title;
protected final List<B> buttons = new ArrayList<>(); protected final List<B> buttons = new ArrayList<>();
protected List<B> visibleButtons = new ArrayList<>(); protected List<B> visibleButtons = new ArrayList<>();
@@ -20,10 +25,22 @@ public abstract class Panel<B extends Button> extends Widget {
protected boolean dragging; protected boolean dragging;
public Panel(String title, int x, int y, int w, int h) { public Panel(String title, int x, int y, int w, int h) {
this(() -> title, x, y, w, h);
}
public Panel(Supplier<String> title, int x, int y, int w, int h) {
super(x, y, w, h); super(x, y, w, h);
this.title = title; this.title = title;
} }
public String getTitle() {
return title.get();
}
protected void setTitle(Supplier<String> title) {
this.title = title;
}
public void drawTooltips(GuiGraphicsExtractor gui, int mouseX, int mouseY) { public void drawTooltips(GuiGraphicsExtractor gui, int mouseX, int mouseY) {
for (Button button : visibleButtons) button.drawTooltip(gui, mouseX, mouseY); for (Button button : visibleButtons) button.drawTooltip(gui, mouseX, mouseY);
} }
@@ -0,0 +1,90 @@
package me.retucio.sputnik.ui.widgets.misc;
import me.retucio.sputnik.ui.screen.Tab;
import me.retucio.sputnik.ui.widgets.Widget;
import me.retucio.sputnik.util.Colors;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import java.util.function.Consumer;
public class TabBarWidget extends Widget {
private static final int PADDING = 14;
private Tab selected = Tab.MODULES;
private final Consumer<Tab> onLeave;
public TabBarWidget(Consumer<Tab> onLeave) {
super(0, 0, 0, 20);
this.onLeave = onLeave;
}
@Override
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
w = totalWidth();
x = (mc.getWindow().getGuiScaledWidth() - w) / 2;
y = 0;
gui.fill(x, 0, x + w, h, Colors.panelBgColor.getRGB());
int tabX = x;
for (Tab tab : Tab.values()) {
int tabW = widthOf(tab);
boolean hovered = isTabHovered(tabX, tabW, mouseX, mouseY);
if (tab == selected) gui.fill(tabX, 0, tabX + tabW, h, Colors.mainColor.getRGB());
else if (hovered) gui.fill(tabX, 0, tabX + tabW, h, Colors.buttonColor.brighter().getRGB());
gui.text(mc.font, tab.getDisplayName(),
tabX + (tabW - mc.font.width(tab.getDisplayName())) / 2,
h / 2 - mc.font.lineHeight / 2,
-1, true);
tabX += tabW;
}
}
// devuelve si el clic era para la barra, para que no llegue también al contenido
public boolean handleClick(int mouseX, int mouseY, int button) {
if (mouseY > h || mouseX < x || mouseX > x + totalWidth()) return false;
if (button != 0) return true;
int tabX = x;
for (Tab tab : Tab.values()) {
int tabW = widthOf(tab);
if (isTabHovered(tabX, tabW, mouseX, mouseY)) {
select(tab);
return true;
}
tabX += tabW;
}
return true;
}
public void select(Tab tab) {
if (selected == tab) return;
Tab previous = selected;
selected = tab;
onLeave.accept(previous);
}
private boolean isTabHovered(int tabX, int tabW, int mouseX, int mouseY) {
return mouseX >= tabX && mouseX < tabX + tabW && mouseY >= 0 && mouseY < h;
}
private int widthOf(Tab tab) {
return mc.font.width(tab.getDisplayName()) + PADDING * 2;
}
private int totalWidth() {
int total = 0;
for (Tab tab : Tab.values()) total += widthOf(tab);
return total;
}
public Tab getSelected() {
return selected;
}
}
@@ -0,0 +1,118 @@
package me.retucio.sputnik.ui.widgets.misc;
import me.retucio.sputnik.ui.widgets.Widget;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.KeyUtil;
import me.retucio.sputnik.util.MiscUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.awt.Color;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class TextFieldWidget extends Widget {
private final StringBuilder buffer = new StringBuilder();
private final Supplier<String> placeholder;
private boolean focused;
private int maxLength = 64;
private Consumer<String> onSubmit;
public TextFieldWidget(Supplier<String> placeholder, int x, int y, int w, int h) {
super(x, y, w, h);
this.placeholder = placeholder;
}
public TextFieldWidget maxLength(int maxLength) {
this.maxLength = maxLength;
return this;
}
public TextFieldWidget onSubmit(Consumer<String> onSubmit) {
this.onSubmit = onSubmit;
return this;
}
@Override
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
Color bg = focused
? Colors.buttonColor.darker()
: (isHovered(mouseX, mouseY) ? Colors.buttonColor.brighter() : Colors.buttonColor);
gui.fill(x, y, x + w, y + h, bg.getRGB());
boolean empty = buffer.isEmpty();
Component label = (!focused && empty)
? Component.literal(ChatFormatting.ITALIC.toString()).append(Component.translatable(placeholderKey()))
: Component.literal(focused ? buffer + "_" : buffer.toString());
gui.text(mc.font, label,
x + 5, y + h / 2 - mc.font.lineHeight / 2,
(empty && !focused) ? Color.LIGHT_GRAY.getRGB() : -1, true);
}
private String placeholderKey() {
return placeholder.get();
}
@Override
public void mouseClicked(int mouseX, int mouseY, int button) {
focused = button == 0 && isHovered(mouseX, mouseY);
}
@Override
public void mouseReleased(int mouseX, int mouseY, int button) {}
@Override
public void onKey(int key, int action) {
if (!focused || action == GLFW.GLFW_RELEASE) return;
if (key == GLFW.GLFW_KEY_V && KeyUtil.isKeyDown(GLFW.GLFW_KEY_LEFT_CONTROL)) {
buffer.append(MiscUtil.getPasteContent(maxLength - buffer.length()));
return;
}
switch (key) {
case GLFW.GLFW_KEY_ENTER -> submit();
case GLFW.GLFW_KEY_ESCAPE -> {
buffer.setLength(0);
focused = false;
}
case GLFW.GLFW_KEY_BACKSPACE -> MiscUtil.backspace(buffer);
case GLFW.GLFW_KEY_SPACE -> charTyped(' ');
default -> {
String c = KeyUtil.getKeyName(key);
if (c.length() == 1)
charTyped(KeyUtil.isShiftDown() ? KeyUtil.shiftKey(c).charAt(0) : c.toLowerCase().charAt(0));
}
}
}
private void submit() {
String text = buffer.toString().trim();
if (!text.isEmpty() && onSubmit != null) onSubmit.accept(text);
buffer.setLength(0);
focused = false;
}
private void charTyped(char c) {
if (buffer.length() >= maxLength) return;
buffer.append(c);
if (mc.font.width(buffer + "_") > w - 10) MiscUtil.backspace(buffer);
}
@Override
public boolean isHovered(int mouseX, int mouseY) {
return mouseX > x && mouseX < x + w && mouseY > y && mouseY < y + h;
}
public String getText() { return buffer.toString(); }
public void clear() { buffer.setLength(0); }
public boolean isFocused() { return focused; }
public void setFocused(boolean focused) { this.focused = focused; }
}
@@ -4,12 +4,13 @@ import me.retucio.sputnik.ui.widgets.Button;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.util.function.Supplier;
public abstract class CloseablePanel<B extends Button> extends Panel<B> { public abstract class CloseablePanel<B extends Button> extends Panel<B> {
protected static final int PADDING = 4; protected static final int PADDING = 4;
public CloseablePanel(String title, int x, int y, int w, int h) { public CloseablePanel(Supplier<String> title, int x, int y, int w, int h) {
super(title, x, y, w, h); super(title, x, y, w, h);
} }
@@ -4,11 +4,12 @@ import me.retucio.sputnik.ui.widgets.Button;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.util.function.Supplier;
public abstract class ExtendablePanel<B extends Button> extends Panel<B> { public abstract class ExtendablePanel<B extends Button> extends Panel<B> {
protected boolean extended; protected boolean extended;
public ExtendablePanel(String title, int x, int y, int w, int h) { public ExtendablePanel(Supplier<String> title, int x, int y, int w, int h) {
super(title, x, y, w, h); super(title, x, y, w, h);
this.extended = true; this.extended = true;
} }
@@ -9,6 +9,7 @@ import me.retucio.sputnik.friend.FriendManager;
import me.retucio.sputnik.ui.screen.ClickGui; import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.buttons.FriendButton; import me.retucio.sputnik.ui.widgets.buttons.FriendButton;
import me.retucio.sputnik.util.Colors; import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@@ -20,7 +21,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
private int offset; private int offset;
public FriendsPanel(int x, int y, int w, int h) { public FriendsPanel(int x, int y, int w, int h) {
super("amigos", x, y, w, h); super(() -> I18nUtil.orElse("sputnik.ui.friends.title", "amigos"), x, y, w, h);
dragging = false; dragging = false;
Sputnik.EVENT_BUS.subscribe(this); Sputnik.EVENT_BUS.subscribe(this);
@@ -34,7 +35,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
@Override @Override
protected void updateWidth() { protected void updateWidth() {
int maxWidth = mc.font.width(title) + mc.font.width(" +"); int maxWidth = mc.font.width(getTitle()) + mc.font.width(" +");
for (FriendButton button : buttons) { for (FriendButton button : buttons) {
int textWidth = mc.font.width(button.getFriend().getName()); int textWidth = mc.font.width(button.getFriend().getName());
maxWidth = Math.max(maxWidth, textWidth); maxWidth = Math.max(maxWidth, textWidth);
@@ -47,7 +48,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
updateWidth(); updateWidth();
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, Component.literal(title), gui.text(mc.font, Component.literal(getTitle()),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -21,7 +21,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
public final Category category; public final Category category;
public ModulePanel(Category category, int x, int y, int w, int h) { public ModulePanel(Category category, int x, int y, int w, int h) {
super(category.getDisplayName(), x, y, w, h); super(category::getDisplayName, x, y, w, h);
this.category = category; this.category = category;
dragging = false; dragging = false;
@@ -35,7 +35,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
@Override @Override
protected void updateWidth() { protected void updateWidth() {
int maxWidth = mc.font.width(title); int maxWidth = mc.font.width(getTitle());
for (ModuleButton button : buttons) { for (ModuleButton button : buttons) {
int textWidth = mc.font.width(button.getModule().getDisplayName()); int textWidth = mc.font.width(button.getModule().getDisplayName());
maxWidth = Math.max(maxWidth, textWidth); maxWidth = Math.max(maxWidth, textWidth);
@@ -48,7 +48,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
updateWidth(); updateWidth();
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, Component.literal(title), gui.text(mc.font, Component.literal(getTitle()),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -27,7 +27,8 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
public boolean drawHeader = true; public boolean drawHeader = true;
public SettingsPanel(Module module, int x, int y, int w, int h) { public SettingsPanel(Module module, int x, int y, int w, int h) {
super(I18nUtil.orElse("sputnik.ui.settingsof", "ajustes de " + module.getDisplayName(), module.getDisplayName()), x, y, w, h); super(() -> I18nUtil.orElse("sputnik.ui.settingsof",
"ajustes de " + module.getDisplayName(), module.getDisplayName()), x, y, w, h);
this.module = module; this.module = module;
this.settingGroups = module.getSgs(); this.settingGroups = module.getSgs();
@@ -59,7 +60,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
: -1; : -1;
gui.fill(x, renderY, x + w, renderY + h, headerColor); gui.fill(x, renderY, x + w, renderY + h, headerColor);
gui.text(mc.font, title, titleX, titleY, -1, true); gui.text(mc.font, getTitle(), titleX, titleY, -1, true);
drawCloseButton(gui, mouseX, mouseY, closeButtonColor); drawCloseButton(gui, mouseX, mouseY, closeButtonColor);
} }
@@ -69,7 +70,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
.toList(); .toList();
// descripción // descripción
List<String> descLines = wrapDescription(module.getDescription(), w - 2 * PADDING); List<String> descLines = wrapDescription(module.getDisplayDescription(), w - 2 * PADDING);
int lineSpacing = 2; int lineSpacing = 2;
int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * lineSpacing; int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * lineSpacing;
@@ -212,7 +213,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
@Override @Override
public void updateWidth() { public void updateWidth() {
// asegurarse de que todos los botones caben en el marco, haciendo que la anchura se ajuste al texto más largo // asegurarse de que todos los botones caben en el marco, haciendo que la anchura se ajuste al texto más largo
int maxWidth = mc.font.width(title); int maxWidth = mc.font.width(getTitle());
for (SettingButton<?> button : buttons) { for (SettingButton<?> button : buttons) {
String text = button.getSetting().getName(); String text = button.getSetting().getName();
@@ -246,7 +247,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
public int getGroupHeaderY(int groupIndex) { public int getGroupHeaderY(int groupIndex) {
int currentY = renderY + h + PADDING; int currentY = renderY + h + PADDING;
List<String> descLines = wrapDescription(module.getDescription(), w - 2 * PADDING); List<String> descLines = wrapDescription(module.getDisplayDescription(), w - 2 * PADDING);
int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * 2; int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * 2;
currentY += descHeight + 3 * PADDING; // descBoxBottom + padding currentY += descHeight + 3 * PADDING; // descBoxBottom + padding
@@ -6,6 +6,7 @@ import me.retucio.sputnik.module.modules.client.ClientSettingsModule;
import me.retucio.sputnik.ui.screen.ClickGui; import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.util.Colors; import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
public class ClientSettingsPanel extends SettingsPanel { public class ClientSettingsPanel extends SettingsPanel {
@@ -15,7 +16,7 @@ public class ClientSettingsPanel extends SettingsPanel {
public ClientSettingsPanel(int x, int y, int w, int h) { public ClientSettingsPanel(int x, int y, int w, int h) {
super(clientSettings, x, y, w, h); super(clientSettings, x, y, w, h);
this.title = "ajustes del mod"; setTitle(() -> I18nUtil.orElse("sputnik.ui.clientsettings.title", "ajustes del mod"));
this.drawHeader = false; // disable the original header this.drawHeader = false; // disable the original header
} }
@@ -25,7 +26,7 @@ public class ClientSettingsPanel extends SettingsPanel {
// Draw our own custom header (no close button, with +/- // Draw our own custom header (no close button, with +/-
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, title, gui.text(mc.font, getTitle(),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -1,5 +1,6 @@
package me.retucio.sputnik.util; package me.retucio.sputnik.util;
import com.github.retucio.neutrino.EventListener;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import me.retucio.sputnik.Sputnik; import me.retucio.sputnik.Sputnik;
import me.retucio.sputnik.command.CommandManager; import me.retucio.sputnik.command.CommandManager;
@@ -15,6 +16,7 @@ public class ChatUtil {
private static final Minecraft mc = Minecraft.getInstance(); private static final Minecraft mc = Minecraft.getInstance();
private static String prefix = getDefaultPrefix(); private static String prefix = getDefaultPrefix();
@EventListener
public static void onLoadModuleManager(LoadModuleManagerEvent event) { public static void onLoadModuleManager(LoadModuleManagerEvent event) {
prefix = Colors.getFormatting(Colors.mainColor) + "[" + ClientSettingsPanel.clientSettings.chatName.getValue() + "] "; prefix = Colors.getFormatting(Colors.mainColor) + "[" + ClientSettingsPanel.clientSettings.chatName.getValue() + "] ";
} }
@@ -3,7 +3,6 @@ package me.retucio.sputnik.util;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.projectile.ProjectileUtil; import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.ClipContext;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.EntityHitResult;
@@ -89,25 +88,6 @@ public class EntityUtil {
return mc.player.getXRot() + Mth.wrapDegrees((float) -Math.toDegrees(Math.atan2(diffY, diffXZ)) - mc.player.getXRot()); return mc.player.getXRot() + Mth.wrapDegrees((float) -Math.toDegrees(Math.atan2(diffY, diffXZ)) - mc.player.getXRot());
} }
public static void lookAt(Vec3 pos) {
lookAtClient(pos);
lookAtServer(pos);
}
public static void lookAtClient(Vec3 pos) {
mc.player.setYRot((float) getYaw(pos));
mc.player.setXRot((float) getPitch(pos));
}
public static void lookAtServer(Vec3 pos) {
mc.getConnection().send(new ServerboundMovePlayerPacket.Rot(
(float) getYaw(pos),
(float) getPitch(pos),
mc.player.onGround(),
mc.player.horizontalCollision
));
}
public static Vec3 getHorizontalVelocity(double bps) { public static Vec3 getHorizontalVelocity(double bps) {
float yaw = mc.player.getYRot(); float yaw = mc.player.getYRot();
@@ -47,6 +47,21 @@ public class I18nUtil {
return Language.getInstance().has(key) ? I18n.get(key, args) : fallback; return Language.getInstance().has(key) ? I18n.get(key, args) : fallback;
} }
// igual que orElse pero rellenando también los %s del texto de reserva.
// para lo que se registra durante el arranque (cargar la config., comprobar la versión...):
// ahí el idioma todavía no está cargado y Component#translatable escupía la clave pelada
public static String fmt(String key, String fallback, Object... args) {
if (Language.getInstance().has(key)) return I18n.get(key, args);
return args.length == 0 ? fallback : String.format(fallback, args);
}
// texto que daba el en_us de vanilla para una clave. sólo para leer configs. viejas:
// las listas guardaban el nombre ya traducido y Language, durante el arranque, es
// siempre el en_us de vanilla, así que lo guardado eran los nombres en inglés
public static String vanillaName(String key) {
return Language.DEFAULT_INSTANCE.getOrDefault(key, key);
}
// nombre visible de una constante de enum. los EnumSetting se guardan por índice // nombre visible de una constante de enum. los EnumSetting se guardan por índice
// (ver ConfigManager#applyEnumSetting), así que su toString() es sólo texto y se traduce. // (ver ConfigManager#applyEnumSetting), así que su toString() es sólo texto y se traduce.
// la clave se deriva de la clase que la declara, así que dos enums con el mismo nombre // la clave se deriva de la clase que la declara, así que dos enums con el mismo nombre
@@ -3,17 +3,26 @@ package me.retucio.sputnik.util;
import net.minecraft.core.particles.ParticleType; import net.minecraft.core.particles.ParticleType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component; import net.minecraft.network.ProtocolInfo;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.PacketType;
import net.minecraft.network.protocol.configuration.ConfigurationProtocols;
import net.minecraft.network.protocol.game.GameProtocols;
import net.minecraft.network.protocol.handshake.HandshakeProtocols;
import net.minecraft.network.protocol.login.LoginProtocols;
import net.minecraft.network.protocol.status.StatusProtocols;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import org.jspecify.annotations.NonNull;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import static me.retucio.sputnik.util.Colors.*; import static me.retucio.sputnik.util.Colors.*;
@@ -37,41 +46,39 @@ public class Lists {
public static final List<MenuType<?>> screenList = BuiltInRegistries.MENU.stream().toList(); public static final List<MenuType<?>> screenList = BuiltInRegistries.MENU.stream().toList();
public static Map<MenuType<?>, String> screenNames; public static Map<MenuType<?>, String> screenNames;
public static List<PacketType<? extends @NonNull Packet<?>>> c2sPacketList = new ArrayList<>();
public static Map<PacketType<? extends @NonNull Packet<?>>, String> c2sPacketNames = new HashMap<>();
public static List<PacketType<? extends @NonNull Packet<?>>> s2cPacketList = new ArrayList<>();
public static Map<PacketType<? extends @NonNull Packet<?>>, String> s2cPacketNames = new HashMap<>();
public static final List<Color> colorList = new ArrayList<>(); public static final List<Color> colorList = new ArrayList<>();
public static final List<String> fontList = new ArrayList<>(); public static final List<String> fontList = new ArrayList<>();
// intentar traducir nombres y fallar miserablemente
public static void init() { public static void init() {
entityNames = getMapOfLists(entityList, entityNames = getMapOfLists(entityList,
entityList.stream().map(entity -> Component.translatable( entityList.stream().map(EntityType::getDescriptionId).toList());
entity.getDescriptionId()
).getString()).toList());
particleNames = getMapOfLists(particleList, particleNames = getMapOfLists(particleList,
particleList.stream().map(particle -> Component.translatable( particleList.stream().map(particle ->
BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toLanguageKey() BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toLanguageKey()
).getString()).toList()); ).toList());
itemNames = getMapOfLists(itemList, itemNames = getMapOfLists(itemList,
itemList.stream().map(item -> Component.translatable( itemList.stream().map(Item::getDescriptionId).toList());
item.getDescriptionId()
).getString()).toList());
blockNames = getMapOfLists(blockList, blockNames = getMapOfLists(blockList,
blockList.stream().map(block -> Component.translatable( blockList.stream().map(Block::getDescriptionId).toList());
block.getDescriptionId()
).getString()).toList());
soundNames = getMapOfLists(soundList, soundNames = getMapOfLists(soundList,
soundList.stream().map(sound -> Component.translatable( soundList.stream().map(sound ->
sound.location().toShortLanguageKey() sound.location().toShortLanguageKey()
).getString()).toList()); ).toList());
soundNames.replace(SoundEvents.EMPTY, "NINGUNO"); soundNames.replace(SoundEvents.EMPTY, "NINGUNO");
screenNames = getMapOfLists(screenList, screenNames = getMapOfLists(screenList,
screenList.stream().map(screen -> Component.translatable( screenList.stream().map(screen ->
BuiltInRegistries.MENU.getKey(screen).toLanguageKey() BuiltInRegistries.MENU.getKey(screen).toLanguageKey()
).getString()).toList()); ).toList());
colorList.addAll(Arrays.asList( colorList.addAll(Arrays.asList(
RED, ORANGE, YELLOW, LIME, GREEN, CYAN, RED, ORANGE, YELLOW, LIME, GREEN, CYAN,
@@ -84,6 +91,31 @@ public class Lists {
"deja-vu-sans-mono", "new-times-roman", "roboto", "segoe-ui", "deja-vu-sans-mono", "new-times-roman", "roboto", "segoe-ui",
"ubuntu" "ubuntu"
)); ));
Stream.of(
StatusProtocols.CLIENTBOUND_TEMPLATE,
LoginProtocols.CLIENTBOUND_TEMPLATE,
ConfigurationProtocols.CLIENTBOUND_TEMPLATE,
GameProtocols.CLIENTBOUND_TEMPLATE
)
.map(ProtocolInfo.DetailsProvider::details)
.forEach(details -> details.listPackets((type, _) -> {
s2cPacketList.add(type);
s2cPacketNames.put(type, type.id().toShortString());
}));
Stream.of(
HandshakeProtocols.SERVERBOUND_TEMPLATE,
StatusProtocols.SERVERBOUND_TEMPLATE,
LoginProtocols.SERVERBOUND_TEMPLATE,
ConfigurationProtocols.SERVERBOUND_TEMPLATE,
GameProtocols.SERVERBOUND_TEMPLATE
)
.map(ProtocolInfo.DetailsProvider::details)
.forEach(details -> details.listPackets((type, _) -> {
c2sPacketList.add(type);
c2sPacketNames.put(type, type.id().toShortString());
}));
} }
public static <T> Map<T, Boolean> allTrue(List<T> options) { public static <T> Map<T, Boolean> allTrue(List<T> options) {
@@ -1,6 +1,7 @@
package me.retucio.sputnik.util; package me.retucio.sputnik.util;
import com.github.retucio.neutrino.EventListener; import com.github.retucio.neutrino.EventListener;
import me.retucio.sputnik.Sputnik;
import me.retucio.sputnik.event.TickEvent; import me.retucio.sputnik.event.TickEvent;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.client.Hud; 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.client.server.IntegratedServer;
import net.minecraft.core.Vec3i; import net.minecraft.core.Vec3i;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import org.apache.commons.io.IOUtils;
import org.joml.Vector3d; import org.joml.Vector3d;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import java.io.*;
import java.time.Instant; import java.time.Instant;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneOffset; 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) { public static Vec3 vec3dOf(Vector3f v) {
return new Vec3(v.x, v.y, v.z); return new Vec3(v.x, v.y, v.z);
} }
@@ -107,9 +107,9 @@ public class NetworkUtil {
mc.player.setShiftKeyDown(wasSneaking); mc.player.setShiftKeyDown(wasSneaking);
} }
// colocar un bloque // colocar un bloque, con rotate a null para no rotar
public static boolean placeBlock(BlockPos blockPos, InteractionHand hand, int slot, public static boolean placeBlock(BlockPos blockPos, InteractionHand hand, int slot,
boolean rotateClient, boolean rotateServer, boolean swingHand, RotationUtil.Mode rotate, int rotationPriority, boolean swingHand,
boolean checkEntities, boolean swapBack) boolean checkEntities, boolean swapBack)
{ {
if (slot < 0 || slot > 8 || mc.player == null) return false; if (slot < 0 || slot > 8 || mc.player == null) return false;
@@ -135,9 +135,7 @@ public class NetworkUtil {
BlockHitResult bhr = new BlockHitResult(hitPos, side.getOpposite(), neighbour, false); BlockHitResult bhr = new BlockHitResult(hitPos, side.getOpposite(), neighbour, false);
int prevSlot = mc.player.getInventory().getSelectedSlot(); int prevSlot = mc.player.getInventory().getSelectedSlot();
if (rotateClient) EntityUtil.lookAtClient(hitPos); Runnable place = () -> {
if (rotateServer) EntityUtil.lookAtServer(hitPos);
mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot));
interactBlock(bhr, hand, swingHand); interactBlock(bhr, hand, swingHand);
@@ -146,6 +144,10 @@ public class NetworkUtil {
mc.player.getInventory().setSelectedSlot(prevSlot); mc.player.getInventory().setSelectedSlot(prevSlot);
mc.getConnection().send(new ServerboundSetCarriedItemPacket(prevSlot)); mc.getConnection().send(new ServerboundSetCarriedItemPacket(prevSlot));
} }
};
if (rotate == null) place.run();
else RotationUtil.lookAt(rotationPriority, hitPos, 0, rotate, place);
return true; return true;
} }
@@ -0,0 +1,490 @@
package me.retucio.sputnik.util;
import com.github.retucio.neutrino.Event;
import com.github.retucio.neutrino.EventListener;
import com.github.retucio.neutrino.EventPriority;
import me.retucio.sputnik.event.TickEvent;
import me.retucio.sputnik.event.network.DisconnectEvent;
import me.retucio.sputnik.event.network.JoinWorldEvent;
import me.retucio.sputnik.event.network.PacketEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.game.GameProtocols;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.network.protocol.status.StatusProtocols;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import static me.retucio.sputnik.Sputnik.mc;
/**
* arbitra las rotaciones entre módulos.
* <p>
* en vez de que cada módulo escriba directamente en {@code player.setYRot()} y se peleen entre ellos,
* cada uno pide una rotación con una prioridad y aquí se decide cuál gana el tick.
*
* @author retucio
*/
public class RotationUtil {
// prioridades sugeridas: gana la más alta del tick
public static final int LOWEST = EventPriority.LOWEST;
public static final int LOW = EventPriority.LOW;
public static final int NORMAL = EventPriority.DEFAULT;
public static final int HIGH = EventPriority.HIGH;
public static final int HIGHEST = EventPriority.HIGHEST;
// tolerancia por defecto de hasReached(), en grados
private static final float DEFAULT_TOLERANCE = 1f;
/**
* CLIENT: mueve la cámara de verdad, el servidor recibe la rotación como cualquier otra
* <p>
* SILENT: la cámara no se mueve, solo se reescribe la rotación de los paquetes de movimiento
*/
public enum Mode {
CLIENT,
SILENT
}
// ajustes globales, pensados para engancharlos a un módulo / a los ajustes del cliente
// grados máximos por tick, >= 180 es instantáneo
public static float speed = 180f;
// redondear los pasos a la precisión real del ratón
public static boolean gcdFix = true;
// ticks máximos para devolver la rotación del servidor a la real al soltar una rotación silenciosa
public static int restoreTicks = 3;
// estado
// rotación que ve el servidor
private static float yaw, pitch;
private static float prevYaw, prevPitch;
// mejor petición recibida desde el último tick
private static Request pending;
// petición que se está aplicando
private static Request active;
// ticks que le quedan a la petición activa sin que nadie vuelva a pedirla
private static int hold;
// devolviendo la rotación del servidor a la del jugador
private static boolean restoring;
private static int restoreLeft;
// este tick mandamos nosotros la rotación del servidor
private static boolean controlled;
// ya ha salido un paquete de movimiento este tick
private static boolean sent;
// lo que ejecutar cuando el servidor ya haya recibido la rotación de la petición activa
private static Runnable callback;
private record Request(int priority, float yaw, float pitch, int holdTicks, Mode mode, Runnable callback) {}
// peticiones
// pide una rotación para este tick
public static void request(int priority, float yaw, float pitch) {
request(priority, yaw, pitch, 0, Mode.SILENT, null);
}
public static void request(int priority, float yaw, float pitch, Mode mode) {
request(priority, yaw, pitch, 0, mode, null);
}
public static void request(int priority, float yaw, float pitch, Runnable callback) {
request(priority, yaw, pitch, 0, Mode.SILENT, callback);
}
public static void request(int priority, float yaw, float pitch, int holdTicks, Mode mode) {
request(priority, yaw, pitch, holdTicks, mode, null);
}
/**
* @param priority gana la petición más alta del tick, en empate gana la primera
* @param holdTicks ticks que se mantiene la rotación tras la última petición
* @param callback se ejecuta al final del tick en el que el servidor ya ha recibido la rotación,
* que es donde hay que interactuar para que el servidor nos vea mirando bien
*/
public static void request(int priority, float yaw, float pitch, int holdTicks, Mode mode, Runnable callback) {
if (mc.player == null) return;
if (pending != null && pending.priority() >= priority) return;
pending = new Request(priority, yaw, Float.isNaN(pitch) ? pitch : Mth.clamp(pitch, -90f, 90f), holdTicks, mode, callback);
}
// pide solo un eje, dejando el otro como esté
public static void requestYaw(int priority, float yaw, Mode mode) {
request(priority, yaw, Float.NaN, 0, mode, null);
}
public static void requestPitch(int priority, float pitch, Mode mode) {
request(priority, Float.NaN, pitch, 0, mode, null);
}
public static void lookAt(int priority, Vec3 pos) {
lookAt(priority, pos, 0, Mode.SILENT, null);
}
public static void lookAt(int priority, Vec3 pos, Mode mode) {
lookAt(priority, pos, 0, mode, null);
}
public static void lookAt(int priority, Vec3 pos, Runnable callback) {
lookAt(priority, pos, 0, Mode.SILENT, callback);
}
public static void lookAt(int priority, Vec3 pos, int holdTicks, Mode mode) {
lookAt(priority, pos, holdTicks, mode, null);
}
public static void lookAt(int priority, Vec3 pos, int holdTicks, Mode mode, Runnable callback) {
if (mc.player == null) return;
float[] rotations = getRotations(pos);
request(priority, rotations[0], rotations[1], holdTicks, mode, callback);
}
public static void lookAt(int priority, BlockPos pos) {
lookAt(priority, Vec3.atCenterOf(pos), 0, Mode.SILENT, null);
}
public static void lookAt(int priority, BlockPos pos, Runnable callback) {
lookAt(priority, Vec3.atCenterOf(pos), 0, Mode.SILENT, callback);
}
public static void lookAt(int priority, BlockPos pos, int holdTicks, Mode mode) {
lookAt(priority, Vec3.atCenterOf(pos), holdTicks, mode, null);
}
public static void lookAt(int priority, Entity entity, EntityUtil.Target target) {
lookAt(priority, entity, target, 0, Mode.SILENT, null);
}
public static void lookAt(int priority, Entity entity, EntityUtil.Target target, Runnable callback) {
lookAt(priority, entity, target, 0, Mode.SILENT, callback);
}
public static void lookAt(int priority, Entity entity, EntityUtil.Target target, int holdTicks, Mode mode) {
lookAt(priority, getTargetPos(entity, target), holdTicks, mode, null);
}
public static void lookAt(int priority, Entity entity, EntityUtil.Target target, int holdTicks, Mode mode, Runnable callback) {
lookAt(priority, getTargetPos(entity, target), holdTicks, mode, callback);
}
// suelta la rotación activa sin esperar a que caduque
public static void release() {
pending = null;
if (active != null && active.mode() == Mode.SILENT) startRestoring();
active = null;
callback = null;
hold = 0;
}
// estado
// rotación que ve el servidor: la falsa si hay una activa, si no la del jugador
public static float getYaw() {
return isOverriding() ? yaw : (mc.player == null ? 0f : mc.player.getYRot());
}
public static float getPitch() {
return isOverriding() ? pitch : (mc.player == null ? 0f : mc.player.getXRot());
}
// interpoladas, para renderizar
public static float getYaw(float tickDelta) {
return isOverriding() ? Mth.rotLerp(tickDelta, prevYaw, yaw) : getYaw();
}
public static float getPitch(float tickDelta) {
return isOverriding() ? Mth.lerp(tickDelta, prevPitch, pitch) : getPitch();
}
// hay una petición aplicándose
public static boolean isRotating() {
return active != null;
}
// la rotación del servidor no es la del jugador
public static boolean isOverriding() {
return controlled;
}
public static Mode getMode() {
return active == null ? null : active.mode();
}
// prioridad de la petición activa, para que un módulo sepa si le han ganado
public static int getPriority() {
return active == null ? Integer.MIN_VALUE : active.priority();
}
// verdadero si nadie con más prioridad está rotando, es decir, si la petición sería aceptada
public static boolean canRotate(int priority) {
return active == null || active.priority() <= priority;
}
// la rotación del servidor ya ha llegado a la que se pidió
public static boolean hasReached() {
return hasReached(DEFAULT_TOLERANCE);
}
public static boolean hasReached(float tolerance) {
if (active == null) return false;
return (Float.isNaN(active.yaw()) || Math.abs(Mth.degreesDifference(yaw, active.yaw())) <= tolerance)
&& (Float.isNaN(active.pitch()) || Math.abs(active.pitch() - pitch) <= tolerance);
}
// el servidor nos ve mirando a pos
public static boolean isAimingAt(Vec3 pos, float tolerance) {
float[] rotations = getRotations(pos);
return Math.abs(Mth.degreesDifference(getYaw(), rotations[0])) <= tolerance
&& Math.abs(rotations[1] - getPitch()) <= tolerance;
}
// helpers
// rotación necesaria para mirar de from a to, en {yaw, pitch}
public static float[] getRotations(Vec3 from, Vec3 to) {
double diffX = to.x() - from.x();
double diffY = to.y() - from.y();
double diffZ = to.z() - from.z();
double diffXZ = Math.sqrt(diffX * diffX + diffZ * diffZ);
return new float[] {
(float) (Math.toDegrees(Math.atan2(diffZ, diffX)) - 90d),
Mth.clamp((float) -Math.toDegrees(Math.atan2(diffY, diffXZ)), -90f, 90f)
};
}
// rotación necesaria para que el jugador mire a pos
public static float[] getRotations(Vec3 pos) {
return getRotations(mc.player.getEyePosition(), pos);
}
public static float[] getRotations(BlockPos pos) {
return getRotations(Vec3.atCenterOf(pos));
}
public static float[] getRotations(Entity entity, EntityUtil.Target target) {
return getRotations(getTargetPos(entity, target));
}
// vector de mirada según la rotación que ve el servidor
public static Vec3 getViewVector() {
return Vec3.directionFromRotation(getPitch(), getYaw());
}
// raycast desde los ojos del jugador con la rotación que ve el servidor
public static HitResult raycast(double range) {
Vec3 eyes = mc.player.getEyePosition();
Vec3 end = eyes.add(getViewVector().scale(range));
return mc.level.clip(new ClipContext(
eyes,
end,
ClipContext.Block.OUTLINE,
ClipContext.Fluid.NONE,
mc.player
));
}
private static Vec3 getTargetPos(Entity entity, EntityUtil.Target target) {
return switch (target) {
case HEAD -> new Vec3(entity.getX(), entity.getEyeY(), entity.getZ());
case BODY -> entity.getBoundingBox().getCenter();
case FEET -> entity.position();
};
}
// lógica
// en TickEvent.Pre, que va justo después de que los módulos tickeen y antes de que LocalPlayer
// mande su paquete de movimiento
@EventListener
public static void onTick(TickEvent.Pre event) {
if (mc.player == null || mc.level == null) {
reset();
return;
}
prevYaw = yaw;
prevPitch = pitch;
// caducar la petición activa si nadie con al menos su prioridad la ha renovado
if (active != null && (pending == null || pending.priority() < active.priority())) {
if (hold > 0) hold--;
else {
if (active.mode() == Mode.SILENT) startRestoring();
active = null;
callback = null;
}
}
// la petición nueva gana si no hay activa o si empata / supera su prioridad
if (pending != null && (active == null || pending.priority() >= active.priority())) {
active = pending;
hold = pending.holdTicks();
callback = pending.callback();
restoring = false;
}
pending = null;
controlled = false;
if (active != null) {
boolean freeYaw = Float.isNaN(active.yaw());
boolean freePitch = Float.isNaN(active.pitch());
yaw = step(yaw, freeYaw ? mc.player.getYRot() : active.yaw());
pitch = Mth.clamp(step(pitch, freePitch ? mc.player.getXRot() : active.pitch()), -90f, 90f);
if (active.mode() == Mode.CLIENT) {
if (!freeYaw) {
mc.player.setYRot(yaw);
mc.player.setYHeadRot(yaw);
}
if (!freePitch) mc.player.setXRot(pitch);
} else controlled = true;
} else if (restoring) {
float realYaw = mc.player.getYRot();
float realPitch = mc.player.getXRot();
if (yaw == realYaw && pitch == realPitch) {
// la rotación real ya salió el tick pasado, soltamos
restoring = false;
} else {
if (--restoreLeft <= 0) {
yaw = realYaw;
pitch = realPitch;
} else {
yaw = step(yaw, realYaw);
pitch = step(pitch, realPitch);
}
controlled = true;
}
}
if (!controlled) {
yaw = mc.player.getYRot();
pitch = mc.player.getXRot();
}
}
// prioridad mínima para ir después de los módulos que cancelan y reenvían estos paquetes
// (nofall, antihunger...), cuyo reemplazo vuelve a pasar por aquí
@EventListener(priority = EventPriority.LOWEST)
public static void onSendPacket(PacketEvent.Send event) {
if (!controlled || event.getStage() != Event.Stage.PRE || event.isCancelled()) return;
if (mc.player == null || mc.getConnection() == null) return;
if (!(event.getPacket() instanceof ServerboundMovePlayerPacket packet)) return;
sent = true;
// ya lleva la nuestra
if (packet.hasRotation() && packet.getYRot(0f) == yaw && packet.getXRot(0f) == pitch) return;
event.cancel();
NetworkUtil.sendPacketNoEvent(packet.hasPosition()
? new ServerboundMovePlayerPacket.PosRot(
packet.getX(mc.player.getX()),
packet.getY(mc.player.getY()),
packet.getZ(mc.player.getZ()),
yaw, pitch,
packet.isOnGround(),
packet.horizontalCollision())
: new ServerboundMovePlayerPacket.Rot(
yaw, pitch,
packet.isOnGround(),
packet.horizontalCollision()));
}
// si el jugador está quieto y sin girar no sale ningún paquete que reescribir, así que lo mandamos nosotros
@EventListener
public static void onTickPost(TickEvent.Post event) {
if (mc.player == null || mc.getConnection() == null) return;
if (controlled && !sent)
NetworkUtil.sendPacketNoEvent(new ServerboundMovePlayerPacket.Rot(
yaw,
pitch,
mc.player.onGround(),
mc.player.horizontalCollision
));
sent = false;
// aquí el paquete de movimiento del tick ya ha salido, así que el servidor nos ve mirando
// donde se pidió y es seguro interactuar
if (callback != null && hasReached()) {
Runnable toRun = callback;
callback = null;
toRun.run();
}
}
@EventListener
public static void onJoinWorld(JoinWorldEvent event) {
reset();
}
@EventListener
public static void onDisconnect(DisconnectEvent event) {
reset();
}
private static void startRestoring() {
restoring = true;
restoreLeft = Math.max(restoreTicks, 1);
}
private static void reset() {
pending = null;
active = null;
callback = null;
hold = 0;
restoring = false;
restoreLeft = 0;
controlled = false;
sent = false;
}
// acerca current a target sin pasarse de speed grados
private static float step(float current, float target) {
float delta = Mth.degreesDifference(current, target);
if (Math.abs(delta) <= speed) return target;
float stepped = current + Math.signum(delta) * speed;
return gcdFix ? applyGcd(current, stepped) : stepped;
}
// el ratón solo puede girar en múltiplos de esta cantidad, así que los pasos que no lo sean cantan
private static float applyGcd(float current, float target) {
double sensitivity = mc.options.sensitivity().get() * 0.6d + 0.2d;
double gcd = sensitivity * sensitivity * sensitivity * 8d * 0.15d;
return (float) (current + Math.round((target - current) / gcd) * gcd);
}
}
@@ -1,7 +1,9 @@
package me.retucio.sputnik.util.render; package me.retucio.sputnik.util.render;
import com.mojang.blaze3d.PrimitiveTopology; import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.DepthStencilState;
import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.platform.CompareOp;
import com.mojang.blaze3d.systems.GpuDevice; import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.DefaultVertexFormat;
@@ -28,12 +30,15 @@ public class Pipelines {
static List<RenderPipeline> PIPELINES = new ArrayList<>(); static List<RenderPipeline> PIPELINES = new ArrayList<>();
// 26.2 usa depth invertido (near = 1), así que el LEQUAL de siempre es GREATER_THAN_OR_EQUAL
private static final DepthStencilState NO_DEPTH = new DepthStencilState(CompareOp.ALWAYS_PASS, false);
private static final DepthStencilState DEPTH = new DepthStencilState(CompareOp.GREATER_THAN_OR_EQUAL, false);
static final RenderPipeline LINES_PIPELINE = add(RenderPipeline.builder(RenderPipelines.LINES_SNIPPET) static final RenderPipeline LINES_PIPELINE = add(RenderPipeline.builder(RenderPipelines.LINES_SNIPPET)
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH)
.withPrimitiveTopology(PrimitiveTopology.LINES) .withPrimitiveTopology(PrimitiveTopology.LINES)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -41,8 +46,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines_cull")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines_cull"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH)
.withPrimitiveTopology(PrimitiveTopology.LINES) .withPrimitiveTopology(PrimitiveTopology.LINES)
// .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withDepthStencilState(DEPTH)
// .withDepthWrite(false)
.withCull(true) .withCull(true)
.build()); .build());
@@ -50,9 +54,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR)
.withPrimitiveTopology(PrimitiveTopology.QUADS) .withPrimitiveTopology(PrimitiveTopology.QUADS)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withBlend(BlendFunction.TRANSLUCENT)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -60,9 +62,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads_cull")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads_cull"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR)
.withPrimitiveTopology(PrimitiveTopology.QUADS) .withPrimitiveTopology(PrimitiveTopology.QUADS)
// .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withDepthStencilState(DEPTH)
// .withBlend(BlendFunction.TRANSLUCENT)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -76,9 +76,7 @@ public class Pipelines {
// siguen usando uniforms con nombre (u_Texture, u_ProjectionMatrix) y seguramente // siguen usando uniforms con nombre (u_Texture, u_ProjectionMatrix) y seguramente
// haya que reescribirlos para que casen con las convenciones nuevas // haya que reescribirlos para que casen con las convenciones nuevas
.withBindGroupLayout(BindGroupLayouts.SAMPLER0) .withBindGroupLayout(BindGroupLayouts.SAMPLER0)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withDepthWrite(false)
// .withBlend(BlendFunction.TRANSLUCENT)
.withCull(false) .withCull(false)
.build()); .build());
@@ -5,24 +5,32 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexFormat;
import me.retucio.sputnik.Sputnik; import me.retucio.sputnik.Sputnik;
import net.minecraft.client.renderer.StagedVertexBuffer; import net.minecraft.client.renderer.StagedVertexBuffer;
import net.minecraft.client.renderer.rendertype.PreparedRenderType;
import net.minecraft.client.renderer.rendertype.RenderType; import net.minecraft.client.renderer.rendertype.RenderType;
import java.util.ArrayList;
import java.util.List;
/** emula el antiguo Tesselator + RenderType.draw(MeshData), que 26.2 se cargó /** emula el antiguo Tesselator + RenderType.draw(MeshData), que 26.2 se cargó
* <p> * <p>
* ahora los vértices se escriben en un StagedVertexBuffer: se pide un draw, se * el ciclo del StagedVertexBuffer es appendDraw -> upload -> drawFromBuffer -> endDraw,
* escriben los vértices, se sube todo a la gpu y se dibuja. el ciclo es * y hay que respetarlo entero: los vértices no llegan al Draw hasta que upload() cierra
* appendDraw -> upload -> drawFromBuffer -> endDraw; upload() peta si se llama * el BufferBuilder, y appendDraw() peta después de un upload(). de ahí que draw() sólo
* dos veces sin endDraw() de por medio. * encole y sea flush() quien suba y dibuje todo de una.
* <p> * <p>
* subimos y dibujamos en cada llamada (en vez de agrupar draws) para mantener el * un upload() por caja tampoco vale: cada uno se pilla un buffer de 256 KB del pool que
* mismo orden de dibujado que antes; es menos eficiente pero se comporta igual. * no vuelve hasta el final del frame.
*/ */
public class SputnikBuffers { public class SputnikBuffers {
private static StagedVertexBuffer buffer; private static StagedVertexBuffer buffer;
private static StagedVertexBuffer.Draw current; private static StagedVertexBuffer.Draw current;
private static final List<QueuedDraw> queue = new ArrayList<>();
private record QueuedDraw(PreparedRenderType type, StagedVertexBuffer.Draw draw) {}
// lazy: al cargar la clase todavía puede no haber device // lazy: al cargar la clase todavía puede no haber device
private static StagedVertexBuffer buffer() { private static StagedVertexBuffer buffer() {
if (buffer == null) { if (buffer == null) {
@@ -32,8 +40,6 @@ public class SputnikBuffers {
} }
public static VertexConsumer begin(VertexFormat format, PrimitiveTopology topology) { public static VertexConsumer begin(VertexFormat format, PrimitiveTopology topology) {
if (current != null) buffer().endDraw(); // por si quedó un draw a medias
current = buffer().appendDraw(format, topology); current = buffer().appendDraw(format, topology);
return buffer().getVertexBuilder(current); return buffer().getVertexBuilder(current);
} }
@@ -41,20 +47,33 @@ public class SputnikBuffers {
public static void draw(RenderType type) { public static void draw(RenderType type) {
if (current == null) return; if (current == null) return;
if (!current.isEmpty()) { queue.add(new QueuedDraw(type.prepare(), current));
buffer().upload(); current = null;
type.prepare().drawFromBuffer(buffer().getExecuteInfo(current));
} }
buffer().endDraw(); /** sube y dibuja lo encolado; hay que llamarlo antes de que el juego pinte el hud */
public static void flush() {
if (buffer == null) return;
current = null; current = null;
if (queue.isEmpty()) return;
buffer.upload();
for (QueuedDraw queued : queue) {
StagedVertexBuffer.ExecuteInfo info = buffer.getExecuteInfo(queued.draw());
if (info != null) queued.type().drawFromBuffer(info);
}
queue.clear();
buffer.endDraw();
} }
// devuelve los buffers al pool; una vez por frame // devuelve los buffers al pool; una vez por frame
public static void endFrame() { public static void endFrame() {
if (buffer == null) return; if (buffer == null) return;
current = null; flush();
buffer.endFrame(); buffer.endFrame();
} }
} }
@@ -29,8 +29,23 @@
"sputnik.commands.friend.removed": "%s is not your friend anymore :(", "sputnik.commands.friend.removed": "%s is not your friend anymore :(",
"sputnik.commands.gc.collected": "garbage collected", "sputnik.commands.gc.collected": "garbage collected",
"sputnik.commands.gc.desc": "frees up ram using the garbage collector (gc)", "sputnik.commands.gc.desc": "frees up ram using the garbage collector (gc)",
"sputnik.commands.help.desc": "lists every command with its description",
"sputnik.commands.macro.added": "macro %s created",
"sputnik.commands.macro.cleared": "lines of %s cleared",
"sputnik.commands.macro.desc": "manages macros",
"sputnik.commands.macro.exists": "a macro named \"%s\" already exists",
"sputnik.commands.macro.lineadded": "line added to %s",
"sputnik.commands.macro.list": "%s macro(s):",
"sputnik.commands.macro.none": "there are no macros",
"sputnik.commands.macro.notfound": "macro \"%s\" not found",
"sputnik.commands.macro.removed": "macro %s removed",
"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.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.desc": "looks for the plugins the server has",
"sputnik.commands.plugins.erroranalyzingcommands": "error analyzing commands: %s", "sputnik.commands.plugins.erroranalyzingcommands": "error analyzing commands: %s",
"sputnik.commands.plugins.namespacesfound": "%s command namespace(s) found:", "sputnik.commands.plugins.namespacesfound": "%s command namespace(s) found:",
@@ -39,6 +54,15 @@
"sputnik.commands.plugins.singleplayer": "you are in singleplayer", "sputnik.commands.plugins.singleplayer": "you are in singleplayer",
"sputnik.commands.prefix.changed": "prefix changed to %s", "sputnik.commands.prefix.changed": "prefix changed to %s",
"sputnik.commands.prefix.desc": "changes the command prefix", "sputnik.commands.prefix.desc": "changes the command prefix",
"sputnik.commands.profile.deleted": "profile %s deleted",
"sputnik.commands.profile.desc": "manages config profiles",
"sputnik.commands.profile.invalidname": "invalid name: \"%s\"",
"sputnik.commands.profile.list": "%s profile(s):",
"sputnik.commands.profile.loaded": "profile %s loaded",
"sputnik.commands.profile.none": "there are no profiles",
"sputnik.commands.profile.notfound": "profile \"%s\" not found",
"sputnik.commands.profile.saved": "current settings saved to %s",
"sputnik.commands.profile.savefailed": "could not save profile \"%s\"",
"sputnik.commands.purge.desc": "removes messages from the chat", "sputnik.commands.purge.desc": "removes messages from the chat",
"sputnik.commands.repeat.desc": "repeats a command x times", "sputnik.commands.repeat.desc": "repeats a command x times",
"sputnik.commands.repeatwait.desc": "combination of $repeat and $wait", "sputnik.commands.repeatwait.desc": "combination of $repeat and $wait",
@@ -50,6 +74,7 @@
"sputnik.commands.wait.desc": "waits a given amount of time before running another command", "sputnik.commands.wait.desc": "waits a given amount of time before running another command",
"sputnik.config.autosaving.desc": "saving settings...", "sputnik.config.autosaving.desc": "saving settings...",
"sputnik.config.autosaving.title": "autosaving...", "sputnik.config.autosaving.title": "autosaving...",
"sputnik.config.error.applying": "error applying settings",
"sputnik.config.error.empty": "config file empty or nonexistent", "sputnik.config.error.empty": "config file empty or nonexistent",
"sputnik.config.error.invalidformat": "invalid file format: %s", "sputnik.config.error.invalidformat": "invalid file format: %s",
"sputnik.config.error.invalidversion": "invalid version: %s", "sputnik.config.error.invalidversion": "invalid version: %s",
@@ -100,6 +125,8 @@
"sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velociraptor", "sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velociraptor",
"sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h", "sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h",
"sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h", "sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h",
"sputnik.enum.module.modules.combat.holdattack.attackmode.spam": "spam",
"sputnik.enum.module.modules.combat.holdattack.attackmode.time": "time",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "dashed", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "dashed",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "points", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "points",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "solid", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "solid",
@@ -322,6 +349,8 @@
"sputnik.module.auradecristales.name": "crystal aura", "sputnik.module.auradecristales.name": "crystal aura",
"sputnik.module.autismo.desc": "hehe", "sputnik.module.autismo.desc": "hehe",
"sputnik.module.autismo.name": "autism", "sputnik.module.autismo.name": "autism",
"sputnik.module.autoataque.desc": "hold the attack button down to attack automatically",
"sputnik.module.autoataque.name": "autoattack",
"sputnik.module.autocartel.desc": "lets you place signs with the same text without typing it out one by one", "sputnik.module.autocartel.desc": "lets you place signs with the same text without typing it out one by one",
"sputnik.module.autocartel.name": "auto sign", "sputnik.module.autocartel.name": "auto sign",
"sputnik.module.autofish.rodbreaking": "the rod is about to break", "sputnik.module.autofish.rodbreaking": "the rod is about to break",
@@ -341,7 +370,7 @@
"sputnik.module.bookbot.created": "created book no. %s", "sputnik.module.bookbot.created": "created book no. %s",
"sputnik.module.bookbot.nobook": "no book found", "sputnik.module.bookbot.nobook": "no book found",
"sputnik.module.brillibrilli.desc": "disables darkness (and applies colors to shaders)", "sputnik.module.brillibrilli.desc": "disables darkness (and applies colors to shaders)",
"sputnik.module.brillibrilli.name": "bling bling", "sputnik.module.brillibrilli.name": "fullbright",
"sputnik.module.bungeespoofer.invalidip": "invalid IP address", "sputnik.module.bungeespoofer.invalidip": "invalid IP address",
"sputnik.module.burglar.dump": "dump", "sputnik.module.burglar.dump": "dump",
"sputnik.module.burglar.steal": "steal", "sputnik.module.burglar.steal": "steal",
@@ -463,6 +492,8 @@
"sputnik.module.notificaciones.name": "notifications", "sputnik.module.notificaciones.name": "notifications",
"sputnik.module.packetdelay.singleplayer": "packet delay doesn't work in singleplayer", "sputnik.module.packetdelay.singleplayer": "packet delay doesn't work in singleplayer",
"sputnik.module.packetmine.pickbreaking": "the pickaxe is about to break", "sputnik.module.packetmine.pickbreaking": "the pickaxe is about to break",
"sputnik.module.canceladordepaquetes.name": "packet cancel",
"sputnik.module.canceladordepaquetes.desc": "cancels packets.",
"sputnik.module.parkour.desc": "automatically jumps on the last possible tick, at the block's edge", "sputnik.module.parkour.desc": "automatically jumps on the last possible tick, at the block's edge",
"sputnik.module.parkour.name": "parkour", "sputnik.module.parkour.name": "parkour",
"sputnik.module.particulas.desc": "emits artificial particles", "sputnik.module.particulas.desc": "emits artificial particles",
@@ -474,7 +505,7 @@
"sputnik.module.perspectivaplus.desc": "adds cool little things to the perspective switch", "sputnik.module.perspectivaplus.desc": "adds cool little things to the perspective switch",
"sputnik.module.perspectivaplus.name": "perspective plus", "sputnik.module.perspectivaplus.name": "perspective plus",
"sputnik.module.precoz.desc": "does things like using items or placing blocks faster", "sputnik.module.precoz.desc": "does things like using items or placing blocks faster",
"sputnik.module.precoz.name": "early", "sputnik.module.precoz.name": "premature ejaculation",
"sputnik.module.prevdeshulkers.desc": "lets you preview shulker contents from the inventory", "sputnik.module.prevdeshulkers.desc": "lets you preview shulker contents from the inventory",
"sputnik.module.prevdeshulkers.name": "shulker preview", "sputnik.module.prevdeshulkers.name": "shulker preview",
"sputnik.module.progresodeminado.desc": "shows you a block's mining progress more visibly", "sputnik.module.progresodeminado.desc": "shows you a block's mining progress more visibly",
@@ -491,9 +522,9 @@
"sputnik.module.reponer.desc": "automatically refills items as they get used up", "sputnik.module.reponer.desc": "automatically refills items as they get used up",
"sputnik.module.reponer.name": "refill", "sputnik.module.reponer.name": "refill",
"sputnik.module.resaltadodebloques.desc": "shows you where a certain kind of block is by rendering a box with its shape", "sputnik.module.resaltadodebloques.desc": "shows you where a certain kind of block is by rendering a box with its shape",
"sputnik.module.resaltadodebloques.name": "block highlight", "sputnik.module.resaltadodebloques.name": "block ESP",
"sputnik.module.resaltadodeentidades.desc": "highlights the outline of the selected entities through blocks", "sputnik.module.resaltadodeentidades.desc": "highlights the outline of the selected entities through blocks",
"sputnik.module.resaltadodeentidades.name": "entity highlight", "sputnik.module.resaltadodeentidades.name": "entity ESP",
"sputnik.module.retroceso.desc": "makes you fat", "sputnik.module.retroceso.desc": "makes you fat",
"sputnik.module.retroceso.name": "knockback", "sputnik.module.retroceso.name": "knockback",
"sputnik.module.rotaciones.desc": "lets you force a specific rotation", "sputnik.module.rotaciones.desc": "lets you force a specific rotation",
@@ -545,6 +576,9 @@
"sputnik.module.yunquesconpluma.name": "anvils with a quill", "sputnik.module.yunquesconpluma.name": "anvils with a quill",
"sputnik.module.zoom.desc": "magnifying glass", "sputnik.module.zoom.desc": "magnifying glass",
"sputnik.module.zoom.name": "zoom", "sputnik.module.zoom.name": "zoom",
"sputnik.profile.error.dir": "could not create the profiles directory",
"sputnik.profile.error.loading": "error loading profile \"%s\"",
"sputnik.profile.error.saving": "error saving profile \"%s\"",
"sputnik.setting.amigos.general.prevenirataques.desc": "don't let you attack them manually", "sputnik.setting.amigos.general.prevenirataques.desc": "don't let you attack them manually",
"sputnik.setting.amigos.general.prevenirataques.name": "prevent attacks", "sputnik.setting.amigos.general.prevenirataques.name": "prevent attacks",
"sputnik.setting.amigos.general.rueditaparaanadiramigo.desc": "press the middle mouse button to (un)mark a player as a friend", "sputnik.setting.amigos.general.rueditaparaanadiramigo.desc": "press the middle mouse button to (un)mark a player as a friend",
@@ -641,6 +675,14 @@
"sputnik.setting.autismo.general.direccion.name": "direction", "sputnik.setting.autismo.general.direccion.name": "direction",
"sputnik.setting.autismo.general.intervalo.desc": "rotation interval", "sputnik.setting.autismo.general.intervalo.desc": "rotation interval",
"sputnik.setting.autismo.general.intervalo.name": "interval", "sputnik.setting.autismo.general.intervalo.name": "interval",
"sputnik.setting.autoataque.general.atacarenpantallas.desc": "attack even while in a screen",
"sputnik.setting.autoataque.general.atacarenpantallas.name": "attack in screens",
"sputnik.setting.autoataque.general.cooldown.desc": "how full the attack bar has to be before attacking",
"sputnik.setting.autoataque.general.cooldown.name": "cooldown",
"sputnik.setting.autoataque.general.cooldown1.desc": "how often to attack, in ticks",
"sputnik.setting.autoataque.general.cooldown1.name": "cooldown",
"sputnik.setting.autoataque.general.mododeataque.desc": "how to attack",
"sputnik.setting.autoataque.general.mododeataque.name": "attack mode",
"sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "switch back to the original tool", "sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "switch back to the original tool",
"sputnik.setting.autoherramienta.general.cambiardevuelta.name": "switch back", "sputnik.setting.autoherramienta.general.cambiardevuelta.name": "switch back",
"sputnik.setting.autoherramienta.general.delay.desc": "delay when switching tools", "sputnik.setting.autoherramienta.general.delay.desc": "delay when switching tools",
@@ -1429,13 +1471,34 @@
"sputnik.setting.zoom.visual.mostrarmanos.name": "show hands", "sputnik.setting.zoom.visual.mostrarmanos.name": "show hands",
"sputnik.ui.availablemodes": "available modes:", "sputnik.ui.availablemodes": "available modes:",
"sputnik.ui.clickgui.title": "gui", "sputnik.ui.clickgui.title": "gui",
"sputnik.ui.clientsettings.title": "mod settings",
"sputnik.ui.friend.manage": "manage %s", "sputnik.ui.friend.manage": "manage %s",
"sputnik.ui.friend.remove": "remove friend", "sputnik.ui.friend.remove": "remove friend",
"sputnik.ui.friends.title": "friends",
"sputnik.ui.list.tooltip": "%s (%s of %s)", "sputnik.ui.list.tooltip": "%s (%s of %s)",
"sputnik.ui.macros.empty": "no macros yet",
"sputnik.ui.macros.instructions": "left click: select / right click: bind / shift + right click: delete",
"sputnik.ui.macros.lineinstructions": "shift + right click on a line to remove it",
"sputnik.ui.macros.lineplaceholder": "command or message...",
"sputnik.ui.macros.nameplaceholder": "macro name...",
"sputnik.ui.macros.presskey": "press a key...",
"sputnik.ui.macros.title": "macros",
"sputnik.ui.preview.echest": "echest", "sputnik.ui.preview.echest": "echest",
"sputnik.ui.preview.shulker": "shulker preview", "sputnik.ui.preview.shulker": "shulker preview",
"sputnik.ui.profiles.deleted": "deleted \"%s\"",
"sputnik.ui.profiles.empty": "no profiles yet",
"sputnik.ui.profiles.instructions": "left click: load / right click: overwrite / shift + right click: delete",
"sputnik.ui.profiles.invalidname": "invalid name: \"%s\"",
"sputnik.ui.profiles.loaded": "loaded \"%s\"",
"sputnik.ui.profiles.placeholder": "profile name...",
"sputnik.ui.profiles.saved": "saved \"%s\"",
"sputnik.ui.profiles.title": "profiles",
"sputnik.ui.searchbar.placeholder": "search...", "sputnik.ui.searchbar.placeholder": "search...",
"sputnik.ui.settingsof": "%s settings", "sputnik.ui.settingsof": "%s settings",
"sputnik.ui.tab.hud": "hud",
"sputnik.ui.tab.macros": "macros",
"sputnik.ui.tab.modules": "modules",
"sputnik.ui.tab.profiles": "profiles",
"sputnik.update.clickhere": "CLICK HERE", "sputnik.update.clickhere": "CLICK HERE",
"sputnik.update.download": "download the latest version (%s) from here:", "sputnik.update.download": "download the latest version (%s) from here:",
"sputnik.update.idgaf": "i don't care", "sputnik.update.idgaf": "i don't care",
@@ -29,6 +29,21 @@
"sputnik.commands.friend.removed": "%s ya no es tu amigo :(", "sputnik.commands.friend.removed": "%s ya no es tu amigo :(",
"sputnik.commands.gc.collected": "basura recolectada", "sputnik.commands.gc.collected": "basura recolectada",
"sputnik.commands.gc.desc": "limpia la ram mediante el recolector de basura (gc)", "sputnik.commands.gc.desc": "limpia la ram mediante el recolector de basura (gc)",
"sputnik.commands.help.desc": "lista todos los comandos con su descripción",
"sputnik.commands.macro.added": "macro %s creada",
"sputnik.commands.macro.cleared": "líneas de %s borradas",
"sputnik.commands.macro.desc": "gestiona las macros",
"sputnik.commands.macro.exists": "ya hay una macro que se llama \"%s\"",
"sputnik.commands.macro.lineadded": "línea añadida a %s",
"sputnik.commands.macro.list": "%s macro(s):",
"sputnik.commands.macro.none": "no hay macros",
"sputnik.commands.macro.notfound": "macro \"%s\" no encontrada",
"sputnik.commands.macro.removed": "macro %s borrada",
"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.peek.desc": "cotillea el inventario de otro jugador",
"sputnik.commands.plugins.calmdown": "calmaaa", "sputnik.commands.plugins.calmdown": "calmaaa",
"sputnik.commands.plugins.desc": "busca los plugins que tenga el server", "sputnik.commands.plugins.desc": "busca los plugins que tenga el server",
@@ -39,6 +54,15 @@
"sputnik.commands.plugins.singleplayer": "estás en un solo jugador", "sputnik.commands.plugins.singleplayer": "estás en un solo jugador",
"sputnik.commands.prefix.changed": "prefijo cambiado a %s", "sputnik.commands.prefix.changed": "prefijo cambiado a %s",
"sputnik.commands.prefix.desc": "cambia el prefijo de los comandos", "sputnik.commands.prefix.desc": "cambia el prefijo de los comandos",
"sputnik.commands.profile.deleted": "perfil %s borrado",
"sputnik.commands.profile.desc": "gestiona los perfiles de configuración",
"sputnik.commands.profile.invalidname": "nombre inválido: \"%s\"",
"sputnik.commands.profile.list": "%s perfil(es):",
"sputnik.commands.profile.loaded": "perfil %s cargado",
"sputnik.commands.profile.none": "no hay perfiles",
"sputnik.commands.profile.notfound": "perfil \"%s\" no encontrado",
"sputnik.commands.profile.saved": "ajustes actuales guardados en %s",
"sputnik.commands.profile.savefailed": "no se pudo guardar el perfil \"%s\"",
"sputnik.commands.purge.desc": "elimina mensajes del chat", "sputnik.commands.purge.desc": "elimina mensajes del chat",
"sputnik.commands.repeat.desc": "repite un comando x veces", "sputnik.commands.repeat.desc": "repite un comando x veces",
"sputnik.commands.repeatwait.desc": "combinación de $repetir y $esperar", "sputnik.commands.repeatwait.desc": "combinación de $repetir y $esperar",
@@ -50,6 +74,7 @@
"sputnik.commands.wait.desc": "espera un tiempo determinado antes de ejecutar otro comando", "sputnik.commands.wait.desc": "espera un tiempo determinado antes de ejecutar otro comando",
"sputnik.config.autosaving.desc": "guardando configuración...", "sputnik.config.autosaving.desc": "guardando configuración...",
"sputnik.config.autosaving.title": "autoguardando...", "sputnik.config.autosaving.title": "autoguardando...",
"sputnik.config.error.applying": "error aplicando ajustes",
"sputnik.config.error.empty": "archivo de config. vacío o inexistente", "sputnik.config.error.empty": "archivo de config. vacío o inexistente",
"sputnik.config.error.invalidformat": "formato de archivo inválido: %s", "sputnik.config.error.invalidformat": "formato de archivo inválido: %s",
"sputnik.config.error.invalidversion": "versión inválida: %s", "sputnik.config.error.invalidversion": "versión inválida: %s",
@@ -100,6 +125,8 @@
"sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velocirráptor", "sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velocirráptor",
"sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h", "sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h",
"sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h", "sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h",
"sputnik.enum.module.modules.combat.holdattack.attackmode.spam": "spamear",
"sputnik.enum.module.modules.combat.holdattack.attackmode.time": "timear",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "discontinua", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "discontinua",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "puntos", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "puntos",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "continua", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "continua",
@@ -322,6 +349,8 @@
"sputnik.module.auradecristales.name": "aura de cristales", "sputnik.module.auradecristales.name": "aura de cristales",
"sputnik.module.autismo.desc": "jeje", "sputnik.module.autismo.desc": "jeje",
"sputnik.module.autismo.name": "autismo", "sputnik.module.autismo.name": "autismo",
"sputnik.module.autoataque.desc": "mantén pulsado el clic para atacar automáticamente",
"sputnik.module.autoataque.name": "autoataque",
"sputnik.module.autocartel.desc": "te permite colocar carteles con el mismo texto sin tener que escribirlo uno por uno", "sputnik.module.autocartel.desc": "te permite colocar carteles con el mismo texto sin tener que escribirlo uno por uno",
"sputnik.module.autocartel.name": "autocartel", "sputnik.module.autocartel.name": "autocartel",
"sputnik.module.autofish.rodbreaking": "caña a punto de romperse", "sputnik.module.autofish.rodbreaking": "caña a punto de romperse",
@@ -545,6 +574,9 @@
"sputnik.module.yunquesconpluma.name": "yunques con pluma", "sputnik.module.yunquesconpluma.name": "yunques con pluma",
"sputnik.module.zoom.desc": "lupa", "sputnik.module.zoom.desc": "lupa",
"sputnik.module.zoom.name": "zoom", "sputnik.module.zoom.name": "zoom",
"sputnik.profile.error.dir": "no se pudo crear el directorio de perfiles",
"sputnik.profile.error.loading": "error al cargar el perfil \"%s\"",
"sputnik.profile.error.saving": "error al guardar el perfil \"%s\"",
"sputnik.setting.amigos.general.prevenirataques.desc": "no dejar que les ataques manualmente", "sputnik.setting.amigos.general.prevenirataques.desc": "no dejar que les ataques manualmente",
"sputnik.setting.amigos.general.prevenirataques.name": "prevenir ataques", "sputnik.setting.amigos.general.prevenirataques.name": "prevenir ataques",
"sputnik.setting.amigos.general.rueditaparaanadiramigo.desc": "pulsara la ruedita del ratón para (des)marcar a un jugador como amigo", "sputnik.setting.amigos.general.rueditaparaanadiramigo.desc": "pulsara la ruedita del ratón para (des)marcar a un jugador como amigo",
@@ -641,6 +673,14 @@
"sputnik.setting.autismo.general.direccion.name": "dirección", "sputnik.setting.autismo.general.direccion.name": "dirección",
"sputnik.setting.autismo.general.intervalo.desc": "intervalo de rotación", "sputnik.setting.autismo.general.intervalo.desc": "intervalo de rotación",
"sputnik.setting.autismo.general.intervalo.name": "intervalo", "sputnik.setting.autismo.general.intervalo.name": "intervalo",
"sputnik.setting.autoataque.general.atacarenpantallas.desc": "atacar aun estando en una pantalla",
"sputnik.setting.autoataque.general.atacarenpantallas.name": "atacar en pantallas",
"sputnik.setting.autoataque.general.cooldown.desc": "cómo de llena tiene que estar la barra de ataque antes de atacar",
"sputnik.setting.autoataque.general.cooldown.name": "cooldown",
"sputnik.setting.autoataque.general.cooldown1.desc": "cada cuánto atacar, en ticks",
"sputnik.setting.autoataque.general.cooldown1.name": "cooldown",
"sputnik.setting.autoataque.general.mododeataque.desc": "cómo atacar",
"sputnik.setting.autoataque.general.mododeataque.name": "modo de ataque",
"sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "cambiar de vuelta a la herramienta inicial", "sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "cambiar de vuelta a la herramienta inicial",
"sputnik.setting.autoherramienta.general.cambiardevuelta.name": "cambiar de vuelta", "sputnik.setting.autoherramienta.general.cambiardevuelta.name": "cambiar de vuelta",
"sputnik.setting.autoherramienta.general.delay.desc": "delay al cambiar de herramienta", "sputnik.setting.autoherramienta.general.delay.desc": "delay al cambiar de herramienta",
@@ -1429,13 +1469,34 @@
"sputnik.setting.zoom.visual.mostrarmanos.name": "mostrar manos", "sputnik.setting.zoom.visual.mostrarmanos.name": "mostrar manos",
"sputnik.ui.availablemodes": "modos disponibles:", "sputnik.ui.availablemodes": "modos disponibles:",
"sputnik.ui.clickgui.title": "interfaz", "sputnik.ui.clickgui.title": "interfaz",
"sputnik.ui.clientsettings.title": "ajustes del mod",
"sputnik.ui.friend.manage": "gestiona a %s", "sputnik.ui.friend.manage": "gestiona a %s",
"sputnik.ui.friend.remove": "eliminar amigo", "sputnik.ui.friend.remove": "eliminar amigo",
"sputnik.ui.friends.title": "amigos",
"sputnik.ui.list.tooltip": "%s (%s de %s)", "sputnik.ui.list.tooltip": "%s (%s de %s)",
"sputnik.ui.macros.empty": "todavía no hay macros",
"sputnik.ui.macros.instructions": "clic izq.: seleccionar / clic der.: asignar tecla / shift + clic der.: borrar",
"sputnik.ui.macros.lineinstructions": "shift + clic der. sobre una línea para quitarla",
"sputnik.ui.macros.lineplaceholder": "comando o mensaje...",
"sputnik.ui.macros.nameplaceholder": "nombre de la macro...",
"sputnik.ui.macros.presskey": "pulsa una tecla...",
"sputnik.ui.macros.title": "macros",
"sputnik.ui.preview.echest": "echest", "sputnik.ui.preview.echest": "echest",
"sputnik.ui.preview.shulker": "previsualización del shulker", "sputnik.ui.preview.shulker": "previsualización del shulker",
"sputnik.ui.profiles.deleted": "borrado \"%s\"",
"sputnik.ui.profiles.empty": "todavía no hay perfiles",
"sputnik.ui.profiles.instructions": "clic izq.: cargar / clic der.: sobrescribir / shift + clic der.: borrar",
"sputnik.ui.profiles.invalidname": "nombre inválido: \"%s\"",
"sputnik.ui.profiles.loaded": "cargado \"%s\"",
"sputnik.ui.profiles.placeholder": "nombre del perfil...",
"sputnik.ui.profiles.saved": "guardado \"%s\"",
"sputnik.ui.profiles.title": "perfiles",
"sputnik.ui.searchbar.placeholder": "buscar...", "sputnik.ui.searchbar.placeholder": "buscar...",
"sputnik.ui.settingsof": "ajustes de %s", "sputnik.ui.settingsof": "ajustes de %s",
"sputnik.ui.tab.hud": "hud",
"sputnik.ui.tab.macros": "macros",
"sputnik.ui.tab.modules": "módulos",
"sputnik.ui.tab.profiles": "perfiles",
"sputnik.update.clickhere": "CLIC AQUÍ", "sputnik.update.clickhere": "CLIC AQUÍ",
"sputnik.update.download": "descarga la última versión (%s) desde aquí:", "sputnik.update.download": "descarga la última versión (%s) desde aquí:",
"sputnik.update.idgaf": "me la pela", "sputnik.update.idgaf": "me la pela",