fix(workflow): scope game tools and externalize prompts

- Install UberWolf CLI only for detected WOLF games
- Move static clipboard prompts into editable shipped skills
- Improve plugin and Ace translation audit workflows
- Add regression coverage for engine filtering and prompt loading
This commit is contained in:
DazedAnon 2026-07-25 14:23:27 -05:00
parent 2e7f816ecd
commit 38ca8e44be
17 changed files with 632 additions and 424 deletions

View file

@ -29,6 +29,11 @@ Edit tool-level markdown:
- `data/skills/system.md` - base translation system prompt
- `data/skills/project_setup.md` - clipboard skill for IDE setup
- `data/skills/wrap_config.md` - RPG Maker wrap-width analysis prompt
- `data/skills/plugin_translation.md` - MV/MZ plugin translation prompt
- `data/skills/ace_script_translation.md` - VX Ace Ruby script translation prompt
- `data/skills/risky_codes.md` - RPG Maker optional event-code audit prompt
- `data/skills/wolf_speakers.md` - WOLF speaker-format audit prompt
- `data/translation_contexts.json` - per-call history templates
Per-game quirks / Translation Frame live under `<game>/skills/` and are edited in Workflow Step 2, not here.

View file

@ -0,0 +1,44 @@
You are an expert RPG Maker VX Ace Ruby/RGSS3 localisation engineer working inside an IDE with access to this game project.
<goal>
Audit ace_json/scripts/*.rb for player-visible Japanese, tell me which scripts need localisation, and—only after I approve—translate the safe items directly in those files without breaking game logic.
</goal>
<inputs>
- Read vocab.txt and the extracted Ruby files under ace_json/scripts/.
- Use any script index/manifest to preserve script names and ordering.
- Treat vocab.txt as authoritative for names and terminology.
- If referenced script sources are missing or still packed, list what is needed and ask me to extract or provide them.
</inputs>
<phase_1_audit_only>
Do not edit anything yet. Return a compact table with:
- Script/file
- Approximate count of player-visible Japanese strings
- 1-3 short examples with line references
- Classification: Easy/safe, Needs review, or No translation needed
- Why it appears visible or risky
Then ask one focused question: which listed scripts should you translate? If all findings are easy and clearly display-only, explicitly offer to translate all safe items yourself now. Do not ask me to inspect every string manually. If nothing needs work, say so and stop.
</phase_1_audit_only>
<translate_only_when_approved>
Translate only literals proven to reach the player through windows, draw_text/help methods, messages, menus, battle logs, or notifications. A string passed to print or p is not automatically player-visible; verify its runtime use.
Never translate hash keys, symbols, identifiers, class/module/method/constant names, lookup values, case/when or equality operands, filenames, paths, URLs, fonts, colors, regexes, save-data keys, or text read back elsewhere. Preserve Ruby interpolation (#{...}), printf/sprintf placeholders, escape sequences, control codes, quote style, encoding, and syntax. Search usages before changing an ambiguous literal; skip it if safety cannot be established.
Edit approved .rb files directly and make the smallest possible changes. Validate Ruby syntax when a suitable local check exists, and preserve the extracted script layout so RV2JSON can repack it.
</translate_only_when_approved>
<response_rules>
Never paste or repost an entire script. After edits, report only:
- Files/scripts changed and number of strings translated
- A few representative before -> after examples with line references
- Skipped ambiguous strings and the reason
- Validation performed
If direct file editing is unavailable, provide only a minimal unified diff or targeted replacements—never complete file contents.
</response_rules>
Start with the audit and approval question. Do not translate yet.

View file

@ -0,0 +1,44 @@
You are an expert RPG Maker MV/MZ localisation engineer working inside an IDE with access to this game project.
<goal>
Audit enabled plugins for player-visible Japanese, tell me which plugins need localisation, and—only after I approve—translate the safe items directly in the project files without breaking plugin behavior.
</goal>
<inputs>
- Read js/plugins.js and vocab.txt.
- For every enabled entry in js/plugins.js, inspect its parameter values.
- If available, also inspect the matching js/plugins/<PluginName>.js source file for visible Japanese literals. If a required source file is missing, list it and ask me to provide it.
- Treat vocab.txt as authoritative for names and terminology.
</inputs>
<phase_1_audit_only>
Do not edit anything yet. Return a compact table with:
- Plugin and file
- Approximate count of player-visible Japanese strings
- 1-3 short examples with line/parameter references
- Classification: Easy/safe, Needs review, or No translation needed
- Why it appears visible or risky
Then ask one focused question: which listed plugins should you translate? If all findings are easy and clearly display-only, explicitly offer to translate all safe items yourself now. Do not ask me to inspect every string manually. If nothing needs work, say so and stop.
</phase_1_audit_only>
<translate_only_when_approved>
Translate only strings proven to be displayed to the player, including UI labels, window titles, help text, notifications, battle messages, and visible plugin parameters.
Never translate plugin names, parameter/property keys, identifiers, lookup values, plugin commands, switch/variable names, filenames, paths, URLs, fonts, color codes, regexes, booleans, numeric strings, or text compared/read back elsewhere. Preserve placeholders, escapes, control codes, interpolation, whitespace, encoding, and JavaScript syntax. Search usages before changing an ambiguous literal; skip it if safety cannot be established.
Edit approved files directly and make the smallest possible changes. Validate syntax when a suitable local check exists.
</translate_only_when_approved>
<response_rules>
Never paste or repost an entire file. After edits, report only:
- Files/plugins changed and number of strings translated
- A few representative before -> after examples with line/parameter references
- Skipped ambiguous strings and the reason
- Validation performed
If direct file editing is unavailable, provide only a minimal unified diff or targeted replacements—never complete file contents.
</response_rules>
Start with the audit and approval question. Do not translate yet.

145
data/skills/risky_codes.md Normal file
View file

@ -0,0 +1,145 @@
You are an expert RPGMaker MV/MZ event system analyst.
<task>
Audit several optional event code types and report: (a) which contain player-visible Japanese text that needs translation, and (b) for code 122, exactly which variable ID ranges carry display text versus internal keys.
</task>
--- attach Actors.json, CommonEvents.json, Troops.json, Map*.json, and js/plugins.js here ---
<actor_context>
Read Actors.json in full before auditing event commands. Use it to resolve actor IDs, \N[n] references, and code 320/324/325 targets. If you see an event reference to a major actor whose full character vocab is missing or only appears as a placeholder mapping like \N[3] (Keimi), report that Actors.json should be included in the glossary build and that a full # Game Characters entry is needed.
</actor_context>
<audit_1 code="122">
Code 122 — Control Variables — which variable IDs carry display text
A code 122 entry looks like:
{ "code": 122, "parameters": [startVarId, endVarId, 0, 4, "\"some string\""] }
parameters[0] is the variable ID being set; parameters[4] is the string value
(only present when parameters[3] == 4, direct string assignment).
Collect every code 122 where parameters[3] == 4. For each variable ID found:
1. Is the string also tested in a code 111 $gameVariables comparison?
→ DO NOT TRANSLATE (would break game logic)
2. Is the string used as an internal ID / plugin key / script argument?
→ DO NOT TRANSLATE
3. Is the string purely player-visible display text?
→ SAFE TO TRANSLATE
Summarise translatable variable IDs as compact numeric ranges,
e.g. 'Translate IDs: 5, 10-18, 42'. Separately list any DO NOT TRANSLATE IDs
with the reason. The ranges will be entered as min/max in the translation tool.
</audit_1>
<audit_2>
Plugin codes with visible text — 357 / 356 / 355-655 / 657 / 320 / 324 / 325 / 108
For each code type below, scan all events and report whether any instance contains Japanese text visible to the player at runtime.
--- Code 357 (MZ Plugin Commands) ---
parameters[0] = plugin header; parameters[3] = data dict.
Look up each unique header in js/plugins.js.
Does any key in parameters[3] hold Japanese text shown on screen?
The module already handles: AdvExtentionllk, VisuMZ_4_ProximityMessages, LL_GalgeChoiceWindow
For other headers with visible text, check the commented-out headerMappings list:
"LL_InfoPopupWIndow": (["messageText"], None),
"QuestSystem": (["DetailNote"], None),
"BalloonInBattle": (["text"], None),
"MNKR_CommonPopupCoreMZ": (["text"], None),
"DestinationWindow": (["destination"], None),
"_TMLogWindowMZ": (["text"], None),
"TorigoyaMZ_NotifyMessage": (["message"], None),
"SoR_GabWindow": (["arg1"], None),
"DarkPlasma_CharacterText": (["text"], None),
"DTextPicture": (["text"], None),
"TextPicture": (["text"], None),
"TRP_SkitMZ": (["name"], None),
"LogWindow": (["text"], None),
"BattleLogOutput": (["message"], None),
"TorigoyaMZ_NotifyMessage_CommandMessage": (["message"], None),
"NUUN_SaveScreen": (["AnyName"], None),
"build/ARPG_Core": (["Text", "SkillByName"], None),
Output: EXACT uncomment line, or describe parameters[3] if header is new.
--- Code 356 (MV Plugin Commands) ---
parameters[0] is a space-delimited string, e.g. 'D_TEXT テキスト 24'.
The module already handles: D_TEXT, ShowInfo, PushGab, addLog, DW_*, CommonPopup, AddCustomChoice.
Does any 356 line have Japanese that is shown on screen?
If yes: ENABLE CODE356 and list the command keywords found.
If no: SKIP CODE356.
--- Code 355/655 (Inline Scripts) ---
Code 355 starts a script block; code 655 continues it.
parameters[0] is the raw JS/script text.
For each block with Japanese in a string passed to a message/popup/log function:
• The leading keyword/pattern that identifies the block
• A regex capturing only the display substring, e.g. テキスト-(.+)
• Whether it is single-line (355 only) or multi-line (355 + 655)
• The exact entry to add to the patterns dict:
"<keyword>": (r"<regex>", <True|False>),
--- Code 657 (Picture Text) ---
parameters[0] is a plain string drawn onto a picture.
Does any 657 entry contain Japanese text visible on screen (not a filename)?
If yes: ENABLE CODE657. If no: SKIP CODE657.
--- Codes 320 / 324 / 325 (Actor Name / Nickname / Profile) ---
parameters[1] is the new name/nickname/profile string.
Do any of these codes set Japanese strings visible to the player
(not just internal IDs or filenames)?
If yes: ENABLE the respective code. If no: SKIP.
--- Code 108 (Comment / Notetag) ---
parameters[0] is a comment string used as a plugin notetag.
The module only translates 108 lines that match specific patterns:
info:, ActiveMessage:, event_text, Menu Name, text_indicator, NW名前指定
Do any 108 entries matching those patterns have Japanese visible to the player?
If yes: ENABLE CODE108 and list the patterns found. If no: SKIP CODE108.
</audit_2>
<output_format>
Reply with these exact sections:
### Code 122 — Variable Translation Ranges
Translate IDs : <compact ranges, e.g. 5, 10-18, 42>
Do NOT translate: <ID reason>
If none: 'No display-text string assignments found.'
### Code 357 — Plugin Handlers to Enable
• Already handled : <header> — no action needed
• Enable in module : uncomment → "<header>": (["<key>"], None),
• New entry needed : <header> — parameters[3] structure: <description>
• No visible text : <header> — internal only, skip
If none: 'No code 357 visible text found.'
### Code 356 — Enable or Skip
ENABLE / SKIP — keywords found: <list>
### Code 355/655 — Script Patterns to Add
• Pattern key : <keyword>
• Regex : <capture regex>
• Multiline : <true/false>
• Module line : "<keyword>": (r"<regex>", <True|False>),
If none: 'No translatable 355/655 scripts found.'
### Code 657 — Enable or Skip
ENABLE / SKIP — <brief reason>
### Codes 320 / 324 / 325 — Enable or Skip
320: ENABLE / SKIP 324: ENABLE / SKIP 325: ENABLE / SKIP
### Code 108 — Enable or Skip
ENABLE / SKIP — patterns found: <list>
### GUI Action Summary
After all audit sections above, output this final block to configure the GUI:
ENABLE CODES : <comma-separated CODE* keys to check, e.g. CODE357, CODE356>
SKIP CODES : <codes that have no visible text>
357 PLUGINS : <comma-separated HEADER_MAPPINGS_357 keys to enable>
355/655 PATTERNS : <comma-separated PATTERNS_355655 keys to enable>
122 VAR RANGE : min=<N>, max=<M>
If a field has nothing to fill in, write NONE.
</output_format>

View file

@ -0,0 +1,26 @@
You are an expert Japanese WOLF RPG Editor translator helping configure a translation tool.
<background>
WolfDawn extracts each line with a 'speaker' and a 'speaker_src' tag describing how it detected the speaker. Two tags carry the speaker name baked into the FIRST line of the 'source' text (the rest is the dialogue body):
- literal_line1 : HIGH confidence - a face/name window precedes the line, so the first line really is a nameplate. The tool always reshapes these; nothing to decide.
- literal_line1_lowconf : LOW confidence - a short first line with NO preceding face window. WolfDawn guesses it is a speaker name, but it might actually be the start of the dialogue or narration.
When a first-line format is trusted, the tool reshapes 'Name\nbody' into '[Name]: body' for translation, then restores 'Name\nbody' on inject (byte-safe either way).
</background>
--- attach the extracted JSON in files/ here (maps / CommonEvent) before continuing ---
<task>
Decide whether the LOW-confidence first-line guesses (speaker_src = literal_line1_lowconf) should be treated as speaker names for THIS game. Inspect a good sample of those entries in files/: look at the first line of each such 'source' and judge whether it is genuinely a character/speaker label as opposed to real dialogue or narration.
- ENABLE if the low-confidence first lines are overwhelmingly real speaker names.
- DISABLE if many are actually dialogue/narration (reshaping them would mislabel lines).
(The high-confidence nameplates are always handled; you are only ruling on the guesses.)
</task>
<output_format>
Return ONLY a fenced code block containing the recommendation and a one-line reason, e.g.
```
LOWCONF_FIRSTLINE: ENABLE - low-confidence first lines are short character names (市民, 兵士...).
```
Use exactly ENABLE or DISABLE after the colon.
</output_format>

View file

@ -0,0 +1,32 @@
You are an expert RPGMaker MV/MZ configuration analyst.
<task>
Calculate the correct text-wrap width settings for a Japanese-to-English RPGMaker MV/MZ translation tool. The tool wraps translated English using character-count limits (not pixels). I need three values: width, listWidth, and noteWidth.
</task>
--- attach System.json and js/plugins.js here before continuing ---
<instructions>
1. Read screenWidth and fontSize from System.json.
Check js/plugins.js for any MessageCore or Window plugin that overrides these values.
2. For each window type, estimate its pixel width, subtract ~48px padding, then calculate:
chars = floor(content_px / (font_size × 0.58))
- width: main dialogue/message box (Show Text) — typically full screen width
- listWidth: item/skill/help description windows — typically full or half screen width
- noteWidth: database note fields — typically the narrowest pane (~4050% screen width)
3. If font size is above 26px and reducing it would meaningfully increase characters per line, note where to change it (System.json or the relevant plugin parameter).
</instructions>
<output_format>
Output only the final values — do not show calculations:
```
width=<N>
listWidth=<N>
noteWidth=<N>
fontSize=<N> # or: no change needed
```
Followed by one sentence of assumptions if anything was estimated.
</output_format>

View file

@ -376,6 +376,21 @@ function Test-WolfLooseDataReady {
return $false
}
function Test-WolfGameRoot {
param([Parameter(Mandatory = $true)][string]$Root)
foreach ($name in @('Data.wolf', 'data.wolf')) {
if (Test-Path -LiteralPath (Join-Path $Root $name) -PathType Leaf) {
return $true
}
}
foreach ($name in @('Data', 'data')) {
if (Test-WolfLooseDataReady -DataDir (Join-Path $Root $name)) {
return $true
}
}
return $false
}
function Find-UberWolfCli {
param([Parameter(Mandatory = $true)][string]$Root)
$candidates = @(
@ -849,10 +864,13 @@ function Invoke-PatchDownloadExtract {
throw ('PATCH_ERR:ZIP:Expected one root folder in archive, found {0}.' -f $dirs.Count)
}
$stagedRoot = $dirs[0].FullName
$isWolfGame = Test-WolfGameRoot -Root $Root
$wolfOnlyNames = @('UberWolfCli.exe', 'UberWolfCli.LICENSE.txt')
# 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')) {
if ($isWolfGame) {
# Stage UberWolfCli before unpack so packed WOLF games can
# convert Data.wolf -> Data/ before English files land.
foreach ($cliName in $wolfOnlyNames) {
$stagedCli = Join-Path $stagedRoot $cliName
$destCli = Join-Path $Root $cliName
if ((Test-Path -LiteralPath $stagedCli -PathType Leaf) -and
@ -861,8 +879,26 @@ function Invoke-PatchDownloadExtract {
}
}
Invoke-WolfPreSetup -Root $Root
}
Copy-Item -Path (Join-Path $stagedRoot '*') -Destination $PWD.Path -Recurse -Force
# Copy file-by-file so WOLF-only tools never leak into RPG Maker,
# SRPG, or other game roots through a downloaded patch archive.
$sourcePrefix = $stagedRoot.TrimEnd(
[IO.Path]::DirectorySeparatorChar,
[IO.Path]::AltDirectorySeparatorChar
) + [IO.Path]::DirectorySeparatorChar
foreach ($file in Get-ChildItem -LiteralPath $stagedRoot -Recurse -File -Force) {
if ((-not $isWolfGame) -and ($file.Name -in $wolfOnlyNames)) {
continue
}
$relative = $file.FullName.Substring($sourcePrefix.Length)
$destination = Join-Path $Root $relative
$destinationDir = Split-Path -Parent $destination
if (-not (Test-Path -LiteralPath $destinationDir -PathType Container)) {
New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null
}
Copy-Item -LiteralPath $file.FullName -Destination $destination -Force
}
}
finally {
if (Test-Path -LiteralPath $zipPath) {

View file

@ -225,6 +225,17 @@ wolf_loose_data_ready() {
return 1
}
is_wolf_game() {
local name data_dir
for name in Data.wolf data.wolf; do
[ -f "$ROOT_DIR/$name" ] && return 0
done
for data_dir in "$ROOT_DIR/Data" "$ROOT_DIR/data"; do
wolf_loose_data_ready "$data_dir" && return 0
done
return 1
}
find_uberwolf_cli() {
if [ -f "$ROOT_DIR/UberWolfCli.exe" ]; then
echo "$ROOT_DIR/UberWolfCli.exe"
@ -389,6 +400,9 @@ download_extract() {
return 1
fi
wolf_patch=0
if is_wolf_game; then
wolf_patch=1
# 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
@ -397,9 +411,24 @@ download_extract() {
fi
done
invoke_wolf_pre_setup
fi
echo "Applying patch..."
if ! cp -rf "$inner"/* "$ROOT_DIR/"; then
copy_failed=0
while IFS= read -r -d '' patch_file; do
relative_path="${patch_file#"$inner"/}"
file_name="${patch_file##*/}"
if [ "$wolf_patch" -ne 1 ] && \
{ [ "$file_name" = "UberWolfCli.exe" ] || [ "$file_name" = "UberWolfCli.LICENSE.txt" ]; }; then
continue
fi
target_path="$ROOT_DIR/$relative_path"
if ! mkdir -p "$(dirname "$target_path")" || ! cp -f "$patch_file" "$target_path"; then
copy_failed=1
break
fi
done < <(find "$inner" -type f -print0)
if [ "$copy_failed" -ne 0 ]; then
echo "Patch application failed!"
rm -rf "$TMP_EX"
rm -f "$ROOT_DIR/repo.zip"

