O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Cliente

Como criar um launcher de atualização profissional (.NET) para MU Online

Construa um launcher .NET completo para seu servidor de MU Online — com manifesto de versão em JSON, verificação por hash SHA-256, download incremental de patches, barra de progresso e inicialização automática do Main.exe.

BR Bruno · Atualizado em 18 jun 2024 · ⏱ 24 min de leitura
Resposta rápida

Um launcher de atualização é a porta de entrada do seu servidor: é a primeira tela que o jogador vê, o mecanismo que garante que todos rodam a mesma build e o canal por onde você distribui correções sem pedir para ninguém baixar ZIP manualmente. Um launcher amador — que só abre o Main.exe — desperdi

Um launcher de atualização é a porta de entrada do seu servidor: é a primeira tela que o jogador vê, o mecanismo que garante que todos rodam a mesma build e o canal por onde você distribui correções sem pedir para ninguém baixar ZIP manualmente. Um launcher amador — que só abre o Main.exe — desperdiça essa oportunidade e gera um fluxo interminável de "meu cliente está desatualizado" no suporte. Neste tutorial você vai construir um launcher profissional em .NET, com manifesto de versão em JSON, verificação por hash, download incremental, barra de progresso, tratamento de erros e inicialização automática do jogo.

O foco aqui é o cliente: o executável que roda na máquina do jogador, os arquivos da pasta do MU e a distribuição de patches. Não vamos configurar o game server — para isso, veja o guia de como criar servidor de MU Online. Aqui tratamos exclusivamente da camada que o jogador enxerga.

Como um launcher profissional funciona

O princípio é simples e não muda entre seasons: o launcher compara o que está instalado localmente com um manifesto hospedado no seu servidor web e baixa apenas as diferenças.

JOGADOR (Launcher.exe)
   │
   │ 1. HTTP GET https://cdn.meuservidor.com/version.json
   │    → manifesto: versão, lista de arquivos, hash, tamanho
   ▼
COMPARAÇÃO LOCAL
   │  para cada arquivo: hash local == hash do manifesto?
   │  se igual → pula;  se diferente/ausente → marca para download
   ▼
DOWNLOAD INCREMENTAL
   │ 2. GET https://cdn.meuservidor.com/patches/Data/Item.bmd ...
   │    grava só os arquivos alterados, atualiza a barra
   ▼
INICIAR JOGO
   │ 3. Process.Start("Main.exe")  → launcher fecha
   ▼
Main.exe conecta ao Connect Server

O manifesto é a peça central. Ele lista cada arquivo distribuível (texturas BMD, arquivos da pasta Data, o próprio Main.exe), com o hash SHA-256 e o tamanho. O launcher nunca "adivinha" o que mudou: ele confia no hash. Isso torna o sistema idempotente — rodar o launcher duas vezes seguidas não baixa nada na segunda vez.

Pré-requisitos

Antes de escrever qualquer linha de código, tenha o ambiente pronto:

ItemRecomendaçãoObservação
IDEVisual Studio 2022 CommunityGratuito; inclua a carga de trabalho ".NET desktop development"
Framework.NET Framework 4.7.2Máxima compatibilidade no público de MU (Windows 7+)
Pacote JSONSystem.Text.Json ou Newtonsoft.JsonVia NuGet
Servidor webApache/Nginx com HTTPSPode ser o mesmo VPS do site
Cliente basePasta completa do MU da sua seasonMain.exe já com IP editado
Ferramenta de hashPowerShell Get-FileHashJá vem no Windows

Você também precisa de uma decisão de arquitetura: onde os patches ficam hospedados. O ideal é separar o servidor de arquivos (um subdomínio tipo cdn. ou patch.) do game server. Assim, um pico de downloads no dia do patch não afeta a latência do jogo. Pode ser o mesmo VPS com um virtual host dedicado, ou uma hospedagem estática barata.

Passo 1 — Definir o manifesto de versão (version.json)

O manifesto é um JSON estático servido por HTTP. Estrutura 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: versão do conteúdo. Só serve para exibir e para logar; a decisão real de baixar vem do hash.
  • min_launcher: permite forçar atualização do próprio launcher. Se o launcher instalado for mais antigo, você avisa o jogador para rebaixar um novo.
  • patch_base_url: prefixo onde cada path será buscado. patch_base_url + path = URL final do arquivo.
  • files[].hash: SHA-256 em hexadecimal minúsculo. É a fonte da verdade.

