Wolf Game Update Patcher

This commit is contained in:
DazedAnon 2026-07-09 17:22:15 -05:00
parent 69f7ca7316
commit 8f5e62ac75
12 changed files with 921 additions and 42 deletions

View file

@ -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`. |

View file

@ -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

View file

@ -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 GitLabs 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.

View file

@ -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.

BIN
gameupdate/UberWolfCli.exe Normal file

Binary file not shown.

View file

@ -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

View file

@ -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 <stem>/ (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

View file

@ -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"
}

View file

@ -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
@ -726,6 +782,38 @@ 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
@ -868,6 +968,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()
def reset_to_defaults_with_save(self):
@ -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):

View file

@ -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."

View file

@ -1716,7 +1716,9 @@ class WorkflowTab(QWidget):
tc_inner.setSpacing(4)
tc_desc = QLabel(
"Copies everything from the <code>gameupdate/</code> folder "
"into the game\'s root folder, overwriting existing files."
"into the game\'s root folder, overwriting existing files. "
"Writes <code>patch-config.txt</code> from Config → Game Update defaults "
"(set your org once there; edit <code>repo=</code> 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:

132
util/gameupdate_config.py Normal file
View file

@ -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 ``<game_root>/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)