<# Janela do PWRO -- o "patcher" que abre antes do jogo. Por que em PowerShell e nao um patcher pronto (Thor, RSU): esses sao executaveis de terceiros, e o projeto so distribui binario do kRO oficial, da Microsoft ou nosso -- ha teste que aborta o pacote se um .exe aparecer. WinForms desenha a janela sem trazer binario nenhum. ESTE ARQUIVO E UTF-8 COM BOM. O PowerShell 5.1 le .ps1 como ANSI quando nao ha BOM, e os acentos viram lixo na tela. Com BOM ele le como UTF-8. Ha teste que confere o BOM -- se alguem salvar sem, a janela abre com texto embaralhado e essa e a primeira coisa que o jogador ve. O motor e o mesmo de sempre: compara o manifest.json publicado pelo servidor com o que esta instalado, baixa so o que mudou e confere o SHA-256 de cada arquivo. Uso: .\PWRO.ps1 .\PWRO.ps1 -UrlBase http://pwro.exemplo/patch -PastaJogo C:\Gravity\WG #> param( [string]$UrlBase, [string]$PastaJogo ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # O PowerShell 5.1 ainda negocia TLS pelo padrao herdado em algumas maquinas, e # um servidor moderno recusa: o sintoma e exatamente este -- "sem conexao com o # servidor" sem motivo visivel, com o site abrindo normalmente no navegador. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # ------------------------------------------------------------ configuracao $cfgPath = Join-Path $PSScriptRoot 'pwro-instalacao.json' # Quem instalou antes de 2026-09-17 tem o arquivo com o nome antigo. Ler os # dois evita que a janela abra sem saber onde e o servidor depois do rename. if (-not (Test-Path -LiteralPath $cfgPath)) { $antigo = Join-Path $PSScriptRoot 'midgard-instalacao.json' if (Test-Path -LiteralPath $antigo) { $cfgPath = $antigo } } $cfg = $null if (Test-Path -LiteralPath $cfgPath) { try { $cfg = Get-Content -LiteralPath $cfgPath -Raw | ConvertFrom-Json } catch { $cfg = $null } } if (-not $PastaJogo) { $PastaJogo = if ($cfg) { $cfg.pastaJogo } else { $null } } if (-not $PastaJogo) { $PastaJogo = $PSScriptRoot } if (-not $UrlBase) { $UrlBase = if ($cfg -and $cfg.urlPatch) { $cfg.urlPatch } else { 'http://localhost/patch' } } $COR_FUNDO = [System.Drawing.Color]::FromArgb(20, 22, 29) $COR_PAINEL = [System.Drawing.Color]::FromArgb(27, 30, 39) $COR_TEXTO = [System.Drawing.Color]::FromArgb(206, 212, 224) $COR_FRACA = [System.Drawing.Color]::FromArgb(138, 146, 163) $COR_OURO = [System.Drawing.Color]::FromArgb(236, 214, 150) $COR_BOTAO = [System.Drawing.Color]::FromArgb(54, 104, 72) $COR_BOTAO_ON = [System.Drawing.Color]::FromArgb(68, 128, 90) $ALTURA_FECHADA = 412 $ALTURA_ABERTA = 578 # ------------------------------------------------------------ a janela # O icone da janela: o do proprio jogo, tirado do executavel do cliente. # Sem isto o Windows mostra o icone azul do PowerShell na barra de tarefas -- # o jogador ve "PowerShell" onde deveria ver o jogo dele. function Icone-Do-Jogo($pasta) { if (-not $pasta -or -not (Test-Path -LiteralPath $pasta)) { return $null } try { $exe = Get-ChildItem -LiteralPath $pasta -Filter '*.exe' -ErrorAction Stop | Where-Object { $_.Name -notmatch 'Warpgate|opensetup|WARP_' } | Select-Object -First 1 if ($exe) { return [System.Drawing.Icon]::ExtractAssociatedIcon($exe.FullName) } } catch { } return $null } $f = New-Object System.Windows.Forms.Form $f.Text = 'PWRO' $f.ClientSize = New-Object System.Drawing.Size(600, $ALTURA_FECHADA) $f.StartPosition = 'CenterScreen' $f.FormBorderStyle = 'None' $f.BackColor = $COR_FUNDO $ico = Icone-Do-Jogo $PastaJogo if ($ico) { $f.Icon = $ico } # --- arte do topo ----------------------------------------------------------- $banner = New-Object System.Windows.Forms.PictureBox $banner.Location = New-Object System.Drawing.Point(0, 0) $banner.Size = New-Object System.Drawing.Size(600, 150) $banner.SizeMode = 'StretchImage' $banner.BackColor = $COR_FUNDO $arte = Join-Path $PSScriptRoot 'PWRO-banner.png' if (Test-Path -LiteralPath $arte) { # lido em memoria para nao segurar o arquivo aberto -- senao o proprio # atualizador nao consegue substituir a arte $bytes = [System.IO.File]::ReadAllBytes($arte) $ms = New-Object System.IO.MemoryStream(,$bytes) $banner.Image = [System.Drawing.Image]::FromStream($ms) } else { $banner.BackColor = [System.Drawing.Color]::FromArgb(16, 18, 24) $rotulo = New-Object System.Windows.Forms.Label $rotulo.Text = 'PWRO' $rotulo.ForeColor = $COR_OURO $rotulo.Font = New-Object System.Drawing.Font('Georgia', 34, [System.Drawing.FontStyle]::Bold) $rotulo.Location = New-Object System.Drawing.Point(34, 40) $rotulo.AutoSize = $true $rotulo.BackColor = [System.Drawing.Color]::Transparent $banner.Controls.Add($rotulo) # o subtitulo que existe na arte -- sem ele a reserva parece outra coisa $sub = New-Object System.Windows.Forms.Label $sub.Text = 'R A G N A R O K O N L I N E' $sub.ForeColor = $COR_FRACA $sub.Font = New-Object System.Drawing.Font('Segoe UI', 8) $sub.Location = New-Object System.Drawing.Point(38, 96) $sub.AutoSize = $true $sub.BackColor = [System.Drawing.Color]::Transparent $banner.Controls.Add($sub) } $f.Controls.Add($banner) # --- arrastar pela arte (a janela nao tem barra de titulo) ------------------- $script:arrastando = $false $script:pMouse = New-Object System.Drawing.Point $script:pJanela = New-Object System.Drawing.Point $banner.Add_MouseDown({ $script:arrastando = $true $script:pMouse = [System.Windows.Forms.Cursor]::Position $script:pJanela = $f.Location }) $banner.Add_MouseMove({ if ($script:arrastando) { $agora = [System.Windows.Forms.Cursor]::Position $f.Location = New-Object System.Drawing.Point( ($script:pJanela.X + $agora.X - $script:pMouse.X), ($script:pJanela.Y + $agora.Y - $script:pMouse.Y)) } }) $banner.Add_MouseUp({ $script:arrastando = $false }) function Botao-Janela($texto, $x, $acao) { $b = New-Object System.Windows.Forms.Label $b.Text = $texto $b.ForeColor = $COR_FRACA $b.BackColor = [System.Drawing.Color]::Transparent $b.Font = New-Object System.Drawing.Font('Segoe UI', 11) $b.Location = New-Object System.Drawing.Point($x, 10) $b.Size = New-Object System.Drawing.Size(26, 22) $b.TextAlign = 'MiddleCenter' $b.Cursor = 'Hand' $b.Add_MouseEnter({ $this.ForeColor = $COR_OURO }) $b.Add_MouseLeave({ $this.ForeColor = $COR_FRACA }) $b.Add_Click($acao) $banner.Controls.Add($b) $b.BringToFront() return $b } Botao-Janela '—' 534 { $f.WindowState = 'Minimized' } | Out-Null Botao-Janela '✕' 564 { $f.Close() } | Out-Null # --- notícias --------------------------------------------------------------- $tituloNoticias = New-Object System.Windows.Forms.Label $tituloNoticias.Text = 'NOTÍCIAS' $tituloNoticias.ForeColor = $COR_OURO $tituloNoticias.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Bold) $tituloNoticias.Location = New-Object System.Drawing.Point(34, 166) $tituloNoticias.Size = New-Object System.Drawing.Size(200, 16) $f.Controls.Add($tituloNoticias) $noticias = New-Object System.Windows.Forms.TextBox $noticias.Multiline = $true $noticias.ReadOnly = $true $noticias.ScrollBars = 'Vertical' $noticias.BorderStyle = 'None' $noticias.BackColor = $COR_PAINEL $noticias.ForeColor = $COR_TEXTO $noticias.Font = New-Object System.Drawing.Font('Segoe UI', 9) $noticias.Location = New-Object System.Drawing.Point(34, 186) $noticias.Size = New-Object System.Drawing.Size(532, 96) $noticias.Text = 'Carregando...' # sem isto a caixa recebe o foco ao abrir e o texto aparece selecionado em azul, # como se o jogador tivesse clicado nele $noticias.TabStop = $false $f.Controls.Add($noticias) # --- progresso e estado ----------------------------------------------------- $barra = New-Object System.Windows.Forms.ProgressBar $barra.Location = New-Object System.Drawing.Point(34, 300) $barra.Size = New-Object System.Drawing.Size(532, 10) $barra.Style = 'Continuous' $f.Controls.Add($barra) $estado = New-Object System.Windows.Forms.Label $estado.Text = 'Verificando atualizações...' $estado.ForeColor = $COR_FRACA $estado.Font = New-Object System.Drawing.Font('Segoe UI', 9) $estado.Location = New-Object System.Drawing.Point(34, 320) $estado.Size = New-Object System.Drawing.Size(360, 34) $f.Controls.Add($estado) $btn = New-Object System.Windows.Forms.Button $btn.Text = 'JOGAR' $btn.Location = New-Object System.Drawing.Point(418, 330) $btn.Size = New-Object System.Drawing.Size(148, 46) $btn.FlatStyle = 'Flat' $btn.FlatAppearance.BorderSize = 0 $btn.BackColor = $COR_BOTAO $btn.ForeColor = [System.Drawing.Color]::White $btn.Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold) $btn.Enabled = $false $btn.Cursor = 'Hand' $btn.Add_MouseEnter({ if ($this.Enabled) { $this.BackColor = $COR_BOTAO_ON } }) $btn.Add_MouseLeave({ $this.BackColor = $COR_BOTAO }) $f.Controls.Add($btn) # --- detalhes (o log, escondido) -------------------------------------------- # Ele continua existindo porque foi o log que revelou o 308 do Caddy. So deixou # de ser a primeira coisa que o jogador ve. $linkDetalhes = New-Object System.Windows.Forms.Label $linkDetalhes.Text = 'detalhes ▾' $linkDetalhes.ForeColor = $COR_FRACA $linkDetalhes.Font = New-Object System.Drawing.Font('Segoe UI', 8) $linkDetalhes.Location = New-Object System.Drawing.Point(34, 384) $linkDetalhes.Size = New-Object System.Drawing.Size(90, 18) $linkDetalhes.Cursor = 'Hand' $linkDetalhes.Add_MouseEnter({ $this.ForeColor = $COR_OURO }) $linkDetalhes.Add_MouseLeave({ $this.ForeColor = $COR_FRACA }) $f.Controls.Add($linkDetalhes) $log = New-Object System.Windows.Forms.TextBox $log.Multiline = $true $log.ReadOnly = $true $log.ScrollBars = 'Vertical' $log.BorderStyle = 'None' $log.BackColor = [System.Drawing.Color]::FromArgb(14, 16, 21) $log.ForeColor = $COR_FRACA $log.Font = New-Object System.Drawing.Font('Consolas', 8) $log.Location = New-Object System.Drawing.Point(34, 408) $log.Size = New-Object System.Drawing.Size(532, 150) $log.Visible = $false $log.TabStop = $false $f.Controls.Add($log) $linkDetalhes.Add_Click({ if ($log.Visible) { $log.Visible = $false $linkDetalhes.Text = 'detalhes ▾' $f.ClientSize = New-Object System.Drawing.Size(600, $ALTURA_FECHADA) } else { $log.Visible = $true $linkDetalhes.Text = 'detalhes ▴' $f.ClientSize = New-Object System.Drawing.Size(600, $ALTURA_ABERTA) } }) function Escreve($t) { $log.AppendText($t + "`r`n") [System.Windows.Forms.Application]::DoEvents() } # ------------------------------------------------------------ o jogo function Acha-Jogo { $cmd = Join-Path $PastaJogo 'JOGAR.cmd' if (Test-Path -LiteralPath $cmd) { return $cmd } $exe = Get-ChildItem -LiteralPath $PastaJogo -Filter '*.exe' -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch 'Warpgate|opensetup|patcher' } | Select-Object -First 1 if ($exe) { return $exe.FullName } return $null } function Jogo-Aberto { $nomes = Get-ChildItem -LiteralPath $PastaJogo -Filter '*.exe' -ErrorAction SilentlyContinue | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_.Name) } foreach ($n in $nomes) { if (Get-Process -Name $n -ErrorAction SilentlyContinue) { return $true } } return $false } function Entra-No-Jogo { $alvo = Acha-Jogo if (-not $alvo) { [System.Windows.Forms.MessageBox]::Show( "Não achei o jogo em:`r`n$PastaJogo`r`n`r`nRode o COMECE-AQUI.cmd.", 'PWRO') | Out-Null return $false } Start-Process -FilePath $alvo -WorkingDirectory $PastaJogo $f.Close() return $true } $btn.Add_Click({ [void](Entra-No-Jogo) }) # ------------------------------------------------------------ telas de abertura # O aviso coreano de faixa etaria e o fundo do login viram a arte do PWRO. O # desenho acontece aqui, na maquina do jogador: a arte viaja como um jpg de # 670 KB e os BMPs prontos dariam 10,6 MB (ver Telas-PWRO.ps1). E idempotente, # entao chamar toda vez custa um SHA-256 e mais nada. function Aplica-Telas { try { $s = Join-Path $PSScriptRoot 'Telas-PWRO.ps1' if (-not (Test-Path -LiteralPath $s)) { $s = Join-Path $PastaJogo 'Telas-PWRO.ps1' } if (Test-Path -LiteralPath $s) { & $s -Cliente $PastaJogo *> $null } } catch { # tela bonita nao vale travar a janela que abre o jogo } } # ------------------------------------------------------------ atualizacao $f.Add_Shown({ $f.Activate() $f.ActiveControl = $btn Escreve "Jogo em: $PastaJogo" # notícias do servidor: se não vier, some do caminho e não atrapalha try { # decodificado como UTF-8 na mao: se dependessemos do charset do cabecalho, # um servidor que nao o mandasse transformaria "atualizacao" em "atualização" # bem no painel que o jogador le primeiro # -UseBasicParsing NAO e opcional: sem ele o PowerShell 5.1 manda o corpo # para o motor do Internet Explorer, e numa maquina onde o IE nunca passou # pela configuracao inicial essa chamada TRAVA -- nao da erro, nao estoura o # -TimeoutSec, fica pendurada segurando a thread da janela para sempre. Foi # exatamente o que aconteceu: janela desenhada, "Carregando..." eterno. # ToArray() e nao CopyTo(): o RawContentStream ja vem com a posicao no fim, # e copiar dali renderia zero byte. $cru = (Invoke-WebRequest -Uri "$UrlBase/noticias.txt" -UseBasicParsing -TimeoutSec 8).RawContentStream $txt = [System.Text.Encoding]::UTF8.GetString($cru.ToArray()) $txt = $txt.TrimStart([char]0xFEFF) $noticias.Text = ($txt -replace "`r`n", "`n").Trim() -replace "`n", "`r`n" } catch { $noticias.Text = 'Sem notícias do servidor no momento.' } if (-not (Test-Path -LiteralPath $PastaJogo)) { Escreve 'Pasta do jogo não encontrada.' $estado.Text = 'Instalação não encontrada. Rode o INSTALAR-2025.cmd.' return } $manifest = $null try { Escreve "Consultando $UrlBase ..." $manifest = Invoke-RestMethod -Uri "$UrlBase/manifest.json" -TimeoutSec 20 } catch { # Sem o motivo não dá para distinguir servidor desligado de endereço # errado ou firewall. Escreve 'Não consegui falar com o servidor de atualizações.' Escreve (" motivo: " + $_.Exception.Message) $estado.Text = 'Sem conexão com o servidor. Você pode jogar assim mesmo.' $btn.Enabled = $true return } Escreve ("Versão publicada: " + $manifest.versao) $baixar = @() foreach ($a in $manifest.arquivos) { if ($a.caminho -eq 'manifest.json') { continue } $local = Join-Path $PastaJogo ($a.caminho -replace '/', '\') if (-not (Test-Path -LiteralPath $local)) { $baixar += $a; continue } $h = (Get-FileHash -LiteralPath $local -Algorithm SHA256).Hash.ToLowerInvariant() if ($h -ne $a.sha256) { $baixar += $a } } if ($baixar.Count -eq 0) { Escreve 'Tudo em dia.' $barra.Value = 100 $estado.Text = 'Tudo em dia. Bom jogo!' $btn.Enabled = $true return } $mb = [math]::Round((($baixar | Measure-Object -Property bytes -Sum).Sum) / 1MB, 2) Escreve "$($baixar.Count) arquivo(s) para atualizar - $mb MB" if (Jogo-Aberto) { Escreve 'O jogo está aberto.' $estado.Text = 'Feche o jogo e abra esta janela de novo para atualizar.' return } $temp = Join-Path $env:TEMP ('pwro-patch-' + [Guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $temp -Force | Out-Null $i = 0 # Enquanto só baixamos, nada no jogo foi tocado e falhar é inofensivo. A # partir do primeiro Copy-Item isso deixa de ser verdade. $aplicando = $false try { foreach ($a in $baixar) { $i++ $estado.Text = "Baixando $i de $($baixar.Count) - $mb MB no total" Escreve (" " + $a.caminho) $destino = Join-Path $temp ($a.caminho -replace '/', '\') New-Item -ItemType Directory -Path (Split-Path -Parent $destino) -Force | Out-Null # -UseBasicParsing aqui pelo mesmo motivo do noticias.txt. Este laco # nunca tinha rodado de verdade nesta maquina (ou o jogo estava aberto, ou # estava tudo em dia), entao a travada estava esperando o primeiro amigo # que fosse realmente baixar uma atualizacao. Invoke-WebRequest -Uri ("$UrlBase/" + $a.caminho) -OutFile $destino -UseBasicParsing -TimeoutSec 600 $h = (Get-FileHash -LiteralPath $destino -Algorithm SHA256).Hash.ToLowerInvariant() if ($h -ne $a.sha256) { throw "$($a.caminho) veio corrompido" } $barra.Value = [int](100 * $i / $baixar.Count) [System.Windows.Forms.Application]::DoEvents() } $estado.Text = 'Aplicando...' $aplicando = $true foreach ($a in $baixar) { $origem = Join-Path $temp ($a.caminho -replace '/', '\') $destino = Join-Path $PastaJogo ($a.caminho -replace '/', '\') New-Item -ItemType Directory -Path (Split-Path -Parent $destino) -Force | Out-Null Copy-Item -LiteralPath $origem -Destination $destino -Force } Aplica-Telas Escreve 'Atualizado.' $estado.Text = 'Atualizado. Bom jogo!' $btn.Enabled = $true } catch { Escreve "FALHOU: $($_.Exception.Message)" if ($aplicando) { Escreve 'A cópia já tinha começado: alguns arquivos foram trocados e outros não.' $estado.Text = 'Atualização incompleta. Abra esta janela de novo para terminar.' } else { Escreve 'Nada foi alterado -- a falha foi durante o download.' $estado.Text = 'Falha ao baixar. Nada foi alterado, você pode jogar.' $btn.Enabled = $true } } finally { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue } }) Aplica-Telas [void]$f.ShowDialog()