Make preserve original optional

This commit is contained in:
DazedAnon 2026-06-15 17:08:45 -05:00
parent 032efac365
commit 9349374ab9
7 changed files with 1049 additions and 979 deletions

View file

@ -1,283 +1,283 @@
""" """
Configuration Integration Helper Configuration Integration Helper
Updates the actual module files with GUI configuration settings Updates the actual module files with GUI configuration settings
""" """
import re import re
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any
class ConfigIntegration: class ConfigIntegration:
"""Helper class to integrate GUI configuration with module files.""" """Helper class to integrate GUI configuration with module files."""
def __init__(self): def __init__(self):
self.modules_dir = Path("modules") self.modules_dir = Path("modules")
def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool: def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool:
"""Update rpgmakermvmz.py with configuration from GUI.""" """Update rpgmakermvmz.py with configuration from GUI."""
module_path = self.modules_dir / "rpgmakermvmz.py" module_path = self.modules_dir / "rpgmakermvmz.py"
if not module_path.exists(): if not module_path.exists():
raise FileNotFoundError(f"Module file not found: {module_path}") raise FileNotFoundError(f"Module file not found: {module_path}")
try: try:
# Read the current module file # Read the current module file
with open(module_path, 'r', encoding='utf-8') as f: with open(module_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Update configuration values # Update configuration values
updated_content = self._update_config_values(content, config) updated_content = self._update_config_values(content, config)
# Write back to file # Write back to file
with open(module_path, 'w', encoding='utf-8') as f: with open(module_path, 'w', encoding='utf-8') as f:
f.write(updated_content) f.write(updated_content)
return True return True
except Exception as e: except Exception as e:
raise e raise e
def _update_config_values(self, content: str, config: Dict[str, Any]) -> str: def _update_config_values(self, content: str, config: Dict[str, Any]) -> str:
"""Update configuration values in the module content.""" """Update configuration values in the module content."""
lines = content.split('\n') lines = content.split('\n')
updated_lines = [] updated_lines = []
# Track which configurations we found and updated # Track which configurations we found and updated
found_configs = set() found_configs = set()
for line in lines: for line in lines:
updated_line = line updated_line = line
# Look for configuration assignments # Look for configuration assignments
for config_key, config_value in config.items(): for config_key, config_value in config.items():
# Match lines like: CONFIG_NAME = value # comment # Match lines like: CONFIG_NAME = value # comment
pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$' pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$'
match = re.match(pattern, line.strip()) match = re.match(pattern, line.strip())
if match: if match:
# Preserve the comment if it exists # Preserve the comment if it exists
comment = match.group(2) if match.group(2) else "" comment = match.group(2) if match.group(2) else ""
if comment: if comment:
comment = " " + comment comment = " " + comment
# Create the new line with proper formatting # Create the new line with proper formatting
updated_line = f"{config_key} = {config_value}{comment}" updated_line = f"{config_key} = {config_value}{comment}"
found_configs.add(config_key) found_configs.add(config_key)
break break
updated_lines.append(updated_line) updated_lines.append(updated_line)
# Check if all configurations were found # Check if all configurations were found
missing_configs = set(config.keys()) - found_configs missing_configs = set(config.keys()) - found_configs
if missing_configs: if missing_configs:
print(f"Warning: Could not find these configurations in module: {missing_configs}") print(f"Warning: Could not find these configurations in module: {missing_configs}")
return '\n'.join(updated_lines) return '\n'.join(updated_lines)
def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool: def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool:
"""Update .env file with configuration.""" """Update .env file with configuration."""
if env_path is None: if env_path is None:
env_path = Path(".env") env_path = Path(".env")
try: try:
from dotenv import set_key from dotenv import set_key
for key, value in config.items(): for key, value in config.items():
set_key(env_path, key, str(value)) set_key(env_path, key, str(value))
return True return True
except Exception as e: except Exception as e:
print(f"Error updating .env file: {e}") print(f"Error updating .env file: {e}")
return False return False
def read_current_config(self, module_path: Path = None) -> Dict[str, Any]: def read_current_config(self, module_path: Path = None) -> Dict[str, Any]:
"""Read current configuration from module file.""" """Read current configuration from module file."""
if module_path is None: if module_path is None:
module_path = self.modules_dir / "rpgmakermvmz.py" module_path = self.modules_dir / "rpgmakermvmz.py"
if not module_path.exists(): if not module_path.exists():
return {} return {}
config = {} config = {}
try: try:
with open(module_path, 'r', encoding='utf-8') as f: with open(module_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Find configuration lines used by the RPG Maker config UI. # Find configuration lines used by the RPG Maker config UI.
bool_pattern = ( bool_pattern = (
r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|' r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|'
r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|TLSYSTEMVARIABLES|' r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|PRESERVEORIGINAL|TLSYSTEMVARIABLES|'
r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)' r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)'
) )
int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)' int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)'
for line in content.split('\n'): for line in content.split('\n'):
line = line.strip() line = line.strip()
match = re.match(bool_pattern, line) match = re.match(bool_pattern, line)
if match: if match:
key = match.group(1) key = match.group(1)
value = match.group(2) == 'True' value = match.group(2) == 'True'
config[key] = value config[key] = value
continue continue
match = re.match(int_pattern, line) match = re.match(int_pattern, line)
if match: if match:
key = match.group(1) key = match.group(1)
config[key] = int(match.group(2)) config[key] = int(match.group(2))
except Exception as e: except Exception as e:
print(f"Error reading configuration from {module_path}: {e}") print(f"Error reading configuration from {module_path}: {e}")
return config return config
def read_plugin_config(self, module_path: Path = None) -> Dict[str, Any]: def read_plugin_config(self, module_path: Path = None) -> Dict[str, Any]:
"""Read ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals from module file.""" """Read ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals from module file."""
if module_path is None: if module_path is None:
module_path = self.modules_dir / "rpgmakermvmz.py" module_path = self.modules_dir / "rpgmakermvmz.py"
result: Dict[str, Any] = { result: Dict[str, Any] = {
"ENABLED_PLUGINS_357": set(), "ENABLED_PLUGINS_357": set(),
"ENABLED_PATTERNS_355655": set(), "ENABLED_PATTERNS_355655": set(),
} }
if not module_path.exists(): if not module_path.exists():
return result return result
try: try:
with open(module_path, "r", encoding="utf-8") as f: with open(module_path, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"): for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"):
# Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"} # Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"}
m = re.search( m = re.search(
rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))', rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))',
content, content,
re.MULTILINE, re.MULTILINE,
) )
if m: if m:
bracket = m.group(1) bracket = m.group(1)
items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket) items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket)
result[var_name] = {a or b for a, b in items} result[var_name] = {a or b for a, b in items}
except Exception as e: except Exception as e:
print(f"Error reading plugin config: {e}") print(f"Error reading plugin config: {e}")
return result return result
def update_plugin_config( def update_plugin_config(
self, self,
enabled_357: set, enabled_357: set,
enabled_355655: set, enabled_355655: set,
module_path: Path = None, module_path: Path = None,
) -> bool: ) -> bool:
"""Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file.""" """Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file."""
if module_path is None: if module_path is None:
module_path = self.modules_dir / "rpgmakermvmz.py" module_path = self.modules_dir / "rpgmakermvmz.py"
if not module_path.exists(): if not module_path.exists():
raise FileNotFoundError(f"Module file not found: {module_path}") raise FileNotFoundError(f"Module file not found: {module_path}")
def _fmt(s: set) -> str: def _fmt(s: set) -> str:
if not s: if not s:
return "set()" return "set()"
items = ", ".join(f'"{k}"' for k in sorted(s)) items = ", ".join(f'"{k}"' for k in sorted(s))
return "{" + items + "}" return "{" + items + "}"
try: try:
with open(module_path, "r", encoding="utf-8") as f: with open(module_path, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
for var_name, val_set in ( for var_name, val_set in (
("ENABLED_PLUGINS_357", enabled_357), ("ENABLED_PLUGINS_357", enabled_357),
("ENABLED_PATTERNS_355655", enabled_355655), ("ENABLED_PATTERNS_355655", enabled_355655),
): ):
content = re.sub( content = re.sub(
rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))', rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))',
f'{var_name}: set = {_fmt(val_set)}', f'{var_name}: set = {_fmt(val_set)}',
content, content,
flags=re.MULTILINE | re.DOTALL, flags=re.MULTILINE | re.DOTALL,
) )
with open(module_path, "w", encoding="utf-8") as f: with open(module_path, "w", encoding="utf-8") as f:
f.write(content) f.write(content)
return True return True
except Exception as e: except Exception as e:
raise e raise e
def validate_module_syntax(self, module_path: Path = None) -> bool: def validate_module_syntax(self, module_path: Path = None) -> bool:
"""Validate that the module file has correct Python syntax.""" """Validate that the module file has correct Python syntax."""
if module_path is None: if module_path is None:
module_path = self.modules_dir / "rpgmakermvmz.py" module_path = self.modules_dir / "rpgmakermvmz.py"
try: try:
with open(module_path, 'r', encoding='utf-8') as f: with open(module_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Try to compile the code # Try to compile the code
compile(content, str(module_path), 'exec') compile(content, str(module_path), 'exec')
return True return True
except SyntaxError as e: except SyntaxError as e:
print(f"Syntax error in {module_path}: {e}") print(f"Syntax error in {module_path}: {e}")
return False return False
except Exception as e: except Exception as e:
print(f"Error validating {module_path}: {e}") print(f"Error validating {module_path}: {e}")
return False return False
def create_config_template(self) -> Dict[str, Any]: def create_config_template(self) -> Dict[str, Any]:
"""Create a template configuration with default values.""" """Create a template configuration with default values."""
return { return {
# General config # General config
"FIRSTLINESPEAKERS": False, "FIRSTLINESPEAKERS": False,
"INLINE401SPEAKERS": False, "INLINE401SPEAKERS": False,
"FACENAME101": False, "FACENAME101": False,
"NAMES": False, "NAMES": False,
"BRFLAG": False, "BRFLAG": False,
"FIXTEXTWRAP": True, "FIXTEXTWRAP": True,
"IGNORETLTEXT": False, "IGNORETLTEXT": False,
# Main Codes # Main Codes
"CODE401": True, "CODE401": True,
"CODE405": True, "CODE405": True,
"CODE102": True, "CODE102": True,
# Optional codes # Optional codes
"CODE101": False, "CODE101": False,
"CODE408": False, "CODE408": False,
# Variable codes # Variable codes
"CODE122": False, "CODE122": False,
# Other codes # Other codes
"CODE355655": False, "CODE355655": False,
"CODE357": False, "CODE357": False,
"CODE657": False, "CODE657": False,
"CODE356": False, "CODE356": False,
"CODE320": False, "CODE320": False,
"CODE324": False, "CODE324": False,
"CODE111": False, "CODE111": False,
"CODE108": False "CODE108": False
} }
def get_config_descriptions(self) -> Dict[str, str]: def get_config_descriptions(self) -> Dict[str, str]:
"""Get descriptions for all configuration options.""" """Get descriptions for all configuration options."""
return { return {
"FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True", "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True",
"INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines", "INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines",
"FACENAME101": "Find Speakers in 101 Codes based on Face Name", "FACENAME101": "Find Speakers in 101 Codes based on Face Name",
"NAMES": "Output a list of all the character names found", "NAMES": "Output a list of all the character names found",
"BRFLAG": "If the game uses <br> instead of newlines", "BRFLAG": "If the game uses <br> instead of newlines",
"FIXTEXTWRAP": "Overwrites textwrap for better formatting", "FIXTEXTWRAP": "Overwrites textwrap for better formatting",
"IGNORETLTEXT": "Ignores all translated text", "IGNORETLTEXT": "Ignores all translated text",
"CODE401": "Show Text - Main dialogue content", "CODE401": "Show Text - Main dialogue content",
"CODE405": "Show Text (Scrolling) - Longer dialogue", "CODE405": "Show Text (Scrolling) - Longer dialogue",
"CODE102": "Show Choices - Player choice options", "CODE102": "Show Choices - Player choice options",
"CODE101": "Character Names - Turn on when names exist in 101", "CODE101": "Character Names - Turn on when names exist in 101",
"CODE408": "Comments - WARNING: Can inflate costs significantly", "CODE408": "Comments - WARNING: Can inflate costs significantly",
"CODE122": "Control Variables - Text stored in variables", "CODE122": "Control Variables - Text stored in variables",
"CODE355655": "Scripts - Text within script commands", "CODE355655": "Scripts - Text within script commands",
"CODE357": "Picture Text - Text displayed on pictures", "CODE357": "Picture Text - Text displayed on pictures",
"CODE657": "Picture Text Extended - Extended picture text", "CODE657": "Picture Text Extended - Extended picture text",
"CODE356": "Plugin Commands - Plugin command parameters", "CODE356": "Plugin Commands - Plugin command parameters",
"CODE320": "Change Name Input - Name input prompts", "CODE320": "Change Name Input - Name input prompts",
"CODE324": "Change Nickname - Nickname changes", "CODE324": "Change Nickname - Nickname changes",
"CODE111": "Conditional Branch - Conditional text", "CODE111": "Conditional Branch - Conditional text",
"CODE108": "Comments - Comment blocks" "CODE108": "Comments - Comment blocks"
} }

View file

@ -130,6 +130,8 @@ BRFLAG = False
FIXTEXTWRAP = True FIXTEXTWRAP = True
# IGNORETLTEXT: Skip Translated Text. # IGNORETLTEXT: Skip Translated Text.
IGNORETLTEXT = False IGNORETLTEXT = False
# PRESERVEORIGINAL: Store Japanese source in _original fields (maps + database) for re-run safety.
PRESERVEORIGINAL = False
# TLSYSTEMVARIABLES: Translate System Variables. (Optional but sometimes necessary. Can break stuff.) # TLSYSTEMVARIABLES: Translate System Variables. (Optional but sometimes necessary. Can break stuff.)
TLSYSTEMVARIABLES = False TLSYSTEMVARIABLES = False
# TLSYSTEMSWITCHES: Translate System Switches. (Optional. Translates switch names in System.json.) # TLSYSTEMSWITCHES: Translate System Switches. (Optional. Translates switch names in System.json.)
@ -487,6 +489,8 @@ def _group_raw_source(codeList, group_start: int, source_parts: list[str]) -> st
def _apply_original(cmd, raw_source: str) -> None: def _apply_original(cmd, raw_source: str) -> None:
"""Set scalar _original only when not already present (re-run safe).""" """Set scalar _original only when not already present (re-run safe)."""
if not PRESERVEORIGINAL:
return
if not raw_source or not str(raw_source).strip(): if not raw_source or not str(raw_source).strip():
return return
if _scalar_original(cmd) is not None: if _scalar_original(cmd) is not None:
@ -510,6 +514,8 @@ def _choice_source(cmd, index: int) -> str:
def _apply_choice_original(cmd, index: int, raw_source: str) -> None: def _apply_choice_original(cmd, index: int, raw_source: str) -> None:
"""Set _original[index] for code 102 only when that slot is empty.""" """Set _original[index] for code 102 only when that slot is empty."""
if not PRESERVEORIGINAL:
return
if not raw_source or not str(raw_source).strip(): if not raw_source or not str(raw_source).strip():
return return
params = cmd.get("parameters") or [[]] params = cmd.get("parameters") or [[]]
@ -581,6 +587,8 @@ def _entry_field_source(entry, field: str) -> str:
def _apply_entry_field_original(entry, field: str, raw: str) -> None: def _apply_entry_field_original(entry, field: str, raw: str) -> None:
"""Set _original[field] only when empty and raw contains Japanese.""" """Set _original[field] only when empty and raw contains Japanese."""
if not PRESERVEORIGINAL:
return
if not isinstance(entry, dict) or not raw or not str(raw).strip(): if not isinstance(entry, dict) or not raw or not str(raw).strip():
return return
if not re.search(LANGREGEX, raw): if not re.search(LANGREGEX, raw):
@ -622,6 +630,8 @@ def _system_scalar_source(data, field: str) -> str:
def _apply_system_scalar_original(data, field: str, raw: str) -> None: def _apply_system_scalar_original(data, field: str, raw: str) -> None:
"""Set root _original[field] only when empty and raw contains Japanese.""" """Set root _original[field] only when empty and raw contains Japanese."""
if not PRESERVEORIGINAL:
return
if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw):
return return
orig = _system_orig(data) orig = _system_orig(data)
@ -650,6 +660,8 @@ def _system_list_source(data, list_name: str, index: int) -> str:
def _apply_system_list_original(data, list_name: str, index: int, raw: str) -> None: def _apply_system_list_original(data, list_name: str, index: int, raw: str) -> None:
"""Set _original[list_name][str(index)] only when empty and raw contains Japanese.""" """Set _original[list_name][str(index)] only when empty and raw contains Japanese."""
if not PRESERVEORIGINAL:
return
if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw):
return return
orig = _system_orig(data) orig = _system_orig(data)
@ -685,6 +697,8 @@ def _system_terms_source(data, category: str, index: int) -> str:
def _apply_system_terms_original(data, category: str, index: int, raw: str) -> None: def _apply_system_terms_original(data, category: str, index: int, raw: str) -> None:
"""Set _original.terms[category][str(index)] only when empty and raw contains Japanese.""" """Set _original.terms[category][str(index)] only when empty and raw contains Japanese."""
if not PRESERVEORIGINAL:
return
if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw):
return return
orig = _system_orig(data) orig = _system_orig(data)
@ -725,6 +739,8 @@ def _system_terms_message_source(data, key: str) -> str:
def _apply_system_terms_message_original(data, key: str, raw: str) -> None: def _apply_system_terms_message_original(data, key: str, raw: str) -> None:
"""Set _original.terms.messages[key] only when empty and raw contains Japanese.""" """Set _original.terms.messages[key] only when empty and raw contains Japanese."""
if not PRESERVEORIGINAL:
return
if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw): if not raw or not str(raw).strip() or not re.search(LANGREGEX, raw):
return return
orig = _system_orig(data) orig = _system_orig(data)

