Wolf Game Update Patcher
This commit is contained in:
parent
69f7ca7316
commit
8f5e62ac75
12 changed files with 921 additions and 42 deletions
|
|
@ -325,7 +325,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
||||||
| Step | Action |
|
| 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. |
|
| **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. |
|
| **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)**. |
|
| **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`. |
|
| **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`. |
|
||||||
|
|
|
||||||
3
gameupdate/.gitignore
vendored
3
gameupdate/.gitignore
vendored
|
|
@ -21,6 +21,7 @@
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!README.md
|
!README.md
|
||||||
!patch-config.txt
|
!patch-config.txt
|
||||||
|
!patch-config.example.txt
|
||||||
!GameUpdate*
|
!GameUpdate*
|
||||||
!patch*
|
!patch*
|
||||||
!Game.dat
|
!Game.dat
|
||||||
|
|
@ -29,6 +30,8 @@
|
||||||
!package.nw
|
!package.nw
|
||||||
!SRPG_Unpacker.exe
|
!SRPG_Unpacker.exe
|
||||||
!(SRPG_Unpacker Patcher).bat
|
!(SRPG_Unpacker Patcher).bat
|
||||||
|
!UberWolfCli.exe
|
||||||
|
!UberWolfCli.LICENSE.txt
|
||||||
!Data/BasicData/CommonEvent.dat
|
!Data/BasicData/CommonEvent.dat
|
||||||
!Data/BasicData/DataBase.dat
|
!Data/BasicData/DataBase.dat
|
||||||
!Data/BasicData/CDataBase.dat
|
!Data/BasicData/CDataBase.dat
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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
|
# Troubleshooting
|
||||||
**GAMEUPDATE.bat doesn't update and closes immediately**
|
**GAMEUPDATE.bat doesn't update and closes immediately**
|
||||||
1. Make sure your path doesn't contain any Japanese characters or lots of whitespace.
|
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
|
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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
# Wolf Games
|
# Wolf Games
|
||||||
1) Download the latest UberWolf.exe release from the following link:
|
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.
|
||||||
https://github.com/Sinflower/UberWolf/releases
|
|
||||||
2) Drag Data.wolf onto UberWolf.exe. This will create a new folder called data.wolf~
|
Just run **`GameUpdate.bat`**. No manual UberWolf download is required.
|
||||||
3) Rename the new data.wolf~ folder to Data
|
|
||||||
4) Delete the Data.wolf file
|
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.
|
||||||
5) Delete previous_patch_sha.txt (this will exist if you ran GameUpdate.bat previously)
|
|
||||||
6) Run GameUpdate.bat
|
|
||||||
|
|
||||||
# Edit/Contribute
|
# Edit/Contribute
|
||||||
TLDR 3 steps.
|
TLDR 3 steps.
|
||||||
|
|
|
||||||
21
gameupdate/UberWolfCli.LICENSE.txt
Normal file
21
gameupdate/UberWolfCli.LICENSE.txt
Normal 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
BIN
gameupdate/UberWolfCli.exe
Normal file
Binary file not shown.
28
gameupdate/gameupdate/patch-config.example.txt
Normal file
28
gameupdate/gameupdate/patch-config.example.txt
Normal 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
|
||||||
|
|
@ -194,7 +194,13 @@ function Test-WrongWorkingDirectory {
|
||||||
|
|
||||||
function Read-PatchConfig {
|
function Read-PatchConfig {
|
||||||
param([string]$ConfigPath)
|
param([string]$ConfigPath)
|
||||||
$cfg = @{ username = ''; repo = ''; branch = '' }
|
$cfg = @{
|
||||||
|
forge = 'gitlab'
|
||||||
|
host = ''
|
||||||
|
username = ''
|
||||||
|
repo = ''
|
||||||
|
branch = ''
|
||||||
|
}
|
||||||
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||||
return $cfg
|
return $cfg
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +212,12 @@ function Read-PatchConfig {
|
||||||
$k = $line.Substring(0, $eq).Trim().ToLowerInvariant()
|
$k = $line.Substring(0, $eq).Trim().ToLowerInvariant()
|
||||||
$v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' }
|
$v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' }
|
||||||
switch ($k) {
|
switch ($k) {
|
||||||
|
'forge' { $cfg.forge = $v }
|
||||||
|
'provider' { $cfg.forge = $v }
|
||||||
|
'host' { $cfg.host = $v }
|
||||||
'username' { $cfg.username = $v }
|
'username' { $cfg.username = $v }
|
||||||
|
'owner' { $cfg.username = $v }
|
||||||
|
'org' { $cfg.username = $v }
|
||||||
'repo' { $cfg.repo = $v }
|
'repo' { $cfg.repo = $v }
|
||||||
'branch' { $cfg.branch = $v }
|
'branch' { $cfg.branch = $v }
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +225,115 @@ function Read-PatchConfig {
|
||||||
return $cfg
|
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 {
|
function Invoke-SrpgUnpacker {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$ExePath,
|
[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 {
|
function Invoke-SrpgPreSetup {
|
||||||
param([string]$Root)
|
param([string]$Root)
|
||||||
$unpacker = Join-Path $Root 'SRPG_Unpacker.exe'
|
$unpacker = Join-Path $Root 'SRPG_Unpacker.exe'
|
||||||
|
|
@ -512,9 +766,7 @@ function Invoke-PatchDownloadExtract {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Root,
|
[Parameter(Mandatory = $true)][string]$Root,
|
||||||
[Parameter(Mandatory = $true)][string]$StateFilePath,
|
[Parameter(Mandatory = $true)][string]$StateFilePath,
|
||||||
[Parameter(Mandatory = $true)][string]$Username,
|
[Parameter(Mandatory = $true)][hashtable]$Resolved
|
||||||
[Parameter(Mandatory = $true)][string]$Repo,
|
|
||||||
[Parameter(Mandatory = $true)][string]$Branch
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($PSVersionTable.PSEdition -ne 'Core') {
|
if ($PSVersionTable.PSEdition -ne 'Core') {
|
||||||
|
|
@ -526,13 +778,11 @@ function Invoke-PatchDownloadExtract {
|
||||||
|
|
||||||
Set-Location -LiteralPath $Root
|
Set-Location -LiteralPath $Root
|
||||||
|
|
||||||
$id = [uri]::EscapeDataString($Username + '/' + $Repo)
|
$headers = New-PatchApiHeaders -Resolved $Resolved
|
||||||
$branchEnc = [uri]::EscapeDataString($Branch)
|
|
||||||
$headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' }
|
|
||||||
$dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2
|
$dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2
|
||||||
|
|
||||||
$latestSha = Invoke-WithRetry -Name 'Resolve latest patch SHA' -Attempts $dlAttempts -Action {
|
$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.' }
|
if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' }
|
||||||
$latestSha = ([string]$latestSha).Trim()
|
$latestSha = ([string]$latestSha).Trim()
|
||||||
|
|
@ -551,8 +801,7 @@ function Invoke-PatchDownloadExtract {
|
||||||
$zipPath = Join-Path $Root ('dazedmtl_patch_' + [Guid]::NewGuid().ToString('N') + '.zip')
|
$zipPath = Join-Path $Root ('dazedmtl_patch_' + [Guid]::NewGuid().ToString('N') + '.zip')
|
||||||
Write-Host ('Downloading patch... ' + $zipPath)
|
Write-Host ('Downloading patch... ' + $zipPath)
|
||||||
$stage = Join-Path ([IO.Path]::GetTempPath()) ('gu_' + [Guid]::NewGuid().ToString('N'))
|
$stage = Join-Path ([IO.Path]::GetTempPath()) ('gu_' + [Guid]::NewGuid().ToString('N'))
|
||||||
$archiveSha = [uri]::EscapeDataString($latestSha)
|
$archiveUrl = Get-PatchArchiveUrl -Resolved $Resolved -Sha $latestSha
|
||||||
$archiveUrl = "https://gitgud.io/api/v4/projects/$id/repository/archive.zip?sha=$archiveSha"
|
|
||||||
|
|
||||||
$dlSw = [System.Diagnostics.Stopwatch]::StartNew()
|
$dlSw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action {
|
Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action {
|
||||||
|
|
@ -599,7 +848,21 @@ function Invoke-PatchDownloadExtract {
|
||||||
if ($dirs.Count -ne 1) {
|
if ($dirs.Count -ne 1) {
|
||||||
throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count)
|
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 {
|
finally {
|
||||||
if (Test-Path -LiteralPath $zipPath) {
|
if (Test-Path -LiteralPath $zipPath) {
|
||||||
|
|
@ -717,8 +980,19 @@ try {
|
||||||
exit 1
|
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
|
Invoke-SrpgPreSetup -Root $GameRoot
|
||||||
|
|
||||||
$stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt'
|
$stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt'
|
||||||
|
|
@ -726,7 +1000,7 @@ try {
|
||||||
$patchDlExit = 1
|
$patchDlExit = 1
|
||||||
try {
|
try {
|
||||||
$patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile `
|
$patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile `
|
||||||
-Username $cfg.username -Repo $cfg.repo -Branch $cfg.branch
|
-Resolved $resolved
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|
@ -753,6 +1027,9 @@ try {
|
||||||
Write-Host 'Applying patch...'
|
Write-Host 'Applying patch...'
|
||||||
|
|
||||||
Invoke-SrpgPostApply -Root $GameRoot
|
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...'
|
Write-Host 'Cleaning up...'
|
||||||
Remove-PatchTempArtifacts -Root $GameRoot
|
Remove-PatchTempArtifacts -Root $GameRoot
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT_DIR="$(pwd)"
|
ROOT_DIR="$(pwd)"
|
||||||
CONFIG_FILE="$SCRIPT_DIR/patch-config.txt"
|
CONFIG_FILE="$SCRIPT_DIR/patch-config.txt"
|
||||||
STATE_FILE="$SCRIPT_DIR/previous_patch_sha.txt"
|
STATE_FILE="$SCRIPT_DIR/previous_patch_sha.txt"
|
||||||
GITGUD_API="https://gitgud.io/api/v4"
|
|
||||||
API_HEADERS=( -H "User-Agent: GameUpdate/1.0" )
|
API_HEADERS=( -H "User-Agent: GameUpdate/1.0" )
|
||||||
|
|
||||||
check_dependency() {
|
check_dependency() {
|
||||||
|
|
@ -38,19 +37,113 @@ echo "Config file path: $CONFIG_FILE"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$CONFIG_FILE"
|
. "$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"
|
echo "ERROR: 'username=' is missing in gameupdate/patch-config.txt"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -z "${repo:-}" ]; then
|
if [ -z "$repo" ]; then
|
||||||
echo "ERROR: 'repo=' is missing in gameupdate/patch-config.txt"
|
echo "ERROR: 'repo=' is missing in gameupdate/patch-config.txt"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -z "${branch:-}" ]; then
|
if [ -z "$branch" ]; then
|
||||||
echo "ERROR: 'branch=' is missing in gameupdate/patch-config.txt"
|
echo "ERROR: 'branch=' is missing in gameupdate/patch-config.txt"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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}"
|
RETRIES="${GAMEUPDATE_DL_ATTEMPTS:-2}"
|
||||||
if ! [[ "$RETRIES" =~ ^[1-9][0-9]*$ ]]; then
|
if ! [[ "$RETRIES" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
RETRIES=2
|
RETRIES=2
|
||||||
|
|
@ -74,23 +167,139 @@ retry_cmd() {
|
||||||
done
|
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
|
# Get the latest hash
|
||||||
echo "Getting latest commit SHA hash"
|
echo "Getting latest commit SHA hash"
|
||||||
latest_patch_sha="$(
|
latest_patch_sha=""
|
||||||
retry_cmd "Resolve latest patch SHA" \
|
attempt=1
|
||||||
curl -fsSL "${API_HEADERS[@]}" \
|
while true; do
|
||||||
"${GITGUD_API}/projects/${project_enc}/repository/branches/${branch_enc}" \
|
if latest_patch_sha="$(resolve_latest_sha)"; then
|
||||||
| jq -r '.commit.id'
|
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:]')"
|
latest_patch_sha="$(printf '%s' "$latest_patch_sha" | tr -d '[:space:]')"
|
||||||
if [ -z "$latest_patch_sha" ] || [ "$latest_patch_sha" = "null" ]; then
|
if [ -z "$latest_patch_sha" ] || [ "$latest_patch_sha" = "null" ]; then
|
||||||
echo "PATCH_ERR:API:Latest commit SHA response was empty."
|
echo "PATCH_ERR:API:Latest commit SHA response was empty."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# PRE-SETUP: Ensure SRPG data and patch structure exists
|
||||||
# Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates
|
# Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates
|
||||||
|
|
@ -132,10 +341,10 @@ fi
|
||||||
|
|
||||||
download_extract() {
|
download_extract() {
|
||||||
echo "Downloading latest patch..."
|
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" \
|
if ! retry_cmd "Download archive with curl" \
|
||||||
curl -fSL --progress-bar "${API_HEADERS[@]}" \
|
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
|
-o "$ROOT_DIR/repo.zip"; then
|
||||||
echo "Download failed!"
|
echo "Download failed!"
|
||||||
rm -f "$ROOT_DIR/repo.zip"
|
rm -f "$ROOT_DIR/repo.zip"
|
||||||
|
|
@ -166,6 +375,15 @@ download_extract() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
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..."
|
echo "Applying patch..."
|
||||||
if ! cp -r "$inner"/* "$ROOT_DIR/"; then
|
if ! cp -r "$inner"/* "$ROOT_DIR/"; then
|
||||||
echo "Patch application failed!"
|
echo "Patch application failed!"
|
||||||
|
|
@ -205,6 +423,9 @@ download_extract() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Retire Data.wolf if loose Data/ is ready after the patch copy.
|
||||||
|
invoke_wolf_pre_setup
|
||||||
|
|
||||||
echo "$latest_patch_sha" > "$STATE_FILE"
|
echo "$latest_patch_sha" > "$STATE_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -585,6 +585,62 @@ class ConfigTab(QWidget):
|
||||||
ui_form.addRow(font_scale_label, self.font_scale_spin)
|
ui_form.addRow(font_scale_label, self.font_scale_spin)
|
||||||
|
|
||||||
right_column.addLayout(ui_form)
|
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()
|
right_column.addStretch()
|
||||||
|
|
||||||
# Add columns to layout
|
# Add columns to layout
|
||||||
|
|
@ -726,6 +782,38 @@ class ConfigTab(QWidget):
|
||||||
# UI settings
|
# UI settings
|
||||||
self.font_scale_spin.setValue(float(_get("font_scale", "1.0")))
|
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):
|
def connect_auto_save(self):
|
||||||
"""Connect all widgets to auto-save on change."""
|
"""Connect all widgets to auto-save on change."""
|
||||||
# Text fields - use editingFinished to avoid saving on every keystroke
|
# 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.input_cost_spin.editingFinished.connect(self.auto_save)
|
||||||
self.output_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.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):
|
def disconnect_auto_save(self):
|
||||||
"""Disconnect all widgets from auto-save."""
|
"""Disconnect all widgets from auto-save."""
|
||||||
|
|
@ -770,6 +862,10 @@ class ConfigTab(QWidget):
|
||||||
self.input_cost_spin.editingFinished.disconnect(self.auto_save)
|
self.input_cost_spin.editingFinished.disconnect(self.auto_save)
|
||||||
self.output_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.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):
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -808,6 +904,10 @@ class ConfigTab(QWidget):
|
||||||
"input_cost": str(self.input_cost_spin.value()),
|
"input_cost": str(self.input_cost_spin.value()),
|
||||||
"output_cost": str(self.output_cost_spin.value()),
|
"output_cost": str(self.output_cost_spin.value()),
|
||||||
"font_scale": str(self.font_scale_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
|
# Save to .env file and update os.environ so subprocesses inherit new values
|
||||||
|
|
@ -868,6 +968,15 @@ class ConfigTab(QWidget):
|
||||||
# UI settings
|
# UI settings
|
||||||
self.font_scale_spin.setValue(1.0)
|
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()
|
self.connect_auto_save()
|
||||||
|
|
||||||
def reset_to_defaults_with_save(self):
|
def reset_to_defaults_with_save(self):
|
||||||
|
|
@ -902,6 +1011,10 @@ class ConfigTab(QWidget):
|
||||||
"input_cost": self.input_cost_spin.value(),
|
"input_cost": self.input_cost_spin.value(),
|
||||||
"output_cost": self.output_cost_spin.value(),
|
"output_cost": self.output_cost_spin.value(),
|
||||||
"font_scale": self.font_scale_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):
|
def validate(self):
|
||||||
|
|
|
||||||
|
|
@ -1030,7 +1030,9 @@ class WolfWorkflowTab(QWidget):
|
||||||
tc_inner.setSpacing(4)
|
tc_inner.setSpacing(4)
|
||||||
tc_desc = QLabel(
|
tc_desc = QLabel(
|
||||||
"Copies everything from the gameupdate/ folder into the game's root folder, "
|
"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.setWordWrap(True)
|
||||||
tc_desc.setStyleSheet("color:#9d9d9d;font-size:13px;")
|
tc_desc.setStyleSheet("color:#9d9d9d;font-size:13px;")
|
||||||
|
|
@ -1456,6 +1458,16 @@ class WolfWorkflowTab(QWidget):
|
||||||
|
|
||||||
self._run_task(task)
|
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):
|
def _run_gameupdate(self):
|
||||||
src = self.pp_gameupdate_edit.text().strip()
|
src = self.pp_gameupdate_edit.text().strip()
|
||||||
dst = self.folder_edit.text().strip()
|
dst = self.folder_edit.text().strip()
|
||||||
|
|
@ -1478,6 +1490,9 @@ class WolfWorkflowTab(QWidget):
|
||||||
self._log(f"✅ gameupdate: copied {count} file(s).")
|
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||||
for e in errors:
|
for e in errors:
|
||||||
self._log(f" ⚠ {e}")
|
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):
|
def _run_all_preprocess(self):
|
||||||
if not self._game_root:
|
if not self._game_root:
|
||||||
|
|
@ -1523,6 +1538,13 @@ class WolfWorkflowTab(QWidget):
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"{rel}: {exc}")
|
errors.append(f"{rel}: {exc}")
|
||||||
log(f" Copied {copied} gameupdate file(s).")
|
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:
|
if errors:
|
||||||
return False, "Pre-process finished with errors (see log)."
|
return False, "Pre-process finished with errors (see log)."
|
||||||
return True, "Pre-process: dazedformat and gameupdate copy finished."
|
return True, "Pre-process: dazedformat and gameupdate copy finished."
|
||||||
|
|
|
||||||
|
|
@ -1716,7 +1716,9 @@ class WorkflowTab(QWidget):
|
||||||
tc_inner.setSpacing(4)
|
tc_inner.setSpacing(4)
|
||||||
tc_desc = QLabel(
|
tc_desc = QLabel(
|
||||||
"Copies everything from the <code>gameupdate/</code> folder "
|
"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.setTextFormat(Qt.RichText)
|
||||||
tc_desc.setWordWrap(True)
|
tc_desc.setWordWrap(True)
|
||||||
|
|
@ -4612,6 +4614,16 @@ class WorkflowTab(QWidget):
|
||||||
self._worker = w
|
self._worker = w
|
||||||
w.start()
|
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):
|
def _run_gameupdate(self):
|
||||||
src = self.pp_gameupdate_edit.text().strip()
|
src = self.pp_gameupdate_edit.text().strip()
|
||||||
dst = self.folder_edit.text().strip()
|
dst = self.folder_edit.text().strip()
|
||||||
|
|
@ -4634,6 +4646,9 @@ class WorkflowTab(QWidget):
|
||||||
self._log(f"✅ gameupdate: copied {count} file(s).")
|
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||||
for e in errors:
|
for e in errors:
|
||||||
self._log(f" ⚠ {e}")
|
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):
|
def _run_all_preprocess(self):
|
||||||
"""Launch all three pre-process tasks in sequence, chaining via signals."""
|
"""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).")
|
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||||
for e in errors:
|
for e in errors:
|
||||||
self._log(f" ⚠ {e}")
|
self._log(f" ⚠ {e}")
|
||||||
|
if not errors and game_root_dst:
|
||||||
|
self._write_gameupdate_patch_config(game_root_dst)
|
||||||
run_next(rest)
|
run_next(rest)
|
||||||
worker.done.connect(on_copy_done)
|
worker.done.connect(on_copy_done)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
132
util/gameupdate_config.py
Normal file
132
util/gameupdate_config.py
Normal 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)
|
||||||
Loading…
Reference in a new issue