diff --git a/gui/config_integration.py b/gui/config_integration.py
index fd15bc8..ec1beae 100644
--- a/gui/config_integration.py
+++ b/gui/config_integration.py
@@ -1,283 +1,283 @@
-"""
-Configuration Integration Helper
-Updates the actual module files with GUI configuration settings
-"""
-
-import re
-import os
-from pathlib import Path
-from typing import Dict, Any
-
-
-class ConfigIntegration:
- """Helper class to integrate GUI configuration with module files."""
-
- def __init__(self):
- self.modules_dir = Path("modules")
-
- def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool:
- """Update rpgmakermvmz.py with configuration from GUI."""
- module_path = self.modules_dir / "rpgmakermvmz.py"
-
- if not module_path.exists():
- raise FileNotFoundError(f"Module file not found: {module_path}")
-
- try:
- # Read the current module file
- with open(module_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Update configuration values
- updated_content = self._update_config_values(content, config)
-
- # Write back to file
- with open(module_path, 'w', encoding='utf-8') as f:
- f.write(updated_content)
-
- return True
-
- except Exception as e:
- raise e
-
- def _update_config_values(self, content: str, config: Dict[str, Any]) -> str:
- """Update configuration values in the module content."""
- lines = content.split('\n')
- updated_lines = []
-
- # Track which configurations we found and updated
- found_configs = set()
-
- for line in lines:
- updated_line = line
-
- # Look for configuration assignments
- for config_key, config_value in config.items():
- # Match lines like: CONFIG_NAME = value # comment
- pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$'
- match = re.match(pattern, line.strip())
-
- if match:
- # Preserve the comment if it exists
- comment = match.group(2) if match.group(2) else ""
- if comment:
- comment = " " + comment
-
- # Create the new line with proper formatting
- updated_line = f"{config_key} = {config_value}{comment}"
- found_configs.add(config_key)
- break
-
- updated_lines.append(updated_line)
-
- # Check if all configurations were found
- missing_configs = set(config.keys()) - found_configs
- if missing_configs:
- print(f"Warning: Could not find these configurations in module: {missing_configs}")
-
- return '\n'.join(updated_lines)
-
- def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool:
- """Update .env file with configuration."""
- if env_path is None:
- env_path = Path(".env")
-
- try:
- from dotenv import set_key
-
- for key, value in config.items():
- set_key(env_path, key, str(value))
-
- return True
-
- except Exception as e:
- print(f"Error updating .env file: {e}")
- return False
-
- def read_current_config(self, module_path: Path = None) -> Dict[str, Any]:
- """Read current configuration from module file."""
- if module_path is None:
- module_path = self.modules_dir / "rpgmakermvmz.py"
-
- if not module_path.exists():
- return {}
-
- config = {}
-
- try:
- with open(module_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Find configuration lines used by the RPG Maker config UI.
- bool_pattern = (
- r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|'
- r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|TLSYSTEMVARIABLES|'
- r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)'
- )
- int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)'
-
- for line in content.split('\n'):
- line = line.strip()
- match = re.match(bool_pattern, line)
- if match:
- key = match.group(1)
- value = match.group(2) == 'True'
- config[key] = value
- continue
-
- match = re.match(int_pattern, line)
- if match:
- key = match.group(1)
- config[key] = int(match.group(2))
-
- except Exception as e:
- print(f"Error reading configuration from {module_path}: {e}")
-
- return config
-
- 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."""
- if module_path is None:
- module_path = self.modules_dir / "rpgmakermvmz.py"
- result: Dict[str, Any] = {
- "ENABLED_PLUGINS_357": set(),
- "ENABLED_PATTERNS_355655": set(),
- }
- if not module_path.exists():
- return result
- try:
- with open(module_path, "r", encoding="utf-8") as f:
- content = f.read()
- for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"):
- # Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"}
- m = re.search(
- rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))',
- content,
- re.MULTILINE,
- )
- if m:
- bracket = m.group(1)
- items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket)
- result[var_name] = {a or b for a, b in items}
- except Exception as e:
- print(f"Error reading plugin config: {e}")
- return result
-
- def update_plugin_config(
- self,
- enabled_357: set,
- enabled_355655: set,
- module_path: Path = None,
- ) -> bool:
- """Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file."""
- if module_path is None:
- module_path = self.modules_dir / "rpgmakermvmz.py"
- if not module_path.exists():
- raise FileNotFoundError(f"Module file not found: {module_path}")
-
- def _fmt(s: set) -> str:
- if not s:
- return "set()"
- items = ", ".join(f'"{k}"' for k in sorted(s))
- return "{" + items + "}"
-
- try:
- with open(module_path, "r", encoding="utf-8") as f:
- content = f.read()
- for var_name, val_set in (
- ("ENABLED_PLUGINS_357", enabled_357),
- ("ENABLED_PATTERNS_355655", enabled_355655),
- ):
- content = re.sub(
- rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))',
- f'{var_name}: set = {_fmt(val_set)}',
- content,
- flags=re.MULTILINE | re.DOTALL,
- )
- with open(module_path, "w", encoding="utf-8") as f:
- f.write(content)
- return True
- except Exception as e:
- raise e
-
- def validate_module_syntax(self, module_path: Path = None) -> bool:
- """Validate that the module file has correct Python syntax."""
- if module_path is None:
- module_path = self.modules_dir / "rpgmakermvmz.py"
-
- try:
- with open(module_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Try to compile the code
- compile(content, str(module_path), 'exec')
- return True
-
- except SyntaxError as e:
- print(f"Syntax error in {module_path}: {e}")
- return False
- except Exception as e:
- print(f"Error validating {module_path}: {e}")
- return False
-
- def create_config_template(self) -> Dict[str, Any]:
- """Create a template configuration with default values."""
- return {
- # General config
- "FIRSTLINESPEAKERS": False,
- "INLINE401SPEAKERS": False,
- "FACENAME101": False,
- "NAMES": False,
- "BRFLAG": False,
- "FIXTEXTWRAP": True,
- "IGNORETLTEXT": False,
-
- # Main Codes
- "CODE401": True,
- "CODE405": True,
- "CODE102": True,
-
- # Optional codes
- "CODE101": False,
- "CODE408": False,
-
- # Variable codes
- "CODE122": False,
-
- # Other codes
- "CODE355655": False,
- "CODE357": False,
- "CODE657": False,
- "CODE356": False,
- "CODE320": False,
- "CODE324": False,
- "CODE111": False,
- "CODE108": False
- }
-
- def get_config_descriptions(self) -> Dict[str, str]:
- """Get descriptions for all configuration options."""
- return {
- "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True",
- "INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines",
- "FACENAME101": "Find Speakers in 101 Codes based on Face Name",
- "NAMES": "Output a list of all the character names found",
- "BRFLAG": "If the game uses
instead of newlines",
- "FIXTEXTWRAP": "Overwrites textwrap for better formatting",
- "IGNORETLTEXT": "Ignores all translated text",
-
- "CODE401": "Show Text - Main dialogue content",
- "CODE405": "Show Text (Scrolling) - Longer dialogue",
- "CODE102": "Show Choices - Player choice options",
- "CODE101": "Character Names - Turn on when names exist in 101",
- "CODE408": "Comments - WARNING: Can inflate costs significantly",
- "CODE122": "Control Variables - Text stored in variables",
-
- "CODE355655": "Scripts - Text within script commands",
- "CODE357": "Picture Text - Text displayed on pictures",
- "CODE657": "Picture Text Extended - Extended picture text",
- "CODE356": "Plugin Commands - Plugin command parameters",
- "CODE320": "Change Name Input - Name input prompts",
- "CODE324": "Change Nickname - Nickname changes",
- "CODE111": "Conditional Branch - Conditional text",
- "CODE108": "Comments - Comment blocks"
- }
+"""
+Configuration Integration Helper
+Updates the actual module files with GUI configuration settings
+"""
+
+import re
+import os
+from pathlib import Path
+from typing import Dict, Any
+
+
+class ConfigIntegration:
+ """Helper class to integrate GUI configuration with module files."""
+
+ def __init__(self):
+ self.modules_dir = Path("modules")
+
+ def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool:
+ """Update rpgmakermvmz.py with configuration from GUI."""
+ module_path = self.modules_dir / "rpgmakermvmz.py"
+
+ if not module_path.exists():
+ raise FileNotFoundError(f"Module file not found: {module_path}")
+
+ try:
+ # Read the current module file
+ with open(module_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Update configuration values
+ updated_content = self._update_config_values(content, config)
+
+ # Write back to file
+ with open(module_path, 'w', encoding='utf-8') as f:
+ f.write(updated_content)
+
+ return True
+
+ except Exception as e:
+ raise e
+
+ def _update_config_values(self, content: str, config: Dict[str, Any]) -> str:
+ """Update configuration values in the module content."""
+ lines = content.split('\n')
+ updated_lines = []
+
+ # Track which configurations we found and updated
+ found_configs = set()
+
+ for line in lines:
+ updated_line = line
+
+ # Look for configuration assignments
+ for config_key, config_value in config.items():
+ # Match lines like: CONFIG_NAME = value # comment
+ pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$'
+ match = re.match(pattern, line.strip())
+
+ if match:
+ # Preserve the comment if it exists
+ comment = match.group(2) if match.group(2) else ""
+ if comment:
+ comment = " " + comment
+
+ # Create the new line with proper formatting
+ updated_line = f"{config_key} = {config_value}{comment}"
+ found_configs.add(config_key)
+ break
+
+ updated_lines.append(updated_line)
+
+ # Check if all configurations were found
+ missing_configs = set(config.keys()) - found_configs
+ if missing_configs:
+ print(f"Warning: Could not find these configurations in module: {missing_configs}")
+
+ return '\n'.join(updated_lines)
+
+ def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool:
+ """Update .env file with configuration."""
+ if env_path is None:
+ env_path = Path(".env")
+
+ try:
+ from dotenv import set_key
+
+ for key, value in config.items():
+ set_key(env_path, key, str(value))
+
+ return True
+
+ except Exception as e:
+ print(f"Error updating .env file: {e}")
+ return False
+
+ def read_current_config(self, module_path: Path = None) -> Dict[str, Any]:
+ """Read current configuration from module file."""
+ if module_path is None:
+ module_path = self.modules_dir / "rpgmakermvmz.py"
+
+ if not module_path.exists():
+ return {}
+
+ config = {}
+
+ try:
+ with open(module_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Find configuration lines used by the RPG Maker config UI.
+ bool_pattern = (
+ r'^(FIRSTLINESPEAKERS|INLINE401SPEAKERS|FACENAME101|NAMES|'
+ r'BRFLAG|FIXTEXTWRAP|IGNORETLTEXT|PRESERVEORIGINAL|TLSYSTEMVARIABLES|'
+ r'TLSYSTEMSWITCHES|JOIN408|SPEAKERS408|CODE\d+)\s*=\s*(True|False)'
+ )
+ int_pattern = r'^(CODE122_VAR_MIN|CODE122_VAR_MAX)\s*=\s*(\d+)'
+
+ for line in content.split('\n'):
+ line = line.strip()
+ match = re.match(bool_pattern, line)
+ if match:
+ key = match.group(1)
+ value = match.group(2) == 'True'
+ config[key] = value
+ continue
+
+ match = re.match(int_pattern, line)
+ if match:
+ key = match.group(1)
+ config[key] = int(match.group(2))
+
+ except Exception as e:
+ print(f"Error reading configuration from {module_path}: {e}")
+
+ return config
+
+ 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."""
+ if module_path is None:
+ module_path = self.modules_dir / "rpgmakermvmz.py"
+ result: Dict[str, Any] = {
+ "ENABLED_PLUGINS_357": set(),
+ "ENABLED_PATTERNS_355655": set(),
+ }
+ if not module_path.exists():
+ return result
+ try:
+ with open(module_path, "r", encoding="utf-8") as f:
+ content = f.read()
+ for var_name in ("ENABLED_PLUGINS_357", "ENABLED_PATTERNS_355655"):
+ # Match: VAR_NAME: set = set() or VAR_NAME: set = {"a", "b"}
+ m = re.search(
+ rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(\{{[^}}]*\}}|set\(\))',
+ content,
+ re.MULTILINE,
+ )
+ if m:
+ bracket = m.group(1)
+ items = re.findall(r'"([^"]+)"|\'([^\']+)\'', bracket)
+ result[var_name] = {a or b for a, b in items}
+ except Exception as e:
+ print(f"Error reading plugin config: {e}")
+ return result
+
+ def update_plugin_config(
+ self,
+ enabled_357: set,
+ enabled_355655: set,
+ module_path: Path = None,
+ ) -> bool:
+ """Write ENABLED_PLUGINS_357 and ENABLED_PATTERNS_355655 set literals to module file."""
+ if module_path is None:
+ module_path = self.modules_dir / "rpgmakermvmz.py"
+ if not module_path.exists():
+ raise FileNotFoundError(f"Module file not found: {module_path}")
+
+ def _fmt(s: set) -> str:
+ if not s:
+ return "set()"
+ items = ", ".join(f'"{k}"' for k in sorted(s))
+ return "{" + items + "}"
+
+ try:
+ with open(module_path, "r", encoding="utf-8") as f:
+ content = f.read()
+ for var_name, val_set in (
+ ("ENABLED_PLUGINS_357", enabled_357),
+ ("ENABLED_PATTERNS_355655", enabled_355655),
+ ):
+ content = re.sub(
+ rf'^{re.escape(var_name)}\s*(?::\s*set)?\s*=\s*(?:\{{[^}}]*\}}|set\(\))',
+ f'{var_name}: set = {_fmt(val_set)}',
+ content,
+ flags=re.MULTILINE | re.DOTALL,
+ )
+ with open(module_path, "w", encoding="utf-8") as f:
+ f.write(content)
+ return True
+ except Exception as e:
+ raise e
+
+ def validate_module_syntax(self, module_path: Path = None) -> bool:
+ """Validate that the module file has correct Python syntax."""
+ if module_path is None:
+ module_path = self.modules_dir / "rpgmakermvmz.py"
+
+ try:
+ with open(module_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Try to compile the code
+ compile(content, str(module_path), 'exec')
+ return True
+
+ except SyntaxError as e:
+ print(f"Syntax error in {module_path}: {e}")
+ return False
+ except Exception as e:
+ print(f"Error validating {module_path}: {e}")
+ return False
+
+ def create_config_template(self) -> Dict[str, Any]:
+ """Create a template configuration with default values."""
+ return {
+ # General config
+ "FIRSTLINESPEAKERS": False,
+ "INLINE401SPEAKERS": False,
+ "FACENAME101": False,
+ "NAMES": False,
+ "BRFLAG": False,
+ "FIXTEXTWRAP": True,
+ "IGNORETLTEXT": False,
+
+ # Main Codes
+ "CODE401": True,
+ "CODE405": True,
+ "CODE102": True,
+
+ # Optional codes
+ "CODE101": False,
+ "CODE408": False,
+
+ # Variable codes
+ "CODE122": False,
+
+ # Other codes
+ "CODE355655": False,
+ "CODE357": False,
+ "CODE657": False,
+ "CODE356": False,
+ "CODE320": False,
+ "CODE324": False,
+ "CODE111": False,
+ "CODE108": False
+ }
+
+ def get_config_descriptions(self) -> Dict[str, str]:
+ """Get descriptions for all configuration options."""
+ return {
+ "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True",
+ "INLINE401SPEAKERS": "Detect speaker from Name\u300cdialogue\u300d inline format in 401 lines",
+ "FACENAME101": "Find Speakers in 101 Codes based on Face Name",
+ "NAMES": "Output a list of all the character names found",
+ "BRFLAG": "If the game uses
instead of newlines",
+ "FIXTEXTWRAP": "Overwrites textwrap for better formatting",
+ "IGNORETLTEXT": "Ignores all translated text",
+
+ "CODE401": "Show Text - Main dialogue content",
+ "CODE405": "Show Text (Scrolling) - Longer dialogue",
+ "CODE102": "Show Choices - Player choice options",
+ "CODE101": "Character Names - Turn on when names exist in 101",
+ "CODE408": "Comments - WARNING: Can inflate costs significantly",
+ "CODE122": "Control Variables - Text stored in variables",
+
+ "CODE355655": "Scripts - Text within script commands",
+ "CODE357": "Picture Text - Text displayed on pictures",
+ "CODE657": "Picture Text Extended - Extended picture text",
+ "CODE356": "Plugin Commands - Plugin command parameters",
+ "CODE320": "Change Name Input - Name input prompts",
+ "CODE324": "Change Nickname - Nickname changes",
+ "CODE111": "Conditional Branch - Conditional text",
+ "CODE108": "Comments - Comment blocks"
+ }
diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py
index 2f7abd5..b6a5a5e 100644
--- a/modules/rpgmakermvmz.py
+++ b/modules/rpgmakermvmz.py
@@ -130,6 +130,8 @@ BRFLAG = False
FIXTEXTWRAP = True
# IGNORETLTEXT: Skip Translated Text.
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 = False
# 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:
"""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():
return
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:
"""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():
return
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:
"""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():
return
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:
"""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):
return
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:
"""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):
return
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:
"""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):
return
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:
"""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):
return
orig = _system_orig(data)
diff --git a/set_defaults.py b/set_defaults.py
index 70d2f81..7f6c24f 100644
--- a/set_defaults.py
+++ b/set_defaults.py
@@ -1,58 +1,59 @@
-import re
-from copy import deepcopy
-
-# Canonical defaults used by the GUI and the `set_defaults` script.
-# Stored as booleans for easier consumption by the GUI.
-DEFAULTS = {
- 'FIRSTLINESPEAKERS': False,
- 'FACENAME101': False,
- 'BRFLAG': False,
- 'FIXTEXTWRAP': True,
- 'IGNORETLTEXT': False,
- 'JOIN408': False,
- # Speakers / Dialogue / Scroll / Choices (Main Codes)
- 'CODE101': True,
- 'CODE401': True,
- 'CODE405': True,
- 'CODE102': True,
- # Optional
- 'CODE408': False,
- # Variables
- 'CODE122': False,
- # Other
- 'CODE355655': False,
- 'CODE357': False,
- 'CODE657': False,
- 'CODE356': False,
- 'CODE320': False,
- 'CODE324': False,
- 'CODE111': False,
- 'CODE108': False
-}
-
-
-def get_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.
-
- 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()
-
- # Update the content with the default values (converted to Python literals)
- for key, value in DEFAULTS.items():
- 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)
-
-
-if __name__ == "__main__":
- set_defaults('modules/rpgmakermvmz.py')
+import re
+from copy import deepcopy
+
+# Canonical defaults used by the GUI and the `set_defaults` script.
+# Stored as booleans for easier consumption by the GUI.
+DEFAULTS = {
+ 'FIRSTLINESPEAKERS': False,
+ 'FACENAME101': False,
+ 'BRFLAG': False,
+ 'FIXTEXTWRAP': True,
+ 'IGNORETLTEXT': False,
+ 'PRESERVEORIGINAL': True,
+ 'JOIN408': False,
+ # Speakers / Dialogue / Scroll / Choices (Main Codes)
+ 'CODE101': True,
+ 'CODE401': True,
+ 'CODE405': True,
+ 'CODE102': True,
+ # Optional
+ 'CODE408': False,
+ # Variables
+ 'CODE122': False,
+ # Other
+ 'CODE355655': False,
+ 'CODE357': False,
+ 'CODE657': False,
+ 'CODE356': False,
+ 'CODE320': False,
+ 'CODE324': False,
+ 'CODE111': False,
+ 'CODE108': False
+}
+
+
+def get_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.
+
+ 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()
+
+ # Update the content with the default values (converted to Python literals)
+ for key, value in DEFAULTS.items():
+ 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)
+
+
+if __name__ == "__main__":
+ set_defaults('modules/rpgmakermvmz.py')
diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md
index 19af199..f10f9f7 100644
--- a/tests/fixtures/README.md
+++ b/tests/fixtures/README.md
@@ -1,69 +1,77 @@
-# Map `_original` fixture
-
-## 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_manifest.json`** — Expected `_original` values after translation (used by tests).
-
-## Event layout (event id 1, page 0)
-
-| Order | Code | Purpose |
-|------:|------|---------|
-| 1–2 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue |
-| 3–4 | 401 | Merged multi-line dialogue (two 401s → one batch) |
-| 5–6 | 101, 405 | Empty face setup + scrolling text line |
-| 7 | 102 | Three choices (middle choice has `if(...)` prefix) |
-| 8–9 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line |
-| 10–11 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue |
-| 10–11 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` |
-
-## Run the test
-
-Using the project venv (recommended):
-
-```bash
-./tests/run_tests.sh
-```
-
-Or explicitly:
-
-```bash
-./tests/run_tests.sh tests.test_mvmz_source_original -v
-./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v
-```
-
-Manual equivalent (must run from project root, with venv activated):
-
-```bash
-cd /path/to/DazedMTLTool
-source .venv/bin/activate # or: source venv/bin/activate
-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.
-
-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.
-
-## Database `_original` fixtures
-
-Mini database JSON files for scalar field preservation (notes deferred to Phase 2):
-
-| Fixture | Parser | Fields |
-|---------|--------|--------|
-| `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` |
-| `Items_original_fixture.json` | `searchNames` | `name`, `description` |
-| `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` |
-| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` |
-| `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` |
-
-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 manifest’s `expected_original` fields in `translated/Map_original_fixture.json`.
+# Map `_original` fixture
+
+## 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_manifest.json`** — Expected `_original` values after translation (used by tests).
+
+## Event layout (event id 1, page 0)
+
+| Order | Code | Purpose |
+|------:|------|---------|
+| 1–2 | 401 | Color speaker line `\C[2]テスト子\C[0]` + following dialogue |
+| 3–4 | 401 | Merged multi-line dialogue (two 401s → one batch) |
+| 5–6 | 101, 405 | Empty face setup + scrolling text line |
+| 7 | 102 | Three choices (middle choice has `if(...)` prefix) |
+| 8–9 | 108, 408 | Choice-help marker (`選択肢ヘルプ`) + help comment line |
+| 10–11 | 101, 401 | Name box with `\C[2]アリス\C[0]` + dialogue |
+| 10–11 | 122 | Variable string `` `変数の中身` `` and `` `セミコロン`; `` |
+
+## Run the test
+
+Using the project venv (recommended):
+
+```bash
+./tests/run_tests.sh
+```
+
+Or explicitly:
+
+```bash
+./tests/run_tests.sh tests.test_mvmz_source_original -v
+./tests/run_tests.sh tests.test_mvmz_source_original.TestFixtureMapOriginal -v
+```
+
+Manual equivalent (must run from project root, with venv activated):
+
+```bash
+cd /path/to/DazedMTLTool
+source .venv/bin/activate # or: source venv/bin/activate
+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.
+
+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.
+
+### Optional `_original` config (in `modules/rpgmakermvmz.py`)
+
+| Flag | Scope | Default |
+|------|-------|---------|
+| `PRESERVEORIGINAL` | Map/event commands and database JSON | `True` |
+
+Set to `False` to skip writing `_original`. Existing `_original` keys are still read on re-run.
+
+## Database `_original` fixtures
+
+Mini database JSON files for scalar field preservation (notes deferred to Phase 2):
+
+| Fixture | Parser | Fields |
+|---------|--------|--------|
+| `Actors_original_fixture.json` | `searchNames` | `name`, `nickname`, `profile` |
+| `Items_original_fixture.json` | `searchNames` | `name`, `description` |
+| `Skills_original_fixture.json` | `searchNames` | `name`, `description`, `message1` |
+| `States_original_fixture.json` | `searchSS` | `name`, `description`, `message1` |
+| `System_original_fixture.json` | `searchSystem` | `gameTitle`, `terms.basic[1]`, `armorTypes[1]`, `terms.messages.alwaysDash` |
+
+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 manifest’s `expected_original` fields in `translated/Map_original_fixture.json`.
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index d141c79..c50f322 100644
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -24,7 +24,7 @@ if ! "$PYTHON" -c "import colorama, dotenv, tqdm" >/dev/null 2>&1; then
fi
if [[ "$#" -eq 0 ]]; then
- set -- tests.test_mvmz_source_original -v
+ set -- discover -s tests -p 'test_mvmz_*.py' -v
fi
exec "$PYTHON" -m unittest "$@"
diff --git a/tests/test_mvmz_db_original.py b/tests/test_mvmz_db_original.py
index f1a0be7..0852505 100644
--- a/tests/test_mvmz_db_original.py
+++ b/tests/test_mvmz_db_original.py
@@ -1,205 +1,222 @@
-#!/usr/bin/env python3
-"""Integration tests for _original source preservation in database parsers."""
-
-from __future__ import annotations
-
-import copy
-import json
-import os
-import re
-import sys
-import unittest
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[1]
-os.chdir(ROOT)
-sys.path.insert(0, str(ROOT))
-
-import modules.rpgmakermvmz as mvmz # noqa: E402
-
-LANGREGEX = mvmz.LANGREGEX
-FIXTURES = ROOT / "tests" / "fixtures"
-MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8"))
-
-
-def _mock_translate(text, history, batch=False):
- def tr(s):
- if not isinstance(s, str):
- return s
- return "EN_TRANSLATED"
-
- if isinstance(text, list):
- return [[tr(t) for t in text], [0, 0]]
- return [tr(text), [0, 0]]
-
-
-def _has_japanese(s: str) -> bool:
- return bool(re.search(LANGREGEX, s or ""))
-
-
-def _run_search_names(data, context, filename):
- captured = []
-
- def translate(text, history, batch=False):
- captured.append(copy.deepcopy(text))
- return _mock_translate(text, history, batch)
-
- orig_t = mvmz.translateAI
- orig_vocab = mvmz.update_vocab_section
- mvmz.translateAI = translate
- mvmz.update_vocab_section = lambda *args, **kwargs: None
- try:
- data_copy = copy.deepcopy(data)
- mvmz.searchNames(data_copy, None, context, filename)
- return data_copy, captured
- finally:
- mvmz.translateAI = orig_t
- mvmz.update_vocab_section = orig_vocab
-
-
-def _run_search_ss(state):
- captured = []
-
- def translate(text, history, batch=False):
- captured.append(copy.deepcopy(text))
- return _mock_translate(text, history, batch)
-
- orig_t = mvmz.translateAI
- mvmz.translateAI = translate
- try:
- state_copy = copy.deepcopy(state)
- mvmz.searchSS(state_copy, None)
- return state_copy, captured
- finally:
- mvmz.translateAI = orig_t
-
-
-def _run_search_system(data):
- captured = []
-
- def translate(text, history, batch=False):
- captured.append(copy.deepcopy(text))
- return _mock_translate(text, history, batch)
-
- orig_t = mvmz.translateAI
- mvmz.translateAI = translate
- try:
- data_copy = copy.deepcopy(data)
- mvmz.searchSystem(data_copy, None)
- return data_copy, captured
- finally:
- mvmz.translateAI = orig_t
-
-
-def _assert_batches_japanese(captured):
- for payload in captured:
- items = payload if isinstance(payload, list) else [payload]
- for item in items:
- if not isinstance(item, str) or item == "EN_TRANSLATED":
- continue
- if item.startswith("Taro"):
- item = item[4:]
- self_fail = not _has_japanese(item)
- if self_fail:
- raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}")
-
-
-class TestActorsOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
- result, _ = _run_search_names(data, "Actors", "Actors.json")
- entry = result[MANIFEST["actors"]["entry_index"]]
- expected = MANIFEST["actors"]["expected_original"]
- self.assertEqual(entry.get("_original"), expected)
- self.assertNotEqual(entry["name"], expected["name"])
- self.assertNotEqual(entry["nickname"], expected["nickname"])
- self.assertNotEqual(entry["profile"], expected["profile"])
-
- def test_rerun_preserves_original(self):
- data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
- result1, _ = _run_search_names(data, "Actors", "Actors.json")
- orig_snapshot = copy.deepcopy(result1[1]["_original"])
- result2, captured2 = _run_search_names(result1, "Actors", "Actors.json")
- self.assertEqual(result2[1]["_original"], orig_snapshot)
- _assert_batches_japanese(captured2)
-
-
-class TestItemsOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
- result, _ = _run_search_names(data, "Items", "Items.json")
- entry = result[MANIFEST["items"]["entry_index"]]
- expected = MANIFEST["items"]["expected_original"]
- self.assertEqual(entry.get("_original"), expected)
- self.assertNotEqual(entry["name"], expected["name"])
- self.assertNotEqual(entry["description"], expected["description"])
-
- def test_rerun_preserves_original(self):
- data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
- result1, _ = _run_search_names(data, "Items", "Items.json")
- orig_snapshot = copy.deepcopy(result1[1]["_original"])
- result2, captured2 = _run_search_names(result1, "Items", "Items.json")
- self.assertEqual(result2[1]["_original"], orig_snapshot)
- _assert_batches_japanese(captured2)
-
-
-class TestSkillsOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
- result, _ = _run_search_names(data, "Skills", "Skills.json")
- entry = result[MANIFEST["skills"]["entry_index"]]
- expected = MANIFEST["skills"]["expected_original"]
- self.assertEqual(entry.get("_original"), expected)
- for field, jp in expected.items():
- self.assertNotEqual(entry[field], jp)
-
- def test_rerun_preserves_original(self):
- data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
- result1, _ = _run_search_names(data, "Skills", "Skills.json")
- orig_snapshot = copy.deepcopy(result1[1]["_original"])
- result2, captured2 = _run_search_names(result1, "Skills", "Items.json")
- self.assertEqual(result2[1]["_original"], orig_snapshot)
- _assert_batches_japanese(captured2)
-
-
-class TestStatesOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
- state = data[MANIFEST["states"]["entry_index"]]
- result, _ = _run_search_ss(state)
- expected = MANIFEST["states"]["expected_original"]
- self.assertEqual(result.get("_original"), expected)
- for field, jp in expected.items():
- self.assertNotEqual(result[field], jp)
-
- def test_rerun_preserves_original(self):
- data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
- state = data[MANIFEST["states"]["entry_index"]]
- result1, _ = _run_search_ss(state)
- orig_snapshot = copy.deepcopy(result1["_original"])
- result2, captured2 = _run_search_ss(result1)
- self.assertEqual(result2["_original"], orig_snapshot)
- _assert_batches_japanese(captured2)
-
-
-class TestSystemOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8"))
- result, _ = _run_search_system(data)
- expected = MANIFEST["system"]["expected_original"]
- self.assertEqual(result.get("_original"), expected)
- self.assertNotEqual(result["gameTitle"], expected["gameTitle"])
- self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"])
- self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"])
-
- def test_rerun_preserves_original(self):
- 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)
-
-
-if __name__ == "__main__":
- unittest.main(verbosity=2)
+#!/usr/bin/env python3
+"""Integration tests for _original source preservation in database parsers."""
+
+from __future__ import annotations
+
+import copy
+import json
+import os
+import re
+import sys
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+os.chdir(ROOT)
+sys.path.insert(0, str(ROOT))
+
+import modules.rpgmakermvmz as mvmz # noqa: E402
+
+LANGREGEX = mvmz.LANGREGEX
+FIXTURES = ROOT / "tests" / "fixtures"
+MANIFEST = json.loads((FIXTURES / "db_original_manifest.json").read_text(encoding="utf-8"))
+
+
+def _mock_translate(text, history, batch=False):
+ def tr(s):
+ if not isinstance(s, str):
+ return s
+ return "EN_TRANSLATED"
+
+ if isinstance(text, list):
+ return [[tr(t) for t in text], [0, 0]]
+ return [tr(text), [0, 0]]
+
+
+def _has_japanese(s: str) -> bool:
+ return bool(re.search(LANGREGEX, s or ""))
+
+
+def _run_search_names(data, context, filename, *, preserve_original=True):
+ captured = []
+
+ def translate(text, history, batch=False):
+ captured.append(copy.deepcopy(text))
+ return _mock_translate(text, history, batch)
+
+ orig_t = mvmz.translateAI
+ orig_vocab = mvmz.update_vocab_section
+ orig_preserve = mvmz.PRESERVEORIGINAL
+ mvmz.translateAI = translate
+ mvmz.update_vocab_section = lambda *args, **kwargs: None
+ mvmz.PRESERVEORIGINAL = preserve_original
+ try:
+ data_copy = copy.deepcopy(data)
+ mvmz.searchNames(data_copy, None, context, filename)
+ return data_copy, captured
+ finally:
+ mvmz.translateAI = orig_t
+ mvmz.update_vocab_section = orig_vocab
+ mvmz.PRESERVEORIGINAL = orig_preserve
+
+
+def _run_search_ss(state):
+ captured = []
+
+ def translate(text, history, batch=False):
+ captured.append(copy.deepcopy(text))
+ return _mock_translate(text, history, batch)
+
+ orig_t = mvmz.translateAI
+ orig_preserve = mvmz.PRESERVEORIGINAL
+ mvmz.translateAI = translate
+ mvmz.PRESERVEORIGINAL = True
+ try:
+ state_copy = copy.deepcopy(state)
+ mvmz.searchSS(state_copy, None)
+ return state_copy, captured
+ finally:
+ mvmz.translateAI = orig_t
+ mvmz.PRESERVEORIGINAL = orig_preserve
+
+
+def _run_search_system(data):
+ captured = []
+
+ def translate(text, history, batch=False):
+ captured.append(copy.deepcopy(text))
+ return _mock_translate(text, history, batch)
+
+ orig_t = mvmz.translateAI
+ orig_preserve = mvmz.PRESERVEORIGINAL
+ mvmz.translateAI = translate
+ mvmz.PRESERVEORIGINAL = True
+ try:
+ data_copy = copy.deepcopy(data)
+ mvmz.searchSystem(data_copy, None)
+ return data_copy, captured
+ finally:
+ mvmz.translateAI = orig_t
+ mvmz.PRESERVEORIGINAL = orig_preserve
+
+
+def _assert_batches_japanese(captured):
+ for payload in captured:
+ items = payload if isinstance(payload, list) else [payload]
+ for item in items:
+ if not isinstance(item, str) or item == "EN_TRANSLATED":
+ continue
+ if item.startswith("Taro"):
+ item = item[4:]
+ self_fail = not _has_japanese(item)
+ if self_fail:
+ raise AssertionError(f"Re-run sent non-Japanese to translateAI: {item!r}")
+
+
+class TestActorsOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
+ result, _ = _run_search_names(data, "Actors", "Actors.json")
+ entry = result[MANIFEST["actors"]["entry_index"]]
+ expected = MANIFEST["actors"]["expected_original"]
+ self.assertEqual(entry.get("_original"), expected)
+ self.assertNotEqual(entry["name"], expected["name"])
+ self.assertNotEqual(entry["nickname"], expected["nickname"])
+ self.assertNotEqual(entry["profile"], expected["profile"])
+
+ def test_rerun_preserves_original(self):
+ data = json.loads((FIXTURES / "Actors_original_fixture.json").read_text(encoding="utf-8"))
+ result1, _ = _run_search_names(data, "Actors", "Actors.json")
+ orig_snapshot = copy.deepcopy(result1[1]["_original"])
+ result2, captured2 = _run_search_names(result1, "Actors", "Actors.json")
+ self.assertEqual(result2[1]["_original"], orig_snapshot)
+ _assert_batches_japanese(captured2)
+
+
+class TestItemsOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
+ result, _ = _run_search_names(data, "Items", "Items.json")
+ entry = result[MANIFEST["items"]["entry_index"]]
+ expected = MANIFEST["items"]["expected_original"]
+ self.assertEqual(entry.get("_original"), expected)
+ self.assertNotEqual(entry["name"], expected["name"])
+ self.assertNotEqual(entry["description"], expected["description"])
+
+ def test_rerun_preserves_original(self):
+ data = json.loads((FIXTURES / "Items_original_fixture.json").read_text(encoding="utf-8"))
+ result1, _ = _run_search_names(data, "Items", "Items.json")
+ orig_snapshot = copy.deepcopy(result1[1]["_original"])
+ result2, captured2 = _run_search_names(result1, "Items", "Items.json")
+ self.assertEqual(result2[1]["_original"], orig_snapshot)
+ _assert_batches_japanese(captured2)
+
+
+class TestSkillsOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
+ result, _ = _run_search_names(data, "Skills", "Skills.json")
+ entry = result[MANIFEST["skills"]["entry_index"]]
+ expected = MANIFEST["skills"]["expected_original"]
+ self.assertEqual(entry.get("_original"), expected)
+ for field, jp in expected.items():
+ self.assertNotEqual(entry[field], jp)
+
+ def test_rerun_preserves_original(self):
+ data = json.loads((FIXTURES / "Skills_original_fixture.json").read_text(encoding="utf-8"))
+ result1, _ = _run_search_names(data, "Skills", "Skills.json")
+ orig_snapshot = copy.deepcopy(result1[1]["_original"])
+ result2, captured2 = _run_search_names(result1, "Skills", "Items.json")
+ self.assertEqual(result2[1]["_original"], orig_snapshot)
+ _assert_batches_japanese(captured2)
+
+
+class TestStatesOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
+ state = data[MANIFEST["states"]["entry_index"]]
+ result, _ = _run_search_ss(state)
+ expected = MANIFEST["states"]["expected_original"]
+ self.assertEqual(result.get("_original"), expected)
+ for field, jp in expected.items():
+ self.assertNotEqual(result[field], jp)
+
+ def test_rerun_preserves_original(self):
+ data = json.loads((FIXTURES / "States_original_fixture.json").read_text(encoding="utf-8"))
+ state = data[MANIFEST["states"]["entry_index"]]
+ result1, _ = _run_search_ss(state)
+ orig_snapshot = copy.deepcopy(result1["_original"])
+ result2, captured2 = _run_search_ss(result1)
+ self.assertEqual(result2["_original"], orig_snapshot)
+ _assert_batches_japanese(captured2)
+
+
+class TestSystemOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ data = json.loads((FIXTURES / "System_original_fixture.json").read_text(encoding="utf-8"))
+ result, _ = _run_search_system(data)
+ expected = MANIFEST["system"]["expected_original"]
+ self.assertEqual(result.get("_original"), expected)
+ self.assertNotEqual(result["gameTitle"], expected["gameTitle"])
+ self.assertNotEqual(result["terms"]["basic"][1], expected["terms"]["basic"]["1"])
+ self.assertNotEqual(result["armorTypes"][1], expected["armorTypes"]["1"])
+
+ def test_rerun_preserves_original(self):
+ 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)
diff --git a/tests/test_mvmz_source_original.py b/tests/test_mvmz_source_original.py
index 5cd7309..b3bc091 100644
--- a/tests/test_mvmz_source_original.py
+++ b/tests/test_mvmz_source_original.py
@@ -1,363 +1,391 @@
-#!/usr/bin/env python3
-"""Integration tests for _original source preservation in rpgmakermvmz searchCodes."""
-
-from __future__ import annotations
-
-import copy
-import json
-import os
-import re
-import sys
-import unittest
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[1]
-os.chdir(ROOT)
-sys.path.insert(0, str(ROOT))
-
-import modules.rpgmakermvmz as mvmz # noqa: E402
-
-LANGREGEX = mvmz.LANGREGEX
-
-
-def _mock_translate(text, history, batch=False):
- def tr(s):
- if not isinstance(s, str):
- return s
- m = re.match(r"^(\[[^\]]+\]:\s?)", s)
- if m:
- return m.group(1) + "EN_TRANSLATED"
- return "EN_TRANSLATED"
-
- if isinstance(text, list):
- return [[tr(t) for t in text], [0, 0]]
- return [tr(text), [0, 0]]
-
-
-def _mock_speaker(name):
- return [f"Speaker_{name}", [0, 0]]
-
-
-FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json"
-FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json"
-CASE_MARKER_RE = re.compile(r"# CASE:(\S+)")
-
-
-def _load_fixture_page():
- 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)
- return {"list": copy.deepcopy(event["pages"][0]["list"])}
-
-
-def _case_commands(page_list):
- """Map manifest case id -> command immediately following its 108 marker."""
- cases = {}
- pending = None
- for cmd in page_list:
- if not cmd:
- continue
- if cmd.get("code") == 108:
- m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0]))
- pending = m.group(1) if m else None
- continue
- if pending:
- cases[pending] = cmd
- pending = None
- return cases
-
-
-def _load_fixture_manifest():
- return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8"))
-
-
-def _load_map_excerpt():
- """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"))
- 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
-
- map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig"))
- choice_cmd = None
- for ev in map1["events"]:
- if not ev:
- continue
- for pg in ev.get("pages") or []:
- if not pg:
- continue
- for cmd in pg.get("list") or []:
- if cmd and cmd.get("code") == 102:
- choice_cmd = copy.deepcopy(cmd)
- break
- if choice_cmd:
- break
- if choice_cmd:
- break
- assert choice_cmd is not None, "Map001 should contain a 102 choice command"
-
- synthetic = [
- {"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]},
- {"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]},
- {
- "code": 122,
- "indent": 0,
- "parameters": [101, 101, 0, 0, "`変数テスト`"],
- },
- ]
-
- return {"list": real + [choice_cmd] + synthetic}
-
-
-def _resolve_case_command(page_list, entry, marked_cases=None):
- """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)
- cid = entry["id"]
- cmd = marked_cases.get(cid)
- if cmd is not None and cmd.get("code") == entry.get("code"):
- return cmd
- exp = entry.get("expected_original")
- code = entry.get("code")
- if code is not None and exp is not None:
- for candidate in page_list:
- if candidate and candidate.get("code") == code and candidate.get("_original") == exp:
- return candidate
- return cmd
-
-
-def _run_search_codes(page):
- """Full Pass 1 -> mock translate -> Pass 2 cycle."""
- captured = []
-
- def translate(text, history, batch=False):
- captured.append(copy.deepcopy(text))
- return _mock_translate(text, history, batch)
-
- def speaker(name):
- return _mock_speaker(name)
-
- orig_t = mvmz.translateAI
- orig_s = mvmz.getSpeaker
- orig_122 = mvmz.CODE122
- orig_408 = mvmz.CODE408
- mvmz.translateAI = translate
- mvmz.getSpeaker = speaker
- mvmz.CODE122 = True
- mvmz.CODE408 = True
- try:
- page_copy = copy.deepcopy(page)
- mvmz.searchCodes(page_copy, None, [], "TestMap.json")
- return page_copy, captured
- finally:
- mvmz.translateAI = orig_t
- mvmz.getSpeaker = orig_s
- mvmz.CODE122 = orig_122
- mvmz.CODE408 = orig_408
-
-
-def _find_commands(page, code):
- return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code]
-
-
-def _has_japanese(s: str) -> bool:
- return bool(re.search(LANGREGEX, s or ""))
-
-
-class TestMVMZSourceOriginal(unittest.TestCase):
- def test_first_pass_writes_original(self):
- page, _ = _run_search_codes(_load_map_excerpt())
-
- cmds401 = _find_commands(page, 401)
- 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:
- self.assertTrue(_has_japanese(c["_original"]))
- self.assertNotEqual(c["parameters"][0], c["_original"])
-
- c102 = _find_commands(page, 102)[0]
- self.assertIsInstance(c102.get("_original"), list)
- self.assertEqual(len(c102["_original"]), len(c102["parameters"][0]))
- for i, orig in enumerate(c102["_original"]):
- if orig:
- self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese")
- self.assertNotEqual(c102["parameters"][0][i], orig)
-
- c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4)
- self.assertIn("_original", c101)
- self.assertIn("アリス", c101["_original"])
- self.assertIn("\\C[2]", c101["_original"])
- self.assertIn("Speaker_", c101["parameters"][4])
-
- c122 = _find_commands(page, 122)[0]
- self.assertEqual(c122["_original"], "変数テスト")
- self.assertIn("EN_TRANSLATED", c122["parameters"][4])
-
- def test_rerun_uses_original_not_display_text(self):
- page1, captured1 = _run_search_codes(_load_map_excerpt())
- originals_snapshot = json.dumps(
- {i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd},
- ensure_ascii=False,
- )
-
- page2, captured2 = _run_search_codes(page1)
- originals_after = json.dumps(
- {i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd},
- ensure_ascii=False,
- )
- self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run")
-
- # Every batch sent to translateAI on re-run should still contain Japanese
- for payload in captured2:
- items = payload if isinstance(payload, list) else [payload]
- for item in items:
- if not isinstance(item, str):
- continue
- if item == "EN_TRANSLATED":
- continue
- if _has_japanese(item):
- continue
- if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item):
- continue
- self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}")
-
- def test_map002_micro_page_real_data(self):
- """Translate a tiny slice of Map002 event 17 only (7 commands)."""
- 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)
- micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only
-
- page, _ = _run_search_codes(micro)
- c401 = _find_commands(page, 401)
- self.assertGreaterEqual(len(c401), 1)
- with_orig = [c for c in c401 if c.get("_original")]
- self.assertGreaterEqual(len(with_orig), 1)
- for c in with_orig:
- self.assertTrue(_has_japanese(c["_original"]))
-
- def test_speaker_color_line_full_original(self):
- """Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original."""
- page = {
- "list": [
- {"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]},
- {"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]},
- ]
- }
- page, _ = _run_search_codes(page)
- speaker_cmd = page["list"][0]
- self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]")
- self.assertIn("\\C[2]", speaker_cmd["parameters"][0])
- self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0])
-
- # Re-run: _original unchanged, getSpeaker still receives Japanese name
- speakers_seen = []
-
- def speaker(name):
- speakers_seen.append(name)
- return _mock_speaker(name)
-
- orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker
- mvmz.getSpeaker = speaker
- mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch)
- try:
- mvmz.searchCodes(page, None, [], "TestMap.json")
- finally:
- mvmz.getSpeaker = orig_s
- mvmz.translateAI = orig_t
- self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]")
- self.assertIn("エルーシャ", speakers_seen)
-
- def test_405_split_rerun_uses_anchor_original_only(self):
- """English 405 siblings after a split must not pollute re-run source."""
- page = {
- "list": [
- {
- "code": 405,
- "indent": 0,
- "parameters": ["EN_LINE_1"],
- "_original": "第一行\n第二行",
- },
- {"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]},
- ]
- }
- _, captured = _run_search_codes(page)
- self.assertGreater(len(captured), 0)
- payloads = captured if isinstance(captured[0], str) else captured
- for payload in payloads:
- if not isinstance(payload, str):
- continue
- self.assertIn("第一行", payload)
- self.assertNotIn("EN_LINE_1", payload)
- self.assertNotIn("EN_LINE_2", payload)
-
-
- def test_408_choice_help_original(self):
- page = {
- "list": [
- {"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]},
- {"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]},
- ]
- }
- page, captured = _run_search_codes(page)
- cmd = _find_commands(page, 408)[0]
- self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。")
- self.assertNotEqual(cmd["parameters"][0], cmd["_original"])
- self.assertGreater(len(captured), 0)
-
- page2, captured2 = _run_search_codes(page)
- self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_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
- self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}")
-
-
-class TestFixtureMapOriginal(unittest.TestCase):
- """Full fixture map covering every _original preservation code path."""
-
- def test_fixture_all_cases_original(self):
- page, _ = _run_search_codes(_load_fixture_page())
- marked = _case_commands(page["list"])
- manifest = _load_fixture_manifest()
-
- for entry in manifest["cases"]:
- cid = entry["id"]
- cmd = _resolve_case_command(page["list"], entry, marked)
- expected = entry["expected_original"]
- with self.subTest(case=cid):
- self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}")
- self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid))
- if isinstance(expected, str):
- self.assertTrue(_has_japanese(expected))
- if cmd.get("parameters"):
- display = cmd["parameters"][0]
- if isinstance(display, str):
- self.assertNotEqual(display, expected)
-
- def test_fixture_rerun_preserves_original(self):
- page1, _ = _run_search_codes(_load_fixture_page())
- page2, captured2 = _run_search_codes(page1)
- manifest = _load_fixture_manifest()
-
- for entry in manifest["cases"]:
- cid = entry["id"]
- cmd1 = _resolve_case_command(page1["list"], entry)
- cmd2 = _resolve_case_command(page2["list"], entry)
- 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}")
-
-
-if __name__ == "__main__":
- unittest.main(verbosity=2)
+#!/usr/bin/env python3
+"""Integration tests for _original source preservation in rpgmakermvmz searchCodes."""
+
+from __future__ import annotations
+
+import copy
+import json
+import os
+import re
+import sys
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+os.chdir(ROOT)
+sys.path.insert(0, str(ROOT))
+
+import modules.rpgmakermvmz as mvmz # noqa: E402
+
+LANGREGEX = mvmz.LANGREGEX
+
+
+def _mock_translate(text, history, batch=False):
+ def tr(s):
+ if not isinstance(s, str):
+ return s
+ m = re.match(r"^(\[[^\]]+\]:\s?)", s)
+ if m:
+ return m.group(1) + "EN_TRANSLATED"
+ return "EN_TRANSLATED"
+
+ if isinstance(text, list):
+ return [[tr(t) for t in text], [0, 0]]
+ return [tr(text), [0, 0]]
+
+
+def _mock_speaker(name):
+ return [f"Speaker_{name}", [0, 0]]
+
+
+FIXTURE_MAP = ROOT / "tests" / "fixtures" / "Map_original_fixture.json"
+FIXTURE_MANIFEST = ROOT / "tests" / "fixtures" / "Map_original_fixture_manifest.json"
+CASE_MARKER_RE = re.compile(r"# CASE:(\S+)")
+
+
+def _load_fixture_page():
+ 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)
+ return {"list": copy.deepcopy(event["pages"][0]["list"])}
+
+
+def _case_commands(page_list):
+ """Map manifest case id -> command immediately following its 108 marker."""
+ cases = {}
+ pending = None
+ for cmd in page_list:
+ if not cmd:
+ continue
+ if cmd.get("code") == 108:
+ m = CASE_MARKER_RE.search(str(cmd.get("parameters", [""])[0]))
+ pending = m.group(1) if m else None
+ continue
+ if pending:
+ cases[pending] = cmd
+ pending = None
+ return cases
+
+
+def _load_fixture_manifest():
+ return json.loads(FIXTURE_MANIFEST.read_text(encoding="utf-8"))
+
+
+def _load_map_excerpt():
+ """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"))
+ 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
+
+ map1 = json.loads((ROOT / "files" / "Map001.json").read_text(encoding="utf-8-sig"))
+ choice_cmd = None
+ for ev in map1["events"]:
+ if not ev:
+ continue
+ for pg in ev.get("pages") or []:
+ if not pg:
+ continue
+ for cmd in pg.get("list") or []:
+ if cmd and cmd.get("code") == 102:
+ choice_cmd = copy.deepcopy(cmd)
+ break
+ if choice_cmd:
+ break
+ if choice_cmd:
+ break
+ assert choice_cmd is not None, "Map001 should contain a 102 choice command"
+
+ synthetic = [
+ {"code": 101, "indent": 0, "parameters": ["", 0, 0, 2, "\\C[2]アリス\\C[0]"]},
+ {"code": 401, "indent": 0, "parameters": ["こんにちは、世界。"]},
+ {
+ "code": 122,
+ "indent": 0,
+ "parameters": [101, 101, 0, 0, "`変数テスト`"],
+ },
+ ]
+
+ return {"list": real + [choice_cmd] + synthetic}
+
+
+def _resolve_case_command(page_list, entry, marked_cases=None):
+ """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)
+ cid = entry["id"]
+ cmd = marked_cases.get(cid)
+ if cmd is not None and cmd.get("code") == entry.get("code"):
+ return cmd
+ exp = entry.get("expected_original")
+ code = entry.get("code")
+ if code is not None and exp is not None:
+ for candidate in page_list:
+ if candidate and candidate.get("code") == code and candidate.get("_original") == exp:
+ return candidate
+ return cmd
+
+
+def _run_search_codes(page, *, preserve_original=True):
+ """Full Pass 1 -> mock translate -> Pass 2 cycle."""
+ captured = []
+
+ def translate(text, history, batch=False):
+ captured.append(copy.deepcopy(text))
+ return _mock_translate(text, history, batch)
+
+ def speaker(name):
+ return _mock_speaker(name)
+
+ orig_t = mvmz.translateAI
+ orig_s = mvmz.getSpeaker
+ orig_122 = mvmz.CODE122
+ orig_408 = mvmz.CODE408
+ orig_101 = mvmz.CODE101
+ orig_401 = mvmz.CODE401
+ orig_405 = mvmz.CODE405
+ orig_102 = mvmz.CODE102
+ orig_preserve = mvmz.PRESERVEORIGINAL
+ mvmz.translateAI = translate
+ mvmz.getSpeaker = speaker
+ mvmz.CODE122 = True
+ mvmz.CODE408 = True
+ mvmz.CODE101 = True
+ mvmz.CODE401 = True
+ mvmz.CODE405 = True
+ mvmz.CODE102 = True
+ mvmz.PRESERVEORIGINAL = preserve_original
+ try:
+ page_copy = copy.deepcopy(page)
+ mvmz.searchCodes(page_copy, None, [], "TestMap.json")
+ return page_copy, captured
+ finally:
+ mvmz.translateAI = orig_t
+ mvmz.getSpeaker = orig_s
+ mvmz.CODE122 = orig_122
+ mvmz.CODE408 = orig_408
+ mvmz.CODE101 = orig_101
+ mvmz.CODE401 = orig_401
+ mvmz.CODE405 = orig_405
+ mvmz.CODE102 = orig_102
+ mvmz.PRESERVEORIGINAL = orig_preserve
+
+
+def _find_commands(page, code):
+ return [cmd for cmd in page["list"] if cmd and cmd.get("code") == code]
+
+
+def _has_japanese(s: str) -> bool:
+ return bool(re.search(LANGREGEX, s or ""))
+
+
+class TestMVMZSourceOriginal(unittest.TestCase):
+ def test_first_pass_writes_original(self):
+ page, _ = _run_search_codes(_load_map_excerpt())
+
+ cmds401 = _find_commands(page, 401)
+ 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:
+ self.assertTrue(_has_japanese(c["_original"]))
+ self.assertNotEqual(c["parameters"][0], c["_original"])
+
+ c102 = _find_commands(page, 102)[0]
+ self.assertIsInstance(c102.get("_original"), list)
+ self.assertEqual(len(c102["_original"]), len(c102["parameters"][0]))
+ for i, orig in enumerate(c102["_original"]):
+ if orig:
+ self.assertTrue(_has_japanese(orig), f"choice {i} _original should be Japanese")
+ self.assertNotEqual(c102["parameters"][0][i], orig)
+
+ c101 = next(c for c in _find_commands(page, 101) if len(c.get("parameters", [])) > 4)
+ self.assertIn("_original", c101)
+ self.assertIn("アリス", c101["_original"])
+ self.assertIn("\\C[2]", c101["_original"])
+ self.assertIn("Speaker_", c101["parameters"][4])
+
+ c122 = _find_commands(page, 122)[0]
+ self.assertEqual(c122["_original"], "変数テスト")
+ self.assertIn("EN_TRANSLATED", c122["parameters"][4])
+
+ def test_rerun_uses_original_not_display_text(self):
+ page1, captured1 = _run_search_codes(_load_map_excerpt())
+ originals_snapshot = json.dumps(
+ {i: cmd.get("_original") for i, cmd in enumerate(page1["list"]) if cmd},
+ ensure_ascii=False,
+ )
+
+ page2, captured2 = _run_search_codes(page1)
+ originals_after = json.dumps(
+ {i: cmd.get("_original") for i, cmd in enumerate(page2["list"]) if cmd},
+ ensure_ascii=False,
+ )
+ self.assertEqual(originals_snapshot, originals_after, "_original must not change on re-run")
+
+ # Every batch sent to translateAI on re-run should still contain Japanese
+ for payload in captured2:
+ items = payload if isinstance(payload, list) else [payload]
+ for item in items:
+ if not isinstance(item, str):
+ continue
+ if item == "EN_TRANSLATED":
+ continue
+ if _has_japanese(item):
+ continue
+ if re.match(r"^\[.+?\]:\s*EN_TRANSLATED$", item):
+ continue
+ self.fail(f"Re-run sent non-Japanese to translateAI: {item!r}")
+
+ def test_map002_micro_page_real_data(self):
+ """Translate a tiny slice of Map002 event 17 only (7 commands)."""
+ 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)
+ micro = {"list": copy.deepcopy(ev17["pages"][0]["list"][8:11])} # 3x401 only
+
+ page, _ = _run_search_codes(micro)
+ c401 = _find_commands(page, 401)
+ self.assertGreaterEqual(len(c401), 1)
+ with_orig = [c for c in c401 if c.get("_original")]
+ self.assertGreaterEqual(len(with_orig), 1)
+ for c in with_orig:
+ self.assertTrue(_has_japanese(c["_original"]))
+
+ def test_speaker_color_line_full_original(self):
+ """Standalone \\C[n]Name\\C[n] speaker 401 lines keep the full string in _original."""
+ page = {
+ "list": [
+ {"code": 401, "indent": 0, "parameters": ["\\C[2]エルーシャ\\C[0]"]},
+ {"code": 401, "indent": 0, "parameters": ["「テストセリフ」"]},
+ ]
+ }
+ page, _ = _run_search_codes(page)
+ speaker_cmd = page["list"][0]
+ self.assertEqual(speaker_cmd.get("_original"), "\\C[2]エルーシャ\\C[0]")
+ self.assertIn("\\C[2]", speaker_cmd["parameters"][0])
+ self.assertIn("Speaker_エルーシャ", speaker_cmd["parameters"][0])
+
+ # Re-run: _original unchanged, getSpeaker still receives Japanese name
+ speakers_seen = []
+
+ def speaker(name):
+ speakers_seen.append(name)
+ return _mock_speaker(name)
+
+ orig_t, orig_s = mvmz.translateAI, mvmz.getSpeaker
+ orig_401 = mvmz.CODE401
+ mvmz.getSpeaker = speaker
+ mvmz.CODE401 = True
+ mvmz.translateAI = lambda text, history, batch=False: _mock_translate(text, history, batch)
+ try:
+ mvmz.searchCodes(page, None, [], "TestMap.json")
+ finally:
+ mvmz.getSpeaker = orig_s
+ mvmz.translateAI = orig_t
+ mvmz.CODE401 = orig_401
+ self.assertEqual(speaker_cmd["_original"], "\\C[2]エルーシャ\\C[0]")
+ self.assertIn("エルーシャ", speakers_seen)
+
+ def test_405_split_rerun_uses_anchor_original_only(self):
+ """English 405 siblings after a split must not pollute re-run source."""
+ page = {
+ "list": [
+ {
+ "code": 405,
+ "indent": 0,
+ "parameters": ["EN_LINE_1"],
+ "_original": "第一行\n第二行",
+ },
+ {"code": 405, "indent": 0, "parameters": ["EN_LINE_2"]},
+ ]
+ }
+ _, captured = _run_search_codes(page)
+ self.assertGreater(len(captured), 0)
+ payloads = captured if isinstance(captured[0], str) else captured
+ for payload in payloads:
+ if not isinstance(payload, str):
+ continue
+ self.assertIn("第一行", payload)
+ self.assertNotIn("EN_LINE_1", payload)
+ self.assertNotIn("EN_LINE_2", payload)
+
+
+ def test_408_choice_help_original(self):
+ page = {
+ "list": [
+ {"code": 108, "indent": 0, "parameters": ["選択肢ヘルプ"]},
+ {"code": 408, "indent": 0, "parameters": ["これは選択肢のヘルプです。"]},
+ ]
+ }
+ page, captured = _run_search_codes(page)
+ cmd = _find_commands(page, 408)[0]
+ self.assertEqual(cmd.get("_original"), "これは選択肢のヘルプです。")
+ self.assertNotEqual(cmd["parameters"][0], cmd["_original"])
+ self.assertGreater(len(captured), 0)
+
+ page2, captured2 = _run_search_codes(page)
+ self.assertEqual(cmd["_original"], _find_commands(page2, 408)[0]["_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
+ self.assertTrue(_has_japanese(item), f"408 re-run sent non-Japanese: {item!r}")
+
+
+class TestFixtureMapOriginal(unittest.TestCase):
+ """Full fixture map covering every _original preservation code path."""
+
+ def test_fixture_all_cases_original(self):
+ page, _ = _run_search_codes(_load_fixture_page())
+ marked = _case_commands(page["list"])
+ manifest = _load_fixture_manifest()
+
+ for entry in manifest["cases"]:
+ cid = entry["id"]
+ cmd = _resolve_case_command(page["list"], entry, marked)
+ expected = entry["expected_original"]
+ with self.subTest(case=cid):
+ self.assertIsNotNone(cmd, f"could not resolve fixture case {cid}")
+ self.assertEqual(cmd.get("_original"), expected, entry.get("summary", cid))
+ if isinstance(expected, str):
+ self.assertTrue(_has_japanese(expected))
+ if cmd.get("parameters"):
+ display = cmd["parameters"][0]
+ if isinstance(display, str):
+ self.assertNotEqual(display, expected)
+
+ def test_fixture_rerun_preserves_original(self):
+ page1, _ = _run_search_codes(_load_fixture_page())
+ page2, captured2 = _run_search_codes(page1)
+ manifest = _load_fixture_manifest()
+
+ for entry in manifest["cases"]:
+ cid = entry["id"]
+ cmd1 = _resolve_case_command(page1["list"], entry)
+ cmd2 = _resolve_case_command(page2["list"], entry)
+ 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)