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.
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:
| Item | Recomendação | Observação |
|---|---|---|
| IDE | Visual Studio 2022 Community | Gratuito; inclua a carga de trabalho ".NET desktop development" |
| Framework | .NET Framework 4.7.2 | Máxima compatibilidade no público de MU (Windows 7+) |
| Pacote JSON | System.Text.Json ou Newtonsoft.Json | Via NuGet |
| Servidor web | Apache/Nginx com HTTPS | Pode ser o mesmo VPS do site |
| Cliente base | Pasta completa do MU da sua season | Main.exe já com IP editado |
| Ferramenta de hash | PowerShell Get-FileHash | Já 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
pathserá 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:
- Download atômico: baixamos para
arquivo.parte só renomeamos ao final. Se o jogador fechar o launcher no meio, o arquivo original não fica corrompido. - 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:
- Altere os arquivos do cliente (por exemplo, um novo
Item.bmdou uma textura). - Copie os arquivos alterados para a pasta pública de patches, preservando a estrutura (
patches/Data/Item.bmd). - Rode o
gerar-manifesto.ps1para recalcular hashes e regenerar oversion.json. - Faça upload do
version.jsonpor ú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
| Sintoma | Causa provável | Solução |
|---|---|---|
| Launcher baixa tudo toda vez | Hash do manifesto não bate com o arquivo publicado | Regenere 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 meio | Repita o download; garanta HTTPS e um servidor estável |
| Antivírus bloqueia o launcher | .exe não assinado baixando outros .exe | Assine com certificado de code signing; evite empacotadores |
| Main.exe não inicia após atualizar | Main.exe baixado incompleto ou de season errada | Valide o hash; confirme que o Main no patch é o da season correta |
| 404 ao baixar um arquivo | patch_base_url + path não existe | Confira a estrutura de pastas no servidor e barras no path |
| Barra de progresso "trava" em arquivo grande | Progresso contado por arquivo, não por bytes | Use progresso por bytes, como no UpdateService acima |
| Jogadores em versões diferentes | Manifesto cacheado por CDN/navegador | Envie 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.jsonpara evitar que proxies sirvam manifestos antigos.
Checklist de lançamento
- Manifesto
version.jsongerado automaticamente por script - Hashes SHA-256 conferem com os arquivos publicados
- Patches hospedados com HTTPS e
Cache-Control: no-cacheno 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.