El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Cliente

Cómo crear un launcher de actualización profesional (.NET) para MU Online

Construye un launcher .NET completo para tu servidor de MU Online — con manifiesto de versión en JSON, verificación por hash SHA-256, descarga incremental de parches, barra de progreso e inicio automático del Main.exe.

BR Bruno · Actualizado el 18 jun 2024 · ⏱ 24 min de lectura
Respuesta rápida

Un launcher de actualización es la puerta de entrada de tu servidor: es la primera pantalla que el jugador ve, el mecanismo que garantiza que todos corran la misma build y el canal por donde distribuyes correcciones sin pedirle a nadie que descargue un ZIP manualmente. Un launcher amateur — que solo

Un launcher de actualización es la puerta de entrada de tu servidor: es la primera pantalla que el jugador ve, el mecanismo que garantiza que todos corran la misma build y el canal por donde distribuyes correcciones sin pedirle a nadie que descargue un ZIP manualmente. Un launcher amateur — que solo abre el Main.exe — desperdicia esa oportunidad y genera un flujo interminable de "mi cliente está desactualizado" en el soporte. En este tutorial vas a construir un launcher profesional en .NET, con manifiesto de versión en JSON, verificación por hash, descarga incremental, barra de progreso, manejo de errores e inicio automático del juego.

El foco aquí es el cliente: el ejecutable que corre en la máquina del jugador, los archivos de la carpeta del MU y la distribución de parches. No vamos a configurar el game server — para eso, mira la guía de cómo crear un servidor de MU Online. Aquí tratamos exclusivamente la capa que el jugador ve.

Cómo funciona un launcher profesional

El principio es simple y no cambia entre seasons: el launcher compara lo que está instalado localmente con un manifiesto alojado en tu servidor web y descarga solo las diferencias.

JUGADOR (Launcher.exe)
   │
   │ 1. HTTP GET https://cdn.meuservidor.com/version.json
   │    → manifiesto: versión, lista de archivos, hash, tamaño
   ▼
COMPARACIÓN LOCAL
   │  para cada archivo: ¿hash local == hash del manifiesto?
   │  si es igual → salta;  si es distinto/ausente → marca para descarga
   ▼
DESCARGA INCREMENTAL
   │ 2. GET https://cdn.meuservidor.com/patches/Data/Item.bmd ...
   │    graba solo los archivos modificados, actualiza la barra
   ▼
INICIAR JUEGO
   │ 3. Process.Start("Main.exe")  → el launcher se cierra
   ▼
Main.exe se conecta al Connect Server

El manifiesto es la pieza central. Lista cada archivo distribuible (texturas BMD, archivos de la carpeta Data, el propio Main.exe), con el hash SHA-256 y el tamaño. El launcher nunca "adivina" lo que cambió: confía en el hash. Esto hace que el sistema sea idempotente — correr el launcher dos veces seguidas no descarga nada la segunda vez.

Requisitos previos

Antes de escribir cualquier línea de código, ten el entorno listo:

ÍtemRecomendaciónObservación
IDEVisual Studio 2022 CommunityGratuito; incluye la carga de trabajo ".NET desktop development"
Framework.NET Framework 4.7.2Máxima compatibilidad en el público de MU (Windows 7+)
Paquete JSONSystem.Text.Json o Newtonsoft.JsonVía NuGet
Servidor webApache/Nginx con HTTPSPuede ser el mismo VPS del sitio
Cliente baseCarpeta completa del MU de tu seasonMain.exe ya con la IP editada
Herramienta de hashPowerShell Get-FileHashYa viene en Windows

También necesitas una decisión de arquitectura: dónde se alojan los parches. Lo ideal es separar el servidor de archivos (un subdominio tipo cdn. o patch.) del game server. Así, un pico de descargas el día del parche no afecta la latencia del juego. Puede ser el mismo VPS con un virtual host dedicado, o un hosting estático barato.

Paso 1 — Definir el manifiesto de versión (version.json)

El manifiesto es un JSON estático servido por HTTP. Estructura recomendada:

