diff --git a/.gitignore b/.gitignore index 6b5f778..679166d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ !.gitignore !README.md !patch-config.txt +!patch-config.example.txt !GameUpdate* !patch* !Game.dat @@ -29,6 +30,8 @@ !package.nw !SRPG_Unpacker.exe !(SRPG_Unpacker Patcher).bat +!UberWolfCli.exe +!UberWolfCli.LICENSE.txt !Data/BasicData/CommonEvent.dat !Data/BasicData/DataBase.dat !Data/BasicData/CDataBase.dat diff --git a/README.md b/README.md index fa94b89..0283b56 100644 --- a/README.md +++ b/README.md @@ -17,22 +17,67 @@ 2. Optional: set `GAMEUPDATE_PROMPT_PWSH=1` before running `GameUpdate.bat` if you want users to be prompted to install PowerShell 7 via winget. 3. Optional: set `GAMEUPDATE_DL_ATTEMPTS` (default `2`) to control retries for API checks/downloads. Lower values fail faster; higher values tolerate flaky networks. +### patch-config.txt + +Create `gameupdate/patch-config.txt` next to `patch.ps1` (see `patch-config.example.txt`). + +In DazedMTLTool, set **Config → Game Update Defaults** (forge, host, org/username, branch) once. Step 1 **Copy gameupdate/** writes those into each game's `patch-config.txt`. You still set `repo=` per game. + +```txt +forge=gitlab +host=gitgud.io +username=YOUR_ORG_OR_USER +repo=YOUR_PATCH_REPO +branch=main +``` + +| Key | Meaning | +|-----|---------| +| `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) | +| `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` | +| `username` | Owner / org / group (`owner=` / `org=` aliases work) | +| `repo` | Repository name | +| `branch` | Branch to track | + +Examples: + +```txt +# GitLab / gitgud +forge=gitlab +host=gitgud.io +username=myorg +repo=cool-game-en +branch=main + +# GitHub +forge=github +host=github.com +username=myorg +repo=cool-game-en +branch=main + +# Forgejo / Codeberg +forge=forgejo +host=codeberg.org +username=myorg +repo=cool-game-en +branch=main +``` + +Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`). + # Troubleshooting **GAMEUPDATE.bat doesn't update and closes immediately** 1. Make sure your path doesn't contain any Japanese characters or lots of whitespace. 2. Make sure you actually have permissions in the folder -3. Auto-update calls GitLab’s public HTTP API (`/api/v4/...`), not the web “Download ZIP” URL—no account or token is required for public patch repos. - -For WOLF RPG games, if you downloaded the game off of DLSite, you will need to do some extra steps to patch it. This is because there is a "master" file called Data.wolf that will take priority over the english patch files. You will need this file to be a folder before patching will work. +3. Auto-update calls the forge's public HTTP API (GitLab `/api/v4`, GitHub `/repos/...`, Forgejo `/api/v1`), not the web “Download ZIP” button - no account is required for public patch repos. # Wolf Games -1) Download the latest UberWolf.exe release from the following link: -https://github.com/Sinflower/UberWolf/releases -2) Drag Data.wolf onto UberWolf.exe. This will create a new folder called data.wolf~ -3) Rename the new data.wolf~ folder to Data -4) Delete the Data.wolf file -5) Delete previous_patch_sha.txt (this will exist if you ran GameUpdate.bat previously) -6) Run GameUpdate.bat +WOLF RPG installs from DLSite ship a master `Data.wolf` archive that takes priority over loose English patch files. `GameUpdate.bat` detects that and unpacks it automatically with the bundled `UberWolfCli.exe` (MIT, [Sinflower/UberWolf](https://github.com/Sinflower/UberWolf)), then renames `Data.wolf` to `Data.wolf.bak` so the patched loose `Data/` files load. + +Just run **`GameUpdate.bat`**. No manual UberWolf download is required. + +If unpack fails (rare Pro / protected builds), unpack once with [UberWolf](https://github.com/Sinflower/UberWolf/releases), ensure a loose `Data/` folder exists, rename or remove `Data.wolf`, delete `gameupdate/previous_patch_sha.txt`, and run `GameUpdate.bat` again. # Edit/Contribute TLDR 3 steps. diff --git a/UberWolfCli.LICENSE.txt b/UberWolfCli.LICENSE.txt new file mode 100644 index 0000000..ac7261b --- /dev/null +++ b/UberWolfCli.LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sinflower + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/UberWolfCli.exe b/UberWolfCli.exe new file mode 100644 index 0000000..25dcf73 Binary files /dev/null and b/UberWolfCli.exe differ diff --git a/gameupdate/patch-config.example.txt b/gameupdate/patch-config.example.txt new file mode 100644 index 0000000..379c363 --- /dev/null +++ b/gameupdate/patch-config.example.txt @@ -0,0 +1,28 @@ +# GameUpdate patch source (copy to patch-config.txt and edit). +# Lines starting with # are comments. Keys are case-insensitive. +# +# Tip: set forge / host / username / branch once in DazedMTLTool +# Config → Game Update Defaults. Step 1 "Copy gameupdate/" writes +# those into this file automatically (you still set repo= per game). + +# Where the patch repo lives: +# gitlab - GitLab / gitgud.io / self-hosted GitLab (default) +# github - GitHub.com or GitHub Enterprise +# forgejo - Forgejo (also accepts gitea) +forge=gitlab + +# Host only (no https://). Defaults: +# gitlab -> gitgud.io +# github -> github.com +# forgejo -> codeberg.org +# Examples: gitgud.io gitlab.com github.com codeberg.org git.example.com +host=gitgud.io + +# Owner / org / group that owns the repo (set in Config; rarely changes) +username=YOUR_ORG_OR_USER + +# Repository name (per game) +repo=YOUR_PATCH_REPO + +# Branch to track +branch=main diff --git a/gameupdate/patch-config.txt b/gameupdate/patch-config.txt index 6798552..e285fe4 100644 --- a/gameupdate/patch-config.txt +++ b/gameupdate/patch-config.txt @@ -1,3 +1,7 @@ -username=dazed-translations -repo=magical-girls-runa-and-nanami -branch=main +# Generated by DazedMTLTool from Config → Game Update defaults. +# Edit repo= per game. forge/host/username usually stay the same. +forge=forgejo +host=git.dazedtl.dev +username=dazed-translations +repo=YOUR_PATCH_REPO +branch=main diff --git a/gameupdate/patch.ps1 b/gameupdate/patch.ps1 index 90a42a2..278af0c 100644 --- a/gameupdate/patch.ps1 +++ b/gameupdate/patch.ps1 @@ -194,7 +194,13 @@ function Test-WrongWorkingDirectory { function Read-PatchConfig { param([string]$ConfigPath) - $cfg = @{ username = ''; repo = ''; branch = '' } + $cfg = @{ + forge = 'gitlab' + host = '' + username = '' + repo = '' + branch = '' + } if (-not (Test-Path -LiteralPath $ConfigPath)) { return $cfg } @@ -206,7 +212,12 @@ function Read-PatchConfig { $k = $line.Substring(0, $eq).Trim().ToLowerInvariant() $v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' } switch ($k) { + 'forge' { $cfg.forge = $v } + 'provider' { $cfg.forge = $v } + 'host' { $cfg.host = $v } 'username' { $cfg.username = $v } + 'owner' { $cfg.username = $v } + 'org' { $cfg.username = $v } 'repo' { $cfg.repo = $v } 'branch' { $cfg.branch = $v } } @@ -214,6 +225,115 @@ function Read-PatchConfig { return $cfg } +function Resolve-PatchForge { + param([Parameter(Mandatory = $true)][hashtable]$Cfg) + + $raw = ([string]$Cfg.forge).Trim().ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($raw)) { $raw = 'gitlab' } + + $kind = switch -Regex ($raw) { + '^(gitlab|gl|gitgud)$' { 'gitlab' } + '^(github|gh)$' { 'github' } + '^(forgejo|gitea|fj|codeberg)$' { 'forgejo' } + default { $null } + } + if (-not $kind) { + throw ("PATCH_ERR:CONFIG:Unknown forge='{0}'. Use gitlab, github, or forgejo." -f $Cfg.forge) + } + + $hostName = ([string]$Cfg.host).Trim() + $hostName = $hostName -replace '^https?://', '' + $hostName = $hostName.TrimEnd('/') + if ($hostName.Contains('/')) { + $hostName = $hostName.Split('/')[0] + } + if ([string]::IsNullOrWhiteSpace($hostName)) { + $hostName = switch ($kind) { + 'gitlab' { 'gitgud.io' } + 'github' { 'github.com' } + 'forgejo' { 'codeberg.org' } + } + } + + $webBase = "https://$hostName" + $apiBase = switch ($kind) { + 'gitlab' { "$webBase/api/v4" } + 'github' { + if ($hostName -eq 'github.com') { 'https://api.github.com' } + else { "$webBase/api/v3" } + } + 'forgejo' { "$webBase/api/v1" } + } + + return @{ + kind = $kind + host = $hostName + webBase = $webBase + apiBase = $apiBase + username = ([string]$Cfg.username).Trim() + repo = ([string]$Cfg.repo).Trim() + branch = ([string]$Cfg.branch).Trim() + } +} + +function New-PatchApiHeaders { + param([Parameter(Mandatory = $true)][hashtable]$Resolved) + $headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' } + if ($Resolved.kind -eq 'github') { + $headers['Accept'] = 'application/vnd.github+json' + } + return $headers +} + +function Get-PatchLatestSha { + param( + [Parameter(Mandatory = $true)][hashtable]$Resolved, + [Parameter(Mandatory = $true)][hashtable]$Headers + ) + $ownerEnc = [uri]::EscapeDataString($Resolved.username) + $repoEnc = [uri]::EscapeDataString($Resolved.repo) + $branchEnc = [uri]::EscapeDataString($Resolved.branch) + + switch ($Resolved.kind) { + 'gitlab' { + $id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo) + $uri = "$($Resolved.apiBase)/projects/$id/repository/branches/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id + } + 'github' { + $uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/commits/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).sha + } + 'forgejo' { + $uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/branches/$branchEnc" + return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id + } + } +} + +function Get-PatchArchiveUrl { + param( + [Parameter(Mandatory = $true)][hashtable]$Resolved, + [Parameter(Mandatory = $true)][string]$Sha + ) + $ownerEnc = [uri]::EscapeDataString($Resolved.username) + $repoEnc = [uri]::EscapeDataString($Resolved.repo) + $shaEnc = [uri]::EscapeDataString($Sha) + + switch ($Resolved.kind) { + 'gitlab' { + $id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo) + return "$($Resolved.apiBase)/projects/$id/repository/archive.zip?sha=$shaEnc" + } + 'github' { + return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/zipball/$shaEnc" + } + 'forgejo' { + return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/archive/$shaEnc.zip" + } + } +} + function Invoke-SrpgUnpacker { param( [Parameter(Mandatory = $true)][string]$ExePath, @@ -230,6 +350,140 @@ function Invoke-SrpgUnpacker { } } +function Test-WolfLooseDataReady { + param([Parameter(Mandatory = $true)][string]$DataDir) + if (-not (Test-Path -LiteralPath $DataDir -PathType Container)) { + return $false + } + $markers = @( + (Join-Path $DataDir 'BasicData\CommonEvent.dat'), + (Join-Path $DataDir 'CommonEvent.dat') + ) + foreach ($marker in $markers) { + if (Test-Path -LiteralPath $marker -PathType Leaf) { + return $true + } + } + if (@(Get-ChildItem -LiteralPath $DataDir -Filter '*.mps' -File -ErrorAction SilentlyContinue).Count -gt 0) { + return $true + } + $basic = Join-Path $DataDir 'BasicData' + if (Test-Path -LiteralPath $basic -PathType Container) { + if (@(Get-ChildItem -LiteralPath $basic -Filter '*.dat' -File -ErrorAction SilentlyContinue).Count -gt 0) { + return $true + } + } + return $false +} + +function Find-UberWolfCli { + param([Parameter(Mandatory = $true)][string]$Root) + $candidates = @( + (Join-Path $Root 'UberWolfCli.exe'), + (Join-Path $Root 'gameupdate\UberWolfCli.exe') + ) + foreach ($path in $candidates) { + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } + } + return $null +} + +function Invoke-WolfPreSetup { + <# + WOLF installs often ship Data.wolf, which overrides loose patched Data/ files. + Unpack once with UberWolfCli (bundled in the patch repo), then retire Data.wolf + so GameUpdate can copy injected .mps/.dat on top. + #> + param([Parameter(Mandatory = $true)][string]$Root) + + $dataWolf = $null + foreach ($name in @('Data.wolf', 'data.wolf')) { + $candidate = Join-Path $Root $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $dataWolf = $candidate + break + } + } + if (-not $dataWolf) { + return + } + + $dataDir = Join-Path $Root 'Data' + if (-not (Test-Path -LiteralPath $dataDir -PathType Container)) { + $dataDirAlt = Join-Path $Root 'data' + if (Test-Path -LiteralPath $dataDirAlt -PathType Container) { + $dataDir = $dataDirAlt + } + } + + if (Test-WolfLooseDataReady -DataDir $dataDir) { + $bak = "$dataWolf.bak" + if (-not (Test-Path -LiteralPath $bak)) { + Write-Host '[Pre-Setup] Loose Data/ already present; retiring Data.wolf -> Data.wolf.bak' + Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force + } + else { + Write-Host '[Pre-Setup] Loose Data/ already present; removing leftover Data.wolf' + Remove-Item -LiteralPath $dataWolf -Force + } + return + } + + $cli = Find-UberWolfCli -Root $Root + if (-not $cli) { + Write-Host '[Pre-Setup] Data.wolf found but UberWolfCli.exe is missing.' + Write-Host ' Copy UberWolfCli.exe into the game root (Step 1 gameupdate/) and re-run.' + return + } + + Write-Host '[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)...' + $code = Invoke-SrpgUnpacker -ExePath $cli -WorkingDirectory $Root -Arguments @('-o', $dataWolf) + if ($code -ne 0) { + Write-Host ('[Pre-Setup] ERROR: UberWolfCli exited with code {0}.' -f $code) + return + } + + # UberWolf writes next to the archive as / (Data/). Also accept data.wolf~ leftovers. + $unpacked = Join-Path $Root 'Data' + if (-not (Test-Path -LiteralPath $unpacked -PathType Container)) { + $unpacked = Join-Path $Root 'data' + } + if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) { + foreach ($alt in @('data.wolf~', 'Data.wolf~', 'Data~', 'data~')) { + $altPath = Join-Path $Root $alt + if (Test-Path -LiteralPath $altPath -PathType Container) { + Write-Host ("[Pre-Setup] Promoting {0} -> Data\" -f $alt) + if (Test-Path -LiteralPath (Join-Path $Root 'Data')) { + Remove-FileOrDirectoryViaCmd -LiteralPath (Join-Path $Root 'Data') + } + Rename-Item -LiteralPath $altPath -NewName 'Data' -Force + $unpacked = Join-Path $Root 'Data' + break + } + } + } + + if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) { + Write-Host '[Pre-Setup] ERROR: Unpack finished but loose Data/ still looks empty.' + return + } + + $bak = "$dataWolf.bak" + if (Test-Path -LiteralPath $dataWolf -PathType Leaf) { + if (-not (Test-Path -LiteralPath $bak)) { + Write-Host '[Pre-Setup] Retiring Data.wolf -> Data.wolf.bak' + Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force + } + else { + Write-Host '[Pre-Setup] Removing Data.wolf so loose Data/ takes priority' + Remove-Item -LiteralPath $dataWolf -Force + } + } + Write-Host '[Pre-Setup] Wolf Data/ is ready for patching.' +} + function Invoke-SrpgPreSetup { param([string]$Root) $unpacker = Join-Path $Root 'SRPG_Unpacker.exe' @@ -512,9 +766,7 @@ function Invoke-PatchDownloadExtract { param( [Parameter(Mandatory = $true)][string]$Root, [Parameter(Mandatory = $true)][string]$StateFilePath, - [Parameter(Mandatory = $true)][string]$Username, - [Parameter(Mandatory = $true)][string]$Repo, - [Parameter(Mandatory = $true)][string]$Branch + [Parameter(Mandatory = $true)][hashtable]$Resolved ) if ($PSVersionTable.PSEdition -ne 'Core') { @@ -526,13 +778,11 @@ function Invoke-PatchDownloadExtract { Set-Location -LiteralPath $Root - $id = [uri]::EscapeDataString($Username + '/' + $Repo) - $branchEnc = [uri]::EscapeDataString($Branch) - $headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' } + $headers = New-PatchApiHeaders -Resolved $Resolved $dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2 $latestSha = Invoke-WithRetry -Name 'Resolve latest patch SHA' -Attempts $dlAttempts -Action { - (Invoke-RestMethod -Uri ("https://gitgud.io/api/v4/projects/$id/repository/branches/$branchEnc") -Headers $headers).commit.id + Get-PatchLatestSha -Resolved $Resolved -Headers $headers } if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' } $latestSha = ([string]$latestSha).Trim() @@ -551,8 +801,7 @@ function Invoke-PatchDownloadExtract { $zipPath = Join-Path $Root ('dazedmtl_patch_' + [Guid]::NewGuid().ToString('N') + '.zip') Write-Host ('Downloading patch... ' + $zipPath) $stage = Join-Path ([IO.Path]::GetTempPath()) ('gu_' + [Guid]::NewGuid().ToString('N')) - $archiveSha = [uri]::EscapeDataString($latestSha) - $archiveUrl = "https://gitgud.io/api/v4/projects/$id/repository/archive.zip?sha=$archiveSha" + $archiveUrl = Get-PatchArchiveUrl -Resolved $Resolved -Sha $latestSha $dlSw = [System.Diagnostics.Stopwatch]::StartNew() Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action { @@ -599,7 +848,21 @@ function Invoke-PatchDownloadExtract { if ($dirs.Count -ne 1) { throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count) } - Copy-Item -Path (Join-Path $dirs[0].FullName '*') -Destination $PWD.Path -Recurse -Force + $stagedRoot = $dirs[0].FullName + + # Stage UberWolfCli into the game root before unpack so first-time + # players can convert Data.wolf -> Data/ before English files land. + foreach ($cliName in @('UberWolfCli.exe', 'UberWolfCli.LICENSE.txt')) { + $stagedCli = Join-Path $stagedRoot $cliName + $destCli = Join-Path $Root $cliName + if ((Test-Path -LiteralPath $stagedCli -PathType Leaf) -and + -not (Test-Path -LiteralPath $destCli -PathType Leaf)) { + Copy-Item -LiteralPath $stagedCli -Destination $destCli -Force + } + } + Invoke-WolfPreSetup -Root $Root + + Copy-Item -Path (Join-Path $stagedRoot '*') -Destination $PWD.Path -Recurse -Force } finally { if (Test-Path -LiteralPath $zipPath) { @@ -717,8 +980,19 @@ try { exit 1 } - Write-Host ('Pulling patch from https://gitgud.io/{0}/{1} (branch: {2})' -f $cfg.username, $cfg.repo, $cfg.branch) + try { + $resolved = Resolve-PatchForge -Cfg $cfg + } + catch { + Write-Host $_.Exception.Message + Wait-ConsolePause + exit 1 + } + Write-Host ('Pulling patch from {0}/{1}/{2} ({3}, branch: {4})' -f ` + $resolved.webBase, $resolved.username, $resolved.repo, $resolved.kind, $resolved.branch) + + Invoke-WolfPreSetup -Root $GameRoot Invoke-SrpgPreSetup -Root $GameRoot $stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt' @@ -726,7 +1000,7 @@ try { $patchDlExit = 1 try { $patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile ` - -Username $cfg.username -Repo $cfg.repo -Branch $cfg.branch + -Resolved $resolved } catch { Write-Host '' @@ -753,6 +1027,9 @@ try { Write-Host 'Applying patch...' Invoke-SrpgPostApply -Root $GameRoot + # Safety: if Data/ is ready (from unpack or patch files) but Data.wolf + # remains, retire it so the archive does not override loose English files. + Invoke-WolfPreSetup -Root $GameRoot Write-Host 'Cleaning up...' Remove-PatchTempArtifacts -Root $GameRoot diff --git a/gameupdate/patch.sh b/gameupdate/patch.sh index ad1b86d..02991a1 100755 --- a/gameupdate/patch.sh +++ b/gameupdate/patch.sh @@ -6,7 +6,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(pwd)" CONFIG_FILE="$SCRIPT_DIR/patch-config.txt" STATE_FILE="$SCRIPT_DIR/previous_patch_sha.txt" -GITGUD_API="https://gitgud.io/api/v4" API_HEADERS=( -H "User-Agent: GameUpdate/1.0" ) check_dependency() { @@ -38,19 +37,113 @@ echo "Config file path: $CONFIG_FILE" # shellcheck disable=SC1090 . "$CONFIG_FILE" -if [ -z "${username:-}" ]; then +# Defaults for older configs that only set username/repo/branch. +forge="${forge:-${provider:-gitlab}}" +host="${host:-}" +username="${username:-${owner:-${org:-}}}" +repo="${repo:-}" +branch="${branch:-}" + +if [ -z "$username" ]; then echo "ERROR: 'username=' is missing in gameupdate/patch-config.txt" exit 1 fi -if [ -z "${repo:-}" ]; then +if [ -z "$repo" ]; then echo "ERROR: 'repo=' is missing in gameupdate/patch-config.txt" exit 1 fi -if [ -z "${branch:-}" ]; then +if [ -z "$branch" ]; then echo "ERROR: 'branch=' is missing in gameupdate/patch-config.txt" exit 1 fi +normalize_forge() { + local raw + raw="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$raw" in + ''|gitlab|gl|gitgud) echo gitlab ;; + github|gh) echo github ;; + forgejo|gitea|fj|codeberg) echo forgejo ;; + *) + echo "ERROR: Unknown forge='$1'. Use gitlab, github, or forgejo." >&2 + return 1 + ;; + esac +} + +FORGE_KIND="$(normalize_forge "$forge")" || exit 1 + +# Strip scheme / path from host if a user pasted a full URL. +HOST_NAME="$(printf '%s' "$host" | sed -E 's#^https?://##; s#/.*##; s#[[:space:]]##g')" +if [ -z "$HOST_NAME" ]; then + case "$FORGE_KIND" in + gitlab) HOST_NAME="gitgud.io" ;; + github) HOST_NAME="github.com" ;; + forgejo) HOST_NAME="codeberg.org" ;; + esac +fi + +WEB_BASE="https://${HOST_NAME}" +case "$FORGE_KIND" in + gitlab) API_BASE="${WEB_BASE}/api/v4" ;; + github) + if [ "$HOST_NAME" = "github.com" ]; then + API_BASE="https://api.github.com" + else + API_BASE="${WEB_BASE}/api/v3" + fi + ;; + forgejo) API_BASE="${WEB_BASE}/api/v1" ;; +esac + +API_HEADERS=( -H "User-Agent: GameUpdate/1.0" ) +if [ "$FORGE_KIND" = "github" ]; then + API_HEADERS+=( -H "Accept: application/vnd.github+json" ) +fi + +owner_enc=$(jq -nr --arg s "$username" '$s | @uri') +repo_enc=$(jq -nr --arg s "$repo" '$s | @uri') +branch_enc=$(jq -nr --arg b "$branch" '$b | @uri') +project_enc=$(jq -nr --arg ns "$username" --arg rp "$repo" '$ns + "/" + $rp | @uri') + +resolve_latest_sha() { + case "$FORGE_KIND" in + gitlab) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/projects/${project_enc}/repository/branches/${branch_enc}" \ + | jq -r '.commit.id' + ;; + github) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/repos/${owner_enc}/${repo_enc}/commits/${branch_enc}" \ + | jq -r '.sha' + ;; + forgejo) + curl -fsSL "${API_HEADERS[@]}" \ + "${API_BASE}/repos/${owner_enc}/${repo_enc}/branches/${branch_enc}" \ + | jq -r '.commit.id' + ;; + esac +} + +archive_url_for_sha() { + local sha_enc + sha_enc=$(jq -nr --arg s "$1" '$s | @uri') + case "$FORGE_KIND" in + gitlab) + echo "${API_BASE}/projects/${project_enc}/repository/archive.zip?sha=${sha_enc}" + ;; + github) + echo "${API_BASE}/repos/${owner_enc}/${repo_enc}/zipball/${sha_enc}" + ;; + forgejo) + echo "${API_BASE}/repos/${owner_enc}/${repo_enc}/archive/${sha_enc}.zip" + ;; + esac +} + +echo "Pulling patch from ${WEB_BASE}/${username}/${repo} (${FORGE_KIND}, branch: ${branch})" + RETRIES="${GAMEUPDATE_DL_ATTEMPTS:-2}" if ! [[ "$RETRIES" =~ ^[1-9][0-9]*$ ]]; then RETRIES=2 @@ -74,23 +167,139 @@ retry_cmd() { done } -project_enc=$(jq -nr --arg ns "$username" --arg rp "$repo" '$ns + "/" + $rp | @uri') -branch_enc=$(jq -nr --arg b "$branch" '$b | @uri') - # Get the latest hash echo "Getting latest commit SHA hash" -latest_patch_sha="$( - retry_cmd "Resolve latest patch SHA" \ - curl -fsSL "${API_HEADERS[@]}" \ - "${GITGUD_API}/projects/${project_enc}/repository/branches/${branch_enc}" \ - | jq -r '.commit.id' -)" +latest_patch_sha="" +attempt=1 +while true; do + if latest_patch_sha="$(resolve_latest_sha)"; then + break + fi + if [ "$attempt" -ge "$RETRIES" ]; then + echo "Resolve latest patch SHA failed after $attempt attempt(s)." + exit 1 + fi + echo "Resolve latest patch SHA failed ($attempt/$RETRIES), retrying..." + attempt=$((attempt + 1)) + sleep 2 +done latest_patch_sha="$(printf '%s' "$latest_patch_sha" | tr -d '[:space:]')" if [ -z "$latest_patch_sha" ] || [ "$latest_patch_sha" = "null" ]; then echo "PATCH_ERR:API:Latest commit SHA response was empty." exit 1 fi +# -------------------------------------------------------- +# PRE-SETUP: Wolf Data.wolf -> loose Data/ (before patch copy) +# -------------------------------------------------------- +wolf_loose_data_ready() { + local data_dir="$1" + [ -d "$data_dir" ] || return 1 + [ -f "$data_dir/BasicData/CommonEvent.dat" ] && return 0 + [ -f "$data_dir/CommonEvent.dat" ] && return 0 + if compgen -G "$data_dir/*.mps" > /dev/null 2>&1; then + return 0 + fi + if compgen -G "$data_dir/BasicData/*.dat" > /dev/null 2>&1; then + return 0 + fi + return 1 +} + +find_uberwolf_cli() { + if [ -f "$ROOT_DIR/UberWolfCli.exe" ]; then + echo "$ROOT_DIR/UberWolfCli.exe" + return 0 + fi + if [ -f "$ROOT_DIR/gameupdate/UberWolfCli.exe" ]; then + echo "$ROOT_DIR/gameupdate/UberWolfCli.exe" + return 0 + fi + return 1 +} + +retire_data_wolf() { + local data_wolf="$1" + local bak="${data_wolf}.bak" + if [ ! -f "$data_wolf" ]; then + return 0 + fi + if [ ! -e "$bak" ]; then + echo "[Pre-Setup] Retiring $(basename "$data_wolf") -> $(basename "$bak")" + mv -f "$data_wolf" "$bak" + else + echo "[Pre-Setup] Removing leftover $(basename "$data_wolf") so loose Data/ takes priority" + rm -f "$data_wolf" + fi +} + +invoke_wolf_pre_setup() { + local data_wolf="" + local name + for name in Data.wolf data.wolf; do + if [ -f "$ROOT_DIR/$name" ]; then + data_wolf="$ROOT_DIR/$name" + break + fi + done + [ -n "$data_wolf" ] || return 0 + + local data_dir="$ROOT_DIR/Data" + if [ ! -d "$data_dir" ] && [ -d "$ROOT_DIR/data" ]; then + data_dir="$ROOT_DIR/data" + fi + + if wolf_loose_data_ready "$data_dir"; then + retire_data_wolf "$data_wolf" + return 0 + fi + + local cli + cli="$(find_uberwolf_cli || true)" + if [ -z "$cli" ]; then + echo "[Pre-Setup] Data.wolf found but UberWolfCli.exe is missing." + echo " Copy UberWolfCli.exe into the game root (Step 1 gameupdate/) and re-run." + return 0 + fi + + echo "[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)..." + # Native .exe works under Windows / Git Bash; wine covers Linux hosts. + if ( cd "$ROOT_DIR" && "$cli" -o "$data_wolf" ); then + : + elif command -v wine > /dev/null 2>&1 && ( cd "$ROOT_DIR" && wine "$cli" -o "$data_wolf" ); then + : + else + echo "[Pre-Setup] ERROR: UberWolfCli unpack failed." + return 0 + fi + + local unpacked="$ROOT_DIR/Data" + if ! wolf_loose_data_ready "$unpacked"; then + unpacked="$ROOT_DIR/data" + fi + if ! wolf_loose_data_ready "$unpacked"; then + local alt + for alt in data.wolf~ Data.wolf~ Data~ data~; do + if [ -d "$ROOT_DIR/$alt" ]; then + echo "[Pre-Setup] Promoting $alt -> Data/" + rm -rf "$ROOT_DIR/Data" + mv -f "$ROOT_DIR/$alt" "$ROOT_DIR/Data" + unpacked="$ROOT_DIR/Data" + break + fi + done + fi + + if wolf_loose_data_ready "$unpacked"; then + retire_data_wolf "$data_wolf" + echo "[Pre-Setup] Wolf Data/ is ready for patching." + else + echo "[Pre-Setup] ERROR: Unpack finished but loose Data/ still looks empty." + fi +} + +invoke_wolf_pre_setup + # -------------------------------------------------------- # PRE-SETUP: Ensure SRPG data and patch structure exists # Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates @@ -132,10 +341,10 @@ fi download_extract() { echo "Downloading latest patch..." - archive_sha_enc=$(jq -nr --arg s "$latest_patch_sha" '$s | @uri') + archive_url="$(archive_url_for_sha "$latest_patch_sha")" if ! retry_cmd "Download archive with curl" \ curl -fSL --progress-bar "${API_HEADERS[@]}" \ - "${GITGUD_API}/projects/${project_enc}/repository/archive.zip?sha=${archive_sha_enc}" \ + "$archive_url" \ -o "$ROOT_DIR/repo.zip"; then echo "Download failed!" rm -f "$ROOT_DIR/repo.zip" @@ -166,6 +375,15 @@ download_extract() { return 1 fi + # Stage UberWolfCli before unpack so Data.wolf -> Data/ happens before + # English patch files are copied on top. + for cli_name in UberWolfCli.exe UberWolfCli.LICENSE.txt; do + if [ -f "$inner/$cli_name" ] && [ ! -f "$ROOT_DIR/$cli_name" ]; then + cp -f "$inner/$cli_name" "$ROOT_DIR/$cli_name" + fi + done + invoke_wolf_pre_setup + echo "Applying patch..." if ! cp -r "$inner"/* "$ROOT_DIR/"; then echo "Patch application failed!" @@ -205,6 +423,9 @@ download_extract() { fi fi + # Retire Data.wolf if loose Data/ is ready after the patch copy. + invoke_wolf_pre_setup + echo "$latest_patch_sha" > "$STATE_FILE" } diff --git a/wolf_json/CDataBase.project.json b/wolf_json/CDataBase.project.json index 5bc53f1..7009a38 100644 --- a/wolf_json/CDataBase.project.json +++ b/wolf_json/CDataBase.project.json @@ -2853,4 +2853,4 @@ } ], "kind": "db" -} +} \ No newline at end of file diff --git a/wolf_json/DataBase.project.json b/wolf_json/DataBase.project.json index ce68e9e..8e4e9ae 100644 --- a/wolf_json/DataBase.project.json +++ b/wolf_json/DataBase.project.json @@ -17573,4 +17573,4 @@ } ], "kind": "db" -} +} \ No newline at end of file diff --git a/wolf_json/db_profile.json b/wolf_json/db_profile.json index d1f0de2..1b7edda 100644 --- a/wolf_json/db_profile.json +++ b/wolf_json/db_profile.json @@ -33,4 +33,4 @@ ], "defer_groups": [], "notes": "Recommended order: Names → DB foundation (258 lines) → DB narrative (2,333 lines) → Maps/events." -} +} \ No newline at end of file diff --git a/wolf_json/wolfdawn-roles.json b/wolf_json/wolfdawn-roles.json index 96c547f..eb70692 100644 --- a/wolf_json/wolfdawn-roles.json +++ b/wolf_json/wolfdawn-roles.json @@ -1,14 +1,14 @@ { - "nameWraps": [ - { - "note": "├■街の噂(MOB)", - "maxLines": 3, - "width": 30 - }, - { - "note": "├■ウワサ・MOB進行度等", - "width": 30, - "maxLines": 3 - } - ] -} + "nameWraps": [ + { + "note": "├■街の噂(MOB)", + "maxLines": 3, + "width": 30 + }, + { + "note": "├■ウワサ・MOB進行度等", + "width": 30, + "maxLines": 3 + } + ] +} \ No newline at end of file diff --git a/wolf_json/wrap_profile.json b/wolf_json/wrap_profile.json index 7bbe390..0391f20 100644 --- a/wolf_json/wrap_profile.json +++ b/wolf_json/wrap_profile.json @@ -28,4 +28,4 @@ "font": 30 } } -} +} \ No newline at end of file