Synchronize Farclip runtime value with the Tool #15

Merged
Dusk-92 merged 10 commits from test/farclip-config-sync into main 2026-09-06 19:22:45 +00:00
7 changed files with 266 additions and 18 deletions
+1
View File
@@ -10,6 +10,7 @@ This update focuses on **better compatibility, safer WoW.exe patching, improved
- Improved support for already-patched clients while preserving client-specific loader code.
- Removed overly strict build/version checks that could reject compatible clients.
- SuperWoW now uses the same FoV selected by the Modernization Tool by synchronizing the `FoV` CVar in `WTF/Config.wtf`.
- Render Distance now keeps the executable Farclip ceiling fixed at **3000** while synchronizing the selected runtime value to `WTF/Config.wtf`, preventing stale higher Farclip values from conflicting with `WoW_Modernized.exe`.
## 🎨 MPQ & Recovery
+2 -2
View File
@@ -111,7 +111,7 @@ class WowSetupTool:
# Tweaks Tab
"fov": "Calculates horizontal Field of View mathematically scaled to maintain vertical aspect space based on your screen ratio.",
"farclip": "Increases the maximum terrain render distance. Vanilla default is 777. Tweaks default is 1500.",
"farclip": "Sets the active terrain render distance used by the game. Vanilla default is 777. The Tool keeps the executable Farclip ceiling fixed at 3000.",
"frill": "Changes the ground clutter (grass) render distance. Vanilla default is 70. Tweaks default is 300.",
"nameplate": "Increases the distance at which enemy nameplates become visible. Vanilla default is 20. Tweaks default is 41.",
"cam": "Increases the maximum camera zoom-out distance. Vanilla default is 50. Max safe limit is 100.",
@@ -749,7 +749,7 @@ class WowSetupTool:
frame_nums.pack(fill='x', padx=15, pady=0)
frame_nums.columnconfigure(1, weight=1)
self.create_slider_row(frame_nums, 0, "Render distance (Farclip) [Safe Max: 1500]:", self.vt_farclip, 777, 1500, 10000, "farclip")
self.create_slider_row(frame_nums, 0, "Render distance (Farclip) [Safe Max: 1500]:", self.vt_farclip, 777, 1500, 3000, "farclip")
self.create_slider_row(frame_nums, 1, "Ground clutter (Frilldistance) [Safe Max: 300]:", self.vt_frill, 70, 300, 1000, "frill")
self.create_slider_row(frame_nums, 2, "Nameplate range [Safe Max: 41]:", self.vt_nameplate, 20, 41, 150, "nameplate")
self.create_slider_row(frame_nums, 3, "Camera distance [Safe Max: 100]:", self.vt_maxcam, 50, 100, 250, "cam")
+105 -7
View File
@@ -80,7 +80,7 @@ _CUSTOM_GLUES_SITES = (
# Numeric fields may already contain legitimate Turtle/Octo/community values.
# Preflight only sanity-checks those fixed-offset fields; normalization later
# overwrites them with the exact values selected in the Tool.
# applies the exact Tool policy without fingerprinting community numeric values.
_NUMERIC_SITES = (
("fov", 0x4089B4, "FoV", 0.5, 3.5),
("farclip", 0x40FED8, "Farclip", 777.0, 10000.0),
@@ -93,6 +93,7 @@ _CLIENT_BUILD_OFFSET = 0x437BFC
_CLIENT_VERSION_OFFSET = 0x437C04
_CLIENT_BUILD = b"5875"
_CLIENT_VERSION = b"1.12.1"
_FARCLIP_EXE_CEILING = 3000.0
def _strict_verify_mpq(path):
@@ -286,16 +287,20 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
def _vanilla_tweaks_signature(self):
signature = super()._vanilla_tweaks_signature()
# Keep the B-total policy generation stable for existing test builds.
# Fixed build/version string anchors are no longer part of preflight.
signature["selected_patch_normalization"] = 3
# Runtime Farclip lives in Config.wtf; the executable ceiling is fixed.
# Keep the executable signature stable when only Render Distance changes.
signature["farclip"] = int(_FARCLIP_EXE_CEILING)
# Bump when normalization changes so existing managed installs get one
# clean WoW_Modernized.exe rebuild under the new policy.
signature["selected_patch_normalization"] = 4
signature["source_fingerprint_policy"] = 1
return signature
def _desired_normalized_values(self):
selected_farclip = float(self.vt_farclip.get())
desired = {
"fov": float(self.vt_fov.get()),
"farclip": float(self.vt_farclip.get()),
"farclip": selected_farclip,
"frill": float(self.vt_frill.get()),
"nameplate": float(self.vt_nameplate.get()),
"maxcam": float(self.vt_maxcam.get()),
@@ -304,7 +309,7 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
ranges = (
("FoV", desired["fov"], 0.5, 3.5),
("Farclip", desired["farclip"], 100.0, 50000.0),
("Farclip", selected_farclip, 100.0, _FARCLIP_EXE_CEILING),
("Frill Distance", desired["frill"], 0.0, 10000.0),
("Nameplate Distance", desired["nameplate"], 1.0, 500.0),
("Max Camera Distance", desired["maxcam"], 1.0, 1000.0),
@@ -321,8 +326,36 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
"Sound Channels value is outside the supported WoW 1.12.1 range."
)
desired["sound_bytes"] = sound_channels.ljust(4, b"\x00")
# The EXE field is the maximum allowed Farclip, not the active distance.
# Keep it fixed at 3000. The selected runtime value is synchronized to
# Config.wtf separately and may never exceed this ceiling.
desired["farclip"] = _FARCLIP_EXE_CEILING
return desired
def validate_limits(self):
if not super().validate_limits():
return False
try:
farclip = float(self.vt_farclip.get())
except (tk.TclError, TypeError, ValueError):
messagebox.showerror(
"Input Error",
"Render Distance (Farclip) must contain a valid number.",
)
return False
if not math.isfinite(farclip) or not 100.0 <= farclip <= _FARCLIP_EXE_CEILING:
messagebox.showerror(
"Limit Exceeded",
"Render distance (Farclip) must stay between 100 and 3000. "
"The 3000 hard maximum also applies when Safety Limits are disabled.",
)
return False
return True
@staticmethod
def _validate_client_identity(data):
"""Compatibility no-op: Turtle/Octo may move build/version strings."""
@@ -554,10 +587,75 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
except OSError:
pass
def _configure_farclip_cvar(self, target):
"""Synchronize the Tool-selected runtime Farclip to WTF/Config.wtf."""
try:
farclip = int(self.vt_farclip.get())
except (tk.TclError, TypeError, ValueError) as exc:
raise RuntimeError("Render Distance (Farclip) is not a valid number.") from exc
if not 100 <= farclip <= int(_FARCLIP_EXE_CEILING):
raise RuntimeError(
"Render Distance (Farclip) must stay between 100 and 3000."
)
wtf_dir = os.path.join(target, "WTF")
config_path = os.path.join(wtf_dir, "Config.wtf")
staged = config_path + ".modernization-farclip"
original_mode = None
restore_readonly = False
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 farclip "{farclip}"'
pattern = re.compile(
r'^\s*SET\s+farclip\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 OSError as exc:
raise RuntimeError(
"Could not synchronize Render Distance in WTF\\Config.wtf. "
"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."""
"""Apply base Config.wtf settings, then synchronize FoV and Farclip."""
super().configure_script_memory(target)
self._configure_superwow_fov_cvar(target)
self._configure_farclip_cvar(target)
def run_installation(self):
"""Run the EXE patch transaction before the installer's first file write.
+137
View File
@@ -0,0 +1,137 @@
import os
import tempfile
import unittest
from unittest import mock
import setup_tool_dynamic
class _Var:
def __init__(self, value):
self.value = value
def get(self):
return self.value
class FarclipConfigSyncTests(unittest.TestCase):
def _tool(self, farclip=777):
tool = object.__new__(setup_tool_dynamic.ModernWowSetupTool)
tool.vt_farclip = _Var(farclip)
return tool
@staticmethod
def _add_numeric_vars(tool):
tool.vt_fov = _Var(1.9199)
tool.vt_frill = _Var(300)
tool.vt_nameplate = _Var(41)
tool.vt_maxcam = _Var(100)
tool.vt_soundchan = _Var(64)
def test_executable_farclip_ceiling_is_always_3000(self):
tool = self._tool(777)
self._add_numeric_vars(tool)
desired = tool._desired_normalized_values()
self.assertEqual(desired["farclip"], 3000.0)
tool.vt_farclip = _Var(3000)
desired = tool._desired_normalized_values()
self.assertEqual(desired["farclip"], 3000.0)
def test_farclip_above_3000_is_rejected_even_with_direct_internal_call(self):
tool = self._tool(5000)
self._add_numeric_vars(tool)
with self.assertRaisesRegex(RuntimeError, "Farclip"):
tool._desired_normalized_values()
def test_normalization_signature_bump_forces_existing_install_refresh(self):
tool = self._tool(777)
self._add_numeric_vars(tool)
tool.vt_quickloot = _Var(True)
tool.vt_bg_sound = _Var(True)
tool.vt_laa = _Var(True)
tool.vt_cam_fix = _Var(True)
tool.vt_crossfaction_res = _Var(False)
tool.vt_custom_glues = _Var(True)
tool.vt_bluemoon = _Var(False)
signature = tool._vanilla_tweaks_signature()
self.assertEqual(signature["selected_patch_normalization"], 4)
self.assertEqual(signature["farclip"], 3000)
tool.vt_farclip = _Var(1500)
runtime_changed = tool._vanilla_tweaks_signature()
self.assertEqual(runtime_changed["farclip"], 3000)
self.assertEqual(signature, runtime_changed)
def test_existing_farclip_cvar_is_replaced_without_touching_other_settings(self):
tool = self._tool(777)
with tempfile.TemporaryDirectory() as root:
wtf_dir = os.path.join(root, "WTF")
os.makedirs(wtf_dir)
config_path = os.path.join(wtf_dir, "Config.wtf")
with open(config_path, "w", encoding="utf-8") as handle:
handle.write('SET locale "enUS"\nSET farclip "2100"\nSET gxWindow "1"\n')
tool._configure_farclip_cvar(root)
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET locale "enUS"', result)
self.assertIn('SET gxWindow "1"', result)
self.assertIn('SET farclip "777"', result)
self.assertNotIn('SET farclip "2100"', result)
def test_missing_farclip_cvar_is_appended(self):
tool = self._tool(1500)
with tempfile.TemporaryDirectory() as root:
wtf_dir = os.path.join(root, "WTF")
os.makedirs(wtf_dir)
config_path = os.path.join(wtf_dir, "Config.wtf")
with open(config_path, "w", encoding="utf-8") as handle:
handle.write('SET locale "enUS"')
tool._configure_farclip_cvar(root)
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(
result,
'SET locale "enUS"\nSET farclip "1500"\n',
)
def test_missing_config_wtf_is_created(self):
tool = self._tool(1000)
with tempfile.TemporaryDirectory() as root:
tool._configure_farclip_cvar(root)
config_path = os.path.join(root, "WTF", "Config.wtf")
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(result, 'SET farclip "1000"\n')
def test_farclip_cvar_rejects_value_above_fixed_ceiling(self):
tool = self._tool(3001)
with tempfile.TemporaryDirectory() as root:
with self.assertRaisesRegex(RuntimeError, "3000"):
tool._configure_farclip_cvar(root)
self.assertFalse(os.path.exists(os.path.join(root, "WTF", "Config.wtf")))
def test_configure_script_memory_also_synchronizes_farclip(self):
tool = self._tool(777)
tool.core_plugins = {"SuperWoWhook.dll": _Var(False)}
with mock.patch.object(
setup_tool_dynamic._ModernWowSetupToolCore,
"configure_script_memory",
) as parent_configure, mock.patch.object(
tool,
"_configure_farclip_cvar",
) as sync_farclip:
tool.configure_script_memory("C:/WoW")
parent_configure.assert_called_once_with("C:/WoW")
sync_farclip.assert_called_once_with("C:/WoW")
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -220,7 +220,7 @@ class VanillaTweaksSourceValidationTests(unittest.TestCase):
1.9199,
places=4,
)
self.assertEqual(struct.unpack_from("<f", result, 0x40FED8)[0], 1500.0)
self.assertEqual(struct.unpack_from("<f", result, 0x40FED8)[0], 3000.0)
self.assertEqual(struct.unpack_from("<f", result, 0x467958)[0], 300.0)
self.assertEqual(struct.unpack_from("<f", result, 0x40C448)[0], 41.0)
self.assertEqual(struct.unpack_from("<f", result, 0x4089A4)[0], 100.0)
+16 -4
View File
@@ -15,12 +15,13 @@ class FakeVar:
class SuperWowFovSyncTests(unittest.TestCase):
def _tool(self, *, superwow=True, fov=1.9199, script_memory=False):
def _tool(self, *, superwow=True, fov=1.9199, farclip=777, script_memory=False):
tool = object.__new__(dynamic.ModernWowSetupTool)
tool.core_plugins = {
"SuperWoWhook.dll": FakeVar(superwow),
}
tool.vt_fov = FakeVar(fov)
tool.vt_farclip = FakeVar(farclip)
tool.vt_script_memory = FakeVar(script_memory)
return tool
@@ -44,6 +45,7 @@ class SuperWowFovSyncTests(unittest.TestCase):
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
self.assertIn('SET gxWindow "1"', result)
self.assertIn('SET locale "enUS"', result)
self.assertNotIn('SET FoV "1.5"', result)
@@ -56,7 +58,10 @@ class SuperWowFovSyncTests(unittest.TestCase):
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(result, 'SET FoV "2.1"\n')
self.assertEqual(
result,
'SET FoV "2.1"\nSET farclip "777"\n',
)
def test_superwow_disabled_leaves_existing_fov_untouched(self):
with tempfile.TemporaryDirectory() as root:
@@ -69,7 +74,11 @@ class SuperWowFovSyncTests(unittest.TestCase):
self._tool(superwow=False).configure_script_memory(root)
with open(path, "r", encoding="utf-8") as handle:
self.assertEqual(handle.read(), original)
result = handle.read()
self.assertIn('SET FoV "1.5"', result)
self.assertIn('SET locale "frFR"', result)
self.assertIn('SET farclip "777"', result)
self.assertNotIn('SET FoV "1.9199"', result)
def test_script_memory_and_superwow_fov_are_both_applied(self):
with tempfile.TemporaryDirectory() as root:
@@ -84,6 +93,7 @@ class SuperWowFovSyncTests(unittest.TestCase):
result = handle.read()
self.assertIn('SET scriptMemory "0"', result)
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
def test_readonly_config_mode_is_restored(self):
with tempfile.TemporaryDirectory() as root:
@@ -98,7 +108,9 @@ class SuperWowFovSyncTests(unittest.TestCase):
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())
result = handle.read()
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
if __name__ == "__main__":
+4 -4
View File
@@ -193,7 +193,7 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
self.assertEqual(result[0x3A4869], 0x14)
self.assertEqual(result[0x126:0x128], b"\x0F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.5708, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 777.0)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 70.0)
self.assertEqual(self._read_float(result, 0x40C448), 20.0)
self.assertEqual(self._read_float(result, 0x4089A4), 50.0)
@@ -237,7 +237,7 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
self.assertEqual(result[0x3A4869], 0x27)
self.assertEqual(result[0x126:0x128], b"\x2F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.9199, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 1500.0)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 300.0)
self.assertEqual(self._read_float(result, 0x40C448), 41.0)
self.assertEqual(self._read_float(result, 0x4089A4), 100.0)
@@ -581,7 +581,7 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
self.assertEqual(result[0x3A4869], 0x14)
self.assertEqual(result[0x126:0x128], b"\x0F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.5708, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 777.0)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 70.0)
self.assertEqual(self._read_float(result, 0x40C448), 20.0)
self.assertEqual(self._read_float(result, 0x4089A4), 50.0)
@@ -627,7 +627,7 @@ class VanillaTweaksNormalizationTests(unittest.TestCase):
new_signature = tool._vanilla_tweaks_signature()
self.assertNotIn("selected_patch_normalization", old_signature)
self.assertEqual(new_signature["selected_patch_normalization"], 3)
self.assertEqual(new_signature["selected_patch_normalization"], 4)
tool.core_plugins["SuperWoWhook.dll"].set(False)
self.assertEqual(new_signature, tool._vanilla_tweaks_signature())