Synchronize Farclip runtime value with the Tool #15
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user