View file

@ -1,58 +1,59 @@
import re import re
from copy import deepcopy from copy import deepcopy
# Canonical defaults used by the GUI and the `set_defaults` script. # Canonical defaults used by the GUI and the `set_defaults` script.
# Stored as booleans for easier consumption by the GUI. # Stored as booleans for easier consumption by the GUI.
DEFAULTS = { DEFAULTS = {
'FIRSTLINESPEAKERS': False, 'FIRSTLINESPEAKERS': False,
'FACENAME101': False, 'FACENAME101': False,
'BRFLAG': False, 'BRFLAG': False,
'FIXTEXTWRAP': True, 'FIXTEXTWRAP': True,
'IGNORETLTEXT': False, 'IGNORETLTEXT': False,
'JOIN408': False, 'PRESERVEORIGINAL': True,
# Speakers / Dialogue / Scroll / Choices (Main Codes) 'JOIN408': False,
'CODE101': True, # Speakers / Dialogue / Scroll / Choices (Main Codes)
'CODE401': True, 'CODE101': True,
'CODE405': True, 'CODE401': True,
'CODE102': True, 'CODE405': True,
# Optional 'CODE102': True,
'CODE408': False, # Optional
# Variables 'CODE408': False,
'CODE122': False, # Variables
# Other 'CODE122': False,
'CODE355655': False, # Other
'CODE357': False, 'CODE355655': False,
'CODE657': False, 'CODE357': False,
'CODE356': False, 'CODE657': False,
'CODE320': False, 'CODE356': False,
'CODE324': False, 'CODE320': False,
'CODE111': False, 'CODE324': False,
'CODE108': False 'CODE111': False,
} 'CODE108': False
}
def get_defaults():
"""Return a shallow copy of the DEFAULTS dictionary.""" def get_defaults():
return deepcopy(DEFAULTS) """Return a shallow copy of the DEFAULTS dictionary."""
return deepcopy(DEFAULTS)
def set_defaults(file_path):
"""Write canonical default values into a module file. def set_defaults(file_path):
"""Write canonical default values into a module file.
The module file contains assignments like `CODE401 = True`. This
function replaces those assignment lines with the canonical defaults. The module file contains assignments like `CODE401 = True`. This
""" function replaces those assignment lines with the canonical defaults.
with open(file_path, 'r', encoding='utf-8') as file: """
content = file.read() with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Update the content with the default values (converted to Python literals)
for key, value in DEFAULTS.items(): # Update the content with the default values (converted to Python literals)
value_str = 'True' if value else 'False' for key, value in DEFAULTS.items():
content = re.sub(rf'{re.escape(key)}\s*=\s*.*', f'{key} = {value_str}', content) value_str = 'True' if value else 'False'
content = re.sub(rf'{re.escape(key)}\s*=\s*.*', f'{key} = {value_str}', content)
with open(file_path, 'w', encoding='utf-8') as file:
file.write(content) with open(file_path, 'w', encoding='utf-8') as file:
file.write(content)
if __name__ == "__main__":
set_defaults('modules/rpgmakermvmz.py') if __name__ == "__main__":
set_defaults('modules/rpgmakermvmz.py')

