254 lines
9.8 KiB
Python
254 lines
9.8 KiB
Python
import math
|
|
import os
|
|
import struct
|
|
import tkinter as tk
|
|
|
|
# Keep the feature-branch implementation intact and layer only the executable
|
|
# normalization policy here. This makes the B-total policy easy to audit and
|
|
# keeps every unrelated remote/fallback behavior byte-for-byte unchanged.
|
|
from setup_tool_dynamic_core import * # noqa: F401,F403
|
|
from setup_tool_dynamic_core import ModernWowSetupTool as _ModernWowSetupToolCore
|
|
|
|
|
|
_CAMERA_REGIONS = (
|
|
(
|
|
0x02CCD0,
|
|
bytes.fromhex(
|
|
"55 8b ec 83 ec 10 8d 45 f0 50 33 c9 e8 4f 8f 00 "
|
|
"00 50 ff 15 64 f6 7f 00 8b 45 f8 99 2b c2 8b c8 "
|
|
"8b 45 fc 99 2b c2 d1 f8 d1 f9 50 51 89 0d 38 4e "
|
|
"88 00 a3 3c 4e 88 00 ff 15 5c f6 7f 00 8b e5 5d "
|
|
"c3 90 90"
|
|
),
|
|
bytes.fromhex(
|
|
"55 8b 05 48 4e 88 00 8b 0d 44 4e 88 00 e9 33 90 "
|
|
"32 00 83 c0 32 83 c1 32 3b 0d a8 eb c4 00 7e 03 "
|
|
"83 e9 01 3b 05 ac eb c4 00 7e 03 83 e8 01 83 e9 "
|
|
"32 83 e8 32 89 05 48 4e 88 00 89 0d 44 4e 88 00 "
|
|
"5d eb 0d"
|
|
),
|
|
),
|
|
(
|
|
0x02D326,
|
|
bytes.fromhex("8b 45 f0 8b 15"),
|
|
bytes.fromhex("e9 b1 8a 32 00"),
|
|
),
|
|
(
|
|
0x02D334,
|
|
bytes.fromhex("8b 35 3c 4e 88 00"),
|
|
bytes.fromhex("8b 35 48 4e 88 00"),
|
|
),
|
|
(
|
|
0x355D15,
|
|
bytes.fromhex(
|
|
"cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc "
|
|
"cc cc cc cc cc"
|
|
),
|
|
bytes.fromhex(
|
|
"83 f8 32 7d 03 83 c0 01 83 f9 32 7d 03 83 c1 01 "
|
|
"e9 b8 6f cd ff"
|
|
),
|
|
),
|
|
(
|
|
0x355DDC,
|
|
bytes.fromhex(
|
|
"cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc "
|
|
"cc cc cc cc cc cc cc cc cc cc cc cc cc cc"
|
|
),
|
|
bytes.fromhex(
|
|
"8d 4d f0 51 ff 35 00 4e 88 00 ff 15 50 f6 7f 00 "
|
|
"8b 45 f0 8b 15 44 4e 88 00 e9 35 75 cd ff"
|
|
),
|
|
),
|
|
)
|
|
|
|
_CUSTOM_GLUES_SITES = (
|
|
(0x2F113A, 0x5F, 0xEB),
|
|
(0x2F113B, 0x5E, 0x19),
|
|
(0x2F1158, 0x01, 0x03),
|
|
(0x2F11A7, 0x01, 0x03),
|
|
(0x2F11F0, 0x5F, 0xEB),
|
|
(0x2F11F1, 0x5E, 0xB2),
|
|
)
|
|
|
|
|
|
class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|
"""Remote-fallback tool with authoritative, fail-safe Vanilla Tweaks output."""
|
|
|
|
def _vanilla_tweaks_signature(self):
|
|
signature = super()._vanilla_tweaks_signature()
|
|
# v2 expands normalization from the original selected subset to every
|
|
# executable tweak exposed by the Tool except the two intentionally
|
|
# legacy upstream-only patches (Blue Moon and Cross-faction Res).
|
|
signature["selected_patch_normalization"] = 2
|
|
return signature
|
|
|
|
@staticmethod
|
|
def _validate_float_field(data, offset, label, minimum, maximum):
|
|
current = struct.unpack_from("<f", data, offset)[0]
|
|
if not math.isfinite(current) or not minimum <= current <= maximum:
|
|
raise RuntimeError(
|
|
f"Unexpected {label} value at 0x{offset:X}; "
|
|
"refusing to alter an unknown client."
|
|
)
|
|
|
|
def _normalize_selected_vanilla_tweaks_output(self, output_exe):
|
|
"""Make Tool-owned executable tweaks authoritative on pre-patched clients.
|
|
|
|
Blue Moon and Cross-faction Resurrection deliberately keep the previous
|
|
vanilla-tweaks behavior because their complete pristine restoration
|
|
signatures are not part of this policy.
|
|
"""
|
|
try:
|
|
with open(output_exe, "rb") as handle:
|
|
data = bytearray(handle.read())
|
|
except OSError as exc:
|
|
raise RuntimeError(
|
|
"Could not inspect WoW_Modernized.exe for Vanilla Tweaks normalization."
|
|
) from exc
|
|
|
|
required_size = 0x46795C
|
|
if len(data) < required_size:
|
|
raise RuntimeError(
|
|
"WoW_Modernized.exe is too small for Vanilla Tweaks normalization."
|
|
)
|
|
|
|
# Validate all code-patch signatures before changing anything. Scalar
|
|
# fields below are data constants, so they are validated by type/range
|
|
# rather than by one exact preset value.
|
|
quickloot_sites = (
|
|
(0x0C1ECF, 0x10),
|
|
(0x0C2B25, 0x0B),
|
|
)
|
|
for offset, displacement in quickloot_sites:
|
|
current = bytes(data[offset:offset + 2])
|
|
known = (
|
|
bytes((0x74, displacement)),
|
|
bytes((0x75, displacement)),
|
|
b"\x90\x90",
|
|
)
|
|
if current not in known:
|
|
raise RuntimeError(
|
|
f"Unexpected QuickLoot bytes at 0x{offset:X}; "
|
|
"refusing to alter an unknown client."
|
|
)
|
|
|
|
if data[0x3A4869] not in (0x14, 0x27):
|
|
raise RuntimeError(
|
|
"Unexpected Background Sound byte; refusing to alter an unknown client."
|
|
)
|
|
|
|
laa_current = bytes(data[0x126:0x128])
|
|
if laa_current not in (b"\x0F\x01", b"\x2F\x01"):
|
|
raise RuntimeError(
|
|
"Unexpected Large Address Aware bytes; refusing to alter an unknown client."
|
|
)
|
|
|
|
for offset, original, patched in _CAMERA_REGIONS:
|
|
current = bytes(data[offset:offset + len(original)])
|
|
if current not in (original, patched):
|
|
raise RuntimeError(
|
|
f"Unexpected Camera Skip Fix bytes at 0x{offset:X}; "
|
|
"refusing to alter an unknown client."
|
|
)
|
|
|
|
custom_state = tuple(data[offset] for offset, _original, _patched in _CUSTOM_GLUES_SITES)
|
|
custom_original = tuple(original for _offset, original, _patched in _CUSTOM_GLUES_SITES)
|
|
custom_patched = tuple(patched for _offset, _original, patched in _CUSTOM_GLUES_SITES)
|
|
if custom_state not in (custom_original, custom_patched):
|
|
raise RuntimeError(
|
|
"Unexpected Custom GlueXML bytes; refusing to alter an unknown client."
|
|
)
|
|
|
|
self._validate_float_field(data, 0x4089B4, "FoV", 0.5, 3.5)
|
|
self._validate_float_field(data, 0x40FED8, "Farclip", 100.0, 50000.0)
|
|
self._validate_float_field(data, 0x467958, "Frill Distance", 0.0, 10000.0)
|
|
self._validate_float_field(data, 0x40C448, "Nameplate Distance", 1.0, 500.0)
|
|
self._validate_float_field(data, 0x4089A4, "Max Camera Distance", 1.0, 1000.0)
|
|
|
|
sound_field = bytes(data[0x435D38:0x435D3C])
|
|
sound_text, separator, sound_tail = sound_field.partition(b"\x00")
|
|
if (
|
|
not separator
|
|
or not sound_text.isdigit()
|
|
or any(sound_tail)
|
|
or not 1 <= int(sound_text) <= 256
|
|
):
|
|
raise RuntimeError(
|
|
"Unexpected Sound Channels field; refusing to alter an unknown client."
|
|
)
|
|
|
|
desired_fov = float(self.vt_fov.get())
|
|
desired_farclip = float(self.vt_farclip.get())
|
|
desired_frill = float(self.vt_frill.get())
|
|
desired_nameplate = float(self.vt_nameplate.get())
|
|
desired_maxcam = float(self.vt_maxcam.get())
|
|
desired_sound = int(self.vt_soundchan.get())
|
|
|
|
desired_values = (
|
|
("FoV", desired_fov, 0.5, 3.5),
|
|
("Farclip", desired_farclip, 100.0, 50000.0),
|
|
("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),
|
|
)
|
|
for label, value, minimum, maximum in desired_values:
|
|
if not math.isfinite(value) or not minimum <= value <= maximum:
|
|
raise RuntimeError(f"{label} value is outside the supported WoW 1.12.1 range.")
|
|
|
|
sound_channels = str(desired_sound).encode("ascii") + b"\x00"
|
|
if not 1 <= desired_sound <= 256 or len(sound_channels) > 4:
|
|
raise RuntimeError("Sound Channels value is outside the supported WoW 1.12.1 range.")
|
|
|
|
# All validation passed. Apply the exact Tool selections in memory.
|
|
desired_quickloot_opcode = 0x75 if self.vt_quickloot.get() else 0x74
|
|
for offset, displacement in quickloot_sites:
|
|
data[offset:offset + 2] = bytes(
|
|
(desired_quickloot_opcode, displacement)
|
|
)
|
|
|
|
data[0x3A4869] = 0x27 if self.vt_bg_sound.get() else 0x14
|
|
data[0x126:0x128] = b"\x2F\x01" if self.vt_laa.get() else b"\x0F\x01"
|
|
|
|
desired_camera_patched = bool(self.vt_cam_fix.get())
|
|
for offset, original, patched in _CAMERA_REGIONS:
|
|
desired = patched if desired_camera_patched else original
|
|
data[offset:offset + len(desired)] = desired
|
|
|
|
desired_custom_patched = bool(self.vt_custom_glues.get())
|
|
for offset, original, patched in _CUSTOM_GLUES_SITES:
|
|
data[offset] = patched if desired_custom_patched else original
|
|
|
|
struct.pack_into("<f", data, 0x4089B4, desired_fov)
|
|
struct.pack_into("<f", data, 0x40FED8, desired_farclip)
|
|
struct.pack_into("<f", data, 0x467958, desired_frill)
|
|
struct.pack_into("<f", data, 0x40C448, desired_nameplate)
|
|
struct.pack_into("<f", data, 0x4089A4, desired_maxcam)
|
|
data[0x435D38:0x435D3C] = sound_channels.ljust(4, b"\x00")
|
|
|
|
# Blue Moon (0x3E5B83) and Cross-faction Res (0x2067DE) are intentionally
|
|
# not normalized here. vanilla-tweaks keeps exactly the previous behavior
|
|
# for those two options.
|
|
|
|
staged = output_exe + ".modernization-normalized"
|
|
try:
|
|
with open(staged, "wb") as handle:
|
|
handle.write(data)
|
|
os.replace(staged, output_exe)
|
|
except OSError as exc:
|
|
raise RuntimeError(
|
|
"Could not write the normalized WoW_Modernized.exe."
|
|
) from exc
|
|
finally:
|
|
if os.path.exists(staged):
|
|
try:
|
|
os.remove(staged)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
app = ModernWowSetupTool(root)
|
|
root.mainloop()
|