From 71117d798d829638d41788481e4f19d91f514e9b Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Sun, 10 May 2026 14:49:36 -0500 Subject: [PATCH] True --- GameUpdate.bat | 78 +++++- GameUpdate_linux.sh | 15 +- README.md | 16 +- gameupdate/patch-download.ps1 | 159 ------------ gameupdate/patch.bat | 237 ------------------ gameupdate/patch.ps1 | 444 ++++++++++++++++++++++++++++++++++ gameupdate/patch.sh | 2 +- 7 files changed, 534 insertions(+), 417 deletions(-) delete mode 100644 gameupdate/patch-download.ps1 delete mode 100644 gameupdate/patch.bat create mode 100644 gameupdate/patch.ps1 diff --git a/GameUpdate.bat b/GameUpdate.bat index b34a01e..8b6180e 100644 --- a/GameUpdate.bat +++ b/GameUpdate.bat @@ -1,9 +1,69 @@ -@echo off -setlocal - -REM Copy patch.bat to a temp name so the live patch.bat can be overwritten during updates -copy /Y "gameupdate\patch.bat" "gameupdate\patch2.bat" >nul -call "gameupdate\patch2.bat" -del /Q "gameupdate\patch2.bat" >nul 2>&1 - -endlocal \ No newline at end of file +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +REM When deployed on a game: this file sits in the game root; patch scripts live in .\gameupdate\ +REM In this repo the same layout is kept for neatness: this bat is under DazedMTLTool\gameupdate\ next to a nested gameupdate\ folder with patch.ps1. +set "GU_ROOT=%~dp0" +REM Game root is this batch file's folder (not %%CD%%, so full-path launches still work). +set "GAME_ROOT=!GU_ROOT!" +if "!GAME_ROOT:~-1!"=="\" set "GAME_ROOT=!GAME_ROOT:~0,-1!" +set "PATCH_SCRIPT_DIR=!GU_ROOT!gameupdate" + +if not exist "!PATCH_SCRIPT_DIR!\patch.ps1" ( + echo ERROR: patch.ps1 not found at: + echo !PATCH_SCRIPT_DIR!\patch.ps1 + echo Expected layout: GameUpdate.bat and a gameupdate folder next to each other ^(same parent^). + pause + exit /b 1 +) + +REM Copy patch.ps1 so the live script can be overwritten during updates +copy /Y "!PATCH_SCRIPT_DIR!\patch.ps1" "!PATCH_SCRIPT_DIR!\patch2.ps1" >nul +if errorlevel 1 ( + echo ERROR: Could not copy patch.ps1 to patch2.ps1 in: + echo !PATCH_SCRIPT_DIR! + pause + exit /b 1 +) + +set "_my_shell=pwsh" +where /q !_my_shell! +if !errorlevel! neq 0 ( + echo PowerShell 7 ^(pwsh^) not found. + if /I "%GAMEUPDATE_PROMPT_PWSH%"=="1" ( + echo. + set /p "INSTALL_PWSH=PowerShell 7 is faster. Install now via winget? (Y/N): " + if /I "!INSTALL_PWSH!"=="Y" ( + echo Installing PowerShell 7 via winget... + winget install --id Microsoft.PowerShell --source winget --accept-package-agreements --accept-source-agreements + where /q pwsh + if !errorlevel! equ 0 ( + set "_my_shell=pwsh" + echo PowerShell 7 installed; using pwsh. + ) else ( + echo Install failed or unavailable; falling back to powershell. + set "_my_shell=powershell" + ) + ) else ( + echo Skipping install; falling back to powershell. + set "_my_shell=powershell" + ) + ) else ( + echo Tip: Set GAMEUPDATE_PROMPT_PWSH=1 to offer PowerShell 7 install. + echo Falling back to powershell... + set "_my_shell=powershell" + ) + where /q !_my_shell! + if !errorlevel! neq 0 ( + echo ERROR: Neither pwsh nor powershell was found. + del /Q "!PATCH_SCRIPT_DIR!\patch2.ps1" >nul 2>&1 + pause + exit /b 1 + ) +) + +!_my_shell! -NoProfile -ExecutionPolicy Bypass -File "!PATCH_SCRIPT_DIR!\patch2.ps1" -GameRoot "!GAME_ROOT!" +set "GU_PATCH_EXIT=!errorlevel!" +del /Q "!PATCH_SCRIPT_DIR!\patch2.ps1" >nul 2>&1 + +exit /b !GU_PATCH_EXIT! diff --git a/GameUpdate_linux.sh b/GameUpdate_linux.sh index 732096a..f3a5b47 100644 --- a/GameUpdate_linux.sh +++ b/GameUpdate_linux.sh @@ -3,11 +3,12 @@ # Enable error handling set -e -# Copy patch.sh to a new file in gameupdate -cp "gameupdate/patch.sh" "gameupdate/patch2.sh" +# Same layout as GameUpdate.bat: patch assets live under ./gameupdate/ next to this file. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GAME_ROOT="$SCRIPT_DIR" +PATCH_DIR="$SCRIPT_DIR/gameupdate" -# Run the new file -bash "gameupdate/patch2.sh" - -# Delete the new file -rm "gameupdate/patch2.sh" +cd "$GAME_ROOT" +cp "$PATCH_DIR/patch.sh" "$PATCH_DIR/patch2.sh" +bash "$PATCH_DIR/patch2.sh" +rm "$PATCH_DIR/patch2.sh" diff --git a/README.md b/README.md index b638701..fa94b89 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,18 @@ 3. Extract to game folder and Replace All. ## Future Patching -1. Run GAMEUPDATE.bat to auto patch. -2. Keep the whole `gameupdate` folder together when you ship the game—`patch.bat` and `patch-download.ps1` must sit side by side. -3. Optional: set `GAMEUPDATE_PROMPT_PWSH=1` before running `GameUpdate.bat` if you want users to be prompted to install PowerShell 7 via winget. -4. Optional: set `GAMEUPDATE_DL_ATTEMPTS` (default `2`) to control retries for API checks/downloads. Lower values fail faster; higher values tolerate flaky networks. +1. Run **`GameUpdate.bat`** to auto patch (Windows). + +### Folder layout + +**In this translation-tool repo:** Patch payloads stay tidy inside **`gameupdate/gameupdate/`** (`patch.ps1`, `patch-config.txt`, etc.). **`GameUpdate.bat`** and **`GameUpdate_linux.sh`** sit one level up, under **`gameupdate/`**. + +**On an installed game (what gets copied over):** Put **`GameUpdate.bat`** in the **game root**, next to the game exe. Put **`patch.ps1`** and friends inside **`gameupdate\`** under that same root (mirror names—still **`gameupdate\`**). **`GameUpdate.bat`** finds **`gameupdate\patch.ps1`** from its own folder, so **`GameRoot`** is correct even if the console cwd is somewhere else. + +**Copy checklist:** From **`DazedMTLTool/gameupdate/`**, copy **`GameUpdate.bat`** to `\`; copy everything inside **`gameupdate/gameupdate/`** into `\gameupdate\`. + +2. Optional: set `GAMEUPDATE_PROMPT_PWSH=1` before running `GameUpdate.bat` if you want users to be prompted to install PowerShell 7 via winget. +3. Optional: set `GAMEUPDATE_DL_ATTEMPTS` (default `2`) to control retries for API checks/downloads. Lower values fail faster; higher values tolerate flaky networks. # Troubleshooting **GAMEUPDATE.bat doesn't update and closes immediately** diff --git a/gameupdate/patch-download.ps1 b/gameupdate/patch-download.ps1 deleted file mode 100644 index 75e80ac..0000000 --- a/gameupdate/patch-download.ps1 +++ /dev/null @@ -1,159 +0,0 @@ -#Requires -Version 5.1 -param( - [Parameter(Mandatory = $true)] - [string]$GameRoot, - [Parameter(Mandatory = $true)] - [string]$StateFile -) - -$ErrorActionPreference = 'Stop' -$exitCode = 1 - -if ($PSVersionTable.PSEdition -ne 'Core') { - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - } - catch {} -} - -function Get-EnvInt { - param( - [Parameter(Mandatory = $true)][string]$Name, - [Parameter(Mandatory = $true)][int]$Default - ) - $raw = [Environment]::GetEnvironmentVariable($Name) - if ([string]::IsNullOrWhiteSpace($raw)) { return $Default } - $parsed = 0 - if ([int]::TryParse([string]$raw, [ref]$parsed) -and $parsed -gt 0) { return $parsed } - return $Default -} - -function Invoke-WithRetry { - param( - [Parameter(Mandatory = $true)][scriptblock]$Action, - [Parameter(Mandatory = $true)][string]$Name, - [int]$Attempts = 2, - [int]$DelaySeconds = 2 - ) - $lastErr = $null - for ($i = 1; $i -le $Attempts; $i++) { - try { - return & $Action - } - catch { - $lastErr = $_ - if ($i -lt $Attempts) { - Write-Host ("{0} failed ({1}/{2}), retrying..." -f $Name, $i, $Attempts) - Start-Sleep -Seconds $DelaySeconds - } - } - } - throw $lastErr -} - -function Download-WithIwrSpeedProgress { - param( - [Parameter(Mandatory = $true)][string]$Url, - [Parameter(Mandatory = $true)][string]$OutFile, - [Parameter(Mandatory = $true)][hashtable]$Headers - ) - - if (Test-Path -LiteralPath $OutFile) { - Remove-Item -LiteralPath $OutFile -Force - } - - # Fast path: let Invoke-WebRequest handle transfer and progress internally. - # Custom stream/polling progress gives richer stats but adds measurable overhead. - $prevPref = $ProgressPreference - try { - $ProgressPreference = 'Continue' - Invoke-WebRequest -Uri $Url -OutFile $OutFile -Headers $Headers -UseBasicParsing - } - finally { - $ProgressPreference = $prevPref - } -} - -try { - Set-Location -LiteralPath $GameRoot - - if (-not $env:GU_USERNAME -or -not $env:GU_REPO -or -not $env:GU_BRANCH) { - throw 'PATCH_ERR:CONFIG:patch.bat must set GU_USERNAME, GU_REPO, and GU_BRANCH.' - } - - $id = [uri]::EscapeDataString($env:GU_USERNAME + '/' + $env:GU_REPO) - $branch = [uri]::EscapeDataString($env:GU_BRANCH) - $ua = 'DazedMTL-Patcher-1.0' - $headers = @{ 'User-Agent' = $ua } - $dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2 - - $latestSha = Invoke-WithRetry -Name 'Resolve latest patch SHA' -Attempts $dlAttempts -Action { - (Invoke-RestMethod -Uri ("https://gitgud.io/api/v4/projects/$id/repository/branches/$branch") -Headers $headers).commit.id - } - if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' } - $latestSha = ([string]$latestSha).Trim() - - $previousSha = '' - if (Test-Path -LiteralPath $StateFile) { - $previousSha = (Get-Content -LiteralPath $StateFile -Raw).Trim() - } else { - Write-Host 'First run: comparing with remote...' - } - if ($previousSha -eq $latestSha) { - exit 10 - } - - Write-Host 'Downloading patch...' - $zipPath = Join-Path $PWD.Path 'repo.zip' - # Use a short temp extract root to avoid Windows MAX_PATH issues when - # GitLab archive root folder includes long commit SHA suffixes. - $stage = Join-Path ([IO.Path]::GetTempPath()) ("gu_" + [Guid]::NewGuid().ToString("N")) - $archiveSha = [uri]::EscapeDataString($latestSha) - $archiveUrl = "https://gitgud.io/api/v4/projects/$id/repository/archive.zip?sha=$archiveSha" - - $dlSw = [System.Diagnostics.Stopwatch]::StartNew() - Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action { - Download-WithIwrSpeedProgress -Url $archiveUrl -OutFile $zipPath -Headers $headers - } | Out-Null - $dlSw.Stop() - $bytes = (Get-Item -LiteralPath $zipPath).Length - $secs = [Math]::Max($dlSw.Elapsed.TotalSeconds, 0.001) - $mbps = ($bytes / 1MB) / $secs - Write-Host ("Download complete via iwr: {0:N1} MB in {1:N1}s ({2:N1} MB/s)" -f ($bytes / 1MB), $secs, $mbps) - - $fs = [System.IO.File]::OpenRead($zipPath) - try { - $hdr = New-Object byte[] 4 - if ($fs.Read($hdr, 0, 4) -lt 4 -or $hdr[0] -ne 0x50 -or $hdr[1] -ne 0x4B) { - throw 'PATCH_ERR:ZIP:Download is not a valid ZIP (error page or empty file).' - } - } - finally { - $fs.Dispose() - } - - if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force } - New-Item -ItemType Directory -Path $stage | Out-Null - try { - Expand-Archive -LiteralPath $zipPath -DestinationPath $stage -Force - $dirs = @(Get-ChildItem -LiteralPath $stage -Directory) - if ($dirs.Count -ne 1) { - throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count) - } - Copy-Item -Path (Join-Path $dirs[0].FullName '*') -Destination $PWD.Path -Recurse -Force - } - finally { - if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force } - } - - Set-Content -LiteralPath $StateFile -Value $latestSha -Encoding Ascii - $exitCode = 0 -} -catch { - Write-Host '' - Write-Host ($_.Exception.Message) - if ($_.InvocationInfo.PositionMessage) { Write-Host $_.InvocationInfo.PositionMessage } - $exitCode = 1 -} - -exit $exitCode diff --git a/gameupdate/patch.bat b/gameupdate/patch.bat deleted file mode 100644 index 581306e..0000000 --- a/gameupdate/patch.bat +++ /dev/null @@ -1,237 +0,0 @@ -@echo off -setlocal EnableExtensions - -REM Check if being run from gameupdate folder (incorrect usage) -for %%I in ("%CD%") do set "CURRENT_FOLDER=%%~nxI" -if /I "%CURRENT_FOLDER%"=="gameupdate" ( - echo. - echo ======================================== - echo ERROR: Do not run patch.bat directly! - echo ======================================== - echo. - echo You are running this from the gameupdate folder. - echo This will not work correctly! - echo. - echo Please go back to the game's root folder and - echo run GameUpdate.bat instead. - echo ======================================== - echo. - pause - exit /b 1 -) - -REM Determine important paths -set "SCRIPT_DIR=%~dp0" -set "ROOT_DIR=%CD%" -set "CONFIG_FILE=%SCRIPT_DIR%patch-config.txt" - -echo Root: %ROOT_DIR% - -setlocal EnableDelayedExpansion - -set "_my_shell=pwsh" -where /q !_my_shell! -if !errorlevel! neq 0 ( - echo PowerShell 7 ^(pwsh^) not found. - if /I "%GAMEUPDATE_PROMPT_PWSH%"=="1" ( - echo. - set /p "INSTALL_PWSH=PowerShell 7 is faster. Install now via winget? (Y/N): " - if /I "!INSTALL_PWSH!"=="Y" ( - echo Installing PowerShell 7 via winget... - winget install --id Microsoft.PowerShell --source winget --accept-package-agreements --accept-source-agreements - where /q pwsh - if !errorlevel! equ 0 ( - set "_my_shell=pwsh" - echo PowerShell 7 installed; using pwsh. - ) else ( - echo Install failed or unavailable; falling back to powershell. - set "_my_shell=powershell" - ) - ) else ( - echo Skipping install; falling back to powershell. - set "_my_shell=powershell" - ) - ) else ( - echo Tip: Set GAMEUPDATE_PROMPT_PWSH=1 to offer PowerShell 7 install. - echo Falling back to powershell... - set "_my_shell=powershell" - ) - where /q !_my_shell! - if !errorlevel! neq 0 ( - echo ERROR: Neither pwsh nor powershell was found. - pause - exit /b 1 - ) -) - -REM Check if patch-config.txt exists in gameupdate folder -if not exist "%CONFIG_FILE%" ( - echo Config file gameupdate\patch-config.txt not found - skipping patch. - pause - exit /b -) - -REM Read configuration from file -for /f "usebackq tokens=1,2 delims==" %%a in ("%CONFIG_FILE%") do ( - if "%%a"=="username" set "username=%%b" - if "%%a"=="repo" set "repo=%%b" - if "%%a"=="branch" set "branch=%%b" -) - -REM Validate required config keys early for clearer errors -if "%username%"=="" ( - echo ERROR: 'username=' is missing in gameupdate\patch-config.txt - pause - exit /b 1 -) -if "%repo%"=="" ( - echo ERROR: 'repo=' is missing in gameupdate\patch-config.txt - pause - exit /b 1 -) -if "%branch%"=="" ( - echo ERROR: 'branch=' is missing in gameupdate\patch-config.txt - pause - exit /b 1 -) - -REM For PowerShell (proper URL encoding; avoids cmd %% pitfalls) -set "GU_USERNAME=%username%" -set "GU_REPO=%repo%" -set "GU_BRANCH=%branch%" - -REM -------------------------------------------------------- -REM PRE-SETUP: Ensure SRPG data and patch structure exists -REM Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates -REM 1) Unpack once if data folder doesn't exist (and data.dts does) -REM 2) Create Patch once if patch folder doesn't exist -REM -------------------------------------------------------- -set "UNPACKER=%ROOT_DIR%\SRPG_Unpacker.exe" -if exist "%ROOT_DIR%\data.dts" ( - if exist "%UNPACKER%" ( - REM Step 1: Unpack (once) - if not exist "%ROOT_DIR%\data\" ( - set "SHOULD_UNPACK=1" - ) else if not exist "%ROOT_DIR%\data\project.dat" ( - set "SHOULD_UNPACK=1" - ) else ( - set "SHOULD_UNPACK=0" - ) - - if "!SHOULD_UNPACK!"=="1" ( - if exist "%ROOT_DIR%\data.dts" ( - echo [Pre-Setup] Unpacking data.dts to data\ - pushd "%ROOT_DIR%" - "%UNPACKER%" -o "data" "data.dts" - if !errorlevel! neq 0 ( - echo [Pre-Setup] ERROR: Unpack failed. Continuing. - ) - popd - ) else ( - echo [Pre-Setup] Skipping unpack: data.dts missing. - ) - - REM Step 2: Create Patch (once) - if not exist "%ROOT_DIR%\patch\" ( - if exist "%ROOT_DIR%\data\project.dat" ( - echo [Pre-Setup] Creating patch folder from data\project.dat - pushd "%ROOT_DIR%" - "%UNPACKER%" ".\data\project.dat" -c - if !errorlevel! neq 0 ( - echo [Pre-Setup] ERROR: Create Patch failed. Continuing. - ) - popd - ) else ( - echo [Pre-Setup] Skipping create patch: data\project.dat not found. - ) - ) - ) else ( - REM SHOULD_UNPACK=0: only Step 2 may still be needed - if not exist "%ROOT_DIR%\patch\" ( - if exist "%ROOT_DIR%\data\project.dat" ( - echo [Pre-Setup] Creating patch folder from data\project.dat - pushd "%ROOT_DIR%" - "%UNPACKER%" ".\data\project.dat" -c - if !errorlevel! neq 0 ( - echo [Pre-Setup] ERROR: Create Patch failed. Continuing. - ) - popd - ) else ( - echo [Pre-Setup] Skipping create patch: data\project.dat not found. - ) - ) - ) - ) else ( - echo [Pre-Setup] SRPG_Unpacker.exe not found; skipping data setup. - ) -) - -:download_extract - -REM Download/extract via patch-download.ps1 (IWR-based); Bypass avoids ExecutionPolicy prompts -if not exist "%SCRIPT_DIR%patch-download.ps1" ( - echo ERROR: patch-download.ps1 not found next to patch.bat in the gameupdate folder. - pause - exit /b 1 -) -!_my_shell! -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%patch-download.ps1" -GameRoot "%ROOT_DIR%" -StateFile "%SCRIPT_DIR%previous_patch_sha.txt" -set "PATCH_DL_EXIT=!errorlevel!" -if "!PATCH_DL_EXIT!"=="10" ( - echo Already up to date. - endlocal - pause - exit /b 0 -) -if not "!PATCH_DL_EXIT!"=="0" ( - echo Download or extraction failed^! - if exist "%ROOT_DIR%\repo.zip" del "%ROOT_DIR%\repo.zip" - if exist "%ROOT_DIR%\_patch_extract_tmp" rmdir /s /q "%ROOT_DIR%\_patch_extract_tmp" - pause - exit /b 1 -) -echo Applying patch... -REM Files merged by Copy-Item above -REM -------------------------------------------------------- -REM POST-APPLY: Run Steps 3 and 4 after patch files are merged -REM 3) Apply Patch to data\project.dat -REM 4) Pack data back into data.dts -REM -------------------------------------------------------- -set "UNPACKER=%ROOT_DIR%\SRPG_Unpacker.exe" -if exist "%ROOT_DIR%\data.dts" ( - if exist "%UNPACKER%" ( - REM Step 3: Apply Patch - if exist "%ROOT_DIR%\data\project.dat" ( - echo Applying patch to data\project.dat... - pushd "%ROOT_DIR%" - "%UNPACKER%" ".\data\project.dat" -a - if !errorlevel! neq 0 ( - echo ERROR: Apply Patch failed. - ) - popd - ) else ( - echo ERROR: data\project.dat not found; cannot apply patch. - ) - - REM Step 4: Pack - if exist "%ROOT_DIR%\data\" ( - echo Packing data folder to data.dts... - pushd "%ROOT_DIR%" - "%UNPACKER%" -o "data.dts" "data" - if !errorlevel! neq 0 ( - echo WARNING: Pack failed. - ) - popd - ) else ( - echo Step 4: Skipping pack - data folder not found. - ) - ) else ( - echo SRPG_Unpacker.exe not found in root; skipping SRPG patch steps. - ) -) -REM Clean up -echo Cleaning up... -del "%ROOT_DIR%\repo.zip" -if exist "%ROOT_DIR%\_patch_extract_tmp" rmdir /s /q "%ROOT_DIR%\_patch_extract_tmp" -endlocal -pause -exit /b diff --git a/gameupdate/patch.ps1 b/gameupdate/patch.ps1 new file mode 100644 index 0000000..3f91bc3 --- /dev/null +++ b/gameupdate/patch.ps1 @@ -0,0 +1,444 @@ +#Requires -Version 5.1 +param( + [Parameter(Mandatory = $false)] + [string]$GameRoot +) + +$ErrorActionPreference = 'Stop' + +if (-not $PSBoundParameters.ContainsKey('GameRoot') -or [string]::IsNullOrWhiteSpace($GameRoot)) { + $GameRoot = (Get-Location).ProviderPath +} +else { + $GameRoot = (Resolve-Path -LiteralPath $GameRoot).ProviderPath +} + +$PatchBundleRoot = $PSScriptRoot + +function Wait-ConsolePause { + $null = cmd /c pause +} + +function Write-BannerWrongFolder { + Write-Host '' + Write-Host '========================================' + Write-Host 'ERROR: Wrong game root folder!' + Write-Host '========================================' + Write-Host '' + Write-Host 'Game root cannot be the gameupdate folder itself.' + Write-Host 'Run GameUpdate.bat from the game root (same folder as GameUpdate.bat).' + Write-Host '========================================' + Write-Host '' +} + +function Test-WrongWorkingDirectory { + param([string]$Root) + return ($Root -and ((Split-Path -Leaf $Root) -ieq 'gameupdate')) +} + +function Read-PatchConfig { + param([string]$ConfigPath) + $cfg = @{ username = ''; repo = ''; branch = '' } + if (-not (Test-Path -LiteralPath $ConfigPath)) { + return $cfg + } + foreach ($rawLine in Get-Content -LiteralPath $ConfigPath) { + $line = $rawLine.Trim() + if (-not $line -or $line.StartsWith('#')) { continue } + $eq = $line.IndexOf('=') + if ($eq -lt 1) { continue } + $k = $line.Substring(0, $eq).Trim().ToLowerInvariant() + $v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' } + switch ($k) { + 'username' { $cfg.username = $v } + 'repo' { $cfg.repo = $v } + 'branch' { $cfg.branch = $v } + } + } + return $cfg +} + +function Invoke-SrpgUnpacker { + param( + [Parameter(Mandatory = $true)][string]$ExePath, + [Parameter(Mandatory = $true)][string]$WorkingDirectory, + [Parameter(Mandatory = $true)][string[]]$Arguments + ) + Push-Location -LiteralPath $WorkingDirectory + try { + & $ExePath @Arguments + return $LASTEXITCODE + } + finally { + Pop-Location + } +} + +function Invoke-SrpgPreSetup { + param([string]$Root) + $unpacker = Join-Path $Root 'SRPG_Unpacker.exe' + $dts = Join-Path $Root 'data.dts' + $dataDir = Join-Path $Root 'data' + $projectDat = Join-Path $dataDir 'project.dat' + $patchDir = Join-Path $Root 'patch' + + if (-not (Test-Path -LiteralPath $dts)) { + return + } + if (-not (Test-Path -LiteralPath $unpacker)) { + Write-Host '[Pre-Setup] SRPG_Unpacker.exe not found; skipping data setup.' + return + } + + $shouldUnpack = + -not (Test-Path -LiteralPath $dataDir) -or + -not (Test-Path -LiteralPath $projectDat) + + $runUnpackBlock = { + if (Test-Path -LiteralPath $dts) { + Write-Host '[Pre-Setup] Unpacking data.dts to data\' + $code = Invoke-SrpgUnpacker -ExePath $unpacker -WorkingDirectory $Root -Arguments @('-o', 'data', 'data.dts') + if ($code -ne 0) { + Write-Host '[Pre-Setup] ERROR: Unpack failed. Continuing.' + } + } + else { + Write-Host '[Pre-Setup] Skipping unpack: data.dts missing.' + } + + if (-not (Test-Path -LiteralPath $patchDir)) { + if (Test-Path -LiteralPath $projectDat) { + Write-Host '[Pre-Setup] Creating patch folder from data\project.dat' + $code = Invoke-SrpgUnpacker -ExePath $unpacker -WorkingDirectory $Root -Arguments @('.\data\project.dat', '-c') + if ($code -ne 0) { + Write-Host '[Pre-Setup] ERROR: Create Patch failed. Continuing.' + } + } + else { + Write-Host '[Pre-Setup] Skipping create patch: data\project.dat not found.' + } + } + } + + if ($shouldUnpack) { + & $runUnpackBlock + } + else { + if (-not (Test-Path -LiteralPath $patchDir)) { + if (Test-Path -LiteralPath $projectDat) { + Write-Host '[Pre-Setup] Creating patch folder from data\project.dat' + $code = Invoke-SrpgUnpacker -ExePath $unpacker -WorkingDirectory $Root -Arguments @('.\data\project.dat', '-c') + if ($code -ne 0) { + Write-Host '[Pre-Setup] ERROR: Create Patch failed. Continuing.' + } + } + else { + Write-Host '[Pre-Setup] Skipping create patch: data\project.dat not found.' + } + } + } +} + +function Get-EnvInt { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][int]$Default + ) + $raw = [Environment]::GetEnvironmentVariable($Name) + if ([string]::IsNullOrWhiteSpace($raw)) { return $Default } + $parsed = 0 + if ([int]::TryParse([string]$raw, [ref]$parsed) -and $parsed -gt 0) { return $parsed } + return $Default +} + +function Invoke-WithRetry { + param( + [Parameter(Mandatory = $true)][scriptblock]$Action, + [Parameter(Mandatory = $true)][string]$Name, + [int]$Attempts = 2, + [int]$DelaySeconds = 2 + ) + $lastErr = $null + for ($i = 1; $i -le $Attempts; $i++) { + try { + return & $Action + } + catch { + $lastErr = $_ + if ($i -lt $Attempts) { + Write-Host ("{0} failed ({1}/{2}), retrying..." -f $Name, $i, $Attempts) + Start-Sleep -Seconds $DelaySeconds + } + } + } + throw $lastErr +} + +function Download-WithIwrSpeedProgress { + param( + [Parameter(Mandatory = $true)][string]$Url, + [Parameter(Mandatory = $true)][string]$OutFile, + [Parameter(Mandatory = $true)][hashtable]$Headers + ) + + if (Test-Path -LiteralPath $OutFile) { + Remove-Item -LiteralPath $OutFile -Force + } + + $prevPref = $ProgressPreference + try { + $ProgressPreference = 'Continue' + Invoke-WebRequest -Uri $Url -OutFile $OutFile -Headers $Headers -UseBasicParsing + } + finally { + $ProgressPreference = $prevPref + } +} + +function Remove-FileOrDirectoryViaCmd { + param([Parameter(Mandatory = $true)][string]$LiteralPath) + if (-not (Test-Path -LiteralPath $LiteralPath)) { + return + } + $full = (Resolve-Path -LiteralPath $LiteralPath).ProviderPath + $item = Get-Item -LiteralPath $LiteralPath + if ($item.PSIsContainer) { + cmd.exe /c "rd /s /q `"$full`"" | Out-Null + } + else { + cmd.exe /c "del /f /q `"$full`"" | Out-Null + } +} + +function Remove-DownloadedRepoZip { + param([Parameter(Mandatory = $true)][string]$LiteralPath) + Remove-FileOrDirectoryViaCmd -LiteralPath $LiteralPath +} + +function Invoke-PatchDownloadExtract { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string]$StateFilePath, + [Parameter(Mandatory = $true)][string]$Username, + [Parameter(Mandatory = $true)][string]$Repo, + [Parameter(Mandatory = $true)][string]$Branch + ) + + if ($PSVersionTable.PSEdition -ne 'Core') { + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + } + catch {} + } + + Set-Location -LiteralPath $Root + + $id = [uri]::EscapeDataString($Username + '/' + $Repo) + $branchEnc = [uri]::EscapeDataString($Branch) + $ua = 'DazedMTL-Patcher-1.0' + $headers = @{ 'User-Agent' = $ua } + $dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2 + + $latestSha = Invoke-WithRetry -Name 'Resolve latest patch SHA' -Attempts $dlAttempts -Action { + (Invoke-RestMethod -Uri ("https://gitgud.io/api/v4/projects/$id/repository/branches/$branchEnc") -Headers $headers).commit.id + } + if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' } + $latestSha = ([string]$latestSha).Trim() + + $previousSha = '' + if (Test-Path -LiteralPath $StateFilePath) { + $previousSha = (Get-Content -LiteralPath $StateFilePath -Raw).Trim() + } + else { + Write-Host 'First run: comparing with remote...' + } + if ($previousSha -eq $latestSha) { + return 10 + } + + Write-Host 'Downloading patch...' + $zipPath = Join-Path $PWD.Path 'repo.zip' + $stage = Join-Path ([IO.Path]::GetTempPath()) ('gu_' + [Guid]::NewGuid().ToString('N')) + $archiveSha = [uri]::EscapeDataString($latestSha) + $archiveUrl = "https://gitgud.io/api/v4/projects/$id/repository/archive.zip?sha=$archiveSha" + + $dlSw = [System.Diagnostics.Stopwatch]::StartNew() + Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action { + Download-WithIwrSpeedProgress -Url $archiveUrl -OutFile $zipPath -Headers $headers + } | Out-Null + $dlSw.Stop() + $bytes = (Get-Item -LiteralPath $zipPath).Length + $secs = [Math]::Max($dlSw.Elapsed.TotalSeconds, 0.001) + $mbps = ($bytes / 1MB) / $secs + Write-Host ("Download complete via iwr: {0:N1} MB in {1:N1}s ({2:N1} MB/s)" -f ($bytes / 1MB), $secs, $mbps) + + $fs = [System.IO.File]::OpenRead($zipPath) + try { + $hdr = New-Object byte[] 4 + if ($fs.Read($hdr, 0, 4) -lt 4 -or $hdr[0] -ne 0x50 -or $hdr[1] -ne 0x4B) { + throw 'PATCH_ERR:ZIP:Download is not a valid ZIP (error page or empty file).' + } + } + finally { + $fs.Dispose() + } + + if (Test-Path -LiteralPath $stage) { + Remove-FileOrDirectoryViaCmd -LiteralPath $stage + } + New-Item -ItemType Directory -Path $stage | Out-Null + try { + Expand-Archive -LiteralPath $zipPath -DestinationPath $stage -Force + $dirs = @(Get-ChildItem -LiteralPath $stage -Directory) + if ($dirs.Count -ne 1) { + throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count) + } + Copy-Item -Path (Join-Path $dirs[0].FullName '*') -Destination $PWD.Path -Recurse -Force + Write-Host 'Removing downloaded archive (repo.zip)...' + Remove-DownloadedRepoZip -LiteralPath $zipPath + } + finally { + if (Test-Path -LiteralPath $stage) { + Remove-FileOrDirectoryViaCmd -LiteralPath $stage + } + } + + Set-Content -LiteralPath $StateFilePath -Value $latestSha -Encoding Ascii + return 0 +} + +function Invoke-SrpgPostApply { + param([string]$Root) + $unpacker = Join-Path $Root 'SRPG_Unpacker.exe' + $dts = Join-Path $Root 'data.dts' + $projectDat = Join-Path $Root 'data\project.dat' + $dataDir = Join-Path $Root 'data' + + if (-not (Test-Path -LiteralPath $dts)) { + return + } + if (-not (Test-Path -LiteralPath $unpacker)) { + Write-Host 'SRPG_Unpacker.exe not found in root; skipping SRPG patch steps.' + return + } + + if (Test-Path -LiteralPath $projectDat) { + Write-Host 'Applying patch to data\project.dat...' + $code = Invoke-SrpgUnpacker -ExePath $unpacker -WorkingDirectory $Root -Arguments @('.\data\project.dat', '-a') + if ($code -ne 0) { + Write-Host 'ERROR: Apply Patch failed.' + } + } + else { + Write-Host 'ERROR: data\project.dat not found; cannot apply patch.' + } + + if (Test-Path -LiteralPath $dataDir) { + Write-Host 'Packing data folder to data.dts...' + $code = Invoke-SrpgUnpacker -ExePath $unpacker -WorkingDirectory $Root -Arguments @('-o', 'data.dts', 'data') + if ($code -ne 0) { + Write-Host 'WARNING: Pack failed.' + } + } + else { + Write-Host 'Step 4: Skipping pack - data folder not found.' + } +} + +function Remove-PatchTempArtifacts { + param([string]$Root) + $zipPath = Join-Path $Root 'repo.zip' + $legacyTmp = Join-Path $Root '_patch_extract_tmp' + + if (Test-Path -LiteralPath $zipPath) { + Write-Host ' Removing repo.zip - large files can sit here for minutes if antivirus is scanning.' + Remove-FileOrDirectoryViaCmd -LiteralPath $zipPath + } + if (Test-Path -LiteralPath $legacyTmp) { + Write-Host ' Removing _patch_extract_tmp - using rd /s /q (avoids PowerShell hangs on huge folders).' + Remove-FileOrDirectoryViaCmd -LiteralPath $legacyTmp + } +} + +try { + if (Test-WrongWorkingDirectory -Root $GameRoot) { + Write-BannerWrongFolder + Wait-ConsolePause + exit 1 + } + + Write-Host "Root: $GameRoot" + + $configPath = Join-Path $PatchBundleRoot 'patch-config.txt' + if (-not (Test-Path -LiteralPath $configPath)) { + Write-Host 'patch-config.txt not found next to patch.ps1 - skipping patch.' + Wait-ConsolePause + exit 0 + } + + $cfg = Read-PatchConfig -ConfigPath $configPath + + if ([string]::IsNullOrWhiteSpace($cfg.username)) { + Write-Host "ERROR: 'username=' is missing in patch-config.txt" + Wait-ConsolePause + exit 1 + } + if ([string]::IsNullOrWhiteSpace($cfg.repo)) { + Write-Host "ERROR: 'repo=' is missing in patch-config.txt" + Wait-ConsolePause + exit 1 + } + if ([string]::IsNullOrWhiteSpace($cfg.branch)) { + Write-Host "ERROR: 'branch=' is missing in patch-config.txt" + Wait-ConsolePause + exit 1 + } + + Invoke-SrpgPreSetup -Root $GameRoot + + $stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt' + + $patchDlExit = 1 + try { + $patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile ` + -Username $cfg.username -Repo $cfg.repo -Branch $cfg.branch + } + catch { + Write-Host '' + Write-Host $_.Exception.Message + if ($_.InvocationInfo.PositionMessage) { Write-Host $_.InvocationInfo.PositionMessage } + Remove-PatchTempArtifacts -Root $GameRoot + Wait-ConsolePause + exit 1 + } + + if ($patchDlExit -eq 10) { + Write-Host 'Already up to date.' + Wait-ConsolePause + exit 0 + } + + if ($patchDlExit -ne 0) { + Write-Host 'Download or extraction failed!' + Remove-PatchTempArtifacts -Root $GameRoot + Wait-ConsolePause + exit 1 + } + + Write-Host 'Applying patch...' + + Invoke-SrpgPostApply -Root $GameRoot + + Write-Host 'Cleaning up...' + Remove-PatchTempArtifacts -Root $GameRoot + Write-Host 'Done.' + + Wait-ConsolePause + exit 0 +} +catch { + Write-Host '' + Write-Host $_.Exception.Message + Wait-ConsolePause + exit 1 +} diff --git a/gameupdate/patch.sh b/gameupdate/patch.sh index 3bd0911..ad1b86d 100644 --- a/gameupdate/patch.sh +++ b/gameupdate/patch.sh @@ -102,7 +102,7 @@ if [ -f "$ROOT_DIR/data.dts" ]; then if [ -f "$UNPACKER" ]; then echo "[Pre-Setup] Running SRPG_Unpacker preparation steps..." - # Step 1: Unpack (once) — mirror patch.bat: unpack if no data/ or no data/project.dat + # Step 1: Unpack (once) — mirror patch.ps1: unpack if no data/ or no data/project.dat if [ ! -d "$ROOT_DIR/data" ] || [ ! -f "$ROOT_DIR/data/project.dat" ]; then if [ -f "$ROOT_DIR/data.dts" ]; then echo "[Pre-Setup] Step 1: Unpacking data.dts -> data"