View file

@ -1,69 +1,77 @@
# Map `_original` fixture # Map `_original` fixture
## Files ## Files
- **`Map_original_fixture.json`** — Minimal RPG Maker MV map (17×13) with one event exercising every `_original` code path: 401, 405, 102, 101, and 122. - **`Map_original_fixture.json`** — Minimal RPG Maker MV map (17×13) with one event exercising every `_original` code path: 401, 405, 102, 101, and 122.
- **`Map_original_fixture_manifest.json`** — Expected `_original` values after translation (used by tests). - **`Map_original_fixture_manifest.json`** — Expected `_original` values after translation (used by tests).
## Event layout (event id 1, page 0) ## Event layout (event id 1, page 0)
| Order | Code | Purpose | | Order | Code | Purpose |
|------:|------|---------| |------:|------|---------|
| 12 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue | | 12 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue |
| 34 | 401 | Merged multi-line dialogue (two 401s → one batch) | | 34 | 401 | Merged multi-line dialogue (two 401s → one batch) |
| 56 | 101, 405 | Empty face setup + scrolling text line | | 56 | 101, 405 | Empty face setup + scrolling text line |
| 7 | 102 | Three choices (middle choice has `if(...)` prefix) | | 7 | 102 | Three choices (middle choice has `if(...)` prefix) |
| 89 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line | | 89 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line |
| 1011 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue | | 1011 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue |
| 1011 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` | | 1011 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` |
## Run the test ## Run the test
Using the project venv (recommended): Using the project venv (recommended):
```bash ```bash
./tests/run_tests.sh ./tests/run_tests.sh
``` ```
Or explicitly: Or explicitly:
```bash ```bash
./tests/run_tests.sh tests.test_mvmz_source_original -v ./tests/run_tests.sh tests.test_mvmz_source_original -v
./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v ./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v
``` ```
Manual equivalent (must run from project root, with venv activated): Manual equivalent (must run from project root, with venv activated):
```bash ```bash
cd /path/to/DazedMTLTool cd /path/to/DazedMTLTool
source .venv/bin/activate # or: source venv/bin/activate source .venv/bin/activate # or: source venv/bin/activate
python -m unittest tests.test_mvmz_source_original -v python -m unittest tests.test_mvmz_source_original -v
``` ```
Do **not** use `pytest` unless you install it yourself — this project uses the stdlib `unittest` runner. Do **not** use `pytest` unless you install it yourself — this project uses the stdlib `unittest` runner.
If you see `ModuleNotFoundError: No module named 'colorama'` (or similar), activate the venv or use `./tests/run_tests.sh` instead of system `python3`. If you see `ModuleNotFoundError: No module named 'colorama'` (or similar), activate the venv or use `./tests/run_tests.sh` instead of system `python3`.
Tests mock `translateAI` / `getSpeaker` — no API key required. Enable `CODE122` for the duration of the run. Tests mock `translateAI` / `getSpeaker` — no API key required.
## Database `_original` fixtures ### Optional `_original` config (in `modules/rpgmakermvmz.py`)
Mini database JSON files for scalar field preservation (notes deferred to Phase 2): | Flag | Scope | Default |
|------|-------|---------|
| Fixture | Parser | Fields | | `PRESERVEORIGINAL` | Map/event commands and database JSON | `True` |
|---------|--------|--------|
| `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` | Set to `False` to skip writing `_original`. Existing `_original` keys are still read on re-run.
| `Items_original_fixture.json` | `searchNames` | `name`, `description` |
| `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` | ## Database `_original` fixtures
| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` |
| `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` | Mini database JSON files for scalar field preservation (notes deferred to Phase 2):
Expected `_original` shapes are in `db_original_manifest.json`. | Fixture | Parser | Fields |
|---------|--------|--------|
```bash | `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` |
./tests/run_tests.sh tests.test_mvmz_db_original -v | `Items_original_fixture.json` | `searchNames` | `name`, `description` |
``` | `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` |
| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` |
## Manual check | `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` |
Copy the fixture into `files/` and run the tool with batch/consume or live translate, then diff against the manifests `expected_original` fields in `translated/Map_original_fixture.json`. Expected `_original` shapes are in `db_original_manifest.json`.
```bash
./tests/run_tests.sh tests.test_mvmz_db_original -v
```
## Manual check
Copy the fixture into `files/` and run the tool with batch/consume or live translate, then diff against the manifests `expected_original` fields in `translated/Map_original_fixture.json`.

