This commit is contained in:
retucio committed 2026-08-19 22:57:41 +02:00
1 parent e0f8523809
commit 272c1059ef
66 files changed
+2300 -314

No files matched your search

+16 -14
View File
@@ -1,29 +1,31 @@
# v5.5 # 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:
- autoataque: mantén el botón de ataque pulsado para atacar automáticamente - cancelador de paquetes: evita que se envíen o reciban ciertos paquetes
#### comandos nuevos: #### comandos nuevos:
- `$?`: muestra los comandos disponibles y los describe
- `$perfil`: gestiona los perfiles de configuración
- `$macro`: gestiona las macros
### arreglos / mejoras ### arreglos / mejoras
- arreglado el guardado al cerrar el juego: petaba antes de escribir nada, así que se perdía - las rotaciones de los módulos se reparten por prioridad en vez de pisarse entre ellas
todo lo cambiado desde el último autoguardado y al volver a entrar salían los ajustes por defecto - el editor del hud ya es una pestaña más de la interfaz, además de su propia pantalla
- el autoguardado cuenta desde el último guardado y no desde el último cambio, que tocando
ajustes seguido no llegaba a guardar nunca
- guardando dos veces seguidas ya no se descarta la segunda (era la más reciente)
- las teclas de los módulos ya no se pisan entre ellas al cargar la config.
- si algo falla al aplicar los ajustes ya no se queda el mod entero sin guardar
- las listas (entidades, ítems, bloques...) se traducen al idioma del juego en vez de salir
siempre en inglés, y se guardan por nombre interno para que no dependan del idioma
- traducidos los títulos de los marcos, las descripciones de los módulos y los mensajes del
arranque, que salían en español o directamente como la clave de traducción
- añadidas las traducciones que faltaban del autoataque
## problemas reconocidos ## problemas reconocidos
+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.6 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.
+10 -1
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());
@@ -151,11 +159,12 @@ 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);
@@ -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,10 +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 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());
}
}
@@ -57,7 +57,7 @@ public class PanoramaCommand extends Command {
@Nullable Path dir = null; @Nullable Path dir = null;
public PanoramaCommand() { public PanoramaCommand() {
super("panorama", Component.translatable("sputnik.commands.panorama.desc").getString()); super("panorama", "sputnik.commands.panorama.desc");
} }
private void takeScreenshot() { private void takeScreenshot() {
@@ -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
@@ -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);
@@ -104,9 +107,8 @@ public class BinarySerializer {
"formato de archivo inválido: %s", magic)); "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(I18nUtil.fmt("sputnik.config.error.invalidversion", throw new IOException(I18nUtil.fmt("sputnik.config.error.invalidversion",
"versión inválida: %s", version)); "versión inválida: %s", version));
} }
@@ -123,6 +125,9 @@ public class BinarySerializer {
Map<String, String> hudImagePaths = readStringStringMap(in); Map<String, String> hudImagePaths = readStringStringMap(in);
Map<String, String> friends = readStringStringMap(in); Map<String, String> friends = readStringStringMap(in);
List<ClientConfig.MacroData> macros = version >= 3 ? readMacros(in) : new ArrayList<>();
String currentProfile = version >= 3 ? readString(in) : "";
// leer pos. de la barra de búsqueda // leer pos. de la barra de búsqueda
int[] searchBarPosition = in.readBoolean() int[] searchBarPosition = in.readBoolean()
? new int[]{in.readInt(), in.readInt()} ? new int[]{in.readInt(), in.readInt()}
@@ -137,6 +142,8 @@ public class BinarySerializer {
config.hudVisibilities = hudVisibilities; config.hudVisibilities = hudVisibilities;
config.hudImagePaths = hudImagePaths; config.hudImagePaths = hudImagePaths;
config.friends = friends; config.friends = friends;
config.macros = macros;
config.currentProfile = currentProfile == null ? "" : currentProfile;
if (searchBarPosition != null) config.searchBarPosition = searchBarPosition; if (searchBarPosition != null) config.searchBarPosition = searchBarPosition;
return config; return config;
@@ -386,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;
@@ -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;
@@ -25,7 +28,9 @@ 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;
@@ -183,6 +188,8 @@ public class ConfigManager {
applySearchBarPosition(); applySearchBarPosition();
applyExtendablePanels(); applyExtendablePanels();
applyFriends(); applyFriends();
applyMacros();
applyHudElements();
Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.applied", "ajustes aplicados")); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.applied", "ajustes aplicados"));
} catch (Exception e) { } catch (Exception e) {
@@ -199,11 +206,44 @@ public class ConfigManager {
} }
// true mientras se está cargando la config. sobre los ajustes. lo mira KeySetting para no // 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 // 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() { public boolean isApplying() {
return applying; 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
public ClientConfig getConfig() { public ClientConfig getConfig() {
ensureConfig(); ensureConfig();
@@ -258,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 ------------------
@@ -271,6 +325,26 @@ public class ConfigManager {
"amigos aplicados")); "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"})
public void applySetting(Setting<?> setting) { public void applySetting(Setting<?> setting) {
String key = getSettingKey(setting); String key = getSettingKey(setting);
@@ -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;
}
}
@@ -158,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());
@@ -142,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 {
@@ -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) {
@@ -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 ->
new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, pos, dir, sequence));
mc.gameMode.startPrediction(mc.level, 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);
mc.gameMode.startPrediction(mc.level, sequence ->
new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, pos, dir, sequence));
mining = true; mining = true;
mc.gameMode.startPrediction(mc.level, sequence ->
new ServerboundPlayerActionPacket(ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, pos, dir, sequence));
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
InteractionResult didPlace = mc.gameMode.useItem(mc.player, InteractionHand.MAIN_HAND); RotationUtil.request(RotationUtil.HIGH, Float.NaN, 90, 0, RotationUtil.Mode.CLIENT, () -> {
if (didPlace.consumesAction()) InteractionResult didPlace = mc.gameMode.useItem(mc.player, InteractionHand.MAIN_HAND);
placed = true; if (didPlace.consumesAction())
placed = true;
});
} }
} }
} }
@@ -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);
} }
} }
@@ -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,18 +207,24 @@ 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()
));
// detectar clics sobre los paneles switch (tabBar.getSelected()) {
friendsPanel.mouseClicked(x, y, click.button()); case MODULES -> {
clientSettingsPanel.mouseClicked(x, y, click.button()); miscWidgets.forEach(w -> w.mouseClicked(x, y, click.button()));
for (ModulePanel mp : modulePanels.reversed()) {
mp.mouseClicked(x, y, click.button()); friendsPanel.mouseClicked(x, y, click.button());
} clientSettingsPanel.mouseClicked(x, y, click.button());
for (SettingsPanel sp : new ArrayList<>(settingsPanels)) { for (ModulePanel mp : modulePanels.reversed()) {
sp.mouseClicked(x, y, click.button()); mp.mouseClicked(x, y, click.button());
}
for (SettingsPanel sp : new ArrayList<>(settingsPanels)) {
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;
}
}
@@ -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; }
}
@@ -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();
@@ -3,16 +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.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.*;
@@ -36,14 +46,14 @@ 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<>();
// estos mapas guardan la CLAVE de traducción, no el texto ya traducido: son los nombres
// internos de las opciones y por tanto claves de la config. (ver ListSetting#getConfigName).
// antes se resolvía aquí con Component#translatable, pero esto corre en la inicialización
// del mod, cuando Language todavía es el en_us de vanilla: salían los nombres en inglés
// pasara lo que pasara. ahora se traducen al mostrarlos (ListSetting#getDisplayName)
public static void init() { public static void init() {
entityNames = getMapOfLists(entityList, entityNames = getMapOfLists(entityList,
entityList.stream().map(EntityType::getDescriptionId).toList()); entityList.stream().map(EntityType::getDescriptionId).toList());
@@ -81,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) {
@@ -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,17 +135,19 @@ 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); if (swapBack) {
mc.player.getInventory().setSelectedSlot(prevSlot);
mc.getConnection().send(new ServerboundSetCarriedItemPacket(prevSlot));
}
};
if (swapBack) { if (rotate == null) place.run();
mc.player.getInventory().setSelectedSlot(prevSlot); else RotationUtil.lookAt(rotationPriority, hitPos, 0, rotate, place);
mc.getConnection().send(new ServerboundSetCarriedItemPacket(prevSlot));
}
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);
}
}
@@ -29,6 +29,16 @@
"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.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.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.packdesc": "generated by Sputnik",
@@ -44,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",
@@ -351,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",
@@ -473,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",
@@ -484,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",
@@ -501,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",
@@ -555,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",
@@ -1452,10 +1476,29 @@
"sputnik.ui.friend.remove": "remove friend", "sputnik.ui.friend.remove": "remove friend",
"sputnik.ui.friends.title": "friends", "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,16 @@
"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.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.errorassemble": "no se pudo crear el pack; existe uno ya con ese nombre?",
"sputnik.commands.panorama.packdesc": "generado por Sputnik", "sputnik.commands.panorama.packdesc": "generado por Sputnik",
@@ -44,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",
@@ -555,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",
@@ -1452,10 +1474,29 @@
"sputnik.ui.friend.remove": "eliminar amigo", "sputnik.ui.friend.remove": "eliminar amigo",
"sputnik.ui.friends.title": "amigos", "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",