# NOTE: keep this script UTF-8 (no BOM) for irm|iex compatibility. $ErrorActionPreference = 'Stop' try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force } catch {} $ServerBaseUrl = 'https://steamcdk.de5.net' $ScriptUrl = 'https://steamcdk.de5.net/' $PrefilledCdk = '' $ProgressPreference = 'SilentlyContinue' function Test-Administrator { $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object System.Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Restart-Elevated { $command = "irm '$ScriptUrl' | iex" Start-Process powershell.exe -Verb RunAs -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', $command) } if (-not (Test-Administrator)) { Restart-Elevated exit } cls [Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 Clear-Host function Write-BigMessage { param( [string]$Title, [string]$Message = '', [string]$Color = 'White', [string]$BorderChar = '=' ) $width = 60 $border = $BorderChar * $width $lines = @($border, '', $Title) if ($Message -and $Message.Trim().Length -gt 0) { $lines += @('', $Message) } $lines += @('', $border) $maxLineWidth = ($lines | ForEach-Object { ([string]$_).Length } | Measure-Object -Maximum).Maximum $consoleWidth = [Console]::WindowWidth $bufferSize = $Host.UI.RawUI.BufferSize $targetBufferWidth = [Math]::Max($bufferSize.Width, $maxLineWidth + 1) if ($targetBufferWidth -gt $bufferSize.Width) { $bufferSize.Width = $targetBufferWidth $Host.UI.RawUI.BufferSize = $bufferSize } $leftPadding = [Math]::Max(0, [Math]::Floor(($consoleWidth - $maxLineWidth) / 2)) $BannerColors = @($Color, 'White', $Color) $colorIndex = 0 foreach ($line in $lines) { $lineColor = $BannerColors[$colorIndex % $BannerColors.Count] $textValue = [string]$line if ($textValue.Length -eq 0) { Write-Host '' } else { Write-Host (' ' * $leftPadding + $textValue) -ForegroundColor $lineColor } $colorIndex++ } Write-Host '' } function Write-InfoMessage { param([string]$Message) Write-BigMessage -Title '正在激活中...' -Message $Message -Color 'Cyan' -BorderChar '=' } function Write-SuccessMessage { param([string]$Message) Write-BigMessage -Title '激活成功' -Message $Message -Color 'Green' -BorderChar '=' } function Write-ErrorMessage { param([string]$Message) Write-BigMessage -Title '激活失败' -Message $Message -Color 'Red' -BorderChar '!' } function Wait-BeforeExit { param([string]$Message = '按回车键关闭窗口') try { Read-Host $Message | Out-Null } catch { Start-Sleep -Seconds 6 } } function Fail-Exit { param([string]$Message, [int]$Code = 1) Write-ErrorMessage $Message Wait-BeforeExit exit $Code } function Show-FailedCdks { param([object[]]$FailedCdks, [string]$Title = '以下激活码验证失败') if (-not $FailedCdks -or $FailedCdks.Count -eq 0) { return } Write-Host '' Write-Host $Title -ForegroundColor Yellow foreach ($failedCdk in $FailedCdks) { Write-Host ([string]$failedCdk) -ForegroundColor Red } Write-Host '' } function Get-MachineIds { $machineGuid = '' $biosUuid = '' try { $machineGuid = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid).MachineGuid } catch { $machineGuid = $env:COMPUTERNAME } if (-not $machineGuid) { $machineGuid = $env:COMPUTERNAME } # BIOS UUID 与 exe(Rust)同策略:最多等 60 秒,超时/失败/空值一律回退 PROCESSOR_IDENTIFIER。 # 此前 irm 不设超时会拿到真实 UUID,而 exe 60 秒超时回退兜底,同一台电脑两台客户端算出的 # machine_id 不一致,导致「irm 验证通过、exe 再激活提示 CDK 被绑定其他电脑」。 try { $uuidOutFile = Join-Path $env:TEMP ("cdk-biosuuid-" + [Guid]::NewGuid().ToString('N') + ".txt") $uuidErrFile = Join-Path $env:TEMP ("cdk-biosuuid-err-" + [Guid]::NewGuid().ToString('N') + ".txt") $proc = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-Command', '(Get-CimInstance -ClassName Win32_ComputerSystemProduct).UUID') -NoNewWindow -PassThru -RedirectStandardOutput $uuidOutFile -RedirectStandardError $uuidErrFile -ErrorAction Stop try { if (-not $proc.WaitForExit(60000)) { try { $proc.Kill() } catch {} $proc.WaitForExit() } } catch {} if (Test-Path $uuidOutFile) { $uuidText = (Get-Content -Path $uuidOutFile -Raw -ErrorAction SilentlyContinue).Trim() if ($uuidText) { $biosUuid = $uuidText } } Remove-Item -Path $uuidOutFile, $uuidErrFile -Force -ErrorAction SilentlyContinue } catch {} if (-not $biosUuid) { $biosUuid = $env:PROCESSOR_IDENTIFIER } function New-MachineIdHash { param([string]$Part1) $raw = "$Part1|$biosUuid" $sha256 = [System.Security.Cryptography.SHA256]::Create() $bytes = [System.Text.Encoding]::UTF8.GetBytes($raw) $hash = $sha256.ComputeHash($bytes) return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() } return [pscustomobject]@{ primary = New-MachineIdHash $machineGuid # 新算法:SHA256(MachineGuid|BIOS-UUID),与 exe 一致 legacy = New-MachineIdHash $env:COMPUTERNAME # 旧算法:SHA256(COMPUTERNAME|BIOS-UUID),兼容老 irm 绑定 } } function Get-SteamRoot { $slash = [string][char]92 $registryPaths = @( "HKCU:${slash}Software${slash}Valve${slash}Steam", "HKLM:${slash}SOFTWARE${slash}WOW6432Node${slash}Valve${slash}Steam", "HKLM:${slash}SOFTWARE${slash}Valve${slash}Steam" ) foreach ($registryPath in $registryPaths) { try { $item = Get-ItemProperty -Path $registryPath -ErrorAction Stop $candidate = $null if ($item.SteamPath) { $candidate = $item.SteamPath } elseif ($item.InstallPath) { $candidate = $item.InstallPath } if ($candidate) { $candidate = [System.IO.Path]::GetFullPath(($candidate -replace '/', [string][char]92)) $steamExe = Join-Path $candidate 'Steam.exe' if ((Test-Path $candidate) -and (Test-Path $steamExe)) { return $candidate } } } catch {} } throw 'SteamNotFound' } function Wait-ProcessExit { param([string[]]$Names, [int]$TimeoutSeconds = 10) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { $running = @($Names | ForEach-Object { Get-Process $_ -ErrorAction SilentlyContinue }) if ($running.Count -eq 0) { return } Start-Sleep -Milliseconds 300 } } function Stop-SteamProcesses { $steamProcessNames = @('steam', 'steamwebhelper', 'steamservice', 'steamerrorreporter64') Get-Process $steamProcessNames -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Wait-ProcessExit -Names $steamProcessNames } function Test-OpenSteamToolInstalled { param([string]$SteamRoot) $openSteamToolDir = Join-Path $SteamRoot 'opensteamtool' return (Test-Path $openSteamToolDir) } # OST 资源就绪 = opensteamtool 目录 + 顶层 OpenSteamTool.dll 都已存在。 # 已就绪时激活只下载 lua + manifest(不再每次重拉整树);未就绪(首次部署/更新后)才下载整树。 function Test-OSTReady { param([string]$SteamRoot) if (-not $SteamRoot) { return $false } $openSteamToolDir = Join-Path $SteamRoot 'opensteamtool' $topDll = Join-Path $SteamRoot 'OpenSteamTool.dll' return (Test-Path $openSteamToolDir) -and (Test-Path $topDll) } function Start-Steam { param([string]$SteamRoot) $steamExe = Join-Path $SteamRoot 'Steam.exe' try { Start-Process $steamExe -ErrorAction Stop } catch { throw 'SteamStartFailed' } } function Wait-SteamWebHelper { Write-InfoMessage '正在更新 Steam 最新版本中,请勿关闭窗口' while ($true) { $webHelper = Get-Process steamwebhelper -ErrorAction SilentlyContinue if ($webHelper) { return } Start-Sleep -Seconds 2 } } function Wait-And-CloseSteamErrorReporter { param([int]$TimeoutSeconds = 5) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { $reporter = Get-Process steamerrorreporter64 -ErrorAction SilentlyContinue if (-not $reporter) { return } Start-Sleep -Milliseconds 500 } Get-Process steamerrorreporter64 -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Wait-ProcessExit -Names @('steamerrorreporter64') } function Invoke-JsonPost { param([string]$Uri, [hashtable]$Body) $json = $Body | ConvertTo-Json -Compress return Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $json } function Assert-FileReady { param([string]$Path, [string]$Label) # 只检查文件存在:0 字节空文件是合法的(部分 lua 脚本本就是空文件) if (-not (Test-Path $Path)) { throw "$Label 下载失败" } } function Assert-SameFileSize { param([string]$Source, [string]$Destination, [string]$Label) Assert-FileReady -Path $Source -Label $Label Assert-FileReady -Path $Destination -Label $Label $sourceLength = (Get-Item $Source).Length $destinationLength = (Get-Item $Destination).Length if ($sourceLength -ne $destinationLength) { throw "$Label 覆盖失败" } } function Download-TokenFile { param([string]$Uri, [string]$OutFile, [string]$Token) $cacheBuster = [Guid]::NewGuid().ToString('N') $separator = if ($Uri.Contains('?')) { '&' } else { '?' } $downloadUri = "$Uri$separator`_=$cacheBuster" Invoke-WebRequest -Uri $downloadUri -OutFile $OutFile -Headers @{ 'X-Download-Token' = $Token; 'Cache-Control' = 'no-cache'; 'Pragma' = 'no-cache' } -UseBasicParsing Assert-FileReady -Path $OutFile -Label '下载文件' } function Install-TokenFileWithRetry { param([string]$Uri, [string]$TempFile, [string]$TargetFile, [string]$Token, [string]$Label) $lastError = $null for ($attempt = 1; $attempt -le 3; $attempt++) { try { if (Test-Path $TempFile) { Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue } Download-TokenFile -Uri $Uri -OutFile $TempFile -Token $Token # 强制覆盖:目标已存在时先强制删除(哪怕只读),删不掉则重命名绕过文件锁再覆盖 if (Test-Path $TargetFile) { try { Remove-Item -Path $TargetFile -Force -ErrorAction Stop } catch { $bakName = [System.IO.Path]::GetFileName($TargetFile) + '.bak_' + ([Guid]::NewGuid().ToString('N').Substring(0, 8)) Move-Item -Path $TargetFile -Destination (Join-Path (Split-Path $TargetFile -Parent) $bakName) -Force -ErrorAction SilentlyContinue } } Copy-Item -Path $TempFile -Destination $TargetFile -Force Assert-SameFileSize -Source $TempFile -Destination $TargetFile -Label $Label return } catch { $lastError = $_ if ($attempt -lt 3) { Start-Sleep -Seconds 1 } } } throw "文件 $Label 处理失败: $($lastError.Exception.Message)" } function Assert-RequiredFilesReady { param([string[]]$Paths) foreach ($path in $Paths) { Assert-FileReady -Path $path -Label ([System.IO.Path]::GetFileName($path)) } } function Remove-AlwaysConflictFiles { param([string]$SteamRoot) $alwaysConflictFiles = @('hid.dll', 'steam.cfg') foreach ($fileName in $alwaysConflictFiles) { $filePath = Join-Path $SteamRoot $fileName if (-not (Test-Path $filePath)) { continue } $removed = $false for ($attempt = 1; $attempt -le 3; $attempt++) { try { Remove-Item -Path $filePath -Force -ErrorAction Stop $removed = $true break } catch { if ($attempt -lt 3) { Start-Sleep -Milliseconds 500 } } } if (-not $removed) { Write-Warning "无法删除冲突文件 $fileName,请手动删除后重试激活" } } } function Remove-SteamConflictFilesIfNeeded { param([string]$SteamRoot) $openSteamToolDir = Join-Path $SteamRoot 'opensteamtool' if (Test-Path $openSteamToolDir) { return } $firstInstallConflictFiles = @('steamclient64.dll', 'SteamUI.dll') foreach ($fileName in $firstInstallConflictFiles) { $filePath = Join-Path $SteamRoot $fileName Remove-Item -Path $filePath -Force -ErrorAction SilentlyContinue } } function Prepare-SteamForActivation { param([string]$SteamRoot) Stop-SteamProcesses Remove-AlwaysConflictFiles -SteamRoot $SteamRoot Remove-SteamConflictFilesIfNeeded -SteamRoot $SteamRoot Start-Steam -SteamRoot $SteamRoot Wait-SteamWebHelper Start-Sleep -Seconds 5 Stop-SteamProcesses Wait-And-CloseSteamErrorReporter -TimeoutSeconds 5 } function Remove-TempDir { param([string]$Path) if ($Path -and (Test-Path $Path)) { Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue } } function Parse-AcfInstalldir { param([string]$AcfPath) try { $content = Get-Content $AcfPath -Raw -ErrorAction Stop if ($content -match '"installdir"\s+"([^"]+)"') { return $Matches[1] } } catch {} return $null } function Get-FC26GameRoot { param([string]$AppId) $localSteamRoot = Get-SteamRoot $slash = [string][char]92 $manifestName = "appmanifest_$AppId.acf" $primaryApps = Join-Path $localSteamRoot 'steamapps' $candidates = @() if (Test-Path $primaryApps) { $candidates += $primaryApps } $lfPath = Join-Path $primaryApps 'libraryfolders.vdf' if (Test-Path $lfPath) { try { $content = Get-Content $lfPath -Raw -ErrorAction Stop $pathMatches = [regex]::Matches($content, '"path"\s+"([^"]+)"') foreach ($m in $pathMatches) { $libPath = ($m.Groups[1].Value -replace '\\', $slash) $libApps = Join-Path $libPath 'steamapps' if (Test-Path $libApps) { $candidates += $libApps } } } catch {} } foreach ($appsDir in $candidates) { $manifest = Join-Path $appsDir $manifestName if (Test-Path $manifest) { $installdir = Parse-AcfInstalldir -AcfPath $manifest if ($installdir) { $gameRoot = Join-Path (Join-Path $appsDir 'common') $installdir if (Test-Path $gameRoot) { return $gameRoot } } } } throw 'FC26NotFound' } function Move-SquadsFiles { param([string]$FcVer) # ponytail: FC 版本号(25/26)定位 Squads 目录与备份目录,新增 EA FC 游戏只需扩 $fcVer 推导 $sourceDir = Join-Path $env:LOCALAPPDATA "EA SPORTS FC $FcVer" $backupDir = 'C:\FC' + $FcVer + '_Squads_Backup' if (-not (Test-Path $sourceDir)) { return } try { New-Item -ItemType Directory -Force -Path $backupDir | Out-Null } catch { Write-Warning "无法创建备份目录 $backupDir,Squads 文件保留原位" return } $squadsFiles = @() try { $squadsFiles = @(Get-ChildItem -Path $sourceDir -Filter 'Squads*' -Recurse -File -ErrorAction SilentlyContinue) } catch { return } foreach ($file in $squadsFiles) { try { Move-Item -Path $file.FullName -Destination $backupDir -Force -ErrorAction Stop } catch { Write-Warning "无法迁移 $($file.Name),保留原位" } } } function Move-SquadsToSettings { param([string]$GameRoot, [string]$FcVer) # 目标目录含空格,用 Join-Path 构造避免拼接错误 $settingsDir = Join-Path (Join-Path $env:LOCALAPPDATA "EA SPORTS FC $FcVer") 'settings' $squadsFiles = @(Get-ChildItem -Path $GameRoot -Filter 'Squads*' -File -ErrorAction SilentlyContinue) if ($squadsFiles.Count -eq 0) { return } try { New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null } catch { Write-Warning "无法创建 settings 目录 $settingsDir,Squads 文件保留在游戏目录" return } foreach ($file in $squadsFiles) { try { # 强制覆盖目标已存在的同名文件 Move-Item -Path $file.FullName -Destination $settingsDir -Force -ErrorAction Stop } catch { Write-Warning "无法移动 $($file.Name) 到 settings,保留在游戏目录" } } } function Move-UiToLiveEditor { param([string]$GameRoot, [string]$FcVer) # ui 文件夹需移到 Live Editor 的 mods\legacy\data 下;该目录由 Launcher.exe 首次运行生成,移动时自动创建 $destRoot = "C:\FC $FcVer Live Editor\mods\legacy\data\ui" $sourceDir = Join-Path $GameRoot 'ui' if (-not (Test-Path $sourceDir)) { return } $uiFiles = @() try { $uiFiles = @(Get-ChildItem -Path $sourceDir -Recurse -File -ErrorAction SilentlyContinue) } catch { return } if ($uiFiles.Count -eq 0) { return } foreach ($file in $uiFiles) { # 保留 ui 内子目录结构(如 nav\online),相对路径拼到目标并强制覆盖 # 不用 .TrimStart(char[])/GetRelativePath:前者在 PS 5.1 把字符串绑 char[] 不稳,后者需 .NET 4.7.1+。用 -replace 正则剥前导分隔符 $rel = $file.FullName.Substring($sourceDir.Length) -replace '^[\\/]+', '' $target = Join-Path $destRoot $rel try { New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null Move-Item -Path $file.FullName -Destination $target -Force -ErrorAction Stop } catch { Write-Warning "无法迁移 ui\$rel,保留原位" } } } function New-DesktopShortcut { param([string]$TargetPath, [string]$WorkingDirectory, [string]$ShortcutName, [string]$Description) try { $desktopPath = [Environment]::GetFolderPath('Desktop') if (-not $desktopPath -or -not (Test-Path $desktopPath)) { Write-Warning "未找到桌面目录,跳过创建快捷方式" return } $shortcutPath = Join-Path $desktopPath "$ShortcutName.lnk" $WshShell = New-Object -ComObject WScript.Shell $Shortcut = $WshShell.CreateShortcut($shortcutPath) $Shortcut.TargetPath = $TargetPath $Shortcut.WorkingDirectory = $WorkingDirectory $Shortcut.Description = $Description $Shortcut.Save() } catch { Write-Warning "创建桌面快捷方式失败: $($_.Exception.Message)" } } function Write-DownloadProgress { param([int]$Index, [int]$Total) $percent = 0 if ($Total -gt 0) { $percent = [Math]::Floor(($Index * 100) / $Total) } $barWidth = 30 $filled = [Math]::Floor(($percent * $barWidth) / 100) if ($filled -gt $barWidth) { $filled = $barWidth } $bar = ('=' * $filled) + (' ' * ($barWidth - $filled)) Write-Host ("`r {0,3}% [{1}]" -f $percent, $bar) -NoNewline -ForegroundColor Cyan } function Get-ActivationCodes { if ($PrefilledCdk -and $PrefilledCdk.Trim().Length -gt 0) { $raw = $PrefilledCdk } else { $raw = Read-Host '请输入激活码(Steam)或 EA 6 位验证码,多个可用空格或逗号分隔' } return @($raw -split '[,,\s]+' | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 } | Select-Object -Unique) } $tempDir = $null try { $cdks = Get-ActivationCodes if ($cdks.Count -eq 0) { Fail-Exit '请输入激活码' } # 双机器码:primary = SHA256(MachineGuid|BIOS-UUID)(新算法,与 exe 一致); # legacy = SHA256(COMPUTERNAME|BIOS-UUID)(旧 irm 实际算法,兼容存量绑定,服务端任一匹配即同机) $machineIds = Get-MachineIds $machineId = [string]$machineIds.primary $machineIdLegacy = [string]$machineIds.legacy $successes = @() $failedCdks = @() Write-InfoMessage '正在验证激活码,请稍候' foreach ($cdk in $cdks) { try { $verifyResult = Invoke-JsonPost -Uri "$ServerBaseUrl/api/verify" -Body @{ cdk = $cdk; machine_id = $machineId; machine_id_legacy = $machineIdLegacy; client_capabilities = @{ multi_appid = $true } } if ($verifyResult.ok -eq $true -and $verifyResult.downloadToken) { $successes += [pscustomobject]@{ cdk = $cdk downloadToken = [string]$verifyResult.downloadToken resources = $verifyResult.resources eaAppIds = @($verifyResult.ea_appids) } } else { $failedCdks += $cdk } } catch { $failedCdks += $cdk } } if ($successes.Count -eq 0) { Show-FailedCdks -FailedCdks $failedCdks Fail-Exit '激活码全部失败,请联系管理员' } if ($failedCdks.Count -gt 0) { Show-FailedCdks -FailedCdks $failedCdks -Title '以下激活码验证失败,将继续激活验证成功的激活码' } $steamSuccesses = @($successes | Where-Object { @($_.eaAppIds).Count -eq 0 }) $steamRoot = $null if ($steamSuccesses.Count -gt 0) { $steamRoot = Get-SteamRoot if (-not (Test-OpenSteamToolInstalled -SteamRoot $steamRoot)) { Prepare-SteamForActivation -SteamRoot $steamRoot } Stop-SteamProcesses Remove-AlwaysConflictFiles -SteamRoot $steamRoot $luaDir = Join-Path (Join-Path $steamRoot 'config') 'lua' New-Item -ItemType Directory -Force -Path $luaDir | Out-Null # depotcache manifest 落盘目录(Steam 根目录下的 depotcache) $depotCacheDir = Join-Path $steamRoot 'depotcache' New-Item -ItemType Directory -Force -Path $depotCacheDir | Out-Null } # OST 已就绪时激活只下载 lua + manifest(每次激活不再重拉 opensteamtool 整树), # 与「只有更新 Steam 才下载 opensteamtool」的产品规则一致;未就绪(首次/更新后)才补拉整树。 $ostReady = Test-OSTReady -SteamRoot $steamRoot if ($steamSuccesses.Count -gt 0 -and -not $ostReady) { Write-InfoMessage '未检测到 opensteamtool 资源,将一并下载(与更新 Steam 同源)' } $tempDir = Join-Path $env:TEMP "cdk-opensteamtool-$([Guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Force -Path $tempDir | Out-Null $requiredFiles = @() # 统计总下载文件数,用于全局百分比进度 $totalFiles = 0 foreach ($s in $successes) { if (@($s.eaAppIds).Count -gt 0) { $totalFiles += @($s.resources.ea).Count } else { # OST 已就绪:本次激活不下发/不下载 OST,只计 lua + manifest if (-not $ostReady) { # OpenSteamTool 整树(含子目录)存在时按 ost 计数,否则回退旧 dlls 契约 if ($s.resources.PSObject.Properties['ost'] -and $s.resources.ost) { $totalFiles += @($s.resources.ost).Count + 1 } else { $totalFiles += @($s.resources.dlls).Count + 1 } } if ($s.resources.PSObject.Properties['additional_lua'] -and $s.resources.additional_lua) { $totalFiles += @($s.resources.additional_lua).Count } if ($s.resources.PSObject.Properties['manifest'] -and $s.resources.manifest) { $totalFiles += @($s.resources.manifest).Count } } } $progressIndex = 0 foreach ($success in $successes) { $token = [string]$success.downloadToken if (@($success.eaAppIds).Count -gt 0) { $eaAppId = [string]($success.eaAppIds[0]) # ponytail: appid → FC 版本号;新增 EA FC 游戏在此追加映射 $fcVer = switch ($eaAppId) { '3629260' { '26' }; '3405690' { '26' }; '2669320' { '25' }; default { '26' } } $gameRoot = Get-FC26GameRoot -AppId $eaAppId $eaFileList = @($success.resources.ea) foreach ($eaFile in $eaFileList) { $eaName = [string]$eaFile if (-not $eaName) { continue } $tempEa = Join-Path $tempDir $eaName $targetEa = Join-Path $gameRoot $eaName # 创建临时和目标的子目录(如有子路径) $tempDirForFile = Split-Path $tempEa -Parent if ($tempDirForFile -ne $tempDir) { New-Item -ItemType Directory -Force -Path $tempDirForFile | Out-Null } $targetDir = Split-Path $targetEa -Parent New-Item -ItemType Directory -Force -Path $targetDir | Out-Null $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/ea/$eaName" -TempFile $tempEa -TargetFile $targetEa -Token $token -Label $eaName # Squads 文件将移到 settings 目录、ui 文件将移到 Live Editor,均不参与游戏目录就绪校验 if ((Split-Path $eaName -Leaf) -notlike 'Squads*' -and -not ($eaName -like 'ui/*' -or $eaName -like 'ui*')) { $requiredFiles += $targetEa } } Move-SquadsFiles -FcVer $fcVer Move-UiToLiveEditor -GameRoot $gameRoot -FcVer $fcVer # 创建桌面快捷方式:FC 26(3629260)需 Launcher.exe 先启动、FC{ver}.exe 再启动,共两个 if ($eaAppId -eq '3629260') { $launcherExe = Join-Path $gameRoot 'Launcher.exe' if (Test-Path $launcherExe) { New-DesktopShortcut -TargetPath $launcherExe -WorkingDirectory $gameRoot -ShortcutName ('FC' + $fcVer + '先启动这个') -Description ('EA FC ' + $fcVer + ' Launcher') } $fcExe = Join-Path $gameRoot ('FC' + $fcVer + '.exe') if (Test-Path $fcExe) { New-DesktopShortcut -TargetPath $fcExe -WorkingDirectory $gameRoot -ShortcutName ('FC' + $fcVer + '再启动这个') -Description ('EA FC ' + $fcVer) } # ponytail: 其他 EA 游戏只创建单个 FC{ver}.exe 快捷方式(如 3405690=FC26、2669320=FC25) } else { $fcExe = Join-Path $gameRoot ('FC' + $fcVer + '.exe') if (Test-Path $fcExe) { New-DesktopShortcut -TargetPath $fcExe -WorkingDirectory $gameRoot -ShortcutName ('FC' + $fcVer) -Description ('EA FC ' + $fcVer) } } # ponytail: 3405690/2669320 在快捷方式创建后自动启动 Launcher.exe(生成 Live Editor 运行时目录) if ($eaAppId -eq '3405690' -or $eaAppId -eq '2669320') { $launcherExe = Join-Path $gameRoot 'Launcher.exe' if (Test-Path $launcherExe) { try { Start-Process -FilePath $launcherExe -WorkingDirectory $gameRoot -ErrorAction Stop } catch { Write-Warning "无法自动启动 Launcher.exe: $($_.Exception.Message)" } } else { Write-Warning '未找到 Launcher.exe,跳过自动启动' } } # 把游戏根目录的 Squads 文件强制移动覆盖到 settings 目录 Move-SquadsToSettings -GameRoot $gameRoot -FcVer $fcVer } else { # opensteamtool 已就绪时激活只下载 lua + manifest(不重拉整树); # 未就绪(首次部署 / Steam 更新后)才下载整树,与「更新 Steam」下载同源 if (-not $ostReady) { # OpenSteamTool 整树资源(新契约):相对路径可含子目录,逐项建目录后落盘 Steam 根; # 树中已含 dwmapi.dll / OpenSteamTool.dll 等顶层文件,故 ost 存在时不再走旧 dlls 循环 if ($success.resources.PSObject.Properties['ost'] -and $success.resources.ost) { foreach ($ostRes in @($success.resources.ost)) { $ostName = [string]$ostRes if (-not $ostName) { continue } $tempOst = Join-Path $tempDir $ostName $targetOst = Join-Path $steamRoot $ostName # 创建临时和目标侧的子目录(如有子路径) $tempOstParent = Split-Path $tempOst -Parent if ($tempOstParent -ne $tempDir) { New-Item -ItemType Directory -Force -Path $tempOstParent | Out-Null } $targetOstParent = Split-Path $targetOst -Parent if ($targetOstParent -ne $steamRoot) { New-Item -ItemType Directory -Force -Path $targetOstParent | Out-Null } $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/ost/$ostName" -TempFile $tempOst -TargetFile $targetOst -Token $token -Label $ostName $requiredFiles += $targetOst } } else { foreach ($dll in $success.resources.dlls) { $dllName = [string]$dll $tempFile = Join-Path $tempDir $dllName $targetFile = Join-Path $steamRoot $dllName $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/dll/$dllName" -TempFile $tempFile -TargetFile $targetFile -Token $token -Label $dllName $requiredFiles += $targetFile } } } else { Write-InfoMessage 'opensteamtool 已就绪,本次激活仅下载 lua + manifest' } $luaName = [string]$success.resources.lua $luaPath = Join-Path $luaDir $luaName $tempLuaFile = Join-Path $tempDir $luaName $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/lua/$luaName" -TempFile $tempLuaFile -TargetFile $luaPath -Token $token -Label $luaName $requiredFiles += $luaPath if ($success.resources.PSObject.Properties['additional_lua'] -and $success.resources.additional_lua) { foreach ($extraLua in @($success.resources.additional_lua)) { $extraName = [string]$extraLua if (-not $extraName) { continue } $extraPath = Join-Path $luaDir $extraName $extraTemp = Join-Path $tempDir $extraName $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/lua/$extraName" -TempFile $extraTemp -TargetFile $extraPath -Token $token -Label $extraName $requiredFiles += $extraPath } } # depotcache manifest:lua 下载完成后,将 CDK 关联的 manifest 文件放入 Steam 根目录 depotcache if ($success.resources.PSObject.Properties['manifest'] -and $success.resources.manifest) { foreach ($manifestName in @($success.resources.manifest)) { $manifestFile = [string]$manifestName if (-not $manifestFile) { continue } $manifestPath = Join-Path $depotCacheDir $manifestFile $tempManifestFile = Join-Path $tempDir $manifestFile $progressIndex++ Write-DownloadProgress -Index $progressIndex -Total $totalFiles Install-TokenFileWithRetry -Uri "$ServerBaseUrl/api/download/manifest/$manifestFile" -TempFile $tempManifestFile -TargetFile $manifestPath -Token $token -Label $manifestFile $requiredFiles += $manifestPath } } } } Write-Host '' Assert-RequiredFilesReady -Paths $requiredFiles Remove-TempDir -Path $tempDir $summaryMessage = "激活成功:$($successes.Count) 个" if ($failedCdks.Count -gt 0) { $summaryMessage = "$summaryMessage;失败:$($failedCdks.Count) 个" } if ($steamRoot) { Start-Steam -SteamRoot $steamRoot } Write-SuccessMessage $summaryMessage if ($failedCdks.Count -gt 0) { Show-FailedCdks -FailedCdks $failedCdks Write-Host '请截图或复制失败激活码联系管理员' -ForegroundColor Yellow Wait-BeforeExit } else { Start-Sleep -Seconds 3 } exit 0 } catch { Remove-TempDir -Path $tempDir if ($_.Exception.Message -eq 'FC26NotFound') { Fail-Exit ('未找到 FC ' + $fcVer + ',请先安装游戏') } if ($_.Exception.Message -eq 'SteamNotFound') { Fail-Exit '未找到 Steam 安装目录' } if ($_.Exception.Message -eq 'SteamStartFailed') { Fail-Exit 'Steam 启动失败' } Write-ErrorMessage '资源下载失败' Write-Host '' Write-Host "失败详情: $($_.Exception.Message)" -ForegroundColor Yellow Wait-BeforeExit exit 1 }