This commit is contained in:
retucio committed 2026-08-18 13:35:24 +02:00
1 parent 81fc25c33d
commit e0f8523809
29 files changed
+502 -181

No files matched your search

+14 -3
View File
@@ -6,13 +6,24 @@
#### módulos nuevos: #### módulos nuevos:
#### comandos nuevos: - autoataque: mantén el botón de ataque pulsado para atacar automáticamente
- `$panorama`: construye un pack de texturas con una panorámica basada en tu posición actual. #### comandos nuevos:
### arreglos / mejoras ### arreglos / mejoras
- arreglado ScreenshotMixin, es decir, que ya no crasheará al tomar capturas - 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.5 mod_version=5.6
maven_group=me.retucio maven_group=me.retucio
archives_base_name=sputnik archives_base_name=sputnik
@@ -135,11 +135,8 @@ public class Sputnik implements ClientModInitializer {
// se ejecuta cada tick, es decir, 20 veces por segundo // se ejecuta cada tick, es decir, 20 veces por segundo
public void onTick() { public void onTick() {
if (!settingsApplied if (!settingsApplied && !ConfigManager.INSTANCE.hasLoaded()) {
&& ConfigManager.INSTANCE.getConfig() != null settingsApplied = ConfigManager.INSTANCE.applyConfig();
&& !ConfigManager.INSTANCE.hasLoaded()) {
ConfigManager.INSTANCE.applyConfig();
settingsApplied = true;
} }
ModuleManager.INSTANCE.getEnabledModules().forEach(Module::onTick); ModuleManager.INSTANCE.getEnabledModules().forEach(Module::onTick);
@@ -161,7 +158,7 @@ public class Sputnik implements ClientModInitializer {
if (BindCommand.onKeyPress(key)) return; if (BindCommand.onKeyPress(key)) return;
if (action == GLFW.GLFW_PRESS) { if (action == GLFW.GLFW_PRESS) {
handleModuleToggle(key, anyFocused); handleModuleToggle(key, anyFocused);
handleClickGUIKey(key, anyFocused); handleClickGuiKey(key, anyFocused);
handleHudEditorKey(key, anyFocused); handleHudEditorKey(key, anyFocused);
} }
// solo reenviar teclas del teclado // solo reenviar teclas del teclado
@@ -208,7 +205,7 @@ public class Sputnik implements ClientModInitializer {
} }
// maneja la lógica de apertura de la interfaz // maneja la lógica de apertura de la interfaz
private void handleClickGUIKey(int key, boolean anyFocused) { private void handleClickGuiKey(int key, boolean anyFocused) {
if (key != ClientSettingsPanel.clientSettings.getKey() || anyFocused || isOnTypingScreen()) if (key != ClientSettingsPanel.clientSettings.getKey() || anyFocused || isOnTypingScreen())
return; return;
@@ -221,7 +218,7 @@ public class Sputnik implements ClientModInitializer {
} }
} }
// manejar la lógica de apertura del editor de elementos del hud, con la misma lógica que handleClickGUIKey() // manejar la lógica de apertura del editor de elementos del hud, con la misma lógica que handleClickGuiKey()
private void handleHudEditorKey(int key, boolean anyFocused) { private void handleHudEditorKey(int key, boolean anyFocused) {
Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class); Hud hud = ModuleManager.INSTANCE.getModuleByClass(Hud.class);
if (key != hud.editorKey.getValue() || anyFocused || isOnTypingScreen()) return; if (key != hud.editorKey.getValue() || anyFocused || isOnTypingScreen()) return;
@@ -1,6 +1,6 @@
package me.retucio.sputnik.config; package me.retucio.sputnik.config;
import net.minecraft.network.chat.Component; import me.retucio.sputnik.util.I18nUtil;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -85,44 +85,59 @@ public class BinarySerializer {
public static ClientConfig readConfig(File file) public static ClientConfig readConfig(File file)
throws IOException { throws IOException {
if (!file.exists() || file.length() == 0) { if (!file.exists() || file.length() == 0) {
throw new IOException(Component.translatable("sputnik.config.error.empty").getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.empty",
"archivo de config. vacío o inexistente"));
} }
try (DataInputStream in = new DataInputStream( try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(file)))) { new BufferedInputStream(new FileInputStream(file)))) {
if (file.length() < 8) { if (file.length() < 8) {
throw new IOException(Component.translatable("sputnik.config.error.toosmall").getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.toosmall",
"archivo demasiado pequeño para ser válido"));
} }
// verificar cabezal // verificar cabezal
int magic = in.readInt(); int magic = in.readInt();
if (magic != MAGIC_NUMBER) { if (magic != MAGIC_NUMBER) {
throw new IOException(Component.translatable("sputnik.config.error.invalidformat", magic).getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.invalidformat",
"formato de archivo inválido: %s", magic));
} }
// todo: sustituir por lógica de migración en vez de simplemente no aceptarlo // 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 != VERSION) {
throw new IOException(Component.translatable("sputnik.config.error.invalidversion", version).getString()); throw new IOException(I18nUtil.fmt("sputnik.config.error.invalidversion",
"versión inválida: %s", version));
} }
ClientConfig config = new ClientConfig(); // leer mapas. se leen a locales y la ClientConfig se construye al final: su
// constructor la suscribe al bus de eventos, así que crearla antes de terminar
// leer mapas // de leer dejaba una config. huérfana escuchando si el archivo estaba corrupto
config.moduleStates = readStringBooleanMap(in); Map<String, Boolean> moduleStates = readStringBooleanMap(in);
config.settings = readSettingsMap(in); Map<String, Object> settings = readSettingsMap(in);
config.settingsPanels = readStringIntArrayMap(in); Map<String, int[]> settingsPanels = readStringIntArrayMap(in);
config.extendablePanels = readExtendablePanels(in); Map<String, ClientConfig.PanelData> extendablePanels = readExtendablePanels(in);
config.hudPositions = readStringIntArrayMap(in); Map<String, int[]> hudPositions = readStringIntArrayMap(in);
config.hudVisibilities = readStringBooleanMap(in); Map<String, Boolean> hudVisibilities = readStringBooleanMap(in);
config.hudImagePaths = readStringStringMap(in); Map<String, String> hudImagePaths = readStringStringMap(in);
config.friends = readStringStringMap(in); Map<String, String> friends = readStringStringMap(in);
// leer pos. de la barra de búsqueda // leer pos. de la barra de búsqueda
if (in.readBoolean()) { int[] searchBarPosition = in.readBoolean()
config.searchBarPosition = new int[]{in.readInt(), in.readInt()}; ? new int[]{in.readInt(), in.readInt()}
} : null;
ClientConfig config = new ClientConfig();
config.moduleStates = moduleStates;
config.settings = settings;
config.settingsPanels = settingsPanels;
config.extendablePanels = extendablePanels;
config.hudPositions = hudPositions;
config.hudVisibilities = hudVisibilities;
config.hudImagePaths = hudImagePaths;
config.friends = friends;
if (searchBarPosition != null) config.searchBarPosition = searchBarPosition;
return config; return config;
} }
@@ -71,7 +71,7 @@ public class ClientConfig {
case StringSetting s: value = s.getValue(); break; case StringSetting s: value = s.getValue(); break;
case ListSetting l: value = l.getConfigValues(); break; case ListSetting l: value = l.getConfigValues(); break;
case ColorSetting c: value = c.getConfigValue(); break; case ColorSetting c: value = c.getConfigValue(); break;
case OptionSetting o: value = o.getDisplayName(); break; case OptionSetting o: value = o.getConfigName(); break;
default: break; default: break;
} }
@@ -19,6 +19,7 @@ import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel; import me.retucio.sputnik.ui.widgets.panels.settings.ClientSettingsPanel;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.util.ChatUtil; import me.retucio.sputnik.util.ChatUtil;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.io.File; import java.io.File;
@@ -30,7 +31,7 @@ import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
// se ocupa de guardar, cargar y aplicar ajustes // se ocupa de guardar, cargar y aplicar ajustes
@@ -42,8 +43,9 @@ public class ConfigManager {
public static ConfigManager INSTANCE; public static ConfigManager INSTANCE;
private boolean loaded = false; private boolean loaded = false;
private boolean applying = false;
private boolean dirty = false; private boolean dirty = false;
private long lastDirtyTime = 0L; private long lastSaveTime = System.currentTimeMillis();
private ClientConfig config = null; private ClientConfig config = null;
private final ExecutorService saveExecutor = Executors.newSingleThreadExecutor(r -> { private final ExecutorService saveExecutor = Executors.newSingleThreadExecutor(r -> {
@@ -52,7 +54,10 @@ public class ConfigManager {
return t; return t;
}); });
private final AtomicBoolean saveQueued = new AtomicBoolean(false); // instantánea pendiente de escribir. si llega otra antes de que el hilo de fondo escriba,
// sustituye a la anterior en vez de descartarse (que era lo que pasaba con un simple
// "ya hay uno en cola": se tiraba justo la más reciente y se perdían esos cambios)
private final AtomicReference<byte[]> pending = new AtomicReference<>();
// ------------------ MÉTODOS PRINCIPALES ------------------ // ------------------ MÉTODOS PRINCIPALES ------------------
@@ -60,58 +65,81 @@ public class ConfigManager {
// guardar configuraciones // guardar configuraciones
public void save() { public void save() {
if (!loaded) return; if (!loaded) return;
ensureConfig(); writeSnapshot(false);
// serializar a bytes en el hilo principal (snapshot inmediato, evita race conditions)
final byte[] snapshot;
try {
snapshot = BinarySerializer.toBytes(config);
} catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.serialization").getString(), e);
return;
}
// si ya hay un guardado en cola, no añadir otro
if (!saveQueued.compareAndSet(false, true)) return;
saveExecutor.execute(() -> {
try {
BinarySerializer.writeBytes(snapshot, CONFIG_FILE);
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.saved").getString());
} catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.saving").getString(), e);
} finally {
saveQueued.set(false);
}
});
} }
public void saveOnShutdown() { public void saveOnShutdown() {
// esperar a que termine cualquier guardado en curso, luego guardar síncronamente // no aceptar más tareas y esperar a que termine el guardado en curso
saveExecutor.shutdown(); saveExecutor.shutdown();
try { try {
// dar hasta 5 segundos para que termine el guardado en curso // dar hasta 5 segundos para que termine el guardado en curso
if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) { if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.thread").getString()); Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.thread",
"el hilo de guardado no terminó a tiempo"));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
// guardado final sincrónico por si había cambios pendientes no guardados // el guardado final va en este mismo hilo: el executor ya está cerrado, así que
if (dirty) save(); // encolar la escritura lanzaba RejectedExecutionException y no se guardaba NADA al
// cerrar el juego. o sea que se perdía todo lo hecho desde el último autoguardado
if (!loaded) return;
writeSnapshot(true);
}
// serializa en el hilo actual (instantánea inmediata, evita race conditions) y escribe
// en el de fondo, salvo que se pida síncrono (al cerrar)
private void writeSnapshot(boolean sync) {
ensureConfig();
final byte[] snapshot;
try {
snapshot = BinarySerializer.toBytes(config);
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.serialization",
"error al serializar ajustes"), e);
return;
}
dirty = false;
lastSaveTime = System.currentTimeMillis();
if (sync) {
writeToDisk(snapshot);
return;
}
// si ya había una instantánea esperando, la tarea encolada recogerá ésta
if (pending.getAndSet(snapshot) != null) return;
saveExecutor.execute(() -> {
byte[] bytes = pending.getAndSet(null);
if (bytes != null) writeToDisk(bytes);
});
}
private static void writeToDisk(byte[] bytes) {
try {
BinarySerializer.writeBytes(bytes, CONFIG_FILE);
Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.saved", "ajustes guardados"));
} catch (IOException e) {
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.saving",
"error al guardar ajustes"), e);
}
} }
public void load() { public void load() {
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.loading").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.loading", "cargando ajustes..."));
if (CONFIG_FILE.exists()) { if (CONFIG_FILE.exists()) {
try { try {
config = BinarySerializer.readConfig(CONFIG_FILE); setConfig(BinarySerializer.readConfig(CONFIG_FILE));
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.loaded").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.loaded", "ajustes cargados"));
return; return;
} catch (IOException e) { } catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.loading").getString(), e); Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.loading",
"error cargando ajustes, intentando json"), e);
} }
} }
@@ -119,35 +147,61 @@ public class ConfigManager {
if (LEGACY_JSON_FILE.exists()) { if (LEGACY_JSON_FILE.exists()) {
try (FileReader reader = new FileReader(LEGACY_JSON_FILE)) { try (FileReader reader = new FileReader(LEGACY_JSON_FILE)) {
Gson gson = new GsonBuilder().setPrettyPrinting().create(); Gson gson = new GsonBuilder().setPrettyPrinting().create();
config = gson.fromJson(reader, ClientConfig.class); setConfig(gson.fromJson(reader, ClientConfig.class));
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.json").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.json",
"ajustes json cargados (migración)"));
save(); // aquí `loaded` todavía es false (se pone al aplicar), así que save() no
// haría nada: la migración se escribe directamente
writeSnapshot(true);
return; return;
} catch (IOException e) { } catch (IOException e) {
Sputnik.LOGGER.error(Component.translatable("sputnik.config.error.json").getString(), e); Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.json",
"error cargando json, usando defaults"), e);
} }
} }
// crear nuevo config // crear nuevo config
ensureConfig(); ensureConfig();
save(); writeSnapshot(true);
} }
// aplicar configuraciones cargadas // aplicar configuraciones cargadas. devuelve false si todavía no se puede (falta algo por
public void applyConfig() { // construir), para que se reintente en el siguiente tick en vez de quedarse sin aplicar
if (!shouldApply()) return; public boolean applyConfig() {
if (!shouldApply()) return false;
applyModuleStates(); applying = true;
applyModuleSettings(); try {
applySettingsPanels(); // los del mod primero: entre ellos está "teclas multimódulo", que decide si al
applySearchBarPosition(); // asignar una tecla se le quita a los demás módulos. aplicándolos los últimos,
applyClientSettings(); // las teclas de los módulos se cargaban con el valor por defecto de ese ajuste
applyExtendablePanels(); applyClientSettings();
applyFriends(); applyModuleStates();
applyModuleSettings();
applySettingsPanels();
applySearchBarPosition();
applyExtendablePanels();
applyFriends();
loaded = true; Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.applied", "ajustes aplicados"));
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.applied").getString()); } catch (Exception e) {
// si algo peta a medio aplicar no se puede dejar `loaded` en false: save() no
// haría nada en toda la sesión y se perdería la config. entera al cerrar
Sputnik.LOGGER.error(I18nUtil.fmt("sputnik.config.error.applying",
"error aplicando ajustes"), e);
} finally {
applying = false;
loaded = true;
}
return true;
}
// true mientras se está cargando la config. sobre los ajustes. lo mira KeySetting para no
// tratar las teclas guardadas como si el usuario las acabara de asignar
public boolean isApplying() {
return applying;
} }
// obtener configuración actual // obtener configuración actual
@@ -213,7 +267,8 @@ public class ConfigManager {
friend.setName(name); friend.setName(name);
FriendManager.INSTANCE.add(friend); FriendManager.INSTANCE.add(friend);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.friends").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.friends",
"amigos aplicados"));
} }
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@@ -233,8 +288,8 @@ public class ConfigManager {
case ListSetting listSetting -> applyListSetting(listSetting, value); case ListSetting listSetting -> applyListSetting(listSetting, value);
case ColorSetting colorSetting -> applyColorSetting(colorSetting, value); case ColorSetting colorSetting -> applyColorSetting(colorSetting, value);
case OptionSetting optionSetting -> applyOptionSetting(optionSetting, value); case OptionSetting optionSetting -> applyOptionSetting(optionSetting, value);
default -> Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.unknownsetting", default -> Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.unknownsetting",
setting.getClass().getSimpleName()).getString()); "watafac queseso: %s", setting.getClass().getSimpleName()));
} }
} }
@@ -244,14 +299,16 @@ public class ConfigManager {
module.setEnabled(config.moduleStates.get(module.getName())); module.setEnabled(config.moduleStates.get(module.getName()));
} }
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.modulestates").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.modulestates",
"estados de módulos aplicados"));
} }
private void applyModuleSettings() { private void applyModuleSettings() {
ModuleManager.INSTANCE.getModules().forEach(module -> { ModuleManager.INSTANCE.getModules().forEach(module -> {
module.getSettings().forEach(this::applySetting); module.getSettings().forEach(this::applySetting);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.modulesettings").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.modulesettings",
"ajustes de módulos aplicados"));
} }
private void applySettingsPanels() { private void applySettingsPanels() {
@@ -259,14 +316,16 @@ public class ConfigManager {
Module module = ModuleManager.INSTANCE.getModuleByName(moduleName); Module module = ModuleManager.INSTANCE.getModuleByName(moduleName);
if (module == null) { if (module == null) {
Sputnik.LOGGER.warn(Component.translatable("sputnik.config.warn.nullmodule", moduleName).getString()); Sputnik.LOGGER.warn(I18nUtil.fmt("sputnik.config.warn.nullmodule",
"\"null\" como módulo (\"%s\")", moduleName));
return; return;
} }
ClickGui.INSTANCE.openSettingsPanel(module, position[0], position[1]); ClickGui.INSTANCE.openSettingsPanel(module, position[0], position[1]);
}); });
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.settingspanels").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.settingspanels",
"estados de marcos de ajustes aplicados"));
} }
private void applySearchBarPosition() { private void applySearchBarPosition() {
@@ -274,13 +333,15 @@ public class ConfigManager {
ClickGui.INSTANCE.getSearchBar().setX(config.searchBarPosition[0]); ClickGui.INSTANCE.getSearchBar().setX(config.searchBarPosition[0]);
ClickGui.INSTANCE.getSearchBar().setY(config.searchBarPosition[1]); ClickGui.INSTANCE.getSearchBar().setY(config.searchBarPosition[1]);
} }
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.searchbar").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.searchbar",
"posición de la barra de búsqueda aplicada"));
} }
private void applyClientSettings() { private void applyClientSettings() {
ClientSettingsPanel.clientSettings.setEnabled(true); ClientSettingsPanel.clientSettings.setEnabled(true);
ClientSettingsPanel.clientSettings.getSettings().forEach(this::applySetting); ClientSettingsPanel.clientSettings.getSettings().forEach(this::applySetting);
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.clientsettings").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.clientsettings",
"ajustes del cliente aplicados"));
} }
private void applyExtendablePanels() { private void applyExtendablePanels() {
@@ -290,7 +351,8 @@ public class ConfigManager {
applyExtendablePanel(ClickGui.INSTANCE.getClientSettingsPanel()); applyExtendablePanel(ClickGui.INSTANCE.getClientSettingsPanel());
applyExtendablePanel(ClickGui.INSTANCE.getFriendsPanel()); applyExtendablePanel(ClickGui.INSTANCE.getFriendsPanel());
Sputnik.LOGGER.info(Component.translatable("sputnik.config.info.extendablepanels").getString()); Sputnik.LOGGER.info(I18nUtil.fmt("sputnik.config.info.extendablepanels",
"posiciones de marcos extendibles aplicadas"));
ClickGui.INSTANCE.refreshListButtons(); ClickGui.INSTANCE.refreshListButtons();
} }
@@ -359,8 +421,14 @@ public class ConfigManager {
Map<T, Boolean> convertedValues = new HashMap<>(); Map<T, Boolean> convertedValues = new HashMap<>();
for (T option : setting.getOptions()) { for (T option : setting.getOptions()) {
String key = setting.getDisplayName(option); String key = setting.getConfigName(option);
Object booleanValue = map.get(key); Object booleanValue = map.get(key);
// compatibilidad con configs. de antes, cuando la clave era el nombre ya
// traducido (el de vanilla en inglés). sin esto la lista entera se quedaba
// en false, o sea que al usuario se le borraba lo que tuviera marcado
if (booleanValue == null) booleanValue = map.get(I18nUtil.vanillaName(key));
convertedValues.put(option, booleanValue instanceof Boolean && (Boolean) booleanValue); convertedValues.put(option, booleanValue instanceof Boolean && (Boolean) booleanValue);
} }
@@ -411,12 +479,24 @@ public class ConfigManager {
return setting.getSg().getModule().getName() + ":" + setting.getTrueName(); return setting.getSg().getModule().getName() + ":" + setting.getTrueName();
} }
// ClickGui incluida: los marcos y la barra de búsqueda se aplican sobre ella, y se
// construye en una tarea aparte. si todavía no está, mejor esperar al siguiente tick
private boolean shouldApply() { private boolean shouldApply() {
return Sputnik.mc != null && ModuleManager.INSTANCE != null && config != null; return Sputnik.mc != null && ModuleManager.INSTANCE != null
&& ClickGui.INSTANCE != null && config != null;
} }
private void ensureConfig() { private void ensureConfig() {
if (config == null) config = new ClientConfig(); if (config == null) setConfig(new ClientConfig());
}
// ClientConfig se suscribe al bus en su constructor, así que al reemplazarla hay que
// dar de baja a la anterior: si no, la vieja seguía escuchando y escribiendo en un
// mapa que ya no era el que se guarda
private void setConfig(ClientConfig newConfig) {
if (config == newConfig) return;
if (config != null) Sputnik.EVENT_BUS.unsubscribe(config);
config = newConfig;
} }
public boolean hasLoaded() { public boolean hasLoaded() {
@@ -425,7 +505,6 @@ public class ConfigManager {
public void markDirty() { public void markDirty() {
this.dirty = true; this.dirty = true;
this.lastDirtyTime = System.currentTimeMillis();
} }
public boolean isDirty() { public boolean isDirty() {
@@ -438,13 +517,14 @@ public class ConfigManager {
ClientSettingsModule settings = ClientSettingsPanel.clientSettings; ClientSettingsModule settings = ClientSettingsPanel.clientSettings;
if (!settings.autosave.getValue()) return; if (!settings.autosave.getValue()) return;
// desde el último guardado, no desde el último cambio: contando desde el cambio,
// tocando ajustes cada menos del intervalo no se autoguardaba nunca
long intervalMs = (long) (settings.autosaveInterval.getValue() * 1000); long intervalMs = (long) (settings.autosaveInterval.getValue() * 1000);
if (System.currentTimeMillis() - lastDirtyTime < intervalMs) return; if (System.currentTimeMillis() - lastSaveTime < intervalMs) return;
HudRenderer.INSTANCE.pushNotification( HudRenderer.INSTANCE.pushNotification(
Component.translatable("sputnik.config.autosaving.title").getString(), Component.translatable("sputnik.config.autosaving.title").getString(),
Component.translatable("sputnik.config.autosaving.desc").getString()); Component.translatable("sputnik.config.autosaving.desc").getString());
save(); save();
dirty = false;
} }
} }
@@ -10,6 +10,7 @@ import me.retucio.sputnik.event.render.Render3DEvent;
import me.retucio.sputnik.event.render.RenderBlockOutlineEvent; import me.retucio.sputnik.event.render.RenderBlockOutlineEvent;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.modules.camera.Freecam; import me.retucio.sputnik.module.modules.camera.Freecam;
import me.retucio.sputnik.util.render.SputnikBuffers;
import net.minecraft.client.DeltaTracker; import net.minecraft.client.DeltaTracker;
import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.SubmitNodeCollector;
@@ -42,6 +43,7 @@ public abstract class LevelRendererMixin {
profiler.push(Sputnik.MOD_ID + "-3d"); profiler.push(Sputnik.MOD_ID + "-3d");
EVENT_BUS.post(new Render3DEvent(matrices, dt, cameraState)); EVENT_BUS.post(new Render3DEvent(matrices, dt, cameraState));
SputnikBuffers.flush(); // antes de que empiece el hud
profiler.pop(); profiler.pop();
matrices.popPose(); matrices.popPose();
@@ -83,10 +83,6 @@ public class Module {
this.bind.setDefaultValue(key); this.bind.setDefaultValue(key);
// puto marcos // puto marcos
// la clave tiene que salir igual que en ConfigManager#getSettingKey, o sea con los
// nombres internos: Module#getName ya lo es, pero en los ajustes el traducido es
// getName() y el interno getTrueName(). si no cuadran no se encuentra la tecla
// guardada y se le pisa al usuario con la de por defecto
if (ConfigManager.INSTANCE.getConfig().settings if (ConfigManager.INSTANCE.getConfig().settings
.get(this.getName() + ":" + bind.getTrueName()) == null) .get(this.getName() + ":" + bind.getTrueName()) == null)
this.bind.setValue(key); this.bind.setValue(key);
@@ -89,6 +89,7 @@ public class ModuleManager {
add(new AttributeSwapper()); add(new AttributeSwapper());
add(new ChestplateSwitch()); add(new ChestplateSwitch());
add(new CrystalAura()); add(new CrystalAura());
add(new HoldAttack());
add(new MaceKill()); add(new MaceKill());
add(new ProjectileTrajectories()); add(new ProjectileTrajectories());
add(new SpearKill()); add(new SpearKill());
@@ -177,15 +177,14 @@ public class Freecam extends Module {
super.onEnable(); super.onEnable();
} }
// allChanged() ya no existe; ahora hay que pasarle el estado a mano
private static void invalidateChunks() { private static void invalidateChunks() {
if (mc.level == null) return; if (mc.level == null) return;
mc.levelRenderer.invalidateCompiledGeometry(mc.level, mc.options, mc.gameRenderer.mainCamera(), mc.getBlockColors()); mc.levelExtractor.allChanged();
} }
@Override @Override
public void onDisable() { public void onDisable() {
if (reloadChunks.getValue()) mc.execute(Freecam::invalidateChunks); if (reloadChunks.getValue()) invalidateChunks();
mc.options.setCameraType(perspective); mc.options.setCameraType(perspective);
crouching = false; crouching = false;
@@ -0,0 +1,95 @@
package me.retucio.sputnik.module.modules.combat;
import me.retucio.sputnik.module.Category;
import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.setting.settings.BooleanSetting;
import me.retucio.sputnik.module.setting.settings.EnumSetting;
import me.retucio.sputnik.module.setting.settings.NumberSetting;
import net.minecraft.world.phys.EntityHitResult;
/**
* @author retucio
*/
public class HoldAttack extends Module {
private final EnumSetting<AttackMode> mode = sgGeneral.add(new EnumSetting<>(
"modo de ataque",
"cómo atacar",
AttackMode.class,
AttackMode.TIME
));
private final NumberSetting timeCooldown = sgGeneral.add(new NumberSetting(
"cooldown",
"cómo de llena tiene que estar la barra de ataque antes de atacar",
84.8f, 0.0f, 100.0f, 0.1f
)).visibility(() -> mode.is(AttackMode.TIME));
private final NumberSetting spamCooldown = sgGeneral.add(new NumberSetting(
"cooldown ",
"cada cuánto atacar, en ticks",
0, 0, 40, 1
)).visibility(() -> mode.is(AttackMode.SPAM));
private final BooleanSetting attackInScreens = sgGeneral.add(new BooleanSetting(
"atacar en pantallas",
"atacar aun estando en una pantalla",
false
));
private int counter = 0;
public HoldAttack() {
super("autoataque", "mantén pulsado el clic para atacar automáticamente", Category.COMBAT);
mode.onUpdate(v -> counter = 0);
}
@Override
public void onEnable() {
counter = 0;
super.onEnable();
}
@Override
public void onTick() {
if (mc.player == null || mc.gameMode == null || mc.getCameraEntity() == null) return;
counter++;
if (mc.gui.screen() != null && !attackInScreens.getValue()) {
return;
}
if (!mc.options.keyAttack.isDown()) {
return;
}
if (!(mc.hitResult instanceof EntityHitResult ehr)) {
return;
}
if (mode.is(AttackMode.TIME)) {
if (mc.player.getAttackStrengthScale(0) < timeCooldown.getNormalizedValue()) {
return;
}
} else if (mode.is(AttackMode.SPAM)) {
if (counter < spamCooldown.getIntValue()) {
return;
}
}
mc.gameMode.attack(mc.player, ehr.getEntity());
counter = 0;
}
private enum AttackMode {
SPAM("spamear"),
TIME("timear");
private final String name;
AttackMode(String name) { this.name = name; }
@Override public String toString() { return name; }
}
}
@@ -8,7 +8,6 @@ import me.retucio.sputnik.module.setting.settings.NumberSetting;
import me.retucio.sputnik.util.Lists; import me.retucio.sputnik.util.Lists;
import net.minecraft.core.particles.SimpleParticleType; import net.minecraft.core.particles.SimpleParticleType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import java.util.List; import java.util.List;
@@ -26,9 +25,10 @@ public class Particles extends Module {
.map(particleType -> (SimpleParticleType) particleType) .map(particleType -> (SimpleParticleType) particleType)
.toList(); .toList();
// claves de traducción, no texto traducido (ver Lists#init)
Map<SimpleParticleType, String> particleNames = Lists.getMapOfLists(particleList, Map<SimpleParticleType, String> particleNames = Lists.getMapOfLists(particleList,
particleList.stream().map(particle -> Component.translatable( particleList.stream().map(particle ->
BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toShortLanguageKey()).getString()) BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toShortLanguageKey())
.toList()); .toList());
private final ListSetting<SimpleParticleType> particles = sgGeneral.add(new ListSetting<>( private final ListSetting<SimpleParticleType> particles = sgGeneral.add(new ListSetting<>(
@@ -1,5 +1,6 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.config.ConfigManager;
import me.retucio.sputnik.module.Module; import me.retucio.sputnik.module.Module;
import me.retucio.sputnik.module.ModuleManager; import me.retucio.sputnik.module.ModuleManager;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
@@ -32,8 +33,17 @@ public class KeySetting extends Setting<Integer> {
// con "teclas multimódulo" puesto se permite repetir tecla, que es justo lo que // con "teclas multimódulo" puesto se permite repetir tecla, que es justo lo que
// dice el ajuste. antes se le quitaba la tecla al otro módulo igualmente, así que // dice el ajuste. antes se le quitaba la tecla al otro módulo igualmente, así que
// el ajuste sólo servía para las teclas de vanilla // el ajuste sólo servía para las teclas de vanilla.
//
// mientras se carga la config. no se toca nada: son las teclas que el usuario ya
// tenía, y quitárselas aquí se las machacaba (y encima guardaba el -1).
//
// clientSettings puede ser null: el módulo de ajustes se construye en el
// inicializador estático de ClientSettingsPanel, y su propia tecla por defecto pasa
// por aquí antes de que el campo esté asignado
if (ModuleManager.INSTANCE != null if (ModuleManager.INSTANCE != null
&& ClientSettingsPanel.clientSettings != null
&& !ConfigManager.INSTANCE.isApplying()
&& !ClientSettingsPanel.clientSettings.multipleKeybinds.getValue()) { && !ClientSettingsPanel.clientSettings.multipleKeybinds.getValue()) {
for (Module module : ModuleManager.INSTANCE.getModules()) for (Module module : ModuleManager.INSTANCE.getModules())
if (module.getKey() == value && value != -1) { if (module.getKey() == value && value != -1) {
@@ -1,6 +1,7 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
import me.retucio.sputnik.util.I18nUtil;
import java.util.*; import java.util.*;
@@ -62,7 +63,7 @@ public class ListSetting<T> extends Setting<Map<T, Boolean>> {
public Map<String, Boolean> getConfigValues() { public Map<String, Boolean> getConfigValues() {
Map<String, Boolean> configValues = new HashMap<>(); Map<String, Boolean> configValues = new HashMap<>();
for (T option : options) { for (T option : options) {
configValues.put(getDisplayName(option), value.getOrDefault(option, false)); configValues.put(getConfigName(option), value.getOrDefault(option, false));
} }
return configValues; return configValues;
} }
@@ -103,10 +104,20 @@ public class ListSetting<T> extends Setting<Map<T, Boolean>> {
return displayNames != null ? new HashMap<>(displayNames) : null; return displayNames != null ? new HashMap<>(displayNames) : null;
} }
public String getDisplayName(T key) { // OJO: nombre interno de la opción. es la clave de la config. (ver
// ConfigManager#applyListSetting) y para las listas del juego es la clave de
// traducción ("entity.minecraft.zombie"), que no depende del idioma. no traducir
public String getConfigName(T key) {
if (displayNames != null && displayNames.containsKey(key)) { if (displayNames != null && displayNames.containsKey(key)) {
return displayNames.get(key); return displayNames.get(key);
} }
return key != null ? key.toString() : "?"; return key != null ? key.toString() : "?";
} }
// nombre visible: se traduce aquí. antes se mostraba el nombre interno tal cual, o sea
// los nombres de vanilla en inglés aunque el juego estuviera en otro idioma
public String getDisplayName(T key) {
String name = getConfigName(key);
return I18nUtil.orElse(name, name);
}
} }
@@ -57,6 +57,10 @@ public class NumberSetting extends Setting<Double> {
return value.byteValue(); return value.byteValue();
} }
public double getNormalizedValue() {
return (value - min ) / (max - min);
}
public void setValue(double value) { public void setValue(double value) {
if (this.value == value || this.locked) return; if (this.value == value || this.locked) return;
double clamped = clamp(value, min, max); double clamped = clamp(value, min, max);
@@ -1,6 +1,7 @@
package me.retucio.sputnik.module.setting.settings; package me.retucio.sputnik.module.setting.settings;
import me.retucio.sputnik.module.setting.Setting; import me.retucio.sputnik.module.setting.Setting;
import me.retucio.sputnik.util.I18nUtil;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -26,14 +27,19 @@ public class OptionSetting<T> extends Setting<T> {
if (options.contains(option)) super.setValue(option); if (options.contains(option)) super.setValue(option);
} }
// por nombre interno, no por el traducido: es lo que se guarda en la config.
public void setValueByName(String name) { public void setValueByName(String name) {
for (T opt : options) { for (T opt : options) {
if (displayNames != null && !displayNames.isEmpty()) { if (getConfigName(opt).equals(name)) {
if (displayNames.get(opt).equals(name)) { setValue(opt);
setValue(opt); return;
return; }
} }
} else if (opt.toString().equals(name)) {
// compatibilidad con configs. de antes, cuando se guardaba el nombre ya traducido
// (el de vanilla en inglés). si no, la opción guardada se perdía y volvía a la de serie
for (T opt : options) {
if (I18nUtil.vanillaName(getConfigName(opt)).equals(name)) {
setValue(opt); setValue(opt);
return; return;
} }
@@ -82,9 +88,22 @@ public class OptionSetting<T> extends Setting<T> {
return displayNames; return displayNames;
} }
// OJO: nombre interno de la opción, o sea la clave de la config. (ver ClientConfig
// #onUpdateSetting). igual que en ListSetting, para las listas del juego es la clave
// de traducción. no traducir
public String getConfigName(T key) {
if (displayNames != null && displayNames.containsKey(key)) return displayNames.get(key);
return key != null ? key.toString() : "?";
}
public String getConfigName() {
return getConfigName(value);
}
// nombre visible, traducido
public String getDisplayName(T key) { public String getDisplayName(T key) {
if (displayNames != null) return displayNames.get(key); String name = getConfigName(key);
return key.toString(); return I18nUtil.orElse(name, name);
} }
public String getDisplayName() { public String getDisplayName() {
@@ -4,11 +4,16 @@ import net.minecraft.client.gui.GuiGraphicsExtractor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
public abstract class Panel<B extends Button> extends Widget { public abstract class Panel<B extends Button> extends Widget {
protected String title; // el título se resuelve al pintarlo, no al construir el marco: los marcos se crean en
// la inicialización del mod, cuando el idioma todavía no ha cargado, así que guardar el
// texto ya traducido dejaba los títulos en español para siempre (y no cambiaban aunque
// se cambiara el idioma del juego en marcha)
private Supplier<String> title;
protected final List<B> buttons = new ArrayList<>(); protected final List<B> buttons = new ArrayList<>();
protected List<B> visibleButtons = new ArrayList<>(); protected List<B> visibleButtons = new ArrayList<>();
@@ -20,10 +25,22 @@ public abstract class Panel<B extends Button> extends Widget {
protected boolean dragging; protected boolean dragging;
public Panel(String title, int x, int y, int w, int h) { public Panel(String title, int x, int y, int w, int h) {
this(() -> title, x, y, w, h);
}
public Panel(Supplier<String> title, int x, int y, int w, int h) {
super(x, y, w, h); super(x, y, w, h);
this.title = title; this.title = title;
} }
public String getTitle() {
return title.get();
}
protected void setTitle(Supplier<String> title) {
this.title = title;
}
public void drawTooltips(GuiGraphicsExtractor gui, int mouseX, int mouseY) { public void drawTooltips(GuiGraphicsExtractor gui, int mouseX, int mouseY) {
for (Button button : visibleButtons) button.drawTooltip(gui, mouseX, mouseY); for (Button button : visibleButtons) button.drawTooltip(gui, mouseX, mouseY);
} }
@@ -4,12 +4,13 @@ import me.retucio.sputnik.ui.widgets.Button;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.util.function.Supplier;
public abstract class CloseablePanel<B extends Button> extends Panel<B> { public abstract class CloseablePanel<B extends Button> extends Panel<B> {
protected static final int PADDING = 4; protected static final int PADDING = 4;
public CloseablePanel(String title, int x, int y, int w, int h) { public CloseablePanel(Supplier<String> title, int x, int y, int w, int h) {
super(title, x, y, w, h); super(title, x, y, w, h);
} }
@@ -4,11 +4,12 @@ import me.retucio.sputnik.ui.widgets.Button;
import me.retucio.sputnik.ui.widgets.Panel; import me.retucio.sputnik.ui.widgets.Panel;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.util.function.Supplier;
public abstract class ExtendablePanel<B extends Button> extends Panel<B> { public abstract class ExtendablePanel<B extends Button> extends Panel<B> {
protected boolean extended; protected boolean extended;
public ExtendablePanel(String title, int x, int y, int w, int h) { public ExtendablePanel(Supplier<String> title, int x, int y, int w, int h) {
super(title, x, y, w, h); super(title, x, y, w, h);
this.extended = true; this.extended = true;
} }
@@ -9,6 +9,7 @@ import me.retucio.sputnik.friend.FriendManager;
import me.retucio.sputnik.ui.screen.ClickGui; import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.buttons.FriendButton; import me.retucio.sputnik.ui.widgets.buttons.FriendButton;
import me.retucio.sputnik.util.Colors; import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@@ -20,7 +21,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
private int offset; private int offset;
public FriendsPanel(int x, int y, int w, int h) { public FriendsPanel(int x, int y, int w, int h) {
super("amigos", x, y, w, h); super(() -> I18nUtil.orElse("sputnik.ui.friends.title", "amigos"), x, y, w, h);
dragging = false; dragging = false;
Sputnik.EVENT_BUS.subscribe(this); Sputnik.EVENT_BUS.subscribe(this);
@@ -34,7 +35,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
@Override @Override
protected void updateWidth() { protected void updateWidth() {
int maxWidth = mc.font.width(title) + mc.font.width(" +"); int maxWidth = mc.font.width(getTitle()) + mc.font.width(" +");
for (FriendButton button : buttons) { for (FriendButton button : buttons) {
int textWidth = mc.font.width(button.getFriend().getName()); int textWidth = mc.font.width(button.getFriend().getName());
maxWidth = Math.max(maxWidth, textWidth); maxWidth = Math.max(maxWidth, textWidth);
@@ -47,7 +48,7 @@ public class FriendsPanel extends ExtendablePanel<FriendButton> {
updateWidth(); updateWidth();
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, Component.literal(title), gui.text(mc.font, Component.literal(getTitle()),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -21,7 +21,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
public final Category category; public final Category category;
public ModulePanel(Category category, int x, int y, int w, int h) { public ModulePanel(Category category, int x, int y, int w, int h) {
super(category.getDisplayName(), x, y, w, h); super(category::getDisplayName, x, y, w, h);
this.category = category; this.category = category;
dragging = false; dragging = false;
@@ -35,7 +35,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
@Override @Override
protected void updateWidth() { protected void updateWidth() {
int maxWidth = mc.font.width(title); int maxWidth = mc.font.width(getTitle());
for (ModuleButton button : buttons) { for (ModuleButton button : buttons) {
int textWidth = mc.font.width(button.getModule().getDisplayName()); int textWidth = mc.font.width(button.getModule().getDisplayName());
maxWidth = Math.max(maxWidth, textWidth); maxWidth = Math.max(maxWidth, textWidth);
@@ -48,7 +48,7 @@ public class ModulePanel extends ExtendablePanel<ModuleButton> {
updateWidth(); updateWidth();
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, Component.literal(title), gui.text(mc.font, Component.literal(getTitle()),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -27,7 +27,8 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
public boolean drawHeader = true; public boolean drawHeader = true;
public SettingsPanel(Module module, int x, int y, int w, int h) { public SettingsPanel(Module module, int x, int y, int w, int h) {
super(I18nUtil.orElse("sputnik.ui.settingsof", "ajustes de " + module.getDisplayName(), module.getDisplayName()), x, y, w, h); super(() -> I18nUtil.orElse("sputnik.ui.settingsof",
"ajustes de " + module.getDisplayName(), module.getDisplayName()), x, y, w, h);
this.module = module; this.module = module;
this.settingGroups = module.getSgs(); this.settingGroups = module.getSgs();
@@ -59,7 +60,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
: -1; : -1;
gui.fill(x, renderY, x + w, renderY + h, headerColor); gui.fill(x, renderY, x + w, renderY + h, headerColor);
gui.text(mc.font, title, titleX, titleY, -1, true); gui.text(mc.font, getTitle(), titleX, titleY, -1, true);
drawCloseButton(gui, mouseX, mouseY, closeButtonColor); drawCloseButton(gui, mouseX, mouseY, closeButtonColor);
} }
@@ -69,7 +70,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
.toList(); .toList();
// descripción // descripción
List<String> descLines = wrapDescription(module.getDescription(), w - 2 * PADDING); List<String> descLines = wrapDescription(module.getDisplayDescription(), w - 2 * PADDING);
int lineSpacing = 2; int lineSpacing = 2;
int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * lineSpacing; int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * lineSpacing;
@@ -212,7 +213,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
@Override @Override
public void updateWidth() { public void updateWidth() {
// asegurarse de que todos los botones caben en el marco, haciendo que la anchura se ajuste al texto más largo // asegurarse de que todos los botones caben en el marco, haciendo que la anchura se ajuste al texto más largo
int maxWidth = mc.font.width(title); int maxWidth = mc.font.width(getTitle());
for (SettingButton<?> button : buttons) { for (SettingButton<?> button : buttons) {
String text = button.getSetting().getName(); String text = button.getSetting().getName();
@@ -246,7 +247,7 @@ public class SettingsPanel extends CloseablePanel<SettingButton<? extends Settin
public int getGroupHeaderY(int groupIndex) { public int getGroupHeaderY(int groupIndex) {
int currentY = renderY + h + PADDING; int currentY = renderY + h + PADDING;
List<String> descLines = wrapDescription(module.getDescription(), w - 2 * PADDING); List<String> descLines = wrapDescription(module.getDisplayDescription(), w - 2 * PADDING);
int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * 2; int descHeight = descLines.size() * mc.font.lineHeight + (descLines.size() - 1) * 2;
currentY += descHeight + 3 * PADDING; // descBoxBottom + padding currentY += descHeight + 3 * PADDING; // descBoxBottom + padding
@@ -6,6 +6,7 @@ import me.retucio.sputnik.module.modules.client.ClientSettingsModule;
import me.retucio.sputnik.ui.screen.ClickGui; import me.retucio.sputnik.ui.screen.ClickGui;
import me.retucio.sputnik.ui.widgets.panels.SettingsPanel; import me.retucio.sputnik.ui.widgets.panels.SettingsPanel;
import me.retucio.sputnik.util.Colors; import me.retucio.sputnik.util.Colors;
import me.retucio.sputnik.util.I18nUtil;
import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.GuiGraphicsExtractor;
public class ClientSettingsPanel extends SettingsPanel { public class ClientSettingsPanel extends SettingsPanel {
@@ -15,7 +16,7 @@ public class ClientSettingsPanel extends SettingsPanel {
public ClientSettingsPanel(int x, int y, int w, int h) { public ClientSettingsPanel(int x, int y, int w, int h) {
super(clientSettings, x, y, w, h); super(clientSettings, x, y, w, h);
this.title = "ajustes del mod"; setTitle(() -> I18nUtil.orElse("sputnik.ui.clientsettings.title", "ajustes del mod"));
this.drawHeader = false; // disable the original header this.drawHeader = false; // disable the original header
} }
@@ -25,7 +26,7 @@ public class ClientSettingsPanel extends SettingsPanel {
// Draw our own custom header (no close button, with +/- // Draw our own custom header (no close button, with +/-
gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB()); gui.fill(x, renderY, x + w, renderY + h, Colors.mainColor.getRGB());
gui.text(mc.font, title, gui.text(mc.font, getTitle(),
x + 8, x + 8,
renderY + (h / 2) - (mc.font.lineHeight / 2), renderY + (h / 2) - (mc.font.lineHeight / 2),
-1, true); -1, true);
@@ -47,6 +47,21 @@ public class I18nUtil {
return Language.getInstance().has(key) ? I18n.get(key, args) : fallback; return Language.getInstance().has(key) ? I18n.get(key, args) : fallback;
} }
// igual que orElse pero rellenando también los %s del texto de reserva.
// para lo que se registra durante el arranque (cargar la config., comprobar la versión...):
// ahí el idioma todavía no está cargado y Component#translatable escupía la clave pelada
public static String fmt(String key, String fallback, Object... args) {
if (Language.getInstance().has(key)) return I18n.get(key, args);
return args.length == 0 ? fallback : String.format(fallback, args);
}
// texto que daba el en_us de vanilla para una clave. sólo para leer configs. viejas:
// las listas guardaban el nombre ya traducido y Language, durante el arranque, es
// siempre el en_us de vanilla, así que lo guardado eran los nombres en inglés
public static String vanillaName(String key) {
return Language.DEFAULT_INSTANCE.getOrDefault(key, key);
}
// nombre visible de una constante de enum. los EnumSetting se guardan por índice // nombre visible de una constante de enum. los EnumSetting se guardan por índice
// (ver ConfigManager#applyEnumSetting), así que su toString() es sólo texto y se traduce. // (ver ConfigManager#applyEnumSetting), así que su toString() es sólo texto y se traduce.
// la clave se deriva de la clase que la declara, así que dos enums con el mismo nombre // la clave se deriva de la clase que la declara, así que dos enums con el mismo nombre
@@ -3,7 +3,6 @@ package me.retucio.sputnik.util;
import net.minecraft.core.particles.ParticleType; import net.minecraft.core.particles.ParticleType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.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;
@@ -40,38 +39,36 @@ public class Lists {
public static final List<Color> colorList = new ArrayList<>(); public static final List<Color> colorList = new ArrayList<>();
public static final List<String> fontList = new ArrayList<>(); public static final List<String> fontList = new ArrayList<>();
// intentar traducir nombres y fallar miserablemente // 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(entity -> Component.translatable( entityList.stream().map(EntityType::getDescriptionId).toList());
entity.getDescriptionId()
).getString()).toList());
particleNames = getMapOfLists(particleList, particleNames = getMapOfLists(particleList,
particleList.stream().map(particle -> Component.translatable( particleList.stream().map(particle ->
BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toLanguageKey() BuiltInRegistries.PARTICLE_TYPE.getKey(particle).toLanguageKey()
).getString()).toList()); ).toList());
itemNames = getMapOfLists(itemList, itemNames = getMapOfLists(itemList,
itemList.stream().map(item -> Component.translatable( itemList.stream().map(Item::getDescriptionId).toList());
item.getDescriptionId()
).getString()).toList());
blockNames = getMapOfLists(blockList, blockNames = getMapOfLists(blockList,
blockList.stream().map(block -> Component.translatable( blockList.stream().map(Block::getDescriptionId).toList());
block.getDescriptionId()
).getString()).toList());
soundNames = getMapOfLists(soundList, soundNames = getMapOfLists(soundList,
soundList.stream().map(sound -> Component.translatable( soundList.stream().map(sound ->
sound.location().toShortLanguageKey() sound.location().toShortLanguageKey()
).getString()).toList()); ).toList());
soundNames.replace(SoundEvents.EMPTY, "NINGUNO"); soundNames.replace(SoundEvents.EMPTY, "NINGUNO");
screenNames = getMapOfLists(screenList, screenNames = getMapOfLists(screenList,
screenList.stream().map(screen -> Component.translatable( screenList.stream().map(screen ->
BuiltInRegistries.MENU.getKey(screen).toLanguageKey() BuiltInRegistries.MENU.getKey(screen).toLanguageKey()
).getString()).toList()); ).toList());
colorList.addAll(Arrays.asList( colorList.addAll(Arrays.asList(
RED, ORANGE, YELLOW, LIME, GREEN, CYAN, RED, ORANGE, YELLOW, LIME, GREEN, CYAN,
@@ -1,7 +1,9 @@
package me.retucio.sputnik.util.render; package me.retucio.sputnik.util.render;
import com.mojang.blaze3d.PrimitiveTopology; import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.DepthStencilState;
import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.platform.CompareOp;
import com.mojang.blaze3d.systems.GpuDevice; import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.DefaultVertexFormat;
@@ -28,12 +30,15 @@ public class Pipelines {
static List<RenderPipeline> PIPELINES = new ArrayList<>(); static List<RenderPipeline> PIPELINES = new ArrayList<>();
// 26.2 usa depth invertido (near = 1), así que el LEQUAL de siempre es GREATER_THAN_OR_EQUAL
private static final DepthStencilState NO_DEPTH = new DepthStencilState(CompareOp.ALWAYS_PASS, false);
private static final DepthStencilState DEPTH = new DepthStencilState(CompareOp.GREATER_THAN_OR_EQUAL, false);
static final RenderPipeline LINES_PIPELINE = add(RenderPipeline.builder(RenderPipelines.LINES_SNIPPET) static final RenderPipeline LINES_PIPELINE = add(RenderPipeline.builder(RenderPipelines.LINES_SNIPPET)
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH)
.withPrimitiveTopology(PrimitiveTopology.LINES) .withPrimitiveTopology(PrimitiveTopology.LINES)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -41,8 +46,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines_cull")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/lines_cull"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR_NORMAL_LINE_WIDTH)
.withPrimitiveTopology(PrimitiveTopology.LINES) .withPrimitiveTopology(PrimitiveTopology.LINES)
// .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withDepthStencilState(DEPTH)
// .withDepthWrite(false)
.withCull(true) .withCull(true)
.build()); .build());
@@ -50,9 +54,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR)
.withPrimitiveTopology(PrimitiveTopology.QUADS) .withPrimitiveTopology(PrimitiveTopology.QUADS)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withBlend(BlendFunction.TRANSLUCENT)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -60,9 +62,7 @@ public class Pipelines {
.withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads_cull")) .withLocation(Identifier.fromNamespaceAndPath(Sputnik.MOD_ID, "pipeline/quads_cull"))
.withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR) .withVertexBinding(0, DefaultVertexFormat.POSITION_COLOR)
.withPrimitiveTopology(PrimitiveTopology.QUADS) .withPrimitiveTopology(PrimitiveTopology.QUADS)
// .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withDepthStencilState(DEPTH)
// .withBlend(BlendFunction.TRANSLUCENT)
// .withDepthWrite(false)
.withCull(false) .withCull(false)
.build()); .build());
@@ -76,9 +76,7 @@ public class Pipelines {
// siguen usando uniforms con nombre (u_Texture, u_ProjectionMatrix) y seguramente // siguen usando uniforms con nombre (u_Texture, u_ProjectionMatrix) y seguramente
// haya que reescribirlos para que casen con las convenciones nuevas // haya que reescribirlos para que casen con las convenciones nuevas
.withBindGroupLayout(BindGroupLayouts.SAMPLER0) .withBindGroupLayout(BindGroupLayouts.SAMPLER0)
// .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withDepthStencilState(NO_DEPTH)
// .withDepthWrite(false)
// .withBlend(BlendFunction.TRANSLUCENT)
.withCull(false) .withCull(false)
.build()); .build());
@@ -5,24 +5,32 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexFormat;
import me.retucio.sputnik.Sputnik; import me.retucio.sputnik.Sputnik;
import net.minecraft.client.renderer.StagedVertexBuffer; import net.minecraft.client.renderer.StagedVertexBuffer;
import net.minecraft.client.renderer.rendertype.PreparedRenderType;
import net.minecraft.client.renderer.rendertype.RenderType; import net.minecraft.client.renderer.rendertype.RenderType;
import java.util.ArrayList;
import java.util.List;
/** emula el antiguo Tesselator + RenderType.draw(MeshData), que 26.2 se cargó /** emula el antiguo Tesselator + RenderType.draw(MeshData), que 26.2 se cargó
* <p> * <p>
* ahora los vértices se escriben en un StagedVertexBuffer: se pide un draw, se * el ciclo del StagedVertexBuffer es appendDraw -> upload -> drawFromBuffer -> endDraw,
* escriben los vértices, se sube todo a la gpu y se dibuja. el ciclo es * y hay que respetarlo entero: los vértices no llegan al Draw hasta que upload() cierra
* appendDraw -> upload -> drawFromBuffer -> endDraw; upload() peta si se llama * el BufferBuilder, y appendDraw() peta después de un upload(). de ahí que draw() sólo
* dos veces sin endDraw() de por medio. * encole y sea flush() quien suba y dibuje todo de una.
* <p> * <p>
* subimos y dibujamos en cada llamada (en vez de agrupar draws) para mantener el * un upload() por caja tampoco vale: cada uno se pilla un buffer de 256 KB del pool que
* mismo orden de dibujado que antes; es menos eficiente pero se comporta igual. * no vuelve hasta el final del frame.
*/ */
public class SputnikBuffers { public class SputnikBuffers {
private static StagedVertexBuffer buffer; private static StagedVertexBuffer buffer;
private static StagedVertexBuffer.Draw current; private static StagedVertexBuffer.Draw current;
private static final List<QueuedDraw> queue = new ArrayList<>();
private record QueuedDraw(PreparedRenderType type, StagedVertexBuffer.Draw draw) {}
// lazy: al cargar la clase todavía puede no haber device // lazy: al cargar la clase todavía puede no haber device
private static StagedVertexBuffer buffer() { private static StagedVertexBuffer buffer() {
if (buffer == null) { if (buffer == null) {
@@ -32,8 +40,6 @@ public class SputnikBuffers {
} }
public static VertexConsumer begin(VertexFormat format, PrimitiveTopology topology) { public static VertexConsumer begin(VertexFormat format, PrimitiveTopology topology) {
if (current != null) buffer().endDraw(); // por si quedó un draw a medias
current = buffer().appendDraw(format, topology); current = buffer().appendDraw(format, topology);
return buffer().getVertexBuilder(current); return buffer().getVertexBuilder(current);
} }
@@ -41,20 +47,33 @@ public class SputnikBuffers {
public static void draw(RenderType type) { public static void draw(RenderType type) {
if (current == null) return; if (current == null) return;
if (!current.isEmpty()) { queue.add(new QueuedDraw(type.prepare(), current));
buffer().upload(); current = null;
type.prepare().drawFromBuffer(buffer().getExecuteInfo(current)); }
/** sube y dibuja lo encolado; hay que llamarlo antes de que el juego pinte el hud */
public static void flush() {
if (buffer == null) return;
current = null;
if (queue.isEmpty()) return;
buffer.upload();
for (QueuedDraw queued : queue) {
StagedVertexBuffer.ExecuteInfo info = buffer.getExecuteInfo(queued.draw());
if (info != null) queued.type().drawFromBuffer(info);
} }
buffer().endDraw(); queue.clear();
current = null; buffer.endDraw();
} }
// devuelve los buffers al pool; una vez por frame // devuelve los buffers al pool; una vez por frame
public static void endFrame() { public static void endFrame() {
if (buffer == null) return; if (buffer == null) return;
current = null; flush();
buffer.endFrame(); buffer.endFrame();
} }
} }
@@ -55,6 +55,7 @@
"sputnik.commands.wait.desc": "waits a given amount of time before running another command", "sputnik.commands.wait.desc": "waits a given amount of time before running another command",
"sputnik.config.autosaving.desc": "saving settings...", "sputnik.config.autosaving.desc": "saving settings...",
"sputnik.config.autosaving.title": "autosaving...", "sputnik.config.autosaving.title": "autosaving...",
"sputnik.config.error.applying": "error applying settings",
"sputnik.config.error.empty": "config file empty or nonexistent", "sputnik.config.error.empty": "config file empty or nonexistent",
"sputnik.config.error.invalidformat": "invalid file format: %s", "sputnik.config.error.invalidformat": "invalid file format: %s",
"sputnik.config.error.invalidversion": "invalid version: %s", "sputnik.config.error.invalidversion": "invalid version: %s",
@@ -105,6 +106,8 @@
"sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velociraptor", "sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velociraptor",
"sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h", "sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h",
"sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h", "sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h",
"sputnik.enum.module.modules.combat.holdattack.attackmode.spam": "spam",
"sputnik.enum.module.modules.combat.holdattack.attackmode.time": "time",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "dashed", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "dashed",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "points", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "points",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "solid", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "solid",
@@ -327,6 +330,8 @@
"sputnik.module.auradecristales.name": "crystal aura", "sputnik.module.auradecristales.name": "crystal aura",
"sputnik.module.autismo.desc": "hehe", "sputnik.module.autismo.desc": "hehe",
"sputnik.module.autismo.name": "autism", "sputnik.module.autismo.name": "autism",
"sputnik.module.autoataque.desc": "hold the attack button down to attack automatically",
"sputnik.module.autoataque.name": "autoattack",
"sputnik.module.autocartel.desc": "lets you place signs with the same text without typing it out one by one", "sputnik.module.autocartel.desc": "lets you place signs with the same text without typing it out one by one",
"sputnik.module.autocartel.name": "auto sign", "sputnik.module.autocartel.name": "auto sign",
"sputnik.module.autofish.rodbreaking": "the rod is about to break", "sputnik.module.autofish.rodbreaking": "the rod is about to break",
@@ -646,6 +651,14 @@
"sputnik.setting.autismo.general.direccion.name": "direction", "sputnik.setting.autismo.general.direccion.name": "direction",
"sputnik.setting.autismo.general.intervalo.desc": "rotation interval", "sputnik.setting.autismo.general.intervalo.desc": "rotation interval",
"sputnik.setting.autismo.general.intervalo.name": "interval", "sputnik.setting.autismo.general.intervalo.name": "interval",
"sputnik.setting.autoataque.general.atacarenpantallas.desc": "attack even while in a screen",
"sputnik.setting.autoataque.general.atacarenpantallas.name": "attack in screens",
"sputnik.setting.autoataque.general.cooldown.desc": "how full the attack bar has to be before attacking",
"sputnik.setting.autoataque.general.cooldown.name": "cooldown",
"sputnik.setting.autoataque.general.cooldown1.desc": "how often to attack, in ticks",
"sputnik.setting.autoataque.general.cooldown1.name": "cooldown",
"sputnik.setting.autoataque.general.mododeataque.desc": "how to attack",
"sputnik.setting.autoataque.general.mododeataque.name": "attack mode",
"sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "switch back to the original tool", "sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "switch back to the original tool",
"sputnik.setting.autoherramienta.general.cambiardevuelta.name": "switch back", "sputnik.setting.autoherramienta.general.cambiardevuelta.name": "switch back",
"sputnik.setting.autoherramienta.general.delay.desc": "delay when switching tools", "sputnik.setting.autoherramienta.general.delay.desc": "delay when switching tools",
@@ -1434,8 +1447,10 @@
"sputnik.setting.zoom.visual.mostrarmanos.name": "show hands", "sputnik.setting.zoom.visual.mostrarmanos.name": "show hands",
"sputnik.ui.availablemodes": "available modes:", "sputnik.ui.availablemodes": "available modes:",
"sputnik.ui.clickgui.title": "gui", "sputnik.ui.clickgui.title": "gui",
"sputnik.ui.clientsettings.title": "mod settings",
"sputnik.ui.friend.manage": "manage %s", "sputnik.ui.friend.manage": "manage %s",
"sputnik.ui.friend.remove": "remove friend", "sputnik.ui.friend.remove": "remove friend",
"sputnik.ui.friends.title": "friends",
"sputnik.ui.list.tooltip": "%s (%s of %s)", "sputnik.ui.list.tooltip": "%s (%s of %s)",
"sputnik.ui.preview.echest": "echest", "sputnik.ui.preview.echest": "echest",
"sputnik.ui.preview.shulker": "shulker preview", "sputnik.ui.preview.shulker": "shulker preview",
@@ -55,6 +55,7 @@
"sputnik.commands.wait.desc": "espera un tiempo determinado antes de ejecutar otro comando", "sputnik.commands.wait.desc": "espera un tiempo determinado antes de ejecutar otro comando",
"sputnik.config.autosaving.desc": "guardando configuración...", "sputnik.config.autosaving.desc": "guardando configuración...",
"sputnik.config.autosaving.title": "autoguardando...", "sputnik.config.autosaving.title": "autoguardando...",
"sputnik.config.error.applying": "error aplicando ajustes",
"sputnik.config.error.empty": "archivo de config. vacío o inexistente", "sputnik.config.error.empty": "archivo de config. vacío o inexistente",
"sputnik.config.error.invalidformat": "formato de archivo inválido: %s", "sputnik.config.error.invalidformat": "formato de archivo inválido: %s",
"sputnik.config.error.invalidversion": "versión inválida: %s", "sputnik.config.error.invalidversion": "versión inválida: %s",
@@ -105,6 +106,8 @@
"sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velocirráptor", "sputnik.enum.module.modules.client.hud.dynosaurs.velocirraptor": "velocirráptor",
"sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h", "sputnik.enum.module.modules.client.hud.timeformat.twelve_hour": "12h",
"sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h", "sputnik.enum.module.modules.client.hud.timeformat.twenty_four_hour": "24h",
"sputnik.enum.module.modules.combat.holdattack.attackmode.spam": "spamear",
"sputnik.enum.module.modules.combat.holdattack.attackmode.time": "timear",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "discontinua", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dashed": "discontinua",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "puntos", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.dotted": "puntos",
"sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "continua", "sputnik.enum.module.modules.combat.projectiletrajectories.trajectorystyle.solid": "continua",
@@ -327,6 +330,8 @@
"sputnik.module.auradecristales.name": "aura de cristales", "sputnik.module.auradecristales.name": "aura de cristales",
"sputnik.module.autismo.desc": "jeje", "sputnik.module.autismo.desc": "jeje",
"sputnik.module.autismo.name": "autismo", "sputnik.module.autismo.name": "autismo",
"sputnik.module.autoataque.desc": "mantén pulsado el clic para atacar automáticamente",
"sputnik.module.autoataque.name": "autoataque",
"sputnik.module.autocartel.desc": "te permite colocar carteles con el mismo texto sin tener que escribirlo uno por uno", "sputnik.module.autocartel.desc": "te permite colocar carteles con el mismo texto sin tener que escribirlo uno por uno",
"sputnik.module.autocartel.name": "autocartel", "sputnik.module.autocartel.name": "autocartel",
"sputnik.module.autofish.rodbreaking": "caña a punto de romperse", "sputnik.module.autofish.rodbreaking": "caña a punto de romperse",
@@ -646,6 +651,14 @@
"sputnik.setting.autismo.general.direccion.name": "dirección", "sputnik.setting.autismo.general.direccion.name": "dirección",
"sputnik.setting.autismo.general.intervalo.desc": "intervalo de rotación", "sputnik.setting.autismo.general.intervalo.desc": "intervalo de rotación",
"sputnik.setting.autismo.general.intervalo.name": "intervalo", "sputnik.setting.autismo.general.intervalo.name": "intervalo",
"sputnik.setting.autoataque.general.atacarenpantallas.desc": "atacar aun estando en una pantalla",
"sputnik.setting.autoataque.general.atacarenpantallas.name": "atacar en pantallas",
"sputnik.setting.autoataque.general.cooldown.desc": "cómo de llena tiene que estar la barra de ataque antes de atacar",
"sputnik.setting.autoataque.general.cooldown.name": "cooldown",
"sputnik.setting.autoataque.general.cooldown1.desc": "cada cuánto atacar, en ticks",
"sputnik.setting.autoataque.general.cooldown1.name": "cooldown",
"sputnik.setting.autoataque.general.mododeataque.desc": "cómo atacar",
"sputnik.setting.autoataque.general.mododeataque.name": "modo de ataque",
"sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "cambiar de vuelta a la herramienta inicial", "sputnik.setting.autoherramienta.general.cambiardevuelta.desc": "cambiar de vuelta a la herramienta inicial",
"sputnik.setting.autoherramienta.general.cambiardevuelta.name": "cambiar de vuelta", "sputnik.setting.autoherramienta.general.cambiardevuelta.name": "cambiar de vuelta",
"sputnik.setting.autoherramienta.general.delay.desc": "delay al cambiar de herramienta", "sputnik.setting.autoherramienta.general.delay.desc": "delay al cambiar de herramienta",
@@ -1434,8 +1447,10 @@
"sputnik.setting.zoom.visual.mostrarmanos.name": "mostrar manos", "sputnik.setting.zoom.visual.mostrarmanos.name": "mostrar manos",
"sputnik.ui.availablemodes": "modos disponibles:", "sputnik.ui.availablemodes": "modos disponibles:",
"sputnik.ui.clickgui.title": "interfaz", "sputnik.ui.clickgui.title": "interfaz",
"sputnik.ui.clientsettings.title": "ajustes del mod",
"sputnik.ui.friend.manage": "gestiona a %s", "sputnik.ui.friend.manage": "gestiona a %s",
"sputnik.ui.friend.remove": "eliminar amigo", "sputnik.ui.friend.remove": "eliminar amigo",
"sputnik.ui.friends.title": "amigos",
"sputnik.ui.list.tooltip": "%s (%s de %s)", "sputnik.ui.list.tooltip": "%s (%s de %s)",
"sputnik.ui.preview.echest": "echest", "sputnik.ui.preview.echest": "echest",
"sputnik.ui.preview.shulker": "previsualización del shulker", "sputnik.ui.preview.shulker": "previsualización del shulker",