# Unitx SkyMP installer — instala el cliente SkyMP en una carpeta de Skyrim SE # y lo apunta al server unitx (195.26.241.62:7777). # # Requisitos: # - Skyrim Special Edition instalado vía Steam # - SKSE64 instalado en la carpeta de Skyrim # - El zip 'dist' del CI de skymp ya descargado en disco # # Uso: # .\Install-UnitxSkyMP.ps1 # # Para sobreescribir parámetros del server: # .\Install-UnitxSkyMP.ps1 -ServerIp 'otra-ip' -ServerPort 7778 [CmdletBinding()] param( [string] $DistZipPath, [string] $SkyrimPath, [string] $ServerIp = '195.26.241.62', # 25565 (puerto Minecraft) en lugar del default skymp 7777 — ISPs LATAM # frecuentemente filtran UDP 7777. Comprobado: UDP 7777 daba 80% loss, 25565 0%. [int] $ServerPort = 25565, # Master server propio (Fase 3): el panel skymp-admin implementa los endpoints # de auth/sessions; el cliente abre Discord OAuth in-game, valida guild y nos # devuelve session → server consume session. [string] $ServerMasterKey = 'unitx-skymp', [string] $MasterUrl = 'https://skymp.unitx-wow.com.mx', [switch] $LaunchAfterInstall ) $ErrorActionPreference = 'Stop' # Log permanente para diagnóstico — siempre se escribe, ganas o pierdas. $logDir = Join-Path $env:LOCALAPPDATA 'UnitxSkyMP\logs' if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $script:LogPath = Join-Path $logDir ("install-{0}.log" -f (Get-Date -Format 'yyyyMMdd-HHmmss')) Start-Transcript -Path $script:LogPath -Append | Out-Null function Write-Section($text) { Write-Host '' Write-Host ('━' * 60) -ForegroundColor DarkGray Write-Host $text -ForegroundColor Cyan Write-Host ('━' * 60) -ForegroundColor DarkGray } function Resolve-SkyrimSEPath { if ($SkyrimPath -and (Test-Path $SkyrimPath)) { return (Resolve-Path $SkyrimPath).Path } # Steam install path detection $steamRoot = $null foreach ($key in @( 'HKLM:\SOFTWARE\WOW6432Node\Valve\Steam', 'HKLM:\SOFTWARE\Valve\Steam', 'HKCU:\SOFTWARE\Valve\Steam' )) { try { $val = (Get-ItemProperty -Path $key -ErrorAction Stop).InstallPath if ($val) { $steamRoot = $val; break } } catch { } } if (-not $steamRoot) { Write-Warning 'No se detectó Steam en el registro.' return Read-Host 'Pega la ruta completa a tu carpeta "Skyrim Special Edition"' } # Read libraryfolders.vdf to find all Steam libraries $libraries = @($steamRoot) $libVdf = Join-Path $steamRoot 'steamapps\libraryfolders.vdf' if (Test-Path $libVdf) { $content = Get-Content $libVdf -Raw foreach ($m in [regex]::Matches($content, '"path"\s+"([^"]+)"')) { $libraries += $m.Groups[1].Value -replace '\\\\', '\' } } foreach ($lib in $libraries | Select-Object -Unique) { $candidate = Join-Path $lib 'steamapps\common\Skyrim Special Edition' if (Test-Path $candidate) { return (Resolve-Path $candidate).Path } } Write-Warning 'No se encontró Skyrim SE en ninguna biblioteca de Steam.' return Read-Host 'Pega la ruta completa a tu carpeta "Skyrim Special Edition"' } function Test-SKSE($skyrimDir) { $loader = Join-Path $skyrimDir 'skse64_loader.exe' if (Test-Path $loader) { return $true } Write-Warning "skse64_loader.exe NO se encontró en $skyrimDir" Write-Host ' Descarga SKSE Special Edition desde: https://skse.silverlock.org/' -ForegroundColor Yellow Write-Host ' Copia los archivos del zip dentro de tu carpeta "Skyrim Special Edition" y vuelve a correr este script.' -ForegroundColor Yellow return $false } function Resolve-DistZip { if ($DistZipPath -and (Test-Path $DistZipPath)) { return (Resolve-Path $DistZipPath).Path } Write-Host '' Write-Host 'Necesito el zip "dist" que bajaste de GitHub Actions.' -ForegroundColor Yellow Write-Host ' Si no lo tienes: ve a un run reciente de "PR Windows Flatrim (AE/SE)" en' -ForegroundColor Gray Write-Host ' https://github.com/skyrim-multiplayer/skymp/actions/workflows/pr-windows-flatrim.yml' -ForegroundColor Gray Write-Host ' y descarga el artifact "dist" (~186 MB).' -ForegroundColor Gray Write-Host '' $p = Read-Host 'Pega la ruta completa al dist.zip' if (-not (Test-Path $p)) { throw "No existe: $p" } return (Resolve-Path $p).Path } function Expand-DistInto($zipPath, $skyrimDir) { $temp = Join-Path $env:TEMP "unitx-skymp-$([guid]::NewGuid())" New-Item -ItemType Directory -Path $temp | Out-Null try { Write-Host " Extrayendo a $temp ..." -ForegroundColor DarkGray Expand-Archive -Path $zipPath -DestinationPath $temp -Force # The CI 'dist' artifact may use either layout: # /Data/... (already merge-ready) # /client/Data/... (older layout) # Detect and pick the right base. $dataRoot = $null if (Test-Path (Join-Path $temp 'Data')) { $dataRoot = $temp } elseif (Test-Path (Join-Path $temp 'client\Data')) { $dataRoot = Join-Path $temp 'client' } else { $found = Get-ChildItem -Path $temp -Recurse -Directory -Filter 'Data' -ErrorAction SilentlyContinue | Where-Object { Test-Path (Join-Path $_.FullName 'Platform') } | Select-Object -First 1 if ($found) { $dataRoot = Split-Path -Parent $found.FullName } } if (-not $dataRoot) { throw "No encontré 'Data/Platform' dentro del zip. ¿Es realmente el artifact 'dist'?" } Write-Host " Copiando archivos del cliente a $skyrimDir ..." -ForegroundColor DarkGray Copy-Item -Path (Join-Path $dataRoot '*') -Destination $skyrimDir -Recurse -Force } finally { Remove-Item -Path $temp -Recurse -Force -ErrorAction SilentlyContinue } } function Write-ClientSettings($skyrimDir) { $settingsPath = Join-Path $skyrimDir 'Data\Platform\Plugins\skymp5-client-settings.txt' $dir = Split-Path -Parent $settingsPath if (-not (Test-Path $dir)) { throw "Directorio de plugins no existe: $dir. ¿Se extrajo correctamente el dist?" } # Public key Ed25519 del server. El engine firma cada JS de gamemode con # `// skymp:sig:y:CPPunitx:` (ver server-settings.json:serverKey). El # cliente requiere `server-public-keys` (o un fetch exitoso de serverinfo) # para poder verificar; si el map de public keys queda vacío, el cliente # rechaza TODO el JS pusheado por el server y no aparece chat ni nada. $serverPublicKeyPem = "-----BEGIN PUBLIC KEY-----`nMCowBQYDK2VwAyEAyhU5i/hjbUJPOF+HVaGx8/OI8w8XWJzNz2xUPWilDe8=`n-----END PUBLIC KEY-----`n" $settings = [ordered]@{ 'server-ip' = $ServerIp 'server-port' = $ServerPort # Master server propio (no gateway.skymp.net). El cliente fetcha # serverinfo (publicKeys) y dispara el flujo Discord OAuth contra acá. 'server-master-key' = $ServerMasterKey 'master' = $MasterUrl # server-info-ignore=false → cliente fetcha /api/servers//serverinfo # del master para obtener publicKeys. Lo dejamos resilente con keys # también en server-public-keys por si el master está caído. 'server-info-ignore' = $false 'server-public-keys' = @{ 'CPPunitx' = $serverPublicKeyPem } # AE 1.6.x trae muchos CC content + ResourcePack que el server vanilla no # incluye. Sin esto, el cliente cae a single-player con LOAD ORDER ERROR. 'ignoreLoadOrderMismatch' = $true } $json = $settings | ConvertTo-Json -Depth 4 # WriteAllText with UTF8Encoding(false) -> NO BOM. Set-Content -Encoding UTF8 in # PS 5.x writes BOM, which breaks the skymp client's JSON.parse on this file. [System.IO.File]::WriteAllText($settingsPath, $json, [System.Text.UTF8Encoding]::new($false)) Write-Host " Config escrita en: $settingsPath (UTF-8 sin BOM)" -ForegroundColor DarkGray # Verificacion explicita - si esto falla algo se rompio en serio $verify = Get-Content $settingsPath -Raw | ConvertFrom-Json $ok = $true if ($verify.'server-port' -ne $ServerPort) { Write-Host " *** ERROR: server-port no escrito correctamente ***" -ForegroundColor Red $ok = $false } if (-not $verify.master) { Write-Host " *** ERROR: master URL vacio (auth Discord no funciona) ***" -ForegroundColor Red $ok = $false } if (-not $verify.'server-public-keys'.CPPunitx) { Write-Host " *** ERROR: server-public-keys.CPPunitx falta ***" -ForegroundColor Red $ok = $false } if ($ok) { Write-Host " Verificacion OK: server-port = $($verify.'server-port'), master = $($verify.master)" -ForegroundColor Green } else { Write-Host '' Write-Host ' Contenido final del archivo:' -ForegroundColor Yellow Get-Content $settingsPath | ForEach-Object { Write-Host " $_" } } # SP intenta monitorear Data/Platform/PluginsDev. Si no existe, escupe error en # skyrim-platform.log (no fatal, pero mejor evitarlo). $devDir = Join-Path $skyrimDir 'Data\Platform\PluginsDev' if (-not (Test-Path $devDir)) { New-Item -ItemType Directory -Path $devDir -Force | Out-Null } } # ---------- Main ---------- Write-Section 'Unitx SkyMP — Instalador' Write-Host "Server: $ServerIp`:$ServerPort master-key: $ServerMasterKey" Write-Section '1. Detectando Skyrim Special Edition' $skyrim = Resolve-SkyrimSEPath if (-not $skyrim) { throw 'No se pudo determinar la carpeta de Skyrim SE.' } Write-Host " $skyrim" -ForegroundColor Green Write-Section '2. Verificando SKSE64' if (-not (Test-SKSE $skyrim)) { exit 1 } Write-Host ' SKSE64 OK' -ForegroundColor Green Write-Section '3. Localizando dist.zip del cliente' $dist = Resolve-DistZip Write-Host " $dist" -ForegroundColor Green Write-Section '4. Instalando cliente' Expand-DistInto -zipPath $dist -skyrimDir $skyrim Write-Section '5. Escribiendo configuración' Write-ClientSettings -skyrimDir $skyrim # Guardar el path detectado para que Play-UnitxSkyMP.ps1 no tenga que re-detectar. $stateDir = Join-Path $env:LOCALAPPDATA 'UnitxSkyMP' if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir | Out-Null } Set-Content -Path (Join-Path $stateDir 'skyrim-path.txt') -Value $skyrim -Encoding UTF8 Write-Section 'Instalación completa' Write-Host "Para jugar:" -ForegroundColor Green Write-Host " 1. Abre $skyrim" -ForegroundColor White Write-Host ' 2. Ejecuta skse64_loader.exe (NO el SkyrimSE.exe normal)' -ForegroundColor White Write-Host '' if ($LaunchAfterInstall) { $loader = Join-Path $skyrim 'skse64_loader.exe' Write-Host "Lanzando $loader ..." -ForegroundColor Cyan Start-Process -FilePath $loader -WorkingDirectory $skyrim } Stop-Transcript | Out-Null Write-Host '' Write-Host "Log guardado en: $script:LogPath" -ForegroundColor Cyan