{
  "version": "3.2.0",
  "min_launcher": "1.4.0",
  "server_name": "ViciadosMU",
  "patch_base_url": "https://cdn.meuservidor.com/patches/",
  "main_exe": "Main.exe",
  "news_url": "https://www.meuservidor.com/api/news.json",
  "files": [
    {
      "path": "Data/Item.bmd",
      "hash": "9f2c1a7b3e5d8c4f0a6b2e9d1c7a4f8b0e3d6c9a2b5f8e1d4c7a0b3e6d9c2f5a",
      "size": 128432
    },
    {
      "path": "Data/Local/Text.bmd",
      "hash": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
      "size": 54120
    },
    {
      "path": "Main.exe",
      "hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4",
      "size": 3841024
    }
  ]
}

Campos importantes:

  • version: versión del contenido. Solo sirve para mostrar y registrar; la decisión real de descargar viene del hash.
  • min_launcher: permite forzar la actualización del propio launcher. Si el launcher instalado es más antiguo, avisas al jugador para que descargue uno nuevo.
  • patch_base_url: prefijo donde se buscará cada path. patch_base_url + path = URL final del archivo.
  • files[].hash: SHA-256 en hexadecimal minúsculo. Es la fuente de la verdad.

> Las rutas en path reflejan la estructura real de la carpeta del cliente. Los nombres Item.bmd, Text.bmd y las subcarpetas son ejemplos comunes de Season 6 — la organización exacta varía por season/cliente. Lo importante es que el path en el manifiesto sea idéntico a la ruta relativa dentro de la carpeta del MU.

Generar el manifiesto automáticamente

Nunca calcules hashes a mano. Usa este script de PowerShell, que recorre la carpeta del parche y emite el JSON listo:

# gerar-manifesto.ps1
$raiz    = "C:\ClienteMU"          # carpeta base del cliente
$version = "3.2.0"
$baseUrl = "https://cdn.meuservidor.com/patches/"