> Os caminhos em path refletem a estrutura real da pasta do cliente. Os nomes Item.bmd, Text.bmd e as subpastas são exemplos comuns de Season 6 — a organização exata varia por season/cliente. O importante é que o path no manifesto seja idêntico ao caminho relativo dentro da pasta do MU.

Gerar o manifesto automaticamente

Nunca calcule hashes à mão. Use este script PowerShell, que varre a pasta do patch e emite o JSON pronto:

# gerar-manifesto.ps1
$raiz    = "C:\ClienteMU"          # pasta base do 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 "Manifesto gerado com $($lista.Count) arquivos."

Rode isso sempre que preparar um patch, faça upload da pasta e do version.json atualizado, e todos os launchers detectarão a mudança na próxima abertura.

Passo 2 — Estrutura do projeto .NET

Crie um projeto Windows Forms App (.NET Framework) chamado MuLauncher. Estrutura sugerida:

MuLauncher/
├── Program.cs           ← ponto de entrada
├── MainForm.cs          ← janela e orquestração
├── MainForm.Designer.cs ← layout dos controles
├── UpdateService.cs     ← lógica de manifesto e download
├── Models.cs            ← classes do JSON
└── Resources/
    ├── background.png
    └── logo.png

Models.cs — mapeamento do 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; }
    }
}

Passo 3 — O serviço de atualização

Toda a inteligência fica em UpdateService.cs: baixar o manifesto, decidir o que atualizar e baixar com progresso. Repare no cálculo de progresso por bytes, não por número de arquivos — assim a barra é honesta mesmo com arquivos de tamanhos muito diferentes.

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 apenas os arquivos que precisam ser baixados
        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));

                // grava em arquivo temporário e só troca ao 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 o hash do arquivo baixado antes de aceitar
                if (LocalHash(tmp) != f.Hash.ToLower())
                {
                    File.Delete(tmp);
                    throw new Exception($"Hash inválido em {f.Path}. Download corrompido.");
                }

                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();
        }
    }
}

Dois detalhes de produção que separam um launcher amador de um profissional:

  1. Download atômico: baixamos para arquivo.part e só renomeamos ao final. Se o jogador fechar o launcher no meio, o arquivo original não fica corrompido.
  2. Validação de hash pós-download: mesmo com HTTPS, um download pode truncar por queda de conexão. Rejeitamos qualquer arquivo cujo hash não bata.

Passo 4 — A janela principal

MainForm.cs orquestra tudo e cuida da experiência: status, barra de progresso e o botão Jogar.

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 versão...";
                _manifest = await svc.FetchManifestAsync(MANIFEST_URL);

                var pending = svc.DiffFiles(_manifest);
                if (pending.Count == 0)
                {
                    lblStatus.Text = $"Cliente atualizado (v{_manifest.Version})";
                }
                else
                {
                    var progress = new Progress<DownloadProgress>(p =>
                    {
                        progressBar.Value = Math.Min(p.Percent, 100);
                        lblStatus.Text = $"Baixando {p.FileName} — {p.Percent}%";
                    });
                    await svc.DownloadAsync(_manifest, pending, progress);
                    progressBar.Value = 100;
                    lblStatus.Text = $"Atualizado para v{_manifest.Version}!";
                }
            }
            catch (Exception ex)
            {
                lblStatus.Text = "Falha na atualização.";
                MessageBox.Show($"Não foi possível atualizar:\n{ex.Message}\n\n" +
                    "Verifique sua conexão e tente novamente.",
                    "Atualização", 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 não encontrado na pasta do cliente!",
                    "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            System.Diagnostics.Process.Start(exe);
            Application.Exit();
        }
    }
}

Controles mínimos no Designer: um Label lblStatus, um ProgressBar progressBar (0–100), um Button btnPlay ("JOGAR"), um PictureBox para o fundo e, opcionalmente, um WebBrowser para notícias vindas do news_url.

Passo 5 — Publicar patches e fazer deploy

O fluxo de publicação de um patch é sempre o mesmo:

  1. Altere os arquivos do cliente (por exemplo, um novo Item.bmd ou uma textura).
  2. Copie os arquivos alterados para a pasta pública de patches, preservando a estrutura (patches/Data/Item.bmd).
  3. Rode o gerar-manifesto.ps1 para recalcular hashes e regenerar o version.json.
  4. Faça upload do version.json por último — assim nenhum jogador pega um manifesto que aponta para um arquivo ainda não enviado.

