diff --git a/README.md b/README.md index 595a529..e32d19a 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel | Step | Action | |------|--------| | **0 Project** | Select the game root folder (browse or Enter — detection also runs when you reopen the tab). If `wolf_json/` does not exist yet, the tool automatically unpacks `.wolf` archives when needed and extracts text into the game's `wolf_json/` folder (maps, common events, databases, `Game.dat`, Evtext, and `names.json`). A checklist lists every JSON file in `wolf_json/`; tick the ones you want and click **Import** (or leave Step 0 — checked files auto-import into the tool's `files/` folder, matching the RPG Maker workflow). Extraction snapshots pristine binaries into `wolf_json/originals/` for idempotent inject in Step 7. | -| **1 Pre-process** | Optional: **dazedformat** normalises JSON in `wolf_json/` and `files/` (`json.dump`, indent 4) for clean git diffs; **Copy gameupdate/** installs the updater scripts, patch scripts, and `.gitignore` into the game root for git-based patching. Paths auto-fill from Step 0. | +| **1 Pre-process** | Optional: **dazedformat** normalises JSON in `wolf_json/` and `files/` (`json.dump`, indent 4) for clean git diffs; **Copy gameupdate/** installs the updater scripts, patch scripts, `.gitignore`, and `UberWolfCli.exe` into the game root for git-based patching (players auto-unpack `Data.wolf` on first GameUpdate). Paths auto-fill from Step 0. | | **2 Glossary** | Build `vocab.txt` before translating: copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save. Item/skill/enemy value names (`names.json`) are translated in Step 3 and harvested into `vocab.txt` automatically during Phase 0 — do not list them here. | | **3 Names** | Translate `names.json` (item/skill/enemy/map value names). WolfDawn tags each name with a per-entry **safety** badge (`safe`, `refs`, or `verify`). Phase 0 translates only `safe` entries; `refs` and `verify` names stay Japanese so inject skips them. Review the category breakdown for this game, pick **Translation mode** (Normal or Batch), and run **Translate safe names (Phase 0)**. | | **4 Database** | Review the **discovery summary** to see where this game's text lives (standard RPG sheets vs custom dialogue tables). Database sheets are classified as foundation (items, skills, descriptions — translate first) or narrative (custom event/profile sheets — translate after foundation). Use **Translate foundation DB** then **Translate narrative DB**, or tick specific sheets and run **Translate checked sheets only**. Optional: copy the **DB structure prompt** for an AI audit and import the returned JSON into `wolf_json/db_profile.json`. | diff --git a/gameupdate/.gitignore b/gameupdate/.gitignore index 6b5f778..679166d 100644 --- a/gameupdate/.gitignore +++ b/gameupdate/.gitignore @@ -21,6 +21,7 @@ !.gitignore !README.md !patch-config.txt +!patch-config.example.txt !GameUpdate* !patch* !Game.dat @@ -29,6 +30,8 @@ !package.nw !SRPG_Unpacker.exe !(SRPG_Unpacker Patcher).bat +!UberWolfCli.exe +!UberWolfCli.LICENSE.txt !Data/BasicData/CommonEvent.dat !Data/BasicData/DataBase.dat !Data/BasicData/CDataBase.dat diff --git a/gameupdate/README.md b/gameupdate/README.md index fa94b89..0283b56 100644 --- a/gameupdate/README.md +++ b/gameupdate/README.md @@ -17,22 +17,67 @@ 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. +### patch-config.txt + +Create `gameupdate/patch-config.txt` next to `patch.ps1` (see `patch-config.example.txt`). + +In DazedMTLTool, set **Config → Game Update Defaults** (forge, host, org/username, branch) once. Step 1 **Copy gameupdate/** writes those into each game's `patch-config.txt`. You still set `repo=` per game. + +```txt +forge=gitlab +host=gitgud.io +username=YOUR_ORG_OR_USER +repo=YOUR_PATCH_REPO +branch=main +``` + +| Key | Meaning | +|-----|---------| +| `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) | +| `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` | +| `username` | Owner / org / group (`owner=` / `org=` aliases work) | +| `repo` | Repository name | +| `branch` | Branch to track | + +Examples: + +```txt +# GitLab / gitgud +forge=gitlab +host=gitgud.io +username=myorg +repo=cool-game-en +branch=main + +# GitHub +forge=github +host=github.com +username=myorg +repo=cool-game-en +branch=main + +# Forgejo / Codeberg +forge=forgejo +host=codeberg.org +username=myorg +repo=cool-game-en +branch=main +``` + +Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`). + # Troubleshooting **GAMEUPDATE.bat doesn't update and closes immediately** 1. Make sure your path doesn't contain any Japanese characters or lots of whitespace. 2. Make sure you actually have permissions in the folder -3. Auto-update calls GitLab’s public HTTP API (`/api/v4/...`), not the web “Download ZIP” URL—no account or token is required for public patch repos. - -For WOLF RPG games, if you downloaded the game off of DLSite, you will need to do some extra steps to patch it. This is because there is a "master" file called Data.wolf that will take priority over the english patch files. You will need this file to be a folder before patching will work. +3. Auto-update calls the forge's public HTTP API (GitLab `/api/v4`, GitHub `/repos/...`, Forgejo `/api/v1`), not the web “Download ZIP” button - no account is required for public patch repos. # Wolf Games -1) Download the latest UberWolf.exe release from the following link: -https://github.com/Sinflower/UberWolf/releases -2) Drag Data.wolf onto UberWolf.exe. This will create a new folder called data.wolf~ -3) Rename the new data.wolf~ folder to Data -4) Delete the Data.wolf file -5) Delete previous_patch_sha.txt (this will exist if you ran GameUpdate.bat previously) -6) Run GameUpdate.bat +WOLF RPG installs from DLSite ship a master `Data.wolf` archive that takes priority over loose English patch files. `GameUpdate.bat` detects that and unpacks it automatically with the bundled `UberWolfCli.exe` (MIT, [Sinflower/UberWolf](https://github.com/Sinflower/UberWolf)), then renames `Data.wolf` to `Data.wolf.bak` so the patched loose `Data/` files load. + +Just run **`GameUpdate.bat`**. No manual UberWolf download is required. + +If unpack fails (rare Pro / protected builds), unpack once with [UberWolf](https://github.com/Sinflower/UberWolf/releases), ensure a loose `Data/` folder exists, rename or remove `Data.wolf`, delete `gameupdate/previous_patch_sha.txt`, and run `GameUpdate.bat` again. # Edit/Contribute TLDR 3 steps. diff --git a/gameupdate/UberWolfCli.LICENSE.txt b/gameupdate/UberWolfCli.LICENSE.txt new file mode 100644 index 0000000..ac7261b --- /dev/null +++ b/gameupdate/UberWolfCli.LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sinflower + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/gameupdate/UberWolfCli.exe b/gameupdate/UberWolfCli.exe new file mode 100644 index 0000000..25dcf73 Binary files /dev/null and b/gameupdate/UberWolfCli.exe differ diff --git a/gameupdate/gameupdate/patch-config.example.txt b/gameupdate/gameupdate/patch-config.example.txt new file mode 100644 index 0000000..379c363 --- /dev/null +++ b/gameupdate/gameupdate/patch-config.example.txt @@ -0,0 +1,28 @@ +# GameUpdate patch source (copy to patch-config.txt and edit). +# Lines starting with # are comments. Keys are case-insensitive. +# +# Tip: set forge / host / username / branch once in DazedMTLTool +# Config → Game Update Defaults. Step 1 "Copy gameupdate/" writes +# those into this file automatically (you still set repo= per game). + +# Where the patch repo lives: +# gitlab - GitLab / gitgud.io / self-hosted GitLab (default) +# github - GitHub.com or GitHub Enterprise +# forgejo - Forgejo (also accepts gitea) +forge=gitlab + +# Host only (no https://). Defaults: +# gitlab -> gitgud.io +# github -> github.com +# forgejo -> codeberg.org +# Examples: gitgud.io gitlab.com github.com codeberg.org git.example.com +host=gitgud.io + +# Owner / org / group that owns the repo (set in Config; rarely changes) +username=YOUR_ORG_OR_USER + +# Repository name (per game) +repo=YOUR_PATCH_REPO + +# Branch to track +branch=main diff --git a/gameupdate/gameupdate/patch.ps1 b/gameupdate/gameupdate/patch.ps1 index 90a42a2..278af0c 100644 --- a/gameupdate/gameupdate/patch.ps1 +++ b/gameupdate/gameupdate/patch.ps1 @@ -194,7 +194,13 @@ function Test-WrongWorkingDirectory { function Read-PatchConfig { param([string]$ConfigPath) - $cfg = @{ username = ''; repo = ''; branch = '' } + $cfg = @{ + forge = 'gitlab' + host = '' + username = '' + repo = '' + branch = '' + } if (-not (Test-Path -LiteralPath $ConfigPath)) { return $cfg } @@ -206,7 +212,12 @@ function Read-PatchConfig { $k = $line.Substring(0, $eq).Trim().ToLowerInvariant() $v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' } switch ($k) { + 'forge' { $cfg.forge = $v } + 'provider' { $cfg.forge = $v } + 'host' { $cfg.host = $v } 'username' { $cfg.username = $v } + 'owner' { $cfg.username = $v } + 'org' { $cfg.username = $v } 'repo' { $cfg.repo = $v } 'branch' { $cfg.branch = $v } } @@ -214,6 +225,115 @@ function Read-PatchConfig { return $cfg } +function Resolve-PatchForge { + param([Parameter(Mandatory = $true)][hashtable]$Cfg) + + $raw = ([string]$Cfg.forge).Trim().ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($raw)) { $raw = 'gitlab' } + + $kind = switch -Regex ($raw) { + '^(gitlab|gl|gitgud)$' { 'gitlab' } + '^(github|gh)$' { 'github' } + '^(forgejo|gitea|fj|codeberg)$' { 'forgejo' } + default { $null } + } + if (-not $kind) { + throw ("PATCH_ERR:CONFIG:Unknown forge='{0}'. Use gitlab, github, or forgejo." -f $Cfg.forge) + } + + $hostName = ([string]$Cfg.host).Trim() + $hostName = $hostName -replace '^https?://', '' + $hostName = $hostName.TrimEnd('/') + if ($hostName.Contains('/')) { + $hostName = $hostName.Split('/')[0] + } + if ([string]::IsNullOrWhiteSpace($hostName)) { + $hostName = switch ($kind) { + 'gitlab' { 'gitgud.io' } + 'github' { 'github.com' } + 'forgejo' { 'codeberg.org' } + } + } + + $webBase = "https://$hostName" + $apiBase = switch ($kind) { + 'gitlab' { "$webBase/api/v4" } + 'github' { + if ($hostName -eq 'github.com') { 'https://api.github.com' } + else { "$webBase/api/v3" } + } + 'forgejo' { "$webBase/api/v1" } + } + + return @{ + kind = $kind + host = $hostName + webBase = $webBase + apiBase = $apiBase + username = ([string]$Cfg.username).Trim() + repo = ([string]$Cfg.repo).Trim() + branch = ([string]$Cfg.branch).Trim() + } +} + +function New-PatchApiHeaders { + param([Parameter(Mandatory = $true)][hashtable]$Resolved) + $headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' } + if ($Resolved.kind -eq 'github') { + $headers['Accept'] = 'application/vnd.github+json' + } + return $headers +} + +function Get-PatchLatestSha { + param( + [Parameter(Mandatory = $true)][hashtable]$Resolved, + [Parameter(Mandatory = $true)][hashtable]$Headers + ) + $ownerEnc = [uri]::EscapeDataString($Resolved.username) + $repoEnc = [uri]::EscapeDataString($Resolved.repo) + $branchEnc = [uri]::EscapeDataString($Resolved.branch) + + switch ($Resolved.kind) { + 'gitlab' { + $id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo) + $uri = "$($Resolved.apiBase)/projects/$id/repository/branches/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id + } + 'github' { + $uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/commits/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).sha + } + 'forgejo' { + $uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/branches/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id + } + } +} + +function Get-PatchArchiveUrl { + param( + [Parameter(Mandatory = $true)][hashtable]$Resolved, + [Parameter(Mandatory = $true)][string]$Sha + ) + $ownerEnc = [uri]::EscapeDataString($Resolved.username) + $repoEnc = [uri]::EscapeDataString($Resolved.repo) + $shaEnc = [uri]::EscapeDataString($Sha) + + switch ($Resolved.kind) { + 'gitlab' { + $id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo) + return "$($Resolved.apiBase)/projects/$id/repository/archive.zip?sha=$shaEnc" + } + 'github' { + return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/zipball/$shaEnc" + } + 'forgejo' { + return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/archive/$shaEnc.zip" + } + } +} + function Invoke-SrpgUnpacker { param( [Parameter(Mandatory = $true)][string]$ExePath, @@ -230,6 +350,140 @@ function Invoke-SrpgUnpacker { } } +function Test-WolfLooseDataReady { + param([Parameter(Mandatory = $true)][string]$DataDir) + if (-not (Test-Path -LiteralPath $DataDir -PathType Container)) { + return $false + } + $markers = @( + (Join-Path $DataDir 'BasicData\CommonEvent.dat'), + (Join-Path $DataDir 'CommonEvent.dat') + ) + foreach ($marker in $markers) { + if (Test-Path -LiteralPath $marker -PathType Leaf) { + return $true + } + } + if (@(Get-ChildItem -LiteralPath $DataDir -Filter '*.mps' -File -ErrorAction SilentlyContinue).Count -gt 0) { + return $true + } + $basic = Join-Path $DataDir 'BasicData' + if (Test-Path -LiteralPath $basic -PathType Container) { + if (@(Get-ChildItem -LiteralPath $basic -Filter '*.dat' -File -ErrorAction SilentlyContinue).Count -gt 0) { + return $true + } + } + return $false +} + +function Find-UberWolfCli { + param([Parameter(Mandatory = $true)][string]$Root) + $candidates = @( + (Join-Path $Root 'UberWolfCli.exe'), + (Join-Path $Root 'gameupdate\UberWolfCli.exe') + ) + foreach ($path in $candidates) { + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } + } + return $null +} + +function Invoke-WolfPreSetup { + <# + WOLF installs often ship Data.wolf, which overrides loose patched Data/ files. + Unpack once with UberWolfCli (bundled in the patch repo), then retire Data.wolf + so GameUpdate can copy injected .mps/.dat on top. + #> + param([Parameter(Mandatory = $true)][string]$Root) + + $dataWolf = $null + foreach ($name in @('Data.wolf', 'data.wolf')) { + $candidate = Join-Path $Root $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $dataWolf = $candidate + break + } + } + if (-not $dataWolf) { + return + } + + $dataDir = Join-Path $Root 'Data' + if (-not (Test-Path -LiteralPath $dataDir -PathType Container)) { + $dataDirAlt = Join-Path $Root 'data' + if (Test-Path -LiteralPath $dataDirAlt -PathType Container) { + $dataDir = $dataDirAlt + } + } + + if (Test-WolfLooseDataReady -DataDir $dataDir) { + $bak = "$dataWolf.bak" + if (-not (Test-Path -LiteralPath $bak)) { + Write-Host '[Pre-Setup] Loose Data/ already present; retiring Data.wolf -> Data.wolf.bak' + Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force + } + else { + Write-Host '[Pre-Setup] Loose Data/ already present; removing leftover Data.wolf' + Remove-Item -LiteralPath $dataWolf -Force + } + return + } + + $cli = Find-UberWolfCli -Root $Root + if (-not $cli) { + Write-Host '[Pre-Setup] Data.wolf found but UberWolfCli.exe is missing.' + Write-Host ' Copy UberWolfCli.exe into the game root (Step 1 gameupdate/) and re-run.' + return + } + + Write-Host '[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)...' + $code = Invoke-SrpgUnpacker -ExePath $cli -WorkingDirectory $Root -Arguments @('-o', $dataWolf) + if ($code -ne 0) { + Write-Host ('[Pre-Setup] ERROR: UberWolfCli exited with code {0}.' -f $code) + return + } + + # UberWolf writes next to the archive as / (Data/). Also accept data.wolf~ leftovers. + $unpacked = Join-Path $Root 'Data' + if (-not (Test-Path -LiteralPath $unpacked -PathType Container)) { + $unpacked = Join-Path $Root 'data' + } + if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) { + foreach ($alt in @('data.wolf~', 'Data.wolf~', 'Data~', 'data~')) { + $altPath = Join-Path $Root $alt + if (Test-Path -LiteralPath $altPath -PathType Container) { + Write-Host ("[Pre-Setup] Promoting {0} -> Data\" -f $alt) + if (Test-Path -LiteralPath (Join-Path $Root 'Data')) { + Remove-FileOrDirectoryViaCmd -LiteralPath (Join-Path $Root 'Data') + } + Rename-Item -LiteralPath $altPath -NewName 'Data' -Force + $unpacked = Join-Path $Root 'Data' + break + } + } + } + + if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) { + Write-Host '[Pre-Setup] ERROR: Unpack finished but loose Data/ still looks empty.' + return + } + + $bak = "$dataWolf.bak" + if (Test-Path -LiteralPath $dataWolf -PathType Leaf) { + if (-not (Test-Path -LiteralPath $bak)) { + Write-Host '[Pre-Setup] Retiring Data.wolf -> Data.wolf.bak' + Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force + } + else { + Write-Host '[Pre-Setup] Removing Data.wolf so loose Data/ takes priority' + Remove-Item -LiteralPath $dataWolf -Force + } + } + Write-Host '[Pre-Setup] Wolf Data/ is ready for patching.' +} + function Invoke-SrpgPreSetup { param([string]$Root) $unpacker = Join-Path $Root 'SRPG_Unpacker.exe' @@ -512,9 +766,7 @@ 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 + [Parameter(Mandatory = $true)][hashtable]$Resolved ) if ($PSVersionTable.PSEdition -ne 'Core') { @@ -526,13 +778,11 @@ function Invoke-PatchDownloadExtract { Set-Location -LiteralPath $Root - $id = [uri]::EscapeDataString($Username + '/' + $Repo) - $branchEnc = [uri]::EscapeDataString($Branch) - $headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' } + $headers = New-PatchApiHeaders -Resolved $Resolved $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 + Get-PatchLatestSha -Resolved $Resolved -Headers $headers } if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' } $latestSha = ([string]$latestSha).Trim() @@ -551,8 +801,7 @@ function Invoke-PatchDownloadExtract { $zipPath = Join-Path $Root ('dazedmtl_patch_' + [Guid]::NewGuid().ToString('N') + '.zip') Write-Host ('Downloading patch... ' + $zipPath) $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" + $archiveUrl = Get-PatchArchiveUrl -Resolved $Resolved -Sha $latestSha $dlSw = [System.Diagnostics.Stopwatch]::StartNew() Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action { @@ -599,7 +848,21 @@ function Invoke-PatchDownloadExtract { 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 + $stagedRoot = $dirs[0].FullName + + # Stage UberWolfCli into the game root before unpack so first-time + # players can convert Data.wolf -> Data/ before English files land. + foreach ($cliName in @('UberWolfCli.exe', 'UberWolfCli.LICENSE.txt')) { + $stagedCli = Join-Path $stagedRoot $cliName + $destCli = Join-Path $Root $cliName + if ((Test-Path -LiteralPath $stagedCli -PathType Leaf) -and + -not (Test-Path -LiteralPath $destCli -PathType Leaf)) { + Copy-Item -LiteralPath $stagedCli -Destination $destCli -Force + } + } + Invoke-WolfPreSetup -Root $Root + + Copy-Item -Path (Join-Path $stagedRoot '*') -Destination $PWD.Path -Recurse -Force } finally { if (Test-Path -LiteralPath $zipPath) { @@ -717,8 +980,19 @@ try { exit 1 } - Write-Host ('Pulling patch from https://gitgud.io/{0}/{1} (branch: {2})' -f $cfg.username, $cfg.repo, $cfg.branch) + try { + $resolved = Resolve-PatchForge -Cfg $cfg + } + catch { + Write-Host $_.Exception.Message + Wait-ConsolePause + exit 1 + } + Write-Host ('Pulling patch from {0}/{1}/{2} ({3}, branch: {4})' -f ` + $resolved.webBase, $resolved.username, $resolved.repo, $resolved.kind, $resolved.branch) + + Invoke-WolfPreSetup -Root $GameRoot Invoke-SrpgPreSetup -Root $GameRoot $stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt' @@ -726,7 +1000,7 @@ try { $patchDlExit = 1 try { $patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile ` - -Username $cfg.username -Repo $cfg.repo -Branch $cfg.branch + -Resolved $resolved } catch { Write-Host '' @@ -753,6 +1027,9 @@ try { Write-Host 'Applying patch...' Invoke-SrpgPostApply -Root $GameRoot + # Safety: if Data/ is ready (from unpack or patch files) but Data.wolf + # remains, retire it so the archive does not override loose English files. + Invoke-WolfPreSetup -Root $GameRoot Write-Host 'Cleaning up...' Remove-PatchTempArtifacts -Root $GameRoot diff --git a/gameupdate/gameupdate/patch.sh b/gameupdate/gameupdate/patch.sh index ad1b86d..02991a1 100755 --- a/gameupdate/gameupdate/patch.sh +++ b/gameupdate/gameupdate/patch.sh @@ -6,7 +6,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(pwd)" CONFIG_FILE="$SCRIPT_DIR/patch-config.txt" STATE_FILE="$SCRIPT_DIR/previous_patch_sha.txt" -GITGUD_API="https://gitgud.io/api/v4" API_HEADERS=( -H "User-Agent: GameUpdate/1.0" ) check_dependency() { @@ -38,19 +37,113 @@ echo "Config file path: $CONFIG_FILE" # shellcheck disable=SC1090 . "$CONFIG_FILE" -if [ -z "${username:-}" ]; then +# Defaults for older configs that only set username/repo/branch. +forge="${forge:-${provider:-gitlab}}" +host="${host:-}" +username="${username:-${owner:-${org:-}}}" +repo="${repo:-}" +branch="${branch:-}" + +if [ -z "$username" ]; then echo "ERROR: 'username=' is missing in gameupdate/patch-config.txt" exit 1 fi -if [ -z "${repo:-}" ]; then +if [ -z "$repo" ]; then echo "ERROR: 'repo=' is missing in gameupdate/patch-config.txt" exit 1 fi -if [ -z "${branch:-}" ]; then +if [ -z "$branch" ]; then echo "ERROR: 'branch=' is missing in gameupdate/patch-config.txt" exit 1 fi +normalize_forge() { + local raw + raw="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$raw" in + ''|gitlab|gl|gitgud) echo gitlab ;; + github|gh) echo github ;; + forgejo|gitea|fj|codeberg) echo forgejo ;; + *) + echo "ERROR: Unknown forge='$1'. Use gitlab, github, or forgejo." >&2 + return 1 + ;; + esac +} + +FORGE_KIND="$(normalize_forge "$forge")" || exit 1 + +# Strip scheme / path from host if a user pasted a full URL. +HOST_NAME="$(printf '%s' "$host" | sed -E 's#^https?://##; s#/.*##; s#[[:space:]]##g')" +if [ -z "$HOST_NAME" ]; then + case "$FORGE_KIND" in + gitlab) HOST_NAME="gitgud.io" ;; + github) HOST_NAME="github.com" ;; + forgejo) HOST_NAME="codeberg.org" ;; + esac +fi + +WEB_BASE="https://${HOST_NAME}" +case "$FORGE_KIND" in + gitlab) API_BASE="${WEB_BASE}/api/v4" ;; + github) + if [ "$HOST_NAME" = "github.com" ]; then + API_BASE="https://api.github.com" + else + API_BASE="${WEB_BASE}/api/v3" + fi + ;; + forgejo) API_BASE="${WEB_BASE}/api/v1" ;; +esac + +API_HEADERS=( -H "User-Agent: GameUpdate/1.0" ) +if [ "$FORGE_KIND" = "github" ]; then + API_HEADERS+=( -H "Accept: application/vnd.github+json" ) +fi + +owner_enc=$(jq -nr --arg s "$username" '$s | @uri') +repo_enc=$(jq -nr --arg s "$repo" '$s | @uri') +branch_enc=$(jq -nr --arg b "$branch" '$b | @uri') +project_enc=$(jq -nr --arg ns "$username" --arg rp "$repo" '$ns + "/" + $rp | @uri') + +resolve_latest_sha() { + case "$FORGE_KIND" in + gitlab) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/projects/${project_enc}/repository/branches/${branch_enc}" \ + | jq -r '.commit.id' + ;; + github) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/repos/${owner_enc}/${repo_enc}/commits/${branch_enc}" \ + | jq -r '.sha' + ;; + forgejo) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/repos/${owner_enc}/${repo_enc}/branches/${branch_enc}" \ + | jq -r '.commit.id' + ;; + esac +} + +archive_url_for_sha() { + local sha_enc + sha_enc=$(jq -nr --arg s "$1" '$s | @uri') + case "$FORGE_KIND" in + gitlab) + echo "${API_BASE}/projects/${project_enc}/repository/archive.zip?sha=${sha_enc}" + ;; + github) + echo "${API_BASE}/repos/${owner_enc}/${repo_enc}/zipball/${sha_enc}" + ;; + forgejo) + echo "${API_BASE}/repos/${owner_enc}/${repo_enc}/archive/${sha_enc}.zip" + ;; + esac +} + +echo "Pulling patch from ${WEB_BASE}/${username}/${repo} (${FORGE_KIND}, branch: ${branch})" + RETRIES="${GAMEUPDATE_DL_ATTEMPTS:-2}" if ! [[ "$RETRIES" =~ ^[1-9][0-9]*$ ]]; then RETRIES=2 @@ -74,23 +167,139 @@ retry_cmd() { done } -project_enc=$(jq -nr --arg ns "$username" --arg rp "$repo" '$ns + "/" + $rp | @uri') -branch_enc=$(jq -nr --arg b "$branch" '$b | @uri') - # Get the latest hash echo "Getting latest commit SHA hash" -latest_patch_sha="$( - retry_cmd "Resolve latest patch SHA" \ - curl -fsSL "${API_HEADERS[@]}" \ - "${GITGUD_API}/projects/${project_enc}/repository/branches/${branch_enc}" \ - | jq -r '.commit.id' -)" +latest_patch_sha="" +attempt=1 +while true; do + if latest_patch_sha="$(resolve_latest_sha)"; then + break + fi + if [ "$attempt" -ge "$RETRIES" ]; then + echo "Resolve latest patch SHA failed after $attempt attempt(s)." + exit 1 + fi + echo "Resolve latest patch SHA failed ($attempt/$RETRIES), retrying..." + attempt=$((attempt + 1)) + sleep 2 +done latest_patch_sha="$(printf '%s' "$latest_patch_sha" | tr -d '[:space:]')" if [ -z "$latest_patch_sha" ] || [ "$latest_patch_sha" = "null" ]; then echo "PATCH_ERR:API:Latest commit SHA response was empty." exit 1 fi +# -------------------------------------------------------- +# PRE-SETUP: Wolf Data.wolf -> loose Data/ (before patch copy) +# -------------------------------------------------------- +wolf_loose_data_ready() { + local data_dir="$1" + [ -d "$data_dir" ] || return 1 + [ -f "$data_dir/BasicData/CommonEvent.dat" ] && return 0 + [ -f "$data_dir/CommonEvent.dat" ] && return 0 + if compgen -G "$data_dir/*.mps" > /dev/null 2>&1; then + return 0 + fi + if compgen -G "$data_dir/BasicData/*.dat" > /dev/null 2>&1; then + return 0 + fi + return 1 +} + +find_uberwolf_cli() { + if [ -f "$ROOT_DIR/UberWolfCli.exe" ]; then + echo "$ROOT_DIR/UberWolfCli.exe" + return 0 + fi + if [ -f "$ROOT_DIR/gameupdate/UberWolfCli.exe" ]; then + echo "$ROOT_DIR/gameupdate/UberWolfCli.exe" + return 0 + fi + return 1 +} + +retire_data_wolf() { + local data_wolf="$1" + local bak="${data_wolf}.bak" + if [ ! -f "$data_wolf" ]; then + return 0 + fi + if [ ! -e "$bak" ]; then + echo "[Pre-Setup] Retiring $(basename "$data_wolf") -> $(basename "$bak")" + mv -f "$data_wolf" "$bak" + else + echo "[Pre-Setup] Removing leftover $(basename "$data_wolf") so loose Data/ takes priority" + rm -f "$data_wolf" + fi +} + +invoke_wolf_pre_setup() { + local data_wolf="" + local name + for name in Data.wolf data.wolf; do + if [ -f "$ROOT_DIR/$name" ]; then + data_wolf="$ROOT_DIR/$name" + break + fi + done + [ -n "$data_wolf" ] || return 0 + + local data_dir="$ROOT_DIR/Data" + if [ ! -d "$data_dir" ] && [ -d "$ROOT_DIR/data" ]; then + data_dir="$ROOT_DIR/data" + fi + + if wolf_loose_data_ready "$data_dir"; then + retire_data_wolf "$data_wolf" + return 0 + fi + + local cli + cli="$(find_uberwolf_cli || true)" + if [ -z "$cli" ]; then + echo "[Pre-Setup] Data.wolf found but UberWolfCli.exe is missing." + echo " Copy UberWolfCli.exe into the game root (Step 1 gameupdate/) and re-run." + return 0 + fi + + echo "[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)..." + # Native .exe works under Windows / Git Bash; wine covers Linux hosts. + if ( cd "$ROOT_DIR" && "$cli" -o "$data_wolf" ); then + : + elif command -v wine > /dev/null 2>&1 && ( cd "$ROOT_DIR" && wine "$cli" -o "$data_wolf" ); then + : + else + echo "[Pre-Setup] ERROR: UberWolfCli unpack failed." + return 0 + fi + + local unpacked="$ROOT_DIR/Data" + if ! wolf_loose_data_ready "$unpacked"; then + unpacked="$ROOT_DIR/data" + fi + if ! wolf_loose_data_ready "$unpacked"; then + local alt + for alt in data.wolf~ Data.wolf~ Data~ data~; do + if [ -d "$ROOT_DIR/$alt" ]; then + echo "[Pre-Setup] Promoting $alt -> Data/" + rm -rf "$ROOT_DIR/Data" + mv -f "$ROOT_DIR/$alt" "$ROOT_DIR/Data" + unpacked="$ROOT_DIR/Data" + break + fi + done + fi + + if wolf_loose_data_ready "$unpacked"; then + retire_data_wolf "$data_wolf" + echo "[Pre-Setup] Wolf Data/ is ready for patching." + else + echo "[Pre-Setup] ERROR: Unpack finished but loose Data/ still looks empty." + fi +} + +invoke_wolf_pre_setup + # -------------------------------------------------------- # PRE-SETUP: Ensure SRPG data and patch structure exists # Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates @@ -132,10 +341,10 @@ fi download_extract() { echo "Downloading latest patch..." - archive_sha_enc=$(jq -nr --arg s "$latest_patch_sha" '$s | @uri') + archive_url="$(archive_url_for_sha "$latest_patch_sha")" if ! retry_cmd "Download archive with curl" \ curl -fSL --progress-bar "${API_HEADERS[@]}" \ - "${GITGUD_API}/projects/${project_enc}/repository/archive.zip?sha=${archive_sha_enc}" \ + "$archive_url" \ -o "$ROOT_DIR/repo.zip"; then echo "Download failed!" rm -f "$ROOT_DIR/repo.zip" @@ -166,6 +375,15 @@ download_extract() { return 1 fi + # Stage UberWolfCli before unpack so Data.wolf -> Data/ happens before + # English patch files are copied on top. + for cli_name in UberWolfCli.exe UberWolfCli.LICENSE.txt; do + if [ -f "$inner/$cli_name" ] && [ ! -f "$ROOT_DIR/$cli_name" ]; then + cp -f "$inner/$cli_name" "$ROOT_DIR/$cli_name" + fi + done + invoke_wolf_pre_setup + echo "Applying patch..." if ! cp -r "$inner"/* "$ROOT_DIR/"; then echo "Patch application failed!" @@ -205,6 +423,9 @@ download_extract() { fi fi + # Retire Data.wolf if loose Data/ is ready after the patch copy. + invoke_wolf_pre_setup + echo "$latest_patch_sha" > "$STATE_FILE" } diff --git a/gui/config_tab.py b/gui/config_tab.py index f3f1182..a4bbbdf 100644 --- a/gui/config_tab.py +++ b/gui/config_tab.py @@ -585,6 +585,62 @@ class ConfigTab(QWidget): ui_form.addRow(font_scale_label, self.font_scale_spin) right_column.addLayout(ui_form) + + # Game Update defaults (written into gameupdate/patch-config.txt on copy) + right_column.addWidget(create_section_header("📦 Game Update Defaults")) + gu_form = QFormLayout() + gu_form.setSpacing(6) + gu_form.setContentsMargins(0, 0, 0, 12) + gu_form.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint) + gu_form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + + def _gu_label(text: str) -> QLabel: + lbl = QLabel(text) + lbl.setFixedWidth(150) + lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + return lbl + + self.gu_forge_combo = QComboBox() + self.gu_forge_combo.addItem("GitLab / gitgud", "gitlab") + self.gu_forge_combo.addItem("GitHub", "github") + self.gu_forge_combo.addItem("Forgejo / Gitea", "forgejo") + self.gu_forge_combo.setFixedWidth(220) + self.gu_forge_combo.setToolTip( + "Where your patch repos live. Used when Step 1 writes patch-config.txt." + ) + self.gu_forge_combo.currentIndexChanged.connect(self._on_gu_forge_changed) + gu_form.addRow(_gu_label("Forge:"), self.gu_forge_combo) + + self.gu_host_edit = QLineEdit() + self.gu_host_edit.setFixedWidth(220) + self.gu_host_edit.setPlaceholderText("gitgud.io") + self.gu_host_edit.setToolTip( + "Hostname only (no https://). Leave blank to use the forge default." + ) + gu_form.addRow(_gu_label("Host:"), self.gu_host_edit) + + self.gu_username_edit = QLineEdit() + self.gu_username_edit.setFixedWidth(220) + self.gu_username_edit.setPlaceholderText("your-org-or-user") + self.gu_username_edit.setToolTip( + "Owner / org / group for all patch repos. Set once; rarely changes." + ) + gu_form.addRow(_gu_label("Org / username:"), self.gu_username_edit) + + self.gu_branch_edit = QLineEdit() + self.gu_branch_edit.setFixedWidth(220) + self.gu_branch_edit.setPlaceholderText("main") + self.gu_branch_edit.setToolTip("Default branch to track (usually main or master).") + gu_form.addRow(_gu_label("Branch:"), self.gu_branch_edit) + + gu_hint = QLabel( + "Copied into each game's gameupdate/patch-config.txt when you run " + "Copy gameupdate/ (repo= is still per-game)." + ) + gu_hint.setWordWrap(True) + gu_hint.setStyleSheet("color:#7a7a7a;font-size:11px;") + right_column.addLayout(gu_form) + right_column.addWidget(gu_hint) right_column.addStretch() # Add columns to layout @@ -725,7 +781,39 @@ class ConfigTab(QWidget): # UI settings self.font_scale_spin.setValue(float(_get("font_scale", "1.0"))) - + + # Game Update defaults + from util.gameupdate_config import ( + DEFAULT_BRANCH, + DEFAULT_FORGE, + default_host_for_forge, + normalize_forge, + ) + + forge = normalize_forge(_get("gameUpdateForge", DEFAULT_FORGE)) + forge_idx = self.gu_forge_combo.findData(forge) + self.gu_forge_combo.setCurrentIndex(forge_idx if forge_idx >= 0 else 0) + host = _get("gameUpdateHost", "") or default_host_for_forge(forge) + self.gu_host_edit.setText(host) + self.gu_username_edit.setText(_get("gameUpdateUsername", "")) + self.gu_branch_edit.setText(_get("gameUpdateBranch", DEFAULT_BRANCH) or DEFAULT_BRANCH) + + def _on_gu_forge_changed(self, _index: int = 0): + """Fill host with the forge default when the user switches forge.""" + from util.gameupdate_config import default_host_for_forge + + forge = self.gu_forge_combo.currentData() or "gitlab" + current = self.gu_host_edit.text().strip() + defaults = { + "gitgud.io", + "gitlab.com", + "github.com", + "codeberg.org", + "", + } + if current.lower() in defaults: + self.gu_host_edit.setText(default_host_for_forge(str(forge))) + def connect_auto_save(self): """Connect all widgets to auto-save on change.""" # Text fields - use editingFinished to avoid saving on every keystroke @@ -750,6 +838,10 @@ class ConfigTab(QWidget): self.input_cost_spin.editingFinished.connect(self.auto_save) self.output_cost_spin.editingFinished.connect(self.auto_save) self.font_scale_spin.editingFinished.connect(self.auto_save) + self.gu_forge_combo.currentIndexChanged.connect(self.auto_save) + self.gu_host_edit.editingFinished.connect(self.auto_save) + self.gu_username_edit.editingFinished.connect(self.auto_save) + self.gu_branch_edit.editingFinished.connect(self.auto_save) def disconnect_auto_save(self): """Disconnect all widgets from auto-save.""" @@ -770,6 +862,10 @@ class ConfigTab(QWidget): self.input_cost_spin.editingFinished.disconnect(self.auto_save) self.output_cost_spin.editingFinished.disconnect(self.auto_save) self.font_scale_spin.editingFinished.disconnect(self.auto_save) + self.gu_forge_combo.currentIndexChanged.disconnect(self.auto_save) + self.gu_host_edit.editingFinished.disconnect(self.auto_save) + self.gu_username_edit.editingFinished.disconnect(self.auto_save) + self.gu_branch_edit.editingFinished.disconnect(self.auto_save) except (TypeError, RuntimeError): pass @@ -808,6 +904,10 @@ class ConfigTab(QWidget): "input_cost": str(self.input_cost_spin.value()), "output_cost": str(self.output_cost_spin.value()), "font_scale": str(self.font_scale_spin.value()), + "gameUpdateForge": str(self.gu_forge_combo.currentData() or "gitlab"), + "gameUpdateHost": self.gu_host_edit.text().strip(), + "gameUpdateUsername": self.gu_username_edit.text().strip(), + "gameUpdateBranch": self.gu_branch_edit.text().strip() or "main", } # Save to .env file and update os.environ so subprocesses inherit new values @@ -867,6 +967,15 @@ class ConfigTab(QWidget): # UI settings self.font_scale_spin.setValue(1.0) + + # Game Update defaults + from util.gameupdate_config import DEFAULT_BRANCH, DEFAULT_FORGE, DEFAULT_HOST + + forge_idx = self.gu_forge_combo.findData(DEFAULT_FORGE) + self.gu_forge_combo.setCurrentIndex(forge_idx if forge_idx >= 0 else 0) + self.gu_host_edit.setText(DEFAULT_HOST) + self.gu_username_edit.clear() + self.gu_branch_edit.setText(DEFAULT_BRANCH) self.connect_auto_save() @@ -902,6 +1011,10 @@ class ConfigTab(QWidget): "input_cost": self.input_cost_spin.value(), "output_cost": self.output_cost_spin.value(), "font_scale": self.font_scale_spin.value(), + "gameUpdateForge": str(self.gu_forge_combo.currentData() or "gitlab"), + "gameUpdateHost": self.gu_host_edit.text().strip(), + "gameUpdateUsername": self.gu_username_edit.text().strip(), + "gameUpdateBranch": self.gu_branch_edit.text().strip() or "main", } def validate(self): diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index b97079b..72ff3ac 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -1030,7 +1030,9 @@ class WolfWorkflowTab(QWidget): tc_inner.setSpacing(4) tc_desc = QLabel( "Copies everything from the gameupdate/ folder into the game's root folder, " - "overwriting existing files (updater scripts, patch scripts, .gitignore)." + "overwriting existing files (updater scripts, patch scripts, .gitignore, " + "and UberWolfCli.exe). Writes patch-config.txt from Config → Game Update " + "defaults (set your org once there; edit repo= per game)." ) tc_desc.setWordWrap(True) tc_desc.setStyleSheet("color:#9d9d9d;font-size:13px;") @@ -1456,6 +1458,16 @@ class WolfWorkflowTab(QWidget): self._run_task(task) + def _write_gameupdate_patch_config(self, game_root: str): + """Write gameupdate/patch-config.txt from Config → Game Update defaults.""" + from util.gameupdate_config import write_patch_config + + ok, msg = write_patch_config(game_root) + if ok: + self._log(f"📝 Wrote patch-config.txt from Config defaults → {msg}") + else: + self._log(f"ℹ patch-config.txt: {msg}") + def _run_gameupdate(self): src = self.pp_gameupdate_edit.text().strip() dst = self.folder_edit.text().strip() @@ -1478,6 +1490,9 @@ class WolfWorkflowTab(QWidget): self._log(f"✅ gameupdate: copied {count} file(s).") for e in errors: self._log(f" ⚠ {e}") + dst = self.folder_edit.text().strip() + if dst and not errors: + self._write_gameupdate_patch_config(dst) def _run_all_preprocess(self): if not self._game_root: @@ -1523,6 +1538,13 @@ class WolfWorkflowTab(QWidget): except Exception as exc: errors.append(f"{rel}: {exc}") log(f" Copied {copied} gameupdate file(s).") + from util.gameupdate_config import write_patch_config + + ok, msg = write_patch_config(dst) + if ok: + log(f" Wrote patch-config.txt from Config defaults → {msg}") + else: + log(f" patch-config.txt: {msg}") if errors: return False, "Pre-process finished with errors (see log)." return True, "Pre-process: dazedformat and gameupdate copy finished." diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index caa5d57..ba51f6a 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -1716,7 +1716,9 @@ class WorkflowTab(QWidget): tc_inner.setSpacing(4) tc_desc = QLabel( "Copies everything from the gameupdate/ folder " - "into the game\'s root folder, overwriting existing files." + "into the game\'s root folder, overwriting existing files. " + "Writes patch-config.txt from Config → Game Update defaults " + "(set your org once there; edit repo= per game)." ) tc_desc.setTextFormat(Qt.RichText) tc_desc.setWordWrap(True) @@ -4612,6 +4614,16 @@ class WorkflowTab(QWidget): self._worker = w w.start() + def _write_gameupdate_patch_config(self, game_root: str): + """Write gameupdate/patch-config.txt from Config → Game Update defaults.""" + from util.gameupdate_config import write_patch_config + + ok, msg = write_patch_config(game_root) + if ok: + self._log(f"📝 Wrote patch-config.txt from Config defaults → {msg}") + else: + self._log(f"ℹ patch-config.txt: {msg}") + def _run_gameupdate(self): src = self.pp_gameupdate_edit.text().strip() dst = self.folder_edit.text().strip() @@ -4634,6 +4646,9 @@ class WorkflowTab(QWidget): self._log(f"✅ gameupdate: copied {count} file(s).") for e in errors: self._log(f" ⚠ {e}") + dst = self.folder_edit.text().strip() + if dst and not errors: + self._write_gameupdate_patch_config(dst) def _run_all_preprocess(self): """Launch all three pre-process tasks in sequence, chaining via signals.""" @@ -4688,6 +4703,8 @@ class WorkflowTab(QWidget): self._log(f"✅ gameupdate: copied {count} file(s).") for e in errors: self._log(f" ⚠ {e}") + if not errors and game_root_dst: + self._write_gameupdate_patch_config(game_root_dst) run_next(rest) worker.done.connect(on_copy_done) else: diff --git a/util/gameupdate_config.py b/util/gameupdate_config.py new file mode 100644 index 0000000..7fa0bef --- /dev/null +++ b/util/gameupdate_config.py @@ -0,0 +1,132 @@ +"""GameUpdate patch-config defaults from the tool's .env / Config tab. + +Translators set forge/host/org once in Config. When Step 1 copies +``gameupdate/`` into a game root, we write ``gameupdate/patch-config.txt`` +from those defaults (repo stays per-game). +""" + +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import dotenv_values + +# .env keys (Config tab) +ENV_FORGE = "gameUpdateForge" +ENV_HOST = "gameUpdateHost" +ENV_USERNAME = "gameUpdateUsername" +ENV_BRANCH = "gameUpdateBranch" + +DEFAULT_FORGE = "gitlab" +DEFAULT_HOST = "gitgud.io" +DEFAULT_BRANCH = "main" + +_FORGE_HOST_DEFAULTS = { + "gitlab": "gitgud.io", + "github": "github.com", + "forgejo": "codeberg.org", +} + + +def normalize_forge(raw: str | None) -> str: + value = (raw or "").strip().lower() + if value in ("", "gitlab", "gl", "gitgud"): + return "gitlab" + if value in ("github", "gh"): + return "github" + if value in ("forgejo", "gitea", "fj", "codeberg"): + return "forgejo" + return "gitlab" + + +def default_host_for_forge(forge: str) -> str: + return _FORGE_HOST_DEFAULTS.get(normalize_forge(forge), DEFAULT_HOST) + + +def load_gameupdate_defaults(env_path: str | Path | None = None) -> dict[str, str]: + """Return forge/host/username/branch from .env (with process env fallback).""" + path = Path(env_path) if env_path is not None else Path(".env") + file_vals = dotenv_values(path) if path.is_file() else {} + + def _get(key: str, default: str = "") -> str: + raw = file_vals.get(key) + if raw is None or str(raw).strip() == "": + raw = os.getenv(key, default) + return str(raw or default).strip() + + forge = normalize_forge(_get(ENV_FORGE, DEFAULT_FORGE)) + host = _get(ENV_HOST, "") or default_host_for_forge(forge) + host = host.replace("https://", "").replace("http://", "").split("/")[0].strip() + return { + "forge": forge, + "host": host or default_host_for_forge(forge), + "username": _get(ENV_USERNAME, ""), + "branch": _get(ENV_BRANCH, DEFAULT_BRANCH) or DEFAULT_BRANCH, + } + + +def format_patch_config( + *, + forge: str, + host: str, + username: str, + repo: str = "YOUR_PATCH_REPO", + branch: str = DEFAULT_BRANCH, +) -> str: + """Render a patch-config.txt body.""" + forge = normalize_forge(forge) + host = (host or default_host_for_forge(forge)).strip() + username = (username or "").strip() or "YOUR_ORG_OR_USER" + repo = (repo or "").strip() or "YOUR_PATCH_REPO" + branch = (branch or "").strip() or DEFAULT_BRANCH + + lines = [ + "# Generated by DazedMTLTool from Config → Game Update defaults.", + "# Edit repo= per game. forge/host/username usually stay the same.", + f"forge={forge}", + f"host={host}", + f"username={username}", + f"repo={repo}", + f"branch={branch}", + "", + ] + return "\n".join(lines) + + +def write_patch_config( + game_root: str | Path, + *, + repo: str | None = None, + env_path: str | Path | None = None, + overwrite: bool = True, +) -> tuple[bool, str]: + """Write ``/gameupdate/patch-config.txt`` from Config defaults. + + Returns ``(ok, message)``. + """ + root = Path(game_root) + dest_dir = root / "gameupdate" + dest = dest_dir / "patch-config.txt" + if dest.is_file() and not overwrite: + return False, f"left existing {dest}" + + defaults = load_gameupdate_defaults(env_path) + if not defaults["username"]: + return False, ( + "skipped patch-config.txt (set Game Update org/username in Config first)" + ) + + body = format_patch_config( + forge=defaults["forge"], + host=defaults["host"], + username=defaults["username"], + repo=repo or "YOUR_PATCH_REPO", + branch=defaults["branch"], + ) + try: + dest_dir.mkdir(parents=True, exist_ok=True) + dest.write_text(body, encoding="utf-8") + except Exception as exc: + return False, f"could not write {dest}: {exc}" + return True, str(dest)