View file

@ -97,6 +97,41 @@ class SkillsTab(QWidget):
),
is_json=False,
)
self._add_file_page(
key="wrap_config",
tab_title="Wrap Config",
path=SKILLS_DIR / "wrap_config.md",
hint="Clipboard prompt that asks the IDE to calculate RPG Maker wrap widths.",
is_json=False,
)
self._add_file_page(
key="plugin_translation",
tab_title="Plugin TL",
path=SKILLS_DIR / "plugin_translation.md",
hint="MV/MZ plugin localisation audit and approved in-place translation prompt.",
is_json=False,
)
self._add_file_page(
key="ace_script_translation",
tab_title="Ace Script TL",
path=SKILLS_DIR / "ace_script_translation.md",
hint="VX Ace Ruby script localisation audit and approved in-place translation prompt.",
is_json=False,
)
self._add_file_page(
key="risky_codes",
tab_title="Risky Codes",
path=SKILLS_DIR / "risky_codes.md",
hint="Clipboard prompt for auditing optional RPG Maker event-code translation settings.",
is_json=False,
)
self._add_file_page(
key="wolf_speakers",
tab_title="WOLF Speakers",
path=SKILLS_DIR / "wolf_speakers.md",
hint="Clipboard prompt for reviewing WOLF low-confidence first-line speakers.",
is_json=False,
)
self._add_file_page(
key="contexts",
tab_title="Contexts",

View file

@ -113,7 +113,7 @@ from util.project_scanner import (
wolf_repair_nested_data_dir,
wolf_unpack_out_dir,
)
from util.skills import load_project_setup
from util.skills import load_clipboard_skill, load_project_setup
# Workflow-level label for the non-batch (live) translation path. The Translation
# tab's own mode is called "Translate"; _workflow_mode_text() maps to that.
@ -129,47 +129,7 @@ WORK_DIR_NAME = "wolf_json"
PHASE_NAMES_KINDS = {"names"}
PHASE_DB_KINDS = {"db"}
PHASE_MAPS_EVENTS_KINDS = {"map", "common", "gamedat", "txt", "txt-dir"}
# Speaker-format prompt for a repo-aware AI. WolfDawn already detects and tags who
# speaks on each line, so the only decision left is whether its LOW-confidence
# first-line guesses are really speaker names for this game. The AI inspects the
# extracted text and returns a single ENABLE/DISABLE recommendation.
_WOLF_SPEAKER_PROMPT = (
"You are an expert Japanese WOLF RPG Editor translator helping configure a translation tool.\n"
"\n"
"<background>\n"
"WolfDawn extracts each line with a 'speaker' and a 'speaker_src' tag describing how it "
"detected the speaker. Two tags carry the speaker name baked into the FIRST line of the "
"'source' text (the rest is the dialogue body):\n"
" - literal_line1 : HIGH confidence - a face/name window precedes the line, so the "
"first line really is a nameplate. The tool always reshapes these; nothing to decide.\n"
" - literal_line1_lowconf : LOW confidence - a short first line with NO preceding face "
"window. WolfDawn guesses it is a speaker name, but it might actually be the start of the "
"dialogue or narration.\n"
"When a first-line format is trusted, the tool reshapes 'Name\\nbody' into '[Name]: body' for "
"translation, then restores 'Name\\nbody' on inject (byte-safe either way).\n"
"</background>\n"
"\n"
"--- attach the extracted JSON in files/ here (maps / CommonEvent) before continuing ---\n"
"\n"
"<task>\n"
"Decide whether the LOW-confidence first-line guesses (speaker_src = literal_line1_lowconf) "
"should be treated as speaker names for THIS game. Inspect a good sample of those entries in "
"files/: look at the first line of each such 'source' and judge whether it is genuinely a "
"character/speaker label as opposed to real dialogue or narration.\n"
" - ENABLE if the low-confidence first lines are overwhelmingly real speaker names.\n"
" - DISABLE if many are actually dialogue/narration (reshaping them would mislabel lines).\n"
"(The high-confidence nameplates are always handled; you are only ruling on the guesses.)\n"
"</task>\n"
"\n"
"<output_format>\n"
"Return ONLY a fenced code block containing the recommendation and a one-line reason, e.g.\n"
"```\n"
"LOWCONF_FIRSTLINE: ENABLE - low-confidence first lines are short character names (市民, 兵士...).\n"
"```\n"
"Use exactly ENABLE or DISABLE after the colon.\n"
"</output_format>\n"
)
# The speaker-format prompt is loaded from editable data/skills/wolf_speakers.md.
class _WolfTaskWorker(QThread):
@ -2160,7 +2120,7 @@ class WolfWorkflowTab(QWidget):
def _copy_wolf_speaker_prompt(self):
try:
QApplication.clipboard().setText(_WOLF_SPEAKER_PROMPT)
QApplication.clipboard().setText(load_clipboard_skill("wolf_speakers.md"))
self._log(
"📋 Speaker-format prompt copied. Paste it into Cursor/Copilot with files/ open; "
"set the low-confidence box to match its recommendation."

View file

@ -21,7 +21,7 @@ import threading
from pathlib import Path
from util.paths import VOCAB_PATH, APP_NAME, ORG_NAME
from util.skills import load_project_setup
from util.skills import load_clipboard_skill, load_project_setup
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
import jsbeautifier
@ -318,6 +318,17 @@ _GAMEUPDATE_COPY_SKIP_NAMES = frozenset({
"previous_patch_sha.txt",
})
# UberWolf is only useful for WOLF RPG Editor archives. The generic workflow
# handles RPG Maker MV/MZ/Ace and must never install these files; the dedicated
# WOLF workflow intentionally continues using the smaller shared skip set.
_WOLF_ONLY_GAMEUPDATE_NAMES = frozenset({
"UberWolfCli.exe",
"UberWolfCli.LICENSE.txt",
})
_RPG_GAMEUPDATE_COPY_SKIP_NAMES = (
_GAMEUPDATE_COPY_SKIP_NAMES | _WOLF_ONLY_GAMEUPDATE_NAMES
)
class _FileCopyWorker(QThread):
"""Recursively copy a source folder into a destination folder."""
@ -1155,345 +1166,7 @@ class WorkflowTab(QWidget):
# ── Step 3: Vocab / Glossary ────────────────────────────────────────────
# ── Copilot prompt templates ────────────────────────────────────────────
_WRAP_PROMPT = (
"You are an expert RPGMaker MV/MZ configuration analyst.\n"
"\n"
"<task>\n"
"Calculate the correct text-wrap width settings for a Japanese-to-English RPGMaker MV/MZ "
"translation tool. The tool wraps translated English using character-count limits (not pixels). "
"I need three values: width, listWidth, and noteWidth.\n"
"</task>\n"
"\n"
"--- attach System.json and js/plugins.js here before continuing ---\n"
"\n"
"<instructions>\n"
"1. Read screenWidth and fontSize from System.json.\n"
" Check js/plugins.js for any MessageCore or Window plugin that overrides these values.\n"
"2. For each window type, estimate its pixel width, subtract ~48px padding, then calculate:\n"
" chars = floor(content_px / (font_size × 0.58))\n"
" - width: main dialogue/message box (Show Text) — typically full screen width\n"
" - listWidth: item/skill/help description windows — typically full or half screen width\n"
" - noteWidth: database note fields — typically the narrowest pane (~4050% screen width)\n"
"3. If font size is above 26px and reducing it would meaningfully increase characters per line, "
"note where to change it (System.json or the relevant plugin parameter).\n"
"</instructions>\n"
"\n"
"<output_format>\n"
"Output only the final values — do not show calculations:\n"
"\n"
"```\n"
"width=<N>\n"
"listWidth=<N>\n"
"noteWidth=<N>\n"
"fontSize=<N> # or: no change needed\n"
"```\n"
"\n"
"Followed by one sentence of assumptions if anything was estimated.\n"
"</output_format>\n"
)
_PLUGINS_JS_TRANSLATE_PROMPT = (
"You are an expert RPGMaker MV/MZ localisation engineer.\n"
"\n"
"<task>\n"
"Translate visible Japanese strings inside js/plugins.js without breaking any game logic "
"or plugin functionality. A vocab.txt glossary is attached — use it as your primary reference. "
"Any name or term that appears in the glossary must be translated exactly as shown there.\n"
"</task>\n"
"\n"
"--- attach js/plugins.js and vocab.txt here before continuing ---\n"
"\n"
"\n"
"<translate>\n"
"Only translate string values directly shown to the player at runtime:\n"
"- Menu/button labels in-game UI (e.g. 決定, キャンセル)\n"
"- Scene/window title text (e.g. アイテム, スキル, 装備)\n"
"- In-game popup, tooltip, or notification messages\n"
"- Default NPC names or pronouns used in UI display\n"
"- Help or description text shown in help windows\n"
"- Battle log messages or status effect messages\n"
"</translate>\n"
"\n"
"<do_not_translate>\n"
"Translating the following WILL BREAK THE GAME:\n"
"\n"
"1. Plugin parameter keys (object property names).\n"
" { \"CommandName\": \"セーブ\" } → translate セーブ but NOT CommandName\n"
"\n"
"2. Strings used as internal identifiers or lookup keys:\n"
" - Switch/variable names matching System.json entries\n"
" - Actor, skill, item, weapon, armour names used as keys inside other plugin parameters\n"
" - Strings passed as plugin command arguments that the engine looks up\n"
"\n"
"3. File paths, filenames, URLs, colour codes, font names, icon indices.\n"
" (e.g. img/faces/Actor1 or #ffffff)\n"
"\n"
"4. Plugin names and script identifiers:\n"
" - The \"name\" property at the top of each plugin block (e.g. { \"name\": \"YEP_CoreEngine\" })\n"
" - Function identifiers, class names, JS event names\n"
"\n"
"5. Any string in the game data files (Actors.json, Skills.json, Items.json, etc.) that is "
"also used as a lookup key in the plugin — changing it here would desync it from the data file.\n"
"\n"
"6. Boolean strings, numeric strings, regex patterns.\n"
"</do_not_translate>\n"
"\n"
"<safety_check>\n"
"Before translating any string, confirm all three are true:\n"
"- It is displayed directly to the player as visible text\n"
"- It is purely a UI label, not used as a key anywhere else\n"
"- Nothing in the codebase compares this exact string to another value\n"
"When in doubt, skip it — untranslated Japanese is better than a broken game.\n"
"</safety_check>\n"
"\n"
"<output_format>\n"
"Provide the translated file as a complete replacement of js/plugins.js, inside a single "
"fenced code block (```js ... ```) so it can be copied in one click. Only change the "
"string values identified above. Preserve all original formatting, indentation, comments, and structure.\n"
"\n"
"After the code block, output a summary:\n"
"\n"
"### Translations Made\n"
" Plugin: <plugin name>\n"
" Parameter: <parameter key>\n"
" Before: <original Japanese>\n"
" After: <English translation>\n"
"\n"
"### Skipped (Ambiguous or Internal)\n"
"List any Japanese strings you detected but did not translate, with a one-line reason.\n"
"</output_format>\n"
)
_ACE_SCRIPTS_TRANSLATE_PROMPT = (
"You are an expert RPGMaker VX Ace (Ruby) localisation engineer.\n"
"\n"
"<task>\n"
"Translate visible Japanese strings inside the game's Ruby scripts (ace_json/scripts/*.rb) "
"without breaking any game logic or script functionality. A vocab.txt glossary is attached - "
"use it as your primary reference. Any name or term in the glossary must be translated exactly as shown.\n"
"</task>\n"
"\n"
"--- attach the .rb script files and vocab.txt here before continuing ---\n"
"\n"
"\n"
"========================================\n"
"## WHAT TO TRANSLATE\n"
"========================================\n"
"\n"
"Only translate string literals that are directly shown to the player at runtime.\n"
"These typically appear as:\n"
" - Strings passed to msgbox, msgbox_p, print, p\n"
" - Labels and text in Window or Scene classes rendered to screen\n"
" - draw_text / draw_item calls with a Japanese string literal\n"
" - Default UI label text (menu names, button labels, status window text)\n"
" - Battle log messages, notifications, popup strings\n"
" - Help or description text shown in help windows\n"
"\n"
"========================================\n"
"## WHAT MUST NOT BE TRANSLATED\n"
"========================================\n"
"\n"
"CRITICAL - translating the following will break the game:\n"
"\n"
" 1. Strings used as hash keys, method names, or symbol equivalents.\n"
" Example: vocab[\"HP\"] = \"体力\" -> translate \"体力\" but NOT the key \"HP\"\n"
"\n"
" 2. Strings used as internal identifiers compared with == or used in case/when:\n"
" - Actor/class/skill/item names used as lookup strings\n"
" - Script-internal state names or flag strings\n"
" Example: if type == \"スキル\" -> do NOT translate \"スキル\"\n"
"\n"
" 3. File paths, filenames, font names, colour strings, URLs.\n"
"\n"
" 4. Regular expressions, format strings used with sprintf or % operator\n"
" where the placeholders must stay in the same position.\n"
" (You may translate the human-readable parts but keep %s / %d / %1 etc intact.)\n"
"\n"
" 5. Script class names, module names, method names, constants.\n"
"\n"
" 6. Any string that is read back elsewhere in the scripts with an exact match.\n"
"\n"
"<safety_check>\n"
"Before translating any string, confirm all three are true:\n"
"- It is displayed directly to the player as visible text\n"
"- It is purely a display string, not compared or looked up anywhere\n"
"- Changing it would break no conditional logic or data lookup\n"
"When in doubt, skip it - untranslated Japanese is better than a broken game.\n"
"</safety_check>\n"
"\n"
"<output_format>\n"
"For each .rb file that needed changes, provide the full translated file content inside its "
"own fenced code block (```ruby ... ```), preceded by the filename, so each file can be "
"copied in one click. Only change string values identified as safe. "
"Preserve all Ruby syntax, indentation, comments, and structure exactly.\n"
"\n"
"After all files, output a summary:\n"
"\n"
"### Translations Made\n"
" File: <script filename.rb>\n"
" Before: <original Japanese>\n"
" After: <English translation>\n"
"\n"
"### Skipped (Ambiguous or Internal)\n"
"List any Japanese strings you detected but did not translate, with a one-line reason.\n"
"</output_format>\n"
)
_PLUGIN_PROMPT = (
"You are an expert RPGMaker MV/MZ event system analyst.\n"
"\n"
"<task>\n"
"Audit several optional event code types and report: (a) which contain player-visible "
"Japanese text that needs translation, and (b) for code 122, exactly which variable ID "
"ranges carry display text versus internal keys.\n"
"</task>\n"
"\n"
"--- attach Actors.json, CommonEvents.json, Troops.json, Map*.json, and js/plugins.js here ---\n"
"\n"
"<actor_context>\n"
"Read Actors.json in full before auditing event commands. Use it to resolve actor IDs, "
"\\N[n] references, and code 320/324/325 targets. If you see an event reference to a "
"major actor whose full character vocab is missing or only appears as a placeholder "
"mapping like \\N[3] (Keimi), report that Actors.json should be included in the glossary "
"build and that a full # Game Characters entry is needed.\n"
"</actor_context>\n"
"\n"
"<audit_1 code=\"122\">\n"
"Code 122 — Control Variables — which variable IDs carry display text\n"
"\n"
"A code 122 entry looks like:\n"
" { \"code\": 122, \"parameters\": [startVarId, endVarId, 0, 4, \"\\\"some string\\\"\"] }\n"
"parameters[0] is the variable ID being set; parameters[4] is the string value\n"
"(only present when parameters[3] == 4, direct string assignment).\n"
"\n"
"Collect every code 122 where parameters[3] == 4. For each variable ID found:\n"
" 1. Is the string also tested in a code 111 $gameVariables comparison?\n"
" → DO NOT TRANSLATE (would break game logic)\n"
" 2. Is the string used as an internal ID / plugin key / script argument?\n"
" → DO NOT TRANSLATE\n"
" 3. Is the string purely player-visible display text?\n"
" → SAFE TO TRANSLATE\n"
"\n"
"Summarise translatable variable IDs as compact numeric ranges,\n"
"e.g. 'Translate IDs: 5, 10-18, 42'. Separately list any DO NOT TRANSLATE IDs\n"
"with the reason. The ranges will be entered as min/max in the translation tool.\n"
"</audit_1>\n"
"\n"
"<audit_2>\n"
"Plugin codes with visible text — 357 / 356 / 355-655 / 657 / 320 / 324 / 325 / 108\n"
"For each code type below, scan all events and report whether any instance contains "
"Japanese text visible to the player at runtime.\n"
"\n"
"--- Code 357 (MZ Plugin Commands) ---\n"
"parameters[0] = plugin header; parameters[3] = data dict.\n"
"Look up each unique header in js/plugins.js.\n"
"Does any key in parameters[3] hold Japanese text shown on screen?\n"
"The module already handles: AdvExtentionllk, VisuMZ_4_ProximityMessages, LL_GalgeChoiceWindow\n"
"For other headers with visible text, check the commented-out headerMappings list:\n"
" \"LL_InfoPopupWIndow\": ([\"messageText\"], None),\n"
" \"QuestSystem\": ([\"DetailNote\"], None),\n"
" \"BalloonInBattle\": ([\"text\"], None),\n"
" \"MNKR_CommonPopupCoreMZ\": ([\"text\"], None),\n"
" \"DestinationWindow\": ([\"destination\"], None),\n"
" \"_TMLogWindowMZ\": ([\"text\"], None),\n"
" \"TorigoyaMZ_NotifyMessage\": ([\"message\"], None),\n"
" \"SoR_GabWindow\": ([\"arg1\"], None),\n"
" \"DarkPlasma_CharacterText\": ([\"text\"], None),\n"
" \"DTextPicture\": ([\"text\"], None),\n"
" \"TextPicture\": ([\"text\"], None),\n"
" \"TRP_SkitMZ\": ([\"name\"], None),\n"
" \"LogWindow\": ([\"text\"], None),\n"
" \"BattleLogOutput\": ([\"message\"], None),\n"
" \"TorigoyaMZ_NotifyMessage_CommandMessage\": ([\"message\"], None),\n"
" \"NUUN_SaveScreen\": ([\"AnyName\"], None),\n"
" \"build/ARPG_Core\": ([\"Text\", \"SkillByName\"], None),\n"
"Output: EXACT uncomment line, or describe parameters[3] if header is new.\n"
"\n"
"--- Code 356 (MV Plugin Commands) ---\n"
"parameters[0] is a space-delimited string, e.g. 'D_TEXT テキスト 24'.\n"
"The module already handles: D_TEXT, ShowInfo, PushGab, addLog, DW_*, CommonPopup, AddCustomChoice.\n"
"Does any 356 line have Japanese that is shown on screen?\n"
"If yes: ENABLE CODE356 and list the command keywords found.\n"
"If no: SKIP CODE356.\n"
"\n"
"--- Code 355/655 (Inline Scripts) ---\n"
"Code 355 starts a script block; code 655 continues it.\n"
"parameters[0] is the raw JS/script text.\n"
"For each block with Japanese in a string passed to a message/popup/log function:\n"
" • The leading keyword/pattern that identifies the block\n"
" • A regex capturing only the display substring, e.g. テキスト-(.+)\n"
" • Whether it is single-line (355 only) or multi-line (355 + 655)\n"
" • The exact entry to add to the patterns dict:\n"
" \"<keyword>\": (r\"<regex>\", <True|False>),\n"
"\n"
"--- Code 657 (Picture Text) ---\n"
"parameters[0] is a plain string drawn onto a picture.\n"
"Does any 657 entry contain Japanese text visible on screen (not a filename)?\n"
"If yes: ENABLE CODE657. If no: SKIP CODE657.\n"
"\n"
"--- Codes 320 / 324 / 325 (Actor Name / Nickname / Profile) ---\n"
"parameters[1] is the new name/nickname/profile string.\n"
"Do any of these codes set Japanese strings visible to the player\n"
"(not just internal IDs or filenames)?\n"
"If yes: ENABLE the respective code. If no: SKIP.\n"
"\n"
"--- Code 108 (Comment / Notetag) ---\n"
"parameters[0] is a comment string used as a plugin notetag.\n"
"The module only translates 108 lines that match specific patterns:\n"
" info:, ActiveMessage:, event_text, Menu Name, text_indicator, NW名前指定\n"
"Do any 108 entries matching those patterns have Japanese visible to the player?\n"
"If yes: ENABLE CODE108 and list the patterns found. If no: SKIP CODE108.\n"
"</audit_2>\n"
"\n"
"<output_format>\n"
"Reply with these exact sections:\n"
"\n"
"### Code 122 — Variable Translation Ranges\n"
" Translate IDs : <compact ranges, e.g. 5, 10-18, 42>\n"
" Do NOT translate: <ID — reason>\n"
"If none: 'No display-text string assignments found.'\n"
"\n"
"### Code 357 — Plugin Handlers to Enable\n"
" • Already handled : <header> — no action needed\n"
" • Enable in module : uncomment → \"<header>\": ([\"<key>\"], None),\n"
" • New entry needed : <header> — parameters[3] structure: <description>\n"
" • No visible text : <header> — internal only, skip\n"
"If none: 'No code 357 visible text found.'\n"
"\n"
"### Code 356 — Enable or Skip\n"
" ENABLE / SKIP — keywords found: <list>\n"
"\n"
"### Code 355/655 — Script Patterns to Add\n"
" • Pattern key : <keyword>\n"
" • Regex : <capture regex>\n"
" • Multiline : <true/false>\n"
" • Module line : \"<keyword>\": (r\"<regex>\", <True|False>),\n"
"If none: 'No translatable 355/655 scripts found.'\n"
"\n"
"### Code 657 — Enable or Skip\n"
" ENABLE / SKIP — <brief reason>\n"
"\n"
"### Codes 320 / 324 / 325 — Enable or Skip\n"
" 320: ENABLE / SKIP 324: ENABLE / SKIP 325: ENABLE / SKIP\n"
"\n"
"### Code 108 — Enable or Skip\n"
" ENABLE / SKIP — patterns found: <list>\n"
"\n"
"### GUI Action Summary\n"
"\n"
"After all audit sections above, output this final block to configure the GUI:\n"
"\n"
"ENABLE CODES : <comma-separated CODE* keys to check, e.g. CODE357, CODE356>\n"
"SKIP CODES : <codes that have no visible text>\n"
"357 PLUGINS : <comma-separated HEADER_MAPPINGS_357 keys to enable>\n"
"355/655 PATTERNS : <comma-separated PATTERNS_355655 keys to enable>\n"
"122 VAR RANGE : min=<N>, max=<M>\n"
"\n"
"If a field has nothing to fill in, write NONE.\n"
"</output_format>\n"
)
# Static clipboard prompts are loaded from editable data/skills/*.md files.
# ── Step 1 (Optional): Pre-process ────────────────────────────────
@ -1622,6 +1295,7 @@ class WorkflowTab(QWidget):
tc_desc = QLabel(
"Copies everything from the <code>gameupdate/</code> folder "
"into the game\'s root folder, overwriting existing files. "
"WOLF-only UberWolf files are omitted for RPG Maker games. "
"Writes <code>patch-config.txt</code> from Config → Game Update defaults "
"(set your org once there; edit <code>repo=</code> per game)."
)
@ -2411,8 +2085,8 @@ class WorkflowTab(QWidget):
self._step6_copy_btn = _make_btn("📋 Copy Prompt for Copilot", "#555")
self._step6_copy_btn.setToolTip(
"Copy a prompt that instructs Copilot/Cursor to translate only "
"visible player-facing strings in plugins.js, using vocab.txt as a glossary."
"Copy a prompt that audits plugins.js and enabled plugin sources, asks what "
"needs translation, then edits approved player-visible strings in place."
)
self._step6_copy_btn.clicked.connect(self._copy_plugins_js_translate_prompt)
@ -3129,13 +2803,13 @@ class WorkflowTab(QWidget):
if is_ace:
btn.setToolTip(
"Copy a prompt that instructs Copilot/Cursor to translate only "
"visible player-facing strings in the Ace .rb script files "
"(ace_json/scripts/*.rb)."
"visible player-facing strings in the Ace .rb script files. "
"It audits first, asks what to translate, then edits approved files in place."
)
else:
btn.setToolTip(
"Copy a prompt that instructs Copilot/Cursor to translate only "
"visible player-facing strings in plugins.js, using vocab.txt as a glossary."
"Copy a prompt that audits plugins.js and enabled plugin sources, asks what "
"needs translation, then edits approved player-visible strings in place."
)
# Step 6 - TL Inspector (MV/MZ only; hidden for Ace)
show_playtest = not is_ace
@ -3567,8 +3241,10 @@ class WorkflowTab(QWidget):
self._copy_project_setup_prompt()
def _copy_wrap_prompt(self):
QApplication.clipboard().setText(self._WRAP_PROMPT)
self._log("Text-wrap analysis prompt copied to clipboard.")
self._copy_clipboard_skill(
"wrap_config.md",
"Text-wrap analysis prompt copied to clipboard.",
)
def _apply_var_range(self):
"""Write CODE122_VAR_MIN / CODE122_VAR_MAX to the module file."""
@ -3736,15 +3412,29 @@ class WorkflowTab(QWidget):
getattr(self, "_ace_rvdata_dir", "") or getattr(self, "_ace_json_dir", "")
)
if is_ace:
QApplication.clipboard().setText(self._ACE_SCRIPTS_TRANSLATE_PROMPT)
self._log("Ace scripts translation prompt copied to clipboard.")
self._copy_clipboard_skill(
"ace_script_translation.md",
"Ace scripts translation prompt copied to clipboard.",
)
else:
QApplication.clipboard().setText(self._PLUGINS_JS_TRANSLATE_PROMPT)
self._log("plugins.js translation prompt copied to clipboard.")
self._copy_clipboard_skill(
"plugin_translation.md",
"plugins.js translation prompt copied to clipboard.",
)
def _copy_plugin_prompt(self):
QApplication.clipboard().setText(self._PLUGIN_PROMPT)
self._log("Risky codes analysis prompt copied to clipboard.")
self._copy_clipboard_skill(
"risky_codes.md",
"Risky codes analysis prompt copied to clipboard.",
)
def _copy_clipboard_skill(self, filename: str, success_message: str):
try:
prompt = load_clipboard_skill(filename)
QApplication.clipboard().setText(prompt)
self._log(success_message)
except Exception as exc:
self._log(f"❌ Could not copy {filename}: {exc}")
def _apply_wrap_config(self):
"""Write width / listWidth / noteWidth back into .env."""
@ -4414,7 +4104,7 @@ class WorkflowTab(QWidget):
if not Path(src).is_dir():
self._log(f"⚠ gameupdate folder not found: {src}")
return
w = _FileCopyWorker(src, dst, skip_names=_GAMEUPDATE_COPY_SKIP_NAMES)
w = _FileCopyWorker(src, dst, skip_names=_RPG_GAMEUPDATE_COPY_SKIP_NAMES)
w.log.connect(self._log)
w.done.connect(self._on_gameupdate_done)
self._worker = w
@ -4454,7 +4144,7 @@ class WorkflowTab(QWidget):
_FileCopyWorker(
gameupdate_src,
game_root_dst,
skip_names=_GAMEUPDATE_COPY_SKIP_NAMES,
skip_names=_RPG_GAMEUPDATE_COPY_SKIP_NAMES,
),
))
else:

