Compare commits

...
2 Commits
Author SHA1 Message Date
retucio 6cb4656365 v5.8 2026-08-22 18:20:52 +02:00
retucio 272c1059ef v5.7 2026-08-19 22:57:41 +02:00
72 changed files with 2878 additions and 326 deletions

No files matched your search

+5 -14
View File
@@ -1,29 +1,20 @@
# v5.5 # v5.8
## novedades ## novedades
nuevo tipo de ajuste: archivo seleccionable
### adiciones ### adiciones
#### módulos nuevos: #### módulos nuevos:
- autoataque: mantén el botón de ataque pulsado para atacar automáticamente - puto gordo: come al tener hambre automáticamente
- logger de paquetes: loguea los paquetes seleccionados
#### comandos nuevos: #### comandos nuevos:
### arreglos / mejoras ### arreglos / mejoras
- arreglado el guardado al cerrar el juego: petaba antes de escribir nada, así que se perdía
todo lo cambiado desde el último autoguardado y al volver a entrar salían los ajustes por defecto
- 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.8
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.
+17 -2
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,12 +19,14 @@ 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;
import me.retucio.sputnik.ui.screen.UpdateScreen; import me.retucio.sputnik.ui.screen.UpdateScreen;
import me.retucio.sputnik.ui.widgets.buttons.settings.BindButton; import me.retucio.sputnik.ui.widgets.buttons.settings.BindButton;
import me.retucio.sputnik.ui.widgets.buttons.SettingButton; import me.retucio.sputnik.ui.widgets.buttons.SettingButton;
import me.retucio.sputnik.ui.widgets.buttons.settings.FileButton;
import me.retucio.sputnik.ui.widgets.buttons.settings.TextButton; import me.retucio.sputnik.ui.widgets.buttons.settings.TextButton;
import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel; import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
@@ -99,12 +103,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 +160,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);
@@ -174,8 +184,12 @@ public class Sputnik implements ClientModInitializer {
private boolean isAnySettingButtonFocused() { private boolean isAnySettingButtonFocused() {
for (SettingsPanel sf : ClickGui.INSTANCE.getSettingsPanels()) for (SettingsPanel sf : ClickGui.INSTANCE.getSettingsPanels())
for (Button sb : sf.getButtons()) for (Button sb : sf.getButtons())
if ((sb instanceof BindButton b && b.isFocused()) || (sb instanceof TextButton t && t.isFocused())) if ((sb instanceof BindButton b && b.isFocused())
|| (sb instanceof TextButton t && t.isFocused())
|| (sb instanceof FileButton f && f.isFocused())
) {
return true; return true;
}
return false; return false;
} }
@@ -199,6 +213,7 @@ public class Sputnik implements ClientModInitializer {
for (SettingsPanel sf : ClickGui.INSTANCE.getSettingsPanels()) { for (SettingsPanel sf : ClickGui.INSTANCE.getSettingsPanels()) {
for (SettingButton<?> sb : sf.getButtons()) { for (SettingButton<?> sb : sf.getButtons()) {
if (sb instanceof BindButton bb) bb.onKey(key, action); if (sb instanceof BindButton bb) bb.onKey(key, action);
if (sb instanceof FileButton fb) fb.onKey(key, action);
if (sb instanceof TextButton tb) tb.onKey(key, action); if (sb instanceof TextButton tb) tb.onKey(key, action);
} }
} }
@@ -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;
}
}
@@ -0,0 +1,212 @@
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.EnumSetting;
import me.retucio.sputnik.module.setting.settings.FileSetting;
import me.retucio.sputnik.module.setting.settings.ListSetting;
import me.retucio.sputnik.ui.hud.HudRenderer;
import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.Lists;
import net.minecraft.client.Minecraft;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.PacketType;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class PacketLogger extends Module {
private static final DateTimeFormatter TIME = DateTimeFormatter
.ofPattern("HH:mm:ss.SSS")
.withZone(ZoneId.systemDefault());
private static final int QUEUE_CAPACITY = 8192;
private static final int BATCH_SIZE = 256;
private final ListSetting<PacketType<? extends Packet<?>>> packets = sgGeneral.add(new ListSetting<>(
"paquetes", "paquetes a loguear",
Lists.packetList, Lists.allFalse(Lists.packetList), Lists.packetNames
));
private final EnumSetting<LogMode> mode = sgGeneral.add(new EnumSetting<>(
"modo de logueo", "de qué manera loguear los paquetes",
LogMode.class, LogMode.FILE
));
private final FileSetting file = sgGeneral.add(new FileSetting(
"archivo", "archivo donde loguear los paquetes",
"sputnik/packets.log"
));
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
private final AtomicInteger dropped = new AtomicInteger();
private volatile boolean running;
private volatile Thread writer;
public PacketLogger() {
super("logger de paquetes", "loguea paquetes", Category.NETWORK);
}
@Override
public void onEnable() {
startWriter();
}
@Override
public void onDisable() {
stopWriter();
}
@EventListener
private void onPacketSend(PacketEvent.Send event) {
handle("OUT", event.getPacket());
}
@EventListener
private void onPacketReceive(PacketEvent.Receive event) {
handle("IN", event.getPacket());
}
private void handle(String direction, Packet<?> packet) {
if (packet == null || !packets.isEnabled(packet.type())) return;
String name = nameOf(packet);
String body = describe(packet);
switch (mode.getValue()) {
case CHAT -> onGameThread(() -> ChatUtil.info("[" + direction + "] " + body));
case NOTIFICATION -> onGameThread(() -> HudRenderer.INSTANCE.pushNotification(name, body));
case FILE -> enqueue("[" + TIME.format(Instant.now()) + "] [" + direction + "] " + name + " " + body);
}
}
private static void onGameThread(Runnable action) {
Minecraft mc = Minecraft.getInstance();
if (mc.isSameThread()) action.run();
else mc.execute(action);
}
private static String nameOf(Packet<?> packet) {
PacketType<?> type = packet.type();
return type.id().toString();
}
private static String describe(Packet<?> packet) {
try {
return String.valueOf(packet);
} catch (Throwable t) {
return packet.getClass().getSimpleName() + " <toString failed: " + t + ">";
}
}
private void enqueue(String line) {
if (!running) return;
if (!queue.offer(line)) dropped.incrementAndGet();
}
private void startWriter() {
if (running) return;
Path path = file.getPath().toAbsolutePath();
queue.clear();
dropped.set(0);
running = true;
Thread thread = new Thread(() -> writerLoop(path), "sputnik-packet-logger");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
writer = thread;
thread.start();
}
private void stopWriter() {
if (!running) return;
running = false;
Thread thread = writer;
writer = null;
if (thread == null) return;
try {
thread.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
int lost = dropped.getAndSet(0);
if (lost > 0) {
onGameThread(() -> ChatUtil.warn("se descartaron " + lost + " paquetes (cola llena)"));
}
}
private void writerLoop(Path path) {
try {
Path parent = path.getParent();
if (parent != null) Files.createDirectories(parent);
try (BufferedWriter out = Files.newBufferedWriter(
path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
out.write("--- sesión iniciada " + Instant.now() + " ---");
out.newLine();
out.flush();
List<String> batch = new ArrayList<>(BATCH_SIZE);
while (running || !queue.isEmpty()) {
String first = queue.poll(250, TimeUnit.MILLISECONDS);
if (first == null) continue;
batch.add(first);
queue.drainTo(batch, BATCH_SIZE - 1);
for (String line : batch) {
out.write(line);
out.newLine();
}
batch.clear();
out.flush();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
running = false;
queue.clear();
onGameThread(() -> {
ChatUtil.error("no se pudo escribir el log: " + e.getMessage());
toggle();
});
}
}
private enum LogMode {
CHAT("chat"),
NOTIFICATION("notis"),
FILE("archivo");
private final String name;
LogMode(String name) { this.name = name; }
@Override public String toString() { return name; }
}
}
@@ -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());
@@ -12,6 +12,7 @@ import me.retucio.sputnik.module.modules.inventory.*;
import me.retucio.sputnik.module.modules.misc.*; import me.retucio.sputnik.module.modules.misc.*;
import me.retucio.sputnik.module.modules.movement.*; import me.retucio.sputnik.module.modules.movement.*;
import me.retucio.sputnik.module.modules.network.*; import me.retucio.sputnik.module.modules.network.*;
import me.retucio.sputnik.module.modules.network.PacketLogger;
import me.retucio.sputnik.module.modules.player.*; import me.retucio.sputnik.module.modules.player.*;
import me.retucio.sputnik.module.modules.render.*; import me.retucio.sputnik.module.modules.render.*;
import me.retucio.sputnik.module.modules.render.CritsPlus; import me.retucio.sputnik.module.modules.render.CritsPlus;
@@ -142,7 +143,9 @@ 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 PacketLogger());
add(new PacketMine()); add(new PacketMine());
add(new Reconnect()); add(new Reconnect());
add(new RPackBypass()); add(new RPackBypass());
@@ -151,6 +154,7 @@ public class ModuleManager {
private void addPlayer() { private void addPlayer() {
add(new AirPlace()); add(new AirPlace());
add(new AntiHunger()); add(new AntiHunger());
add(new AutoEat());
add(new AutoFish()); add(new AutoFish());
add(new Capes()); add(new Capes());
add(new FastUse()); add(new FastUse());
@@ -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(
"C2S", "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(
"S2C", "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() {
@@ -0,0 +1,82 @@
package me.retucio.sputnik.module.modules.player;
import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.InventoryUtil;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import java.util.Comparator;
import java.util.List;
public class AutoEat extends Module {
private final NumberSetting hunger = sgGeneral.add(new NumberSetting(
"hambre", "cuánta hambre es necesaria para comer (puntos de comida)",
6, 0, 19, 1
));
private boolean eating;
public AutoEat() {
super("puto gordo", "come automáticamente al tener hambre", Category.PLAYER);
}
@Override
public void onTick() {
if (mc.player == null) return;
if (eating) {
if (mc.player.getUseItemRemainingTicks() > 0) {
return;
} else {
stopEating();
}
}
if (mc.player.getFoodData().getFoodLevel() <= hunger.getIntValue()) {
List<ItemStack> sorted = InventoryUtil.findAllStacks(
stack -> stack.getItem().components().get(DataComponents.FOOD) != null
);
sorted.sort(Comparator.comparingInt(stack -> stack.getItem().components().get(DataComponents.FOOD).nutrition()));
if (sorted.isEmpty()) return;
boolean inHotbar = false;
for (ItemStack stack : sorted) {
int slot = InventoryUtil.findSlot(s -> stack == s);
if (Inventory.isHotbarSlot(slot)) {
mc.player.getInventory().setSelectedSlot(slot);
inHotbar = true;
}
}
if (!inHotbar) {
int free = mc.player.getInventory().getFreeSlot();
InventoryUtil.move(InventoryUtil.findSlot(stack -> stack == sorted.getFirst()), free != -1
? free
: mc.player.getInventory().getSelectedSlot());
if (free != -1) {
mc.player.getInventory().setSelectedSlot(free);
}
}
startEating();
}
}
private void startEating() {
eating = true;
mc.options.keyUse.setDown(true);
}
private void stopEating() {
eating = false;
mc.options.keyUse.setDown(false);
}
}
@@ -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,75 @@
package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.module.setting.Setting;
import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.I18nUtil;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileSetting extends Setting<String> {
private File file;
public FileSetting(String name, String description, String defaultValue) {
super(name, description, defaultValue);
file = new File(defaultValue);
}
public boolean isDirectory() {
return file.isDirectory();
}
public boolean exists() {
return file.exists();
}
public boolean canWrite() {
return file.canWrite();
}
public File getFile() {
return file;
}
public String getFilename() {
return file.getName();
}
public Path getPath() {
return Path.of(file.getPath());
}
public String getAbsPathStr() {
return file.getAbsolutePath();
}
public String getRelPathStr() {
return file.getPath();
}
@Override
public void setValue(String value) {
super.setValue(value);
file = new File(value);
if (!exists()) {
ChatUtil.error(I18nUtil.fmt(
"sputnik.filesetting.doesnotexist",
"el archivo \"%s\" no existe!".formatted(value),
value
));
}
if (!canWrite()) {
if (!exists()) {
ChatUtil.warn(I18nUtil.fmt(
"sputnik.filesetting.cannotwrite",
"no se puede escribir a \"%s\"!".formatted(value),
value
));
}
}
}
}
@@ -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,152 @@
package me.retucio.sputnik.ui.widgets.buttons.settings;
import me.retucio.sputnik.module.setting.settings.FileSetting;
import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.buttons.SettingButton;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.KeyUtil;
import me.retucio.sputnik.util.MiscUtil;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.resources.language.I18n;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import java.awt.*;
import java.nio.file.Path;
public class FileButton extends SettingButton<FileSetting> {
private boolean typing;
private final FileSetting setting;
private final StringBuilder buffer = new StringBuilder();
public FileButton(FileSetting setting, SettingsPanel parent, int offset) {
super(setting, parent, offset);
this.setting = setting;
}
@Override
public void render(GuiGraphicsExtractor gui, int mouseX, int mouseY, float delta) {
// fondo
int bgColor = isHovered(mouseX, mouseY)
? Colors.buttonColor.brighter().getRGB()
: Colors.buttonColor.getRGB();
gui.fill(x, y, x + w - 20, y + h, bgColor);
// botón de navegar
int browseButtonColor = isBrowseButtonHovered(mouseX, mouseY)
? Colors.buttonColor.brighter().getRGB()
: Colors.buttonColor.getRGB();
gui.fill(x + w - 20, y, x + w, y + h, browseButtonColor);
gui.text(mc.font, "...", x + w - 20 + (20 - mc.font.width("...")) / 2, y + 3, -1, true);
// texto
String label = setting.getName() + ": " + (typing ? buffer + "_" : setting.getValue());
gui.text(mc.font, label, x + 5, y + 3, -1, true);
}
@Override
public void mouseClicked(int mouseX, int mouseY, int button) {
if (isHovered(mouseX, mouseY) && ClickGui.INSTANCE.trySelect(this)) {
if (button == 0) {
typing = true;
buffer.setLength(0);
buffer.append(setting.getValue()); // precargar valor anterior
} else if (button == 1 && KeyUtil.isShiftDown()) {
typing = false;
setting.reset();
}
} else {
typing = false;
if (isBrowseButtonHovered(mouseX, mouseY)) {
Path path = pickFile();
if (path != null) {
setting.setValue(path.toString());
}
}
}
}
@Override
public void onKey(int key, int action) {
if (!typing || action == GLFW.GLFW_RELEASE) return;
// pegar con ctrl + v
if (key == GLFW.GLFW_KEY_V && KeyUtil.isKeyDown(GLFW.GLFW_KEY_LEFT_CONTROL)) {
paste();
return;
}
switch (key) { // casos para teclas especiales como enter, escape o borrar
case GLFW.GLFW_KEY_ENTER -> {
setting.setValue(buffer.toString());
typing = false;
} case GLFW.GLFW_KEY_BACKSPACE -> {
onBackspace();
} case GLFW.GLFW_KEY_ESCAPE -> {
typing = false;
} case GLFW.GLFW_KEY_SPACE -> {
charTyped(' ');
} default -> {
String c = KeyUtil.getKeyName(key);
if (c.length() == 1) {
if (KeyUtil.isShiftDown())
charTyped(KeyUtil.shiftKey(c).charAt(0));
else
charTyped(c.toLowerCase().charAt(0));
}
}
}
}
private void charTyped(char c) {
if (!typing) return;
buffer.append(c);
if (mc.font.width(setting.getName() + ": " + buffer + "_") > w - 8) {
onBackspace();
}
}
public boolean isFocused() {
return typing;
}
public void onBackspace() {
MiscUtil.backspace(buffer);
}
public void paste() {
buffer.append(MiscUtil.getPasteContent(100));
}
private boolean isBrowseButtonHovered(int mx, int my) {
return mx >= x + w - 20 && mx <= x + w
&& my >= y && my <= y + h;
}
@Override
public boolean isHovered(int mx, int my) {
return mx > x && mx < x + w - 20
&& my > y && my <= y + h;
}
@Nullable
public Path pickFile() {
String result = TinyFileDialogs.tinyfd_openFileDialog(
I18n.get("sputnik.filesetting.choose"),
setting.isDirectory()
? setting.getValue()
: setting.getPath().getParent().toString(),
null,
I18n.get("sputnik.filesetting.supportedfiles"),
false);
return result == null ? null : Path.of(result);
}
}
@@ -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; }
}
@@ -221,6 +221,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
case SliderButton sliderButton -> text += ": " + sliderButton.df.format((sliderButton.getSetting()).getValue()); case SliderButton sliderButton -> text += ": " + sliderButton.df.format((sliderButton.getSetting()).getValue());
case CycleButton<?> cycleButton -> text += ": " + cycleButton.getSetting().getValue(); case CycleButton<?> cycleButton -> text += ": " + cycleButton.getSetting().getValue();
case BindButton bindButton -> text += ": " + bindButton.getSetting().getKeyName(); case BindButton bindButton -> text += ": " + bindButton.getSetting().getKeyName();
case FileButton fileButton -> text += ": " + fileButton.getSetting().getValue() + " ...";
case TextButton textButton -> text += ": " + textButton.getSetting().getValue(); case TextButton textButton -> text += ": " + textButton.getSetting().getValue();
case ColorButton colorButton -> text += colorButton.getSetting().getTooltipText(); case ColorButton colorButton -> text += colorButton.getSetting().getTooltipText();
default -> {} default -> {}
@@ -307,6 +308,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
case BooleanSetting b -> new ToggleButton(b, this, offset); case BooleanSetting b -> new ToggleButton(b, this, offset);
case NumberSetting n -> new SliderButton(n, this, offset); case NumberSetting n -> new SliderButton(n, this, offset);
case EnumSetting<?> e -> new CycleButton<>(e, this, offset); case EnumSetting<?> e -> new CycleButton<>(e, this, offset);
case FileSetting f -> new FileButton(f, this, offset);
case KeySetting k -> new BindButton(k, this, offset); case KeySetting k -> new BindButton(k, this, offset);
case StringSetting s -> new TextButton(s, this, offset); case StringSetting s -> new TextButton(s, this, offset);
case ListSetting<?> l -> new ListButton<>(l, this, offset); case ListSetting<?> l -> new ListButton<>(l, this, offset);
@@ -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,16 @@ 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 Packet<?>>> packetList = new ArrayList<>();
public static Map<PacketType<? extends @NonNull Packet<?>>, String> packetNames = new HashMap<>();
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 +93,36 @@ 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());
}));
packetList.addAll(c2sPacketList);
packetList.addAll(s2cPacketList);
packetNames.putAll(c2sPacketNames);
packetNames.putAll(s2cPacketNames);
} }
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",
@@ -116,7 +135,7 @@
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorytargets.entities": "entities", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorytargets.entities": "entities",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorytargets.none": "none", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorytargets.none": "none",
"sputnik.enum.module.modules.misc.anvilfont.fontmode.circled": "round", "sputnik.enum.module.modules.misc.anvilfont.fontmode.circled": "round",
"sputnik.enum.module.modules.misc.anvilfont.fontmode.cursed": "well weird", "sputnik.enum.module.modules.misc.anvilfont.fontmode.cursed": "cursed",
"sputnik.enum.module.modules.misc.anvilfont.fontmode.greek": "greek", "sputnik.enum.module.modules.misc.anvilfont.fontmode.greek": "greek",
"sputnik.enum.module.modules.misc.anvilfont.fontmode.normal": "normal", "sputnik.enum.module.modules.misc.anvilfont.fontmode.normal": "normal",
"sputnik.enum.module.modules.misc.anvilfont.fontmode.uppercase": "uppercase", "sputnik.enum.module.modules.misc.anvilfont.fontmode.uppercase": "uppercase",
@@ -195,6 +214,10 @@
"sputnik.enum.module.modules.world.timechanger.moonphases.waning_gibbous": "waning gibbous", "sputnik.enum.module.modules.world.timechanger.moonphases.waning_gibbous": "waning gibbous",
"sputnik.enum.module.modules.world.timechanger.moonphases.waxing_crescent": "waxing crescent", "sputnik.enum.module.modules.world.timechanger.moonphases.waxing_crescent": "waxing crescent",
"sputnik.enum.module.modules.world.timechanger.moonphases.waxing_gibbous": "waxing gibbous", "sputnik.enum.module.modules.world.timechanger.moonphases.waxing_gibbous": "waxing gibbous",
"sputnik.filesetting.cannotwrite": "cannot write to \"%s\"!",
"sputnik.filesetting.choose": "choose a file",
"sputnik.filesetting.supportedfiles": "supported files",
"sputnik.filesetting.doesnotexist": "file \"%s\" does not exist!",
"sputnik.group.andamios.bloques": "blocks", "sputnik.group.andamios.bloques": "blocks",
"sputnik.group.andamios.puente": "bridge", "sputnik.group.andamios.puente": "bridge",
"sputnik.group.andamios.renderizado": "render", "sputnik.group.andamios.renderizado": "render",
@@ -351,7 +374,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",
@@ -376,9 +399,9 @@
"sputnik.module.cocaina.desc": "changes the speed of everything in the game", "sputnik.module.cocaina.desc": "changes the speed of everything in the game",
"sputnik.module.cocaina.name": "cocaine", "sputnik.module.cocaina.name": "cocaine",
"sputnik.module.colocarenaire.desc": "lets you place blocks in the air", "sputnik.module.colocarenaire.desc": "lets you place blocks in the air",
"sputnik.module.colocarenaire.name": "place in air", "sputnik.module.colocarenaire.name": "airplace",
"sputnik.module.conejo.desc": "do the elytra bounce by holding space", "sputnik.module.conejo.desc": "do the elytra bounce by holding space",
"sputnik.module.conejo.name": "rabbit", "sputnik.module.conejo.name": "ebounce",
"sputnik.module.confetti.desc": "changes the color of the totem pop particles", "sputnik.module.confetti.desc": "changes the color of the totem pop particles",
"sputnik.module.confetti.name": "confetti", "sputnik.module.confetti.name": "confetti",
"sputnik.module.contorno.desc": "customizes the block outline", "sputnik.module.contorno.desc": "customizes the block outline",
@@ -387,13 +410,13 @@
"sputnik.module.criticos.name": "crits", "sputnik.module.criticos.name": "crits",
"sputnik.module.delaydepaquetes.desc": "applies a delay to packets", "sputnik.module.delaydepaquetes.desc": "applies a delay to packets",
"sputnik.module.delaydepaquetes.name": "packet delay", "sputnik.module.delaydepaquetes.name": "packet delay",
"sputnik.module.delfin.desc": "*dolphin noise*", "sputnik.module.delfin.desc": "*dolphin noises*",
"sputnik.module.delfin.name": "dolphin", "sputnik.module.delfin.name": "dolphin",
"sputnik.module.destellodeenchants.desc": "changes the color of the enchantment glint", "sputnik.module.destellodeenchants.desc": "changes the color of the enchantment glint",
"sputnik.module.destellodeenchants.name": "enchant glint", "sputnik.module.destellodeenchants.name": "enchant glint",
"sputnik.module.desyncdehitbox.desc": "chinese exploit", "sputnik.module.desyncdehitbox.desc": "chinese exploit",
"sputnik.module.desyncdehitbox.name": "hitbox desync", "sputnik.module.desyncdehitbox.name": "hitbox desync",
"sputnik.module.discordrpc.desc": "customizes your activity on dishcord", "sputnik.module.discordrpc.desc": "customizes your activity on dickord",
"sputnik.module.discordrpc.disconnected": "disconnected: %s - %s", "sputnik.module.discordrpc.disconnected": "disconnected: %s - %s",
"sputnik.module.discordrpc.error": "error: %s - %s", "sputnik.module.discordrpc.error": "error: %s - %s",
"sputnik.module.discordrpc.initerror": "error initializing rpc: %s", "sputnik.module.discordrpc.initerror": "error initializing rpc: %s",
@@ -407,7 +430,7 @@
"sputnik.module.dupedebundle.desc": "dupes items using a bundle and a book", "sputnik.module.dupedebundle.desc": "dupes items using a bundle and a book",
"sputnik.module.dupedebundle.name": "bundle dupe", "sputnik.module.dupedebundle.name": "bundle dupe",
"sputnik.module.elytras.desc": "fly with elytra", "sputnik.module.elytras.desc": "fly with elytra",
"sputnik.module.elytras.name": "elytra", "sputnik.module.elytras.name": "elytrafly",
"sputnik.module.escalerasveloces.desc": "climb ladders faster", "sputnik.module.escalerasveloces.desc": "climb ladders faster",
"sputnik.module.escalerasveloces.name": "fast ladders", "sputnik.module.escalerasveloces.name": "fast ladders",
"sputnik.module.escalones.desc": "makes the step-up height whatever you want", "sputnik.module.escalones.desc": "makes the step-up height whatever you want",
@@ -446,6 +469,8 @@
"sputnik.module.keypearl.nopearls": "you have no pearls", "sputnik.module.keypearl.nopearls": "you have no pearls",
"sputnik.module.lsd.desc": "what the actual FUCK", "sputnik.module.lsd.desc": "what the actual FUCK",
"sputnik.module.lsd.name": "lsd", "sputnik.module.lsd.name": "lsd",
"sputnik.module.loggerdepaquetes.name": "packet logger",
"sputnik.module.loggerdepaquetes.desc": "logs packets",
"sputnik.module.macekill.oops": "oops: %s", "sputnik.module.macekill.oops": "oops: %s",
"sputnik.module.manos.desc": "changes the way hands are rendered", "sputnik.module.manos.desc": "changes the way hands are rendered",
"sputnik.module.manos.name": "hands", "sputnik.module.manos.name": "hands",
@@ -473,6 +498,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,14 +511,16 @@
"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",
"sputnik.module.progresodeminado.name": "mining progress", "sputnik.module.progresodeminado.name": "mining progress",
"sputnik.module.puntosdedesconexion.desc": "shows you the spots where other players log out", "sputnik.module.puntosdedesconexion.desc": "shows you the spots where other players log out",
"sputnik.module.puntosdedesconexion.name": "logout spots", "sputnik.module.puntosdedesconexion.name": "logout spots",
"sputnik.module.racismo.desc": "gimme a smoke bro", "sputnik.module.putogordo.name": "fatass",
"sputnik.moduke.putogordo.desc": "automatically eats when hungry",
"sputnik.module.racismo.desc": "detroit moment",
"sputnik.module.racismo.name": "racism", "sputnik.module.racismo.name": "racism",
"sputnik.module.reconectar.desc": "adds a button to reconnect to a server after being disconnected", "sputnik.module.reconectar.desc": "adds a button to reconnect to a server after being disconnected",
"sputnik.module.reconectar.name": "reconnect", "sputnik.module.reconectar.name": "reconnect",
@@ -501,9 +530,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 +584,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",
@@ -719,6 +751,10 @@
"sputnik.setting.camaralibre.velocidad.tecladelscroll.name": "scroll key", "sputnik.setting.camaralibre.velocidad.tecladelscroll.name": "scroll key",
"sputnik.setting.camaralibre.velocidad.velocidad.desc": "camera movement speed", "sputnik.setting.camaralibre.velocidad.velocidad.desc": "camera movement speed",
"sputnik.setting.camaralibre.velocidad.velocidad.name": "speed", "sputnik.setting.camaralibre.velocidad.velocidad.name": "speed",
"sputnik.setting.canceladordepaquetes.general.c2s.name": "C2S",
"sputnik.setting.canceladordepaquetes.general.c2s.desc": "serverbound packets to cancel",
"sputnik.setting.canceladordepaquetes.general.s2c.name": "S2C",
"sputnik.setting.canceladordepaquetes.general.s2c.desc": "clientbound packets to cancel",
"sputnik.setting.capas.general.capa.desc": "which cape to wear", "sputnik.setting.capas.general.capa.desc": "which cape to wear",
"sputnik.setting.capas.general.capa.name": "cape", "sputnik.setting.capas.general.capa.name": "cape",
"sputnik.setting.capturasdepantalla.botones.botondeabrir.desc": "show a button to open the screenshot file", "sputnik.setting.capturasdepantalla.botones.botondeabrir.desc": "show a button to open the screenshot file",
@@ -983,6 +1019,12 @@
"sputnik.setting.jugadorfalso.general.nombre.desc": "what name to give the player", "sputnik.setting.jugadorfalso.general.nombre.desc": "what name to give the player",
"sputnik.setting.jugadorfalso.general.nombre.name": "name", "sputnik.setting.jugadorfalso.general.nombre.name": "name",
"sputnik.setting.key.reset": "keybind for module %s reset", "sputnik.setting.key.reset": "keybind for module %s reset",
"sputnik.setting.loggerdepaquetes.general.paquetes.name": "packets",
"sputnik.setting.loggerdepaquetes.general.paquetes.desc": "packets to log",
"sputnik.setting.loggerdepaquetes.general.mododelogueo.name": "logging mode",
"sputnik.setting.loggerdepaquetes.general.mododelogueo.desc": "how to log packets",
"sputnik.setting.loggerdepaquetes.general.archivo.name": "file",
"sputnik.setting.loggerdepaquetes.general.archivo.desc": "file to use as log file",
"sputnik.setting.lsd.general.intensidad.desc": "how much the image is distorted and tinted", "sputnik.setting.lsd.general.intensidad.desc": "how much the image is distorted and tinted",
"sputnik.setting.lsd.general.intensidad.name": "strength", "sputnik.setting.lsd.general.intensidad.name": "strength",
"sputnik.setting.lsd.general.rotaciones.desc": "how much the offset swirls; higher = more whirls", "sputnik.setting.lsd.general.rotaciones.desc": "how much the offset swirls; higher = more whirls",
@@ -1257,6 +1299,8 @@
"sputnik.setting.puntosdedesconexion.relleno.colordelrelleno.name": "fill color", "sputnik.setting.puntosdedesconexion.relleno.colordelrelleno.name": "fill color",
"sputnik.setting.puntosdedesconexion.relleno.relleno.desc": "render the box fill (faces)", "sputnik.setting.puntosdedesconexion.relleno.relleno.desc": "render the box fill (faces)",
"sputnik.setting.puntosdedesconexion.relleno.relleno.name": "fill", "sputnik.setting.puntosdedesconexion.relleno.relleno.name": "fill",
"sputnik.setting.putogordo.hambre.name": "hunger",
"sputnik.setting.putogordo.hambre.desc": "how hungry you have to be to eat (food points)",
"sputnik.setting.recorrido.general.color.desc": "color to use", "sputnik.setting.recorrido.general.color.desc": "color to use",
"sputnik.setting.recorrido.general.color.name": "color", "sputnik.setting.recorrido.general.color.name": "color",
"sputnik.setting.recorrido.general.esperanzadevida.desc": "how long the trails last", "sputnik.setting.recorrido.general.esperanzadevida.desc": "how long the trails last",
@@ -1452,10 +1496,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",
@@ -195,6 +214,10 @@
"sputnik.enum.module.modules.world.timechanger.moonphases.waning_gibbous": "menguante gibosa", "sputnik.enum.module.modules.world.timechanger.moonphases.waning_gibbous": "menguante gibosa",
"sputnik.enum.module.modules.world.timechanger.moonphases.waxing_crescent": "creciente", "sputnik.enum.module.modules.world.timechanger.moonphases.waxing_crescent": "creciente",
"sputnik.enum.module.modules.world.timechanger.moonphases.waxing_gibbous": "creciente gibosa", "sputnik.enum.module.modules.world.timechanger.moonphases.waxing_gibbous": "creciente gibosa",
"sputnik.filesetting.doesnotexist": "el archivo \"%s\" no existe!",
"sputnik.filesetting.cannotwrite": "no se puede escribir a \"%s\"!",
"sputnik.filesetting.choose": "elige un archivo",
"sputnik.filesetting.supportedfiles": "archivos soportados",
"sputnik.group.andamios.bloques": "bloques", "sputnik.group.andamios.bloques": "bloques",
"sputnik.group.andamios.puente": "puente", "sputnik.group.andamios.puente": "puente",
"sputnik.group.andamios.renderizado": "renderizado", "sputnik.group.andamios.renderizado": "renderizado",
@@ -446,6 +469,8 @@
"sputnik.module.keypearl.nopearls": "no tienes perlas", "sputnik.module.keypearl.nopearls": "no tienes perlas",
"sputnik.module.lsd.desc": "distorsiona y tiñe la pantalla", "sputnik.module.lsd.desc": "distorsiona y tiñe la pantalla",
"sputnik.module.lsd.name": "lsd", "sputnik.module.lsd.name": "lsd",
"sputnik.module.loggerdepaquetes.name": "logger de paquetes",
"sputnik.module.loggerdepaquetes.desc": "loguea paquetes",
"sputnik.module.macekill.oops": "upsi: %s", "sputnik.module.macekill.oops": "upsi: %s",
"sputnik.module.manos.desc": "modifica la manera en la que se renderizan las manos", "sputnik.module.manos.desc": "modifica la manera en la que se renderizan las manos",
"sputnik.module.manos.name": "manos", "sputnik.module.manos.name": "manos",
@@ -473,6 +498,8 @@
"sputnik.module.notificaciones.name": "notificaciones", "sputnik.module.notificaciones.name": "notificaciones",
"sputnik.module.packetdelay.singleplayer": "delay de paquetes no funciona en un solo jugador", "sputnik.module.packetdelay.singleplayer": "delay de paquetes no funciona en un solo jugador",
"sputnik.module.packetmine.pickbreaking": "pico por romperse", "sputnik.module.packetmine.pickbreaking": "pico por romperse",
"sputnik.module.canceladordepaquetes.name": "cancelador de paquetes",
"sputnik.module.canceladordepaquetes.desc": "cancela paquetes.",
"sputnik.module.parkour.desc": "salta automáticamente en el último tick posible, en la esquina del bloque", "sputnik.module.parkour.desc": "salta automáticamente en el último tick posible, en la esquina del bloque",
"sputnik.module.parkour.name": "parkour", "sputnik.module.parkour.name": "parkour",
"sputnik.module.particulas.desc": "emite particulas artificiales", "sputnik.module.particulas.desc": "emite particulas artificiales",
@@ -491,6 +518,8 @@
"sputnik.module.progresodeminado.name": "progreso de minado", "sputnik.module.progresodeminado.name": "progreso de minado",
"sputnik.module.puntosdedesconexion.desc": "te muestra los puntos donde otros jugadores se desconectan", "sputnik.module.puntosdedesconexion.desc": "te muestra los puntos donde otros jugadores se desconectan",
"sputnik.module.puntosdedesconexion.name": "puntos de desconexión", "sputnik.module.puntosdedesconexion.name": "puntos de desconexión",
"sputnik.module.putogordo.name": "puto gordo",
"sputnik.moduke.putogordo.desc": "come automáticamente al tener hambre",
"sputnik.module.racismo.desc": "amego segarro", "sputnik.module.racismo.desc": "amego segarro",
"sputnik.module.racismo.name": "racismo", "sputnik.module.racismo.name": "racismo",
"sputnik.module.reconectar.desc": "añade un botón para reconectarse a un servidor tras ser desconectado", "sputnik.module.reconectar.desc": "añade un botón para reconectarse a un servidor tras ser desconectado",
@@ -555,6 +584,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",
@@ -719,6 +751,10 @@
"sputnik.setting.camaralibre.velocidad.tecladelscroll.name": "tecla del scroll", "sputnik.setting.camaralibre.velocidad.tecladelscroll.name": "tecla del scroll",
"sputnik.setting.camaralibre.velocidad.velocidad.desc": "velocidad de movimiento de la cámara", "sputnik.setting.camaralibre.velocidad.velocidad.desc": "velocidad de movimiento de la cámara",
"sputnik.setting.camaralibre.velocidad.velocidad.name": "velocidad", "sputnik.setting.camaralibre.velocidad.velocidad.name": "velocidad",
"sputnik.setting.canceladordepaquetes.general.c2s.name": "C2S",
"sputnik.setting.canceladordepaquetes.general.c2s.desc": "paquetes a cancelar, dirección servidor",
"sputnik.setting.canceladordepaquetes.general.s2c.name": "S2C",
"sputnik.setting.canceladordepaquetes.general.s2c.desc": "paquetes a cancelar, dirección cliente",
"sputnik.setting.capas.general.capa.desc": "qué capa llevar puesta", "sputnik.setting.capas.general.capa.desc": "qué capa llevar puesta",
"sputnik.setting.capas.general.capa.name": "capa", "sputnik.setting.capas.general.capa.name": "capa",
"sputnik.setting.capturasdepantalla.botones.botondeabrir.desc": "mostrar botón para abrir el archivo de la captura", "sputnik.setting.capturasdepantalla.botones.botondeabrir.desc": "mostrar botón para abrir el archivo de la captura",
@@ -983,6 +1019,12 @@
"sputnik.setting.jugadorfalso.general.nombre.desc": "qué nombre asignarle al jugador", "sputnik.setting.jugadorfalso.general.nombre.desc": "qué nombre asignarle al jugador",
"sputnik.setting.jugadorfalso.general.nombre.name": "nombre", "sputnik.setting.jugadorfalso.general.nombre.name": "nombre",
"sputnik.setting.key.reset": "tecla del módulo %s restablecida", "sputnik.setting.key.reset": "tecla del módulo %s restablecida",
"sputnik.setting.loggerdepaquetes.general.paquetes.name": "paquetes",
"sputnik.setting.loggerdepaquetes.general.paquetes.desc": "paquetes a loguear",
"sputnik.setting.loggerdepaquetes.general.mododelogueo.name": "modo de logueo",
"sputnik.setting.loggerdepaquetes.general.mododelogueo.desc": "de qué manera loguear los paquetes",
"sputnik.setting.loggerdepaquetes.general.archivo.name": "archivo",
"sputnik.setting.loggerdepaquetes.general.archivo.desc": "archivo al que loguear los paquetes",
"sputnik.setting.lsd.general.intensidad.desc": "cuánto se distorsiona y se tiñe la imagen", "sputnik.setting.lsd.general.intensidad.desc": "cuánto se distorsiona y se tiñe la imagen",
"sputnik.setting.lsd.general.intensidad.name": "intensidad", "sputnik.setting.lsd.general.intensidad.name": "intensidad",
"sputnik.setting.lsd.general.rotaciones.desc": "cuántas vueltas da el desplazamiento; más = más remolinos", "sputnik.setting.lsd.general.rotaciones.desc": "cuántas vueltas da el desplazamiento; más = más remolinos",
@@ -1257,6 +1299,8 @@
"sputnik.setting.puntosdedesconexion.relleno.colordelrelleno.name": "color del relleno", "sputnik.setting.puntosdedesconexion.relleno.colordelrelleno.name": "color del relleno",
"sputnik.setting.puntosdedesconexion.relleno.relleno.desc": "renderizar relleno de la caja (caras)", "sputnik.setting.puntosdedesconexion.relleno.relleno.desc": "renderizar relleno de la caja (caras)",
"sputnik.setting.puntosdedesconexion.relleno.relleno.name": "relleno", "sputnik.setting.puntosdedesconexion.relleno.relleno.name": "relleno",
"sputnik.setting.putogordo.hambre.name": "hambre",
"sputnik.setting.putogordo.hambre.desc": "cuánta hambre es necesaria para comer (puntos de comida)",
"sputnik.setting.recorrido.general.color.desc": "color a usar", "sputnik.setting.recorrido.general.color.desc": "color a usar",
"sputnik.setting.recorrido.general.color.name": "color", "sputnik.setting.recorrido.general.color.name": "color",
"sputnik.setting.recorrido.general.esperanzadevida.desc": "cuánto duran los trazos", "sputnik.setting.recorrido.general.esperanzadevida.desc": "cuánto duran los trazos",
@@ -1452,10 +1496,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",
@@ -7,10 +7,6 @@ uniform sampler2D InSampler;
in vec2 texCoord; in vec2 texCoord;
out vec4 fragColor; out vec4 fragColor;
// 26.2 pasó los samplers con nombre a bind groups; los uniforms van en un bloque std140
// que rellena LsdPostProcessor cada frame. el tiempo se lleva aquí en vez de leerlo de
// Globals.GameTime porque ese es una fracción del día (se reinicia cada 24000 ticks) y
// animaría la distorsión lentísimo con un salto cada 20 minutos
layout(std140) uniform LsdConfig { layout(std140) uniform LsdConfig {
float strength; float strength;
float seed; float seed;