From b99a8100921d68683f0dc64a09e44c4d72905161 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 5 Sep 2026 11:02:28 +0200 Subject: [PATCH 1/2] Sync SuperWoW FoV with tool selection --- setup_tool_dynamic.py | 76 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/setup_tool_dynamic.py b/setup_tool_dynamic.py index ed01b22..6a233b8 100644 --- a/setup_tool_dynamic.py +++ b/setup_tool_dynamic.py @@ -1,5 +1,7 @@ import math import os +import re +import stat import struct import sys import tkinter as tk @@ -483,6 +485,80 @@ class ModernWowSetupTool(_ModernWowSetupToolCore): finally: _restore_mpq_runtime_hooks(originals) + def _configure_superwow_fov_cvar(self, target): + """Keep SuperWoW's FoV CVar aligned with the Tool-selected FoV.""" + superwow = self.core_plugins.get("SuperWoWhook.dll") + if superwow is None or not superwow.get(): + return + + try: + fov = float(self.vt_fov.get()) + except (TypeError, ValueError, tk.TclError) as exc: + raise RuntimeError("Field of View is not a valid numeric value.") from exc + if not math.isfinite(fov) or not 0.5 <= fov <= 3.5: + raise RuntimeError( + "Field of View value is outside the supported WoW 1.12.1 range." + ) + + wtf_dir = os.path.join(target, "WTF") + config_path = os.path.join(wtf_dir, "Config.wtf") + original_mode = None + restore_readonly = False + staged = config_path + ".modernization-fov" + + try: + os.makedirs(wtf_dir, exist_ok=True) + + if os.path.exists(config_path): + original_mode = os.stat(config_path).st_mode + if not (original_mode & stat.S_IWRITE): + os.chmod(config_path, original_mode | stat.S_IWRITE) + restore_readonly = True + + existing = "" + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8", errors="ignore") as handle: + existing = handle.read() + + setting = f'SET FoV "{format(fov, ".9g")}"' + pattern = re.compile( + r'^\s*SET\s+FoV\s+"[^"]*"\s*$', + re.IGNORECASE | re.MULTILINE, + ) + if pattern.search(existing): + updated = pattern.sub(setting, existing) + else: + if existing and not existing.endswith(("\n", "\r")): + existing += "\n" + updated = existing + setting + "\n" + + with open(staged, "w", encoding="utf-8", newline="") as handle: + handle.write(updated) + os.replace(staged, config_path) + + except PermissionError as exc: + raise RuntimeError( + "Windows denied access to WTF\\Config.wtf while synchronizing " + "the SuperWoW FoV. Close WoW and any program using the file, " + "then try again." + ) from exc + finally: + if os.path.exists(staged): + try: + os.remove(staged) + except OSError: + pass + if restore_readonly and original_mode is not None and os.path.exists(config_path): + try: + os.chmod(config_path, original_mode) + except OSError: + pass + + def configure_script_memory(self, target): + """Apply base Config.wtf settings, then synchronize SuperWoW FoV.""" + super().configure_script_memory(target) + self._configure_superwow_fov_cvar(target) + def run_installation(self): """Run the EXE patch transaction before the installer's first file write. -- 2.52.0 From a3f320f55a6237397b50d5a295a9e239dbc62332 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 5 Sep 2026 11:02:51 +0200 Subject: [PATCH 2/2] Test SuperWoW FoV synchronization --- tests/test_superwow_fov_sync.py | 105 ++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_superwow_fov_sync.py diff --git a/tests/test_superwow_fov_sync.py b/tests/test_superwow_fov_sync.py new file mode 100644 index 0000000..3c6a6eb --- /dev/null +++ b/tests/test_superwow_fov_sync.py @@ -0,0 +1,105 @@ +import os +import stat +import tempfile +import unittest + +import setup_tool_dynamic as dynamic + + +class FakeVar: + def __init__(self, value): + self.value = value + + def get(self): + return self.value + + +class SuperWowFovSyncTests(unittest.TestCase): + def _tool(self, *, superwow=True, fov=1.9199, script_memory=False): + tool = object.__new__(dynamic.ModernWowSetupTool) + tool.core_plugins = { + "SuperWoWhook.dll": FakeVar(superwow), + } + tool.vt_fov = FakeVar(fov) + tool.vt_script_memory = FakeVar(script_memory) + return tool + + @staticmethod + def _config_path(root): + return os.path.join(root, "WTF", "Config.wtf") + + def test_superwow_enabled_updates_existing_fov(self): + with tempfile.TemporaryDirectory() as root: + path = self._config_path(root) + os.makedirs(os.path.dirname(path)) + with open(path, "w", encoding="utf-8") as handle: + handle.write( + 'SET gxWindow "1"\n' + 'SET FoV "1.5"\n' + 'SET locale "enUS"\n' + ) + + self._tool().configure_script_memory(root) + + with open(path, "r", encoding="utf-8") as handle: + result = handle.read() + self.assertIn('SET FoV "1.9199"', result) + self.assertIn('SET gxWindow "1"', result) + self.assertIn('SET locale "enUS"', result) + self.assertNotIn('SET FoV "1.5"', result) + + def test_superwow_enabled_creates_missing_fov_setting(self): + with tempfile.TemporaryDirectory() as root: + path = self._config_path(root) + + self._tool(fov=2.1).configure_script_memory(root) + + with open(path, "r", encoding="utf-8") as handle: + result = handle.read() + self.assertEqual(result, 'SET FoV "2.1"\n') + + def test_superwow_disabled_leaves_existing_fov_untouched(self): + with tempfile.TemporaryDirectory() as root: + path = self._config_path(root) + os.makedirs(os.path.dirname(path)) + original = 'SET FoV "1.5"\nSET locale "frFR"\n' + with open(path, "w", encoding="utf-8") as handle: + handle.write(original) + + self._tool(superwow=False).configure_script_memory(root) + + with open(path, "r", encoding="utf-8") as handle: + self.assertEqual(handle.read(), original) + + def test_script_memory_and_superwow_fov_are_both_applied(self): + with tempfile.TemporaryDirectory() as root: + path = self._config_path(root) + os.makedirs(os.path.dirname(path)) + with open(path, "w", encoding="utf-8") as handle: + handle.write('SET locale "enUS"\n') + + self._tool(script_memory=True).configure_script_memory(root) + + with open(path, "r", encoding="utf-8") as handle: + result = handle.read() + self.assertIn('SET scriptMemory "0"', result) + self.assertIn('SET FoV "1.9199"', result) + + def test_readonly_config_mode_is_restored(self): + with tempfile.TemporaryDirectory() as root: + path = self._config_path(root) + os.makedirs(os.path.dirname(path)) + with open(path, "w", encoding="utf-8") as handle: + handle.write('SET FoV "1.5"\n') + os.chmod(path, stat.S_IREAD) + + self._tool().configure_script_memory(root) + + mode = os.stat(path).st_mode + self.assertFalse(mode & stat.S_IWRITE) + with open(path, "r", encoding="utf-8") as handle: + self.assertIn('SET FoV "1.9199"', handle.read()) + + +if __name__ == "__main__": + unittest.main() -- 2.52.0