Compare commits
6 commits
f25ce9f990
...
8f744fafbb
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f744fafbb | |||
| 8f5e62ac75 | |||
| 69f7ca7316 | |||
| 8fb9841eec | |||
| 7fd0bd648b | |||
| 4abb10df15 |
14 changed files with 1231 additions and 171 deletions
|
|
@ -325,7 +325,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
|||
| Step | Action |
|
||||
|------|--------|
|
||||
| **0 Project** | Select the game root folder (browse or Enter — detection also runs when you reopen the tab). If `wolf_json/` does not exist yet, the tool automatically unpacks `.wolf` archives when needed and extracts text into the game's `wolf_json/` folder (maps, common events, databases, `Game.dat`, Evtext, and `names.json`). A checklist lists every JSON file in `wolf_json/`; tick the ones you want and click **Import** (or leave Step 0 — checked files auto-import into the tool's `files/` folder, matching the RPG Maker workflow). Extraction snapshots pristine binaries into `wolf_json/originals/` for idempotent inject in Step 7. |
|
||||
| **1 Pre-process** | Optional: **dazedformat** normalises JSON in `wolf_json/` and `files/` (`json.dump`, indent 4) for clean git diffs; **Copy gameupdate/** installs the updater scripts, patch scripts, and `.gitignore` into the game root for git-based patching. Paths auto-fill from Step 0. |
|
||||
| **1 Pre-process** | Optional: **dazedformat** normalises JSON in `wolf_json/` and `files/` (`json.dump`, indent 4) for clean git diffs; **Copy gameupdate/** installs the updater scripts, patch scripts, `.gitignore`, and `UberWolfCli.exe` into the game root for git-based patching (players auto-unpack `Data.wolf` on first GameUpdate). Paths auto-fill from Step 0. |
|
||||
| **2 Glossary** | Build `vocab.txt` before translating: copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save. Item/skill/enemy value names (`names.json`) are translated in Step 3 and harvested into `vocab.txt` automatically during Phase 0 — do not list them here. |
|
||||
| **3 Names** | Translate `names.json` (item/skill/enemy/map value names). WolfDawn tags each name with a per-entry **safety** badge (`safe`, `refs`, or `verify`). Phase 0 translates only `safe` entries; `refs` and `verify` names stay Japanese so inject skips them. Review the category breakdown for this game, pick **Translation mode** (Normal or Batch), and run **Translate safe names (Phase 0)**. |
|
||||
| **4 Database** | Review the **discovery summary** to see where this game's text lives (standard RPG sheets vs custom dialogue tables). Database sheets are classified as foundation (items, skills, descriptions — translate first) or narrative (custom event/profile sheets — translate after foundation). Use **Translate foundation DB** then **Translate narrative DB**, or tick specific sheets and run **Translate checked sheets only**. Optional: copy the **DB structure prompt** for an AI audit and import the returned JSON into `wolf_json/db_profile.json`. |
|
||||
|
|
|
|||
3
gameupdate/.gitignore
vendored
3
gameupdate/.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
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 {
|
||||
param([string]$ConfigPath)
|
||||
$cfg = @{ username = ''; repo = ''; branch = '' }
|
||||
$cfg = @{
|
||||
forge = 'gitlab'
|
||||
host = ''
|
||||
username = ''
|
||||
repo = ''
|
||||
branch = ''
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||
return $cfg
|
||||
}
|
||||
|
|
@ -206,7 +212,12 @@ function Read-PatchConfig {
|
|||
$k = $line.Substring(0, $eq).Trim().ToLowerInvariant()
|
||||
$v = if ($eq -lt $line.Length - 1) { $line.Substring($eq + 1).Trim() } else { '' }
|
||||
switch ($k) {
|
||||
'forge' { $cfg.forge = $v }
|
||||
'provider' { $cfg.forge = $v }
|
||||
'host' { $cfg.host = $v }
|
||||
'username' { $cfg.username = $v }
|
||||
'owner' { $cfg.username = $v }
|
||||
'org' { $cfg.username = $v }
|
||||
'repo' { $cfg.repo = $v }
|
||||
'branch' { $cfg.branch = $v }
|
||||
}
|
||||
|
|
@ -214,6 +225,115 @@ function Read-PatchConfig {
|
|||
return $cfg
|
||||
}
|
||||
|
||||
function Resolve-PatchForge {
|
||||
param([Parameter(Mandatory = $true)][hashtable]$Cfg)
|
||||
|
||||
$raw = ([string]$Cfg.forge).Trim().ToLowerInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) { $raw = 'gitlab' }
|
||||
|
||||
$kind = switch -Regex ($raw) {
|
||||
'^(gitlab|gl|gitgud)$' { 'gitlab' }
|
||||
'^(github|gh)$' { 'github' }
|
||||
'^(forgejo|gitea|fj|codeberg)$' { 'forgejo' }
|
||||
default { $null }
|
||||
}
|
||||
if (-not $kind) {
|
||||
throw ("PATCH_ERR:CONFIG:Unknown forge='{0}'. Use gitlab, github, or forgejo." -f $Cfg.forge)
|
||||
}
|
||||
|
||||
$hostName = ([string]$Cfg.host).Trim()
|
||||
$hostName = $hostName -replace '^https?://', ''
|
||||
$hostName = $hostName.TrimEnd('/')
|
||||
if ($hostName.Contains('/')) {
|
||||
$hostName = $hostName.Split('/')[0]
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($hostName)) {
|
||||
$hostName = switch ($kind) {
|
||||
'gitlab' { 'gitgud.io' }
|
||||
'github' { 'github.com' }
|
||||
'forgejo' { 'codeberg.org' }
|
||||
}
|
||||
}
|
||||
|
||||
$webBase = "https://$hostName"
|
||||
$apiBase = switch ($kind) {
|
||||
'gitlab' { "$webBase/api/v4" }
|
||||
'github' {
|
||||
if ($hostName -eq 'github.com') { 'https://api.github.com' }
|
||||
else { "$webBase/api/v3" }
|
||||
}
|
||||
'forgejo' { "$webBase/api/v1" }
|
||||
}
|
||||
|
||||
return @{
|
||||
kind = $kind
|
||||
host = $hostName
|
||||
webBase = $webBase
|
||||
apiBase = $apiBase
|
||||
username = ([string]$Cfg.username).Trim()
|
||||
repo = ([string]$Cfg.repo).Trim()
|
||||
branch = ([string]$Cfg.branch).Trim()
|
||||
}
|
||||
}
|
||||
|
||||
function New-PatchApiHeaders {
|
||||
param([Parameter(Mandatory = $true)][hashtable]$Resolved)
|
||||
$headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' }
|
||||
if ($Resolved.kind -eq 'github') {
|
||||
$headers['Accept'] = 'application/vnd.github+json'
|
||||
}
|
||||
return $headers
|
||||
}
|
||||
|
||||
function Get-PatchLatestSha {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][hashtable]$Resolved,
|
||||
[Parameter(Mandatory = $true)][hashtable]$Headers
|
||||
)
|
||||
$ownerEnc = [uri]::EscapeDataString($Resolved.username)
|
||||
$repoEnc = [uri]::EscapeDataString($Resolved.repo)
|
||||
$branchEnc = [uri]::EscapeDataString($Resolved.branch)
|
||||
|
||||
switch ($Resolved.kind) {
|
||||
'gitlab' {
|
||||
$id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo)
|
||||
$uri = "$($Resolved.apiBase)/projects/$id/repository/branches/$branchEnc"
|
||||
return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id
|
||||
}
|
||||
'github' {
|
||||
$uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/commits/$branchEnc"
|
||||
return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).sha
|
||||
}
|
||||
'forgejo' {
|
||||
$uri = "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/branches/$branchEnc"
|
||||
return [string](Invoke-RestMethod -Uri $uri -Headers $Headers).commit.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PatchArchiveUrl {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][hashtable]$Resolved,
|
||||
[Parameter(Mandatory = $true)][string]$Sha
|
||||
)
|
||||
$ownerEnc = [uri]::EscapeDataString($Resolved.username)
|
||||
$repoEnc = [uri]::EscapeDataString($Resolved.repo)
|
||||
$shaEnc = [uri]::EscapeDataString($Sha)
|
||||
|
||||
switch ($Resolved.kind) {
|
||||
'gitlab' {
|
||||
$id = [uri]::EscapeDataString($Resolved.username + '/' + $Resolved.repo)
|
||||
return "$($Resolved.apiBase)/projects/$id/repository/archive.zip?sha=$shaEnc"
|
||||
}
|
||||
'github' {
|
||||
return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/zipball/$shaEnc"
|
||||
}
|
||||
'forgejo' {
|
||||
return "$($Resolved.apiBase)/repos/$ownerEnc/$repoEnc/archive/$shaEnc.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SrpgUnpacker {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ExePath,
|
||||
|
|
@ -230,6 +350,140 @@ function Invoke-SrpgUnpacker {
|
|||
}
|
||||
}
|
||||
|
||||
function Test-WolfLooseDataReady {
|
||||
param([Parameter(Mandatory = $true)][string]$DataDir)
|
||||
if (-not (Test-Path -LiteralPath $DataDir -PathType Container)) {
|
||||
return $false
|
||||
}
|
||||
$markers = @(
|
||||
(Join-Path $DataDir 'BasicData\CommonEvent.dat'),
|
||||
(Join-Path $DataDir 'CommonEvent.dat')
|
||||
)
|
||||
foreach ($marker in $markers) {
|
||||
if (Test-Path -LiteralPath $marker -PathType Leaf) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
if (@(Get-ChildItem -LiteralPath $DataDir -Filter '*.mps' -File -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
return $true
|
||||
}
|
||||
$basic = Join-Path $DataDir 'BasicData'
|
||||
if (Test-Path -LiteralPath $basic -PathType Container) {
|
||||
if (@(Get-ChildItem -LiteralPath $basic -Filter '*.dat' -File -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Find-UberWolfCli {
|
||||
param([Parameter(Mandatory = $true)][string]$Root)
|
||||
$candidates = @(
|
||||
(Join-Path $Root 'UberWolfCli.exe'),
|
||||
(Join-Path $Root 'gameupdate\UberWolfCli.exe')
|
||||
)
|
||||
foreach ($path in $candidates) {
|
||||
if (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
return $path
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-WolfPreSetup {
|
||||
<#
|
||||
WOLF installs often ship Data.wolf, which overrides loose patched Data/ files.
|
||||
Unpack once with UberWolfCli (bundled in the patch repo), then retire Data.wolf
|
||||
so GameUpdate can copy injected .mps/.dat on top.
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)][string]$Root)
|
||||
|
||||
$dataWolf = $null
|
||||
foreach ($name in @('Data.wolf', 'data.wolf')) {
|
||||
$candidate = Join-Path $Root $name
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
$dataWolf = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $dataWolf) {
|
||||
return
|
||||
}
|
||||
|
||||
$dataDir = Join-Path $Root 'Data'
|
||||
if (-not (Test-Path -LiteralPath $dataDir -PathType Container)) {
|
||||
$dataDirAlt = Join-Path $Root 'data'
|
||||
if (Test-Path -LiteralPath $dataDirAlt -PathType Container) {
|
||||
$dataDir = $dataDirAlt
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-WolfLooseDataReady -DataDir $dataDir) {
|
||||
$bak = "$dataWolf.bak"
|
||||
if (-not (Test-Path -LiteralPath $bak)) {
|
||||
Write-Host '[Pre-Setup] Loose Data/ already present; retiring Data.wolf -> Data.wolf.bak'
|
||||
Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force
|
||||
}
|
||||
else {
|
||||
Write-Host '[Pre-Setup] Loose Data/ already present; removing leftover Data.wolf'
|
||||
Remove-Item -LiteralPath $dataWolf -Force
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
$cli = Find-UberWolfCli -Root $Root
|
||||
if (-not $cli) {
|
||||
Write-Host '[Pre-Setup] Data.wolf found but UberWolfCli.exe is missing.'
|
||||
Write-Host ' Copy UberWolfCli.exe into the game root (Step 1 gameupdate/) and re-run.'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host '[Pre-Setup] Unpacking Data.wolf with UberWolfCli (one-time)...'
|
||||
$code = Invoke-SrpgUnpacker -ExePath $cli -WorkingDirectory $Root -Arguments @('-o', $dataWolf)
|
||||
if ($code -ne 0) {
|
||||
Write-Host ('[Pre-Setup] ERROR: UberWolfCli exited with code {0}.' -f $code)
|
||||
return
|
||||
}
|
||||
|
||||
# UberWolf writes next to the archive as <stem>/ (Data/). Also accept data.wolf~ leftovers.
|
||||
$unpacked = Join-Path $Root 'Data'
|
||||
if (-not (Test-Path -LiteralPath $unpacked -PathType Container)) {
|
||||
$unpacked = Join-Path $Root 'data'
|
||||
}
|
||||
if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) {
|
||||
foreach ($alt in @('data.wolf~', 'Data.wolf~', 'Data~', 'data~')) {
|
||||
$altPath = Join-Path $Root $alt
|
||||
if (Test-Path -LiteralPath $altPath -PathType Container) {
|
||||
Write-Host ("[Pre-Setup] Promoting {0} -> Data\" -f $alt)
|
||||
if (Test-Path -LiteralPath (Join-Path $Root 'Data')) {
|
||||
Remove-FileOrDirectoryViaCmd -LiteralPath (Join-Path $Root 'Data')
|
||||
}
|
||||
Rename-Item -LiteralPath $altPath -NewName 'Data' -Force
|
||||
$unpacked = Join-Path $Root 'Data'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-WolfLooseDataReady -DataDir $unpacked)) {
|
||||
Write-Host '[Pre-Setup] ERROR: Unpack finished but loose Data/ still looks empty.'
|
||||
return
|
||||
}
|
||||
|
||||
$bak = "$dataWolf.bak"
|
||||
if (Test-Path -LiteralPath $dataWolf -PathType Leaf) {
|
||||
if (-not (Test-Path -LiteralPath $bak)) {
|
||||
Write-Host '[Pre-Setup] Retiring Data.wolf -> Data.wolf.bak'
|
||||
Rename-Item -LiteralPath $dataWolf -NewName ([IO.Path]::GetFileName($bak)) -Force
|
||||
}
|
||||
else {
|
||||
Write-Host '[Pre-Setup] Removing Data.wolf so loose Data/ takes priority'
|
||||
Remove-Item -LiteralPath $dataWolf -Force
|
||||
}
|
||||
}
|
||||
Write-Host '[Pre-Setup] Wolf Data/ is ready for patching.'
|
||||
}
|
||||
|
||||
function Invoke-SrpgPreSetup {
|
||||
param([string]$Root)
|
||||
$unpacker = Join-Path $Root 'SRPG_Unpacker.exe'
|
||||
|
|
@ -512,9 +766,7 @@ function Invoke-PatchDownloadExtract {
|
|||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Root,
|
||||
[Parameter(Mandatory = $true)][string]$StateFilePath,
|
||||
[Parameter(Mandatory = $true)][string]$Username,
|
||||
[Parameter(Mandatory = $true)][string]$Repo,
|
||||
[Parameter(Mandatory = $true)][string]$Branch
|
||||
[Parameter(Mandatory = $true)][hashtable]$Resolved
|
||||
)
|
||||
|
||||
if ($PSVersionTable.PSEdition -ne 'Core') {
|
||||
|
|
@ -526,13 +778,11 @@ function Invoke-PatchDownloadExtract {
|
|||
|
||||
Set-Location -LiteralPath $Root
|
||||
|
||||
$id = [uri]::EscapeDataString($Username + '/' + $Repo)
|
||||
$branchEnc = [uri]::EscapeDataString($Branch)
|
||||
$headers = @{ 'User-Agent' = 'DazedMTL-Patcher-1.0' }
|
||||
$headers = New-PatchApiHeaders -Resolved $Resolved
|
||||
$dlAttempts = Get-EnvInt -Name 'GAMEUPDATE_DL_ATTEMPTS' -Default 2
|
||||
|
||||
$latestSha = Invoke-WithRetry -Name 'Resolve latest patch SHA' -Attempts $dlAttempts -Action {
|
||||
(Invoke-RestMethod -Uri ("https://gitgud.io/api/v4/projects/$id/repository/branches/$branchEnc") -Headers $headers).commit.id
|
||||
Get-PatchLatestSha -Resolved $Resolved -Headers $headers
|
||||
}
|
||||
if (-not $latestSha) { throw 'PATCH_ERR:API:Latest commit SHA response was empty.' }
|
||||
$latestSha = ([string]$latestSha).Trim()
|
||||
|
|
@ -551,8 +801,7 @@ function Invoke-PatchDownloadExtract {
|
|||
$zipPath = Join-Path $Root ('dazedmtl_patch_' + [Guid]::NewGuid().ToString('N') + '.zip')
|
||||
Write-Host ('Downloading patch... ' + $zipPath)
|
||||
$stage = Join-Path ([IO.Path]::GetTempPath()) ('gu_' + [Guid]::NewGuid().ToString('N'))
|
||||
$archiveSha = [uri]::EscapeDataString($latestSha)
|
||||
$archiveUrl = "https://gitgud.io/api/v4/projects/$id/repository/archive.zip?sha=$archiveSha"
|
||||
$archiveUrl = Get-PatchArchiveUrl -Resolved $Resolved -Sha $latestSha
|
||||
|
||||
$dlSw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
Invoke-WithRetry -Name 'Download archive with Invoke-WebRequest' -Attempts $dlAttempts -Action {
|
||||
|
|
@ -599,7 +848,21 @@ function Invoke-PatchDownloadExtract {
|
|||
if ($dirs.Count -ne 1) {
|
||||
throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count)
|
||||
}
|
||||
Copy-Item -Path (Join-Path $dirs[0].FullName '*') -Destination $PWD.Path -Recurse -Force
|
||||
$stagedRoot = $dirs[0].FullName
|
||||
|
||||
# Stage UberWolfCli into the game root before unpack so first-time
|
||||
# players can convert Data.wolf -> Data/ before English files land.
|
||||
foreach ($cliName in @('UberWolfCli.exe', 'UberWolfCli.LICENSE.txt')) {
|
||||
$stagedCli = Join-Path $stagedRoot $cliName
|
||||
$destCli = Join-Path $Root $cliName
|
||||
if ((Test-Path -LiteralPath $stagedCli -PathType Leaf) -and
|
||||
-not (Test-Path -LiteralPath $destCli -PathType Leaf)) {
|
||||
Copy-Item -LiteralPath $stagedCli -Destination $destCli -Force
|
||||
}
|
||||
}
|
||||
Invoke-WolfPreSetup -Root $Root
|
||||
|
||||
Copy-Item -Path (Join-Path $stagedRoot '*') -Destination $PWD.Path -Recurse -Force
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $zipPath) {
|
||||
|
|
@ -717,8 +980,19 @@ try {
|
|||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ('Pulling patch from https://gitgud.io/{0}/{1} (branch: {2})' -f $cfg.username, $cfg.repo, $cfg.branch)
|
||||
try {
|
||||
$resolved = Resolve-PatchForge -Cfg $cfg
|
||||
}
|
||||
catch {
|
||||
Write-Host $_.Exception.Message
|
||||
Wait-ConsolePause
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ('Pulling patch from {0}/{1}/{2} ({3}, branch: {4})' -f `
|
||||
$resolved.webBase, $resolved.username, $resolved.repo, $resolved.kind, $resolved.branch)
|
||||
|
||||
Invoke-WolfPreSetup -Root $GameRoot
|
||||
Invoke-SrpgPreSetup -Root $GameRoot
|
||||
|
||||
$stateFile = Join-Path $PatchBundleRoot 'previous_patch_sha.txt'
|
||||
|
|
@ -726,7 +1000,7 @@ try {
|
|||
$patchDlExit = 1
|
||||
try {
|
||||
$patchDlExit = Invoke-PatchDownloadExtract -Root $GameRoot -StateFilePath $stateFile `
|
||||
-Username $cfg.username -Repo $cfg.repo -Branch $cfg.branch
|
||||
-Resolved $resolved
|
||||
}
|
||||
catch {
|
||||
Write-Host ''
|
||||
|
|
@ -753,6 +1027,9 @@ try {
|
|||
Write-Host 'Applying patch...'
|
||||
|
||||
Invoke-SrpgPostApply -Root $GameRoot
|
||||
# Safety: if Data/ is ready (from unpack or patch files) but Data.wolf
|
||||
# remains, retire it so the archive does not override loose English files.
|
||||
Invoke-WolfPreSetup -Root $GameRoot
|
||||
|
||||
Write-Host 'Cleaning up...'
|
||||
Remove-PatchTempArtifacts -Root $GameRoot
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -585,6 +585,62 @@ class ConfigTab(QWidget):
|
|||
ui_form.addRow(font_scale_label, self.font_scale_spin)
|
||||
|
||||
right_column.addLayout(ui_form)
|
||||
|
||||
# Game Update defaults (written into gameupdate/patch-config.txt on copy)
|
||||
right_column.addWidget(create_section_header("📦 Game Update Defaults"))
|
||||
gu_form = QFormLayout()
|
||||
gu_form.setSpacing(6)
|
||||
gu_form.setContentsMargins(0, 0, 0, 12)
|
||||
gu_form.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint)
|
||||
gu_form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
|
||||
def _gu_label(text: str) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
lbl.setFixedWidth(150)
|
||||
lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
return lbl
|
||||
|
||||
self.gu_forge_combo = QComboBox()
|
||||
self.gu_forge_combo.addItem("GitLab / gitgud", "gitlab")
|
||||
self.gu_forge_combo.addItem("GitHub", "github")
|
||||
self.gu_forge_combo.addItem("Forgejo / Gitea", "forgejo")
|
||||
self.gu_forge_combo.setFixedWidth(220)
|
||||
self.gu_forge_combo.setToolTip(
|
||||
"Where your patch repos live. Used when Step 1 writes patch-config.txt."
|
||||
)
|
||||
self.gu_forge_combo.currentIndexChanged.connect(self._on_gu_forge_changed)
|
||||
gu_form.addRow(_gu_label("Forge:"), self.gu_forge_combo)
|
||||
|
||||
self.gu_host_edit = QLineEdit()
|
||||
self.gu_host_edit.setFixedWidth(220)
|
||||
self.gu_host_edit.setPlaceholderText("gitgud.io")
|
||||
self.gu_host_edit.setToolTip(
|
||||
"Hostname only (no https://). Leave blank to use the forge default."
|
||||
)
|
||||
gu_form.addRow(_gu_label("Host:"), self.gu_host_edit)
|
||||
|
||||
self.gu_username_edit = QLineEdit()
|
||||
self.gu_username_edit.setFixedWidth(220)
|
||||
self.gu_username_edit.setPlaceholderText("your-org-or-user")
|
||||
self.gu_username_edit.setToolTip(
|
||||
"Owner / org / group for all patch repos. Set once; rarely changes."
|
||||
)
|
||||
gu_form.addRow(_gu_label("Org / username:"), self.gu_username_edit)
|
||||
|
||||
self.gu_branch_edit = QLineEdit()
|
||||
self.gu_branch_edit.setFixedWidth(220)
|
||||
self.gu_branch_edit.setPlaceholderText("main")
|
||||
self.gu_branch_edit.setToolTip("Default branch to track (usually main or master).")
|
||||
gu_form.addRow(_gu_label("Branch:"), self.gu_branch_edit)
|
||||
|
||||
gu_hint = QLabel(
|
||||
"Copied into each game's gameupdate/patch-config.txt when you run "
|
||||
"Copy gameupdate/ (repo= is still per-game)."
|
||||
)
|
||||
gu_hint.setWordWrap(True)
|
||||
gu_hint.setStyleSheet("color:#7a7a7a;font-size:11px;")
|
||||
right_column.addLayout(gu_form)
|
||||
right_column.addWidget(gu_hint)
|
||||
right_column.addStretch()
|
||||
|
||||
# Add columns to layout
|
||||
|
|
@ -725,7 +781,39 @@ class ConfigTab(QWidget):
|
|||
|
||||
# UI settings
|
||||
self.font_scale_spin.setValue(float(_get("font_scale", "1.0")))
|
||||
|
||||
|
||||
# Game Update defaults
|
||||
from util.gameupdate_config import (
|
||||
DEFAULT_BRANCH,
|
||||
DEFAULT_FORGE,
|
||||
default_host_for_forge,
|
||||
normalize_forge,
|
||||
)
|
||||
|
||||
forge = normalize_forge(_get("gameUpdateForge", DEFAULT_FORGE))
|
||||
forge_idx = self.gu_forge_combo.findData(forge)
|
||||
self.gu_forge_combo.setCurrentIndex(forge_idx if forge_idx >= 0 else 0)
|
||||
host = _get("gameUpdateHost", "") or default_host_for_forge(forge)
|
||||
self.gu_host_edit.setText(host)
|
||||
self.gu_username_edit.setText(_get("gameUpdateUsername", ""))
|
||||
self.gu_branch_edit.setText(_get("gameUpdateBranch", DEFAULT_BRANCH) or DEFAULT_BRANCH)
|
||||
|
||||
def _on_gu_forge_changed(self, _index: int = 0):
|
||||
"""Fill host with the forge default when the user switches forge."""
|
||||
from util.gameupdate_config import default_host_for_forge
|
||||
|
||||
forge = self.gu_forge_combo.currentData() or "gitlab"
|
||||
current = self.gu_host_edit.text().strip()
|
||||
defaults = {
|
||||
"gitgud.io",
|
||||
"gitlab.com",
|
||||
"github.com",
|
||||
"codeberg.org",
|
||||
"",
|
||||
}
|
||||
if current.lower() in defaults:
|
||||
self.gu_host_edit.setText(default_host_for_forge(str(forge)))
|
||||
|
||||
def connect_auto_save(self):
|
||||
"""Connect all widgets to auto-save on change."""
|
||||
# Text fields - use editingFinished to avoid saving on every keystroke
|
||||
|
|
@ -750,6 +838,10 @@ class ConfigTab(QWidget):
|
|||
self.input_cost_spin.editingFinished.connect(self.auto_save)
|
||||
self.output_cost_spin.editingFinished.connect(self.auto_save)
|
||||
self.font_scale_spin.editingFinished.connect(self.auto_save)
|
||||
self.gu_forge_combo.currentIndexChanged.connect(self.auto_save)
|
||||
self.gu_host_edit.editingFinished.connect(self.auto_save)
|
||||
self.gu_username_edit.editingFinished.connect(self.auto_save)
|
||||
self.gu_branch_edit.editingFinished.connect(self.auto_save)
|
||||
|
||||
def disconnect_auto_save(self):
|
||||
"""Disconnect all widgets from auto-save."""
|
||||
|
|
@ -770,6 +862,10 @@ class ConfigTab(QWidget):
|
|||
self.input_cost_spin.editingFinished.disconnect(self.auto_save)
|
||||
self.output_cost_spin.editingFinished.disconnect(self.auto_save)
|
||||
self.font_scale_spin.editingFinished.disconnect(self.auto_save)
|
||||
self.gu_forge_combo.currentIndexChanged.disconnect(self.auto_save)
|
||||
self.gu_host_edit.editingFinished.disconnect(self.auto_save)
|
||||
self.gu_username_edit.editingFinished.disconnect(self.auto_save)
|
||||
self.gu_branch_edit.editingFinished.disconnect(self.auto_save)
|
||||
except (TypeError, RuntimeError):
|
||||
pass
|
||||
|
||||
|
|
@ -808,6 +904,10 @@ class ConfigTab(QWidget):
|
|||
"input_cost": str(self.input_cost_spin.value()),
|
||||
"output_cost": str(self.output_cost_spin.value()),
|
||||
"font_scale": str(self.font_scale_spin.value()),
|
||||
"gameUpdateForge": str(self.gu_forge_combo.currentData() or "gitlab"),
|
||||
"gameUpdateHost": self.gu_host_edit.text().strip(),
|
||||
"gameUpdateUsername": self.gu_username_edit.text().strip(),
|
||||
"gameUpdateBranch": self.gu_branch_edit.text().strip() or "main",
|
||||
}
|
||||
|
||||
# Save to .env file and update os.environ so subprocesses inherit new values
|
||||
|
|
@ -867,6 +967,15 @@ class ConfigTab(QWidget):
|
|||
|
||||
# UI settings
|
||||
self.font_scale_spin.setValue(1.0)
|
||||
|
||||
# Game Update defaults
|
||||
from util.gameupdate_config import DEFAULT_BRANCH, DEFAULT_FORGE, DEFAULT_HOST
|
||||
|
||||
forge_idx = self.gu_forge_combo.findData(DEFAULT_FORGE)
|
||||
self.gu_forge_combo.setCurrentIndex(forge_idx if forge_idx >= 0 else 0)
|
||||
self.gu_host_edit.setText(DEFAULT_HOST)
|
||||
self.gu_username_edit.clear()
|
||||
self.gu_branch_edit.setText(DEFAULT_BRANCH)
|
||||
|
||||
self.connect_auto_save()
|
||||
|
||||
|
|
@ -902,6 +1011,10 @@ class ConfigTab(QWidget):
|
|||
"input_cost": self.input_cost_spin.value(),
|
||||
"output_cost": self.output_cost_spin.value(),
|
||||
"font_scale": self.font_scale_spin.value(),
|
||||
"gameUpdateForge": str(self.gu_forge_combo.currentData() or "gitlab"),
|
||||
"gameUpdateHost": self.gu_host_edit.text().strip(),
|
||||
"gameUpdateUsername": self.gu_username_edit.text().strip(),
|
||||
"gameUpdateBranch": self.gu_branch_edit.text().strip() or "main",
|
||||
}
|
||||
|
||||
def validate(self):
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
|||
Step 7 Inject - inject every JSON into Data/ from translated/ (default)
|
||||
or from the game's wolf_json/; translated inject also
|
||||
refreshes wolf_json/ (full pass avoids name/DB wipe bugs);
|
||||
wolf_json inject optionally copies those JSON into files/
|
||||
and translated/ so tool progress is not left behind;
|
||||
optional layout-restore re-applies source whitespace pads
|
||||
Step 8 Package - run from a loose Data/ folder or repack Data.wolf;
|
||||
optional save rewrite for existing .sav files
|
||||
|
|
@ -1028,7 +1030,9 @@ class WolfWorkflowTab(QWidget):
|
|||
tc_inner.setSpacing(4)
|
||||
tc_desc = QLabel(
|
||||
"Copies everything from the gameupdate/ folder into the game's root folder, "
|
||||
"overwriting existing files (updater scripts, patch scripts, .gitignore)."
|
||||
"overwriting existing files (updater scripts, patch scripts, .gitignore, "
|
||||
"and UberWolfCli.exe). Writes patch-config.txt from Config → Game Update "
|
||||
"defaults (set your org once there; edit repo= per game)."
|
||||
)
|
||||
tc_desc.setWordWrap(True)
|
||||
tc_desc.setStyleSheet("color:#9d9d9d;font-size:13px;")
|
||||
|
|
@ -1405,6 +1409,10 @@ class WolfWorkflowTab(QWidget):
|
|||
self._last_import_signature = self._pending_import_signature
|
||||
self._pending_import_signature = None
|
||||
self._log(f"✅ Imported {count} file(s) into files/")
|
||||
# Step 4 discovery scans files/; refresh if we're already on Database
|
||||
# (auto-import from Step 0 often finishes after the first discovery pass).
|
||||
if getattr(self, "_current_step_index", None) == 4:
|
||||
self._refresh_db_discovery()
|
||||
|
||||
def _populate_preprocess_paths(self):
|
||||
game_root = self.folder_edit.text().strip()
|
||||
|
|
@ -1450,6 +1458,16 @@ class WolfWorkflowTab(QWidget):
|
|||
|
||||
self._run_task(task)
|
||||
|
||||
def _write_gameupdate_patch_config(self, game_root: str):
|
||||
"""Write gameupdate/patch-config.txt from Config → Game Update defaults."""
|
||||
from util.gameupdate_config import write_patch_config
|
||||
|
||||
ok, msg = write_patch_config(game_root)
|
||||
if ok:
|
||||
self._log(f"📝 Wrote patch-config.txt from Config defaults → {msg}")
|
||||
else:
|
||||
self._log(f"ℹ patch-config.txt: {msg}")
|
||||
|
||||
def _run_gameupdate(self):
|
||||
src = self.pp_gameupdate_edit.text().strip()
|
||||
dst = self.folder_edit.text().strip()
|
||||
|
|
@ -1472,6 +1490,9 @@ class WolfWorkflowTab(QWidget):
|
|||
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||
for e in errors:
|
||||
self._log(f" ⚠ {e}")
|
||||
dst = self.folder_edit.text().strip()
|
||||
if dst and not errors:
|
||||
self._write_gameupdate_patch_config(dst)
|
||||
|
||||
def _run_all_preprocess(self):
|
||||
if not self._game_root:
|
||||
|
|
@ -1517,6 +1538,13 @@ class WolfWorkflowTab(QWidget):
|
|||
except Exception as exc:
|
||||
errors.append(f"{rel}: {exc}")
|
||||
log(f" Copied {copied} gameupdate file(s).")
|
||||
from util.gameupdate_config import write_patch_config
|
||||
|
||||
ok, msg = write_patch_config(dst)
|
||||
if ok:
|
||||
log(f" Wrote patch-config.txt from Config defaults → {msg}")
|
||||
else:
|
||||
log(f" patch-config.txt: {msg}")
|
||||
if errors:
|
||||
return False, "Pre-process finished with errors (see log)."
|
||||
return True, "Pre-process: dazedformat and gameupdate copy finished."
|
||||
|
|
@ -1817,13 +1845,17 @@ class WolfWorkflowTab(QWidget):
|
|||
# ── Step 4: Database ───────────────────────────────────────────────────────
|
||||
|
||||
def _build_step4_database(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 4 · Database (discover + translate)"))
|
||||
layout.addWidget(self._desc(
|
||||
layout.setSpacing(6)
|
||||
title = _make_section_label("Step 4 · Database")
|
||||
title.setToolTip(
|
||||
"WOLF database files (DataBase, CDataBase, SysDatabase) hold item/skill "
|
||||
"descriptions in classic RPG games, but many games store most dialogue in "
|
||||
"custom database sheets instead of maps. Review the discovery summary below, "
|
||||
"then translate foundation sheets (items, skills, system text) before "
|
||||
"narrative custom sheets."
|
||||
"custom database sheets instead of maps. Review discovery, then translate "
|
||||
"foundation sheets (items, skills, system text) before narrative custom sheets."
|
||||
)
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(self._desc(
|
||||
"Review discovery → tick sheets → translate foundation first, then narrative."
|
||||
))
|
||||
|
||||
self.db_discovery_label = QLabel(
|
||||
|
|
@ -1831,17 +1863,21 @@ class WolfWorkflowTab(QWidget):
|
|||
)
|
||||
self.db_discovery_label.setWordWrap(True)
|
||||
self.db_discovery_label.setStyleSheet(
|
||||
"color:#9cdcfe;font-size:13px;padding:8px 10px;"
|
||||
"color:#9cdcfe;font-size:12px;padding:6px 8px;"
|
||||
"background-color:#1a2430;border:1px solid #2a4a6a;border-radius:4px;"
|
||||
)
|
||||
layout.addWidget(self.db_discovery_label)
|
||||
|
||||
disc_row = QHBoxLayout()
|
||||
refresh_disc_btn = _make_btn("↻ Refresh discovery", "#555")
|
||||
disc_row.setSpacing(6)
|
||||
refresh_disc_btn = _make_btn("↻ Refresh", "#555")
|
||||
refresh_disc_btn.setToolTip("Re-scan files/ for database / map / CommonEvent line counts.")
|
||||
refresh_disc_btn.clicked.connect(self._refresh_db_discovery)
|
||||
audit_btn = _make_btn("📋 Copy DB structure prompt for AI audit", "#5a3a7a")
|
||||
audit_btn = _make_btn("📋 Copy AI audit prompt", "#5a3a7a")
|
||||
audit_btn.setToolTip("Copy a DB structure prompt for Cursor/Copilot to audit sheet roles.")
|
||||
audit_btn.clicked.connect(self._copy_db_audit_prompt)
|
||||
import_btn = _make_btn("Import AI profile JSON", "#3a3a3a")
|
||||
import_btn = _make_btn("Import AI profile", "#3a3a3a")
|
||||
import_btn.setToolTip("Paste JSON returned by the AI audit into db_profile.json.")
|
||||
import_btn.clicked.connect(self._import_db_profile_dialog)
|
||||
disc_row.addWidget(refresh_disc_btn)
|
||||
disc_row.addWidget(audit_btn)
|
||||
|
|
@ -1849,26 +1885,25 @@ class WolfWorkflowTab(QWidget):
|
|||
disc_row.addStretch()
|
||||
layout.addLayout(disc_row)
|
||||
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Database sheets"))
|
||||
layout.addWidget(self._desc(
|
||||
"Each sheet is classified as foundation (translate first), system, or "
|
||||
"narrative (custom dialogue - defer until foundation is done). Tick the "
|
||||
"sheets to include in the next database translation run."
|
||||
))
|
||||
|
||||
self._db_groups_list = QListWidget()
|
||||
self._db_groups_list.setMinimumHeight(200)
|
||||
self._db_groups_list.setMinimumHeight(160)
|
||||
self._db_groups_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._db_groups_list.setStyleSheet(
|
||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||
"QListWidget::item{padding:3px 4px;}"
|
||||
"color:#cccccc;font-size:12px;padding:1px;}"
|
||||
"QListWidget::item{padding:2px 4px;}"
|
||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
||||
)
|
||||
self._db_groups_list.itemChanged.connect(self._save_db_profile_from_ui)
|
||||
layout.addWidget(self._db_groups_list)
|
||||
self._db_groups_list.setToolTip(
|
||||
"Each sheet is foundation (translate first), system, or narrative "
|
||||
"(custom dialogue - defer until foundation is done). Tick sheets for "
|
||||
"the next database translation run."
|
||||
)
|
||||
layout.addWidget(self._db_groups_list, 1)
|
||||
|
||||
grp_row = QHBoxLayout()
|
||||
grp_row.setSpacing(6)
|
||||
sel_found_btn = _make_btn("Select foundation", "#3a3a3a")
|
||||
sel_found_btn.clicked.connect(self._select_db_foundation_groups)
|
||||
sel_narr_btn = _make_btn("Select narrative", "#3a3a3a")
|
||||
|
|
@ -1884,32 +1919,36 @@ class WolfWorkflowTab(QWidget):
|
|||
grp_row.addStretch()
|
||||
layout.addLayout(grp_row)
|
||||
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Translate database"))
|
||||
layout.addWidget(self._desc(
|
||||
"Run after Step 3 (names) so vocab.txt stays consistent. Only checked sheets "
|
||||
"are sent to the model. Re-running skips lines already translated. Short "
|
||||
"foundation labels (map names, titles) are merged into vocab.txt."
|
||||
))
|
||||
self._add_tl_mode_selector(layout)
|
||||
|
||||
found_btn = self._register(_make_btn("▶ Translate foundation DB", "#00a86b"))
|
||||
tl_row = QHBoxLayout()
|
||||
tl_row.setSpacing(6)
|
||||
found_btn = self._register(_make_btn("▶ Foundation DB", "#00a86b"))
|
||||
found_btn.setToolTip(
|
||||
"Translate foundation/system sheets (items, skills, system text). "
|
||||
"Run after Step 3 (names). Re-running skips lines already translated."
|
||||
)
|
||||
found_btn.clicked.connect(
|
||||
lambda: self._translate_db_tiers(wolf_db.FOUNDATION_TIERS, auto_start=True)
|
||||
)
|
||||
layout.addWidget(found_btn)
|
||||
|
||||
narr_btn = self._register(_make_btn("▶ Translate narrative DB", "#00a86b"))
|
||||
narr_btn = self._register(_make_btn("▶ Narrative DB", "#00a86b"))
|
||||
narr_btn.setToolTip(
|
||||
"Translate narrative custom sheets after foundation is done. "
|
||||
"Short foundation labels are merged into vocab.txt."
|
||||
)
|
||||
narr_btn.clicked.connect(
|
||||
lambda: self._translate_db_tiers(wolf_db.NARRATIVE_TIERS, auto_start=True)
|
||||
)
|
||||
layout.addWidget(narr_btn)
|
||||
|
||||
checked_btn = self._register(_make_btn("▶ Translate checked sheets only", "#3a3a3a"))
|
||||
checked_btn = self._register(_make_btn("▶ Checked sheets", "#3a3a3a"))
|
||||
checked_btn.setToolTip("Translate only the sheets currently ticked above.")
|
||||
checked_btn.clicked.connect(
|
||||
lambda: self._translate_db_checked(auto_start=True)
|
||||
)
|
||||
layout.addWidget(checked_btn)
|
||||
tl_row.addWidget(found_btn)
|
||||
tl_row.addWidget(narr_btn)
|
||||
tl_row.addWidget(checked_btn)
|
||||
tl_row.addStretch()
|
||||
layout.addLayout(tl_row)
|
||||
|
||||
self._db_content_dist: wolf_db.ContentDistribution | None = None
|
||||
# Defer discovery until the step is shown - files/ may hold RPGMaker JSON.
|
||||
|
|
@ -2435,39 +2474,38 @@ class WolfWorkflowTab(QWidget):
|
|||
# ── Step 6: Precheck ───────────────────────────────────────────────────────
|
||||
|
||||
def _build_step6_precheck(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 6 · Inject Precheck"))
|
||||
layout.setSpacing(6)
|
||||
title = _make_section_label("Step 6 · Inject Precheck")
|
||||
title.setToolTip(
|
||||
"Reconcile names.json → glossary spellings, report name inconsistencies, "
|
||||
"then dry-run selected JSON for safety skips (control-code mismatch, or "
|
||||
"text not Shift-JIS encodable). Fix safety rows here, then Step 7 (Inject)."
|
||||
)
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(self._desc(
|
||||
"Before inject: reconcile names.json → extract glossary spellings, "
|
||||
"report name inconsistencies, then dry-run the selected JSON for real "
|
||||
"safety skips (control-code mismatch, or text not Shift-JIS encodable). "
|
||||
"Fix safety rows here, then continue to Step 7 (Inject)."
|
||||
"Pick files → Precheck → fix safety skips below → Step 7 Inject."
|
||||
))
|
||||
|
||||
layout.addWidget(self._subheading("Translated files"))
|
||||
self.precheck_list = QListWidget()
|
||||
self.precheck_list.setMaximumHeight(200)
|
||||
self.precheck_list.setMinimumHeight(72)
|
||||
self.precheck_list.setMaximumHeight(120)
|
||||
self.precheck_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
self.precheck_list.setStyleSheet(
|
||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||
"QListWidget::item{padding:3px 4px;}"
|
||||
"color:#cccccc;font-size:12px;padding:1px;}"
|
||||
"QListWidget::item{padding:2px 4px;}"
|
||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
||||
)
|
||||
layout.addWidget(self.precheck_list)
|
||||
|
||||
list_row = QHBoxLayout()
|
||||
list_row.setSpacing(6)
|
||||
refresh_btn = self._register(_make_btn("↻ Refresh", "#3a3a3a"))
|
||||
refresh_btn.clicked.connect(self._refresh_precheck_list)
|
||||
sel_all_btn = self._register(_make_btn("Select all", "#3a3a3a"))
|
||||
sel_all_btn.clicked.connect(lambda: self._set_checklist_checks(self.precheck_list, True))
|
||||
sel_none_btn = self._register(_make_btn("Select none", "#3a3a3a"))
|
||||
sel_none_btn.clicked.connect(lambda: self._set_checklist_checks(self.precheck_list, False))
|
||||
list_row.addWidget(refresh_btn)
|
||||
list_row.addWidget(sel_all_btn)
|
||||
list_row.addWidget(sel_none_btn)
|
||||
list_row.addStretch()
|
||||
layout.addLayout(list_row)
|
||||
|
||||
precheck_row = QHBoxLayout()
|
||||
precheck_btn = self._register(_make_btn("Precheck selected", "#007acc"))
|
||||
precheck_btn.setToolTip(
|
||||
"Reconcile names → glossary, run names-check, then dry-run inject "
|
||||
|
|
@ -2479,25 +2517,32 @@ class WolfWorkflowTab(QWidget):
|
|||
"Same as Precheck selected, for every injectable file in translated/."
|
||||
)
|
||||
precheck_all_btn.clicked.connect(self._precheck_inject_all)
|
||||
precheck_row.addWidget(precheck_btn)
|
||||
precheck_row.addWidget(precheck_all_btn)
|
||||
precheck_row.addStretch()
|
||||
layout.addLayout(precheck_row)
|
||||
list_row.addWidget(refresh_btn)
|
||||
list_row.addWidget(sel_all_btn)
|
||||
list_row.addWidget(sel_none_btn)
|
||||
list_row.addStretch()
|
||||
list_row.addWidget(precheck_btn)
|
||||
list_row.addWidget(precheck_all_btn)
|
||||
layout.addLayout(list_row)
|
||||
|
||||
self._inject_precheck_label = QLabel("Run precheck to list safety-guard skips.")
|
||||
self._inject_precheck_label.setWordWrap(True)
|
||||
self._inject_precheck_label.setStyleSheet("color:#9cdcfe;font-size:12px;")
|
||||
self._inject_precheck_label.setStyleSheet(
|
||||
"color:#9cdcfe;font-size:11px;background:transparent;padding:0;"
|
||||
)
|
||||
layout.addWidget(self._inject_precheck_label)
|
||||
|
||||
self._inject_issue_list = QListWidget()
|
||||
self._inject_issue_list.setMaximumHeight(200)
|
||||
self._inject_issue_list.setMinimumHeight(80)
|
||||
self._inject_issue_list.setMaximumHeight(140)
|
||||
self._inject_issue_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
self._inject_issue_list.setWordWrap(True)
|
||||
self._inject_issue_list.setTextElideMode(Qt.ElideNone)
|
||||
self._inject_issue_list.setUniformItemSizes(False)
|
||||
self._inject_issue_list.setStyleSheet(
|
||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||
"QListWidget::item{padding:4px;}"
|
||||
"color:#cccccc;font-size:12px;padding:1px;}"
|
||||
"QListWidget::item{padding:3px 6px;}"
|
||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
||||
)
|
||||
self._inject_issue_list.currentItemChanged.connect(self._on_inject_issue_selected)
|
||||
|
|
@ -2505,22 +2550,26 @@ class WolfWorkflowTab(QWidget):
|
|||
|
||||
self._inject_issue_meta = QLabel("")
|
||||
self._inject_issue_meta.setWordWrap(True)
|
||||
self._inject_issue_meta.setStyleSheet("color:#808080;font-size:11px;")
|
||||
self._inject_issue_meta.setStyleSheet(
|
||||
"color:#808080;font-size:11px;background:transparent;padding:0;"
|
||||
)
|
||||
layout.addWidget(self._inject_issue_meta)
|
||||
|
||||
self._inject_issue_edit = QTextEdit()
|
||||
self._inject_issue_edit.setPlaceholderText(
|
||||
"Select a precheck row to edit its translated text, then Save line."
|
||||
)
|
||||
self._inject_issue_edit.setMaximumHeight(120)
|
||||
self._inject_issue_edit.setMinimumHeight(100)
|
||||
self._inject_issue_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._inject_issue_edit.setStyleSheet(
|
||||
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;border:1px solid #3c3c3c;"
|
||||
"border-radius:4px;padding:6px;font-size:12px;}"
|
||||
"border-left:3px solid #007acc;border-radius:0;padding:4px;font-size:12px;}"
|
||||
)
|
||||
layout.addWidget(self._inject_issue_edit)
|
||||
layout.addWidget(self._inject_issue_edit, 1)
|
||||
|
||||
edit_row = QHBoxLayout()
|
||||
save_line_btn = self._register(_make_btn("Save line", "#3a3a3a"))
|
||||
edit_row.setSpacing(6)
|
||||
save_line_btn = self._register(_make_btn("Save line", "#00a86b"))
|
||||
save_line_btn.clicked.connect(self._save_inject_issue_line)
|
||||
edit_row.addWidget(save_line_btn)
|
||||
edit_row.addStretch()
|
||||
|
|
@ -2536,7 +2585,8 @@ class WolfWorkflowTab(QWidget):
|
|||
layout.addWidget(self._desc(
|
||||
"Inject every JSON into the game's Data/ binaries in one pass. "
|
||||
"Inject all uses translated/; Inject from wolf_json uses the game's "
|
||||
"wolf_json/ when that folder is the source of truth. "
|
||||
"wolf_json/ when that folder is the source of truth (and can copy those "
|
||||
"JSON into files/ and translated/ so tool progress is not left behind). "
|
||||
"A full inject keeps names.json and DB/map files in sync (partial injects "
|
||||
"can wipe name-only fields like rumor boards). Run Step 6 (Precheck) first "
|
||||
"if you want to catch safety-guard skips before writing."
|
||||
|
|
@ -2589,8 +2639,8 @@ class WolfWorkflowTab(QWidget):
|
|||
inject_wolf_btn.setToolTip(
|
||||
"Inject every injectable JSON from the game's wolf_json/ folder instead "
|
||||
"of translated/. Use when wolf_json/ is the source of truth (e.g. after "
|
||||
"editing or restoring committed game-repo JSON). Does not copy from "
|
||||
"translated/."
|
||||
"editing or restoring committed game-repo JSON). Asks whether to copy "
|
||||
"those JSON into files/ and translated/ so tool progress is not left behind."
|
||||
)
|
||||
inject_wolf_btn.clicked.connect(self._inject_all_from_wolf_json)
|
||||
layout_restore_btn = self._register(_make_btn("Layout-restore", "#3a3a3a"))
|
||||
|
|
@ -2615,15 +2665,21 @@ class WolfWorkflowTab(QWidget):
|
|||
|
||||
def _build_step9_relayout(self, layout: QVBoxLayout):
|
||||
"""Search-first fix wrapping: find sheet by in-game text, wrap, Inject all."""
|
||||
layout.addWidget(_make_section_label("Step 9 · Fix wrapping"))
|
||||
layout.setSpacing(6)
|
||||
title = _make_section_label("Step 9 · Fix wrapping")
|
||||
title.setToolTip(
|
||||
"Paste overflowing in-game text to find it in translated JSON. "
|
||||
"Relayout: cell width + max lines (names.json → wolf names-wrap). "
|
||||
"Manual: wrap width + body font; emphasis \\f[N] scales with the body. "
|
||||
"Then Inject all (Step 7) and re-package."
|
||||
)
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(self._desc(
|
||||
"After packaging and playtesting, paste overflowing in-game text to find "
|
||||
"it in translated JSON. Relayout uses cell width + max lines "
|
||||
"(names.json → wolf names-wrap). Manual uses wrap width + body font; "
|
||||
"emphasis \\f[N] scales with the body. Then Inject all (Step 7) and re-package."
|
||||
"Paste overflowing text → pick a hit → Relayout or Manual → Wrap → Inject all."
|
||||
))
|
||||
|
||||
search_row = QHBoxLayout()
|
||||
search_row.setSpacing(6)
|
||||
self._wrap_search_edit = QLineEdit()
|
||||
self._wrap_search_edit.setPlaceholderText(
|
||||
'Paste in-game text, e.g. "there\'s a pure" or "gatekeeper just randomly"'
|
||||
|
|
@ -2636,15 +2692,17 @@ class WolfWorkflowTab(QWidget):
|
|||
layout.addLayout(search_row)
|
||||
|
||||
self._wrap_results_list = QListWidget()
|
||||
self._wrap_results_list.setMaximumHeight(220)
|
||||
self._wrap_results_list.setMinimumHeight(72)
|
||||
self._wrap_results_list.setMaximumHeight(110)
|
||||
self._wrap_results_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
self._wrap_results_list.setWordWrap(True)
|
||||
self._wrap_results_list.setTextElideMode(Qt.ElideNone)
|
||||
self._wrap_results_list.setUniformItemSizes(False)
|
||||
self._wrap_results_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self._wrap_results_list.setStyleSheet(
|
||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||
"QListWidget::item{padding:8px;}"
|
||||
"color:#cccccc;font-size:12px;padding:1px;}"
|
||||
"QListWidget::item{padding:3px 6px;}"
|
||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
||||
)
|
||||
self._wrap_results_list.currentItemChanged.connect(self._on_wrap_hit_selected)
|
||||
|
|
@ -2653,16 +2711,19 @@ class WolfWorkflowTab(QWidget):
|
|||
self._wrap_detail_label = QLabel("Select a search result to edit.")
|
||||
self._wrap_detail_label.setWordWrap(True)
|
||||
self._wrap_detail_label.setStyleSheet(
|
||||
"color:#9cdcfe;font-size:12px;background:transparent;"
|
||||
"color:#9cdcfe;font-size:11px;background:transparent;padding:0;"
|
||||
)
|
||||
layout.addWidget(self._wrap_detail_label)
|
||||
|
||||
mode_row = QHBoxLayout()
|
||||
# Mode + active controls on one row to cut vertical chrome.
|
||||
controls_row = QHBoxLayout()
|
||||
controls_row.setSpacing(8)
|
||||
mode_lbl = QLabel("Mode:")
|
||||
mode_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_mode_combo = QComboBox()
|
||||
self._wrap_mode_combo.addItem("Relayout (cell width + max lines)", "relayout")
|
||||
self._wrap_mode_combo.addItem("Manual (wrap width + font)", "manual")
|
||||
self._wrap_mode_combo.addItem("Relayout", "relayout")
|
||||
self._wrap_mode_combo.addItem("Manual", "manual")
|
||||
self._wrap_mode_combo.setMinimumWidth(110)
|
||||
saved_mode = str(self._setting("wrap_fix_mode", "relayout") or "relayout")
|
||||
idx = self._wrap_mode_combo.findData(saved_mode)
|
||||
self._wrap_mode_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
|
|
@ -2672,16 +2733,15 @@ class WolfWorkflowTab(QWidget):
|
|||
"Manual: force wrap width and body font; emphasis \\f[N] scales with the body."
|
||||
)
|
||||
self._wrap_mode_combo.currentIndexChanged.connect(self._on_wrap_mode_changed)
|
||||
mode_row.addWidget(mode_lbl)
|
||||
mode_row.addWidget(self._wrap_mode_combo)
|
||||
mode_row.addStretch()
|
||||
layout.addLayout(mode_row)
|
||||
controls_row.addWidget(mode_lbl)
|
||||
controls_row.addWidget(self._wrap_mode_combo)
|
||||
|
||||
# Relayout controls: cell width + max lines
|
||||
self._wrap_relayout_row = QWidget()
|
||||
relayout_row = QHBoxLayout(self._wrap_relayout_row)
|
||||
relayout_row.setContentsMargins(0, 0, 0, 0)
|
||||
w_lbl = QLabel("Cell width (0=auto):")
|
||||
relayout_row.setSpacing(6)
|
||||
w_lbl = QLabel("Width:")
|
||||
w_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_width_spin = QSpinBox()
|
||||
self._wrap_width_spin.setRange(0, 200)
|
||||
|
|
@ -2689,13 +2749,13 @@ class WolfWorkflowTab(QWidget):
|
|||
self._wrap_width_spin.setValue(int(self._setting("wrap_fix_width", 36) or 36))
|
||||
except (TypeError, ValueError):
|
||||
self._wrap_width_spin.setValue(36)
|
||||
self._wrap_width_spin.setFixedWidth(70)
|
||||
self._wrap_width_spin.setFixedWidth(64)
|
||||
self._wrap_width_spin.setToolTip(
|
||||
"Halfwidth cells (ASCII=1, fullwidth=2). For names.json Relayout: "
|
||||
"0 = measure from JP; N > 0 forces wolf names-wrap --width."
|
||||
"Cell width in halfwidth cells (ASCII=1, fullwidth=2). 0 = auto "
|
||||
"(names: measure from JP). N > 0 forces wolf names-wrap --width."
|
||||
)
|
||||
self._wrap_width_spin.valueChanged.connect(self._on_wrap_width_changed)
|
||||
lines_lbl = QLabel("Max lines (0=auto):")
|
||||
lines_lbl = QLabel("Lines:")
|
||||
lines_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_max_lines_spin = QSpinBox()
|
||||
self._wrap_max_lines_spin.setRange(0, 20)
|
||||
|
|
@ -2705,11 +2765,10 @@ class WolfWorkflowTab(QWidget):
|
|||
)
|
||||
except (TypeError, ValueError):
|
||||
self._wrap_max_lines_spin.setValue(0)
|
||||
self._wrap_max_lines_spin.setFixedWidth(70)
|
||||
self._wrap_max_lines_spin.setFixedWidth(64)
|
||||
self._wrap_max_lines_spin.setToolTip(
|
||||
"Max soft lines after Relayout. For names: wolf names-wrap --lines "
|
||||
"(0 = each entry's JP line count). For DB/dialogue: wrap to width, "
|
||||
"then shrink \\f[N] until the line count fits. "
|
||||
"Max soft lines (0 = auto). Names: wolf names-wrap --lines. "
|
||||
"DB/dialogue: wrap to width, then shrink \\f[N] to fit. "
|
||||
"Saved per sheet in wolfdawn-roles.json."
|
||||
)
|
||||
self._wrap_max_lines_spin.valueChanged.connect(self._on_wrap_max_lines_changed)
|
||||
|
|
@ -2717,14 +2776,14 @@ class WolfWorkflowTab(QWidget):
|
|||
relayout_row.addWidget(self._wrap_width_spin)
|
||||
relayout_row.addWidget(lines_lbl)
|
||||
relayout_row.addWidget(self._wrap_max_lines_spin)
|
||||
relayout_row.addStretch()
|
||||
layout.addWidget(self._wrap_relayout_row)
|
||||
controls_row.addWidget(self._wrap_relayout_row)
|
||||
|
||||
# Manual controls: wrap width + font size
|
||||
self._wrap_manual_row = QWidget()
|
||||
manual_row = QHBoxLayout(self._wrap_manual_row)
|
||||
manual_row.setContentsMargins(0, 0, 0, 0)
|
||||
mw_lbl = QLabel("Wrap width:")
|
||||
manual_row.setSpacing(6)
|
||||
mw_lbl = QLabel("Width:")
|
||||
mw_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_manual_width_spin = QSpinBox()
|
||||
self._wrap_manual_width_spin.setRange(10, 200)
|
||||
|
|
@ -2734,12 +2793,12 @@ class WolfWorkflowTab(QWidget):
|
|||
)
|
||||
except (TypeError, ValueError):
|
||||
self._wrap_manual_width_spin.setValue(36)
|
||||
self._wrap_manual_width_spin.setFixedWidth(70)
|
||||
self._wrap_manual_width_spin.setFixedWidth(64)
|
||||
self._wrap_manual_width_spin.setToolTip(
|
||||
"Hard wrap width in halfwidth cells for Manual mode (dazedwrap)."
|
||||
)
|
||||
self._wrap_manual_width_spin.valueChanged.connect(self._on_wrap_manual_width_changed)
|
||||
font_lbl = QLabel("Body font \\f[N]:")
|
||||
font_lbl = QLabel("Font:")
|
||||
font_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._wrap_font_spin = QSpinBox()
|
||||
self._wrap_font_spin.setRange(0, 32)
|
||||
|
|
@ -2749,11 +2808,10 @@ class WolfWorkflowTab(QWidget):
|
|||
except (TypeError, ValueError):
|
||||
saved_font = 18
|
||||
self._wrap_font_spin.setValue(max(0, saved_font))
|
||||
self._wrap_font_spin.setFixedWidth(90)
|
||||
self._wrap_font_spin.setFixedWidth(80)
|
||||
self._wrap_font_spin.setToolTip(
|
||||
"Target body font applied on Wrap. 0 = do not add or change \\f[N] "
|
||||
"(wrap width only). N > 0 scales emphasis (e.g. \\f[20]…\\f[18]) and "
|
||||
"sets a leading \\f[body]."
|
||||
"Body \\f[N] on Wrap. 0 = leave fonts alone (width only). "
|
||||
"N > 0 scales emphasis and sets a leading \\f[body]."
|
||||
)
|
||||
self._wrap_font_spin.valueChanged.connect(self._on_wrap_font_changed)
|
||||
self._wrap_font_detect_label = QLabel("")
|
||||
|
|
@ -2765,29 +2823,33 @@ class WolfWorkflowTab(QWidget):
|
|||
manual_row.addWidget(font_lbl)
|
||||
manual_row.addWidget(self._wrap_font_spin)
|
||||
manual_row.addWidget(self._wrap_font_detect_label, 1)
|
||||
layout.addWidget(self._wrap_manual_row)
|
||||
controls_row.addWidget(self._wrap_manual_row, 1)
|
||||
controls_row.addStretch()
|
||||
layout.addLayout(controls_row)
|
||||
|
||||
self._sync_wrap_mode_controls()
|
||||
|
||||
self._wrap_preview_label = QLabel("Wrap preview")
|
||||
self._wrap_preview_label.setStyleSheet(
|
||||
"color:#858585;font-size:11px;background:transparent;"
|
||||
"color:#858585;font-size:11px;background:transparent;padding:0;"
|
||||
)
|
||||
layout.addWidget(self._wrap_preview_label)
|
||||
|
||||
self._wrap_preview = QTextEdit()
|
||||
self._wrap_preview.setReadOnly(True)
|
||||
self._wrap_preview.setMaximumHeight(200)
|
||||
self._wrap_preview.setMinimumHeight(140)
|
||||
self._wrap_preview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._wrap_preview.setPlaceholderText(
|
||||
"Select a search result to preview wrap with simulated \\c / \\f styling."
|
||||
"Wrap preview — select a hit to see \\c / \\f styling."
|
||||
)
|
||||
self._wrap_preview.setStyleSheet(
|
||||
"QTextEdit{background-color:#1e1e1e;color:#b5cea8;border:1px solid #3c3c3c;"
|
||||
"border-left:3px solid #007acc;font-family:monospace;font-size:12px;padding:6px;}"
|
||||
"border-left:3px solid #007acc;font-family:monospace;font-size:12px;padding:4px;}"
|
||||
)
|
||||
layout.addWidget(self._wrap_preview)
|
||||
layout.addWidget(self._wrap_preview, 1)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(6)
|
||||
wrap_row_btn = _make_btn("Wrap this line", "#00a86b")
|
||||
wrap_row_btn.clicked.connect(self._wrap_current_row)
|
||||
wrap_group_btn = _make_btn("Wrap all in group", "#00a86b")
|
||||
|
|
@ -2801,15 +2863,13 @@ class WolfWorkflowTab(QWidget):
|
|||
btn_row.addWidget(wrap_row_btn)
|
||||
btn_row.addWidget(wrap_group_btn)
|
||||
btn_row.addWidget(inject_btn)
|
||||
btn_row.addStretch()
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self._wrap_status_label = QLabel("")
|
||||
self._wrap_status_label.setWordWrap(True)
|
||||
self._wrap_status_label.setStyleSheet(
|
||||
"color:#8fbc8f;font-size:12px;background:transparent;"
|
||||
"color:#8fbc8f;font-size:11px;background:transparent;"
|
||||
)
|
||||
layout.addWidget(self._wrap_status_label)
|
||||
btn_row.addWidget(self._wrap_status_label, 1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self._current_wrap_hit: wolf_ws.WrapHit | None = None
|
||||
self._current_wrap_hit_id: dict | None = None
|
||||
|
|
@ -3717,28 +3777,55 @@ class WolfWorkflowTab(QWidget):
|
|||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
|
||||
def _sync_json_to_dir(
|
||||
self, json_name: str, source_dir: Path, dest_dir: Path
|
||||
) -> tuple[bool, str | None]:
|
||||
"""Copy one JSON from *source_dir* into *dest_dir* (e.g. files/ or translated/)."""
|
||||
src = source_dir / json_name
|
||||
if not src.is_file():
|
||||
return False, "source missing"
|
||||
dest = dest_dir / json_name
|
||||
try:
|
||||
if src.resolve() == dest.resolve():
|
||||
return True, None
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
return True, None
|
||||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
|
||||
def _inject(
|
||||
self,
|
||||
selected: set[str],
|
||||
*,
|
||||
source_dir: Path | None = None,
|
||||
sync_to_wolf_json: bool = True,
|
||||
sync_to_files: bool = False,
|
||||
sync_to_translated: bool = False,
|
||||
):
|
||||
"""Inject the selected JSON files into live Data/.
|
||||
|
||||
*source_dir* defaults to ``translated/``. When injecting from the game's
|
||||
``wolf_json/``, pass that path and set *sync_to_wolf_json* False (the
|
||||
source already is wolf_json/).
|
||||
source already is wolf_json/). Set *sync_to_files* / *sync_to_translated*
|
||||
to also copy the injected JSON into the tool's ``files/`` and/or
|
||||
``translated/`` folders.
|
||||
"""
|
||||
if not self._require_manifest():
|
||||
return
|
||||
manifest = self._read_manifest()
|
||||
en_punct = self._inject_flag_en_punct()
|
||||
game_json_dir = self._work_dir()
|
||||
tool_root = self._tool_root()
|
||||
files_dir = tool_root / "files"
|
||||
translated_dir = tool_root / "translated"
|
||||
inject_dir = (
|
||||
Path(source_dir)
|
||||
if source_dir is not None
|
||||
else self._tool_root() / "translated"
|
||||
else translated_dir
|
||||
)
|
||||
|
||||
def task(log, progress=None):
|
||||
|
|
@ -3776,6 +3863,27 @@ class WolfWorkflowTab(QWidget):
|
|||
)
|
||||
log(f" ✗ sync {json_name}: {err}")
|
||||
|
||||
for label, enabled, dest in (
|
||||
("files/", sync_to_files, files_dir),
|
||||
("translated/", sync_to_translated, translated_dir),
|
||||
):
|
||||
if not enabled:
|
||||
continue
|
||||
synced = 0
|
||||
for json_name in sorted(selected):
|
||||
ok, err = self._sync_json_to_dir(
|
||||
json_name, inject_dir, dest
|
||||
)
|
||||
if not ok:
|
||||
report.sync_failures.append(
|
||||
(json_name, f"{label}: {err or 'unknown error'}")
|
||||
)
|
||||
log(f" ✗ sync {label}{json_name}: {err}")
|
||||
else:
|
||||
synced += 1
|
||||
if synced:
|
||||
log(f"Synced {synced} file(s) → {label}")
|
||||
|
||||
dialog = wolf_inject.format_report_dialog(report)
|
||||
status = wolf_inject.format_report_status(report)
|
||||
inject_state["dialog"] = dialog
|
||||
|
|
@ -3842,7 +3950,9 @@ class WolfWorkflowTab(QWidget):
|
|||
if self._step_tabs.currentIndex() != idx:
|
||||
self._step_tabs.setCurrentIndex(idx)
|
||||
else:
|
||||
self._refresh_step_strip(idx)
|
||||
# Re-selecting the current step still runs enter hooks (e.g. Step 4
|
||||
# discovery refresh) so the strip click always feels live.
|
||||
self._on_step_changed(idx)
|
||||
|
||||
def _advance_step(self, from_idx: int):
|
||||
"""Mark *from_idx* done and move to the next step."""
|
||||
|
|
@ -3855,6 +3965,10 @@ class WolfWorkflowTab(QWidget):
|
|||
self._refresh_step_strip(idx)
|
||||
if previous == 0 and idx != 0:
|
||||
self._auto_import_if_needed()
|
||||
self._enter_step(idx)
|
||||
|
||||
def _enter_step(self, idx: int):
|
||||
"""Refresh step-local data whenever a step is shown or re-selected."""
|
||||
# Index 3 == Names: refresh the per-name safety summary.
|
||||
if idx == 3:
|
||||
self._refresh_names_summary()
|
||||
|
|
@ -4164,7 +4278,39 @@ class WolfWorkflowTab(QWidget):
|
|||
f"No injectable JSON found in {work}.",
|
||||
)
|
||||
return
|
||||
self._inject(set(files), source_dir=work, sync_to_wolf_json=False)
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Inject from wolf_json",
|
||||
f"About to inject {len(files)} file(s) from wolf_json/.\n\n"
|
||||
"Also copy those JSON into files/ and translated/ so the tool "
|
||||
"keeps the same progress (overwrites matching names)?\n\n"
|
||||
"Yes = inject and sync to files/ + translated/\n"
|
||||
"No = inject only, leave files/ and translated/ as-is\n"
|
||||
"Cancel = do nothing",
|
||||
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
|
||||
QMessageBox.Yes,
|
||||
)
|
||||
if reply == QMessageBox.Cancel:
|
||||
self._log("Inject from wolf_json cancelled.")
|
||||
return
|
||||
sync_tool_dirs = reply == QMessageBox.Yes
|
||||
if sync_tool_dirs:
|
||||
self._log(
|
||||
f"Will sync {len(files)} wolf_json/ file(s) → files/ and "
|
||||
"translated/ after inject."
|
||||
)
|
||||
else:
|
||||
self._log(
|
||||
"Skipping wolf_json/ → files/ + translated/ sync; "
|
||||
"leaving those folders as-is."
|
||||
)
|
||||
self._inject(
|
||||
set(files),
|
||||
source_dir=work,
|
||||
sync_to_wolf_json=False,
|
||||
sync_to_files=sync_tool_dirs,
|
||||
sync_to_translated=sync_tool_dirs,
|
||||
)
|
||||
|
||||
def _layout_restore_all(self):
|
||||
"""Re-run wolf layout-restore on every injectable translated JSON."""
|
||||
|
|
|
|||
|
|
@ -1716,7 +1716,9 @@ class WorkflowTab(QWidget):
|
|||
tc_inner.setSpacing(4)
|
||||
tc_desc = QLabel(
|
||||
"Copies everything from the <code>gameupdate/</code> folder "
|
||||
"into the game\'s root folder, overwriting existing files."
|
||||
"into the game\'s root folder, overwriting existing files. "
|
||||
"Writes <code>patch-config.txt</code> from Config → Game Update defaults "
|
||||
"(set your org once there; edit <code>repo=</code> per game)."
|
||||
)
|
||||
tc_desc.setTextFormat(Qt.RichText)
|
||||
tc_desc.setWordWrap(True)
|
||||
|
|
@ -4612,6 +4614,16 @@ class WorkflowTab(QWidget):
|
|||
self._worker = w
|
||||
w.start()
|
||||
|
||||
def _write_gameupdate_patch_config(self, game_root: str):
|
||||
"""Write gameupdate/patch-config.txt from Config → Game Update defaults."""
|
||||
from util.gameupdate_config import write_patch_config
|
||||
|
||||
ok, msg = write_patch_config(game_root)
|
||||
if ok:
|
||||
self._log(f"📝 Wrote patch-config.txt from Config defaults → {msg}")
|
||||
else:
|
||||
self._log(f"ℹ patch-config.txt: {msg}")
|
||||
|
||||
def _run_gameupdate(self):
|
||||
src = self.pp_gameupdate_edit.text().strip()
|
||||
dst = self.folder_edit.text().strip()
|
||||
|
|
@ -4634,6 +4646,9 @@ class WorkflowTab(QWidget):
|
|||
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||
for e in errors:
|
||||
self._log(f" ⚠ {e}")
|
||||
dst = self.folder_edit.text().strip()
|
||||
if dst and not errors:
|
||||
self._write_gameupdate_patch_config(dst)
|
||||
|
||||
def _run_all_preprocess(self):
|
||||
"""Launch all three pre-process tasks in sequence, chaining via signals."""
|
||||
|
|
@ -4688,6 +4703,8 @@ class WorkflowTab(QWidget):
|
|||
self._log(f"✅ gameupdate: copied {count} file(s).")
|
||||
for e in errors:
|
||||
self._log(f" ⚠ {e}")
|
||||
if not errors and game_root_dst:
|
||||
self._write_gameupdate_patch_config(game_root_dst)
|
||||
run_next(rest)
|
||||
worker.done.connect(on_copy_done)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -412,28 +412,37 @@ def _body_after_dropped_tag(text: str, original_speaker: str) -> str:
|
|||
return text
|
||||
|
||||
|
||||
def _maybe_fix_nameplate(entry, total_tokens: list) -> None:
|
||||
"""Swap a Japanese first-line nameplate for English on an otherwise-done line."""
|
||||
if ESTIMATE or _batch_phase() == "collect":
|
||||
return
|
||||
def _maybe_fix_nameplate(entry, total_tokens: list) -> bool:
|
||||
"""Resolve a Japanese first-line nameplate on an otherwise-done line.
|
||||
|
||||
Returns True when the nameplate was rewritten. During batch collect the
|
||||
in-memory rewrite is discarded (translated/ is not written), but
|
||||
``getSpeaker`` still runs so the English gloss is cached before dialogue
|
||||
batches are queued - otherwise those live speaker calls only show up in
|
||||
Pass 2.
|
||||
"""
|
||||
if ESTIMATE:
|
||||
return False
|
||||
speaker_src = entry.get("speaker_src", "")
|
||||
if not wolf_speakers.is_firstline_enabled(speaker_src, SPEAKER_CONFIG):
|
||||
return
|
||||
return False
|
||||
txt = entry.get("text")
|
||||
if not isinstance(txt, str) or not txt.strip():
|
||||
return
|
||||
return False
|
||||
# Only touch finished dialogue (body already English / skip-translated).
|
||||
if _text_still_needs_translation(entry):
|
||||
return
|
||||
return False
|
||||
split = wolf_speakers.split_source(txt, speaker_src, SPEAKER_CONFIG)
|
||||
if split is None:
|
||||
return
|
||||
return False
|
||||
prefix, speaker, body = split
|
||||
if not re.search(LANGREGEX, speaker):
|
||||
return
|
||||
return False
|
||||
speaker_en = _resolve_nameplate(speaker, total_tokens)
|
||||
if speaker_en and speaker_en != speaker:
|
||||
entry["text"] = wolf_speakers.restore_source(prefix, speaker_en, body)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parseDocument(data, filename):
|
||||
|
|
@ -457,9 +466,11 @@ def parseDocument(data, filename):
|
|||
return False
|
||||
return True
|
||||
|
||||
# Fix Japanese nameplates on lines whose dialogue body is already done, so
|
||||
# resume / re-wrap runs do not leave ``セルリア\\nPlease hold on...`` behind.
|
||||
if IGNORETLTEXT and not ESTIMATE and _batch_phase() != "collect":
|
||||
# Resolve Japanese nameplates on lines whose dialogue body is already done.
|
||||
# Must run during batch collect too: those lines are skip-translated so they
|
||||
# never hit the reshape/getSpeaker path, and deferring to consume is what
|
||||
# made speaker Input/Output spam show up in Pass 2 after the batch returned.
|
||||
if IGNORETLTEXT and not ESTIMATE:
|
||||
for entry in entries:
|
||||
if _translatable(entry):
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -470,6 +470,52 @@ class TestTranslationWriteback(unittest.TestCase):
|
|||
# Short-string speaker resolve, then the remaining dialogue batch.
|
||||
self.assertEqual(captured, ["司祭", ["まだだ"]])
|
||||
|
||||
def test_collect_resolves_japanese_nameplate_before_queueing(self):
|
||||
"""Pass 1 must live-translate skip-translated JP nameplates (not defer to Pass 2)."""
|
||||
doc = {
|
||||
"kind": "map",
|
||||
"scenes": [
|
||||
{
|
||||
"event": 1,
|
||||
"name": "ev",
|
||||
"lines": [
|
||||
{
|
||||
"cmd": 0,
|
||||
"str": 0,
|
||||
"speaker": "客F",
|
||||
"speaker_src": "literal_line1_lowconf",
|
||||
"source": "客F\nまぁまぁ。",
|
||||
"text": "客F\nNow, now.",
|
||||
},
|
||||
{
|
||||
"cmd": 1,
|
||||
"str": 0,
|
||||
"speaker": "UI",
|
||||
"speaker_src": "ui",
|
||||
"source": "まだだ",
|
||||
"text": "まだだ",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
orig_phase = os.environ.get("BATCH_PHASE")
|
||||
os.environ["BATCH_PHASE"] = "collect"
|
||||
try:
|
||||
(data, _t, err), captured = _WolfTranslateHarness().run(doc, "collect_nameplate.mps.json")
|
||||
finally:
|
||||
if orig_phase is None:
|
||||
os.environ.pop("BATCH_PHASE", None)
|
||||
else:
|
||||
os.environ["BATCH_PHASE"] = orig_phase
|
||||
|
||||
self.assertIsNone(err)
|
||||
# Speaker resolved live during collect; dialogue list is also seen (queued).
|
||||
self.assertEqual(captured[0], "客F")
|
||||
self.assertEqual(captured[1], ["まだだ"])
|
||||
# Collect does not persist translated/, but in-memory text is updated.
|
||||
self.assertEqual(data["scenes"][0]["lines"][0]["text"], "En_客F\nNow, now.")
|
||||
|
||||
def test_ignore_tl_text_false_retranslates(self):
|
||||
doc = {
|
||||
"kind": "map",
|
||||
|
|
|
|||
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