View file

@ -24,7 +24,7 @@ if ! "$PYTHON" -c "import colorama, dotenv, tqdm" >/dev/null 2>&1; then
fi fi
if [[ "$#" -eq 0 ]]; then if [[ "$#" -eq 0 ]]; then
set -- tests.test_mvmz_source_original -v set -- discover -s tests -p 'test_mvmz_*.py' -v
fi fi
exec "$PYTHON" -m unittest "$@" exec "$PYTHON" -m unittest "$@"

View file

@ -1,205 +1,222 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Integration tests for _original source preservation in database parsers.""" """Integration tests for _original source preservation in database parsers."""
from __future__ import annotations from __future__ import annotations
import copy import copy
import json import json
import os import os
import re import re
import sys import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
os.chdir(ROOT) os.chdir(ROOT)
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
import modules.rpgmakermvmz as mvmz # noqa: E402 import modules.rpgmakermvmz as mvmz # noqa: E402
LANGREGEX = mvmz.LANGREGEX LANGREGEX = mvmz.LANGREGEX
FIXTURES = ROOT / "tests" / "fixtures" FIXTURES = ROOT / "tests" / "fixtures"
MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8")) MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8"))
def _mock_translate(text, history, batch=False): def _mock_translate(text, history, batch=False):
def tr(s): def tr(s):
if not isinstance(s, str): if not isinstance(s, str):
return s return s
return "EN_TRANSLATED" return "EN_TRANSLATED"
if isinstance(text, list): if isinstance(text, list):
return [[tr(t) for t in text], [0, 0]] return [[tr(t) for t in text], [0, 0]]
return [tr(text), [0, 0]] return [tr(text), [0, 0]]
def _has_japanese(s: str) -> bool: def _has_japanese(s: str) -> bool:
return bool(re.search(LANGREGEX, s or "")) return bool(re.search(LANGREGEX, s or ""))
def _run_search_names(data, context, filename): def _run_search_names(data, context, filename, *, preserve_original=True):
captured = [] captured = []
def translate(text, history, batch=False): def translate(text, history, batch=False):
captured.append(copy.deepcopy(text)) captured.append(copy.deepcopy(text))
return _mock_translate(text, history, batch) return _mock_translate(text, history, batch)
orig_t = mvmz.translateAI orig_t = mvmz.translateAI
orig_vocab = mvmz.update_vocab_section orig_vocab = mvmz.update_vocab_section
mvmz.translateAI = translate orig_preserve = mvmz.PRESERVEORIGINAL
mvmz.update_vocab_section = lambda *args, **kwargs: None mvmz.translateAI = translate
try: mvmz.update_vocab_section = lambda *args, **kwargs: None
data_copy = copy.deepcopy(data) mvmz.PRESERVEORIGINAL = preserve_original
mvmz.searchNames(data_copy, None, context, filename) try:
return data_copy, captured data_copy = copy.deepcopy(data)
finally: mvmz.searchNames(data_copy, None, context, filename)
mvmz.translateAI = orig_t return data_copy, captured
mvmz.update_vocab_section = orig_vocab finally:
mvmz.translateAI = orig_t
mvmz.update_vocab_section = orig_vocab
def _run_search_ss(state): mvmz.PRESERVEORIGINAL = orig_preserve
captured = []
def translate(text, history, batch=False): def _run_search_ss(state):
captured.append(copy.deepcopy(text)) captured = []
return _mock_translate(text, history, batch)
def translate(text, history, batch=False):
orig_t = mvmz.translateAI captured.append(copy.deepcopy(text))
mvmz.translateAI = translate return _mock_translate(text, history, batch)
try:
state_copy = copy.deepcopy(state) orig_t = mvmz.translateAI
mvmz.searchSS(state_copy, None) orig_preserve = mvmz.PRESERVEORIGINAL
return state_copy, captured mvmz.translateAI = translate
finally: mvmz.PRESERVEORIGINAL = True
mvmz.translateAI = orig_t try:
state_copy = copy.deepcopy(state)
mvmz.searchSS(state_copy, None)
def _run_search_system(data): return state_copy, captured
captured = [] finally:
mvmz.translateAI = orig_t
def translate(text, history, batch=False): mvmz.PRESERVEORIGINAL = orig_preserve
captured.append(copy.deepcopy(text))
return _mock_translate(text, history, batch)
def _run_search_system(data):
orig_t = mvmz.translateAI captured = []
mvmz.translateAI = translate
try: def translate(text, history, batch=False):
data_copy = copy.deepcopy(data) captured.append(copy.deepcopy(text))
mvmz.searchSystem(data_copy, None) return _mock_translate(text, history, batch)
return data_copy, captured
finally: orig_t = mvmz.translateAI
mvmz.translateAI = orig_t orig_preserve = mvmz.PRESERVEORIGINAL
mvmz.translateAI = translate
mvmz.PRESERVEORIGINAL = True
def _assert_batches_japanese(captured): try:
for payload in captured: data_copy = copy.deepcopy(data)
items = payload if isinstance(payload, list) else [payload] mvmz.searchSystem(data_copy, None)
for item in items: return data_copy, captured
if not isinstance(item, str) or item == "EN_TRANSLATED": finally:
continue mvmz.translateAI = orig_t
if item.startswith("Taro"): mvmz.PRESERVEORIGINAL = orig_preserve
item = item[4:]
self_fail = not _has_japanese(item)
if self_fail: def _assert_batches_japanese(captured):
raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}") for payload in captured:
items = payload if isinstance(payload, list) else [payload]
for item in items:
class TestActorsOriginal(unittest.TestCase): if not isinstance(item, str) or item == "EN_TRANSLATED":
def test_first_pass_writes_original(self): continue
data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) if item.startswith("Taro"):
result, _ = _run_search_names(data, "Actors", "Actors.json") item = item[4:]
entry = result[MANIFEST["actors"]["entry_index"]] self_fail = not _has_japanese(item)
expected = MANIFEST["actors"]["expected_original"] if self_fail:
self.assertEqual(entry.get("_original"), expected) raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}")
self.assertNotEqual(entry["name"], expected["name"])
self.assertNotEqual(entry["nickname"], expected["nickname"])
self.assertNotEqual(entry["profile"], expected["profile"]) class TestActorsOriginal(unittest.TestCase):
def test_first_pass_writes_original(self):
def test_rerun_preserves_original(self): data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8")) result, _ = _run_search_names(data, "Actors", "Actors.json")
result1, _ = _run_search_names(data, "Actors", "Actors.json") entry = result[MANIFEST["actors"]["entry_index"]]
orig_snapshot = copy.deepcopy(result1[1]["_original"]) expected = MANIFEST["actors"]["expected_original"]
result2, captured2 = _run_search_names(result1, "Actors", "Actors.json") self.assertEqual(entry.get("_original"), expected)
self.assertEqual(result2[1]["_original"], orig_snapshot) self.assertNotEqual(entry["name"], expected["name"])
_assert_batches_japanese(captured2) self.assertNotEqual(entry["nickname"], expected["nickname"])
self.assertNotEqual(entry["profile"], expected["profile"])
class TestItemsOriginal(unittest.TestCase): def test_rerun_preserves_original(self):
def test_first_pass_writes_original(self): data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) result1, _ = _run_search_names(data, "Actors", "Actors.json")
result, _ = _run_search_names(data, "Items", "Items.json") orig_snapshot = copy.deepcopy(result1[1]["_original"])
entry = result[MANIFEST["items"]["entry_index"]] result2, captured2 = _run_search_names(result1, "Actors", "Actors.json")
expected = MANIFEST["items"]["expected_original"] self.assertEqual(result2[1]["_original"], orig_snapshot)
self.assertEqual(entry.get("_original"), expected) _assert_batches_japanese(captured2)
self.assertNotEqual(entry["name"], expected["name"])
self.assertNotEqual(entry["description"], expected["description"])
class TestItemsOriginal(unittest.TestCase):
def test_rerun_preserves_original(self): def test_first_pass_writes_original(self):
data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8")) data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
result1, _ = _run_search_names(data, "Items", "Items.json") result, _ = _run_search_names(data, "Items", "Items.json")
orig_snapshot = copy.deepcopy(result1[1]["_original"]) entry = result[MANIFEST["items"]["entry_index"]]
result2, captured2 = _run_search_names(result1, "Items", "Items.json") expected = MANIFEST["items"]["expected_original"]
self.assertEqual(result2[1]["_original"], orig_snapshot) self.assertEqual(entry.get("_original"), expected)
_assert_batches_japanese(captured2) self.assertNotEqual(entry["name"], expected["name"])
self.assertNotEqual(entry["description"], expected["description"])
class TestSkillsOriginal(unittest.TestCase): def test_rerun_preserves_original(self):
def test_first_pass_writes_original(self): data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) result1, _ = _run_search_names(data, "Items", "Items.json")
result, _ = _run_search_names(data, "Skills", "Skills.json") orig_snapshot = copy.deepcopy(result1[1]["_original"])
entry = result[MANIFEST["skills"]["entry_index"]] result2, captured2 = _run_search_names(result1, "Items", "Items.json")
expected = MANIFEST["skills"]["expected_original"] self.assertEqual(result2[1]["_original"], orig_snapshot)
self.assertEqual(entry.get("_original"), expected) _assert_batches_japanese(captured2)
for field, jp in expected.items():
self.assertNotEqual(entry[field], jp)
class TestSkillsOriginal(unittest.TestCase):
def test_rerun_preserves_original(self): def test_first_pass_writes_original(self):
data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8")) data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
result1, _ = _run_search_names(data, "Skills", "Skills.json") result, _ = _run_search_names(data, "Skills", "Skills.json")
orig_snapshot = copy.deepcopy(result1[1]["_original"]) entry = result[MANIFEST["skills"]["entry_index"]]
result2, captured2 = _run_search_names(result1, "Skills", "Items.json") expected = MANIFEST["skills"]["expected_original"]
self.assertEqual(result2[1]["_original"], orig_snapshot) self.assertEqual(entry.get("_original"), expected)
_assert_batches_japanese(captured2) for field, jp in expected.items():
self.assertNotEqual(entry[field], jp)
class TestStatesOriginal(unittest.TestCase): def test_rerun_preserves_original(self):
def test_first_pass_writes_original(self): data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) result1, _ = _run_search_names(data, "Skills", "Skills.json")
state = data[MANIFEST["states"]["entry_index"]] orig_snapshot = copy.deepcopy(result1[1]["_original"])
result, _ = _run_search_ss(state) result2, captured2 = _run_search_names(result1, "Skills", "Items.json")
expected = MANIFEST["states"]["expected_original"] self.assertEqual(result2[1]["_original"], orig_snapshot)
self.assertEqual(result.get("_original"), expected) _assert_batches_japanese(captured2)
for field, jp in expected.items():
self.assertNotEqual(result[field], jp)
class TestStatesOriginal(unittest.TestCase):
def test_rerun_preserves_original(self): def test_first_pass_writes_original(self):
data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8")) data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
state = data[MANIFEST["states"]["entry_index"]] state = data[MANIFEST["states"]["entry_index"]]
result1, _ = _run_search_ss(state) result, _ = _run_search_ss(state)
orig_snapshot = copy.deepcopy(result1["_original"]) expected = MANIFEST["states"]["expected_original"]
result2, captured2 = _run_search_ss(result1) self.assertEqual(result.get("_original"), expected)
self.assertEqual(result2["_original"], orig_snapshot) for field, jp in expected.items():
_assert_batches_japanese(captured2) self.assertNotEqual(result[field], jp)
def test_rerun_preserves_original(self):
class TestSystemOriginal(unittest.TestCase): data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
def test_first_pass_writes_original(self): state = data[MANIFEST["states"]["entry_index"]]
data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) result1, _ = _run_search_ss(state)
result, _ = _run_search_system(data) orig_snapshot = copy.deepcopy(result1["_original"])
expected = MANIFEST["system"]["expected_original"] result2, captured2 = _run_search_ss(result1)
self.assertEqual(result.get("_original"), expected) self.assertEqual(result2["_original"], orig_snapshot)
self.assertNotEqual(result["gameTitle"], expected["gameTitle"]) _assert_batches_japanese(captured2)
self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"])
self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"])
class TestSystemOriginal(unittest.TestCase):
def test_rerun_preserves_original(self): def test_first_pass_writes_original(self):
data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8")) data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8"))
result1, _ = _run_search_system(data) result, _ = _run_search_system(data)
orig_snapshot = copy.deepcopy(result1["_original"]) expected = MANIFEST["system"]["expected_original"]
result2, captured2 = _run_search_system(result1) self.assertEqual(result.get("_original"), expected)
self.assertEqual(result2["_original"], orig_snapshot) self.assertNotEqual(result["gameTitle"], expected["gameTitle"])
_assert_batches_japanese(captured2) self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"])
self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"])
if __name__ == "__main__": def test_rerun_preserves_original(self):
unittest.main(verbosity=2) data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8"))
result1, _ = _run_search_system(data)
orig_snapshot = copy.deepcopy(result1["_original"])
result2, captured2 = _run_search_system(result1)
self.assertEqual(result2["_original"], orig_snapshot)
_assert_batches_japanese(captured2)
class TestPreserveOriginalDisabled(unittest.TestCase):
def test_db_preserve_disabled_skips_original(self):
data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
result, _ = _run_search_names(data, "Items", "Items.json", preserve_original=False)
entry = result[MANIFEST["items"]["entry_index"]]
self.assertNotIn("_original", entry)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,363 +1,391 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Integration tests for _original source preservation in rpgmakermvmz searchCodes.""" """Integration tests for _original source preservation in rpgmakermvmz searchCodes."""
from __future__ import annotations from __future__ import annotations
import copy import copy
import json import json
import os import os
import re import re
import sys import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
os.chdir(ROOT) os.chdir(ROOT)
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
import modules.rpgmakermvmz as mvmz # noqa: E402 import modules.rpgmakermvmz as mvmz # noqa: E402
LANGREGEX = mvmz.LANGREGEX LANGREGEX = mvmz.LANGREGEX
def _mock_translate(text, history, batch=False): def _mock_translate(text, history, batch=False):
def tr(s): def tr(s):
if not isinstance(s, str): if not isinstance(s, str):
return s return s
m = re.match(r"^(\[[^\]]+\]:\s?)", s) m = re.match(r"^(\[[^\]]+\]:\s?)", s)
if m: if m:
return m.group(1) + "EN_TRANSLATED" return m.group(1) + "EN_TRANSLATED"
return "EN_TRANSLATED" return "EN_TRANSLATED"
if isinstance(text, list): if isinstance(text, list):
return [[tr(t) for t in text], [0, 0]] return [[tr(t) for t in text], [0, 0]]
return [tr(text), [0, 0]] return [tr(text), [0, 0]]
def _mock_speaker(name): def _mock_speaker(name):
return [f"Speaker_{name}", [0, 0]] return [f"Speaker_{name}", [0, 0]]
FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json" FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json"
FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json" FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json"
CASE_MARKER_RE = re.compile(r"# CASE:(\S+)") CASE_MARKER_RE = re.compile(r"# CASE:(\S+)")
def _load_fixture_page(): def _load_fixture_page():
data = json.loads(FIXTURE_MAP.read_text(encoding="utf-8-sig")) data = json.loads(FIXTURE_MAP.read_text(encoding="utf-8-sig"))
event = next(e for e in data["events"] if e and e.get("id") == 1) event = next(e for e in data["events"] if e and e.get("id") == 1)
return {"list": copy.deepcopy(event["pages"][0]["list"])} return {"list": copy.deepcopy(event["pages"][0]["list"])}
def _case_commands(page_list): def _case_commands(page_list):
"""Map manifest case id -> command immediately following its 108 marker.""" """Map manifest case id -> command immediately following its 108 marker."""
cases = {} cases = {}
pending = None pending = None
for cmd in page_list: for cmd in page_list:
if not cmd: if not cmd:
continue continue
if cmd.get("code") == 108: if cmd.get("code") == 108:
m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0])) m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0]))
pending = m.group(1) if m else None pending = m.group(1) if m else None
continue continue
if pending: if pending:
cases[pending] = cmd cases[pending] = cmd
pending = None pending = None
return cases return cases
def _load_fixture_manifest(): def _load_fixture_manifest():
return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8")) return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8"))
def _load_map_excerpt(): def _load_map_excerpt():
"""Small real snippets from Map002 event 17 + Map001 102 + synthetic 101/122.""" """Small real snippets from Map002 event 17 + Map001 102 + synthetic 101/122."""
map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig"))
ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) ev17 = next(e for e in map2["events"] if e and e.get("id") == 17)
real = copy.deepcopy(ev17["pages"][0]["list"][7:14]) # 101 + 401 dialogue block real = copy.deepcopy(ev17["pages"][0]["list"][7:14]) # 101 + 401 dialogue block
map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig")) map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig"))
choice_cmd = None choice_cmd = None
for ev in map1["events"]: for ev in map1["events"]:
if not ev: if not ev:
continue continue
for pg in ev.get("pages") or []: for pg in ev.get("pages") or []:
if not pg: if not pg:
continue continue
for cmd in pg.get("list") or []: for cmd in pg.get("list") or []:
if cmd and cmd.get("code") == 102: if cmd and cmd.get("code") == 102:
choice_cmd = copy.deepcopy(cmd) choice_cmd = copy.deepcopy(cmd)
break break
if choice_cmd: if choice_cmd:
break break
if choice_cmd: if choice_cmd:
break break
assert choice_cmd is not None, "Map001 should contain a 102 choice command" assert choice_cmd is not None, "Map001 should contain a 102 choice command"
synthetic = [ synthetic = [
{"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]}, {"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]},
{"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]}, {"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]},
{ {
"code": 122, "code": 122,
"indent": 0, "indent": 0,
"parameters": [101, 101, 0, 0, "`変数テスト`"], "parameters": [101, 101, 0, 0, "`変数テスト`"],
}, },
] ]
return {"list": real + [choice_cmd] + synthetic} return {"list": real + [choice_cmd] + synthetic}
def _resolve_case_command(page_list, entry, marked_cases=None): def _resolve_case_command(page_list, entry, marked_cases=None):
"""Find manifest case command by CASE marker or by code + expected _original.""" """Find manifest case command by CASE marker or by code + expected _original."""
marked_cases = marked_cases if marked_cases is not None else _case_commands(page_list) marked_cases = marked_cases if marked_cases is not None else _case_commands(page_list)
cid = entry["id"] cid = entry["id"]
cmd = marked_cases.get(cid) cmd = marked_cases.get(cid)
if cmd is not None and cmd.get("code") == entry.get("code"): if cmd is not None and cmd.get("code") == entry.get("code"):
return cmd return cmd
exp = entry.get("expected_original") exp = entry.get("expected_original")
code = entry.get("code") code = entry.get("code")
if code is not None and exp is not None: if code is not None and exp is not None:
for candidate in page_list: for candidate in page_list:
if candidate and candidate.get("code") == code and candidate.get("_original") == exp: if candidate and candidate.get("code") == code and candidate.get("_original") == exp:
return candidate return candidate
return cmd return cmd
def _run_search_codes(page): def _run_search_codes(page, *, preserve_original=True):
"""Full Pass 1 -> mock translate -> Pass 2 cycle.""" """Full Pass 1 -> mock translate -> Pass 2 cycle."""
captured = [] captured = []
def translate(text, history, batch=False): def translate(text, history, batch=False):
captured.append(copy.deepcopy(text)) captured.append(copy.deepcopy(text))
return _mock_translate(text, history, batch) return _mock_translate(text, history, batch)
def speaker(name): def speaker(name):
return _mock_speaker(name) return _mock_speaker(name)
orig_t = mvmz.translateAI orig_t = mvmz.translateAI
orig_s = mvmz.getSpeaker orig_s = mvmz.getSpeaker
orig_122 = mvmz.CODE122 orig_122 = mvmz.CODE122
orig_408 = mvmz.CODE408 orig_408 = mvmz.CODE408
mvmz.translateAI = translate orig_101 = mvmz.CODE101
mvmz.getSpeaker = speaker orig_401 = mvmz.CODE401
mvmz.CODE122 = True orig_405 = mvmz.CODE405
mvmz.CODE408 = True orig_102 = mvmz.CODE102
try: orig_preserve = mvmz.PRESERVEORIGINAL
page_copy = copy.deepcopy(page) mvmz.translateAI = translate
mvmz.searchCodes(page_copy, None, [], "TestMap.json") mvmz.getSpeaker = speaker
return page_copy, captured mvmz.CODE122 = True
finally: mvmz.CODE408 = True
mvmz.translateAI = orig_t mvmz.CODE101 = True
mvmz.getSpeaker = orig_s mvmz.CODE401 = True
mvmz.CODE122 = orig_122 mvmz.CODE405 = True
mvmz.CODE408 = orig_408 mvmz.CODE102 = True
mvmz.PRESERVEORIGINAL = preserve_original
try:
def _find_commands(page, code): page_copy = copy.deepcopy(page)
return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code] mvmz.searchCodes(page_copy, None, [], "TestMap.json")
return page_copy, captured
finally:
def _has_japanese(s: str) -> bool: mvmz.translateAI = orig_t
return bool(re.search(LANGREGEX, s or "")) mvmz.getSpeaker = orig_s
mvmz.CODE122 = orig_122
mvmz.CODE408 = orig_408
class TestMVMZSourceOriginal(unittest.TestCase): mvmz.CODE101 = orig_101
def test_first_pass_writes_original(self): mvmz.CODE401 = orig_401
page, _ = _run_search_codes(_load_map_excerpt()) mvmz.CODE405 = orig_405
mvmz.CODE102 = orig_102
cmds401 = _find_commands(page, 401) mvmz.PRESERVEORIGINAL = orig_preserve
with_orig = [c for c in cmds401 if c.get("_original")]
self.assertGreater(len(with_orig), 0, "401 dialogue should have _original")
for c in with_orig: def _find_commands(page, code):
self.assertTrue(_has_japanese(c["_original"])) return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code]
self.assertNotEqual(c["parameters"][0], c["_original"])
c102 = _find_commands(page, 102)[0] def _has_japanese(s: str) -> bool:
self.assertIsInstance(c102.get("_original"), list) return bool(re.search(LANGREGEX, s or ""))
self.assertEqual(len(c102["_original"]), len(c102["parameters"][0]))
for i, orig in enumerate(c102["_original"]):
if orig: class TestMVMZSourceOriginal(unittest.TestCase):
self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese") def test_first_pass_writes_original(self):
self.assertNotEqual(c102["parameters"][0][i], orig) page, _ = _run_search_codes(_load_map_excerpt())
c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4) cmds401 = _find_commands(page, 401)
self.assertIn("_original", c101) with_orig = [c for c in cmds401 if c.get("_original")]
self.assertIn("アリス", c101["_original"]) self.assertGreater(len(with_orig), 0, "401 dialogue should have _original")
self.assertIn("\\C[2]", c101["_original"]) for c in with_orig:
self.assertIn("Speaker_", c101["parameters"][4]) self.assertTrue(_has_japanese(c["_original"]))
self.assertNotEqual(c["parameters"][0], c["_original"])
c122 = _find_commands(page, 122)[0]
self.assertEqual(c122["_original"], "変数テスト") c102 = _find_commands(page, 102)[0]
self.assertIn("EN_TRANSLATED", c122["parameters"][4]) self.assertIsInstance(c102.get("_original"), list)
self.assertEqual(len(c102["_original"]), len(c102["parameters"][0]))
def test_rerun_uses_original_not_display_text(self): for i, orig in enumerate(c102["_original"]):
page1, captured1 = _run_search_codes(_load_map_excerpt()) if orig:
originals_snapshot = json.dumps( self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese")
{i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd}, self.assertNotEqual(c102["parameters"][0][i], orig)
ensure_ascii=False,
) c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4)
self.assertIn("_original", c101)
page2, captured2 = _run_search_codes(page1) self.assertIn("アリス", c101["_original"])
originals_after = json.dumps( self.assertIn("\\C[2]", c101["_original"])
{i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd}, self.assertIn("Speaker_", c101["parameters"][4])
ensure_ascii=False,
) c122 = _find_commands(page, 122)[0]
self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run") self.assertEqual(c122["_original"], "変数テスト")
self.assertIn("EN_TRANSLATED", c122["parameters"][4])
# Every batch sent to translateAI on re-run should still contain Japanese
for payload in captured2: def test_rerun_uses_original_not_display_text(self):
items = payload if isinstance(payload, list) else [payload] page1, captured1 = _run_search_codes(_load_map_excerpt())
for item in items: originals_snapshot = json.dumps(
if not isinstance(item, str): {i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd},
continue ensure_ascii=False,
if item == "EN_TRANSLATED": )
continue
if _has_japanese(item): page2, captured2 = _run_search_codes(page1)
continue originals_after = json.dumps(
if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): {i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd},
continue ensure_ascii=False,
self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}") )
self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run")
def test_map002_micro_page_real_data(self):
"""Translate a tiny slice of Map002 event 17 only (7 commands).""" # Every batch sent to translateAI on re-run should still contain Japanese
map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig")) for payload in captured2:
ev17 = next(e for e in map2["events"] if e and e.get("id") == 17) items = payload if isinstance(payload, list) else [payload]
micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only for item in items:
if not isinstance(item, str):
page, _ = _run_search_codes(micro) continue
c401 = _find_commands(page, 401) if item == "EN_TRANSLATED":
self.assertGreaterEqual(len(c401), 1) continue
with_orig = [c for c in c401 if c.get("_original")] if _has_japanese(item):
self.assertGreaterEqual(len(with_orig), 1) continue
for c in with_orig: if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item):
self.assertTrue(_has_japanese(c["_original"])) continue
self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}")
def test_speaker_color_line_full_original(self):
"""Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original.""" def test_map002_micro_page_real_data(self):
page = { """Translate a tiny slice of Map002 event 17 only (7 commands)."""
"list": [ map2 = json.loads((ROOT / "files" / "Map002.json").read_text(encoding="utf-8-sig"))
{"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]}, ev17 = next(e for e in map2["events"] if e and e.get("id") == 17)
{"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]}, micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only
]
} page, _ = _run_search_codes(micro)
page, _ = _run_search_codes(page) c401 = _find_commands(page, 401)
speaker_cmd = page["list"][0] self.assertGreaterEqual(len(c401), 1)
self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]") with_orig = [c for c in c401 if c.get("_original")]
self.assertIn("\\C[2]", speaker_cmd["parameters"][0]) self.assertGreaterEqual(len(with_orig), 1)
self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0]) for c in with_orig:
self.assertTrue(_has_japanese(c["_original"]))
# Re-run: _original unchanged, getSpeaker still receives Japanese name
speakers_seen = [] def test_speaker_color_line_full_original(self):
"""Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original."""
def speaker(name): page = {
speakers_seen.append(name) "list": [
return _mock_speaker(name) {"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]},
{"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]},
orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker ]
mvmz.getSpeaker = speaker }
mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch) page, _ = _run_search_codes(page)
try: speaker_cmd = page["list"][0]
mvmz.searchCodes(page, None, [], "TestMap.json") self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]")
finally: self.assertIn("\\C[2]", speaker_cmd["parameters"][0])
mvmz.getSpeaker = orig_s self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0])
mvmz.translateAI = orig_t
self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]") # Re-run: _original unchanged, getSpeaker still receives Japanese name
self.assertIn("エルーシャ", speakers_seen) speakers_seen = []
def test_405_split_rerun_uses_anchor_original_only(self): def speaker(name):
"""English 405 siblings after a split must not pollute re-run source.""" speakers_seen.append(name)
page = { return _mock_speaker(name)
"list": [
{ orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker
"code": 405, orig_401 = mvmz.CODE401
"indent": 0, mvmz.getSpeaker = speaker
"parameters": ["EN_LINE_1"], mvmz.CODE401 = True
"_original": "第一行\n第二行", mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch)
}, try:
{"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]}, mvmz.searchCodes(page, None, [], "TestMap.json")
] finally:
} mvmz.getSpeaker = orig_s
_, captured = _run_search_codes(page) mvmz.translateAI = orig_t
self.assertGreater(len(captured), 0) mvmz.CODE401 = orig_401
payloads = captured if isinstance(captured[0], str) else captured self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]")
for payload in payloads: self.assertIn("エルーシャ", speakers_seen)
if not isinstance(payload, str):
continue def test_405_split_rerun_uses_anchor_original_only(self):
self.assertIn("第一行", payload) """English 405 siblings after a split must not pollute re-run source."""
self.assertNotIn("EN_LINE_1", payload) page = {
self.assertNotIn("EN_LINE_2", payload) "list": [
{
"code": 405,
def test_408_choice_help_original(self): "indent": 0,
page = { "parameters": ["EN_LINE_1"],
"list": [ "_original": "第一行\n第二行",
{"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]}, },
{"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]}, {"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]},
] ]
} }
page, captured = _run_search_codes(page) _, captured = _run_search_codes(page)
cmd = _find_commands(page, 408)[0] self.assertGreater(len(captured), 0)
self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。") payloads = captured if isinstance(captured[0], str) else captured
self.assertNotEqual(cmd["parameters"][0], cmd["_original"]) for payload in payloads:
self.assertGreater(len(captured), 0) if not isinstance(payload, str):
continue
page2, captured2 = _run_search_codes(page) self.assertIn("第一行", payload)
self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_original"]) self.assertNotIn("EN_LINE_1", payload)
for payload in captured2: self.assertNotIn("EN_LINE_2", payload)
items = payload if isinstance(payload, list) else [payload]
for item in items:
if not isinstance(item, str) or item == "EN_TRANSLATED": def test_408_choice_help_original(self):
continue page = {
self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}") "list": [
{"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]},
{"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]},
class TestFixtureMapOriginal(unittest.TestCase): ]
"""Full fixture map covering every _original preservation code path.""" }
page, captured = _run_search_codes(page)
def test_fixture_all_cases_original(self): cmd = _find_commands(page, 408)[0]
page, _ = _run_search_codes(_load_fixture_page()) self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。")
marked = _case_commands(page["list"]) self.assertNotEqual(cmd["parameters"][0], cmd["_original"])
manifest = _load_fixture_manifest() self.assertGreater(len(captured), 0)
for entry in manifest["cases"]: page2, captured2 = _run_search_codes(page)
cid = entry["id"] self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_original"])
cmd = _resolve_case_command(page["list"], entry, marked) for payload in captured2:
expected = entry["expected_original"] items = payload if isinstance(payload, list) else [payload]
with self.subTest(case=cid): for item in items:
self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}") if not isinstance(item, str) or item == "EN_TRANSLATED":
self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid)) continue
if isinstance(expected, str): self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}")
self.assertTrue(_has_japanese(expected))
if cmd.get("parameters"):
display = cmd["parameters"][0] class TestFixtureMapOriginal(unittest.TestCase):
if isinstance(display, str): """Full fixture map covering every _original preservation code path."""
self.assertNotEqual(display, expected)
def test_fixture_all_cases_original(self):
def test_fixture_rerun_preserves_original(self): page, _ = _run_search_codes(_load_fixture_page())
page1, _ = _run_search_codes(_load_fixture_page()) marked = _case_commands(page["list"])
page2, captured2 = _run_search_codes(page1) manifest = _load_fixture_manifest()
manifest = _load_fixture_manifest()
for entry in manifest["cases"]:
for entry in manifest["cases"]: cid = entry["id"]
cid = entry["id"] cmd = _resolve_case_command(page["list"], entry, marked)
cmd1 = _resolve_case_command(page1["list"], entry) expected = entry["expected_original"]
cmd2 = _resolve_case_command(page2["list"], entry) with self.subTest(case=cid):
with self.subTest(case=cid): self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}")
self.assertIsNotNone(cmd1) self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid))
self.assertIsNotNone(cmd2) if isinstance(expected, str):
self.assertEqual(cmd1.get("_original"), cmd2.get("_original")) self.assertTrue(_has_japanese(expected))
if cmd.get("parameters"):
for payload in captured2: display = cmd["parameters"][0]
items = payload if isinstance(payload, list) else [payload] if isinstance(display, str):
for item in items: self.assertNotEqual(display, expected)
if not isinstance(item, str) or item == "EN_TRANSLATED":
continue def test_fixture_rerun_preserves_original(self):
if _has_japanese(item): page1, _ = _run_search_codes(_load_fixture_page())
continue page2, captured2 = _run_search_codes(page1)
if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item): manifest = _load_fixture_manifest()
continue
self.fail(f"Fixture re-run sent non-Japanese to translateAI: {item!r}") for entry in manifest["cases"]:
cid = entry["id"]
cmd1 = _resolve_case_command(page1["list"], entry)
if __name__ == "__main__": cmd2 = _resolve_case_command(page2["list"], entry)
unittest.main(verbosity=2) with self.subTest(case=cid):
self.assertIsNotNone(cmd1)
self.assertIsNotNone(cmd2)
self.assertEqual(cmd1.get("_original"), cmd2.get("_original"))
for payload in captured2:
items = payload if isinstance(payload, list) else [payload]
for item in items:
if not isinstance(item, str) or item == "EN_TRANSLATED":
continue
if _has_japanese(item):
continue
if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item):
continue
self.fail(f"Fixture re-run sent non-Japanese to translateAI: {item!r}")
def test_preserve_original_codes_disabled(self):
"""PRESERVEORIGINAL=False must not write _original on map commands."""
page = {
"list": [
{"code": 401, "indent": 0, "parameters": ["こんにちは"]},
]
}
page, _ = _run_search_codes(page, preserve_original=False)
self.assertNotIn("_original", page["list"][0])
if __name__ == "__main__":
unittest.main(verbosity=2)