Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
MechFix
Acelera la carga de lunas en modpacks pesados de Lethal Company. DunGen genera por frames: MechFix libera frames durante la carga y recorta el spam de log que se los comia.
CHANGELOG
Changelog
0.7.1 — el tope de tamaño, ahora sobre el campo correcto
La v0.7.0 topeaba LengthMultiplier de DunGen. El log dijo que no servía:
tamaño de dungeon: 0.75 (ya por debajo del tope 1.20, sin cambios)
Ese 0.75 es el valor base del generador, no el multiplicador de la luna. El número que importa es
el que LLL reporta como DungeonSize Is: 2.2, y vive en SelectableLevel.factorySizeMultiplier.
Ahora se topea ahí, con el valor por defecto en 1.8.
tamaño de dungeon: 2.20 -> 1.80 (recorte del 18%)
⚠️ En cooperativo, los 4 jugadores necesitan el MISMO valor
Esta opción es distinta al resto de MechFix: no cambia solo el rendimiento, cambia la geometría generada. DunGen arma el mapa a partir de la semilla y del tamaño. Si un jugador usa 1.8 y otro 2.2, con la misma semilla generan mapas distintos y la partida se desincroniza.
El resto de las opciones —gráficos, sombras, vSync, bloqueo de log— son locales e inofensivas si no coinciden. Esta no.
0.7.0 — el tope de tamaño, ahora dentro de MechFix
🔴 La restricción de tamaño de LethalLevelLoader hacía lo contrario de lo que dice
Se configuró Enable Dynamic Dungeon Size Restriction = true, Maximum = 1.2, Scaler = 0,
que según el texto del propio config debería topear el tamaño. El log:
Ultimatum DungeonSize Is: 2.2 | Overriding DungeonSize To: 3.666667
Cabal DungeonSize Is: 1.65 | Overriding DungeonSize To: 2.475
El dungeon creció un 67 %. Consecuencia medida: la generación pasó de ~21 s a 75-85 s y los FPS durante la carga se hundieron hasta 1.5.
No se reintenta por ese camino. Ahora el tope se aplica sobre el LengthMultiplier del generador
de DunGen, justo antes de colocar el primer tile, y el log registra el antes y el después:
tamaño de dungeon: 2.20 -> 1.20 (recorte del 45%)
Si alguna vez vuelve a invertirse, se ve en la primera partida en lugar de descubrirse por un tiempo de carga raro.
⚠️ Desactivá la restricción de LLL si la tenías puesta. Las dos a la vez se pelean.
Esta es la única opción de MechFix que cambia la jugabilidad —mapas más chicos— así que es
explícita y -1 la desactiva.
El recolector de basura queda descartado
La instrumentación de la v0.5.0 hizo su trabajo. Frames lentos con sus contadores:
frame lento 3824ms GC gen0 +0 gen2 +0
frame lento 2410ms GC gen0 +0 gen2 +0
frame lento 1063ms GC gen0 +0 gen2 +0
Frames de casi cuatro segundos con cero recolecciones. El GC no es la causa. pauseGCDuringLoad
se mantiene apagado y ya no es la hipótesis principal.
0.6.0 — los cuatro ajustes de gráficos, funcionando
Encontrados los dos campos que faltaban
El volcado de campos de la v0.5.1 dio los nombres reales, que no se parecían a lo que se venía probando:
| Opción del menú | Campo real | Valor del jugador |
|---|---|---|
| Terrain / Grass Detail | terrainGrassDistance (Int32) |
3, con el menú en Low |
| Indirect lighting | advancedLightMode (Boolean) |
False, con la casilla desmarcada |
Ambos confirman el mismo patrón: índice alto = calidad baja.
Corregido: Pixel Resolution se quedaba en "Default"
La v0.5.1 traía el valor correcto en el código (3 = Retro) y aun así aplicaba 0:
graficos BAJADOS Pixel Resolution: 2->0
La causa no estaba en el código sino en el archivo de configuración. BepInEx respeta el valor
guardado en el .cfg e ignora el default del código. El config generado por la v0.4.0 tenía
pixelResDuringLoad = 0 con la semántica vieja —cuando se creía que 0 era lo más bajo— y ese
archivo seguía mandando después de corregir el código.
Motion Blur se salvó por casualidad: había cambiado de bool a int, así que el valor viejo no
se pudo leer y cayó al default correcto.
Solución: las claves de la sección Graficos están renombradas. Las viejas quedan huérfanas
y se ignoran, y los defaults correctos entran solos. No hay que borrar ningún archivo a mano.
Es un modo de fallo que vale la pena recordar: cambiar un valor por defecto no afecta a nadie que ya tenga el mod instalado.
0.5.1 — arreglo urgente: la v0.5.0 colgaba el juego
No usar la v0.5.0. El precompilado de shaders que introdujo dejaba el juego en la pantalla de "Loading..." indefinidamente y terminaba abriendo el reportador de crashes de Unity.
La causa, medida en el log:
[t=72.672s] host de runtime creado
[t=187.308s] shaders precompilados en 114.64s
Shader.WarmupAllShaders() bloqueó el hilo principal 114 segundos. En un juego HDRP con mods,
compilar todas las variantes de shader no es una optimización: es un cuelgue.
El razonamiento que lo justificaba —"no cuesta FPS, solo mueve el costo de compilar fuera de la carga"— era cierto en lo que decía y omitía lo único que importaba: cuánto tarda.
Eliminado por completo, no desactivado por config. Un footgun detrás de una casilla es un footgun que alguien va a pisar. Queda una nota en el código para que no se reintroduzca.
Todo lo demás de la v0.5.0 se mantiene: el mapeo corregido de gráficos, la verificación de la restauración, el volcado de campos y el perfilado del frame lento.
0.5.0 — corregido el mapeo de gráficos, y a la caza del frame de 4 segundos
🔴 Corregido: MechFix SUBÍA la calidad durante la carga en vez de bajarla
La v0.4.0 asumió que el índice 0 era la calidad más baja. Es exactamente al revés. Los desplegables del menú van de mejor a peor:
| Opción | Orden real | Valor buscado |
|---|---|---|
| Terrain / Grass Detail | Ultra=0, High(Default)=1, Medium=2, Low=3 | 3 |
| Motion Blur | Moderate=0, Subtle=1, Off=2 | 2 |
| Pixel Resolution | Default=0, Performance=1, Ultra performance=2, Retro=3 | 3 |
El log de la v0.4.0 ya contenía la prueba y no se leyó bien: con el menú del jugador en Off, el
campo motionBlur valía 2, y aun así el mod lo bajaba a 0. Resultado: durante la carga los
gráficos subían al máximo, que es lo contrario de lo que se buscaba.
Ahora los valores son configurables uno por uno, con -1 para no tocar un ajuste.
Dos campos siguen sin aparecer
Terrain / Grass Detail e Indirect lighting no están en IngamePlayerSettings+Settings con
ninguno de los nombres probados. En vez de adivinar más —que es el error que costó diez versiones
en el proyecto anterior— MechFix ahora vuelca al log todos los campos simples del objeto de
ajustes, con nombre, tipo y valor. El nombre real se lee de ahí.
Verificación de la restauración
Tras devolver los ajustes, MechFix los relee y compara contra lo que había guardado. Si algo no
volvió a su sitio, el log lo dice con ¡ESPERABA x! en vez de dejarlo pasar en silencio.
Nuevo: perfilado del frame lento
El último gran desconocido son frames de 3-4 segundos dentro de la generación (media 3 024 ms, pico 4 109 ms). Los datos ya descartaron las dos hipótesis heredadas: hay frames de casi cuatro segundos sin un solo error de navmesh y sin un solo collider.
El sospechoso que queda es el recolector de basura. MechFix ahora registra, para cada frame que pase de 1 s, cuántas recolecciones ocurrieron y cuánto creció el heap. Si el contador salta justo ahí, se acabó el misterio.
También trae la mitigación (pauseGCDuringLoad), apagada por defecto hasta que la medición
confirme la causa. Encender un arreglo para un problema no probado es cómo se acumulan diez
versiones de código que no hace nada.
Nuevo: precompilado de shaders
Shader.WarmupAllShaders una sola vez al entrar a la partida, antes de aterrizar. No cuesta FPS
en juego: mueve el costo de compilar fuera del momento de la carga. El log dice cuánto tardó.
Eliminado
El cambio de nivel de calidad de Unity. El log de la v0.3.0 probó que el juego define un solo
nivel (High), así que esa medida no podía hacer nada.
0.4.0 — bajar los gráficos del menú del juego durante la carga
La v0.3.0 intentó bajar la calidad con QualitySettings.SetQualityLevel. El log contestó:
niveles de calidad disponibles: High
Lethal Company define un solo nivel de calidad de Unity. No había nada más bajo al que cambiar, así que esa medida no podía hacer nada. Código eliminado.
Los ajustes que sí mueven la aguja son los del menú GRAPHICS del juego, y ahora MechFix los baja al empezar la generación y los devuelve exactamente como estaban al terminar:
| Opción del menú | Campo interno |
|---|---|
| Terrain / Grass Detail | graphicsLevel |
| Motion Blur | motionBlur |
| Pixel Resolution | pixelRes |
| Indirect lighting | indirectLight |
Todo por reflexión sobre IngamePlayerSettings, sin dependencia de compilación con el juego. Y
antes de tocar nada, MechFix escribe en el log el tipo y el valor actual de cada campo: si un
valor objetivo estuviera mal elegido, el log lo dice en la primera partida en vez de dejar el
juego en calidad máxima justo durante la carga.
Cada valor es configurable por separado en la sección Graficos del config.
Se toca settings, nunca unsavedSettings
unsavedSettings es la copia que el menú muestra y que el juego persiste al pulsar Apply. Si
MechFix la modificara y el jugador abriera el menú en ese momento, se le guardarían los ajustes
bajos de forma permanente. Perder la configuración de alguien para ahorrar unos segundos de carga
no es un trato aceptable.
En partidas con amigos
Los ajustes gráficos son locales de cada máquina, y en Lethal Company cada cliente genera su propio dungeon. No hace falta sincronizar nada por red: alcanza con que cada jugador tenga MechFix instalado. Si solo lo tiene el anfitrión, los demás lo esperan igual.
0.3.0 — atacar los FPS, que es lo que de verdad manda
La v0.2.0 limpió el log de forma espectacular —Dissonance pasó de 5 157 líneas a 97, el log bajó a la mitad por aterrizaje— y el tiempo de carga casi no se movió. Los stalls por aterrizaje bajaron de 6.8 s a 6.1 s, y la generación seguía promediando 31.5 s con picos de 85 s.
El cronómetro que la v0.2.0 instaló explicó por qué:
| Luna / Interior | Duración | FPS |
|---|---|---|
| Rockwell / Raven Manor | 85.3 s | ~2.5 |
| Riptide / junkrooms | 78.4 s | ~6.0 |
| Praetor / CIDOM Metro | 16.6 s | ~22.0 |
DunGen genera por frames. La duración es inversamente proporcional a los FPS: el mismo interior (CIDOM Metro) tardó 16.6 s a 22 fps y 31.9 s a 11.5 fps. Limpiar el log ayudaba solo en la medida en que devolvía frames — y ya no quedaba mucho log que limpiar.
Nuevo: LoadBoost
Libera frames durante la generación y revierte todo al terminar:
- Baja el nivel de calidad gráfica entero (sombras, texturas, LOD, anisotrópico, partículas).
Acepta un nivel por nombre —
Retro, si tu build lo define— o usa el más bajo disponible. El mod escribe en el log qué niveles encontró, en vez de asumir que existe alguno en particular. - Apaga las sombras durante la carga.
- Destapa el límite de frames: con vSync activo, los frames que podían salir rápido se perdían esperando el refresco de pantalla, y cada uno de esos era una porción de generación.
- Difiere la sincronización de físicas — apagado por defecto, es el único con riesgo real.
El orden de restauración importa y está resuelto: SetQualityLevel pisa shadowDistance, así que
la calidad se restaura primero y las sombras después. Y hay red de seguridad: si una generación no
cierra en 300 s, o el host muere, los ajustes se revierten igual.
En partidas con amigos
Los ajustes gráficos son locales, y en Lethal Company cada cliente genera su propio dungeon. No hace falta sincronizar nada por red: alcanza con que cada jugador tenga el mod instalado. Si solo lo tiene el anfitrión, los demás lo esperan igual.
Corregido
El postfix de fin de generación registraba dos veces por carga, porque Netcode genera un envoltorio
para las ClientRpc. El segundo aparecía sin duración.
0.2.0 — la primera versión que optimiza algo
Resuelto: el bug que arruinó diez versiones del proyecto anterior
BaronessNavMeshFix instaló hooks durante diez versiones —Harmony, corrutinas, eventos
estáticos, un watchdog con su propio GameObject— y ninguno disparó jamás. Nunca se pudo
distinguir "el hook no corrió" de "el DLL no estaba cargado".
La v0.1.0 instrumentó las cuatro vías por separado y el log dio la respuesta en una línea:
[t=0.084s] sonda 4/4 Harmony : instalada, 4/4 metodos enganchados
[t=9.605s] OnDestroy: frames=0 corrutina=0 escenas=0
BepInEx destruye el objeto del plugin en la primera transición de escena, a los 9.6 segundos —
antes incluso del menú principal. frames=0: Update() no llegó a correr ni un solo frame.
Y el OnDestroy llamaba a _harmony.UnpatchSelf(), así que borraba sus propios parches mucho
antes de que existiera un dungeon que generar.
El mod se desarmaba solo, cada partida, desde la versión 1.0.0. Los hooks nunca estuvieron mal elegidos; simplemente ya no existían cuando llegaba el momento de disparar.
Corregido de dos formas, para no depender de una sola:
OnDestroyya no desparcha nada. Los parches de Harmony son estáticos y sobreviven a la destrucción del objeto — que es justamente lo que los hace útiles, y por qué el resto de los mods del perfil funcionan sin problema.- Lo que necesita vivir en runtime se planta en un
GameObjectpropio conDontDestroyOnLoad, creado desde un postfix sobreStartOfRound.Awake, muy después de esa destrucción.
Optimización
- Sin stack traces en
Debug.LogyDebug.LogWarning. Unity arma una traza gestionada para cada llamada; con ~34 500 líneas por sesión eso es trabajo puro a cambio de nada. Alcanza al 92 % del log, incluidas las líneas que emite el motor en código nativo. Los errores conservan su traza. - Bloqueo en origen del spam por frame. Un prefix sobre
Debug.Log/LogWarningcorta 12 patrones antes de que Unity los procese: la IA del Bracken (9 945 líneas deDebug.Logque quedaron en el código vanilla) y el pipeline de micrófono de Dissonance (5 018 líneas, con el jugador solo en la partida). Cubre 14 498 líneas, el 39 % del log medido.
Ninguna de las dos toca la jugabilidad: no se modifica el tamaño de los dungeons, ni la generación, ni el pathfinding. Solo se decide si una línea de texto se escribe.
Se preservan a propósito
Detected a frame skip (la única métrica fiable de stalls), los errores con su traza, y los
marcadores de generación. Sin ellos no se puede comprobar si el mod sirve.
Instrumentación
El cronómetro de la v0.1.0 sigue, y ahora sobrevive lo suficiente para servir: latido con reloj monótono, marcas de inicio y fin de generación con precisión de milisegundos, y contador de líneas bloqueadas.
0.1.0 — build de diagnóstico
Primera versión. No optimizaba nada a propósito: existía para bisecar por qué el proyecto anterior
no ejecutaba nada después de Awake(), y para devolverle un reloj al log.
Cumplió su único objetivo en la primera partida.