Cómo contribuir
Prefer English? Read the English version.
¡Gracias por querer ayudar! Esta guía explica cómo compilar el proyecto y cómo hacer los cambios más habituales.
Requisitos
- Windows, Linux o macOS (la interfaz usa Avalonia, multiplataforma).
- .NET 9 SDK.
- (Solo para generar el instalador) Inno Setup 6.
- (Solo para recompilar el agente de memoria) un JDK 8 o posterior (
JAVA_HOMEojavacen elPATH) y Python 3:python agent/build.py. Las pruebas que arrancan una JVM de verdad usan la Java deJAVA_HOMEo delPATH(12 o posterior); sin una no tienen nada que ejecutar y pasan, cosa que los runners de la CI nunca sufren.
Compilar y ejecutar
git clone https://github.com/JuanP-G/MC-ServerLauncher.git
cd MC-ServerLauncher
dotnet run --project McServerLauncher
Ejecutar las pruebas:
dotnet test McServerLauncher.Tests/McServerLauncher.Tests.csproj
Generar el sitio de documentación en local (esta página):
dotnet tool install -g docfx # solo la primera vez
.\docs\build-docs.ps1 # compila y sirve en http://localhost:8080
Pruebas
McServerLauncher.Tests/ es un proyecto de xUnit que referencia la app directamente — la lógica
de decisión que merece la pena probar es internal a propósito (es implementación, no API) y llega a
las pruebas por InternalsVisibleTo, así que renombrar un miembro rompe la compilación en vez de
fallar en ejecución como haría la reflexión.
.github/workflows/tests.yml las ejecuta en todas las ramas y en Windows y Linux a la vez.
Es deliberado: varias usan sockets, named pipes y bloqueos de archivo reales, y .NET los implementa
de forma distinta en cada plataforma (una named pipe es un socket de dominio Unix en Linux). Una
prueba que solo tenga sentido en una plataforma debe saltarse a sí misma en la otra, no comprobar
otra cosa.
Las pruebas que necesitan controles reales de Avalonia usan AvaloniaFixture, una única aplicación
headless compartida por toda la ejecución — Avalonia solo se puede inicializar una vez por proceso, y
sus controles hay que tocarlos desde el hilo que la inicializó. Existe porque un fallo se publicó dos
veces y nada más lo pillaba: el selector de tipo informando de la selección anterior dentro de su
propio evento de cambio.
Van en siete capas, y conviene saber a cuál pertenece una prueba nueva:
| Capa | Ejemplo | Qué ve |
|---|---|---|
| Los modelos solos | ServerConfigFormatTests, ModelNotificationTests |
Qué se escribe en disco; qué propiedades avisan |
| La tabla | ServerConfigEffectsTests |
Que no se ha quedado ningún campo de la config sin declarar |
| View models, construidos de verdad | ServerViewModelRefreshTests, MainViewModelFlowTests |
Que un cambio en la config llega a la tarjeta, a los paneles y a servers.json |
| Controles reales | ServerSettingsViewTests, NewServerCreateFormTests, NewServerStepsTests |
Que llega a la pantalla — la mitad que una prueba unitaria no ve |
| Puertas sobre el código | StartDependencyGateTests, StoreHashTests, ExportSelectionTests |
Que una promesa se cumple en el propio archivo — sin red, una sola definición |
| El artefacto, ejecutado | InstallScriptSmokeTests |
Que el script que recibe un jugador funciona de verdad — con bash, en Linux en la CI |
| Una JVM de verdad | MemoryAgentIntegrationTests |
Que el agente hace lo que dice dentro de una Java, arrancada como la arranca la app |
ServerViewModel y MainViewModel aceptan una carpeta de datos y no arrancan nada hasta
Activate(), así que una prueba puede tener uno sin tocar %APPDATA%, Playit ni la red. Las pruebas
nunca llaman a Activate(). ServerModsView no se puede renderizar en headless (la fuente de
iconos), así que sus enlaces se comprueban leyendo el .axaml y mirando el view model por reflexión
— ver MissingDependencyPanelTests.
Estilo de código
Todo el repositorio está escrito con un mismo estilo, y la mayor parte se aplica sola: el
.editorconfig de la raíz tiene las reglas de formato y de nombres, y Visual Studio, Rider y VS Code
lo leen sin configurar nada. Antes de abrir un pull request:
dotnet format McServerLauncher.sln
Así no queda nada que discutir en la revisión. El .gitattributes zanja igual los finales de línea
—el repositorio guarda LF y a tu copia de trabajo le da lo que espere tu plataforma—, para que quien
trabaje en Windows y quien trabaje en Linux no produzcan nunca un diff donde han cambiado todas las
líneas y no ha cambiado nada.
La plantilla de pull request repite la lista de comprobación, así que nada de esto hay que recordarlo justo en el momento en que hace falta. Las reglas de abajo son las que conviene saberse, y las pocas que una herramienta no puede comprobar.
Nombres
| Qué | Estilo | Ejemplo |
|---|---|---|
| Tipos, métodos, propiedades, eventos | PascalCase |
ServerProcessManager, EnsureJavaAsync |
| Campos privados de instancia | _camelCase |
private readonly ServerConfig _config; |
Campos privados estáticos y const |
PascalCase, sin guion bajo |
private static readonly HttpClient Http |
| Locales y parámetros | camelCase |
var levelName = … |
| Interfaces / parámetros de tipo | IPascalCase / TPascalCase |
IProgress<string>, TResult |
El guion bajo de las dos primeras filas es justo el motivo de la regla: es como distingues el estado
de la instancia de todo lo demás sin tener que buscar, y por eso un campo static no lo lleva.
Los métodos asíncronos acaban en Async. La única excepción es un método [RelayCommand], que
se llama como el botón (private async Task Start() → StartCommand); el toolkit genera el comando
a partir de ese nombre, y StartAsyncCommand se lee peor en todos los sitios donde se enlaza.
Los nombres dicen para qué sirve la cosa, no de qué está hecha: CrossplayService, no
GeyserHelper. Un nombre que necesita un comentario para entenderse es culpa del comentario la mitad
de las veces y del nombre la otra mitad — casi siempre sale mejor arreglar el nombre.
Declarar estado
- Una clase empieza por sus colaboradores,
private readonlye inicializados en la misma línea:private readonly ModrinthService _modrinth = new();. Ese bloque es donde quien lee se entera de qué está hecha la clase, y por eso aquí no se usan constructores primarios: esconden eso detrás de una lista de parámetros. - El estado enlazable es
[ObservableProperty] private string _searchQuery = string.Empty;y nada más. Nunca unOnPropertyChangedescrito a mano. - Lo derivado es una propiedad con cuerpo de expresión:
public bool UpdateAvailable => Update is not null; - Un modelo que se guarda y al que se le escribe mientras está en pantalla es observable.
ServerConfigyNotificationSettingsderivan deObservableObject, y una propiedad nueva en cualquiera de los dos entra como[ObservableProperty] private T _foo;igual que en todas partes. No cambia lo que se escribe en disco, yServerConfigFormatTestsestá para demostrar que sigue siendo así.AppSettingses la excepción y sigue plano: nada se enlaza a él, porqueSettingsViewModelenvuelve los campos que enseña y avisa él mismo de sus cambios. - Un campo nuevo en
ServerConfignecesita una fila enServerConfigEffects: qué propiedades de view model alimenta y qué hay que rehacer, o una línea diciendo por qué no lo enseña nadie.ServerConfigEffectsTestsfalla hasta que está, y el fallo dice qué escribir. No es burocracia: un campo que se queda fuera enseña la respuesta correcta hasta que alguien edita ese servidor, y la equivocada desde entonces hasta que se reinicia la app. varcuando el tipo ya está en la línea (var view = new ServerSettingsView(…)), y el tipo escrito cuando no lo está.- Un constructor monta;
Activate()arranca. Nada que sondee, abra un socket, se suscriba a un singleton compartido o vaya a la red va en un constructor: va enActivate(), cuyo espejo esShutdownAsync(), y los dos tienen que aguantar que se les llame dos veces.ServerViewModelyMainViewModelestán partidos así, que es la única razón de que se puedan construir en una prueba.
Capas
- Mantén la separación MVVM: lógica en
Services/(sin interfaz), estado y comandos enlazables enViewModels/, solo code-behind ligero enViews/, y datos puros enModels/. - Una decisión de color se parte en dos: los hex en una clase sin interfaz de
Services/, los brushes de Avalonia al lado enViewModels/—ConsoleColors/ConsolePalette,NotificationPalette/NotificationBrushes,ServerTypeCatalog/ServerTypeBrushes. Un color que acaba ensettings.jsonno puede necesitar un tipo que sepa que Avalonia existe. - Los cuatro colores de estado (bien, información, aviso, error) tienen una sola fuente. En XAML son
las brochas
Semantic*deApp.axaml; en C# sonNotificationPalette.Default*. No vuelvas a escribir el hex: los avisos, los paneles, los puntos de estado y las líneas de consola deben decir "algo ha fallado" con el mismo rojo. Y nada de emojis ni de signos de visto o advertencia en el texto de la interfaz: una marca es un icono (SymbolIcon,FluentIcons.Common.Symbol), un estado es un color y una palabra.LocalizationTestslo comprueba. - Los estilos que necesita más de una vista van en
Styles/Shared.axaml, no dentro del<Window.Styles>de una vista. Ponle a un estilo compartido un nombre que signifique una sola cosa: dos vistas tuvieron cada una suBorder.cardqueriendo decir cosas distintas, y Avalonia las mantuvo separadas solo porque eran locales — al unirlas habrían chocado y una vista habría cambiado de aspecto sin que fallara nada. - Nada de rutas absolutas del equipo. Usa
Environment.GetFolderPath(...); los datos por usuario viven en%APPDATA%\McServerLauncher\.
Comentarios y documentación
- Los comentarios y los nombres del código van en inglés, en todo lo que lee quien contribuye:
McServerLauncher/(los.axamlincluidos),McServerLauncher.Tests/y.github/workflows/. El texto que ve el usuario no se escribe a mano: pasa por el sistema de localización (ver abajo). Los scripts propios del mantenedor para publicar y para la web (publish.ps1,installer/,tools/,web/_i18n/) siguen en español, tanto los comentarios como lo que imprimen; son la única excepción documentada, no un sitio del que copiar la costumbre. - Los tipos públicos llevan un resumen XML
///, y también cualquier miembro cuyo contrato no se entienda por el nombre. Es lo que alimenta la referencia de API. - Un comentario dice por qué, no qué. Los que merecen la pena aquí son los que dejan constancia de una decisión y de qué pasaba sin ella — "Purpur solo publica un MD5, y este es el motivo de que baste" vale un párrafo; "// recorrer los mods" vale un borrado.
- Ajusta los bloques
///y la prosa a unas 100 columnas, y el código a unas 110.
Reglas de corrección aprendidas a golpes
Cada una está aquí porque saltársela publicó un fallo que ninguna prueba pilló en su momento.
- Un fallo nunca puede parecer una buena noticia. Cuando una consulta no se puede hacer, se
devuelve
nullo un estado distinto — nunca el resultado vacío que también significa «no hay nada que hacer». La comprobación de actualizaciones decía «todo está actualizado» sin conexión, y Modrinth contestaba a los hashes en mayúsculas con un200 OKvacío, así que «Buscar actualizaciones» no encontró nada mientras existió. Nadie reporta buenas noticias.UpdateCheckTestssujeta el tipo de retorno por reflexión justo por eso. - Una pregunta, una definición. Cuando dos sitios responden a lo mismo, se pasan los dos por uno
y se añade una prueba que falle si aparece una segunda copia. Se ha separado tres veces: el
emparejamiento de túneles (
PlayitApiService.Match), los hashes que van a la tienda (ModrinthService.ApiHashes) y las dependencias que declara un jar (ContentManifest). Cada vez solo una de las copias aprendió lo que hacía falta. - Una clave
.resxnueva se usa, y una que ya no se usa se borra. Añádela a los cinco archivos con el código que la pide.LocalizationTests.EveryKeyIsAskedForSomewherefalla ante una clave que nadie nombra; las pocas que se piden construyendo su nombre (Cmd_,Console_Kind,MotdColor_…) están en esa prueba, cada una con el sitio donde se arma. - La prueba de un arreglo tiene que fallar con el código de antes. Compruébalo: aparta el arreglo, ejecuta la prueba y mírala ponerse roja. Una prueba que pasa en los dos casos no demuestra nada, y es fácil escribirla.
- Lee lo que lee el cargador. Los mods llevan otros mods dentro (
META-INF/jars/,META-INF/jarjar/), y los cargadores traen librerías propias (mixinextras). Todo lo que decida qué hay instalado tiene que ver las dos cosas, o dará por perdido lo que el servidor carga sin problema. - Los recursos incrustados con puntos en el nombre necesitan
WithCulture=falsey unLogicalName. MSBuild deduce una cultura de los segmentos del nombre,shes una de verdad, einstall-mods-unix.sh.inacabó en silencio en un ensamblado satélite con la compilación en verde. - Lo que tiene que sobrevivir a una reconstrucción no puede vivir en lo que se reconstruye. La pestaña de Mods reconstruye sus filas desde el disco después de cada actualización, activación y borrado, y las actualizaciones que encontraba una comprobación vivían en las filas — así que actualizar un mod hacía desaparecer los botones de todos los demás. Ese estado va en el view model y se le devuelve a cada fila nueva.
- Una opción de la JVM que añade la app va condicionada a la versión de Java. Una opción que la
JVM no reconoce le impide arrancar, que es peor que cualquier aviso que se quisiera callar. Ver
ServerProcessManager.ImpliedJvmFlags, y nunca por encima de lo que el usuario puso en sus argumentos extra. Lo mismo vale para los flags de memoria y para-javaagent:MemoryProfileda cada uno solo a la Java que lo conoce,GcLoggingsolo si se pudo crear su carpeta yMemoryAgentsolo si su jar está en su sitio y esa Java no lo ha rechazado antes. - Lee la consola de un servidor desde el principio del mensaje, después de quitarle los colores.
Todos los detectores se anclan en lo que sigue al prefijo del log (un jugador puede escribir cualquier
cosa en el chat), y Paper envuelve las líneas de entrada y salida en secuencias ANSI incluso por una
tubería, así que un nombre que empieza con un escape no es un nombre: entraban jugadores y la app no los
veía.
AnsiEscapes.Stripse ejecuta una vez, donde entra la salida (ServerProcessManager); nada aguas abajo debería ver nunca un escape. - Arregla un formato con una línea de un servidor de verdad. Los analizadores de la sonda de
velocidad se escribieron primero de memoria de lo que imprime spark, y una ejecución real mostró que
imprime dos decimales en unas líneas y uno en otras, un
[⚡]mal codificado en Windows y la coma decimal de un equipo en español. Captura la salida, pon las líneas en la prueba y ejecuta el código contra un servidor vivo antes de darlo por hecho. - El agente de memoria se compila con
agent/build.py, nunca con eljardel JDK.jarescribe una líneaCreated-Bycon su propia versión, así que dos máquinas dan dos jars; el script escribe un zip determinista, yagent.ymlmás una prueba fallan si el jar del repositorio y su SHA-256 dejan de coincidir con lo que compila el código. Tras tocaragent/src/, ejecútalo y sube los dos archivos. - Un doble que comparten las pruebas entrega copias.
FakeBackupHost.Linesera la lista viva, y una prueba que la recorría mientras una copia aún informaba de su progreso fallaba de vez en cuando con «Collection was modified». Una propiedad que una prueba lee mientras el código probado aún escribe devuelve una instantánea. - Lo que lee o reemplaza el mundo lo sostiene de principio a fin. Las copias, las restauraciones
y el arranque y la parada de la JVM pasan por la única puerta de
BackupCoordinator, y cada uno la sostiene durante toda la operación, no solo en la parte de «la copia». Cogerla solo alrededor del zip dejaba que Parar mandarastopdentro de una copia que aún se leía, y que una restauración ocurriera con un servidor que se había despertado mientras tanto. - El mensaje propio de una excepción no se enseña nunca. Está escrito para quien programa, en
inglés o en el idioma del sistema, con rutas y códigos, y salía en mitad de frases en español en
unos cincuenta sitios. Lanza una
UserFacingExceptioncuando el mensaje es una frase de los .resx, y enseña cualquier fallo conErrorMessages.Describe(ex), que deja el detalle en el registro.ErrorMessagesTestsfalla con unex.Messageque no vaya al registro. - Lo que edita una copia sigue al original, y lo cambia al final. La página de configuración edita una copia de la config del servidor. Antes leía el original una vez —así que un interruptor que la app volvía a apagar seguía encendido, y guardarlo no era un cambio— y copiaba sus campos al original antes de escribir los archivos que van con ellos, así que un archivo que fallaba dejaba el servidor a medio cambiar. Recoge lo que cambia fuera salvo que el usuario lo haya editado, y cambia el original solo cuando todo lo demás está en disco.
- Scripts escritos para jugadores. La lógica en una plantilla
.in, cada línea visible en los .resx, y cada marcador sustituido por un mensaje entero y terminado — nunca una cadena de formato. Finales de línea por archivo (.shLF,.batCRLF), sin BOM en ningún sitio, y todochoicede un.batlleva/ty/d, porque batch no sabe si hay alguien para pulsar una tecla.
Commits, pull requests e issues
Todo lo que se escribe sobre un cambio sigue una misma forma, para que git log se lea como un
historial de decisiones y no como una lista de ediciones. Es Conventional Commits, en español:
tipo(ámbito): qué cambia, contado como se lo contarías a alguien
Qué pasaba antes, y por qué era un problema de verdad. Qué hace ahora. Por qué
así y no de la otra forma, con lo que se descartó.
Closes #12
Ejecuta esto una vez y git te abrirá esa forma, con las reglas como comentarios:
git config commit.template .gitmessage
El asunto
En español, en minúscula, sin punto final, unos 70 caracteres (90 como tope). Describe el problema o el resultado, no el mecanismo — qué líneas se han movido ya lo dice el diff:
| Bien | fix(bedrock): el puerto que no aparecía, y los túneles que se pisaban |
| Mal | fix(bedrock): cambiar PickBedrockPort para usar accountTunnels |
Dos cosas relacionadas se unen con , y. Si no caben en una frase, son dos commits.
Tipos
| Tipo | Para |
|---|---|
feat |
Algo nuevo que puede hacer el usuario |
fix |
Un comportamiento que estaba mal |
seg |
Seguridad: verificar, sanear, acotar, cifrar |
perf |
Más rápido o más barato, sin cambiar lo que hace |
refactor |
Misma conducta, mejor forma |
docs |
Documentación — los .md, los comentarios ///, la web |
test |
Pruebas, y solo pruebas |
ci |
Workflows de GitHub Actions y empaquetado |
chore |
Dependencias, tooling, limpieza |
release |
Siempre release: X.Y.Z, sin ámbito |
seg es un tipo propio a propósito, y no un sabor de fix. Esta app descarga y ejecuta código en la
máquina del usuario, así que «¿qué hemos cambiado de eso desde la última auditoría?» es una pregunta
que alguien hace cada cierto tiempo — y tiene que poder responderse con
git log --grep '^seg' en vez de leyendo cuatrocientos commits. Cómo se hace una auditoría, cuánto
hace de la última y qué encontró cada una: Auditorías del código.
Ámbitos
El área funcional, en español, opcional. Usa uno que ya exista antes de inventar otro, y usa una
sola forma por concepto — en el historial hay notif y notificaciones, que es exactamente para
lo que está esta lista:
consola · mods · tienda · jugadores · servidor · tipos · crossplay · bedrock ·
playit · java · paper · forge · fabric · puertos · backups · avisos · colores ·
actualizaciones · ajustes · nombres · seguridad · datos · ui · web · tests · ci ·
release
¿Añades uno que se va a repetir? Añádelo a esta lista en el mismo commit.
El cuerpo
En español, a 80 columnas, y se espera en todo lo que no sea trivial. Escribe lo que un
git blame dentro de dos años necesita y no puede sacar del diff: cuál era el comportamiento
anterior, por qué era un problema de verdad, y qué descartaste por el camino. El historial que ya
hay es la referencia — git show bf6453a es un buen ejemplo que leer antes de escribir el primero.
Los pies son Closes #12, Refs #12, o BREAKING CHANGE: con qué se rompe y qué hacer al respecto.
Pull requests
- El título es el asunto de un commit — mismos tipos, mismos ámbitos, mismo español.
.github/pull_request_template.mdrellena el resto, la lista de comprobación incluida. - La descripción es el cuerpo del commit que habrías escrito para toda la rama.
- Una idea por pull request. El commit de mezcla es
Merge pull request #N: <título corto> (versión si aplica).
Issues
Abrir uno pasa por un formulario (.github/ISSUE_TEMPLATE/), que deja el título con la misma forma:
un fallo empieza por fix(ámbito): y una propuesta por feat(ámbito): . Escribe en inglés o en
español, el que prefieras — los dos se leen.
Un problema de seguridad nunca es un issue público. SECURITY.md
explica el canal privado y enumera los compromisos que ya están asumidos, para que puedas distinguir
un hallazgo de una decisión documentada.
La documentación es parte del cambio
En este repositorio la documentación es parte del cambio, no trabajo para después:
- Una función nueva o distinta →
README.mdyREADME.es.md, los dos. - Un servicio, flujo o carpeta nuevos →
docs/articles/architecture.mdyarchitecture.es.md. - Una convención o flujo de trabajo nuevos → este archivo y
contributing.es.md, y.github/copilot-instructions.mdpara que los asistentes de IA sugieran lo mismo que pediría una revisión. - Un texto nuevo visible para el usuario → los cinco
.resx(esto lo comprueba una prueba).
Las páginas en inglés y en español son el mismo documento dos veces. Actualizar una y no la otra es justo como alguien acaba fiándose de una página que va una versión por detrás sin saberlo.
Sin emojis en la documentación. Ni en títulos, listas, tablas, formularios de issues ni mensajes
de commit. Un estado se marca con una palabra en negrita (Funciona, No), y cuando una página
necesita algo visual se usan los iconos de línea de docs/media/icons/ (24 px, #2da44e), como hace
la rejilla de funciones de README.md.
Recetas paso a paso
Añadir un idioma
- Copia
Resources/Strings.resxaResources/Strings.<código>.resx(p. ej.Strings.it.resx) y traduce cada<value>. - Añade el código a
<SatelliteResourceLanguages>enMcServerLauncher.csproj. - Añádelo a la lista
LanguagesdeMainViewModelpara que salga en el selector de la barra lateral.
Añadir un texto traducible
- Añade la misma entrada
<data name="MiClave">a los cinco archivos.resxcon la traducción. - Úsalo desde XAML como
{loc:Loc MiClave}, o desde código comoLocalizer.Get("MiClave")(usaLocalizer.Format("MiClave", arg)si tiene huecos{0}).
Añadir un ajuste de server.properties a la configuración del servidor
- Añade el control + etiqueta/descripción en la página que le toca dentro de
Views/ServerSettings(GameSection,WorldSection,NetworkSection…), como unDockPanel.settingcon el control en la clasectl. - Átalo a su clave en el constructor de la página con
PropertyBindings(Toggle,Number,Choice) y añade la clave a la fila de esa página enServerSettingsPages.All. Guardar la escribe conServerPropertiesService.Update, que conserva el resto del archivo, los comentarios y el orden.
Añadir un tipo de servidor
- Una fila en
Services/ServerTypeCatalog.cs: nombre visible, familia (plugins / mods / ninguna), color de la insignia y suCrossplayLevel. El selector, las insignias, la tienda, la carpeta de contenido y las reglas de crossplay leen todos de esa tabla. - Una rama en
Services/ServerJarInstaller.cs, que es el único sitio que sabe cómo se obtiene cada tipo. El panel de nuevo servidor y la página Tipo y versión de la configuración llaman los dos ahí. - Añade la clave de descripción del tipo a los cinco
.resx(la línea bajo su nombre en el selector). - Nunca renumeres el enum
ServerType.servers.jsonlo guarda como entero, así que mover un miembro reinterpreta todos los servidores ya guardados en todas las máquinas. Los tipos nuevos van al final.
Resiste la tentación de añadir un
switchsobreServerTypeen ningún otro sitio. Seis de ellos es lo que sustituyó el catálogo, y un tipo presente en cinco de los seis parecía correcto y se comportaba como otra cosa.
Añadir una pantalla, una página o un servicio nuevo
- La interfaz va dentro de la ventana, nunca en una
Windownueva. Una parte de la configuración de un servidor es una página enViews/ServerSettingsque implementaIServerSettingsSectiony edita elServerSettingsDraft; un flujo más grande es un panel que aloja el detalle, comoNewServerViewyServerSettingsView. Las confirmaciones se piden en el sitio (el botón de borrar el historial enAdvancedSectionse convierte en la pregunta), no conMessageBox. Localiza cada texto con{loc:Loc ...}. Quedan algunas ventanas de antes (WhatsNewDialog,PlayitApiKeyDialog,DeleteServerDialog,MessageBox) que se están pasando dentro; no añadas más. - Servicio: añade una clase centrada en
Services/, sin interfaz, e instánciala desde el ViewModel correspondiente (mira cómoServerViewModelcompone sus servicios).
Sacar una versión nueva
- Sube
<Version>enMcServerLauncher/McServerLauncher.csproj— es la fuente única de verdad.publish.ps1la lee y se la pasa a Inno Setup. Mantén alineado el fallbackMyAppVersiondel.isssolo para que una compilación manual/directa con Inno Setup no genere un nombre antiguo. - Añade una entrada de novedades para que el diálogo de actualización tenga algo que mostrar:
- Una nueva tupla al principio de
EntriesenServices/Changelog.cs(la más nueva primero), p. ej.(new Version(1, 6, 0), "Whatsnew_1_6_0"). - La clave
Whatsnew_x_y_zcorrespondiente en los cinco archivos.resx(el texto en español en el neutralStrings.resx, las traducciones en el resto). Mira Añadir un texto traducible más arriba.
- Una nueva tupla al principio de
- Ejecuta
publish.ps1. Publica el build self-containedwin-x64, genera el instalador de Inno Setup endist/, escribedist/SHA256SUMS.txtjunto a él y firma el instalador (dist/…Setup-X.Y.Z.exe.sig2y.sig; pide la contraseña de la clave de firma). Sin la clave avisa y se salta la firma — ver Actualizaciones firmadas. - Crea la release con los cuatro assets y notas bilingües:
gh release create vX.Y.Z dist/MC-ServerLauncher-Setup-X.Y.Z.exe dist/SHA256SUMS.txt dist/MC-ServerLauncher-Setup-X.Y.Z.exe.sig2 dist/MC-ServerLauncher-Setup-X.Y.Z.exe.sig- El actualizador de la app busca el asset
.exe(así que adjúntalo siempre), elSHA256SUMS.txt, que usa para verificar el instalador antes de ejecutarlo (UpdateService.CheckAsync), y el.sig2, que prueba que lo hizo quien publica (el.siges para las apps que aún estén en la 1.21.1). Las dos comprobaciones son obligatorias: sin cualquiera de esos archivos el actualizador rechaza la instalación silenciosa y solo abre la página de la release — no los omitas nunca.
- El actualizador de la app busca el asset
Antes de cambiar cómo se numeran las versiones, comprueba qué sabe leer la versión instalada. Una release cuyo tag tiene una forma que los clientes antiguos no entienden es invisible para ellos por muy correcto que sea el código nuevo — y el código que la entiende viaja dentro de esa release. Ha pasado dos veces: las pre-releases eran invisibles antes de la 1.10.1, y los tags de cuatro números antes de la 1.10.3.1, porque en ambos casos había que cambiar
UpdateServicepara verlas. La primera release de una forma nueva siempre hay que pasarla a mano; dílo en vez de prometer una actualización automática.
Numeración. Una beta lleva un cuarto número que prolonga la estable a la que sigue: tras
1.10.3vienen1.10.3.1,1.10.3.2, y lo terminado sale como la siguiente estable (1.11.0). Es así a propósito: numerar las betas según la versión a la que llevan haría que la estable quedara por debajo de sus propias betas y dejara tirado a quien las probó.
- Si es una beta, publícala como pre-release desde su rama:
GitHub deja las pre-releases fuera degh release create vX.Y.Z --prerelease --target <rama> dist/MC-ServerLauncher-Setup-X.Y.Z.exe dist/SHA256SUMS.txt/releases/latest, así que a quien está en la línea estable no se le empuja a una. Desde la 1.10.1 el actualizador lee la lista de releases, que es lo que hace que una beta sea alcanzable, y avisa de que lo es antes de que se pulse Actualizar. Cualquier versión anterior a la 1.10.1 no puede ver betas, así que la primera beta después de una estable hay que pasarla a mano. - Publicar la release dispara automáticamente los workflows de Linux (
release-linux.yml) y macOS (release-macos.yml), que generan y adjuntan el.AppImagey los dos.dmg. No los subas a mano — basta con esperar a que terminen los workflows. - Después, fírmalos:
tools\signing\sign-release.ps1 -Tag vX.Y.Z. Los workflows no pueden — la clave no debe vivir en GitHub — así que este script espera a los paquetes, comprueba cada uno contra su.sha256, los firma con tu clave local y sube los.sig2y.sig. Hasta que se ejecute, quien pulse Actualizar en Linux o macOS irá a la página de la release.
Sitio de documentación (GitHub Pages)
La referencia de API + estos artículos se publican automáticamente en GitHub Pages mediante
.github/workflows/docs.yml en cada push a main. El sitio se genera con DocFX a partir de los
comentarios /// y el markdown de docs/.
Paso único del propietario del repo: activar Pages en Settings → Pages → Source: “GitHub Actions”. A partir de ahí, cada push actualiza el sitio solo.