From 737a7a8adebafdd2369eb7416aa5744b07e0b5ba Mon Sep 17 00:00:00 2001 From: Cristian Date: Thu, 5 Nov 2026 23:42:31 -0500 Subject: [PATCH 01/24] feat(scripts): traducir mensajes de chat de KubeJS, logros y funciones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runCommand/runCommandSilent: traduce el texto de tellraw, title, say, msg, tell y w. - Mensajes armados por partes (variable que acaba en tell), sus miembros name y sus tablas de nombres, incluso si la tabla está en otro archivo. - Logros y funciones .mcfunction de kubejs/data, con copia de seguridad y en el kit para servidores. --- CHANGELOG.md | 11 ++ README.md | 2 +- src/traductor_es/formatos/comandos.py | 47 +++++++ src/traductor_es/formatos/flujo_js.py | 86 +++++++++++++ src/traductor_es/modulos/datos.py | 145 +++++++++++++++++++++ src/traductor_es/modulos/scripts.py | 126 ++++++++++++++++-- src/traductor_es/proceso.py | 4 +- src/traductor_es/salida/exportar.py | 5 +- tests/test_comandos_y_datos.py | 176 ++++++++++++++++++++++++++ 9 files changed, 583 insertions(+), 19 deletions(-) create mode 100644 src/traductor_es/formatos/comandos.py create mode 100644 src/traductor_es/formatos/flujo_js.py create mode 100644 src/traductor_es/modulos/datos.py create mode 100644 tests/test_comandos_y_datos.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 710ce27..4d56ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ## [Sin publicar] +### Agregado + +- Mensajes de chat de los scripts de KubeJS: el texto de `tellraw`, `title` y `say` (y `msg`, `tell`, `w`) + cuando se manda con `runCommand` o `runCommandSilent`. Antes solo se traducía lo que pasaba por `tell`, + `Text.of` y similares. +- Mensajes que un script arma por partes (`let mensaje = 'Tienes '; mensaje += efecto.name; player.tell(mensaje)`): + los trozos de texto, los `name` que se suman y las tablas de nombres que se suman (`nombres[clave]`). Solo + cuando la variable acaba en una llamada de texto como `tell`. +- Logros (título y descripción) y funciones `.mcfunction` de `kubejs/data`, que también muestran mensajes en + el chat. Van en el grupo Scripts, con copia de seguridad, y entran en el kit para servidores. + ## [2.2.0] - 2026-11-04 Interfaz nueva: una ventana guiada en pasos, con tarjetas y un solo botón principal, y un tema propio diff --git a/README.md b/README.md index f03d634..cfe0e6e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Textos de mods (objetos, bloques, menús, tooltips) | `lang/*.json` (1.13+) y `.lang` (1.12 y antes), incluidos los jars anidados, KubeJS y los resource packs del modpack | Resource pack `Traduccion_ES` | | Misiones | FTB Quests (texto en los capítulos, carpeta `lang/` de 1.21+ y NBT de 1.12), BetterQuesting, HQM, Heracles | En su sitio, con copia de seguridad | | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | -| Scripts de KubeJS | Lo que ve el jugador: `tell`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI | En su sitio, con copia de seguridad | +| Scripts de KubeJS | Lo que ve el jugador: `tell`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand`. También los logros y funciones de `kubejs/data` | En su sitio, con copia de seguridad | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir diff --git a/src/traductor_es/formatos/comandos.py b/src/traductor_es/formatos/comandos.py new file mode 100644 index 0000000..0e41a01 --- /dev/null +++ b/src/traductor_es/formatos/comandos.py @@ -0,0 +1,47 @@ +"""Comandos de chat de Minecraft (`tellraw`, `title`, `say`, `msg`…): separa el texto que ve el jugador. + +Sirve para una línea de un `.mcfunction` y para la cadena que un script de KubeJS le pasa a `runCommand`. +Solo reconoce la forma clásica, con el texto como JSON (`tellraw @a {"text":"Hola"}`) o como texto suelto +(`say Hola`). Lo que tenga otra forma (SNBT, macros `$`) no se toca. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +_INICIO = re.compile(r"(?:^\s*/?|\brun\s+)(?Ptellraw|title|say|msg|tell|w)[ \t]+") +#: `@a`, `@a[distance=..5, tag=x]` (con espacios dentro de los corchetes) o un nombre. +_SELECTOR = re.compile(r"(?:@[a-z]+(?:\[[^\]]*\])?|\S+)[ \t]+") +_TITULO = re.compile(r"(?:title|subtitle|actionbar)[ \t]+") +_CON_SELECTOR = frozenset({"tellraw", "title", "msg", "tell", "w"}) + + +@dataclass(frozen=True, slots=True) +class Comando: + prefijo: str #: todo lo que va antes del texto (`execute as @a run tellraw @s `) + cuerpo: str #: lo que ve el jugador: un componente JSON o un texto suelto + json: bool #: el cuerpo es un componente JSON (tellraw, title) y no texto suelto (say, msg) + + +def analizar(texto: str) -> Comando | None: + """El comando de chat que contiene el texto, o None si no es de los que muestran algo al jugador.""" + inicio = _INICIO.search(texto) + if inicio is None: + return None + orden = inicio.group("orden") + pos = inicio.end() + if orden in _CON_SELECTOR: + selector = _SELECTOR.match(texto, pos) + if selector is None: + return None + pos = selector.end() + if orden == "title": + titulo = _TITULO.match(texto, pos) + if titulo is None: # title … times / clear / reset + return None + pos = titulo.end() + cuerpo = texto[pos:] + if not cuerpo.strip(): + return None + return Comando(texto[:pos], cuerpo, json=orden in ("tellraw", "title")) diff --git a/src/traductor_es/formatos/flujo_js.py b/src/traductor_es/formatos/flujo_js.py new file mode 100644 index 0000000..c4e2578 --- /dev/null +++ b/src/traductor_es/formatos/flujo_js.py @@ -0,0 +1,86 @@ +"""Mensajes que un script arma por partes y manda después. + + let mensaje = 'Te has ganado ' + mensaje += efecto.name // «name» es un texto que ve el jugador + mensaje += global.nombres[clave] // «nombres» es una tabla de textos que ve el jugador + player.tell(mensaje) + +Aquí solo se lee el código para saber qué variables, miembros y tablas acaban en un mensaje. Decidir qué +cadenas se traducen es cosa del módulo de scripts. +""" + +from __future__ import annotations + +import re +from collections.abc import Iterable, Sequence +from dataclasses import dataclass + +from .javascript import CadenaJs + +_IDENT = r"[A-Za-z_$][\w$]*" +_RUTA = rf"{_IDENT}(?:\.{_IDENT})*" +_AGREGADO = re.compile(rf"\b({_IDENT})\s*\+=\s*({_RUTA})(?:(\[[^\]\n]*\])(?:\.({_IDENT}))?)?") +_ASIGNADA = re.compile(rf"({_IDENT})\s*\+?=\s*$") +_CLAVE = re.compile(rf"(?:({_IDENT})|['\"]([^'\"\n]+)['\"])\s*:\s*$") +_VENTANA = 160 #: cuánto código hacia atrás se mira para saber a qué se asigna una cadena + + +@dataclass(frozen=True, slots=True) +class Flujo: + variables: frozenset[str] = frozenset() #: variables que se pasan a una llamada de texto: `tell(mensaje)` + miembros: frozenset[str] = frozenset() #: `mensaje += efecto.name` → «name» + tablas: frozenset[str] = frozenset() #: `mensaje += global.nombres[clave]` → «nombres» + + +def analizar(src: str, llamadas: str) -> Flujo: + """`llamadas`: expresión regular con las llamadas cuyo argumento es texto para el jugador.""" + pasadas = re.compile(rf"(?:{llamadas})\s*\(\s*({_IDENT})\s*[,)]") + variables = frozenset(pasadas.findall(src)) + if not variables: + return Flujo() + miembros: set[str] = set() + tablas: set[str] = set() + for variable, ruta, indice, miembro in _AGREGADO.findall(src): + if variable not in variables: + continue + ultimo = ruta.rsplit(".", 1)[-1] + if miembro: # `efectos[i].name`: el texto es el miembro + miembros.add(miembro) + elif indice: # `nombres[clave]`: el texto está en la tabla + tablas.add(ultimo) + elif "." in ruta: + miembros.add(ultimo) + return Flujo(variables, frozenset(miembros), frozenset(tablas)) + + +def asignada_a(src: str, cadena: CadenaJs) -> str | None: + """La variable a la que se asigna o se suma esta cadena (`msg = '…'`, `msg += '…'`).""" + m = _ASIGNADA.search(src[max(0, cadena.inicio - _VENTANA) : cadena.inicio]) + return m.group(1) if m else None + + +def clave_de(src: str, cadena: CadenaJs) -> str | None: + """La clave de objeto a la que pertenece esta cadena como valor (`name: '…'`).""" + m = _CLAVE.search(src[max(0, cadena.inicio - _VENTANA) : cadena.inicio]) + return (m.group(1) or m.group(2)) if m else None + + +def cuerpos_de_tabla(src: str, cadenas: Sequence[CadenaJs], tablas: Iterable[str]) -> list[tuple[int, int]]: + """Posiciones (inicio, fin) del contenido de las tablas `x.nombres = { … }` con esos nombres.""" + nombres = frozenset(tablas) + if not nombres: + return [] + en_cadena = {c.inicio: c.fin for c in cadenas} + cuerpos: list[tuple[int, int]] = [] + for m in re.finditer(rf"({_RUTA})\s*=\s*\{{", src): + if m.group(1).rsplit(".", 1)[-1] not in nombres: + continue + i, profundidad = m.end(), 1 + while i < len(src) and profundidad: + if i in en_cadena: + i = en_cadena[i] + continue + profundidad += {"{": 1, "}": -1}.get(src[i], 0) + i += 1 + cuerpos.append((m.end(), i)) + return cuerpos diff --git a/src/traductor_es/modulos/datos.py b/src/traductor_es/modulos/datos.py new file mode 100644 index 0000000..755eb16 --- /dev/null +++ b/src/traductor_es/modulos/datos.py @@ -0,0 +1,145 @@ +"""Textos de los datos de KubeJS (`kubejs/data`): títulos de logros y mensajes de las funciones. + +Los logros salen en el chat cuando el jugador los consigue, y las funciones (`.mcfunction`) pueden mandar +mensajes con `tellraw`, `title` o `say`. Los archivos se traducen en el sitio, con copia de seguridad. +""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, cast + +from ..formatos import comandos +from ..nucleo.mascara import es_traducible +from .base import Contexto, Pieza, Resumen, aplicar, contar, parece_clave, unidades +from .scripts import AMBITO, aplicar_comando, unidades_comando + +RAIZ = "kubejs/data" +#: Carpeta de logros y de funciones dentro de un espacio de nombres (el nombre cambió entre versiones). +LOGROS = frozenset({"advancements", "advancement"}) +FUNCIONES = frozenset({"functions", "function"}) +CAMPOS_LOGRO = ("title", "description") + + +def _componente(valor: Any) -> str: + return json.dumps(valor, ensure_ascii=False) + + +def _logro(datos: Any) -> dict[str, Any] | None: + """El bloque `display` de un logro, o None si el archivo no es un logro con texto.""" + if not isinstance(datos, dict): + return None + display = cast("dict[str, Any]", datos).get("display") + return cast("dict[str, Any]", display) if isinstance(display, dict) else None + + +@dataclass(slots=True) +class _Archivo: + ruta: Path + texto: str + #: Logros: el JSON entero. Funciones: None (se trabaja por líneas). + datos: Any = None + piezas: list[str] = field(default_factory=list[str]) + + +def _unidades_logro(display: Mapping[str, Any]) -> list[str]: + return [t for campo in CAMPOS_LOGRO if campo in display for t in unidades(_componente(display[campo]), True)] + + +def _lineas(texto: str) -> list[tuple[str, comandos.Comando | None, str]]: + """Cada línea separada en (línea sin fin, su comando de chat, fin de línea).""" + res: list[tuple[str, comandos.Comando | None, str]] = [] + for linea in texto.split("\n"): + fin = "\r" if linea.endswith("\r") else "" + cuerpo = linea[: len(linea) - len(fin)] + cmd = comandos.analizar(cuerpo) + res.append((cuerpo, cmd if cmd is not None and unidades_comando(cmd) else None, fin)) + return res + + +class DatosKubeJS: + nombre = "Logros y funciones de KubeJS" + ambito = AMBITO + + def __init__(self, ctx: Contexto) -> None: + self.ctx = ctx + self._archivos_leidos: list[_Archivo] = [] + + def _archivos(self) -> list[Path]: + raiz = self.ctx.instancia.juego / RAIZ + if not raiz.is_dir(): + return [] + encontrados: list[Path] = [] + for ruta in sorted(raiz.rglob("*")): + partes = ruta.relative_to(raiz).parts # (espacio de nombres, carpeta, …) + if len(partes) < 3 or not ruta.is_file(): + continue + if (partes[1] in LOGROS and ruta.suffix == ".json") or ( + partes[1] in FUNCIONES and ruta.suffix == ".mcfunction" + ): + encontrados.append(ruta) + return encontrados + + def detectar(self) -> bool: + return bool(self._archivos()) + + def describir(self) -> str: + return contar(len(self._archivos()), "archivo de datos", "archivos de datos") + + def recolectar(self) -> list[Pieza]: + self._archivos_leidos = [] + piezas: list[Pieza] = [] + for ruta in self._archivos(): + original = self.ctx.respaldo.original(ruta) + if original is None: + continue + try: + texto = original.decode("utf-8-sig") + archivo = _Archivo(ruta, texto) + if ruta.suffix == ".json": + archivo.datos = json.loads(texto) + display = _logro(archivo.datos) + archivo.piezas = _unidades_logro(display) if display is not None else [] + pista = "logro del juego" + else: + archivo.piezas = [t for _, cmd, _ in _lineas(texto) if cmd for t in unidades_comando(cmd)] + pista = "mensaje de una función" + except (ValueError, UnicodeDecodeError): + self.ctx.registro(f"{ruta.name}: no pude leerlo con seguridad, se deja igual") + continue + if archivo.piezas: + self._archivos_leidos.append(archivo) + piezas += [Pieza(t, pista) for t in archivo.piezas if es_traducible(t) and not parece_clave(t)] + return piezas + + def _reescribir(self, a: _Archivo, traducciones: Mapping[str, str]) -> str: + if a.ruta.suffix == ".mcfunction": + return "\n".join( + (aplicar_comando(cmd, traducciones) if cmd else linea) + fin for linea, cmd, fin in _lineas(a.texto) + ) + datos: dict[str, Any] = a.datos + display = _logro(datos) + assert display is not None + for campo in CAMPOS_LOGRO: + if campo in display: + display[campo] = json.loads(aplicar(_componente(display[campo]), traducciones, True)) + return json.dumps(datos, indent=2, ensure_ascii=False) + "\n" + + def escribir(self, traducciones: Mapping[str, str]) -> Resumen: + resumen = Resumen() + for a in self._archivos_leidos: + hechas = sum(1 for t in set(a.piezas) if t in traducciones) + resumen.traducidos += hechas + resumen.pendientes += len(set(a.piezas)) - hechas + nuevo = self._reescribir(a, traducciones) + actual = a.ruta.read_text(encoding="utf-8-sig") if a.ruta.is_file() else None + if hechas and nuevo not in (a.texto, actual): + self.ctx.respaldo.escribir(a.ruta, nuevo.encode("utf-8")) + resumen.archivos.append(a.ruta) + return resumen + + def restaurar(self) -> list[Path]: + return self.ctx.respaldo.restaurar(RAIZ + "/") diff --git a/src/traductor_es/modulos/scripts.py b/src/traductor_es/modulos/scripts.py index ae4dd1c..19ddccc 100644 --- a/src/traductor_es/modulos/scripts.py +++ b/src/traductor_es/modulos/scripts.py @@ -8,10 +8,10 @@ import re from collections.abc import Mapping -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from ..formatos import javascript +from ..formatos import comandos, flujo_js, javascript, json_texto from ..nucleo.mascara import es_traducible from .base import Contexto, Pieza, Resumen, contar, parece_clave @@ -24,6 +24,8 @@ "displayName", "setDisplayName", "tooltip", "addTooltip", "title", "setTitle", "subtitle", "setSubtitle", "lore", "setLore", "actionbar", "setActionbar", }) # fmt: skip +#: Llamadas que ejecutan un comando: si es `tellraw`, `title`, `say`…, el texto del mensaje se traduce. +LLAMADAS_COMANDO = frozenset({"runCommand", "runCommandSilent"}) #: `Text.of("…")`, `Text.red("…")`, `Component.literal("…")`… AYUDAS_TEXTO = frozenset({ "of", "literal", "string", "black", "darkBlue", "darkGreen", "darkAqua", "darkRed", "darkPurple", "gold", "gray", @@ -37,6 +39,11 @@ "onEvent:client.generate_assets", ) # fmt: skip LLAMADAS_EN_EVENTO = frozenset({"add", "addAll", "insert", "addAdvanced", "addItem", "addFluid", "addLang", "addLine"}) +#: Las llamadas de texto (`tell`, `Text.of`…) como expresión regular: sirve para ver qué variables acaban en un mensaje. +_LLAMADAS_RE = ( + rf"\b(?:{'|'.join(sorted(LLAMADAS_TEXTO))})|\b(?:{'|'.join(sorted(RECEPTORES_TEXTO))})" + rf"\.(?:{'|'.join(sorted(AYUDAS_TEXTO))})" +) _EXPRESION = re.compile(r"\$\{[^{}\n]*\}") @@ -63,12 +70,85 @@ def es_texto_visible(c: javascript.CadenaJs) -> bool: return c.funcion in LLAMADAS_EN_EVENTO and any(ctx.startswith(EVENTOS_TEXTO) for ctx in c.contexto) +def _plantilla_segura(c: javascript.CadenaJs) -> bool: + return c.comilla != "`" or c.texto.count("${") == len(_EXPRESION.findall(c.texto)) + + +def textos_armados( + src: str, cadenas: list[javascript.CadenaJs], flujo: flujo_js.Flujo, tablas: frozenset[str] +) -> list[javascript.CadenaJs]: + """Cadenas que son parte de un mensaje armado por trozos: `let msg = 'Hola '; msg += x; player.tell(msg)`. + + Se eligen las que se asignan o suman a una variable que acaba en `tell`…, los valores `name:` de lo que se + suma a ella (`msg += efecto.name`) y los valores de las tablas de nombres que se suman (`msg += nombres[k]`). + """ + cuerpos = flujo_js.cuerpos_de_tabla(src, cadenas, tablas) + elegidas: list[javascript.CadenaJs] = [] + for c in cadenas: + if c.comparada or not _plantilla_segura(c) or not any(ch.isalpha() for ch in c.texto): + continue + if flujo_js.asignada_a(src, c) in flujo.variables: + # Trozos como «or » o «and » no parecen texto solos, pero aquí sí lo son + visible = es_traducible(c.texto.strip()) and not parece_clave(c.texto) + else: + clave = flujo_js.clave_de(src, c) + visible = ( + clave is not None + and (clave in flujo.miembros or any(a <= c.inicio < b for a, b in cuerpos)) + and _parece_texto(c.texto) + ) + if visible: + elegidas.append(c) + return elegidas + + +def unidades_comando(cmd: comandos.Comando) -> list[str]: + """Los textos que ve el jugador dentro de un comando de chat.""" + if not cmd.json: + return [cmd.cuerpo] if _parece_texto(cmd.cuerpo) else [] + partes = json_texto.extraer(cmd.cuerpo, literal=True) # None: no es JSON (p. ej. SNBT), no se toca + return [p for p in partes or [] if es_traducible(p) and not parece_clave(p)] + + +def aplicar_comando(cmd: comandos.Comando, traducciones: Mapping[str, str]) -> str: + if not cmd.json: + return cmd.prefijo + traducciones.get(cmd.cuerpo, cmd.cuerpo) + return cmd.prefijo + json_texto.reconstruir(cmd.cuerpo, traducciones, literal=True) + + +def comando_visible(c: javascript.CadenaJs) -> comandos.Comando | None: + """El comando de chat que esta cadena le pasa a `runCommand`, si lo hay y trae texto para traducir.""" + if c.comparada or c.funcion not in LLAMADAS_COMANDO: + return None + if c.comilla == "`" and c.texto.count("${") != len(_EXPRESION.findall(c.texto)): + return None + cmd = comandos.analizar(c.texto) + return cmd if cmd is not None and unidades_comando(cmd) else None + + @dataclass(slots=True) class _Script: ruta: Path src: str cadenas: list[javascript.CadenaJs] elegidas: list[javascript.CadenaJs] + #: Cadenas elegidas que son un comando de chat (por posición): se traduce el mensaje, no la línea entera. + ordenes: dict[int, comandos.Comando] = field(default_factory=dict[int, comandos.Comando]) + + def unidades(self, c: javascript.CadenaJs) -> list[str]: + cmd = self.ordenes.get(c.inicio) + return [c.texto] if cmd is None else unidades_comando(cmd) + + def nuevo(self, c: javascript.CadenaJs, traducciones: Mapping[str, str]) -> str | None: + """El texto nuevo de la cadena, o None si no hay traducción o no es seguro usarla.""" + cmd = self.ordenes.get(c.inicio) + if cmd is None: + nuevo = traducciones.get(c.texto) + else: + nuevo = aplicar_comando(cmd, traducciones) + if nuevo == c.texto: + return None # ninguna parte del mensaje tiene traducción + return nuevo if nuevo is not None and _seguro(c, nuevo) else None class ScriptsKubeJS: @@ -89,31 +169,49 @@ def detectar(self) -> bool: def describir(self) -> str: return contar(len(self._archivos()), "script") - def recolectar(self) -> list[Pieza]: - self._scripts = [] + def _leer(self) -> list[tuple[Path, str, list[javascript.CadenaJs]]]: + leidos: list[tuple[Path, str, list[javascript.CadenaJs]]] = [] for ruta in self._archivos(): original = self.ctx.respaldo.original(ruta) if original is None: continue try: src = original.decode("utf-8") - cadenas = javascript.escanear(src) + leidos.append((ruta, src, javascript.escanear(src))) except (ValueError, UnicodeDecodeError): self.ctx.registro(f"{ruta.name}: no pude leerlo con seguridad, se deja igual") - continue - elegidas = [c for c in cadenas if es_texto_visible(c)] + return leidos + + def recolectar(self) -> list[Pieza]: + self._scripts = [] + leidos = self._leer() + flujos = [flujo_js.analizar(src, _LLAMADAS_RE) for _, src, _ in leidos] + # Una tabla de nombres se define en un archivo y se usa en otro + tablas: frozenset[str] = frozenset(t for f in flujos for t in f.tablas) + for (ruta, src, cadenas), flujo in zip(leidos, flujos, strict=True): + elegidas: list[javascript.CadenaJs] = [] + en_comando: dict[int, comandos.Comando] = {} + for c in cadenas: + if es_texto_visible(c): + elegidas.append(c) + elif (cmd := comando_visible(c)) is not None: + elegidas.append(c) + en_comando[c.inicio] = cmd + vistas = {c.inicio for c in elegidas} + elegidas += [c for c in textos_armados(src, cadenas, flujo, tablas) if c.inicio not in vistas] if elegidas: - self._scripts.append(_Script(ruta, src, cadenas, elegidas)) - return [Pieza(c.texto, f"script: {c.funcion}") for s in self._scripts for c in s.elegidas] + self._scripts.append(_Script(ruta, src, cadenas, elegidas, en_comando)) + return [ + Pieza(t, f"script: {c.funcion or 'mensaje'}") + for s in self._scripts + for c in s.elegidas + for t in s.unidades(c) + ] def escribir(self, traducciones: Mapping[str, str]) -> Resumen: resumen = Resumen() for s in self._scripts: - nuevos = { - c.inicio: traducciones[c.texto] - for c in s.elegidas - if c.texto in traducciones and _seguro(c, traducciones[c.texto]) - } + nuevos = {c.inicio: n for c in s.elegidas if (n := s.nuevo(c, traducciones)) is not None} resumen.traducidos += len(nuevos) resumen.pendientes += len(s.elegidas) - len(nuevos) datos = javascript.reemplazar(s.src, s.cadenas, nuevos).encode("utf-8") diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index c965bca..f8704f0 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -11,7 +11,7 @@ from . import configuracion from .fuentes.instancia import Instancia -from .modulos import libros, misiones, mods, scripts +from .modulos import datos, libros, misiones, mods, scripts from .modulos.base import Contexto, Modulo, Resumen from .modulos.mods import es_nombre from .motores import fabrica @@ -159,7 +159,7 @@ def modulos(self) -> list[tuple[str, Modulo]]: if "misiones" in self.grupos: lista += [("misiones", m) for m in misiones.todos(self.ctx)] if "scripts" in self.grupos: - lista.append(("scripts", scripts.ScriptsKubeJS(self.ctx))) + lista += [("scripts", scripts.ScriptsKubeJS(self.ctx)), ("scripts", datos.DatosKubeJS(self.ctx))] return [(g, m) for g, m in lista if m.detectar()] # ------------------------------------------------------------------ acciones diff --git a/src/traductor_es/salida/exportar.py b/src/traductor_es/salida/exportar.py index 5f4b97e..6f99aad 100644 --- a/src/traductor_es/salida/exportar.py +++ b/src/traductor_es/salida/exportar.py @@ -9,8 +9,9 @@ from .copias import Respaldo from .resource_pack import NOMBRE -#: Lo que lee el servidor: misiones y scripts de servidor. Los client_scripts los ejecuta cada jugador. -_DEL_SERVIDOR = ("config/", "kubejs/server_scripts/", "kubejs/startup_scripts/", "patchouli_books/") +#: Lo que lee el servidor: misiones, scripts de servidor y datos (logros, funciones). +#: Los client_scripts los ejecuta cada jugador. +_DEL_SERVIDOR = ("config/", "kubejs/server_scripts/", "kubejs/startup_scripts/", "kubejs/data/", "patchouli_books/") LEEME_KIT = """KIT DE TRADUCCIÓN PARA SERVIDOR ================================ diff --git a/tests/test_comandos_y_datos.py b/tests/test_comandos_y_datos.py new file mode 100644 index 0000000..d962abb --- /dev/null +++ b/tests/test_comandos_y_datos.py @@ -0,0 +1,176 @@ +"""Mensajes de chat: comandos `tellraw`/`say` en scripts de KubeJS, y logros y funciones de `kubejs/data`.""" + +import json +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.formatos import comandos +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.datos import DatosKubeJS +from traductor_es.modulos.scripts import ScriptsKubeJS +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.salida.copias import Respaldo + +from . import fabrica + +SCRIPT = r"""ServerEvents.commandRegistry(event => {}) +PlayerEvents.loggedIn(event => { + event.server.runCommandSilent('tellraw @a {"text":"Welcome to the village!","color":"gold"}') + event.server.runCommand("title @a actionbar {\"text\":\"Market opens soon\"}") + event.server.runCommand('say The festival begins') + event.server.runCommand(`execute as @a run tellraw @s ["",{"text":"Hello ${event.player.username}"}]`) + event.server.runCommandSilent('title @a times 10 70 20') + event.server.runCommandSilent('give @p minecraft:stone 2') + event.server.runCommandSilent('tellraw @a {text:"snbt not touched"}') +}) +""" + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +@pytest.mark.parametrize( + ("texto", "prefijo", "cuerpo", "es_json"), + [ + ('tellraw @a {"text":"Hi"}', "tellraw @a ", '{"text":"Hi"}', True), + ('/tellraw @a[distance=..5, tag=x] ["Hi"]', "/tellraw @a[distance=..5, tag=x] ", '["Hi"]', True), + ('title @s subtitle {"text":"Hi"}', "title @s subtitle ", '{"text":"Hi"}', True), + ("say Hello there", "say ", "Hello there", False), + ("execute as @a run msg @s Hello", "execute as @a run msg @s ", "Hello", False), + ], +) +def test_analiza_comandos_de_chat(texto: str, prefijo: str, cuerpo: str, es_json: bool) -> None: + assert comandos.analizar(texto) == comandos.Comando(prefijo, cuerpo, es_json) + + +@pytest.mark.parametrize("texto", ["give @p stone", "title @a times 1 2 3", "title @a clear", "tellraw @a", "# say hi"]) +def test_ignora_lo_que_no_muestra_texto(texto: str) -> None: + assert comandos.analizar(texto) is None + + +def test_runcommand_traduce_solo_el_mensaje(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "server_scripts" / "chat.js" + ruta.parent.mkdir(parents=True) + ruta.write_text(SCRIPT, encoding="utf-8") + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + resumen = traducir_modulo(ScriptsKubeJS(ctx), Traductor(Simulador(), memoria, Glosario())) + assert resumen.traducidos == 4 and resumen.pendientes == 0 + nuevo = ruta.read_text(encoding="utf-8") + assert """tellraw @a {"text": "«ES» Welcome to the village!", "color": "gold"}""" in nuevo + assert r"""title @a actionbar {\"text\": \"«ES» Market opens soon\"}""" in nuevo + assert "say «ES» The festival begins" in nuevo + assert "${event.player.username}" in nuevo and "execute as @a run tellraw @s " in nuevo + assert "times 10 70 20" in nuevo and "give @p minecraft:stone 2" in nuevo + assert 'tellraw @a {text:"snbt not touched"}' in nuevo + ScriptsKubeJS(ctx).restaurar() + assert ruta.read_text(encoding="utf-8") == SCRIPT + + +LOGRO = { + "display": { + "icon": {"item": "minecraft:wheat"}, + "title": {"text": "First Harvest", "color": "gold"}, + "description": "Harvest your first crop", + "frame": "task", + }, + "criteria": {"x": {"trigger": "minecraft:impossible"}}, +} +FUNCION = ( + "# say no se toca\r\n" + 'tellraw @a {"text":"The shop is open"}\r\n' + "scoreboard players add @a visits 1\r\n" + "say Come in\r\n" +) + + +def test_logros_y_funciones(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + datos = juego / "kubejs" / "data" / "fc" / "advancements" / "farm" + funciones = juego / "kubejs" / "data" / "fc" / "functions" + datos.mkdir(parents=True) + funciones.mkdir(parents=True) + (datos / "harvest.json").write_text(json.dumps(LOGRO), encoding="utf-8") + (juego / "kubejs" / "data" / "fc" / "recipes").mkdir() + (juego / "kubejs" / "data" / "fc" / "recipes" / "r.json").write_text(json.dumps(LOGRO), encoding="utf-8") + (funciones / "open.mcfunction").write_bytes(FUNCION.encode()) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + modulo = DatosKubeJS(ctx) + assert modulo.detectar() and modulo.describir() == "2 archivos de datos" + resumen = traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) + assert resumen.traducidos == 4 and resumen.pendientes == 0 and len(resumen.archivos) == 2 + logro = json.loads((datos / "harvest.json").read_text(encoding="utf-8")) + assert logro["display"]["title"] == {"text": "«ES» First Harvest", "color": "gold"} + assert logro["display"]["description"] == "«ES» Harvest your first crop" + assert logro["display"]["icon"] == LOGRO["display"]["icon"] and logro["criteria"] == LOGRO["criteria"] + nueva = (funciones / "open.mcfunction").read_bytes().decode() + assert nueva == ( + "# say no se toca\r\n" + 'tellraw @a {"text": "«ES» The shop is open"}\r\n' + "scoreboard players add @a visits 1\r\n" + "say «ES» Come in\r\n" + ) + assert json.loads((juego / "kubejs" / "data" / "fc" / "recipes" / "r.json").read_text("utf-8")) == LOGRO + modulo.restaurar() + assert json.loads((datos / "harvest.json").read_text(encoding="utf-8")) == LOGRO + assert (funciones / "open.mcfunction").read_bytes().decode() == FUNCION + + +ARMADO = """const efectos = [ + { id: 'minecraft:luck', name: 'Luck' }, + { id: 'minecraft:speed', name: 'Speed' }, +] +const avisar = (player, lista) => { + let mensaje = 'The food has given you ' + for (let i = 0; i < lista.length; i++) { + if (i > 0) mensaje += ', ' + if (i > 0 && i === lista.length - 1) mensaje += 'and ' + mensaje += lista[i].name + mensaje += global.nombres[lista[i]] + } + let otro = 'Not a message at all' + player.tell(mensaje) + if (player.name == 'Steve') player.tell('Hi') +} +""" +TABLA = """global.nombres = { + fcSweet: 'Sweet', + fcSavory: 'Savory', +} +global.ids = { dulce: 'Not shown' } +""" + + +def test_mensajes_armados_por_partes(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + server = juego / "kubejs" / "server_scripts" / "dieta.js" + startup = juego / "kubejs" / "startup_scripts" / "globales.js" + for ruta, src in ((server, ARMADO), (startup, TABLA)): + ruta.parent.mkdir(parents=True, exist_ok=True) + ruta.write_text(src, encoding="utf-8") + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + resumen = traducir_modulo(ScriptsKubeJS(ctx), Traductor(Simulador(), memoria, Glosario())) + nuevo = server.read_text(encoding="utf-8") + assert "let mensaje = '«ES» The food has given you '" in nuevo and "mensaje += '«ES» and '" in nuevo + assert "name: '«ES» Luck'" in nuevo and "name: '«ES» Speed'" in nuevo + assert "id: 'minecraft:luck'" in nuevo and "mensaje += ', '" in nuevo # sin letras: no es texto + assert "let otro = 'Not a message at all'" in nuevo # no llega a ningún mensaje + assert "tell('«ES» Hi')" in nuevo and "'Steve'" in nuevo + tabla = startup.read_text(encoding="utf-8") + assert "fcSweet: '«ES» Sweet'" in tabla and "fcSavory: '«ES» Savory'" in tabla + assert "dulce: 'Not shown'" in tabla # esa tabla no se suma a ningún mensaje + assert resumen.pendientes == 0 + ScriptsKubeJS(ctx).restaurar() + assert server.read_text(encoding="utf-8") == ARMADO and startup.read_text(encoding="utf-8") == TABLA From f6292040fb670b1dd9dadde8932570906abaa825 Mon Sep 17 00:00:00 2001 From: Cristian Date: Fri, 6 Nov 2026 00:30:48 -0500 Subject: [PATCH 02/24] feat(datos): traducir datapacks, logros y tips en cualquier modpack e informar de lo que queda MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Funciones, logros y tips se buscan por su estructura (data//function(s)...) en cualquier carpeta, sueltos o dentro de .zip, con copia de seguridad. - Componentes de texto en JSON y en SNBT (1.21.5+). - notify, kick y los títulos de menús de KubeJS, tomados de su código. - Más ▸ Qué queda en inglés y `modpackes cobertura`: frases de scripts sin elegir y texto escrito en el código de los mods (tabla de constantes de cada clase). Solo lee. - Pruebas con modpacks públicos de Modrinth y con la instancia propia: validez, idempotencia y restauración byte a byte. --- CHANGELOG.md | 26 +- CONTRIBUTING.md | 11 +- README.md | 6 +- scripts/descargar_corpus.py | 39 ++- src/traductor_es/__main__.py | 10 + src/traductor_es/formatos/componente.py | 50 ++++ src/traductor_es/modulos/cobertura.py | 153 ++++++++++++ src/traductor_es/modulos/datos.py | 315 +++++++++++++++++------- src/traductor_es/modulos/scripts.py | 37 ++- src/traductor_es/proceso.py | 27 +- src/traductor_es/salida/exportar.py | 13 +- src/traductor_es/ui/ventana.py | 33 ++- tests/test_cobertura.py | 73 ++++++ tests/test_comandos_y_datos.py | 8 +- tests/test_corpus_datos.py | 135 ++++++++++ tests/test_datos_generales.py | 184 ++++++++++++++ tests/test_ui.py | 16 ++ 17 files changed, 1025 insertions(+), 111 deletions(-) create mode 100644 src/traductor_es/formatos/componente.py create mode 100644 src/traductor_es/modulos/cobertura.py create mode 100644 tests/test_cobertura.py create mode 100644 tests/test_corpus_datos.py create mode 100644 tests/test_datos_generales.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d56ea0..76d83f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,24 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado -- Mensajes de chat de los scripts de KubeJS: el texto de `tellraw`, `title` y `say` (y `msg`, `tell`, `w`) - cuando se manda con `runCommand` o `runCommandSilent`. Antes solo se traducía lo que pasaba por `tell`, - `Text.of` y similares. -- Mensajes que un script arma por partes (`let mensaje = 'Tienes '; mensaje += efecto.name; player.tell(mensaje)`): - los trozos de texto, los `name` que se suman y las tablas de nombres que se suman (`nombres[clave]`). Solo - cuando la variable acaba en una llamada de texto como `tell`. -- Logros (título y descripción) y funciones `.mcfunction` de `kubejs/data`, que también muestran mensajes en - el chat. Van en el grupo Scripts, con copia de seguridad, y entran en el kit para servidores. +- **Mensajes de chat y datos de texto**, que antes quedaban en inglés: + - El texto de `tellraw`, `title`, `say`, `msg`, `tell` y `w` en las funciones (`.mcfunction`) y en lo que un script de + KubeJS manda con `runCommand` o `runCommandSilent`. + - Títulos y descripciones de logros, y los consejos de las pantallas de carga (mod Tips, en sus dos formatos). + - Mensajes que un script arma por partes (`mensaje += efecto.name; player.tell(mensaje)`), con sus tablas de nombres. + - `notify`, `kick`, `openChestGUI` y `openInventoryGUI` de KubeJS (métodos tomados del código de KubeJS 1.20 y 1.21). +- Estos datos se buscan por su estructura y no por la carpeta de un modpack: `kubejs/data`, `global_packs`, `datapacks`, + `config/openloader`, los datapacks de un mundo y cualquier otra con `data//functions/…`, sueltos o dentro de un `.zip` + (se reescribe el zip y se guarda el original). Los componentes de texto valen en JSON y en SNBT (1.21.5 y posteriores). +- **Más ▸ Qué queda en inglés** y `modpackes cobertura`: lista lo que no se puede traducir y por qué (frases de scripts cuyo uso + no se entiende y texto escrito en el código de los mods, leído de la tabla de constantes de cada clase). Solo lee. + El análisis avisa de las frases de scripts que parecen texto. +- Pruebas con modpacks públicos de Modrinth (9 más en `scripts/descargar_corpus.py`) y con la instancia propia: lo traducido + sigue siendo válido, repetir no cambia nada y restaurar devuelve los bytes originales. + +### Cambiado + +- El kit para servidores incluye las funciones y los logros traducidos dondequiera que estén. ## [2.2.0] - 2026-11-04 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7fcbd5..e2a08ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,12 +78,21 @@ referencia también baja el puntaje, así que revisa los ejemplos que lista el s | `src/traductor_es/nucleo` | Marcadores de códigos, memoria y correcciones, glosario, variantes de español y el orquestador de traducción | | `src/traductor_es/formatos` | Lectura y escritura sin pérdidas: SNBT, JSON, NBT, `.lang`, JavaScript y markdown | | `src/traductor_es/fuentes` | Lanzadores, versión, loader, jars y prioridad de los resource packs | -| `src/traductor_es/modulos` | Misiones, libros, scripts y mods: qué se traduce y dónde se escribe | +| `src/traductor_es/modulos` | Misiones, libros, scripts, datos (funciones, logros, tips) y mods: qué se traduce y dónde se escribe; `cobertura.py` dice qué queda en inglés | | `src/traductor_es/motores` | Gemini, el motor compatible con OpenAI, las instrucciones comunes a todos, la fábrica de motores y el simulador para pruebas | | `src/traductor_es/salida` | Resource pack, `options.txt`, copias de seguridad y exportación | | `src/traductor_es/validacion` | Reglas de calidad, revisión automática, medición con el banco de referencia y el validador con los parsers del juego (Java) | | `src/traductor_es/ui` | La ventana y el editor de correcciones (PySide6) | +## Agregar un formato de texto nuevo + +Los archivos de texto de datapacks y packs de recursos (funciones, logros, tips…) se reconocen por su ruta y su formato, +no por el modpack. Para agregar uno, escribe un `Formato` en `src/traductor_es/modulos/datos.py` (qué ruta lo reconoce, +cómo saca los textos y cómo los reescribe), cita su documentación en un comentario y agrega a `tests/` una prueba que +traduzca un caso real recortado, otra con algo que **no** debe tocarse y la de ida y vuelta (restaurar devuelve los bytes +originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks públicos de Modrinth a `.descargas/` +(Git lo ignora); `uv run pytest -m corpus` los usa para comprobar que lo traducido sigue siendo válido. + ## Licencia de las contribuciones ModpackES usa la licencia [GPL-3.0-or-later](LICENSE). Al mandar un cambio aceptas que se publique bajo esa diff --git a/README.md b/README.md index cfe0e6e..552bfbe 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Textos de mods (objetos, bloques, menús, tooltips) | `lang/*.json` (1.13+) y `.lang` (1.12 y antes), incluidos los jars anidados, KubeJS y los resource packs del modpack | Resource pack `Traduccion_ES` | | Misiones | FTB Quests (texto en los capítulos, carpeta `lang/` de 1.21+ y NBT de 1.12), BetterQuesting, HQM, Heracles | En su sitio, con copia de seguridad | | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | -| Scripts de KubeJS | Lo que ve el jugador: `tell`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand`. También los logros y funciones de `kubejs/data` | En su sitio, con copia de seguridad | +| Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | +| Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -84,6 +85,8 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. +**Más ▸ Qué queda en inglés** (o `modpackes cobertura RUTA [--java]`) lista lo que no se traduce y por qué, sin cambiar nada: frases de scripts cuyo uso no se entiende y texto escrito en el código de los mods, que un resource pack no puede alcanzar. El análisis ya avisa de las frases de scripts. + **Más ▸ Restaurar el original** devuelve misiones, scripts y libros propios al texto del modpack. Para quitar la traducción de los mods basta con desactivar el resource pack. @@ -182,6 +185,7 @@ uv run modpackes instancias uv run modpackes analizar "RUTA DE LA INSTANCIA" uv run modpackes traducir "RUTA" [--dialecto es_es] [--solo misiones,libros] [--lotes] [--modelo gemini-3.5-flash-lite] [--contexto "granja acogedora"] [--revisar] uv run modpackes restaurar "RUTA" +uv run modpackes cobertura "RUTA" [--java] # qué queda en inglés y por qué (solo lee) uv run modpackes clave guardar [--motor openrouter] uv run modpackes modelos [--motor ollama] ``` diff --git a/scripts/descargar_corpus.py b/scripts/descargar_corpus.py index c28a2f7..b353932 100644 --- a/scripts/descargar_corpus.py +++ b/scripts/descargar_corpus.py @@ -25,6 +25,26 @@ "technical-electrical": "https://cdn.modrinth.com/data/PmwWNPV6/versions/mkpGUYy6/Technical%20Electrical%20Temporal%20Totality-26.1.8.mrpack", "supernova-empires-2": "https://cdn.modrinth.com/data/Or9UFKR8/versions/ndm0obMO/Supernova%20Empires%202%207.0.3.mrpack", "prominence-2-fabric": "https://cdn.modrinth.com/data/EGs3lC8D/versions/ga7DDH29/Prominence%E2%84%A2%20II%20Hasturian%20Era%20v4.1.6.mrpack", + # Packs con datapacks, funciones, logros y scripts propios (para probar los datos de texto). Elegidos + # mirando el índice del .mrpack: el contenido de cada uno está en el comentario. + # 1.20.1 fabric · funciones, logros, datapacks .zip + "animatecraft-lankeren": "https://cdn.modrinth.com/data/rqoNUSYX/versions/iRG9ktbA/Lankeren%201.1.5-fixed.mrpack", + # 1.20.1 neoforge · kubejs y kubejs/data + "slimecrew": "https://cdn.modrinth.com/data/u3eBhKdv/versions/VHuCfVgS/SlimeCrew%20Frontier%201.3.0.mrpack", + # 1.20.1 forge · kubejs/data + "omnicraft-modpack": "https://cdn.modrinth.com/data/hcyfOf6Q/versions/Zh2npQSZ/Omnicraft%202.0.mrpack", + # 1.20.1 forge · kubejs y kubejs/data + "stormforge": "https://cdn.modrinth.com/data/Mclf6Juw/versions/Q1Znj0cU/Stormforge%202.1.7.mrpack", + # 1.21.1 neoforge · kubejs + "create-cobblemon-season-1": "https://cdn.modrinth.com/data/Mo3YEbmr/versions/KFjm9wO7/Create%20Cobblemon%20Season%201%201.0.2.mrpack", + # 1.20.1 fabric · logros, datapacks .zip + "prominence-2-chinese": "https://cdn.modrinth.com/data/2Vp3Sdd5/versions/ttpetMlI/Prominence%20II%20Hasturian%20Era%20CN%203.9.2.mrpack", + # 1.20.1 forge · funciones + "battlearmorytacz": "https://cdn.modrinth.com/data/rQiXwLhB/versions/TX0XBjIG/BattleArmory%20TACZ%2026.0.5.mrpack", + # 1.20.1 forge · datapacks .zip + "better-mc-forge-bmc4": "https://cdn.modrinth.com/data/4BV47HRn/versions/bdRIjRgs/Better%20MC%20%5BFORGE%5D%201.20.1%201.20.1%20v59.mrpack", + # 1.20.1 fabric · kubejs, datapacks .zip + "landscapes-reimagined-genesis": "https://cdn.modrinth.com/data/MdznSbKA/versions/pBrfi38A/Landscapes%20Reimagined%20Genesis%203.2.mrpack", } #: Qué se baja de cada pack (rutas dentro del .mrpack). @@ -40,6 +60,12 @@ "overrides/kubejs/server_scripts/", "overrides/kubejs/startup_scripts/", "overrides/patchouli_books/", + # Datos de texto que ve el jugador: funciones, logros y datapacks, vengan de donde vengan + "overrides/kubejs/data/", + "overrides/global_packs/", + "overrides/datapacks/", + "overrides/config/openloader/", + "overrides/config/paxi/", ) EXTENSIONES_KUBEJS_ASSETS = ("/lang/",) # de kubejs/assets solo los idiomas LIMITE_POR_PACK = 40 * 1024 * 1024 @@ -111,7 +137,9 @@ def _elegida(nombre: str) -> bool: return False if nombre in EXACTOS or nombre.startswith(PREFIJOS): return True - return nombre.startswith("overrides/kubejs/assets/") and any(x in nombre for x in EXTENSIONES_KUBEJS_ASSETS) + if nombre.startswith(("overrides/kubejs/assets/", "overrides/resourcepacks/")) and nombre.endswith(".json"): + return any(x in nombre for x in (*EXTENSIONES_KUBEJS_ASSETS, "/tips/")) + return False def _bajar(url: str, e: Entrada) -> bytes: @@ -151,9 +179,14 @@ def bajar_pack(slug: str, url: str) -> None: def main() -> int: elegidos = sys.argv[1:] or list(PACKS) + fallos = 0 for slug in elegidos: - bajar_pack(slug, PACKS[slug]) - return 0 + try: + bajar_pack(slug, PACKS[slug]) + except (RuntimeError, OSError) as e: # un pack que no se puede bajar no detiene a los demás + print(f"{slug}: no se bajó ({e})") + fallos += 1 + return 1 if fallos == len(elegidos) else 0 if __name__ == "__main__": diff --git a/src/traductor_es/__main__.py b/src/traductor_es/__main__.py index fd24e3e..73cc50b 100644 --- a/src/traductor_es/__main__.py +++ b/src/traductor_es/__main__.py @@ -85,6 +85,9 @@ def comunes(p: argparse.ArgumentParser) -> None: traducir.add_argument("--simular", action="store_true", help="prueba sin API: antepone «ES» a todo") traducir.add_argument("--no-activar", action="store_true", help="no tocar options.txt") traducir.add_argument("--no-idioma", action="store_true", help="no cambiar el idioma del juego") + cob = sub.add_parser("cobertura", help="dice qué texto queda en inglés y por qué (solo lee, no escribe nada)") + cob.add_argument("carpeta", help="carpeta de la instancia (o su carpeta minecraft)") + cob.add_argument("--java", action="store_true", help="también el texto escrito en el código de los mods (tarda)") restaurar = sub.add_parser("restaurar", help="devuelve misiones, scripts y libros propios a su original") restaurar.add_argument("carpeta") modelos = sub.add_parser("modelos", help="lista los modelos que ofrece el motor") @@ -124,6 +127,13 @@ def main(argv: Sequence[str] | None = None) -> int: if args.orden == "instancias": for e in buscar(): _escribir(f"{e.lanzador}: {e.nombre}\n {e.juego}") + elif args.orden == "cobertura": + with Proceso(args.carpeta, Simulador(), registro=_escribir) as p: + _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") + hallazgos = p.que_queda(java=args.java) + for h in hallazgos: + _escribir(f"[{h.categoria}] {h}") + _escribir(f"{len(hallazgos)} hallazgos." if hallazgos else "No encontré texto que quede en inglés.") elif args.orden == "clave": nombre = fabrica.MOTORES[args.motor] if args.accion == "guardar": diff --git a/src/traductor_es/formatos/componente.py b/src/traductor_es/formatos/componente.py new file mode 100644 index 0000000..df018a3 --- /dev/null +++ b/src/traductor_es/formatos/componente.py @@ -0,0 +1,50 @@ +"""Texto de un componente de Minecraft, sea cual sea la versión: JSON o SNBT. + +- Hasta 1.21.4 los componentes (`tellraw`, `title`, títulos de logros…) son JSON: `{"text":"Hola"}`. +- Desde 1.21.5 se guardan como NBT y en los comandos se escriben en SNBT: `{text:"Hola"}` (Minecraft Wiki, + «Text component format»). Una cadena suelta entre comillas y las listas valen en los dos formatos. + +Solo se traduce lo que ve el jugador: los campos `text`, las cadenas sueltas y el contenido de `extra`. Los +colores, las claves `translate`, los `score`/`selector`/`nbt` (los resuelve el servidor) y las acciones quedan igual. +""" + +from __future__ import annotations + +from collections.abc import Mapping + +from ..nucleo.mascara import es_traducible +from . import json_texto, snbt + +#: Campos SNBT cuyo valor es texto visible: `text`, el contenido de `extra` y las cadenas sueltas (sin campo). +_CAMPOS_VISIBLES = frozenset({None, "text", "extra"}) + + +def _cadenas_snbt(cuerpo: str) -> list[snbt.Cadena] | None: + t = cuerpo.strip() + if not t or t[0] not in "{[\"'": + return None + try: + return [c for c in snbt.escanear(cuerpo) if c.campo in _CAMPOS_VISIBLES] + except ValueError: + return None + + +def unidades(cuerpo: str) -> list[str] | None: + """Los textos visibles del componente, o None si no se reconoce el formato (no se toca).""" + partes = json_texto.extraer(cuerpo, literal=True) + if partes is not None: + return [p for p in partes if es_traducible(p)] + cadenas = _cadenas_snbt(cuerpo) + return None if cadenas is None else [c.texto for c in cadenas if c.texto.strip() and es_traducible(c.texto)] + + +def reconstruir(cuerpo: str, traducciones: Mapping[str, str]) -> str: + """El componente con los textos traducidos; lo demás (formato, colores, espacios del SNBT) queda igual.""" + if json_texto.cargar(cuerpo, literal=True) is not None: + return json_texto.reconstruir(cuerpo, traducciones, literal=True) + cadenas = _cadenas_snbt(cuerpo) + if cadenas is None: + return cuerpo + return snbt.reemplazar( + cuerpo, cadenas, {c.inicio: traducciones[c.texto] for c in cadenas if c.texto in traducciones} + ) diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py new file mode 100644 index 0000000..57826fc --- /dev/null +++ b/src/traductor_es/modulos/cobertura.py @@ -0,0 +1,153 @@ +"""Qué texto en inglés queda en la instancia aunque se traduzca todo, y por qué. + +La traducción no puede cubrirlo todo: un resource pack no llega al texto escrito en el código de un mod, y hay +cadenas de scripts cuyo uso no se entiende. Este informe lo dice con datos en lugar de dejar al jugador con la duda. +Solo lee; no escribe nada en la instancia. +""" + +from __future__ import annotations + +import re +import struct +import zipfile +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path + +from ..fuentes.instancia import Instancia +from ..nucleo.mascara import es_traducible +from .base import Contexto, parece_clave +from .scripts import ScriptsKubeJS + +MAX_EJEMPLOS = 3 +#: Nombres que delatan una clase que construye texto: `Component.literal` (Mojang), `m_237113_` (SRG 1.19+), +#: `method_43470` (Fabric) y los `TextComponent`, `StringTextComponent` y `TextComponentString` anteriores. +_MARCAS_TEXTO = ("literal", "m_237113_", "method_43470") +_CLASES_TEXTO = ("TextComponent", "StringTextComponent", "TextComponentString") +_FRASE = re.compile(r"[A-Z][A-Za-z']+(?: [A-Za-z0-9,.'!?:%\-]+){2,}[.!?:]?") +_ESPANOL = re.compile(r"\b(de|la|el|los|las|con|sin|para|por|una|un|del)\b") +#: Frases de registros y errores internos: casi nunca llegan al jugador. +_INTERNO = re.compile( + r"^(Failed|Could not|Couldn't|Unable|Unexpected|Invalid|Missing|Exception|Error|Cannot|No )", re.I +) +#: Tamaño de cada tipo de constante de un .class (tras la etiqueta), según la especificación de la JVM +_TAMANO_CONSTANTE = { + 3: 4, + 4: 4, + 5: 8, + 6: 8, + 7: 2, + 8: 2, + 9: 4, + 10: 4, + 11: 4, + 12: 4, + 15: 3, + 16: 2, + 17: 4, + 18: 4, + 19: 2, + 20: 2, +} + + +@dataclass(frozen=True, slots=True) +class Hallazgo: + """Una cosa que queda en inglés: dónde, cuántos textos y unos ejemplos.""" + + categoria: str #: «script» o «java» + donde: str + cantidad: int + motivo: str + ejemplos: tuple[str, ...] = () + + def __str__(self) -> str: + ej = f" Por ejemplo: {' | '.join(f'«{e}»' for e in self.ejemplos)}." if self.ejemplos else "" + return f"{self.donde}: {self.cantidad} {'texto' if self.cantidad == 1 else 'textos'}. {self.motivo}.{ej}" + + +def hallazgos_de_scripts(juego: Path, sin_elegir: list[tuple[Path, list[str]]]) -> list[Hallazgo]: + """Frases de los scripts que parecen texto y que la traducción no tocó (pueden ser mensajes).""" + return [ + Hallazgo( + "script", ruta.relative_to(juego).as_posix(), len(textos), + "Parece una frase, pero no se usa como texto visible (puede ser lógica o un mensaje armado de otra forma)", + tuple(t[:60] for t in textos[:MAX_EJEMPLOS]), + ) + for ruta, textos in sin_elegir + ] # fmt: skip + + +def constantes_utf8(datos: bytes) -> list[str]: + """Las cadenas de la tabla de constantes de un .class (JVMS §4.4). Vacío si el archivo no se entiende.""" + if len(datos) < 10 or datos[:4] != b"\xca\xfe\xba\xbe": + return [] + (cantidad,) = struct.unpack_from(">H", datos, 8) + pos, i = 10, 1 + textos: list[str] = [] + try: + while i < cantidad: + etiqueta = datos[pos] + pos += 1 + if etiqueta == 1: + (largo,) = struct.unpack_from(">H", datos, pos) + textos.append(datos[pos + 2 : pos + 2 + largo].decode("utf-8", "ignore")) + pos += 2 + largo + else: + pos += _TAMANO_CONSTANTE[etiqueta] + i += 1 if etiqueta in (5, 6) else 0 # long y double ocupan dos casillas + i += 1 + except (IndexError, KeyError, struct.error): + return [] + return textos if pos <= len(datos) else [] # si la tabla se pasa del final, el archivo está truncado + + +def _frases_de_clase(datos: bytes) -> list[str]: + constantes = constantes_utf8(datos) + if not any(c in _MARCAS_TEXTO or any(n in c for n in _CLASES_TEXTO) for c in constantes): + return [] # la clase no construye texto para el jugador + return [ + c for c in constantes + if len(c) > 14 and _FRASE.fullmatch(c) and not _INTERNO.match(c) and not _ESPANOL.search(c.lower()) + ] # fmt: skip + + +def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = lambda _a, _b: None) -> list[Hallazgo]: + """Frases en inglés escritas en el código de los mods. Un resource pack no puede traducirlas.""" + res: list[Hallazgo] = [] + jars = instancia.jars() + for i, jar in enumerate(jars): + progreso(i, len(jars)) + frases: list[str] = [] + try: + with zipfile.ZipFile(jar) as z: + for info in z.infolist(): + if ( + info.filename.endswith(".class") + and not info.filename.startswith("META-INF") + and info.file_size < 3_000_000 + ): + frases += _frases_de_clase(z.read(info)) + except (zipfile.BadZipFile, OSError): + continue + unicas = list(dict.fromkeys(f for f in frases if es_traducible(f) and not parece_clave(f))) + if unicas: + res.append(Hallazgo( + "java", jar.name, len(unicas), + "Escrito en el código del mod: un resource pack no lo traduce", + tuple(f[:60] for f in unicas[:MAX_EJEMPLOS]), + )) # fmt: skip + return sorted(res, key=lambda h: -h.cantidad) + + +def revisar_cobertura( + ctx: Contexto, java: bool = False, progreso: Callable[[int, int], None] = lambda _a, _b: None +) -> list[Hallazgo]: + """Lo que queda en inglés. El análisis del código Java tarda (lee todos los mods): solo si se pide.""" + hallazgos = hallazgos_de_scripts(ctx.instancia.juego, ScriptsKubeJS(ctx).candidatas()) + if java: + hallazgos += textos_en_java(ctx.instancia, progreso) + return hallazgos + + +__all__ = ["Hallazgo", "revisar_cobertura"] diff --git a/src/traductor_es/modulos/datos.py b/src/traductor_es/modulos/datos.py index 755eb16..a5fd297 100644 --- a/src/traductor_es/modulos/datos.py +++ b/src/traductor_es/modulos/datos.py @@ -1,26 +1,37 @@ -"""Textos de los datos de KubeJS (`kubejs/data`): títulos de logros y mensajes de las funciones. +"""Textos de datapacks y packs de recursos que el jugador ve en el chat o en pantalla: funciones, logros y tips. -Los logros salen en el chat cuando el jugador los consigue, y las funciones (`.mcfunction`) pueden mandar -mensajes con `tellraw`, `title` o `say`. Los archivos se traducen en el sitio, con copia de seguridad. +Los archivos se buscan por su **estructura** y no por la carpeta de un modpack concreto: cualquier ruta que +contenga `data//function(s)/…`, `data//advancement(s)/…` o `assets//tips/…` cuenta, +esté en `kubejs/data`, `global_packs/*/*`, `datapacks`, `config/openloader`, `saves/*/datapacks`… Cada formato +reconocido es un `Formato` con su documentación; lo que no se reconoce no se toca. Se traduce en el sitio, con +copia de seguridad. """ from __future__ import annotations +import io import json -from collections.abc import Mapping +import os +import zipfile +from collections.abc import Callable, Iterator, Mapping from dataclasses import dataclass, field from pathlib import Path from typing import Any, cast -from ..formatos import comandos +from ..formatos import comandos, componente from ..nucleo.mascara import es_traducible -from .base import Contexto, Pieza, Resumen, aplicar, contar, parece_clave, unidades +from ..salida.resource_pack import NOMBRE as NOMBRE_PACK_SALIDA +from .base import Contexto, Pieza, Resumen, contar, parece_clave from .scripts import AMBITO, aplicar_comando, unidades_comando -RAIZ = "kubejs/data" -#: Carpeta de logros y de funciones dentro de un espacio de nombres (el nombre cambió entre versiones). -LOGROS = frozenset({"advancements", "advancement"}) -FUNCIONES = frozenset({"functions", "function"}) +#: Carpetas que nunca se recorren: cachés, registros, mods (sus jars se leen en otro sitio) y lo que genera la app. +IGNORAR = frozenset({ + "mods", "logs", "crash-reports", "traductor_es", "cache", "libraries", "versions", "screenshots", "shaderpacks", + "backups", "probe", "node_modules", "modernfix", "xaerowaypoints", "xaeroworldmap", "journeymap", + NOMBRE_PACK_SALIDA.lower(), +}) # fmt: skip +PROFUNDIDAD_MAXIMA = 10 +LIMITE_ZIP = 64 * 1024 * 1024 #: más grande que esto no es un datapack, es un resource pack pesado: no se abre CAMPOS_LOGRO = ("title", "description") @@ -28,27 +39,19 @@ def _componente(valor: Any) -> str: return json.dumps(valor, ensure_ascii=False) -def _logro(datos: Any) -> dict[str, Any] | None: - """El bloque `display` de un logro, o None si el archivo no es un logro con texto.""" - if not isinstance(datos, dict): - return None - display = cast("dict[str, Any]", datos).get("display") - return cast("dict[str, Any]", display) if isinstance(display, dict) else None - - -@dataclass(slots=True) -class _Archivo: - ruta: Path - texto: str - #: Logros: el JSON entero. Funciones: None (se trabaja por líneas). - datos: Any = None - piezas: list[str] = field(default_factory=list[str]) +def _traducibles(partes: list[str] | None) -> list[str]: + return [p for p in partes or [] if not parece_clave(p)] -def _unidades_logro(display: Mapping[str, Any]) -> list[str]: - return [t for campo in CAMPOS_LOGRO if campo in display for t in unidades(_componente(display[campo]), True)] +def _nodos(datos: Any, claves: tuple[str, ...]) -> list[tuple[dict[str, Any], str]]: + """(diccionario, clave) de cada valor de texto que hay que traducir dentro de un JSON.""" + if not isinstance(datos, dict): + return [] + d = cast("dict[str, Any]", datos) + return [(d, k) for k in claves if k in d] +# ---------------------------------------------------------------- funciones (.mcfunction) def _lineas(texto: str) -> list[tuple[str, comandos.Comando | None, str]]: """Cada línea separada en (línea sin fin, su comando de chat, fin de línea).""" res: list[tuple[str, comandos.Comando | None, str]] = [] @@ -60,86 +63,230 @@ def _lineas(texto: str) -> list[tuple[str, comandos.Comando | None, str]]: return res -class DatosKubeJS: - nombre = "Logros y funciones de KubeJS" +def _unidades_funcion(texto: str) -> list[str]: + return [t for _, cmd, _ in _lineas(texto) if cmd for t in unidades_comando(cmd)] + + +def _reescribir_funcion(texto: str, traducciones: Mapping[str, str]) -> str: + return "\n".join((aplicar_comando(cmd, traducciones) if cmd else linea) + fin for linea, cmd, fin in _lineas(texto)) + + +# ---------------------------------------------------------------- JSON con componentes (logros, tips) +def _nodos_logro(datos: Any) -> list[tuple[dict[str, Any], str]]: + """`display.title` y `display.description` de un logro.""" + display = cast("dict[str, Any]", datos).get("display") if isinstance(datos, dict) else None + return _nodos(display, CAMPOS_LOGRO) + + +def _nodos_tip(datos: Any) -> list[tuple[dict[str, Any], str]]: + """Tips: `{"tip": …}` (Tips de Darkhax) y `{"tips": [{"text": …}]}` (el port de NeoForge).""" + if not isinstance(datos, dict): + return [] + d = cast("dict[str, Any]", datos) + nodos = _nodos(d, ("tip",)) + lista = d.get("tips") + if isinstance(lista, list): + for t in cast("list[Any]", lista): + nodos += _nodos(t, ("text",)) + return nodos + + +def _unidades_json(texto: str, nodos: Callable[[Any], list[tuple[dict[str, Any], str]]]) -> list[str]: + return [u for d, k in nodos(json.loads(texto)) for u in _traducibles(componente.unidades(_componente(d[k])))] + + +def _reescribir_json( + texto: str, traducciones: Mapping[str, str], nodos: Callable[[Any], list[tuple[dict[str, Any], str]]] +) -> str: + datos = json.loads(texto) + for d, k in nodos(datos): + d[k] = json.loads(componente.reconstruir(_componente(d[k]), traducciones)) + return json.dumps(datos, indent=2, ensure_ascii=False) + "\n" + + +@dataclass(frozen=True, slots=True) +class Formato: + nombre: str + pista: str #: contexto que se le da al motor de traducción + reconoce: Callable[[tuple[str, ...]], bool] #: recibe la ruta desde «data» o «assets»: (data, espacio, carpeta, …) + unidades: Callable[[str], list[str]] #: puede lanzar ValueError si el archivo no se entiende + reescribir: Callable[[str, Mapping[str, str]], str] + + +def _es(ruta: tuple[str, ...], raiz: str, carpetas: tuple[str, ...], sufijo: str) -> bool: + return len(ruta) >= 4 and ruta[0] == raiz and ruta[2] in carpetas and ruta[-1].lower().endswith(sufijo) + + +FORMATOS: tuple[Formato, ...] = ( + Formato( + "función", "mensaje de una función", + lambda r: _es(r, "data", ("function", "functions"), ".mcfunction"), + _unidades_funcion, _reescribir_funcion, + ), + Formato( + "logro", "logro del juego", + lambda r: _es(r, "data", ("advancement", "advancements"), ".json"), + lambda t: _unidades_json(t, _nodos_logro), lambda t, tr: _reescribir_json(t, tr, _nodos_logro), + ), + Formato( + "tip", "consejo de las pantallas de carga", + lambda r: _es(r, "assets", ("tips",), ".json") or (_es(r, "data", ("tips",), ".json") and r[1] == "tipsmod"), + lambda t: _unidades_json(t, _nodos_tip), lambda t, tr: _reescribir_json(t, tr, _nodos_tip), + ), +) # fmt: skip + + +def clasificar(ruta: Path) -> Formato | None: + """El formato de este archivo según su ruta, o None. Busca el ancla «data» o «assets» en cualquier posición.""" + partes = ruta.parts + for i, parte in enumerate(partes): + if parte in ("data", "assets"): + for formato in FORMATOS: + if formato.reconoce(partes[i:]): + return formato + return None + + +def _recorrer(juego: Path) -> Iterator[Path]: + """Cada archivo de la instancia, sin entrar en mods, cachés ni mundos (de un mundo solo sus datapacks).""" + base = len(juego.parts) + for raiz, carpetas, archivos in os.walk(juego): + actual = Path(raiz) + if len(actual.parts) - base >= PROFUNDIDAD_MAXIMA: + carpetas.clear() + elif actual.parent.name.lower() == "saves": + carpetas[:] = [c for c in carpetas if c.lower() == "datapacks"] + else: + carpetas[:] = sorted(c for c in carpetas if c.lower() not in IGNORAR and not c.startswith(".")) + for nombre in sorted(archivos): + yield actual / nombre + + +def buscar_archivos(juego: Path) -> list[tuple[Path, Formato]]: + """Todos los archivos de texto conocidos que están sueltos en la instancia.""" + encontrados: list[tuple[Path, Formato]] = [] + for ruta in _recorrer(juego): + if (formato := clasificar(ruta.relative_to(juego))) is not None: + encontrados.append((ruta, formato)) + return encontrados + + +def _entradas_del_zip(ruta: Path) -> list[tuple[str, Formato]]: + """Las entradas de un .zip (datapack o pack de recursos) con texto conocido. Solo lee su índice.""" + if ruta.stat().st_size > LIMITE_ZIP: + return [] + try: + with zipfile.ZipFile(ruta) as z: + return [(n, f) for n in z.namelist() if not n.endswith("/") and (f := clasificar(Path(n))) is not None] + except (zipfile.BadZipFile, OSError): + return [] + + +def buscar_zips(juego: Path) -> list[tuple[Path, list[tuple[str, Formato]]]]: + """Los .zip con texto conocido dentro (datapacks de `global_packs`, `datapacks`, mundos…).""" + res: list[tuple[Path, list[tuple[str, Formato]]]] = [] + for ruta in _recorrer(juego): + if ruta.suffix.lower() == ".zip" and (entradas := _entradas_del_zip(ruta)): + res.append((ruta, entradas)) + return res + + +@dataclass(slots=True) +class _Archivo: + ruta: Path #: el archivo suelto o el .zip que lo contiene + formato: Formato + texto: str + piezas: list[str] = field(default_factory=list[str]) + entrada: str | None = None #: nombre dentro del .zip; None si es un archivo suelto + + +def _reescribir_zip(original: bytes, nuevos: Mapping[str, str]) -> bytes: + """El mismo .zip con esas entradas cambiadas; las demás, el orden y las fechas quedan igual.""" + salida = io.BytesIO() + with zipfile.ZipFile(io.BytesIO(original)) as entrada, zipfile.ZipFile(salida, "w") as z: + for info in entrada.infolist(): + datos = nuevos[info.filename].encode("utf-8") if info.filename in nuevos else entrada.read(info) + z.writestr(info, datos, compress_type=info.compress_type) + return salida.getvalue() + + +class Datos: + nombre = "Funciones, logros y tips" ambito = AMBITO def __init__(self, ctx: Contexto) -> None: self.ctx = ctx - self._archivos_leidos: list[_Archivo] = [] - - def _archivos(self) -> list[Path]: - raiz = self.ctx.instancia.juego / RAIZ - if not raiz.is_dir(): - return [] - encontrados: list[Path] = [] - for ruta in sorted(raiz.rglob("*")): - partes = ruta.relative_to(raiz).parts # (espacio de nombres, carpeta, …) - if len(partes) < 3 or not ruta.is_file(): - continue - if (partes[1] in LOGROS and ruta.suffix == ".json") or ( - partes[1] in FUNCIONES and ruta.suffix == ".mcfunction" - ): - encontrados.append(ruta) - return encontrados + self._leidos: list[_Archivo] = [] + self._zips: dict[Path, bytes] = {} #: contenido original de cada .zip leído def detectar(self) -> bool: - return bool(self._archivos()) + juego = self.ctx.instancia.juego + return bool(buscar_archivos(juego) or buscar_zips(juego)) def describir(self) -> str: - return contar(len(self._archivos()), "archivo de datos", "archivos de datos") + juego = self.ctx.instancia.juego + n = len(buscar_archivos(juego)) + sum(len(e) for _, e in buscar_zips(juego)) + return contar(n, "archivo de datos", "archivos de datos") + + def _leer(self, ruta: Path, formato: Formato, datos: bytes, entrada: str | None) -> list[Pieza]: + try: + texto = datos.decode("utf-8-sig") + unidades = [t for t in formato.unidades(texto) if es_traducible(t)] + except (ValueError, UnicodeDecodeError): + self.ctx.registro(f"{entrada or ruta.name}: no pude leerlo con seguridad, se deja igual") + return [] + if not unidades: + return [] + self._leidos.append(_Archivo(ruta, formato, texto, unidades, entrada)) + return [Pieza(t, formato.pista) for t in unidades] def recolectar(self) -> list[Pieza]: - self._archivos_leidos = [] + self._leidos, self._zips = [], {} + juego = self.ctx.instancia.juego piezas: list[Pieza] = [] - for ruta in self._archivos(): - original = self.ctx.respaldo.original(ruta) - if original is None: + for ruta, formato in buscar_archivos(juego): + if (original := self.ctx.respaldo.original(ruta)) is not None: + piezas += self._leer(ruta, formato, original, None) + for ruta, entradas in buscar_zips(juego): + if (original := self.ctx.respaldo.original(ruta)) is None: continue try: - texto = original.decode("utf-8-sig") - archivo = _Archivo(ruta, texto) - if ruta.suffix == ".json": - archivo.datos = json.loads(texto) - display = _logro(archivo.datos) - archivo.piezas = _unidades_logro(display) if display is not None else [] - pista = "logro del juego" - else: - archivo.piezas = [t for _, cmd, _ in _lineas(texto) if cmd for t in unidades_comando(cmd)] - pista = "mensaje de una función" - except (ValueError, UnicodeDecodeError): - self.ctx.registro(f"{ruta.name}: no pude leerlo con seguridad, se deja igual") + with zipfile.ZipFile(io.BytesIO(original)) as z: + contenidos = [(n, f, z.read(n)) for n, f in entradas] + except (zipfile.BadZipFile, OSError, KeyError): + self.ctx.registro(f"{ruta.name}: no pude abrirlo, se deja igual") continue - if archivo.piezas: - self._archivos_leidos.append(archivo) - piezas += [Pieza(t, pista) for t in archivo.piezas if es_traducible(t) and not parece_clave(t)] + self._zips[ruta] = original + for nombre, formato, datos in contenidos: + piezas += self._leer(ruta, formato, datos, nombre) return piezas - def _reescribir(self, a: _Archivo, traducciones: Mapping[str, str]) -> str: - if a.ruta.suffix == ".mcfunction": - return "\n".join( - (aplicar_comando(cmd, traducciones) if cmd else linea) + fin for linea, cmd, fin in _lineas(a.texto) - ) - datos: dict[str, Any] = a.datos - display = _logro(datos) - assert display is not None - for campo in CAMPOS_LOGRO: - if campo in display: - display[campo] = json.loads(aplicar(_componente(display[campo]), traducciones, True)) - return json.dumps(datos, indent=2, ensure_ascii=False) + "\n" - def escribir(self, traducciones: Mapping[str, str]) -> Resumen: resumen = Resumen() - for a in self._archivos_leidos: + en_zip: dict[Path, dict[str, str]] = {} + for a in self._leidos: hechas = sum(1 for t in set(a.piezas) if t in traducciones) resumen.traducidos += hechas resumen.pendientes += len(set(a.piezas)) - hechas - nuevo = self._reescribir(a, traducciones) - actual = a.ruta.read_text(encoding="utf-8-sig") if a.ruta.is_file() else None - if hechas and nuevo not in (a.texto, actual): + nuevo = a.formato.reescribir(a.texto, traducciones) + if not hechas or nuevo == a.texto: + continue + if a.entrada is not None: + en_zip.setdefault(a.ruta, {})[a.entrada] = nuevo + elif not a.ruta.is_file() or a.ruta.read_bytes() != nuevo.encode("utf-8"): # en bytes: sin traducir \r\n self.ctx.respaldo.escribir(a.ruta, nuevo.encode("utf-8")) resumen.archivos.append(a.ruta) + for ruta, nuevos in en_zip.items(): + datos = _reescribir_zip(self._zips[ruta], nuevos) + if ruta.read_bytes() != datos: + self.ctx.respaldo.escribir(ruta, datos) + resumen.archivos.append(ruta) return resumen def restaurar(self) -> list[Path]: - return self.ctx.respaldo.restaurar(RAIZ + "/") + juego = self.ctx.instancia.juego + propios = [r for r in self.ctx.respaldo.registrados() if clasificar(Path(r)) is not None or r.endswith(".zip")] + hechos: list[Path] = [] + for ruta in propios: # Respaldo restaura por prefijo: la ruta completa es el prefijo más estricto + hechos += self.ctx.respaldo.restaurar(ruta) if (juego / ruta).exists() else [] + return hechos diff --git a/src/traductor_es/modulos/scripts.py b/src/traductor_es/modulos/scripts.py index 19ddccc..2e3a14a 100644 --- a/src/traductor_es/modulos/scripts.py +++ b/src/traductor_es/modulos/scripts.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field from pathlib import Path -from ..formatos import comandos, flujo_js, javascript, json_texto +from ..formatos import comandos, componente, flujo_js, javascript from ..nucleo.mascara import es_traducible from .base import Contexto, Pieza, Resumen, contar, parece_clave @@ -23,6 +23,9 @@ "tell", "statusMessage", "setStatusMessage", "displayClientMessage", "sendSystemMessage", "sendMessage", "displayName", "setDisplayName", "tooltip", "addTooltip", "title", "setTitle", "subtitle", "setSubtitle", "lore", "setLore", "actionbar", "setActionbar", + # Del código de KubeJS (ramas 2001 y 2101, core/PlayerKJS, ServerPlayerKJS y MessageSenderKJS): + # `notify(título, texto)`, `kick(motivo)`, y el título de `openChestGUI` / `openInventoryGUI` + "notify", "kick", "openChestGUI", "openInventoryGUI", }) # fmt: skip #: Llamadas que ejecutan un comando: si es `tellraw`, `title`, `say`…, el texto del mensaje se traduce. LLAMADAS_COMANDO = frozenset({"runCommand", "runCommandSilent"}) @@ -70,6 +73,21 @@ def es_texto_visible(c: javascript.CadenaJs) -> bool: return c.funcion in LLAMADAS_EN_EVENTO and any(ctx.startswith(EVENTOS_TEXTO) for ctx in c.contexto) +_LOGICA = ("recipes", "tags", "loot", "worldgen", "registry", "highPriorityData") + + +def parece_frase_sin_elegir(c: javascript.CadenaJs) -> bool: + """Una cadena con aspecto de frase (3 palabras o más, empieza en mayúscula) fuera de recetas y etiquetas.""" + t = c.texto.strip() + return ( + not c.comparada + and t[:1].isupper() + and len(t.split()) >= 3 + and _parece_texto(t) + and not any(m in ctx for ctx in c.contexto for m in _LOGICA) + ) + + def _plantilla_segura(c: javascript.CadenaJs) -> bool: return c.comilla != "`" or c.texto.count("${") == len(_EXPRESION.findall(c.texto)) @@ -106,14 +124,14 @@ def unidades_comando(cmd: comandos.Comando) -> list[str]: """Los textos que ve el jugador dentro de un comando de chat.""" if not cmd.json: return [cmd.cuerpo] if _parece_texto(cmd.cuerpo) else [] - partes = json_texto.extraer(cmd.cuerpo, literal=True) # None: no es JSON (p. ej. SNBT), no se toca - return [p for p in partes or [] if es_traducible(p) and not parece_clave(p)] + partes = componente.unidades(cmd.cuerpo) # None: formato que no se reconoce, no se toca + return [p for p in partes or [] if not parece_clave(p)] def aplicar_comando(cmd: comandos.Comando, traducciones: Mapping[str, str]) -> str: if not cmd.json: return cmd.prefijo + traducciones.get(cmd.cuerpo, cmd.cuerpo) - return cmd.prefijo + json_texto.reconstruir(cmd.cuerpo, traducciones, literal=True) + return cmd.prefijo + componente.reconstruir(cmd.cuerpo, traducciones) def comando_visible(c: javascript.CadenaJs) -> comandos.Comando | None: @@ -158,6 +176,7 @@ class ScriptsKubeJS: def __init__(self, ctx: Contexto) -> None: self.ctx = ctx self._scripts: list[_Script] = [] + self.sin_elegir: list[tuple[Path, list[str]]] = [] def _archivos(self) -> list[Path]: juego = self.ctx.instancia.juego @@ -182,8 +201,14 @@ def _leer(self) -> list[tuple[Path, str, list[javascript.CadenaJs]]]: self.ctx.registro(f"{ruta.name}: no pude leerlo con seguridad, se deja igual") return leidos + def candidatas(self) -> list[tuple[Path, list[str]]]: + """Por script, las frases que no se eligieron para traducir (para el informe de cobertura).""" + self.recolectar() + return self.sin_elegir + def recolectar(self) -> list[Pieza]: self._scripts = [] + self.sin_elegir = [] leidos = self._leer() flujos = [flujo_js.analizar(src, _LLAMADAS_RE) for _, src, _ in leidos] # Una tabla de nombres se define en un archivo y se usa en otro @@ -201,6 +226,10 @@ def recolectar(self) -> list[Pieza]: elegidas += [c for c in textos_armados(src, cadenas, flujo, tablas) if c.inicio not in vistas] if elegidas: self._scripts.append(_Script(ruta, src, cadenas, elegidas, en_comando)) + vistas = {c.inicio for c in elegidas} + frases = [c.texto for c in cadenas if c.inicio not in vistas and parece_frase_sin_elegir(c)] + if frases: + self.sin_elegir.append((ruta, frases)) return [ Pieza(t, f"script: {c.funcion or 'mensaje'}") for s in self._scripts diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index f8704f0..e7b89fc 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -13,6 +13,7 @@ from .fuentes.instancia import Instancia from .modulos import datos, libros, misiones, mods, scripts from .modulos.base import Contexto, Modulo, Resumen +from .modulos.cobertura import Hallazgo, hallazgos_de_scripts, revisar_cobertura from .modulos.mods import es_nombre from .motores import fabrica from .motores.base import Motor @@ -56,6 +57,8 @@ class Informe: revision_automatica: revision.ResumenRevision | None = None pack_activado: list[str] = field(default_factory=list[str]) fallidos: list[str] = field(default_factory=list[str]) #: grupos que el análisis no pudo leer + #: Lo que queda en inglés aunque se traduzca todo, y por qué (ver `modulos/cobertura.py`) + cobertura: list[Hallazgo] = field(default_factory=list[Hallazgo]) @property def a_traducir(self) -> int: @@ -159,14 +162,19 @@ def modulos(self) -> list[tuple[str, Modulo]]: if "misiones" in self.grupos: lista += [("misiones", m) for m in misiones.todos(self.ctx)] if "scripts" in self.grupos: - lista += [("scripts", scripts.ScriptsKubeJS(self.ctx)), ("scripts", datos.DatosKubeJS(self.ctx))] + lista += [("scripts", scripts.ScriptsKubeJS(self.ctx)), ("scripts", datos.Datos(self.ctx))] return [(g, m) for g, m in lista if m.detectar()] # ------------------------------------------------------------------ acciones + def que_queda(self, java: bool = False, progreso: Callable[[int, int], None] | None = None) -> list[Hallazgo]: + """Lo que quedaría en inglés. Solo lee. `java`: también el texto escrito en el código de los mods (tarda).""" + return revisar_cobertura(self.ctx, java, progreso or (lambda _a, _b: None)) + def analizar(self) -> Informe: """Cuenta lo que hay que traducir. Solo lee: no escribe nada en la instancia.""" self._preparar_glosario() informe = Informe() + scripts_leidos: list[scripts.ScriptsKubeJS] = [] for grupo, modulo in self.modulos(): try: piezas = modulo.recolectar() @@ -175,13 +183,30 @@ def analizar(self) -> Informe: if grupo not in informe.fallidos: informe.fallidos.append(grupo) continue + if isinstance(modulo, scripts.ScriptsKubeJS): + scripts_leidos.append(modulo) pendientes = self.traductor.pendientes([p.texto for p in piezas], modulo.ambito) informe.modulos.append( InformeModulo(grupo, modulo.nombre, modulo.describir(), len({p.texto for p in piezas}), len(pendientes), sum(len(t) for t in pendientes)) ) # fmt: skip + informe.avisos.extend(self._avisos_de_cobertura(informe, scripts_leidos)) return informe + def _avisos_de_cobertura(self, informe: Informe, scripts_leidos: list[scripts.ScriptsKubeJS]) -> list[str]: + """Lo barato de saber antes de traducir: frases de scripts que parecen texto y no se tocan. + + Se apoya en lo que el análisis ya leyó y nunca lo detiene. + """ + for s in scripts_leidos: + informe.cobertura += hallazgos_de_scripts(self.instancia.juego, s.sin_elegir) + if not informe.cobertura: + return [] + return [ + f"{sum(h.cantidad for h in informe.cobertura)} frases de {len(informe.cobertura)} scripts parecen texto " + "pero no se traducen (puede ser lógica). Detalle con «modpackes cobertura»." + ] + def ejecutar(self, activar: bool = True, poner_idioma: bool = True) -> Informe: self._preparar_glosario() informe = Informe() diff --git a/src/traductor_es/salida/exportar.py b/src/traductor_es/salida/exportar.py index 6f99aad..2879929 100644 --- a/src/traductor_es/salida/exportar.py +++ b/src/traductor_es/salida/exportar.py @@ -6,12 +6,13 @@ import zipfile from pathlib import Path +from ..modulos.datos import clasificar from .copias import Respaldo from .resource_pack import NOMBRE -#: Lo que lee el servidor: misiones, scripts de servidor y datos (logros, funciones). +#: Lo que lee el servidor: misiones y scripts de servidor, más los datos (logros y funciones) donde estén. #: Los client_scripts los ejecuta cada jugador. -_DEL_SERVIDOR = ("config/", "kubejs/server_scripts/", "kubejs/startup_scripts/", "kubejs/data/", "patchouli_books/") +_DEL_SERVIDOR = ("config/", "kubejs/server_scripts/", "kubejs/startup_scripts/", "patchouli_books/") LEEME_KIT = """KIT DE TRADUCCIÓN PARA SERVIDOR ================================ @@ -35,6 +36,12 @@ def _agregar_carpeta(zf: zipfile.ZipFile, carpeta: Path, prefijo: str = "") -> i return n +def _es_dato_del_servidor(relativa: str) -> bool: + """Funciones y logros de un datapack los lee el servidor; los tips son de cliente.""" + formato = clasificar(Path(relativa)) + return formato is not None and formato.nombre != "tip" + + def exportar_pack(juego: Path, destino: Path) -> int: """El resource pack de la traducción como .zip listo para compartir. Devuelve cuántos archivos lleva.""" pack = juego / "resourcepacks" / NOMBRE @@ -48,7 +55,7 @@ def exportar_pack(juego: Path, destino: Path) -> int: def exportar_kit_servidor(juego: Path, destino: Path) -> int: """Kit para servidores: archivos traducidos que lee el servidor, el resource pack y unas instrucciones.""" respaldo = Respaldo(juego) - archivos = [r for r in respaldo.registrados() if r.startswith(_DEL_SERVIDOR)] + archivos = [r for r in respaldo.registrados() if r.startswith(_DEL_SERVIDOR) or _es_dato_del_servidor(r)] pack = juego / "resourcepacks" / NOMBRE if not archivos and not (pack / "pack.mcmeta").is_file(): raise ValueError("Todavía no hay nada traducido para el servidor: traduce primero.") diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index f666bb9..ac048c9 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -49,6 +49,7 @@ from ..actualizaciones import Novedad from ..fuentes.instancia import resolver_carpeta_juego from ..fuentes.lanzadores import InstanciaEncontrada, buscar +from ..modulos.cobertura import Hallazgo from ..motores import fabrica from ..motores.base import Cancelado, ErrorFatal, Motor from ..motores.compatible import Compatible @@ -165,6 +166,8 @@ def ejecutar(self) -> None: resultado: object = p.analizar() elif self.accion == "traducir": resultado = p.ejecutar(activar=self.opciones["activar"], poner_idioma=self.opciones["idioma"]) + elif self.accion == "cobertura": + resultado = p.que_queda(java=True, progreso=self.progreso.emit) else: resultado = p.restaurar() self.terminado.emit(resultado) @@ -498,6 +501,8 @@ def agregar(texto: str, nombre_icono: str, funcion: Callable[[], object]) -> QAc self.a_correcciones = agregar("Correcciones…", "pencil", self.abrir_correcciones) self.a_correcciones.setToolTip("Buscar y corregir a mano traducciones de la memoria") + self.a_cobertura = agregar("Qué queda en inglés", "circle-alert", lambda: self.lanzar("cobertura")) + self.a_cobertura.setToolTip("Lista el texto que no se puede traducir y por qué (solo lee, no cambia nada)") self.a_restaurar = agregar("Restaurar el original…", "undo-2", self._confirmar_restaurar) self.a_exportar = agregar("Exportar pack…", "package", lambda: self._exportar(kit=False)) self.a_kit = agregar("Kit de servidor…", "server", lambda: self._exportar(kit=True)) @@ -889,7 +894,11 @@ def lanzar(self, accion: str) -> None: self.barra.setMaximum(0) # sin total todavía: barra en movimiento self._inicio = None self._fase = "" - self.estado.setText({"analizar": "Analizando…", "traducir": "Preparando…"}.get(accion, "Restaurando…")) + self.estado.setText( + {"analizar": "Analizando…", "traducir": "Preparando…", "cobertura": "Buscando texto en inglés…"}.get( + accion, "Restaurando…" + ) + ) self._accion = accion self._antes = self._estado self._estado = "trabajando" @@ -923,7 +932,10 @@ def _progreso(self, hechos: int, total: int) -> None: self._inicio = (ahora, total) # cada módulo cuenta desde cero self.barra.setMaximum(max(total, 1)) self.barra.setValue(hechos) - texto = f"Traduciendo{f' {self._fase}' if self._fase else ''}… {_miles(hechos)} de {_miles(total)}" + if self._accion == "cobertura": + texto = f"Leyendo los mods… {_miles(hechos)} de {_miles(total)}" + else: + texto = f"Traduciendo{f' {self._fase}' if self._fase else ''}… {_miles(hechos)} de {_miles(total)}" pasado = ahora - self._inicio[0] if 0 < hechos < total and pasado > 3: texto += f" · quedan {_restante(pasado / hechos * (total - hechos))}" @@ -956,6 +968,9 @@ def _terminar(self, resultado: object) -> None: self.estado.setText("¡Listo! Abre el juego: ya está en español.") self.aviso.mostrar("Traducción terminada. Abre Minecraft para verla.", "exito") self._marcar_traducida() + elif accion == "cobertura": + self._estado = self._antes # no cambia nada en la instancia: el botón principal sigue como estaba + self._mostrar_cobertura(cast("list[Hallazgo]", resultado)) else: self._olvidar_analisis() # el texto volvió al original: lo contado antes ya no vale n = len(resultado) if isinstance(resultado, list) else 0 # pyright: ignore[reportUnknownArgumentType] @@ -964,6 +979,20 @@ def _terminar(self, resultado: object) -> None: self._al_cambiar_opciones() self.escribir(self.estado.text()) + def _mostrar_cobertura(self, hallazgos: list[Hallazgo]) -> None: + if not hallazgos: + self.estado.setText("No encontré texto que se quede en inglés.") + return + self.estado.setText(f"Quedan {len(hallazgos)} lugares con texto en inglés: el detalle está en Detalles.") + self.escribir("Texto que se queda en inglés aunque traduzcas todo:") + for h in hallazgos: + self.escribir(f"· [{h.categoria}] {h}") + self.escribir( + "Lo de «java» está escrito en el código del mod: un resource pack no lo alcanza. " + "Lo de «script» puede ser lógica y no un mensaje." + ) + self.detalles.fijar_abierta(True) + def _marcar_traducida(self) -> None: carpeta = self.carpeta() tarjeta = self.lista.actual() diff --git a/tests/test_cobertura.py b/tests/test_cobertura.py new file mode 100644 index 0000000..dea9955 --- /dev/null +++ b/tests/test_cobertura.py @@ -0,0 +1,73 @@ +"""Informe de lo que queda en inglés: frases de scripts sin elegir y texto escrito en el código de los mods.""" + +import struct +from pathlib import Path + +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto +from traductor_es.modulos.cobertura import constantes_utf8, revisar_cobertura +from traductor_es.motores.simulador import Simulador +from traductor_es.proceso import Proceso +from traductor_es.salida.copias import Respaldo + +from . import fabrica + +SCRIPT = """PlayerEvents.loggedIn(event => { + event.player.tell('Welcome to the village') + let aviso = 'The market is closed today' + console.log('This should be a debug message here') + if (event.player.name == 'Some Long Name Here') return +}) +ServerEvents.recipes(event => { event.shapeless('minecraft:stone', ['Not A Real Recipe Name']) }) +""" + + +def _clase(*constantes: str) -> bytes: + """Un .class mínimo: solo la cabecera y la tabla de constantes con esas cadenas (más un entero entre medias).""" + cuerpo = b"".join(b"\x01" + struct.pack(">H", len(c.encode())) + c.encode() for c in constantes[:1]) + cuerpo += b"\x03\x00\x00\x00\x07" # un Integer: el lector tiene que saltarlo bien + cuerpo += b"".join(b"\x01" + struct.pack(">H", len(c.encode())) + c.encode() for c in constantes[1:]) + cantidad = len(constantes) + 2 # las casillas empiezan en 1 + return b"\xca\xfe\xba\xbe" + struct.pack(">HHH", 0, 52, cantidad) + cuerpo + + +def test_lee_la_tabla_de_constantes_de_un_class() -> None: + assert constantes_utf8(_clase("literal", "You can't do that here!", "ñandú")) == [ + "literal", "You can't do that here!", "ñandú", + ] # fmt: skip + assert constantes_utf8(b"no es un class") == [] + assert constantes_utf8(_clase("literal")[:-3]) == [] # truncado: no se inventa nada + + +def test_cobertura_de_scripts_y_java(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + (juego / "kubejs" / "server_scripts").mkdir(parents=True) + (juego / "kubejs" / "server_scripts" / "a.js").write_text(SCRIPT, encoding="utf-8") + fabrica.jar( + juego / "mods" / "m.jar", + { + "com/m/Chat.class": _clase("literal", "You can't use this item here!", "Failed to load the config file"), + "com/m/Fabric.class": _clase("method_43470", "Click the block to open the menu"), + "com/m/Otra.class": _clase("nada", "Another English sentence here"), # no construye texto + "com/m/Spanish.class": _clase("literal", "Alacena de abedul sin corteza"), # ya está en español + }, + ) + with Instancia(juego) as inst: + sin_java = revisar_cobertura(Contexto(inst, Respaldo(juego))) + con_java = revisar_cobertura(Contexto(inst, Respaldo(juego)), java=True) + assert [(h.categoria, h.donde) for h in sin_java] == [("script", "kubejs/server_scripts/a.js")] + assert sin_java[0].cantidad == 2 # `tell` ya se traduce; recetas y comparaciones no cuentan + assert "This should be a debug message here" in sin_java[0].ejemplos + de_java = [h for h in con_java if h.categoria == "java"] + assert len(de_java) == 1 + assert set(de_java[0].ejemplos) == {"You can't use this item here!", "Click the block to open the menu"} + + +def test_analizar_avisa_sin_detener_el_analisis(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + (juego / "kubejs" / "server_scripts").mkdir(parents=True) + (juego / "kubejs" / "server_scripts" / "a.js").write_text(SCRIPT, encoding="utf-8") + with Proceso(juego, Simulador(), grupos=["scripts"]) as p: + informe = p.analizar() + assert any("2 frases de 1 scripts" in a for a in informe.avisos) + assert informe.modulos and informe.modulos[0].grupo == "scripts" diff --git a/tests/test_comandos_y_datos.py b/tests/test_comandos_y_datos.py index d962abb..0884396 100644 --- a/tests/test_comandos_y_datos.py +++ b/tests/test_comandos_y_datos.py @@ -9,7 +9,7 @@ from traductor_es.formatos import comandos from traductor_es.fuentes.instancia import Instancia from traductor_es.modulos.base import Contexto, traducir_modulo -from traductor_es.modulos.datos import DatosKubeJS +from traductor_es.modulos.datos import Datos from traductor_es.modulos.scripts import ScriptsKubeJS from traductor_es.motores.simulador import Simulador from traductor_es.nucleo.glosario import Glosario @@ -65,14 +65,14 @@ def test_runcommand_traduce_solo_el_mensaje(tmp_path: Path, memoria: Memoria) -> with Instancia(juego) as inst: ctx = Contexto(inst, Respaldo(juego)) resumen = traducir_modulo(ScriptsKubeJS(ctx), Traductor(Simulador(), memoria, Glosario())) - assert resumen.traducidos == 4 and resumen.pendientes == 0 + assert resumen.traducidos == 5 and resumen.pendientes == 0 nuevo = ruta.read_text(encoding="utf-8") assert """tellraw @a {"text": "«ES» Welcome to the village!", "color": "gold"}""" in nuevo assert r"""title @a actionbar {\"text\": \"«ES» Market opens soon\"}""" in nuevo assert "say «ES» The festival begins" in nuevo assert "${event.player.username}" in nuevo and "execute as @a run tellraw @s " in nuevo assert "times 10 70 20" in nuevo and "give @p minecraft:stone 2" in nuevo - assert 'tellraw @a {text:"snbt not touched"}' in nuevo + assert 'tellraw @a {text:"«ES» snbt not touched"}' in nuevo # SNBT (1.21.5+): se conserva el formato ScriptsKubeJS(ctx).restaurar() assert ruta.read_text(encoding="utf-8") == SCRIPT @@ -106,7 +106,7 @@ def test_logros_y_funciones(tmp_path: Path, memoria: Memoria) -> None: (funciones / "open.mcfunction").write_bytes(FUNCION.encode()) with Instancia(juego) as inst: ctx = Contexto(inst, Respaldo(juego)) - modulo = DatosKubeJS(ctx) + modulo = Datos(ctx) assert modulo.detectar() and modulo.describir() == "2 archivos de datos" resumen = traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) assert resumen.traducidos == 4 and resumen.pendientes == 0 and len(resumen.archivos) == 2 diff --git a/tests/test_corpus_datos.py b/tests/test_corpus_datos.py new file mode 100644 index 0000000..0ef7aba --- /dev/null +++ b/tests/test_corpus_datos.py @@ -0,0 +1,135 @@ +"""Datos de texto reales (modpacks públicos de .descargas y tu FC4): siempre sobre copias en una carpeta temporal. + +Para cada modpack: lo traducido sigue siendo válido (JSON, zips, JS), repetir no cambia nada y restaurar devuelve +los bytes originales. Los modpacks se bajan con `uv run python scripts/descargar_corpus.py`. +""" + +from __future__ import annotations + +import io +import json +import os +import shutil +import subprocess +import zipfile +from collections.abc import Iterator +from pathlib import Path +from typing import Any, cast + +import pytest + +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.cobertura import revisar_cobertura +from traductor_es.modulos.datos import Datos, buscar_archivos, buscar_zips +from traductor_es.modulos.scripts import ScriptsKubeJS +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.salida.copias import Respaldo + +from . import fabrica + +pytestmark = pytest.mark.corpus + +RAIZ = Path(__file__).resolve().parent.parent +DESCARGAS = RAIZ / ".descargas" +FC4 = ( + Path(os.environ.get("APPDATA", Path.home())) / "PrismLauncher" / "instances" / "Farming Crossing 4(1)" / "minecraft" +) +#: Lo que se copia de cada modpack: donde viven los datos de texto y los scripts. +CARPETAS = ("kubejs", "global_packs", "datapacks", "config/openloader", "config/paxi", "resourcepacks") +LOADERS = {"forge": "forge", "neoforge": "neoforge", "fabric-loader": "fabric", "quilt-loader": "quilt"} + + +def _packs() -> list[str]: + return sorted(p.parent.name for p in DESCARGAS.glob("*/resumen.json")) + + +def _version_y_loader(slug: str) -> tuple[str, str | None]: + resumen = cast("dict[str, Any]", json.loads((DESCARGAS / slug / "resumen.json").read_text(encoding="utf-8"))) + deps = cast("dict[str, str]", resumen["dependencias"]) + return deps["minecraft"], next((v for k, v in LOADERS.items() if k in deps), None) + + +def _copiar(origen: Path, juego: Path) -> None: + for c in CARPETAS: + if (origen / c).is_dir(): + shutil.copytree(origen / c, juego / c, dirs_exist_ok=True) + + +def _todos(juego: Path) -> dict[str, bytes]: + return {p.relative_to(juego).as_posix(): p.read_bytes() for p in juego.rglob("*") if p.is_file()} + + +def _node() -> str | None: + return shutil.which("node") + + +def _js_valido(ruta: Path) -> bool: + node = _node() + assert node + return subprocess.run([node, "--check", str(ruta)], capture_output=True, check=False).returncode == 0 + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +def _verificar(juego: Path, antes: dict[str, bytes]) -> int: + """Lo cambiado sigue siendo válido. Devuelve cuántos archivos cambiaron.""" + cambiados = 0 + for rel, datos in _todos(juego).items(): + if rel.startswith("traductor_es/") or antes.get(rel) == datos: + continue + cambiados += 1 + if rel.endswith(".json"): + json.loads(datos.decode("utf-8-sig")) + elif rel.endswith(".mcfunction"): + assert datos.count(b"\n") == antes[rel].count(b"\n"), rel # ni se pierden ni se parten líneas + elif rel.endswith(".zip"): + with zipfile.ZipFile(io.BytesIO(datos)) as z, zipfile.ZipFile(io.BytesIO(antes[rel])) as o: + assert z.testzip() is None and z.namelist() == o.namelist(), rel + elif rel.endswith(".js") and _node() and _js_valido(juego / rel) is False: + ruta_antes = juego / rel + ruta_antes.write_bytes(antes[rel]) # ¿ya era inválido antes? + assert not _js_valido(ruta_antes), f"{rel} era JavaScript válido y la traducción lo rompió" + ruta_antes.write_bytes(datos) + return cambiados + + +def _probar(origen: Path, version: str, loader: str | None, tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, version, loader) + _copiar(origen, juego) + antes = _todos(juego) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + if not (buscar_archivos(juego) or buscar_zips(juego) or ScriptsKubeJS(ctx).detectar()): + pytest.skip("Este modpack no trae datos de texto ni scripts") + traductor = Traductor(Simulador(), memoria, Glosario()) + for modulo in (Datos(ctx), ScriptsKubeJS(ctx)): + traducir_modulo(modulo, traductor) + assert _verificar(juego, antes) >= 0 + despues = _todos(juego) + for modulo in (Datos(ctx), ScriptsKubeJS(ctx)): + traducir_modulo(modulo, traductor) + assert _todos(juego) == despues # repetir no cambia nada + revisar_cobertura(ctx) # no debe fallar con ningún modpack + Datos(ctx).restaurar() + ScriptsKubeJS(ctx).restaurar() + restaurado = {k: v for k, v in _todos(juego).items() if not k.startswith("traductor_es/")} + assert restaurado == {k: v for k, v in antes.items() if not k.startswith("traductor_es/")} + + +@pytest.mark.parametrize("slug", _packs()) +def test_datos_de_modpacks_descargados(slug: str, tmp_path: Path, memoria: Memoria) -> None: + version, loader = _version_y_loader(slug) + _probar(DESCARGAS / slug, version, loader, tmp_path, memoria) + + +@pytest.mark.skipif(not (FC4 / "kubejs").is_dir(), reason="Falta FC4") +def test_datos_de_fc4(tmp_path: Path, memoria: Memoria) -> None: + _probar(FC4, "1.20.1", "forge", tmp_path, memoria) diff --git a/tests/test_datos_generales.py b/tests/test_datos_generales.py new file mode 100644 index 0000000..bcd7f74 --- /dev/null +++ b/tests/test_datos_generales.py @@ -0,0 +1,184 @@ +"""Datos de texto en cualquier sitio y versión: se descubren por su estructura, no por la carpeta de un modpack.""" + +import json +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.formatos import componente +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.datos import Datos, buscar_archivos, clasificar +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.salida.copias import Respaldo + +from . import fabrica + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +@pytest.mark.parametrize( + ("cuerpo", "esperado"), + [ + ('{"text":"Hi","color":"red"}', ["Hi"]), + ('["",{"text":"Hi "},{"text":"there"}]', ["Hi ", "there"]), + ('"Hi"', ["Hi"]), + ('{text:"Hi",color:"red"}', ["Hi"]), # SNBT, desde 1.21.5 + ('["",{text:"Aa"},{text:"Bb",extra:["Cc"]}]', ["Aa", "Bb", "Cc"]), + ('{translate:"chat.type.text",with:["Steve"],color:"red"}', []), + ('{"score":{"name":"@p","objective":"x"}}', []), + ("esto no es un componente", None), + ], +) +def test_componente_json_y_snbt(cuerpo: str, esperado: list[str] | None) -> None: + assert componente.unidades(cuerpo) == esperado + + +def test_componente_snbt_conserva_el_formato() -> None: + cuerpo = '{text:"Hi", color:"red", hover_event:{action:"show_text", value:"Hola"}}' + nuevo = componente.reconstruir(cuerpo, {"Hi": "Hola, amigo", "Hola": "no debe usarse"}) + assert nuevo == '{text:"Hola, amigo", color:"red", hover_event:{action:"show_text", value:"Hola"}}' + + +@pytest.mark.parametrize( + ("ruta", "formato"), + [ + ("kubejs/data/a/functions/x.mcfunction", "función"), + ("global_packs/required_data/p/data/a/function/sub/x.mcfunction", "función"), # 1.21: «function» + ("datapacks/p/data/a/advancement/x.json", "logro"), + ("saves/w/datapacks/p/data/a/advancements/b/x.json", "logro"), + ("kubejs/assets/a/tips/t.json", "tip"), + ("resourcepacks/p/assets/a/tips/t.json", "tip"), + ("kubejs/data/tipsmod/tips/t.json", "tip"), + ("kubejs/data/a/recipes/x.json", None), + ("kubejs/data/a/functions/x.txt", None), + ("config/functions/x.mcfunction", None), # sin «data»: no es un datapack + ("assets/a/lang/en_us.json", None), + ], +) +def test_clasificar_por_estructura(ruta: str, formato: str | None) -> None: + resultado = clasificar(Path(ruta)) + assert (resultado.nombre if resultado else None) == formato + + +def _escribir(base: Path, ruta: str, texto: str) -> Path: + p = base / ruta + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(texto, encoding="utf-8", newline="") + return p + + +def test_descubre_en_cualquier_carpeta_e_ignora_lo_que_no_toca(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fn = 'tellraw @a {"text":"Hi"}\n' + buenos = [ + "kubejs/data/a/functions/x.mcfunction", + "global_packs/required_data/p/data/a/functions/x.mcfunction", + "datapacks/p/data/a/functions/x.mcfunction", + "saves/Mundo/datapacks/p/data/a/functions/x.mcfunction", + "config/openloader/data/p/data/a/functions/x.mcfunction", + ] + malos = [ + "mods/m/data/a/functions/x.mcfunction", # los jars no se tocan + "saves/Mundo/region/data/a/functions/x.mcfunction", # de un mundo solo importan sus datapacks + "resourcepacks/Traduccion_ES/data/a/functions/x.mcfunction", # lo que escribe la app + "traductor_es/respaldo/kubejs/data/a/functions/x.mcfunction", # las copias de seguridad + "logs/data/a/functions/x.mcfunction", + ] + for r in (*buenos, *malos): + _escribir(juego, r, fn) + encontrados = {p.relative_to(juego).as_posix() for p, _ in buscar_archivos(juego)} + assert encontrados == set(buenos) + + +def test_traduce_logros_snbt_funciones_y_tips(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.21.5") + fn = _escribir( + juego, "datapacks/p/data/a/function/x.mcfunction", 'tellraw @a {text:"Hello",color:"gold"}\nsay Bye\n' + ) + logro = _escribir( + juego, + "kubejs/data/a/advancement/x.json", + json.dumps({"display": {"title": "Big Win", "description": {"translate": "k.d"}}, "criteria": {}}), + ) + tip1 = _escribir(juego, "kubejs/assets/a/tips/t1.json", json.dumps({"tip": {"text": "Eat your greens."}})) + tip2 = _escribir( + juego, "kubejs/data/tipsmod/tips/t2.json", json.dumps({"tips": [{"text": "Sleep in beds.", "category": "x"}]}) + ) + negativo = _escribir(juego, "kubejs/data/a/recipes/r.json", json.dumps({"result": {"item": "Big Win"}})) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + modulo = Datos(ctx) + assert modulo.describir() == "4 archivos de datos" + resumen = traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) + assert resumen.pendientes == 0 and len(resumen.archivos) == 4 + assert fn.read_text("utf-8") == 'tellraw @a {text:"«ES» Hello",color:"gold"}\nsay «ES» Bye\n' + assert json.loads(logro.read_text("utf-8"))["display"] == { + "title": "«ES» Big Win", + "description": {"translate": "k.d"}, + } + assert json.loads(tip1.read_text("utf-8")) == {"tip": {"text": "«ES» Eat your greens."}} + assert json.loads(tip2.read_text("utf-8"))["tips"] == [{"text": "«ES» Sleep in beds.", "category": "x"}] + assert "«ES»" not in negativo.read_text("utf-8") + # Repetir no cambia nada, y restaurar devuelve los originales + antes = {p: p.read_bytes() for p in (fn, logro, tip1, tip2)} + traducir_modulo(Datos(ctx), Traductor(Simulador(), memoria, Glosario())) + assert {p: p.read_bytes() for p in antes} == antes + assert len(Datos(ctx).restaurar()) == 4 + assert fn.read_text("utf-8") == 'tellraw @a {text:"Hello",color:"gold"}\nsay Bye\n' + assert json.loads(tip1.read_text("utf-8")) == {"tip": {"text": "Eat your greens."}} + + +def test_archivo_ilegible_se_deja_igual(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + roto = _escribir(juego, "kubejs/data/a/advancements/roto.json", '{"display": {"title": "Hi",}') + avisos: list[str] = [] + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), registro=avisos.append) + traducir_modulo(Datos(ctx), Traductor(Simulador(), memoria, Glosario())) + assert roto.read_text("utf-8") == '{"display": {"title": "Hi",}' and any("roto.json" in a for a in avisos) + + +def test_datapack_comprimido_se_traduce_y_se_restaura(tmp_path: Path, memoria: Memoria) -> None: + import io + import zipfile + + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "global_packs" / "required_data" / "mi pack v1.zip" + ruta.parent.mkdir(parents=True) + with zipfile.ZipFile(ruta, "w", zipfile.ZIP_DEFLATED) as z: + z.writestr("pack.mcmeta", '{"pack": {"pack_format": 15, "description": "Hello pack"}}') + z.writestr("data/a/functions/x.mcfunction", 'tellraw @a {"text":"Hello there"}\n') + z.writestr("data/a/recipes/r.json", '{"result": "Hello there"}') + z.writestr("data/a/advancements/g.json", json.dumps({"display": {"title": "Big Win"}})) + original = ruta.read_bytes() + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + modulo = Datos(ctx) + assert modulo.describir() == "2 archivos de datos" + resumen = traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) + assert resumen.traducidos == 2 and resumen.archivos == [ruta] + with zipfile.ZipFile(io.BytesIO(ruta.read_bytes())) as z: + assert z.testzip() is None + assert z.namelist() == [ + "pack.mcmeta", + "data/a/functions/x.mcfunction", + "data/a/recipes/r.json", + "data/a/advancements/g.json", + ] + assert "«ES» Hello there" in z.read("data/a/functions/x.mcfunction").decode() + assert json.loads(z.read("data/a/advancements/g.json"))["display"]["title"] == "«ES» Big Win" + assert "«ES»" not in z.read("data/a/recipes/r.json").decode() + z.read("pack.mcmeta").decode() + despues = ruta.read_bytes() + traducir_modulo(Datos(ctx), Traductor(Simulador(), memoria, Glosario())) + assert ruta.read_bytes() == despues # repetir no cambia nada + assert Datos(ctx).restaurar() == [ruta] + assert ruta.read_bytes() == original diff --git a/tests/test_ui.py b/tests/test_ui.py index 2f86880..779445d 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -465,3 +465,19 @@ def romper(_self: object) -> list[object]: _analizar(qtbot, v) assert not v.grupos["scripts"].incluido and "No se pudo leer" in v.grupos["scripts"].e_detalle.text() assert v.b_principal.isEnabled() + + +def test_que_queda_en_ingles_solo_lee(qtbot: QtBot, tmp_path: Path) -> None: + juego = _modpack(tmp_path) + ruta = juego / "kubejs" / "server_scripts" / "s.js" + ruta.write_text( + ruta.read_text("utf-8") + "\nlet nota = 'This sentence looks like a message but is not used'\n", + encoding="utf-8", + ) + antes = ruta.read_bytes() + v = _ventana(qtbot, juego) + v.a_cobertura.trigger() + qtbot.waitUntil(lambda: v._hilo is None and "lugares" in v.estado.text(), timeout=30000) # pyright: ignore[reportPrivateUsage] + assert "This sentence looks like a message" in v.log.toPlainText() + assert ruta.read_bytes() == antes and not (juego / "resourcepacks").exists() # solo lee + assert v.b_principal.isEnabled() # el botón principal sigue disponible From 3f4d271747f7febd1d8780bbbdaf97ca060457a2 Mon Sep 17 00:00:00 2001 From: Cristian Date: Fri, 6 Nov 2026 01:16:55 -0500 Subject: [PATCH 03/24] =?UTF-8?q?fix:=20endurecer=20lectura,=20escritura?= =?UTF-8?q?=20y=20errores=20tras=20la=20verificaci=C3=B3n=20por=20seccione?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Memoria: cierra la conexión si el archivo está dañado y el proceso dice cuál es. - NBT: un gzip dañado o truncado se convierte siempre en ValueError. - Carpeta de recursos: rechaza rutas que salen de su raíz. - Resource pack: un espacio de nombres imposible o reservado de Windows se salta y se avisa, en vez de cancelar toda la traducción (mods y libros). - options.txt: conserva los bytes que no son UTF-8. - Pruebas de casos límite por sección, línea de comandos real y ventana. --- src/traductor_es/formatos/nbt.py | 5 +- src/traductor_es/fuentes/contenedores.py | 5 +- src/traductor_es/modulos/libros.py | 50 ++-- src/traductor_es/modulos/mods.py | 8 +- src/traductor_es/nucleo/memoria.py | 7 + src/traductor_es/proceso.py | 16 +- src/traductor_es/salida/opciones.py | 4 +- src/traductor_es/salida/resource_pack.py | 12 +- tests/test_ui.py | 39 ++++ tests/test_verificacion_formatos.py | 205 +++++++++++++++++ tests/test_verificacion_fuentes.py | 180 +++++++++++++++ tests/test_verificacion_modulos.py | 276 +++++++++++++++++++++++ tests/test_verificacion_motores.py | 191 ++++++++++++++++ tests/test_verificacion_nucleo.py | 191 ++++++++++++++++ tests/test_verificacion_salida.py | 155 +++++++++++++ tests/test_verificacion_validacion.py | 87 +++++++ 16 files changed, 1401 insertions(+), 30 deletions(-) create mode 100644 tests/test_verificacion_formatos.py create mode 100644 tests/test_verificacion_fuentes.py create mode 100644 tests/test_verificacion_modulos.py create mode 100644 tests/test_verificacion_motores.py create mode 100644 tests/test_verificacion_nucleo.py create mode 100644 tests/test_verificacion_salida.py create mode 100644 tests/test_verificacion_validacion.py diff --git a/src/traductor_es/formatos/nbt.py b/src/traductor_es/formatos/nbt.py index 60acad8..8123e4d 100644 --- a/src/traductor_es/formatos/nbt.py +++ b/src/traductor_es/formatos/nbt.py @@ -137,7 +137,10 @@ def valor(self, tipo: int) -> Valor: def descomprimir(datos: bytes) -> tuple[bytes, Compresion]: if datos[:2] == b"\x1f\x8b": - return gzip.decompress(datos), "gzip" + try: + return gzip.decompress(datos), "gzip" + except (EOFError, OSError, zlib.error) as e: # gzip truncado o dañado: un solo tipo de error para quien llama + raise ValueError(f"Archivo NBT comprimido dañado: {e}") from e if datos[:1] == b"\x78": try: return zlib.decompress(datos), "zlib" diff --git a/src/traductor_es/fuentes/contenedores.py b/src/traductor_es/fuentes/contenedores.py index 5cd3232..b1358af 100644 --- a/src/traductor_es/fuentes/contenedores.py +++ b/src/traductor_es/fuentes/contenedores.py @@ -73,7 +73,10 @@ def nombres(self) -> list[str]: def leer(self, ruta: str) -> bytes: if not ruta.startswith(self._prefijo): raise KeyError(ruta) - return (self.raiz / ruta[len(self._prefijo) :]).read_bytes() + destino = (self.raiz / ruta[len(self._prefijo) :]).resolve() + if not destino.is_relative_to(self.raiz.resolve()): # «../» o una ruta absoluta: nunca se sale de la carpeta + raise KeyError(ruta) + return destino.read_bytes() def cerrar(self) -> None: return None diff --git a/src/traductor_es/modulos/libros.py b/src/traductor_es/modulos/libros.py index fa3dc6a..fa4a2c6 100644 --- a/src/traductor_es/modulos/libros.py +++ b/src/traductor_es/modulos/libros.py @@ -255,30 +255,36 @@ def generar(trad: Mapping[str, str]) -> tuple[bytes, int, int]: def escribir(self, traducciones: Mapping[str, str]) -> Resumen: resumen = Resumen() for a in self._archivos: - generado: bytes | None = None - for variante in self.ctx.variantes: - destino = a.destino(variante) - if variante in a.existentes: - # Ya hay traducción del autor: si el pack tiene una copia vieja en esa ruta, que sea la oficial - if not a.externo and self._en_pack(destino) not in (None, a.existentes[variante]): - resumen.archivos.append(self.ctx.pack_listo().escribir(destino, a.existentes[variante])) - continue - copia = next((a.existentes[v] for v in (*_PREFERIDAS, *a.existentes) if v in a.existentes), None) - if copia is not None: - contenido = copia - else: - if a.generar is None: - continue - if generado is None: - generado, hechos, faltan = a.generar(traducciones) - resumen.traducidos += hechos - resumen.pendientes += faltan - if hechos == 0: - break # nada traducido: el juego ya usa el inglés por su cuenta - contenido = generado - resumen.archivos.append(self._escribir(a, destino, contenido)) + try: + self._escribir_libro(a, traducciones, resumen) + except ValueError as e: # una ruta imposible de guardar en el pack: ese libro se deja en inglés + self.ctx.registro(f"No pude guardar un libro: {e}") return resumen + def _escribir_libro(self, a: _Archivo, traducciones: Mapping[str, str], resumen: Resumen) -> None: + generado: bytes | None = None + for variante in self.ctx.variantes: + destino = a.destino(variante) + if variante in a.existentes: + # Ya hay traducción del autor: si el pack tiene una copia vieja en esa ruta, que sea la oficial + if not a.externo and self._en_pack(destino) not in (None, a.existentes[variante]): + resumen.archivos.append(self.ctx.pack_listo().escribir(destino, a.existentes[variante])) + continue + copia = next((a.existentes[v] for v in (*_PREFERIDAS, *a.existentes) if v in a.existentes), None) + if copia is not None: + contenido = copia + else: + if a.generar is None: + continue + if generado is None: + generado, hechos, faltan = a.generar(traducciones) + resumen.traducidos += hechos + resumen.pendientes += faltan + if hechos == 0: + break # nada traducido: el juego ya usa el inglés por su cuenta + contenido = generado + resumen.archivos.append(self._escribir(a, destino, contenido)) + def _en_pack(self, relativa: str) -> bytes | None: pack = self.ctx.pack ruta = (pack.raiz if pack else self.ctx.instancia.juego / "resourcepacks" / "Traduccion_ES") / relativa diff --git a/src/traductor_es/modulos/mods.py b/src/traductor_es/modulos/mods.py index 23da12d..f9a10bb 100644 --- a/src/traductor_es/modulos/mods.py +++ b/src/traductor_es/modulos/mods.py @@ -82,7 +82,13 @@ def escribir(self, traducciones: Mapping[str, str]) -> Resumen: return resumen # nada que escribir y nada viejo que vaciar: no se crea un pack vacío pack = self.ctx.pack_listo() for (namespace, variante), textos in sorted(por_archivo.items()): - resumen.archivos.append(pack.escribir_lang(namespace, variante, textos)) + try: + resumen.archivos.append(pack.escribir_lang(namespace, variante, textos)) + except ValueError: # un mod con un espacio de nombres imposible de guardar no detiene a los demás + self.ctx.registro( + f"El espacio de nombres «{namespace}» no se puede guardar en el pack: se deja en inglés" + ) + resumen.pendientes += len(textos) pack.vaciar_idiomas_viejos() return resumen diff --git a/src/traductor_es/nucleo/memoria.py b/src/traductor_es/nucleo/memoria.py index 5a76860..611470b 100644 --- a/src/traductor_es/nucleo/memoria.py +++ b/src/traductor_es/nucleo/memoria.py @@ -45,6 +45,13 @@ def __init__(self, ruta: Path | str) -> None: self.ruta.parent.mkdir(parents=True, exist_ok=True) self._lock = threading.Lock() self._db = sqlite3.connect(str(ruta), check_same_thread=False) + try: + self._crear_tablas() + except sqlite3.Error: # archivo dañado o bloqueado: no se deja la conexión abierta + self._db.close() + raise + + def _crear_tablas(self) -> None: self._db.execute( """CREATE TABLE IF NOT EXISTS traducciones ( origen TEXT NOT NULL, dialecto TEXT NOT NULL, ambito TEXT NOT NULL, diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index e7b89fc..fd85f4d 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -2,6 +2,7 @@ from __future__ import annotations +import sqlite3 import threading from collections.abc import Callable, Sequence from dataclasses import dataclass, field @@ -16,7 +17,7 @@ from .modulos.cobertura import Hallazgo, hallazgos_de_scripts, revisar_cobertura from .modulos.mods import es_nombre from .motores import fabrica -from .motores.base import Motor +from .motores.base import ErrorFatal, Motor from .motores.compatible import Compatible from .motores.gemini import PRECIOS, Gemini from .nucleo.dialectos import DIALECTO_POR_DEFECTO, VARIANTES, nombre_archivo_lang @@ -119,7 +120,7 @@ def __init__( self.registro = registro self.grupos = [g for g in GRUPOS if g in grupos] self._memoria_propia = memoria is None - self.memoria = memoria or Memoria(configuracion.ruta_memoria()) + self.memoria = memoria or self._abrir_memoria() self.glosario = Glosario() self.ctx = Contexto( self.instancia, Respaldo(self.instancia.juego), dialecto, VARIANTES, registro, PackSalida(self.instancia) @@ -166,6 +167,17 @@ def modulos(self) -> list[tuple[str, Modulo]]: return [(g, m) for g, m in lista if m.detectar()] # ------------------------------------------------------------------ acciones + def _abrir_memoria(self) -> Memoria: + """La memoria de traducciones del usuario. Si el archivo está dañado, dice cuál es y no falla a medias.""" + ruta = configuracion.ruta_memoria() + try: + return Memoria(ruta) + except sqlite3.Error as e: + self.instancia.cerrar() + raise ErrorFatal( + f"La memoria de traducciones está dañada ({e}). Muévela o bórrala y vuelve a intentarlo: {ruta}" + ) from e + def que_queda(self, java: bool = False, progreso: Callable[[int, int], None] | None = None) -> list[Hallazgo]: """Lo que quedaría en inglés. Solo lee. `java`: también el texto escrito en el código de los mods (tarda).""" return revisar_cobertura(self.ctx, java, progreso or (lambda _a, _b: None)) diff --git a/src/traductor_es/salida/opciones.py b/src/traductor_es/salida/opciones.py index bc2425d..f08f931 100644 --- a/src/traductor_es/salida/opciones.py +++ b/src/traductor_es/salida/opciones.py @@ -53,7 +53,7 @@ def activar(juego: Path, nombre_pack: str, idioma: str | None) -> list[str]: return [] crudo = ruta.read_bytes() salto = "\r\n" if b"\r\n" in crudo else "\n" - lineas = crudo.decode("utf-8", errors="replace").splitlines() + lineas = crudo.decode("utf-8", errors="surrogateescape").splitlines() # lo que no es UTF-8 se conserva igual cambios: list[str] = [] vio_packs = vio_idioma = False for i, linea in enumerate(lineas): @@ -78,6 +78,6 @@ def activar(juego: Path, nombre_pack: str, idioma: str | None) -> list[str]: cambios.append(f"idioma del juego: {idioma}") if cambios: temporal = ruta.with_name("options.txt.tmp") - temporal.write_bytes((salto.join(lineas) + salto).encode("utf-8")) + temporal.write_bytes((salto.join(lineas) + salto).encode("utf-8", errors="surrogateescape")) temporal.replace(ruta) return cambios diff --git a/src/traductor_es/salida/resource_pack.py b/src/traductor_es/salida/resource_pack.py index f95e241..0ae8223 100644 --- a/src/traductor_es/salida/resource_pack.py +++ b/src/traductor_es/salida/resource_pack.py @@ -18,6 +18,16 @@ NOMBRE = "Traduccion_ES" #: Un segmento de ruta seguro: sin barras invertidas, dos puntos ni caracteres que Windows no admite. _PARTE_VALIDA = re.compile(r"[^\\/:*?\"<>|\x00-\x1f]+") +#: Nombres de dispositivo de Windows (CON, NUL, COM1…): no se pueden usar como carpeta ni archivo. +_RESERVADOS = frozenset( + {"CON", "PRN", "AUX", "NUL", *(f"COM{n}" for n in range(1, 10)), *(f"LPT{n}" for n in range(1, 10))} +) + + +def _parte_segura(parte: str) -> bool: + if not _PARTE_VALIDA.fullmatch(parte) or parte in (".", "..") or parte != parte.rstrip(". "): + return False # Windows quita los puntos y espacios del final: «a.» y «a» serían la misma carpeta + return parte.split(".", maxsplit=1)[0].upper() not in _RESERVADOS class PackSalida: @@ -30,7 +40,7 @@ def __init__(self, instancia: Instancia, nombre: str = NOMBRE) -> None: def _ruta(self, relativa: str) -> Path: """Ruta dentro del pack. Rechaza todo lo que pueda salir de él (también trucos de Windows).""" partes = relativa.split("/") - if not relativa or any(not _PARTE_VALIDA.fullmatch(p) or p in (".", "..") for p in partes): + if not relativa or any(not _parte_segura(p) for p in partes): raise ValueError(f"Ruta fuera del pack: {relativa!r}") ruta = self.raiz.joinpath(*partes) if not ruta.resolve().is_relative_to(self.raiz.resolve()): diff --git a/tests/test_ui.py b/tests/test_ui.py index 779445d..f08bbdb 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -481,3 +481,42 @@ def test_que_queda_en_ingles_solo_lee(qtbot: QtBot, tmp_path: Path) -> None: assert "This sentence looks like a message" in v.log.toPlainText() assert ruta.read_bytes() == antes and not (juego / "resourcepacks").exists() # solo lee assert v.b_principal.isEnabled() # el botón principal sigue disponible + + +def test_sin_instancia_elegida_no_lanza_nada(qtbot: QtBot, tmp_path: Path) -> None: + v = Ventana(buscar_instancias=lambda: [], fabrica_motor=siempre(Simulador())) + qtbot.addWidget(v) + v.lanzar("analizar") + v.a_cobertura.trigger() + assert v._hilo is None and v.carpeta() is None # pyright: ignore[reportPrivateUsage] + + +def test_instancia_sin_version_detectable_se_recupera(qtbot: QtBot, tmp_path: Path) -> None: + juego = tmp_path / "Pack" / "minecraft" + (juego / "mods").mkdir(parents=True) # sin lanzador, sin log y sin mods con versión + v = _ventana(qtbot, juego) + v.b_principal.click() + qtbot.waitUntil(lambda: v._hilo is None and "Se detuvo" in v.estado.text(), timeout=30000) # pyright: ignore[reportPrivateUsage] + assert v.b_principal.isEnabled() and "Traceback" not in v.log.toPlainText() + + +def test_cobertura_sin_nada_que_decir(qtbot: QtBot, tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fabrica.jar(juego / "mods" / "m.jar", fabrica.lang("m", "en_us", {"item.m.a": "Iron Plate"})) + v = _ventana(qtbot, juego) + v.a_cobertura.trigger() + qtbot.waitUntil(lambda: v._hilo is None and "No encontré" in v.estado.text(), timeout=30000) # pyright: ignore[reportPrivateUsage] + + +def test_soltar_rutas_que_no_son_carpetas(qtbot: QtBot, tmp_path: Path) -> None: + from PySide6.QtCore import QMimeData, QUrl + + archivo = tmp_path / "x.txt" + archivo.write_text("x", encoding="utf-8") + datos = QMimeData() + datos.setUrls( + [QUrl.fromLocalFile(str(archivo)), QUrl.fromLocalFile(str(tmp_path / "no_existe")), QUrl("https://x.y/z")] + ) + assert Ventana.carpetas_soltadas(datos) == [] + datos.setUrls([QUrl.fromLocalFile(str(tmp_path))]) + assert Ventana.carpetas_soltadas(datos) == [str(tmp_path)] diff --git a/tests/test_verificacion_formatos.py b/tests/test_verificacion_formatos.py new file mode 100644 index 0000000..3cdd9ad --- /dev/null +++ b/tests/test_verificacion_formatos.py @@ -0,0 +1,205 @@ +"""Casos límite de los formatos: idioma JSON y .lang, JSON/SNBT/JS/NBT/markdown con entradas raras, y UTF-8.""" + +import contextlib +import json + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from traductor_es.formatos import ( + comandos, + componente, + javascript, + json_cadenas, + json_texto, + lang_json, + lang_legacy, + markdown, + nbt, + snbt, +) + +TEXTO = st.text(st.characters(blacklist_categories=("Cs",)), max_size=40) +SEPARADORES = "\r\n\x85" + chr(0x2028) + chr(0x2029) + chr(0xFEFF) # saltos de línea y BOM +SIN_SALTOS = st.text(st.characters(blacklist_categories=("Cs",), blacklist_characters=SEPARADORES), max_size=40) +SUAVE = settings(max_examples=200, suppress_health_check=[HealthCheck.too_slow], deadline=None) + + +# ------------------------------------------------------------------ lang JSON +@SUAVE +@given(st.dictionaries(TEXTO, TEXTO, max_size=8)) +def test_lang_json_ida_y_vuelta_y_utf8(textos: dict[str, str]) -> None: + datos = lang_json.escribir(textos) + assert lang_json.leer(datos) == textos + assert json.loads(datos.decode("utf-8")) == textos # es JSON válido y UTF-8 puro + assert not datos.startswith(b"\xef\xbb\xbf") # sin BOM: el juego no lo quiere + + +def test_lang_json_guarda_tildes_y_ene_sin_escapar() -> None: + datos = lang_json.escribir({"a": "¡Ñandú, canción y pingüino!"}) + assert "¡Ñandú, canción y pingüino!".encode() in datos and b"\\u" not in datos + + +@pytest.mark.parametrize( + "crudo", + [ + b'\xef\xbb\xbf{"a": "b"}', # con BOM + b'{"a": "b",}', # coma final + b'{"a": "b" /* comentario */}', + b"{'a': 'b'}", # comillas simples + b'{"a": "b", "n": 5, "l": [1], "o": {"x": "y"}, "z": null}', # solo se conservan los textos + ], +) +def test_lang_json_tolerante(crudo: bytes) -> None: + assert lang_json.leer(crudo)["a"] == "b" + + +@pytest.mark.parametrize("crudo", [b"[1, 2]", b'"texto"', b"5"]) +def test_lang_json_que_no_es_objeto_falla_con_valueerror(crudo: bytes) -> None: + with pytest.raises(ValueError, match="no es un objeto"): + lang_json.leer(crudo) + + +@pytest.mark.parametrize("crudo", [b"", b"{", b'{"a": ', b"no es json", b"\xff\xfe\x00\x00"]) +def test_lang_json_roto_falla_con_valueerror(crudo: bytes) -> None: + with pytest.raises(ValueError): + lang_json.leer(crudo) + + +# ------------------------------------------------------------------ .lang antiguo +@SUAVE +@given( + st.dictionaries(SIN_SALTOS.filter(lambda k: "=" not in k and not k.startswith("#") and k), SIN_SALTOS, max_size=8) +) +def test_lang_legacy_ida_y_vuelta(textos: dict[str, str]) -> None: + datos = lang_legacy.escribir(textos) + assert lang_legacy.leer(datos) == textos + datos.decode("utf-8") + + +def test_lang_legacy_detalles() -> None: + crudo = "# comentario\r\nitem.a=Espada §a%s\r\n\r\nsin_igual\r\nitem.b=a=b=c\nitem.c=\n".encode() + assert lang_legacy.leer(crudo) == {"item.a": "Espada §a%s", "item.b": "a=b=c", "item.c": ""} + with pytest.raises(ValueError, match="salto de línea"): + lang_legacy.escribir({"k": "dos\nlíneas"}) + with pytest.raises(ValueError, match="salto de línea"): + lang_legacy.escribir({"k=v": "x"}) + + +# ------------------------------------------------------------------ escáneres: nunca fallan con otra cosa que ValueError +@SUAVE +@given(st.text(max_size=120)) +def test_escaneres_solo_lanzan_valueerror(texto: str) -> None: + for escaner in (javascript.escanear, snbt.escanear, json_cadenas.escanear): + try: + cadenas = escaner(texto) + except ValueError: + continue + assert isinstance(cadenas, list) + + +@SUAVE +@given(st.text(alphabet="{}[]\"':,/\\ ab1\n`$()+-*=", max_size=80)) +def test_escaneres_con_sintaxis_hostil(texto: str) -> None: + for escaner in (javascript.escanear, snbt.escanear, json_cadenas.escanear): + with contextlib.suppress(ValueError): + escaner(texto) + + +@SUAVE +@given(st.text(max_size=100)) +def test_componentes_y_comandos_no_fallan(texto: str) -> None: + json_texto.cargar(texto) + componente.unidades(texto) + assert componente.reconstruir(texto, {}) == texto # sin traducciones no cambia nada + comandos.analizar(texto) + comandos.analizar("tellraw @a " + texto) + comandos.analizar("execute as @a run say " + texto) + + +def test_json_cadenas_unicode_y_escapes() -> None: + src = '{"a": "ca\\u00f1\\u00f3n \\"x\\" \\n fin", "b": ["\\ud83d\\ude00", "plano"]}' + cadenas = json_cadenas.escanear(src) + assert [c.texto for c in cadenas] == ['cañón "x" \n fin', "😀", "plano"] + nuevo = json_cadenas.reemplazar(src, cadenas, {cadenas[2].inicio: "llano ñ"}) + assert json.loads(nuevo)["b"][1] == "llano ñ" and json.loads(nuevo)["a"] == json.loads(src)["a"] + + +@SUAVE +@given(TEXTO) +def test_json_cadenas_reemplazo_arbitrario_es_json_valido(texto: str) -> None: + src = '{"k": "valor", "l": ["x"]}' + cadenas = json_cadenas.escanear(src) + nuevo = json_cadenas.reemplazar(src, cadenas, {cadenas[0].inicio: texto, cadenas[1].inicio: texto}) + assert json.loads(nuevo) == {"k": texto, "l": [texto]} + + +# ------------------------------------------------------------------ JavaScript +@SUAVE +@given(TEXTO, st.sampled_from("'\"`")) +def test_js_escapar_ida_y_vuelta_con_especiales(texto: str, comilla: str) -> None: + if comilla == "`" and "${" in texto: + return + src = f"x.tell({comilla}{javascript.escapar(texto, comilla)}{comilla})\n" + assert [c.texto for c in javascript.escanear(src)] == [texto] + + +@pytest.mark.parametrize( + "src", + ["x.tell('sin cerrar", 'x.tell("sin cerrar\n")', "x.tell(`sin ${cerrar`)"], +) +def test_js_cadena_sin_cerrar_es_valueerror(src: str) -> None: + with pytest.raises(ValueError, match="sin cerrar"): + javascript.escanear(src) + + +def test_js_crlf_comentarios_regex_y_division() -> None: + src = "let a = 10 / 2 / 5\r\n// 'no'\r\nlet r = /'x'/g; /* \"no\" */ x.tell('sí ñ')\r\n" + assert [c.texto for c in javascript.escanear(src)] == ["sí ñ"] + + +# ------------------------------------------------------------------ SNBT / NBT +def test_snbt_escapes_y_unicode() -> None: + src = r'{title: "Hola \"mundo\" ñ\n", list: ["a", "b"]}' + cadenas = snbt.escanear(src) + nuevo = snbt.reemplazar(src, cadenas, {cadenas[0].inicio: 'Adiós "mundo" ¡ñ!'}) + assert next(c.texto for c in snbt.escanear(nuevo)) == 'Adiós "mundo" ¡ñ!' and '"a", "b"' in nuevo + + +@pytest.mark.parametrize( + "datos", [b"", b"\x00", b"\x0a\x00", b"\x1f\x8b\x08garbage", b"\x0a\x00\x00\x08\x00\x01a\xff\xff"] +) +def test_nbt_basura_falla_con_valueerror(datos: bytes) -> None: + with pytest.raises(ValueError): + nbt.leer(datos) + + +@SUAVE +@given(st.binary(max_size=60)) +def test_nbt_bytes_cualesquiera_no_revientan(datos: bytes) -> None: + with contextlib.suppress(ValueError): + nbt.leer(datos) + + +@SUAVE +@given(TEXTO) +def test_mutf8_ida_y_vuelta(texto: str) -> None: + assert nbt.mutf8_decodificar(nbt.mutf8_codificar(texto)) == texto + + +# ------------------------------------------------------------------ markdown +@SUAVE +@given(st.text(alphabet="abc ñ#-*>`~!\n\r\t[]()$<>=", max_size=120)) +def test_markdown_sin_traducir_no_cambia_nada(md: str) -> None: + lineas, segs = markdown.segmentar(md) + assert markdown.reconstruir(lineas, segs, {}) == md + + +def test_markdown_traduce_sin_tocar_codigo_ni_crlf() -> None: + md = "# Title here\r\n\r\nSome text about iron.\r\n\r\n```\r\ncode stays\r\n```\r\n- list item here\r\n" + lineas, segs = markdown.segmentar(md) + trad = {s.texto: "«ES» " + s.texto for s in segs} + salida = markdown.reconstruir(lineas, segs, trad) + assert "code stays" in salida and "«ES» Some text about iron." in salida + assert salida.count("\r\n") == md.count("\r\n") diff --git a/tests/test_verificacion_fuentes.py b/tests/test_verificacion_fuentes.py new file mode 100644 index 0000000..cc24771 --- /dev/null +++ b/tests/test_verificacion_fuentes.py @@ -0,0 +1,180 @@ +"""Casos límite de las fuentes: instancias rotas o vacías, jars dañados, options.txt raro y versiones inesperadas.""" + +import json +import zipfile +from pathlib import Path + +import pytest + +from traductor_es.fuentes import opciones +from traductor_es.fuentes.contenedores import Carpeta, Zip, abrir_mod +from traductor_es.fuentes.deteccion import detectar +from traductor_es.fuentes.instancia import Instancia, resolver_carpeta_juego +from traductor_es.fuentes.version import Version, formato_datos, formato_recursos + +from . import fabrica + + +# ------------------------------------------------------------------ versiones +@pytest.mark.parametrize( + ("texto", "partes"), + [ + ("1.20.1", (1, 20, 1)), + ("1.20", (1, 20, 0)), + ("1.12.2-forge", (1, 12, 2)), + (" 26.1 ", (26, 1, 0)), + ("1.21.11", (1, 21, 11)), + ], +) +def test_version_se_lee_de_formas_distintas(texto: str, partes: tuple[int, ...]) -> None: + assert Version.de(texto).partes == partes + + +@pytest.mark.parametrize("texto", ["", "abc", "1", "v1.2", "..", "latest"]) +def test_version_invalida_falla_con_valueerror(texto: str) -> None: + with pytest.raises(ValueError, match="no reconocida"): + Version.de(texto) + + +def test_formatos_de_pack_son_monotonos_y_cubren_todo() -> None: + versiones = [ + "1.6", + "1.9", + "1.11", + "1.13", + "1.16.2", + "1.18", + "1.19.4", + "1.20", + "1.20.5", + "1.21", + "1.21.5", + "26.1", + "26.3", + ] + previos = [formato_recursos(Version.de(v)) for v in versiones] + assert previos == sorted(previos) # el formato nunca baja al subir la versión + assert formato_recursos(Version.de("1.5")) == (1, 0) # versiones muy viejas: valor por defecto + assert formato_datos(Version.de("1.12")) is None and formato_datos(Version.de("1.13")) == (4, 0) + + +# ------------------------------------------------------------------ carpetas y detección +def test_carpeta_que_no_existe_o_vacia(tmp_path: Path) -> None: + assert resolver_carpeta_juego(tmp_path / "no_existe") is None + vacia = tmp_path / "vacia" + vacia.mkdir() + with pytest.raises(ValueError): + Instancia(vacia) + + +def test_resolver_carpeta_acepta_la_instancia_o_su_minecraft(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + assert resolver_carpeta_juego(juego) == juego + assert resolver_carpeta_juego(juego.parent) == juego + + +def test_deteccion_con_archivos_de_configuracion_rotos(tmp_path: Path) -> None: + juego = tmp_path / "Pack" / "minecraft" + juego.mkdir(parents=True) + (juego.parent / "mmc-pack.json").write_text("{esto no es json", encoding="utf-8") + (juego / "minecraftinstance.json").write_bytes(b"\xff\xfe\x00basura") + (juego / "mods").mkdir() + (juego / "mods" / "roto.jar").write_bytes(b"no es un zip") + with pytest.raises(ValueError): + Instancia(juego) # sin versión: dice que no la pudo detectar, no revienta con otro error + + +def test_deteccion_ignora_lo_que_no_es_cadena(tmp_path: Path) -> None: + juego = tmp_path / "Pack" / "minecraft" + (juego / "mods").mkdir(parents=True) + (juego.parent / "mmc-pack.json").write_text( + json.dumps({"components": [None, 5, {"uid": 3}, {"uid": "net.minecraft"}]}) + ) + d = detectar(juego, []) # no revienta: simplemente no encuentra la versión + assert d.version is None and d.origen == "no se pudo detectar" + + +# ------------------------------------------------------------------ options.txt +@pytest.mark.parametrize( + ("contenido", "idioma", "packs"), + [ + ('lang:es_mx\nresourcePacks:["vanilla","file/Mi Pack","mod_resources"]\n', "es_mx", ("Mi Pack",)), + ("lang:\n", None, ()), + ("resourcePacks:esto no es json\n", None, ()), + ('resourcePacks:{"a": 1}\n', None, ()), # JSON válido pero no es una lista + ('lang:EN_US\r\nresourcePacks:["file/Ñandú ¡pack!"]\r\n', "en_us", ("Ñandú ¡pack!",)), + ("", None, ()), + ], +) +def test_options_txt(tmp_path: Path, contenido: str, idioma: str | None, packs: tuple[str, ...]) -> None: + (tmp_path / "options.txt").write_text(contenido, encoding="utf-8", newline="") + o = opciones.leer(tmp_path) + assert o.idioma == idioma and o.packs == packs + + +def test_options_txt_inexistente_o_con_bytes_invalidos(tmp_path: Path) -> None: + assert opciones.leer(tmp_path) == opciones.Opciones() + (tmp_path / "options.txt").write_bytes(b"lang:es_mx\n\xff\xfe\x00 basura \x80\n") + assert opciones.leer(tmp_path).idioma == "es_mx" + + +# ------------------------------------------------------------------ contenedores y jars +def test_jar_danado_no_detiene_la_lectura(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + (juego / "mods" / "roto.jar").write_bytes(b"PK\x03\x04 truncado") + fabrica.jar(juego / "mods" / "bueno.jar", fabrica.lang("m", "en_us", {"item.m.a": "Iron Plate"})) + with Instancia(juego) as inst: + tabla = inst.tabla_idiomas() + assert tabla.textos("m", "en_us")["item.m.a"] == "Iron Plate" + + +def test_jar_con_nombres_unicode_y_lang_con_ene(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fabrica.jar(juego / "mods" / "Ñandú ¡mod! 日本.jar", fabrica.lang("ñ", "en_us", {"item.a": "Plátano ñ"})) + with Instancia(juego) as inst: + assert [c.nombre for c in inst.contenedores_mods()] == ["Ñandú ¡mod! 日本.jar"] + + +def test_zip_con_entradas_peligrosas_no_se_extrae(tmp_path: Path) -> None: + ruta = tmp_path / "m.jar" + with zipfile.ZipFile(ruta, "w") as z: + z.writestr("../../fuera.txt", "x") + z.writestr("assets/m/lang/en_us.json", '{"a": "b"}') + contenedores = list(abrir_mod(ruta)) + try: + assert contenedores and contenedores[0].leer("assets/m/lang/en_us.json") == b'{"a": "b"}' + finally: + for c in contenedores: + c.cerrar() + assert not (tmp_path.parent / "fuera.txt").exists() # solo se lee en memoria: nada se escribe en disco + + +def test_jars_anidados_con_limite_de_profundidad(tmp_path: Path) -> None: + interior = fabrica.zip_en_memoria(fabrica.lang("m", "en_us", {"a": "Hello there"})) + for _ in range(6): # un jar dentro de un jar dentro de otro… + interior = fabrica.zip_en_memoria({"META-INF/jarjar/x.jar": interior}) + ruta = tmp_path / "profundo.jar" + ruta.write_bytes(interior) + contenedores = list(abrir_mod(ruta)) # no debe colgarse ni lanzar + for c in contenedores: + c.cerrar() + assert len(contenedores) >= 1 + + +def test_carpeta_como_contenedor_ignora_lo_que_sale_de_la_raiz(tmp_path: Path) -> None: + (tmp_path / "pack" / "assets" / "m" / "lang").mkdir(parents=True) + (tmp_path / "pack" / "assets" / "m" / "lang" / "en_us.json").write_text('{"a": "b"}', encoding="utf-8") + (tmp_path / "secreto.txt").write_text("no", encoding="utf-8") + c = Carpeta(tmp_path / "pack") + assert "assets/m/lang/en_us.json" in c.nombres() + for fuera in ("../secreto.txt", "assets/../../secreto.txt", str(tmp_path / "secreto.txt")): + with pytest.raises(KeyError): + c.leer(fuera) + assert c.leer("assets/m/lang/en_us.json") == b'{"a": "b"}' + + +def test_zip_abrir_un_archivo_que_no_es_zip(tmp_path: Path) -> None: + ruta = tmp_path / "x.zip" + ruta.write_bytes(b"nada") + with pytest.raises(zipfile.BadZipFile): + Zip.abrir(ruta) diff --git a/tests/test_verificacion_modulos.py b/tests/test_verificacion_modulos.py new file mode 100644 index 0000000..7c1d463 --- /dev/null +++ b/tests/test_verificacion_modulos.py @@ -0,0 +1,276 @@ +"""Casos límite de los módulos: lang con textos difíciles, scripts con codificaciones raras y archivos dañados.""" + +import json +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.formatos import lang_json, lang_legacy +from traductor_es.fuentes import vanilla +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.libros import Libros +from traductor_es.modulos.misiones import HQM, BetterQuesting, FTBQuests, Heracles +from traductor_es.modulos.mods import Mods +from traductor_es.modulos.scripts import ScriptsKubeJS +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.dialectos import VARIANTES +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.mascara import enmascarar +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.salida.copias import Respaldo +from traductor_es.salida.resource_pack import PackSalida + +from . import fabrica + +DIFICILES = { + "item.m.a": "Iron Plate", + "item.m.b": "§aGreen %s with %d items §r and %1$s", + "item.m.c": 'Say "hello" \\ back\\slash and {0} and ${x}', + "item.m.d": "Línea uno\nLínea dos con tab\tfin", + "item.m.e": "Ñandú ¡Ágüita! cañón 日本語 😀 click here", + "item.m.f": "Visit https://example.com/a_b?x=1&y=2 now please", + "item.m.g": "%s", # solo un código: no hay nada que traducir + "item.m.h": "Bold text and
a break", +} + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +@pytest.fixture(autouse=True) +def _sin_lanzadores(monkeypatch: pytest.MonkeyPatch) -> None: + + def sin_lanzadores() -> list[Path]: + return [] # sin el juego real del PC + + monkeypatch.setattr(vanilla, "raices_de_lanzadores", sin_lanzadores) + + +def _traducir_mods(juego: Path, memoria: Memoria) -> Path: + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst)) + resumen = traducir_modulo(Mods(ctx), Traductor(Simulador(), memoria, Glosario())) + assert resumen.archivos + return ctx.pack_listo().raiz + + +def test_mods_modernos_producen_json_valido_con_los_mismos_codigos(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fabrica.jar(juego / "mods" / "m.jar", fabrica.lang("m", "en_us", DIFICILES)) + pack = _traducir_mods(juego, memoria) + json.loads((pack / "pack.mcmeta").read_text(encoding="utf-8")) + for variante in VARIANTES: + ruta = pack / "assets" / "m" / "lang" / f"{variante}.json" + crudo = ruta.read_bytes() + crudo.decode("utf-8") # UTF-8 puro + assert not crudo.startswith(b"\xef\xbb\xbf") + textos = json.loads(crudo) # JSON válido + assert set(textos) <= set(DIFICILES) + for clave, valor in textos.items(): + assert sorted(enmascarar(valor).tokens) == sorted(enmascarar(DIFICILES[clave]).tokens), (variante, clave) + assert textos["item.m.e"] == "«ES» Ñandú ¡Ágüita! cañón 日本語 😀 click here" + assert "item.m.g" not in textos # sin nada que traducir no se inventa nada + + +def test_mods_legacy_lang_sin_saltos_de_linea_reales(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.12.2") + textos = {k: v for k, v in DIFICILES.items() if "\n" not in v} + fabrica.jar(juego / "mods" / "m.jar", fabrica.lang("m", "en_us", textos, legacy=True)) + pack = _traducir_mods(juego, memoria) + for ruta in (pack / "assets" / "m" / "lang").glob("*.lang"): + crudo = ruta.read_bytes() + crudo.decode("utf-8") + leidos = lang_legacy.leer(crudo) + assert set(leidos) <= set(textos) + for clave, valor in leidos.items(): + assert sorted(enmascarar(valor).tokens) == sorted(enmascarar(textos[clave]).tokens) + assert any((pack / "assets" / "m" / "lang").glob("*.lang")) + + +def test_lang_vacio_roto_o_sin_texto_no_detiene_a_los_demas(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fabrica.jar(juego / "mods" / "vacio.jar", {"assets/v/lang/en_us.json": "{}"}) + fabrica.jar(juego / "mods" / "roto.jar", {"assets/r/lang/en_us.json": '{"a": "Hello there'}) + fabrica.jar(juego / "mods" / "mixto.jar", {"assets/x/lang/en_us.json": '{"a": 5, "b": null, "c": "Hello there"}'}) + fabrica.jar(juego / "mods" / "bueno.jar", fabrica.lang("m", "en_us", {"item.m.a": "Iron Plate"})) + pack = _traducir_mods(juego, memoria) + assert json.loads((pack / "assets" / "m" / "lang" / "es_mx.json").read_text("utf-8")) == { + "item.m.a": "«ES» Iron Plate" + } + assert json.loads((pack / "assets" / "x" / "lang" / "es_mx.json").read_text("utf-8")) == {"c": "«ES» Hello there"} + + +# ------------------------------------------------------------------ scripts +def _script(tmp_path: Path, contenido: bytes) -> tuple[Path, Path]: + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "server_scripts" / "s.js" + ruta.parent.mkdir(parents=True) + ruta.write_bytes(contenido) + return juego, ruta + + +def _correr(juego: Path, memoria: Memoria, avisos: list[str] | None = None) -> int: + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), registro=(avisos.append if avisos is not None else lambda _m: None)) + return traducir_modulo(ScriptsKubeJS(ctx), Traductor(Simulador(), memoria, Glosario())).traducidos + + +def test_script_con_bom_y_crlf_se_conserva(tmp_path: Path, memoria: Memoria) -> None: + fuente = "PlayerEvents.chat(e => {\r\n e.player.tell('Hello there ñ')\r\n})\r\n".encode() + juego, ruta = _script(tmp_path, fuente) + assert _correr(juego, memoria) == 1 + nuevo = ruta.read_bytes() + assert nuevo.startswith(b"\xef\xbb\xbf") and nuevo.count(b"\r\n") == 3 and "«ES» Hello there ñ".encode() in nuevo + + +@pytest.mark.parametrize( + "contenido", + [ + b"", + b"\n\n", + b"// solo un comentario 'x'\n", + b"let a = 'sin cerrar\n", + b"x.tell('\xe9\xe9 latin-1')\n", + b"\x00\x01\x02", + b"`${", + ], +) +def test_script_vacio_roto_o_con_otra_codificacion_se_deja_igual( + tmp_path: Path, memoria: Memoria, contenido: bytes +) -> None: + juego, ruta = _script(tmp_path, contenido) + avisos: list[str] = [] + assert _correr(juego, memoria, avisos) == 0 + assert ruta.read_bytes() == contenido + + +def test_script_enorme(tmp_path: Path, memoria: Memoria) -> None: + cuerpo = "".join(f"e.player.tell('Mensaje número {i} para ti')\n" for i in range(5000)).encode() + juego, ruta = _script(tmp_path, cuerpo) + assert _correr(juego, memoria) == 5000 + assert ruta.read_text("utf-8").count("«ES»") == 5000 + + +# ------------------------------------------------------------------ misiones y libros dañados +@pytest.mark.parametrize( + ("modulo", "ruta", "contenido"), + [ + (FTBQuests, "config/ftbquests/quests/chapters/a.snbt", b'{ title: "sin cerrar '), + (FTBQuests, "config/ftbquests/quests/chapters/b.snbt", b"\xff\xfe\x00 no utf8"), + (FTBQuests, "config/ftbquests/quests/chapters/c.nbt", b"\x1f\x8b\x08 gzip roto"), + (BetterQuesting, "config/betterquesting/DefaultQuests.json", b'{"questDatabase:9": {'), + (HQM, "config/hqm/quests.json", b"no es json"), + (Heracles, "config/heracles/quests/q.json", b"\xff\xfe"), + ], +) +def test_mision_danada_se_deja_igual_sin_reventar( + tmp_path: Path, memoria: Memoria, modulo: type, ruta: str, contenido: bytes +) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + destino = juego / ruta + destino.parent.mkdir(parents=True) + destino.write_bytes(contenido) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + m = modulo(ctx) + if m.detectar(): + traducir_modulo(m, Traductor(Simulador(), memoria, Glosario())) + assert destino.read_bytes() == contenido + + +def test_libro_con_json_roto_y_libro_bueno(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + libros = juego / "patchouli_books" / "guia" + (libros / "en_us" / "entries").mkdir(parents=True) + (libros / "book.json").write_text( + json.dumps({"name": "Guide book", "landing_text": "Welcome to the guide."}), "utf-8" + ) + (libros / "en_us" / "entries" / "roto.json").write_text('{"name": "Roto', "utf-8") + (libros / "en_us" / "entries" / "bueno.json").write_text( + json.dumps( + { + "name": "Good entry", + "category": "x", + "pages": [{"type": "patchouli:text", "text": "Hello $(l)there$()."}], + } + ), + "utf-8", + ) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst)) + modulo = Libros(ctx) + if modulo.detectar(): + traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) + assert (libros / "en_us" / "entries" / "roto.json").read_text("utf-8") == '{"name": "Roto' + salida = json.dumps(lang_json.leer("{}")) # el lector de lang sigue sano + assert salida == "{}" + + +# ------------------------------------------------------------------ datos (funciones, logros, tips) raros +def test_datos_con_formas_inesperadas_no_revientan(tmp_path: Path, memoria: Memoria) -> None: + from traductor_es.modulos.datos import Datos + + juego = fabrica.prism(tmp_path, "1.20.1") + base = juego / "kubejs" / "data" / "a" + casos = { + "advancements/texto.json": json.dumps({"display": "no es un objeto"}), + "advancements/lista.json": json.dumps(["x", {"display": {"title": "Hi there"}}]), + "advancements/nulos.json": json.dumps({"display": {"title": None, "description": 5}}), + "advancements/bom.json": "\ufeff" + + json.dumps({"display": {"title": "Big Win", "description": {"translate": "k"}}}), + "functions/macro.mcfunction": '$tellraw @a {"text":"$(x) hello there"}\n', + "functions/vacia.mcfunction": "", + "functions/latin.mcfunction": None, # bytes que no son UTF-8 + } + for rel, contenido in casos.items(): + ruta = base / rel + ruta.parent.mkdir(parents=True, exist_ok=True) + if contenido is None: + ruta.write_bytes(b"say \xe9\xe9 hola\n") + else: + ruta.write_text(contenido, encoding="utf-8") + antes = {p: p.read_bytes() for p in base.rglob("*") if p.is_file()} + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + traducir_modulo(Datos(ctx), Traductor(Simulador(), memoria, Glosario())) + despues = {p: p.read_bytes() for p in base.rglob("*") if p.is_file()} + cambiados = sorted(p.name for p in antes if antes[p] != despues[p]) + assert cambiados == ["bom.json"] # solo el logro bueno: lo raro se deja igual + assert json.loads((base / "advancements" / "bom.json").read_text("utf-8"))["display"]["title"] == "«ES» Big Win" + + +def test_zip_danado_se_ignora(tmp_path: Path, memoria: Memoria) -> None: + from traductor_es.modulos.datos import Datos + + juego = fabrica.prism(tmp_path, "1.20.1") + (juego / "datapacks").mkdir() + (juego / "datapacks" / "roto.zip").write_bytes(b"PK\x03\x04 no es un zip de verdad") + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + assert not Datos(ctx).detectar() + + +@pytest.mark.parametrize("namespace", ["..", "a:b", "nul", "COM1.x", "con.", "trailing ", "a|b"]) +def test_namespace_imposible_no_tumba_el_resto(tmp_path: Path, memoria: Memoria, namespace: str) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + avisos: list[str] = [] + fabrica.jar( + juego / "mods" / "x.jar", + { + f"assets/{namespace}/lang/en_us.json": '{"a": "Hello there"}', + **fabrica.lang("ok", "en_us", {"b": "Good one"}), + }, + ) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst), registro=avisos.append) + resumen = traducir_modulo(Mods(ctx), Traductor(Simulador(), memoria, Glosario())) + pack = ctx.pack_listo().raiz + assert json.loads((pack / "assets" / "ok" / "lang" / "es_mx.json").read_text("utf-8")) == {"b": "«ES» Good one"} + assert resumen.pendientes >= 1 and any("no se puede guardar" in a for a in avisos) + assert not (tmp_path / "Pack" / "minecraft" / "lang").exists() # nada se escribió fuera del pack diff --git a/tests/test_verificacion_motores.py b/tests/test_verificacion_motores.py new file mode 100644 index 0000000..c0114d9 --- /dev/null +++ b/tests/test_verificacion_motores.py @@ -0,0 +1,191 @@ +"""Casos límite de los motores: respuestas raras o rotas, errores de red, cancelación y creación con datos faltantes.""" + +import json +import threading +from collections.abc import Callable, Iterator +from typing import Any + +import httpx +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from traductor_es.motores import fabrica +from traductor_es.motores.base import Cancelado, Entrada, ErrorFatal, Lote +from traductor_es.motores.compatible import PERFILES, Compatible +from traductor_es.motores.instrucciones import interpretar, sistema, usuario +from traductor_es.motores.simulador import Simulador + +SUAVE = settings(max_examples=200, suppress_health_check=[HealthCheck.too_slow], deadline=None) + + +def lote(*textos: str, tarea: str = "traducir") -> Lote: + return Lote(tuple(Entrada(str(i), t) for i, t in enumerate(textos)), "ui", "español", {}, "", tarea) + + +@pytest.fixture +def armar() -> Iterator[Callable[..., tuple[Compatible, list[httpx.Request], list[float]]]]: + abiertos: list[Compatible] = [] + + def crear( + *respuestas: httpx.Response | Exception, **kw: Any + ) -> tuple[Compatible, list[httpx.Request], list[float]]: + pedidos: list[httpx.Request] = [] + esperas: list[float] = [] + cola = list(respuestas) + + def manejar(pedido: httpx.Request) -> httpx.Response: + pedidos.append(pedido) + r = cola.pop(0) if len(cola) > 1 else cola[0] + if isinstance(r, Exception): + raise r + return r + + cliente = httpx.Client(transport=httpx.MockTransport(manejar)) + m = Compatible(PERFILES["openrouter"], "modelo", cliente=cliente, esperar=esperas.append, clave="sk-x", **kw) + abiertos.append(m) + return m, pedidos, esperas + + yield crear + for m in abiertos: + m.cerrar() + + +# ------------------------------------------------------------------ interpretar +@SUAVE +@given(st.one_of(st.none(), st.text(max_size=200))) +def test_interpretar_nunca_falla_y_siempre_devuelve_textos(texto: str | None) -> None: + resultado = interpretar(texto) + assert isinstance(resultado, dict) and all(isinstance(k, str) and isinstance(v, str) for k, v in resultado.items()) + + +@pytest.mark.parametrize( + ("crudo", "esperado"), + [ + ('{"0": "Hola ñ"}', {"0": "Hola ñ"}), + ('```json\n{"0": "Hola"}\n```', {"0": "Hola"}), + ('```\n{"0": "Hola"}\n```', {"0": "Hola"}), + (' {"0": "Hola", "1": 5, "2": null, "3": ["x"]} ', {"0": "Hola"}), + ('{"0": "con \\"comillas\\" y \\u00f1"}', {"0": 'con "comillas" y ñ'}), + ("[]", {}), + ('"solo texto"', {}), + ("", {}), + ("no es json", {}), + ('{"0": "cortado', {}), + ], +) +def test_interpretar_respuestas_tipicas_y_rotas(crudo: str, esperado: dict[str, str]) -> None: + assert interpretar(crudo) == esperado + + +# ------------------------------------------------------------------ motor compatible +@pytest.mark.parametrize( + "cuerpo", + [ # pyright: ignore[reportUnknownArgumentType] + {}, + {"choices": []}, + {"choices": [{}]}, + {"choices": [{"message": None}]}, + {"choices": [{"message": {"content": None}}]}, + {"choices": [{"message": {"content": ["no", "texto"]}}]}, + {"choices": [{"message": {"content": 5}}]}, + {"choices": "no es lista"}, + [1, 2, 3], + "texto plano", + ], +) +def test_respuesta_con_forma_inesperada_no_rompe(armar: Callable[..., Any], cuerpo: object) -> None: + m, _, _ = armar(httpx.Response(200, json=cuerpo)) + assert m.traducir_lote(lote("Hello there")) == {} + + +def test_respuesta_que_no_es_json(armar: Callable[..., Any]) -> None: + m, _, _ = armar(httpx.Response(200, content=b"Bad gateway")) + assert m.traducir_lote(lote("Hello there")) == {} + + +def test_error_de_servidor_persistente_devuelve_vacio_tras_reintentar(armar: Callable[..., Any]) -> None: + m, pedidos, esperas = armar(httpx.Response(503, text="caído"), reintentos=3) + assert m.traducir_lote(lote("Hello there")) == {} + assert len(pedidos) == 3 and len(esperas) == 2 # esperó entre intentos, no tras el último + + +@pytest.mark.parametrize( + "excepcion", [httpx.ReadTimeout("lento"), httpx.RemoteProtocolError("cortado"), httpx.WriteError("x")] +) +def test_fallos_de_red_se_reintentan_y_no_revientan(armar: Callable[..., Any], excepcion: Exception) -> None: + m, pedidos, _ = armar(excepcion, reintentos=2) + assert m.traducir_lote(lote("Hello there")) == {} + assert len(pedidos) == 2 + + +def test_retry_after_basura_no_rompe(armar: Callable[..., Any]) -> None: + ok = httpx.Response(200, json={"choices": [{"message": {"content": json.dumps({"0": "Hola"})}}]}) + m, _, _ = armar(httpx.Response(429, headers={"retry-after": "soon"}), ok, reintentos=3) + assert m.traducir_lote(lote("Hello there")) == {"0": "Hola"} + + +def test_cancelar_corta_antes_de_pedir(armar: Callable[..., Any]) -> None: + m, pedidos, _ = armar(httpx.Response(200, json={})) + m.cancelar = threading.Event() + m.cancelar.set() + with pytest.raises(Cancelado): + m.traducir_lote(lote("Hello there")) + assert pedidos == [] + + +@pytest.mark.parametrize(("codigo", "texto"), [(401, "clave"), (403, "clave"), (402, "saldo"), (404, "modelo")]) +def test_errores_fatales_dicen_que_hacer(armar: Callable[..., Any], codigo: int, texto: str) -> None: + m, pedidos, _ = armar(httpx.Response(codigo, json={"error": {"message": "x"}})) + with pytest.raises(ErrorFatal, match=texto): + m.traducir_lote(lote("Hello there")) + assert len(pedidos) == 1 # no se insiste cuando reintentar no sirve + + +def test_el_pedido_lleva_el_texto_con_utf8_intacto(armar: Callable[..., Any]) -> None: + m, pedidos, _ = armar(httpx.Response(200, json={"choices": [{"message": {"content": "{}"}}]})) + m.traducir_lote(lote("Ñandú ¡sí! 日本語 😀 ⟦0⟧")) + cuerpo = json.loads(pedidos[0].content.decode("utf-8")) + assert "Ñandú ¡sí! 日本語 😀 ⟦0⟧" in cuerpo["messages"][1]["content"] + + +# ------------------------------------------------------------------ instrucciones, simulador, fábrica +def test_instrucciones_para_cada_ambito_y_tarea() -> None: + for ambito in ("ui", "mision", "libro", "script", "desconocido"): + for tarea in ("traducir", "revisar"): + lt = Lote( + (Entrada("0", "Hello ⟦0⟧", "p", "Hola ⟦0⟧"),), + ambito, + "español mexicano", + {"Iron": "Hierro"}, + "granja", + tarea, + ) + assert "español mexicano" in sistema(lt) or tarea == "revisar" + assert "Hello ⟦0⟧" in usuario(lt) + + +def test_simulador_conserva_marcadores_y_bordes() -> None: + r = Simulador().traducir_lote(lote("⟦0⟧ Hello ⟦1⟧")) + assert r == {"0": "«ES» ⟦0⟧ Hello ⟦1⟧"} or "⟦0⟧" in r["0"] + + +@pytest.mark.parametrize("motor", list(fabrica.MOTORES)) +def test_fabrica_pide_lo_que_falta(motor: str) -> None: + if fabrica.necesita_clave(motor): + with pytest.raises(ErrorFatal, match="Falta la clave"): + fabrica.crear(motor, None, "modelo") + elif motor == "personalizado": + with pytest.raises(ErrorFatal, match="dirección"): + fabrica.crear(motor, None, "modelo") + else: + m = fabrica.crear(motor, None, "modelo") + assert m.nombre + cerrar = getattr(m, "cerrar", None) + if callable(cerrar): + cerrar() + + +def test_fabrica_motor_desconocido() -> None: + with pytest.raises(ErrorFatal, match="desconocido"): + fabrica.crear("no-existe", "clave", "modelo") diff --git a/tests/test_verificacion_nucleo.py b/tests/test_verificacion_nucleo.py new file mode 100644 index 0000000..1864c01 --- /dev/null +++ b/tests/test_verificacion_nucleo.py @@ -0,0 +1,191 @@ +"""Casos límite del núcleo: memoria, glosario y traductor con entradas raras, motores que se portan mal y UTF-8.""" + +import sqlite3 +import threading +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.motores.base import ErrorFatal, Lote +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.mascara import desenmascarar, enmascarar, es_traducible, separar_bordes +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor + +ESPECIALES = "Ñandú sí, ¿cómo estás? ¡Ágüita! 日本語 😀 §a%s §r%d {0} ${x}" + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +# ------------------------------------------------------------------ memoria +def test_memoria_guarda_y_lee_utf8_en_disco(tmp_path: Path) -> None: + ruta = tmp_path / "datos" / "memoria.db" + with Memoria(ruta) as m: + m.guardar({ESPECIALES: "Ñandú ¡sí! §a%s"}, "es_mx", "ui", "prueba") + assert m.obtener(ESPECIALES, "es_mx", "ui") == "Ñandú ¡sí! §a%s" + with Memoria(ruta) as m: # se vuelve a abrir: persiste y sigue siendo UTF-8 + assert m.obtener(ESPECIALES, "es_mx", "ui") == "Ñandú ¡sí! §a%s" + assert m.cantidad() == 1 + + +def test_memoria_buscar_no_trata_comodines_como_comodines(memoria: Memoria) -> None: + memoria.guardar({"100% real": "100 % real", "abc": "abc", "a_c": "a_c", "a!c": "a!c"}, "es_mx", "ui", "p") + assert [c.origen for c in memoria.buscar("%", "es_mx")] == ["100% real"] + assert [c.origen for c in memoria.buscar("a_c", "es_mx")] == ["a_c"] + assert [c.origen for c in memoria.buscar("a!c", "es_mx")] == ["a!c"] + + +def test_memoria_con_inyeccion_sql_en_el_texto(memoria: Memoria) -> None: + raro = "'; DROP TABLE traducciones; --" + memoria.guardar({raro: "x"}, "es_mx", "ui", "p") + assert memoria.obtener(raro, "es_mx", "ui") == "x" and memoria.cantidad() == 1 + assert memoria.buscar(raro, "es_mx")[0].origen == raro + + +def test_memoria_desde_varios_hilos(memoria: Memoria) -> None: + errores: list[BaseException] = [] + + def trabajar(n: int) -> None: + try: + for i in range(40): + memoria.guardar({f"texto {n}-{i}": f"texto {n}-{i} ñ"}, "es_mx", "ui", "p") + assert memoria.obtener(f"texto {n}-{i}", "es_mx", "ui") == f"texto {n}-{i} ñ" + except BaseException as e: + errores.append(e) + + hilos = [threading.Thread(target=trabajar, args=(n,)) for n in range(8)] + for h in hilos: + h.start() + for h in hilos: + h.join() + assert not errores and memoria.cantidad() == 8 * 40 + + +def test_correccion_manda_y_se_puede_quitar(memoria: Memoria) -> None: + memoria.guardar({"Hello": "Hola"}, "es_mx", "ui", "p") + memoria.corregir("Hello", "es_mx", "¡Buenas!") + assert memoria.correccion("Hello", "es_mx") == "¡Buenas!" and memoria.correcciones("es_mx") == {"Hello": "¡Buenas!"} + memoria.quitar_correccion("Hello", "es_mx") + assert memoria.correccion("Hello", "es_mx") is None + + +def test_memoria_corrupta_da_un_error_claro(tmp_path: Path) -> None: + ruta = tmp_path / "memoria.db" + ruta.write_bytes(b"esto no es una base de datos sqlite" * 50) + with pytest.raises(sqlite3.DatabaseError): + Memoria(ruta) + + +# ------------------------------------------------------------------ glosario +@pytest.mark.parametrize( + "termino", ["C++", "(Test)", "a.b", "Tom's [Storage]", "50% off", "back\\slash", "$HOME", "x|y"] +) +def test_glosario_con_caracteres_de_regex(termino: str) -> None: + g = Glosario() + g.agregar_pares({"k": termino}, {"k": "traducido"}) + g.fijar({termino + " extra": "fijo"}) + assert isinstance(g.relevantes([f"use {termino} here and {termino} extra"]), dict) + + +def test_glosario_respeta_palabras_completas() -> None: + g = Glosario() + g.agregar_pares({"a": "Wheat", "b": "Iron Ingot"}, {"a": "Trigo", "b": "Lingote de hierro"}) + assert g.relevantes(["Wheat bread", "Iron Ingot"]) == {"Wheat": "Trigo", "Iron Ingot": "Lingote de hierro"} + assert g.relevantes(["Wheatgrass", "Ironwork"]) == {} # no son palabras completas + assert g.relevantes([]) == {} + + +# ------------------------------------------------------------------ máscara y UTF-8 +def test_mascara_ida_y_vuelta_con_especiales() -> None: + m = enmascarar(ESPECIALES) + assert desenmascarar(m.texto, m.tokens) == ESPECIALES + assert "Ñ" in m.texto and "😀" in m.texto and "日本語" in m.texto # lo que no es código se queda legible + + +@pytest.mark.parametrize("texto", ["", " ", "\n", "§a", "%s", "{0}", "
", "https://x.y/z", "12345", "---"]) +def test_textos_sin_nada_que_traducir(texto: str) -> None: + assert not es_traducible(texto) + assert separar_bordes(texto)[1] in ("", texto.strip()) + + +# ------------------------------------------------------------------ traductor con motores que se portan mal +class _Motor: + nombre = "malo" + + def __init__(self, respuesta: object) -> None: + self.respuesta = respuesta + self.llamadas = 0 + + def traducir_lote(self, lote: Lote) -> dict[str, str]: + self.llamadas += 1 + r = self.respuesta + if callable(r): + return r(lote) # type: ignore[no-any-return] + return r # type: ignore[return-value] + + +def test_traductor_con_lista_vacia_y_duplicados(memoria: Memoria) -> None: + t = Traductor(Simulador(), memoria, Glosario()) + assert t.traducir([], "ui").traducciones == {} + r = t.traducir(["Hello world", "Hello world", " Hello world "], "ui") + assert r.traducciones["Hello world"] == "«ES» Hello world" + assert r.traducciones[" Hello world "] == " «ES» Hello world " # los bordes se conservan + + +def test_traductor_con_codigos_y_especiales(memoria: Memoria) -> None: + t = Traductor(Simulador(), memoria, Glosario()) + textos = ["§aYou have %d items in %s", "Press {0} to open the menu ¡ya!", "Ñandú ${nombre} says §chello there"] + r = t.traducir(textos, "ui") + for original in textos: + assert enmascarar(original).tokens == enmascarar(r.traducciones[original]).tokens # mismos códigos + assert "«ES»" in r.traducciones[original] + + +def test_motor_que_pierde_los_codigos_se_descarta(memoria: Memoria) -> None: + def sin_codigos(lote: Lote) -> dict[str, str]: + return {e.id: "Texto sin marcadores" for e in lote.entradas} + + motor = _Motor(sin_codigos) + r = Traductor(motor, memoria, Glosario()).traducir(["§aHello %s there"], "ui") + assert "§aHello %s there" not in r.traducciones or r.traducciones["§aHello %s there"] == "§aHello %s there" + assert motor.llamadas >= 1 # reintentó y no aceptó la traducción rota + + +@pytest.mark.parametrize("respuesta", [{}, {"id-que-no-existe": "x"}, {"0": ""}]) +def test_motor_que_omite_o_inventa_ids(memoria: Memoria, respuesta: dict[str, str]) -> None: + r = Traductor(_Motor(respuesta), memoria, Glosario()).traducir(["Hello brave world"], "ui") + assert r.traducciones.get("Hello brave world", "Hello brave world") == "Hello brave world" # queda en inglés + + +def test_error_fatal_del_motor_se_propaga(memoria: Memoria) -> None: + def falla(_lote: Lote) -> dict[str, str]: + raise ErrorFatal("clave inválida") + + with pytest.raises(ErrorFatal): + Traductor(_Motor(falla), memoria, Glosario()).traducir(["Hello brave world"], "ui") + + +def test_texto_muy_largo_se_traduce_sin_perder_nada(memoria: Memoria) -> None: + largo = " ".join(f"Palabra{i}" for i in range(4000)) + r = Traductor(Simulador(), memoria, Glosario()).traducir([largo], "libro") + assert r.traducciones[largo] == "«ES» " + largo + + +def test_memoria_danada_da_un_error_claro_en_el_proceso(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + from traductor_es.proceso import Proceso + + from . import fabrica + + datos = tmp_path / "datos" + datos.mkdir() + (datos / "memoria.db").write_bytes(b"basura" * 100) + monkeypatch.setenv("TRADUCTOR_ES_DATOS", str(datos)) + juego = fabrica.prism(tmp_path, "1.20.1") + with pytest.raises(ErrorFatal, match="memoria de traducciones está dañada"): + Proceso(juego, Simulador()) diff --git a/tests/test_verificacion_salida.py b/tests/test_verificacion_salida.py new file mode 100644 index 0000000..6a9f66b --- /dev/null +++ b/tests/test_verificacion_salida.py @@ -0,0 +1,155 @@ +"""Casos límite de la salida: options.txt, copias de seguridad, exportación y la línea de comandos real.""" + +import json +import os +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest + +from traductor_es.salida import exportar, opciones +from traductor_es.salida.copias import Respaldo + +from . import fabrica + +RAIZ = Path(__file__).resolve().parent.parent + + +# ------------------------------------------------------------------ options.txt +def _opts(tmp_path: Path, crudo: bytes) -> Path: + (tmp_path / "options.txt").write_bytes(crudo) + return tmp_path + + +def test_activar_conserva_crlf_y_otras_lineas_y_es_idempotente(tmp_path: Path) -> None: + crudo = ( + 'fov:0.5\r\nlang:en_us\r\nresourcePacks:["vanilla","file/Otro"]\r\n' + 'incompatibleResourcePacks:["file/Traduccion_ES"]\r\n' + ) + _opts(tmp_path, crudo.encode()) + cambios = opciones.activar(tmp_path, "Traduccion_ES", "es_mx") + nuevo = (tmp_path / "options.txt").read_bytes().decode() + assert cambios and nuevo.count("\r\n") == nuevo.count("\n") and "fov:0.5\r\n" in nuevo + assert 'resourcePacks:["vanilla","file/Otro","file/Traduccion_ES"]' in nuevo and "lang:es_mx" in nuevo + assert "incompatibleResourcePacks:[]" in nuevo + assert opciones.activar(tmp_path, "Traduccion_ES", "es_mx") == [] # ya está: no cambia nada + assert (tmp_path / "options.txt").read_bytes().decode() == nuevo + + +def test_activar_sin_options_no_crea_nada_y_con_lista_danada_no_toca(tmp_path: Path) -> None: + assert opciones.activar(tmp_path, "Traduccion_ES", "es_mx") == [] and not (tmp_path / "options.txt").exists() + crudo = b"lang:en_us\nresourcePacks:[esto no es json\n" + _opts(tmp_path, crudo) + with pytest.raises(ValueError, match="lista de resource packs"): + opciones.activar(tmp_path, "Traduccion_ES", "es_mx") + assert (tmp_path / "options.txt").read_bytes() == crudo + + +def test_activar_conserva_bytes_que_no_son_utf8_y_nombres_con_ene(tmp_path: Path) -> None: + crudo = b'lastServer:caf\xe9\nresourcePacks:["vanilla","file/\xc3\x91and\xc3\xba"]\n' + _opts(tmp_path, crudo) + opciones.activar(tmp_path, "Traduccion_ES", None) + nuevo = (tmp_path / "options.txt").read_bytes() + assert b"lastServer:caf\xe9\n" in nuevo # el byte raro sigue igual + assert '"file/Ñandú","file/Traduccion_ES"'.encode() in nuevo + + +# ------------------------------------------------------------------ copias de seguridad +def test_respaldo_restaura_cuando_el_archivo_cambio_o_se_borro(tmp_path: Path) -> None: + juego = tmp_path / "juego" + ruta = juego / "kubejs" / "ñandú ¡x!.js" + ruta.parent.mkdir(parents=True) + ruta.write_bytes(b"original") + r = Respaldo(juego) + r.escribir(ruta, b"traducido") + assert r.original(ruta) == b"original" + ruta.unlink() # el usuario borró el archivo + assert r.restaurar() == [ruta] and ruta.read_bytes() == b"original" + ruta.write_bytes(b"actualizado por el modpack") # y luego lo actualizó otra versión del modpack + assert r.original(ruta) == b"actualizado por el modpack" + + +def test_respaldo_con_registro_corrupto_empieza_de_cero(tmp_path: Path) -> None: + juego = tmp_path / "juego" + (juego / "traductor_es").mkdir(parents=True) + (juego / "traductor_es" / "registro.json").write_bytes(b"{no es json") + ruta = juego / "a.js" + ruta.write_bytes(b"x") + r = Respaldo(juego) + r.escribir(ruta, b"y") + assert json.loads((juego / "traductor_es" / "registro.json").read_text("utf-8")) + + +def test_respaldo_rechaza_rutas_fuera_de_la_instancia(tmp_path: Path) -> None: + juego = tmp_path / "juego" + juego.mkdir() + fuera = tmp_path / "fuera.txt" + fuera.write_bytes(b"x") + with pytest.raises(ValueError): + Respaldo(juego).escribir(fuera, b"y") + assert fuera.read_bytes() == b"x" + + +# ------------------------------------------------------------------ exportar +def test_exportar_kit_y_pack(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + pack = juego / "resourcepacks" / "Traduccion_ES" + (pack / "assets" / "m" / "lang").mkdir(parents=True) + (pack / "pack.mcmeta").write_text("{}", encoding="utf-8") + (pack / "assets" / "m" / "lang" / "es_mx.json").write_text('{"a": "Ñandú"}', encoding="utf-8") + script = juego / "kubejs" / "server_scripts" / "s.js" + script.parent.mkdir(parents=True) + script.write_text("x", encoding="utf-8") + Respaldo(juego).escribir(script, b"traducido") + destino = tmp_path / "salida" / "kit.zip" + assert exportar.exportar_kit_servidor(juego, destino) >= 2 + with zipfile.ZipFile(destino) as z: + assert z.testzip() is None and "kubejs/server_scripts/s.js" in z.namelist() + assert exportar.exportar_pack(juego, tmp_path / "salida" / "pack.zip") == 2 + with pytest.raises(ValueError, match="traduce primero"): + exportar.exportar_pack(tmp_path / "otro", tmp_path / "x.zip") + + +# ------------------------------------------------------------------ línea de comandos real +def _cli(*args: str, datos: Path) -> subprocess.CompletedProcess[str]: + entorno = {**os.environ, "TRADUCTOR_ES_DATOS": str(datos), "PYTHONIOENCODING": "utf-8"} + return subprocess.run( + [sys.executable, "-m", "traductor_es", *args], capture_output=True, text=True, encoding="utf-8", + env=entorno, cwd=RAIZ, timeout=120, check=False, + ) # fmt: skip + + +def test_cli_ayuda_version_y_argumentos_invalidos(tmp_path: Path) -> None: + assert _cli("--version", datos=tmp_path).returncode == 0 + assert "traducir" in _cli("--help", datos=tmp_path).stdout + malo = _cli("traducir", datos=tmp_path) # falta la carpeta + assert malo.returncode != 0 and "carpeta" in malo.stderr + assert _cli("analizar", "x", "--dialecto", "klingon", datos=tmp_path).returncode != 0 + + +def test_cli_carpeta_que_no_existe_da_un_error_claro_sin_traza(tmp_path: Path) -> None: + r = _cli("analizar", str(tmp_path / "no_existe"), datos=tmp_path) + assert r.returncode != 0 and "Traceback" not in r.stderr + r.stdout + + +def test_cli_analiza_traduce_simulado_restaura_y_cobertura(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + script = juego / "kubejs" / "server_scripts" / "s.js" + script.parent.mkdir(parents=True) + original = "PlayerEvents.chat(e => { e.player.tell('Welcome to the pack ñ') })\n" + script.write_text(original, encoding="utf-8") + fabrica.jar(juego / "mods" / "m.jar", fabrica.lang("m", "en_us", {"item.m.a": "Iron Plate ñ"})) + datos = tmp_path / "datos" + carpeta = str(juego.parent) + a = _cli("analizar", carpeta, datos=datos) + assert a.returncode == 0 and "Traceback" not in a.stderr and script.read_text("utf-8") == original # solo lee + t = _cli("traducir", carpeta, "--simular", "--no-activar", "--no-idioma", datos=datos) + assert t.returncode == 0, t.stderr + assert "«ES» Welcome to the pack ñ" in script.read_text("utf-8") + assert json.loads((juego / "resourcepacks" / "Traduccion_ES" / "assets" / "m" / "lang" / "es_mx.json").read_bytes()) + c = _cli("cobertura", carpeta, datos=datos) + assert c.returncode == 0 and "Traceback" not in c.stderr + r = _cli("restaurar", carpeta, datos=datos) + assert r.returncode == 0 and script.read_text("utf-8") == original diff --git a/tests/test_verificacion_validacion.py b/tests/test_verificacion_validacion.py new file mode 100644 index 0000000..dc403d5 --- /dev/null +++ b/tests/test_verificacion_validacion.py @@ -0,0 +1,87 @@ +"""Casos límite de las reglas de calidad y de la revisión automática.""" + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from traductor_es.validacion.calidad import revisar_glosario, validar +from traductor_es.validacion.revision import ResumenRevision, interpretar_veredicto + +SUAVE = settings(max_examples=200, suppress_health_check=[HealthCheck.too_slow], deadline=None) + + +@pytest.mark.parametrize( + ("original", "traduccion", "motivo"), + [ + ("Iron Plate", "Placa de hierro", None), + ("Iron Plate", "", "vacía"), + ("Iron Plate", " \t ", "vacía"), + ("One line", "Dos\nlíneas", "salto de línea nuevo"), + ("One\nline", "Una\nlínea", None), + ("A long enough original text here", "x", "largo raro"), + ("Short", "Un texto absurdamente largo para algo tan corto, de verdad que sí lo es", None), + ( + "You can use the tool to break the stone and then place it with your hand", + "You can use the tool to break the stone and then place it with your hand", + "sin traducir", + ), + ("⟦0⟧ and ⟦1⟧", "⟦0⟧ y ⟦1⟧", None), + ("Ñandú ¡Ágüita! cañón", "Ñandú ¡Ágüita! cañón", None), # nombres propios iguales: no es error + ("日本語のテキスト", "日本語のテキスト", None), + ], +) +def test_validar_casos(original: str, traduccion: str, motivo: str | None) -> None: + assert validar(original, traduccion) == motivo + + +@SUAVE +@given(st.text(max_size=200), st.text(max_size=200)) +def test_validar_nunca_falla(original: str, traduccion: str) -> None: + resultado = validar(original, traduccion) + assert resultado is None or isinstance(resultado, str) + + +def test_revisar_glosario_con_regex_y_utf8() -> None: + glosario = {"C++": "C++", "(Test)": "(Prueba)", "Wheat": "Trigo", "Ñu": "Ñu"} + traducciones = { + "Use C++ here": "Usa C++ aquí", + "A (Test) item": "Un objeto", + "Wheat bread": "Pan de cebada", + "Ñu is Ñu": "Ñu es Ñu", + } + r = revisar_glosario(traducciones, glosario, list(glosario)) + assert r.inconsistentes == {"(Test)": 1, "Wheat": 1} + assert "Términos del glosario no respetados" in str(r) + assert str(revisar_glosario({}, glosario, [])) == "Glosario respetado en todas las traducciones." + + +@pytest.mark.parametrize( + ("valor", "esperado"), + [ + ("OK", None), + ("ok, todo bien", None), + ("", None), + ("grave|exactitud|cambia el sentido", ("grave", "exactitud", "cambia el sentido")), + ("Crítica | Terminología | falta el glosario", ("critica", "terminologia", "falta el glosario")), + ("grave|exactitud", None), # falta la explicación + ("enorme|exactitud|x", None), # gravedad desconocida + ("grave|ortografia|x", None), # categoría desconocida + ("grave|estilo|" + "x" * 1000, ("grave", "estilo", "x" * 300)), # se recorta + ("grave|estilo|con | barras | extra", ("grave", "estilo", "con | barras | extra")), + ], +) +def test_veredicto(valor: str, esperado: tuple[str, str, str] | None) -> None: + assert interpretar_veredicto(valor) == esperado + + +@SUAVE +@given(st.text(max_size=100)) +def test_veredicto_nunca_falla(valor: str) -> None: + interpretar_veredicto(valor) + + +def test_resumen_de_revision_suma_y_se_lee() -> None: + a = ResumenRevision(revisados=3, marcados={"exactitud": 1}, leves=1, corregidos=1) + a.sumar(ResumenRevision(revisados=2, marcados={"exactitud": 1, "estilo": 2}, leves=0, corregidos=2)) + assert a.revisados == 5 and a.marcados == {"exactitud": 2, "estilo": 2} and a.corregidos == 3 + assert "4 con errores graves" in str(a) and "ninguno con errores graves" in str(ResumenRevision(revisados=7)) From e86c88348929a156bfaf4b7c4a23370d1df0f827 Mon Sep 17 00:00:00 2001 From: Cristian Date: Fri, 6 Nov 2026 01:49:16 -0500 Subject: [PATCH 04/24] =?UTF-8?q?feat(runtime):=20mod=20de=20apoyo=20para?= =?UTF-8?q?=20el=20texto=20escrito=20en=20el=20c=C3=B3digo=20de=20los=20mo?= =?UTF-8?q?ds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Módulo TextoDeJava: traduce las frases del código Java a una tabla del resource pack. - Mod de cliente ModpackES Runtime (Forge 1.20.1) que cambia chat, barra de acción y tooltips cuyo texto es idéntico al de la tabla; con pruebas JUnit. - Instalar y quitar el mod desde la ventana y con `instalar-runtime`; opción --java. - scripts/construir_mod.py y paso de CI para compilarlo; el .exe lo incluye. --- .github/workflows/ci.yml | 11 + .gitignore | 4 + CHANGELOG.md | 5 + CONTRIBUTING.md | 10 + README.md | 3 + mod-runtime/forge-1.20.1/.gitignore | 7 + mod-runtime/forge-1.20.1/build.gradle | 81 ++++++ mod-runtime/forge-1.20.1/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 62076 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + mod-runtime/forge-1.20.1/gradlew | 245 ++++++++++++++++++ mod-runtime/forge-1.20.1/gradlew.bat | 92 +++++++ mod-runtime/forge-1.20.1/settings.gradle | 13 + .../consterna/modpackes/ModpackESRuntime.java | 118 +++++++++ .../consterna/modpackes/Reescritor.java | 79 ++++++ .../io/github/consterna/modpackes/Tabla.java | 87 +++++++ .../src/main/resources/META-INF/mods.toml | 26 ++ .../src/main/resources/pack.mcmeta | 8 + .../consterna/modpackes/ReescritorTest.java | 76 ++++++ .../github/consterna/modpackes/TablaTest.java | 76 ++++++ scripts/construir_mod.py | 79 ++++++ src/traductor_es/__main__.py | 40 ++- src/traductor_es/modulos/cobertura.py | 32 ++- src/traductor_es/modulos/runtime.py | 65 +++++ src/traductor_es/proceso.py | 6 +- src/traductor_es/salida/mod_apoyo.py | 75 ++++++ src/traductor_es/ui/ventana.py | 51 +++- tests/test_runtime.py | 151 +++++++++++ tests/test_ui.py | 20 ++ 29 files changed, 1463 insertions(+), 21 deletions(-) create mode 100644 mod-runtime/forge-1.20.1/.gitignore create mode 100644 mod-runtime/forge-1.20.1/build.gradle create mode 100644 mod-runtime/forge-1.20.1/gradle.properties create mode 100644 mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/forge-1.20.1/gradlew create mode 100644 mod-runtime/forge-1.20.1/gradlew.bat create mode 100644 mod-runtime/forge-1.20.1/settings.gradle create mode 100644 mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java create mode 100644 mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java create mode 100644 mod-runtime/forge-1.20.1/src/main/resources/META-INF/mods.toml create mode 100644 mod-runtime/forge-1.20.1/src/main/resources/pack.mcmeta create mode 100644 mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java create mode 100644 scripts/construir_mod.py create mode 100644 src/traductor_es/modulos/runtime.py create mode 100644 src/traductor_es/salida/mod_apoyo.py create mode 100644 tests/test_runtime.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ea186a..95c8c9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,17 @@ jobs: - name: Puerta de calidad (ruff, pyright y pytest con el validador Java) run: uv run python scripts/verificar.py + - id: java17 + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + distribution: temurin + java-version: "17" # ForgeGradle 6 (Forge 1.20.1) pide Java 17 + + - name: Compilar el mod de apoyo (ModpackES Runtime) con sus pruebas + run: uv run python scripts/construir_mod.py + env: + JAVA_HOME: ${{ steps.java17.outputs.path }} + - name: Construir ModpackES.exe run: uv run python scripts/construir_exe.py diff --git a/.gitignore b/.gitignore index b142568..a55e258 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ build_exe.log # Prototipo antiguo que quedó en la carpeta del proyecto (no forma parte de la app) /traducir_mods.py /TraductorModpacks/ + +# Herramientas de compilación del mod de apoyo (JDK, Gradle, caché de Forge) y su jar compilado +.herramientas/ +src/traductor_es/recursos/mods/*.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d83f6..a18018f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado +- **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1) las muestra en + español en el chat, la barra de acción y los tooltips. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y + «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. + - **Mensajes de chat y datos de texto**, que antes quedaban en inglés: - El texto de `tellraw`, `title`, `say`, `msg`, `tell` y `w` en las funciones (`.mcfunction`) y en lo que un script de KubeJS manda con `runCommand` o `runCommandSilent`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2a08ce..013353f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,6 +93,16 @@ traduzca un caso real recortado, otra con algo que **no** debe tocarse y la de i originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks públicos de Modrinth a `.descargas/` (Git lo ignora); `uv run pytest -m corpus` los usa para comprobar que lo traducido sigue siendo válido. +## El mod de apoyo (Java) + +`mod-runtime/forge-1.20.1/` es un mod de Forge solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Se compila con Gradle y ForgeGradle 6 y necesita un JDK 17: + +``` +uv run python scripts/construir_mod.py +``` + +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` y una entrada en `SOPORTADOS` de `salida/mod_apoyo.py`. + ## Licencia de las contribuciones ModpackES usa la licencia [GPL-3.0-or-later](LICENSE). Al mandar un cambio aceptas que se publique bajo esa diff --git a/README.md b/README.md index 552bfbe..cdda3ca 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1 por ahora) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -85,6 +86,8 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), no cubre pantallas ni frases con nombres o números en medio, y por ahora existe para Forge 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). + **Más ▸ Qué queda en inglés** (o `modpackes cobertura RUTA [--java]`) lista lo que no se traduce y por qué, sin cambiar nada: frases de scripts cuyo uso no se entiende y texto escrito en el código de los mods, que un resource pack no puede alcanzar. El análisis ya avisa de las frases de scripts. **Más ▸ Restaurar el original** devuelve misiones, scripts y libros propios al texto del modpack. Para quitar diff --git a/mod-runtime/forge-1.20.1/.gitignore b/mod-runtime/forge-1.20.1/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/forge-1.20.1/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/forge-1.20.1/build.gradle b/mod-runtime/forge-1.20.1/build.gradle new file mode 100644 index 0000000..406b1a6 --- /dev/null +++ b/mod-runtime/forge-1.20.1/build.gradle @@ -0,0 +1,81 @@ +plugins { + id 'eclipse' + id 'idea' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-forge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(17) + +minecraft { + mappings channel: mapping_channel, version: mapping_version + + runs { + client { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + mods { + "${mod_id}" { + source sourceSets.main + } + } + } + } +} + +repositories { + mavenCentral() + maven { url = 'https://libraries.minecraft.net/' } +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version: forge_version, forge_version_range: forge_version_range, + loader_version_range: loader_version_range, + mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors: mod_authors, mod_description: mod_description, + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + [project: project] + } +} + +tasks.named('jar', Jar).configure { + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : mod_authors, + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : mod_authors + ]) + } + + finalizedBy 'reobfJar' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/forge-1.20.1/gradle.properties b/mod-runtime/forge-1.20.1/gradle.properties new file mode 100644 index 0000000..0883481 --- /dev/null +++ b/mod-runtime/forge-1.20.1/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.20.1 +minecraft_version_range=[1.20.1,1.20.2) +forge_version=47.4.0 +forge_version_range=[47,48) +loader_version_range=[47,48) +mapping_channel=official +mapping_version=1.20.1 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1962a79e29d3e0ab67b14947c167a862655af9b GIT binary patch literal 62076 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 literal 0 HcmV?d00001 diff --git a/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2617362 --- /dev/null +++ b/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.20.1/gradlew b/mod-runtime/forge-1.20.1/gradlew new file mode 100644 index 0000000..aeb74cb --- /dev/null +++ b/mod-runtime/forge-1.20.1/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/forge-1.20.1/gradlew.bat b/mod-runtime/forge-1.20.1/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/mod-runtime/forge-1.20.1/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/forge-1.20.1/settings.gradle b/mod-runtime/forge-1.20.1/settings.gradle new file mode 100644 index 0000000..93f1b80 --- /dev/null +++ b/mod-runtime/forge-1.20.1/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = 'https://maven.minecraftforge.net/' + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} \ No newline at end of file diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..640e732 --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,118 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES. Si el juego no está en una variante de español, no hace nada. + */ +@Mod(ModpackESRuntime.ID) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(ModpackESRuntime::alRegistrarRecargas); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + } + + private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { + evento.registerReloadListener(new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla", nueva.tamano()); + } + } +} diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java new file mode 100644 index 0000000..577996e --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -0,0 +1,79 @@ +package io.github.consterna.modpackes; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.LiteralContents; +import net.minecraft.network.chat.contents.TranslatableContents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni + * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + */ +public final class Reescritor { + private Reescritor() { + } + + /** El componente traducido, o {@code null} si no cambió nada (así no se copia lo que no hace falta). */ + public static Component reescribir(Component original, Tabla tabla) { + if (original == null || tabla.vacia()) { + return null; + } + ComponentContents contenido = original.getContents(); + ComponentContents nuevoContenido = contenido; + boolean cambio = false; + + if (contenido instanceof LiteralContents literal) { + String traducido = tabla.traducir(literal.text()); + if (traducido != null) { + nuevoContenido = new LiteralContents(traducido); + cambio = true; + } + } else if (contenido instanceof TranslatableContents traducible) { + Object[] argumentos = traducible.getArgs(); + Object[] nuevos = null; + for (int i = 0; i < argumentos.length; i++) { + Object reemplazo = null; + if (argumentos[i] instanceof Component hijo) { + reemplazo = reescribir(hijo, tabla); + } else if (argumentos[i] instanceof String texto) { + reemplazo = tabla.traducir(texto); + } + if (reemplazo != null) { + if (nuevos == null) { + nuevos = argumentos.clone(); + } + nuevos[i] = reemplazo; + } + } + if (nuevos != null) { + nuevoContenido = new TranslatableContents(traducible.getKey(), traducible.getFallback(), nuevos); + cambio = true; + } + } + + List hermanos = original.getSiblings(); + List nuevosHermanos = null; + for (int i = 0; i < hermanos.size(); i++) { + Component reemplazo = reescribir(hermanos.get(i), tabla); + if (reemplazo != null) { + if (nuevosHermanos == null) { + nuevosHermanos = new ArrayList<>(hermanos); + } + nuevosHermanos.set(i, reemplazo); + } + } + + if (!cambio && nuevosHermanos == null) { + return null; + } + MutableComponent resultado = MutableComponent.create(nuevoContenido).setStyle(original.getStyle()); + for (Component hermano : nuevosHermanos != null ? nuevosHermanos : hermanos) { + resultado.append(hermano); + } + return resultado; + } +} diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java new file mode 100644 index 0000000..7f8bc02 --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java @@ -0,0 +1,87 @@ +package io.github.consterna.modpackes; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +/** + * Tabla de textos «inglés exacto → español» que genera ModpackES. Solo hay coincidencia si el texto es idéntico + * (salvo los espacios de los bordes, que se conservan): no hay traducción aproximada ni en vivo. + */ +public final class Tabla { + public static final Tabla VACIA = new Tabla(Map.of()); + + private final Map textos; + + public Tabla(Map textos) { + this.textos = Map.copyOf(textos); + } + + /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ + public static Tabla leer(Reader lector) throws IOException { + JsonElement raiz; + try { + raiz = JsonParser.parseReader(lector); + } catch (JsonParseException e) { + throw new IOException("La tabla de textos no es JSON válido", e); + } + if (!raiz.isJsonObject()) { + throw new IOException("La tabla de textos no es un objeto JSON"); + } + JsonObject objeto = raiz.getAsJsonObject(); + Map mapa = new HashMap<>(); + for (Map.Entry e : objeto.entrySet()) { + JsonElement v = e.getValue(); + if (v.isJsonPrimitive() && v.getAsJsonPrimitive().isString() && !v.getAsString().isEmpty()) { + mapa.put(e.getKey(), v.getAsString()); + } + } + return new Tabla(mapa); + } + + /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ + public Tabla combinar(Tabla otra) { + if (otra.textos.isEmpty()) { + return this; + } + Map mapa = new HashMap<>(textos); + mapa.putAll(otra.textos); + return new Tabla(mapa); + } + + public boolean vacia() { + return textos.isEmpty(); + } + + public int tamano() { + return textos.size(); + } + + /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ + public String traducir(String texto) { + if (texto == null || texto.isEmpty() || textos.isEmpty()) { + return null; + } + String directo = textos.get(texto); + if (directo != null) { + return directo.equals(texto) ? null : directo; + } + String nucleo = texto.strip(); + if (nucleo.isEmpty() || nucleo.length() == texto.length()) { + return null; + } + String traduccion = textos.get(nucleo); + if (traduccion == null || traduccion.equals(nucleo)) { + return null; + } + int inicio = texto.length() - texto.stripLeading().length(); + int fin = texto.length() - texto.stripTrailing().length(); + return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); + } +} diff --git a/mod-runtime/forge-1.20.1/src/main/resources/META-INF/mods.toml b/mod-runtime/forge-1.20.1/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..94577b0 --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +# Solo cliente: Forge no lo carga en servidores dedicados y un servidor sin el mod no marca error. +clientSideOnly=true + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] + modId="forge" + mandatory=true + versionRange="${forge_version_range}" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/forge-1.20.1/src/main/resources/pack.mcmeta b/mod-runtime/forge-1.20.1/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..e22de93 --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/main/resources/pack.mcmeta @@ -0,0 +1,8 @@ +{ + "pack": { + "description": { + "text": "${mod_name}" + }, + "pack_format": 15 + } +} diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..3e93f7d --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,76 @@ +package io.github.consterna.modpackes; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.contents.TranslatableContents; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static final Tabla TABLA = new Tabla(Map.of( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí")); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(ChatFormatting.RED).withBold(true); + Component original = Component.literal("TPA request expired!").setStyle(estilo); + Component nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(Component.literal("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(Component.literal("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + MutableComponent raiz = Component.literal(" ").withStyle(ChatFormatting.AQUA); + raiz.append(Component.literal("Hello").withStyle(ChatFormatting.WHITE)); + raiz.append(Component.literal(" Steve")); + Component nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com")); + Component nuevo = Reescritor.reescribir(Component.literal("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals("https://example.com", nuevo.getStyle().getClickEvent().getValue()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + Component original = Component.translatable("chat.type.text", Component.literal("Hello"), "Hello"); + Component nuevo = Reescritor.reescribir(original, TABLA); + TranslatableContents contenido = (TranslatableContents) nuevo.getContents(); + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((Component) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = new Tabla(Map.of("Ñandú says hi", "Ñandú dice ¡hola! 😀")); + Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } +} diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java new file mode 100644 index 0000000..ab1b8c3 --- /dev/null +++ b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java @@ -0,0 +1,76 @@ +package io.github.consterna.modpackes; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TablaTest { + private static Tabla tabla(String... pares) { + Map mapa = new java.util.HashMap<>(); + for (int i = 0; i < pares.length; i += 2) { + mapa.put(pares[i], pares[i + 1]); + } + return new Tabla(mapa); + } + + @Test + void coincidenciaExactaYSinCoincidencia() { + Tabla t = tabla("TPA request expired!", "¡La solicitud de TPA expiró!"); + assertEquals("¡La solicitud de TPA expiró!", t.traducir("TPA request expired!")); + assertNull(t.traducir("tpa request expired!")); // mayúsculas distintas: no es el mismo texto + assertNull(t.traducir("TPA request expired! Now")); + assertNull(t.traducir("")); + assertNull(t.traducir(null)); + } + + @Test + void conservaLosEspaciosDeLosBordes() { + Tabla t = tabla("Hello there", "Hola"); + assertEquals(" Hola\n", t.traducir(" Hello there\n")); + assertNull(t.traducir(" ")); + } + + @Test + void unTextoIgualQueSuTraduccionNoCuentaComoCambio() { + Tabla t = tabla("Creeper", "Creeper"); + assertNull(t.traducir("Creeper")); + } + + @Test + void conservaTildesEneYEmoji() { + Tabla t = tabla("Pick 😀", "Elige ñandú ¡sí! 日本語 §a%s"); + assertEquals("Elige ñandú ¡sí! 日本語 §a%s", t.traducir("Pick 😀")); + } + + @Test + void leeJsonValidoEIgnoraLoQueNoEsTexto() throws IOException { + Tabla t = Tabla.leer(new StringReader("{\"a b\": \"uno\", \"n\": 5, \"v\": \"\", \"l\": [\"x\"], \"z\": null}")); + assertEquals(1, t.tamano()); + assertEquals("uno", t.traducir("a b")); + } + + @Test + void jsonRotoOQueNoEsObjetoFallaConIOException() { + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("{no es json"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("[1, 2]"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("\"texto\""))); + } + + @Test + void combinarDejaMandarALaSegunda() { + Tabla total = tabla("a b", "viejo", "c d", "queda").combinar(tabla("a b", "nuevo")); + assertEquals("nuevo", total.traducir("a b")); + assertEquals("queda", total.traducir("c d")); + assertTrue(Tabla.VACIA.vacia()); + assertFalse(total.vacia()); + assertEquals(total, total.combinar(Tabla.VACIA)); + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py new file mode 100644 index 0000000..33b7a7b --- /dev/null +++ b/scripts/construir_mod.py @@ -0,0 +1,79 @@ +"""Compila el mod de apoyo (ModpackES Runtime) y deja el jar donde la aplicación lo busca. + +Uso: uv run python scripts/construir_mod.py [--sin-pruebas] + +Necesita un JDK 17 (la variable JAVA_HOME, o la carpeta `.herramientas/jdk-17*` si existe) y acceso a internet la +primera vez, para bajar Gradle, Forge y Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git +ignora. +En GitHub Actions el jar lo construye el mismo comando (`.github/workflows/release.yml`). +""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +RAIZ = Path(__file__).resolve().parent.parent +PROYECTO = RAIZ / "mod-runtime" / "forge-1.20.1" +DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" + + +def _herramientas() -> Path: + """`.herramientas` de este repositorio o, si se trabaja en un worktree, la del repositorio principal.""" + for carpeta in (RAIZ, *RAIZ.parents): + if (carpeta / ".herramientas").is_dir(): + return carpeta / ".herramientas" + return RAIZ / ".herramientas" + + +HERRAMIENTAS = _herramientas() + + +def _jdk17() -> Path: + candidatos = [Path(os.environ["JAVA_HOME"])] if os.environ.get("JAVA_HOME") else [] + candidatos += sorted(HERRAMIENTAS.glob("jdk-17*")) + for c in candidatos: + if (c / "bin" / ("java.exe" if os.name == "nt" else "java")).is_file(): + return c + raise SystemExit("No encuentro un JDK 17: define JAVA_HOME o ponlo en .herramientas/jdk-17*.") + + +def _gradle() -> list[str]: + envoltorio = PROYECTO / ("gradlew.bat" if os.name == "nt" else "gradlew") + local = sorted(HERRAMIENTAS.glob("gradle-*/bin/gradle" + (".bat" if os.name == "nt" else ""))) + return [str(local[-1])] if local else [str(envoltorio)] + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--sin-pruebas", action="store_true", help="compila sin correr las pruebas de Java") + args = ap.parse_args() + jdk = _jdk17() + entorno = {**os.environ, "JAVA_HOME": str(jdk), "PATH": str(jdk / "bin") + os.pathsep + os.environ["PATH"]} + if HERRAMIENTAS.is_dir(): # la caché de Gradle y de Forge queda junto al proyecto y no en la carpeta del usuario + entorno.setdefault("GRADLE_USER_HOME", str(HERRAMIENTAS / "gradle-home")) + orden = [*_gradle(), "build", "--no-daemon", "--console=plain"] + if args.sin_pruebas: + orden += ["-x", "test"] + codigo = subprocess.run(orden, cwd=PROYECTO, env=entorno, check=False).returncode + if codigo: + return codigo + jars = sorted((PROYECTO / "build" / "libs").glob("modpackes_runtime-*.jar")) + if not jars: + print("La compilación terminó pero no hay jar en build/libs.", file=sys.stderr) + return 1 + DESTINO.mkdir(parents=True, exist_ok=True) + for viejo in DESTINO.glob("modpackes_runtime-*.jar"): + viejo.unlink() + final = DESTINO / jars[-1].name + shutil.copyfile(jars[-1], final) + print(f"Mod de apoyo listo: {final}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/traductor_es/__main__.py b/src/traductor_es/__main__.py index 73cc50b..3f53e3b 100644 --- a/src/traductor_es/__main__.py +++ b/src/traductor_es/__main__.py @@ -9,6 +9,7 @@ from typing import Any, cast from . import NOMBRE, __version__, configuracion +from .fuentes.instancia import Instancia from .fuentes.lanzadores import buscar from .motores import fabrica from .motores.base import Cancelado, ErrorFatal, Motor @@ -17,6 +18,7 @@ from .nucleo.dialectos import DIALECTOS from .proceso import GRUPOS, Informe, Proceso from .registro import LOGGER, configurar +from .salida import mod_apoyo def _escribir(texto: str) -> None: @@ -77,6 +79,11 @@ def comunes(p: argparse.ArgumentParser) -> None: p.add_argument("--lotes", action="store_true", help="usar la Batch API de Gemini (mitad de precio, tarda más)") p.add_argument("--contexto", default="", help="ambientación del modpack para el tono y los términos") p.add_argument("--revisar", action="store_true", help="revisión automática de misiones y libros (cuesta más)") + p.add_argument( + "--java", + action="store_true", + help="también el texto escrito en el código de los mods (lo muestra el mod de apoyo; ver instalar-runtime)", + ) comunes(sub.add_parser("analizar", help="cuenta lo que falta traducir y estima el costo (no escribe nada)")) traducir = sub.add_parser("traducir", help="traduce y escribe el resultado") @@ -88,6 +95,11 @@ def comunes(p: argparse.ArgumentParser) -> None: cob = sub.add_parser("cobertura", help="dice qué texto queda en inglés y por qué (solo lee, no escribe nada)") cob.add_argument("carpeta", help="carpeta de la instancia (o su carpeta minecraft)") cob.add_argument("--java", action="store_true", help="también el texto escrito en el código de los mods (tarda)") + rt = sub.add_parser( + "instalar-runtime", help="pone o quita el mod de apoyo que muestra el texto del código de los mods" + ) + rt.add_argument("carpeta", help="carpeta de la instancia (o su carpeta minecraft)") + rt.add_argument("--quitar", action="store_true", help="quita el mod de apoyo de mods/") restaurar = sub.add_parser("restaurar", help="devuelve misiones, scripts y libros propios a su original") restaurar.add_argument("carpeta") modelos = sub.add_parser("modelos", help="lista los modelos que ofrece el motor") @@ -109,6 +121,7 @@ def _traducir_o_analizar(args: argparse.Namespace) -> None: with Proceso( args.carpeta, motor, args.dialecto, grupos, registro=_escribir, hilos=getattr(args, "hilos", 4), usar_lotes=args.lotes, contexto=args.contexto, revisar=args.revisar, + texto_java=args.java, ) as p: # fmt: skip _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") if args.orden == "analizar": @@ -118,6 +131,24 @@ def _traducir_o_analizar(args: argparse.Namespace) -> None: _mostrar(informe, args) +def _instalar_runtime(args: argparse.Namespace) -> None: + with Instancia(args.carpeta) as inst: + if not args.quitar: + _escribir(f"Instalado: {mod_apoyo.instalar(inst)}") + return + quitados = mod_apoyo.quitar(inst) + _escribir("Quité: " + ", ".join(p.name for p in quitados) if quitados else "No había mod de apoyo instalado.") + + +def _cobertura(args: argparse.Namespace) -> None: + with Proceso(args.carpeta, Simulador(), registro=_escribir) as p: + _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") + hallazgos = p.que_queda(java=args.java) + for h in hallazgos: + _escribir(f"[{h.categoria}] {h}") + _escribir(f"{len(hallazgos)} hallazgos." if hallazgos else "No encontré texto que quede en inglés.") + + def main(argv: Sequence[str] | None = None) -> int: cast(Any, sys.stdout).reconfigure(encoding="utf-8", errors="replace") args = _argumentos().parse_args(argv) @@ -127,13 +158,10 @@ def main(argv: Sequence[str] | None = None) -> int: if args.orden == "instancias": for e in buscar(): _escribir(f"{e.lanzador}: {e.nombre}\n {e.juego}") + elif args.orden == "instalar-runtime": + _instalar_runtime(args) elif args.orden == "cobertura": - with Proceso(args.carpeta, Simulador(), registro=_escribir) as p: - _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") - hallazgos = p.que_queda(java=args.java) - for h in hallazgos: - _escribir(f"[{h.categoria}] {h}") - _escribir(f"{len(hallazgos)} hallazgos." if hallazgos else "No encontré texto que quede en inglés.") + _cobertura(args) elif args.orden == "clave": nombre = fabrica.MOTORES[args.motor] if args.accion == "guardar": diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index 57826fc..cb0e28e 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -24,7 +24,8 @@ #: `method_43470` (Fabric) y los `TextComponent`, `StringTextComponent` y `TextComponentString` anteriores. _MARCAS_TEXTO = ("literal", "m_237113_", "method_43470") _CLASES_TEXTO = ("TextComponent", "StringTextComponent", "TextComponentString") -_FRASE = re.compile(r"[A-Z][A-Za-z']+(?: [A-Za-z0-9,.'!?:%\-]+){2,}[.!?:]?") +#: Una frase en inglés: puede empezar con códigos de color (§c) y llevar %s, números y puntuación. +_FRASE = re.compile(r"(?:§[0-9a-fk-or])*[A-Z][A-Za-z']+(?: [§A-Za-z0-9,.'!?:%\-]+){2,}[.!?:]?") _ESPANOL = re.compile(r"\b(de|la|el|los|las|con|sin|para|por|una|un|del)\b") #: Frases de registros y errores internos: casi nunca llegan al jugador. _INTERNO = re.compile( @@ -112,9 +113,11 @@ def _frases_de_clase(datos: bytes) -> list[str]: ] # fmt: skip -def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = lambda _a, _b: None) -> list[Hallazgo]: - """Frases en inglés escritas en el código de los mods. Un resource pack no puede traducirlas.""" - res: list[Hallazgo] = [] +def frases_por_mod( + instancia: Instancia, progreso: Callable[[int, int], None] = lambda _a, _b: None +) -> list[tuple[Path, list[str]]]: + """Por cada mod, las frases en inglés escritas en su código y mostradas como texto (sin repetir).""" + res: list[tuple[Path, list[str]]] = [] jars = instancia.jars() for i, jar in enumerate(jars): progreso(i, len(jars)) @@ -132,11 +135,20 @@ def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = continue unicas = list(dict.fromkeys(f for f in frases if es_traducible(f) and not parece_clave(f))) if unicas: - res.append(Hallazgo( - "java", jar.name, len(unicas), - "Escrito en el código del mod: un resource pack no lo traduce", - tuple(f[:60] for f in unicas[:MAX_EJEMPLOS]), - )) # fmt: skip + res.append((jar, unicas)) + return res + + +def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = lambda _a, _b: None) -> list[Hallazgo]: + """Frases en inglés escritas en el código de los mods. Un resource pack no puede traducirlas.""" + res = [ + Hallazgo( + "java", jar.name, len(unicas), + "Escrito en el código del mod: un resource pack no lo traduce", + tuple(f[:60] for f in unicas[:MAX_EJEMPLOS]), + ) + for jar, unicas in frases_por_mod(instancia, progreso) + ] # fmt: skip return sorted(res, key=lambda h: -h.cantidad) @@ -150,4 +162,4 @@ def revisar_cobertura( return hallazgos -__all__ = ["Hallazgo", "revisar_cobertura"] +__all__ = ["Hallazgo", "frases_por_mod", "revisar_cobertura"] diff --git a/src/traductor_es/modulos/runtime.py b/src/traductor_es/modulos/runtime.py new file mode 100644 index 0000000..f0840d2 --- /dev/null +++ b/src/traductor_es/modulos/runtime.py @@ -0,0 +1,65 @@ +"""Texto escrito en el código Java de los mods: se traduce a una tabla que lee el mod de apoyo (ModpackES Runtime). + +Un resource pack no puede cambiar un texto que el mod tiene escrito en su código. El mod de apoyo, en el +cliente, cambia un mensaje de chat, de la barra de acción o un tooltip solo si su texto es **exactamente** uno de +la tabla `assets/modpackes_runtime/textos/.json` que se genera aquí (inglés → español). +""" + +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path + +from ..formatos import lang_json +from ..nucleo.mascara import es_traducible +from .base import Contexto, Pieza, Resumen, contar +from .cobertura import frases_por_mod + +AMBITO = "ui" +RUTA_TABLA = "assets/modpackes_runtime/textos/{variante}.json" + + +class TextoDeJava: + nombre = "Texto del código de los mods" + ambito = AMBITO + + def __init__(self, ctx: Contexto) -> None: + self.ctx = ctx + self._frases: list[tuple[Path, list[str]]] = [] + + def detectar(self) -> bool: + return bool(self.ctx.instancia.jars()) + + def describir(self) -> str: + total = sum(len(f) for _, f in self._frases) + return ( + f"{contar(total, 'frase')} en {contar(len(self._frases), 'mod')}" + if self._frases + else "frases del código de los mods" + ) + + def recolectar(self) -> list[Pieza]: + self._frases = frases_por_mod(self.ctx.instancia) + return [Pieza(f, f"texto del código de {jar.name}") for jar, frases in self._frases for f in frases] + + def escribir(self, traducciones: Mapping[str, str]) -> Resumen: + resumen = Resumen() + tabla: dict[str, str] = {} + for _, frases in self._frases: + for frase in frases: + traducida = traducciones.get(frase) + if traducida is not None and traducida != frase and es_traducible(frase): + tabla[frase] = traducida + else: + resumen.pendientes += 1 + resumen.traducidos = len(tabla) + if not tabla: + return resumen + pack = self.ctx.pack_listo() + datos = lang_json.escribir(dict(sorted(tabla.items()))) + for variante in self.ctx.variantes: # el mod lee la variante que use el jugador + resumen.archivos.append(pack.escribir(RUTA_TABLA.format(variante=variante), datos)) + return resumen + + def restaurar(self) -> list[Path]: + return [] # la tabla va en el resource pack: basta con desactivarlo diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index fd85f4d..7268e5e 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -12,7 +12,7 @@ from . import configuracion from .fuentes.instancia import Instancia -from .modulos import datos, libros, misiones, mods, scripts +from .modulos import datos, libros, misiones, mods, runtime, scripts from .modulos.base import Contexto, Modulo, Resumen from .modulos.cobertura import Hallazgo, hallazgos_de_scripts, revisar_cobertura from .modulos.mods import es_nombre @@ -114,9 +114,11 @@ def __init__( usar_lotes: bool = False, contexto: str = "", revisar: bool = False, + texto_java: bool = False, ) -> None: self.instancia = Instancia(carpeta) self.revisar = revisar + self.texto_java = texto_java self.registro = registro self.grupos = [g for g in GRUPOS if g in grupos] self._memoria_propia = memoria is None @@ -158,6 +160,8 @@ def modulos(self) -> list[tuple[str, Modulo]]: lista: list[tuple[str, Modulo]] = [] if "mods" in self.grupos: lista.append(("mods", mods.Mods(self.ctx))) + if self.texto_java: # texto escrito en el código de los mods: lo muestra el mod de apoyo + lista.append(("mods", runtime.TextoDeJava(self.ctx))) if "libros" in self.grupos: lista.append(("libros", libros.Libros(self.ctx))) if "misiones" in self.grupos: diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py new file mode 100644 index 0000000..afd9ae0 --- /dev/null +++ b/src/traductor_es/salida/mod_apoyo.py @@ -0,0 +1,75 @@ +"""El mod de apoyo (ModpackES Runtime): qué versión le toca a una instancia y cómo ponerlo o quitarlo de `mods/`. + +El jar se construye aparte (`mod-runtime/`) y viaja dentro de la aplicación, en `traductor_es/recursos/mods/`. +La app solo lo copia cuando el jugador lo pide y solo lo quita si es uno de los suyos. +""" + +from __future__ import annotations + +import re +import shutil +from importlib import resources +from pathlib import Path + +from ..fuentes.instancia import Instancia + +PREFIJO = "modpackes_runtime-" +#: (loader, versión de Minecraft) para los que hay mod de apoyo. Cada entrada es un proyecto en `mod-runtime/`. +SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = {("forge", (1, 20, 1)): "forge-1.20.1"} +_NOMBRE = re.compile(rf"{re.escape(PREFIJO)}(?P[a-z]+-[\d.]+)-(?P[\w.+-]+)\.jar") + + +def _carpeta_de_jars() -> Path: + return Path(str(resources.files("traductor_es") / "recursos" / "mods")) + + +def destino_de(instancia: Instancia) -> str | None: + """«forge-1.20.1» si hay mod de apoyo para esta instancia; None si no.""" + return SOPORTADOS.get((instancia.loader or "", instancia.version.partes)) + + +def jar_empaquetado(instancia: Instancia, carpeta: Path | None = None) -> Path | None: + """El jar más nuevo del mod de apoyo para esta instancia, si viene con la aplicación.""" + destino = destino_de(instancia) + carpeta = carpeta or _carpeta_de_jars() + if destino is None or not carpeta.is_dir(): + return None + candidatos = [p for p in carpeta.glob(f"{PREFIJO}{destino}-*.jar") if p.is_file()] + return max(candidatos, key=lambda p: p.name, default=None) + + +def instalados(instancia: Instancia) -> list[Path]: + """Los jars de ModpackES Runtime que ya están en `mods/` (de cualquier versión).""" + carpeta = instancia.carpeta_mods + return sorted(p for p in carpeta.glob(f"{PREFIJO}*.jar") if _NOMBRE.fullmatch(p.name)) if carpeta.is_dir() else [] + + +def instalar(instancia: Instancia, carpeta_jars: Path | None = None) -> Path: + """Copia el mod de apoyo a `mods/` y quita sus versiones anteriores. ValueError si no hay uno.""" + destino = destino_de(instancia) + if destino is None: + loader = instancia.loader or "sin loader" + raise ValueError( + f"Todavía no hay mod de apoyo para Minecraft {instancia.version} con {loader}. " + "El texto escrito en el código de los mods seguirá en inglés." + ) + origen = jar_empaquetado(instancia, carpeta_jars) + if origen is None: + raise ValueError( + f"Esta copia de la aplicación no trae el mod de apoyo ({destino}). Descarga la versión completa." + ) + instancia.carpeta_mods.mkdir(parents=True, exist_ok=True) + final = instancia.carpeta_mods / origen.name + for viejo in instalados(instancia): + if viejo != final: + viejo.unlink() + shutil.copyfile(origen, final) + return final + + +def quitar(instancia: Instancia) -> list[Path]: + """Quita de `mods/` los jars del mod de apoyo. No toca ningún otro mod.""" + quitados = instalados(instancia) + for ruta in quitados: + ruta.unlink() + return quitados diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index ac048c9..7c6659e 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -47,7 +47,7 @@ from .. import AUTOR, NOMBRE, REPOSITORIO, __version__, actualizaciones, configuracion, registro from ..actualizaciones import Novedad -from ..fuentes.instancia import resolver_carpeta_juego +from ..fuentes.instancia import Instancia, resolver_carpeta_juego from ..fuentes.lanzadores import InstanciaEncontrada, buscar from ..modulos.cobertura import Hallazgo from ..motores import fabrica @@ -57,7 +57,7 @@ from ..motores.simulador import Simulador from ..nucleo.traductor import LARGO_CONTEXTO from ..proceso import GRUPOS, Informe, Proceso -from ..salida import exportar +from ..salida import exportar, mod_apoyo from . import correcciones, iconos, tema from .bienvenida import TarjetaBienvenida from .componentes import ( @@ -160,6 +160,7 @@ def ejecutar(self) -> None: usar_lotes=self.opciones["lotes"], contexto=self.opciones["contexto"], revisar=self.opciones["revisar"], + texto_java=self.opciones["texto_java"], ) as p: self.registro.emit(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'}") if self.accion == "analizar": @@ -263,7 +264,7 @@ def _construir(self) -> None: self.c_motor.setCurrentIndex(max(0, self.c_motor.findData(self._preferencias.get("motor", fabrica.GEMINI)))) self.c_motor.currentIndexChanged.connect(self._al_cambiar_motor) self._al_cambiar_motor() - for senal in (self.c_lotes.toggled, self.c_revisar.toggled, self.c_activar.toggled): + for senal in (self.c_lotes.toggled, self.c_revisar.toggled, self.c_activar.toggled, self.c_java.toggled): senal.connect(self._al_cambiar_opciones) self.c_hilos.valueChanged.connect(self._al_cambiar_opciones) self.c_dialecto.currentIndexChanged.connect(self._al_cambiar_opciones) @@ -454,6 +455,14 @@ def _opciones_avanzadas(self) -> QWidget: ) self.c_revisar.setChecked(bool(self._preferencias.get("revisar", False))) formulario.addRow("", self.c_revisar) + self.c_java = QCheckBox("Incluir texto escrito en el código de los mods (necesita el mod de apoyo)") + self.c_java.setToolTip( + "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " + "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " + "Solo Forge 1.20.1 por ahora." + ) + self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) + formulario.addRow("", self.c_java) self.c_hilos = QSpinBox() self.c_hilos.setRange(1, 16) self.c_hilos.setSuffix(" pedidos a la vez") @@ -503,6 +512,11 @@ def agregar(texto: str, nombre_icono: str, funcion: Callable[[], object]) -> QAc self.a_correcciones.setToolTip("Buscar y corregir a mano traducciones de la memoria") self.a_cobertura = agregar("Qué queda en inglés", "circle-alert", lambda: self.lanzar("cobertura")) self.a_cobertura.setToolTip("Lista el texto que no se puede traducir y por qué (solo lee, no cambia nada)") + self.a_apoyo = agregar("Instalar el mod de apoyo", "package", self._instalar_apoyo) + self.a_apoyo.setToolTip( + "Pone ModpackES Runtime en la carpeta mods: muestra en español el texto del código de los mods" + ) + self.a_quitar_apoyo = agregar("Quitar el mod de apoyo", "undo-2", self._quitar_apoyo) self.a_restaurar = agregar("Restaurar el original…", "undo-2", self._confirmar_restaurar) self.a_exportar = agregar("Exportar pack…", "package", lambda: self._exportar(kit=False)) self.a_kit = agregar("Kit de servidor…", "server", lambda: self._exportar(kit=True)) @@ -838,6 +852,7 @@ def opciones(self) -> dict[str, Any]: "idioma": self.c_activar.isChecked(), "contexto": self.e_contexto.text().strip(), "revisar": self.c_revisar.isChecked(), + "texto_java": self.c_java.isChecked(), } def _confirmar_traducir(self) -> None: @@ -979,6 +994,34 @@ def _terminar(self, resultado: object) -> None: self._al_cambiar_opciones() self.escribir(self.estado.text()) + def _con_instancia(self, accion: Callable[[Instancia], str]) -> None: + carpeta = self.carpeta() + if not carpeta: + self.avisar("Elige primero una instancia.") + return + try: + with Instancia(carpeta) as inst: + mensaje = accion(inst) + except (ValueError, OSError) as e: + self.avisar(str(e), "aviso", anotar=False) + return + self.escribir(mensaje) + self.aviso.mostrar(mensaje, "exito") + + def _instalar_apoyo(self) -> None: + self._con_instancia( + lambda inst: ( + f"Mod de apoyo instalado: {mod_apoyo.instalar(inst).name}. Abre el juego para que empiece a actuar." + ) + ) + + def _quitar_apoyo(self) -> None: + def quitar(inst: Instancia) -> str: + quitados = mod_apoyo.quitar(inst) + return "Quité: " + ", ".join(p.name for p in quitados) if quitados else "No había mod de apoyo instalado." + + self._con_instancia(quitar) + def _mostrar_cobertura(self, hallazgos: list[Hallazgo]) -> None: if not hallazgos: self.estado.setText("No encontré texto que se quede en inglés.") @@ -1114,7 +1157,7 @@ def _guardar_preferencias(self, carpeta: str) -> None: "carpeta": carpeta, "dialecto": o["dialecto"], "grupos": o["grupos"], "motor": o["motor"], "modelo": modelos.get(fabrica.GEMINI, MODELO_POR_DEFECTO), "modelos": modelos, "direcciones": direcciones, "lotes": o["lotes"], "hilos": o["hilos"], "contextos": contextos, - "revisar": o["revisar"], "avisar_version": self.c_avisar.isChecked(), + "revisar": o["revisar"], "texto_java": o["texto_java"], "avisar_version": self.c_avisar.isChecked(), "tema": self._preferencias.get("tema", "sistema"), "bienvenida_vista": bool(self._preferencias.get("bienvenida_vista", False)), } # fmt: skip diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..5e24dda --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,151 @@ +"""Texto del código de los mods: la tabla para el mod de apoyo y su instalación en mods/.""" + +import json +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.__main__ import main +from traductor_es.fuentes import vanilla +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.runtime import RUTA_TABLA, TextoDeJava +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.dialectos import VARIANTES +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.mascara import enmascarar +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.proceso import Proceso +from traductor_es.salida import mod_apoyo +from traductor_es.salida.copias import Respaldo +from traductor_es.salida.resource_pack import PackSalida + +from . import fabrica +from .test_cobertura import _clase # pyright: ignore[reportPrivateUsage] + +FRASES = { + "A": "You can't use chat, you've been muted by an admin!", + "B": "Click §ahere§r to open the menu %s now", + "C": "§cThe market isn't open today, come back later!", +} + + +@pytest.fixture(autouse=True) +def _sin_lanzadores(monkeypatch: pytest.MonkeyPatch) -> None: + def ninguno() -> list[Path]: + return [] + + monkeypatch.setattr(vanilla, "raices_de_lanzadores", ninguno) + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +def _instancia_con_mod(tmp_path: Path, version: str = "1.20.1", loader: str | None = "forge") -> Path: + juego = fabrica.prism(tmp_path, version, loader) + fabrica.jar( + juego / "mods" / "m.jar", + { + "com/m/Chat.class": _clase("literal", *FRASES.values()), + "com/m/Otra.class": _clase("nada", "Another English sentence here"), # no construye texto + }, + ) + return juego + + +def test_tabla_con_las_frases_del_codigo(tmp_path: Path, memoria: Memoria) -> None: + juego = _instancia_con_mod(tmp_path) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst)) + modulo = TextoDeJava(ctx) + resumen = traducir_modulo(modulo, Traductor(Simulador(), memoria, Glosario())) + pack = ctx.pack_listo().raiz + assert resumen.traducidos == 3 and len(resumen.archivos) == len(VARIANTES) + for variante in VARIANTES: + crudo = (pack / RUTA_TABLA.format(variante=variante)).read_bytes() + tabla = json.loads(crudo.decode("utf-8")) + assert set(tabla) == set(FRASES.values()) # la clave es el inglés exacto + for ingles, espanol in tabla.items(): + assert espanol.startswith("«ES» ") + assert sorted(enmascarar(espanol).tokens) == sorted(enmascarar(ingles).tokens) # §a, §r y %s intactos + assert "Another English sentence here" not in json.dumps(tabla) + + +def test_sin_frases_no_crea_nada(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + fabrica.jar(juego / "mods" / "m.jar", fabrica.lang("m", "en_us", {"item.m.a": "Iron Plate"})) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst)) + resumen = traducir_modulo(TextoDeJava(ctx), Traductor(Simulador(), memoria, Glosario())) + assert resumen.archivos == [] and not (juego / "resourcepacks" / "Traduccion_ES" / "assets").exists() + + +def test_proceso_solo_incluye_el_modulo_si_se_pide(tmp_path: Path, memoria: Memoria) -> None: + juego = _instancia_con_mod(tmp_path) + with Proceso(juego, Simulador(), memoria=memoria, grupos=["mods"]) as p: + assert not any(isinstance(m, TextoDeJava) for _, m in p.modulos()) + sin = p.analizar() + with Proceso(juego, Simulador(), memoria=memoria, grupos=["mods"], texto_java=True) as p: + assert any(isinstance(m, TextoDeJava) for _, m in p.modulos()) + con = p.analizar() + p.ejecutar(activar=False, poner_idioma=False) + assert con.a_traducir == sin.a_traducir + 3 + assert (juego / "resourcepacks" / "Traduccion_ES" / RUTA_TABLA.format(variante="es_mx")).is_file() + + +# ------------------------------------------------------------------ mod de apoyo +def _jars(tmp_path: Path, *nombres: str) -> Path: + carpeta = tmp_path / "empaquetados" + carpeta.mkdir() + for n in nombres: + (carpeta / n).write_bytes(b"PK-" + n.encode()) + return carpeta + + +def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: + casos = [ + ("1.20.1", "forge", "forge-1.20.1"), + ("1.20.1", "fabric", None), + ("1.21.1", "neoforge", None), + ("1.12.2", "forge", None), + ] + for i, (version, loader, esperado) in enumerate(casos): + juego = fabrica.prism(tmp_path / str(i), version, loader) + with Instancia(juego) as inst: + assert mod_apoyo.destino_de(inst) == esperado + + +def test_instalar_reemplaza_versiones_y_respeta_otros_mods(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1", "forge") + (juego / "mods" / "otro-mod.jar").write_bytes(b"otro") + (juego / "mods" / "modpackes_runtime-forge-1.20.1-0.0.9.jar").write_bytes(b"viejo") + carpeta = _jars(tmp_path, "modpackes_runtime-forge-1.20.1-0.1.0.jar", "modpackes_runtime-forge-1.20.1-0.0.9.jar") + with Instancia(juego) as inst: + final = mod_apoyo.instalar(inst, carpeta) + assert final.name == "modpackes_runtime-forge-1.20.1-0.1.0.jar" and final.read_bytes().startswith(b"PK-") + assert [p.name for p in mod_apoyo.instalados(inst)] == [final.name] # la versión vieja ya no está + assert (juego / "mods" / "otro-mod.jar").read_bytes() == b"otro" + assert mod_apoyo.quitar(inst) == [final] and mod_apoyo.quitar(inst) == [] + assert (juego / "mods" / "otro-mod.jar").is_file() # nunca toca otro mod + + +def test_instalar_con_errores_claros(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path / "a", "1.20.1", "fabric") + with Instancia(juego) as inst, pytest.raises(ValueError, match="Todavía no hay mod de apoyo"): + mod_apoyo.instalar(inst, tmp_path) + juego2 = fabrica.prism(tmp_path / "b", "1.20.1", "forge") + with Instancia(juego2) as inst, pytest.raises(ValueError, match="no trae el mod de apoyo"): + mod_apoyo.instalar(inst, tmp_path / "no_hay") + + +def test_cli_instalar_runtime_sin_soporte(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + juego = fabrica.prism(tmp_path, "1.21.1", "neoforge") + assert main(["instalar-runtime", str(juego.parent)]) != 0 + assert "Todavía no hay mod de apoyo" in capsys.readouterr().out # los errores van por la salida normal + assert main(["instalar-runtime", str(juego.parent), "--quitar"]) == 0 + assert "No había mod de apoyo" in capsys.readouterr().out diff --git a/tests/test_ui.py b/tests/test_ui.py index f08bbdb..a14182a 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -520,3 +520,23 @@ def test_soltar_rutas_que_no_son_carpetas(qtbot: QtBot, tmp_path: Path) -> None: assert Ventana.carpetas_soltadas(datos) == [] datos.setUrls([QUrl.fromLocalFile(str(tmp_path))]) assert Ventana.carpetas_soltadas(datos) == [str(tmp_path)] + + +def test_opcion_de_texto_java_y_mod_de_apoyo_en_la_ventana( + qtbot: QtBot, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from traductor_es.salida import mod_apoyo + + vacia = tmp_path / "sin_jars" + vacia.mkdir() + monkeypatch.setattr(mod_apoyo, "_carpeta_de_jars", lambda: vacia) # sin importar si esta copia ya compiló el jar + juego = _modpack(tmp_path) + v = _ventana(qtbot, juego) + assert not v.c_java.isChecked() and v.opciones()["texto_java"] is False # apagada por defecto + v.c_java.setChecked(True) + assert v.opciones()["texto_java"] is True + v.a_apoyo.trigger() # Forge 1.20.1 sí tiene mod de apoyo, pero sin el jar empaquetado debe avisar, no romper + assert "no trae el mod de apoyo" in v.aviso.e_texto.text() + assert not list((juego / "mods").glob("modpackes_runtime-*.jar")) + v.a_quitar_apoyo.trigger() + assert "No había mod de apoyo" in v.aviso.e_texto.text() From 391531283aee57a429fd3ecf6766a7181894898d Mon Sep 17 00:00:00 2001 From: Cristian Date: Fri, 6 Nov 2026 02:17:39 -0500 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20m=C3=A9todos=20de=20KubeJS=20por?= =?UTF-8?q?=20versi=C3=B3n,=20formatos=20desconocidos,=20modo=20exhaustivo?= =?UTF-8?q?=20y=20m=C3=A1s=20corpus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Métodos de texto de KubeJS según la versión de Minecraft (formatos/kubejs_api.py). - El informe de cobertura avisa de carpetas con frases cuyo formato no se conoce. - Formato del mercado de Farming for Blockheads. - Modo exhaustivo opcional para los scripts (--exhaustivo y casilla). - Siete modpacks más en el corpus, de 1.16.5 a 26.x con Quilt incluido. --- CHANGELOG.md | 6 ++- README.md | 2 + scripts/descargar_corpus.py | 15 ++++++++ src/traductor_es/__main__.py | 6 +++ src/traductor_es/formatos/kubejs_api.py | 32 ++++++++++++++++ src/traductor_es/modulos/cobertura.py | 49 +++++++++++++++++++++++- src/traductor_es/modulos/datos.py | 21 ++++++++-- src/traductor_es/modulos/scripts.py | 51 ++++++++++++++++--------- src/traductor_es/proceso.py | 7 +++- src/traductor_es/ui/ventana.py | 20 +++++++++- tests/test_cobertura.py | 27 +++++++++++-- tests/test_datos_generales.py | 19 +++++++++ tests/test_kubejs_y_mods.py | 9 +++++ tests/test_verificacion_modulos.py | 23 +++++++++++ 14 files changed, 257 insertions(+), 30 deletions(-) create mode 100644 src/traductor_es/formatos/kubejs_api.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a18018f..1e3a504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,17 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si - Títulos y descripciones de logros, y los consejos de las pantallas de carga (mod Tips, en sus dos formatos). - Mensajes que un script arma por partes (`mensaje += efecto.name; player.tell(mensaje)`), con sus tablas de nombres. - `notify`, `kick`, `openChestGUI` y `openInventoryGUI` de KubeJS (métodos tomados del código de KubeJS 1.20 y 1.21). +- **Modo exhaustivo** para los scripts de KubeJS (opcional, apagado por defecto; `--exhaustivo` y una casilla en «Opciones avanzadas»): traduce también toda frase que parezca texto aunque no se sepa dónde se muestra. Puede tocar lógica; todo queda con copia de seguridad. Nunca toca `console.log` ni comparaciones. +- Los métodos de texto de KubeJS ahora dependen de la versión (`notify`, `openChestGUI`… solo donde existen), comprobados en el código de KubeJS de 1.12 a 26.x (`formatos/kubejs_api.py`). +- Mercado de Farming for Blockheads (`farmingforblockheads_compat`): se traducen los nombres de las pestañas. +- El informe «Qué queda en inglés» también avisa de carpetas de datos con frases en inglés cuyo formato la app no conoce. - Estos datos se buscan por su estructura y no por la carpeta de un modpack: `kubejs/data`, `global_packs`, `datapacks`, `config/openloader`, los datapacks de un mundo y cualquier otra con `data//functions/…`, sueltos o dentro de un `.zip` (se reescribe el zip y se guarda el original). Los componentes de texto valen en JSON y en SNBT (1.21.5 y posteriores). - **Más ▸ Qué queda en inglés** y `modpackes cobertura`: lista lo que no se puede traducir y por qué (frases de scripts cuyo uso no se entiende y texto escrito en el código de los mods, leído de la tabla de constantes de cada clase). Solo lee. El análisis avisa de las frases de scripts que parecen texto. -- Pruebas con modpacks públicos de Modrinth (9 más en `scripts/descargar_corpus.py`) y con la instancia propia: lo traducido +- Pruebas con modpacks públicos de Modrinth (16 más en `scripts/descargar_corpus.py`, de 1.16.5 a 26.x con Forge, NeoForge, Fabric y Quilt) y con la instancia propia: lo traducido sigue siendo válido, repetir no cambia nada y restaurar devuelve los bytes originales. ### Cambiado diff --git a/README.md b/README.md index cdda3ca..9fa3a1c 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ infalibles: sirven para decidir qué rehacer, no como nota de calidad. **Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), no cubre pantallas ni frases con nombres o números en medio, y por ahora existe para Forge 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». + **Más ▸ Qué queda en inglés** (o `modpackes cobertura RUTA [--java]`) lista lo que no se traduce y por qué, sin cambiar nada: frases de scripts cuyo uso no se entiende y texto escrito en el código de los mods, que un resource pack no puede alcanzar. El análisis ya avisa de las frases de scripts. **Más ▸ Restaurar el original** devuelve misiones, scripts y libros propios al texto del modpack. Para quitar diff --git a/scripts/descargar_corpus.py b/scripts/descargar_corpus.py index b353932..7202f1c 100644 --- a/scripts/descargar_corpus.py +++ b/scripts/descargar_corpus.py @@ -45,6 +45,21 @@ "better-mc-forge-bmc4": "https://cdn.modrinth.com/data/4BV47HRn/versions/bdRIjRgs/Better%20MC%20%5BFORGE%5D%201.20.1%201.20.1%20v59.mrpack", # 1.20.1 fabric · kubejs, datapacks .zip "landscapes-reimagined-genesis": "https://cdn.modrinth.com/data/MdznSbKA/versions/pBrfi38A/Landscapes%20Reimagined%20Genesis%203.2.mrpack", + # Más versiones y loaders (1.16.5, 1.18.2, 1.19.2 Quilt, 1.20.1, 1.21.11 y 26.x): SNBT y formatos viejos + # 1.19.2 quilt · kubejs 171 / data 1297 / funciones 9 / logros 130 / zips 0 + "taidumcraft": "https://cdn.modrinth.com/data/qH6Phyax/versions/dmDeTjMb/TaidumCraft.mrpack", + # 26.1.2 fabric · kubejs 0 / data 0 / funciones 5 / logros 176 / zips 0 + "legacies": "https://cdn.modrinth.com/data/nWCaRO9f/versions/x4WpILDH/Legacies%2026-v4.1.mrpack", + # 1.16.5 forge · kubejs 125 / data 5 / funciones 0 / logros 0 / zips 0 + "techblock-skytech-3": "https://cdn.modrinth.com/data/jRXzxVGA/versions/IHN5lWsY/TechBlock%20-%20SkyTech%203%201.2.1.mrpack", + # 1.18.2 forge · kubejs 7 / data 0 / funciones 0 / logros 0 / zips 0 + "adventures-of-berk": "https://cdn.modrinth.com/data/zTK0MLqP/versions/yIog8kuK/Adventures%20of%20Berk%200.0.1.mrpack", + # 1.20.1 fabric · kubejs 3 / data 258 / funciones 0 / logros 123 / zips 0 + "spectral-creation": "https://cdn.modrinth.com/data/eqbP6Ycz/versions/iJhfeFZL/Spectral%20Creation%201.20.1-7.1.1.mrpack", + # 1.18.2 forge · kubejs 0 / data 0 / funciones 0 / logros 0 / zips 16 + "kings-dragons-of-berk": "https://cdn.modrinth.com/data/UmIII5vC/versions/b82038X3/Kings_dragon%201.0.0.mrpack", + # 1.21.11 fabric · kubejs 0 / data 0 / funciones 0 / logros 0 / zips 6 + "better-adventures-plus-plus": "https://cdn.modrinth.com/data/23niDfW7/versions/e18PwvTU/Better%20Adventures%2B%2B%201.3.mrpack", } #: Qué se baja de cada pack (rutas dentro del .mrpack). diff --git a/src/traductor_es/__main__.py b/src/traductor_es/__main__.py index 3f53e3b..ad83d03 100644 --- a/src/traductor_es/__main__.py +++ b/src/traductor_es/__main__.py @@ -79,6 +79,11 @@ def comunes(p: argparse.ArgumentParser) -> None: p.add_argument("--lotes", action="store_true", help="usar la Batch API de Gemini (mitad de precio, tarda más)") p.add_argument("--contexto", default="", help="ambientación del modpack para el tono y los términos") p.add_argument("--revisar", action="store_true", help="revisión automática de misiones y libros (cuesta más)") + p.add_argument( + "--exhaustivo", + action="store_true", + help="en los scripts de KubeJS traduce toda frase, aunque no se sepa dónde se muestra (puede tocar lógica)", + ) p.add_argument( "--java", action="store_true", @@ -122,6 +127,7 @@ def _traducir_o_analizar(args: argparse.Namespace) -> None: args.carpeta, motor, args.dialecto, grupos, registro=_escribir, hilos=getattr(args, "hilos", 4), usar_lotes=args.lotes, contexto=args.contexto, revisar=args.revisar, texto_java=args.java, + scripts_exhaustivo=args.exhaustivo, ) as p: # fmt: skip _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") if args.orden == "analizar": diff --git a/src/traductor_es/formatos/kubejs_api.py b/src/traductor_es/formatos/kubejs_api.py new file mode 100644 index 0000000..3664a8a --- /dev/null +++ b/src/traductor_es/formatos/kubejs_api.py @@ -0,0 +1,32 @@ +"""Métodos de KubeJS que le muestran texto al jugador, según la versión de Minecraft. + +Los verificados salen del código de KubeJS (repositorio KubeJS-Mods/KubeJS, ramas 1202, 1605, 1802, 1902, 2001, +2101 y 2601: `MessageSender(KJS)`, `PlayerJS`/`PlayerKJS`, `ServerPlayerJS`/`ServerPlayerKJS`). El resto son +los de siempre (constructores de objetos, tooltips, títulos) y se aceptan en todas las versiones. +""" + +from __future__ import annotations + +#: método → primera versión de Minecraft (mayor, menor) en la que existe, comprobado en el código de cada rama +VERIFICADOS: dict[str, tuple[int, int]] = { + "tell": (1, 12), # MessageSender.tell(Component) + "setStatusMessage": (1, 12), # MessageSender / PlayerJS.setStatusMessage(Component) + "kick": (1, 12), # ServerPlayerJS.kick(Component) + "notify": (1, 19), # PlayerKJS.notify(título, texto): aparece en la rama 1902 + "openInventoryGUI": (1, 19), # ServerPlayerKJS.openInventoryGUI(…, Component título) + "openChestGUI": (1, 20), # ServerPlayerKJS.openChestGUI(Component título, …): aparece en la rama 2001 +} + +#: Se aceptan en cualquier versión; no se comprobaron una por una contra el código de KubeJS. +GENERALES = frozenset({ + "statusMessage", "displayClientMessage", "sendSystemMessage", "sendMessage", "displayName", "setDisplayName", + "tooltip", "addTooltip", "title", "setTitle", "subtitle", "setSubtitle", "lore", "setLore", "actionbar", + "setActionbar", +}) # fmt: skip + + +def llamadas_de_texto(version: tuple[int, ...] | None) -> frozenset[str]: + """Los métodos de texto que existen en esa versión de Minecraft. Sin versión: todos (es lo más prudente).""" + if version is None: + return GENERALES | VERIFICADOS.keys() + return GENERALES | {m for m, desde in VERIFICADOS.items() if tuple(version[:2]) >= desde} diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index cb0e28e..88bc504 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -17,6 +17,7 @@ from ..fuentes.instancia import Instancia from ..nucleo.mascara import es_traducible from .base import Contexto, parece_clave +from .datos import clasificar, recorrer from .scripts import ScriptsKubeJS MAX_EJEMPLOS = 3 @@ -56,7 +57,7 @@ class Hallazgo: """Una cosa que queda en inglés: dónde, cuántos textos y unos ejemplos.""" - categoria: str #: «script» o «java» + categoria: str #: «script», «formato» o «java» donde: str cantidad: int motivo: str @@ -152,11 +153,57 @@ def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = return sorted(res, key=lambda h: -h.cantidad) +#: Carpetas de un datapack o resource pack que no llevan texto para el jugador (o que ya se traducen por otro lado). +_SIN_TEXTO = frozenset({ + "recipes", "recipe", "tags", "loot_tables", "loot_table", "predicates", "worldgen", "dimension", "dimension_type", + "structures", "structure", "lang", "textures", "models", "blockstates", "sounds", "particles", "shaders", "font", + "atlases", "item_modifiers", "chat_type", "damage_type", "enchantment", "trim_pattern", "trim_material", + "banner_pattern", "jukebox_song", "painting_variant", "patchouli_books", "optifine", "equipment", "items", +}) # fmt: skip +#: Un valor de texto con aspecto de frase en inglés dentro de un JSON. +_TEXTO_JSON = re.compile(r'"(?:text|title|description|message|tip|name|lore)"\s*:\s*"([A-Z][^"\\]{11,})"') +_TAMANO_MAXIMO = 300_000 + + +def formatos_desconocidos(juego: Path) -> list[Hallazgo]: + """Carpetas de datos o recursos con frases en inglés cuyo formato la app no conoce (no se tocan).""" + por_carpeta: dict[str, list[str]] = {} + for ruta in recorrer(juego): + partes = ruta.relative_to(juego).parts + if ruta.suffix != ".json" or clasificar(Path(*partes)) is not None: + continue + ancla = next( + (i for i, parte in enumerate(partes) if parte in ("data", "assets") and len(partes) - i >= 4), None + ) + if ancla is None or partes[ancla + 2] in _SIN_TEXTO: + continue + try: + if ruta.stat().st_size > _TAMANO_MAXIMO: + continue + frases = [ + f + for f in _TEXTO_JSON.findall(ruta.read_text(encoding="utf-8-sig", errors="ignore")) + if es_traducible(f) + ] + except OSError: + continue + if frases: + por_carpeta.setdefault("/".join(partes[: ancla + 3]), []).extend(frases) + return [ + Hallazgo( + "formato", carpeta, len(frases), "Formato que la app no conoce: no se traduce (no se toca ningún archivo)", + tuple(f[:60] for f in frases[:MAX_EJEMPLOS]), + ) + for carpeta, frases in sorted(por_carpeta.items(), key=lambda kv: -len(kv[1])) + ] # fmt: skip + + def revisar_cobertura( ctx: Contexto, java: bool = False, progreso: Callable[[int, int], None] = lambda _a, _b: None ) -> list[Hallazgo]: """Lo que queda en inglés. El análisis del código Java tarda (lee todos los mods): solo si se pide.""" hallazgos = hallazgos_de_scripts(ctx.instancia.juego, ScriptsKubeJS(ctx).candidatas()) + hallazgos += formatos_desconocidos(ctx.instancia.juego) if java: hallazgos += textos_en_java(ctx.instancia, progreso) return hallazgos diff --git a/src/traductor_es/modulos/datos.py b/src/traductor_es/modulos/datos.py index a5fd297..84d1acf 100644 --- a/src/traductor_es/modulos/datos.py +++ b/src/traductor_es/modulos/datos.py @@ -91,6 +91,16 @@ def _nodos_tip(datos: Any) -> list[tuple[dict[str, Any], str]]: return nodos +def _nodos_mercado(datos: Any) -> list[tuple[dict[str, Any], str]]: + """Mercado de Farming for Blockheads: el `name` de cada `customCategories.` (lo que se ve en las pestañas).""" + if not isinstance(datos, dict): + return [] + categorias = cast("dict[str, Any]", datos).get("customCategories") + if not isinstance(categorias, dict): + return [] + return [n for c in cast("dict[str, Any]", categorias).values() for n in _nodos(c, ("name",))] + + def _unidades_json(texto: str, nodos: Callable[[Any], list[tuple[dict[str, Any], str]]]) -> list[str]: return [u for d, k in nodos(json.loads(texto)) for u in _traducibles(componente.unidades(_componente(d[k])))] @@ -128,6 +138,11 @@ def _es(ruta: tuple[str, ...], raiz: str, carpetas: tuple[str, ...], sufijo: str lambda r: _es(r, "data", ("advancement", "advancements"), ".json"), lambda t: _unidades_json(t, _nodos_logro), lambda t, tr: _reescribir_json(t, tr, _nodos_logro), ), + Formato( + "mercado", "pestaña de un mercado", + lambda r: _es(r, "data", ("farmingforblockheads_compat",), ".json"), + lambda t: _unidades_json(t, _nodos_mercado), lambda t, tr: _reescribir_json(t, tr, _nodos_mercado), + ), Formato( "tip", "consejo de las pantallas de carga", lambda r: _es(r, "assets", ("tips",), ".json") or (_es(r, "data", ("tips",), ".json") and r[1] == "tipsmod"), @@ -147,7 +162,7 @@ def clasificar(ruta: Path) -> Formato | None: return None -def _recorrer(juego: Path) -> Iterator[Path]: +def recorrer(juego: Path) -> Iterator[Path]: """Cada archivo de la instancia, sin entrar en mods, cachés ni mundos (de un mundo solo sus datapacks).""" base = len(juego.parts) for raiz, carpetas, archivos in os.walk(juego): @@ -165,7 +180,7 @@ def _recorrer(juego: Path) -> Iterator[Path]: def buscar_archivos(juego: Path) -> list[tuple[Path, Formato]]: """Todos los archivos de texto conocidos que están sueltos en la instancia.""" encontrados: list[tuple[Path, Formato]] = [] - for ruta in _recorrer(juego): + for ruta in recorrer(juego): if (formato := clasificar(ruta.relative_to(juego))) is not None: encontrados.append((ruta, formato)) return encontrados @@ -185,7 +200,7 @@ def _entradas_del_zip(ruta: Path) -> list[tuple[str, Formato]]: def buscar_zips(juego: Path) -> list[tuple[Path, list[tuple[str, Formato]]]]: """Los .zip con texto conocido dentro (datapacks de `global_packs`, `datapacks`, mundos…).""" res: list[tuple[Path, list[tuple[str, Formato]]]] = [] - for ruta in _recorrer(juego): + for ruta in recorrer(juego): if ruta.suffix.lower() == ".zip" and (entradas := _entradas_del_zip(ruta)): res.append((ruta, entradas)) return res diff --git a/src/traductor_es/modulos/scripts.py b/src/traductor_es/modulos/scripts.py index 2e3a14a..85b56a6 100644 --- a/src/traductor_es/modulos/scripts.py +++ b/src/traductor_es/modulos/scripts.py @@ -12,21 +12,16 @@ from pathlib import Path from ..formatos import comandos, componente, flujo_js, javascript +from ..formatos.kubejs_api import llamadas_de_texto from ..nucleo.mascara import es_traducible from .base import Contexto, Pieza, Resumen, contar, parece_clave AMBITO = "script" CARPETAS = ("kubejs/client_scripts", "kubejs/server_scripts", "kubejs/startup_scripts") -#: Llamadas cuyo argumento de texto es siempre algo que ve el jugador. -LLAMADAS_TEXTO = frozenset({ - "tell", "statusMessage", "setStatusMessage", "displayClientMessage", "sendSystemMessage", "sendMessage", - "displayName", "setDisplayName", "tooltip", "addTooltip", "title", "setTitle", "subtitle", "setSubtitle", - "lore", "setLore", "actionbar", "setActionbar", - # Del código de KubeJS (ramas 2001 y 2101, core/PlayerKJS, ServerPlayerKJS y MessageSenderKJS): - # `notify(título, texto)`, `kick(motivo)`, y el título de `openChestGUI` / `openInventoryGUI` - "notify", "kick", "openChestGUI", "openInventoryGUI", -}) # fmt: skip +#: Llamadas cuyo argumento de texto es siempre algo que ve el jugador (de todas las versiones de KubeJS; la lista por +#: versión está en `formatos/kubejs_api.py`). +LLAMADAS_TEXTO = llamadas_de_texto(None) #: Llamadas que ejecutan un comando: si es `tellraw`, `title`, `say`…, el texto del mensaje se traduce. LLAMADAS_COMANDO = frozenset({"runCommand", "runCommandSilent"}) #: `Text.of("…")`, `Text.red("…")`, `Component.literal("…")`… @@ -42,11 +37,16 @@ "onEvent:client.generate_assets", ) # fmt: skip LLAMADAS_EN_EVENTO = frozenset({"add", "addAll", "insert", "addAdvanced", "addItem", "addFluid", "addLang", "addLine"}) -#: Las llamadas de texto (`tell`, `Text.of`…) como expresión regular: sirve para ver qué variables acaban en un mensaje. -_LLAMADAS_RE = ( - rf"\b(?:{'|'.join(sorted(LLAMADAS_TEXTO))})|\b(?:{'|'.join(sorted(RECEPTORES_TEXTO))})" - rf"\.(?:{'|'.join(sorted(AYUDAS_TEXTO))})" -) + + +def _llamadas_re(llamadas: frozenset[str]) -> str: + """Las llamadas de texto (`tell`, `Text.of`…) como regex, para saber qué variables acaban en un mensaje.""" + return ( + rf"\b(?:{'|'.join(sorted(llamadas))})|\b(?:{'|'.join(sorted(RECEPTORES_TEXTO))})" + rf"\.(?:{'|'.join(sorted(AYUDAS_TEXTO))})" + ) + + _EXPRESION = re.compile(r"\$\{[^{}\n]*\}") @@ -61,12 +61,12 @@ def _seguro(c: javascript.CadenaJs, traduccion: str) -> bool: return c.comilla != "`" or traduccion.count("${") == c.texto.count("${") -def es_texto_visible(c: javascript.CadenaJs) -> bool: +def es_texto_visible(c: javascript.CadenaJs, llamadas: frozenset[str] = LLAMADAS_TEXTO) -> bool: if c.comparada or not _parece_texto(c.texto): return False if c.comilla == "`" and c.texto.count("${") != len(_EXPRESION.findall(c.texto)): return False # plantilla con expresiones complejas: no se puede proteger con seguridad - if c.funcion in LLAMADAS_TEXTO: + if c.funcion in llamadas: return True if c.funcion in AYUDAS_TEXTO and c.receptor in RECEPTORES_TEXTO: return True @@ -81,6 +81,7 @@ def parece_frase_sin_elegir(c: javascript.CadenaJs) -> bool: t = c.texto.strip() return ( not c.comparada + and c.receptor != "console" # los registros del script no los ve el jugador and t[:1].isupper() and len(t.split()) >= 3 and _parece_texto(t) @@ -173,8 +174,13 @@ class ScriptsKubeJS: nombre = "Scripts de KubeJS" ambito = AMBITO - def __init__(self, ctx: Contexto) -> None: + def __init__(self, ctx: Contexto, exhaustivo: bool = False) -> None: + """`exhaustivo`: traduce también toda cadena con aspecto de frase, aunque no se sepa dónde se muestra. + + Puede tocar lógica del script (una frase que se compara o se guarda), por eso es opcional. + """ self.ctx = ctx + self.exhaustivo = exhaustivo self._scripts: list[_Script] = [] self.sin_elegir: list[tuple[Path, list[str]]] = [] @@ -210,20 +216,27 @@ def recolectar(self) -> list[Pieza]: self._scripts = [] self.sin_elegir = [] leidos = self._leer() - flujos = [flujo_js.analizar(src, _LLAMADAS_RE) for _, src, _ in leidos] + llamadas = llamadas_de_texto(self.ctx.instancia.version.partes) # las de esta versión de KubeJS + patron = _llamadas_re(llamadas) + flujos = [flujo_js.analizar(src, patron) for _, src, _ in leidos] # Una tabla de nombres se define en un archivo y se usa en otro tablas: frozenset[str] = frozenset(t for f in flujos for t in f.tablas) for (ruta, src, cadenas), flujo in zip(leidos, flujos, strict=True): elegidas: list[javascript.CadenaJs] = [] en_comando: dict[int, comandos.Comando] = {} for c in cadenas: - if es_texto_visible(c): + if es_texto_visible(c, llamadas): elegidas.append(c) elif (cmd := comando_visible(c)) is not None: elegidas.append(c) en_comando[c.inicio] = cmd vistas = {c.inicio for c in elegidas} elegidas += [c for c in textos_armados(src, cadenas, flujo, tablas) if c.inicio not in vistas] + if self.exhaustivo: + vistas = {c.inicio for c in elegidas} + elegidas += [ + c for c in cadenas if c.inicio not in vistas and parece_frase_sin_elegir(c) and _plantilla_segura(c) + ] if elegidas: self._scripts.append(_Script(ruta, src, cadenas, elegidas, en_comando)) vistas = {c.inicio for c in elegidas} diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index 7268e5e..121d463 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -115,10 +115,12 @@ def __init__( contexto: str = "", revisar: bool = False, texto_java: bool = False, + scripts_exhaustivo: bool = False, ) -> None: self.instancia = Instancia(carpeta) self.revisar = revisar self.texto_java = texto_java + self.scripts_exhaustivo = scripts_exhaustivo self.registro = registro self.grupos = [g for g in GRUPOS if g in grupos] self._memoria_propia = memoria is None @@ -167,7 +169,10 @@ def modulos(self) -> list[tuple[str, Modulo]]: if "misiones" in self.grupos: lista += [("misiones", m) for m in misiones.todos(self.ctx)] if "scripts" in self.grupos: - lista += [("scripts", scripts.ScriptsKubeJS(self.ctx)), ("scripts", datos.Datos(self.ctx))] + lista += [ + ("scripts", scripts.ScriptsKubeJS(self.ctx, self.scripts_exhaustivo)), + ("scripts", datos.Datos(self.ctx)), + ] return [(g, m) for g, m in lista if m.detectar()] # ------------------------------------------------------------------ acciones diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 7c6659e..05a6bdd 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -161,6 +161,7 @@ def ejecutar(self) -> None: contexto=self.opciones["contexto"], revisar=self.opciones["revisar"], texto_java=self.opciones["texto_java"], + scripts_exhaustivo=self.opciones["exhaustivo"], ) as p: self.registro.emit(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'}") if self.accion == "analizar": @@ -264,7 +265,13 @@ def _construir(self) -> None: self.c_motor.setCurrentIndex(max(0, self.c_motor.findData(self._preferencias.get("motor", fabrica.GEMINI)))) self.c_motor.currentIndexChanged.connect(self._al_cambiar_motor) self._al_cambiar_motor() - for senal in (self.c_lotes.toggled, self.c_revisar.toggled, self.c_activar.toggled, self.c_java.toggled): + for senal in ( + self.c_lotes.toggled, + self.c_revisar.toggled, + self.c_activar.toggled, + self.c_java.toggled, + self.c_exhaustivo.toggled, + ): senal.connect(self._al_cambiar_opciones) self.c_hilos.valueChanged.connect(self._al_cambiar_opciones) self.c_dialecto.currentIndexChanged.connect(self._al_cambiar_opciones) @@ -463,6 +470,13 @@ def _opciones_avanzadas(self) -> QWidget: ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) + self.c_exhaustivo = QCheckBox("Scripts: traducir toda frase, aunque no se sepa dónde se muestra") + self.c_exhaustivo.setToolTip( + "Puede tocar lógica de los scripts de KubeJS (una frase que se compara o se guarda). Todo queda con copia " + "de seguridad y se puede restaurar. Mira antes «Más ▸ Qué queda en inglés»." + ) + self.c_exhaustivo.setChecked(bool(self._preferencias.get("exhaustivo", False))) + formulario.addRow("", self.c_exhaustivo) self.c_hilos = QSpinBox() self.c_hilos.setRange(1, 16) self.c_hilos.setSuffix(" pedidos a la vez") @@ -853,6 +867,7 @@ def opciones(self) -> dict[str, Any]: "contexto": self.e_contexto.text().strip(), "revisar": self.c_revisar.isChecked(), "texto_java": self.c_java.isChecked(), + "exhaustivo": self.c_exhaustivo.isChecked(), } def _confirmar_traducir(self) -> None: @@ -1157,7 +1172,8 @@ def _guardar_preferencias(self, carpeta: str) -> None: "carpeta": carpeta, "dialecto": o["dialecto"], "grupos": o["grupos"], "motor": o["motor"], "modelo": modelos.get(fabrica.GEMINI, MODELO_POR_DEFECTO), "modelos": modelos, "direcciones": direcciones, "lotes": o["lotes"], "hilos": o["hilos"], "contextos": contextos, - "revisar": o["revisar"], "texto_java": o["texto_java"], "avisar_version": self.c_avisar.isChecked(), + "revisar": o["revisar"], "texto_java": o["texto_java"], "exhaustivo": o["exhaustivo"], + "avisar_version": self.c_avisar.isChecked(), "tema": self._preferencias.get("tema", "sistema"), "bienvenida_vista": bool(self._preferencias.get("bienvenida_vista", False)), } # fmt: skip diff --git a/tests/test_cobertura.py b/tests/test_cobertura.py index dea9955..347f4ef 100644 --- a/tests/test_cobertura.py +++ b/tests/test_cobertura.py @@ -56,8 +56,8 @@ def test_cobertura_de_scripts_y_java(tmp_path: Path) -> None: sin_java = revisar_cobertura(Contexto(inst, Respaldo(juego))) con_java = revisar_cobertura(Contexto(inst, Respaldo(juego)), java=True) assert [(h.categoria, h.donde) for h in sin_java] == [("script", "kubejs/server_scripts/a.js")] - assert sin_java[0].cantidad == 2 # `tell` ya se traduce; recetas y comparaciones no cuentan - assert "This should be a debug message here" in sin_java[0].ejemplos + assert sin_java[0].cantidad == 1 # `tell` ya se traduce; recetas, comparaciones y console.log no cuentan + assert sin_java[0].ejemplos == ("The market is closed today",) de_java = [h for h in con_java if h.categoria == "java"] assert len(de_java) == 1 assert set(de_java[0].ejemplos) == {"You can't use this item here!", "Click the block to open the menu"} @@ -69,5 +69,26 @@ def test_analizar_avisa_sin_detener_el_analisis(tmp_path: Path) -> None: (juego / "kubejs" / "server_scripts" / "a.js").write_text(SCRIPT, encoding="utf-8") with Proceso(juego, Simulador(), grupos=["scripts"]) as p: informe = p.analizar() - assert any("2 frases de 1 scripts" in a for a in informe.avisos) + assert any("1 frases de 1 scripts" in a for a in informe.avisos) assert informe.modulos and informe.modulos[0].grupo == "scripts" + + +def test_formatos_desconocidos_se_informan_sin_tocar(tmp_path: Path) -> None: + import json + + from traductor_es.modulos.cobertura import formatos_desconocidos + + juego = fabrica.prism(tmp_path, "1.20.1") + quiz = juego / "kubejs" / "data" / "a" / "quizzes" / "q1.json" + quiz.parent.mkdir(parents=True) + quiz.write_text(json.dumps({"title": "What do cows eat for breakfast?", "id": "x"}), encoding="utf-8") + conocido = juego / "kubejs" / "data" / "a" / "advancements" / "g.json" + conocido.parent.mkdir(parents=True) + conocido.write_text(json.dumps({"display": {"title": {"text": "A conocido title here"}}}), encoding="utf-8") + receta = juego / "kubejs" / "data" / "a" / "recipes" / "r.json" + receta.parent.mkdir(parents=True) + receta.write_text(json.dumps({"result": {"text": "Not shown to the player, a recipe"}}), encoding="utf-8") + antes = quiz.read_bytes() + hallazgos = formatos_desconocidos(juego) + assert [(h.categoria, h.donde, h.cantidad) for h in hallazgos] == [("formato", "kubejs/data/a/quizzes", 1)] + assert hallazgos[0].ejemplos == ("What do cows eat for breakfast?",) and quiz.read_bytes() == antes diff --git a/tests/test_datos_generales.py b/tests/test_datos_generales.py index bcd7f74..12b29bb 100644 --- a/tests/test_datos_generales.py +++ b/tests/test_datos_generales.py @@ -182,3 +182,22 @@ def test_datapack_comprimido_se_traduce_y_se_restaura(tmp_path: Path, memoria: M assert ruta.read_bytes() == despues # repetir no cambia nada assert Datos(ctx).restaurar() == [ruta] assert ruta.read_bytes() == original + + +def test_mercado_de_farming_for_blockheads(tmp_path: Path, memoria: Memoria) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "data" / "farmingforblockheads" / "farmingforblockheads_compat" / "kits.json" + contenido = { + "customCategories": {"kits": {"name": "Clothes and Decoration Kits", "icon": {"item": "minecraft:stone"}}}, + "customEntries": [{"output": {"item": "x:y", "nbt": {"Name": "Item name stays"}}}], + } + ruta.parent.mkdir(parents=True) + ruta.write_text(json.dumps(contenido), encoding="utf-8") + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + traducir_modulo(Datos(ctx), Traductor(Simulador(), memoria, Glosario())) + nuevo = json.loads(ruta.read_text("utf-8")) + assert nuevo["customCategories"]["kits"]["name"] == "«ES» Clothes and Decoration Kits" + assert nuevo["customEntries"] == contenido["customEntries"] and nuevo["customCategories"]["kits"]["icon"] + Datos(ctx).restaurar() + assert json.loads(ruta.read_text("utf-8")) == contenido diff --git a/tests/test_kubejs_y_mods.py b/tests/test_kubejs_y_mods.py index 90bdbf6..c15c455 100644 --- a/tests/test_kubejs_y_mods.py +++ b/tests/test_kubejs_y_mods.py @@ -196,3 +196,12 @@ def sin_lanzadores() -> list[Path]: with Instancia(juego) as inst: traducir_modulo(Mods(Contexto(inst, Respaldo(juego))), Traductor(Simulador(), memoria, Glosario())) assert not (juego / "resourcepacks" / "Traduccion_ES").exists() + + +def test_metodos_de_texto_de_kubejs_por_version() -> None: + from traductor_es.formatos.kubejs_api import llamadas_de_texto + + assert {"tell", "setStatusMessage", "kick"} <= llamadas_de_texto((1, 12, 2)) + assert "notify" not in llamadas_de_texto((1, 16, 5)) and "notify" in llamadas_de_texto((1, 19, 2)) + assert "openChestGUI" not in llamadas_de_texto((1, 19, 2)) and "openChestGUI" in llamadas_de_texto((1, 20, 1)) + assert "openChestGUI" in llamadas_de_texto((26, 1, 0)) and "openChestGUI" in llamadas_de_texto(None) diff --git a/tests/test_verificacion_modulos.py b/tests/test_verificacion_modulos.py index 7c1d463..9a7b7d2 100644 --- a/tests/test_verificacion_modulos.py +++ b/tests/test_verificacion_modulos.py @@ -274,3 +274,26 @@ def test_namespace_imposible_no_tumba_el_resto(tmp_path: Path, memoria: Memoria, assert json.loads((pack / "assets" / "ok" / "lang" / "es_mx.json").read_text("utf-8")) == {"b": "«ES» Good one"} assert resumen.pendientes >= 1 and any("no se puede guardar" in a for a in avisos) assert not (tmp_path / "Pack" / "minecraft" / "lang").exists() # nada se escribió fuera del pack + + +EXHAUSTIVO = """const aviso = 'The harvest festival starts tomorrow evening' +console.log('This is only a debug line for the log here') +if (nombre == 'Some Name With Spaces') return +function f(e) { e.player.tell('Welcome to the farm') } +""" + + +def test_modo_exhaustivo_traduce_mas_pero_no_los_registros_ni_las_comparaciones( + tmp_path: Path, memoria: Memoria +) -> None: + juego, ruta = _script(tmp_path, EXHAUSTIVO.encode()) + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego)) + normal = traducir_modulo(ScriptsKubeJS(ctx), Traductor(Simulador(), memoria, Glosario())) + assert normal.traducidos == 1 # solo el `tell` + ScriptsKubeJS(ctx).restaurar() + todo = traducir_modulo(ScriptsKubeJS(ctx, exhaustivo=True), Traductor(Simulador(), memoria, Glosario())) + nuevo = ruta.read_text("utf-8") + assert todo.traducidos == 2 and "'«ES» The harvest festival starts tomorrow evening'" in nuevo + assert "'This is only a debug line for the log here'" in nuevo # console.log no es para el jugador + assert "'Some Name With Spaces'" in nuevo # una comparación es lógica From b93c013adba60b619705602de653f416c973d5fc Mon Sep 17 00:00:00 2001 From: Cristian Date: Fri, 6 Nov 2026 02:48:50 -0500 Subject: [PATCH 06/24] feat(scripts): traducir lo que se pasa a funciones propias que solo muestran texto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Analiza las funciones del script cuyo parámetro solo se muestra (directo, partido en líneas o dentro de una plantilla que se muestra) y traduce lo que se les pasa en esa posición. - Conservador: si el parámetro se compara, busca, guarda, devuelve o va en un id o comando, o la función se usa como valor o está repetida, no se toca nada. - Las cadenas del escáner de JavaScript guardan la posición de su argumento. --- CHANGELOG.md | 1 + src/traductor_es/formatos/flujo_js.py | 153 +++++++++++++++++++++++- src/traductor_es/formatos/javascript.py | 7 +- src/traductor_es/modulos/scripts.py | 17 +++ tests/test_funciones_de_texto.py | 122 +++++++++++++++++++ 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 tests/test_funciones_de_texto.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3a504..af46924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si - Mensajes que un script arma por partes (`mensaje += efecto.name; player.tell(mensaje)`), con sus tablas de nombres. - `notify`, `kick`, `openChestGUI` y `openInventoryGUI` de KubeJS (métodos tomados del código de KubeJS 1.20 y 1.21). - **Modo exhaustivo** para los scripts de KubeJS (opcional, apagado por defecto; `--exhaustivo` y una casilla en «Opciones avanzadas»): traduce también toda frase que parezca texto aunque no se sepa dónde se muestra. Puede tocar lógica; todo queda con copia de seguridad. Nunca toca `console.log` ni comparaciones. +- **Funciones propias de un script que solo muestran texto** (`CreateStringTooltip(evento, objeto, texto)`, `crear(id, nombre)`…): se traduce lo que se les pasa en la posición de ese parámetro. Es conservador: si el parámetro se compara, se busca, se guarda, se devuelve, va en un id o en un comando, o la función se usa como valor o está repetida, no se toca nada. Un `id` junto a un `nombre` queda intacto. En un modpack real (TaidumCraft) pasó de 101 a 35 frases sin elegir. - Los métodos de texto de KubeJS ahora dependen de la versión (`notify`, `openChestGUI`… solo donde existen), comprobados en el código de KubeJS de 1.12 a 26.x (`formatos/kubejs_api.py`). - Mercado de Farming for Blockheads (`farmingforblockheads_compat`): se traducen los nombres de las pestañas. - El informe «Qué queda en inglés» también avisa de carpetas de datos con frases en inglés cuyo formato la app no conoce. diff --git a/src/traductor_es/formatos/flujo_js.py b/src/traductor_es/formatos/flujo_js.py index c4e2578..573ed59 100644 --- a/src/traductor_es/formatos/flujo_js.py +++ b/src/traductor_es/formatos/flujo_js.py @@ -12,7 +12,7 @@ from __future__ import annotations import re -from collections.abc import Iterable, Sequence +from collections.abc import Callable, Iterable, Sequence from dataclasses import dataclass from .javascript import CadenaJs @@ -84,3 +84,154 @@ def cuerpos_de_tabla(src: str, cadenas: Sequence[CadenaJs], tablas: Iterable[str i += 1 cuerpos.append((m.end(), i)) return cuerpos + + +# ---------------------------------------------------------------------------------------------- funciones de texto +#: Métodos que transforman un texto sin cambiar de qué se trata (el resultado sigue siendo ese texto o sus líneas). +_TRANSFORMA = frozenset({ + "split", "trim", "trimStart", "trimEnd", "replace", "replaceAll", "slice", "substring", "concat", "join", "map", + "filter", "forEach", "toString", "padStart", "padEnd", "repeat", +}) # fmt: skip +#: Si en la línea se compara o se busca el texto, es lógica: esa función no se toca. +_LOGICA = re.compile( + r"[!=]==?(?!>)|\.(?:includes|indexOf|startsWith|endsWith|match|test|search|equals|localeCompare)\s*\(|\bswitch\b|\bcase\b" +) +_FUNCIONES = ( + re.compile(rf"\bfunction\s+({_IDENT})\s*\(([^)]*)\)\s*\{{"), + re.compile( + rf"\b(?:const|let|var)\s+({_IDENT})\s*=\s*(?:async\s*)?(?:function\s*\w*\s*)?\(([^)]*)\)\s*(?:=>\s*)?\{{" + ), + re.compile(rf"\b(?:const|let|var)\s+({_IDENT})\s*=\s*({_IDENT})\s*=>\s*\{{"), +) +_ASIGNACION = re.compile(rf"\s*(?:(?:const|let|var)\s+)?({_IDENT})\s*(\+?=)(?!=)") +_CALLBACK = re.compile(rf"(?:\(\s*({_IDENT})\s*(?:,[^)]*)?\)|\b({_IDENT}))\s*=>|function\s*\(\s*({_IDENT})") + + +def _en_blanco(src: str, cadenas: Sequence[CadenaJs]) -> str: + """El código con las cadenas y los comentarios en blanco (mismas posiciones): solo queda lo que es código.""" + letras = list(src) + for c in cadenas: + for i in range(c.inicio, c.fin): + if letras[i] != "\n": + letras[i] = " " + limpio = "".join(letras) + return re.sub(r"//[^\n]*|/\*.*?\*/", lambda m: re.sub(r"[^\n]", " ", m.group(0)), limpio, flags=re.S) + + +def _cierre(limpio: str, abierta: int) -> int: + profundidad, i = 1, abierta + while i < len(limpio) and profundidad: + profundidad += {"{": 1, "}": -1}.get(limpio[i], 0) + i += 1 + return i if profundidad == 0 else -1 + + +def _linea(cuerpo: str, pos: int) -> tuple[int, str]: + inicio = cuerpo.rfind("\n", 0, pos) + 1 + fin = cuerpo.find("\n", pos) + return inicio, cuerpo[inicio : len(cuerpo) if fin < 0 else fin] + + +def _tomar_callbacks(trozo: str, base: int, vistos: set[str], pendientes: list[str], declaraciones: set[int]) -> None: + """`texto.map(l => …)`: el parámetro del callback también es texto y se sigue; su declaración no es un uso.""" + for cb in _CALLBACK.finditer(trozo): + grupo = 1 if cb.group(1) else 2 if cb.group(2) else 3 + nuevo = cb.group(grupo) + if nuevo: + declaraciones.add(base + cb.start(grupo)) + if nuevo not in vistos: + vistos.add(nuevo) + pendientes.append(nuevo) + + +def _uso_permitido( + cuerpo: str, nombre: str, m: re.Match[str], vistos: set[str], pendientes: list[str], declaraciones: set[int] +) -> bool: + """Un uso de un nombre de texto que no es mostrarlo directamente: ¿es solo derivar otro texto de él?""" + inicio_linea, linea = _linea(cuerpo, m.start()) + if _LOGICA.search(linea): + return False + asignacion = _ASIGNACION.match(linea) + if asignacion: + if asignacion.start(1) + inicio_linea <= m.start() < asignacion.end(1) + inicio_linea: + return True # es el lado izquierdo: se vuelve a asignar + derivado = asignacion.group(1) + if derivado not in vistos: + vistos.add(derivado) + pendientes.append(derivado) + _tomar_callbacks(linea[m.end() - inicio_linea :], m.end(), vistos, pendientes, declaraciones) + return True + metodo = re.match(rf"\s*{re.escape(nombre)}\s*\.\s*(\w+)\s*\(", linea) + if metodo and metodo.group(1) in _TRANSFORMA and metodo.start() + inicio_linea <= m.start(): + _tomar_callbacks(linea[metodo.end() :], inicio_linea + metodo.end(), vistos, pendientes, declaraciones) + return True + return False + + +def _parametro_es_solo_texto( + cuerpo: str, + parametro: str, + patron: str, + plantillas: list[tuple[CadenaJs, set[str]]], + es_sumidero: Callable[[CadenaJs], bool], +) -> bool: + """Todos los usos del parámetro (y de lo derivado) son mostrarlo: nunca se compara, busca, guarda ni devuelve.""" + sumidero_antes = re.compile(rf"(?:{patron})\s*\(\s*$") + pendientes, vistos, sumideros = [parametro], {parametro}, 0 + declaraciones: set[int] = set() # dónde se declara el parámetro de un callback (`l => …`): no es un uso + while pendientes: + nombre = pendientes.pop() + # un `${nombre}` dentro de una plantilla vale solo si la plantilla se muestra tal cual + for c, nombres in plantillas: + if nombre in nombres and not es_sumidero(c): + return False + sumideros += nombre in nombres + for m in re.finditer(rf"(? 0 + + +def funciones_de_texto( + src: str, cadenas: Sequence[CadenaJs], patron: str, es_sumidero: Callable[[CadenaJs], bool] +) -> set[tuple[str, int]]: + """Funciones del propio script cuyo parámetro N solo sirve para mostrarse: {(nombre, posición del parámetro)}. + + Pasar una frase en esa posición es escribir un texto visible, aunque la llamada no sea `tell` ni `Text.of`. Es + conservador: si el parámetro se compara, se busca, se guarda, se devuelve o se pasa a algo que no se entiende, la + función queda fuera. La función tampoco puede usarse como valor (`callback = f`): solo se llama. + """ + limpio = _en_blanco(src, cadenas) + plantillas_todas = [ + (c, set(re.findall(_IDENT, " ".join(re.findall(r"\$\{([^{}\n]*)\}", c.texto))))) + for c in cadenas + if c.comilla == "`" and "${" in c.texto + ] + resultado: set[tuple[str, int]] = set() + definiciones: dict[str, int] = {} + encontradas: list[tuple[str, list[str], int, int, int]] = [] + for patron_def in _FUNCIONES: + for m in patron_def.finditer(limpio): + nombre, params = m.group(1), [p.strip() for p in m.group(2).split(",")] + fin = _cierre(limpio, m.end()) + if fin > 0: + definiciones[nombre] = definiciones.get(nombre, 0) + 1 + encontradas.append((nombre, params, m.start(), m.end(), fin)) + for nombre, params, desde, inicio, fin in encontradas: + fuera = limpio[:desde] + " " + limpio[fin:] # el resto del script, sin la propia definición + usos_como_valor = re.search(rf"(? str: base = f"{self.receptor}.{self.funcion}" if self.receptor else str(self.funcion) @@ -178,13 +180,16 @@ def escanear(src: str) -> list[CadenaJs]: if pila and pila[-1].tipo == "(" and pila[-1].primera is None: pila[-1].primera = texto contexto = tuple(m.nombre() for m in pila if m.tipo == "(" and m.funcion) - res.append(CadenaJs(i, fin, c, texto, funcion, receptor, _comparada(src, i, fin), contexto)) + argumento = marco.arg if marco and marco.tipo == "(" else -1 + res.append(CadenaJs(i, fin, c, texto, funcion, receptor, _comparada(src, i, fin), contexto, argumento)) i = fin continue if c == "(": pila.append(_Marco("(", *_llamada(src, i))) elif c in "[{": pila.append(_Marco(c)) + elif c == "," and pila and pila[-1].tipo == "(": + pila[-1].arg += 1 elif c in ")]}" and pila: pila.pop() i += 1 diff --git a/src/traductor_es/modulos/scripts.py b/src/traductor_es/modulos/scripts.py index 85b56a6..ff3d7e3 100644 --- a/src/traductor_es/modulos/scripts.py +++ b/src/traductor_es/modulos/scripts.py @@ -93,6 +93,19 @@ def _plantilla_segura(c: javascript.CadenaJs) -> bool: return c.comilla != "`" or c.texto.count("${") == len(_EXPRESION.findall(c.texto)) +def _es_sumidero(c: javascript.CadenaJs, llamadas: frozenset[str]) -> bool: + """La cadena se le pasa directamente a algo que le muestra texto al jugador (`tell`, `Text.of`…).""" + return c.funcion in llamadas or (c.funcion in AYUDAS_TEXTO and c.receptor in RECEPTORES_TEXTO) + + +def es_texto_de_funcion(c: javascript.CadenaJs, funciones: set[tuple[str, int]]) -> bool: + """La cadena va en la posición de un parámetro que la función solo muestra (ver `flujo_js.funciones_de_texto`).""" + if c.receptor is not None or c.funcion is None or c.comparada or (c.funcion, c.arg) not in funciones: + return False # una llamada con receptor (`x.f(...)`) es un método, no la función del script + t = c.texto.strip() + return _plantilla_segura(c) and es_traducible(t) and not parece_clave(t) and any(ch.isalpha() for ch in t) + + def textos_armados( src: str, cadenas: list[javascript.CadenaJs], flujo: flujo_js.Flujo, tablas: frozenset[str] ) -> list[javascript.CadenaJs]: @@ -232,6 +245,10 @@ def recolectar(self) -> list[Pieza]: en_comando[c.inicio] = cmd vistas = {c.inicio for c in elegidas} elegidas += [c for c in textos_armados(src, cadenas, flujo, tablas) if c.inicio not in vistas] + propias = flujo_js.funciones_de_texto(src, cadenas, patron, lambda c: _es_sumidero(c, llamadas)) + if propias: # funciones del propio script cuyo parámetro solo se muestra + vistas = {c.inicio for c in elegidas} + elegidas += [c for c in cadenas if c.inicio not in vistas and es_texto_de_funcion(c, propias)] if self.exhaustivo: vistas = {c.inicio for c in elegidas} elegidas += [ diff --git a/tests/test_funciones_de_texto.py b/tests/test_funciones_de_texto.py new file mode 100644 index 0000000..644f986 --- /dev/null +++ b/tests/test_funciones_de_texto.py @@ -0,0 +1,122 @@ +"""Funciones del propio script que solo muestran texto: se traduce lo que se les pasa, y nada que pueda ser lógica.""" + +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from traductor_es.fuentes import vanilla +from traductor_es.fuentes.instancia import Instancia +from traductor_es.modulos.base import Contexto, traducir_modulo +from traductor_es.modulos.scripts import ScriptsKubeJS +from traductor_es.motores.simulador import Simulador +from traductor_es.nucleo.glosario import Glosario +from traductor_es.nucleo.memoria import Memoria +from traductor_es.nucleo.traductor import Traductor +from traductor_es.salida.copias import Respaldo + +from . import fabrica + + +@pytest.fixture(autouse=True) +def _sin_lanzadores(monkeypatch: pytest.MonkeyPatch) -> None: + def ninguno() -> list[Path]: + return [] + + monkeypatch.setattr(vanilla, "raices_de_lanzadores", ninguno) + + +@pytest.fixture +def memoria() -> Iterator[Memoria]: + with Memoria(":memory:") as m: + yield m + + +def _traducir(tmp_path: Path, memoria: Memoria, src: str) -> tuple[str, int]: + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "server_scripts" / "s.js" + ruta.parent.mkdir(parents=True) + ruta.write_text(src, encoding="utf-8") + with Instancia(juego) as inst: + resumen = traducir_modulo( + ScriptsKubeJS(Contexto(inst, Respaldo(juego))), Traductor(Simulador(), memoria, Glosario()) + ) + return ruta.read_text("utf-8"), resumen.traducidos + + +# Parámetros que solo se muestran, directamente o después de partirlos en líneas (como el tooltip de Create de un modpack real) +TEXTO_PURO = r""" +function aviso(player, msg) { player.tell(msg) } +const consejo = (evento, objeto, texto) => { + texto = texto.trim() + const lineas = texto.split('\n') + lineas.forEach(linea => { + evento.add(objeto, Text.of(linea).gold()) + }) +} +const decir = (p, quien, frase) => { p.tell(`${quien}: ${frase}`) } +aviso(player, 'Welcome back to the farm') +consejo(e, 'minecraft:stone', 'Hold shift to see the summary\nIt can be mined with a pickaxe') +decir(player, 'Laly', 'The museum opens at dawn') +""" + + +def test_funciones_que_solo_muestran_texto(tmp_path: Path, memoria: Memoria) -> None: + nuevo, n = _traducir(tmp_path, memoria, TEXTO_PURO) + assert "aviso(player, '«ES» Welcome back to the farm')" in nuevo + assert "'«ES» Hold shift to see the summary\\nIt can be mined with a pickaxe'" in nuevo + assert "'«ES» The museum opens at dawn'" in nuevo # `quien` y `frase` se muestran juntos en la plantilla + assert "'minecraft:stone'" in nuevo and n >= 3 + + +# Cada función usa su parámetro como lógica de algún modo: ninguna debe tocarse +PELIGROSAS = r""" +function comparar(p, texto) { if (texto == 'Open the door now') p.tell(texto) } +function buscar(p, texto) { if (texto.includes('door')) p.tell(texto) } +function guardar(p, texto) { p.persistentData.put(texto, 1); p.tell(texto) } +function devolver(texto) { return texto } +function mostrar_y_clave(p, texto) { p.tell(texto); p.data[texto] = true } +function por_plantilla(p, texto) { p.runCommand(`give @s ${texto}`) ; p.tell(texto) } +function como_valor(p, texto) { p.tell(texto) } +const alias = como_valor +function repetida(p, texto) { p.tell(texto) } +function repetida(p, texto) { p.tell(texto) } +comparar(player, 'Open the door now') +buscar(player, 'Look at the door here') +guardar(player, 'Some key with spaces') +devolver('Return this phrase please') +mostrar_y_clave(player, 'Another key with spaces') +por_plantilla(player, 'Some item with spaces') +como_valor(player, 'Called through a value') +repetida(player, 'Defined twice in the file') +""" + + +def test_si_el_parametro_es_logica_no_se_toca_nada(tmp_path: Path, memoria: Memoria) -> None: + nuevo, n = _traducir(tmp_path, memoria, PELIGROSAS) + assert nuevo == PELIGROSAS and n == 0 + + +def test_un_metodo_con_el_mismo_nombre_no_es_la_funcion_del_script(tmp_path: Path, memoria: Memoria) -> None: + src = "function aviso(p, msg) { p.tell(msg) }\ninventario.aviso(player, 'This is another object method')\n" + nuevo, n = _traducir(tmp_path, memoria, src) + assert nuevo == src and n == 0 + + +def test_solo_la_posicion_del_parametro_y_nunca_ids(tmp_path: Path, memoria: Memoria) -> None: + src = ( + "function aviso(p, msg, id) { p.tell(msg) }\naviso(player, 'Something to show here', 'quest id with spaces')\n" + ) + nuevo, _ = _traducir(tmp_path, memoria, src) + assert "'«ES» Something to show here'" in nuevo and "'quest id with spaces'" in nuevo + + +def test_el_id_que_va_en_una_plantilla_no_se_toca_pero_el_nombre_si(tmp_path: Path, memoria: Memoria) -> None: + src = ( + "function crear(id, nombre) {\n" + " event.create(id).displayName(nombre).model(`kubejs:block/${id}`)\n" + "}\n" + "crear('plushie with spaces', 'Taidum Plushie')\n" + ) + nuevo, _ = _traducir(tmp_path, memoria, src) + assert "'plushie with spaces'" in nuevo and "'«ES» Taidum Plushie'" in nuevo From 4fde5f122b90f9f98996c066d1083569df24ddd1 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sat, 7 Nov 2026 19:27:43 -0500 Subject: [PATCH 07/24] feat: plantillas en el mod de apoyo, informe en cuadro y guardable, comprobar el mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - El mod reconoce frases con datos (%s, %d) como plantillas; se descartan las traducciones que cambian el orden de los datos o pierden alguno. - El informe de lo que queda en inglés se ve por categorías, se guarda en un archivo y se escribe como JSON con `cobertura --json`. - Más ▸ Comprobar el mod de apoyo informa de su estado sin abrir el juego. --- CHANGELOG.md | 2 + README.md | 2 +- .../io/github/consterna/modpackes/Tabla.java | 223 +++++++++++++++++- .../github/consterna/modpackes/TablaTest.java | 66 ++++++ src/traductor_es/__main__.py | 10 +- src/traductor_es/modulos/cobertura.py | 45 +++- src/traductor_es/modulos/runtime.py | 25 +- src/traductor_es/salida/mod_apoyo.py | 30 +++ src/traductor_es/ui/informe.py | 31 +++ src/traductor_es/ui/ventana.py | 30 ++- tests/test_cobertura.py | 19 ++ tests/test_runtime.py | 56 +++++ tests/test_ui.py | 20 ++ 13 files changed, 537 insertions(+), 22 deletions(-) create mode 100644 src/traductor_es/ui/informe.py diff --git a/CHANGELOG.md b/CHANGELOG.md index af46924..39082e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si - Títulos y descripciones de logros, y los consejos de las pantallas de carga (mod Tips, en sus dos formatos). - Mensajes que un script arma por partes (`mensaje += efecto.name; player.tell(mensaje)`), con sus tablas de nombres. - `notify`, `kick`, `openChestGUI` y `openInventoryGUI` de KubeJS (métodos tomados del código de KubeJS 1.20 y 1.21). +- **Plantillas en el mod de apoyo:** una frase con datos variables escrita con `%s` o `%d` (p. ej. «Hello %s, welcome back!») se traduce y el mod la reconoce con cualquier dato («Hello Steve, welcome back!»). Se descartan las traducciones que cambian el orden de los datos o pierden alguno, y las plantillas con casi nada de texto fijo. +- **Informe «Qué queda en inglés»:** ahora se ve en un cuadro con las categorías separadas y se puede guardar (`Más ▸ Guardar el informe…`); `modpackes cobertura --json` lo escribe como JSON para otros programas. `Más ▸ Comprobar el mod de apoyo` dice si el mod está en `mods/`, si esta copia lo trae y si ya hay tabla de textos. - **Modo exhaustivo** para los scripts de KubeJS (opcional, apagado por defecto; `--exhaustivo` y una casilla en «Opciones avanzadas»): traduce también toda frase que parezca texto aunque no se sepa dónde se muestra. Puede tocar lógica; todo queda con copia de seguridad. Nunca toca `console.log` ni comparaciones. - **Funciones propias de un script que solo muestran texto** (`CreateStringTooltip(evento, objeto, texto)`, `crear(id, nombre)`…): se traduce lo que se les pasa en la posición de ese parámetro. Es conservador: si el parámetro se compara, se busca, se guarda, se devuelve, va en un id o en un comando, o la función se usa como valor o está repetida, no se toca nada. Un `id` junto a un `nombre` queda intacto. En un modpack real (TaidumCraft) pasó de 101 a 35 frases sin elegir. - Los métodos de texto de KubeJS ahora dependen de la versión (`notify`, `openChestGUI`… solo donde existen), comprobados en el código de KubeJS de 1.12 a 26.x (`formatos/kubejs_api.py`). diff --git a/README.md b/README.md index 9fa3a1c..bf87d04 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), no cubre pantallas ni frases con nombres o números en medio, y por ahora existe para Forge 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java index 7f8bc02..2cce7ce 100644 --- a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java @@ -7,20 +7,58 @@ import java.io.IOException; import java.io.Reader; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * Tabla de textos «inglés exacto → español» que genera ModpackES. Solo hay coincidencia si el texto es idéntico - * (salvo los espacios de los bordes, que se conservan): no hay traducción aproximada ni en vivo. + * Tabla de textos «inglés → español» que genera ModpackES. Hay dos clases de entrada: + *

    + *
  • Exactas: solo coinciden si el texto es idéntico (salvo los espacios de los bordes, que se conservan).
  • + *
  • Plantillas: la clave lleva {@code %s} o {@code %d} (como el {@code String.format} del mod original) y coinciden + * con cualquier texto que sea ese mismo molde con otros datos: «Hello %s!» coincide con «Hello Steve!». El español + * reutiliza lo capturado; puede llevar {@code %1$s}, {@code %2$s} para cambiar el orden.
  • + *
+ * No hay traducción aproximada ni en vivo. */ public final class Tabla { public static final Tabla VACIA = new Tabla(Map.of()); - private final Map textos; + private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); + private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa + private static final int TAMANO_CACHE = 512; + private static final String SIN_COINCIDENCIA = "\u0000"; + + private final Map exactos; + private final List plantillas; + private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > TAMANO_CACHE; + } + }; public Tabla(Map textos) { - this.textos = Map.copyOf(textos); + Map exactos = new HashMap<>(); + List plantillas = new ArrayList<>(); + for (Map.Entry e : textos.entrySet()) { + if (esPlantilla(e.getKey())) { + Plantilla p = Plantilla.crear(e.getKey(), e.getValue()); + if (p != null) { + plantillas.add(p); + } + } else { + exactos.put(e.getKey(), e.getValue()); + } + } + plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero + this.exactos = Map.copyOf(exactos); + this.plantillas = List.copyOf(plantillas); } /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ @@ -47,28 +85,40 @@ public static Tabla leer(Reader lector) throws IOException { /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ public Tabla combinar(Tabla otra) { - if (otra.textos.isEmpty()) { + if (otra.vacia()) { return this; } - Map mapa = new HashMap<>(textos); - mapa.putAll(otra.textos); + Map mapa = new HashMap<>(this.comoMapa()); + mapa.putAll(otra.comoMapa()); return new Tabla(mapa); } + private Map comoMapa() { + Map mapa = new HashMap<>(exactos); + for (Plantilla p : plantillas) { + mapa.put(p.clave, p.valor); + } + return mapa; + } + public boolean vacia() { - return textos.isEmpty(); + return exactos.isEmpty() && plantillas.isEmpty(); } public int tamano() { - return textos.size(); + return exactos.size() + plantillas.size(); + } + + public int plantillas() { + return plantillas.size(); } /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ public String traducir(String texto) { - if (texto == null || texto.isEmpty() || textos.isEmpty()) { + if (texto == null || texto.isEmpty() || vacia()) { return null; } - String directo = textos.get(texto); + String directo = buscar(texto); if (directo != null) { return directo.equals(texto) ? null : directo; } @@ -76,7 +126,7 @@ public String traducir(String texto) { if (nucleo.isEmpty() || nucleo.length() == texto.length()) { return null; } - String traduccion = textos.get(nucleo); + String traduccion = buscar(nucleo); if (traduccion == null || traduccion.equals(nucleo)) { return null; } @@ -84,4 +134,153 @@ public String traducir(String texto) { int fin = texto.length() - texto.stripTrailing().length(); return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); } + + private String buscar(String texto) { + String exacto = exactos.get(texto); + if (exacto != null) { + return exacto; + } + if (plantillas.isEmpty()) { + return null; + } + synchronized (cache) { // los tooltips piden el mismo texto cada cuadro: se recuerda el resultado + String guardado = cache.get(texto); + if (guardado != null) { + return guardado.equals(SIN_COINCIDENCIA) ? null : guardado; + } + String resultado = null; + for (Plantilla p : plantillas) { + resultado = p.aplicar(texto); + if (resultado != null) { + break; + } + } + cache.put(texto, resultado == null ? SIN_COINCIDENCIA : resultado); + return resultado; + } + } + + private static boolean esPlantilla(String clave) { + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + if (!m.group(2).equals("%")) { + return true; + } + } + return false; + } + + /** Un molde con datos variables: «Hello %s!» → «¡Hola %s!». */ + private static final class Plantilla { + final String clave; + final String valor; + final String fijo; // el texto de la clave sin los marcadores + final String prefijo; // lo que va antes del primer marcador: descarta rápido lo que no coincide + final Pattern patron; + final List destino; // trozos de texto y números de grupo (Integer, desde 1) + + private Plantilla(String clave, String valor, String fijo, String prefijo, Pattern patron, List destino) { + this.clave = clave; + this.valor = valor; + this.fijo = fijo; + this.prefijo = prefijo; + this.patron = patron; + this.destino = destino; + } + + /** {@code null} si la plantilla no es segura (poco texto fijo o marcadores que no cuadran). */ + static Plantilla crear(String clave, String valor) { + StringBuilder regex = new StringBuilder("^"); + StringBuilder fijo = new StringBuilder(); + String prefijo = null; + int grupos = 0; + int ultimo = 0; + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + String tramo = clave.substring(ultimo, m.start()); + fijo.append(tramo); + if (!tramo.isEmpty()) { + regex.append(Pattern.quote(tramo)); + } + if (m.group(2).equals("%")) { + fijo.append('%'); + regex.append(Pattern.quote("%")); + } else { + if (prefijo == null) { + prefijo = clave.substring(0, m.start()); + } + grupos++; + regex.append(m.group(2).equals("d") ? "(-?\\d+)" : "(.+?)"); + } + ultimo = m.end(); + } + String cola = clave.substring(ultimo); + fijo.append(cola); + if (!cola.isEmpty()) { + regex.append(Pattern.quote(cola)); + } + regex.append('$'); + if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { + return null; + } + List destino = destino(valor, grupos); + if (destino == null) { + return null; + } + return new Plantilla(clave, valor, fijo.toString(), prefijo == null ? "" : prefijo, + Pattern.compile(regex.toString(), Pattern.DOTALL), destino); + } + + private static List destino(String valor, int grupos) { + List partes = new ArrayList<>(); + Matcher m = MARCADOR.matcher(valor); + int ultimo = 0; + int siguiente = 1; + boolean[] usado = new boolean[grupos + 1]; + while (m.find()) { + if (m.start() > ultimo) { + partes.add(valor.substring(ultimo, m.start())); + } + if (m.group(2).equals("%")) { + partes.add("%"); + } else { + int indice = m.group(1) != null ? Integer.parseInt(m.group(1)) : siguiente++; + if (indice < 1 || indice > grupos) { + return null; + } + usado[indice] = true; + partes.add(indice); + } + ultimo = m.end(); + } + if (ultimo < valor.length()) { + partes.add(valor.substring(ultimo)); + } + for (int i = 1; i <= grupos; i++) { + if (!usado[i]) { + return null; // la traducción perdió un dato: mejor no usarla + } + } + return partes; + } + + String aplicar(String texto) { + if (!prefijo.isEmpty() && !texto.startsWith(prefijo)) { + return null; + } + Matcher m = patron.matcher(texto); + if (!m.matches()) { + return null; + } + StringBuilder salida = new StringBuilder(); + for (Object parte : destino) { + if (parte instanceof Integer indice) { + salida.append(m.group(indice)); + } else { + salida.append((String) parte); + } + } + return salida.toString(); + } + } } diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java index ab1b8c3..a9a7e52 100644 --- a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java +++ b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java @@ -73,4 +73,70 @@ void combinarDejaMandarALaSegunda() { assertFalse(total.vacia()); assertEquals(total, total.combinar(Tabla.VACIA)); } + + @Test + void plantillaConUnDatoVariable() { + Tabla t = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!"); + assertEquals("¡Hola Steve, bienvenido de nuevo!", t.traducir("Hello Steve, welcome back!")); + assertEquals("¡Hola Ñandú 😀, bienvenido de nuevo!", t.traducir("Hello Ñandú 😀, welcome back!")); + assertNull(t.traducir("Goodbye Steve, welcome back!")); + assertNull(t.traducir("Hello Steve")); + } + + @Test + void plantillaConNumeroSoloCoincideConNumeros() { + Tabla t = tabla("You have %d items left", "Te quedan %d objetos"); + assertEquals("Te quedan 5 objetos", t.traducir("You have 5 items left")); + assertEquals("Te quedan -3 objetos", t.traducir("You have -3 items left")); + assertNull(t.traducir("You have many items left")); + } + + @Test + void plantillaPuedeCambiarElOrden() { + Tabla t = tabla("%s gave %d coins", "%2$d monedas de %1$s"); + assertEquals("7 monedas de Laly", t.traducir("Laly gave 7 coins")); + } + + @Test + void plantillaConBordesYPorcentajeLiteral() { + Tabla t = tabla("Progress: %d%% of %s", "Progreso: %d%% de %s"); + assertEquals(" Progreso: 50% de la misión ", t.traducir(" Progress: 50% of la misión ")); + } + + @Test + void plantillasInseguras() { + // sin texto fijo: coincidiría con todo + assertNull(tabla("%s", "%s").traducir("cualquier cosa")); + assertNull(tabla("%s %s", "%s %s").traducir("uno dos")); + // la traducción pierde un dato o inventa uno + assertNull(tabla("Hello %s and %s here", "Hola %s aquí").traducir("Hello A and B here")); + assertNull(tabla("Hello %s there friend", "Hola %s y %s").traducir("Hello A there friend")); + assertNull(tabla("Hello %s there friend", "Hola %3$s").traducir("Hello A there friend")); + } + + @Test + void laPlantillaMasEspecificaGana() { + Tabla t = tabla( + "Welcome %s", "Bienvenido %s", + "Welcome %s to the farm", "Bienvenido %s a la granja"); + assertEquals("Bienvenido Steve a la granja", t.traducir("Welcome Steve to the farm")); + assertEquals("Bienvenido Steve", t.traducir("Welcome Steve")); + } + + @Test + void caracteresDeRegexEnElTextoFijoNoSonEspeciales() { + Tabla t = tabla("Cost (%d) [gold]. Really?", "Costo (%d) [oro]. ¿En serio?"); + assertEquals("Costo (9) [oro]. ¿En serio?", t.traducir("Cost (9) [gold]. Really?")); + assertNull(t.traducir("Cost 9 gold. Really?")); + } + + @Test + void lasPlantillasSobrevivenAlCombinarYSeCuentan() { + Tabla a = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!", "Exact text here", "Texto exacto"); + Tabla total = a.combinar(tabla("Another %d things here", "Otras %d cosas aquí")); + assertEquals(3, total.tamano()); + assertEquals(2, total.plantillas()); + assertEquals("Otras 3 cosas aquí", total.traducir("Another 3 things here")); + assertEquals("Texto exacto", total.traducir("Exact text here")); + } } diff --git a/src/traductor_es/__main__.py b/src/traductor_es/__main__.py index ad83d03..1b08c88 100644 --- a/src/traductor_es/__main__.py +++ b/src/traductor_es/__main__.py @@ -11,6 +11,7 @@ from . import NOMBRE, __version__, configuracion from .fuentes.instancia import Instancia from .fuentes.lanzadores import buscar +from .modulos.cobertura import informe_en_json from .motores import fabrica from .motores.base import Cancelado, ErrorFatal, Motor from .motores.gemini import MODELO_POR_DEFECTO @@ -100,6 +101,7 @@ def comunes(p: argparse.ArgumentParser) -> None: cob = sub.add_parser("cobertura", help="dice qué texto queda en inglés y por qué (solo lee, no escribe nada)") cob.add_argument("carpeta", help="carpeta de la instancia (o su carpeta minecraft)") cob.add_argument("--java", action="store_true", help="también el texto escrito en el código de los mods (tarda)") + cob.add_argument("--json", action="store_true", help="escribe solo el informe, como JSON (UTF-8)") rt = sub.add_parser( "instalar-runtime", help="pone o quita el mod de apoyo que muestra el texto del código de los mods" ) @@ -147,9 +149,13 @@ def _instalar_runtime(args: argparse.Namespace) -> None: def _cobertura(args: argparse.Namespace) -> None: - with Proceso(args.carpeta, Simulador(), registro=_escribir) as p: - _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") + with Proceso(args.carpeta, Simulador(), registro=(lambda _m: None) if args.json else _escribir) as p: + if not args.json: + _escribir(f"Minecraft {p.instancia.version} · {p.instancia.loader or '¿loader?'} · {p.instancia.juego}") hallazgos = p.que_queda(java=args.java) + if args.json: + _escribir(informe_en_json(hallazgos)) + return for h in hallazgos: _escribir(f"[{h.categoria}] {h}") _escribir(f"{len(hallazgos)} hallazgos." if hallazgos else "No encontré texto que quede en inglés.") diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index 88bc504..8fbf55e 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -7,6 +7,7 @@ from __future__ import annotations +import json import re import struct import zipfile @@ -25,8 +26,8 @@ #: `method_43470` (Fabric) y los `TextComponent`, `StringTextComponent` y `TextComponentString` anteriores. _MARCAS_TEXTO = ("literal", "m_237113_", "method_43470") _CLASES_TEXTO = ("TextComponent", "StringTextComponent", "TextComponentString") -#: Una frase en inglés: puede empezar con códigos de color (§c) y llevar %s, números y puntuación. -_FRASE = re.compile(r"(?:§[0-9a-fk-or])*[A-Z][A-Za-z']+(?: [§A-Za-z0-9,.'!?:%\-]+){2,}[.!?:]?") +#: Una frase en inglés: puede empezar con códigos de color (§c) o con un dato (%s) y llevar números y puntuación. +_FRASE = re.compile(r"(?:§[0-9a-fk-or])*(?:[A-Z][A-Za-z']+|%[sd])(?: [§A-Za-z0-9,.'!?:%\-]+){2,}[.!?:]?") _ESPANOL = re.compile(r"\b(de|la|el|los|las|con|sin|para|por|una|un|del)\b") #: Frases de registros y errores internos: casi nunca llegan al jugador. _INTERNO = re.compile( @@ -68,6 +69,46 @@ def __str__(self) -> str: return f"{self.donde}: {self.cantidad} {'texto' if self.cantidad == 1 else 'textos'}. {self.motivo}.{ej}" +_TITULOS = { + "java": "Escrito en el código de los mods (un resource pack no lo alcanza; el mod de apoyo cubre Forge 1.20.1)", + "script": "Frases de scripts que parecen texto y no se tocaron (pueden ser lógica o un mensaje armado distinto)", + "formato": "Carpetas con frases en inglés cuyo formato la app no conoce (no se tocó ningún archivo)", +} + + +def texto_del_informe(hallazgos: list[Hallazgo]) -> str: + """El informe como texto para leer o guardar, agrupado por categoría.""" + if not hallazgos: + return "No encontré texto que se quede en inglés.\n" + lineas = [f"Texto que se queda en inglés aunque se traduzca todo: {len(hallazgos)} lugares.", ""] + for categoria in ("script", "formato", "java"): + grupo = [h for h in hallazgos if h.categoria == categoria] + if not grupo: + continue + lineas += [f"== {_TITULOS[categoria]} ==", ""] + lineas += [f"- {h}" for h in grupo] + lineas.append("") + return "\n".join(lineas) + + +def informe_en_json(hallazgos: list[Hallazgo]) -> str: + """El informe como JSON (UTF-8), para que lo lean otros programas.""" + return json.dumps( + [ + { + "categoria": h.categoria, + "donde": h.donde, + "textos": h.cantidad, + "motivo": h.motivo, + "ejemplos": list(h.ejemplos), + } + for h in hallazgos + ], + ensure_ascii=False, + indent=2, + ) + + def hallazgos_de_scripts(juego: Path, sin_elegir: list[tuple[Path, list[str]]]) -> list[Hallazgo]: """Frases de los scripts que parecen texto y que la traducción no tocó (pueden ser mensajes).""" return [ diff --git a/src/traductor_es/modulos/runtime.py b/src/traductor_es/modulos/runtime.py index f0840d2..d1cf05e 100644 --- a/src/traductor_es/modulos/runtime.py +++ b/src/traductor_es/modulos/runtime.py @@ -7,6 +7,7 @@ from __future__ import annotations +import re from collections.abc import Mapping from pathlib import Path @@ -17,6 +18,12 @@ AMBITO = "ui" RUTA_TABLA = "assets/modpackes_runtime/textos/{variante}.json" +#: Datos variables de un `String.format`: el mod los trata como una plantilla («Hello %s!» coincide con «Hello Steve!»). +_MARCADORES = re.compile(r"%(?:\d+\$)?[sd%]") + + +def _marcadores(texto: str) -> list[str]: + return _MARCADORES.findall(texto) class TextoDeJava: @@ -40,7 +47,12 @@ def describir(self) -> str: def recolectar(self) -> list[Pieza]: self._frases = frases_por_mod(self.ctx.instancia) - return [Pieza(f, f"texto del código de {jar.name}") for jar, frases in self._frases for f in frases] + return [Pieza(f, self._pista(jar.name, f)) for jar, frases in self._frases for f in frases] + + @staticmethod + def _pista(mod: str, frase: str) -> str: + base = f"texto del código de {mod}" + return f"{base}; los marcadores %s y %d son datos: déjalos en el mismo orden" if _marcadores(frase) else base def escribir(self, traducciones: Mapping[str, str]) -> Resumen: resumen = Resumen() @@ -48,10 +60,15 @@ def escribir(self, traducciones: Mapping[str, str]) -> Resumen: for _, frases in self._frases: for frase in frases: traducida = traducciones.get(frase) - if traducida is not None and traducida != frase and es_traducible(frase): - tabla[frase] = traducida - else: + if traducida is None or traducida == frase or not es_traducible(frase): + resumen.pendientes += 1 + elif _marcadores(traducida) != _marcadores(frase): + # El mod rellena los datos en orden: si la traducción los cambia de sitio, mostraría un dato + # en el lugar de otro. Mejor dejar el inglés. + self.ctx.registro(f"No uso «{traducida[:50]}»: cambió el orden de los datos de «{frase[:50]}»") resumen.pendientes += 1 + else: + tabla[frase] = traducida resumen.traducidos = len(tabla) if not tabla: return resumen diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index afd9ae0..bf97f93 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -44,6 +44,36 @@ def instalados(instancia: Instancia) -> list[Path]: return sorted(p for p in carpeta.glob(f"{PREFIJO}*.jar") if _NOMBRE.fullmatch(p.name)) if carpeta.is_dir() else [] +def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: + """Lo que hay que saber del mod de apoyo en esta instancia, en frases (no abre el juego).""" + destino = destino_de(instancia) + if destino is None: + loader = instancia.loader or "sin loader" + return [ + f"No hay mod de apoyo para Minecraft {instancia.version} con {loader}: " + "el texto del código Java seguirá en inglés." + ] + lineas = [f"Hay mod de apoyo para esta instancia ({destino})."] + lineas.append( + "Esta copia de la aplicación lo trae." + if jar_empaquetado(instancia, carpeta_jars) + else "Esta copia de la aplicación NO lo trae." + ) + puestos = instalados(instancia) + lineas.append(f"En mods/: {', '.join(p.name for p in puestos)}." if puestos else "No está instalado en mods/.") + tablas = sorted( + (instancia.juego / "resourcepacks" / "Traduccion_ES" / "assets" / "modpackes_runtime" / "textos").glob("*.json") + ) + lineas.append( + f"Tabla de textos en el resource pack: {len(tablas)} variantes." + if tablas + else "Todavía no hay tabla de textos: traduce con la opción activada." + ) + if puestos and tablas: + lineas.append("Todo listo: abre el juego en español.") + return lineas + + def instalar(instancia: Instancia, carpeta_jars: Path | None = None) -> Path: """Copia el mod de apoyo a `mods/` y quita sus versiones anteriores. ValueError si no hay uno.""" destino = destino_de(instancia) diff --git a/src/traductor_es/ui/informe.py b/src/traductor_es/ui/informe.py new file mode 100644 index 0000000..8a02873 --- /dev/null +++ b/src/traductor_es/ui/informe.py @@ -0,0 +1,31 @@ +"""El cuadro «Qué queda en inglés»: el informe por categorías, con un botón para guardarlo.""" + +from __future__ import annotations + +from collections.abc import Callable + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget + +from .componentes import etiqueta + + +class DialogoInforme(QDialog): + """Solo muestra el texto del informe; qué hace «Guardar…» lo decide la ventana.""" + + def __init__(self, texto: str, guardar: Callable[[], None], padre: QWidget | None = None) -> None: + super().__init__(padre) + self.setWindowTitle("Qué queda en inglés") + self.setMinimumSize(720, 460) + capa = QVBoxLayout(self) + capa.setSpacing(12) + capa.addWidget(etiqueta("Qué queda en inglés", "titulo")) + self.texto = QPlainTextEdit() + self.texto.setReadOnly(True) + self.texto.setPlainText(texto) + capa.addWidget(self.texto, 1) + botones = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + self.b_guardar = QPushButton("Guardar…") + self.b_guardar.clicked.connect(guardar) + botones.addButton(self.b_guardar, QDialogButtonBox.ButtonRole.ActionRole) + botones.rejected.connect(self.close) + capa.addWidget(botones) diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 05a6bdd..5a3df50 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -49,7 +49,7 @@ from ..actualizaciones import Novedad from ..fuentes.instancia import Instancia, resolver_carpeta_juego from ..fuentes.lanzadores import InstanciaEncontrada, buscar -from ..modulos.cobertura import Hallazgo +from ..modulos.cobertura import Hallazgo, texto_del_informe from ..motores import fabrica from ..motores.base import Cancelado, ErrorFatal, Motor from ..motores.compatible import Compatible @@ -71,6 +71,7 @@ icono_en_boton, seguir_tema, ) +from .informe import DialogoInforme from .instancias import Lector, ListaInstancias, traducida from .motor import DialogoMotor @@ -221,6 +222,8 @@ def __init__( self._fase = "" self._clave_lista = False self.ultimo_informe: Informe | None = None + self.ultima_cobertura: list[Hallazgo] = [] + self._dialogo_informe: DialogoInforme | None = None self._preferencias: dict[str, Any] = configuracion.leer_preferencias() self._motor_anterior: str | None = None self._construir() @@ -526,6 +529,8 @@ def agregar(texto: str, nombre_icono: str, funcion: Callable[[], object]) -> QAc self.a_correcciones.setToolTip("Buscar y corregir a mano traducciones de la memoria") self.a_cobertura = agregar("Qué queda en inglés", "circle-alert", lambda: self.lanzar("cobertura")) self.a_cobertura.setToolTip("Lista el texto que no se puede traducir y por qué (solo lee, no cambia nada)") + self.a_guardar_informe = agregar("Guardar el informe de lo que queda…", "file-text", self._guardar_informe) + self.a_comprobar_apoyo = agregar("Comprobar el mod de apoyo", "circle-check", self._comprobar_apoyo) self.a_apoyo = agregar("Instalar el mod de apoyo", "package", self._instalar_apoyo) self.a_apoyo.setToolTip( "Pone ModpackES Runtime en la carpeta mods: muestra en español el texto del código de los mods" @@ -1037,10 +1042,33 @@ def quitar(inst: Instancia) -> str: self._con_instancia(quitar) + def _comprobar_apoyo(self) -> None: + self._con_instancia(lambda inst: "\n".join(mod_apoyo.estado(inst))) + + def _guardar_informe(self, destino: str | None = None) -> None: + if not self.ultima_cobertura: + self.avisar("Primero usa «Más ▸ Qué queda en inglés».") + return + if destino is None: + destino, _ = QFileDialog.getSaveFileName( + self, "Guardar el informe", str(Path.home() / "Que_queda_en_ingles.txt"), "Texto (*.txt)" + ) + if not destino: + return + try: + Path(destino).write_text(texto_del_informe(self.ultima_cobertura), encoding="utf-8") + except OSError as e: + self.avisar(f"No pude guardar el informe: {e}", "error") + return + self.aviso.mostrar(f"Informe guardado: {destino}", "exito") + def _mostrar_cobertura(self, hallazgos: list[Hallazgo]) -> None: + self.ultima_cobertura = hallazgos if not hallazgos: self.estado.setText("No encontré texto que se quede en inglés.") return + self._dialogo_informe = DialogoInforme(texto_del_informe(hallazgos), self._guardar_informe, self) + self._dialogo_informe.show() self.estado.setText(f"Quedan {len(hallazgos)} lugares con texto en inglés: el detalle está en Detalles.") self.escribir("Texto que se queda en inglés aunque traduzcas todo:") for h in hallazgos: diff --git a/tests/test_cobertura.py b/tests/test_cobertura.py index 347f4ef..3d3acf9 100644 --- a/tests/test_cobertura.py +++ b/tests/test_cobertura.py @@ -92,3 +92,22 @@ def test_formatos_desconocidos_se_informan_sin_tocar(tmp_path: Path) -> None: hallazgos = formatos_desconocidos(juego) assert [(h.categoria, h.donde, h.cantidad) for h in hallazgos] == [("formato", "kubejs/data/a/quizzes", 1)] assert hallazgos[0].ejemplos == ("What do cows eat for breakfast?",) and quiz.read_bytes() == antes + + +def test_texto_y_json_del_informe() -> None: + import json + + from traductor_es.modulos.cobertura import Hallazgo, informe_en_json, texto_del_informe + + hallazgos = [ + Hallazgo("java", "m.jar", 2, "Escrito en el código", ("You can't do that!", "Ñandú ¡hola!")), + Hallazgo("script", "kubejs/a.js", 1, "Parece una frase", ("The market is closed",)), + ] + texto = texto_del_informe(hallazgos) + assert texto.index("Frases de scripts") < texto.index("Escrito en el código de los mods") # script antes que java + assert "m.jar: 2 textos" in texto and "Ñandú ¡hola!" in texto + assert texto_del_informe([]) == "No encontré texto que se quede en inglés.\n" + datos = json.loads(informe_en_json(hallazgos)) + assert datos[0] == {"categoria": "java", "donde": "m.jar", "textos": 2, "motivo": "Escrito en el código", + "ejemplos": ["You can't do that!", "Ñandú ¡hola!"]} # fmt: skip + assert "Ñandú" in informe_en_json(hallazgos) # UTF-8 sin escapar diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 5e24dda..cd8f530 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -149,3 +149,59 @@ def test_cli_instalar_runtime_sin_soporte(tmp_path: Path, capsys: pytest.Capture assert "Todavía no hay mod de apoyo" in capsys.readouterr().out # los errores van por la salida normal assert main(["instalar-runtime", str(juego.parent), "--quitar"]) == 0 assert "No había mod de apoyo" in capsys.readouterr().out + + +def test_plantillas_con_datos_variables_y_orden(tmp_path: Path, memoria: Memoria) -> None: + from traductor_es.motores.base import Lote + + class Reordena(Simulador): + """Traduce bien, salvo una frase donde cambia el orden de los datos.""" + + def traducir_lote(self, lote: Lote) -> dict[str, str]: + salida = super().traducir_lote(lote) + for e in lote.entradas: + if "gave" in e.texto: # «⟦0⟧ gave ⟦1⟧ coins» → los marcadores al revés + salida[e.id] = "⟦1⟧ monedas de ⟦0⟧" + return salida + + juego = fabrica.prism(tmp_path, "1.20.1", "forge") + fabrica.jar( + juego / "mods" / "m.jar", + {"com/m/Chat.class": _clase("literal", "Hello %s, welcome back to the farm!", "%s gave %d coins to the shop")}, + ) + avisos: list[str] = [] + with Instancia(juego) as inst: + ctx = Contexto(inst, Respaldo(juego), pack=PackSalida(inst), registro=avisos.append) + modulo = TextoDeJava(ctx) + piezas = modulo.recolectar() + assert all("marcadores" in p.pista for p in piezas) # el motor sabe que debe respetar el orden + resumen = traducir_modulo(modulo, Traductor(Reordena(), memoria, Glosario())) + pack = ctx.pack_listo().raiz + tabla = json.loads((pack / RUTA_TABLA.format(variante="es_mx")).read_text("utf-8")) + assert tabla == {"Hello %s, welcome back to the farm!": "«ES» Hello %s, welcome back to the farm!"} + assert resumen.traducidos == 1 and resumen.pendientes == 1 and any("cambió el orden" in a for a in avisos) + + +def test_estado_del_mod_de_apoyo(tmp_path: Path) -> None: + sin_soporte = fabrica.prism(tmp_path / "a", "1.21.1", "neoforge") + with Instancia(sin_soporte) as inst: + assert "No hay mod de apoyo" in mod_apoyo.estado(inst)[0] + juego = fabrica.prism(tmp_path / "b", "1.20.1", "forge") + carpeta = _jars(tmp_path, "modpackes_runtime-forge-1.20.1-0.1.0.jar") + with Instancia(juego) as inst: + texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) + assert "NO lo trae" in texto and "No está instalado" in texto and "Todavía no hay tabla" in texto + mod_apoyo.instalar(inst, carpeta) + tabla = juego / "resourcepacks" / "Traduccion_ES" / "assets" / "modpackes_runtime" / "textos" / "es_mx.json" + tabla.parent.mkdir(parents=True) + tabla.write_text("{}", encoding="utf-8") + texto = " ".join(mod_apoyo.estado(inst, carpeta)) + assert "lo trae" in texto and "modpackes_runtime-forge-1.20.1-0.1.0.jar" in texto and "Todo listo" in texto + + +def test_cli_cobertura_en_json(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + juego = _instancia_con_mod(tmp_path) + assert main(["cobertura", str(juego.parent), "--java", "--json"]) == 0 + salida = capsys.readouterr().out + datos = json.loads(salida) # solo JSON: sin cabecera ni líneas de texto + assert any(h["categoria"] == "java" and "You can't use chat" in " ".join(h["ejemplos"]) for h in datos) diff --git a/tests/test_ui.py b/tests/test_ui.py index a14182a..c47c3f5 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -540,3 +540,23 @@ def test_opcion_de_texto_java_y_mod_de_apoyo_en_la_ventana( assert not list((juego / "mods").glob("modpackes_runtime-*.jar")) v.a_quitar_apoyo.trigger() assert "No había mod de apoyo" in v.aviso.e_texto.text() + + +def test_informe_en_cuadro_y_guardado_en_archivo(qtbot: QtBot, tmp_path: Path) -> None: + juego = _modpack(tmp_path) + ruta = juego / "kubejs" / "server_scripts" / "s.js" + ruta.write_text( + ruta.read_text("utf-8") + "\nlet nota = 'This sentence looks like a message but is not used'\n", + encoding="utf-8", + ) + v = _ventana(qtbot, juego) + v._guardar_informe(str(tmp_path / "antes.txt")) # pyright: ignore[reportPrivateUsage] + assert not (tmp_path / "antes.txt").exists() # todavía no hay informe: no escribe nada + v.a_cobertura.trigger() + qtbot.waitUntil(lambda: v._hilo is None and v._dialogo_informe is not None, timeout=30000) # pyright: ignore[reportPrivateUsage] + assert "This sentence looks like a message" in v._dialogo_informe.texto.toPlainText() # pyright: ignore[reportPrivateUsage, reportOptionalMemberAccess] + destino = tmp_path / "informe.txt" + v._guardar_informe(str(destino)) # pyright: ignore[reportPrivateUsage] + assert "This sentence looks like a message" in destino.read_text("utf-8") + v.a_comprobar_apoyo.trigger() + assert "mod de apoyo" in v.aviso.e_texto.text().lower() or "mod de apoyo" in v.log.toPlainText().lower() From f893d9cb1781071f29ab3181d947366217c3d510 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sat, 7 Nov 2026 20:17:44 -0500 Subject: [PATCH 08/24] feat(runtime): mod de apoyo para NeoForge 1.21.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo proyecto mod-runtime/neoforge-1.21.1 (ModDevGradle, Java 21) con la misma tabla y plantillas que el de Forge; comprobado en el código de NeoForge el evento de chat (ClientChatReceivedEvent, System y overlay) y el de tooltips. - La app lo reconoce como compatible con NeoForge 1.21.1 y lo instala o quita de mods/. - scripts/construir_mod.py compila cada proyecto con su Java y su Gradle, y puede usar un archivo de hosts solo para Java si el DNS del equipo falla con algunos servidores. - El CI instala Java 17 y 21 y compila los dos mods con sus pruebas. --- .github/workflows/ci.yml | 9 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- README.md | 4 +- mod-runtime/neoforge-1.21.1/.gitignore | 7 + mod-runtime/neoforge-1.21.1/build.gradle | 76 +++++ mod-runtime/neoforge-1.21.1/gradle.properties | 15 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + mod-runtime/neoforge-1.21.1/gradlew | 251 +++++++++++++++ mod-runtime/neoforge-1.21.1/gradlew.bat | 94 ++++++ mod-runtime/neoforge-1.21.1/settings.gradle | 11 + .../consterna/modpackes/ModpackESRuntime.java | 120 ++++++++ .../consterna/modpackes/Reescritor.java | 79 +++++ .../io/github/consterna/modpackes/Tabla.java | 286 ++++++++++++++++++ .../templates/META-INF/neoforge.mods.toml | 25 ++ .../consterna/modpackes/ReescritorTest.java | 76 +++++ .../github/consterna/modpackes/TablaTest.java | 142 +++++++++ scripts/construir_mod.py | 76 +++-- src/traductor_es/modulos/cobertura.py | 2 +- src/traductor_es/salida/mod_apoyo.py | 5 +- src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 9 +- 23 files changed, 1259 insertions(+), 43 deletions(-) create mode 100644 mod-runtime/neoforge-1.21.1/.gitignore create mode 100644 mod-runtime/neoforge-1.21.1/build.gradle create mode 100644 mod-runtime/neoforge-1.21.1/gradle.properties create mode 100644 mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/neoforge-1.21.1/gradlew create mode 100644 mod-runtime/neoforge-1.21.1/gradlew.bat create mode 100644 mod-runtime/neoforge-1.21.1/settings.gradle create mode 100644 mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java create mode 100644 mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java create mode 100644 mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml create mode 100644 mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95c8c9e..19f3af9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,16 +52,15 @@ jobs: - name: Puerta de calidad (ruff, pyright y pytest con el validador Java) run: uv run python scripts/verificar.py - - id: java17 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin java-version: "17" # ForgeGradle 6 (Forge 1.20.1) pide Java 17 - - name: Compilar el mod de apoyo (ModpackES Runtime) con sus pruebas + # Con las dos versiones instaladas (17 aquí y 21 más arriba), el script elige la que pide cada mod + # (Forge 1.20.1: Java 17; NeoForge 1.21.1: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. + - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py - env: - JAVA_HOME: ${{ steps.java17.outputs.path }} - name: Construir ModpackES.exe run: uv run python scripts/construir_exe.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 39082e9..6aeaf04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.20.1 y NeoForge 1.21.1) las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 013353f..35da40e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,10 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -`mod-runtime/forge-1.20.1/` es un mod de Forge solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Se compila con Gradle y ForgeGradle 6 y necesita un JDK 17: +`mod-runtime/forge-1.20.1/` y `mod-runtime/neoforge-1.21.1/` son mods solo de cliente (con la misma lógica de tabla y plantillas) que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.20.1 se compila con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21: ``` -uv run python scripts/construir_mod.py +uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1] ``` Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` y una entrada en `SOPORTADOS` de `salida/mod_apoyo.py`. diff --git a/README.md b/README.md index bf87d04..955f047 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1 por ahora) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1 y NeoForge 1.21.1) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1 y NeoForge 1.21.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/neoforge-1.21.1/.gitignore b/mod-runtime/neoforge-1.21.1/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/neoforge-1.21.1/build.gradle b/mod-runtime/neoforge-1.21.1/build.gradle new file mode 100644 index 0000000..79f20cd --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/build.gradle @@ -0,0 +1,76 @@ +plugins { + id 'java-library' + id 'net.neoforged.moddev' version '2.0.148' + id 'idea' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-neoforge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +repositories { +} + +neoForge { + version = project.neo_version + + runs { + client { + client() + } + } + + mods { + "${mod_id}" { + sourceSet(sourceSets.main) + } + } + + unitTest { + enable() + testedMod = mods."${mod_id}" + } +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description, + ] + inputs.properties replaceProperties + expand replaceProperties + from "src/main/templates" + into "build/generated/sources/modMetadata" +} +sourceSets.main.resources.srcDir generateModMetadata +neoForge.ideSyncTask generateModMetadata + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/neoforge-1.21.1/gradle.properties b/mod-runtime/neoforge-1.21.1/gradle.properties new file mode 100644 index 0000000..2737e21 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/gradle.properties @@ -0,0 +1,15 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.21.1 +minecraft_version_range=[1.21.1] +neo_version=21.1.265 +loader_version_range=[1,) + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/neoforge-1.21.1/gradlew.bat b/mod-runtime/neoforge-1.21.1/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/neoforge-1.21.1/settings.gradle b/mod-runtime/neoforge-1.21.1/settings.gradle new file mode 100644 index 0000000..a2d1087 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..3ed09d7 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,120 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientChatReceivedEvent; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES (o encaje en una de sus plantillas con {@code %s} o {@code %d}). Si el juego no está en una + * variante de español, no hace nada. + */ +@Mod(value = ModpackESRuntime.ID, dist = Dist.CLIENT) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime(IEventBus busDelMod) { + busDelMod.addListener(ModpackESRuntime::alRegistrarRecargas); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + } + + private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { + evento.registerReloadListener(new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", nueva.tamano(), nueva.plantillas()); + } + } +} diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java new file mode 100644 index 0000000..d6beacd --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -0,0 +1,79 @@ +package io.github.consterna.modpackes; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.PlainTextContents; +import net.minecraft.network.chat.contents.TranslatableContents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni + * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + */ +public final class Reescritor { + private Reescritor() { + } + + /** El componente traducido, o {@code null} si no cambió nada (así no se copia lo que no hace falta). */ + public static Component reescribir(Component original, Tabla tabla) { + if (original == null || tabla.vacia()) { + return null; + } + ComponentContents contenido = original.getContents(); + ComponentContents nuevoContenido = contenido; + boolean cambio = false; + + if (contenido instanceof PlainTextContents.LiteralContents literal) { + String traducido = tabla.traducir(literal.text()); + if (traducido != null) { + nuevoContenido = new PlainTextContents.LiteralContents(traducido); + cambio = true; + } + } else if (contenido instanceof TranslatableContents traducible) { + Object[] argumentos = traducible.getArgs(); + Object[] nuevos = null; + for (int i = 0; i < argumentos.length; i++) { + Object reemplazo = null; + if (argumentos[i] instanceof Component hijo) { + reemplazo = reescribir(hijo, tabla); + } else if (argumentos[i] instanceof String texto) { + reemplazo = tabla.traducir(texto); + } + if (reemplazo != null) { + if (nuevos == null) { + nuevos = argumentos.clone(); + } + nuevos[i] = reemplazo; + } + } + if (nuevos != null) { + nuevoContenido = new TranslatableContents(traducible.getKey(), traducible.getFallback(), nuevos); + cambio = true; + } + } + + List hermanos = original.getSiblings(); + List nuevosHermanos = null; + for (int i = 0; i < hermanos.size(); i++) { + Component reemplazo = reescribir(hermanos.get(i), tabla); + if (reemplazo != null) { + if (nuevosHermanos == null) { + nuevosHermanos = new ArrayList<>(hermanos); + } + nuevosHermanos.set(i, reemplazo); + } + } + + if (!cambio && nuevosHermanos == null) { + return null; + } + MutableComponent resultado = MutableComponent.create(nuevoContenido).setStyle(original.getStyle()); + for (Component hermano : nuevosHermanos != null ? nuevosHermanos : hermanos) { + resultado.append(hermano); + } + return resultado; + } +} diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java new file mode 100644 index 0000000..2cce7ce --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java @@ -0,0 +1,286 @@ +package io.github.consterna.modpackes; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Tabla de textos «inglés → español» que genera ModpackES. Hay dos clases de entrada: + *
    + *
  • Exactas: solo coinciden si el texto es idéntico (salvo los espacios de los bordes, que se conservan).
  • + *
  • Plantillas: la clave lleva {@code %s} o {@code %d} (como el {@code String.format} del mod original) y coinciden + * con cualquier texto que sea ese mismo molde con otros datos: «Hello %s!» coincide con «Hello Steve!». El español + * reutiliza lo capturado; puede llevar {@code %1$s}, {@code %2$s} para cambiar el orden.
  • + *
+ * No hay traducción aproximada ni en vivo. + */ +public final class Tabla { + public static final Tabla VACIA = new Tabla(Map.of()); + + private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); + private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa + private static final int TAMANO_CACHE = 512; + private static final String SIN_COINCIDENCIA = "\u0000"; + + private final Map exactos; + private final List plantillas; + private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > TAMANO_CACHE; + } + }; + + public Tabla(Map textos) { + Map exactos = new HashMap<>(); + List plantillas = new ArrayList<>(); + for (Map.Entry e : textos.entrySet()) { + if (esPlantilla(e.getKey())) { + Plantilla p = Plantilla.crear(e.getKey(), e.getValue()); + if (p != null) { + plantillas.add(p); + } + } else { + exactos.put(e.getKey(), e.getValue()); + } + } + plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero + this.exactos = Map.copyOf(exactos); + this.plantillas = List.copyOf(plantillas); + } + + /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ + public static Tabla leer(Reader lector) throws IOException { + JsonElement raiz; + try { + raiz = JsonParser.parseReader(lector); + } catch (JsonParseException e) { + throw new IOException("La tabla de textos no es JSON válido", e); + } + if (!raiz.isJsonObject()) { + throw new IOException("La tabla de textos no es un objeto JSON"); + } + JsonObject objeto = raiz.getAsJsonObject(); + Map mapa = new HashMap<>(); + for (Map.Entry e : objeto.entrySet()) { + JsonElement v = e.getValue(); + if (v.isJsonPrimitive() && v.getAsJsonPrimitive().isString() && !v.getAsString().isEmpty()) { + mapa.put(e.getKey(), v.getAsString()); + } + } + return new Tabla(mapa); + } + + /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ + public Tabla combinar(Tabla otra) { + if (otra.vacia()) { + return this; + } + Map mapa = new HashMap<>(this.comoMapa()); + mapa.putAll(otra.comoMapa()); + return new Tabla(mapa); + } + + private Map comoMapa() { + Map mapa = new HashMap<>(exactos); + for (Plantilla p : plantillas) { + mapa.put(p.clave, p.valor); + } + return mapa; + } + + public boolean vacia() { + return exactos.isEmpty() && plantillas.isEmpty(); + } + + public int tamano() { + return exactos.size() + plantillas.size(); + } + + public int plantillas() { + return plantillas.size(); + } + + /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ + public String traducir(String texto) { + if (texto == null || texto.isEmpty() || vacia()) { + return null; + } + String directo = buscar(texto); + if (directo != null) { + return directo.equals(texto) ? null : directo; + } + String nucleo = texto.strip(); + if (nucleo.isEmpty() || nucleo.length() == texto.length()) { + return null; + } + String traduccion = buscar(nucleo); + if (traduccion == null || traduccion.equals(nucleo)) { + return null; + } + int inicio = texto.length() - texto.stripLeading().length(); + int fin = texto.length() - texto.stripTrailing().length(); + return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); + } + + private String buscar(String texto) { + String exacto = exactos.get(texto); + if (exacto != null) { + return exacto; + } + if (plantillas.isEmpty()) { + return null; + } + synchronized (cache) { // los tooltips piden el mismo texto cada cuadro: se recuerda el resultado + String guardado = cache.get(texto); + if (guardado != null) { + return guardado.equals(SIN_COINCIDENCIA) ? null : guardado; + } + String resultado = null; + for (Plantilla p : plantillas) { + resultado = p.aplicar(texto); + if (resultado != null) { + break; + } + } + cache.put(texto, resultado == null ? SIN_COINCIDENCIA : resultado); + return resultado; + } + } + + private static boolean esPlantilla(String clave) { + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + if (!m.group(2).equals("%")) { + return true; + } + } + return false; + } + + /** Un molde con datos variables: «Hello %s!» → «¡Hola %s!». */ + private static final class Plantilla { + final String clave; + final String valor; + final String fijo; // el texto de la clave sin los marcadores + final String prefijo; // lo que va antes del primer marcador: descarta rápido lo que no coincide + final Pattern patron; + final List destino; // trozos de texto y números de grupo (Integer, desde 1) + + private Plantilla(String clave, String valor, String fijo, String prefijo, Pattern patron, List destino) { + this.clave = clave; + this.valor = valor; + this.fijo = fijo; + this.prefijo = prefijo; + this.patron = patron; + this.destino = destino; + } + + /** {@code null} si la plantilla no es segura (poco texto fijo o marcadores que no cuadran). */ + static Plantilla crear(String clave, String valor) { + StringBuilder regex = new StringBuilder("^"); + StringBuilder fijo = new StringBuilder(); + String prefijo = null; + int grupos = 0; + int ultimo = 0; + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + String tramo = clave.substring(ultimo, m.start()); + fijo.append(tramo); + if (!tramo.isEmpty()) { + regex.append(Pattern.quote(tramo)); + } + if (m.group(2).equals("%")) { + fijo.append('%'); + regex.append(Pattern.quote("%")); + } else { + if (prefijo == null) { + prefijo = clave.substring(0, m.start()); + } + grupos++; + regex.append(m.group(2).equals("d") ? "(-?\\d+)" : "(.+?)"); + } + ultimo = m.end(); + } + String cola = clave.substring(ultimo); + fijo.append(cola); + if (!cola.isEmpty()) { + regex.append(Pattern.quote(cola)); + } + regex.append('$'); + if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { + return null; + } + List destino = destino(valor, grupos); + if (destino == null) { + return null; + } + return new Plantilla(clave, valor, fijo.toString(), prefijo == null ? "" : prefijo, + Pattern.compile(regex.toString(), Pattern.DOTALL), destino); + } + + private static List destino(String valor, int grupos) { + List partes = new ArrayList<>(); + Matcher m = MARCADOR.matcher(valor); + int ultimo = 0; + int siguiente = 1; + boolean[] usado = new boolean[grupos + 1]; + while (m.find()) { + if (m.start() > ultimo) { + partes.add(valor.substring(ultimo, m.start())); + } + if (m.group(2).equals("%")) { + partes.add("%"); + } else { + int indice = m.group(1) != null ? Integer.parseInt(m.group(1)) : siguiente++; + if (indice < 1 || indice > grupos) { + return null; + } + usado[indice] = true; + partes.add(indice); + } + ultimo = m.end(); + } + if (ultimo < valor.length()) { + partes.add(valor.substring(ultimo)); + } + for (int i = 1; i <= grupos; i++) { + if (!usado[i]) { + return null; // la traducción perdió un dato: mejor no usarla + } + } + return partes; + } + + String aplicar(String texto) { + if (!prefijo.isEmpty() && !texto.startsWith(prefijo)) { + return null; + } + Matcher m = patron.matcher(texto); + if (!m.matches()) { + return null; + } + StringBuilder salida = new StringBuilder(); + for (Object parte : destino) { + if (parte instanceof Integer indice) { + salida.append(m.group(indice)); + } else { + salida.append((String) parte); + } + } + return salida.toString(); + } + } +} diff --git a/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml b/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..e25d781 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +# Solo cliente: la clase principal se declara con dist = CLIENT, así que un servidor no la carga. +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="[${neo_version},)" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..3e93f7d --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,76 @@ +package io.github.consterna.modpackes; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.contents.TranslatableContents; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static final Tabla TABLA = new Tabla(Map.of( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí")); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(ChatFormatting.RED).withBold(true); + Component original = Component.literal("TPA request expired!").setStyle(estilo); + Component nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(Component.literal("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(Component.literal("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + MutableComponent raiz = Component.literal(" ").withStyle(ChatFormatting.AQUA); + raiz.append(Component.literal("Hello").withStyle(ChatFormatting.WHITE)); + raiz.append(Component.literal(" Steve")); + Component nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com")); + Component nuevo = Reescritor.reescribir(Component.literal("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals("https://example.com", nuevo.getStyle().getClickEvent().getValue()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + Component original = Component.translatable("chat.type.text", Component.literal("Hello"), "Hello"); + Component nuevo = Reescritor.reescribir(original, TABLA); + TranslatableContents contenido = (TranslatableContents) nuevo.getContents(); + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((Component) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = new Tabla(Map.of("Ñandú says hi", "Ñandú dice ¡hola! 😀")); + Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } +} diff --git a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java new file mode 100644 index 0000000..a9a7e52 --- /dev/null +++ b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java @@ -0,0 +1,142 @@ +package io.github.consterna.modpackes; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TablaTest { + private static Tabla tabla(String... pares) { + Map mapa = new java.util.HashMap<>(); + for (int i = 0; i < pares.length; i += 2) { + mapa.put(pares[i], pares[i + 1]); + } + return new Tabla(mapa); + } + + @Test + void coincidenciaExactaYSinCoincidencia() { + Tabla t = tabla("TPA request expired!", "¡La solicitud de TPA expiró!"); + assertEquals("¡La solicitud de TPA expiró!", t.traducir("TPA request expired!")); + assertNull(t.traducir("tpa request expired!")); // mayúsculas distintas: no es el mismo texto + assertNull(t.traducir("TPA request expired! Now")); + assertNull(t.traducir("")); + assertNull(t.traducir(null)); + } + + @Test + void conservaLosEspaciosDeLosBordes() { + Tabla t = tabla("Hello there", "Hola"); + assertEquals(" Hola\n", t.traducir(" Hello there\n")); + assertNull(t.traducir(" ")); + } + + @Test + void unTextoIgualQueSuTraduccionNoCuentaComoCambio() { + Tabla t = tabla("Creeper", "Creeper"); + assertNull(t.traducir("Creeper")); + } + + @Test + void conservaTildesEneYEmoji() { + Tabla t = tabla("Pick 😀", "Elige ñandú ¡sí! 日本語 §a%s"); + assertEquals("Elige ñandú ¡sí! 日本語 §a%s", t.traducir("Pick 😀")); + } + + @Test + void leeJsonValidoEIgnoraLoQueNoEsTexto() throws IOException { + Tabla t = Tabla.leer(new StringReader("{\"a b\": \"uno\", \"n\": 5, \"v\": \"\", \"l\": [\"x\"], \"z\": null}")); + assertEquals(1, t.tamano()); + assertEquals("uno", t.traducir("a b")); + } + + @Test + void jsonRotoOQueNoEsObjetoFallaConIOException() { + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("{no es json"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("[1, 2]"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("\"texto\""))); + } + + @Test + void combinarDejaMandarALaSegunda() { + Tabla total = tabla("a b", "viejo", "c d", "queda").combinar(tabla("a b", "nuevo")); + assertEquals("nuevo", total.traducir("a b")); + assertEquals("queda", total.traducir("c d")); + assertTrue(Tabla.VACIA.vacia()); + assertFalse(total.vacia()); + assertEquals(total, total.combinar(Tabla.VACIA)); + } + + @Test + void plantillaConUnDatoVariable() { + Tabla t = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!"); + assertEquals("¡Hola Steve, bienvenido de nuevo!", t.traducir("Hello Steve, welcome back!")); + assertEquals("¡Hola Ñandú 😀, bienvenido de nuevo!", t.traducir("Hello Ñandú 😀, welcome back!")); + assertNull(t.traducir("Goodbye Steve, welcome back!")); + assertNull(t.traducir("Hello Steve")); + } + + @Test + void plantillaConNumeroSoloCoincideConNumeros() { + Tabla t = tabla("You have %d items left", "Te quedan %d objetos"); + assertEquals("Te quedan 5 objetos", t.traducir("You have 5 items left")); + assertEquals("Te quedan -3 objetos", t.traducir("You have -3 items left")); + assertNull(t.traducir("You have many items left")); + } + + @Test + void plantillaPuedeCambiarElOrden() { + Tabla t = tabla("%s gave %d coins", "%2$d monedas de %1$s"); + assertEquals("7 monedas de Laly", t.traducir("Laly gave 7 coins")); + } + + @Test + void plantillaConBordesYPorcentajeLiteral() { + Tabla t = tabla("Progress: %d%% of %s", "Progreso: %d%% de %s"); + assertEquals(" Progreso: 50% de la misión ", t.traducir(" Progress: 50% of la misión ")); + } + + @Test + void plantillasInseguras() { + // sin texto fijo: coincidiría con todo + assertNull(tabla("%s", "%s").traducir("cualquier cosa")); + assertNull(tabla("%s %s", "%s %s").traducir("uno dos")); + // la traducción pierde un dato o inventa uno + assertNull(tabla("Hello %s and %s here", "Hola %s aquí").traducir("Hello A and B here")); + assertNull(tabla("Hello %s there friend", "Hola %s y %s").traducir("Hello A there friend")); + assertNull(tabla("Hello %s there friend", "Hola %3$s").traducir("Hello A there friend")); + } + + @Test + void laPlantillaMasEspecificaGana() { + Tabla t = tabla( + "Welcome %s", "Bienvenido %s", + "Welcome %s to the farm", "Bienvenido %s a la granja"); + assertEquals("Bienvenido Steve a la granja", t.traducir("Welcome Steve to the farm")); + assertEquals("Bienvenido Steve", t.traducir("Welcome Steve")); + } + + @Test + void caracteresDeRegexEnElTextoFijoNoSonEspeciales() { + Tabla t = tabla("Cost (%d) [gold]. Really?", "Costo (%d) [oro]. ¿En serio?"); + assertEquals("Costo (9) [oro]. ¿En serio?", t.traducir("Cost (9) [gold]. Really?")); + assertNull(t.traducir("Cost 9 gold. Really?")); + } + + @Test + void lasPlantillasSobrevivenAlCombinarYSeCuentan() { + Tabla a = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!", "Exact text here", "Texto exacto"); + Tabla total = a.combinar(tabla("Another %d things here", "Otras %d cosas aquí")); + assertEquals(3, total.tamano()); + assertEquals(2, total.plantillas()); + assertEquals("Otras 3 cosas aquí", total.traducir("Another 3 things here")); + assertEquals("Texto exacto", total.traducir("Exact text here")); + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 33b7a7b..50f2785 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -1,11 +1,11 @@ """Compila el mod de apoyo (ModpackES Runtime) y deja el jar donde la aplicación lo busca. -Uso: uv run python scripts/construir_mod.py [--sin-pruebas] +Uso: uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | todos] [--sin-pruebas] -Necesita un JDK 17 (la variable JAVA_HOME, o la carpeta `.herramientas/jdk-17*` si existe) y acceso a internet la -primera vez, para bajar Gradle, Forge y Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git -ignora. -En GitHub Actions el jar lo construye el mismo comando (`.github/workflows/release.yml`). +Sin nombre compila todos. Cada uno necesita su JDK (Forge 1.20.1: 17; NeoForge 1.21.1: 21), que se toma de JAVA_HOME +o de `.herramientas/jdk-*` si existe, y acceso a internet la primera vez, para bajar Gradle, el loader y +Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo +construye el mismo comando (`.github/workflows/ci.yml`). """ from __future__ import annotations @@ -18,8 +18,9 @@ from pathlib import Path RAIZ = Path(__file__).resolve().parent.parent -PROYECTO = RAIZ / "mod-runtime" / "forge-1.20.1" DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" +#: proyecto → versión de Java que pide su compilación +PROYECTOS = {"forge-1.20.1": 17, "neoforge-1.21.1": 21} def _herramientas() -> Path: @@ -33,41 +34,50 @@ def _herramientas() -> Path: HERRAMIENTAS = _herramientas() -def _jdk17() -> Path: - candidatos = [Path(os.environ["JAVA_HOME"])] if os.environ.get("JAVA_HOME") else [] - candidatos += sorted(HERRAMIENTAS.glob("jdk-17*")) +def _jdk(version: int) -> Path: + ejecutable = "java.exe" if os.name == "nt" else "java" + candidatos = [Path(os.environ[v]) for v in ("JAVA_HOME", f"JAVA_HOME_{version}_X64") if os.environ.get(v)] + candidatos += sorted(HERRAMIENTAS.glob(f"jdk-{version}*")) for c in candidatos: - if (c / "bin" / ("java.exe" if os.name == "nt" else "java")).is_file(): + if (c / "bin" / ejecutable).is_file() and f'"{version}.' in _version_de_java(c / "bin" / ejecutable): return c - raise SystemExit("No encuentro un JDK 17: define JAVA_HOME o ponlo en .herramientas/jdk-17*.") + raise SystemExit(f"No encuentro un JDK {version}: define JAVA_HOME o ponlo en .herramientas/jdk-{version}*.") -def _gradle() -> list[str]: - envoltorio = PROYECTO / ("gradlew.bat" if os.name == "nt" else "gradlew") - local = sorted(HERRAMIENTAS.glob("gradle-*/bin/gradle" + (".bat" if os.name == "nt" else ""))) - return [str(local[-1])] if local else [str(envoltorio)] +def _version_de_java(java: Path) -> str: + return subprocess.run([str(java), "-version"], capture_output=True, text=True, check=False).stderr -def main() -> int: - ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("--sin-pruebas", action="store_true", help="compila sin correr las pruebas de Java") - args = ap.parse_args() - jdk = _jdk17() +def _gradle(proyecto: Path) -> list[str]: + envoltorio = proyecto / ("gradlew.bat" if os.name == "nt" else "gradlew") + mayor = "8" if proyecto.name.startswith("forge") else "9" # ForgeGradle 6 va con Gradle 8; NeoForge con Gradle 9 + local = sorted(HERRAMIENTAS.glob(f"gradle-{mayor}*/bin/gradle" + (".bat" if os.name == "nt" else ""))) + return [str(local[-1])] if local else [str(envoltorio)] # el de .herramientas si está; si no, el del proyecto + + +def construir(nombre: str, sin_pruebas: bool) -> int: + proyecto = RAIZ / "mod-runtime" / nombre + jdk = _jdk(PROYECTOS[nombre]) entorno = {**os.environ, "JAVA_HOME": str(jdk), "PATH": str(jdk / "bin") + os.pathsep + os.environ["PATH"]} - if HERRAMIENTAS.is_dir(): # la caché de Gradle y de Forge queda junto al proyecto y no en la carpeta del usuario + hosts = HERRAMIENTAS / "hosts-java.txt" + if hosts.is_file(): + # Solo para Java y solo en esta compilación: ayuda si el DNS del equipo falla con algunos servidores. Va en + # JAVA_TOOL_OPTIONS porque así llega a todos los Java que se lanzan (cliente de Gradle, su proceso y ayudantes). + entorno["JAVA_TOOL_OPTIONS"] = f"{entorno.get('JAVA_TOOL_OPTIONS', '')} -Djdk.net.hosts.file={hosts}".strip() + if HERRAMIENTAS.is_dir(): # la caché de Gradle y del loader queda junto al proyecto y no en la carpeta del usuario entorno.setdefault("GRADLE_USER_HOME", str(HERRAMIENTAS / "gradle-home")) - orden = [*_gradle(), "build", "--no-daemon", "--console=plain"] - if args.sin_pruebas: + orden = [*_gradle(proyecto), "build", "--no-daemon", "--console=plain"] + if sin_pruebas: orden += ["-x", "test"] - codigo = subprocess.run(orden, cwd=PROYECTO, env=entorno, check=False).returncode + codigo = subprocess.run(orden, cwd=proyecto, env=entorno, check=False).returncode if codigo: return codigo - jars = sorted((PROYECTO / "build" / "libs").glob("modpackes_runtime-*.jar")) + jars = sorted((proyecto / "build" / "libs").glob("modpackes_runtime-*.jar")) if not jars: - print("La compilación terminó pero no hay jar en build/libs.", file=sys.stderr) + print(f"{nombre}: la compilación terminó pero no hay jar en build/libs.", file=sys.stderr) return 1 DESTINO.mkdir(parents=True, exist_ok=True) - for viejo in DESTINO.glob("modpackes_runtime-*.jar"): + for viejo in DESTINO.glob(f"modpackes_runtime-{nombre}-*.jar"): viejo.unlink() final = DESTINO / jars[-1].name shutil.copyfile(jars[-1], final) @@ -75,5 +85,17 @@ def main() -> int: return 0 +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("proyecto", nargs="?", default="todos", choices=[*PROYECTOS, "todos"]) + ap.add_argument("--sin-pruebas", action="store_true", help="compila sin correr las pruebas de Java") + args = ap.parse_args() + elegidos = list(PROYECTOS) if args.proyecto == "todos" else [args.proyecto] + for nombre in elegidos: + if (codigo := construir(nombre, args.sin_pruebas)) != 0: + return codigo + return 0 + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index 8fbf55e..79732fa 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -70,7 +70,7 @@ def __str__(self) -> str: _TITULOS = { - "java": "Escrito en el código de los mods (un resource pack no lo alcanza; el mod de apoyo cubre Forge 1.20.1)", + "java": "Escrito en el código de los mods (un resource pack no lo alcanza; el mod de apoyo cubre Forge y NeoForge)", "script": "Frases de scripts que parecen texto y no se tocaron (pueden ser lógica o un mensaje armado distinto)", "formato": "Carpetas con frases en inglés cuyo formato la app no conoce (no se tocó ningún archivo)", } diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index bf97f93..926549a 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -15,7 +15,10 @@ PREFIJO = "modpackes_runtime-" #: (loader, versión de Minecraft) para los que hay mod de apoyo. Cada entrada es un proyecto en `mod-runtime/`. -SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = {("forge", (1, 20, 1)): "forge-1.20.1"} +SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { + ("forge", (1, 20, 1)): "forge-1.20.1", + ("neoforge", (1, 21, 1)): "neoforge-1.21.1", +} _NOMBRE = re.compile(rf"{re.escape(PREFIJO)}(?P[a-z]+-[\d.]+)-(?P[\w.+-]+)\.jar") diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 5a3df50..0a1f8b3 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,7 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Solo Forge 1.20.1 por ahora." + "Por ahora existe para Forge 1.20.1 y NeoForge 1.21.1." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index cd8f530..db57898 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -110,8 +110,11 @@ def _jars(tmp_path: Path, *nombres: str) -> Path: def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: casos = [ ("1.20.1", "forge", "forge-1.20.1"), + ("1.21.1", "neoforge", "neoforge-1.21.1"), ("1.20.1", "fabric", None), - ("1.21.1", "neoforge", None), + ("1.21.1", "fabric", None), + ("1.21.4", "neoforge", None), + ("1.20.1", "neoforge", None), ("1.12.2", "forge", None), ] for i, (version, loader, esperado) in enumerate(casos): @@ -144,7 +147,7 @@ def test_instalar_con_errores_claros(tmp_path: Path) -> None: def test_cli_instalar_runtime_sin_soporte(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - juego = fabrica.prism(tmp_path, "1.21.1", "neoforge") + juego = fabrica.prism(tmp_path, "1.21.4", "neoforge") assert main(["instalar-runtime", str(juego.parent)]) != 0 assert "Todavía no hay mod de apoyo" in capsys.readouterr().out # los errores van por la salida normal assert main(["instalar-runtime", str(juego.parent), "--quitar"]) == 0 @@ -183,7 +186,7 @@ def traducir_lote(self, lote: Lote) -> dict[str, str]: def test_estado_del_mod_de_apoyo(tmp_path: Path) -> None: - sin_soporte = fabrica.prism(tmp_path / "a", "1.21.1", "neoforge") + sin_soporte = fabrica.prism(tmp_path / "a", "1.21.4", "neoforge") with Instancia(sin_soporte) as inst: assert "No hay mod de apoyo" in mod_apoyo.estado(inst)[0] juego = fabrica.prism(tmp_path / "b", "1.20.1", "forge") From 1c525437593af21031a6bd6f6034f56e36162a38 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sat, 7 Nov 2026 22:35:15 -0500 Subject: [PATCH 09/24] feat(runtime): mod de apoyo para Fabric 1.20.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tercer loader del mod de apoyo (client-only): mensajes del juego con ClientReceiveMessageEvents.MODIFY_GAME, tooltips con ItemTooltipCallback y la tabla se recarga con un SimpleSynchronousResourceReloadListener. Misma lógica de tabla y plantillas que Forge y NeoForge, con sus 21 pruebas JUnit. Se compila con Loom 1.17 y Java 21 (la 1.19 pide Java 25), a bytecode de Java 17. La app lo instala en instancias Fabric 1.20.1; el script de construcción, el CI y la documentación lo incluyen. --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- README.md | 4 +- mod-runtime/fabric-1.20.1/.gitignore | 7 + mod-runtime/fabric-1.20.1/build.gradle | 54 ++++ mod-runtime/fabric-1.20.1/gradle.properties | 16 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.20.1/gradlew | 248 +++++++++++++++ mod-runtime/fabric-1.20.1/gradlew.bat | 112 +++++++ mod-runtime/fabric-1.20.1/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 113 +++++++ .../consterna/modpackes/Reescritor.java | 79 +++++ .../io/github/consterna/modpackes/Tabla.java | 286 ++++++++++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ .../consterna/modpackes/ReescritorTest.java | 76 +++++ .../github/consterna/modpackes/TablaTest.java | 142 +++++++++ scripts/construir_mod.py | 30 +- src/traductor_es/modulos/cobertura.py | 5 +- src/traductor_es/salida/mod_apoyo.py | 1 + src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 4 +- 23 files changed, 1208 insertions(+), 22 deletions(-) create mode 100644 mod-runtime/fabric-1.20.1/.gitignore create mode 100644 mod-runtime/fabric-1.20.1/build.gradle create mode 100644 mod-runtime/fabric-1.20.1/gradle.properties create mode 100644 mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.20.1/gradlew create mode 100644 mod-runtime/fabric-1.20.1/gradlew.bat create mode 100644 mod-runtime/fabric-1.20.1/settings.gradle create mode 100644 mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java create mode 100644 mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java create mode 100644 mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19f3af9..37321bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: java-version: "17" # ForgeGradle 6 (Forge 1.20.1) pide Java 17 # Con las dos versiones instaladas (17 aquí y 21 más arriba), el script elige la que pide cada mod - # (Forge 1.20.1: Java 17; NeoForge 1.21.1: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. + # (Forge 1.20.1: Java 17; NeoForge 1.21.1 y Fabric 1.20.1: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aeaf04..801f807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.20.1 y NeoForge 1.21.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35da40e..fe23dde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,10 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -`mod-runtime/forge-1.20.1/` y `mod-runtime/neoforge-1.21.1/` son mods solo de cliente (con la misma lógica de tabla y plantillas) que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.20.1 se compila con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21: +`mod-runtime/forge-1.20.1/`, `mod-runtime/neoforge-1.21.1/` y `mod-runtime/fabric-1.20.1/` son mods solo de cliente (con la misma lógica de tabla y plantillas) que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.20.1 se compila con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric 1.20.1, con Loom 1.17, Gradle 9 y Java 21 (el mod queda compilado para Java 17): ``` -uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1] +uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | fabric-1.20.1] ``` Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` y una entrada en `SOPORTADOS` de `salida/mod_apoyo.py`. diff --git a/README.md b/README.md index 955f047..5eb2321 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1 y NeoForge 1.21.1) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1 y NeoForge 1.21.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/fabric-1.20.1/.gitignore b/mod-runtime/fabric-1.20.1/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.20.1/build.gradle b/mod-runtime/fabric-1.20.1/build.gradle new file mode 100644 index 0000000..757e643 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 17 // Minecraft 1.20.1 funciona con Java 17 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.20.1/gradle.properties b/mod-runtime/fabric-1.20.1/gradle.properties new file mode 100644 index 0000000..0e6147d --- /dev/null +++ b/mod-runtime/fabric-1.20.1/gradle.properties @@ -0,0 +1,16 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Versiones del ejemplo oficial de Fabric para 1.20.1 (FabricMC/fabric-example-mod, rama 1.20.1). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25); el mod se compila a Java 17. +minecraft_version=1.20.1 +loader_version=0.20.1 +loom_version=1.17.21 +fabric_api_version=0.92.12+1.20.1 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.20.1/gradlew.bat b/mod-runtime/fabric-1.20.1/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.20.1/settings.gradle b/mod-runtime/fabric-1.20.1/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..1e71131 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,113 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

El chat de otros jugadores no se toca: Fabric API 1.20.1 no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, lineas) -> alCrearTooltip(lineas)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return new ResourceLocation(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java new file mode 100644 index 0000000..577996e --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -0,0 +1,79 @@ +package io.github.consterna.modpackes; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.LiteralContents; +import net.minecraft.network.chat.contents.TranslatableContents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni + * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + */ +public final class Reescritor { + private Reescritor() { + } + + /** El componente traducido, o {@code null} si no cambió nada (así no se copia lo que no hace falta). */ + public static Component reescribir(Component original, Tabla tabla) { + if (original == null || tabla.vacia()) { + return null; + } + ComponentContents contenido = original.getContents(); + ComponentContents nuevoContenido = contenido; + boolean cambio = false; + + if (contenido instanceof LiteralContents literal) { + String traducido = tabla.traducir(literal.text()); + if (traducido != null) { + nuevoContenido = new LiteralContents(traducido); + cambio = true; + } + } else if (contenido instanceof TranslatableContents traducible) { + Object[] argumentos = traducible.getArgs(); + Object[] nuevos = null; + for (int i = 0; i < argumentos.length; i++) { + Object reemplazo = null; + if (argumentos[i] instanceof Component hijo) { + reemplazo = reescribir(hijo, tabla); + } else if (argumentos[i] instanceof String texto) { + reemplazo = tabla.traducir(texto); + } + if (reemplazo != null) { + if (nuevos == null) { + nuevos = argumentos.clone(); + } + nuevos[i] = reemplazo; + } + } + if (nuevos != null) { + nuevoContenido = new TranslatableContents(traducible.getKey(), traducible.getFallback(), nuevos); + cambio = true; + } + } + + List hermanos = original.getSiblings(); + List nuevosHermanos = null; + for (int i = 0; i < hermanos.size(); i++) { + Component reemplazo = reescribir(hermanos.get(i), tabla); + if (reemplazo != null) { + if (nuevosHermanos == null) { + nuevosHermanos = new ArrayList<>(hermanos); + } + nuevosHermanos.set(i, reemplazo); + } + } + + if (!cambio && nuevosHermanos == null) { + return null; + } + MutableComponent resultado = MutableComponent.create(nuevoContenido).setStyle(original.getStyle()); + for (Component hermano : nuevosHermanos != null ? nuevosHermanos : hermanos) { + resultado.append(hermano); + } + return resultado; + } +} diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java new file mode 100644 index 0000000..2cce7ce --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java @@ -0,0 +1,286 @@ +package io.github.consterna.modpackes; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Tabla de textos «inglés → español» que genera ModpackES. Hay dos clases de entrada: + *

    + *
  • Exactas: solo coinciden si el texto es idéntico (salvo los espacios de los bordes, que se conservan).
  • + *
  • Plantillas: la clave lleva {@code %s} o {@code %d} (como el {@code String.format} del mod original) y coinciden + * con cualquier texto que sea ese mismo molde con otros datos: «Hello %s!» coincide con «Hello Steve!». El español + * reutiliza lo capturado; puede llevar {@code %1$s}, {@code %2$s} para cambiar el orden.
  • + *
+ * No hay traducción aproximada ni en vivo. + */ +public final class Tabla { + public static final Tabla VACIA = new Tabla(Map.of()); + + private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); + private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa + private static final int TAMANO_CACHE = 512; + private static final String SIN_COINCIDENCIA = "\u0000"; + + private final Map exactos; + private final List plantillas; + private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > TAMANO_CACHE; + } + }; + + public Tabla(Map textos) { + Map exactos = new HashMap<>(); + List plantillas = new ArrayList<>(); + for (Map.Entry e : textos.entrySet()) { + if (esPlantilla(e.getKey())) { + Plantilla p = Plantilla.crear(e.getKey(), e.getValue()); + if (p != null) { + plantillas.add(p); + } + } else { + exactos.put(e.getKey(), e.getValue()); + } + } + plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero + this.exactos = Map.copyOf(exactos); + this.plantillas = List.copyOf(plantillas); + } + + /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ + public static Tabla leer(Reader lector) throws IOException { + JsonElement raiz; + try { + raiz = JsonParser.parseReader(lector); + } catch (JsonParseException e) { + throw new IOException("La tabla de textos no es JSON válido", e); + } + if (!raiz.isJsonObject()) { + throw new IOException("La tabla de textos no es un objeto JSON"); + } + JsonObject objeto = raiz.getAsJsonObject(); + Map mapa = new HashMap<>(); + for (Map.Entry e : objeto.entrySet()) { + JsonElement v = e.getValue(); + if (v.isJsonPrimitive() && v.getAsJsonPrimitive().isString() && !v.getAsString().isEmpty()) { + mapa.put(e.getKey(), v.getAsString()); + } + } + return new Tabla(mapa); + } + + /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ + public Tabla combinar(Tabla otra) { + if (otra.vacia()) { + return this; + } + Map mapa = new HashMap<>(this.comoMapa()); + mapa.putAll(otra.comoMapa()); + return new Tabla(mapa); + } + + private Map comoMapa() { + Map mapa = new HashMap<>(exactos); + for (Plantilla p : plantillas) { + mapa.put(p.clave, p.valor); + } + return mapa; + } + + public boolean vacia() { + return exactos.isEmpty() && plantillas.isEmpty(); + } + + public int tamano() { + return exactos.size() + plantillas.size(); + } + + public int plantillas() { + return plantillas.size(); + } + + /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ + public String traducir(String texto) { + if (texto == null || texto.isEmpty() || vacia()) { + return null; + } + String directo = buscar(texto); + if (directo != null) { + return directo.equals(texto) ? null : directo; + } + String nucleo = texto.strip(); + if (nucleo.isEmpty() || nucleo.length() == texto.length()) { + return null; + } + String traduccion = buscar(nucleo); + if (traduccion == null || traduccion.equals(nucleo)) { + return null; + } + int inicio = texto.length() - texto.stripLeading().length(); + int fin = texto.length() - texto.stripTrailing().length(); + return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); + } + + private String buscar(String texto) { + String exacto = exactos.get(texto); + if (exacto != null) { + return exacto; + } + if (plantillas.isEmpty()) { + return null; + } + synchronized (cache) { // los tooltips piden el mismo texto cada cuadro: se recuerda el resultado + String guardado = cache.get(texto); + if (guardado != null) { + return guardado.equals(SIN_COINCIDENCIA) ? null : guardado; + } + String resultado = null; + for (Plantilla p : plantillas) { + resultado = p.aplicar(texto); + if (resultado != null) { + break; + } + } + cache.put(texto, resultado == null ? SIN_COINCIDENCIA : resultado); + return resultado; + } + } + + private static boolean esPlantilla(String clave) { + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + if (!m.group(2).equals("%")) { + return true; + } + } + return false; + } + + /** Un molde con datos variables: «Hello %s!» → «¡Hola %s!». */ + private static final class Plantilla { + final String clave; + final String valor; + final String fijo; // el texto de la clave sin los marcadores + final String prefijo; // lo que va antes del primer marcador: descarta rápido lo que no coincide + final Pattern patron; + final List destino; // trozos de texto y números de grupo (Integer, desde 1) + + private Plantilla(String clave, String valor, String fijo, String prefijo, Pattern patron, List destino) { + this.clave = clave; + this.valor = valor; + this.fijo = fijo; + this.prefijo = prefijo; + this.patron = patron; + this.destino = destino; + } + + /** {@code null} si la plantilla no es segura (poco texto fijo o marcadores que no cuadran). */ + static Plantilla crear(String clave, String valor) { + StringBuilder regex = new StringBuilder("^"); + StringBuilder fijo = new StringBuilder(); + String prefijo = null; + int grupos = 0; + int ultimo = 0; + Matcher m = MARCADOR.matcher(clave); + while (m.find()) { + String tramo = clave.substring(ultimo, m.start()); + fijo.append(tramo); + if (!tramo.isEmpty()) { + regex.append(Pattern.quote(tramo)); + } + if (m.group(2).equals("%")) { + fijo.append('%'); + regex.append(Pattern.quote("%")); + } else { + if (prefijo == null) { + prefijo = clave.substring(0, m.start()); + } + grupos++; + regex.append(m.group(2).equals("d") ? "(-?\\d+)" : "(.+?)"); + } + ultimo = m.end(); + } + String cola = clave.substring(ultimo); + fijo.append(cola); + if (!cola.isEmpty()) { + regex.append(Pattern.quote(cola)); + } + regex.append('$'); + if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { + return null; + } + List destino = destino(valor, grupos); + if (destino == null) { + return null; + } + return new Plantilla(clave, valor, fijo.toString(), prefijo == null ? "" : prefijo, + Pattern.compile(regex.toString(), Pattern.DOTALL), destino); + } + + private static List destino(String valor, int grupos) { + List partes = new ArrayList<>(); + Matcher m = MARCADOR.matcher(valor); + int ultimo = 0; + int siguiente = 1; + boolean[] usado = new boolean[grupos + 1]; + while (m.find()) { + if (m.start() > ultimo) { + partes.add(valor.substring(ultimo, m.start())); + } + if (m.group(2).equals("%")) { + partes.add("%"); + } else { + int indice = m.group(1) != null ? Integer.parseInt(m.group(1)) : siguiente++; + if (indice < 1 || indice > grupos) { + return null; + } + usado[indice] = true; + partes.add(indice); + } + ultimo = m.end(); + } + if (ultimo < valor.length()) { + partes.add(valor.substring(ultimo)); + } + for (int i = 1; i <= grupos; i++) { + if (!usado[i]) { + return null; // la traducción perdió un dato: mejor no usarla + } + } + return partes; + } + + String aplicar(String texto) { + if (!prefijo.isEmpty() && !texto.startsWith(prefijo)) { + return null; + } + Matcher m = patron.matcher(texto); + if (!m.matches()) { + return null; + } + StringBuilder salida = new StringBuilder(); + for (Object parte : destino) { + if (parte instanceof Integer indice) { + salida.append(m.group(indice)); + } else { + salida.append((String) parte); + } + } + return salida.toString(); + } + } +} diff --git a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..0a1f616 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.14.0", + "minecraft": "~${minecraft_version}", + "java": ">=17", + "fabric-api": "*" + } +} diff --git a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..3e93f7d --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,76 @@ +package io.github.consterna.modpackes; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.contents.TranslatableContents; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static final Tabla TABLA = new Tabla(Map.of( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí")); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(ChatFormatting.RED).withBold(true); + Component original = Component.literal("TPA request expired!").setStyle(estilo); + Component nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(Component.literal("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(Component.literal("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + MutableComponent raiz = Component.literal(" ").withStyle(ChatFormatting.AQUA); + raiz.append(Component.literal("Hello").withStyle(ChatFormatting.WHITE)); + raiz.append(Component.literal(" Steve")); + Component nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com")); + Component nuevo = Reescritor.reescribir(Component.literal("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals("https://example.com", nuevo.getStyle().getClickEvent().getValue()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + Component original = Component.translatable("chat.type.text", Component.literal("Hello"), "Hello"); + Component nuevo = Reescritor.reescribir(original, TABLA); + TranslatableContents contenido = (TranslatableContents) nuevo.getContents(); + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((Component) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = new Tabla(Map.of("Ñandú says hi", "Ñandú dice ¡hola! 😀")); + Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } +} diff --git a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java new file mode 100644 index 0000000..a9a7e52 --- /dev/null +++ b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java @@ -0,0 +1,142 @@ +package io.github.consterna.modpackes; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TablaTest { + private static Tabla tabla(String... pares) { + Map mapa = new java.util.HashMap<>(); + for (int i = 0; i < pares.length; i += 2) { + mapa.put(pares[i], pares[i + 1]); + } + return new Tabla(mapa); + } + + @Test + void coincidenciaExactaYSinCoincidencia() { + Tabla t = tabla("TPA request expired!", "¡La solicitud de TPA expiró!"); + assertEquals("¡La solicitud de TPA expiró!", t.traducir("TPA request expired!")); + assertNull(t.traducir("tpa request expired!")); // mayúsculas distintas: no es el mismo texto + assertNull(t.traducir("TPA request expired! Now")); + assertNull(t.traducir("")); + assertNull(t.traducir(null)); + } + + @Test + void conservaLosEspaciosDeLosBordes() { + Tabla t = tabla("Hello there", "Hola"); + assertEquals(" Hola\n", t.traducir(" Hello there\n")); + assertNull(t.traducir(" ")); + } + + @Test + void unTextoIgualQueSuTraduccionNoCuentaComoCambio() { + Tabla t = tabla("Creeper", "Creeper"); + assertNull(t.traducir("Creeper")); + } + + @Test + void conservaTildesEneYEmoji() { + Tabla t = tabla("Pick 😀", "Elige ñandú ¡sí! 日本語 §a%s"); + assertEquals("Elige ñandú ¡sí! 日本語 §a%s", t.traducir("Pick 😀")); + } + + @Test + void leeJsonValidoEIgnoraLoQueNoEsTexto() throws IOException { + Tabla t = Tabla.leer(new StringReader("{\"a b\": \"uno\", \"n\": 5, \"v\": \"\", \"l\": [\"x\"], \"z\": null}")); + assertEquals(1, t.tamano()); + assertEquals("uno", t.traducir("a b")); + } + + @Test + void jsonRotoOQueNoEsObjetoFallaConIOException() { + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("{no es json"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("[1, 2]"))); + assertThrows(IOException.class, () -> Tabla.leer(new StringReader("\"texto\""))); + } + + @Test + void combinarDejaMandarALaSegunda() { + Tabla total = tabla("a b", "viejo", "c d", "queda").combinar(tabla("a b", "nuevo")); + assertEquals("nuevo", total.traducir("a b")); + assertEquals("queda", total.traducir("c d")); + assertTrue(Tabla.VACIA.vacia()); + assertFalse(total.vacia()); + assertEquals(total, total.combinar(Tabla.VACIA)); + } + + @Test + void plantillaConUnDatoVariable() { + Tabla t = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!"); + assertEquals("¡Hola Steve, bienvenido de nuevo!", t.traducir("Hello Steve, welcome back!")); + assertEquals("¡Hola Ñandú 😀, bienvenido de nuevo!", t.traducir("Hello Ñandú 😀, welcome back!")); + assertNull(t.traducir("Goodbye Steve, welcome back!")); + assertNull(t.traducir("Hello Steve")); + } + + @Test + void plantillaConNumeroSoloCoincideConNumeros() { + Tabla t = tabla("You have %d items left", "Te quedan %d objetos"); + assertEquals("Te quedan 5 objetos", t.traducir("You have 5 items left")); + assertEquals("Te quedan -3 objetos", t.traducir("You have -3 items left")); + assertNull(t.traducir("You have many items left")); + } + + @Test + void plantillaPuedeCambiarElOrden() { + Tabla t = tabla("%s gave %d coins", "%2$d monedas de %1$s"); + assertEquals("7 monedas de Laly", t.traducir("Laly gave 7 coins")); + } + + @Test + void plantillaConBordesYPorcentajeLiteral() { + Tabla t = tabla("Progress: %d%% of %s", "Progreso: %d%% de %s"); + assertEquals(" Progreso: 50% de la misión ", t.traducir(" Progress: 50% of la misión ")); + } + + @Test + void plantillasInseguras() { + // sin texto fijo: coincidiría con todo + assertNull(tabla("%s", "%s").traducir("cualquier cosa")); + assertNull(tabla("%s %s", "%s %s").traducir("uno dos")); + // la traducción pierde un dato o inventa uno + assertNull(tabla("Hello %s and %s here", "Hola %s aquí").traducir("Hello A and B here")); + assertNull(tabla("Hello %s there friend", "Hola %s y %s").traducir("Hello A there friend")); + assertNull(tabla("Hello %s there friend", "Hola %3$s").traducir("Hello A there friend")); + } + + @Test + void laPlantillaMasEspecificaGana() { + Tabla t = tabla( + "Welcome %s", "Bienvenido %s", + "Welcome %s to the farm", "Bienvenido %s a la granja"); + assertEquals("Bienvenido Steve a la granja", t.traducir("Welcome Steve to the farm")); + assertEquals("Bienvenido Steve", t.traducir("Welcome Steve")); + } + + @Test + void caracteresDeRegexEnElTextoFijoNoSonEspeciales() { + Tabla t = tabla("Cost (%d) [gold]. Really?", "Costo (%d) [oro]. ¿En serio?"); + assertEquals("Costo (9) [oro]. ¿En serio?", t.traducir("Cost (9) [gold]. Really?")); + assertNull(t.traducir("Cost 9 gold. Really?")); + } + + @Test + void lasPlantillasSobrevivenAlCombinarYSeCuentan() { + Tabla a = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!", "Exact text here", "Texto exacto"); + Tabla total = a.combinar(tabla("Another %d things here", "Otras %d cosas aquí")); + assertEquals(3, total.tamano()); + assertEquals(2, total.plantillas()); + assertEquals("Otras 3 cosas aquí", total.traducir("Another 3 things here")); + assertEquals("Texto exacto", total.traducir("Exact text here")); + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 50f2785..ef22359 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -1,11 +1,12 @@ """Compila el mod de apoyo (ModpackES Runtime) y deja el jar donde la aplicación lo busca. -Uso: uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | todos] [--sin-pruebas] +Uso: uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | fabric-1.20.1 | todos] [--sin-pruebas] -Sin nombre compila todos. Cada uno necesita su JDK (Forge 1.20.1: 17; NeoForge 1.21.1: 21), que se toma de JAVA_HOME -o de `.herramientas/jdk-*` si existe, y acceso a internet la primera vez, para bajar Gradle, el loader y -Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo -construye el mismo comando (`.github/workflows/ci.yml`). +Sin nombre compila todos. Cada uno necesita su JDK (Forge 1.20.1: 17; NeoForge 1.21.1 y Fabric 1.20.1: 21) y su +Gradle, que se toman de JAVA_HOME (o JAVA_HOME__X64) y de `.herramientas/` si existen, y acceso a +internet la primera vez, para bajar el loader y Minecraft. El resultado queda en +`src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo construye el mismo comando +(`.github/workflows/ci.yml`). """ from __future__ import annotations @@ -19,8 +20,8 @@ RAIZ = Path(__file__).resolve().parent.parent DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" -#: proyecto → versión de Java que pide su compilación -PROYECTOS = {"forge-1.20.1": 17, "neoforge-1.21.1": 21} +#: proyecto → (versión de Java, versión de Gradle) que pide su compilación +PROYECTOS = {"forge-1.20.1": (17, "8.8"), "neoforge-1.21.1": (21, "9.2.1"), "fabric-1.20.1": (21, "9.8.2")} def _herramientas() -> Path: @@ -50,14 +51,15 @@ def _version_de_java(java: Path) -> str: def _gradle(proyecto: Path) -> list[str]: envoltorio = proyecto / ("gradlew.bat" if os.name == "nt" else "gradlew") - mayor = "8" if proyecto.name.startswith("forge") else "9" # ForgeGradle 6 va con Gradle 8; NeoForge con Gradle 9 - local = sorted(HERRAMIENTAS.glob(f"gradle-{mayor}*/bin/gradle" + (".bat" if os.name == "nt" else ""))) - return [str(local[-1])] if local else [str(envoltorio)] # el de .herramientas si está; si no, el del proyecto + local = ( + HERRAMIENTAS / f"gradle-{PROYECTOS[proyecto.name][1]}" / "bin" / ("gradle.bat" if os.name == "nt" else "gradle") + ) + return [str(local)] if local.is_file() else [str(envoltorio)] # el de .herramientas si está; si no, el del proyecto def construir(nombre: str, sin_pruebas: bool) -> int: proyecto = RAIZ / "mod-runtime" / nombre - jdk = _jdk(PROYECTOS[nombre]) + jdk = _jdk(PROYECTOS[nombre][0]) entorno = {**os.environ, "JAVA_HOME": str(jdk), "PATH": str(jdk / "bin") + os.pathsep + os.environ["PATH"]} hosts = HERRAMIENTAS / "hosts-java.txt" if hosts.is_file(): @@ -72,7 +74,11 @@ def construir(nombre: str, sin_pruebas: bool) -> int: codigo = subprocess.run(orden, cwd=proyecto, env=entorno, check=False).returncode if codigo: return codigo - jars = sorted((proyecto / "build" / "libs").glob("modpackes_runtime-*.jar")) + jars = sorted( + j + for j in (proyecto / "build" / "libs").glob("modpackes_runtime-*.jar") + if not j.stem.endswith(("-sources", "-dev")) + ) if not jars: print(f"{nombre}: la compilación terminó pero no hay jar en build/libs.", file=sys.stderr) return 1 diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index 79732fa..a6e68e2 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -70,7 +70,10 @@ def __str__(self) -> str: _TITULOS = { - "java": "Escrito en el código de los mods (un resource pack no lo alcanza; el mod de apoyo cubre Forge y NeoForge)", + "java": ( + "Escrito en el código de los mods " + "(un resource pack no lo alcanza; el mod de apoyo cubre Forge, NeoForge y Fabric)" + ), "script": "Frases de scripts que parecen texto y no se tocaron (pueden ser lógica o un mensaje armado distinto)", "formato": "Carpetas con frases en inglés cuyo formato la app no conoce (no se tocó ningún archivo)", } diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 926549a..a8967d8 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -18,6 +18,7 @@ SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { ("forge", (1, 20, 1)): "forge-1.20.1", ("neoforge", (1, 21, 1)): "neoforge-1.21.1", + ("fabric", (1, 20, 1)): "fabric-1.20.1", } _NOMBRE = re.compile(rf"{re.escape(PREFIJO)}(?P[a-z]+-[\d.]+)-(?P[\w.+-]+)\.jar") diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 0a1f8b3..d07c66b 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,7 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.20.1 y NeoForge 1.21.1." + "Por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index db57898..d659094 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -111,7 +111,7 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: casos = [ ("1.20.1", "forge", "forge-1.20.1"), ("1.21.1", "neoforge", "neoforge-1.21.1"), - ("1.20.1", "fabric", None), + ("1.20.1", "fabric", "fabric-1.20.1"), ("1.21.1", "fabric", None), ("1.21.4", "neoforge", None), ("1.20.1", "neoforge", None), @@ -138,7 +138,7 @@ def test_instalar_reemplaza_versiones_y_respeta_otros_mods(tmp_path: Path) -> No def test_instalar_con_errores_claros(tmp_path: Path) -> None: - juego = fabrica.prism(tmp_path / "a", "1.20.1", "fabric") + juego = fabrica.prism(tmp_path / "a", "1.21.1", "fabric") with Instancia(juego) as inst, pytest.raises(ValueError, match="Todavía no hay mod de apoyo"): mod_apoyo.instalar(inst, tmp_path) juego2 = fabrica.prism(tmp_path / "b", "1.20.1", "forge") From 3e32b5dc6431801e9e32f661498e9fd639e8274e Mon Sep 17 00:00:00 2001 From: Cristian Date: Sat, 7 Nov 2026 23:16:23 -0500 Subject: [PATCH 10/24] =?UTF-8?q?feat(runtime):=20el=20mod=20de=20apoyo=20?= =?UTF-8?q?tambi=C3=A9n=20traduce=20botones=20de=20pantallas=20y=20tooltip?= =?UTF-8?q?s=20sin=20objeto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al abrir una pantalla cambia el texto de sus botones, casillas y etiquetas (ScreenEvent.Init.Post en Forge y NeoForge, ScreenEvents.AFTER_INIT en Fabric). En Forge y NeoForge también cambia los tooltips que no son de objetos con RenderTooltipEvent.GatherComponents. Fabric API no tiene evento para eso. El texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca sin Mixin; la documentación lo dice. Las funciones nuevas de Reescritor llevan pruebas JUnit en los tres mods (23 en cada uno). --- CHANGELOG.md | 2 +- README.md | 4 +- .../consterna/modpackes/ModpackESRuntime.java | 22 +++++++++++ .../consterna/modpackes/Reescritor.java | 34 +++++++++++++++++ .../consterna/modpackes/ReescritorTest.java | 37 +++++++++++++++++++ .../consterna/modpackes/ModpackESRuntime.java | 33 +++++++++++++++++ .../consterna/modpackes/Reescritor.java | 34 +++++++++++++++++ .../consterna/modpackes/ReescritorTest.java | 37 +++++++++++++++++++ .../consterna/modpackes/ModpackESRuntime.java | 33 +++++++++++++++++ .../consterna/modpackes/Reescritor.java | 34 +++++++++++++++++ .../consterna/modpackes/ReescritorTest.java | 37 +++++++++++++++++++ 11 files changed, 304 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 801f807..4a34d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) las muestra en - español en el chat, la barra de acción y los tooltips. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y + español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. - **Mensajes de chat y datos de texto**, que antes quedaban en inglés: diff --git a/README.md b/README.md index 5eb2321..e8f4c4a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción y tooltips que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, no cubre pantallas, y por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java index 1e71131..a76d153 100644 --- a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -4,9 +4,13 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackType; @@ -24,6 +28,9 @@ * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * *

El chat de otros jugadores no se toca: Fabric API 1.20.1 no permite modificarlo, y no hace falta. */ public final class ModpackESRuntime implements ClientModInitializer { @@ -38,6 +45,7 @@ public void onInitializeClient() { ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); ItemTooltipCallback.EVENT.register((pila, contexto, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); } private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { @@ -67,6 +75,20 @@ private static void alCrearTooltip(List lineas) { } } + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ private static synchronized void avisar(Throwable e) { if (!avisado) { diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java index 577996e..28b7267 100644 --- a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -1,10 +1,14 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; +import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.contents.LiteralContents; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import java.util.ArrayList; import java.util.List; @@ -76,4 +80,34 @@ public static Component reescribir(Component original, Tabla tabla) { } return resultado; } + + /** + * Cambia en su sitio las líneas de texto de un tooltip (las que son un {@link Component}); lo que no es texto + * o no está en la tabla se deja como está. + */ + public static void reescribirElementos(List> elementos, Tabla tabla) { + if (elementos == null || tabla.vacia()) { + return; + } + for (int i = 0; i < elementos.size(); i++) { + Either elemento = elementos.get(i); + if (elemento != null && elemento.left().orElse(null) instanceof Component linea) { + Component nuevo = reescribir(linea, tabla); + if (nuevo != null) { + elementos.set(i, Either.left(nuevo)); + } + } + } + } + + /** Cambia el texto de un botón, casilla o etiqueta si está en la tabla. */ + public static void reescribirMensaje(AbstractWidget widget, Tabla tabla) { + if (widget == null || tabla.vacia()) { + return; + } + Component nuevo = reescribir(widget.getMessage(), tabla); + if (nuevo != null) { + widget.setMessage(nuevo); + } + } } diff --git a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java index 3e93f7d..592ea54 100644 --- a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java +++ b/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -1,13 +1,18 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -73,4 +78,36 @@ void noInventaTextoConEspaciosYAcentos() { Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); } + + @Test + void cambiaLasLineasDeUnTooltipYDejaLoQueNoEsTexto() { + TooltipComponent imagen = new TooltipComponent() { + }; + FormattedText plano = FormattedText.of("Hello"); + Component otra = Component.literal("Other"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(Component.literal("Hello"))); + elementos.add(Either.right(imagen)); + elementos.add(Either.left(plano)); + elementos.add(Either.left(otra)); + + Reescritor.reescribirElementos(elementos, TABLA); + + assertEquals("Hola", ((Component) elementos.get(0).left().orElseThrow()).getString()); + assertSame(imagen, elementos.get(1).right().orElseThrow()); + assertSame(plano, elementos.get(2).left().orElseThrow()); + assertSame(otra, elementos.get(3).left().orElseThrow()); + } + + @Test + void losTooltipsSinTablaOSinListaNoHacenNada() { + Component original = Component.literal("Hello"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(original)); + + Reescritor.reescribirElementos(elementos, Tabla.VACIA); + Reescritor.reescribirElementos(null, TABLA); + + assertSame(original, elementos.get(0).left().orElseThrow()); + } } diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java index 640e732..1f56a8f 100644 --- a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -2,6 +2,8 @@ import com.mojang.logging.LogUtils; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; @@ -10,6 +12,8 @@ import net.minecraft.util.profiling.ProfilerFiller; import net.minecraftforge.client.event.ClientChatReceivedEvent; import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.event.ScreenEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.player.ItemTooltipEvent; import net.minecraftforge.fml.common.Mod; @@ -24,6 +28,9 @@ * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} * que genera ModpackES. Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. */ @Mod(ModpackESRuntime.ID) public final class ModpackESRuntime { @@ -37,6 +44,8 @@ public ModpackESRuntime() { FMLJavaModLoadingContext.get().getModEventBus().addListener(ModpackESRuntime::alRegistrarRecargas); MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); } private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { @@ -72,6 +81,30 @@ private static void alCrearTooltip(ItemTooltipEvent evento) { } } + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ private static synchronized void avisar(Throwable e) { if (!avisado) { diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java index 577996e..28b7267 100644 --- a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java +++ b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -1,10 +1,14 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; +import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.contents.LiteralContents; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import java.util.ArrayList; import java.util.List; @@ -76,4 +80,34 @@ public static Component reescribir(Component original, Tabla tabla) { } return resultado; } + + /** + * Cambia en su sitio las líneas de texto de un tooltip (las que son un {@link Component}); lo que no es texto + * o no está en la tabla se deja como está. + */ + public static void reescribirElementos(List> elementos, Tabla tabla) { + if (elementos == null || tabla.vacia()) { + return; + } + for (int i = 0; i < elementos.size(); i++) { + Either elemento = elementos.get(i); + if (elemento != null && elemento.left().orElse(null) instanceof Component linea) { + Component nuevo = reescribir(linea, tabla); + if (nuevo != null) { + elementos.set(i, Either.left(nuevo)); + } + } + } + } + + /** Cambia el texto de un botón, casilla o etiqueta si está en la tabla. */ + public static void reescribirMensaje(AbstractWidget widget, Tabla tabla) { + if (widget == null || tabla.vacia()) { + return; + } + Component nuevo = reescribir(widget.getMessage(), tabla); + if (nuevo != null) { + widget.setMessage(nuevo); + } + } } diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java index 3e93f7d..592ea54 100644 --- a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java +++ b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -1,13 +1,18 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -73,4 +78,36 @@ void noInventaTextoConEspaciosYAcentos() { Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); } + + @Test + void cambiaLasLineasDeUnTooltipYDejaLoQueNoEsTexto() { + TooltipComponent imagen = new TooltipComponent() { + }; + FormattedText plano = FormattedText.of("Hello"); + Component otra = Component.literal("Other"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(Component.literal("Hello"))); + elementos.add(Either.right(imagen)); + elementos.add(Either.left(plano)); + elementos.add(Either.left(otra)); + + Reescritor.reescribirElementos(elementos, TABLA); + + assertEquals("Hola", ((Component) elementos.get(0).left().orElseThrow()).getString()); + assertSame(imagen, elementos.get(1).right().orElseThrow()); + assertSame(plano, elementos.get(2).left().orElseThrow()); + assertSame(otra, elementos.get(3).left().orElseThrow()); + } + + @Test + void losTooltipsSinTablaOSinListaNoHacenNada() { + Component original = Component.literal("Hello"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(original)); + + Reescritor.reescribirElementos(elementos, Tabla.VACIA); + Reescritor.reescribirElementos(null, TABLA); + + assertSame(original, elementos.get(0).left().orElseThrow()); + } } diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java index 3ed09d7..ecf9f11 100644 --- a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java +++ b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -2,6 +2,8 @@ import com.mojang.logging.LogUtils; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; @@ -13,6 +15,8 @@ import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.client.event.ClientChatReceivedEvent; import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RenderTooltipEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; import org.slf4j.Logger; @@ -26,6 +30,9 @@ * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} * que genera ModpackES (o encaje en una de sus plantillas con {@code %s} o {@code %d}). Si el juego no está en una * variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. */ @Mod(value = ModpackESRuntime.ID, dist = Dist.CLIENT) public final class ModpackESRuntime { @@ -39,6 +46,8 @@ public ModpackESRuntime(IEventBus busDelMod) { busDelMod.addListener(ModpackESRuntime::alRegistrarRecargas); NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); } private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { @@ -74,6 +83,30 @@ private static void alCrearTooltip(ItemTooltipEvent evento) { } } + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ private static synchronized void avisar(Throwable e) { if (!avisado) { diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java index d6beacd..60b82e6 100644 --- a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java +++ b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -1,10 +1,14 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; +import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.contents.PlainTextContents; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import java.util.ArrayList; import java.util.List; @@ -76,4 +80,34 @@ public static Component reescribir(Component original, Tabla tabla) { } return resultado; } + + /** + * Cambia en su sitio las líneas de texto de un tooltip (las que son un {@link Component}); lo que no es texto + * o no está en la tabla se deja como está. + */ + public static void reescribirElementos(List> elementos, Tabla tabla) { + if (elementos == null || tabla.vacia()) { + return; + } + for (int i = 0; i < elementos.size(); i++) { + Either elemento = elementos.get(i); + if (elemento != null && elemento.left().orElse(null) instanceof Component linea) { + Component nuevo = reescribir(linea, tabla); + if (nuevo != null) { + elementos.set(i, Either.left(nuevo)); + } + } + } + } + + /** Cambia el texto de un botón, casilla o etiqueta si está en la tabla. */ + public static void reescribirMensaje(AbstractWidget widget, Tabla tabla) { + if (widget == null || tabla.vacia()) { + return; + } + Component nuevo = reescribir(widget.getMessage(), tabla); + if (nuevo != null) { + widget.setMessage(nuevo); + } + } } diff --git a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java index 3e93f7d..592ea54 100644 --- a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java +++ b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -1,13 +1,18 @@ package io.github.consterna.modpackes; +import com.mojang.datafixers.util.Either; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -73,4 +78,36 @@ void noInventaTextoConEspaciosYAcentos() { Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); } + + @Test + void cambiaLasLineasDeUnTooltipYDejaLoQueNoEsTexto() { + TooltipComponent imagen = new TooltipComponent() { + }; + FormattedText plano = FormattedText.of("Hello"); + Component otra = Component.literal("Other"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(Component.literal("Hello"))); + elementos.add(Either.right(imagen)); + elementos.add(Either.left(plano)); + elementos.add(Either.left(otra)); + + Reescritor.reescribirElementos(elementos, TABLA); + + assertEquals("Hola", ((Component) elementos.get(0).left().orElseThrow()).getString()); + assertSame(imagen, elementos.get(1).right().orElseThrow()); + assertSame(plano, elementos.get(2).left().orElseThrow()); + assertSame(otra, elementos.get(3).left().orElseThrow()); + } + + @Test + void losTooltipsSinTablaOSinListaNoHacenNada() { + Component original = Component.literal("Hello"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(original)); + + Reescritor.reescribirElementos(elementos, Tabla.VACIA); + Reescritor.reescribirElementos(null, TABLA); + + assertSame(original, elementos.get(0).left().orElseThrow()); + } } From 1d638ea77cf53891283d60362dc3b2ac784f2c21 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 00:23:53 -0500 Subject: [PATCH 11/24] =?UTF-8?q?feat(cobertura):=20avisar=20de=20los=20co?= =?UTF-8?q?mandos=20de=20otros=20mods=20con=20texto=20en=20ingl=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un script que le pasa a runCommand un comando de otro mod (p. ej. `emberstextapi queue` o `/eta queue`) con una frase en inglés ya no pasa en silencio: el informe «Qué queda en inglés» lo lista en una categoría nueva, «comando». La app no conoce el formato de esos comandos y no los toca, porque su texto va mezclado con etiquetas propias del mod. Se detecta por comandos que no son de chat con cinco o más palabras y dos o más palabras de enlace del inglés. Con los 20 modpacks del corpus y FC4 solo salta en stormforge, que usa ese mod; no hay falsos positivos. --- CHANGELOG.md | 1 + README.md | 2 +- src/traductor_es/modulos/cobertura.py | 21 ++++++++++-- src/traductor_es/modulos/scripts.py | 25 ++++++++++++++ src/traductor_es/proceso.py | 8 +++-- tests/test_cobertura.py | 48 ++++++++++++++++++++++++++- 6 files changed, 97 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a34d31..7aa417c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si - Los métodos de texto de KubeJS ahora dependen de la versión (`notify`, `openChestGUI`… solo donde existen), comprobados en el código de KubeJS de 1.12 a 26.x (`formatos/kubejs_api.py`). - Mercado de Farming for Blockheads (`farmingforblockheads_compat`): se traducen los nombres de las pestañas. - El informe «Qué queda en inglés» también avisa de carpetas de datos con frases en inglés cuyo formato la app no conoce. +- El informe también avisa de los comandos de otros mods (como `emberstextapi queue` o `/eta queue`) que llevan una frase en inglés dentro de un script: la app no conoce su formato y no los toca, pero ya no pasan en silencio. - Estos datos se buscan por su estructura y no por la carpeta de un modpack: `kubejs/data`, `global_packs`, `datapacks`, `config/openloader`, los datapacks de un mundo y cualquier otra con `data//functions/…`, sueltos o dentro de un `.zip` (se reescribe el zip y se guarda el original). Los componentes de texto valen en JSON y en SNBT (1.21.5 y posteriores). diff --git a/README.md b/README.md index e8f4c4a..ab0426b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ infalibles: sirven para decidir qué rehacer, no como nota de calidad. **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». -**Más ▸ Qué queda en inglés** (o `modpackes cobertura RUTA [--java]`) lista lo que no se traduce y por qué, sin cambiar nada: frases de scripts cuyo uso no se entiende y texto escrito en el código de los mods, que un resource pack no puede alcanzar. El análisis ya avisa de las frases de scripts. +**Más ▸ Qué queda en inglés** (o `modpackes cobertura RUTA [--java]`) lista lo que no se traduce y por qué, sin cambiar nada: frases de scripts cuyo uso no se entiende, frases dentro de comandos de otros mods (p. ej. `emberstextapi queue`), cuyo formato la app no conoce y por eso no toca, y texto escrito en el código de los mods, que un resource pack no puede alcanzar. El análisis ya avisa de las frases de scripts. **Más ▸ Restaurar el original** devuelve misiones, scripts y libros propios al texto del modpack. Para quitar la traducción de los mods basta con desactivar el resource pack. diff --git a/src/traductor_es/modulos/cobertura.py b/src/traductor_es/modulos/cobertura.py index a6e68e2..feada26 100644 --- a/src/traductor_es/modulos/cobertura.py +++ b/src/traductor_es/modulos/cobertura.py @@ -58,7 +58,7 @@ class Hallazgo: """Una cosa que queda en inglés: dónde, cuántos textos y unos ejemplos.""" - categoria: str #: «script», «formato» o «java» + categoria: str #: «script», «comando», «formato» o «java» donde: str cantidad: int motivo: str @@ -75,6 +75,7 @@ def __str__(self) -> str: "(un resource pack no lo alcanza; el mod de apoyo cubre Forge, NeoForge y Fabric)" ), "script": "Frases de scripts que parecen texto y no se tocaron (pueden ser lógica o un mensaje armado distinto)", + "comando": "Comandos de otros mods con una frase en inglés (la app no conoce su formato y no los toca)", "formato": "Carpetas con frases en inglés cuyo formato la app no conoce (no se tocó ningún archivo)", } @@ -84,7 +85,7 @@ def texto_del_informe(hallazgos: list[Hallazgo]) -> str: if not hallazgos: return "No encontré texto que se quede en inglés.\n" lineas = [f"Texto que se queda en inglés aunque se traduzca todo: {len(hallazgos)} lugares.", ""] - for categoria in ("script", "formato", "java"): + for categoria in ("script", "comando", "formato", "java"): grupo = [h for h in hallazgos if h.categoria == categoria] if not grupo: continue @@ -209,6 +210,18 @@ def textos_en_java(instancia: Instancia, progreso: Callable[[int, int], None] = _TAMANO_MAXIMO = 300_000 +def hallazgos_de_comandos(juego: Path, ajenos: list[tuple[Path, list[str]]]) -> list[Hallazgo]: + """Comandos de otros mods (p. ej. `emberstextapi queue`) con una frase en inglés: no se traducen.""" + return [ + Hallazgo( + "comando", ruta.relative_to(juego).as_posix(), len(textos), + "Se le pasa a un comando de otro mod cuyo formato la app no conoce: traducirlo podría romper sus etiquetas", + tuple(t[:100] for t in textos[:MAX_EJEMPLOS]), # más largo: la frase va detrás del nombre del comando + ) + for ruta, textos in ajenos + ] # fmt: skip + + def formatos_desconocidos(juego: Path) -> list[Hallazgo]: """Carpetas de datos o recursos con frases en inglés cuyo formato la app no conoce (no se tocan).""" por_carpeta: dict[str, list[str]] = {} @@ -246,7 +259,9 @@ def revisar_cobertura( ctx: Contexto, java: bool = False, progreso: Callable[[int, int], None] = lambda _a, _b: None ) -> list[Hallazgo]: """Lo que queda en inglés. El análisis del código Java tarda (lee todos los mods): solo si se pide.""" - hallazgos = hallazgos_de_scripts(ctx.instancia.juego, ScriptsKubeJS(ctx).candidatas()) + scripts = ScriptsKubeJS(ctx) + hallazgos = hallazgos_de_scripts(ctx.instancia.juego, scripts.candidatas()) + hallazgos += hallazgos_de_comandos(ctx.instancia.juego, scripts.comandos_ajenos) hallazgos += formatos_desconocidos(ctx.instancia.juego) if java: hallazgos += textos_en_java(ctx.instancia, progreso) diff --git a/src/traductor_es/modulos/scripts.py b/src/traductor_es/modulos/scripts.py index ff3d7e3..cda6a29 100644 --- a/src/traductor_es/modulos/scripts.py +++ b/src/traductor_es/modulos/scripts.py @@ -48,6 +48,14 @@ def _llamadas_re(llamadas: frozenset[str]) -> str: _EXPRESION = re.compile(r"\$\{[^{}\n]*\}") +#: Lo que va entre `<>` o `[]` en el texto de otro mod (etiquetas de formato) o entre `${}` (código del script). +_ETIQUETA = re.compile(r"<[^<>\n]*>|\[[^\[\]\n]*\]|\$\{[^{}\n]*\}") +_PALABRA = re.compile(r"[A-Za-z']+") +#: Palabras de enlace del inglés que casi nunca aparecen en un comando: dos o más indican una frase. +_ENLACES_INGLES = frozenset({ + "the", "and", "of", "to", "is", "are", "you", "your", "it", "we", "that", "this", "be", "have", "will", + "can", "not", "with", "for", "from", "until", "when", "if", "my", "i", +}) # fmt: skip def _parece_texto(texto: str) -> bool: @@ -158,6 +166,17 @@ def comando_visible(c: javascript.CadenaJs) -> comandos.Comando | None: return cmd if cmd is not None and unidades_comando(cmd) else None +def frase_en_comando_ajeno(c: javascript.CadenaJs) -> bool: + """`runCommand` recibe un comando que no es de chat y que lleva una frase en inglés (p. ej. `emberstextapi queue`). + + La app no conoce el formato de los comandos de otros mods y no los toca: solo se avisa en el informe. + """ + if c.comparada or c.funcion not in LLAMADAS_COMANDO or comandos.analizar(c.texto) is not None: + return False + palabras = _PALABRA.findall(_ETIQUETA.sub(" ", c.texto)) + return len(palabras) >= 5 and sum(p.lower() in _ENLACES_INGLES for p in palabras) >= 2 + + @dataclass(slots=True) class _Script: ruta: Path @@ -196,6 +215,8 @@ def __init__(self, ctx: Contexto, exhaustivo: bool = False) -> None: self.exhaustivo = exhaustivo self._scripts: list[_Script] = [] self.sin_elegir: list[tuple[Path, list[str]]] = [] + #: Por script, los comandos de otros mods con una frase en inglés (no se tocan; van al informe) + self.comandos_ajenos: list[tuple[Path, list[str]]] = [] def _archivos(self) -> list[Path]: juego = self.ctx.instancia.juego @@ -228,6 +249,7 @@ def candidatas(self) -> list[tuple[Path, list[str]]]: def recolectar(self) -> list[Pieza]: self._scripts = [] self.sin_elegir = [] + self.comandos_ajenos = [] leidos = self._leer() llamadas = llamadas_de_texto(self.ctx.instancia.version.partes) # las de esta versión de KubeJS patron = _llamadas_re(llamadas) @@ -260,6 +282,9 @@ def recolectar(self) -> list[Pieza]: frases = [c.texto for c in cadenas if c.inicio not in vistas and parece_frase_sin_elegir(c)] if frases: self.sin_elegir.append((ruta, frases)) + ajenos = [c.texto for c in cadenas if c.inicio not in vistas and frase_en_comando_ajeno(c)] + if ajenos: + self.comandos_ajenos.append((ruta, ajenos)) return [ Pieza(t, f"script: {c.funcion or 'mensaje'}") for s in self._scripts diff --git a/src/traductor_es/proceso.py b/src/traductor_es/proceso.py index 121d463..25e68dc 100644 --- a/src/traductor_es/proceso.py +++ b/src/traductor_es/proceso.py @@ -14,7 +14,7 @@ from .fuentes.instancia import Instancia from .modulos import datos, libros, misiones, mods, runtime, scripts from .modulos.base import Contexto, Modulo, Resumen -from .modulos.cobertura import Hallazgo, hallazgos_de_scripts, revisar_cobertura +from .modulos.cobertura import Hallazgo, hallazgos_de_comandos, hallazgos_de_scripts, revisar_cobertura from .modulos.mods import es_nombre from .motores import fabrica from .motores.base import ErrorFatal, Motor @@ -221,11 +221,13 @@ def _avisos_de_cobertura(self, informe: Informe, scripts_leidos: list[scripts.Sc """ for s in scripts_leidos: informe.cobertura += hallazgos_de_scripts(self.instancia.juego, s.sin_elegir) + informe.cobertura += hallazgos_de_comandos(self.instancia.juego, s.comandos_ajenos) if not informe.cobertura: return [] return [ - f"{sum(h.cantidad for h in informe.cobertura)} frases de {len(informe.cobertura)} scripts parecen texto " - "pero no se traducen (puede ser lógica). Detalle con «modpackes cobertura»." + f"{sum(h.cantidad for h in informe.cobertura)} frases de {len(informe.cobertura)} lugares de los scripts " + "no se traducen (parecen texto pero puede ser lógica, o van dentro de un comando de otro mod). " + "Detalle con «modpackes cobertura»." ] def ejecutar(self, activar: bool = True, poner_idioma: bool = True) -> Informe: diff --git a/tests/test_cobertura.py b/tests/test_cobertura.py index 3d3acf9..2c85167 100644 --- a/tests/test_cobertura.py +++ b/tests/test_cobertura.py @@ -69,7 +69,7 @@ def test_analizar_avisa_sin_detener_el_analisis(tmp_path: Path) -> None: (juego / "kubejs" / "server_scripts" / "a.js").write_text(SCRIPT, encoding="utf-8") with Proceso(juego, Simulador(), grupos=["scripts"]) as p: informe = p.analizar() - assert any("1 frases de 1 scripts" in a for a in informe.avisos) + assert any("1 frases de 1 lugares de los scripts" in a for a in informe.avisos) assert informe.modulos and informe.modulos[0].grupo == "scripts" @@ -111,3 +111,49 @@ def test_texto_y_json_del_informe() -> None: assert datos[0] == {"categoria": "java", "donde": "m.jar", "textos": 2, "motivo": "Escrito en el código", "ejemplos": ["You can't do that!", "Ñandú ¡hola!"]} # fmt: skip assert "Ñandú" in informe_en_json(hallazgos) # UTF-8 sin escapar + + +#: `queue` es de otro mod (Embers Text API): la app no conoce su formato y no debe tocarlo, solo avisar. +SCRIPT_CON_COMANDOS = """ +const cola = (p, texto) => p.server.runCommandSilent(`emberstextapi queue "${p.name.string}" hotbar [bg gradient=1]${texto}`) +ServerEvents.commandRegistry(event => { + event.server.runCommandSilent(`/eta queue "${event.player.name.string}" credits Thank you for playing, we hope that you enjoyed it`) + event.server.runCommand('scoreboard objectives add Hurt minecraft.custom:minecraft.damage_dealt') + event.server.runCommand('execute as @a at @s run function foo:bar') + event.server.runCommandSilent('tellraw @a {"text":"Welcome back to the village, friend"}') + console.log('You can see this only in the log of the server') +}) +""" + + +def test_comandos_de_otros_mods_con_frase_se_avisan_y_no_se_tocan(tmp_path: Path) -> None: + from traductor_es.modulos.cobertura import hallazgos_de_comandos + from traductor_es.modulos.scripts import ScriptsKubeJS + + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "server_scripts" / "a.js" + ruta.parent.mkdir(parents=True) + ruta.write_text(SCRIPT_CON_COMANDOS, encoding="utf-8") + antes = ruta.read_bytes() + with Instancia(juego) as inst: + s = ScriptsKubeJS(Contexto(inst, Respaldo(juego))) + piezas = s.recolectar() + hallazgos = hallazgos_de_comandos(juego, s.comandos_ajenos) + assert [(h.categoria, h.donde, h.cantidad) for h in hallazgos] == [("comando", "kubejs/server_scripts/a.js", 1)] + assert "Thank you for playing" in hallazgos[0].ejemplos[0] + # el comando de chat (tellraw) sí se traduce; el de otro mod no entra entre lo que se traduce + assert [p.texto for p in piezas] == ["Welcome back to the village, friend"] + assert ruta.read_bytes() == antes # analizar no escribe nada + + +def test_el_informe_incluye_los_comandos_de_otros_mods(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1") + ruta = juego / "kubejs" / "server_scripts" / "a.js" + ruta.parent.mkdir(parents=True) + ruta.write_text(SCRIPT_CON_COMANDOS, encoding="utf-8") + with Instancia(juego) as inst: + hallazgos = revisar_cobertura(Contexto(inst, Respaldo(juego))) + assert "comando" in {h.categoria for h in hallazgos} + from traductor_es.modulos.cobertura import texto_del_informe + + assert "Comandos de otros mods" in texto_del_informe(hallazgos) From 039e5de242258c28cc0dfd8b9557d4802367d42d Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 01:14:39 -0500 Subject: [PATCH 12/24] fix(exportar): el kit de servidor incluye los datapacks comprimidos con datos del servidor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El filtro miraba la ruta del .zip y no lo que lleva dentro, así que un datapack comprimido en global_packs con logros o funciones traducidos no entraba en el kit. Ahora un .zip cuenta por sus entradas (los que solo traen tips siguen fuera, porque son de cliente). --- src/traductor_es/modulos/datos.py | 4 ++-- src/traductor_es/salida/exportar.py | 17 +++++++++++----- tests/test_verificacion_salida.py | 30 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/traductor_es/modulos/datos.py b/src/traductor_es/modulos/datos.py index 84d1acf..34d2aa9 100644 --- a/src/traductor_es/modulos/datos.py +++ b/src/traductor_es/modulos/datos.py @@ -186,7 +186,7 @@ def buscar_archivos(juego: Path) -> list[tuple[Path, Formato]]: return encontrados -def _entradas_del_zip(ruta: Path) -> list[tuple[str, Formato]]: +def entradas_del_zip(ruta: Path) -> list[tuple[str, Formato]]: """Las entradas de un .zip (datapack o pack de recursos) con texto conocido. Solo lee su índice.""" if ruta.stat().st_size > LIMITE_ZIP: return [] @@ -201,7 +201,7 @@ def buscar_zips(juego: Path) -> list[tuple[Path, list[tuple[str, Formato]]]]: """Los .zip con texto conocido dentro (datapacks de `global_packs`, `datapacks`, mundos…).""" res: list[tuple[Path, list[tuple[str, Formato]]]] = [] for ruta in recorrer(juego): - if ruta.suffix.lower() == ".zip" and (entradas := _entradas_del_zip(ruta)): + if ruta.suffix.lower() == ".zip" and (entradas := entradas_del_zip(ruta)): res.append((ruta, entradas)) return res diff --git a/src/traductor_es/salida/exportar.py b/src/traductor_es/salida/exportar.py index 2879929..05198f4 100644 --- a/src/traductor_es/salida/exportar.py +++ b/src/traductor_es/salida/exportar.py @@ -6,7 +6,7 @@ import zipfile from pathlib import Path -from ..modulos.datos import clasificar +from ..modulos.datos import clasificar, entradas_del_zip from .copias import Respaldo from .resource_pack import NOMBRE @@ -36,9 +36,16 @@ def _agregar_carpeta(zf: zipfile.ZipFile, carpeta: Path, prefijo: str = "") -> i return n -def _es_dato_del_servidor(relativa: str) -> bool: - """Funciones y logros de un datapack los lee el servidor; los tips son de cliente.""" - formato = clasificar(Path(relativa)) +def _es_dato_del_servidor(juego: Path, relativa: str) -> bool: + """Funciones y logros de un datapack los lee el servidor; los tips son de cliente. + + Un `.zip` (datapack comprimido) cuenta por lo que lleva dentro, no por su propia ruta. + """ + ruta = Path(relativa) + if ruta.suffix.lower() == ".zip": + comprimido = juego / ruta + return comprimido.is_file() and any(f.nombre != "tip" for _, f in entradas_del_zip(comprimido)) + formato = clasificar(ruta) return formato is not None and formato.nombre != "tip" @@ -55,7 +62,7 @@ def exportar_pack(juego: Path, destino: Path) -> int: def exportar_kit_servidor(juego: Path, destino: Path) -> int: """Kit para servidores: archivos traducidos que lee el servidor, el resource pack y unas instrucciones.""" respaldo = Respaldo(juego) - archivos = [r for r in respaldo.registrados() if r.startswith(_DEL_SERVIDOR) or _es_dato_del_servidor(r)] + archivos = [r for r in respaldo.registrados() if r.startswith(_DEL_SERVIDOR) or _es_dato_del_servidor(juego, r)] pack = juego / "resourcepacks" / NOMBRE if not archivos and not (pack / "pack.mcmeta").is_file(): raise ValueError("Todavía no hay nada traducido para el servidor: traduce primero.") diff --git a/tests/test_verificacion_salida.py b/tests/test_verificacion_salida.py index 6a9f66b..c90b919 100644 --- a/tests/test_verificacion_salida.py +++ b/tests/test_verificacion_salida.py @@ -112,6 +112,36 @@ def test_exportar_kit_y_pack(tmp_path: Path) -> None: exportar.exportar_pack(tmp_path / "otro", tmp_path / "x.zip") +def test_el_kit_incluye_datapacks_comprimidos_con_datos_del_servidor(tmp_path: Path) -> None: + import io + + def comprimido(entradas: dict[str, str]) -> bytes: + salida = io.BytesIO() + with zipfile.ZipFile(salida, "w") as z: + for nombre, texto in entradas.items(): + z.writestr(nombre, texto) + return salida.getvalue() + + juego = fabrica.prism(tmp_path, "1.20.1") + con_logros = juego / "global_packs" / "required_data" / "logros.zip" + solo_tips = juego / "global_packs" / "required_data" / "tips.zip" + for ruta in (con_logros, solo_tips): + ruta.parent.mkdir(parents=True, exist_ok=True) + datos_logros = comprimido({"data/m/advancements/a.json": '{"display": {"title": {"text": "Hola"}}}'}) + datos_tips = comprimido({"assets/m/tips/t.json": '{"tip": "Un consejo"}'}) + con_logros.write_bytes(datos_logros) + solo_tips.write_bytes(datos_tips) + respaldo = Respaldo(juego) + respaldo.escribir(con_logros, datos_logros) + respaldo.escribir(solo_tips, datos_tips) + destino = tmp_path / "kit.zip" + exportar.exportar_kit_servidor(juego, destino) + with zipfile.ZipFile(destino) as z: + nombres = set(z.namelist()) + assert "global_packs/required_data/logros.zip" in nombres # lo lee el servidor + assert "global_packs/required_data/tips.zip" not in nombres # los tips son de cliente + + # ------------------------------------------------------------------ línea de comandos real def _cli(*args: str, datos: Path) -> subprocess.CompletedProcess[str]: entorno = {**os.environ, "TRADUCTOR_ES_DATOS": str(datos), "PYTHONIOENCODING": "utf-8"} From 898ed6a864594fb22a57b871751f0abb85fe89ca Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 02:11:00 -0500 Subject: [PATCH 13/24] feat(runtime): mods de apoyo para Forge 1.19.2 y Fabric 1.19.4, 1.21.1 y 1.21.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cuatro combinaciones más de las más jugadas (por modpacks y descargas en Modrinth): Forge 1.19.2, Fabric 1.19.4, Fabric 1.21.1 y Fabric 1.21.4. Cada una con su gancho de chat, tooltips y botones leído del código real de su versión, y 23 pruebas JUnit. La lógica común sale de las copias: `mod-runtime/comun/` guarda la tabla y tres variantes del reescritor según cómo guarda el texto cada versión (literal, plano desde 1.20.3 y sin texto de reserva en 1.19.2). Fabric 1.19.2 no puede tener mod: Fabric API no trae un evento de chat del cliente en esa versión. La app lo dice con ese motivo en vez de «todavía no». --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 4 +- .../consterna/modpackes/Reescritor.java | 0 .../consterna/modpackes/ReescritorTest.java | 0 .../consterna/modpackes/Reescritor.java | 0 .../consterna/modpackes/ReescritorTest.java | 0 .../consterna/modpackes/Reescritor.java | 4 +- .../consterna/modpackes/ReescritorTest.java | 0 .../io/github/consterna/modpackes/Tabla.java | 0 .../github/consterna/modpackes/TablaTest.java | 0 mod-runtime/fabric-1.19.4/.gitignore | 7 + mod-runtime/fabric-1.19.4/build.gradle | 62 ++++ mod-runtime/fabric-1.19.4/gradle.properties | 16 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.19.4/gradlew | 248 +++++++++++++++ mod-runtime/fabric-1.19.4/gradlew.bat | 112 +++++++ mod-runtime/fabric-1.19.4/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 +++++++++ .../src/main/resources/fabric.mod.json | 22 ++ mod-runtime/fabric-1.20.1/build.gradle | 8 + .../consterna/modpackes/ModpackESRuntime.java | 2 +- .../src/main/resources/fabric.mod.json | 2 +- mod-runtime/fabric-1.21.1/.gitignore | 7 + mod-runtime/fabric-1.21.1/build.gradle | 62 ++++ mod-runtime/fabric-1.21.1/gradle.properties | 16 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.21.1/gradlew | 248 +++++++++++++++ mod-runtime/fabric-1.21.1/gradlew.bat | 112 +++++++ mod-runtime/fabric-1.21.1/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 +++++++++ .../src/main/resources/fabric.mod.json | 22 ++ mod-runtime/fabric-1.21.4/.gitignore | 7 + mod-runtime/fabric-1.21.4/build.gradle | 62 ++++ mod-runtime/fabric-1.21.4/gradle.properties | 16 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.21.4/gradlew | 248 +++++++++++++++ mod-runtime/fabric-1.21.4/gradlew.bat | 112 +++++++ mod-runtime/fabric-1.21.4/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 +++++++++ .../src/main/resources/fabric.mod.json | 22 ++ mod-runtime/forge-1.19.2/.gitignore | 7 + mod-runtime/forge-1.19.2/build.gradle | 89 ++++++ mod-runtime/forge-1.19.2/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 62076 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + mod-runtime/forge-1.19.2/gradlew | 245 +++++++++++++++ mod-runtime/forge-1.19.2/gradlew.bat | 92 ++++++ mod-runtime/forge-1.19.2/settings.gradle | 13 + .../consterna/modpackes/ModpackESRuntime.java | 151 +++++++++ .../src/main/resources/META-INF/mods.toml | 26 ++ .../src/main/resources/pack.mcmeta | 8 + mod-runtime/forge-1.20.1/build.gradle | 8 + .../io/github/consterna/modpackes/Tabla.java | 286 ------------------ .../github/consterna/modpackes/TablaTest.java | 142 --------- mod-runtime/neoforge-1.21.1/build.gradle | 8 + .../io/github/consterna/modpackes/Tabla.java | 286 ------------------ .../github/consterna/modpackes/TablaTest.java | 142 --------- scripts/construir_mod.py | 14 +- src/traductor_es/salida/mod_apoyo.py | 32 +- src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 14 +- 66 files changed, 2605 insertions(+), 885 deletions(-) rename mod-runtime/{fabric-1.20.1 => comun/reescritor-literal}/src/main/java/io/github/consterna/modpackes/Reescritor.java (100%) rename mod-runtime/{fabric-1.20.1 => comun/reescritor-literal}/src/test/java/io/github/consterna/modpackes/ReescritorTest.java (100%) rename mod-runtime/{neoforge-1.21.1 => comun/reescritor-plano}/src/main/java/io/github/consterna/modpackes/Reescritor.java (100%) rename mod-runtime/{forge-1.20.1 => comun/reescritor-plano}/src/test/java/io/github/consterna/modpackes/ReescritorTest.java (100%) rename mod-runtime/{forge-1.20.1 => comun/reescritor-sin-reserva}/src/main/java/io/github/consterna/modpackes/Reescritor.java (95%) rename mod-runtime/{neoforge-1.21.1 => comun/reescritor-sin-reserva}/src/test/java/io/github/consterna/modpackes/ReescritorTest.java (100%) rename mod-runtime/{fabric-1.20.1 => comun/tabla}/src/main/java/io/github/consterna/modpackes/Tabla.java (100%) rename mod-runtime/{fabric-1.20.1 => comun/tabla}/src/test/java/io/github/consterna/modpackes/TablaTest.java (100%) create mode 100644 mod-runtime/fabric-1.19.4/.gitignore create mode 100644 mod-runtime/fabric-1.19.4/build.gradle create mode 100644 mod-runtime/fabric-1.19.4/gradle.properties create mode 100644 mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.19.4/gradlew create mode 100644 mod-runtime/fabric-1.19.4/gradlew.bat create mode 100644 mod-runtime/fabric-1.19.4/settings.gradle create mode 100644 mod-runtime/fabric-1.19.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/fabric-1.21.1/.gitignore create mode 100644 mod-runtime/fabric-1.21.1/build.gradle create mode 100644 mod-runtime/fabric-1.21.1/gradle.properties create mode 100644 mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.21.1/gradlew create mode 100644 mod-runtime/fabric-1.21.1/gradlew.bat create mode 100644 mod-runtime/fabric-1.21.1/settings.gradle create mode 100644 mod-runtime/fabric-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/fabric-1.21.4/.gitignore create mode 100644 mod-runtime/fabric-1.21.4/build.gradle create mode 100644 mod-runtime/fabric-1.21.4/gradle.properties create mode 100644 mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.21.4/gradlew create mode 100644 mod-runtime/fabric-1.21.4/gradlew.bat create mode 100644 mod-runtime/fabric-1.21.4/settings.gradle create mode 100644 mod-runtime/fabric-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/forge-1.19.2/.gitignore create mode 100644 mod-runtime/forge-1.19.2/build.gradle create mode 100644 mod-runtime/forge-1.19.2/gradle.properties create mode 100644 mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/forge-1.19.2/gradlew create mode 100644 mod-runtime/forge-1.19.2/gradlew.bat create mode 100644 mod-runtime/forge-1.19.2/settings.gradle create mode 100644 mod-runtime/forge-1.19.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/forge-1.19.2/src/main/resources/META-INF/mods.toml create mode 100644 mod-runtime/forge-1.19.2/src/main/resources/pack.mcmeta delete mode 100644 mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java delete mode 100644 mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java delete mode 100644 mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java delete mode 100644 mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37321bf..cf0b5d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,10 +55,10 @@ jobs: - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin - java-version: "17" # ForgeGradle 6 (Forge 1.20.1) pide Java 17 + java-version: "17" # ForgeGradle 6 (Forge 1.19.2 y 1.20.1) pide Java 17 # Con las dos versiones instaladas (17 aquí y 21 más arriba), el script elige la que pide cada mod - # (Forge 1.20.1: Java 17; NeoForge 1.21.1 y Fabric 1.20.1: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. + # (Forge 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y todos los Fabric: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa417c..b6a41b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe23dde..f1c5e44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,13 +95,13 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -`mod-runtime/forge-1.20.1/`, `mod-runtime/neoforge-1.21.1/` y `mod-runtime/fabric-1.20.1/` son mods solo de cliente (con la misma lógica de tabla y plantillas) que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.20.1 se compila con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric 1.20.1, con Loom 1.17, Gradle 9 y Java 21 (el mod queda compilado para Java 17): +Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`) es un mod solo de cliente. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21): ``` -uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | fabric-1.20.1] +uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4] ``` -Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` y una entrada en `SOPORTADOS` de `salida/mod_apoyo.py`. +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). ## Licencia de las contribuciones diff --git a/README.md b/README.md index ab0426b..bc75f72 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/comun/reescritor-literal/src/main/java/io/github/consterna/modpackes/Reescritor.java similarity index 100% rename from mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java rename to mod-runtime/comun/reescritor-literal/src/main/java/io/github/consterna/modpackes/Reescritor.java diff --git a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-literal/src/test/java/io/github/consterna/modpackes/ReescritorTest.java similarity index 100% rename from mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java rename to mod-runtime/comun/reescritor-literal/src/test/java/io/github/consterna/modpackes/ReescritorTest.java diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/comun/reescritor-plano/src/main/java/io/github/consterna/modpackes/Reescritor.java similarity index 100% rename from mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Reescritor.java rename to mod-runtime/comun/reescritor-plano/src/main/java/io/github/consterna/modpackes/Reescritor.java diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-plano/src/test/java/io/github/consterna/modpackes/ReescritorTest.java similarity index 100% rename from mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java rename to mod-runtime/comun/reescritor-plano/src/test/java/io/github/consterna/modpackes/ReescritorTest.java diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/comun/reescritor-sin-reserva/src/main/java/io/github/consterna/modpackes/Reescritor.java similarity index 95% rename from mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java rename to mod-runtime/comun/reescritor-sin-reserva/src/main/java/io/github/consterna/modpackes/Reescritor.java index 28b7267..48ba1c2 100644 --- a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Reescritor.java +++ b/mod-runtime/comun/reescritor-sin-reserva/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -15,7 +15,7 @@ /** * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni - * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + * el resto de la estructura. Variante para Minecraft sin «fallback» en los textos traducibles (1.19.2). Nunca lanza por un componente raro: si algo no se entiende, se deja igual. */ public final class Reescritor { private Reescritor() { @@ -54,7 +54,7 @@ public static Component reescribir(Component original, Tabla tabla) { } } if (nuevos != null) { - nuevoContenido = new TranslatableContents(traducible.getKey(), traducible.getFallback(), nuevos); + nuevoContenido = new TranslatableContents(traducible.getKey(), nuevos); cambio = true; } } diff --git a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-sin-reserva/src/test/java/io/github/consterna/modpackes/ReescritorTest.java similarity index 100% rename from mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/ReescritorTest.java rename to mod-runtime/comun/reescritor-sin-reserva/src/test/java/io/github/consterna/modpackes/ReescritorTest.java diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java similarity index 100% rename from mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java rename to mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java diff --git a/mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/comun/tabla/src/test/java/io/github/consterna/modpackes/TablaTest.java similarity index 100% rename from mod-runtime/fabric-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java rename to mod-runtime/comun/tabla/src/test/java/io/github/consterna/modpackes/TablaTest.java diff --git a/mod-runtime/fabric-1.19.4/.gitignore b/mod-runtime/fabric-1.19.4/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.19.4/build.gradle b/mod-runtime/fabric-1.19.4/build.gradle new file mode 100644 index 0000000..feb910e --- /dev/null +++ b/mod-runtime/fabric-1.19.4/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 17 // Minecraft 1.19.4 funciona con Java 17 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-literal/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-literal/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.19.4/gradle.properties b/mod-runtime/fabric-1.19.4/gradle.properties new file mode 100644 index 0000000..2145cf8 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/gradle.properties @@ -0,0 +1,16 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.87.2 para 1.19.4. Loom 1.17 corre con Java 21 (la 1.19 pide Java 25); el mod se compila a Java 17. +minecraft_version=1.19.4 +loader_version=0.14.21 +loom_version=1.17.21 +fabric_api_version=0.87.2+1.19.4 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.19.4/gradlew.bat b/mod-runtime/fabric-1.19.4/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.19.4/settings.gradle b/mod-runtime/fabric-1.19.4/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.19.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.19.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..bfcb994 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return new ResourceLocation(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..b264b08 --- /dev/null +++ b/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.14.21", + "minecraft": "${minecraft_version}", + "java": ">=17", + "fabric-api": "*" + } +} diff --git a/mod-runtime/fabric-1.20.1/build.gradle b/mod-runtime/fabric-1.20.1/build.gradle index 757e643..444b6bb 100644 --- a/mod-runtime/fabric-1.20.1/build.gradle +++ b/mod-runtime/fabric-1.20.1/build.gradle @@ -49,6 +49,14 @@ java { targetCompatibility = JavaVersion.VERSION_17 } +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-literal/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-literal/src/test/java' +} + tasks.named('test', Test).configure { useJUnitPlatform() } diff --git a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java index a76d153..bfcb994 100644 --- a/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java +++ b/mod-runtime/fabric-1.20.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -31,7 +31,7 @@ *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. * - *

El chat de otros jugadores no se toca: Fabric API 1.20.1 no permite modificarlo, y no hace falta. + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. */ public final class ModpackESRuntime implements ClientModInitializer { public static final String ID = "modpackes_runtime"; diff --git a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json index 0a1f616..391e587 100644 --- a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json @@ -15,7 +15,7 @@ }, "depends": { "fabricloader": ">=0.14.0", - "minecraft": "~${minecraft_version}", + "minecraft": "${minecraft_version}", "java": ">=17", "fabric-api": "*" } diff --git a/mod-runtime/fabric-1.21.1/.gitignore b/mod-runtime/fabric-1.21.1/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.21.1/build.gradle b/mod-runtime/fabric-1.21.1/build.gradle new file mode 100644 index 0000000..1e40e62 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 // Minecraft 1.21.1 funciona con Java 21 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.21.1/gradle.properties b/mod-runtime/fabric-1.21.1/gradle.properties new file mode 100644 index 0000000..52fe37c --- /dev/null +++ b/mod-runtime/fabric-1.21.1/gradle.properties @@ -0,0 +1,16 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.110.0 para 1.21.1 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). +minecraft_version=1.21.1 +loader_version=0.15.11 +loom_version=1.17.21 +fabric_api_version=0.110.0+1.21.1 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.21.1/gradlew.bat b/mod-runtime/fabric-1.21.1/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.21.1/settings.gradle b/mod-runtime/fabric-1.21.1/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..b6680a5 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return ResourceLocation.fromNamespaceAndPath(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..03aca45 --- /dev/null +++ b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.15.11", + "minecraft": "${minecraft_version}", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/mod-runtime/fabric-1.21.4/.gitignore b/mod-runtime/fabric-1.21.4/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.21.4/build.gradle b/mod-runtime/fabric-1.21.4/build.gradle new file mode 100644 index 0000000..8f77839 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 // Minecraft 1.21.4 funciona con Java 21 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.21.4/gradle.properties b/mod-runtime/fabric-1.21.4/gradle.properties new file mode 100644 index 0000000..26a3050 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/gradle.properties @@ -0,0 +1,16 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.110.5 para 1.21.4 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). +minecraft_version=1.21.4 +loader_version=0.16.9 +loom_version=1.17.21 +fabric_api_version=0.110.5+1.21.4 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 zcma&OWmH_-wk?cn;V!}5-QC?ScmajGYe;Z+3GVI$cX#&$ClK7-AN!ow?)Tol@9ytZ zYpZ@#TYu)5WA;9JA7hq^JR}q*7#tiN7#JAo$MgSvP{H8A6eTr88D*8Em=$EjC6&}A zH8r5Y!2fz>U02U#l?(gJbWM+?xy1>dlx+LyS(BfdX^AFxGJ8U2nvfU;4Nx4NK}2`y z?MV=4jb>Eo&P_0FDL*_g(B*N;1NlbI_t0W?+_`1|)}VZc+)j4pR8fmNBBH1e`Df0JO} zzN@#As4%03;W^B;Jq|jdQ%$el(ah)4ic-SrE8j@BX1H|PmfUVnb`M-!Maw}vlnB-< zm$QvEDUidyLK07uE?e{4r)LTR<@y7Eh*0FjjSZseKnaESu`YKnef+9^i z40Nb`Lpj~w)7ND^*&{KY&q}VXU7eNRKu&|OrY3Nb&!0}h06X_) zg)zK9u-@~uVL32XREM+De3*%wgU*J@i7|j4mz!@YzD{_F+{q0U9N$LT-|4I?lotNA zAu>QwAr%TsJlT7>wy8bIx)yb|> z-nF9tw;6zSL}|%D=J?WmKrXGotgP(_$c5cB=;{A@gO3yIo?be z3yrViT1>n}n1AnnkK^hb3SukF6#j!u20jHAOAR|Ui>UN8a&W4Gyl-DY$2jPCa&79* zc6W_OL%2n1rN9#upQ0w>v5m_LzT*0(Q&kBjLsVqH+XVlZv#s{cG6Gyjj#!RuM@)p9 z1+s&1JDL%i*@mrx5T=sREHAeT#|au%MY;YIXrTi)gAANPyquKWq+fZ2PbgyzJvIv_ z0Ns-7T&luzgk@w&ft@-1gn7F$L|-ML=}JbbxzRX2N<6u;VW+k^89ctUN6&Pt{ydJAD$lJ{M*>!Nii z?41o-$75hgfPNAE_N(am&iQ@E>*~sF(QslX&D;=s`5A7!R)#a~>k=3)&OvaMElmtE z_OhFV(9o2gkumnJl=i5KWl3+58R>KT&Y&7*9sMTc8)X(4np)<1vq$73i%{IxPBq=*z@w0Up1J@dmdWtQj2wH zq%Wwm^xagF5^d8|FYj4O!H%xVJ>%s~Q@LZ(q&j;qquq(qZjm|MhgP=9l%wW*rNORficy^#gkhhb1 zF1$Ivg2W!cZ#Wl4$(Iil?$ZU%w+0?AIctg9`-eseLF|M^%UZqENCRiHNpQOsi);7( zEx>~&W9VI-SSQ~QSNN%CCq$dx2-H$Pa?+XK7&B?|RRz@9I=g;)O4sDr7@vh!FU5GN zZGo__oGvrc;knQLTUqov#ZTZE0Nof;6$+IZCvjIsZy}pMg1N^ew#>v>bsf2=x3k67 z@%y4^Do8!d?>MM~E8jk0IQ3wFrV-%H`$lJ=P6}!(PM%>_73aUrkB)^I7&g|&gw%815G?CS^53-@t#0p3A%23-{lv&O_qLy23)+Lcx_ayKOje73D$o zXzBVD@T726Jod{Kw>T!MiE*E?5~$ae+EarzzI1(oPQiuJ7+J$p~7zOG`ZW z>41=7n#r)2aN)rHBFo+0Xz!i^F6&`QFb3=-<;*Q8EF%b>a`?)Y>nisIb*g>B{<;8R zU6XQ>kg^pHbVv}wWa)I$jAbF=H6?Q`T{ZIe{DV$Z`Z?%6t=@iA-`O@2r00Q%ysh?D zoeEzzj*==pFNG2J2(hn$e2y9LM4dfXBa|`(z#nKLF*|U0NH3cM5dwA@twF+pW|G#y zBP6s9x{XE-ev!GS-fx$Sbw3W8=5!{G91rMel zSylKXy%9YbKd`ggAZHG49Dfcu^<$z!o9mRv1jn910PEC7G9scBRKk#H*I#~mLOF@% zsLKrI46NY_1_IQo*(5CU#oej%Nq$;Y>8uH)$euq|PSDl@up4(f* z`6@&HB$%H%|CYqRNrIpTj%U$GEmJTb3mN2DbiOTL|a%21!jgI?< zA|_4KHB_DUeH^>XQX7FQZkFb-HfyZp=8No@7Kv2tVXUYko~uHW5_Y;ovX5$Ljx$RR z)aUHwdGMqew>~_Ev~~^f={*9yOuf%A)v1cDz7CGPa<{U@sNcRK$7Ml7*xllzctv#` zItsxev7;kW=D~Mvy5NL7xY_@i*pwh@NsbX0Iv`cMjsSqlW2PC!Du0`C5s!yd(!H#k zbSFxiQBjtjG2U>dsnp(n;vo~pxK-!bD5YD}G$pV+rZ9o1b$rKJ^NO%C+hQ32W7k)p zO4kyd9uVk>DY+aD@jBbs$q!f47Hvolxm5sDf-y>?R|ylAu4aqb<)~kRAMfnhnzuaf zOGb+vJ&REA6U`tiQdFGWxlGfd$e9|{AoFT*I5$XwS_EO#_p=aHwPi+19`8y`^G zE5!KDv+HB+{-OwWUmgzK*6MkX8^PzC?i~;<7k$bsJHaHgB1FgH2)eQ=^BDlnf`g z7_VcwnlDU{eg z;tfUx7cNX(XTsAJ1jh?oy!JC16@SC1l+_P@8Ml70^|KiV&#l@Wn#I!_`N6K~cOzs` z0uJQ(gkKsJZ-kBAf_(vJJ=8jLqeUtYijm9nrARt>p3?BJH`QT^uu9g`e}0f=1Z?_p zwx~d?sQKqa5}cO0^~ta5`a-A>#fM1buGFu^NQG`COdB`r( zFJOhjlrUPqam6rGPjwEpVvD@l)}f70D!g>8FmU&_gK*bl7p zb*FUuFk@E;4+y7L5ajZwJABP?j+Rx2na$LI$>bADlrPQ?8OauBz`g$B z^}gK%R#9WftbM*bzR29%y;*&Rc=>$#v|f|Dx3j+&y7x+qDR!QEU$Khh?`OOX^3|{G z1)>mR+gx4(UoY)l#url@5D7XygT4B!Xv|kZu1`2>^C00|WO8;c%1j^ja7y5GiCw0m za%ZGoHO%sENqDY-E&oER=T|>kp<*w6_R&fo`h!!oKglq>A^hip_d6zz-;8qwL7RY) z$sU%7f7`bkW4w#fJRtP}T$VeRH9YrcR1t9@wu6Q0*`6*rH-mxIGZVJ4=)@f6p=T@% z=*&ZIZ~35@q}6Q!vJJf;M%WjafP&S_@yFf+URq+FBzPtp4!aX-h+W;EW9zOVu@(K= z5Ob-J`ZX8!g6)1GK@cY6+VC>Qp2d+k&W4sEmYJ>JHh8L=d@W)$1(vKkIECz7N7GFQ zzQ^YrO!2e#FZ!A|-tJ_1&LG6D<07wQu`Y^oKb~B$V}j=I(My6x;H5M=QrhqswaI@j zn5K>(Vf7+zLiOr_0XEi%I_jKJd2mBzW|U!_45bK*$B$I!DXmVsyTI|bqEWlnwgz2b zURZD<(QP1fNlBQ`I%IdtX-Q4Q28XbPk zt?PYQB&@~Q(PG1oI!S|9+AnZua8-0CK<91l#?B+H_Ky-tH%_{tEKamnMb9shRmFz+ zLKP*LSdEv%y3M;e%H`IeSY&Of?Fr5D`>5|g|5r*JP292NVA51U<6+Z;atwH49{sR+ za%kz`?>jT!l;gD`9vu4uEjI7w3}f&wPOp5_XI%8Jx3nERt{6?s^=+U5eHLnJV; zE^06^mH$yMV*a0cQBhP$R!UM`gGo{9zciy;j3`1M3##P(w>45&$AGpICY^XOJ*dXE z=)(q>ISvZZ8V74EC@22wxA&R^Ee~8Wj)OoVyCE%KSy@(|K*(P-rhI*>(t&uZUO(~8 zESR&z-^DpCS7-feq3J7k{7GnjMMZb*zeX1A)=o34SO;?%dsFFq;H#%Os;#3gC2CSO z21mz}`i4H5-fdr>t=gN={?zs2gme4VMGjZwI2KVKq;3#touMu^it*Q?q0-?B%RiJZ z)Q8^vpBK&gv1kWp3uX&vpqVX*+0@pGS=_PiDT)^rcCm`%L$d)CYV~rkORaboGiT%1Tvj?&w$5@+Q^;vfuEpO zh0Z3(uaLXLA?bd-`}~e{XXuZv*aySW4gOc`WuFsIFhAn1{g1Jy`p4KS|GxqbQd?D0 z7sWIXmq)QfNll5|g1+?33r?X^VKnw7HIzZXl_$Y9D&7a2_Ox>B2jG*3=WOFVRHDJB zz&$5p27tdxx#wej&B? zdxie=EQ)j|2+QPJbQ;BD>g=FXI~-i`@;^}3OhmHkEK$7n1q+U?z=#C9rg&&_~NMm_Dv_e5a-7x(bTs^9F4V}Nsj8@9|caD z3pB5D>c$tzj;jJ2L2cG=qa0T>Gmh>WL_FK}J~$@XVzdM6Lf6*A_H^H=xa@HOfvwk_ zd1DM?d7>V&pm-q+agWK8_qGW=o9$Oz@BW%PbRD;CDJ-)QKya65CK+2Y8Fm}iDP5b@ zbWpY=Z(~915(0=hUL@*nI1wX*8F}U%DKG;T_t)?Rwf@iH#rMZX@hEl;Y7|v7&)9hQ zh%=U#o{1|908ZD|#!zU15pj4qq)UVOtW%JaE6st%2;9pM&7ZPtl5I~835LF*H;BK| z`Sm2x(is8_EEDpdpoIM&(^&=R>;iJ85H)uNIsZGgUH%3p$?68m7^0Yh^Vlg&MMY!} zRV@Kc!S&%Qg= zqV}Y*bcqd>^if`c1HFXG>e)md^JN#<}z*hh*d5moCfu>mz71-h1Q1e^&Gu+Zsw z$K;EO_7X0EfGE*Tg!D1&X?uR;gpkKBP zH^KPmfJkw1zHzMq@;qP?aP18d$WrbHW!Ew<{$q&cX(ieICI~3RO_;rD9=3nAN_!x4 zIKvX(hnpZ9Mbgi5cug)JhmYkJ)60@oads_k@2$TyStZk2-<7fn`VO}%+_Dbd0v`X0 zz^qj$zM_u9w=*_Vs(mE_9P zoj+JzfvJNwbNiPpj%OCwC*yC%j0vv5P?eIF_6sZ&!<-k{%K0<6pQG+i*dV~==3gm` zD)7}@_9JC8p#CXk1^$zi6$6<&ID@3DoLyYi-Aw*BJN_+q8#V15=Cv@t*sm^@$$l%w zyc>cuB(Xy0uB7xsWVRV!C9(SkDA$l#jCRRraXT4tKPa|JzGJ`h6aK1LnT!qpz-1+j zz|+)knW1(KE!pHU?KjmrEqKqn|91bBg9FC&8@%6Pbrq$=UzJXO71LZZO|!v`vcgiU zpp0N`kx|`FM^n^Y4>y26fa-WnwXto6D@&_i)qJ^iJFIf9vJ7cM!;Lv)Bz9VZ4kM_s zWb?7684w6i4n9@UU{i?Q8>YG%t3hLdX*FxKRa~1x>SeL|*{DDHM^k*4LwuT1CmM#k zLUJARzV#H2pOtxuF=M9bcZzQER7a3dNSVs!UU73(E2F&!17&Kvn3%A5x88Er03}Qj zE_SiV0exFp`!cS5N#8asT32futTwJ6p(GR}&SsX#| zLOYGOR=FovU6s)&)(qlEQlm!maTf7W6P}~jkray}eRT3A^6}xLG{q$cT-JgpGJ!n6 zM;qR2VOUYzkiZ^BZEk0e7ImZDNij^Lt#NSQm>S%WwqnXm&_NY^>h_Ld{&+G{UTab@ zlRD-;YU(A@6!9qgNMMjdI2Jd+<~Y99N>|U^*aLUP8WYEH)-=WJJG|nF(7WFlA&95A zbC{TH+MZJ~Uex_7W+7WxzY-0HSj*QtIclEG%qWi1FKonjSwC#5LRi)_gst)u`v5x7hlBDp~JjQIvj=8thIOHTiclTwb$C@FFWbuL+gLEAvD?LRkR`nCGPu z0c^)XM5p9DYF_N#TS&CGn&tws2`{uicx2VLN@FEz>r6qN-!h2qQlj8v-8}i*@4;tZ zdM>0rjecoQNq_SC`Yci3O8uKD0^o<}s;#o_vsI*3Kz2gxHQ0^@17BsQ#`|mvP>oGG zfH9>G?MRV*_wM=lKBvT|oRJqMNoTTn8#1P9kHqlz_GFlE&*|#C0c=wUej7HzEQ#&9 zbtUNEqn!SI;{p&GUjF%srY4St`7N@-YBTBSQyteF2tOLMG}r!c_L%@-}}E`7h@>Kp!+T1N%?sTJS$P*Yfrb9`^sHqJNW< zx+~BX^tXeZtYzhap^5v&K7-8;H@@MkhUgGn)8rf-TuQx6S?NlHWaNS|R`IAEpDP=N z1r?*4sMF>>63$xhYbmv2taHrMD9V>*;auVu@a*5tGfx|7vf+x2H{xs_XMX1{Irm!1>;Q{N?I4?soRr0KRkRko&!lcMoO^f5OIk{yX&MQ4LQPt7q0>kwmxidBB6 zo9S^#^me;-FK)oA`G(wO1!oq!K2SHUDXOU0Z6jr_v5AR2Lo?b{jQxRVvA(sjX_aL- zx+`;QfAWUEyq^*ZcWJuaE5X(`+Vm+YhTAkiSiG@J@R-hR#g&Mic)LL(2_NGZ`T#Xt%{S+>+Dm{;P z6>r1NFn9I3ov+&h(WL}LEbitqE`)chgB=vK2S9RZq1^5u(K2a`DuP?ggV`T1mCVAe zNar64qp6biKXQIcMbpT@O$ni3o$lBP@zpQ?gf z{#NEg?TmS#^0U{V&3aYOjH93kSuu=-f%iwt^uipv3E*4js9?KU1404MFWi)=Vy11j zh~}y2DE1wMR=pt@eWC$x+d>b{tYQw@`xb`-RApX0-MjG?h*Pn-$!d{ZQKfw{V&;#f z5VAEH+r$-a457ZBXBDBI@MZ3FNoliXa<3L-c_}Kp$cs^n`;_Ee*iNOX$jQQ&I6=1s_j>*==Zmv+3cgHln#0aVpV$9>&JroHI6uj&0e9L?$}f4KLbT5s^B zq%g6tBz#3rmHSBB9K9!lEi#l;t<;7%EhK0QmPu2>C<>&NIB4@Z5i08aG&oe@7s`%G zy`O!B9kR$f|2QmR1?xM-InbB_ev9rpeNT_6TAu=~F}01zt@S#N{`@A>Qh_Y(_@ykl z^>x)|{hr13X4g%+0Gpk>x+=;gGAFs?UM<-YxmjbF;#MoEd_WM`EUYZF77Git2TRLH zw@fLZFsek%Pj(Fwi0B>kgo=&thEbfT7VL%{@q|!DXfL55#2cU3hvAsDBp8EJiHNyz z0X_q=vApgH8=oRT@XSz``CCn{ceLn+RPQuD*X6(Uybl(-s&@ov2l9k0kH9PA>J2%a z_i)QxWe(b*H`X-Vr09(OwzGD|o=!K#ob{0G_D4I9Z{$wSt`j=^+|9gnCKH=fjx~|>b_(BFSktmy>$_U zl~nK$18%}iPx?MqF|GEgP6ndPIG9eytRp3kEO=>RM?b=H`CXvw)bw8{IWVbZ8c2pn z3t10L9g29rt*gW^LMxG}yj}tlC`LluJ*{D^>VvS+wOd0qG{wIaZq*Wu$A_5fY+%YJ zNURsC1S|#12&!*ETaIp=tKdW;#*>ZW9`4evV1!c^)IHhCO3tf0O^dwRfxOXQB*}Cu z5No0BZ$!{AMuR+#vQ`mVGKeaWVQ5nQbZ@1Vjb~*WGzN~lTJw*<1s5=$(PP=^N``2n zv&|n;o`k*)OU7hozWwa~(iN79uDZ#F7XFl?C@4{{?fLK|0`8EY3UB`u`T)?E&i(3v z4=I_+!}Y4(@fBs@K^)>ezK2-|WAU^!LkhOsW|ju1kuQ%t-vXGZikd8`Q|gPKkE0N{ zf@KnjCP8}Z(|t;fd;i4B9rRq6Pqw`@_l8@vN0ok9xHF>er=(I#Tu>_{yZA`AxbIXi zAbR_VsJUN*`Lapl_AUXb7YwpRGH6V8FmEkpGE2|D&o{ z(aOce%HHDN{Q2P%{lCaFE78DVo&__+s9A5`oYn>rdT5!%cIG_aPvj?Rn4+U5m&7b; zyNh}=qI@QuEH&u>#TyEaLI9alFUpoQsZu?rUFqD4$*6ngzF(}1@5}LhYz|n83h<$! z`@Q^9)WOA6dnBeSpTM9qpCD$PoAE3ort+7P^h_H)!-rWx?9rml=^j*fjAW~9bm%;e zfz`QL@vFm#6B4DaSp~LXtpogQP8<4Sjw)osio3iA%L}%(P9gKr_~BR zM6#OXFF zEjj?6+7+l{on0yj@Eiap-^40dJ;mY?VNRK-teJ%2MCjpbSkhq@a#F<00nz6Hcp2Re zh-3!-?7z&rW_*Y5J=b^YwH+Rr_JEBolew7Lo`cp^nFC&PuaPt(7$z<`N>lTW@VrRxN~FVZ&!PMm8!l@Z4+W!nhVFtST{ z#e}WfBeM=Q(c?#leL<^*CWI?uPxxoXLSbI1$mDCUGLH)GxKvArS}Y2%hy;-BjBCps zcJOA0D_CtAQRW)Z92teJ5wu70H^t;qNGkFU-M{r83D{ss4T^t9Esm170PHM$bv>kK6JMj#4rzIX?oAvd{6o$_>{Sd=DIft1!pwV>ggA#I zE#ZbG?Gt;(-T8N_?!95x@_Y!mfPWNl|52g$zjM?B=nVRw3%y|VLnRDV%r_?JVnz^c z1_={SfO`>~#u!;4ItNuFswKrON^6$kSP#Qw7OhwmMOUowLM=u51DH_KB>);E&zHEi zmwE1M(b@g}@&Ij&5DCaZL0M)aUPNDZ57yMO(6p0REKRr5atmy4v!hgOXOLkECT$^? zX`Rd}Y+`IMC^6?dI>F{9j8~~hE*!0pUH^@G7@nqPl~Bv(0_@gg2cVz8AEMI3vzZ>& zjW&Fgl|!id%{FQnHLl_;3aj-A+X7Tjk$YpmmK*{P*d9e?RkG)*mLv0+6IQo<1VD}z3A!Fd z_L_h4&+(E*@-Y-+247pm-IqxbVBk%hQ`^phSc2Y2&UT12r<$&s+(h55xUNFrjqG6; zty@%|5JK?y2D<3Iw6v^?YxP6XE844D+v3OMMp2yGeoqDG?l^(Pvh>ZkAd2*2(2+2w z)@Yg+HIQ?p(qV`}&5~r3xwF?3q|wO4Wh5{5WhB3t3q8Agxp3;yFx2ef!7v9?OG=)o z8&=a7N|MHqVu_&t)?T{I4CGG&DN}gUjLYV!zIwJejjW8va25v{>f;PrOY zA%+olTM9e;;{*3Ne}Q{uNuoj5$BChb{wHwf`A?+L|F0@i`M2-iXr$YCdwoFZSvtp+ zk6n%>inx>4U@{oktjMR>yl&fPP9C6hLG6Vu@BsOuxKNFS%%F52AhFlkPYpfiey*AYG< zXFT8{LTCDPf4U4uZJqxv59~rkhVToJffD?_lw6H6_p`$S3jWf(01M3E7gQS@=AaK% z{q4?H`TCp0YI2`FY}fQ+E;**5)2FTSD$F|hlJ?*w>sw1 zcn=5|la`;FO-99CMhnvDes;;jqOm7gT8jSUI!zMgMLQho3VaITN4J9JebRMy?n%f| z(=V}(WRss+*VggPLkvWUc*Hc9&HoQ{^B;M!|K4SFSLY8N|C{V}Q`2)m7sY&oL8W0*%f+fnJ!%>&VxZ7Mw~a>O zx-8QrFNQIiIAJqrTGumA?ZVA?V1I$`vYwm!mm4c5I8JHUqi}bUcCP;?<9y{G@tk?XX>Yr=# zD8XpOab{sIR&1E={;#3_#wiF+kLr zCP?OFW(&`Gb|o8$&|oB0KXxeNH0b76wzGeObT*XytV>k9v|K`JtScdD6c3#lx23jJ zCU#tfTl@ZqT_&lKu=pjrD8Q{=_G$QVTw0$>5(a%2{PY*?Rqz}tw~+B3y;gf8ckmqV zf;whs3%|r3xg18z9rcTqggAMIr0pqn#HnGwM;AaYb6u@Ug(WU+Vmu>tVq$;!85WGNhbl1j@}cd~jCZY5dhavi_~~cO@x@*h z86x;9>u+~)j)CZ2AAZF?aPqXkhd3r+#`?AeP-l(ws@oIFiDqMLdOqWH7jBb2|FR(? z2SGFwR46C%W25w(yK?^nVS=lP@cxA`ns&ac+-r%J?P@C)d~hvSzr?n&xJlDb!;hr| z`Ung0l3-uHttQZKcDceFNb!!ajSwMH=3~H=jZN91UDfJ*CBzIPx{!vYU6)AhZ;^q? zNWQC);${$)|h3%T)~%fBPOCo|Y^it}m{ z1>WE(2KCATT&2DR{`mlzvS>VOFTBh(20CmXG4Z}T6yZR&O( zEg;k{DNRW!KXk@25F)6p^?nh7hj-v&P_R;Qee{4(F@AKe62acjofxQ^Qe3`2fF}lo zfB0)}G{4tWb43$E^zAL|&9g1CvpQd|@0T4u%7*@n)2*%BKAgs(TfuGoqlEJ1#e{Mn zrT%&^V=w!kmjF?x58&|=9sP8cdiyEw{84dOJN0PoPEywHJ1CTUOlySnj|GlRBX4;! zV#$N|d=qUgbD?r2rcUXE_MbnCHGal`L!=s7x68G9=>B|a#mS_#+Th~Vl%^wIWRK|b zy$EgnCo!c?w|o8bg7ytKtn2|U2%<2G9l#HR?bI!AJoGOIY#QYT@O-)B09Gc&klrJv z&HeOA1NBw7!oBpQqvh#3z>(yiu;GI(YMFD4@y37>0NJ~+sYgIXIJ0fSi!V@J6lQr| zhOB}l`VA9(gjU6o@g@Z8h7`-Y0Dz|s#oW?OHd#!*MiStlzTy(D3S+aQUW*=MN@dh? zL^z)b&LX>s6A;*`r3U_lV zQivJozxTajo^6ARpCtrPTx~O(;7ZKUe(7wYzcFsDt;!8MLshf8$O+O}!7x81oHTHjM)+j}|9Tat@A4F+++rdCnOzPo_wbj&VVRHCsU` zF)uNVg*~rIAM*y~flA=1FVe;fTs^6A#=^+=3g>~p;f&o%uEw=WY3k>7mc#k$so&kp z&3eLCQGL7C7i4NB6<%B6$>iKFFjSH7?YNECWAHoY1TXn2Pk-%e zp}FOh6^wG_u{BtG?J5ei9l(x&2eaY14%QAFs`dvKoRe>`w$FW8Ihk?g3geT&sFN8h4U@t4Av?#2u1I5#8S=Ae|hf-C}=TBpF@!e?W zS9yuRcWgFV&rne-S7?HSvtRYDP-rEx3)_-z@py4>NRuhh$#(J8Xop2AffK z$3&Ji3T@{qiv8Q^KqGWSfR8rp;z1Z`nFK_q+8V_o$9Dg-4SvbCqA0Oj?y2K=T?t1? zM@$7Cy3_4%Na6L2-xN~8{I2i}Azh0ru4#H7V^oT`v#l z>%r(xJ$RpI=6S|Zz4q)SmLN$_Z<{NW{dKfu*S-A(+v{)T^B zT6f6btiYu#NbmiEaM7rZ2!E>JCTY&qys%~ccyouEBPRF)2}G6Gt=zeW;iYc71;?RS z0U6ps+nBOjayu~@DkN?_c;0rP?yLe)# zl-{KZ;I|4Y|G^PUy(j(kPj#hQu@)`dcw=s~RH~Sf60?|d0(iT)b4NmvqLTd+$g_Xl z$5Ij_C);T5%+3P^>UXOol##LW*&Ny3kbm!64#~T(rwgM>OKa_%wu=af?rhku6uxf5OVDP+O^X zp714vQVEW~AlHRmu^+hU6s&if#B8G>4_LW{Zle&6tE_})^L2gquD?;9j!jzO63E^E zO!J-}Tt|7+dZ>mL1D=&7e=xuW&4^$RCT|%!ysi@ZXYUPl^n>g|Z_cTk&ShJO^@9bO z+V}ncsv_RZVwJEDuX| zeoR^@Y_C8qUF-3HnpC6In6Ambo*4-Ik#S~UtFx;9{`LL|);n^agw{D`iGW$)4V?KE z&(Jc{`m6?g`a$@{AcxJ9b1! zy;N7Ob$u1SH-iIB=Ev_XwoXq@22CbE-LlskBE0B1TB&TXV?z*HbqtvXG~xqeWA(?( zq(+xBONVN(IHLi2YYjB?cvK<7q0+#dwXLnHej1KD!E)GZKC7pcRjlj4%gW$*;d^e^-n7D9Feh(^xNPP2^iY*U_iSHj4q z@hC3%Y_3ZKifP3i zSsILJUKDvp<$lR|HnpOE@<1@tTQl{V^6^TJI%v{@ zh=`RCnTdY|E$e-Gp#Qb1p$+MY9w3y7g(f}A^fJZTP|C#(T4jOW$-^IM)_(>GZ-{pD zSbZf`d2bIQg$WNKmt=GYGrc&9c{JnnKypgrjqwQH4EDaVv8;(B0Vd_W zZ%P#&@t^fe&gze5AqrDT*bF%y&Om8~NJ60P(_+H!4;Twhc+2X4ETkWf~J=EgJ@0SKk2^0O_g!m%}Z*k^ECPlg??7m-D(W&my{|% zbnZmrhEi3WHAb|b4U2;dGISaQTC`B7%r*sbX8;-H4ocNOk>N zoFO8PQn;AqwBYh7EN}7jouc=1p58oMunWN)*$t5<77|gHHJS5b9dTD;-;Da7>w6fqyq;TcbLzjT%24Q;*hH8iH&qOHnH_-#L)gv%jmdJ|LWlT;gT zh#K5Xy*Ve}2hou#)@$mMG-uY7ffFTMZ$nNL@5{CPQ9Sug)$hA7=((-XfI#}l=)2u> zDp;sh`q1+!Phy}T-y3~;ygT^b(PJO0$pu0WRsh{R4YE4Qa6cm`==xRJmH#0ImFV&i zb$M;DNrJ;ub|bgnefAk*+@+mInXENZ2>lwunGY+vh`i2~2m7HVNSTl$C!(Z4u+*A~ zuJ_0yZXFUk{E;f50BFsK)%QXCF5S7j9WGz>bqkH61>i~-J2LEx(HnATqIfBZpy|m) zyhlpu$AY4|(-sGBjutc{AE1i* zz;hPE$Jb8J-YzB~CR`SZitq&fT55^ueZ1#<#=B2+*10K6Yy>t{t9;R>ztNm?8289d z1l=J1idLmAjJz*aFEJ2LFa*xsv6H|)8O;OybxXaLbI7sKz;2>;&6gFs9ye)FH?>tY zf4yC6)lvS+Ei6~WF%Up+r-n59;qXji`;C4>O_x(anLCq7fxI#dWEpzs$=INne zMdbRM#Y}Yu1MPfzIpgODYErRnufS-2-7>LKYtG6Dx*3i_u|5Xj4VQO1+@S&eV79jy zHO`PDEm^!in$9qFEHuxGlW~EF%ImO$>9i73C#GUiy>`KRpc))I_kCt>9hGsv7xT zt6DtJXU=+_6*Cqs;1PTK-21*vweG)6jZVM&zC(WXYngPse!A`5sHlVB9ttvux*d#~ z)nPN|!CG$cH0E>H9;8J`@aaQe!zpBN=^m#+G@Yo?dePaWY4(keYAvcp@Mu>EC-uG~ zWWc;js9NUV7uIcLK#xM4rq)2ealdyxKMglf@5LqKEjhy5N&4uvF%8of$q_N@l7W7G zyRfw{l9Em4>kqv894n7%Uj#z#Q?HH6dqpSSr;S`KS~%>Z1VSNbp;}XMvnAp2IC;|y z_b{pP{I?}^__sr`}5Maj7- zAK(YxUUW(n!kO2F|6S|o`~Kx<<)J#kJ*%ZMKi!IQEJL^m7u%`EDT%EZRfHj-?I8FK zD?_9O{wQHgF06DZJe8BIma!-wuw|k$bIuEmS@z!4MKx!JUPiqv>>YAP4s}qr(s~d+ zGhc-Z9(QR7N2^*?qR$r^&+48Igvt}m#@Liqqsi|YI_LkLss9qvmuk|bJ&MuaC zi}M3k6pO7+=AX-QE3CaOr;zCV6V}zVTDPD0e%YpeZ3yH6Vk6*ZZJ3#PjhxQT4?-Cl_@vAdovC>k|qiR`!O zqx0QI)$=mBBJ3$1#gw#5@UK&~sS3Wr(Xk2M)&eJ|AnMKq;`=Y#i=rdwmF=Ttg;BAf z&siGKZKgeA=sS$rB0OgCdo}elejq=lxB-}k?_fCZ2&JyD2JO}5Ok z;J1scgoO>Q5vx)Qe@Q>+%xz(V1cARKE&8-CX`Of#5i{2X#>snWE;S5_ zG^(K*(&QDj6C%8Rgd*WN#$M4?YDO+$TJ{A?HskR9N&=~(;GgcH+RYzLhQ5!uEf3&N6@D2pH#Vh(lL3)o7m*r z?cNpgf{8rFaqLgk$h~LrdVg_shuO4V@t*$R4TE#dPP5A|)v&`Mfn<)9TFffdsx3C? zXd>Yv7R_A6Okc*2V&>m><-!K@YD(LXxRr+E0>+d``Dm++Q zR?AiLP@_dD1jMO8X+i|#s+?C1r$*lkx%yb3k&<27l&X``(9k}p_N>u{1u&XhY;<@% zb3A_xs-J@q?GSCx@9kwAhLWgx_Z~8=pxg+oz`LgG4wcWD9rBEtGkErDLNtFI_6d4gZ$dGgY^_*zNsI^9#eC^_5?^rV`6|G#En@ytqAay;JtW2}|N2 z%S1JJbgo&;dc(?obKf&XY-!`f%)_-eyy^I){7Y` z+suk0gevqu6$hDahk5b`Lab%9Iq152t0jJwCRMN~f_*HiIB3MbCN7W19Icq)Jep$g z(Gr6d#U)cvnv{cIh)=d*@(PaYW05MWOuv!>uXEt*#=SZfP%?t<@@#`xc-UyWD-*&Z zn;QYW!nAt?F`63-xcTI7M?WCSPvtro7yVjg&!!F#mue``n$g>l6vU4EyNiX|ijk#+ zw_>|?sJ7XEomWHIB90C-j-Da(4v#s&=kLK&8CQ5U-NODw#+)rjcR3$KO#hDt_TR2R zxw%@|GXGa@XgdQP9YN0G&LFdodr($D+rQshh}K$F$I!-o|CHE5DuE;_Dalxj-4_z) ztu3LKTWSTN18&!V&7Fav^YdhAQcAL-s`KabE8C|W{;mh(Ptm$1Rm!f#?@Bo*JVl#7 zgM1D~C^N4O{P*wuHhEue&uh9NSHg+$str25ZO`{qbXA92Z!yI!>{22i1SU9?UGC`NJYIgzgBHU*q$MLzR1Wk62nzBOqb$x-B2Q#HL?R zP?PXgC-hc`!?@oSj@oMOfARH>L6&sg)@XNEmu=g&ZQHi(>auOywae_XZQHhO+L_tcdVU}nfphs94qITbIg%S$XSxh!`nP8baYIDyI9mOHiM4L%Yh`p8f#MN zs3i>Z1W*$@1B=QDA=m9u+Ms1=pBLX)0`Up7sZy_y()dcf82~-oKoAuucBQ@+Q_ra`XVk=ihrcYUA;`5?t z;_cUdDj!DH!|7*)-)jIb;vNt9VnAtpGQ1yDtX84u$$5Dap2dsCW3P zY5Y=xxgq->PvkF^9U#tsQ?sgC9(CFUkWUZ82P#QuVc?T(=r!wk7^+LfMqRHGuPLJ5 z$eKK$7n~5RGgC^e8XEG6GsW*;V)=OcP=-dQ$5S}~YQQ#g`bdNYYH8WG*){^qFeH6Z5A9ymt@WtJ#fS2(42XP-Pi~tDGtbYgP{1+(+NUo>S)MU31 zm%|Ddf=ofhePGbNL}Zd1r`SUVEK@+)KI)C$PqO!cAy4ZaLi=$S0-FD5nlHcUD;L#v zE_NUM19)zf$yk~mbxEum9o^d=PjaN!9{!HIop$vEC^%g}e!hK{^2TjJrzJWj@MXw< ztcZ#S86^jPn4*Ak2eJ%wCTklJvbsEt@_^D)JA(EG+K+1PuYa%}BZ z>bFRa9btY(k#yz@am-BE7O(QOA5zRGB|rz{`Q(msmAO}VtDAy7sWnD9&ZES#E#y@f zuhXIi!;&JH1Q;pU>z0UPZJ*CWuP|_sb6KGmGECG=e1x!P@y(2KXMoQGoRgrQOn*uc zOD%ZFOpKUhC(gDBSW|q4nYu&zCwgb`)Q@E(7&#DdXoQXIU$(676eG6X(xg9S#(FmQ zwdbYtYvXU}iir5x)ptpZUT989;gL`EG819x@Pu6Hv;@imC`Kl{~Ff){1@4ve_RE;`Tmv_MSj$Ajl$$BCck zC1gXoi+B$@_)*@X;rMY2bivwvl2*Vso2g!%6Fo^cYd2{%nUIzGwOxWY&T8_J(`*|M z3o{}M6fWZxlPUPWCuDEo6)DjCnZ`!<^lh?XA1FtR(OL;vK+uC7sT%W7F}k)$YY?CR zy@fy9On3>|c@}!6_~Fw#Ce}%faZI+;H{XM)=o4Y}P$f<5NuXfkkVC$6=iOrvq8W`4 zbG89~8tMBEx5Xf@;qX)=l%ci5Nr2)E+z}yL9GbJF<_gAs~3LVO6)f3 z5%NLqzNL+jsGiWx1xiv6J@@8Qaw1&mk3=Ta`g1ge#AB>9b8O`5751NJ5W7ZmHrBV~ z(h>6CLKB|8y({PF-9XIV0c-!n*o+w+mz?3Bo#Gwrv?sH>yOG zb`vs~TsbZXA>AZj@R|1)=+ zRBypwXe+%+yo9eoF*7|zcb!;$mpPH8VpH^}G@lgg1qJH4iDWsOuQGL*0VU$zqInFK zq^U_g6-oPbS`ODXm##nUnrtS9FpFS;FK4Z)5v~?p=z21jSHK&_gY_&eow-2QJ#Ff! zo0Jv2tY*$|4;b4s#tvZ1?8nE_M|=HiPG55_;?!vf#SJhgvCGuoavrOl$LgE8-@+Kg zJw@v=Ll8tWT)qNj?#c))8*8)R2~(VTMuHcWbisgH*$Dme_YNh505Z+@Get-E_8Q=r z6m3K61yYxo)1E0R{~-;-$#A0 z*d{9{blEoz(+f*@!X!TcaM5_hx#MVZ3wwgE>(PDpIDuN9@EkE~zQ?x5_G6)pXcD)2 z7q;gg2XF0c29yNbjOdt!;=EFbO~>qze0WG6>x9Qlz=4*J<=D|M(&6r!IX^;+G}0?| zWT211z}W5xh0ftm28q$y7F1iP;D!vZ!_faB?JsJ>uR`*e7#DJBc7FE2Brs{Shk@Ks>y=<-SSMf(SiS>BhYso7u_97GGjTt5hIQ`* z`UhME1ywS~-@)C)zt(VR|G(gJ{qK;DR>0QQ=^Ih{cGCLhHsA8)f3QSE{~u_wmY0_O zZn)+q#H~)GsBiFHD~?FMe*l(9SVcfhZ|+T9?hmqe8#FocvBp5tSl~XAXeI)$>J&1D&l-W`z|L zM+LZ%l&N4^4k0Kf1d%u^#Mv*4xO~noE={g|*d!*zzx%aTrrv|JM#0vTIxlpBfGg3B zPoZoayuB*t{o*R;{g2^xg-pKpUPkw&;z;*2(@xZKZo^jxGrnf`BX2`C6%NHO9jWM| z7QV{Qu4x3Fd=(*N&R^xYZq8(eQDfj%*D2hF`;DHLHaIgHYlX z1tK$-kj)ag>}ImrU~}GyMlks?T|`G{+xJ0hIYbFbHi)lPv6z1ued(#zJfWc(8<-_~ zZpPfT=fr#rjp6!$*r7;5ZEbXQX}m0&**gk0TN}>MaSlf6yLWV6Tu(o)D@<8J-oEw1 zbHO>S9Pp2vLpTzKy8G2G@5tbpM0P5l-gu8Bv-iKJ-Bm7P>3@u_J zr)A}2Ww2gC`_h|1)80zkPq_RRT)3;OJGg5k9M$BxdJ59*+_i3r9$Fi!4uy%yysq?` zd!o|)&A$O7umhnh9FPBZlJ(EZPL;Bbc_k#HQk;Ppd$uunj(+~9->-+~x>34mfH*ad z0wF5z@As}OxiUd7U8_|cr1t=Lrb0}H8jJ_W9+!)2#$dyj7$zk(L$Q5GOOD{!nOp;* zp&zs6WM~U{`d#yN98ORY7jS}&9{>%)eSIVL+phJ{X?(z5k3&0`kdF0bwxUouWd)@g82=`*PDCZYNO!&%lno5nesF1 z;lPBe*u67)Z4Y}{(3Mwl-D{D&c4 zEsSUx!S?)&G|BD;XR5^m4?kenia20UFApSN(`d*wVJgX9Uzcqru|Efo3`rjAl|A8sdH%{ID@H;!1 z{X@SE^_^`0Gv0|(QkVTlxNid~g%0o!#iTh7<)E%thBvkkXwCJ7<1_3F_!jQ3`khF1 zFan3g_4Do)Oj*;`Di^skxZWO8goVvW+nYr?^8FEim7kw^iqL24p5?Udl`kTWgjm|z6iyo zAgF8$Lg`>knt*~)hPYg$j5bR9E8}zDO8DpP>pAGj`Jn$alsnp}V;a!@4tB&<4;BB6 zI5vt^itA8-r?p;Gu{Vx6cKn_7>f}**gzUYcPI#+Qn(ntVORK(3Bz#v2s0zyK(|q!j zCz8C~*X%Vk%PHF(>ohI~{4r0=J4ioJ`-HwVnm;CKRt3AS9fHy)d{hg7kPh-M2?o>9 zoQZS^F8H%E5g7%KfSa&d(hzR8bs*#Ecgn(QR#0l~Sagt>yUtO zD2+;y9`oSFT>aSx?YRc@R+lL2%YDVWPNM4lZ76r;W41B7W_eDX#`DbFCGP58zC<0` z@5sbZ49!FP;-S+}mVfscQtKWOIHAZ_BZdo~P>eyy(-pH~M<6!LF9K?YW0edIPeYD+ zesHy!am85w)!E)v2m{2vC`#mt;-P|`61WfQt#NMISmJj-O#Z5KaJJ91Sda8+#T5Wj zD_VSH2|Yb>*+8QYZBRK9Pde*e(gS5~t-Z5g7v<7oIc9Quhh&&b`Y$F_#6f7tfq}{} za5gjv6UWsYYygK`M}jaT`j1E%CS)gw8FgXIO4wt-9Jf###*~$T?v|)idwD*oxpf}5 zQbS=g=kt=8K&lIR_4c#}mQO&D8^B)?+29P+$*O1go_j;6!e7WVJQ;$M6dj|iwp4boT1)cF%;{JevcQy;gF5Qqq!rJw3e zD3%XIz(`z;g$h#N6<<)UozuhBb@|W1Bved)u6%801BRir*J7iPrFDGod3=MvHL#_% z<(!JeTu4J@V}r`~Pfe${EDuu`Q(S(_KkHw-M_*reIZy0go+m?7UNGI1`y2j4GC4do z2XTY^lsqQIAs7WwH#>uigKi{A(nOV>1PL~h#=%y**qr8?>=_XSDpk}x7DGc0K}uEh z99G1RE|!Kah1ABzYX}m3oAo=BJv~wDXIA7nxHLowLRd{%SfvQHMP7<;gVIqk>2UnH zw2F4Sk5xjmC>5h@gxG}9%!&1!GHfa=mYmp6Mlyv7=$6C2vU0BD95&8GmG@#pojryw zT(-FqHST_f8;nO%{K^cB3?a#HUNaM!V%!$311p3E3D>{a78X-nJ(KmUI*iM!XWbCtdAz%*?U1a3WZsdna zKKFC`C7@h8ihFGwrHA*PB?zKRC9`dmrTGx(Vo-%XbCWcV*kwZ@R?%m+IET#q(!V6`0KqnskSZ8SV!WE!|t zH*J%2LogE+gI&+jFzq?GXHK&bUk+kV z>jJEo8{zTiJvWJJncvCd_a$M1`a>qQ^qqQojC$-9&bhMW<0-La09Y;n!+RZ92#)o* zf;N>Vc7dNU0oo^o-f)MrT{h}%;e6K=QM{o~g`jqJ! zdJCy6^uE{=t^ObYVN6*86S-bjw)dmeSrngFf>gGj`-mMd zxynduA<;_&geDlSxeg8W5zRitC?(0xsXL2}s1>%@UP;pvvi&<2IACR1z3BxM9x2H@&VD6RH59rtX+9r3&m7ht~_r|oG zP=Z6Ohj3O^hZ)1<=^FTWbhGINx#~a|V>+vepI3Cpb zNZ7r}A{&p+8Eb)WLp~D}5#}LWoX`v`%~xi025v1$>jF;K*DCU6qwzkcHD3!H&dCzm z()5q$UWcUG3Ek%%OSA4v@IHm+M(>{I9MFrf+vbpAt`|mwypZFbkNlAxi%zJB;srHq zKLu*6e%^2wKac&;>DHwOW3rlqYFbFN+dM>|#`v_$dgvj|8-(|C-t;pc`x(SVvBo_@u272Pm0l zYEX}(qd;+c>Dqa-*zz&WjcNYuDa*d9og>r$`V@qI27WKrn+eDQ@#YKJp|@Dry4Nd4 ziT)b=`fZ22WWsD>w7dxOH6*2V<6mGtCOB~s!F9ff$EgFZCxLRqN3bM~f>j(lu_nlW zV-Ayn(1rV243m2^)>2PvdjQ#zRX}9i#C z;A9CVRyhxB6gJOzmWUZJ3{HtEK5jQ>BO%ruGRCkd&70xCF=JQ+5>nCOvWd~Sd#)>k-doj9!W;nS*>W2^%btxk0d>(hd*1C}>A{dz zEAaRV?7i6Ysi^*Ds(D}4{<-3Or|POPVuY(5IQasxf)pF&AZndt1;|TNi$|*KsN;0v z?1OM?j&->+7^<2LG`dVQ1KLQ*(!7B4FeRl|n@;*kYCfvXTYIM4EqAb~n!azt{6X2V zRIQ1xMjlO?sx5V5!mpLEW)Z@n?1P^COAF}vY1v+;a> zQb!3f-Tjp};Q^jRbuSRVF^e5b(<1Bmf}Hh`*wz|UiZ(0xLu9 zB+;J@^@BBgFPcnm9xzuY%#eplZ}uI>BM0IxAcDCaoGDw zl$bkN@hJ=mfPQZHh)H;cVmrbYa!ejl5Wa!j<%mQ zj?!978#hXFaj8_c{kgi$U2~A<%eGvK%M)RFZ}euLd%opxK#V@l-fHqMKIBAOVqrO$SBo+hIFVYni{h; zx>~Fay%FF{$|JJ*mZbkdPZrnc0a?l~k{VKaHg0rFVX!F4z~0&L;TeAy7ECeuLSsr_ zzNFg1Nkbmhps~$%YkV2)iSKxCInn2^INZeOW3|0$VUT0pT-&5?JkRPkOoU-5|HWoQ znSbz0hbB}dsGP<(?J(eYer3a7av45CI0{nl9<`R!LZ=(22U$sSo-JavW~aB+An-$< z(F&>dYI>#gsQ2@5VS?Vfuk*0+F!|8_oHqvOXvPNzbZXB}94opZa!>il{+_EM*bSag z)lP7ED|Y*w!Ge^2uy0RH7VitHMTh8W`-!d$V22n<5il^aX z|IiM-iS_)`UvXK+_!O7sr(%^1{?)!IIEvzs|DIallf6#m6o#yi8!vvq-bMl&x@9yr zyFx#qM`3a3oJWLP-y^}Kke6^f@hR=@#Z1{@@?u6Wk42#c)|I>a6wxN8rCX-NbGLAc z7&OZ+cYD<)Hl`T~b_(Hnbh+JMP7%9u&dp%xOR_2i|5$Q=qZPa=oGw1g7Al&~bM{cK z#aWGh(TL|FLFle^O8Fe255(sM0v_ZD73tkMrXwOAL`&=;#yvLGe#hG7%|n&XsFRi{ zR!e%$7*YEPUgA2{vGlxt6TACxtf7+P;AWz$-71lN0_(l! zGlI^U>nCAPN-gDnUeucl`}eVd>>BuotN~StJx~op;dZo#CDxt4Bn?lxuF9?|wk&<0 z$pqd=ZG<()1#DdS5fKUxL@v~(BP{<^JWNlbDr)0b&(4j^Y4v3^+t$9L#@YQt^?c`NZ4*;g_ zWJ0p);r1=;L~(UL@Xhz@SCqS11?rN27+avHtkI|-v_d(^TIlCYdJ?TVz+29T&iY-o z7(!MyfK<{{6!caeckcx{EB&?U_Juqvxdvi8?d72Ggo16UiH6{Or}#bsw|7WeO*Dwt zuUPA!VMMh0`-pccl7F^n>vUDWhw3^|@;;QZYv@WuU?|r>i87Y+K(GbSE$xn{BGBHI|QJ&+^iaztKly2Whe-6@TZ2Oe{jAZc0B7HMsI z+abFdgKxf;RJqQIk;yN=|B%sLc@w1Mj|w8F)DO)Xpe3ozlG*=u^_v?dj#s~m-&rvi z5DM1X@c@kQ~g9mKrQ@-Zc(!}DhwcPoAFBGIJr_< z-K6bzg|721CZ^+DPqvs4+GxIfK2z>4fM7bvM%tGmj~FjM0c|1{E$lTx4MpHdNhE&d z9js=lDPNb;A9g`55sk%!N<8JqZUJC97kps0Vl~=AdMxjJxhiZ8!PoAF-O=@qB0IU2 zrr$lrz=Uq^Hg!GNwGc1Hm?640FOaKG(;=*Mvy!w?V)vTgnn+{6Ee1~gX(FGe(u~eXH0kM zxU(eN_vv^XqmtFfKXF3aA1ySoSc7i4^JVh66pm$x&R}TGL4U7^%)*U1XTO;n!0I(O zem?;zvvr%m(ms*l{bt_ONR!{VUfn)cu3i^(hjJ2|@{JHmSi_(PkZ*@BA z|D8IE{+D~M^0&dIn60(3l&!6$v)zBt=qQE%cDEGt+wjg+lb3tfLYC-tL97)QHt<*2 z&I6fKK*&iyxpZoCaxrCWi@+B_@QJ7AF9_r9#x-uI#wd8+9vlDQILh`g?ezM3NA4!1 zqHpJ9H(%INVp`-4$343k7kRuzQA1)qp@-R}b0i%p`Qs&?i@ zgIH@Z(20G!3?qV-|9EX*-6Yb``U$x;Ran>^Hw|8^kgHRvykxPci5QtTt&-+4;DQI- zS2x?aXL%{U)4s?R9pvINXhRC`t{RkII!_k?EyXa7b4z$BhVwG1b0ybY>SrDtR5|!) zQrxn&VQy{1jJYAxHgkG;*nw` zornft9c&=h;;Z#V*D@M5C934WvnHYW$Sd>8EF@sEOUpZsxGz#+l64=iQDUz)I`AwB zq-@sc#yTv$F+-0V()&VvEH=q+4n8Byi(o2M~vNsKE*Ypqb z#8GS>qI=2QOmyJ|4)}y8bQz7JMg&GW@#foPAQ}FJ(xrE}AUd8rmQ!OFj(MQzl-c|0 z`*!6=PbGTv+kyALd~_6em~uzHD-vn{T9Nnwwm`9H#djho!6Qx8UH$DbKRc%*x-6{%Tk){(jlC4Q=B4IIxOxDuIanSFF$ ztao4IPg6&z-vwK>?jf6}*p3mbhi@$dO(D&J(rKg1pn>TsN$Hw@sz+U*pIb|B>@o6* z^zj)UD)B0x;z?PdNJ=Vx!0gx=SI%AK)aQq>Ep-6(OLr+$ojONG@vYZyf$rzpiw=OM zp?G)ULsA(RO7eV!=+SU_8}CcIswC6IN)*m!#Ex9|2}V;16-!d<)KXy^S~PI~y+rF3 z8y!cNn}OYLdMM&$2LA+o|IoLeLS6rWZ$v{e^Z47V8u4FM(-i*(zW>+t{14$iH_M1ZPRT`yC#~hxn{~V4;WWg}s!=Yg&lLXL$PdW;)U&t{ld1N+pL ziwAde!-J5aO<6PlxC|)67?89GO7{Dow#um)m(3e=M0L;?aaOQg#*lw}SX9_|+^m{+ zHjw9)2(bWWC6l})4AsIOS!_;Qm7q@%R?@8C&l$5+I_7vgxAKMrW;T_X*V0TPtE-?X zZ<>V|6xFNCY@^LNLVdeTCJN={wGC%O%X@^V3SpTJ`zjpb696Mh0&{67QqYv}Ogzw_ zl|eF6VQDfscKy#Zp=~8nfHbBE^GPE)gsefOUv=KpX3J3FBXCh=?t6{Y-f-!HHfbhv zS|p~A(L%+H$S13f)Rl2H$M{`Xq*Tj?Rg#ZKf zNGd0O_Si~UhRldE`5Gi`?&`m)`jvDprdZF)(y?XANUNos=YMFXWd_!Z@c*^{oBz}k zdSzJWkN+&|Tm^@oR=3l$%D|xrU_7QGS?c!+OAa8`X5@B|qWu9+*{#D*DS3=_>zDD% zhGeD%-IgwSjk#|weIeUfgJ5+7A;O$KY;uf@Sz09@GL>j2bh}&JQfi|TnD8h#AOEAB zYa_CyK0(7QOG>&v+uYqp>O$2ZO-_GRiww%4sPraBqFy$WhY^F$ym+>B6h!-Fre z1bVg%JW%vzH6N<`u_*3@fhnLMIAUu8p2aV)e9s&~ldq3AoH360W z9e-A2Ht69Nl7Q4PPKEu4d7LbDrPivQz|Sx~;SrQ*m@lMzmCiX`hCiK_>J<-F+W^8yffxzr{*p?t9GK^cJ zt3=T#cWN4aG@Wdf)^InB%#cSyrzgZfNz`qBDYbiP{5HQ3oK9J^ zCE0R>K*9Oie@`vuN5uuifZ6bH5I<$iqI{uM}==i=fsGoGmtZu#z$9w!Xk z+rQG%`J@H#1ZnxG6BM4pyfUezW8b5rvGTwzErhD3F7vzMhoBiDxRb3P!w$$RePx&{ zhm;L*hadG`SmPjWCb)$Clx+Z`fAB>(fh~W)v2`@!v#N}SI#=ukZver!XO%fo+3Qgw zt0Yv#S8YzYL~QrwQ6zwZNjIQ3{&DgVLBO}?j|(7X$C!+orf=lGmb^Dk3qJk~du7tf z<&-Q(;06PZR$R{Qf5WR2(&g{d(_rZ|={7>1+?@)tQhc>vC%iAQOAJ5+)7Q)wgzJ_Q z(+ad;X|imvQ=1@KA}TWCkm{4VJnf}3`OQT#*T+P#iuLuoY7Q@E>>iH0r~ch2Pp(BI z2z)~CQp~C7iP$~f-0J};+R~tUG$b{5orza=>>kOBThSJJt${!{f{J*CHTF$lS9-}n zpv(%m0kD*pZ9Dsy*eUWer1Y~;A&m09NHzYm2$%N@nfL(dy-@1 zsAMR_6k(U8kBGv&seWxMIe#Li=@jG$h*Y~C5tq|eIZjXAAjmu2TicU=beq5}KS2)cj;%;uHLWE+#Ejm1&+S=6guF&fk@jQsrr z<1}ECStz{L;1uL?%Y}brKM1F2F+D<7L<|v!5_;;Li5eaVfiuO8JRk#Bcyl_AELPH< zYJO6>Bi8=Ac7Lke_+&M4zsx{qbRo^21eI@BBkfEmeQyEgQ6^p{FvGrJ8ZsEy;hMp$ zoHuSOrs!^Ap4*zF^hVvYkq&z>Ck+s|Qi)rwr=gX_$t^XTUke+BR&7v5Lq}+ZLr5-W z<-6ExHtg2G9^#7jF~9~j*W1qcHzGjZ%B-%TN=^3L)^hKC&3gzw0uii>?YOQoX-FaqRjB(Qr2{ zxew5feRz)|>KXZv^AZ(Ib}6$txE8S!mPMA9R^#Zv*T3G^F1VlXCRwP9)d(p+=f zr@-aJzH7^^?Bqhka#6~*Gqd#}C_*{`&!m0Z=Fp)??nRv*EHQhMK@<6q@2ja968hL4p zvGyFx*W$)m6BF~Bri<;F*-D*9AD|J1RCGqfA&~h_fvDhxD?o-m{@NG|k;MO1@~)JV z?I-Flb>`NokvD0+ahk_ejG0INo|KHqSaj(+PqwBb%&i_j)td$XNoMvIzEn+CcfwAY z!?U1*R!Fnq>W9bh0=kuiSCgTWP7QE34Jn&DGA$xm8SIpQQ_Ml3fmfu3|aP z&eiuP+wS!c%+bD{bUp*FChzz+Th35VQ!Z|>k+ilwbtM00bJ@0LRB0S<(oH;h*HQOTg5F7rrHwXlmcWh2;L6_!pSCiE@a;JkNu4 z2=v4km%$Q4)&oNdw}Utn18WV?@U*8FnoleGY5Lz2l)m$k_Vg9x8wgw4%R!LxNzXdJ zIZa+ZG_0-`<%AZ57ER_`rtVO~UwA(ASr{i^DC4SZiWr_g zixGl$8vO%rnL5j+sb}_&P**J`N(_|o`yz`jFJ!!9apOxvyBPW=Vf%0gVPS-y%sm!F zy6`13$|$)d;KrfF-pTUW89KY2xH!}2+`5nFS|=P3s+s-@`@J%u=cnd>*=8KZDef&w zt!tMD_F?1CRNtt+#Eyf$&?&_mVZ3}9*FCxMHiw013wi!#)NtQo+!S={$1X3n5O23= z$3SEMCH!Nr0y}^VfCxweu6x*5^s)KnlbHi2JtGfj2?~rJbSBf;Eq1s99!y6pWSTh? zX^jNzpCPWrfa65Ty$6p^UDE?30T62m%iBidg)#0bhsEfXd03I9WWy7KS;M2HS|qGR z2f8MAcb;0Y@5y3Yc{HFtm^~=HW^js8dXBfQILQ;BBDjQ&3^Qz_&1sO zH?GFs>PDx#bZkhXc@T^HWejCP{i?Xy!;OqNm8#EgyYsk4G(K}VRFUP>yhtP!cZBvE zN8&!c*PJiRH))3SU%#PcHjULl_DoBt45dvLqnqtU9sw29*SCm_F1#i;3@^^QcgysfV5TS!-pw=Kg0+K5FiK2H(P)B5|_8y=3mzc<&DlVE% zLSbjAdUk>F%Ir+YPs%Rx>94{1A1+p6PaXQi@X(KWPt1lIYBN1wa4tXq{@!N$)Ve_l^*niVJ+R<&w$FsQ(N8d}a8L{bUwWxx&%6$=PEHiXXTSqY~zZ}U=3 zi8FwsUI4Yaw34gXdX*D#B#UHW7f6V($P`KEUe7yN#VRry2{VhVra4~fm?yK@RDRHfrFKua5K zQYU!?QYYU9_fN6%0DPL-@#%{7d7HE>AEIWEa|ML;%ya^4Ky9ZkZ8Q?{c;cw8tuCac z<&5GM;qK8kNN^40s~(9|Yz`d2r7?%{#L9CoMzvCiTWKRK;i@IJ4MGuN#2h|IzK0%& zGh`#($eM2VKYqe_C7*yWOK5g%Q6Y)Q_+As`FvDXh~(Ej)#lca-Ys z()0m_?+9~ycS&&b&J7^tUnM0z2?_A*(sbysKr=oB>j2U6d;Oa?v#0hY9PiC7W|J#U zcW`C{Ugkq6?sJC>9mNy+;e+-DZP4Z99RJy%^&=VRUK_E#eYkPR&7FIMD)zChh@lZf z4ad(IGsIc)(Y{$4bmbEr3(73yr!{VVJbW?`ISuP*F?^B zh4`aQkKNq1l6Czx!a!w=?r~Kkjf};J;>gMihodotq-H+$>0 z#-94|YvYK~afsD6Z*;aNPHVraG;2UQIbd&uZMe>{3Sn5^8%9iBH~gKSb9!!x8_$9u z(`dslc+xK#F>M!#Tmu*sdf`e5*AdC~I*~%6(_=ga_V_Qa`qTO*lb2X zb8eDvI`wfdr#lt=dQ&+)x6?)*~Ltw zt}pm1gtJtA=l-G%lA*Y=2jSv1Z# zN1rVl`rZr062*)}IQG|$@8w%lh!{W6d6&fRV}*u@-TjWSHE85FdjFP*lu8vo$`^8o z@~ioRUrW4W7d-&`erEo}5U!%ijs%JLFo`E!WA*txeJ<`R#KQ6eMS9?bEGgE@Dq~!_ zmW$R>J-=@d#H>>?%D=72tWyr$7Q1Ld0Jn||6%o#ja)#hXvfhby89gh_wtQj%mGE7Z zzjDETCZR;cjg?Y*fKm)b01R9gs$sdGRXx;!GxUl%(6&o%;bjoYo4p&9Khdx_s9E{} zM7m9jpNX0cKw+$sY3>%Ih00bg?La%-I7{$g@x>|K8GfRXWg6>@!aWnikuFyXyd1m4K6ah1Uv zp5DaG4I1mCh`;Q|=$Kz4k`L?^FH}8t!R#xkm1}~Qt@a{Ipn6B;Cz^BQb-AB5=Fgiw zqOW>r8y?;tw77@5q%Te6pC5EzMJyvew0k}zFI&sh{BX@Ggb_*r{5kl2JfT~a~qYraWtWW7G4*io3yDjeY8iHz8iQSse zxPiPiO}48OQWcF3K%e2gw|W0bcuh{p&c!19`0-5gZ`o#o|2^R){y*Z#|44K(M@rj# zzn?B>XqqHc8Fo5p2|QVX5O`6!sET3+Q7Tg$EUiB|t2>`Up?U!rbX%@OB(%vb;_4%l z)`#?mTTq(s)}DQW&)6?#NiXsx1yzQ)Ri{RMER4D0EOK%?QK@6{K+XHo&3K!SFL)>XKP9TnjQ=&dweSLJc+zTE7%IzYpyD+XyJIp|n(B3q>P&j7=q2!6S;SHL_yP2MU1HS&bOkAH; zWs~1|!SnC$|GlsG|B{p!w{djRx3VJoKjVKnDg4J3GmaaR0HQ|<{!(i|9nxvHUXSvx zY=lvReDKHQM?&fm$QyG?mr-OyjW4C?_mA1n2c!8X)5@iY49^~$>3Ew(8{6*F-uZKR z2%~S#&2!GPrft0gnSY9>exp;nVPiU`aJLpJARYJd_d|o+_g{tp*;riiH8+{;W4_f) zCBZ9m-dga)STNX!4sP-{X_Bh2`W(Iy7M0kO*5gw$r?*9Ixc7OF`_k`g?1;7PN>TLG zAX`k`FkO-FtXtv;3vi|3zfNdya=SDe-T=jwe;8wow}%)Ub*tOs(>!Y;mPBa{iE_(c zC|?uH&l16&NVQ)RYcvM+xA-27Og;_(o0=C%HYx!w$XQTRBX&lid?qIUBj`fr2zVe1 zJh=?9f(uBIiCMXj=f8CO5YD_U8s8b+$$!o2;{9K4^8cQ_|Cq2!O-Q}Og(RM%$-SI3 zs93mPFtFI0e?<9y!F1yJ{*KoJr^n{EkAKG}Oh{vc#nY&S^<20}Sf$Y@T5y@OZYW0~ z%`0hMqiJo>Y|?Jq;IXnkYqTt1;Ayg)Id{GMnj)mj+qk5dOnc*bT6gTa%i?@qaP7Qj zg3l`c3_m%e1?~vz$upyhk0#lL4hZ8xd=O_e5m6w) z03|35-u(I3RhEJ=RRiucreDG2!#{5WIv_4DxMWUy#l-gk)ur76}X7FUgIP(2&=Kg1vjppEyxs+wp7-9l1DbtkG z<}C3mZzsdhSFo5j*Nzm4ikz6oG{UP8&s4j zxTxX@BQ6BXQ_&%STE43+lIAMiiJjDCCmu!&fYzY3>=mkw>#p$2D( z{gN<%B5m;<7m2lGRFtuFR#wqGJw?kYIfrXM$@cHZegVS^%aW{jI>FYYNjf-h*duIp z3}AM|OjZQTUXvisWL04UAj_L>7H_;*kL;O;sJ+*=G@rQMaljK{wL)+S>G&FP#9;Ol z%Ib0`oxJ0V^1VJ3<;@JAI&Ir&Q2<1lQTasU7}e53yx-tj*eso)BzeRV2Bs)vjSh3} z{xrQ&K(CjA5otf^nA6x8-Y0vv34|PRTL7tpKa-9uCsNIA67NS`bx_ zZc1Q^7E%XY)S9hgTn*XQAm1i6(=*@7w46ex*yBckeJ_kb+?X&kf13@ysVO)U8)@PLKHxjVKET{JJlJj?*cTpW>{(F{WXh$j-VY-L*W zi<^;LE)lh_(yqjLT#|tlWq7p8Vi=LJ?28~w@->$gl}|lLeV8+LZVNd1p zdVQn*%cu#gIMS@Pf{85&x-=|n0}%AjptFKD=@!8nGxYPaDc;QpL@kyU%am*&mSxE| z&&0fsX8e;tJhsQgyFoUkg=4CRV-IX?+DCWTId#OfrdGE)(~qNs%;mDL%RmHU>9+#)la5*Nsg(}Ii>KAW2`Y^c^O@e4K6=`1_f z`5&NK3!IB#eO)n^k2Bc6G4>{*9)xFW3j6?zDlJ5~xv=5RJc$`9nl5jA47IeggnD1_ z<8zpjg;Km;O8M*9POyh3bAtmCzB|s|uWTO;QVwF53|#g3bc&P)xiHo_-SPm$jPVb4 zA-03Eef*uD9OngZCkX<|tfxe5iFz=1K$5&lZp7;O z{4QQ%^|AM~=Yq0OshiR>Vq=3_u>|puV#JXW1+gFTBYVp~@f#etNfv77O~--)fD)K{ zruGyK1aN{9G?Qng^%V8w8T|s9>*Lp2(pMkzj{?DD!MJegR*}=9K$G5aAq6h@vE#)+ zgeES){D#wKPZy_8Y1&AuVxRTLN)HWJQg>4a|kuFmHPo$>QRVzcu)nkcu-7 zo#Q+!l2$8QgOk0dwJipyf)aK;CQS?=|74qwgF6v|c|~oRtj0|7 z?40=hUPZoI-m)j(^`iHf2KO#ej}*jV*l&+oajT|gOD{U(^jA6m87GN~4pxZbUm!dn zODD2KpoG^?caqyv(FpUoiH1IL(V?U~KQA@SHj=}N3j#>Q^2`gyN@50yGG%|>9FIdm z6aO?NvAI(pkUXL-fpOB!H{{_Ell3Q1=Yb(1L^3D9Skm%)!w`AqB+8@iynuSE@ve+xwN(Nx0D!acsZp!ooM9AJ7RK9Fys#bK0l(&-}_XCwZh7V5%TE6|E7c6J4CXa8iHo>N#5C&<=c1jnUI zmiLx{ir60!O6FxYl-3{Odxy3suDjS>Lp{kF)49CeNl$+4+vA(@de2+;qA$&&BKV6h z?laO=N~5h`#AupL#LS6Tlgr-fj5D>8jOz-7&?0~<+Ch@#p2mhWo72_B*1;(l7kBmA zh|(59diqT#YfDIgZ!efmx4uvWR{s-9^{zf4YDoh%HLooC`GAGEknHqYv`{-CBG4Z# zZGsQ>zoLg#&?&WBLp<5>V+0q}Xk5@QKV_PTQN(w=Rb`r;S`h*$)ov}S>MxU`r?rAS z1`fCq!ZNr#QEX^A`ov$yKQ!M+M-ntXxH67@c>SSKyp|&xOtdgUAtF5xt zj@=6s2Qvcd@m?4Od0T&`MXJLTD)|5D>@1+-TAHCy z>y$Hy8*;*Fq`>fG5uvEMH-T(MMNRY7%x~OE8=TMlu!IrT&*T4*DRKB#;BfQ(>`G+A zdzt z9ZxT3JMVR*4#>EdE0Ki_rrH$&JfIh~fh+l9&qXwATY5P*3j*DvcVS+_erb6b5C8zVSz(fg>I-JRqK=r}Ch z&?GA9*PZ1nCc6qEk;e(i4TF7v_=*-$Hky&}D>9S*8_Swv(s}WvpwFxraxw^14Dr5U zsN$!FnlZY}tU>Q6l5n0S0s;Lq@oHupo*S=Bi0@W{Bm#v|`Xnnq;;dkf5lSV+*5zO; zp^J8PtMKl9RSLr@&Lk1jdo$GU#d8o_!MaoYaTfpu7lS1jZbSc^9B~#GYx+rT9CNA; z$KE=l|10@xNdF#%5l>wR$8$@ovm<3Mq6-7UXpuTy)ccNYpL5RyKo?!)J*hAB9FukR z>uK2wlvnE6@S=(e}CM)Z# z;3m?ssM=H1#JyDXxsSEc@TrYo(eQ%qfKg^AzT?q|pO*{4kq|+>N6f54=ZZi7!r8Mp zkIRqq@=^?~=drOR+VDqKjxX4PI4%^%-9qG?mbDbe6{Q^8LdCfnP@LvMmOHF*lsCM- zgM+I(Kpd8FJ}^Ks&-LZk-bB4#Zi=(f-SZ%TFBSDCVITar_O?9qU7X!may21m9s<3ie~aDxza+I;QfOQxHmL%)UT80=GrYrSkE4#246lm4atO z=|7|D{)n;f#jz%Q*KpS{1V2STIz5XND^R=(dbAs>i}rXguNLMWm&jC-#>i3dzL{%> z3wZiM+*NS1rrq6<_KZo=@Fu0 zk*Om(S3`+~akR_&d{m0?=caf*r*rb0 zRNRqCx`@f$oS=s-{jxsfNCVIv-MaK}nD}$)E zvf<*gx!3Wz+o9TJO9!LgR$*GXj5-C?^aPx@Ruq0{N#mEXNIZ(q)_nXQ(UMAT9hICI zu8Wo_Rh3Cp)@FZYtk>tc-52(NCKAz2M9i6=w1&7X%4Gn@-rXPjh$CjZc+NAop=v$n zal5~gya+2@gc(OMSA4+`%wv_`isc@+1a*5ZCIq@_^I5UJNc-9Yhp$&F_-dRdjy>Z@ z&)xE*oMVn*lh+TGf4U_qjo4K)nsiTTu@q`$MZwRCkb@=&MunC**pBBwarrExVLv|Q z(!WO(AmIwBr&g)6r)2m#iXwZzBLCgGa+X!`7o;m}DZ#IeSxsE$9!X0S{MFfCoa{LX z%OqsMT5Fi&PRj+B_LMsik~3BL_aY*e=)GYmZ?;;Mx^kq+6&Ehw5?`ftQ=<+Lvk$rS z-0y3;Zg?Hnm0_el_m|4zXEviuNXu<_ryc|+rRm9Q(In?h&+%eEAE1gjYVuZ3k)<(a zm~edjqk5lkzd%(LWGHnDhaFOC;=2qfL`(SIK-hfhD;XkcN7paVXT+Ln%4aCffas_D z;*S*GdJ084*Wzs1Y$zb5x0PooTV*=(GF@joddwgut-*Q(QBZdOlvWIT>oH{ROqf0K@rHKr z4LdabEt}YADI4;P3|ULQ_cZ2XzRZ9EKq_GD9ghhzfiMLK{4kLK3(}9YTWu| zz=XRjtWotFEo*X;{&2gYv_?%W55}v363bCI-N>2nFUjV^{49572h3X7b|i&!3i?|X zFVwg{@=Kmrm1mCjk*O!o6Ft`V2GeIa6jI zX?=0(Hf)~U+C-Fp$g~hWgB_CxRIOQDpXJrp&8y#}I-~Xo!O3$nm-m^S3N>U|VXEP}I14~--(h?O$9?r~7FRm6GuErz^op`U?0xVg+|{5X z8^Zk@T2!CeNKWvJTv-HK*Y!u~MpHC9-CRhYVn0wxlOzjRy8>FT9ksOaeBnFW#Ukdu z*2vOF6WRnc85H6T0Q+i*du0JV0s7rG)d33#DY0E08%bYmNjBZ44tp-uKLIE{8oSOa z8?&v%6%rCDOnVOQHpR|Pe=egce#%egmL&72$eq zzh{pGiAy2&>Dt)LAVHmreT#@Jj#KoEw*qGy(fA?F*bynH`0{IYm8r`p# z!;f`-^*1YYQ7*sgD^6TLGQ#hMDi;N>IdaPcCzA zt%N+qg2WEdN;t4AI0NoP{0!HL*52Yn%5AhV3UkDvhCAIc3r4eZZwt1iy~1x8Zt?DS z>)`?jHlef9Rr9;fDY zb!>evHN$?uNlSxsD=^;@89XH)q0__3{asuR|BoVWF z9-YAwJim7^G3A7_BEAKoRbk4`ZtX6hTd*y@A`o}bpgfw@5Ft=OmZLukE^U#P+t!$< zuy=vJxPi7sc3r^W8$w(ZP>YJ_*S!JLQF6QP&V7T;r5_Iz%QojEJjLjgEWX-eDDy`4 z$>+bI=dmWr?Jj-#tb;$4OonJoVIOO&7Pm)dLbx2JR@rM37VxRhuZ_&HasB-gq|U0S zy66+vx~ir_xHj5%tOLq`_2cs=E+}^Fle-L6!LTOoIK3hiuyuT>gO~7780iEKs;{%u z&E3J}@oXcUY&-pEgU+evg-&K-+fcx|ku?Q4w<@-_yIH_w!Sp9bkN4@Sxd(7VAVcc$ zJ00wkoENC{pGObR!^=XiwD~DNBi77l>VnYhKXI_aJis}&MhHuLRkZxLJd?-?c4hK; zu`ciI)wIIYfU%bh7g4X_YFNh?T}_`ZuI|vjgUOk8zc#~2U=IMn};c8{+UB zJ$ylAEpm_q7EBc45YxquKS2IKsS{x826%&~d7G^Tbsn)XVlV>6@f zCQX_cy$v7pTuIxWHjV`Qi$Rxf`9*|3fYD{#jfrwEM?{h0UiW}X&?mDG>btAYdQw7@vf*&Bxvtdg|Wd2_9** z0~GM5N!iS!DZbk&U0dbg{Wxb79Q(>#>>TfoZ~ zYR8D+s8r;O3?)HkH*iQvH^`r&-L%Wxv=IUb-Zzc(lNHy$D`DB;7uBySN49Q&PN#Vcts8AOZxP>RynmMO=$;kX(>&wVM+jpnO@1n9t^75|nT7YmsK z`Ejl-Xu9IdpKkl%LJe`1jigpEZuhoNCfFwlchoxCS003c1FNJ}Rf#^VKOKVX?gEro zia6X5?~bRYs{&NPZ7hO{UDe%J0C|GaHTNAhR7f10MHBNX6S{B8UMAxyLmz0)<;CXO z9@3kh=y2w;GQ}z&k+-pwd8wK*O*1z_`5-GHM(;)L4HFbC^-_}5X zB|NY~1^D9geuauUm)rV&E3d>{k|wslW_{T`T46ujBO{1RnzFpb+bZ2f-)=wX=u-Y& zt}-L!n&9*CeZRKn{UXH0e!@eaz}`ah)nvEggnso#n{>M2)Haje932Z`ZWJu98hoL2 z%=XHLOQ@$kpUbe8P~I`|nr8*sRk=BY!$)KVEK&BQB7c<#v$e2=V)KxNt9D^){)f~ zD>!?s>Y+TkI<@?mz2McUgW{-|S9bDyFX(%)8ync-hOsX`^wl4=8``mNl(!6h6h{?K zHnq&Ipy~D3?2i@DlJvIhKOOsi`EK27sU;p7Oi3p}x@3!9SD(LfjVMy3Zo?gZqNW5HgF}#siB(F?1{G|l9kJa|UPnySdQhxXu644) zHJ%$$3w-g`PNbS)%o96vn?RN&(uY&HzRlq5BH^ZRnMF_9=Yz0vOJLD)$J*=A9%qsw zggN&1eRmQSj_fyZP^;!7B#_)4M89I2n;NW=XV?QB){I`m9;nj@3Pv#pnPdeA*_&9@TK6H=Oc18IW06z!F6zN zRjg!@!r-Mk-Vj83W!Qz98rpw`HjX5}L-q|OsQgO-@bOvTk*s!*1ugHp@z1;;-6zvB z)9kxCy`XENu|qfq2Ng!D-|{S=2F)sRs(7pGK6hO>myHN5`h;cMOeT2|O!1P`!Z8L` z%v)g>R>%Z)e}JJGqPD<8e0oQ3I*~Z9!J|!2KVPHEs~0GqEY=(M1=5Kkw_Qg|kG8ZL zwg(S`o%7h$akT-eFY*&%`ua%-MFXzD1>d^0>Hae-zYSWS{SCO53S3X$LwW_A!&2Ms@d-3c5{NH5ji;Y9ytacu`m+8 zR;^B;A&WZyS!HnN9v=2^R%z}z3tVueK|CI39$$j(8<)mGED{Wg7G0?gh@w!YVEm9H z25^Krr0}QEqf$mFnDAp87)fK=%Z3`<+M_$f7uFk07|{$gS60pA>Ua$&vA*G%p)@CY zrWyyscD84-FHgKmWJaLVj$zmZn%~X3Ur%b`1_rQc6ja&G7#d5?3G+;%wp^(6BJGv! zy$X%f#r2U93h}7$GkoUTB zzf@iCU$X2fHXpvt%vn#&d?w*oV#4@VmvhF|=zZ*C%P>amnO32Qfan8>`BL_RIu)ml zBEsQCRreCmO`L+Yz7F8|{T;P%Ia%_7JT|*zh)%v#06e z^Vsf_;seb1K(%u%^ZDmnv@FzPh*(Qbr2G_Za=-z5R7-vF`o-vSVnxh& zD&fWeCkaYh6#c7wgP6*E*n>!b0NMzm3-%8jv1<^?Zh&lo+ICLiTDIPOSSBINgja7! z9tLaeDU+!p=(I?~Z%>Rc{Q7bNsuTyR)b3(w>ikf;U((Rdo=)aq19(O21S@@MK9jg+kQUEaK_UY_@N7ZLj4I^AwasK8bI^lJgNsZ&?G$^jPU z4m_Cwm$nVOB1)uXY1PlF@-I>{bWXVZO~+;SOslFreflFz!_)VUOvb1SY0?u|Oi%>e z?~HPpggOu3`gl!q%CXouEaX?1q!Iv#0I89;co4>l&P~Amn<7J}kd}kw847i6c6X22 zIik?Ez2z!7PD2NU>oF@F|Ed6#kVAG`5t)p1RcS_?`4$E&7nalHmLXfu0Cj>mB=gO8 z-ODwzEaQdK*X4{zto+s7DR_^^TOg8hXTR**HV#Sv#nFUP!kMC3PdSZ>ZD$KcT$mJu zI7MEm6V=M$oNl@1T67 z8^q;acdE==L)JX#1`nL#R7F~+6A87nE=MXRl$Sg52XenLNiz&FZngB}3urc7CyP`( z6tAvNS1-z^dR2~>BzM+iBup!LKCp}T$%}WeUx;`}l&^rP>TwvGh&p1y8vn)=+R9we z0ZXP;Sk`jAasa#z+$J9-z6Z)Cf`EadWF?}PjHm;;uAwZM2%Clp>ET5^hh?af-T?NV z#8_yM+*>@gy&U`Z(w!vh=?KsZ_`M%!YokIP^2L%}-Jm(8K4wqptXJaY$wGvaOi)vR zPDWvqfR0Tq@O7;nG)|EKcb9BrM-$uSd5b{@AxDNxkB&8s66CsiJQuT~?Q zO>t!_z7ZP66`#SYp>%{VQ2cBLx$iBy|2^xRt`}M32}K|LYThG5EM)3*1GBs%$S2M( zgE|NJxrJ9h%P3g6JDSOSjONP@=oLUy(Pd~mH`7fhxMxK+7;X+yT9@+OcI-Uy}RQ_si-G z^rH_UPn5lA?@FkZF+BI4%NO5$*fVhb-*?6O%n4NpS#{hGD^%+HPwO`T4hb~RwGpSa;wrYEQ z#Pm(&qu7(S$s>V2uVZeDbpi1Z;wqLc+_Kcew9<|F%_RV}$03S(ZR(_pO5E4i=MqLW z#*r275M`HE+foP3Z&)o9>J`wWpP#3+QEj^!Zed<{%)1$=uB)v_9T;pIvM^V-COs$?t|hw{}T-=J8FYkAKwa+2JFS4@E)u5MVv->WEfFpkr|{> zFzsYrmNuFE0m?gal2>73ofc{;dxs7YW(t|0-JmF7SEzR(b5S4JZ?{_oJCgK01M4khME&^!TW;Ylno0B)#CEW9h=z6-}mz zSVp&QRXK3yyq`V^oa(@1X}p)08_~QyakYQVYcMn+GY-y}1mNgE7x`SzjFRPrGf!!` z5k?;hjEt+{Pw_ust&nfAba_pCz7wf6Q}9{BDmg%OU9o;r;9zR>t!D=$296$H^12!@OCbR^L&r$dx*j?O)UT#dlfXoXBHpA5@;x&R zkztjcaBvS{qkfi-xi&&KF%f&UP*5*9&Ed}4J2S9u*!KR;-#U#@d1YiJZ6kZ~@atzo zo|N>*xLHR0Ed85oUs=W0dC^+LcH8}i{+eD#Gap5vgC;NFzm;02`t|+r@1PTVX=?2H zi?XR|);2`y*mrg>7V1C9C%im%S(zFyIx_CCPH36vlbYqQla@1n`p$L6#>mQSq5e~t zn;S=ceeHV*X~>4)WC1ENHbo0dGAyn1nQJmSf-sU&75Fd`$r~Y3vLnbaK}pH>f)LO- zjU%3SDi(zu&J(wX+^hT-na;pLz|FA;nA?W8|I^7#sn1?)@$;<4;*R7qn8PMdY9$9~ z635yPQ`MODYQ&NDZJ*fhng%B1dvIstv$Gg)OXm{p4ewvop+_V-9R=uxXUv35ci5`) zycetJa5nUAil0<=T&fvuknW#|K8&t99NH9u^H-cHTg2b=VYP=7rQ=;lt3QitpXRpv zR2XmQCa@Lt^=YJRf2|F)kAKqBvsn*wJ;og#)FK4}d5gHm88r3r- zmX*Nut(TRsM{8N2ppkX1d_9zFXoBV}6mvJ`J)mmT#L&);tvkZe?BetZq25{%BiKEW zGVZ10HC;G!#0K;nklBc880fM$RBM!RNeyzG1_Zo%*5}H5tg(vTYi5J4p=<4npapf~ zE<=K@(bdvEQfFO=JdLI`%}`SYBPygr8au3QDk4^lqzo%STVoXDSu^e;Hn8z33)Lv2 zo1VKb%}tPciVrw5jz+*5@%p_jXPJtca5H(0u7P?K1*cW?>YI@XG?7l3H8m-b=BT{Q z?fH@Urovj~T0&2gDWDb?AaC7;Sn8CzMcY7*{)1fu5P6;WGiFI~Ah~tTv(0jC>$=yd zUnwqq(%WF_;bE;fg;PY&K)GY+h^Tu11 zWUBF6OO!IU!u$?pMwZ=)BtsUh0q6lJl^JCSQPrNa`D7I;U$^375eZ;57t~NbhG@^Dh)qmr|A$FN%0C>&Q=& zD1y#4aqQum3XAcWQ>NN?C<}J?@V)4RijQ1jdN@irMuhl^H@F#%CRvM~^S_QsneDM@ z0@O2>y`0Xt-4Skfh&lue;Zao6`pD#z^3262h)DoSAiOeOHx~2o_G0sPalQh+bWK`g zH^)}hiQRT2Ue*1)4fuf7j^VCab=BD&(tjmdxXbjk46mKi0qBSQ0#$&|jwgSe*_dUe zu5bl|!IQ!&GHBfBW#?@uF0jlY`E2hoZ@?v)w)=b?VykkSV^+y?cU+T$^9xEJ@|crB zvUY|H8C4Bf30rF&g;CEx@-*|ob5QpS5 zL7Sgq57RVV(3b#QkO{=-OZM%iBY;d2UaHYWCRnt??lkeb*_a!whH*c7{e)30dbFFo z7vWC-u?=UVs~^`Kv2DyiUMaqXvqly{YESwl*1TML0NTL*&jXq_rDr6(TdwB)r^!Grz1`oQC`AS$p)3?;@_@+?ymL~CW(94`c4mJ+{>_%t6J#4#a35~|HERVc z`$6vEl6KE4{t8j3aU;x$qo&pUXl=)0$(G3}{wgm{VuwM24@^!(-G;Tn{p1qVB!&wq zbwq`ky|KI~n~A*1{gm9{X6SB%C~*#hL0mf%jD4P`9B|t-&K>eX&(`@1YZ9p1p&YS+ z2JhORQ*kYnuX**U+Qbs5OD&plpUjra@&Jm-Q7&=k*Kr$eBtFN&-7)t06q%6RY!Q=A z^6(AJDKLpmvPc1p!Uaajy|afeF%nLRFi#J-x*z{&a$+h%^iuNTjIvT9V)Dvjsw&W6phw*Qb_10T zS5OA}I~3@@SU@>1er$|?+U;{WJM>TAMhERY*@(C*SJf&;V;f7x?^L#hoXlNqLB-ZVLU^VQznG~0-16Jc2^LPEIZ7sIj`FaK^38J5 ze#iNx3hmF%(fIt#q(IIyKr`d*jChyK`1Zn8gM%2_&t1g*^fLj^_3pCB`f z6LdR4NLo-)@*kVI_2dtbf3w^SolO6{nJ&w+er}+^=LT`Pe&|{p&=>Tdga7Zi`M>rm zO85sgJP=9~6hJ?Q=?&E% zddlRx>*BvS|6cg{L4^C8oq_g7eu}vNa{ixy`xnIbDyKh2Sq9$^i2qfu{8K&YpRY{c z3uiv)>U=YF;op${&l;*fbH5i$`|+MdCHe#RmjY@3EYkTi=X>3#2kEtMRw4cGoIey| z`x*MZjM0N6)i;Be{{i|xyAOXPQ1vtAd)1X6hYqOlOUnNw6aOCtl78lXFFo;~>GI8r zlz-3tSB00KQQw#9B>Mq{ul`%qzkI*Guj=@)koV10%zo+j56XW2{Mo;+bogV0`&;~m z@%LKJKUa(Vne%;u`9s~rZ#HfDXPkfOtnX8^AM(_{S+CbG0soY>{_`jQK5O{LQP}hT zwc~%!^ZuFf{kHW({_!^}_Wd2>U;5 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.21.4/gradlew.bat b/mod-runtime/fabric-1.21.4/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.21.4/settings.gradle b/mod-runtime/fabric-1.21.4/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..b6680a5 --- /dev/null +++ b/mod-runtime/fabric-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return ResourceLocation.fromNamespaceAndPath(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e20091c --- /dev/null +++ b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.16.9", + "minecraft": "${minecraft_version}", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/mod-runtime/forge-1.19.2/.gitignore b/mod-runtime/forge-1.19.2/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/forge-1.19.2/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/forge-1.19.2/build.gradle b/mod-runtime/forge-1.19.2/build.gradle new file mode 100644 index 0000000..b3f830d --- /dev/null +++ b/mod-runtime/forge-1.19.2/build.gradle @@ -0,0 +1,89 @@ +plugins { + id 'eclipse' + id 'idea' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-forge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(17) + +minecraft { + mappings channel: mapping_channel, version: mapping_version + + runs { + client { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + mods { + "${mod_id}" { + source sourceSets.main + } + } + } + } +} + +repositories { + mavenCentral() + maven { url = 'https://libraries.minecraft.net/' } +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version: forge_version, forge_version_range: forge_version_range, + loader_version_range: loader_version_range, + mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors: mod_authors, mod_description: mod_description, + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + [project: project] + } +} + +tasks.named('jar', Jar).configure { + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : mod_authors, + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : mod_authors + ]) + } + + finalizedBy 'reobfJar' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-sin-reserva/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-sin-reserva/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/forge-1.19.2/gradle.properties b/mod-runtime/forge-1.19.2/gradle.properties new file mode 100644 index 0000000..e7bbf68 --- /dev/null +++ b/mod-runtime/forge-1.19.2/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.19.2 +minecraft_version_range=[1.19.2,1.19.3) +forge_version=43.5.2 +forge_version_range=[43,44) +loader_version_range=[43,44) +mapping_channel=official +mapping_version=1.19.2 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1962a79e29d3e0ab67b14947c167a862655af9b GIT binary patch literal 62076 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 literal 0 HcmV?d00001 diff --git a/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2617362 --- /dev/null +++ b/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.19.2/gradlew b/mod-runtime/forge-1.19.2/gradlew new file mode 100644 index 0000000..aeb74cb --- /dev/null +++ b/mod-runtime/forge-1.19.2/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/forge-1.19.2/gradlew.bat b/mod-runtime/forge-1.19.2/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/mod-runtime/forge-1.19.2/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/forge-1.19.2/settings.gradle b/mod-runtime/forge-1.19.2/settings.gradle new file mode 100644 index 0000000..93f1b80 --- /dev/null +++ b/mod-runtime/forge-1.19.2/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = 'https://maven.minecraftforge.net/' + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} \ No newline at end of file diff --git a/mod-runtime/forge-1.19.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/forge-1.19.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..1f56a8f --- /dev/null +++ b/mod-runtime/forge-1.19.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,151 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES. Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. + */ +@Mod(ModpackESRuntime.ID) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(ModpackESRuntime::alRegistrarRecargas); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); + } + + private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { + evento.registerReloadListener(new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla", nueva.tamano()); + } + } +} diff --git a/mod-runtime/forge-1.19.2/src/main/resources/META-INF/mods.toml b/mod-runtime/forge-1.19.2/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..94577b0 --- /dev/null +++ b/mod-runtime/forge-1.19.2/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +# Solo cliente: Forge no lo carga en servidores dedicados y un servidor sin el mod no marca error. +clientSideOnly=true + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] + modId="forge" + mandatory=true + versionRange="${forge_version_range}" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/forge-1.19.2/src/main/resources/pack.mcmeta b/mod-runtime/forge-1.19.2/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..7ebeefc --- /dev/null +++ b/mod-runtime/forge-1.19.2/src/main/resources/pack.mcmeta @@ -0,0 +1,8 @@ +{ + "pack": { + "description": { + "text": "${mod_name}" + }, + "pack_format": 9 + } +} diff --git a/mod-runtime/forge-1.20.1/build.gradle b/mod-runtime/forge-1.20.1/build.gradle index 406b1a6..62546de 100644 --- a/mod-runtime/forge-1.20.1/build.gradle +++ b/mod-runtime/forge-1.20.1/build.gradle @@ -76,6 +76,14 @@ tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-literal/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-literal/src/test/java' +} + tasks.named('test', Test).configure { useJUnitPlatform() } diff --git a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java deleted file mode 100644 index 2cce7ce..0000000 --- a/mod-runtime/forge-1.20.1/src/main/java/io/github/consterna/modpackes/Tabla.java +++ /dev/null @@ -1,286 +0,0 @@ -package io.github.consterna.modpackes; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Tabla de textos «inglés → español» que genera ModpackES. Hay dos clases de entrada: - *

    - *
  • Exactas: solo coinciden si el texto es idéntico (salvo los espacios de los bordes, que se conservan).
  • - *
  • Plantillas: la clave lleva {@code %s} o {@code %d} (como el {@code String.format} del mod original) y coinciden - * con cualquier texto que sea ese mismo molde con otros datos: «Hello %s!» coincide con «Hello Steve!». El español - * reutiliza lo capturado; puede llevar {@code %1$s}, {@code %2$s} para cambiar el orden.
  • - *
- * No hay traducción aproximada ni en vivo. - */ -public final class Tabla { - public static final Tabla VACIA = new Tabla(Map.of()); - - private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); - private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa - private static final int TAMANO_CACHE = 512; - private static final String SIN_COINCIDENCIA = "\u0000"; - - private final Map exactos; - private final List plantillas; - private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > TAMANO_CACHE; - } - }; - - public Tabla(Map textos) { - Map exactos = new HashMap<>(); - List plantillas = new ArrayList<>(); - for (Map.Entry e : textos.entrySet()) { - if (esPlantilla(e.getKey())) { - Plantilla p = Plantilla.crear(e.getKey(), e.getValue()); - if (p != null) { - plantillas.add(p); - } - } else { - exactos.put(e.getKey(), e.getValue()); - } - } - plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero - this.exactos = Map.copyOf(exactos); - this.plantillas = List.copyOf(plantillas); - } - - /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ - public static Tabla leer(Reader lector) throws IOException { - JsonElement raiz; - try { - raiz = JsonParser.parseReader(lector); - } catch (JsonParseException e) { - throw new IOException("La tabla de textos no es JSON válido", e); - } - if (!raiz.isJsonObject()) { - throw new IOException("La tabla de textos no es un objeto JSON"); - } - JsonObject objeto = raiz.getAsJsonObject(); - Map mapa = new HashMap<>(); - for (Map.Entry e : objeto.entrySet()) { - JsonElement v = e.getValue(); - if (v.isJsonPrimitive() && v.getAsJsonPrimitive().isString() && !v.getAsString().isEmpty()) { - mapa.put(e.getKey(), v.getAsString()); - } - } - return new Tabla(mapa); - } - - /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ - public Tabla combinar(Tabla otra) { - if (otra.vacia()) { - return this; - } - Map mapa = new HashMap<>(this.comoMapa()); - mapa.putAll(otra.comoMapa()); - return new Tabla(mapa); - } - - private Map comoMapa() { - Map mapa = new HashMap<>(exactos); - for (Plantilla p : plantillas) { - mapa.put(p.clave, p.valor); - } - return mapa; - } - - public boolean vacia() { - return exactos.isEmpty() && plantillas.isEmpty(); - } - - public int tamano() { - return exactos.size() + plantillas.size(); - } - - public int plantillas() { - return plantillas.size(); - } - - /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ - public String traducir(String texto) { - if (texto == null || texto.isEmpty() || vacia()) { - return null; - } - String directo = buscar(texto); - if (directo != null) { - return directo.equals(texto) ? null : directo; - } - String nucleo = texto.strip(); - if (nucleo.isEmpty() || nucleo.length() == texto.length()) { - return null; - } - String traduccion = buscar(nucleo); - if (traduccion == null || traduccion.equals(nucleo)) { - return null; - } - int inicio = texto.length() - texto.stripLeading().length(); - int fin = texto.length() - texto.stripTrailing().length(); - return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); - } - - private String buscar(String texto) { - String exacto = exactos.get(texto); - if (exacto != null) { - return exacto; - } - if (plantillas.isEmpty()) { - return null; - } - synchronized (cache) { // los tooltips piden el mismo texto cada cuadro: se recuerda el resultado - String guardado = cache.get(texto); - if (guardado != null) { - return guardado.equals(SIN_COINCIDENCIA) ? null : guardado; - } - String resultado = null; - for (Plantilla p : plantillas) { - resultado = p.aplicar(texto); - if (resultado != null) { - break; - } - } - cache.put(texto, resultado == null ? SIN_COINCIDENCIA : resultado); - return resultado; - } - } - - private static boolean esPlantilla(String clave) { - Matcher m = MARCADOR.matcher(clave); - while (m.find()) { - if (!m.group(2).equals("%")) { - return true; - } - } - return false; - } - - /** Un molde con datos variables: «Hello %s!» → «¡Hola %s!». */ - private static final class Plantilla { - final String clave; - final String valor; - final String fijo; // el texto de la clave sin los marcadores - final String prefijo; // lo que va antes del primer marcador: descarta rápido lo que no coincide - final Pattern patron; - final List destino; // trozos de texto y números de grupo (Integer, desde 1) - - private Plantilla(String clave, String valor, String fijo, String prefijo, Pattern patron, List destino) { - this.clave = clave; - this.valor = valor; - this.fijo = fijo; - this.prefijo = prefijo; - this.patron = patron; - this.destino = destino; - } - - /** {@code null} si la plantilla no es segura (poco texto fijo o marcadores que no cuadran). */ - static Plantilla crear(String clave, String valor) { - StringBuilder regex = new StringBuilder("^"); - StringBuilder fijo = new StringBuilder(); - String prefijo = null; - int grupos = 0; - int ultimo = 0; - Matcher m = MARCADOR.matcher(clave); - while (m.find()) { - String tramo = clave.substring(ultimo, m.start()); - fijo.append(tramo); - if (!tramo.isEmpty()) { - regex.append(Pattern.quote(tramo)); - } - if (m.group(2).equals("%")) { - fijo.append('%'); - regex.append(Pattern.quote("%")); - } else { - if (prefijo == null) { - prefijo = clave.substring(0, m.start()); - } - grupos++; - regex.append(m.group(2).equals("d") ? "(-?\\d+)" : "(.+?)"); - } - ultimo = m.end(); - } - String cola = clave.substring(ultimo); - fijo.append(cola); - if (!cola.isEmpty()) { - regex.append(Pattern.quote(cola)); - } - regex.append('$'); - if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { - return null; - } - List destino = destino(valor, grupos); - if (destino == null) { - return null; - } - return new Plantilla(clave, valor, fijo.toString(), prefijo == null ? "" : prefijo, - Pattern.compile(regex.toString(), Pattern.DOTALL), destino); - } - - private static List destino(String valor, int grupos) { - List partes = new ArrayList<>(); - Matcher m = MARCADOR.matcher(valor); - int ultimo = 0; - int siguiente = 1; - boolean[] usado = new boolean[grupos + 1]; - while (m.find()) { - if (m.start() > ultimo) { - partes.add(valor.substring(ultimo, m.start())); - } - if (m.group(2).equals("%")) { - partes.add("%"); - } else { - int indice = m.group(1) != null ? Integer.parseInt(m.group(1)) : siguiente++; - if (indice < 1 || indice > grupos) { - return null; - } - usado[indice] = true; - partes.add(indice); - } - ultimo = m.end(); - } - if (ultimo < valor.length()) { - partes.add(valor.substring(ultimo)); - } - for (int i = 1; i <= grupos; i++) { - if (!usado[i]) { - return null; // la traducción perdió un dato: mejor no usarla - } - } - return partes; - } - - String aplicar(String texto) { - if (!prefijo.isEmpty() && !texto.startsWith(prefijo)) { - return null; - } - Matcher m = patron.matcher(texto); - if (!m.matches()) { - return null; - } - StringBuilder salida = new StringBuilder(); - for (Object parte : destino) { - if (parte instanceof Integer indice) { - salida.append(m.group(indice)); - } else { - salida.append((String) parte); - } - } - return salida.toString(); - } - } -} diff --git a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java deleted file mode 100644 index a9a7e52..0000000 --- a/mod-runtime/forge-1.20.1/src/test/java/io/github/consterna/modpackes/TablaTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.github.consterna.modpackes; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.StringReader; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TablaTest { - private static Tabla tabla(String... pares) { - Map mapa = new java.util.HashMap<>(); - for (int i = 0; i < pares.length; i += 2) { - mapa.put(pares[i], pares[i + 1]); - } - return new Tabla(mapa); - } - - @Test - void coincidenciaExactaYSinCoincidencia() { - Tabla t = tabla("TPA request expired!", "¡La solicitud de TPA expiró!"); - assertEquals("¡La solicitud de TPA expiró!", t.traducir("TPA request expired!")); - assertNull(t.traducir("tpa request expired!")); // mayúsculas distintas: no es el mismo texto - assertNull(t.traducir("TPA request expired! Now")); - assertNull(t.traducir("")); - assertNull(t.traducir(null)); - } - - @Test - void conservaLosEspaciosDeLosBordes() { - Tabla t = tabla("Hello there", "Hola"); - assertEquals(" Hola\n", t.traducir(" Hello there\n")); - assertNull(t.traducir(" ")); - } - - @Test - void unTextoIgualQueSuTraduccionNoCuentaComoCambio() { - Tabla t = tabla("Creeper", "Creeper"); - assertNull(t.traducir("Creeper")); - } - - @Test - void conservaTildesEneYEmoji() { - Tabla t = tabla("Pick 😀", "Elige ñandú ¡sí! 日本語 §a%s"); - assertEquals("Elige ñandú ¡sí! 日本語 §a%s", t.traducir("Pick 😀")); - } - - @Test - void leeJsonValidoEIgnoraLoQueNoEsTexto() throws IOException { - Tabla t = Tabla.leer(new StringReader("{\"a b\": \"uno\", \"n\": 5, \"v\": \"\", \"l\": [\"x\"], \"z\": null}")); - assertEquals(1, t.tamano()); - assertEquals("uno", t.traducir("a b")); - } - - @Test - void jsonRotoOQueNoEsObjetoFallaConIOException() { - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("{no es json"))); - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("[1, 2]"))); - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("\"texto\""))); - } - - @Test - void combinarDejaMandarALaSegunda() { - Tabla total = tabla("a b", "viejo", "c d", "queda").combinar(tabla("a b", "nuevo")); - assertEquals("nuevo", total.traducir("a b")); - assertEquals("queda", total.traducir("c d")); - assertTrue(Tabla.VACIA.vacia()); - assertFalse(total.vacia()); - assertEquals(total, total.combinar(Tabla.VACIA)); - } - - @Test - void plantillaConUnDatoVariable() { - Tabla t = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!"); - assertEquals("¡Hola Steve, bienvenido de nuevo!", t.traducir("Hello Steve, welcome back!")); - assertEquals("¡Hola Ñandú 😀, bienvenido de nuevo!", t.traducir("Hello Ñandú 😀, welcome back!")); - assertNull(t.traducir("Goodbye Steve, welcome back!")); - assertNull(t.traducir("Hello Steve")); - } - - @Test - void plantillaConNumeroSoloCoincideConNumeros() { - Tabla t = tabla("You have %d items left", "Te quedan %d objetos"); - assertEquals("Te quedan 5 objetos", t.traducir("You have 5 items left")); - assertEquals("Te quedan -3 objetos", t.traducir("You have -3 items left")); - assertNull(t.traducir("You have many items left")); - } - - @Test - void plantillaPuedeCambiarElOrden() { - Tabla t = tabla("%s gave %d coins", "%2$d monedas de %1$s"); - assertEquals("7 monedas de Laly", t.traducir("Laly gave 7 coins")); - } - - @Test - void plantillaConBordesYPorcentajeLiteral() { - Tabla t = tabla("Progress: %d%% of %s", "Progreso: %d%% de %s"); - assertEquals(" Progreso: 50% de la misión ", t.traducir(" Progress: 50% of la misión ")); - } - - @Test - void plantillasInseguras() { - // sin texto fijo: coincidiría con todo - assertNull(tabla("%s", "%s").traducir("cualquier cosa")); - assertNull(tabla("%s %s", "%s %s").traducir("uno dos")); - // la traducción pierde un dato o inventa uno - assertNull(tabla("Hello %s and %s here", "Hola %s aquí").traducir("Hello A and B here")); - assertNull(tabla("Hello %s there friend", "Hola %s y %s").traducir("Hello A there friend")); - assertNull(tabla("Hello %s there friend", "Hola %3$s").traducir("Hello A there friend")); - } - - @Test - void laPlantillaMasEspecificaGana() { - Tabla t = tabla( - "Welcome %s", "Bienvenido %s", - "Welcome %s to the farm", "Bienvenido %s a la granja"); - assertEquals("Bienvenido Steve a la granja", t.traducir("Welcome Steve to the farm")); - assertEquals("Bienvenido Steve", t.traducir("Welcome Steve")); - } - - @Test - void caracteresDeRegexEnElTextoFijoNoSonEspeciales() { - Tabla t = tabla("Cost (%d) [gold]. Really?", "Costo (%d) [oro]. ¿En serio?"); - assertEquals("Costo (9) [oro]. ¿En serio?", t.traducir("Cost (9) [gold]. Really?")); - assertNull(t.traducir("Cost 9 gold. Really?")); - } - - @Test - void lasPlantillasSobrevivenAlCombinarYSeCuentan() { - Tabla a = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!", "Exact text here", "Texto exacto"); - Tabla total = a.combinar(tabla("Another %d things here", "Otras %d cosas aquí")); - assertEquals(3, total.tamano()); - assertEquals(2, total.plantillas()); - assertEquals("Otras 3 cosas aquí", total.traducir("Another 3 things here")); - assertEquals("Texto exacto", total.traducir("Exact text here")); - } -} diff --git a/mod-runtime/neoforge-1.21.1/build.gradle b/mod-runtime/neoforge-1.21.1/build.gradle index 79f20cd..030e3f4 100644 --- a/mod-runtime/neoforge-1.21.1/build.gradle +++ b/mod-runtime/neoforge-1.21.1/build.gradle @@ -71,6 +71,14 @@ tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + tasks.named('test', Test).configure { useJUnitPlatform() } diff --git a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java deleted file mode 100644 index 2cce7ce..0000000 --- a/mod-runtime/neoforge-1.21.1/src/main/java/io/github/consterna/modpackes/Tabla.java +++ /dev/null @@ -1,286 +0,0 @@ -package io.github.consterna.modpackes; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Tabla de textos «inglés → español» que genera ModpackES. Hay dos clases de entrada: - *
    - *
  • Exactas: solo coinciden si el texto es idéntico (salvo los espacios de los bordes, que se conservan).
  • - *
  • Plantillas: la clave lleva {@code %s} o {@code %d} (como el {@code String.format} del mod original) y coinciden - * con cualquier texto que sea ese mismo molde con otros datos: «Hello %s!» coincide con «Hello Steve!». El español - * reutiliza lo capturado; puede llevar {@code %1$s}, {@code %2$s} para cambiar el orden.
  • - *
- * No hay traducción aproximada ni en vivo. - */ -public final class Tabla { - public static final Tabla VACIA = new Tabla(Map.of()); - - private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); - private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa - private static final int TAMANO_CACHE = 512; - private static final String SIN_COINCIDENCIA = "\u0000"; - - private final Map exactos; - private final List plantillas; - private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > TAMANO_CACHE; - } - }; - - public Tabla(Map textos) { - Map exactos = new HashMap<>(); - List plantillas = new ArrayList<>(); - for (Map.Entry e : textos.entrySet()) { - if (esPlantilla(e.getKey())) { - Plantilla p = Plantilla.crear(e.getKey(), e.getValue()); - if (p != null) { - plantillas.add(p); - } - } else { - exactos.put(e.getKey(), e.getValue()); - } - } - plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero - this.exactos = Map.copyOf(exactos); - this.plantillas = List.copyOf(plantillas); - } - - /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ - public static Tabla leer(Reader lector) throws IOException { - JsonElement raiz; - try { - raiz = JsonParser.parseReader(lector); - } catch (JsonParseException e) { - throw new IOException("La tabla de textos no es JSON válido", e); - } - if (!raiz.isJsonObject()) { - throw new IOException("La tabla de textos no es un objeto JSON"); - } - JsonObject objeto = raiz.getAsJsonObject(); - Map mapa = new HashMap<>(); - for (Map.Entry e : objeto.entrySet()) { - JsonElement v = e.getValue(); - if (v.isJsonPrimitive() && v.getAsJsonPrimitive().isString() && !v.getAsString().isEmpty()) { - mapa.put(e.getKey(), v.getAsString()); - } - } - return new Tabla(mapa); - } - - /** Une dos tablas; si un texto está en las dos, manda la de {@code otra}. */ - public Tabla combinar(Tabla otra) { - if (otra.vacia()) { - return this; - } - Map mapa = new HashMap<>(this.comoMapa()); - mapa.putAll(otra.comoMapa()); - return new Tabla(mapa); - } - - private Map comoMapa() { - Map mapa = new HashMap<>(exactos); - for (Plantilla p : plantillas) { - mapa.put(p.clave, p.valor); - } - return mapa; - } - - public boolean vacia() { - return exactos.isEmpty() && plantillas.isEmpty(); - } - - public int tamano() { - return exactos.size() + plantillas.size(); - } - - public int plantillas() { - return plantillas.size(); - } - - /** La traducción de {@code texto}, o {@code null} si no está en la tabla o no cambia nada. */ - public String traducir(String texto) { - if (texto == null || texto.isEmpty() || vacia()) { - return null; - } - String directo = buscar(texto); - if (directo != null) { - return directo.equals(texto) ? null : directo; - } - String nucleo = texto.strip(); - if (nucleo.isEmpty() || nucleo.length() == texto.length()) { - return null; - } - String traduccion = buscar(nucleo); - if (traduccion == null || traduccion.equals(nucleo)) { - return null; - } - int inicio = texto.length() - texto.stripLeading().length(); - int fin = texto.length() - texto.stripTrailing().length(); - return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); - } - - private String buscar(String texto) { - String exacto = exactos.get(texto); - if (exacto != null) { - return exacto; - } - if (plantillas.isEmpty()) { - return null; - } - synchronized (cache) { // los tooltips piden el mismo texto cada cuadro: se recuerda el resultado - String guardado = cache.get(texto); - if (guardado != null) { - return guardado.equals(SIN_COINCIDENCIA) ? null : guardado; - } - String resultado = null; - for (Plantilla p : plantillas) { - resultado = p.aplicar(texto); - if (resultado != null) { - break; - } - } - cache.put(texto, resultado == null ? SIN_COINCIDENCIA : resultado); - return resultado; - } - } - - private static boolean esPlantilla(String clave) { - Matcher m = MARCADOR.matcher(clave); - while (m.find()) { - if (!m.group(2).equals("%")) { - return true; - } - } - return false; - } - - /** Un molde con datos variables: «Hello %s!» → «¡Hola %s!». */ - private static final class Plantilla { - final String clave; - final String valor; - final String fijo; // el texto de la clave sin los marcadores - final String prefijo; // lo que va antes del primer marcador: descarta rápido lo que no coincide - final Pattern patron; - final List destino; // trozos de texto y números de grupo (Integer, desde 1) - - private Plantilla(String clave, String valor, String fijo, String prefijo, Pattern patron, List destino) { - this.clave = clave; - this.valor = valor; - this.fijo = fijo; - this.prefijo = prefijo; - this.patron = patron; - this.destino = destino; - } - - /** {@code null} si la plantilla no es segura (poco texto fijo o marcadores que no cuadran). */ - static Plantilla crear(String clave, String valor) { - StringBuilder regex = new StringBuilder("^"); - StringBuilder fijo = new StringBuilder(); - String prefijo = null; - int grupos = 0; - int ultimo = 0; - Matcher m = MARCADOR.matcher(clave); - while (m.find()) { - String tramo = clave.substring(ultimo, m.start()); - fijo.append(tramo); - if (!tramo.isEmpty()) { - regex.append(Pattern.quote(tramo)); - } - if (m.group(2).equals("%")) { - fijo.append('%'); - regex.append(Pattern.quote("%")); - } else { - if (prefijo == null) { - prefijo = clave.substring(0, m.start()); - } - grupos++; - regex.append(m.group(2).equals("d") ? "(-?\\d+)" : "(.+?)"); - } - ultimo = m.end(); - } - String cola = clave.substring(ultimo); - fijo.append(cola); - if (!cola.isEmpty()) { - regex.append(Pattern.quote(cola)); - } - regex.append('$'); - if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { - return null; - } - List destino = destino(valor, grupos); - if (destino == null) { - return null; - } - return new Plantilla(clave, valor, fijo.toString(), prefijo == null ? "" : prefijo, - Pattern.compile(regex.toString(), Pattern.DOTALL), destino); - } - - private static List destino(String valor, int grupos) { - List partes = new ArrayList<>(); - Matcher m = MARCADOR.matcher(valor); - int ultimo = 0; - int siguiente = 1; - boolean[] usado = new boolean[grupos + 1]; - while (m.find()) { - if (m.start() > ultimo) { - partes.add(valor.substring(ultimo, m.start())); - } - if (m.group(2).equals("%")) { - partes.add("%"); - } else { - int indice = m.group(1) != null ? Integer.parseInt(m.group(1)) : siguiente++; - if (indice < 1 || indice > grupos) { - return null; - } - usado[indice] = true; - partes.add(indice); - } - ultimo = m.end(); - } - if (ultimo < valor.length()) { - partes.add(valor.substring(ultimo)); - } - for (int i = 1; i <= grupos; i++) { - if (!usado[i]) { - return null; // la traducción perdió un dato: mejor no usarla - } - } - return partes; - } - - String aplicar(String texto) { - if (!prefijo.isEmpty() && !texto.startsWith(prefijo)) { - return null; - } - Matcher m = patron.matcher(texto); - if (!m.matches()) { - return null; - } - StringBuilder salida = new StringBuilder(); - for (Object parte : destino) { - if (parte instanceof Integer indice) { - salida.append(m.group(indice)); - } else { - salida.append((String) parte); - } - } - return salida.toString(); - } - } -} diff --git a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java b/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java deleted file mode 100644 index a9a7e52..0000000 --- a/mod-runtime/neoforge-1.21.1/src/test/java/io/github/consterna/modpackes/TablaTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.github.consterna.modpackes; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.StringReader; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TablaTest { - private static Tabla tabla(String... pares) { - Map mapa = new java.util.HashMap<>(); - for (int i = 0; i < pares.length; i += 2) { - mapa.put(pares[i], pares[i + 1]); - } - return new Tabla(mapa); - } - - @Test - void coincidenciaExactaYSinCoincidencia() { - Tabla t = tabla("TPA request expired!", "¡La solicitud de TPA expiró!"); - assertEquals("¡La solicitud de TPA expiró!", t.traducir("TPA request expired!")); - assertNull(t.traducir("tpa request expired!")); // mayúsculas distintas: no es el mismo texto - assertNull(t.traducir("TPA request expired! Now")); - assertNull(t.traducir("")); - assertNull(t.traducir(null)); - } - - @Test - void conservaLosEspaciosDeLosBordes() { - Tabla t = tabla("Hello there", "Hola"); - assertEquals(" Hola\n", t.traducir(" Hello there\n")); - assertNull(t.traducir(" ")); - } - - @Test - void unTextoIgualQueSuTraduccionNoCuentaComoCambio() { - Tabla t = tabla("Creeper", "Creeper"); - assertNull(t.traducir("Creeper")); - } - - @Test - void conservaTildesEneYEmoji() { - Tabla t = tabla("Pick 😀", "Elige ñandú ¡sí! 日本語 §a%s"); - assertEquals("Elige ñandú ¡sí! 日本語 §a%s", t.traducir("Pick 😀")); - } - - @Test - void leeJsonValidoEIgnoraLoQueNoEsTexto() throws IOException { - Tabla t = Tabla.leer(new StringReader("{\"a b\": \"uno\", \"n\": 5, \"v\": \"\", \"l\": [\"x\"], \"z\": null}")); - assertEquals(1, t.tamano()); - assertEquals("uno", t.traducir("a b")); - } - - @Test - void jsonRotoOQueNoEsObjetoFallaConIOException() { - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("{no es json"))); - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("[1, 2]"))); - assertThrows(IOException.class, () -> Tabla.leer(new StringReader("\"texto\""))); - } - - @Test - void combinarDejaMandarALaSegunda() { - Tabla total = tabla("a b", "viejo", "c d", "queda").combinar(tabla("a b", "nuevo")); - assertEquals("nuevo", total.traducir("a b")); - assertEquals("queda", total.traducir("c d")); - assertTrue(Tabla.VACIA.vacia()); - assertFalse(total.vacia()); - assertEquals(total, total.combinar(Tabla.VACIA)); - } - - @Test - void plantillaConUnDatoVariable() { - Tabla t = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!"); - assertEquals("¡Hola Steve, bienvenido de nuevo!", t.traducir("Hello Steve, welcome back!")); - assertEquals("¡Hola Ñandú 😀, bienvenido de nuevo!", t.traducir("Hello Ñandú 😀, welcome back!")); - assertNull(t.traducir("Goodbye Steve, welcome back!")); - assertNull(t.traducir("Hello Steve")); - } - - @Test - void plantillaConNumeroSoloCoincideConNumeros() { - Tabla t = tabla("You have %d items left", "Te quedan %d objetos"); - assertEquals("Te quedan 5 objetos", t.traducir("You have 5 items left")); - assertEquals("Te quedan -3 objetos", t.traducir("You have -3 items left")); - assertNull(t.traducir("You have many items left")); - } - - @Test - void plantillaPuedeCambiarElOrden() { - Tabla t = tabla("%s gave %d coins", "%2$d monedas de %1$s"); - assertEquals("7 monedas de Laly", t.traducir("Laly gave 7 coins")); - } - - @Test - void plantillaConBordesYPorcentajeLiteral() { - Tabla t = tabla("Progress: %d%% of %s", "Progreso: %d%% de %s"); - assertEquals(" Progreso: 50% de la misión ", t.traducir(" Progress: 50% of la misión ")); - } - - @Test - void plantillasInseguras() { - // sin texto fijo: coincidiría con todo - assertNull(tabla("%s", "%s").traducir("cualquier cosa")); - assertNull(tabla("%s %s", "%s %s").traducir("uno dos")); - // la traducción pierde un dato o inventa uno - assertNull(tabla("Hello %s and %s here", "Hola %s aquí").traducir("Hello A and B here")); - assertNull(tabla("Hello %s there friend", "Hola %s y %s").traducir("Hello A there friend")); - assertNull(tabla("Hello %s there friend", "Hola %3$s").traducir("Hello A there friend")); - } - - @Test - void laPlantillaMasEspecificaGana() { - Tabla t = tabla( - "Welcome %s", "Bienvenido %s", - "Welcome %s to the farm", "Bienvenido %s a la granja"); - assertEquals("Bienvenido Steve a la granja", t.traducir("Welcome Steve to the farm")); - assertEquals("Bienvenido Steve", t.traducir("Welcome Steve")); - } - - @Test - void caracteresDeRegexEnElTextoFijoNoSonEspeciales() { - Tabla t = tabla("Cost (%d) [gold]. Really?", "Costo (%d) [oro]. ¿En serio?"); - assertEquals("Costo (9) [oro]. ¿En serio?", t.traducir("Cost (9) [gold]. Really?")); - assertNull(t.traducir("Cost 9 gold. Really?")); - } - - @Test - void lasPlantillasSobrevivenAlCombinarYSeCuentan() { - Tabla a = tabla("Hello %s, welcome back!", "¡Hola %s, bienvenido de nuevo!", "Exact text here", "Texto exacto"); - Tabla total = a.combinar(tabla("Another %d things here", "Otras %d cosas aquí")); - assertEquals(3, total.tamano()); - assertEquals(2, total.plantillas()); - assertEquals("Otras 3 cosas aquí", total.traducir("Another 3 things here")); - assertEquals("Texto exacto", total.traducir("Exact text here")); - } -} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index ef22359..956b6e5 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -1,8 +1,8 @@ """Compila el mod de apoyo (ModpackES Runtime) y deja el jar donde la aplicación lo busca. -Uso: uv run python scripts/construir_mod.py [forge-1.20.1 | neoforge-1.21.1 | fabric-1.20.1 | todos] [--sin-pruebas] +Uso: uv run python scripts/construir_mod.py [ | todos] [--sin-pruebas] -Sin nombre compila todos. Cada uno necesita su JDK (Forge 1.20.1: 17; NeoForge 1.21.1 y Fabric 1.20.1: 21) y su +Sin nombre compila todos. Cada uno necesita su JDK (Forge: 17; NeoForge y Fabric: 21) y su Gradle, que se toman de JAVA_HOME (o JAVA_HOME__X64) y de `.herramientas/` si existen, y acceso a internet la primera vez, para bajar el loader y Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo construye el mismo comando @@ -21,7 +21,15 @@ RAIZ = Path(__file__).resolve().parent.parent DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" #: proyecto → (versión de Java, versión de Gradle) que pide su compilación -PROYECTOS = {"forge-1.20.1": (17, "8.8"), "neoforge-1.21.1": (21, "9.2.1"), "fabric-1.20.1": (21, "9.8.2")} +PROYECTOS = { + "forge-1.19.2": (17, "8.8"), + "forge-1.20.1": (17, "8.8"), + "neoforge-1.21.1": (21, "9.2.1"), + "fabric-1.19.4": (21, "9.8.2"), + "fabric-1.20.1": (21, "9.8.2"), + "fabric-1.21.1": (21, "9.8.2"), + "fabric-1.21.4": (21, "9.8.2"), +} def _herramientas() -> Path: diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index a8967d8..b59b618 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -16,9 +16,20 @@ PREFIJO = "modpackes_runtime-" #: (loader, versión de Minecraft) para los que hay mod de apoyo. Cada entrada es un proyecto en `mod-runtime/`. SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { + ("forge", (1, 19, 2)): "forge-1.19.2", ("forge", (1, 20, 1)): "forge-1.20.1", ("neoforge", (1, 21, 1)): "neoforge-1.21.1", + ("fabric", (1, 19, 4)): "fabric-1.19.4", ("fabric", (1, 20, 1)): "fabric-1.20.1", + ("fabric", (1, 21, 1)): "fabric-1.21.1", + ("fabric", (1, 21, 4)): "fabric-1.21.4", +} +#: Combinaciones que no tendrán mod de apoyo y por qué (para decírselo al jugador en vez de «todavía no»). +IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { + ("fabric", (1, 19, 2)): ( + "Fabric API 1.19.2 no tiene un evento para los mensajes que recibe el cliente, " + "y sin él el mod de apoyo no podría cambiar el chat. Con Fabric 1.19.4 en adelante sí." + ), } _NOMBRE = re.compile(rf"{re.escape(PREFIJO)}(?P[a-z]+-[\d.]+)-(?P[\w.+-]+)\.jar") @@ -32,6 +43,15 @@ def destino_de(instancia: Instancia) -> str | None: return SOPORTADOS.get((instancia.loader or "", instancia.version.partes)) +def _sin_mod(instancia: Instancia) -> str: + """Por qué no hay mod de apoyo para esta instancia, en una frase.""" + razon = IMPOSIBLES.get((instancia.loader or "", instancia.version.partes)) + if razon is not None: + return f"No hay mod de apoyo para Minecraft {instancia.version} con {instancia.loader}: {razon}" + loader = instancia.loader or "sin loader" + return f"Todavía no hay mod de apoyo para Minecraft {instancia.version} con {loader}." + + def jar_empaquetado(instancia: Instancia, carpeta: Path | None = None) -> Path | None: """El jar más nuevo del mod de apoyo para esta instancia, si viene con la aplicación.""" destino = destino_de(instancia) @@ -52,11 +72,7 @@ def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: """Lo que hay que saber del mod de apoyo en esta instancia, en frases (no abre el juego).""" destino = destino_de(instancia) if destino is None: - loader = instancia.loader or "sin loader" - return [ - f"No hay mod de apoyo para Minecraft {instancia.version} con {loader}: " - "el texto del código Java seguirá en inglés." - ] + return [f"{_sin_mod(instancia)} El texto del código Java seguirá en inglés."] lineas = [f"Hay mod de apoyo para esta instancia ({destino})."] lineas.append( "Esta copia de la aplicación lo trae." @@ -82,11 +98,7 @@ def instalar(instancia: Instancia, carpeta_jars: Path | None = None) -> Path: """Copia el mod de apoyo a `mods/` y quita sus versiones anteriores. ValueError si no hay uno.""" destino = destino_de(instancia) if destino is None: - loader = instancia.loader or "sin loader" - raise ValueError( - f"Todavía no hay mod de apoyo para Minecraft {instancia.version} con {loader}. " - "El texto escrito en el código de los mods seguirá en inglés." - ) + raise ValueError(f"{_sin_mod(instancia)} El texto escrito en el código de los mods seguirá en inglés.") origen = jar_empaquetado(instancia, carpeta_jars) if origen is None: raise ValueError( diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index d07c66b..7f7e31e 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,7 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.20.1, NeoForge 1.21.1 y Fabric 1.20.1." + "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index d659094..8b7d94e 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -109,13 +109,18 @@ def _jars(tmp_path: Path, *nombres: str) -> Path: def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: casos = [ + ("1.19.2", "forge", "forge-1.19.2"), ("1.20.1", "forge", "forge-1.20.1"), ("1.21.1", "neoforge", "neoforge-1.21.1"), + ("1.19.4", "fabric", "fabric-1.19.4"), ("1.20.1", "fabric", "fabric-1.20.1"), - ("1.21.1", "fabric", None), + ("1.21.1", "fabric", "fabric-1.21.1"), + ("1.21.4", "fabric", "fabric-1.21.4"), + ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión ("1.21.4", "neoforge", None), ("1.20.1", "neoforge", None), ("1.12.2", "forge", None), + ("1.19.4", "forge", None), ] for i, (version, loader, esperado) in enumerate(casos): juego = fabrica.prism(tmp_path / str(i), version, loader) @@ -138,9 +143,12 @@ def test_instalar_reemplaza_versiones_y_respeta_otros_mods(tmp_path: Path) -> No def test_instalar_con_errores_claros(tmp_path: Path) -> None: - juego = fabrica.prism(tmp_path / "a", "1.21.1", "fabric") + juego = fabrica.prism(tmp_path / "a", "1.18.2", "forge") with Instancia(juego) as inst, pytest.raises(ValueError, match="Todavía no hay mod de apoyo"): mod_apoyo.instalar(inst, tmp_path) + imposible = fabrica.prism(tmp_path / "c", "1.19.2", "fabric") # se dice por qué no, no solo «todavía» + with Instancia(imposible) as inst, pytest.raises(ValueError, match="no tiene un evento"): + mod_apoyo.instalar(inst, tmp_path) juego2 = fabrica.prism(tmp_path / "b", "1.20.1", "forge") with Instancia(juego2) as inst, pytest.raises(ValueError, match="no trae el mod de apoyo"): mod_apoyo.instalar(inst, tmp_path / "no_hay") @@ -188,7 +196,7 @@ def traducir_lote(self, lote: Lote) -> dict[str, str]: def test_estado_del_mod_de_apoyo(tmp_path: Path) -> None: sin_soporte = fabrica.prism(tmp_path / "a", "1.21.4", "neoforge") with Instancia(sin_soporte) as inst: - assert "No hay mod de apoyo" in mod_apoyo.estado(inst)[0] + assert "no hay mod de apoyo" in mod_apoyo.estado(inst)[0].lower() juego = fabrica.prism(tmp_path / "b", "1.20.1", "forge") carpeta = _jars(tmp_path, "modpackes_runtime-forge-1.20.1-0.1.0.jar") with Instancia(juego) as inst: From 9de5f0a10b98e2a8db4429d56446e816d565f1af Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 03:22:14 -0500 Subject: [PATCH 14/24] feat(runtime): mods de apoyo para Fabric de 1.21.5 a 1.21.11 y rangos de versiones vecinas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fabric 1.21.5 (válido de 1.21.5 a 1.21.10) y Fabric 1.21.11, que renombra ResourceLocation a Identifier. Con esto Fabric queda cubierto sin huecos de 1.21 a 1.21.11, más 1.19.4 y de 1.20 a 1.20.2. Un mismo jar sirve para varias versiones cuando el mismo código compila y pasa sus pruebas contra cada una: `construir_mod.py --propiedad` lo comprueba sin generar jar, y el rango comprobado queda en `minecraft_range` de cada proyecto y en `SOPORTADOS`. El código compilado sigue sin probarse dentro del juego. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 4 +- .../consterna/modpackes/ReescritorTest.java | 114 ++++++++ mod-runtime/fabric-1.20.1/build.gradle | 1 + mod-runtime/fabric-1.20.1/gradle.properties | 2 + .../src/main/resources/fabric.mod.json | 2 +- mod-runtime/fabric-1.21.1/build.gradle | 1 + mod-runtime/fabric-1.21.1/gradle.properties | 2 + .../src/main/resources/fabric.mod.json | 2 +- mod-runtime/fabric-1.21.11/.gitignore | 7 + mod-runtime/fabric-1.21.11/build.gradle | 62 +++++ mod-runtime/fabric-1.21.11/gradle.properties | 16 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.21.11/gradlew | 248 ++++++++++++++++++ mod-runtime/fabric-1.21.11/gradlew.bat | 112 ++++++++ mod-runtime/fabric-1.21.11/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 ++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ mod-runtime/fabric-1.21.4/build.gradle | 1 + mod-runtime/fabric-1.21.4/gradle.properties | 2 + .../src/main/resources/fabric.mod.json | 2 +- mod-runtime/fabric-1.21.5/.gitignore | 7 + mod-runtime/fabric-1.21.5/build.gradle | 63 +++++ mod-runtime/fabric-1.21.5/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.21.5/gradlew | 248 ++++++++++++++++++ mod-runtime/fabric-1.21.5/gradlew.bat | 112 ++++++++ mod-runtime/fabric-1.21.5/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 ++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ scripts/construir_mod.py | 17 +- src/traductor_es/salida/mod_apoyo.py | 13 + src/traductor_es/ui/ventana.py | 3 +- tests/test_runtime.py | 12 + 37 files changed, 1421 insertions(+), 14 deletions(-) create mode 100644 mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/fabric-1.21.11/.gitignore create mode 100644 mod-runtime/fabric-1.21.11/build.gradle create mode 100644 mod-runtime/fabric-1.21.11/gradle.properties create mode 100644 mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.21.11/gradlew create mode 100644 mod-runtime/fabric-1.21.11/gradlew.bat create mode 100644 mod-runtime/fabric-1.21.11/settings.gradle create mode 100644 mod-runtime/fabric-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/fabric-1.21.5/.gitignore create mode 100644 mod-runtime/fabric-1.21.5/build.gradle create mode 100644 mod-runtime/fabric-1.21.5/gradle.properties create mode 100644 mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.21.5/gradlew create mode 100644 mod-runtime/fabric-1.21.5/gradlew.bat create mode 100644 mod-runtime/fabric-1.21.5/settings.gradle create mode 100644 mod-runtime/fabric-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a41b7..e9fd5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1c5e44..9a90b06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,13 +95,13 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`) es un mod solo de cliente. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21): +Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21): ``` -uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4] +uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11] ``` -Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). ## Licencia de las contribuciones diff --git a/README.md b/README.md index bc75f72..f4581d4 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..eef0937 --- /dev/null +++ b/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,114 @@ +package io.github.consterna.modpackes; + +import com.mojang.datafixers.util.Either; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static final Tabla TABLA = new Tabla(Map.of( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí")); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(ChatFormatting.RED).withBold(true); + Component original = Component.literal("TPA request expired!").setStyle(estilo); + Component nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(Component.literal("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(Component.literal("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + MutableComponent raiz = Component.literal(" ").withStyle(ChatFormatting.AQUA); + raiz.append(Component.literal("Hello").withStyle(ChatFormatting.WHITE)); + raiz.append(Component.literal(" Steve")); + Component nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent.OpenUrl(URI.create("https://example.com"))); + Component nuevo = Reescritor.reescribir(Component.literal("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals(estilo.getClickEvent(), nuevo.getStyle().getClickEvent()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + Component original = Component.translatable("chat.type.text", Component.literal("Hello"), "Hello"); + Component nuevo = Reescritor.reescribir(original, TABLA); + TranslatableContents contenido = (TranslatableContents) nuevo.getContents(); + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((Component) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = new Tabla(Map.of("Ñandú says hi", "Ñandú dice ¡hola! 😀")); + Component nuevo = Reescritor.reescribir(Component.literal(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } + + @Test + void cambiaLasLineasDeUnTooltipYDejaLoQueNoEsTexto() { + TooltipComponent imagen = new TooltipComponent() { + }; + FormattedText plano = FormattedText.of("Hello"); + Component otra = Component.literal("Other"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(Component.literal("Hello"))); + elementos.add(Either.right(imagen)); + elementos.add(Either.left(plano)); + elementos.add(Either.left(otra)); + + Reescritor.reescribirElementos(elementos, TABLA); + + assertEquals("Hola", ((Component) elementos.get(0).left().orElseThrow()).getString()); + assertSame(imagen, elementos.get(1).right().orElseThrow()); + assertSame(plano, elementos.get(2).left().orElseThrow()); + assertSame(otra, elementos.get(3).left().orElseThrow()); + } + + @Test + void losTooltipsSinTablaOSinListaNoHacenNada() { + Component original = Component.literal("Hello"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(original)); + + Reescritor.reescribirElementos(elementos, Tabla.VACIA); + Reescritor.reescribirElementos(null, TABLA); + + assertSame(original, elementos.get(0).left().orElseThrow()); + } +} diff --git a/mod-runtime/fabric-1.20.1/build.gradle b/mod-runtime/fabric-1.20.1/build.gradle index 444b6bb..11e61ed 100644 --- a/mod-runtime/fabric-1.20.1/build.gradle +++ b/mod-runtime/fabric-1.20.1/build.gradle @@ -31,6 +31,7 @@ processResources { mod_authors: project.mod_authors, mod_description: project.mod_description, minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, loader_version: project.loader_version, ] inputs.properties propiedades diff --git a/mod-runtime/fabric-1.20.1/gradle.properties b/mod-runtime/fabric-1.20.1/gradle.properties index 0e6147d..06011e8 100644 --- a/mod-runtime/fabric-1.20.1/gradle.properties +++ b/mod-runtime/fabric-1.20.1/gradle.properties @@ -3,6 +3,8 @@ org.gradle.daemon=false # Versiones del ejemplo oficial de Fabric para 1.20.1 (FabricMC/fabric-example-mod, rama 1.20.1). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25); el mod se compila a Java 17. minecraft_version=1.20.1 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.20 <=1.20.2 loader_version=0.20.1 loom_version=1.17.21 fabric_api_version=0.92.12+1.20.1 diff --git a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json index 391e587..fd9841c 100644 --- a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json @@ -15,7 +15,7 @@ }, "depends": { "fabricloader": ">=0.14.0", - "minecraft": "${minecraft_version}", + "minecraft": "${minecraft_range}", "java": ">=17", "fabric-api": "*" } diff --git a/mod-runtime/fabric-1.21.1/build.gradle b/mod-runtime/fabric-1.21.1/build.gradle index 1e40e62..324ab62 100644 --- a/mod-runtime/fabric-1.21.1/build.gradle +++ b/mod-runtime/fabric-1.21.1/build.gradle @@ -31,6 +31,7 @@ processResources { mod_authors: project.mod_authors, mod_description: project.mod_description, minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, loader_version: project.loader_version, ] inputs.properties propiedades diff --git a/mod-runtime/fabric-1.21.1/gradle.properties b/mod-runtime/fabric-1.21.1/gradle.properties index 52fe37c..2e0483b 100644 --- a/mod-runtime/fabric-1.21.1/gradle.properties +++ b/mod-runtime/fabric-1.21.1/gradle.properties @@ -3,6 +3,8 @@ org.gradle.daemon=false # Fabric API 0.110.0 para 1.21.1 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). minecraft_version=1.21.1 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.21 <=1.21.1 loader_version=0.15.11 loom_version=1.17.21 fabric_api_version=0.110.0+1.21.1 diff --git a/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json index 03aca45..97825e2 100644 --- a/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json @@ -15,7 +15,7 @@ }, "depends": { "fabricloader": ">=0.15.11", - "minecraft": "${minecraft_version}", + "minecraft": "${minecraft_range}", "java": ">=21", "fabric-api": "*" } diff --git a/mod-runtime/fabric-1.21.11/.gitignore b/mod-runtime/fabric-1.21.11/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.21.11/build.gradle b/mod-runtime/fabric-1.21.11/build.gradle new file mode 100644 index 0000000..1eb43cf --- /dev/null +++ b/mod-runtime/fabric-1.21.11/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 // Minecraft 1.21.11 funciona con Java 21 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano-1215/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.21.11/gradle.properties b/mod-runtime/fabric-1.21.11/gradle.properties new file mode 100644 index 0000000..d6ceb07 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/gradle.properties @@ -0,0 +1,16 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.141.6 para 1.21.11 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). +minecraft_version=1.21.11 +loader_version=0.17.3 +loom_version=1.17.21 +fabric_api_version=0.141.6+1.21.11 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 zcma&OWmH_-wk?cn;V!}5-QC?ScmajGYe;Z+3GVI$cX#&$ClK7-AN!ow?)Tol@9ytZ zYpZ@#TYu)5WA;9JA7hq^JR}q*7#tiN7#JAo$MgSvP{H8A6eTr88D*8Em=$EjC6&}A zH8r5Y!2fz>U02U#l?(gJbWM+?xy1>dlx+LyS(BfdX^AFxGJ8U2nvfU;4Nx4NK}2`y z?MV=4jb>Eo&P_0FDL*_g(B*N;1NlbI_t0W?+_`1|)}VZc+)j4pR8fmNBBH1e`Df0JO} zzN@#As4%03;W^B;Jq|jdQ%$el(ah)4ic-SrE8j@BX1H|PmfUVnb`M-!Maw}vlnB-< zm$QvEDUidyLK07uE?e{4r)LTR<@y7Eh*0FjjSZseKnaESu`YKnef+9^i z40Nb`Lpj~w)7ND^*&{KY&q}VXU7eNRKu&|OrY3Nb&!0}h06X_) zg)zK9u-@~uVL32XREM+De3*%wgU*J@i7|j4mz!@YzD{_F+{q0U9N$LT-|4I?lotNA zAu>QwAr%TsJlT7>wy8bIx)yb|> z-nF9tw;6zSL}|%D=J?WmKrXGotgP(_$c5cB=;{A@gO3yIo?be z3yrViT1>n}n1AnnkK^hb3SukF6#j!u20jHAOAR|Ui>UN8a&W4Gyl-DY$2jPCa&79* zc6W_OL%2n1rN9#upQ0w>v5m_LzT*0(Q&kBjLsVqH+XVlZv#s{cG6Gyjj#!RuM@)p9 z1+s&1JDL%i*@mrx5T=sREHAeT#|au%MY;YIXrTi)gAANPyquKWq+fZ2PbgyzJvIv_ z0Ns-7T&luzgk@w&ft@-1gn7F$L|-ML=}JbbxzRX2N<6u;VW+k^89ctUN6&Pt{ydJAD$lJ{M*>!Nii z?41o-$75hgfPNAE_N(am&iQ@E>*~sF(QslX&D;=s`5A7!R)#a~>k=3)&OvaMElmtE z_OhFV(9o2gkumnJl=i5KWl3+58R>KT&Y&7*9sMTc8)X(4np)<1vq$73i%{IxPBq=*z@w0Up1J@dmdWtQj2wH zq%Wwm^xagF5^d8|FYj4O!H%xVJ>%s~Q@LZ(q&j;qquq(qZjm|MhgP=9l%wW*rNORficy^#gkhhb1 zF1$Ivg2W!cZ#Wl4$(Iil?$ZU%w+0?AIctg9`-eseLF|M^%UZqENCRiHNpQOsi);7( zEx>~&W9VI-SSQ~QSNN%CCq$dx2-H$Pa?+XK7&B?|RRz@9I=g;)O4sDr7@vh!FU5GN zZGo__oGvrc;knQLTUqov#ZTZE0Nof;6$+IZCvjIsZy}pMg1N^ew#>v>bsf2=x3k67 z@%y4^Do8!d?>MM~E8jk0IQ3wFrV-%H`$lJ=P6}!(PM%>_73aUrkB)^I7&g|&gw%815G?CS^53-@t#0p3A%23-{lv&O_qLy23)+Lcx_ayKOje73D$o zXzBVD@T726Jod{Kw>T!MiE*E?5~$ae+EarzzI1(oPQiuJ7+J$p~7zOG`ZW z>41=7n#r)2aN)rHBFo+0Xz!i^F6&`QFb3=-<;*Q8EF%b>a`?)Y>nisIb*g>B{<;8R zU6XQ>kg^pHbVv}wWa)I$jAbF=H6?Q`T{ZIe{DV$Z`Z?%6t=@iA-`O@2r00Q%ysh?D zoeEzzj*==pFNG2J2(hn$e2y9LM4dfXBa|`(z#nKLF*|U0NH3cM5dwA@twF+pW|G#y zBP6s9x{XE-ev!GS-fx$Sbw3W8=5!{G91rMel zSylKXy%9YbKd`ggAZHG49Dfcu^<$z!o9mRv1jn910PEC7G9scBRKk#H*I#~mLOF@% zsLKrI46NY_1_IQo*(5CU#oej%Nq$;Y>8uH)$euq|PSDl@up4(f* z`6@&HB$%H%|CYqRNrIpTj%U$GEmJTb3mN2DbiOTL|a%21!jgI?< zA|_4KHB_DUeH^>XQX7FQZkFb-HfyZp=8No@7Kv2tVXUYko~uHW5_Y;ovX5$Ljx$RR z)aUHwdGMqew>~_Ev~~^f={*9yOuf%A)v1cDz7CGPa<{U@sNcRK$7Ml7*xllzctv#` zItsxev7;kW=D~Mvy5NL7xY_@i*pwh@NsbX0Iv`cMjsSqlW2PC!Du0`C5s!yd(!H#k zbSFxiQBjtjG2U>dsnp(n;vo~pxK-!bD5YD}G$pV+rZ9o1b$rKJ^NO%C+hQ32W7k)p zO4kyd9uVk>DY+aD@jBbs$q!f47Hvolxm5sDf-y>?R|ylAu4aqb<)~kRAMfnhnzuaf zOGb+vJ&REA6U`tiQdFGWxlGfd$e9|{AoFT*I5$XwS_EO#_p=aHwPi+19`8y`^G zE5!KDv+HB+{-OwWUmgzK*6MkX8^PzC?i~;<7k$bsJHaHgB1FgH2)eQ=^BDlnf`g z7_VcwnlDU{eg z;tfUx7cNX(XTsAJ1jh?oy!JC16@SC1l+_P@8Ml70^|KiV&#l@Wn#I!_`N6K~cOzs` z0uJQ(gkKsJZ-kBAf_(vJJ=8jLqeUtYijm9nrARt>p3?BJH`QT^uu9g`e}0f=1Z?_p zwx~d?sQKqa5}cO0^~ta5`a-A>#fM1buGFu^NQG`COdB`r( zFJOhjlrUPqam6rGPjwEpVvD@l)}f70D!g>8FmU&_gK*bl7p zb*FUuFk@E;4+y7L5ajZwJABP?j+Rx2na$LI$>bADlrPQ?8OauBz`g$B z^}gK%R#9WftbM*bzR29%y;*&Rc=>$#v|f|Dx3j+&y7x+qDR!QEU$Khh?`OOX^3|{G z1)>mR+gx4(UoY)l#url@5D7XygT4B!Xv|kZu1`2>^C00|WO8;c%1j^ja7y5GiCw0m za%ZGoHO%sENqDY-E&oER=T|>kp<*w6_R&fo`h!!oKglq>A^hip_d6zz-;8qwL7RY) z$sU%7f7`bkW4w#fJRtP}T$VeRH9YrcR1t9@wu6Q0*`6*rH-mxIGZVJ4=)@f6p=T@% z=*&ZIZ~35@q}6Q!vJJf;M%WjafP&S_@yFf+URq+FBzPtp4!aX-h+W;EW9zOVu@(K= z5Ob-J`ZX8!g6)1GK@cY6+VC>Qp2d+k&W4sEmYJ>JHh8L=d@W)$1(vKkIECz7N7GFQ zzQ^YrO!2e#FZ!A|-tJ_1&LG6D<07wQu`Y^oKb~B$V}j=I(My6x;H5M=QrhqswaI@j zn5K>(Vf7+zLiOr_0XEi%I_jKJd2mBzW|U!_45bK*$B$I!DXmVsyTI|bqEWlnwgz2b zURZD<(QP1fNlBQ`I%IdtX-Q4Q28XbPk zt?PYQB&@~Q(PG1oI!S|9+AnZua8-0CK<91l#?B+H_Ky-tH%_{tEKamnMb9shRmFz+ zLKP*LSdEv%y3M;e%H`IeSY&Of?Fr5D`>5|g|5r*JP292NVA51U<6+Z;atwH49{sR+ za%kz`?>jT!l;gD`9vu4uEjI7w3}f&wPOp5_XI%8Jx3nERt{6?s^=+U5eHLnJV; zE^06^mH$yMV*a0cQBhP$R!UM`gGo{9zciy;j3`1M3##P(w>45&$AGpICY^XOJ*dXE z=)(q>ISvZZ8V74EC@22wxA&R^Ee~8Wj)OoVyCE%KSy@(|K*(P-rhI*>(t&uZUO(~8 zESR&z-^DpCS7-feq3J7k{7GnjMMZb*zeX1A)=o34SO;?%dsFFq;H#%Os;#3gC2CSO z21mz}`i4H5-fdr>t=gN={?zs2gme4VMGjZwI2KVKq;3#touMu^it*Q?q0-?B%RiJZ z)Q8^vpBK&gv1kWp3uX&vpqVX*+0@pGS=_PiDT)^rcCm`%L$d)CYV~rkORaboGiT%1Tvj?&w$5@+Q^;vfuEpO zh0Z3(uaLXLA?bd-`}~e{XXuZv*aySW4gOc`WuFsIFhAn1{g1Jy`p4KS|GxqbQd?D0 z7sWIXmq)QfNll5|g1+?33r?X^VKnw7HIzZXl_$Y9D&7a2_Ox>B2jG*3=WOFVRHDJB zz&$5p27tdxx#wej&B? zdxie=EQ)j|2+QPJbQ;BD>g=FXI~-i`@;^}3OhmHkEK$7n1q+U?z=#C9rg&&_~NMm_Dv_e5a-7x(bTs^9F4V}Nsj8@9|caD z3pB5D>c$tzj;jJ2L2cG=qa0T>Gmh>WL_FK}J~$@XVzdM6Lf6*A_H^H=xa@HOfvwk_ zd1DM?d7>V&pm-q+agWK8_qGW=o9$Oz@BW%PbRD;CDJ-)QKya65CK+2Y8Fm}iDP5b@ zbWpY=Z(~915(0=hUL@*nI1wX*8F}U%DKG;T_t)?Rwf@iH#rMZX@hEl;Y7|v7&)9hQ zh%=U#o{1|908ZD|#!zU15pj4qq)UVOtW%JaE6st%2;9pM&7ZPtl5I~835LF*H;BK| z`Sm2x(is8_EEDpdpoIM&(^&=R>;iJ85H)uNIsZGgUH%3p$?68m7^0Yh^Vlg&MMY!} zRV@Kc!S&%Qg= zqV}Y*bcqd>^if`c1HFXG>e)md^JN#<}z*hh*d5moCfu>mz71-h1Q1e^&Gu+Zsw z$K;EO_7X0EfGE*Tg!D1&X?uR;gpkKBP zH^KPmfJkw1zHzMq@;qP?aP18d$WrbHW!Ew<{$q&cX(ieICI~3RO_;rD9=3nAN_!x4 zIKvX(hnpZ9Mbgi5cug)JhmYkJ)60@oads_k@2$TyStZk2-<7fn`VO}%+_Dbd0v`X0 zz^qj$zM_u9w=*_Vs(mE_9P zoj+JzfvJNwbNiPpj%OCwC*yC%j0vv5P?eIF_6sZ&!<-k{%K0<6pQG+i*dV~==3gm` zD)7}@_9JC8p#CXk1^$zi6$6<&ID@3DoLyYi-Aw*BJN_+q8#V15=Cv@t*sm^@$$l%w zyc>cuB(Xy0uB7xsWVRV!C9(SkDA$l#jCRRraXT4tKPa|JzGJ`h6aK1LnT!qpz-1+j zz|+)knW1(KE!pHU?KjmrEqKqn|91bBg9FC&8@%6Pbrq$=UzJXO71LZZO|!v`vcgiU zpp0N`kx|`FM^n^Y4>y26fa-WnwXto6D@&_i)qJ^iJFIf9vJ7cM!;Lv)Bz9VZ4kM_s zWb?7684w6i4n9@UU{i?Q8>YG%t3hLdX*FxKRa~1x>SeL|*{DDHM^k*4LwuT1CmM#k zLUJARzV#H2pOtxuF=M9bcZzQER7a3dNSVs!UU73(E2F&!17&Kvn3%A5x88Er03}Qj zE_SiV0exFp`!cS5N#8asT32futTwJ6p(GR}&SsX#| zLOYGOR=FovU6s)&)(qlEQlm!maTf7W6P}~jkray}eRT3A^6}xLG{q$cT-JgpGJ!n6 zM;qR2VOUYzkiZ^BZEk0e7ImZDNij^Lt#NSQm>S%WwqnXm&_NY^>h_Ld{&+G{UTab@ zlRD-;YU(A@6!9qgNMMjdI2Jd+<~Y99N>|U^*aLUP8WYEH)-=WJJG|nF(7WFlA&95A zbC{TH+MZJ~Uex_7W+7WxzY-0HSj*QtIclEG%qWi1FKonjSwC#5LRi)_gst)u`v5x7hlBDp~JjQIvj=8thIOHTiclTwb$C@FFWbuL+gLEAvD?LRkR`nCGPu z0c^)XM5p9DYF_N#TS&CGn&tws2`{uicx2VLN@FEz>r6qN-!h2qQlj8v-8}i*@4;tZ zdM>0rjecoQNq_SC`Yci3O8uKD0^o<}s;#o_vsI*3Kz2gxHQ0^@17BsQ#`|mvP>oGG zfH9>G?MRV*_wM=lKBvT|oRJqMNoTTn8#1P9kHqlz_GFlE&*|#C0c=wUej7HzEQ#&9 zbtUNEqn!SI;{p&GUjF%srY4St`7N@-YBTBSQyteF2tOLMG}r!c_L%@-}}E`7h@>Kp!+T1N%?sTJS$P*Yfrb9`^sHqJNW< zx+~BX^tXeZtYzhap^5v&K7-8;H@@MkhUgGn)8rf-TuQx6S?NlHWaNS|R`IAEpDP=N z1r?*4sMF>>63$xhYbmv2taHrMD9V>*;auVu@a*5tGfx|7vf+x2H{xs_XMX1{Irm!1>;Q{N?I4?soRr0KRkRko&!lcMoO^f5OIk{yX&MQ4LQPt7q0>kwmxidBB6 zo9S^#^me;-FK)oA`G(wO1!oq!K2SHUDXOU0Z6jr_v5AR2Lo?b{jQxRVvA(sjX_aL- zx+`;QfAWUEyq^*ZcWJuaE5X(`+Vm+YhTAkiSiG@J@R-hR#g&Mic)LL(2_NGZ`T#Xt%{S+>+Dm{;P z6>r1NFn9I3ov+&h(WL}LEbitqE`)chgB=vK2S9RZq1^5u(K2a`DuP?ggV`T1mCVAe zNar64qp6biKXQIcMbpT@O$ni3o$lBP@zpQ?gf z{#NEg?TmS#^0U{V&3aYOjH93kSuu=-f%iwt^uipv3E*4js9?KU1404MFWi)=Vy11j zh~}y2DE1wMR=pt@eWC$x+d>b{tYQw@`xb`-RApX0-MjG?h*Pn-$!d{ZQKfw{V&;#f z5VAEH+r$-a457ZBXBDBI@MZ3FNoliXa<3L-c_}Kp$cs^n`;_Ee*iNOX$jQQ&I6=1s_j>*==Zmv+3cgHln#0aVpV$9>&JroHI6uj&0e9L?$}f4KLbT5s^B zq%g6tBz#3rmHSBB9K9!lEi#l;t<;7%EhK0QmPu2>C<>&NIB4@Z5i08aG&oe@7s`%G zy`O!B9kR$f|2QmR1?xM-InbB_ev9rpeNT_6TAu=~F}01zt@S#N{`@A>Qh_Y(_@ykl z^>x)|{hr13X4g%+0Gpk>x+=;gGAFs?UM<-YxmjbF;#MoEd_WM`EUYZF77Git2TRLH zw@fLZFsek%Pj(Fwi0B>kgo=&thEbfT7VL%{@q|!DXfL55#2cU3hvAsDBp8EJiHNyz z0X_q=vApgH8=oRT@XSz``CCn{ceLn+RPQuD*X6(Uybl(-s&@ov2l9k0kH9PA>J2%a z_i)QxWe(b*H`X-Vr09(OwzGD|o=!K#ob{0G_D4I9Z{$wSt`j=^+|9gnCKH=fjx~|>b_(BFSktmy>$_U zl~nK$18%}iPx?MqF|GEgP6ndPIG9eytRp3kEO=>RM?b=H`CXvw)bw8{IWVbZ8c2pn z3t10L9g29rt*gW^LMxG}yj}tlC`LluJ*{D^>VvS+wOd0qG{wIaZq*Wu$A_5fY+%YJ zNURsC1S|#12&!*ETaIp=tKdW;#*>ZW9`4evV1!c^)IHhCO3tf0O^dwRfxOXQB*}Cu z5No0BZ$!{AMuR+#vQ`mVGKeaWVQ5nQbZ@1Vjb~*WGzN~lTJw*<1s5=$(PP=^N``2n zv&|n;o`k*)OU7hozWwa~(iN79uDZ#F7XFl?C@4{{?fLK|0`8EY3UB`u`T)?E&i(3v z4=I_+!}Y4(@fBs@K^)>ezK2-|WAU^!LkhOsW|ju1kuQ%t-vXGZikd8`Q|gPKkE0N{ zf@KnjCP8}Z(|t;fd;i4B9rRq6Pqw`@_l8@vN0ok9xHF>er=(I#Tu>_{yZA`AxbIXi zAbR_VsJUN*`Lapl_AUXb7YwpRGH6V8FmEkpGE2|D&o{ z(aOce%HHDN{Q2P%{lCaFE78DVo&__+s9A5`oYn>rdT5!%cIG_aPvj?Rn4+U5m&7b; zyNh}=qI@QuEH&u>#TyEaLI9alFUpoQsZu?rUFqD4$*6ngzF(}1@5}LhYz|n83h<$! z`@Q^9)WOA6dnBeSpTM9qpCD$PoAE3ort+7P^h_H)!-rWx?9rml=^j*fjAW~9bm%;e zfz`QL@vFm#6B4DaSp~LXtpogQP8<4Sjw)osio3iA%L}%(P9gKr_~BR zM6#OXFF zEjj?6+7+l{on0yj@Eiap-^40dJ;mY?VNRK-teJ%2MCjpbSkhq@a#F<00nz6Hcp2Re zh-3!-?7z&rW_*Y5J=b^YwH+Rr_JEBolew7Lo`cp^nFC&PuaPt(7$z<`N>lTW@VrRxN~FVZ&!PMm8!l@Z4+W!nhVFtST{ z#e}WfBeM=Q(c?#leL<^*CWI?uPxxoXLSbI1$mDCUGLH)GxKvArS}Y2%hy;-BjBCps zcJOA0D_CtAQRW)Z92teJ5wu70H^t;qNGkFU-M{r83D{ss4T^t9Esm170PHM$bv>kK6JMj#4rzIX?oAvd{6o$_>{Sd=DIft1!pwV>ggA#I zE#ZbG?Gt;(-T8N_?!95x@_Y!mfPWNl|52g$zjM?B=nVRw3%y|VLnRDV%r_?JVnz^c z1_={SfO`>~#u!;4ItNuFswKrON^6$kSP#Qw7OhwmMOUowLM=u51DH_KB>);E&zHEi zmwE1M(b@g}@&Ij&5DCaZL0M)aUPNDZ57yMO(6p0REKRr5atmy4v!hgOXOLkECT$^? zX`Rd}Y+`IMC^6?dI>F{9j8~~hE*!0pUH^@G7@nqPl~Bv(0_@gg2cVz8AEMI3vzZ>& zjW&Fgl|!id%{FQnHLl_;3aj-A+X7Tjk$YpmmK*{P*d9e?RkG)*mLv0+6IQo<1VD}z3A!Fd z_L_h4&+(E*@-Y-+247pm-IqxbVBk%hQ`^phSc2Y2&UT12r<$&s+(h55xUNFrjqG6; zty@%|5JK?y2D<3Iw6v^?YxP6XE844D+v3OMMp2yGeoqDG?l^(Pvh>ZkAd2*2(2+2w z)@Yg+HIQ?p(qV`}&5~r3xwF?3q|wO4Wh5{5WhB3t3q8Agxp3;yFx2ef!7v9?OG=)o z8&=a7N|MHqVu_&t)?T{I4CGG&DN}gUjLYV!zIwJejjW8va25v{>f;PrOY zA%+olTM9e;;{*3Ne}Q{uNuoj5$BChb{wHwf`A?+L|F0@i`M2-iXr$YCdwoFZSvtp+ zk6n%>inx>4U@{oktjMR>yl&fPP9C6hLG6Vu@BsOuxKNFS%%F52AhFlkPYpfiey*AYG< zXFT8{LTCDPf4U4uZJqxv59~rkhVToJffD?_lw6H6_p`$S3jWf(01M3E7gQS@=AaK% z{q4?H`TCp0YI2`FY}fQ+E;**5)2FTSD$F|hlJ?*w>sw1 zcn=5|la`;FO-99CMhnvDes;;jqOm7gT8jSUI!zMgMLQho3VaITN4J9JebRMy?n%f| z(=V}(WRss+*VggPLkvWUc*Hc9&HoQ{^B;M!|K4SFSLY8N|C{V}Q`2)m7sY&oL8W0*%f+fnJ!%>&VxZ7Mw~a>O zx-8QrFNQIiIAJqrTGumA?ZVA?V1I$`vYwm!mm4c5I8JHUqi}bUcCP;?<9y{G@tk?XX>Yr=# zD8XpOab{sIR&1E={;#3_#wiF+kLr zCP?OFW(&`Gb|o8$&|oB0KXxeNH0b76wzGeObT*XytV>k9v|K`JtScdD6c3#lx23jJ zCU#tfTl@ZqT_&lKu=pjrD8Q{=_G$QVTw0$>5(a%2{PY*?Rqz}tw~+B3y;gf8ckmqV zf;whs3%|r3xg18z9rcTqggAMIr0pqn#HnGwM;AaYb6u@Ug(WU+Vmu>tVq$;!85WGNhbl1j@}cd~jCZY5dhavi_~~cO@x@*h z86x;9>u+~)j)CZ2AAZF?aPqXkhd3r+#`?AeP-l(ws@oIFiDqMLdOqWH7jBb2|FR(? z2SGFwR46C%W25w(yK?^nVS=lP@cxA`ns&ac+-r%J?P@C)d~hvSzr?n&xJlDb!;hr| z`Ung0l3-uHttQZKcDceFNb!!ajSwMH=3~H=jZN91UDfJ*CBzIPx{!vYU6)AhZ;^q? zNWQC);${$)|h3%T)~%fBPOCo|Y^it}m{ z1>WE(2KCATT&2DR{`mlzvS>VOFTBh(20CmXG4Z}T6yZR&O( zEg;k{DNRW!KXk@25F)6p^?nh7hj-v&P_R;Qee{4(F@AKe62acjofxQ^Qe3`2fF}lo zfB0)}G{4tWb43$E^zAL|&9g1CvpQd|@0T4u%7*@n)2*%BKAgs(TfuGoqlEJ1#e{Mn zrT%&^V=w!kmjF?x58&|=9sP8cdiyEw{84dOJN0PoPEywHJ1CTUOlySnj|GlRBX4;! zV#$N|d=qUgbD?r2rcUXE_MbnCHGal`L!=s7x68G9=>B|a#mS_#+Th~Vl%^wIWRK|b zy$EgnCo!c?w|o8bg7ytKtn2|U2%<2G9l#HR?bI!AJoGOIY#QYT@O-)B09Gc&klrJv z&HeOA1NBw7!oBpQqvh#3z>(yiu;GI(YMFD4@y37>0NJ~+sYgIXIJ0fSi!V@J6lQr| zhOB}l`VA9(gjU6o@g@Z8h7`-Y0Dz|s#oW?OHd#!*MiStlzTy(D3S+aQUW*=MN@dh? zL^z)b&LX>s6A;*`r3U_lV zQivJozxTajo^6ARpCtrPTx~O(;7ZKUe(7wYzcFsDt;!8MLshf8$O+O}!7x81oHTHjM)+j}|9Tat@A4F+++rdCnOzPo_wbj&VVRHCsU` zF)uNVg*~rIAM*y~flA=1FVe;fTs^6A#=^+=3g>~p;f&o%uEw=WY3k>7mc#k$so&kp z&3eLCQGL7C7i4NB6<%B6$>iKFFjSH7?YNECWAHoY1TXn2Pk-%e zp}FOh6^wG_u{BtG?J5ei9l(x&2eaY14%QAFs`dvKoRe>`w$FW8Ihk?g3geT&sFN8h4U@t4Av?#2u1I5#8S=Ae|hf-C}=TBpF@!e?W zS9yuRcWgFV&rne-S7?HSvtRYDP-rEx3)_-z@py4>NRuhh$#(J8Xop2AffK z$3&Ji3T@{qiv8Q^KqGWSfR8rp;z1Z`nFK_q+8V_o$9Dg-4SvbCqA0Oj?y2K=T?t1? zM@$7Cy3_4%Na6L2-xN~8{I2i}Azh0ru4#H7V^oT`v#l z>%r(xJ$RpI=6S|Zz4q)SmLN$_Z<{NW{dKfu*S-A(+v{)T^B zT6f6btiYu#NbmiEaM7rZ2!E>JCTY&qys%~ccyouEBPRF)2}G6Gt=zeW;iYc71;?RS z0U6ps+nBOjayu~@DkN?_c;0rP?yLe)# zl-{KZ;I|4Y|G^PUy(j(kPj#hQu@)`dcw=s~RH~Sf60?|d0(iT)b4NmvqLTd+$g_Xl z$5Ij_C);T5%+3P^>UXOol##LW*&Ny3kbm!64#~T(rwgM>OKa_%wu=af?rhku6uxf5OVDP+O^X zp714vQVEW~AlHRmu^+hU6s&if#B8G>4_LW{Zle&6tE_})^L2gquD?;9j!jzO63E^E zO!J-}Tt|7+dZ>mL1D=&7e=xuW&4^$RCT|%!ysi@ZXYUPl^n>g|Z_cTk&ShJO^@9bO z+V}ncsv_RZVwJEDuX| zeoR^@Y_C8qUF-3HnpC6In6Ambo*4-Ik#S~UtFx;9{`LL|);n^agw{D`iGW$)4V?KE z&(Jc{`m6?g`a$@{AcxJ9b1! zy;N7Ob$u1SH-iIB=Ev_XwoXq@22CbE-LlskBE0B1TB&TXV?z*HbqtvXG~xqeWA(?( zq(+xBONVN(IHLi2YYjB?cvK<7q0+#dwXLnHej1KD!E)GZKC7pcRjlj4%gW$*;d^e^-n7D9Feh(^xNPP2^iY*U_iSHj4q z@hC3%Y_3ZKifP3i zSsILJUKDvp<$lR|HnpOE@<1@tTQl{V^6^TJI%v{@ zh=`RCnTdY|E$e-Gp#Qb1p$+MY9w3y7g(f}A^fJZTP|C#(T4jOW$-^IM)_(>GZ-{pD zSbZf`d2bIQg$WNKmt=GYGrc&9c{JnnKypgrjqwQH4EDaVv8;(B0Vd_W zZ%P#&@t^fe&gze5AqrDT*bF%y&Om8~NJ60P(_+H!4;Twhc+2X4ETkWf~J=EgJ@0SKk2^0O_g!m%}Z*k^ECPlg??7m-D(W&my{|% zbnZmrhEi3WHAb|b4U2;dGISaQTC`B7%r*sbX8;-H4ocNOk>N zoFO8PQn;AqwBYh7EN}7jouc=1p58oMunWN)*$t5<77|gHHJS5b9dTD;-;Da7>w6fqyq;TcbLzjT%24Q;*hH8iH&qOHnH_-#L)gv%jmdJ|LWlT;gT zh#K5Xy*Ve}2hou#)@$mMG-uY7ffFTMZ$nNL@5{CPQ9Sug)$hA7=((-XfI#}l=)2u> zDp;sh`q1+!Phy}T-y3~;ygT^b(PJO0$pu0WRsh{R4YE4Qa6cm`==xRJmH#0ImFV&i zb$M;DNrJ;ub|bgnefAk*+@+mInXENZ2>lwunGY+vh`i2~2m7HVNSTl$C!(Z4u+*A~ zuJ_0yZXFUk{E;f50BFsK)%QXCF5S7j9WGz>bqkH61>i~-J2LEx(HnATqIfBZpy|m) zyhlpu$AY4|(-sGBjutc{AE1i* zz;hPE$Jb8J-YzB~CR`SZitq&fT55^ueZ1#<#=B2+*10K6Yy>t{t9;R>ztNm?8289d z1l=J1idLmAjJz*aFEJ2LFa*xsv6H|)8O;OybxXaLbI7sKz;2>;&6gFs9ye)FH?>tY zf4yC6)lvS+Ei6~WF%Up+r-n59;qXji`;C4>O_x(anLCq7fxI#dWEpzs$=INne zMdbRM#Y}Yu1MPfzIpgODYErRnufS-2-7>LKYtG6Dx*3i_u|5Xj4VQO1+@S&eV79jy zHO`PDEm^!in$9qFEHuxGlW~EF%ImO$>9i73C#GUiy>`KRpc))I_kCt>9hGsv7xT zt6DtJXU=+_6*Cqs;1PTK-21*vweG)6jZVM&zC(WXYngPse!A`5sHlVB9ttvux*d#~ z)nPN|!CG$cH0E>H9;8J`@aaQe!zpBN=^m#+G@Yo?dePaWY4(keYAvcp@Mu>EC-uG~ zWWc;js9NUV7uIcLK#xM4rq)2ealdyxKMglf@5LqKEjhy5N&4uvF%8of$q_N@l7W7G zyRfw{l9Em4>kqv894n7%Uj#z#Q?HH6dqpSSr;S`KS~%>Z1VSNbp;}XMvnAp2IC;|y z_b{pP{I?}^__sr`}5Maj7- zAK(YxUUW(n!kO2F|6S|o`~Kx<<)J#kJ*%ZMKi!IQEJL^m7u%`EDT%EZRfHj-?I8FK zD?_9O{wQHgF06DZJe8BIma!-wuw|k$bIuEmS@z!4MKx!JUPiqv>>YAP4s}qr(s~d+ zGhc-Z9(QR7N2^*?qR$r^&+48Igvt}m#@Liqqsi|YI_LkLss9qvmuk|bJ&MuaC zi}M3k6pO7+=AX-QE3CaOr;zCV6V}zVTDPD0e%YpeZ3yH6Vk6*ZZJ3#PjhxQT4?-Cl_@vAdovC>k|qiR`!O zqx0QI)$=mBBJ3$1#gw#5@UK&~sS3Wr(Xk2M)&eJ|AnMKq;`=Y#i=rdwmF=Ttg;BAf z&siGKZKgeA=sS$rB0OgCdo}elejq=lxB-}k?_fCZ2&JyD2JO}5Ok z;J1scgoO>Q5vx)Qe@Q>+%xz(V1cARKE&8-CX`Of#5i{2X#>snWE;S5_ zG^(K*(&QDj6C%8Rgd*WN#$M4?YDO+$TJ{A?HskR9N&=~(;GgcH+RYzLhQ5!uEf3&N6@D2pH#Vh(lL3)o7m*r z?cNpgf{8rFaqLgk$h~LrdVg_shuO4V@t*$R4TE#dPP5A|)v&`Mfn<)9TFffdsx3C? zXd>Yv7R_A6Okc*2V&>m><-!K@YD(LXxRr+E0>+d``Dm++Q zR?AiLP@_dD1jMO8X+i|#s+?C1r$*lkx%yb3k&<27l&X``(9k}p_N>u{1u&XhY;<@% zb3A_xs-J@q?GSCx@9kwAhLWgx_Z~8=pxg+oz`LgG4wcWD9rBEtGkErDLNtFI_6d4gZ$dGgY^_*zNsI^9#eC^_5?^rV`6|G#En@ytqAay;JtW2}|N2 z%S1JJbgo&;dc(?obKf&XY-!`f%)_-eyy^I){7Y` z+suk0gevqu6$hDahk5b`Lab%9Iq152t0jJwCRMN~f_*HiIB3MbCN7W19Icq)Jep$g z(Gr6d#U)cvnv{cIh)=d*@(PaYW05MWOuv!>uXEt*#=SZfP%?t<@@#`xc-UyWD-*&Z zn;QYW!nAt?F`63-xcTI7M?WCSPvtro7yVjg&!!F#mue``n$g>l6vU4EyNiX|ijk#+ zw_>|?sJ7XEomWHIB90C-j-Da(4v#s&=kLK&8CQ5U-NODw#+)rjcR3$KO#hDt_TR2R zxw%@|GXGa@XgdQP9YN0G&LFdodr($D+rQshh}K$F$I!-o|CHE5DuE;_Dalxj-4_z) ztu3LKTWSTN18&!V&7Fav^YdhAQcAL-s`KabE8C|W{;mh(Ptm$1Rm!f#?@Bo*JVl#7 zgM1D~C^N4O{P*wuHhEue&uh9NSHg+$str25ZO`{qbXA92Z!yI!>{22i1SU9?UGC`NJYIgzgBHU*q$MLzR1Wk62nzBOqb$x-B2Q#HL?R zP?PXgC-hc`!?@oSj@oMOfARH>L6&sg)@XNEmu=g&ZQHi(>auOywae_XZQHhO+L_tcdVU}nfphs94qITbIg%S$XSxh!`nP8baYIDyI9mOHiM4L%Yh`p8f#MN zs3i>Z1W*$@1B=QDA=m9u+Ms1=pBLX)0`Up7sZy_y()dcf82~-oKoAuucBQ@+Q_ra`XVk=ihrcYUA;`5?t z;_cUdDj!DH!|7*)-)jIb;vNt9VnAtpGQ1yDtX84u$$5Dap2dsCW3P zY5Y=xxgq->PvkF^9U#tsQ?sgC9(CFUkWUZ82P#QuVc?T(=r!wk7^+LfMqRHGuPLJ5 z$eKK$7n~5RGgC^e8XEG6GsW*;V)=OcP=-dQ$5S}~YQQ#g`bdNYYH8WG*){^qFeH6Z5A9ymt@WtJ#fS2(42XP-Pi~tDGtbYgP{1+(+NUo>S)MU31 zm%|Ddf=ofhePGbNL}Zd1r`SUVEK@+)KI)C$PqO!cAy4ZaLi=$S0-FD5nlHcUD;L#v zE_NUM19)zf$yk~mbxEum9o^d=PjaN!9{!HIop$vEC^%g}e!hK{^2TjJrzJWj@MXw< ztcZ#S86^jPn4*Ak2eJ%wCTklJvbsEt@_^D)JA(EG+K+1PuYa%}BZ z>bFRa9btY(k#yz@am-BE7O(QOA5zRGB|rz{`Q(msmAO}VtDAy7sWnD9&ZES#E#y@f zuhXIi!;&JH1Q;pU>z0UPZJ*CWuP|_sb6KGmGECG=e1x!P@y(2KXMoQGoRgrQOn*uc zOD%ZFOpKUhC(gDBSW|q4nYu&zCwgb`)Q@E(7&#DdXoQXIU$(676eG6X(xg9S#(FmQ zwdbYtYvXU}iir5x)ptpZUT989;gL`EG819x@Pu6Hv;@imC`Kl{~Ff){1@4ve_RE;`Tmv_MSj$Ajl$$BCck zC1gXoi+B$@_)*@X;rMY2bivwvl2*Vso2g!%6Fo^cYd2{%nUIzGwOxWY&T8_J(`*|M z3o{}M6fWZxlPUPWCuDEo6)DjCnZ`!<^lh?XA1FtR(OL;vK+uC7sT%W7F}k)$YY?CR zy@fy9On3>|c@}!6_~Fw#Ce}%faZI+;H{XM)=o4Y}P$f<5NuXfkkVC$6=iOrvq8W`4 zbG89~8tMBEx5Xf@;qX)=l%ci5Nr2)E+z}yL9GbJF<_gAs~3LVO6)f3 z5%NLqzNL+jsGiWx1xiv6J@@8Qaw1&mk3=Ta`g1ge#AB>9b8O`5751NJ5W7ZmHrBV~ z(h>6CLKB|8y({PF-9XIV0c-!n*o+w+mz?3Bo#Gwrv?sH>yOG zb`vs~TsbZXA>AZj@R|1)=+ zRBypwXe+%+yo9eoF*7|zcb!;$mpPH8VpH^}G@lgg1qJH4iDWsOuQGL*0VU$zqInFK zq^U_g6-oPbS`ODXm##nUnrtS9FpFS;FK4Z)5v~?p=z21jSHK&_gY_&eow-2QJ#Ff! zo0Jv2tY*$|4;b4s#tvZ1?8nE_M|=HiPG55_;?!vf#SJhgvCGuoavrOl$LgE8-@+Kg zJw@v=Ll8tWT)qNj?#c))8*8)R2~(VTMuHcWbisgH*$Dme_YNh505Z+@Get-E_8Q=r z6m3K61yYxo)1E0R{~-;-$#A0 z*d{9{blEoz(+f*@!X!TcaM5_hx#MVZ3wwgE>(PDpIDuN9@EkE~zQ?x5_G6)pXcD)2 z7q;gg2XF0c29yNbjOdt!;=EFbO~>qze0WG6>x9Qlz=4*J<=D|M(&6r!IX^;+G}0?| zWT211z}W5xh0ftm28q$y7F1iP;D!vZ!_faB?JsJ>uR`*e7#DJBc7FE2Brs{Shk@Ks>y=<-SSMf(SiS>BhYso7u_97GGjTt5hIQ`* z`UhME1ywS~-@)C)zt(VR|G(gJ{qK;DR>0QQ=^Ih{cGCLhHsA8)f3QSE{~u_wmY0_O zZn)+q#H~)GsBiFHD~?FMe*l(9SVcfhZ|+T9?hmqe8#FocvBp5tSl~XAXeI)$>J&1D&l-W`z|L zM+LZ%l&N4^4k0Kf1d%u^#Mv*4xO~noE={g|*d!*zzx%aTrrv|JM#0vTIxlpBfGg3B zPoZoayuB*t{o*R;{g2^xg-pKpUPkw&;z;*2(@xZKZo^jxGrnf`BX2`C6%NHO9jWM| z7QV{Qu4x3Fd=(*N&R^xYZq8(eQDfj%*D2hF`;DHLHaIgHYlX z1tK$-kj)ag>}ImrU~}GyMlks?T|`G{+xJ0hIYbFbHi)lPv6z1ued(#zJfWc(8<-_~ zZpPfT=fr#rjp6!$*r7;5ZEbXQX}m0&**gk0TN}>MaSlf6yLWV6Tu(o)D@<8J-oEw1 zbHO>S9Pp2vLpTzKy8G2G@5tbpM0P5l-gu8Bv-iKJ-Bm7P>3@u_J zr)A}2Ww2gC`_h|1)80zkPq_RRT)3;OJGg5k9M$BxdJ59*+_i3r9$Fi!4uy%yysq?` zd!o|)&A$O7umhnh9FPBZlJ(EZPL;Bbc_k#HQk;Ppd$uunj(+~9->-+~x>34mfH*ad z0wF5z@As}OxiUd7U8_|cr1t=Lrb0}H8jJ_W9+!)2#$dyj7$zk(L$Q5GOOD{!nOp;* zp&zs6WM~U{`d#yN98ORY7jS}&9{>%)eSIVL+phJ{X?(z5k3&0`kdF0bwxUouWd)@g82=`*PDCZYNO!&%lno5nesF1 z;lPBe*u67)Z4Y}{(3Mwl-D{D&c4 zEsSUx!S?)&G|BD;XR5^m4?kenia20UFApSN(`d*wVJgX9Uzcqru|Efo3`rjAl|A8sdH%{ID@H;!1 z{X@SE^_^`0Gv0|(QkVTlxNid~g%0o!#iTh7<)E%thBvkkXwCJ7<1_3F_!jQ3`khF1 zFan3g_4Do)Oj*;`Di^skxZWO8goVvW+nYr?^8FEim7kw^iqL24p5?Udl`kTWgjm|z6iyo zAgF8$Lg`>knt*~)hPYg$j5bR9E8}zDO8DpP>pAGj`Jn$alsnp}V;a!@4tB&<4;BB6 zI5vt^itA8-r?p;Gu{Vx6cKn_7>f}**gzUYcPI#+Qn(ntVORK(3Bz#v2s0zyK(|q!j zCz8C~*X%Vk%PHF(>ohI~{4r0=J4ioJ`-HwVnm;CKRt3AS9fHy)d{hg7kPh-M2?o>9 zoQZS^F8H%E5g7%KfSa&d(hzR8bs*#Ecgn(QR#0l~Sagt>yUtO zD2+;y9`oSFT>aSx?YRc@R+lL2%YDVWPNM4lZ76r;W41B7W_eDX#`DbFCGP58zC<0` z@5sbZ49!FP;-S+}mVfscQtKWOIHAZ_BZdo~P>eyy(-pH~M<6!LF9K?YW0edIPeYD+ zesHy!am85w)!E)v2m{2vC`#mt;-P|`61WfQt#NMISmJj-O#Z5KaJJ91Sda8+#T5Wj zD_VSH2|Yb>*+8QYZBRK9Pde*e(gS5~t-Z5g7v<7oIc9Quhh&&b`Y$F_#6f7tfq}{} za5gjv6UWsYYygK`M}jaT`j1E%CS)gw8FgXIO4wt-9Jf###*~$T?v|)idwD*oxpf}5 zQbS=g=kt=8K&lIR_4c#}mQO&D8^B)?+29P+$*O1go_j;6!e7WVJQ;$M6dj|iwp4boT1)cF%;{JevcQy;gF5Qqq!rJw3e zD3%XIz(`z;g$h#N6<<)UozuhBb@|W1Bved)u6%801BRir*J7iPrFDGod3=MvHL#_% z<(!JeTu4J@V}r`~Pfe${EDuu`Q(S(_KkHw-M_*reIZy0go+m?7UNGI1`y2j4GC4do z2XTY^lsqQIAs7WwH#>uigKi{A(nOV>1PL~h#=%y**qr8?>=_XSDpk}x7DGc0K}uEh z99G1RE|!Kah1ABzYX}m3oAo=BJv~wDXIA7nxHLowLRd{%SfvQHMP7<;gVIqk>2UnH zw2F4Sk5xjmC>5h@gxG}9%!&1!GHfa=mYmp6Mlyv7=$6C2vU0BD95&8GmG@#pojryw zT(-FqHST_f8;nO%{K^cB3?a#HUNaM!V%!$311p3E3D>{a78X-nJ(KmUI*iM!XWbCtdAz%*?U1a3WZsdna zKKFC`C7@h8ihFGwrHA*PB?zKRC9`dmrTGx(Vo-%XbCWcV*kwZ@R?%m+IET#q(!V6`0KqnskSZ8SV!WE!|t zH*J%2LogE+gI&+jFzq?GXHK&bUk+kV z>jJEo8{zTiJvWJJncvCd_a$M1`a>qQ^qqQojC$-9&bhMW<0-La09Y;n!+RZ92#)o* zf;N>Vc7dNU0oo^o-f)MrT{h}%;e6K=QM{o~g`jqJ! zdJCy6^uE{=t^ObYVN6*86S-bjw)dmeSrngFf>gGj`-mMd zxynduA<;_&geDlSxeg8W5zRitC?(0xsXL2}s1>%@UP;pvvi&<2IACR1z3BxM9x2H@&VD6RH59rtX+9r3&m7ht~_r|oG zP=Z6Ohj3O^hZ)1<=^FTWbhGINx#~a|V>+vepI3Cpb zNZ7r}A{&p+8Eb)WLp~D}5#}LWoX`v`%~xi025v1$>jF;K*DCU6qwzkcHD3!H&dCzm z()5q$UWcUG3Ek%%OSA4v@IHm+M(>{I9MFrf+vbpAt`|mwypZFbkNlAxi%zJB;srHq zKLu*6e%^2wKac&;>DHwOW3rlqYFbFN+dM>|#`v_$dgvj|8-(|C-t;pc`x(SVvBo_@u272Pm0l zYEX}(qd;+c>Dqa-*zz&WjcNYuDa*d9og>r$`V@qI27WKrn+eDQ@#YKJp|@Dry4Nd4 ziT)b=`fZ22WWsD>w7dxOH6*2V<6mGtCOB~s!F9ff$EgFZCxLRqN3bM~f>j(lu_nlW zV-Ayn(1rV243m2^)>2PvdjQ#zRX}9i#C z;A9CVRyhxB6gJOzmWUZJ3{HtEK5jQ>BO%ruGRCkd&70xCF=JQ+5>nCOvWd~Sd#)>k-doj9!W;nS*>W2^%btxk0d>(hd*1C}>A{dz zEAaRV?7i6Ysi^*Ds(D}4{<-3Or|POPVuY(5IQasxf)pF&AZndt1;|TNi$|*KsN;0v z?1OM?j&->+7^<2LG`dVQ1KLQ*(!7B4FeRl|n@;*kYCfvXTYIM4EqAb~n!azt{6X2V zRIQ1xMjlO?sx5V5!mpLEW)Z@n?1P^COAF}vY1v+;a> zQb!3f-Tjp};Q^jRbuSRVF^e5b(<1Bmf}Hh`*wz|UiZ(0xLu9 zB+;J@^@BBgFPcnm9xzuY%#eplZ}uI>BM0IxAcDCaoGDw zl$bkN@hJ=mfPQZHh)H;cVmrbYa!ejl5Wa!j<%mQ zj?!978#hXFaj8_c{kgi$U2~A<%eGvK%M)RFZ}euLd%opxK#V@l-fHqMKIBAOVqrO$SBo+hIFVYni{h; zx>~Fay%FF{$|JJ*mZbkdPZrnc0a?l~k{VKaHg0rFVX!F4z~0&L;TeAy7ECeuLSsr_ zzNFg1Nkbmhps~$%YkV2)iSKxCInn2^INZeOW3|0$VUT0pT-&5?JkRPkOoU-5|HWoQ znSbz0hbB}dsGP<(?J(eYer3a7av45CI0{nl9<`R!LZ=(22U$sSo-JavW~aB+An-$< z(F&>dYI>#gsQ2@5VS?Vfuk*0+F!|8_oHqvOXvPNzbZXB}94opZa!>il{+_EM*bSag z)lP7ED|Y*w!Ge^2uy0RH7VitHMTh8W`-!d$V22n<5il^aX z|IiM-iS_)`UvXK+_!O7sr(%^1{?)!IIEvzs|DIallf6#m6o#yi8!vvq-bMl&x@9yr zyFx#qM`3a3oJWLP-y^}Kke6^f@hR=@#Z1{@@?u6Wk42#c)|I>a6wxN8rCX-NbGLAc z7&OZ+cYD<)Hl`T~b_(Hnbh+JMP7%9u&dp%xOR_2i|5$Q=qZPa=oGw1g7Al&~bM{cK z#aWGh(TL|FLFle^O8Fe255(sM0v_ZD73tkMrXwOAL`&=;#yvLGe#hG7%|n&XsFRi{ zR!e%$7*YEPUgA2{vGlxt6TACxtf7+P;AWz$-71lN0_(l! zGlI^U>nCAPN-gDnUeucl`}eVd>>BuotN~StJx~op;dZo#CDxt4Bn?lxuF9?|wk&<0 z$pqd=ZG<()1#DdS5fKUxL@v~(BP{<^JWNlbDr)0b&(4j^Y4v3^+t$9L#@YQt^?c`NZ4*;g_ zWJ0p);r1=;L~(UL@Xhz@SCqS11?rN27+avHtkI|-v_d(^TIlCYdJ?TVz+29T&iY-o z7(!MyfK<{{6!caeckcx{EB&?U_Juqvxdvi8?d72Ggo16UiH6{Or}#bsw|7WeO*Dwt zuUPA!VMMh0`-pccl7F^n>vUDWhw3^|@;;QZYv@WuU?|r>i87Y+K(GbSE$xn{BGBHI|QJ&+^iaztKly2Whe-6@TZ2Oe{jAZc0B7HMsI z+abFdgKxf;RJqQIk;yN=|B%sLc@w1Mj|w8F)DO)Xpe3ozlG*=u^_v?dj#s~m-&rvi z5DM1X@c@kQ~g9mKrQ@-Zc(!}DhwcPoAFBGIJr_< z-K6bzg|721CZ^+DPqvs4+GxIfK2z>4fM7bvM%tGmj~FjM0c|1{E$lTx4MpHdNhE&d z9js=lDPNb;A9g`55sk%!N<8JqZUJC97kps0Vl~=AdMxjJxhiZ8!PoAF-O=@qB0IU2 zrr$lrz=Uq^Hg!GNwGc1Hm?640FOaKG(;=*Mvy!w?V)vTgnn+{6Ee1~gX(FGe(u~eXH0kM zxU(eN_vv^XqmtFfKXF3aA1ySoSc7i4^JVh66pm$x&R}TGL4U7^%)*U1XTO;n!0I(O zem?;zvvr%m(ms*l{bt_ONR!{VUfn)cu3i^(hjJ2|@{JHmSi_(PkZ*@BA z|D8IE{+D~M^0&dIn60(3l&!6$v)zBt=qQE%cDEGt+wjg+lb3tfLYC-tL97)QHt<*2 z&I6fKK*&iyxpZoCaxrCWi@+B_@QJ7AF9_r9#x-uI#wd8+9vlDQILh`g?ezM3NA4!1 zqHpJ9H(%INVp`-4$343k7kRuzQA1)qp@-R}b0i%p`Qs&?i@ zgIH@Z(20G!3?qV-|9EX*-6Yb``U$x;Ran>^Hw|8^kgHRvykxPci5QtTt&-+4;DQI- zS2x?aXL%{U)4s?R9pvINXhRC`t{RkII!_k?EyXa7b4z$BhVwG1b0ybY>SrDtR5|!) zQrxn&VQy{1jJYAxHgkG;*nw` zornft9c&=h;;Z#V*D@M5C934WvnHYW$Sd>8EF@sEOUpZsxGz#+l64=iQDUz)I`AwB zq-@sc#yTv$F+-0V()&VvEH=q+4n8Byi(o2M~vNsKE*Ypqb z#8GS>qI=2QOmyJ|4)}y8bQz7JMg&GW@#foPAQ}FJ(xrE}AUd8rmQ!OFj(MQzl-c|0 z`*!6=PbGTv+kyALd~_6em~uzHD-vn{T9Nnwwm`9H#djho!6Qx8UH$DbKRc%*x-6{%Tk){(jlC4Q=B4IIxOxDuIanSFF$ ztao4IPg6&z-vwK>?jf6}*p3mbhi@$dO(D&J(rKg1pn>TsN$Hw@sz+U*pIb|B>@o6* z^zj)UD)B0x;z?PdNJ=Vx!0gx=SI%AK)aQq>Ep-6(OLr+$ojONG@vYZyf$rzpiw=OM zp?G)ULsA(RO7eV!=+SU_8}CcIswC6IN)*m!#Ex9|2}V;16-!d<)KXy^S~PI~y+rF3 z8y!cNn}OYLdMM&$2LA+o|IoLeLS6rWZ$v{e^Z47V8u4FM(-i*(zW>+t{14$iH_M1ZPRT`yC#~hxn{~V4;WWg}s!=Yg&lLXL$PdW;)U&t{ld1N+pL ziwAde!-J5aO<6PlxC|)67?89GO7{Dow#um)m(3e=M0L;?aaOQg#*lw}SX9_|+^m{+ zHjw9)2(bWWC6l})4AsIOS!_;Qm7q@%R?@8C&l$5+I_7vgxAKMrW;T_X*V0TPtE-?X zZ<>V|6xFNCY@^LNLVdeTCJN={wGC%O%X@^V3SpTJ`zjpb696Mh0&{67QqYv}Ogzw_ zl|eF6VQDfscKy#Zp=~8nfHbBE^GPE)gsefOUv=KpX3J3FBXCh=?t6{Y-f-!HHfbhv zS|p~A(L%+H$S13f)Rl2H$M{`Xq*Tj?Rg#ZKf zNGd0O_Si~UhRldE`5Gi`?&`m)`jvDprdZF)(y?XANUNos=YMFXWd_!Z@c*^{oBz}k zdSzJWkN+&|Tm^@oR=3l$%D|xrU_7QGS?c!+OAa8`X5@B|qWu9+*{#D*DS3=_>zDD% zhGeD%-IgwSjk#|weIeUfgJ5+7A;O$KY;uf@Sz09@GL>j2bh}&JQfi|TnD8h#AOEAB zYa_CyK0(7QOG>&v+uYqp>O$2ZO-_GRiww%4sPraBqFy$WhY^F$ym+>B6h!-Fre z1bVg%JW%vzH6N<`u_*3@fhnLMIAUu8p2aV)e9s&~ldq3AoH360W z9e-A2Ht69Nl7Q4PPKEu4d7LbDrPivQz|Sx~;SrQ*m@lMzmCiX`hCiK_>J<-F+W^8yffxzr{*p?t9GK^cJ zt3=T#cWN4aG@Wdf)^InB%#cSyrzgZfNz`qBDYbiP{5HQ3oK9J^ zCE0R>K*9Oie@`vuN5uuifZ6bH5I<$iqI{uM}==i=fsGoGmtZu#z$9w!Xk z+rQG%`J@H#1ZnxG6BM4pyfUezW8b5rvGTwzErhD3F7vzMhoBiDxRb3P!w$$RePx&{ zhm;L*hadG`SmPjWCb)$Clx+Z`fAB>(fh~W)v2`@!v#N}SI#=ukZver!XO%fo+3Qgw zt0Yv#S8YzYL~QrwQ6zwZNjIQ3{&DgVLBO}?j|(7X$C!+orf=lGmb^Dk3qJk~du7tf z<&-Q(;06PZR$R{Qf5WR2(&g{d(_rZ|={7>1+?@)tQhc>vC%iAQOAJ5+)7Q)wgzJ_Q z(+ad;X|imvQ=1@KA}TWCkm{4VJnf}3`OQT#*T+P#iuLuoY7Q@E>>iH0r~ch2Pp(BI z2z)~CQp~C7iP$~f-0J};+R~tUG$b{5orza=>>kOBThSJJt${!{f{J*CHTF$lS9-}n zpv(%m0kD*pZ9Dsy*eUWer1Y~;A&m09NHzYm2$%N@nfL(dy-@1 zsAMR_6k(U8kBGv&seWxMIe#Li=@jG$h*Y~C5tq|eIZjXAAjmu2TicU=beq5}KS2)cj;%;uHLWE+#Ejm1&+S=6guF&fk@jQsrr z<1}ECStz{L;1uL?%Y}brKM1F2F+D<7L<|v!5_;;Li5eaVfiuO8JRk#Bcyl_AELPH< zYJO6>Bi8=Ac7Lke_+&M4zsx{qbRo^21eI@BBkfEmeQyEgQ6^p{FvGrJ8ZsEy;hMp$ zoHuSOrs!^Ap4*zF^hVvYkq&z>Ck+s|Qi)rwr=gX_$t^XTUke+BR&7v5Lq}+ZLr5-W z<-6ExHtg2G9^#7jF~9~j*W1qcHzGjZ%B-%TN=^3L)^hKC&3gzw0uii>?YOQoX-FaqRjB(Qr2{ zxew5feRz)|>KXZv^AZ(Ib}6$txE8S!mPMA9R^#Zv*T3G^F1VlXCRwP9)d(p+=f zr@-aJzH7^^?Bqhka#6~*Gqd#}C_*{`&!m0Z=Fp)??nRv*EHQhMK@<6q@2ja968hL4p zvGyFx*W$)m6BF~Bri<;F*-D*9AD|J1RCGqfA&~h_fvDhxD?o-m{@NG|k;MO1@~)JV z?I-Flb>`NokvD0+ahk_ejG0INo|KHqSaj(+PqwBb%&i_j)td$XNoMvIzEn+CcfwAY z!?U1*R!Fnq>W9bh0=kuiSCgTWP7QE34Jn&DGA$xm8SIpQQ_Ml3fmfu3|aP z&eiuP+wS!c%+bD{bUp*FChzz+Th35VQ!Z|>k+ilwbtM00bJ@0LRB0S<(oH;h*HQOTg5F7rrHwXlmcWh2;L6_!pSCiE@a;JkNu4 z2=v4km%$Q4)&oNdw}Utn18WV?@U*8FnoleGY5Lz2l)m$k_Vg9x8wgw4%R!LxNzXdJ zIZa+ZG_0-`<%AZ57ER_`rtVO~UwA(ASr{i^DC4SZiWr_g zixGl$8vO%rnL5j+sb}_&P**J`N(_|o`yz`jFJ!!9apOxvyBPW=Vf%0gVPS-y%sm!F zy6`13$|$)d;KrfF-pTUW89KY2xH!}2+`5nFS|=P3s+s-@`@J%u=cnd>*=8KZDef&w zt!tMD_F?1CRNtt+#Eyf$&?&_mVZ3}9*FCxMHiw013wi!#)NtQo+!S={$1X3n5O23= z$3SEMCH!Nr0y}^VfCxweu6x*5^s)KnlbHi2JtGfj2?~rJbSBf;Eq1s99!y6pWSTh? zX^jNzpCPWrfa65Ty$6p^UDE?30T62m%iBidg)#0bhsEfXd03I9WWy7KS;M2HS|qGR z2f8MAcb;0Y@5y3Yc{HFtm^~=HW^js8dXBfQILQ;BBDjQ&3^Qz_&1sO zH?GFs>PDx#bZkhXc@T^HWejCP{i?Xy!;OqNm8#EgyYsk4G(K}VRFUP>yhtP!cZBvE zN8&!c*PJiRH))3SU%#PcHjULl_DoBt45dvLqnqtU9sw29*SCm_F1#i;3@^^QcgysfV5TS!-pw=Kg0+K5FiK2H(P)B5|_8y=3mzc<&DlVE% zLSbjAdUk>F%Ir+YPs%Rx>94{1A1+p6PaXQi@X(KWPt1lIYBN1wa4tXq{@!N$)Ve_l^*niVJ+R<&w$FsQ(N8d}a8L{bUwWxx&%6$=PEHiXXTSqY~zZ}U=3 zi8FwsUI4Yaw34gXdX*D#B#UHW7f6V($P`KEUe7yN#VRry2{VhVra4~fm?yK@RDRHfrFKua5K zQYU!?QYYU9_fN6%0DPL-@#%{7d7HE>AEIWEa|ML;%ya^4Ky9ZkZ8Q?{c;cw8tuCac z<&5GM;qK8kNN^40s~(9|Yz`d2r7?%{#L9CoMzvCiTWKRK;i@IJ4MGuN#2h|IzK0%& zGh`#($eM2VKYqe_C7*yWOK5g%Q6Y)Q_+As`FvDXh~(Ej)#lca-Ys z()0m_?+9~ycS&&b&J7^tUnM0z2?_A*(sbysKr=oB>j2U6d;Oa?v#0hY9PiC7W|J#U zcW`C{Ugkq6?sJC>9mNy+;e+-DZP4Z99RJy%^&=VRUK_E#eYkPR&7FIMD)zChh@lZf z4ad(IGsIc)(Y{$4bmbEr3(73yr!{VVJbW?`ISuP*F?^B zh4`aQkKNq1l6Czx!a!w=?r~Kkjf};J;>gMihodotq-H+$>0 z#-94|YvYK~afsD6Z*;aNPHVraG;2UQIbd&uZMe>{3Sn5^8%9iBH~gKSb9!!x8_$9u z(`dslc+xK#F>M!#Tmu*sdf`e5*AdC~I*~%6(_=ga_V_Qa`qTO*lb2X zb8eDvI`wfdr#lt=dQ&+)x6?)*~Ltw zt}pm1gtJtA=l-G%lA*Y=2jSv1Z# zN1rVl`rZr062*)}IQG|$@8w%lh!{W6d6&fRV}*u@-TjWSHE85FdjFP*lu8vo$`^8o z@~ioRUrW4W7d-&`erEo}5U!%ijs%JLFo`E!WA*txeJ<`R#KQ6eMS9?bEGgE@Dq~!_ zmW$R>J-=@d#H>>?%D=72tWyr$7Q1Ld0Jn||6%o#ja)#hXvfhby89gh_wtQj%mGE7Z zzjDETCZR;cjg?Y*fKm)b01R9gs$sdGRXx;!GxUl%(6&o%;bjoYo4p&9Khdx_s9E{} zM7m9jpNX0cKw+$sY3>%Ih00bg?La%-I7{$g@x>|K8GfRXWg6>@!aWnikuFyXyd1m4K6ah1Uv zp5DaG4I1mCh`;Q|=$Kz4k`L?^FH}8t!R#xkm1}~Qt@a{Ipn6B;Cz^BQb-AB5=Fgiw zqOW>r8y?;tw77@5q%Te6pC5EzMJyvew0k}zFI&sh{BX@Ggb_*r{5kl2JfT~a~qYraWtWW7G4*io3yDjeY8iHz8iQSse zxPiPiO}48OQWcF3K%e2gw|W0bcuh{p&c!19`0-5gZ`o#o|2^R){y*Z#|44K(M@rj# zzn?B>XqqHc8Fo5p2|QVX5O`6!sET3+Q7Tg$EUiB|t2>`Up?U!rbX%@OB(%vb;_4%l z)`#?mTTq(s)}DQW&)6?#NiXsx1yzQ)Ri{RMER4D0EOK%?QK@6{K+XHo&3K!SFL)>XKP9TnjQ=&dweSLJc+zTE7%IzYpyD+XyJIp|n(B3q>P&j7=q2!6S;SHL_yP2MU1HS&bOkAH; zWs~1|!SnC$|GlsG|B{p!w{djRx3VJoKjVKnDg4J3GmaaR0HQ|<{!(i|9nxvHUXSvx zY=lvReDKHQM?&fm$QyG?mr-OyjW4C?_mA1n2c!8X)5@iY49^~$>3Ew(8{6*F-uZKR z2%~S#&2!GPrft0gnSY9>exp;nVPiU`aJLpJARYJd_d|o+_g{tp*;riiH8+{;W4_f) zCBZ9m-dga)STNX!4sP-{X_Bh2`W(Iy7M0kO*5gw$r?*9Ixc7OF`_k`g?1;7PN>TLG zAX`k`FkO-FtXtv;3vi|3zfNdya=SDe-T=jwe;8wow}%)Ub*tOs(>!Y;mPBa{iE_(c zC|?uH&l16&NVQ)RYcvM+xA-27Og;_(o0=C%HYx!w$XQTRBX&lid?qIUBj`fr2zVe1 zJh=?9f(uBIiCMXj=f8CO5YD_U8s8b+$$!o2;{9K4^8cQ_|Cq2!O-Q}Og(RM%$-SI3 zs93mPFtFI0e?<9y!F1yJ{*KoJr^n{EkAKG}Oh{vc#nY&S^<20}Sf$Y@T5y@OZYW0~ z%`0hMqiJo>Y|?Jq;IXnkYqTt1;Ayg)Id{GMnj)mj+qk5dOnc*bT6gTa%i?@qaP7Qj zg3l`c3_m%e1?~vz$upyhk0#lL4hZ8xd=O_e5m6w) z03|35-u(I3RhEJ=RRiucreDG2!#{5WIv_4DxMWUy#l-gk)ur76}X7FUgIP(2&=Kg1vjppEyxs+wp7-9l1DbtkG z<}C3mZzsdhSFo5j*Nzm4ikz6oG{UP8&s4j zxTxX@BQ6BXQ_&%STE43+lIAMiiJjDCCmu!&fYzY3>=mkw>#p$2D( z{gN<%B5m;<7m2lGRFtuFR#wqGJw?kYIfrXM$@cHZegVS^%aW{jI>FYYNjf-h*duIp z3}AM|OjZQTUXvisWL04UAj_L>7H_;*kL;O;sJ+*=G@rQMaljK{wL)+S>G&FP#9;Ol z%Ib0`oxJ0V^1VJ3<;@JAI&Ir&Q2<1lQTasU7}e53yx-tj*eso)BzeRV2Bs)vjSh3} z{xrQ&K(CjA5otf^nA6x8-Y0vv34|PRTL7tpKa-9uCsNIA67NS`bx_ zZc1Q^7E%XY)S9hgTn*XQAm1i6(=*@7w46ex*yBckeJ_kb+?X&kf13@ysVO)U8)@PLKHxjVKET{JJlJj?*cTpW>{(F{WXh$j-VY-L*W zi<^;LE)lh_(yqjLT#|tlWq7p8Vi=LJ?28~w@->$gl}|lLeV8+LZVNd1p zdVQn*%cu#gIMS@Pf{85&x-=|n0}%AjptFKD=@!8nGxYPaDc;QpL@kyU%am*&mSxE| z&&0fsX8e;tJhsQgyFoUkg=4CRV-IX?+DCWTId#OfrdGE)(~qNs%;mDL%RmHU>9+#)la5*Nsg(}Ii>KAW2`Y^c^O@e4K6=`1_f z`5&NK3!IB#eO)n^k2Bc6G4>{*9)xFW3j6?zDlJ5~xv=5RJc$`9nl5jA47IeggnD1_ z<8zpjg;Km;O8M*9POyh3bAtmCzB|s|uWTO;QVwF53|#g3bc&P)xiHo_-SPm$jPVb4 zA-03Eef*uD9OngZCkX<|tfxe5iFz=1K$5&lZp7;O z{4QQ%^|AM~=Yq0OshiR>Vq=3_u>|puV#JXW1+gFTBYVp~@f#etNfv77O~--)fD)K{ zruGyK1aN{9G?Qng^%V8w8T|s9>*Lp2(pMkzj{?DD!MJegR*}=9K$G5aAq6h@vE#)+ zgeES){D#wKPZy_8Y1&AuVxRTLN)HWJQg>4a|kuFmHPo$>QRVzcu)nkcu-7 zo#Q+!l2$8QgOk0dwJipyf)aK;CQS?=|74qwgF6v|c|~oRtj0|7 z?40=hUPZoI-m)j(^`iHf2KO#ej}*jV*l&+oajT|gOD{U(^jA6m87GN~4pxZbUm!dn zODD2KpoG^?caqyv(FpUoiH1IL(V?U~KQA@SHj=}N3j#>Q^2`gyN@50yGG%|>9FIdm z6aO?NvAI(pkUXL-fpOB!H{{_Ell3Q1=Yb(1L^3D9Skm%)!w`AqB+8@iynuSE@ve+xwN(Nx0D!acsZp!ooM9AJ7RK9Fys#bK0l(&-}_XCwZh7V5%TE6|E7c6J4CXa8iHo>N#5C&<=c1jnUI zmiLx{ir60!O6FxYl-3{Odxy3suDjS>Lp{kF)49CeNl$+4+vA(@de2+;qA$&&BKV6h z?laO=N~5h`#AupL#LS6Tlgr-fj5D>8jOz-7&?0~<+Ch@#p2mhWo72_B*1;(l7kBmA zh|(59diqT#YfDIgZ!efmx4uvWR{s-9^{zf4YDoh%HLooC`GAGEknHqYv`{-CBG4Z# zZGsQ>zoLg#&?&WBLp<5>V+0q}Xk5@QKV_PTQN(w=Rb`r;S`h*$)ov}S>MxU`r?rAS z1`fCq!ZNr#QEX^A`ov$yKQ!M+M-ntXxH67@c>SSKyp|&xOtdgUAtF5xt zj@=6s2Qvcd@m?4Od0T&`MXJLTD)|5D>@1+-TAHCy z>y$Hy8*;*Fq`>fG5uvEMH-T(MMNRY7%x~OE8=TMlu!IrT&*T4*DRKB#;BfQ(>`G+A zdzt z9ZxT3JMVR*4#>EdE0Ki_rrH$&JfIh~fh+l9&qXwATY5P*3j*DvcVS+_erb6b5C8zVSz(fg>I-JRqK=r}Ch z&?GA9*PZ1nCc6qEk;e(i4TF7v_=*-$Hky&}D>9S*8_Swv(s}WvpwFxraxw^14Dr5U zsN$!FnlZY}tU>Q6l5n0S0s;Lq@oHupo*S=Bi0@W{Bm#v|`Xnnq;;dkf5lSV+*5zO; zp^J8PtMKl9RSLr@&Lk1jdo$GU#d8o_!MaoYaTfpu7lS1jZbSc^9B~#GYx+rT9CNA; z$KE=l|10@xNdF#%5l>wR$8$@ovm<3Mq6-7UXpuTy)ccNYpL5RyKo?!)J*hAB9FukR z>uK2wlvnE6@S=(e}CM)Z# z;3m?ssM=H1#JyDXxsSEc@TrYo(eQ%qfKg^AzT?q|pO*{4kq|+>N6f54=ZZi7!r8Mp zkIRqq@=^?~=drOR+VDqKjxX4PI4%^%-9qG?mbDbe6{Q^8LdCfnP@LvMmOHF*lsCM- zgM+I(Kpd8FJ}^Ks&-LZk-bB4#Zi=(f-SZ%TFBSDCVITar_O?9qU7X!may21m9s<3ie~aDxza+I;QfOQxHmL%)UT80=GrYrSkE4#246lm4atO z=|7|D{)n;f#jz%Q*KpS{1V2STIz5XND^R=(dbAs>i}rXguNLMWm&jC-#>i3dzL{%> z3wZiM+*NS1rrq6<_KZo=@Fu0 zk*Om(S3`+~akR_&d{m0?=caf*r*rb0 zRNRqCx`@f$oS=s-{jxsfNCVIv-MaK}nD}$)E zvf<*gx!3Wz+o9TJO9!LgR$*GXj5-C?^aPx@Ruq0{N#mEXNIZ(q)_nXQ(UMAT9hICI zu8Wo_Rh3Cp)@FZYtk>tc-52(NCKAz2M9i6=w1&7X%4Gn@-rXPjh$CjZc+NAop=v$n zal5~gya+2@gc(OMSA4+`%wv_`isc@+1a*5ZCIq@_^I5UJNc-9Yhp$&F_-dRdjy>Z@ z&)xE*oMVn*lh+TGf4U_qjo4K)nsiTTu@q`$MZwRCkb@=&MunC**pBBwarrExVLv|Q z(!WO(AmIwBr&g)6r)2m#iXwZzBLCgGa+X!`7o;m}DZ#IeSxsE$9!X0S{MFfCoa{LX z%OqsMT5Fi&PRj+B_LMsik~3BL_aY*e=)GYmZ?;;Mx^kq+6&Ehw5?`ftQ=<+Lvk$rS z-0y3;Zg?Hnm0_el_m|4zXEviuNXu<_ryc|+rRm9Q(In?h&+%eEAE1gjYVuZ3k)<(a zm~edjqk5lkzd%(LWGHnDhaFOC;=2qfL`(SIK-hfhD;XkcN7paVXT+Ln%4aCffas_D z;*S*GdJ084*Wzs1Y$zb5x0PooTV*=(GF@joddwgut-*Q(QBZdOlvWIT>oH{ROqf0K@rHKr z4LdabEt}YADI4;P3|ULQ_cZ2XzRZ9EKq_GD9ghhzfiMLK{4kLK3(}9YTWu| zz=XRjtWotFEo*X;{&2gYv_?%W55}v363bCI-N>2nFUjV^{49572h3X7b|i&!3i?|X zFVwg{@=Kmrm1mCjk*O!o6Ft`V2GeIa6jI zX?=0(Hf)~U+C-Fp$g~hWgB_CxRIOQDpXJrp&8y#}I-~Xo!O3$nm-m^S3N>U|VXEP}I14~--(h?O$9?r~7FRm6GuErz^op`U?0xVg+|{5X z8^Zk@T2!CeNKWvJTv-HK*Y!u~MpHC9-CRhYVn0wxlOzjRy8>FT9ksOaeBnFW#Ukdu z*2vOF6WRnc85H6T0Q+i*du0JV0s7rG)d33#DY0E08%bYmNjBZ44tp-uKLIE{8oSOa z8?&v%6%rCDOnVOQHpR|Pe=egce#%egmL&72$eq zzh{pGiAy2&>Dt)LAVHmreT#@Jj#KoEw*qGy(fA?F*bynH`0{IYm8r`p# z!;f`-^*1YYQ7*sgD^6TLGQ#hMDi;N>IdaPcCzA zt%N+qg2WEdN;t4AI0NoP{0!HL*52Yn%5AhV3UkDvhCAIc3r4eZZwt1iy~1x8Zt?DS z>)`?jHlef9Rr9;fDY zb!>evHN$?uNlSxsD=^;@89XH)q0__3{asuR|BoVWF z9-YAwJim7^G3A7_BEAKoRbk4`ZtX6hTd*y@A`o}bpgfw@5Ft=OmZLukE^U#P+t!$< zuy=vJxPi7sc3r^W8$w(ZP>YJ_*S!JLQF6QP&V7T;r5_Iz%QojEJjLjgEWX-eDDy`4 z$>+bI=dmWr?Jj-#tb;$4OonJoVIOO&7Pm)dLbx2JR@rM37VxRhuZ_&HasB-gq|U0S zy66+vx~ir_xHj5%tOLq`_2cs=E+}^Fle-L6!LTOoIK3hiuyuT>gO~7780iEKs;{%u z&E3J}@oXcUY&-pEgU+evg-&K-+fcx|ku?Q4w<@-_yIH_w!Sp9bkN4@Sxd(7VAVcc$ zJ00wkoENC{pGObR!^=XiwD~DNBi77l>VnYhKXI_aJis}&MhHuLRkZxLJd?-?c4hK; zu`ciI)wIIYfU%bh7g4X_YFNh?T}_`ZuI|vjgUOk8zc#~2U=IMn};c8{+UB zJ$ylAEpm_q7EBc45YxquKS2IKsS{x826%&~d7G^Tbsn)XVlV>6@f zCQX_cy$v7pTuIxWHjV`Qi$Rxf`9*|3fYD{#jfrwEM?{h0UiW}X&?mDG>btAYdQw7@vf*&Bxvtdg|Wd2_9** z0~GM5N!iS!DZbk&U0dbg{Wxb79Q(>#>>TfoZ~ zYR8D+s8r;O3?)HkH*iQvH^`r&-L%Wxv=IUb-Zzc(lNHy$D`DB;7uBySN49Q&PN#Vcts8AOZxP>RynmMO=$;kX(>&wVM+jpnO@1n9t^75|nT7YmsK z`Ejl-Xu9IdpKkl%LJe`1jigpEZuhoNCfFwlchoxCS003c1FNJ}Rf#^VKOKVX?gEro zia6X5?~bRYs{&NPZ7hO{UDe%J0C|GaHTNAhR7f10MHBNX6S{B8UMAxyLmz0)<;CXO z9@3kh=y2w;GQ}z&k+-pwd8wK*O*1z_`5-GHM(;)L4HFbC^-_}5X zB|NY~1^D9geuauUm)rV&E3d>{k|wslW_{T`T46ujBO{1RnzFpb+bZ2f-)=wX=u-Y& zt}-L!n&9*CeZRKn{UXH0e!@eaz}`ah)nvEggnso#n{>M2)Haje932Z`ZWJu98hoL2 z%=XHLOQ@$kpUbe8P~I`|nr8*sRk=BY!$)KVEK&BQB7c<#v$e2=V)KxNt9D^){)f~ zD>!?s>Y+TkI<@?mz2McUgW{-|S9bDyFX(%)8ync-hOsX`^wl4=8``mNl(!6h6h{?K zHnq&Ipy~D3?2i@DlJvIhKOOsi`EK27sU;p7Oi3p}x@3!9SD(LfjVMy3Zo?gZqNW5HgF}#siB(F?1{G|l9kJa|UPnySdQhxXu644) zHJ%$$3w-g`PNbS)%o96vn?RN&(uY&HzRlq5BH^ZRnMF_9=Yz0vOJLD)$J*=A9%qsw zggN&1eRmQSj_fyZP^;!7B#_)4M89I2n;NW=XV?QB){I`m9;nj@3Pv#pnPdeA*_&9@TK6H=Oc18IW06z!F6zN zRjg!@!r-Mk-Vj83W!Qz98rpw`HjX5}L-q|OsQgO-@bOvTk*s!*1ugHp@z1;;-6zvB z)9kxCy`XENu|qfq2Ng!D-|{S=2F)sRs(7pGK6hO>myHN5`h;cMOeT2|O!1P`!Z8L` z%v)g>R>%Z)e}JJGqPD<8e0oQ3I*~Z9!J|!2KVPHEs~0GqEY=(M1=5Kkw_Qg|kG8ZL zwg(S`o%7h$akT-eFY*&%`ua%-MFXzD1>d^0>Hae-zYSWS{SCO53S3X$LwW_A!&2Ms@d-3c5{NH5ji;Y9ytacu`m+8 zR;^B;A&WZyS!HnN9v=2^R%z}z3tVueK|CI39$$j(8<)mGED{Wg7G0?gh@w!YVEm9H z25^Krr0}QEqf$mFnDAp87)fK=%Z3`<+M_$f7uFk07|{$gS60pA>Ua$&vA*G%p)@CY zrWyyscD84-FHgKmWJaLVj$zmZn%~X3Ur%b`1_rQc6ja&G7#d5?3G+;%wp^(6BJGv! zy$X%f#r2U93h}7$GkoUTB zzf@iCU$X2fHXpvt%vn#&d?w*oV#4@VmvhF|=zZ*C%P>amnO32Qfan8>`BL_RIu)ml zBEsQCRreCmO`L+Yz7F8|{T;P%Ia%_7JT|*zh)%v#06e z^Vsf_;seb1K(%u%^ZDmnv@FzPh*(Qbr2G_Za=-z5R7-vF`o-vSVnxh& zD&fWeCkaYh6#c7wgP6*E*n>!b0NMzm3-%8jv1<^?Zh&lo+ICLiTDIPOSSBINgja7! z9tLaeDU+!p=(I?~Z%>Rc{Q7bNsuTyR)b3(w>ikf;U((Rdo=)aq19(O21S@@MK9jg+kQUEaK_UY_@N7ZLj4I^AwasK8bI^lJgNsZ&?G$^jPU z4m_Cwm$nVOB1)uXY1PlF@-I>{bWXVZO~+;SOslFreflFz!_)VUOvb1SY0?u|Oi%>e z?~HPpggOu3`gl!q%CXouEaX?1q!Iv#0I89;co4>l&P~Amn<7J}kd}kw847i6c6X22 zIik?Ez2z!7PD2NU>oF@F|Ed6#kVAG`5t)p1RcS_?`4$E&7nalHmLXfu0Cj>mB=gO8 z-ODwzEaQdK*X4{zto+s7DR_^^TOg8hXTR**HV#Sv#nFUP!kMC3PdSZ>ZD$KcT$mJu zI7MEm6V=M$oNl@1T67 z8^q;acdE==L)JX#1`nL#R7F~+6A87nE=MXRl$Sg52XenLNiz&FZngB}3urc7CyP`( z6tAvNS1-z^dR2~>BzM+iBup!LKCp}T$%}WeUx;`}l&^rP>TwvGh&p1y8vn)=+R9we z0ZXP;Sk`jAasa#z+$J9-z6Z)Cf`EadWF?}PjHm;;uAwZM2%Clp>ET5^hh?af-T?NV z#8_yM+*>@gy&U`Z(w!vh=?KsZ_`M%!YokIP^2L%}-Jm(8K4wqptXJaY$wGvaOi)vR zPDWvqfR0Tq@O7;nG)|EKcb9BrM-$uSd5b{@AxDNxkB&8s66CsiJQuT~?Q zO>t!_z7ZP66`#SYp>%{VQ2cBLx$iBy|2^xRt`}M32}K|LYThG5EM)3*1GBs%$S2M( zgE|NJxrJ9h%P3g6JDSOSjONP@=oLUy(Pd~mH`7fhxMxK+7;X+yT9@+OcI-Uy}RQ_si-G z^rH_UPn5lA?@FkZF+BI4%NO5$*fVhb-*?6O%n4NpS#{hGD^%+HPwO`T4hb~RwGpSa;wrYEQ z#Pm(&qu7(S$s>V2uVZeDbpi1Z;wqLc+_Kcew9<|F%_RV}$03S(ZR(_pO5E4i=MqLW z#*r275M`HE+foP3Z&)o9>J`wWpP#3+QEj^!Zed<{%)1$=uB)v_9T;pIvM^V-COs$?t|hw{}T-=J8FYkAKwa+2JFS4@E)u5MVv->WEfFpkr|{> zFzsYrmNuFE0m?gal2>73ofc{;dxs7YW(t|0-JmF7SEzR(b5S4JZ?{_oJCgK01M4khME&^!TW;Ylno0B)#CEW9h=z6-}mz zSVp&QRXK3yyq`V^oa(@1X}p)08_~QyakYQVYcMn+GY-y}1mNgE7x`SzjFRPrGf!!` z5k?;hjEt+{Pw_ust&nfAba_pCz7wf6Q}9{BDmg%OU9o;r;9zR>t!D=$296$H^12!@OCbR^L&r$dx*j?O)UT#dlfXoXBHpA5@;x&R zkztjcaBvS{qkfi-xi&&KF%f&UP*5*9&Ed}4J2S9u*!KR;-#U#@d1YiJZ6kZ~@atzo zo|N>*xLHR0Ed85oUs=W0dC^+LcH8}i{+eD#Gap5vgC;NFzm;02`t|+r@1PTVX=?2H zi?XR|);2`y*mrg>7V1C9C%im%S(zFyIx_CCPH36vlbYqQla@1n`p$L6#>mQSq5e~t zn;S=ceeHV*X~>4)WC1ENHbo0dGAyn1nQJmSf-sU&75Fd`$r~Y3vLnbaK}pH>f)LO- zjU%3SDi(zu&J(wX+^hT-na;pLz|FA;nA?W8|I^7#sn1?)@$;<4;*R7qn8PMdY9$9~ z635yPQ`MODYQ&NDZJ*fhng%B1dvIstv$Gg)OXm{p4ewvop+_V-9R=uxXUv35ci5`) zycetJa5nUAil0<=T&fvuknW#|K8&t99NH9u^H-cHTg2b=VYP=7rQ=;lt3QitpXRpv zR2XmQCa@Lt^=YJRf2|F)kAKqBvsn*wJ;og#)FK4}d5gHm88r3r- zmX*Nut(TRsM{8N2ppkX1d_9zFXoBV}6mvJ`J)mmT#L&);tvkZe?BetZq25{%BiKEW zGVZ10HC;G!#0K;nklBc880fM$RBM!RNeyzG1_Zo%*5}H5tg(vTYi5J4p=<4npapf~ zE<=K@(bdvEQfFO=JdLI`%}`SYBPygr8au3QDk4^lqzo%STVoXDSu^e;Hn8z33)Lv2 zo1VKb%}tPciVrw5jz+*5@%p_jXPJtca5H(0u7P?K1*cW?>YI@XG?7l3H8m-b=BT{Q z?fH@Urovj~T0&2gDWDb?AaC7;Sn8CzMcY7*{)1fu5P6;WGiFI~Ah~tTv(0jC>$=yd zUnwqq(%WF_;bE;fg;PY&K)GY+h^Tu11 zWUBF6OO!IU!u$?pMwZ=)BtsUh0q6lJl^JCSQPrNa`D7I;U$^375eZ;57t~NbhG@^Dh)qmr|A$FN%0C>&Q=& zD1y#4aqQum3XAcWQ>NN?C<}J?@V)4RijQ1jdN@irMuhl^H@F#%CRvM~^S_QsneDM@ z0@O2>y`0Xt-4Skfh&lue;Zao6`pD#z^3262h)DoSAiOeOHx~2o_G0sPalQh+bWK`g zH^)}hiQRT2Ue*1)4fuf7j^VCab=BD&(tjmdxXbjk46mKi0qBSQ0#$&|jwgSe*_dUe zu5bl|!IQ!&GHBfBW#?@uF0jlY`E2hoZ@?v)w)=b?VykkSV^+y?cU+T$^9xEJ@|crB zvUY|H8C4Bf30rF&g;CEx@-*|ob5QpS5 zL7Sgq57RVV(3b#QkO{=-OZM%iBY;d2UaHYWCRnt??lkeb*_a!whH*c7{e)30dbFFo z7vWC-u?=UVs~^`Kv2DyiUMaqXvqly{YESwl*1TML0NTL*&jXq_rDr6(TdwB)r^!Grz1`oQC`AS$p)3?;@_@+?ymL~CW(94`c4mJ+{>_%t6J#4#a35~|HERVc z`$6vEl6KE4{t8j3aU;x$qo&pUXl=)0$(G3}{wgm{VuwM24@^!(-G;Tn{p1qVB!&wq zbwq`ky|KI~n~A*1{gm9{X6SB%C~*#hL0mf%jD4P`9B|t-&K>eX&(`@1YZ9p1p&YS+ z2JhORQ*kYnuX**U+Qbs5OD&plpUjra@&Jm-Q7&=k*Kr$eBtFN&-7)t06q%6RY!Q=A z^6(AJDKLpmvPc1p!Uaajy|afeF%nLRFi#J-x*z{&a$+h%^iuNTjIvT9V)Dvjsw&W6phw*Qb_10T zS5OA}I~3@@SU@>1er$|?+U;{WJM>TAMhERY*@(C*SJf&;V;f7x?^L#hoXlNqLB-ZVLU^VQznG~0-16Jc2^LPEIZ7sIj`FaK^38J5 ze#iNx3hmF%(fIt#q(IIyKr`d*jChyK`1Zn8gM%2_&t1g*^fLj^_3pCB`f z6LdR4NLo-)@*kVI_2dtbf3w^SolO6{nJ&w+er}+^=LT`Pe&|{p&=>Tdga7Zi`M>rm zO85sgJP=9~6hJ?Q=?&E% zddlRx>*BvS|6cg{L4^C8oq_g7eu}vNa{ixy`xnIbDyKh2Sq9$^i2qfu{8K&YpRY{c z3uiv)>U=YF;op${&l;*fbH5i$`|+MdCHe#RmjY@3EYkTi=X>3#2kEtMRw4cGoIey| z`x*MZjM0N6)i;Be{{i|xyAOXPQ1vtAd)1X6hYqOlOUnNw6aOCtl78lXFFo;~>GI8r zlz-3tSB00KQQw#9B>Mq{ul`%qzkI*Guj=@)koV10%zo+j56XW2{Mo;+bogV0`&;~m z@%LKJKUa(Vne%;u`9s~rZ#HfDXPkfOtnX8^AM(_{S+CbG0soY>{_`jQK5O{LQP}hT zwc~%!^ZuFf{kHW({_!^}_Wd2>U;5 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.21.11/gradlew.bat b/mod-runtime/fabric-1.21.11/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.21.11/settings.gradle b/mod-runtime/fabric-1.21.11/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..cbb3c52 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public Identifier getFabricId() { + return Identifier.fromNamespaceAndPath(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + Identifier ruta = Identifier.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..6b3c140 --- /dev/null +++ b/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.17.3", + "minecraft": "${minecraft_version}", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/mod-runtime/fabric-1.21.4/build.gradle b/mod-runtime/fabric-1.21.4/build.gradle index 8f77839..7afcb4b 100644 --- a/mod-runtime/fabric-1.21.4/build.gradle +++ b/mod-runtime/fabric-1.21.4/build.gradle @@ -31,6 +31,7 @@ processResources { mod_authors: project.mod_authors, mod_description: project.mod_description, minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, loader_version: project.loader_version, ] inputs.properties propiedades diff --git a/mod-runtime/fabric-1.21.4/gradle.properties b/mod-runtime/fabric-1.21.4/gradle.properties index 26a3050..6b6e1ff 100644 --- a/mod-runtime/fabric-1.21.4/gradle.properties +++ b/mod-runtime/fabric-1.21.4/gradle.properties @@ -3,6 +3,8 @@ org.gradle.daemon=false # Fabric API 0.110.5 para 1.21.4 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). minecraft_version=1.21.4 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.21.2 <=1.21.4 loader_version=0.16.9 loom_version=1.17.21 fabric_api_version=0.110.5+1.21.4 diff --git a/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json index e20091c..ff3c4eb 100644 --- a/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json @@ -15,7 +15,7 @@ }, "depends": { "fabricloader": ">=0.16.9", - "minecraft": "${minecraft_version}", + "minecraft": "${minecraft_range}", "java": ">=21", "fabric-api": "*" } diff --git a/mod-runtime/fabric-1.21.5/.gitignore b/mod-runtime/fabric-1.21.5/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.21.5/build.gradle b/mod-runtime/fabric-1.21.5/build.gradle new file mode 100644 index 0000000..ea90dc5 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 // Minecraft 1.21.5 funciona con Java 21 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano-1215/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.21.5/gradle.properties b/mod-runtime/fabric-1.21.5/gradle.properties new file mode 100644 index 0000000..f8aa153 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.128.2 para 1.21.5 (compatible con cualquier Fabric API posterior de esa versión). Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). +minecraft_version=1.21.5 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.21.5 <=1.21.10 +loader_version=0.16.10 +loom_version=1.17.21 +fabric_api_version=0.128.2+1.21.5 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.21.5/gradlew.bat b/mod-runtime/fabric-1.21.5/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.21.5/settings.gradle b/mod-runtime/fabric-1.21.5/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..b6680a5 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return ResourceLocation.fromNamespaceAndPath(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..615a5a7 --- /dev/null +++ b/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.16.10", + "minecraft": "${minecraft_range}", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 956b6e5..bc42abd 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -29,6 +29,8 @@ "fabric-1.20.1": (21, "9.8.2"), "fabric-1.21.1": (21, "9.8.2"), "fabric-1.21.4": (21, "9.8.2"), + "fabric-1.21.5": (21, "9.8.2"), + "fabric-1.21.11": (21, "9.8.2"), } @@ -65,7 +67,7 @@ def _gradle(proyecto: Path) -> list[str]: return [str(local)] if local.is_file() else [str(envoltorio)] # el de .herramientas si está; si no, el del proyecto -def construir(nombre: str, sin_pruebas: bool) -> int: +def construir(nombre: str, sin_pruebas: bool, propiedades: list[str] | None = None) -> int: proyecto = RAIZ / "mod-runtime" / nombre jdk = _jdk(PROYECTOS[nombre][0]) entorno = {**os.environ, "JAVA_HOME": str(jdk), "PATH": str(jdk / "bin") + os.pathsep + os.environ["PATH"]} @@ -76,11 +78,14 @@ def construir(nombre: str, sin_pruebas: bool) -> int: entorno["JAVA_TOOL_OPTIONS"] = f"{entorno.get('JAVA_TOOL_OPTIONS', '')} -Djdk.net.hosts.file={hosts}".strip() if HERRAMIENTAS.is_dir(): # la caché de Gradle y del loader queda junto al proyecto y no en la carpeta del usuario entorno.setdefault("GRADLE_USER_HOME", str(HERRAMIENTAS / "gradle-home")) - orden = [*_gradle(proyecto), "build", "--no-daemon", "--console=plain"] + # Con propiedades (`-P`) solo se comprueba que el mismo código compila y pasa sus pruebas con otra versión + # de Minecraft: no se toca ningún jar. + orden = [*_gradle(proyecto), "test" if propiedades else "build", "--no-daemon", "--console=plain"] + orden += [f"-P{p}" for p in propiedades or []] if sin_pruebas: orden += ["-x", "test"] codigo = subprocess.run(orden, cwd=proyecto, env=entorno, check=False).returncode - if codigo: + if codigo or propiedades: return codigo jars = sorted( j @@ -103,10 +108,14 @@ def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("proyecto", nargs="?", default="todos", choices=[*PROYECTOS, "todos"]) ap.add_argument("--sin-pruebas", action="store_true", help="compila sin correr las pruebas de Java") + ap.add_argument( + "--propiedad", action="append", metavar="CLAVE=VALOR", default=[], + help="prueba el mismo código con otra versión (p. ej. minecraft_version=1.21.8); no genera jar", + ) # fmt: skip args = ap.parse_args() elegidos = list(PROYECTOS) if args.proyecto == "todos" else [args.proyecto] for nombre in elegidos: - if (codigo := construir(nombre, args.sin_pruebas)) != 0: + if (codigo := construir(nombre, args.sin_pruebas, args.propiedad)) != 0: return codigo return 0 diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index b59b618..00d3b2b 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -20,9 +20,22 @@ ("forge", (1, 20, 1)): "forge-1.20.1", ("neoforge", (1, 21, 1)): "neoforge-1.21.1", ("fabric", (1, 19, 4)): "fabric-1.19.4", + # Un jar sirve para varias versiones cuando el mismo código compila y pasa las pruebas contra cada una de ellas + ("fabric", (1, 20, 0)): "fabric-1.20.1", ("fabric", (1, 20, 1)): "fabric-1.20.1", + ("fabric", (1, 20, 2)): "fabric-1.20.1", + ("fabric", (1, 21, 0)): "fabric-1.21.1", ("fabric", (1, 21, 1)): "fabric-1.21.1", + ("fabric", (1, 21, 2)): "fabric-1.21.4", + ("fabric", (1, 21, 3)): "fabric-1.21.4", ("fabric", (1, 21, 4)): "fabric-1.21.4", + ("fabric", (1, 21, 5)): "fabric-1.21.5", + ("fabric", (1, 21, 6)): "fabric-1.21.5", + ("fabric", (1, 21, 7)): "fabric-1.21.5", + ("fabric", (1, 21, 8)): "fabric-1.21.5", + ("fabric", (1, 21, 9)): "fabric-1.21.5", + ("fabric", (1, 21, 10)): "fabric-1.21.5", + ("fabric", (1, 21, 11)): "fabric-1.21.11", } #: Combinaciones que no tendrán mod de apoyo y por qué (para decírselo al jugador en vez de «todavía no»). IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 7f7e31e..8980ef4 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,8 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, 1.20.1, 1.21.1 y 1.21.4." + "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, " + "de 1.20 a 1.20.2 y de 1.21 a 1.21.11." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 8b7d94e..fe1b208 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -116,6 +116,18 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("1.20.1", "fabric", "fabric-1.20.1"), ("1.21.1", "fabric", "fabric-1.21.1"), ("1.21.4", "fabric", "fabric-1.21.4"), + ("1.20", "fabric", "fabric-1.20.1"), # un jar sirve para varias versiones vecinas + ("1.20.2", "fabric", "fabric-1.20.1"), + ("1.21", "fabric", "fabric-1.21.1"), + ("1.21.2", "fabric", "fabric-1.21.4"), + ("1.21.3", "fabric", "fabric-1.21.4"), + ("1.21.5", "fabric", "fabric-1.21.5"), + ("1.21.8", "fabric", "fabric-1.21.5"), + ("1.21.10", "fabric", "fabric-1.21.5"), + ("1.21.11", "fabric", "fabric-1.21.11"), + ("1.20.3", "fabric", None), # sin jar probado para esa versión + ("1.20.4", "fabric", None), + ("1.21.12", "fabric", None), ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión ("1.21.4", "neoforge", None), ("1.20.1", "neoforge", None), From e45d59585a20262697cb465c7c29443c10afed5d Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 18:25:05 -0500 Subject: [PATCH 15/24] feat(runtime): mod de apoyo para Fabric de 26.1 a 26.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minecraft 26.x ya no está ofuscado: el proyecto usa Loom 1.19 sin remapeo, Java 25 y `Screens.getWidgets` (antes `getButtons`). El mismo código compila y pasa las pruebas contra 26.1, 26.1.1, 26.1.2, 26.2 y 26.3, y ese rango queda declarado en el jar y en `SOPORTADOS`. 26.4 no se incluye: aún solo tiene snapshots. El CI instala también Java 25 para compilarlo. Las pruebas del reescritor para 1.21.5 en adelante ya no usan `ChatFormatting.getColor()`, que desaparece en 26.2. El código compilado sigue sin probarse dentro del juego. --- .github/workflows/ci.yml | 10 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- README.md | 4 +- .../consterna/modpackes/ReescritorTest.java | 5 +- mod-runtime/fabric-26.1/.gitignore | 7 + mod-runtime/fabric-26.1/build.gradle | 62 +++++ mod-runtime/fabric-26.1/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-26.1/gradlew | 248 ++++++++++++++++++ mod-runtime/fabric-26.1/gradlew.bat | 112 ++++++++ mod-runtime/fabric-26.1/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 ++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ scripts/construir_mod.py | 3 +- src/traductor_es/salida/mod_apoyo.py | 5 + src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 5 + 19 files changed, 654 insertions(+), 11 deletions(-) create mode 100644 mod-runtime/fabric-26.1/.gitignore create mode 100644 mod-runtime/fabric-26.1/build.gradle create mode 100644 mod-runtime/fabric-26.1/gradle.properties create mode 100644 mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-26.1/gradlew create mode 100644 mod-runtime/fabric-26.1/gradlew.bat create mode 100644 mod-runtime/fabric-26.1/settings.gradle create mode 100644 mod-runtime/fabric-26.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf0b5d9..e54f414 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,14 @@ jobs: distribution: temurin java-version: "17" # ForgeGradle 6 (Forge 1.19.2 y 1.20.1) pide Java 17 - # Con las dos versiones instaladas (17 aquí y 21 más arriba), el script elige la que pide cada mod - # (Forge 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y todos los Fabric: Java 21) por las variables JAVA_HOME_17_X64 y JAVA_HOME_21_X64. + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + distribution: temurin + java-version: "25" # Minecraft 26.x (Fabric 26.1) pide Java 25 + + # Con las tres versiones instaladas (17 y 25 aquí, 21 más arriba), el script elige la que pide cada mod + # (Forge 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25) + # por las variables JAVA_HOME_17_X64, JAVA_HOME_21_X64 y JAVA_HOME_25_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fd5ba..1445086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a90b06..6428e55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,10 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21): +Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: ``` -uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11] +uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). diff --git a/README.md b/README.md index f4581d4..52475d6 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2 y de 1.21 a 1.21.11. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java index eef0937..eee0890 100644 --- a/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java +++ b/mod-runtime/comun/reescritor-plano-1215/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -50,8 +50,9 @@ void recorreLosHermanosYSoloCambiaLosQueCoinciden() { raiz.append(Component.literal(" Steve")); Component nuevo = Reescritor.reescribir(raiz, TABLA); assertEquals(" Hola Steve", nuevo.getString()); - assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); - assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + // los colores se comparan con los del original: ChatFormatting.getColor() desaparece en Minecraft 26.2 + assertEquals(raiz.getStyle().getColor(), nuevo.getStyle().getColor()); + assertEquals(raiz.getSiblings().get(0).getStyle().getColor(), nuevo.getSiblings().get(0).getStyle().getColor()); assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia } diff --git a/mod-runtime/fabric-26.1/.gitignore b/mod-runtime/fabric-26.1/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-26.1/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-26.1/build.gradle b/mod-runtime/fabric-26.1/build.gradle new file mode 100644 index 0000000..1573081 --- /dev/null +++ b/mod-runtime/fabric-26.1/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'net.fabricmc.fabric-loom' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + implementation "net.fabricmc:fabric-loader:${project.loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 25 // Minecraft 26.x pide Java 25 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano-1215/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-26.1/gradle.properties b/mod-runtime/fabric-26.1/gradle.properties new file mode 100644 index 0000000..b3c516a --- /dev/null +++ b/mod-runtime/fabric-26.1/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.145.1 para 26.1. Desde 26.1 Minecraft no está ofuscado: Loom 1.19 sin remapeo y Java 25. +minecraft_version=26.1 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=26.1 <=26.3 +loader_version=0.18.4 +loom_version=1.19.4 +fabric_api_version=0.145.1+26.1 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-26.1/gradlew.bat b/mod-runtime/fabric-26.1/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-26.1/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-26.1/settings.gradle b/mod-runtime/fabric-26.1/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-26.1/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-26.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-26.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..5d1d35c --- /dev/null +++ b/mod-runtime/fabric-26.1/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getWidgets(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public Identifier getFabricId() { + return Identifier.fromNamespaceAndPath(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + Identifier ruta = Identifier.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e937c03 --- /dev/null +++ b/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.18.4", + "minecraft": "${minecraft_range}", + "java": ">=25", + "fabric-api": "*" + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index bc42abd..7c6e74f 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -2,7 +2,7 @@ Uso: uv run python scripts/construir_mod.py [ | todos] [--sin-pruebas] -Sin nombre compila todos. Cada uno necesita su JDK (Forge: 17; NeoForge y Fabric: 21) y su +Sin nombre compila todos. Cada uno necesita su JDK (Forge: 17; NeoForge y Fabric: 21, y Fabric 26.x: 25) y su Gradle, que se toman de JAVA_HOME (o JAVA_HOME__X64) y de `.herramientas/` si existen, y acceso a internet la primera vez, para bajar el loader y Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo construye el mismo comando @@ -31,6 +31,7 @@ "fabric-1.21.4": (21, "9.8.2"), "fabric-1.21.5": (21, "9.8.2"), "fabric-1.21.11": (21, "9.8.2"), + "fabric-26.1": (25, "9.8.2"), } diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 00d3b2b..3f11c54 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -36,6 +36,11 @@ ("fabric", (1, 21, 9)): "fabric-1.21.5", ("fabric", (1, 21, 10)): "fabric-1.21.5", ("fabric", (1, 21, 11)): "fabric-1.21.11", + ("fabric", (26, 1, 0)): "fabric-26.1", + ("fabric", (26, 1, 1)): "fabric-26.1", + ("fabric", (26, 1, 2)): "fabric-26.1", + ("fabric", (26, 2, 0)): "fabric-26.1", + ("fabric", (26, 3, 0)): "fabric-26.1", } #: Combinaciones que no tendrán mod de apoyo y por qué (para decírselo al jugador en vez de «todavía no»). IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 8980ef4..015a45b 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -470,7 +470,7 @@ def _opciones_avanzadas(self) -> QWidget: "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, " - "de 1.20 a 1.20.2 y de 1.21 a 1.21.11." + "de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index fe1b208..90f7cc8 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -125,6 +125,11 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("1.21.8", "fabric", "fabric-1.21.5"), ("1.21.10", "fabric", "fabric-1.21.5"), ("1.21.11", "fabric", "fabric-1.21.11"), + ("26.1", "fabric", "fabric-26.1"), + ("26.1.2", "fabric", "fabric-26.1"), + ("26.2", "fabric", "fabric-26.1"), + ("26.3", "fabric", "fabric-26.1"), + ("26.4", "fabric", None), # todavía solo tiene snapshots ("1.20.3", "fabric", None), # sin jar probado para esa versión ("1.20.4", "fabric", None), ("1.21.12", "fabric", None), From 0ff3a9c7de23863989bbc4922e2e39418efa61c4 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 18:58:08 -0500 Subject: [PATCH 16/24] feat(runtime): mod de apoyo para Fabric de 1.20.3 a 1.20.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos proyectos más: Fabric 1.20.4 (válido para 1.20.3 y 1.20.4, con el reescritor de contenidos planos y el tooltip de tres parámetros) y Fabric 1.20.6 (válido para 1.20.5 y 1.20.6, con el tooltip de cuatro parámetros). Con esto Fabric queda cubierto sin huecos de 1.20 a 1.21.11. Cada rango se comprobó compilando el mismo código y pasando sus pruebas contra la versión vecina. El código compilado sigue sin probarse dentro del juego. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- README.md | 4 +- mod-runtime/fabric-1.20.4/.gitignore | 7 + mod-runtime/fabric-1.20.4/build.gradle | 63 +++++ mod-runtime/fabric-1.20.4/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.20.4/gradlew | 248 ++++++++++++++++++ mod-runtime/fabric-1.20.4/gradlew.bat | 112 ++++++++ mod-runtime/fabric-1.20.4/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 ++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ mod-runtime/fabric-1.20.6/.gitignore | 7 + mod-runtime/fabric-1.20.6/build.gradle | 63 +++++ mod-runtime/fabric-1.20.6/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48749 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + mod-runtime/fabric-1.20.6/gradlew | 248 ++++++++++++++++++ mod-runtime/fabric-1.20.6/gradlew.bat | 112 ++++++++ mod-runtime/fabric-1.20.6/settings.gradle | 12 + .../consterna/modpackes/ModpackESRuntime.java | 135 ++++++++++ .../src/main/resources/fabric.mod.json | 22 ++ scripts/construir_mod.py | 2 + src/traductor_es/salida/mod_apoyo.py | 4 + src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 7 +- 27 files changed, 1269 insertions(+), 8 deletions(-) create mode 100644 mod-runtime/fabric-1.20.4/.gitignore create mode 100644 mod-runtime/fabric-1.20.4/build.gradle create mode 100644 mod-runtime/fabric-1.20.4/gradle.properties create mode 100644 mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.20.4/gradlew create mode 100644 mod-runtime/fabric-1.20.4/gradlew.bat create mode 100644 mod-runtime/fabric-1.20.4/settings.gradle create mode 100644 mod-runtime/fabric-1.20.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json create mode 100644 mod-runtime/fabric-1.20.6/.gitignore create mode 100644 mod-runtime/fabric-1.20.6/build.gradle create mode 100644 mod-runtime/fabric-1.20.6/gradle.properties create mode 100644 mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/fabric-1.20.6/gradlew create mode 100644 mod-runtime/fabric-1.20.6/gradlew.bat create mode 100644 mod-runtime/fabric-1.20.6/settings.gradle create mode 100644 mod-runtime/fabric-1.20.6/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1445086..bca9ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6428e55..0f731a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,10 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4 y 1.20.1 quedan compilados para Java 17, los de 1.21 para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: +Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: ``` -uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] +uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). diff --git a/README.md b/README.md index 52475d6..2ce60f5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/fabric-1.20.4/.gitignore b/mod-runtime/fabric-1.20.4/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.20.4/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.20.4/build.gradle b/mod-runtime/fabric-1.20.4/build.gradle new file mode 100644 index 0000000..5ce0d3e --- /dev/null +++ b/mod-runtime/fabric-1.20.4/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 17 // Minecraft 1.20.4 funciona con Java 17 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.20.4/gradle.properties b/mod-runtime/fabric-1.20.4/gradle.properties new file mode 100644 index 0000000..a1d0181 --- /dev/null +++ b/mod-runtime/fabric-1.20.4/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.97.3 para 1.20.4. Loom 1.17 corre con Java 21 (la 1.19 pide Java 25); el mod se compila a Java 17. +minecraft_version=1.20.4 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.20.3 <=1.20.4 +loader_version=0.15.1 +loom_version=1.17.21 +fabric_api_version=0.97.3+1.20.4 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.20.4/gradlew.bat b/mod-runtime/fabric-1.20.4/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.20.4/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.20.4/settings.gradle b/mod-runtime/fabric-1.20.4/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.20.4/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.20.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.20.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..bfcb994 --- /dev/null +++ b/mod-runtime/fabric-1.20.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return new ResourceLocation(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..9c935da --- /dev/null +++ b/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.15.1", + "minecraft": "${minecraft_range}", + "java": ">=17", + "fabric-api": "*" + } +} diff --git a/mod-runtime/fabric-1.20.6/.gitignore b/mod-runtime/fabric-1.20.6/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/fabric-1.20.6/build.gradle b/mod-runtime/fabric-1.20.6/build.gradle new file mode 100644 index 0000000..e99ab98 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" +} + +base { + archivesName = "modpackes_runtime-fabric-${project.minecraft_version}" +} + +version = project.mod_version +group = project.mod_group_id + +repositories { +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +processResources { + def propiedades = [ + version : project.mod_version, + mod_id : project.mod_id, + mod_name : project.mod_name, + mod_license: project.mod_license, + mod_authors: project.mod_authors, + mod_description: project.mod_description, + minecraft_version: project.minecraft_version, + minecraft_range: project.findProperty('minecraft_range') ?: project.minecraft_version, + loader_version: project.loader_version, + ] + inputs.properties propiedades + filesMatching("fabric.mod.json") { + expand propiedades + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 // Minecraft 1.20.6 funciona con Java 21 + it.options.encoding = 'UTF-8' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/fabric-1.20.6/gradle.properties b/mod-runtime/fabric-1.20.6/gradle.properties new file mode 100644 index 0000000..d277e37 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# Fabric API 0.100.8 para 1.20.6. Loom 1.17 corre con Java 21 (la 1.19 pide Java 25). +minecraft_version=1.20.6 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_range=>=1.20.5 <=1.20.6 +loader_version=0.15.6 +loom_version=1.17.21 +fabric_api_version=0.100.8+1.20.6 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9fa7f288283f81eec4952784c271abd7e0938dee GIT binary patch literal 48749 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/fabric-1.20.6/gradlew.bat b/mod-runtime/fabric-1.20.6/gradlew.bat new file mode 100644 index 0000000..3185a43 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/gradlew.bat @@ -0,0 +1,112 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. + +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel + +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/mod-runtime/fabric-1.20.6/settings.gradle b/mod-runtime/fabric-1.20.6/settings.gradle new file mode 100644 index 0000000..604d9a6 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/fabric-1.20.6/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/fabric-1.20.6/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..3787729 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,135 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes del juego (los de + * {@code tellraw}, los de los mods y la barra de acción) y tooltips cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas con + * {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla. Fabric API no tiene evento para los + * tooltips que no son de objetos ni para el texto que una pantalla dibuja directamente: esos no se tocan. + * + *

El chat de otros jugadores no se toca: Fabric API no permite modificarlo, y no hace falta. + */ +public final class ModpackESRuntime implements ClientModInitializer { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new Cargador()); + ClientReceiveMessageEvents.MODIFY_GAME.register(ModpackESRuntime::alRecibirMensaje); + ItemTooltipCallback.EVENT.register((pila, contexto, tipo, lineas) -> alCrearTooltip(lineas)); + ScreenEvents.AFTER_INIT.register((cliente, pantalla, ancho, alto) -> alIniciarPantalla(pantalla)); + } + + private static Component alRecibirMensaje(Component mensaje, boolean barraDeAccion) { + try { + Component nuevo = Reescritor.reescribir(mensaje, tabla); + return nuevo != null ? nuevo : mensaje; + } catch (RuntimeException | LinkageError e) { + avisar(e); + return mensaje; + } + } + + private static void alCrearTooltip(List lineas) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(Screen pantalla) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (AbstractWidget widget : Screens.getButtons(pantalla)) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador implements SimpleSynchronousResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return new ResourceLocation(ID, "textos"); + } + + @Override + public void onResourceManagerReload(ResourceManager recursos) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + tabla = total; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", total.tamano(), total.plantillas()); + } + } +} diff --git a/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..7f68351 --- /dev/null +++ b/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": ["${mod_authors}"], + "contact": { + "sources": "https://github.com/Consterna/ModpackES" + }, + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": ["io.github.consterna.modpackes.ModpackESRuntime"] + }, + "depends": { + "fabricloader": ">=0.15.6", + "minecraft": "${minecraft_range}", + "java": ">=21", + "fabric-api": "*" + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 7c6e74f..cab4d59 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -27,6 +27,8 @@ "neoforge-1.21.1": (21, "9.2.1"), "fabric-1.19.4": (21, "9.8.2"), "fabric-1.20.1": (21, "9.8.2"), + "fabric-1.20.4": (21, "9.8.2"), + "fabric-1.20.6": (21, "9.8.2"), "fabric-1.21.1": (21, "9.8.2"), "fabric-1.21.4": (21, "9.8.2"), "fabric-1.21.5": (21, "9.8.2"), diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 3f11c54..09921db 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -24,6 +24,10 @@ ("fabric", (1, 20, 0)): "fabric-1.20.1", ("fabric", (1, 20, 1)): "fabric-1.20.1", ("fabric", (1, 20, 2)): "fabric-1.20.1", + ("fabric", (1, 20, 3)): "fabric-1.20.4", + ("fabric", (1, 20, 4)): "fabric-1.20.4", + ("fabric", (1, 20, 5)): "fabric-1.20.6", + ("fabric", (1, 20, 6)): "fabric-1.20.6", ("fabric", (1, 21, 0)): "fabric-1.21.1", ("fabric", (1, 21, 1)): "fabric-1.21.1", ("fabric", (1, 21, 2)): "fabric-1.21.4", diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 015a45b..eb295c3 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -470,7 +470,7 @@ def _opciones_avanzadas(self) -> QWidget: "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, " - "de 1.20 a 1.20.2, de 1.21 a 1.21.11 y de 26.1 a 26.3." + "de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 90f7cc8..4d4366c 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -130,8 +130,11 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("26.2", "fabric", "fabric-26.1"), ("26.3", "fabric", "fabric-26.1"), ("26.4", "fabric", None), # todavía solo tiene snapshots - ("1.20.3", "fabric", None), # sin jar probado para esa versión - ("1.20.4", "fabric", None), + ("1.20.3", "fabric", "fabric-1.20.4"), + ("1.20.4", "fabric", "fabric-1.20.4"), + ("1.20.5", "fabric", "fabric-1.20.6"), + ("1.20.6", "fabric", "fabric-1.20.6"), + ("1.19.3", "fabric", None), # sin jar probado para esa versión ("1.21.12", "fabric", None), ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión ("1.21.4", "neoforge", None), From 589e494c0debc7fe0aa0a8a71a2a6e684f47b83a Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 19:32:24 -0500 Subject: [PATCH 17/24] feat(runtime): Quilt usa los mods de apoyo de Fabric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quilt carga los mods de Fabric con Quilted Fabric API, que porta los cuatro módulos que usa el mod (mensajes, pantallas, objetos y recarga de recursos). La app instala el jar de Fabric en instancias de Quilt de 1.19.4, 1.20 a 1.20.2, 1.20.4, 1.20.6, 1.21 y 1.21.1, y el estado avisa de que no se ha probado en Quilt. Para que Quilt pueda cargarlo, los mods de Fabric piden ahora esos cuatro módulos en lugar de «fabric-api» y ya no exigen un fabricloader alto, porque el mod no usa su API. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- .../fabric-1.19.4/src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-1.20.1/src/main/resources/fabric.mod.json | 5 ++++- .../fabric-1.20.4/src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-1.20.6/src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-1.21.1/src/main/resources/fabric.mod.json | 7 +++++-- .../src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-1.21.4/src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-1.21.5/src/main/resources/fabric.mod.json | 7 +++++-- .../fabric-26.1/src/main/resources/fabric.mod.json | 7 +++++-- src/traductor_es/salida/mod_apoyo.py | 10 ++++++++++ src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 12 ++++++++++++ 15 files changed, 70 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bca9ba8..f5f5037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f731a8..55e7c57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,7 @@ Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1 uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` -Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). ## Licencia de las contribuciones diff --git a/README.md b/README.md index 2ce60f5..29bd888 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json index b264b08..af845f1 100644 --- a/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.19.4/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.14.21", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_version}", "java": ">=17", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json index fd9841c..41d694d 100644 --- a/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.20.1/src/main/resources/fabric.mod.json @@ -17,6 +17,9 @@ "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=17", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json index 9c935da..41d694d 100644 --- a/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.20.4/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.15.1", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=17", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json index 7f68351..0eb5416 100644 --- a/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.20.6/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.15.6", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=21", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json index 97825e2..0eb5416 100644 --- a/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.1/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.15.11", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=21", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json index 6b3c140..210482c 100644 --- a/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.11/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.17.3", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_version}", "java": ">=21", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json index ff3c4eb..0eb5416 100644 --- a/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.4/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.16.9", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=21", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json b/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json index 615a5a7..0eb5416 100644 --- a/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-1.21.5/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.16.10", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=21", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json b/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json index e937c03..c6e02e9 100644 --- a/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json +++ b/mod-runtime/fabric-26.1/src/main/resources/fabric.mod.json @@ -14,9 +14,12 @@ "client": ["io.github.consterna.modpackes.ModpackESRuntime"] }, "depends": { - "fabricloader": ">=0.18.4", + "fabricloader": ">=0.14.0", "minecraft": "${minecraft_range}", "java": ">=25", - "fabric-api": "*" + "fabric-message-api-v1": "*", + "fabric-screen-api-v1": "*", + "fabric-item-api-v1": "*", + "fabric-resource-loader-v0": "*" } } diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 09921db..c6f1a48 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -46,6 +46,11 @@ ("fabric", (26, 2, 0)): "fabric-26.1", ("fabric", (26, 3, 0)): "fabric-26.1", } +#: Quilt carga los mods de Fabric con Quilted Fabric API, que existe para estas versiones (no para 1.20.3 ni 1.20.5). +_VERSIONES_DE_QUILT: tuple[tuple[int, ...], ...] = ( + (1, 19, 4), (1, 20, 0), (1, 20, 1), (1, 20, 2), (1, 20, 4), (1, 20, 6), (1, 21, 0), (1, 21, 1), +) # fmt: skip +SOPORTADOS.update({("quilt", v): SOPORTADOS[("fabric", v)] for v in _VERSIONES_DE_QUILT}) #: Combinaciones que no tendrán mod de apoyo y por qué (para decírselo al jugador en vez de «todavía no»). IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { ("fabric", (1, 19, 2)): ( @@ -96,6 +101,11 @@ def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: if destino is None: return [f"{_sin_mod(instancia)} El texto del código Java seguirá en inglés."] lineas = [f"Hay mod de apoyo para esta instancia ({destino})."] + if instancia.loader == "quilt": + lineas.append( + "Es el mod de Fabric: Quilt lo carga con Quilted Fabric API (necesita que esté instalada) y no se ha " + "probado en Quilt." + ) lineas.append( "Esta copia de la aplicación lo trae." if jar_empaquetado(instancia, carpeta_jars) diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index eb295c3..3c02e03 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -470,7 +470,7 @@ def _opciones_avanzadas(self) -> QWidget: "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, " - "de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3." + "de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. En Quilt usa los mods de Fabric." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 4d4366c..f5b602e 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -135,6 +135,11 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("1.20.5", "fabric", "fabric-1.20.6"), ("1.20.6", "fabric", "fabric-1.20.6"), ("1.19.3", "fabric", None), # sin jar probado para esa versión + ("1.20.1", "quilt", "fabric-1.20.1"), # Quilt carga los mods de Fabric + ("1.21.1", "quilt", "fabric-1.21.1"), + ("1.19.4", "quilt", "fabric-1.19.4"), + ("1.20.3", "quilt", None), # no hay Quilted Fabric API para esa versión + ("1.21.4", "quilt", None), ("1.21.12", "fabric", None), ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión ("1.21.4", "neoforge", None), @@ -236,3 +241,10 @@ def test_cli_cobertura_en_json(tmp_path: Path, capsys: pytest.CaptureFixture[str salida = capsys.readouterr().out datos = json.loads(salida) # solo JSON: sin cabecera ni líneas de texto assert any(h["categoria"] == "java" and "You can't use chat" in " ".join(h["ejemplos"]) for h in datos) + + +def test_estado_en_quilt_avisa_que_es_el_mod_de_fabric(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1", "quilt") + with Instancia(juego) as inst: + texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) + assert "fabric-1.20.1" in texto and "Quilted Fabric API" in texto and "no se ha probado en Quilt" in texto From 5f31438adcaa4fb7d25df34a7c21347d2a6e00bb Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 20:54:46 -0500 Subject: [PATCH 18/24] feat(runtime): mods de apoyo para NeoForge de 1.21 a 1.21.11 y NeoForge 1.20.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tres proyectos nuevos de NeoForge: 1.21.4 (que cambió el evento para registrar la recarga de recursos), 1.21.5 (válido también para 1.21.8 y 1.21.10) y 1.21.11 (que renombra ResourceLocation a Identifier). El de 1.21.1 ahora vale también para 1.21 y 1.21.3, comprobado compilando y pasando las pruebas contra esas versiones. NeoForge solo publicó esas versiones en la serie 1.21. NeoForge 1.20.1 es un fork de Forge 1.20.1 y usa el jar de Forge; el estado de la app avisa de que no se ha probado. El mínimo de NeoForge de cada jar sale ahora de `neo_version_minimo` para poder declarar rangos. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 4 +- README.md | 4 +- mod-runtime/neoforge-1.21.1/build.gradle | 1 + mod-runtime/neoforge-1.21.1/gradle.properties | 4 +- .../templates/META-INF/neoforge.mods.toml | 2 +- mod-runtime/neoforge-1.21.11/.gitignore | 7 + mod-runtime/neoforge-1.21.11/build.gradle | 85 ++++++ .../neoforge-1.21.11/gradle.properties | 17 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + mod-runtime/neoforge-1.21.11/gradlew | 251 ++++++++++++++++++ mod-runtime/neoforge-1.21.11/gradlew.bat | 94 +++++++ mod-runtime/neoforge-1.21.11/settings.gradle | 11 + .../consterna/modpackes/ModpackESRuntime.java | 153 +++++++++++ .../templates/META-INF/neoforge.mods.toml | 25 ++ mod-runtime/neoforge-1.21.4/.gitignore | 7 + mod-runtime/neoforge-1.21.4/build.gradle | 85 ++++++ mod-runtime/neoforge-1.21.4/gradle.properties | 17 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + mod-runtime/neoforge-1.21.4/gradlew | 251 ++++++++++++++++++ mod-runtime/neoforge-1.21.4/gradlew.bat | 94 +++++++ mod-runtime/neoforge-1.21.4/settings.gradle | 11 + .../consterna/modpackes/ModpackESRuntime.java | 153 +++++++++++ .../templates/META-INF/neoforge.mods.toml | 25 ++ mod-runtime/neoforge-1.21.5/.gitignore | 7 + mod-runtime/neoforge-1.21.5/build.gradle | 85 ++++++ mod-runtime/neoforge-1.21.5/gradle.properties | 17 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + mod-runtime/neoforge-1.21.5/gradlew | 251 ++++++++++++++++++ mod-runtime/neoforge-1.21.5/gradlew.bat | 94 +++++++ mod-runtime/neoforge-1.21.5/settings.gradle | 11 + .../consterna/modpackes/ModpackESRuntime.java | 153 +++++++++++ .../templates/META-INF/neoforge.mods.toml | 25 ++ scripts/construir_mod.py | 3 + src/traductor_es/salida/mod_apoyo.py | 29 +- src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 23 +- 40 files changed, 2007 insertions(+), 17 deletions(-) create mode 100644 mod-runtime/neoforge-1.21.11/.gitignore create mode 100644 mod-runtime/neoforge-1.21.11/build.gradle create mode 100644 mod-runtime/neoforge-1.21.11/gradle.properties create mode 100644 mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/neoforge-1.21.11/gradlew create mode 100644 mod-runtime/neoforge-1.21.11/gradlew.bat create mode 100644 mod-runtime/neoforge-1.21.11/settings.gradle create mode 100644 mod-runtime/neoforge-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/neoforge-1.21.11/src/main/templates/META-INF/neoforge.mods.toml create mode 100644 mod-runtime/neoforge-1.21.4/.gitignore create mode 100644 mod-runtime/neoforge-1.21.4/build.gradle create mode 100644 mod-runtime/neoforge-1.21.4/gradle.properties create mode 100644 mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/neoforge-1.21.4/gradlew create mode 100644 mod-runtime/neoforge-1.21.4/gradlew.bat create mode 100644 mod-runtime/neoforge-1.21.4/settings.gradle create mode 100644 mod-runtime/neoforge-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/neoforge-1.21.4/src/main/templates/META-INF/neoforge.mods.toml create mode 100644 mod-runtime/neoforge-1.21.5/.gitignore create mode 100644 mod-runtime/neoforge-1.21.5/build.gradle create mode 100644 mod-runtime/neoforge-1.21.5/gradle.properties create mode 100644 mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/neoforge-1.21.5/gradlew create mode 100644 mod-runtime/neoforge-1.21.5/gradlew.bat create mode 100644 mod-runtime/neoforge-1.21.5/settings.gradle create mode 100644 mod-runtime/neoforge-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/neoforge-1.21.5/src/main/templates/META-INF/neoforge.mods.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f5037..86f48c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55e7c57..9dca65e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,10 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: +Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `neoforge-1.21.4`, `neoforge-1.21.5`, `neoforge-1.21.11`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: ``` -uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] +uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). diff --git a/README.md b/README.md index 29bd888..ee9d368 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/neoforge-1.21.1/build.gradle b/mod-runtime/neoforge-1.21.1/build.gradle index 030e3f4..4c5ad49 100644 --- a/mod-runtime/neoforge-1.21.1/build.gradle +++ b/mod-runtime/neoforge-1.21.1/build.gradle @@ -51,6 +51,7 @@ var generateModMetadata = tasks.register("generateModMetadata", ProcessResources minecraft_version : minecraft_version, minecraft_version_range: minecraft_version_range, neo_version : neo_version, + neo_version_minimo : neo_version_minimo, loader_version_range : loader_version_range, mod_id : mod_id, mod_name : mod_name, diff --git a/mod-runtime/neoforge-1.21.1/gradle.properties b/mod-runtime/neoforge-1.21.1/gradle.properties index 2737e21..dfacda1 100644 --- a/mod-runtime/neoforge-1.21.1/gradle.properties +++ b/mod-runtime/neoforge-1.21.1/gradle.properties @@ -2,7 +2,9 @@ org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false minecraft_version=1.21.1 -minecraft_version_range=[1.21.1] +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_version_range=[1.21,1.21.4) +neo_version_minimo=21.0.0 neo_version=21.1.265 loader_version_range=[1,) diff --git a/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml b/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml index e25d781..1eec55d 100644 --- a/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml +++ b/mod-runtime/neoforge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml @@ -13,7 +13,7 @@ description='''${mod_description}''' [[dependencies.${mod_id}]] modId="neoforge" type="required" - versionRange="[${neo_version},)" + versionRange="[${neo_version_minimo},)" ordering="NONE" side="CLIENT" diff --git a/mod-runtime/neoforge-1.21.11/.gitignore b/mod-runtime/neoforge-1.21.11/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/neoforge-1.21.11/build.gradle b/mod-runtime/neoforge-1.21.11/build.gradle new file mode 100644 index 0000000..b5472d8 --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/build.gradle @@ -0,0 +1,85 @@ +plugins { + id 'java-library' + id 'net.neoforged.moddev' version '2.0.148' + id 'idea' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-neoforge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +repositories { +} + +neoForge { + version = project.neo_version + + runs { + client { + client() + } + } + + mods { + "${mod_id}" { + sourceSet(sourceSets.main) + } + } + + unitTest { + enable() + testedMod = mods."${mod_id}" + } +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_minimo : neo_version_minimo, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description, + ] + inputs.properties replaceProperties + expand replaceProperties + from "src/main/templates" + into "build/generated/sources/modMetadata" +} +sourceSets.main.resources.srcDir generateModMetadata +neoForge.ideSyncTask generateModMetadata + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano-1215/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/neoforge-1.21.11/gradle.properties b/mod-runtime/neoforge-1.21.11/gradle.properties new file mode 100644 index 0000000..0862e5b --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/gradle.properties @@ -0,0 +1,17 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.21.11 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_version_range=[1.21.11] +neo_version_minimo=21.11.0 +neo_version=21.11.48 +loader_version_range=[1,) + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/neoforge-1.21.11/gradlew.bat b/mod-runtime/neoforge-1.21.11/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/neoforge-1.21.11/settings.gradle b/mod-runtime/neoforge-1.21.11/settings.gradle new file mode 100644 index 0000000..a2d1087 --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/neoforge-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/neoforge-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..a68accd --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,153 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientChatReceivedEvent; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RenderTooltipEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES (o encaje en una de sus plantillas con {@code %s} o {@code %d}). Si el juego no está en una + * variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. + */ +@Mod(value = ModpackESRuntime.ID, dist = Dist.CLIENT) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime(IEventBus busDelMod) { + busDelMod.addListener(ModpackESRuntime::alRegistrarRecargas); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); + } + + private static void alRegistrarRecargas(AddClientReloadListenersEvent evento) { + evento.addListener(Identifier.fromNamespaceAndPath(ID, "textos"), new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + Identifier ruta = Identifier.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", nueva.tamano(), nueva.plantillas()); + } + } +} diff --git a/mod-runtime/neoforge-1.21.11/src/main/templates/META-INF/neoforge.mods.toml b/mod-runtime/neoforge-1.21.11/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..1eec55d --- /dev/null +++ b/mod-runtime/neoforge-1.21.11/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +# Solo cliente: la clase principal se declara con dist = CLIENT, así que un servidor no la carga. +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="[${neo_version_minimo},)" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/neoforge-1.21.4/.gitignore b/mod-runtime/neoforge-1.21.4/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/neoforge-1.21.4/build.gradle b/mod-runtime/neoforge-1.21.4/build.gradle new file mode 100644 index 0000000..4c5ad49 --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/build.gradle @@ -0,0 +1,85 @@ +plugins { + id 'java-library' + id 'net.neoforged.moddev' version '2.0.148' + id 'idea' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-neoforge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +repositories { +} + +neoForge { + version = project.neo_version + + runs { + client { + client() + } + } + + mods { + "${mod_id}" { + sourceSet(sourceSets.main) + } + } + + unitTest { + enable() + testedMod = mods."${mod_id}" + } +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_minimo : neo_version_minimo, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description, + ] + inputs.properties replaceProperties + expand replaceProperties + from "src/main/templates" + into "build/generated/sources/modMetadata" +} +sourceSets.main.resources.srcDir generateModMetadata +neoForge.ideSyncTask generateModMetadata + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/neoforge-1.21.4/gradle.properties b/mod-runtime/neoforge-1.21.4/gradle.properties new file mode 100644 index 0000000..4d77d3f --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/gradle.properties @@ -0,0 +1,17 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.21.4 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_version_range=[1.21.4] +neo_version_minimo=21.4.0 +neo_version=21.4.161 +loader_version_range=[1,) + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/neoforge-1.21.4/gradlew.bat b/mod-runtime/neoforge-1.21.4/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/neoforge-1.21.4/settings.gradle b/mod-runtime/neoforge-1.21.4/settings.gradle new file mode 100644 index 0000000..a2d1087 --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/neoforge-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/neoforge-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..6e6ce26 --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,153 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientChatReceivedEvent; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RenderTooltipEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES (o encaje en una de sus plantillas con {@code %s} o {@code %d}). Si el juego no está en una + * variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. + */ +@Mod(value = ModpackESRuntime.ID, dist = Dist.CLIENT) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime(IEventBus busDelMod) { + busDelMod.addListener(ModpackESRuntime::alRegistrarRecargas); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); + } + + private static void alRegistrarRecargas(AddClientReloadListenersEvent evento) { + evento.addListener(ResourceLocation.fromNamespaceAndPath(ID, "textos"), new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", nueva.tamano(), nueva.plantillas()); + } + } +} diff --git a/mod-runtime/neoforge-1.21.4/src/main/templates/META-INF/neoforge.mods.toml b/mod-runtime/neoforge-1.21.4/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..1eec55d --- /dev/null +++ b/mod-runtime/neoforge-1.21.4/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +# Solo cliente: la clase principal se declara con dist = CLIENT, así que un servidor no la carga. +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="[${neo_version_minimo},)" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/neoforge-1.21.5/.gitignore b/mod-runtime/neoforge-1.21.5/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/neoforge-1.21.5/build.gradle b/mod-runtime/neoforge-1.21.5/build.gradle new file mode 100644 index 0000000..b5472d8 --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/build.gradle @@ -0,0 +1,85 @@ +plugins { + id 'java-library' + id 'net.neoforged.moddev' version '2.0.148' + id 'idea' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-neoforge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +repositories { +} + +neoForge { + version = project.neo_version + + runs { + client { + client() + } + } + + mods { + "${mod_id}" { + sourceSet(sourceSets.main) + } + } + + unitTest { + enable() + testedMod = mods."${mod_id}" + } +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_minimo : neo_version_minimo, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description, + ] + inputs.properties replaceProperties + expand replaceProperties + from "src/main/templates" + into "build/generated/sources/modMetadata" +} +sourceSets.main.resources.srcDir generateModMetadata +neoForge.ideSyncTask generateModMetadata + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-plano/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-plano-1215/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/neoforge-1.21.5/gradle.properties b/mod-runtime/neoforge-1.21.5/gradle.properties new file mode 100644 index 0000000..e75f595 --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/gradle.properties @@ -0,0 +1,17 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.21.5 +# Versiones en las que el mismo código compila y pasa las pruebas (comprobado con `construir_mod.py --propiedad`) +minecraft_version_range=[1.21.5,1.21.11) +neo_version_minimo=21.5.0 +neo_version=21.5.101 +loader_version_range=[1,) + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/neoforge-1.21.5/gradlew.bat b/mod-runtime/neoforge-1.21.5/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/neoforge-1.21.5/settings.gradle b/mod-runtime/neoforge-1.21.5/settings.gradle new file mode 100644 index 0000000..a2d1087 --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } + } +} + +rootProject.name = 'modpackes_runtime' diff --git a/mod-runtime/neoforge-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/neoforge-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..6e6ce26 --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,153 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientChatReceivedEvent; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RenderTooltipEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES (o encaje en una de sus plantillas con {@code %s} o {@code %d}). Si el juego no está en una + * variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. + */ +@Mod(value = ModpackESRuntime.ID, dist = Dist.CLIENT) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime(IEventBus busDelMod) { + busDelMod.addListener(ModpackESRuntime::alRegistrarRecargas); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + NeoForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); + } + + private static void alRegistrarRecargas(AddClientReloadListenersEvent evento) { + evento.addListener(ResourceLocation.fromNamespaceAndPath(ID, "textos"), new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.Init.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = ResourceLocation.fromNamespaceAndPath(ID, "textos/" + variante + ".json"); + for (Resource recurso : recursos.getResourceStack(ruta)) { // de menor a mayor prioridad + try (Reader lector = recurso.openAsReader()) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.sourcePackId(), e); + } + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", nueva.tamano(), nueva.plantillas()); + } + } +} diff --git a/mod-runtime/neoforge-1.21.5/src/main/templates/META-INF/neoforge.mods.toml b/mod-runtime/neoforge-1.21.5/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..1eec55d --- /dev/null +++ b/mod-runtime/neoforge-1.21.5/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +# Solo cliente: la clase principal se declara con dist = CLIENT, así que un servidor no la carga. +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="[${neo_version_minimo},)" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index cab4d59..01c902f 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -25,6 +25,9 @@ "forge-1.19.2": (17, "8.8"), "forge-1.20.1": (17, "8.8"), "neoforge-1.21.1": (21, "9.2.1"), + "neoforge-1.21.4": (21, "9.2.1"), + "neoforge-1.21.5": (21, "9.2.1"), + "neoforge-1.21.11": (21, "9.2.1"), "fabric-1.19.4": (21, "9.8.2"), "fabric-1.20.1": (21, "9.8.2"), "fabric-1.20.4": (21, "9.8.2"), diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index c6f1a48..37b3352 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -18,7 +18,16 @@ SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { ("forge", (1, 19, 2)): "forge-1.19.2", ("forge", (1, 20, 1)): "forge-1.20.1", + ("neoforge", (1, 20, 1)): "forge-1.20.1", # NeoForge 1.20.1 es un fork de Forge y carga sus mods + # NeoForge solo publicó 1.21, 1.21.1, 1.21.3, 1.21.4, 1.21.5, 1.21.8, 1.21.10 y 1.21.11 en esa serie + ("neoforge", (1, 21, 0)): "neoforge-1.21.1", ("neoforge", (1, 21, 1)): "neoforge-1.21.1", + ("neoforge", (1, 21, 3)): "neoforge-1.21.1", + ("neoforge", (1, 21, 4)): "neoforge-1.21.4", + ("neoforge", (1, 21, 5)): "neoforge-1.21.5", + ("neoforge", (1, 21, 8)): "neoforge-1.21.5", + ("neoforge", (1, 21, 10)): "neoforge-1.21.5", + ("neoforge", (1, 21, 11)): "neoforge-1.21.11", ("fabric", (1, 19, 4)): "fabric-1.19.4", # Un jar sirve para varias versiones cuando el mismo código compila y pasa las pruebas contra cada una de ellas ("fabric", (1, 20, 0)): "fabric-1.20.1", @@ -79,6 +88,18 @@ def _sin_mod(instancia: Instancia) -> str: return f"Todavía no hay mod de apoyo para Minecraft {instancia.version} con {loader}." +def _nota_de_compatibilidad(instancia: Instancia) -> str | None: + """Cuando el jar es de otro loader que el de la instancia (y no se ha probado en él), se dice.""" + if instancia.loader == "quilt": + return ( + "Es el mod de Fabric: Quilt lo carga con Quilted Fabric API (necesita que esté instalada) y no se ha " + "probado en Quilt." + ) + if instancia.loader == "neoforge" and instancia.version.partes == (1, 20, 1): + return "Es el mod de Forge 1.20.1: NeoForge 1.20.1 es un fork de Forge y debería cargarlo; no se ha probado." + return None + + def jar_empaquetado(instancia: Instancia, carpeta: Path | None = None) -> Path | None: """El jar más nuevo del mod de apoyo para esta instancia, si viene con la aplicación.""" destino = destino_de(instancia) @@ -101,11 +122,9 @@ def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: if destino is None: return [f"{_sin_mod(instancia)} El texto del código Java seguirá en inglés."] lineas = [f"Hay mod de apoyo para esta instancia ({destino})."] - if instancia.loader == "quilt": - lineas.append( - "Es el mod de Fabric: Quilt lo carga con Quilted Fabric API (necesita que esté instalada) y no se ha " - "probado en Quilt." - ) + nota = _nota_de_compatibilidad(instancia) + if nota: + lineas.append(nota) lineas.append( "Esta copia de la aplicación lo trae." if jar_empaquetado(instancia, carpeta_jars) diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index 3c02e03..df31611 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,7 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.21.1 y Fabric 1.19.4, " + "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, " "de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. En Quilt usa los mods de Fabric." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index f5b602e..bf9f697 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -142,8 +142,16 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("1.21.4", "quilt", None), ("1.21.12", "fabric", None), ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión - ("1.21.4", "neoforge", None), - ("1.20.1", "neoforge", None), + ("1.21.4", "neoforge", "neoforge-1.21.4"), + ("1.21", "neoforge", "neoforge-1.21.1"), + ("1.21.3", "neoforge", "neoforge-1.21.1"), + ("1.21.5", "neoforge", "neoforge-1.21.5"), + ("1.21.8", "neoforge", "neoforge-1.21.5"), + ("1.21.10", "neoforge", "neoforge-1.21.5"), + ("1.21.11", "neoforge", "neoforge-1.21.11"), + ("1.20.4", "neoforge", None), + ("26.1", "neoforge", None), + ("1.20.1", "neoforge", "forge-1.20.1"), # NeoForge 1.20.1 carga los mods de Forge ("1.12.2", "forge", None), ("1.19.4", "forge", None), ] @@ -180,7 +188,7 @@ def test_instalar_con_errores_claros(tmp_path: Path) -> None: def test_cli_instalar_runtime_sin_soporte(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - juego = fabrica.prism(tmp_path, "1.21.4", "neoforge") + juego = fabrica.prism(tmp_path, "1.20.4", "neoforge") assert main(["instalar-runtime", str(juego.parent)]) != 0 assert "Todavía no hay mod de apoyo" in capsys.readouterr().out # los errores van por la salida normal assert main(["instalar-runtime", str(juego.parent), "--quitar"]) == 0 @@ -219,7 +227,7 @@ def traducir_lote(self, lote: Lote) -> dict[str, str]: def test_estado_del_mod_de_apoyo(tmp_path: Path) -> None: - sin_soporte = fabrica.prism(tmp_path / "a", "1.21.4", "neoforge") + sin_soporte = fabrica.prism(tmp_path / "a", "1.20.4", "neoforge") with Instancia(sin_soporte) as inst: assert "no hay mod de apoyo" in mod_apoyo.estado(inst)[0].lower() juego = fabrica.prism(tmp_path / "b", "1.20.1", "forge") @@ -248,3 +256,10 @@ def test_estado_en_quilt_avisa_que_es_el_mod_de_fabric(tmp_path: Path) -> None: with Instancia(juego) as inst: texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) assert "fabric-1.20.1" in texto and "Quilted Fabric API" in texto and "no se ha probado en Quilt" in texto + + +def test_estado_en_neoforge_1_20_1_avisa_que_es_el_mod_de_forge(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.20.1", "neoforge") + with Instancia(juego) as inst: + texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) + assert "forge-1.20.1" in texto and "fork de Forge" in texto and "no se ha probado" in texto From 1e669a32e35185be16438a8981535b7959ae3046 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 21:54:49 -0500 Subject: [PATCH 19/24] feat(runtime): mod de apoyo para Forge 1.18.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forge 1.18.2 tiene su propio proyecto: el texto aún es TextComponent y TranslatableComponent (variante `reescritor-texto` del reescritor, con sus pruebas), la recarga de recursos usa la API antigua y el evento de pantalla se llama `ScreenEvent.InitScreenEvent`. Fabric 1.18.2 no puede tener mod: Fabric API no trae un evento de chat del cliente en esa versión, igual que en 1.19.2. La app lo dice con ese motivo. --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 4 +- .../consterna/modpackes/Reescritor.java | 115 ++++++++ .../consterna/modpackes/ReescritorTest.java | 114 ++++++++ mod-runtime/forge-1.18.2/.gitignore | 7 + mod-runtime/forge-1.18.2/build.gradle | 89 +++++++ mod-runtime/forge-1.18.2/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 62076 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + mod-runtime/forge-1.18.2/gradlew | 245 ++++++++++++++++++ mod-runtime/forge-1.18.2/gradlew.bat | 92 +++++++ mod-runtime/forge-1.18.2/settings.gradle | 13 + .../consterna/modpackes/ModpackESRuntime.java | 160 ++++++++++++ .../src/main/resources/META-INF/mods.toml | 26 ++ .../src/main/resources/pack.mcmeta | 8 + scripts/construir_mod.py | 1 + src/traductor_es/salida/mod_apoyo.py | 11 +- src/traductor_es/ui/ventana.py | 4 +- tests/test_runtime.py | 12 +- 21 files changed, 919 insertions(+), 18 deletions(-) create mode 100644 mod-runtime/comun/reescritor-texto/src/main/java/io/github/consterna/modpackes/Reescritor.java create mode 100644 mod-runtime/comun/reescritor-texto/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/forge-1.18.2/.gitignore create mode 100644 mod-runtime/forge-1.18.2/build.gradle create mode 100644 mod-runtime/forge-1.18.2/gradle.properties create mode 100644 mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/forge-1.18.2/gradlew create mode 100644 mod-runtime/forge-1.18.2/gradlew.bat create mode 100644 mod-runtime/forge-1.18.2/settings.gradle create mode 100644 mod-runtime/forge-1.18.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/forge-1.18.2/src/main/resources/META-INF/mods.toml create mode 100644 mod-runtime/forge-1.18.2/src/main/resources/pack.mcmeta diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e54f414..da84370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin - java-version: "17" # ForgeGradle 6 (Forge 1.19.2 y 1.20.1) pide Java 17 + java-version: "17" # ForgeGradle 6 (Forge 1.18.2, 1.19.2 y 1.20.1) pide Java 17 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: @@ -63,7 +63,7 @@ jobs: java-version: "25" # Minecraft 26.x (Fabric 26.1) pide Java 25 # Con las tres versiones instaladas (17 y 25 aquí, 21 más arriba), el script elige la que pide cada mod - # (Forge 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25) + # (Forge 1.18.2, 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25) # por las variables JAVA_HOME_17_X64, JAVA_HOME_21_X64 y JAVA_HOME_25_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f48c5..faab30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dca65e..6f941ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,13 +95,13 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `neoforge-1.21.4`, `neoforge-1.21.5`, `neoforge-1.21.11`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2), y cada proyecto solo trae su gancho con el loader. Forge 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: +Cada carpeta de `mod-runtime/` (`forge-1.18.2`, `forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `neoforge-1.21.4`, `neoforge-1.21.5`, `neoforge-1.21.11`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2 y `reescritor-texto` para 1.18.2, donde el texto aún es `TextComponent`), y cada proyecto solo trae su gancho con el loader. Forge 1.18.2, 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: ``` -uv run python scripts/construir_mod.py [forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] +uv run python scripts/construir_mod.py [forge-1.18.2 | forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` -Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.19.2 no tiene evento de chat del cliente). +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.18.2 y 1.19.2 no tienen evento de chat del cliente). ## Licencia de las contribuciones diff --git a/README.md b/README.md index ee9d368..c0bf998 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esa versión, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.18.2 y 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esas versiones, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/comun/reescritor-texto/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/comun/reescritor-texto/src/main/java/io/github/consterna/modpackes/Reescritor.java new file mode 100644 index 0000000..02a3a48 --- /dev/null +++ b/mod-runtime/comun/reescritor-texto/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -0,0 +1,115 @@ +package io.github.consterna.modpackes; + +import com.mojang.datafixers.util.Either; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.world.inventory.tooltip.TooltipComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni + * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + */ +public final class Reescritor { + private Reescritor() { + } + + /** El componente traducido, o {@code null} si no cambió nada (así no se copia lo que no hace falta). */ + public static Component reescribir(Component original, Tabla tabla) { + if (original == null || tabla.vacia()) { + return null; + } + // Antes de 1.19 el texto es un componente propio (TextComponent, TranslatableComponent), no un «contenido» + String textoNuevo = null; + Object[] argumentosNuevos = null; + boolean cambio = false; + + if (original instanceof TextComponent literal) { + textoNuevo = tabla.traducir(literal.getText()); + cambio = textoNuevo != null; + } else if (original instanceof TranslatableComponent traducible) { + Object[] argumentos = traducible.getArgs(); + for (int i = 0; i < argumentos.length; i++) { + Object reemplazo = null; + if (argumentos[i] instanceof Component hijo) { + reemplazo = reescribir(hijo, tabla); + } else if (argumentos[i] instanceof String texto) { + reemplazo = tabla.traducir(texto); + } + if (reemplazo != null) { + if (argumentosNuevos == null) { + argumentosNuevos = argumentos.clone(); + } + argumentosNuevos[i] = reemplazo; + } + } + cambio = argumentosNuevos != null; + } + + List hermanos = original.getSiblings(); + List nuevosHermanos = null; + for (int i = 0; i < hermanos.size(); i++) { + Component reemplazo = reescribir(hermanos.get(i), tabla); + if (reemplazo != null) { + if (nuevosHermanos == null) { + nuevosHermanos = new ArrayList<>(hermanos); + } + nuevosHermanos.set(i, reemplazo); + } + } + + if (!cambio && nuevosHermanos == null) { + return null; + } + MutableComponent resultado; + if (original instanceof TextComponent literal) { + resultado = new TextComponent(textoNuevo != null ? textoNuevo : literal.getText()); + } else if (original instanceof TranslatableComponent traducible) { + resultado = new TranslatableComponent( + traducible.getKey(), argumentosNuevos != null ? argumentosNuevos : traducible.getArgs()); + } else { + resultado = original.plainCopy(); // otro tipo de componente: se copia tal cual y solo cambian sus hermanos + } + resultado.setStyle(original.getStyle()); + for (Component hermano : nuevosHermanos != null ? nuevosHermanos : hermanos) { + resultado.append(hermano); + } + return resultado; + } + + /** + * Cambia en su sitio las líneas de texto de un tooltip (las que son un {@link Component}); lo que no es texto + * o no está en la tabla se deja como está. + */ + public static void reescribirElementos(List> elementos, Tabla tabla) { + if (elementos == null || tabla.vacia()) { + return; + } + for (int i = 0; i < elementos.size(); i++) { + Either elemento = elementos.get(i); + if (elemento != null && elemento.left().orElse(null) instanceof Component linea) { + Component nuevo = reescribir(linea, tabla); + if (nuevo != null) { + elementos.set(i, Either.left(nuevo)); + } + } + } + } + + /** Cambia el texto de un botón, casilla o etiqueta si está en la tabla. */ + public static void reescribirMensaje(AbstractWidget widget, Tabla tabla) { + if (widget == null || tabla.vacia()) { + return; + } + Component nuevo = reescribir(widget.getMessage(), tabla); + if (nuevo != null) { + widget.setMessage(nuevo); + } + } +} diff --git a/mod-runtime/comun/reescritor-texto/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-texto/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..2d10c38 --- /dev/null +++ b/mod-runtime/comun/reescritor-texto/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,114 @@ +package io.github.consterna.modpackes; + +import com.mojang.datafixers.util.Either; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static final Tabla TABLA = new Tabla(Map.of( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí")); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(ChatFormatting.RED).withBold(true); + Component original = new TextComponent("TPA request expired!").setStyle(estilo); + Component nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(new TextComponent("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(new TextComponent("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + MutableComponent raiz = new TextComponent(" ").withStyle(ChatFormatting.AQUA); + raiz.append(new TextComponent("Hello").withStyle(ChatFormatting.WHITE)); + raiz.append(new TextComponent(" Steve")); + Component nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(ChatFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(ChatFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com")); + Component nuevo = Reescritor.reescribir(new TextComponent("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals("https://example.com", nuevo.getStyle().getClickEvent().getValue()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + Component original = new TranslatableComponent("chat.type.text", new TextComponent("Hello"), "Hello"); + Component nuevo = Reescritor.reescribir(original, TABLA); + TranslatableComponent contenido = (TranslatableComponent) nuevo; + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((Component) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = new Tabla(Map.of("Ñandú says hi", "Ñandú dice ¡hola! 😀")); + Component nuevo = Reescritor.reescribir(new TextComponent(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } + + @Test + void cambiaLasLineasDeUnTooltipYDejaLoQueNoEsTexto() { + TooltipComponent imagen = new TooltipComponent() { + }; + FormattedText plano = FormattedText.of("Hello"); + Component otra = new TextComponent("Other"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(new TextComponent("Hello"))); + elementos.add(Either.right(imagen)); + elementos.add(Either.left(plano)); + elementos.add(Either.left(otra)); + + Reescritor.reescribirElementos(elementos, TABLA); + + assertEquals("Hola", ((Component) elementos.get(0).left().orElseThrow()).getString()); + assertSame(imagen, elementos.get(1).right().orElseThrow()); + assertSame(plano, elementos.get(2).left().orElseThrow()); + assertSame(otra, elementos.get(3).left().orElseThrow()); + } + + @Test + void losTooltipsSinTablaOSinListaNoHacenNada() { + Component original = new TextComponent("Hello"); + List> elementos = new ArrayList<>(); + elementos.add(Either.left(original)); + + Reescritor.reescribirElementos(elementos, Tabla.VACIA); + Reescritor.reescribirElementos(null, TABLA); + + assertSame(original, elementos.get(0).left().orElseThrow()); + } +} diff --git a/mod-runtime/forge-1.18.2/.gitignore b/mod-runtime/forge-1.18.2/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/forge-1.18.2/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/forge-1.18.2/build.gradle b/mod-runtime/forge-1.18.2/build.gradle new file mode 100644 index 0000000..e707ba1 --- /dev/null +++ b/mod-runtime/forge-1.18.2/build.gradle @@ -0,0 +1,89 @@ +plugins { + id 'eclipse' + id 'idea' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-forge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(17) + +minecraft { + mappings channel: mapping_channel, version: mapping_version + + runs { + client { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + mods { + "${mod_id}" { + source sourceSets.main + } + } + } + } +} + +repositories { + mavenCentral() + maven { url = 'https://libraries.minecraft.net/' } +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version: forge_version, forge_version_range: forge_version_range, + loader_version_range: loader_version_range, + mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors: mod_authors, mod_description: mod_description, + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + [project: project] + } +} + +tasks.named('jar', Jar).configure { + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : mod_authors, + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : mod_authors + ]) + } + + finalizedBy 'reobfJar' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-texto/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-texto/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/forge-1.18.2/gradle.properties b/mod-runtime/forge-1.18.2/gradle.properties new file mode 100644 index 0000000..4995bfe --- /dev/null +++ b/mod-runtime/forge-1.18.2/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.18.2 +minecraft_version_range=[1.18.2,1.19) +forge_version=40.3.12 +forge_version_range=[40,41) +loader_version_range=[40,41) +mapping_channel=official +mapping_version=1.18.2 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1962a79e29d3e0ab67b14947c167a862655af9b GIT binary patch literal 62076 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 literal 0 HcmV?d00001 diff --git a/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2617362 --- /dev/null +++ b/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.18.2/gradlew b/mod-runtime/forge-1.18.2/gradlew new file mode 100644 index 0000000..aeb74cb --- /dev/null +++ b/mod-runtime/forge-1.18.2/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/forge-1.18.2/gradlew.bat b/mod-runtime/forge-1.18.2/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/mod-runtime/forge-1.18.2/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/forge-1.18.2/settings.gradle b/mod-runtime/forge-1.18.2/settings.gradle new file mode 100644 index 0000000..93f1b80 --- /dev/null +++ b/mod-runtime/forge-1.18.2/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = 'https://maven.minecraftforge.net/' + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} \ No newline at end of file diff --git a/mod-runtime/forge-1.18.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/forge-1.18.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..d0c0dcd --- /dev/null +++ b/mod-runtime/forge-1.18.2/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,160 @@ +package io.github.consterna.modpackes; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat, de la barra de + * acción y tooltips cuyo texto sea exactamente uno de la tabla {@code assets/modpackes_runtime/textos/.json} + * que genera ModpackES. Si el juego no está en una variante de español, no hace nada. + * + *

También cambia el texto de los botones de una pantalla al abrirla y los tooltips que no son de objetos. El + * texto que una pantalla dibuja directamente no pasa por ningún evento y no se toca. + */ +@Mod(ModpackESRuntime.ID) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + private static final Logger LOG = LogUtils.getLogger(); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + public ModpackESRuntime() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(ModpackESRuntime::alRegistrarRecargas); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alRecibirChat); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alCrearTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alReunirTooltip); + MinecraftForge.EVENT_BUS.addListener(ModpackESRuntime::alIniciarPantalla); + } + + private static void alRegistrarRecargas(RegisterClientReloadListenersEvent evento) { + evento.registerReloadListener(new Cargador()); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + Component nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + Component nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alReunirTooltip(RenderTooltipEvent.GatherComponents evento) { + try { + Reescritor.reescribirElementos(evento.getTooltipElements(), tabla); + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(ScreenEvent.InitScreenEvent.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (GuiEventListener hijo : evento.getListenersList()) { + if (hijo instanceof AbstractWidget widget) { + Reescritor.reescribirMensaje(widget, actual); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase().startsWith("es_")) { + return List.of(); + } + String codigo = codigoDeIdioma.toLowerCase(); + return List.of(codigo, "es_mx", "es_es").stream().distinct().toList(); + } + + private static final class Cargador extends SimplePreparableReloadListener { + @Override + protected Tabla prepare(ResourceManager recursos, ProfilerFiller perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ID, "textos/" + variante + ".json"); + if (!recursos.hasResource(ruta)) { + continue; + } + try { + for (Resource recurso : recursos.getResources(ruta)) { // de menor a mayor prioridad + try (Resource r = recurso; Reader lector = new InputStreamReader(r.getInputStream(), StandardCharsets.UTF_8)) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.getSourceName(), e); + } + } + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude listar {}", ruta, e); + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, ResourceManager recursos, ProfilerFiller perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla", nueva.tamano()); + } + } +} diff --git a/mod-runtime/forge-1.18.2/src/main/resources/META-INF/mods.toml b/mod-runtime/forge-1.18.2/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..94577b0 --- /dev/null +++ b/mod-runtime/forge-1.18.2/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +# Solo cliente: Forge no lo carga en servidores dedicados y un servidor sin el mod no marca error. +clientSideOnly=true + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] + modId="forge" + mandatory=true + versionRange="${forge_version_range}" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/forge-1.18.2/src/main/resources/pack.mcmeta b/mod-runtime/forge-1.18.2/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..a452264 --- /dev/null +++ b/mod-runtime/forge-1.18.2/src/main/resources/pack.mcmeta @@ -0,0 +1,8 @@ +{ + "pack": { + "description": { + "text": "${mod_name}" + }, + "pack_format": 8 + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 01c902f..120e434 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -22,6 +22,7 @@ DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" #: proyecto → (versión de Java, versión de Gradle) que pide su compilación PROYECTOS = { + "forge-1.18.2": (17, "8.8"), "forge-1.19.2": (17, "8.8"), "forge-1.20.1": (17, "8.8"), "neoforge-1.21.1": (21, "9.2.1"), diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 37b3352..bc3de04 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -16,6 +16,7 @@ PREFIJO = "modpackes_runtime-" #: (loader, versión de Minecraft) para los que hay mod de apoyo. Cada entrada es un proyecto en `mod-runtime/`. SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { + ("forge", (1, 18, 2)): "forge-1.18.2", ("forge", (1, 19, 2)): "forge-1.19.2", ("forge", (1, 20, 1)): "forge-1.20.1", ("neoforge", (1, 20, 1)): "forge-1.20.1", # NeoForge 1.20.1 es un fork de Forge y carga sus mods @@ -61,11 +62,13 @@ ) # fmt: skip SOPORTADOS.update({("quilt", v): SOPORTADOS[("fabric", v)] for v in _VERSIONES_DE_QUILT}) #: Combinaciones que no tendrán mod de apoyo y por qué (para decírselo al jugador en vez de «todavía no»). +_SIN_EVENTO_DE_CHAT = ( + "Fabric API no tiene en esa versión un evento para los mensajes que recibe el cliente, " + "y sin él el mod de apoyo no podría cambiar el chat. Con Fabric 1.19.4 en adelante sí." +) IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { - ("fabric", (1, 19, 2)): ( - "Fabric API 1.19.2 no tiene un evento para los mensajes que recibe el cliente, " - "y sin él el mod de apoyo no podría cambiar el chat. Con Fabric 1.19.4 en adelante sí." - ), + ("fabric", (1, 18, 2)): _SIN_EVENTO_DE_CHAT, + ("fabric", (1, 19, 2)): _SIN_EVENTO_DE_CHAT, } _NOMBRE = re.compile(rf"{re.escape(PREFIJO)}(?P[a-z]+-[\d.]+)-(?P[\w.+-]+)\.jar") diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index df31611..d68125c 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,8 +469,8 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, " - "de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. En Quilt usa los mods de Fabric." + "Por ahora existe para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, " + "y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. En Quilt usa los mods de Fabric." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) formulario.addRow("", self.c_java) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index bf9f697..b305b90 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -142,6 +142,8 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("1.21.4", "quilt", None), ("1.21.12", "fabric", None), ("1.19.2", "fabric", None), # Fabric API no tiene evento de chat en esa versión + ("1.18.2", "fabric", None), + ("1.18.2", "forge", "forge-1.18.2"), ("1.21.4", "neoforge", "neoforge-1.21.4"), ("1.21", "neoforge", "neoforge-1.21.1"), ("1.21.3", "neoforge", "neoforge-1.21.1"), @@ -153,6 +155,7 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("26.1", "neoforge", None), ("1.20.1", "neoforge", "forge-1.20.1"), # NeoForge 1.20.1 carga los mods de Forge ("1.12.2", "forge", None), + ("1.16.5", "forge", None), ("1.19.4", "forge", None), ] for i, (version, loader, esperado) in enumerate(casos): @@ -176,12 +179,13 @@ def test_instalar_reemplaza_versiones_y_respeta_otros_mods(tmp_path: Path) -> No def test_instalar_con_errores_claros(tmp_path: Path) -> None: - juego = fabrica.prism(tmp_path / "a", "1.18.2", "forge") + juego = fabrica.prism(tmp_path / "a", "1.16.5", "forge") with Instancia(juego) as inst, pytest.raises(ValueError, match="Todavía no hay mod de apoyo"): mod_apoyo.instalar(inst, tmp_path) - imposible = fabrica.prism(tmp_path / "c", "1.19.2", "fabric") # se dice por qué no, no solo «todavía» - with Instancia(imposible) as inst, pytest.raises(ValueError, match="no tiene un evento"): - mod_apoyo.instalar(inst, tmp_path) + for i, version in enumerate(("1.18.2", "1.19.2")): # se dice por qué no, no solo «todavía» + imposible = fabrica.prism(tmp_path / f"c{i}", version, "fabric") + with Instancia(imposible) as inst, pytest.raises(ValueError, match="no tiene en esa versión un evento"): + mod_apoyo.instalar(inst, tmp_path) juego2 = fabrica.prism(tmp_path / "b", "1.20.1", "forge") with Instancia(juego2) as inst, pytest.raises(ValueError, match="no trae el mod de apoyo"): mod_apoyo.instalar(inst, tmp_path / "no_hay") From 322090231c51adc9aa852c3bfe60798976ba94a5 Mon Sep 17 00:00:00 2001 From: Cristian Date: Sun, 8 Nov 2026 22:40:47 -0500 Subject: [PATCH 20/24] feat(runtime): mod de apoyo para Forge 1.16.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forge 1.16.5 corre con Java 8 y tiene su propio proyecto: usa los nombres antiguos de las clases (ITextComponent, StringTextComponent, IResourceManager…), no tiene evento para registrar recargas del cliente (se hace con el gestor de recursos en FMLClientSetupEvent), no tiene tooltips por elementos y separa el código de cliente con DistExecutor para no molestar a un servidor. Para poder compartir `Tabla`, se reescribió sin nada posterior a Java 8 (Map.of, String.strip, copyOf, patrones de instanceof, JsonParser.parseReader) con el mismo comportamiento y las mismas pruebas. `construir_mod.py` compila este proyecto con un JDK 8 que le pasa a Gradle como toolchain, y el CI instala Java 8. Fabric 1.16.5 no puede tener mod: su Fabric API no trae evento de chat del cliente. --- .github/workflows/ci.yml | 11 +- CHANGELOG.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 4 +- .../consterna/modpackes/Reescritor.java | 96 +++++++ .../consterna/modpackes/ReescritorTest.java | 86 ++++++ .../io/github/consterna/modpackes/Tabla.java | 45 +++- mod-runtime/forge-1.16.5/.gitignore | 7 + mod-runtime/forge-1.16.5/build.gradle | 89 +++++++ mod-runtime/forge-1.16.5/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 62076 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + mod-runtime/forge-1.16.5/gradlew | 245 ++++++++++++++++++ mod-runtime/forge-1.16.5/gradlew.bat | 92 +++++++ mod-runtime/forge-1.16.5/settings.gradle | 13 + .../github/consterna/modpackes/Cliente.java | 155 +++++++++++ .../consterna/modpackes/ModpackESRuntime.java | 32 +++ .../src/main/resources/META-INF/mods.toml | 26 ++ .../src/main/resources/pack.mcmeta | 8 + scripts/construir_mod.py | 18 +- src/traductor_es/salida/mod_apoyo.py | 2 + src/traductor_es/ui/ventana.py | 2 +- tests/test_runtime.py | 8 +- 23 files changed, 942 insertions(+), 29 deletions(-) create mode 100644 mod-runtime/comun/reescritor-1165/src/main/java/io/github/consterna/modpackes/Reescritor.java create mode 100644 mod-runtime/comun/reescritor-1165/src/test/java/io/github/consterna/modpackes/ReescritorTest.java create mode 100644 mod-runtime/forge-1.16.5/.gitignore create mode 100644 mod-runtime/forge-1.16.5/build.gradle create mode 100644 mod-runtime/forge-1.16.5/gradle.properties create mode 100644 mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.jar create mode 100644 mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties create mode 100644 mod-runtime/forge-1.16.5/gradlew create mode 100644 mod-runtime/forge-1.16.5/gradlew.bat create mode 100644 mod-runtime/forge-1.16.5/settings.gradle create mode 100644 mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java create mode 100644 mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java create mode 100644 mod-runtime/forge-1.16.5/src/main/resources/META-INF/mods.toml create mode 100644 mod-runtime/forge-1.16.5/src/main/resources/pack.mcmeta diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da84370..b37967e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,14 @@ jobs: distribution: temurin java-version: "25" # Minecraft 26.x (Fabric 26.1) pide Java 25 - # Con las tres versiones instaladas (17 y 25 aquí, 21 más arriba), el script elige la que pide cada mod - # (Forge 1.18.2, 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25) - # por las variables JAVA_HOME_17_X64, JAVA_HOME_21_X64 y JAVA_HOME_25_X64. + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + distribution: temurin + java-version: "8" # Minecraft 1.16.5 (Forge 1.16.5) se compila con Java 8 + + # Con las cuatro versiones instaladas (8, 17 y 25 aquí, 21 más arriba), el script elige la que pide cada mod + # (Forge 1.18.2, 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25; Forge 1.16.5 compila su código con Java 8) + # por las variables JAVA_HOME_8_X64, JAVA_HOME_17_X64, JAVA_HOME_21_X64 y JAVA_HOME_25_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas run: uv run python scripts/construir_mod.py diff --git a/CHANGELOG.md b/CHANGELOG.md index faab30c..ed9e308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Agregado - **Texto escrito en el código de los mods** (opcional, apagado por defecto): la app traduce las frases que encuentra en el código Java de los - mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en + mods y las guarda en una tabla del resource pack; el nuevo mod de apoyo **ModpackES Runtime** (solo cliente, para Forge 1.16.5, 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3, y Quilt con los mods de Fabric hasta 1.21.1) las muestra en español en el chat, la barra de acción, los tooltips y el texto de los botones de una pantalla al abrirla (en Forge y NeoForge también los tooltips que no son de objetos). No cambia el texto que una pantalla dibuja directamente. Solo cambia textos idénticos a los de la tabla. «Más ▸ Instalar el mod de apoyo» y «Quitar el mod de apoyo», `modpackes instalar-runtime` y `--java` en `analizar` y `traducir`. El CI compila el mod con sus pruebas. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f941ad..2a19fb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,13 +95,13 @@ originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks ## El mod de apoyo (Java) -Cada carpeta de `mod-runtime/` (`forge-1.18.2`, `forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `neoforge-1.21.4`, `neoforge-1.21.5`, `neoforge-1.21.11`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2 y `reescritor-texto` para 1.18.2, donde el texto aún es `TextComponent`), y cada proyecto solo trae su gancho con el loader. Forge 1.18.2, 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17; NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: +Cada carpeta de `mod-runtime/` (`forge-1.16.5`, `forge-1.18.2`, `forge-1.19.2`, `forge-1.20.1`, `neoforge-1.21.1`, `neoforge-1.21.4`, `neoforge-1.21.5`, `neoforge-1.21.11`, `fabric-1.19.4`, `fabric-1.20.1`, `fabric-1.20.4`, `fabric-1.20.6`, `fabric-1.21.1`, `fabric-1.21.4`, `fabric-1.21.5`, `fabric-1.21.11`, `fabric-26.1`) es un mod solo de cliente que cambia, al mostrarlos, los textos que otros mods tienen en su código y que están en la tabla de la app. Todos comparten la lógica de tabla y plantillas, que vive en `mod-runtime/comun/` (la tabla es de todos; el reescritor tiene una variante por forma de guardar el texto: `reescritor-literal`, `reescritor-plano` para 1.20.3 en adelante, con `reescritor-plano-1215` solo para sus pruebas desde 1.21.5, donde cambió `ClickEvent`, y `reescritor-sin-reserva` para 1.19.2 `reescritor-texto` para 1.18.2, donde el texto aún es `TextComponent`, y `reescritor-1165` para 1.16.5, con los nombres antiguos de las clases, sin tooltips por elementos y en Java 8; por eso `Tabla` evita todo lo posterior a Java 8), y cada proyecto solo trae su gancho con el loader. Forge 1.16.5, 1.18.2, 1.19.2 y 1.20.1 se compilan con ForgeGradle 6 y Java 17 (el de 1.16.5 compila su código con un JDK 8, que `construir_mod.py` le pasa a Gradle como toolchain); NeoForge 1.21.1, con ModDevGradle, Gradle 9 y Java 21; Fabric, con Loom 1.17, Gradle 9 y Java 21 (los de 1.19.4, 1.20.1 y 1.20.4 quedan compilados para Java 17; los de 1.20.6 en adelante, para Java 21); Fabric 26.1, con Loom 1.19 sin remapeo (Minecraft 26.x ya no está ofuscado) y Java 25: ``` -uv run python scripts/construir_mod.py [forge-1.18.2 | forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] +uv run python scripts/construir_mod.py [forge-1.16.5 | forge-1.18.2 | forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` -Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.18.2 y 1.19.2 no tienen evento de chat del cliente). +Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.16.5, 1.18.2 y 1.19.2 no tienen evento de chat del cliente). ## Licencia de las contribuciones diff --git a/README.md b/README.md index c0bf998..dfca95a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ y **cualquier lanzador**, y escribe a la vez las siete variantes de español del | Libros guía | Patchouli, Mantle (Tinkers), Citadel (Alex's Mobs), GuideME (AE2), Lavender. Modonomicon va por claves de idioma | Resource pack (o en su sitio, si es un libro propio del modpack) | | Scripts de KubeJS | Lo que ve el jugador: `tell`, `notify`, `displayName`, `Text.of`, tooltips, información de JEI/REI/EMI, los mensajes armados por partes antes de un `tell`, y el texto de `tellraw`, `title` y `say` en `runCommand` | En su sitio, con copia de seguridad | | Datapacks y tips | Funciones (`.mcfunction`: `tellraw`, `title`, `say`), títulos y descripciones de logros, y consejos de pantalla de carga (mod Tips). Se buscan por su estructura (`data//functions/…`), estén en `kubejs/data`, `global_packs`, `datapacks`, `config/openloader` o en los datapacks de un mundo, sueltos o dentro de un `.zip`. Los componentes de texto valen en JSON (hasta 1.21.4) y en SNBT (desde 1.21.5) | En su sitio, con copia de seguridad | -| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | +| Texto escrito en el código de los mods | Mensajes de chat, de la barra de acción, tooltips y texto de botones que un mod tiene en su código Java. Un resource pack no los alcanza: la app los traduce a una tabla que lee el mod de apoyo **ModpackES Runtime** (solo cliente, Forge 1.16.5, 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3) | Tabla en el resource pack; el mod se instala solo si lo pides | Lo que el mod ya trae en español no se toca, y si existe una variante (por ejemplo `es_es`) se copia a las demás sin gastar. Los libros antiguos de Patchouli que guardan el texto dentro del jar no se pueden traducir @@ -86,7 +86,7 @@ traducir indicando el error. La nueva traducción tiene que pasar las mismas reg Tus correcciones nunca se revisan. Sube el costo de esos textos (el análisis lo incluye) y sus marcas no son infalibles: sirven para decidir qué rehacer, no como nota de calidad. -**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.18.2 y 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esas versiones, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). +**Texto del código de los mods** (opcional): activa «Incluir texto escrito en el código de los mods» en «Opciones avanzadas» y pulsa **Más ▸ Instalar el mod de apoyo**. La app traduce las frases que encuentra en el código de los mods y el mod de apoyo las muestra en español en el chat, la barra de acción y los tooltips. Solo cambia un texto si es **idéntico** al de la tabla (no hay traducción en vivo ni en internet), reconoce frases con datos escritos con `%s` o `%d` como plantillas, cambia también el texto de los botones de una pantalla al abrirla y, en Forge y NeoForge, los tooltips que no son de objetos; el texto que una pantalla dibuja directamente no se puede cambiar sin Mixin y no se toca; por ahora existe para Forge 1.16.5, 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. Quilt usa los mods de Fabric con Quilted Fabric API (hasta 1.21.1) y no se ha probado en Quilt. Para Fabric 1.16.5, 1.18.2 y 1.19.2 no puede existir: Fabric API no tiene un evento de chat del cliente en esas versiones, y la app lo dice. **Más ▸ Quitar el mod de apoyo** lo retira de la carpeta mods; en la línea de comandos: `modpackes instalar-runtime RUTA [--quitar]` y `--java` en `analizar` y `traducir`. Para comprobarlo en el juego, con el mod instalado y el juego en español, escribe `/tellraw @s "TPA request expired!"` (o cualquier frase de la tabla). **Modo exhaustivo de scripts** (opcional, en «Opciones avanzadas» o con `--exhaustivo`): traduce también las frases de los scripts de KubeJS cuyo uso no se entiende (por ejemplo textos que pasan por una función propia del pack). Puede tocar lógica del script, por eso viene apagado; todo queda con copia de seguridad y se restaura con «Restaurar el original». diff --git a/mod-runtime/comun/reescritor-1165/src/main/java/io/github/consterna/modpackes/Reescritor.java b/mod-runtime/comun/reescritor-1165/src/main/java/io/github/consterna/modpackes/Reescritor.java new file mode 100644 index 0000000..aac7a6f --- /dev/null +++ b/mod-runtime/comun/reescritor-1165/src/main/java/io/github/consterna/modpackes/Reescritor.java @@ -0,0 +1,96 @@ +package io.github.consterna.modpackes; + + +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.util.text.IFormattableTextComponent; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TranslationTextComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cambia el texto literal de un componente por su traducción, sin tocar estilos, colores, eventos de clic ni + * el resto de la estructura. Nunca lanza por un componente raro: si algo no se entiende, se deja igual. + * Variante de Minecraft 1.16.5: Java 8, el texto es un componente propio (StringTextComponent, TranslationTextComponent) + * y no hay tooltips por elementos. + */ +public final class Reescritor { + private Reescritor() { + } + + /** El componente traducido, o {@code null} si no cambió nada (así no se copia lo que no hace falta). */ + public static ITextComponent reescribir(ITextComponent original, Tabla tabla) { + if (original == null || tabla.vacia()) { + return null; + } + String textoNuevo = null; + Object[] argumentosNuevos = null; + boolean cambio = false; + + if (original instanceof StringTextComponent) { + textoNuevo = tabla.traducir(((StringTextComponent) original).getText()); + cambio = textoNuevo != null; + } else if (original instanceof TranslationTextComponent) { + Object[] argumentos = ((TranslationTextComponent) original).getArgs(); + for (int i = 0; i < argumentos.length; i++) { + Object reemplazo = null; + if (argumentos[i] instanceof ITextComponent) { + reemplazo = reescribir((ITextComponent) argumentos[i], tabla); + } else if (argumentos[i] instanceof String) { + reemplazo = tabla.traducir((String) argumentos[i]); + } + if (reemplazo != null) { + if (argumentosNuevos == null) { + argumentosNuevos = argumentos.clone(); + } + argumentosNuevos[i] = reemplazo; + } + } + cambio = argumentosNuevos != null; + } + + List hermanos = original.getSiblings(); + List nuevosHermanos = null; + for (int i = 0; i < hermanos.size(); i++) { + ITextComponent reemplazo = reescribir(hermanos.get(i), tabla); + if (reemplazo != null) { + if (nuevosHermanos == null) { + nuevosHermanos = new ArrayList<>(hermanos); + } + nuevosHermanos.set(i, reemplazo); + } + } + + if (!cambio && nuevosHermanos == null) { + return null; + } + IFormattableTextComponent resultado; + if (original instanceof StringTextComponent) { + resultado = new StringTextComponent(textoNuevo != null ? textoNuevo : ((StringTextComponent) original).getText()); + } else if (original instanceof TranslationTextComponent) { + TranslationTextComponent traducible = (TranslationTextComponent) original; + resultado = new TranslationTextComponent( + traducible.getKey(), argumentosNuevos != null ? argumentosNuevos : traducible.getArgs()); + } else { + resultado = original.plainCopy(); // otro tipo de componente: se copia tal cual y solo cambian sus hermanos + } + resultado.setStyle(original.getStyle()); + for (ITextComponent hermano : nuevosHermanos != null ? nuevosHermanos : hermanos) { + resultado.append(hermano); + } + return resultado; + } + + /** Cambia el texto de un botón, casilla o etiqueta si está en la tabla. */ + public static void reescribirMensaje(Widget widget, Tabla tabla) { + if (widget == null || tabla.vacia()) { + return; + } + ITextComponent nuevo = reescribir(widget.getMessage(), tabla); + if (nuevo != null) { + widget.setMessage(nuevo); + } + } +} diff --git a/mod-runtime/comun/reescritor-1165/src/test/java/io/github/consterna/modpackes/ReescritorTest.java b/mod-runtime/comun/reescritor-1165/src/test/java/io/github/consterna/modpackes/ReescritorTest.java new file mode 100644 index 0000000..2fe3ec7 --- /dev/null +++ b/mod-runtime/comun/reescritor-1165/src/test/java/io/github/consterna/modpackes/ReescritorTest.java @@ -0,0 +1,86 @@ +package io.github.consterna.modpackes; + +import net.minecraft.util.text.TextFormatting; +import net.minecraft.util.text.event.ClickEvent; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.IFormattableTextComponent; +import net.minecraft.util.text.Style; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TranslationTextComponent; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReescritorTest { + private static Tabla tabla(String... pares) { // Map.of no existe en Java 8 + Map mapa = new HashMap<>(); + for (int i = 0; i < pares.length; i += 2) { + mapa.put(pares[i], pares[i + 1]); + } + return new Tabla(mapa); + } + + private static final Tabla TABLA = tabla( + "TPA request expired!", "¡La solicitud de TPA expiró!", + "Hello", "Hola", + "Click here", "Haz clic aquí"); + + @Test + void cambiaUnLiteralYConservaElEstilo() { + Style estilo = Style.EMPTY.withColor(TextFormatting.RED).withBold(true); + ITextComponent original = new StringTextComponent("TPA request expired!").setStyle(estilo); + ITextComponent nuevo = Reescritor.reescribir(original, TABLA); + assertEquals("¡La solicitud de TPA expiró!", nuevo.getString()); + assertEquals(estilo, nuevo.getStyle()); + } + + @Test + void sinCoincidenciaDevuelveNullYNoCopia() { + assertNull(Reescritor.reescribir(new StringTextComponent("Nothing to see here"), TABLA)); + assertNull(Reescritor.reescribir(new StringTextComponent("Hello"), Tabla.VACIA)); + assertNull(Reescritor.reescribir(null, TABLA)); + } + + @Test + void recorreLosHermanosYSoloCambiaLosQueCoinciden() { + IFormattableTextComponent raiz = new StringTextComponent(" ").withStyle(TextFormatting.AQUA); + raiz.append(new StringTextComponent("Hello").withStyle(TextFormatting.WHITE)); + raiz.append(new StringTextComponent(" Steve")); + ITextComponent nuevo = Reescritor.reescribir(raiz, TABLA); + assertEquals(" Hola Steve", nuevo.getString()); + assertEquals(TextFormatting.AQUA.getColor(), nuevo.getStyle().getColor().getValue()); + assertEquals(TextFormatting.WHITE.getColor(), nuevo.getSiblings().get(0).getStyle().getColor().getValue()); + assertSame(raiz.getSiblings().get(1), nuevo.getSiblings().get(1)); // lo que no cambia, no se copia + } + + @Test + void conservaElEventoDeClic() { + Style estilo = Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com")); + ITextComponent nuevo = Reescritor.reescribir(new StringTextComponent("Click here").setStyle(estilo), TABLA); + assertEquals("Haz clic aquí", nuevo.getString()); + assertEquals("https://example.com", nuevo.getStyle().getClickEvent().getValue()); + } + + @Test + void cambiaLosArgumentosDeUnTraducible() { + ITextComponent original = new TranslationTextComponent("chat.type.text", new StringTextComponent("Hello"), "Hello"); + ITextComponent nuevo = Reescritor.reescribir(original, TABLA); + TranslationTextComponent contenido = (TranslationTextComponent) nuevo; + assertEquals("chat.type.text", contenido.getKey()); + assertEquals("Hola", ((ITextComponent) contenido.getArgs()[0]).getString()); + assertEquals("Hola", contenido.getArgs()[1]); + } + + @Test + void noInventaTextoConEspaciosYAcentos() { + Tabla t = tabla("Ñandú says hi", "Ñandú dice ¡hola! 😀"); + ITextComponent nuevo = Reescritor.reescribir(new StringTextComponent(" Ñandú says hi "), t); + assertTrue(nuevo.getString().equals(" Ñandú dice ¡hola! 😀 ")); + } +} diff --git a/mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java b/mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java index 2cce7ce..5f2e72c 100644 --- a/mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java +++ b/mod-runtime/comun/tabla/src/main/java/io/github/consterna/modpackes/Tabla.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; @@ -27,7 +28,7 @@ * No hay traducción aproximada ni en vivo. */ public final class Tabla { - public static final Tabla VACIA = new Tabla(Map.of()); + public static final Tabla VACIA = new Tabla(Collections.emptyMap()); private static final Pattern MARCADOR = Pattern.compile("%(?:(\\d+)\\$)?([sd%])"); private static final int MINIMO_FIJO = 4; // una plantilla con casi nada de texto fijo coincidiría con cualquier cosa @@ -36,7 +37,7 @@ public final class Tabla { private final Map exactos; private final List plantillas; - private final Map cache = new LinkedHashMap<>(64, 0.75f, true) { + private final Map cache = new LinkedHashMap(64, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > TAMANO_CACHE; @@ -57,15 +58,15 @@ public Tabla(Map textos) { } } plantillas.sort(Comparator.comparingInt((Plantilla p) -> p.fijo.length()).reversed()); // la más específica primero - this.exactos = Map.copyOf(exactos); - this.plantillas = List.copyOf(plantillas); + this.exactos = Collections.unmodifiableMap(exactos); + this.plantillas = Collections.unmodifiableList(plantillas); } /** Lee un objeto JSON {"inglés": "español"}. Lo que no sea texto se ignora. */ public static Tabla leer(Reader lector) throws IOException { JsonElement raiz; try { - raiz = JsonParser.parseReader(lector); + raiz = new JsonParser().parse(lector); // el único que existe también en el Gson de 1.16.5 } catch (JsonParseException e) { throw new IOException("La tabla de textos no es JSON válido", e); } @@ -122,7 +123,7 @@ public String traducir(String texto) { if (directo != null) { return directo.equals(texto) ? null : directo; } - String nucleo = texto.strip(); + String nucleo = sinBordes(texto); if (nucleo.isEmpty() || nucleo.length() == texto.length()) { return null; } @@ -130,11 +131,33 @@ public String traducir(String texto) { if (traduccion == null || traduccion.equals(nucleo)) { return null; } - int inicio = texto.length() - texto.stripLeading().length(); - int fin = texto.length() - texto.stripTrailing().length(); + int inicio = espaciosAlInicio(texto); + int fin = espaciosAlFinal(texto); return texto.substring(0, inicio) + traduccion + texto.substring(texto.length() - fin); } + // Como String.strip(), String.stripLeading() y String.stripTrailing() de Java 11, que Java 8 no tiene + private static int espaciosAlInicio(String texto) { + int i = 0; + while (i < texto.length() && Character.isWhitespace(texto.charAt(i))) { + i++; + } + return i; + } + + private static int espaciosAlFinal(String texto) { + int i = 0; + while (i < texto.length() && Character.isWhitespace(texto.charAt(texto.length() - 1 - i))) { + i++; + } + return i; + } + + private static String sinBordes(String texto) { + int inicio = espaciosAlInicio(texto); + return inicio == texto.length() ? "" : texto.substring(inicio, texto.length() - espaciosAlFinal(texto)); + } + private String buscar(String texto) { String exacto = exactos.get(texto); if (exacto != null) { @@ -220,7 +243,7 @@ static Plantilla crear(String clave, String valor) { regex.append(Pattern.quote(cola)); } regex.append('$'); - if (grupos == 0 || fijo.toString().strip().length() < MINIMO_FIJO) { + if (grupos == 0 || sinBordes(fijo.toString()).length() < MINIMO_FIJO) { return null; } List destino = destino(valor, grupos); @@ -274,8 +297,8 @@ String aplicar(String texto) { } StringBuilder salida = new StringBuilder(); for (Object parte : destino) { - if (parte instanceof Integer indice) { - salida.append(m.group(indice)); + if (parte instanceof Integer) { + salida.append(m.group((Integer) parte)); } else { salida.append((String) parte); } diff --git a/mod-runtime/forge-1.16.5/.gitignore b/mod-runtime/forge-1.16.5/.gitignore new file mode 100644 index 0000000..96458f6 --- /dev/null +++ b/mod-runtime/forge-1.16.5/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +run/ +out/ +*.iml +.idea/ +eclipse/ diff --git a/mod-runtime/forge-1.16.5/build.gradle b/mod-runtime/forge-1.16.5/build.gradle new file mode 100644 index 0000000..35cd078 --- /dev/null +++ b/mod-runtime/forge-1.16.5/build.gradle @@ -0,0 +1,89 @@ +plugins { + id 'eclipse' + id 'idea' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' +} + +version = mod_version +group = mod_group_id + +base { + archivesName = "${mod_id}-forge-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(8) // Minecraft 1.16.5 corre con Java 8 + +minecraft { + mappings channel: mapping_channel, version: mapping_version + + runs { + client { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + mods { + "${mod_id}" { + source sourceSets.main + } + } + } + } +} + +repositories { + mavenCentral() + maven { url = 'https://libraries.minecraft.net/' } +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2' +} + +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version: forge_version, forge_version_range: forge_version_range, + loader_version_range: loader_version_range, + mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors: mod_authors, mod_description: mod_description, + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + [project: project] + } +} + +tasks.named('jar', Jar).configure { + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : mod_authors, + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : mod_authors + ]) + } + + finalizedBy 'reobfJar' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +// La lógica común vive en ../comun: la tabla es de todos y el reescritor depende de cómo guarda el texto esa versión +sourceSets { + main.java.srcDir '../comun/tabla/src/main/java' + main.java.srcDir '../comun/reescritor-1165/src/main/java' + test.java.srcDir '../comun/tabla/src/test/java' + test.java.srcDir '../comun/reescritor-1165/src/test/java' +} + +tasks.named('test', Test).configure { + useJUnitPlatform() +} diff --git a/mod-runtime/forge-1.16.5/gradle.properties b/mod-runtime/forge-1.16.5/gradle.properties new file mode 100644 index 0000000..1c96c09 --- /dev/null +++ b/mod-runtime/forge-1.16.5/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.16.5 +minecraft_version_range=[1.16.5,1.17) +forge_version=36.2.42 +forge_version_range=[36,37) +loader_version_range=[36,37) +mapping_channel=official +mapping_version=1.16.5 + +mod_id=modpackes_runtime +mod_name=ModpackES Runtime +mod_license=GPL-3.0-or-later +mod_version=0.1.0 +mod_group_id=io.github.consterna.modpackes +mod_authors=Consterna +mod_description=Muestra en español el texto que otros mods escriben en su código y que un resource pack no puede traducir. Solo cambia mensajes de chat, de la barra de acción y tooltips cuyo texto está en la tabla que genera ModpackES. diff --git a/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.jar b/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1962a79e29d3e0ab67b14947c167a862655af9b GIT binary patch literal 62076 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 literal 0 HcmV?d00001 diff --git a/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2617362 --- /dev/null +++ b/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.16.5/gradlew b/mod-runtime/forge-1.16.5/gradlew new file mode 100644 index 0000000..aeb74cb --- /dev/null +++ b/mod-runtime/forge-1.16.5/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mod-runtime/forge-1.16.5/gradlew.bat b/mod-runtime/forge-1.16.5/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/mod-runtime/forge-1.16.5/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mod-runtime/forge-1.16.5/settings.gradle b/mod-runtime/forge-1.16.5/settings.gradle new file mode 100644 index 0000000..93f1b80 --- /dev/null +++ b/mod-runtime/forge-1.16.5/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = 'https://maven.minecraftforge.net/' + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} \ No newline at end of file diff --git a/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java new file mode 100644 index 0000000..1af07f1 --- /dev/null +++ b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java @@ -0,0 +1,155 @@ +package io.github.consterna.modpackes; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.resources.ReloadListener; +import net.minecraft.profiler.IProfiler; +import net.minecraft.resources.IReloadableResourceManager; +import net.minecraft.resources.IResource; +import net.minecraft.resources.IResourceManager; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.ITextComponent; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +/** Lo que solo existe en el cliente: engancha los eventos y carga la tabla. Java 8, como Minecraft 1.16.5. */ +final class Cliente { + private static final Logger LOG = LogManager.getLogger(ModpackESRuntime.ID); + + private static volatile Tabla tabla = Tabla.VACIA; + private static boolean avisado; + + private Cliente() { + } + + static void iniciar() { + IEventBus busDelMod = FMLJavaModLoadingContext.get().getModEventBus(); + busDelMod.addListener(Cliente::alIniciarCliente); + MinecraftForge.EVENT_BUS.addListener(Cliente::alRecibirChat); + MinecraftForge.EVENT_BUS.addListener(Cliente::alCrearTooltip); + MinecraftForge.EVENT_BUS.addListener(Cliente::alIniciarPantalla); + } + + /** En 1.16.5 no hay un evento para registrar recargas del cliente: se hace con el gestor de recursos. */ + private static void alIniciarCliente(FMLClientSetupEvent evento) { + evento.enqueueWork(() -> { + IResourceManager gestor = Minecraft.getInstance().getResourceManager(); + if (gestor instanceof IReloadableResourceManager) { + ((IReloadableResourceManager) gestor).registerReloadListener(new Cargador()); + } + }); + } + + private static void alRecibirChat(ClientChatReceivedEvent evento) { + try { + ITextComponent nuevo = Reescritor.reescribir(evento.getMessage(), tabla); + if (nuevo != null) { + evento.setMessage(nuevo); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alCrearTooltip(ItemTooltipEvent evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + List lineas = evento.getToolTip(); + for (int i = 0; i < lineas.size(); i++) { + ITextComponent nuevo = Reescritor.reescribir(lineas.get(i), actual); + if (nuevo != null) { + lineas.set(i, nuevo); + } + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + private static void alIniciarPantalla(GuiScreenEvent.InitGuiEvent.Post evento) { + try { + Tabla actual = tabla; + if (actual.vacia()) { + return; + } + for (Widget widget : evento.getWidgetList()) { + Reescritor.reescribirMensaje(widget, actual); + } + } catch (RuntimeException | LinkageError e) { + avisar(e); + } + } + + /** Un fallo se anota una sola vez: el juego nunca debe romperse ni llenar el registro por esto. */ + private static synchronized void avisar(Throwable e) { + if (!avisado) { + avisado = true; + LOG.warn("ModpackES Runtime: no pude cambiar un texto; se deja el original", e); + } + } + + /** Variantes de la tabla a probar, en orden. Vacío si el juego no está en español. */ + static List variantes(String codigoDeIdioma) { + if (codigoDeIdioma == null || !codigoDeIdioma.toLowerCase(Locale.ROOT).startsWith("es_")) { + return Collections.emptyList(); + } + String codigo = codigoDeIdioma.toLowerCase(Locale.ROOT); + return new ArrayList<>(new LinkedHashSet<>(Arrays.asList(codigo, "es_mx", "es_es"))); + } + + private static final class Cargador extends ReloadListener { + @Override + protected Tabla prepare(IResourceManager recursos, IProfiler perfil) { + Tabla total = Tabla.VACIA; + for (String variante : variantes(Minecraft.getInstance().options.languageCode)) { + ResourceLocation ruta = new ResourceLocation(ModpackESRuntime.ID, "textos/" + variante + ".json"); + if (!recursos.hasResource(ruta)) { + continue; + } + try { + for (IResource recurso : recursos.getResources(ruta)) { // de menor a mayor prioridad + try (IResource r = recurso; + Reader lector = new InputStreamReader(r.getInputStream(), StandardCharsets.UTF_8)) { + total = total.combinar(Tabla.leer(lector)); + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude leer {} de {}", ruta, recurso.getSourceName(), e); + } + } + } catch (IOException | RuntimeException e) { + LOG.warn("ModpackES Runtime: no pude listar {}", ruta, e); + } + if (!total.vacia()) { + break; + } + } + return total; + } + + @Override + protected void apply(Tabla nueva, IResourceManager recursos, IProfiler perfil) { + tabla = nueva; + LOG.info("ModpackES Runtime: {} textos en la tabla ({} plantillas)", nueva.tamano(), nueva.plantillas()); + } + } +} diff --git a/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java new file mode 100644 index 0000000..843d54c --- /dev/null +++ b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/ModpackESRuntime.java @@ -0,0 +1,32 @@ +package io.github.consterna.modpackes; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ExtensionPoint; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.network.FMLNetworkConstants; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Muestra en español el texto que otros mods escriben en su código. Solo cambia mensajes de chat y tooltips de + * objetos, y el texto de los botones de una pantalla al abrirla, cuyo texto sea exactamente uno de la tabla + * {@code assets/modpackes_runtime/textos/.json} que genera ModpackES (o encaje en una de sus plantillas + * con {@code %s} o {@code %d}). Si el juego no está en una variante de español, no hace nada. + * + *

Esta clase solo decide dónde se carga: todo lo del cliente está en {@link Cliente}, para que un servidor + * dedicado no cargue clases que no tiene. Minecraft 1.16.5 no tiene tooltips por elementos, así que los + * tooltips que no son de objetos no se tocan. + */ +@Mod(ModpackESRuntime.ID) +public final class ModpackESRuntime { + public static final String ID = "modpackes_runtime"; + + public ModpackESRuntime() { + // Que un servidor sin el mod (o un cliente sin él en un servidor) no marque error por esto + ModLoadingContext.get().registerExtensionPoint( + ExtensionPoint.DISPLAYTEST, + () -> Pair.of(() -> FMLNetworkConstants.IGNORESERVERONLY, (version, remoto) -> true)); + DistExecutor.safeRunWhenOn(Dist.CLIENT, () -> Cliente::iniciar); + } +} diff --git a/mod-runtime/forge-1.16.5/src/main/resources/META-INF/mods.toml b/mod-runtime/forge-1.16.5/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..94577b0 --- /dev/null +++ b/mod-runtime/forge-1.16.5/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +# Solo cliente: Forge no lo carga en servidores dedicados y un servidor sin el mod no marca error. +clientSideOnly=true + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] + modId="forge" + mandatory=true + versionRange="${forge_version_range}" + ordering="NONE" + side="CLIENT" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="CLIENT" diff --git a/mod-runtime/forge-1.16.5/src/main/resources/pack.mcmeta b/mod-runtime/forge-1.16.5/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..0575a4a --- /dev/null +++ b/mod-runtime/forge-1.16.5/src/main/resources/pack.mcmeta @@ -0,0 +1,8 @@ +{ + "pack": { + "description": { + "text": "${mod_name}" + }, + "pack_format": 6 + } +} diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 120e434..3fe2a49 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -2,9 +2,9 @@ Uso: uv run python scripts/construir_mod.py [ | todos] [--sin-pruebas] -Sin nombre compila todos. Cada uno necesita su JDK (Forge: 17; NeoForge y Fabric: 21, y Fabric 26.x: 25) y su -Gradle, que se toman de JAVA_HOME (o JAVA_HOME__X64) y de `.herramientas/` si existen, y acceso a -internet la primera vez, para bajar el loader y Minecraft. El resultado queda en +Sin nombre compila todos. Cada uno necesita su JDK (Forge: 17, y 8 para compilar 1.16.5; NeoForge y Fabric: 21, +y Fabric 26.x: 25) y su Gradle, que se toman de JAVA_HOME (o JAVA_HOME__X64) y de `.herramientas/` si +existen, y acceso a internet la primera vez, para bajar el loader y Minecraft. El resultado queda en `src/traductor_es/recursos/mods/`, que Git ignora. En GitHub Actions el jar lo construye el mismo comando (`.github/workflows/ci.yml`). """ @@ -22,6 +22,7 @@ DESTINO = RAIZ / "src" / "traductor_es" / "recursos" / "mods" #: proyecto → (versión de Java, versión de Gradle) que pide su compilación PROYECTOS = { + "forge-1.16.5": (17, "8.8"), "forge-1.18.2": (17, "8.8"), "forge-1.19.2": (17, "8.8"), "forge-1.20.1": (17, "8.8"), @@ -52,12 +53,17 @@ def _herramientas() -> Path: HERRAMIENTAS = _herramientas() +#: Proyectos cuyo código se compila con otro JDK que el que ejecuta Gradle (Forge 1.16.5 corre con Java 8). +TOOLCHAINS = {"forge-1.16.5": 8} + + def _jdk(version: int) -> Path: ejecutable = "java.exe" if os.name == "nt" else "java" candidatos = [Path(os.environ[v]) for v in ("JAVA_HOME", f"JAVA_HOME_{version}_X64") if os.environ.get(v)] - candidatos += sorted(HERRAMIENTAS.glob(f"jdk-{version}*")) + candidatos += sorted(HERRAMIENTAS.glob(f"jdk-{version}*")) + sorted(HERRAMIENTAS.glob(f"jdk{version}u*")) + marca = '"1.8.' if version == 8 else f'"{version}.' # Java 8 se presenta como «1.8.0_x» for c in candidatos: - if (c / "bin" / ejecutable).is_file() and f'"{version}.' in _version_de_java(c / "bin" / ejecutable): + if (c / "bin" / ejecutable).is_file() and marca in _version_de_java(c / "bin" / ejecutable): return c raise SystemExit(f"No encuentro un JDK {version}: define JAVA_HOME o ponlo en .herramientas/jdk-{version}*.") @@ -89,6 +95,8 @@ def construir(nombre: str, sin_pruebas: bool, propiedades: list[str] | None = No # de Minecraft: no se toca ningún jar. orden = [*_gradle(proyecto), "test" if propiedades else "build", "--no-daemon", "--console=plain"] orden += [f"-P{p}" for p in propiedades or []] + if nombre in TOOLCHAINS: # que Gradle encuentre el JDK de compilación sin bajarlo por su cuenta + orden.append(f"-Porg.gradle.java.installations.paths={_jdk(TOOLCHAINS[nombre])}") if sin_pruebas: orden += ["-x", "test"] codigo = subprocess.run(orden, cwd=proyecto, env=entorno, check=False).returncode diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index bc3de04..78ad04b 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -16,6 +16,7 @@ PREFIJO = "modpackes_runtime-" #: (loader, versión de Minecraft) para los que hay mod de apoyo. Cada entrada es un proyecto en `mod-runtime/`. SOPORTADOS: dict[tuple[str, tuple[int, ...]], str] = { + ("forge", (1, 16, 5)): "forge-1.16.5", ("forge", (1, 18, 2)): "forge-1.18.2", ("forge", (1, 19, 2)): "forge-1.19.2", ("forge", (1, 20, 1)): "forge-1.20.1", @@ -67,6 +68,7 @@ "y sin él el mod de apoyo no podría cambiar el chat. Con Fabric 1.19.4 en adelante sí." ) IMPOSIBLES: dict[tuple[str, tuple[int, ...]], str] = { + ("fabric", (1, 16, 5)): _SIN_EVENTO_DE_CHAT, ("fabric", (1, 18, 2)): _SIN_EVENTO_DE_CHAT, ("fabric", (1, 19, 2)): _SIN_EVENTO_DE_CHAT, } diff --git a/src/traductor_es/ui/ventana.py b/src/traductor_es/ui/ventana.py index d68125c..7612ebe 100644 --- a/src/traductor_es/ui/ventana.py +++ b/src/traductor_es/ui/ventana.py @@ -469,7 +469,7 @@ def _opciones_avanzadas(self) -> QWidget: self.c_java.setToolTip( "Algunos mods tienen mensajes escritos en su código y un resource pack no los alcanza. Esto los " "traduce a una tabla que lee el mod de apoyo «ModpackES Runtime» (Más ▸ Instalar el mod de apoyo). " - "Por ahora existe para Forge 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, " + "Por ahora existe para Forge 1.16.5, 1.18.2, 1.19.2 y 1.20.1, NeoForge 1.20.1 y de 1.21 a 1.21.11, " "y Fabric 1.19.4, de 1.20 a 1.20.6, de 1.21 a 1.21.11 y de 26.1 a 26.3. En Quilt usa los mods de Fabric." ) self.c_java.setChecked(bool(self._preferencias.get("texto_java", False))) diff --git a/tests/test_runtime.py b/tests/test_runtime.py index b305b90..959fee1 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -155,7 +155,9 @@ def test_mod_de_apoyo_solo_para_lo_soportado(tmp_path: Path) -> None: ("26.1", "neoforge", None), ("1.20.1", "neoforge", "forge-1.20.1"), # NeoForge 1.20.1 carga los mods de Forge ("1.12.2", "forge", None), - ("1.16.5", "forge", None), + ("1.16.5", "forge", "forge-1.16.5"), + ("1.16.5", "fabric", None), + ("1.17.1", "forge", None), ("1.19.4", "forge", None), ] for i, (version, loader, esperado) in enumerate(casos): @@ -179,10 +181,10 @@ def test_instalar_reemplaza_versiones_y_respeta_otros_mods(tmp_path: Path) -> No def test_instalar_con_errores_claros(tmp_path: Path) -> None: - juego = fabrica.prism(tmp_path / "a", "1.16.5", "forge") + juego = fabrica.prism(tmp_path / "a", "1.17.1", "forge") with Instancia(juego) as inst, pytest.raises(ValueError, match="Todavía no hay mod de apoyo"): mod_apoyo.instalar(inst, tmp_path) - for i, version in enumerate(("1.18.2", "1.19.2")): # se dice por qué no, no solo «todavía» + for i, version in enumerate(("1.16.5", "1.18.2", "1.19.2")): # se dice por qué no, no solo «todavía» imposible = fabrica.prism(tmp_path / f"c{i}", version, "fabric") with Instancia(imposible) as inst, pytest.raises(ValueError, match="no tiene en esa versión un evento"): mod_apoyo.instalar(inst, tmp_path) From a63ef5ad50b676f5a0a676fe1c52f321295604b8 Mon Sep 17 00:00:00 2001 From: Cristian Date: Mon, 9 Nov 2026 00:29:25 -0500 Subject: [PATCH 21/24] =?UTF-8?q?fix(runtime):=20registrar=20la=20recarga?= =?UTF-8?q?=20de=201.16.5=20a=20tiempo=20y=20cuidar=20la=20instalaci=C3=B3?= =?UTF-8?q?n=20del=20mod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En Forge 1.16.5 FMLClientSetupEvent se dispara durante la primera recarga de recursos, así que un listener registrado ahí quedaba fuera de ella y la tabla no se cargaba hasta recargar los recursos. Ahora se registra al construir el mod. Instalar el mod de apoyo copia el nuevo antes de borrar los viejos, para no dejar al jugador sin mod si la copia falla, y el estado avisa cuando el jar instalado es de otra versión de Minecraft en vez de decir que todo está listo. En el CI Java 17 vuelve a ser el predeterminado tras instalar Java 8 y 25. Una prueba nueva comprueba que cada versión de la tabla cae dentro del rango que declara su proyecto, que ningún proyecto queda sin usar y que construir_mod.py los conoce todos. --- .github/workflows/ci.yml | 8 +- .../github/consterna/modpackes/Cliente.java | 24 ++-- src/traductor_es/salida/mod_apoyo.py | 10 +- tests/test_mod_apoyo_coherencia.py | 122 ++++++++++++++++++ tests/test_runtime.py | 26 ++++ 5 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 tests/test_mod_apoyo_coherencia.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b37967e..8bddd07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,19 +55,19 @@ jobs: - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin - java-version: "17" # ForgeGradle 6 (Forge 1.18.2, 1.19.2 y 1.20.1) pide Java 17 + java-version: "25" # Minecraft 26.x (Fabric 26.1) pide Java 25 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin - java-version: "25" # Minecraft 26.x (Fabric 26.1) pide Java 25 + java-version: "8" # Minecraft 1.16.5 (Forge 1.16.5) se compila con Java 8 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin - java-version: "8" # Minecraft 1.16.5 (Forge 1.16.5) se compila con Java 8 + java-version: "17" # ForgeGradle 6 (Forge 1.18.2, 1.19.2 y 1.20.1) pide Java 17 - # Con las cuatro versiones instaladas (8, 17 y 25 aquí, 21 más arriba), el script elige la que pide cada mod + # Con las cuatro versiones instaladas (25, 8 y 17 aquí, 21 más arriba; la última, 17, es la predeterminada), el script elige la que pide cada mod # (Forge 1.18.2, 1.19.2 y 1.20.1: Java 17; NeoForge 1.21.1 y Fabric hasta 1.21.11: Java 21; Fabric 26.1: Java 25; Forge 1.16.5 compila su código con Java 8) # por las variables JAVA_HOME_8_X64, JAVA_HOME_17_X64, JAVA_HOME_21_X64 y JAVA_HOME_25_X64. - name: Compilar los mods de apoyo (ModpackES Runtime) con sus pruebas diff --git a/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java index 1af07f1..3f2bd5a 100644 --- a/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java +++ b/mod-runtime/forge-1.16.5/src/main/java/io/github/consterna/modpackes/Cliente.java @@ -13,9 +13,6 @@ import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.player.ItemTooltipEvent; -import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -40,24 +37,21 @@ final class Cliente { private Cliente() { } + /** + * Se llama al construir el mod, antes de la primera recarga de recursos. En 1.16.5 no hay un evento para registrar + * recargas del cliente y {@code FMLClientSetupEvent} se dispara durante esa primera recarga: un listener + * registrado ahí queda fuera y la tabla no se cargaría hasta que el jugador recargara los recursos. + */ static void iniciar() { - IEventBus busDelMod = FMLJavaModLoadingContext.get().getModEventBus(); - busDelMod.addListener(Cliente::alIniciarCliente); + IResourceManager gestor = Minecraft.getInstance().getResourceManager(); + if (gestor instanceof IReloadableResourceManager) { + ((IReloadableResourceManager) gestor).registerReloadListener(new Cargador()); + } MinecraftForge.EVENT_BUS.addListener(Cliente::alRecibirChat); MinecraftForge.EVENT_BUS.addListener(Cliente::alCrearTooltip); MinecraftForge.EVENT_BUS.addListener(Cliente::alIniciarPantalla); } - /** En 1.16.5 no hay un evento para registrar recargas del cliente: se hace con el gestor de recursos. */ - private static void alIniciarCliente(FMLClientSetupEvent evento) { - evento.enqueueWork(() -> { - IResourceManager gestor = Minecraft.getInstance().getResourceManager(); - if (gestor instanceof IReloadableResourceManager) { - ((IReloadableResourceManager) gestor).registerReloadListener(new Cargador()); - } - }); - } - private static void alRecibirChat(ClientChatReceivedEvent evento) { try { ITextComponent nuevo = Reescritor.reescribir(evento.getMessage(), tabla); diff --git a/src/traductor_es/salida/mod_apoyo.py b/src/traductor_es/salida/mod_apoyo.py index 78ad04b..095a869 100644 --- a/src/traductor_es/salida/mod_apoyo.py +++ b/src/traductor_es/salida/mod_apoyo.py @@ -137,6 +137,12 @@ def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: ) puestos = instalados(instancia) lineas.append(f"En mods/: {', '.join(p.name for p in puestos)}." if puestos else "No está instalado en mods/.") + ajenos = [p.name for p in puestos if not p.name.startswith(f"{PREFIJO}{destino}-")] + if ajenos: + lineas.append( + f"Ojo: {', '.join(ajenos)} no es el de esta versión ({destino}). " + "Pulsa «Instalar el mod de apoyo» para cambiarlo." + ) tablas = sorted( (instancia.juego / "resourcepacks" / "Traduccion_ES" / "assets" / "modpackes_runtime" / "textos").glob("*.json") ) @@ -145,7 +151,7 @@ def estado(instancia: Instancia, carpeta_jars: Path | None = None) -> list[str]: if tablas else "Todavía no hay tabla de textos: traduce con la opción activada." ) - if puestos and tablas: + if puestos and not ajenos and tablas: lineas.append("Todo listo: abre el juego en español.") return lineas @@ -162,10 +168,10 @@ def instalar(instancia: Instancia, carpeta_jars: Path | None = None) -> Path: ) instancia.carpeta_mods.mkdir(parents=True, exist_ok=True) final = instancia.carpeta_mods / origen.name + shutil.copyfile(origen, final) # primero el nuevo: si la copia falla, el mod que había sigue ahí for viejo in instalados(instancia): if viejo != final: viejo.unlink() - shutil.copyfile(origen, final) return final diff --git a/tests/test_mod_apoyo_coherencia.py b/tests/test_mod_apoyo_coherencia.py new file mode 100644 index 0000000..03e1620 --- /dev/null +++ b/tests/test_mod_apoyo_coherencia.py @@ -0,0 +1,122 @@ +"""La tabla de versiones de la app, los proyectos de `mod-runtime/` y los rangos que declaran tienen que coincidir. + +Si se añade una versión a `SOPORTADOS` sin que su proyecto la acepte, el jugador instalaría un mod que el juego +rechaza al abrirse; y un proyecto que ninguna entrada usa es un mod que nadie recibe. Esto lo vigila sin necesidad de +compilar nada. +""" + +from __future__ import annotations + +import importlib.util +import re +from pathlib import Path +from types import ModuleType + +import pytest + +from traductor_es.fuentes.version import Version +from traductor_es.salida.mod_apoyo import IMPOSIBLES, SOPORTADOS + +RAIZ = Path(__file__).resolve().parent.parent +PROYECTOS = RAIZ / "mod-runtime" +#: Quilt carga el jar de Fabric, y NeoForge 1.20.1 (un fork de Forge) el de Forge 1.20.1. +FAMILIA_DEL_JAR = {"forge": "forge", "neoforge": "neoforge", "fabric": "fabric", "quilt": "fabric"} + + +def _propiedades(proyecto: str) -> dict[str, str]: + texto = (PROYECTOS / proyecto / "gradle.properties").read_text(encoding="utf-8") + pares = (linea.split("=", 1) for linea in texto.splitlines() if "=" in linea and not linea.startswith("#")) + return {k.strip(): v.strip() for k, v in pares} + + +def _rango(proyecto: str) -> str: + """El rango de Minecraft que declara el proyecto, tal cual (Fabric: `>=a <=b`; Forge y NeoForge: `[a,b)`).""" + p = _propiedades(proyecto) + return p.get("minecraft_range") or p.get("minecraft_version_range") or p["minecraft_version"] + + +def _acepta(rango: str, version: Version) -> bool: + rango = rango.strip() + if rango.startswith(("[", "(")): # intervalo de Maven: [1.21,1.21.4) · [1.21.11] + abre, cierra = rango[0], rango[-1] + extremos = [e.strip() for e in rango[1:-1].split(",")] + if len(extremos) == 1: + return version == Version.de(extremos[0]) + inferior = Version.de(extremos[0]) + ok = version >= inferior if abre == "[" else version > inferior + if extremos[1]: + superior = Version.de(extremos[1]) + ok = ok and (version <= superior if cierra == "]" else version < superior) + return ok + if rango.startswith((">=", "<=", ">", "<")): # rango de Fabric: >=1.20 <=1.20.2 + for condicion in rango.split(): + m = re.fullmatch(r"(>=|<=|>|<)(.+)", condicion) + assert m, condicion + limite = Version.de(m.group(2)) + if not {">=": version >= limite, "<=": version <= limite, ">": version > limite, "<": version < limite}[ + m.group(1) + ]: + return False + return True + return version == Version.de(rango) # una sola versión + + +def _proyectos_de_mod_runtime() -> list[str]: + return sorted(p.name for p in PROYECTOS.iterdir() if (p / "build.gradle").is_file()) + + +def _construir_mod() -> ModuleType: + spec = importlib.util.spec_from_file_location("construir_mod", RAIZ / "scripts" / "construir_mod.py") + assert spec and spec.loader + modulo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(modulo) + return modulo + + +def test_cada_destino_de_la_tabla_es_un_proyecto_que_se_construye() -> None: + destinos = set(SOPORTADOS.values()) + proyectos = set(_proyectos_de_mod_runtime()) + assert destinos <= proyectos, f"destinos sin proyecto en mod-runtime/: {sorted(destinos - proyectos)}" + construibles = set(_construir_mod().PROYECTOS) + assert construibles == proyectos, ( + f"proyectos que construir_mod.py no conoce o que ya no existen: {sorted(construibles ^ proyectos)}" + ) + + +def test_ningun_proyecto_queda_sin_usar() -> None: + sin_usar = set(_proyectos_de_mod_runtime()) - set(SOPORTADOS.values()) + assert not sin_usar, f"proyectos que ninguna versión de la tabla recibe: {sorted(sin_usar)}" + + +@pytest.mark.parametrize(("clave", "destino"), sorted(SOPORTADOS.items())) +def test_la_version_cae_en_el_rango_de_su_proyecto(clave: tuple[str, tuple[int, ...]], destino: str) -> None: + loader, partes = clave + version = Version.de(".".join(str(n) for n in partes)) + rango = _rango(destino) + assert _acepta(rango, version), f"{loader} {version} usa {destino}, que solo acepta {rango}" + familia_del_jar = destino.split("-", maxsplit=1)[0] + esperado = {FAMILIA_DEL_JAR[loader]} + if (loader, partes) == ("neoforge", (1, 20, 1)): + esperado = {"forge"} # NeoForge 1.20.1 carga los mods de Forge 1.20.1 + assert familia_del_jar in esperado, f"{loader} {version} no puede usar el jar {destino}" + + +def test_lo_imposible_no_esta_tambien_soportado() -> None: + assert not set(IMPOSIBLES) & set(SOPORTADOS) + + +@pytest.mark.parametrize( + ("rango", "version", "esperado"), + [ + (">=1.20 <=1.20.2", "1.20.1", True), + (">=1.20 <=1.20.2", "1.20.3", False), + ("[1.21,1.21.4)", "1.21.3", True), + ("[1.21,1.21.4)", "1.21.4", False), + ("[1.21.11]", "1.21.11", True), + ("[1.21.11]", "1.21.10", False), + ("1.19.4", "1.19.4", True), + ("1.19.4", "1.19.3", False), + ], +) +def test_el_lector_de_rangos(rango: str, version: str, esperado: bool) -> None: + assert _acepta(rango, Version.de(version)) is esperado diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 959fee1..3cb3276 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -269,3 +269,29 @@ def test_estado_en_neoforge_1_20_1_avisa_que_es_el_mod_de_forge(tmp_path: Path) with Instancia(juego) as inst: texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) assert "forge-1.20.1" in texto and "fork de Forge" in texto and "no se ha probado" in texto + + +def test_estado_avisa_si_el_jar_instalado_es_de_otra_version(tmp_path: Path) -> None: + juego = fabrica.prism(tmp_path, "1.21.1", "fabric") + (juego / "mods" / "modpackes_runtime-fabric-1.20.1-0.1.0.jar").write_bytes(b"viejo") + pack = juego / "resourcepacks" / "Traduccion_ES" / "assets" / "modpackes_runtime" / "textos" + pack.mkdir(parents=True) + (pack / "es_mx.json").write_text("{}", encoding="utf-8") + with Instancia(juego) as inst: + texto = " ".join(mod_apoyo.estado(inst, tmp_path / "vacia")) + assert "no es el de esta versión (fabric-1.21.1)" in texto and "Todo listo" not in texto + + +def test_instalar_no_borra_el_mod_viejo_si_la_copia_falla(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + juego = fabrica.prism(tmp_path, "1.20.1", "forge") + viejo = juego / "mods" / "modpackes_runtime-forge-1.20.1-0.0.9.jar" + viejo.write_bytes(b"viejo") + carpeta = _jars(tmp_path, "modpackes_runtime-forge-1.20.1-0.1.0.jar") + + def copia_que_falla(*_a: object, **_k: object) -> None: + raise OSError("disco lleno") + + monkeypatch.setattr(mod_apoyo.shutil, "copyfile", copia_que_falla) + with Instancia(juego) as inst, pytest.raises(OSError, match="disco lleno"): + mod_apoyo.instalar(inst, carpeta) + assert viejo.read_bytes() == b"viejo" # el que ya funcionaba sigue ahí From bbb5c27fe0a372670a3e6e34b5d1cd6c7b1bb7eb Mon Sep 17 00:00:00 2001 From: Cristian Date: Mon, 9 Nov 2026 00:52:59 -0500 Subject: [PATCH 22/24] =?UTF-8?q?fix(ci):=20dar=20m=C3=A1s=20tiempo=20a=20?= =?UTF-8?q?la=20descarga=20de=20Gradle=20en=20los=20proyectos=20de=20mod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El wrapper de cada proyecto de mod-runtime descarga su propia distribución de Gradle con un tiempo máximo de lectura de 10 segundos, y en el CI `services.gradle.org` tardó más dos veces seguidas («Read timed out») al empezar el primer proyecto. Se sube a 120 segundos en los 17 proyectos. --- .../fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties | 2 +- .../fabric-26.1/gradle/wrapper/gradle-wrapper.properties | 2 +- .../forge-1.16.5/gradle/wrapper/gradle-wrapper.properties | 2 +- .../forge-1.18.2/gradle/wrapper/gradle-wrapper.properties | 2 +- .../forge-1.19.2/gradle/wrapper/gradle-wrapper.properties | 2 +- .../forge-1.20.1/gradle/wrapper/gradle-wrapper.properties | 2 +- .../neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties | 2 +- .../neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties | 2 +- .../neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties | 2 +- .../neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.19.4/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.20.1/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.20.4/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.20.6/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.21.1/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.21.11/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.21.4/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-1.21.5/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.properties index 91c4fc6..f69664a 100644 --- a/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/fabric-26.1/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip -networkTimeout=10000 +networkTimeout=120000 retries=0 retryBackOffMs=500 validateDistributionUrl=true diff --git a/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties index 2617362..de5ba6d 100644 --- a/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/forge-1.16.5/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 +networkTimeout=120000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties index 2617362..de5ba6d 100644 --- a/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/forge-1.18.2/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 +networkTimeout=120000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties index 2617362..de5ba6d 100644 --- a/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/forge-1.19.2/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 +networkTimeout=120000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties index 2617362..de5ba6d 100644 --- a/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/forge-1.20.1/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip -networkTimeout=10000 +networkTimeout=120000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties index 23449a2..e1a5ec8 100644 --- a/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/neoforge-1.21.1/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 +networkTimeout=120000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties index 23449a2..e1a5ec8 100644 --- a/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/neoforge-1.21.11/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 +networkTimeout=120000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties index 23449a2..e1a5ec8 100644 --- a/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/neoforge-1.21.4/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 +networkTimeout=120000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties b/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties index 23449a2..e1a5ec8 100644 --- a/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties +++ b/mod-runtime/neoforge-1.21.5/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 +networkTimeout=120000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d82b5da9e77f133eba2717e5551740450046d7a0 Mon Sep 17 00:00:00 2001 From: Cristian Date: Mon, 9 Nov 2026 01:52:51 -0500 Subject: [PATCH 23/24] =?UTF-8?q?fix(ci):=20reintentar=20la=20compilaci?= =?UTF-8?q?=C3=B3n=20de=20los=20mods=20cuando=20falla=20la=20red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El CI falló tres veces seguidas al bajar la distribución de Gradle (dos «Read timed out» y un HTTP 504 de GitHub), sin que el código tuviera nada que ver. Subir el tiempo de espera del wrapper arregló el primer caso; esto cubre el resto: `construir_mod.py` repite Gradle hasta 3 veces, pero solo si lo que falló es la red. Un error de compilación o de una prueba se devuelve a la primera, sin reintento. Se anotan en el CHANGELOG el kit de servidor con datapacks comprimidos y los cambios de instalación del mod, y en CONTRIBUTING los reintentos y la prueba de coherencia de versiones. --- CHANGELOG.md | 3 ++- CONTRIBUTING.md | 2 ++ scripts/construir_mod.py | 34 +++++++++++++++++++++++++++- tests/test_mod_apoyo_coherencia.py | 36 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed9e308..bcb8c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,8 @@ Todos los cambios importantes de ModpackES quedan en este archivo. El formato si ### Cambiado -- El kit para servidores incluye las funciones y los logros traducidos dondequiera que estén. +- El kit para servidores incluye las funciones y los logros traducidos dondequiera que estén, también los de un datapack comprimido (`.zip`); los `.zip` que solo traen tips siguen fuera porque son de cliente. +- «Instalar el mod de apoyo» copia el mod nuevo antes de borrar los viejos, para no dejarte sin mod si la copia falla, y «Estado del mod de apoyo» avisa cuando el jar instalado es de otra versión de Minecraft en lugar de decir que todo está listo. ## [2.2.0] - 2026-11-04 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a19fb7..787a299 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,6 +101,8 @@ Cada carpeta de `mod-runtime/` (`forge-1.16.5`, `forge-1.18.2`, `forge-1.19.2`, uv run python scripts/construir_mod.py [forge-1.16.5 | forge-1.18.2 | forge-1.19.2 | forge-1.20.1 | neoforge-1.21.1 | neoforge-1.21.4 | neoforge-1.21.5 | neoforge-1.21.11 | fabric-1.19.4 | fabric-1.20.1 | fabric-1.20.4 | fabric-1.20.6 | fabric-1.21.1 | fabric-1.21.4 | fabric-1.21.5 | fabric-1.21.11 | fabric-26.1] ``` +Si Gradle falla por la red (por ejemplo, un «Read timed out» o un HTTP 5xx al bajar su distribución o Minecraft), el script lo reintenta hasta 3 veces; un error del código o de una prueba no se reintenta. El wrapper de cada proyecto espera hasta 120 s en esas descargas (`networkTimeout`). `tests/test_mod_apoyo_coherencia.py` comprueba que la tabla `SOPORTADOS`, los proyectos de `mod-runtime/` y los rangos de versiones que declaran coincidan, sin compilar nada. + Corre las pruebas de Java (JUnit) y deja el jar en `src/traductor_es/recursos/mods/` (Git lo ignora; el CI lo construye y `construir_exe.py` lo incluye). Un loader o versión nuevo es un proyecto nuevo en `mod-runtime/` (copia el más cercano y lee el código real de esa versión: los nombres de las clases y los eventos cambian), una entrada en `PROYECTOS` de `scripts/construir_mod.py` y otra en `SOPORTADOS` de `salida/mod_apoyo.py`. Un mismo jar puede servir para varias versiones vecinas si el mismo código compila y pasa las pruebas contra cada una: `uv run python scripts/construir_mod.py fabric-1.21.5 --propiedad minecraft_version=1.21.8 --propiedad fabric_api_version=0.136.1+1.21.8 --propiedad loader_version=0.16.13` lo comprueba sin generar jar, y el rango comprobado va en `minecraft_range` del proyecto y en `SOPORTADOS`. Quilt no tiene proyectos propios: carga los mods de Fabric con Quilted Fabric API, y `_VERSIONES_DE_QUILT` lista las versiones donde esa API existe. Si una versión no puede tener mod, se anota en `IMPOSIBLES` con el motivo (Fabric 1.16.5, 1.18.2 y 1.19.2 no tienen evento de chat del cliente). ## Licencia de las contribuciones diff --git a/scripts/construir_mod.py b/scripts/construir_mod.py index 3fe2a49..ddf89c0 100644 --- a/scripts/construir_mod.py +++ b/scripts/construir_mod.py @@ -16,6 +16,7 @@ import shutil import subprocess import sys +import time from pathlib import Path RAIZ = Path(__file__).resolve().parent.parent @@ -80,6 +81,37 @@ def _gradle(proyecto: Path) -> list[str]: return [str(local)] if local.is_file() else [str(envoltorio)] # el de .herramientas si está; si no, el del proyecto +#: Lo que dice Gradle cuando falla la red al bajar su distribución, el loader o Minecraft (y no el código del mod). +_FALLOS_DE_RED = ( + "Read timed out", "Connect timed out", "Connection reset", "Premature end of Content-Length", + "Server returned HTTP response code: 5", "Could not GET", "UnknownHostException", "Host desconocido", +) # fmt: skip +INTENTOS = 3 + + +def _ejecutar(orden: list[str], proyecto: Path, entorno: dict[str, str], espera: float = 20.0) -> int: + """Corre Gradle y repite hasta 3 veces solo si el fallo es de red; un error del código se devuelve a la primera.""" + codigo = 1 + for intento in range(1, INTENTOS + 1): + salida: list[str] = [] + with subprocess.Popen( + orden, cwd=proyecto, env=entorno, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding="utf-8", errors="replace", + ) as proceso: # fmt: skip + assert proceso.stdout is not None + for linea in proceso.stdout: + sys.stdout.buffer.write(linea.encode("utf-8", "replace")) + sys.stdout.buffer.flush() + salida.append(linea) + codigo = proceso.returncode + texto = "".join(salida) + if codigo == 0 or intento == INTENTOS or not any(f in texto for f in _FALLOS_DE_RED): + return codigo + print(f"{proyecto.name}: falló la red; reintento {intento + 1} de {INTENTOS} en {espera:.0f} s.", flush=True) + time.sleep(espera) + return codigo + + def construir(nombre: str, sin_pruebas: bool, propiedades: list[str] | None = None) -> int: proyecto = RAIZ / "mod-runtime" / nombre jdk = _jdk(PROYECTOS[nombre][0]) @@ -99,7 +131,7 @@ def construir(nombre: str, sin_pruebas: bool, propiedades: list[str] | None = No orden.append(f"-Porg.gradle.java.installations.paths={_jdk(TOOLCHAINS[nombre])}") if sin_pruebas: orden += ["-x", "test"] - codigo = subprocess.run(orden, cwd=proyecto, env=entorno, check=False).returncode + codigo = _ejecutar(orden, proyecto, entorno) if codigo or propiedades: return codigo jars = sorted( diff --git a/tests/test_mod_apoyo_coherencia.py b/tests/test_mod_apoyo_coherencia.py index 03e1620..5598932 100644 --- a/tests/test_mod_apoyo_coherencia.py +++ b/tests/test_mod_apoyo_coherencia.py @@ -120,3 +120,39 @@ def test_lo_imposible_no_esta_tambien_soportado() -> None: ) def test_el_lector_de_rangos(rango: str, version: str, esperado: bool) -> None: assert _acepta(rango, Version.de(version)) is esperado + + +# ------------------------------------------------------------------ reintentos de red al compilar +def _comando(tmp_path: Path, mensaje: str, falla_la_primera_vez: bool) -> list[str]: + """Un «Gradle» de mentira: escribe `mensaje` y falla (siempre, o solo la primera vez).""" + import sys + + guion = tmp_path / "falso.py" + guion.write_text( + "import pathlib, sys\n" + f"marca = pathlib.Path({str(tmp_path / 'marca')!r})\n" + f"print({mensaje!r})\n" + f"if {falla_la_primera_vez!r} and marca.exists():\n sys.exit(0)\n" + "marca.write_text('x')\nsys.exit(1)\n", + encoding="utf-8", + ) + return [sys.executable, str(guion)] + + +def test_un_fallo_de_red_se_reintenta(tmp_path: Path) -> None: + construir = _construir_mod() + orden = _comando(tmp_path, "java.net.SocketTimeoutException: Read timed out", falla_la_primera_vez=True) + assert construir._ejecutar(orden, tmp_path, dict(__import__("os").environ), espera=0) == 0 + + +def test_un_error_del_codigo_no_se_reintenta(tmp_path: Path) -> None: + construir = _construir_mod() + orden = _comando(tmp_path, "error: cannot find symbol", falla_la_primera_vez=True) + assert construir._ejecutar(orden, tmp_path, dict(__import__("os").environ), espera=0) == 1 + assert (tmp_path / "marca").exists() # corrió una sola vez: no hubo segundo intento que lo hiciera pasar + + +def test_un_fallo_de_red_que_no_se_arregla_termina_tras_tres_intentos(tmp_path: Path) -> None: + construir = _construir_mod() + orden = _comando(tmp_path, "Server returned HTTP response code: 504", falla_la_primera_vez=False) + assert construir._ejecutar(orden, tmp_path, dict(__import__("os").environ), espera=0) == 1 From cec841b5033810dd150873f1921c35db0331fe94 Mon Sep 17 00:00:00 2001 From: Cristian Date: Mon, 9 Nov 2026 02:47:54 -0500 Subject: [PATCH 24/24] test(corpus): bajar y probar los datapacks de resources/ y de los mundos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al revisar las 6 pruebas del corpus que se saltaban, 3 tenían datos de texto reales que la descarga no guardaba: logros de datapacks en la carpeta `resources/` de Legacies, los datapacks de cada mundo en `saves/` y los de `config/paxi` de dos modpacks que se bajaron antes de que esa carpeta entrara en la descarga. `descargar_corpus.py` ahora baja la parte `data/` de `resources/` y los datapacks de `saves/`, y la prueba del corpus copia esas dos carpetas. Con los cuatro modpacks vueltos a bajar, las pruebas pasan sobre ellos (JSON y zips válidos, repetir no cambia nada, restaurar devuelve los bytes originales) y quedan 3 saltadas, todas justificadas: modpacks sin texto para el jugador. --- CONTRIBUTING.md | 5 +++++ scripts/descargar_corpus.py | 7 +++++++ tests/test_corpus_datos.py | 11 ++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 787a299..cd5e288 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,6 +92,11 @@ cómo saca los textos y cómo los reescribe), cita su documentación en un comen traduzca un caso real recortado, otra con algo que **no** debe tocarse y la de ida y vuelta (restaurar devuelve los bytes originales). `uv run python scripts/descargar_corpus.py` baja partes de modpacks públicos de Modrinth a `.descargas/` (Git lo ignora); `uv run pytest -m corpus` los usa para comprobar que lo traducido sigue siendo válido. +Solo se baja lo que lleva texto (misiones, scripts, `kubejs/data`, `global_packs`, `datapacks`, `config/openloader`, `config/paxi`, +la parte `data/` de `resources/` y los datapacks de cada mundo en `saves/`): si añades una carpeta nueva a `descargar_corpus.py`, +añádela también a `CARPETAS` en `tests/test_corpus_datos.py` y vuelve a bajar los modpacks que ya tenías, que no la traen. +Las 3 pruebas del corpus que se saltan son modpacks sin texto para el jugador (`technical-electrical`, `supernova-empires-2` y +los datapacks de `better-mc-neoforge-bmc5`). ## El mod de apoyo (Java) diff --git a/scripts/descargar_corpus.py b/scripts/descargar_corpus.py index 7202f1c..95aeb25 100644 --- a/scripts/descargar_corpus.py +++ b/scripts/descargar_corpus.py @@ -83,6 +83,10 @@ "overrides/config/paxi/", ) EXTENSIONES_KUBEJS_ASSETS = ("/lang/",) # de kubejs/assets solo los idiomas +#: Otros sitios donde viven datapacks: la carpeta `resources/` de algunos packs (Legacies) y los de cada mundo. +#: Se baja solo la parte de datos de texto, no texturas ni el mundo entero. +DATOS_EN = {"overrides/resources/": ("/data/",), "overrides/saves/": ("/datapacks/",)} +EXTENSIONES_DE_DATOS = (".json", ".mcfunction", ".snbt", ".zip") LIMITE_POR_PACK = 40 * 1024 * 1024 @@ -154,6 +158,9 @@ def _elegida(nombre: str) -> bool: return True if nombre.startswith(("overrides/kubejs/assets/", "overrides/resourcepacks/")) and nombre.endswith(".json"): return any(x in nombre for x in (*EXTENSIONES_KUBEJS_ASSETS, "/tips/")) + for inicio, marcas in DATOS_EN.items(): + if nombre.startswith(inicio) and nombre.endswith(EXTENSIONES_DE_DATOS): + return any(m in nombre for m in marcas) return False diff --git a/tests/test_corpus_datos.py b/tests/test_corpus_datos.py index 0ef7aba..786575e 100644 --- a/tests/test_corpus_datos.py +++ b/tests/test_corpus_datos.py @@ -39,7 +39,16 @@ Path(os.environ.get("APPDATA", Path.home())) / "PrismLauncher" / "instances" / "Farming Crossing 4(1)" / "minecraft" ) #: Lo que se copia de cada modpack: donde viven los datos de texto y los scripts. -CARPETAS = ("kubejs", "global_packs", "datapacks", "config/openloader", "config/paxi", "resourcepacks") +CARPETAS = ( + "kubejs", + "global_packs", + "datapacks", + "config/openloader", + "config/paxi", + "resourcepacks", + "resources", + "saves", +) LOADERS = {"forge": "forge", "neoforge": "neoforge", "fabric-loader": "fabric", "quilt-loader": "quilt"}