$arquivos = Get-ChildItem $raiz -Recurse -File
$lista = foreach ($f in $arquivos) {
    $rel  = $f.FullName.Substring($raiz.Length + 1).Replace('\','/')
    $hash = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower()
    [ordered]@{ path = $rel; hash = $hash; size = $f.Length }
}

$manifesto = [ordered]@{
    version        = $version
    patch_base_url = $baseUrl
    main_exe       = "Main.exe"
    files          = $lista
}

$manifesto | ConvertTo-Json -Depth 4 | Out-File "version.json" -Encoding utf8
Write-Host "Manifiesto generado con $($lista.Count) archivos."

Ejecuta esto siempre que prepares un parche, sube la carpeta y el version.json actualizado, y todos los launchers detectarán el cambio en la próxima apertura.

Paso 2 — Estructura del proyecto .NET

Crea un proyecto Windows Forms App (.NET Framework) llamado MuLauncher. Estructura sugerida:

MuLauncher/
├── Program.cs           ← punto de entrada
├── MainForm.cs          ← ventana y orquestación
├── MainForm.Designer.cs ← layout de los controles
├── UpdateService.cs     ← lógica de manifiesto y descarga
├── Models.cs            ← clases del JSON
└── Resources/
    ├── background.png
    └── logo.png

Models.cs — mapeo del JSON

using System.Text.Json.Serialization;

namespace MuLauncher
{
    public class Manifest
    {
        [JsonPropertyName("version")]        public string Version { get; set; }
        [JsonPropertyName("patch_base_url")] public string PatchBaseUrl { get; set; }
        [JsonPropertyName("main_exe")]       public string MainExe { get; set; }
        [JsonPropertyName("files")]          public FileEntry[] Files { get; set; }
    }

    public class FileEntry
    {
        [JsonPropertyName("path")] public string Path { get; set; }
        [JsonPropertyName("hash")] public string Hash { get; set; }
        [JsonPropertyName("size")] public long   Size { get; set; }
    }
}

Paso 3 — El servicio de actualización

Toda la inteligencia queda en UpdateService.cs: descargar el manifiesto, decidir qué actualizar y descargar con progreso. Fíjate en el cálculo de progreso por bytes, no por número de archivos — así la barra es honesta incluso con archivos de tamaños muy distintos.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;

namespace MuLauncher
{
    public class DownloadProgress
    {
        public string FileName { get; set; }
        public long   BytesDone { get; set; }
        public long   BytesTotal { get; set; }
        public int    Percent => BytesTotal == 0 ? 0 : (int)(BytesDone * 100 / BytesTotal);
    }

    public class UpdateService
    {
        private readonly HttpClient _http = new HttpClient();
        private readonly string _clientDir;

        public UpdateService(string clientDir) => _clientDir = clientDir;

        public async Task<Manifest> FetchManifestAsync(string url)
        {
            string json = await _http.GetStringAsync(url);
            return JsonSerializer.Deserialize<Manifest>(json);
        }

        // Retorna solo los archivos que necesitan descargarse
        public List<FileEntry> DiffFiles(Manifest m)
        {
            var pending = new List<FileEntry>();
            foreach (var f in m.Files)
            {
                string local = Path.Combine(_clientDir, f.Path.Replace('/', '\\'));
                if (!File.Exists(local) || LocalHash(local) != f.Hash.ToLower())
                    pending.Add(f);
            }
            return pending;
        }

        public async Task DownloadAsync(Manifest m, List<FileEntry> pending,
                                        IProgress<DownloadProgress> progress)
        {
            long total = 0, done = 0;
            foreach (var f in pending) total += f.Size;

            foreach (var f in pending)
            {
                string url   = m.PatchBaseUrl.TrimEnd('/') + "/" + f.Path;
                string local = Path.Combine(_clientDir, f.Path.Replace('/', '\\'));
                Directory.CreateDirectory(Path.GetDirectoryName(local));

                // graba en un archivo temporal y solo lo cambia al final (atómico)
                string tmp = local + ".part";
                using (var resp = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
                using (var src  = await resp.Content.ReadAsStreamAsync())
                using (var dst  = File.Create(tmp))
                {
                    byte[] buffer = new byte[81920];
                    int read;
                    while ((read = await src.ReadAsync(buffer, 0, buffer.Length)) > 0)
                    {
                        await dst.WriteAsync(buffer, 0, read);
                        done += read;
                        progress?.Report(new DownloadProgress
                        {
                            FileName  = Path.GetFileName(local),
                            BytesDone = done,
                            BytesTotal = total
                        });
                    }
                }

                // valida el hash del archivo descargado antes de aceptarlo
                if (LocalHash(tmp) != f.Hash.ToLower())
                {
                    File.Delete(tmp);
                    throw new Exception($"Hash inválido en {f.Path}. Descarga corrupta.");
                }

                if (File.Exists(local)) File.Delete(local);
                File.Move(tmp, local);
            }
        }

        private static string LocalHash(string path)
        {
            using var sha = SHA256.Create();
            using var stream = File.OpenRead(path);
            return BitConverter.ToString(sha.ComputeHash(stream))
                               .Replace("-", "").ToLower();
        }
    }
}

Dos detalles de producción que separan un launcher amateur de uno profesional:

  1. Descarga atómica: descargamos a arquivo.part y solo renombramos al final. Si el jugador cierra el launcher a la mitad, el archivo original no queda corrupto.
  2. Validación de hash tras la descarga: incluso con HTTPS, una descarga puede truncarse por una caída de conexión. Rechazamos cualquier archivo cuyo hash no coincida.

Paso 4 — La ventana principal

MainForm.cs orquesta todo y cuida la experiencia: estado, barra de progreso y el botón Jugar.

using System;
using System.IO;
using System.Windows.Forms;

namespace MuLauncher
{
    public partial class MainForm : Form
    {
        private const string MANIFEST_URL = "https://cdn.meuservidor.com/version.json";
        private readonly string _dir = AppDomain.CurrentDomain.BaseDirectory;
        private Manifest _manifest;

        public MainForm()
        {
            InitializeComponent();
            Shown += async (s, e) => await RunUpdateFlow();
        }

        private async System.Threading.Tasks.Task RunUpdateFlow()
        {
            btnPlay.Enabled = false;
            var svc = new UpdateService(_dir);
            try
            {
                lblStatus.Text = "Verificando versión...";
                _manifest = await svc.FetchManifestAsync(MANIFEST_URL);

                var pending = svc.DiffFiles(_manifest);
                if (pending.Count == 0)
                {
                    lblStatus.Text = $"Cliente actualizado (v{_manifest.Version})";
                }
                else
                {
                    var progress = new Progress<DownloadProgress>(p =>
                    {
                        progressBar.Value = Math.Min(p.Percent, 100);
                        lblStatus.Text = $"Descargando {p.FileName} — {p.Percent}%";
                    });
                    await svc.DownloadAsync(_manifest, pending, progress);
                    progressBar.Value = 100;
                    lblStatus.Text = $"¡Actualizado a v{_manifest.Version}!";
                }
            }
            catch (Exception ex)
            {
                lblStatus.Text = "Falló la actualización.";
                MessageBox.Show($"No se pudo actualizar:\n{ex.Message}\n\n" +
                    "Verifica tu conexión e inténtalo de nuevo.",
                    "Actualización", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
            btnPlay.Enabled = true;
        }

        private void btnPlay_Click(object sender, EventArgs e)
        {
            string exe = Path.Combine(_dir, _manifest?.MainExe ?? "Main.exe");
            if (!File.Exists(exe))
            {
                MessageBox.Show("¡Main.exe no encontrado en la carpeta del cliente!",
                    "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            System.Diagnostics.Process.Start(exe);
            Application.Exit();
        }
    }
}

Controles mínimos en el Designer: un Label lblStatus, un ProgressBar progressBar (0–100), un Button btnPlay ("JUGAR"), un PictureBox para el fondo y, opcionalmente, un WebBrowser para las noticias provenientes del news_url.

Paso 5 — Publicar parches y hacer deploy

El flujo de publicación de un parche es siempre el mismo:

  1. Modifica los archivos del cliente (por ejemplo, un nuevo Item.bmd o una textura).
  2. Copia los archivos modificados a la carpeta pública de parches, preservando la estructura (patches/Data/Item.bmd).
  3. Ejecuta el gerar-manifesto.ps1 para recalcular hashes y regenerar el version.json.
  4. Sube el version.json al último — así ningún jugador toma un manifiesto que apunta a un archivo aún no subido.

En el servidor Linux, garantiza los permisos y el acceso HTTP:

sudo mkdir -p /var/www/cdn/patches/Data
# subida vía rsync/sftp a /var/www/cdn/patches/
sudo chown -R www-data:www-data /var/www/cdn
sudo chmod -R 755 /var/www/cdn
curl -I https://cdn.meuservidor.com/version.json   # debe retornar 200

Errores comunes y soluciones

SíntomaCausa probableSolución
El launcher descarga todo cada vezEl hash del manifiesto no coincide con el archivo publicadoRegenera el manifiesto DESPUÉS de subir los archivos; revisa mayúsculas/minúsculas del hash
"Hash inválido / descarga corrupta"La conexión se cayó o el archivo cambió a la mitadRepite la descarga; garantiza HTTPS y un servidor estable
El antivirus bloquea el launcher.exe sin firmar descargando otros .exeFirma con certificado de code signing; evita empaquetadores
Main.exe no inicia tras actualizarMain.exe descargado incompleto o de season equivocadaValida el hash; confirma que el Main del parche es el de la season correcta
404 al descargar un archivopatch_base_url + path no existeRevisa la estructura de carpetas en el servidor y las barras en el path
La barra de progreso "se traba" en un archivo grandeProgreso contado por archivo, no por bytesUsa progreso por bytes, como en el UpdateService de arriba
Jugadores en versiones diferentesManifiesto cacheado por CDN/navegadorEnvía Cache-Control: no-cache en el version.json

Buenas prácticas de seguridad y experiencia

  • HTTPS obligatorio para el manifiesto y los parches. Sin él, un atacante en la red puede inyectar un Main.exe malicioso.
  • Firma el ejecutable del launcher. Esto reduce drásticamente los falsos positivos del antivirus y transmite confianza.
  • Nunca pidas la contraseña en el launcher. El login se hace dentro del Main.exe, en el game server. Un launcher que recopila credenciales es un antipatrón y un riesgo.
  • Trata el launcher como recuperable: en caso de error de red, deja que el jugador intente jugar de todas formas cuando tenga sentido, o al menos ofrece "reintentar".
  • Cache-Control en el version.json para evitar que proxies sirvan manifiestos viejos.

Lista de verificación de lanzamiento

  • Manifiesto version.json generado automáticamente por script
  • Hashes SHA-256 coinciden con los archivos publicados
  • Parches alojados con HTTPS y Cache-Control: no-cache en el manifiesto
  • Descarga atómica (.part) implementada y probada
  • Validación de hash tras la descarga activa
  • Barra de progreso basada en bytes
  • El botón Jugar inicia el Main.exe correcto y cierra el launcher
  • Ejecutable del launcher firmado con certificado
  • Prueba en máquina limpia (sin el cliente instalado)
  • Prueba de parche incremental (solo descargan los archivos modificados)
  • Prueba de caída de conexión a mitad de la descarga
  • Flujo de publicación documentado para el equipo

Preguntas frecuentes

¿Necesito un launcher si ya distribuyo el cliente en ZIP?

El ZIP funciona para la instalación inicial, pero cada parche exigiría que el jugador descargara y extrajera manualmente. Un launcher .NET verifica la versión en cada apertura y descarga solo los archivos modificados, reduciendo el soporte y manteniendo a todos en la misma build.

¿Qué framework elegir, .NET Framework o .NET moderno?

.NET Framework 4.7.2 corre en prácticamente cualquier Windows sin instalar nada extra, ideal para el público de MU. .NET 6/8 exige runtime o publicación self-contained, pero da binarios más nuevos. Para máxima compatibilidad, empieza por el Framework 4.7.2.

¿El launcher necesita HTTPS?

Muy recomendado. Sin HTTPS el manifiesto y los parches viajan en texto plano y pueden ser adulterados por un atacante en la red. Combina HTTPS con verificación de hash SHA-256 para garantizar que el archivo descargado es exactamente el publicado.

¿Cómo evito que el antivirus bloquee el launcher?

Firma el ejecutable con un certificado de code signing, evita empaquetadores agresivos y aloja los parches en un dominio propio con HTTPS. Los launchers sin firmar que descargan .exe son el patrón que dispara las heurísticas del antivirus.

¿El launcher funciona para cualquier season?

Sí. El launcher solo copia archivos a la carpeta del cliente y ejecuta el Main.exe — es agnóstico de la season. Lo que cambia por season es el contenido de los archivos (Data, offsets del Main), no la lógica de actualización. Los detalles de archivo varían por season/cliente.

BR
Editor de eventos, mapas e ítems

Bruno es especialista en eventos, mapas, bosses y economía de ítems de MU Online. Documenta cada detalle basándose en el juego real.

Sigue leyendo

Artículos relacionados

🔃
Tutorial

Cómo configurar parche automático mediante launcher en MU Online

Monta un flujo de parche automático de punta a punta para el cliente de MU Online — desde el empaquetado de los archivos modificados hasta la publicación del manifiesto, con versionado, rollback y distribución mediante el launcher.

23 min · Avanzado ·
🚀
Tutorial

Cómo crear un Launcher de actualización para servidor de MU Online

Guía completa para crear y configurar un Launcher (actualizador automático) para tu servidor de MU Online: qué es el launcher y por qué es esencial para un servidor público (no es solo cosmético — es cómo actualizas el cliente de todos los jugadores a la vez sin que tengan que reinstalar nada), los 3 componentes del sistema de launcher (el programa .exe del jugador, el servidor de archivos con los updates, y la lista de versiones), qué información va en la lista de versiones (nombre, hash MD5, URL de descarga), cómo alojar los archivos de actualización en tu sitio web o en un servidor de archivos dedicado, cómo configurar el launcher con tu dominio y la URL de las noticias, cómo probar el launcher en una instalación limpia, los errores más comunes en launchers de MU Online y sus soluciones, y cómo actualizar el cliente de todos tus jugadores cuando publicas un parche.

12 min · Intermedio ·
Tutorial

Cómo optimizar los FPS del cliente de MU en máquinas débiles

Gana FPS en el cliente de MU Online en PCs débiles ajustando resolución, efectos, drivers y configuraciones de Windows, con un método para medir antes y después y hallar el cuello de botella real.

22 min · Intermedio ·