View file

@ -163,14 +163,14 @@ CODE122_VAR_MAX = 2000
# Plugins / Scripts
CODE355655 = False
CODE357 = False
CODE357 = True
CODE657 = False
CODE356 = False
CODE320 = False
CODE324 = False
CODE325 = False
CODE111 = True
CODE108 = False
CODE111 = False
CODE108 = True
# ─── Plugin Manager ──────────────────────────────────────────────────────────
# All known code-357 headerMapping entries. Enable entries via ENABLED_PLUGINS_357.
@ -201,7 +201,7 @@ HEADER_MAPPINGS_357 = {
"SceneGlossary": (["category"], None),
}
# Subset of HEADER_MAPPINGS_357 keys that should be processed (empty = none).
ENABLED_PLUGINS_357: set = set()
ENABLED_PLUGINS_357: set = {"DTextPicture"}
# All known code-355/655 script patterns. Enable entries via ENABLED_PATTERNS_355655.
PATTERNS_355655 = {
@ -236,7 +236,7 @@ PATTERNS_355655 = {
"AddAddress": (r'AddAddress\(\d+,\s*\\?"(.+?)\\?"', False),
}
# Subset of PATTERNS_355655 keys that should be processed (empty = none).
ENABLED_PATTERNS_355655: set = {"BattleManager._logWindow.addText"}
ENABLED_PATTERNS_355655: set = set()
def _pat355655_captured_text(match):

View file

@ -23,6 +23,11 @@ _SHIPPED_DATA_FILES = (
"data/translation_contexts.json",
"data/skills/system.md",
"data/skills/project_setup.md",
"data/skills/wrap_config.md",
"data/skills/plugin_translation.md",
"data/skills/ace_script_translation.md",
"data/skills/risky_codes.md",
"data/skills/wolf_speakers.md",
"data/help/index.json",
"data/help/00-welcome.md",
"data/vocab_base.txt",

View file

@ -0,0 +1,76 @@
"""Regression tests for keeping UberWolf tools exclusive to WOLF games."""
from __future__ import annotations
import subprocess
import tempfile
import unittest
from pathlib import Path
from gui.workflow_tab import (
_FileCopyWorker,
_GAMEUPDATE_COPY_SKIP_NAMES,
_RPG_GAMEUPDATE_COPY_SKIP_NAMES,
_WOLF_ONLY_GAMEUPDATE_NAMES,
)
ROOT = Path(__file__).resolve().parents[1]
class GameUpdateGuiCopyTests(unittest.TestCase):
def _copy_with(self, skip_names: frozenset[str]) -> tuple[Path, tempfile.TemporaryDirectory]:
tmp = tempfile.TemporaryDirectory()
base = Path(tmp.name)
src = base / "source"
dst = base / "game"
src.mkdir()
(src / "GameUpdate.bat").write_text("launcher", encoding="utf-8")
(src / "UberWolfCli.exe").write_bytes(b"wolf-cli")
(src / "UberWolfCli.LICENSE.txt").write_text("license", encoding="utf-8")
result = []
worker = _FileCopyWorker(str(src), str(dst), skip_names=skip_names)
worker.done.connect(lambda count, errors: result.append((count, errors)))
worker.run()
self.assertEqual(result, [(1 if _WOLF_ONLY_GAMEUPDATE_NAMES <= skip_names else 3, [])])
return dst, tmp
def test_rpg_copy_omits_uberwolf_files(self):
dst, tmp = self._copy_with(_RPG_GAMEUPDATE_COPY_SKIP_NAMES)
try:
self.assertTrue((dst / "GameUpdate.bat").is_file())
for name in _WOLF_ONLY_GAMEUPDATE_NAMES:
self.assertFalse((dst / name).exists())
finally:
tmp.cleanup()
def test_wolf_copy_keeps_uberwolf_files(self):
dst, tmp = self._copy_with(_GAMEUPDATE_COPY_SKIP_NAMES)
try:
for name in _WOLF_ONLY_GAMEUPDATE_NAMES:
self.assertTrue((dst / name).is_file())
finally:
tmp.cleanup()
class GameUpdateSelfUpdateTests(unittest.TestCase):
def test_powershell_patch_gates_uberwolf_on_wolf_detection(self):
text = (ROOT / "gameupdate/gameupdate/patch.ps1").read_text(encoding="utf-8")
self.assertIn("function Test-WolfGameRoot", text)
self.assertIn("$isWolfGame = Test-WolfGameRoot -Root $Root", text)
self.assertIn("(-not $isWolfGame) -and ($file.Name -in $wolfOnlyNames)", text)
def test_shell_patch_gates_uberwolf_and_is_valid_bash(self):
path = ROOT / "gameupdate/gameupdate/patch.sh"
text = path.read_text(encoding="utf-8")
self.assertIn("is_wolf_game()", text)
self.assertIn('[ "$wolf_patch" -ne 1 ]', text)
subprocess.run(["bash", "-n", str(path)], check=True)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,67 @@
"""Regression tests for concise, interactive plugin translation prompts."""
import unittest
from pathlib import Path
from util.skills import load_clipboard_skill
ROOT = Path(__file__).resolve().parents[1]
class WorkflowTranslationPromptTests(unittest.TestCase):
def _assert_interactive_in_place_prompt(self, prompt: str):
lowered = prompt.casefold()
self.assertIn("do not edit anything yet", lowered)
self.assertIn("ask one focused question", lowered)
self.assertIn("offer to translate all safe items yourself", lowered)
self.assertIn("edit approved", lowered)
self.assertIn("never paste or repost an entire", lowered)
self.assertIn("minimal unified diff", lowered)
self.assertNotIn("complete replacement", lowered)
self.assertNotIn("full translated file", lowered)
def test_mvmz_prompt_audits_enabled_plugins_before_editing(self):
prompt = load_clipboard_skill("plugin_translation.md")
self._assert_interactive_in_place_prompt(prompt)
self.assertIn("js/plugins.js", prompt)
self.assertIn("js/plugins/<PluginName>.js", prompt)
self.assertIn("which listed plugins should you translate", prompt)
def test_ace_prompt_audits_scripts_before_editing(self):
prompt = load_clipboard_skill("ace_script_translation.md")
self._assert_interactive_in_place_prompt(prompt)
self.assertIn("ace_json/scripts/*.rb", prompt)
self.assertIn("which listed scripts should you translate", prompt)
self.assertIn("Ruby interpolation (#{...})", prompt)
def test_static_clipboard_prompts_live_under_data_skills(self):
expected = (
"wrap_config.md",
"plugin_translation.md",
"ace_script_translation.md",
"risky_codes.md",
"wolf_speakers.md",
)
for name in expected:
with self.subTest(name=name):
self.assertTrue(load_clipboard_skill(name).strip())
workflow_source = (ROOT / "gui/workflow_tab.py").read_text(encoding="utf-8")
wolf_source = (ROOT / "gui/wolf_workflow_tab.py").read_text(encoding="utf-8")
for constant in (
"_WRAP_PROMPT",
"_PLUGINS_JS_TRANSLATE_PROMPT",
"_ACE_SCRIPTS_TRANSLATE_PROMPT",
"_PLUGIN_PROMPT",
):
self.assertNotIn(constant, workflow_source)
self.assertNotIn("_WOLF_SPEAKER_PROMPT", wolf_source)
def test_clipboard_skill_loader_rejects_paths(self):
with self.assertRaises(ValueError):
load_clipboard_skill("../system.md")
if __name__ == "__main__":
unittest.main()

View file

@ -3,7 +3,7 @@
from __future__ import annotations
from util.skills.contexts import ctx, reload_contexts
from util.skills.setup import load_project_setup, skills_dir
from util.skills.setup import load_clipboard_skill, load_project_setup, skills_dir
from util.skills.system import (
custom_skill_path_for_game,
game_skill_path_for_game,
@ -19,6 +19,7 @@ __all__ = [
"custom_skill_path_for_game",
"game_skill_path_for_game",
"list_custom_skill_paths",
"load_clipboard_skill",
"load_project_setup",
"load_system_prompt",
"migrate_game_skill_text",

View file

@ -58,5 +58,18 @@ def load_project_setup(engine: str = "rpgmaker", *, prepend: str = "") -> str:
return body
def load_clipboard_skill(name: str) -> str:
"""Load a static clipboard prompt from ``data/skills``.
Read on every copy so edits made in the Skills tab take effect immediately.
Only a plain Markdown filename is accepted to keep callers inside the shipped
skills directory.
"""
path_name = Path(name)
if path_name.name != name or path_name.suffix.casefold() != ".md":
raise ValueError(f"Invalid clipboard skill filename: {name!r}")
return _read_skill_file(name).strip() + "\n"
def skills_dir() -> Path:
return SKILLS_DIR