I'm doing stuff
This commit is contained in:
parent
c3ac172818
commit
5fd6e82b53
11 changed files with 2862 additions and 8 deletions
14
gameupdate/GameUpdate.bat
Normal file
14
gameupdate/GameUpdate.bat
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
@echo off
|
||||
setlocal
|
||||
|
||||
REM Copy GAMEUPDATE.bat to a new file
|
||||
copy "gameupdate\patch.bat" "gameupdate\patch2.bat"
|
||||
|
||||
REM Run the new file
|
||||
call "gameupdate\patch2.bat"
|
||||
|
||||
REM Delete the new file
|
||||
del "gameupdate\patch2.bat"
|
||||
|
||||
endlocal
|
||||
@echo on
|
||||
13
gameupdate/GameUpdate_linux.sh
Normal file
13
gameupdate/GameUpdate_linux.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Enable error handling
|
||||
set -e
|
||||
|
||||
# Copy patch.sh to a new file in gameupdate
|
||||
cp "gameupdate/patch.sh" "gameupdate/patch2.sh"
|
||||
|
||||
# Run the new file
|
||||
bash "gameupdate/patch2.sh"
|
||||
|
||||
# Delete the new file
|
||||
rm "gameupdate/patch2.sh"
|
||||
102
gameupdate/README.md
Normal file
102
gameupdate/README.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Apply Patch
|
||||
1. Click Code
|
||||
2. Click Download ZIP
|
||||
3. Extract to game folder and Replace All.
|
||||
|
||||
## Future Patching
|
||||
1. Run GAMEUPDATE.bat to auto patch.
|
||||
|
||||
# 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
|
||||
|
||||
For WOLF RPG games, if you downloaded the game off of DLSite, you will need to do some extra steps to patch it. This is because there is a "master" file called Data.wolf that will take priority over the english patch files. You will need this file to be a folder before patching will work.
|
||||
|
||||
# Wolf Games
|
||||
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
|
||||
|
||||
# Edit/Contribute
|
||||
TLDR 3 steps.
|
||||
|
||||
Fork the repository.
|
||||
Make the changes.
|
||||
Submit a merge request.
|
||||
|
||||
If everything looks good and doesn't break things I'll merge it in.
|
||||
|
||||
Longer Version:
|
||||
|
||||
# Required Software:
|
||||
* [VSCode](https://code.visualstudio.com/) Make sure you check all the boxes for context menus.
|
||||
* [Git](https://git-scm.com/downloads) (Use the default for everything. Just keep clicking Next)
|
||||
|
||||
# Guide to contributing
|
||||
|
||||
### 1. Fork the Repository
|
||||
- Go to the repository you want to fork.
|
||||
- Click the "Fork" button.
|
||||
|
||||
### 2. Clone Your Fork
|
||||
- Clone your forked repository to your local machine.
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/REPO_NAME.git
|
||||
```
|
||||
|
||||
### 3. Make Your Changes (In VSCode)
|
||||
- Edit the files locally on your new branch using VSCode.
|
||||
- Add and commit your changes.
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "Description of your changes"
|
||||
```
|
||||
|
||||
### 4. Push Your Changes
|
||||
- Push your changes to your fork on GitGud.io.
|
||||
```sh
|
||||
git push origin your-feature-branch
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to your fork on GitGud.io.
|
||||
- Click on "Merge Requests" in the sidebar.
|
||||
- Click the "New merge request" button.
|
||||
- Select the branch you made changes to and the target project (the original repo).
|
||||
- Provide a title and description for your merge request and submit it.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Assuming you want to fork a repository named `example-project`:
|
||||
|
||||
### 1. Fork the Repo
|
||||
- Navigate to `https://gitgud.io/original_user/example-project` and click "Fork".
|
||||
|
||||
### 2. Clone Your Fork
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/example-project.git
|
||||
```
|
||||
|
||||
### 3. Make Changes and Commit
|
||||
```sh
|
||||
# Make changes to the files
|
||||
git add .
|
||||
git commit -m "Add new feature to example project"
|
||||
```
|
||||
|
||||
### 4. Push Changes
|
||||
```sh
|
||||
git push origin add-new-feature
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to `https://gitgud.io/YOUR_USERNAME/example-project/merge_requests` and click on "New merge request"
|
||||
- Choose the source branch `add-new-feature` and target branch (default: `main` or `master`)
|
||||
- Fill in the details and submit the merge request
|
||||
265
gameupdate/gameupdate/patch.bat
Normal file
265
gameupdate/gameupdate/patch.bat
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
@echo off
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
REM Check if being run from gameupdate folder (incorrect usage)
|
||||
for %%I in ("%CD%") do set "CURRENT_FOLDER=%%~nxI"
|
||||
if /I "%CURRENT_FOLDER%"=="gameupdate" (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo ERROR: Do not run patch.bat directly!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo You are running this from the gameupdate folder.
|
||||
echo This will not work correctly!
|
||||
echo.
|
||||
echo Please go back to the game's root folder and
|
||||
echo run GameUpdate.bat instead.
|
||||
echo ========================================
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Determine important paths
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "ROOT_DIR=%CD%"
|
||||
set "CONFIG_FILE=%SCRIPT_DIR%patch-config.txt"
|
||||
|
||||
echo Using root directory: "%ROOT_DIR%"
|
||||
echo Using config: "%CONFIG_FILE%"
|
||||
|
||||
echo Checking for pwsh...
|
||||
set _my_shell=pwsh
|
||||
|
||||
REM Check if pwsh.exe exists
|
||||
where /q !_my_shell!
|
||||
if !errorlevel! neq 0 (
|
||||
echo pwsh not found.
|
||||
echo.
|
||||
echo PowerShell 7 ^(pwsh^) is faster and recommended.
|
||||
set /p "INSTALL_PWSH=Would you like to install it now via winget? (Y/N): "
|
||||
if /I "!INSTALL_PWSH!"=="Y" (
|
||||
echo Installing PowerShell 7 via winget...
|
||||
winget install --id Microsoft.PowerShell --source winget --accept-package-agreements --accept-source-agreements
|
||||
if !errorlevel! neq 0 (
|
||||
echo Install failed or winget not available. Falling back to powershell...
|
||||
set _my_shell=powershell
|
||||
) else (
|
||||
echo PowerShell 7 installed successfully.
|
||||
echo Please re-run GameUpdate.bat to use pwsh.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
) else (
|
||||
echo Skipping install. Falling back to powershell...
|
||||
set _my_shell=powershell
|
||||
)
|
||||
|
||||
REM Check if powershell.exe exists
|
||||
echo Checking for powershell...
|
||||
where /q !_my_shell!
|
||||
if !errorlevel! neq 0 (
|
||||
echo.Error: Powershell not found!
|
||||
pause
|
||||
exit /B 1
|
||||
) else (
|
||||
echo powershell found.
|
||||
)
|
||||
) else (
|
||||
echo pwsh found.
|
||||
)
|
||||
|
||||
echo Using !_my_shell! for script execution.
|
||||
|
||||
REM Check if patch-config.txt exists in gameupdate folder
|
||||
if not exist "%CONFIG_FILE%" (
|
||||
echo "Config file (gameupdate\patch-config.txt) not found! Assuming no patching needed."
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM Read configuration from file
|
||||
for /f "usebackq tokens=1,2 delims==" %%a in ("%CONFIG_FILE%") do (
|
||||
if "%%a"=="username" set "username=%%b"
|
||||
if "%%a"=="repo" set "repo=%%b"
|
||||
if "%%a"=="branch" set "branch=%%b"
|
||||
)
|
||||
|
||||
REM --------------------------------------------------------
|
||||
REM PRE-SETUP: Ensure SRPG data and patch structure exists
|
||||
REM Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates
|
||||
REM 1) Unpack once if data folder doesn't exist (and data.dts does)
|
||||
REM 2) Create Patch once if patch folder doesn't exist
|
||||
REM --------------------------------------------------------
|
||||
set "UNPACKER=%ROOT_DIR%\SRPG_Unpacker.exe"
|
||||
if exist "%ROOT_DIR%\data.dts" (
|
||||
if exist "%UNPACKER%" (
|
||||
echo [Pre-Setup] Running SRPG_Unpacker preparation steps...
|
||||
|
||||
REM Step 1: Unpack (once)
|
||||
if not exist "%ROOT_DIR%\data\" (
|
||||
set "SHOULD_UNPACK=1"
|
||||
) else if not exist "%ROOT_DIR%\data\project.dat" (
|
||||
set "SHOULD_UNPACK=1"
|
||||
) else (
|
||||
set "SHOULD_UNPACK=0"
|
||||
)
|
||||
|
||||
if "!SHOULD_UNPACK!"=="1" (
|
||||
if exist "%ROOT_DIR%\data.dts" (
|
||||
echo [Pre-Setup] Step 1: Unpacking data.dts to data
|
||||
pushd "%ROOT_DIR%"
|
||||
"%UNPACKER%" -o "data" "data.dts"
|
||||
if !errorlevel! neq 0 (
|
||||
echo [Pre-Setup] ERROR: Unpack failed. Continuing.
|
||||
)
|
||||
popd
|
||||
) else (
|
||||
echo [Pre-Setup] Step 1: Skipping unpack - no data folder and no data.dts found.
|
||||
)
|
||||
) else (
|
||||
echo [Pre-Setup] Step 1: data folder exists; skipping unpack.
|
||||
)
|
||||
|
||||
REM Step 2: Create Patch (once)
|
||||
if not exist "%ROOT_DIR%\patch\" (
|
||||
if exist "%ROOT_DIR%\data\project.dat" (
|
||||
echo [Pre-Setup] Step 2: Creating patch from data\project.dat
|
||||
pushd "%ROOT_DIR%"
|
||||
"%UNPACKER%" ".\data\project.dat" -c
|
||||
if !errorlevel! neq 0 (
|
||||
echo [Pre-Setup] ERROR: Create Patch failed. Continuing.
|
||||
)
|
||||
popd
|
||||
) else (
|
||||
echo [Pre-Setup] Step 2: Skipping create patch - data\project.dat not found.
|
||||
)
|
||||
) else (
|
||||
echo [Pre-Setup] Step 2: patch folder exists; skipping create.
|
||||
)
|
||||
) else (
|
||||
echo [Pre-Setup] SRPG_Unpacker.exe not found in root; skipping pre-setup steps.
|
||||
)
|
||||
) else (
|
||||
echo [Pre-Setup] data.dts not found; skipping pre-setup SRPG steps.
|
||||
)
|
||||
|
||||
REM Get the latest hash
|
||||
echo "Getting latest commit SHA hash"
|
||||
!_my_shell! -Command "(Invoke-RestMethod -Uri 'https://gitgud.io/api/v4/projects/%username%%%2F%repo%/repository/branches/%branch%').commit.id" > "%ROOT_DIR%\latest_patch_sha.txt"
|
||||
|
||||
REM Read the latest SHA from the file
|
||||
set /p latest_patch_sha=<"%ROOT_DIR%\latest_patch_sha.txt"
|
||||
|
||||
REM Check if previous_patch_sha.txt exists in gameupdate
|
||||
if not exist "%SCRIPT_DIR%previous_patch_sha.txt" (
|
||||
echo "Previous SHA hash not found!"
|
||||
echo "Assuming first time patching..."
|
||||
goto download_extract
|
||||
)
|
||||
|
||||
REM Read the stored SHA from previous check
|
||||
set /p previous_patch_sha=<"%SCRIPT_DIR%previous_patch_sha.txt"
|
||||
|
||||
REM Trim whitespace from SHA strings
|
||||
set "previous_patch_sha=%previous_patch_sha: =%"
|
||||
set "latest_patch_sha=%latest_patch_sha: =%"
|
||||
|
||||
REM Compare trimmed SHAs
|
||||
if "%latest_patch_sha%" neq "%previous_patch_sha%" (
|
||||
echo "Update found! Patching..."
|
||||
goto download_extract
|
||||
) else (
|
||||
echo "Patch is up to date."
|
||||
)
|
||||
|
||||
REM Delete latest_patch_sha.txt
|
||||
del "%ROOT_DIR%\latest_patch_sha.txt"
|
||||
|
||||
endlocal
|
||||
pause
|
||||
exit /b
|
||||
|
||||
:download_extract
|
||||
|
||||
REM Escape single quotes in paths
|
||||
set "escaped_root=%ROOT_DIR:'=''%"
|
||||
|
||||
REM Download zip file to root
|
||||
echo "Downloading latest patch..."
|
||||
!_my_shell! -Command "Set-Location -LiteralPath '%escaped_root%'; $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri 'https://gitgud.io/%username%/%repo%/-/archive/%branch%/%repo%-%branch%.zip' -OutFile 'repo.zip'"
|
||||
if !errorlevel! neq 0 (
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM Extract contents, overwriting conflicts into root
|
||||
echo "Extracting..."
|
||||
!_my_shell! -Command "Set-Location -LiteralPath '%escaped_root%'; $ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '.\repo.zip' -DestinationPath '.' -Force"
|
||||
if !errorlevel! neq 0 (
|
||||
echo Extraction failed!
|
||||
del "%ROOT_DIR%\repo.zip"
|
||||
rmdir /s /q "%ROOT_DIR%\%repo%-%branch%"
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
echo "Applying patch..."
|
||||
xcopy /s /e /y "%ROOT_DIR%\%repo%-%branch%\*" "%ROOT_DIR%\"
|
||||
if !errorlevel! neq 0 (
|
||||
echo Patch application failed!
|
||||
del "%ROOT_DIR%\repo.zip"
|
||||
rmdir /s /q "%ROOT_DIR%\%repo%-%branch%"
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
REM --------------------------------------------------------
|
||||
REM POST-APPLY: Run Steps 3 and 4 after patch files are merged
|
||||
REM 3) Apply Patch to data\project.dat
|
||||
REM 4) Pack data back into data.dts
|
||||
REM --------------------------------------------------------
|
||||
set "UNPACKER=%ROOT_DIR%\SRPG_Unpacker.exe"
|
||||
if exist "%ROOT_DIR%\data.dts" (
|
||||
if exist "%UNPACKER%" (
|
||||
echo Running SRPG_Unpacker apply/pack steps...
|
||||
|
||||
REM Step 3: Apply Patch
|
||||
if exist "%ROOT_DIR%\data\project.dat" (
|
||||
echo Step 3: Applying patch to data\project.dat
|
||||
pushd "%ROOT_DIR%"
|
||||
"%UNPACKER%" ".\data\project.dat" -a
|
||||
if !errorlevel! neq 0 (
|
||||
echo ERROR: Apply Patch failed.
|
||||
)
|
||||
popd
|
||||
) else (
|
||||
echo ERROR: data\project.dat not found; cannot apply patch.
|
||||
)
|
||||
|
||||
REM Step 4: Pack
|
||||
if exist "%ROOT_DIR%\data\" (
|
||||
echo Step 4: Packing data to data.dts
|
||||
pushd "%ROOT_DIR%"
|
||||
"%UNPACKER%" -o "data.dts" "data"
|
||||
if !errorlevel! neq 0 (
|
||||
echo WARNING: Pack failed.
|
||||
)
|
||||
popd
|
||||
) else (
|
||||
echo Step 4: Skipping pack - data folder not found.
|
||||
)
|
||||
) else (
|
||||
echo SRPG_Unpacker.exe not found in root; skipping SRPG patch steps.
|
||||
)
|
||||
) else (
|
||||
echo data.dts not found; skipping SRPG patch steps.
|
||||
)
|
||||
REM Clean up
|
||||
echo "Cleaning up..."
|
||||
del "%ROOT_DIR%\repo.zip"
|
||||
rmdir /s /q "%ROOT_DIR%\%repo%-%branch%"
|
||||
del "%ROOT_DIR%\latest_patch_sha.txt"
|
||||
REM Store latest SHA for next check in gameupdate
|
||||
echo %latest_patch_sha% > "%SCRIPT_DIR%previous_patch_sha.txt"
|
||||
endlocal
|
||||
pause
|
||||
exit /b
|
||||
169
gameupdate/gameupdate/patch.sh
Normal file
169
gameupdate/gameupdate/patch.sh
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/patch-config.txt"
|
||||
|
||||
check_dependency() {
|
||||
if ! command -v "$1" > /dev/null 2>&1; then
|
||||
echo "Error: '$1' is not installed. Please install it using 'pkg install $1'."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for jq, unzip, and curl
|
||||
check_dependency jq
|
||||
check_dependency unzip
|
||||
check_dependency curl
|
||||
|
||||
# Check if CONFIG_FILE exists
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "Config file '$CONFIG_FILE' not found! Assuming no patching needed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert line endings to Unix format
|
||||
sed -i 's/\r$//' "$CONFIG_FILE"
|
||||
|
||||
# Debug information
|
||||
echo "Root directory: $ROOT_DIR"
|
||||
echo "Config file path: $CONFIG_FILE"
|
||||
|
||||
# Read configuration from file
|
||||
. "$CONFIG_FILE"
|
||||
|
||||
# Get the latest hash
|
||||
echo "Getting latest commit SHA hash"
|
||||
latest_patch_sha=$(curl -s "https://gitgud.io/api/v4/projects/$username%2F$repo/repository/branches/$branch" | jq -r '.commit.id')
|
||||
|
||||
# --------------------------------------------------------
|
||||
# PRE-SETUP: Ensure SRPG data and patch structure exists
|
||||
# Run Steps 1 and 2 BEFORE pulling repo patch to avoid overwriting updates
|
||||
# 1) Unpack once if data folder doesn't exist (and data.dts does)
|
||||
# 2) Create Patch once if patch folder doesn't exist
|
||||
# --------------------------------------------------------
|
||||
UNPACKER="$ROOT_DIR/SRPG_Unpacker.exe"
|
||||
if [ -f "$ROOT_DIR/data.dts" ]; then
|
||||
if [ -f "$UNPACKER" ]; then
|
||||
echo "[Pre-Setup] Running SRPG_Unpacker preparation steps..."
|
||||
|
||||
# Step 1: Unpack (once)
|
||||
if [ ! -d "$ROOT_DIR/data" ]; then
|
||||
if [ -f "$ROOT_DIR/data.dts" ]; then
|
||||
echo "[Pre-Setup] Step 1: Unpacking data.dts -> data"
|
||||
( cd "$ROOT_DIR" && "$UNPACKER" -o data data.dts ) || echo "[Pre-Setup] ERROR: Unpack failed."
|
||||
else
|
||||
echo "[Pre-Setup] Step 1: Skipping unpack (no data folder and no data.dts found)."
|
||||
fi
|
||||
else
|
||||
echo "[Pre-Setup] Step 1: data folder exists; skipping unpack."
|
||||
fi
|
||||
|
||||
# Step 2: Create Patch (once)
|
||||
if [ ! -d "$ROOT_DIR/patch" ]; then
|
||||
if [ -f "$ROOT_DIR/data/project.dat" ]; then
|
||||
echo "[Pre-Setup] Step 2: Creating patch from data/project.dat"
|
||||
( cd "$ROOT_DIR" && "$UNPACKER" ./data/project.dat -c ) || echo "[Pre-Setup] ERROR: Create Patch failed."
|
||||
else
|
||||
echo "[Pre-Setup] Step 2: Skipping create patch (data/project.dat not found)."
|
||||
fi
|
||||
else
|
||||
echo "[Pre-Setup] Step 2: patch folder exists; skipping create."
|
||||
fi
|
||||
else
|
||||
echo "[Pre-Setup] SRPG_Unpacker.exe not found in root; skipping pre-setup steps."
|
||||
fi
|
||||
else
|
||||
echo "[Pre-Setup] data.dts not found; skipping pre-setup SRPG steps."
|
||||
fi
|
||||
|
||||
download_extract() {
|
||||
# Download zip file into root
|
||||
echo "Downloading latest patch..."
|
||||
curl -sL "https://gitgud.io/$username/$repo/-/archive/$branch/$repo-$branch.zip" -o "$ROOT_DIR/repo.zip"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Download failed!"
|
||||
rm -f "$ROOT_DIR/repo.zip"
|
||||
rm -rf "$ROOT_DIR/$repo-$branch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract contents, overwriting conflicts into root
|
||||
echo "Extracting..."
|
||||
unzip -qo "$ROOT_DIR/repo.zip" -d "$ROOT_DIR"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Extraction failed!"
|
||||
rm -f "$ROOT_DIR/repo.zip"
|
||||
rm -rf "$ROOT_DIR/$repo-$branch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Applying patch..."
|
||||
cp -r "$ROOT_DIR/$repo-$branch/"* "$ROOT_DIR/"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Patch application failed!"
|
||||
rm -f "$ROOT_DIR/repo.zip"
|
||||
rm -rf "$ROOT_DIR/$repo-$branch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Cleaning up..."
|
||||
rm -f "$ROOT_DIR/repo.zip"
|
||||
rm -rf "$ROOT_DIR/$repo-$branch"
|
||||
rm -f "$ROOT_DIR/latest_patch_sha.txt"
|
||||
|
||||
# Store latest SHA for next check in gameupdate
|
||||
echo "$latest_patch_sha" > "$SCRIPT_DIR/previous_patch_sha.txt"
|
||||
|
||||
# --------------------------------------------------------
|
||||
# POST-APPLY: Run Steps 3 and 4 after patch files are merged
|
||||
# 3) Apply Patch to data/project.dat
|
||||
# 4) Pack data back into data.dts
|
||||
# --------------------------------------------------------
|
||||
UNPACKER="$ROOT_DIR/SRPG_Unpacker.exe"
|
||||
if [ -f "$ROOT_DIR/data.dts" ]; then
|
||||
if [ -f "$UNPACKER" ]; then
|
||||
echo "Running SRPG_Unpacker apply/pack steps..."
|
||||
|
||||
# Step 3: Apply Patch
|
||||
if [ -f "$ROOT_DIR/data/project.dat" ]; then
|
||||
echo "Step 3: Applying patch to data/project.dat"
|
||||
( cd "$ROOT_DIR" && "$UNPACKER" ./data/project.dat -a ) || echo "ERROR: Apply Patch failed."
|
||||
else
|
||||
echo "ERROR: data/project.dat not found; cannot apply patch."
|
||||
fi
|
||||
|
||||
# Step 4: Pack
|
||||
if [ -d "$ROOT_DIR/data" ]; then
|
||||
echo "Step 4: Packing data -> data.dts"
|
||||
( cd "$ROOT_DIR" && "$UNPACKER" -o data.dts data ) || echo "WARNING: Pack failed."
|
||||
else
|
||||
echo "Step 4: Skipping pack (data folder not found)."
|
||||
fi
|
||||
else
|
||||
echo "SRPG_Unpacker.exe not found in root; skipping SRPG patch steps."
|
||||
fi
|
||||
else
|
||||
echo "data.dts not found; skipping SRPG patch steps."
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if previous_patch_sha.txt exists in gameupdate
|
||||
if [ ! -f "$SCRIPT_DIR/previous_patch_sha.txt" ]; then
|
||||
echo "Previous SHA hash not found!"
|
||||
echo "Assuming first time patching..."
|
||||
download_extract
|
||||
else
|
||||
# Read the stored SHA from previous check
|
||||
previous_patch_sha=$(cat "$SCRIPT_DIR/previous_patch_sha.txt")
|
||||
|
||||
# Compare trimmed SHAs
|
||||
if [ "$latest_patch_sha" != "$previous_patch_sha" ]; then
|
||||
echo "Update found! Patching..."
|
||||
download_extract
|
||||
else
|
||||
echo "Patch is up to date."
|
||||
fi
|
||||
fi
|
||||
|
|
@ -11,11 +11,13 @@ from .config_tab import ConfigTab
|
|||
from .rpgmaker_tab import RPGMakerTab
|
||||
from .log_viewer import LogViewer
|
||||
from .file_manager import FileManager
|
||||
from .workflow_tab import WorkflowTab
|
||||
|
||||
__all__ = [
|
||||
"DazedMTLGUI",
|
||||
"ConfigTab",
|
||||
"ConfigTab",
|
||||
"RPGMakerTab",
|
||||
"LogViewer",
|
||||
"FileManager"
|
||||
"FileManager",
|
||||
"WorkflowTab",
|
||||
]
|
||||
|
|
|
|||
24
gui/main.py
24
gui/main.py
|
|
@ -19,6 +19,7 @@ from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen
|
|||
# Import configuration widgets
|
||||
from gui.config_tab import ConfigTab
|
||||
from gui.translation_tab import TranslationTab
|
||||
from gui.workflow_tab import WorkflowTab
|
||||
|
||||
class DazedMTLGUI(QMainWindow):
|
||||
"""Main GUI window for the DazedMTLTool."""
|
||||
|
|
@ -188,10 +189,17 @@ class DazedMTLGUI(QMainWindow):
|
|||
btn_translation.clicked.connect(lambda: self.switch_page(0))
|
||||
sidebar_layout.addWidget(btn_translation)
|
||||
self.nav_buttons.append(btn_translation)
|
||||
|
||||
# Configuration button (second)
|
||||
|
||||
# Workflow / Automation button (second)
|
||||
btn_workflow = self.create_nav_button("⚡", "Workflow")
|
||||
btn_workflow.setToolTip("Workflow — automated translation pipeline")
|
||||
btn_workflow.clicked.connect(lambda: self.switch_page(1))
|
||||
sidebar_layout.addWidget(btn_workflow)
|
||||
self.nav_buttons.append(btn_workflow)
|
||||
|
||||
# Configuration button (third)
|
||||
btn_config = self.create_nav_button("⚙️", "Configuration")
|
||||
btn_config.clicked.connect(lambda: self.switch_page(1))
|
||||
btn_config.clicked.connect(lambda: self.switch_page(2))
|
||||
sidebar_layout.addWidget(btn_config)
|
||||
self.nav_buttons.append(btn_config)
|
||||
|
||||
|
|
@ -252,11 +260,15 @@ class DazedMTLGUI(QMainWindow):
|
|||
|
||||
def setup_tabs(self):
|
||||
"""Set up all the tabs in the interface."""
|
||||
# Translation Execution Tab (first)
|
||||
# Translation Execution Tab (index 0)
|
||||
self.translation_tab = TranslationTab(self)
|
||||
self.content_stack.addWidget(self.translation_tab)
|
||||
|
||||
# Configuration Tab (second)
|
||||
|
||||
# Workflow / Automation Tab (index 1)
|
||||
self.workflow_tab = WorkflowTab(self)
|
||||
self.content_stack.addWidget(self.workflow_tab)
|
||||
|
||||
# Configuration Tab (index 2)
|
||||
self.config_tab = ConfigTab()
|
||||
self.config_tab.config_changed.connect(self.on_config_changed)
|
||||
self.content_stack.addWidget(self.config_tab)
|
||||
|
|
|
|||
1569
gui/workflow_tab.py
Normal file
1569
gui/workflow_tab.py
Normal file
File diff suppressed because it is too large
Load diff
248
util/actor_substitutor.py
Normal file
248
util/actor_substitutor.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""
|
||||
Actor Variable Substitutor
|
||||
|
||||
RPGMaker uses \\n[X] (stored as \\n[X] in JSON, single backslash after json.load)
|
||||
to pull actor names dynamically at runtime. This module:
|
||||
|
||||
1. Builds a map of actor_id -> name from Actors.json
|
||||
(prefers translated/Actors.json over files/Actors.json so the AI
|
||||
receives the target-language name in context)
|
||||
|
||||
2. substitute_in_files() – walks every .json file inside `files/`,
|
||||
replaces \\n[X] with the actor's name, writes the file back.
|
||||
Returns per-file statistics.
|
||||
|
||||
3. restore_in_translated() – walks every .json file inside `translated/`,
|
||||
replaces the substituted name back with \\n[X].
|
||||
Returns per-file statistics.
|
||||
|
||||
The rename-able protagonist case is handled automatically: substitute the
|
||||
default/translated name for context during translation, then restore the
|
||||
\\n[X] variable so the player's custom name still works at runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Pattern that matches \n[X] after json.load() (single literal backslash)
|
||||
_VAR_RE = re.compile(r"\\n\[(\d+)\]", re.IGNORECASE)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_actor_map(
|
||||
files_dir: str | Path = "files",
|
||||
translated_dir: str | Path = "translated",
|
||||
) -> dict[int, str]:
|
||||
"""Return {actor_id: name}.
|
||||
|
||||
Prefers translated/Actors.json (English names) over files/Actors.json.
|
||||
"""
|
||||
for candidates in (
|
||||
Path(translated_dir) / "Actors.json",
|
||||
Path(files_dir) / "Actors.json",
|
||||
):
|
||||
if candidates.is_file():
|
||||
try:
|
||||
with open(candidates, "r", encoding="utf-8-sig") as fh:
|
||||
data = json.load(fh)
|
||||
actor_map: dict[int, str] = {}
|
||||
for entry in data:
|
||||
if not entry or not isinstance(entry, dict):
|
||||
continue
|
||||
actor_id = entry.get("id")
|
||||
name = (entry.get("name") or "").strip()
|
||||
if actor_id is not None and name:
|
||||
actor_map[int(actor_id)] = name
|
||||
if actor_map:
|
||||
return actor_map
|
||||
except Exception:
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def substitute_in_files(
|
||||
files_dir: str | Path = "files",
|
||||
actor_map: Optional[dict[int, str]] = None,
|
||||
translated_dir: str | Path = "translated",
|
||||
) -> dict:
|
||||
"""Replace \\n[X] with actor names in all .json files inside *files_dir*.
|
||||
|
||||
If *actor_map* is None it is built automatically via load_actor_map().
|
||||
Returns a summary dict:
|
||||
{
|
||||
"files_modified": int,
|
||||
"total_substitutions": int,
|
||||
"variables_found": {actor_id: name, ...},
|
||||
"errors": [str, ...]
|
||||
}
|
||||
"""
|
||||
files_dir = Path(files_dir)
|
||||
if actor_map is None:
|
||||
actor_map = load_actor_map(files_dir, translated_dir)
|
||||
|
||||
stats = {
|
||||
"files_modified": 0,
|
||||
"total_substitutions": 0,
|
||||
"variables_found": {},
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for fp in sorted(files_dir.glob("*.json")):
|
||||
try:
|
||||
original = fp.read_text(encoding="utf-8-sig")
|
||||
data = json.loads(original)
|
||||
count = [0]
|
||||
found: dict[int, str] = {}
|
||||
|
||||
new_data = _walk(data, actor_map, count, found, substitute=True)
|
||||
|
||||
if count[0] > 0:
|
||||
stats["total_substitutions"] += count[0]
|
||||
stats["files_modified"] += 1
|
||||
stats["variables_found"].update(found)
|
||||
out = json.dumps(new_data, ensure_ascii=False, indent=4)
|
||||
fp.write_text(out + "\n", encoding="utf-8")
|
||||
except Exception as exc:
|
||||
stats["errors"].append(f"{fp.name}: {exc}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def restore_in_translated(
|
||||
translated_dir: str | Path = "translated",
|
||||
actor_map: Optional[dict[int, str]] = None,
|
||||
files_dir: str | Path = "files",
|
||||
) -> dict:
|
||||
"""Replace actor names back with \\n[X] in all .json files inside *translated_dir*.
|
||||
|
||||
If *actor_map* is None it is built via load_actor_map() which prefers
|
||||
translated/Actors.json (the already-translated names).
|
||||
|
||||
Returns a summary dict with the same shape as substitute_in_files().
|
||||
"""
|
||||
translated_dir = Path(translated_dir)
|
||||
if actor_map is None:
|
||||
actor_map = load_actor_map(files_dir, translated_dir)
|
||||
|
||||
# Build reverse map: name (lowercased) → "\\n[X]"
|
||||
# We match case-insensitively so the AI's casing doesn't break restore.
|
||||
# We keep the original-case name for display in stats.
|
||||
reverse: dict[str, tuple[str, int]] = {} # lower_name -> (var_string, actor_id)
|
||||
for aid, name in actor_map.items():
|
||||
if name:
|
||||
reverse[name.lower()] = (f"\\n[{aid}]", aid)
|
||||
|
||||
stats = {
|
||||
"files_modified": 0,
|
||||
"total_substitutions": 0,
|
||||
"variables_found": {},
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for fp in sorted(translated_dir.glob("*.json")):
|
||||
# Never restore inside Actors.json itself — that file has the names
|
||||
# as first-class translated data, not as variable stand-ins.
|
||||
if fp.name == "Actors.json":
|
||||
continue
|
||||
try:
|
||||
data = json.loads(fp.read_text(encoding="utf-8-sig"))
|
||||
count = [0]
|
||||
found: dict[int, str] = {}
|
||||
|
||||
new_data = _walk_restore(data, reverse, count, found)
|
||||
|
||||
if count[0] > 0:
|
||||
stats["total_substitutions"] += count[0]
|
||||
stats["files_modified"] += 1
|
||||
stats["variables_found"].update(found)
|
||||
out = json.dumps(new_data, ensure_ascii=False, indent=4)
|
||||
fp.write_text(out + "\n", encoding="utf-8")
|
||||
except Exception as exc:
|
||||
stats["errors"].append(f"{fp.name}: {exc}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def scan_variables(
|
||||
files_dir: str | Path = "files",
|
||||
) -> dict[int, int]:
|
||||
"""Scan *files_dir* and return {actor_id: occurrence_count} without modifying files."""
|
||||
counts: dict[int, int] = {}
|
||||
for fp in Path(files_dir).glob("*.json"):
|
||||
try:
|
||||
text = fp.read_text(encoding="utf-8-sig")
|
||||
# Search in raw JSON text (\\n[ in file = single \n[ after parse)
|
||||
# The raw JSON file stores it as \\n[X] so we search that directly:
|
||||
for m in re.finditer(r"\\\\n\[(\d+)\]", text):
|
||||
aid = int(m.group(1))
|
||||
counts[aid] = counts.get(aid, 0) + 1
|
||||
except Exception:
|
||||
pass
|
||||
return counts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sub_string(s: str, actor_map: dict[int, str], count: list, found: dict) -> str:
|
||||
def replace(m: re.Match) -> str:
|
||||
aid = int(m.group(1))
|
||||
name = actor_map.get(aid)
|
||||
if name:
|
||||
count[0] += 1
|
||||
found[aid] = name
|
||||
return name
|
||||
return m.group(0)
|
||||
return _VAR_RE.sub(replace, s)
|
||||
|
||||
|
||||
def _restore_string(s: str, reverse: dict[str, tuple[str, int]], count: list, found: dict) -> str:
|
||||
"""Replace actor names with \\n[X] in a translated string."""
|
||||
# We need to handle the fact that names can be substrings of other words.
|
||||
# Build a single regex alternation sorted by length (longest first) to
|
||||
# prevent short names like "Al" matching inside "Alicia".
|
||||
if not reverse:
|
||||
return s
|
||||
pattern = re.compile(
|
||||
"|".join(re.escape(name) for name in sorted(reverse.keys(), key=len, reverse=True)),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
def replace(m: re.Match) -> str:
|
||||
lower_matched = m.group(0).lower()
|
||||
entry = reverse.get(lower_matched)
|
||||
if entry:
|
||||
var_str, aid = entry
|
||||
count[0] += 1
|
||||
found[aid] = m.group(0)
|
||||
return var_str
|
||||
return m.group(0)
|
||||
|
||||
return pattern.sub(replace, s)
|
||||
|
||||
|
||||
def _walk(obj, actor_map, count, found, substitute: bool):
|
||||
if isinstance(obj, str):
|
||||
return _sub_string(obj, actor_map, count, found)
|
||||
if isinstance(obj, list):
|
||||
return [_walk(item, actor_map, count, found, substitute) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: _walk(v, actor_map, count, found, substitute) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
def _walk_restore(obj, reverse, count, found):
|
||||
if isinstance(obj, str):
|
||||
return _restore_string(obj, reverse, count, found)
|
||||
if isinstance(obj, list):
|
||||
return [_walk_restore(item, reverse, count, found) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: _walk_restore(v, reverse, count, found) for k, v in obj.items()}
|
||||
return obj
|
||||
235
util/project_scanner.py
Normal file
235
util/project_scanner.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
"""
|
||||
Project Scanner - Detect game engine and import/export data files.
|
||||
|
||||
RPGMaker layouts scanned (in priority order):
|
||||
MV/MZ : <root>/www/data/ or <root>/data/
|
||||
Ace : <root>/Data/
|
||||
XP/VX : <root>/Data/ (rvdata/rxdata - not currently handled)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File categories used to decide what to import by default
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Core name/database files – almost always needed
|
||||
CORE_FILES = {
|
||||
"Actors.json",
|
||||
"Armors.json",
|
||||
"Classes.json",
|
||||
"Enemies.json",
|
||||
"Items.json",
|
||||
"MapInfos.json",
|
||||
"Skills.json",
|
||||
"States.json",
|
||||
"System.json",
|
||||
"Troops.json",
|
||||
"Weapons.json",
|
||||
"CommonEvents.json",
|
||||
"Animations.json",
|
||||
"Tilesets.json",
|
||||
}
|
||||
|
||||
# Map files – large but contain the bulk of dialogue
|
||||
MAP_PATTERN = "Map[0-9]*.json"
|
||||
|
||||
# Engine detection markers
|
||||
_MVMZ_MARKERS = { # files / dirs that hint at MV or MZ
|
||||
"www", # MV web build
|
||||
"package.json", # MZ desktop
|
||||
"Game.rpgproject", # MV
|
||||
"game.rmmzproject", # MZ
|
||||
}
|
||||
_ACE_MARKERS = {
|
||||
"Game.rgss3a",
|
||||
"Game.exe", # not conclusive but common
|
||||
}
|
||||
_ACE_DATA_SCRIPTS = {".rvdata2", ".rvdata"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def find_data_folder(game_root: str | Path) -> tuple[Optional[Path], str]:
|
||||
"""Return (data_path, engine_name) for *game_root*.
|
||||
|
||||
engine_name is one of: "MVMZ", "ACE", "UNKNOWN".
|
||||
data_path is None when nothing is found.
|
||||
"""
|
||||
root = Path(game_root)
|
||||
if not root.is_dir():
|
||||
return None, "UNKNOWN"
|
||||
|
||||
# ---- RPGMaker MV/MZ: www/data or data ----
|
||||
for candidate in (root / "www" / "data", root / "data", root / "Data"):
|
||||
if candidate.is_dir() and _has_json_data(candidate):
|
||||
# Confirm engine variant
|
||||
engine = "MVMZ"
|
||||
for m in _MVMZ_MARKERS:
|
||||
if (root / m).exists():
|
||||
engine = "MVMZ"
|
||||
break
|
||||
return candidate, engine
|
||||
|
||||
# ---- RPGMaker Ace: Data/ with .rvdata2 ----
|
||||
ace_data = root / "Data"
|
||||
if ace_data.is_dir():
|
||||
rvdata = list(ace_data.glob("*.rvdata2")) + list(ace_data.glob("*.rvdata"))
|
||||
if rvdata:
|
||||
return ace_data, "ACE"
|
||||
|
||||
# ---- Fallback: any sub-directory that holds .json files ----
|
||||
for child in root.iterdir():
|
||||
if child.is_dir() and _has_json_data(child):
|
||||
return child, "UNKNOWN"
|
||||
|
||||
return None, "UNKNOWN"
|
||||
|
||||
|
||||
def list_data_files(data_path: str | Path, engine: str = "MVMZ") -> list[dict]:
|
||||
"""Return a sorted list of importable file descriptors.
|
||||
|
||||
Each item: {"name": str, "path": Path, "size_kb": float,
|
||||
"category": "core" | "map" | "other", "default": bool}
|
||||
"""
|
||||
data_path = Path(data_path)
|
||||
results: list[dict] = []
|
||||
|
||||
if engine == "ACE":
|
||||
# Ace uses binary rvdata2; not JSON-based, skip for now
|
||||
return results
|
||||
|
||||
seen: set[str] = set()
|
||||
for fp in sorted(data_path.iterdir()):
|
||||
if not fp.is_file():
|
||||
continue
|
||||
if fp.suffix.lower() != ".json":
|
||||
continue
|
||||
name = fp.name
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
|
||||
size_kb = fp.stat().st_size / 1024
|
||||
if name in CORE_FILES:
|
||||
cat = "core"
|
||||
default = True
|
||||
elif fp.match(MAP_PATTERN):
|
||||
cat = "map"
|
||||
default = True
|
||||
else:
|
||||
cat = "other"
|
||||
default = False
|
||||
|
||||
results.append({
|
||||
"name": name,
|
||||
"path": fp,
|
||||
"size_kb": round(size_kb, 1),
|
||||
"category": cat,
|
||||
"default": default,
|
||||
})
|
||||
|
||||
# Sort: core first, then maps (numeric), then other
|
||||
def _sort_key(item):
|
||||
if item["category"] == "core":
|
||||
return (0, item["name"])
|
||||
if item["category"] == "map":
|
||||
# extract numeric part for natural order
|
||||
digits = "".join(c for c in item["name"] if c.isdigit()) or "0"
|
||||
return (1, int(digits))
|
||||
return (2, item["name"])
|
||||
|
||||
results.sort(key=_sort_key)
|
||||
return results
|
||||
|
||||
|
||||
def import_to_files(
|
||||
file_items: list[dict],
|
||||
dest_dir: str | Path = "files",
|
||||
overwrite: bool = True,
|
||||
) -> tuple[int, list[str]]:
|
||||
"""Copy *file_items* (from list_data_files) into *dest_dir*.
|
||||
|
||||
Returns (count_copied, list_of_errors).
|
||||
"""
|
||||
dest = Path(dest_dir)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
copied = 0
|
||||
errors: list[str] = []
|
||||
|
||||
for item in file_items:
|
||||
src: Path = item["path"]
|
||||
dst = dest / item["name"]
|
||||
try:
|
||||
if not overwrite and dst.exists():
|
||||
continue
|
||||
shutil.copy2(src, dst)
|
||||
copied += 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{item['name']}: {exc}")
|
||||
|
||||
return copied, errors
|
||||
|
||||
|
||||
def export_to_game(
|
||||
translated_dir: str | Path,
|
||||
game_data_path: str | Path,
|
||||
filenames: Optional[list[str]] = None,
|
||||
overwrite: bool = True,
|
||||
) -> tuple[int, list[str]]:
|
||||
"""Copy translated files back into the game's data folder.
|
||||
|
||||
If *filenames* is None, all .json files in *translated_dir* are copied.
|
||||
Returns (count_copied, list_of_errors).
|
||||
"""
|
||||
src_dir = Path(translated_dir)
|
||||
dst_dir = Path(game_data_path)
|
||||
copied = 0
|
||||
errors: list[str] = []
|
||||
|
||||
if not src_dir.is_dir():
|
||||
return 0, [f"Translated folder not found: {src_dir}"]
|
||||
if not dst_dir.is_dir():
|
||||
return 0, [f"Game data folder not found: {dst_dir}"]
|
||||
|
||||
candidates = (
|
||||
[src_dir / fn for fn in filenames]
|
||||
if filenames
|
||||
else list(src_dir.glob("*.json"))
|
||||
)
|
||||
|
||||
for fp in candidates:
|
||||
if not fp.is_file():
|
||||
continue
|
||||
dst = dst_dir / fp.name
|
||||
try:
|
||||
if not overwrite and dst.exists():
|
||||
continue
|
||||
shutil.copy2(fp, dst)
|
||||
copied += 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{fp.name}: {exc}")
|
||||
|
||||
return copied, errors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _has_json_data(folder: Path) -> bool:
|
||||
"""Return True if the folder contains at least one .json data file."""
|
||||
try:
|
||||
for p in folder.iterdir():
|
||||
if p.suffix.lower() == ".json":
|
||||
return True
|
||||
except PermissionError:
|
||||
pass
|
||||
return False
|
||||
225
util/speaker_detector.py
Normal file
225
util/speaker_detector.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
"""
|
||||
Speaker Format Detector for RPGMaker MV/MZ
|
||||
|
||||
Scans map / event files and scores three detection modes:
|
||||
|
||||
INLINE401SPEAKERS – 401 lines contain Name「dialogue」
|
||||
FIRSTLINESPEAKERS – a short (< 40 char) 401 followed by 401/405 starting
|
||||
with 「 " ( ( * [
|
||||
FACENAME101 – a 101 code (Show Text / face cmd) immediately precedes
|
||||
a 401 block
|
||||
|
||||
Returns the best mode and confidence scores for each.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Regex used to detect inline quote speaker: "Name「..." at start of 401 text
|
||||
_INLINE_RE = re.compile(r"^([^\s「」。、!?…\\\n]{1,20})「")
|
||||
|
||||
# Characters that signal the next 401 is dialogue (first-line speaker heuristic)
|
||||
_DIALOGUE_STARTERS = ("「", '"', "(", "(", "*", "[")
|
||||
|
||||
# Minimum number of qualifying events to consider a result reliable
|
||||
_MIN_HITS = 3
|
||||
|
||||
|
||||
def detect_speaker_format(
|
||||
files_dir: str | Path = "files",
|
||||
sample_size: int = 20,
|
||||
) -> dict:
|
||||
"""Scan up to *sample_size* map files and score speaker detection modes.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"best_mode": "INLINE401SPEAKERS" | "FIRSTLINESPEAKERS" | "FACENAME101" | "NONE",
|
||||
"scores": {
|
||||
"INLINE401SPEAKERS": int, # raw hit count
|
||||
"FIRSTLINESPEAKERS": int,
|
||||
"FACENAME101": int,
|
||||
},
|
||||
"total_401_groups": int, # total dialogue groups examined
|
||||
"files_scanned": int,
|
||||
"recommended_config": { # ready to apply to rpgmakermvmz.py
|
||||
"FIRSTLINESPEAKERS": bool,
|
||||
"INLINE401SPEAKERS": bool,
|
||||
"FACENAME101": bool,
|
||||
},
|
||||
"confidence": "high" | "medium" | "low",
|
||||
"note": str, # human-readable explanation
|
||||
}
|
||||
"""
|
||||
files_dir = Path(files_dir)
|
||||
scores = {
|
||||
"INLINE401SPEAKERS": 0,
|
||||
"FIRSTLINESPEAKERS": 0,
|
||||
"FACENAME101": 0,
|
||||
}
|
||||
total_groups = 0
|
||||
files_scanned = 0
|
||||
|
||||
# Collect map files (Maps only, not MapInfos)
|
||||
map_files = sorted(
|
||||
[p for p in files_dir.glob("Map[0-9]*.json") if p.is_file()],
|
||||
key=lambda p: p.stat().st_size,
|
||||
reverse=True, # scan larger maps first for better signal
|
||||
)[:sample_size]
|
||||
|
||||
for fp in map_files:
|
||||
try:
|
||||
with open(fp, "r", encoding="utf-8-sig") as fh:
|
||||
data = json.load(fh)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
files_scanned += 1
|
||||
events = data.get("events") or []
|
||||
for evt in events:
|
||||
if not evt:
|
||||
continue
|
||||
for page in evt.get("pages") or []:
|
||||
cmd_list = (page or {}).get("list") or []
|
||||
_score_command_list(cmd_list, scores)
|
||||
# Count 401 groups
|
||||
i = 0
|
||||
while i < len(cmd_list):
|
||||
if cmd_list[i].get("code") in (401, 405):
|
||||
total_groups += 1
|
||||
while i < len(cmd_list) and cmd_list[i].get("code") in (401, 405, -1):
|
||||
i += 1
|
||||
continue
|
||||
i += 1
|
||||
|
||||
# Also scan CommonEvents
|
||||
ce_path = files_dir / "CommonEvents.json"
|
||||
if ce_path.is_file():
|
||||
try:
|
||||
with open(ce_path, "r", encoding="utf-8-sig") as fh:
|
||||
ce_data = json.load(fh)
|
||||
files_scanned += 1
|
||||
for entry in ce_data or []:
|
||||
if not entry:
|
||||
continue
|
||||
cmd_list = entry.get("list") or []
|
||||
_score_command_list(cmd_list, scores)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Determine best mode
|
||||
best = max(scores, key=lambda k: scores[k])
|
||||
best_score = scores[best]
|
||||
second_best = sorted(scores.values(), reverse=True)[1]
|
||||
|
||||
if best_score < _MIN_HITS:
|
||||
best_mode = "NONE"
|
||||
confidence = "low"
|
||||
note = (
|
||||
f"No strong speaker pattern detected "
|
||||
f"(scanned {files_scanned} file(s), {total_groups} dialogue group(s)). "
|
||||
"Consider checking speaker settings manually."
|
||||
)
|
||||
elif best_score > second_best * 2:
|
||||
best_mode = best
|
||||
confidence = "high"
|
||||
note = (
|
||||
f"Strong signal for {best} "
|
||||
f"({best_score} hits vs {second_best} for next best, "
|
||||
f"{total_groups} dialogue group(s) in {files_scanned} file(s))."
|
||||
)
|
||||
else:
|
||||
best_mode = best
|
||||
confidence = "medium"
|
||||
note = (
|
||||
f"Moderate signal for {best} "
|
||||
f"({best_score} hits, {second_best} for next-best mode). "
|
||||
"Review a sample of dialogue manually to confirm."
|
||||
)
|
||||
|
||||
recommended = {
|
||||
"FIRSTLINESPEAKERS": best_mode == "FIRSTLINESPEAKERS",
|
||||
"INLINE401SPEAKERS": best_mode == "INLINE401SPEAKERS",
|
||||
"FACENAME101": best_mode == "FACENAME101",
|
||||
}
|
||||
|
||||
return {
|
||||
"best_mode": best_mode,
|
||||
"scores": scores,
|
||||
"total_401_groups": total_groups,
|
||||
"files_scanned": files_scanned,
|
||||
"recommended_config": recommended,
|
||||
"confidence": confidence,
|
||||
"note": note,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _score_command_list(cmd_list: list, scores: dict) -> None:
|
||||
"""Walk a command list and increment scores for detected patterns."""
|
||||
if not cmd_list:
|
||||
return
|
||||
|
||||
last_101_face: str | None = None # face name from most recent 101 cmd
|
||||
|
||||
i = 0
|
||||
while i < len(cmd_list):
|
||||
cmd = cmd_list[i]
|
||||
code = cmd.get("code")
|
||||
params = cmd.get("parameters") or []
|
||||
|
||||
# ---- 101: Show Text (face) ----
|
||||
if code == 101:
|
||||
# params[0] = face name, params[1] = face index
|
||||
last_101_face = (params[0] if params else None) or ""
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# ---- 401 / 405: dialogue line ----
|
||||
if code in (401, 405):
|
||||
text = (params[0] if params else "") or ""
|
||||
|
||||
# Score INLINE401SPEAKERS
|
||||
if _INLINE_RE.match(text):
|
||||
scores["INLINE401SPEAKERS"] += 1
|
||||
|
||||
# Score FACENAME101 — if face was set right before this block
|
||||
if last_101_face is not None:
|
||||
scores["FACENAME101"] += 1
|
||||
last_101_face = None # only count once per block
|
||||
|
||||
# Score FIRSTLINESPEAKERS — short line with no dialogue starters,
|
||||
# followed by a 401/405 that starts with a dialogue starter
|
||||
if (
|
||||
len(text) < 40
|
||||
and not any(text.lstrip().startswith(s) for s in _DIALOGUE_STARTERS)
|
||||
and _has_japanese(text)
|
||||
):
|
||||
j = i + 1
|
||||
while j < len(cmd_list) and cmd_list[j].get("code") == -1:
|
||||
j += 1
|
||||
if j < len(cmd_list) and cmd_list[j].get("code") in (401, 405):
|
||||
next_text = ((cmd_list[j].get("parameters") or [""])[0]) or ""
|
||||
if next_text.lstrip().startswith(_DIALOGUE_STARTERS):
|
||||
scores["FIRSTLINESPEAKERS"] += 1
|
||||
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Any non-dialogue code resets the face context
|
||||
if code not in (-1,):
|
||||
last_101_face = None
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
_JP_RE = re.compile(r"[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uF900-\uFAFF\uFF61-\uFF9F]")
|
||||
|
||||
|
||||
def _has_japanese(text: str) -> bool:
|
||||
return bool(_JP_RE.search(text))
|
||||
Loading…
Reference in a new issue