No servidor Linux, garanta as permissões e o acesso HTTP:

sudo mkdir -p /var/www/cdn/patches/Data
# upload via rsync/sftp para /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   # deve retornar 200

Erros comuns e soluções

SintomaCausa provávelSolução
Launcher baixa tudo toda vezHash do manifesto não bate com o arquivo publicadoRegenere o manifesto DEPOIS de subir os arquivos; confira maiúsculas/minúsculas do hash
"Hash inválido / download corrompido"Conexão caiu ou arquivo trocado no meioRepita o download; garanta HTTPS e um servidor estável
Antivírus bloqueia o launcher.exe não assinado baixando outros .exeAssine com certificado de code signing; evite empacotadores
Main.exe não inicia após atualizarMain.exe baixado incompleto ou de season erradaValide o hash; confirme que o Main no patch é o da season correta
404 ao baixar um arquivopatch_base_url + path não existeConfira a estrutura de pastas no servidor e barras no path
Barra de progresso "trava" em arquivo grandeProgresso contado por arquivo, não por bytesUse progresso por bytes, como no UpdateService acima
Jogadores em versões diferentesManifesto cacheado por CDN/navegadorEnvie Cache-Control: no-cache no version.json

Boas práticas de segurança e experiência

  • HTTPS obrigatório para o manifesto e os patches. Sem ele, um atacante na rede pode injetar um Main.exe malicioso.
  • Assine o executável do launcher. Isso reduz drasticamente falsos positivos de antivírus e transmite confiança.
  • Nunca peça senha no launcher. O login é feito dentro do Main.exe, no game server. Um launcher que coleta credenciais é um antipadrão e um risco.
  • Trate o launcher como recuperável: em caso de erro de rede, deixe o jogador tentar jogar mesmo assim quando fizer sentido, ou ao menos ofereça "tentar novamente".
  • Cache-Control no version.json para evitar que proxies sirvam manifestos antigos.

Checklist de lançamento

  • Manifesto version.json gerado automaticamente por script
  • Hashes SHA-256 conferem com os arquivos publicados
  • Patches hospedados com HTTPS e Cache-Control: no-cache no manifesto
  • Download atômico (.part) implementado e testado
  • Validação de hash pós-download ativa
  • Barra de progresso baseada em bytes
  • Botão Jogar inicia o Main.exe correto e fecha o launcher
  • Executável do launcher assinado com certificado
  • Teste em máquina limpa (sem o cliente instalado)
  • Teste de patch incremental (só arquivos alterados baixam)
  • Teste de queda de conexão no meio do download
  • Fluxo de publicação documentado para a equipe

Perguntas frequentes

Preciso de um launcher se já distribuo o cliente em ZIP?

O ZIP funciona para a instalação inicial, mas cada patch exigiria que o jogador baixasse e extraísse manualmente. Um launcher .NET verifica a versão a cada abertura e baixa apenas os arquivos alterados, reduzindo suporte e mantendo todos na mesma build.

Qual framework escolher, .NET Framework ou .NET moderno?

.NET Framework 4.7.2 roda em praticamente qualquer Windows sem instalar nada extra, ideal para o público de MU. .NET 6/8 exige runtime ou publicação self-contained, mas dá binários mais novos. Para máxima compatibilidade, comece pelo Framework 4.7.2.

O launcher precisa de HTTPS?

Fortemente recomendado. Sem HTTPS o manifesto e os patches trafegam em texto puro e podem ser adulterados por um atacante na rede. Combine HTTPS com verificação de hash SHA-256 para garantir que o arquivo baixado é exatamente o publicado.

Como evito que o antivírus bloqueie o launcher?

Assine o executável com um certificado de code signing, evite empacotadores agressivos e hospede os patches em domínio próprio com HTTPS. Launchers não assinados que baixam .exe são o padrão que dispara heurísticas de antivírus.

O launcher funciona para qualquer season?

Sim. O launcher só copia arquivos para a pasta do cliente e executa o Main.exe — ele é agnóstico de season. O que muda por season é o conteúdo dos arquivos (Data, offsets do Main), não a lógica de atualização. Detalhes de arquivo variam por season/cliente.

BR
Editor de eventos, mapas e itens

Bruno é especialista em eventos, mapas, bosses e economia de itens do MU Online. Documenta cada detalhe com base em jogo real.

Continue lendo

Artigos relacionados