Como criar um Launcher de atualização para MU Online
Crie um Launcher de auto-atualização para seu servidor de MU Online — com checklist de versão via JSON, download de patches, barra de progresso e lançamento automático do Main.exe.
Um Launcher profissional melhora muito a experiência dos jogadores — ao abrir o Launcher, ele verifica automaticamente se o cliente está atualizado, baixa os patches necessários com barra de progresso, e só então abre o Main.exe. Você nunca mais precisará pedir para jogadores baixar e extrair ZIPs d
Um Launcher profissional melhora muito a experiência dos jogadores — ao abrir o Launcher, ele verifica automaticamente se o cliente está atualizado, baixa os patches necessários com barra de progresso, e só então abre o Main.exe. Você nunca mais precisará pedir para jogadores baixar e extrair ZIPs de patch manualmente.
Arquitetura do sistema de Launcher
JOGADOR (Launcher.exe)
│
│ 1. HTTP GET: /version.json
│ {"version": "1.5", "files": [...]}
▼
SERVIDOR DE ATUALIZAÇÃO (Apache/Nginx)
│
│ 2. Se versão diferente: baixar arquivos de patch
│ GET /patches/1.5/Data/MonsterSetBase.txt
▼
PASTA LOCAL DO CLIENTE
│
│ 3. Arquivos atualizados
▼
Main.exe ← Launcher inicia o jogo
Passo 1 — Configurar o servidor de atualização
O servidor de atualização é um servidor web que serve os arquivos de patch. Pode ser a mesma VPS do servidor de MU ou a hospedagem do site.
Estrutura de pastas no servidor web
public_html/atualização/
├── version.json ← arquivo de controle de versão
└── patches/
├── 1.0/ ← patch da versão 1.0 (instalação inicial)
│ └── Data/
│ └── Monster.txt
├── 1.1/ ← patch com novos conteúdos
│ └── Data/
│ └── MonsterSetBase.txt
└── 1.5/ ← versão atual
├── Data/
│ └── Drop.txt
└── Main.exe
version.json — arquivo de controle de versão
{
"version": "1.5",
"min_version": "1.0",
"server_name": "MeuServidor",
"patch_url": "https://www.meuservidor.com.br/atualizacao/patches/",
"news_url": "https://www.meuservidor.com.br/api/news.json",
"files": [
{
"path": "Data/MonsterSetBase.txt",
"version": "1.3",
"size": 45678,
"hash": "a3b4c5d6e7f8901234..."
},
{
"path": "Data/Drop.txt",
"version": "1.5",
"size": 89012,
"hash": "b4c5d6e7f8901234ab..."
},
{
"path": "Main.exe",
"version": "1.5",
"size": 2345678,
"hash": "c5d6e7f890123456bc..."
}
]
}
Gerar hash SHA256 dos arquivos
# PowerShell — gerar hash de todos os arquivos do patch
$files = Get-ChildItem -Recurse "C:\ClienteSource\patches\1.5\"
foreach ($file in $files) {
$hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLower()
$rel = $file.FullName.Replace("C:\ClienteSource\patches\1.5\", "")
Write-Output "'$rel': '$hash'"
}
Passo 2 — Criar o Launcher em C# (WinForms)
Requisitos
- Visual Studio 2022 Community (gratuito)
- .NET Framework 4.7.2 ou .NET 6+
Estrutura do projeto
MuLauncher/
├── Form1.cs ← tela principal do Launcher
├── Form1.Designer.cs ← design da janela
├── Updater.cs ← lógica de atualização
├── Program.cs ← ponto de entrada
└── Resources/
├── background.jpg
└── logo.png
Program.cs — ponto de entrada
using System;
using System.Windows.Forms;
namespace MuLauncher
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
Form1.cs — janela principal
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json; // NuGet: Newtonsoft.Json
namespace MuLauncher
{
public partial class Form1 : Form
{
private const string VERSION_URL = "https://www.meuservidor.com.br/atualizacao/version.json";
private const string LOCAL_VERSION_FILE = "version.txt";
private const string MAIN_EXE = "Main.exe";
private HttpClient _http = new HttpClient();
public Form1()
{
InitializeComponent();
Load += Form1_Load;
}
private async void Form1_Load(object sender, EventArgs e)
{
lblStatus.Text = "Verificando atualizações...";
btnJogar.Enabled = false;
try
{
await CheckAndUpdate();
}
catch (Exception ex)
{
MessageBox.Show($"Erro ao verificar atualizações: {ex.Message}\n\nVocê pode tentar jogar mesmo assim.",
"Aviso", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
btnJogar.Enabled = true;
lblStatus.Text = "Pronto para jogar!";
}
private async Task CheckAndUpdate()
{
// Baixar version.json do servidor
string json = await _http.GetStringAsync(VERSION_URL);
var serverVersion = JsonConvert.DeserializeObject<VersionInfo>(json);
// Ler versão local
string localVersion = "0.0";
if (File.Exists(LOCAL_VERSION_FILE))
localVersion = File.ReadAllText(LOCAL_VERSION_FILE).Trim();
// Comparar versões
if (localVersion == serverVersion.Version)
{
lblStatus.Text = $"Cliente atualizado (v{localVersion})";
return;
}
// Atualizar
lblStatus.Text = $"Atualizando de v{localVersion} para v{serverVersion.Version}...";
var updater = new Updater(_http, serverVersion.PatchUrl);
updater.ProgressChanged += (s, p) =>
{
progressBar.Value = p.Percentage;
lblStatus.Text = $"Baixando: {p.FileName} ({p.Percentage}%)";
};
await updater.DownloadFiles(serverVersion.Files);
// Salvar versão atualizada
File.WriteAllText(LOCAL_VERSION_FILE, serverVersion.Version);
lblStatus.Text = $"Atualizado para v{serverVersion.Version}!";
}
private void btnJogar_Click(object sender, EventArgs e)
{
if (!File.Exists(MAIN_EXE))
{
MessageBox.Show("Main.exe não encontrado!", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
System.Diagnostics.Process.Start(MAIN_EXE);
Application.Exit();
}
}
// Modelo do version.json
public class VersionInfo
{
[JsonProperty("version")] public string Version { get; set; }
[JsonProperty("patch_url")] public string PatchUrl { get; set; }
[JsonProperty("files")] public FileEntry[] Files { get; set; }
}
public class FileEntry
{
[JsonProperty("path")] public string Path { get; set; }
[JsonProperty("version")] public string Version { get; set; }
[JsonProperty("hash")] public string Hash { get; set; }
[JsonProperty("size")] public long Size { get; set; }
}
}
Updater.cs — lógica de download
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace MuLauncher
{
public class ProgressEventArgs : EventArgs
{
public string FileName { get; set; }
public int Percentage { get; set; }
}
public class Updater
{
private HttpClient _http;
private string _patchBaseUrl;
public event EventHandler<ProgressEventArgs> ProgressChanged;
public Updater(HttpClient http, string patchBaseUrl)
{
_http = http;
_patchBaseUrl = patchBaseUrl.TrimEnd('/');
}
public async Task DownloadFiles(FileEntry[] files)
{
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
string localPath = file.Path.Replace('/', '\\');
// Verificar se precisa atualizar (comparar hash)
if (File.Exists(localPath) && GetFileHash(localPath) == file.Hash)
continue;
// Criar diretório se não existir
string dir = Path.GetDirectoryName(localPath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
// Baixar o arquivo
int percent = (int)((double)(i + 1) / files.Length * 100);
ProgressChanged?.Invoke(this, new ProgressEventArgs
{
FileName = Path.GetFileName(localPath),
Percentage = percent
});
string url = $"{_patchBaseUrl}/{file.Path}";
byte[] data = await _http.GetByteArrayAsync(url);
File.WriteAllBytes(localPath, data);
}
}
private string GetFileHash(string path)
{
using var sha256 = SHA256.Create();
using var stream = File.OpenRead(path);
byte[] hash = sha256.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
}
Form1.Designer.cs — layout básico da janela
// Controles básicos para adicionar no Designer do Visual Studio:
// - Label: lblStatus (status atual)
// - ProgressBar: progressBar (0-100)
// - Button: btnJogar ("JOGAR")
// - PictureBox: picLogo (logo do servidor)
// - WebBrowser ou RichTextBox: notícias (opcional)
Passo 3 — Alternativa: Launcher em Python
Para quem prefere Python (requer Python instalado no PC do jogador):
# launcher.py
import tkinter as tk
from tkinter import ttk
import requests, json, hashlib, os, subprocess, threading
VERSION_URL = "https://www.meuservidor.com.br/atualizacao/version.json"
LOCAL_VERSION_FILE = "version.txt"
class Launcher:
def __init__(self):
self.root = tk.Tk()
self.root.title("MeuServidor Launcher")
self.root.geometry("600x400")
self.root.resizable(False, False)
self.status_var = tk.StringVar(value="Verificando atualizações...")
tk.Label(self.root, textvariable=self.status_var).pack(pady=10)
self.progress = ttk.Progressbar(self.root, length=500, mode='determinate')
self.progress.pack(pady=10)
self.play_btn = tk.Button(self.root, text="JOGAR", state=tk.DISABLED,
command=self.launch_game, width=20, height=2)
self.play_btn.pack(pady=20)
# Iniciar verificação em thread separada
thread = threading.Thread(target=self.check_updates)
thread.daemon = True
thread.start()
self.root.mainloop()
def check_updates(self):
try:
resp = requests.get(VERSION_URL, timeout=10)
server_info = resp.json()
server_version = server_info["version"]
local_version = "0.0"
if os.path.exists(LOCAL_VERSION_FILE):
with open(LOCAL_VERSION_FILE) as f:
local_version = f.read().strip()
if local_version == server_version:
self.status_var.set(f"Atualizado (v{local_version})")
self.play_btn.config(state=tk.NORMAL)
return
files = server_info["files"]
patch_url = server_info["patch_url"]
for i, file_info in enumerate(files):
path = file_info["path"]
url = patch_url + path
# Verificar hash
if os.path.exists(path):
sha256 = hashlib.sha256(open(path, 'rb').read()).hexdigest()
if sha256 == file_info["hash"]:
continue
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
self.status_var.set(f"Baixando: {os.path.basename(path)}...")
data = requests.get(url).content
with open(path, 'wb') as f:
f.write(data)
percent = int((i + 1) / len(files) * 100)
self.progress["value"] = percent
self.root.update_idletasks()
with open(LOCAL_VERSION_FILE, 'w') as f:
f.write(server_version)
self.status_var.set(f"Pronto! (v{server_version})")
self.play_btn.config(state=tk.NORMAL)
except Exception as e:
self.status_var.set(f"Erro: {e}")
self.play_btn.config(state=tk.NORMAL) # permitir jogar mesmo com erro
def launch_game(self):
if os.path.exists("Main.exe"):
subprocess.Popen(["Main.exe"])
self.root.destroy()
if __name__ == "__main__":
Launcher()
Passo 4 — Deploy do servidor de atualização
# No servidor web (VPS Ubuntu)
# Criar pasta para os patches
sudo mkdir -p /var/www/mu-site/atualizacao/patches/1.5
# Fazer upload dos arquivos de patch via FTP/SFTP
# (use FileZilla ou rsync)
# Permissões corretas
sudo chown -R www-data:www-data /var/www/mu-site/atualizacao/
sudo chmod -R 755 /var/www/mu-site/atualizacao/
# Certificar que os arquivos são acessíveis via HTTP
curl https://www.meuservidor.com.br/atualizacao/version.json
Processo de publicar um patch
- Modifique os arquivos do cliente (ex: novo
Monster.txt); - Calcule o hash SHA256 do novo arquivo;
- Copie o arquivo para
/patches/[versão]/pasta/arquivo.txtno servidor; - Atualize o
version.jsoncom a nova versão e o novo hash; - O Launcher dos jogadores detectará automaticamente na próxima abertura.
# Script para gerar version.json automaticamente
$patchFolder = "C:\Patches\1.6"
$files = Get-ChildItem $patchFolder -Recurse -File
$fileList = @()
foreach ($f in $files) {
$hash = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower()
$rel = $f.FullName.Replace("$patchFolder\", "").Replace("\", "/")
$fileList += @{
path = $rel
version = "1.6"
hash = $hash
size = $f.Length
}
}
$versionJson = @{
version = "1.6"
patch_url = "https://www.meuservidor.com.br/atualizacao/patches/1.6/"
files = $fileList
} | ConvertTo-Json -Depth 3
$versionJson | Out-File "version.json" -Encoding utf8
Write-Host "version.json gerado para v1.6!"
Perguntas frequentes
O Launcher é obrigatório?
Não. Você pode distribuir patches manualmente (ZIP). O Launcher automatiza esse processo e melhora muito a experiência do jogador — eles não precisam baixar e extrair patches manualmente.
Em qual linguagem criar o Launcher?
C# (WPF ou WinForms) é a opção mais comum e resulta em um executável nativo rápido. Python com tkinter funciona, mas requer instalação do Python. Electron (JavaScript) cria launchers modernos mas é mais pesado.
O servidor de atualização é o mesmo do jogo?
Não necessariamente. O servidor de atualização é um servidor web (Apache/Nginx) que serve arquivos de patch. Pode estar no mesmo VPS ou em hospedagem separada.
Como funciona o sistema de versão?
O Launcher lê um arquivo JSON (ou XML) no servidor com a versão atual e a lista de arquivos. Compara com a versão instalada. Se houver diferença, baixa apenas os arquivos novos.