Guest User

Untitled

a guest
Jul 9th, 2026
581
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.22 KB | None | 0 0
  1. """
  2. OctoWoW Updater
  3.  
  4. Copyright (c) 2026 rebazed
  5.  
  6. This project is open for personal use, modification, and redistribution.
  7.  
  8. You must retain credit to the original author in any distributed or modified versions.
  9.  
  10. The software is provided "as is", without warranty of any kind.
  11. """
  12.  
  13. import json
  14. import hashlib
  15. import os
  16. import urllib.request
  17. import shutil
  18. import struct
  19. import time
  20. import math
  21. from pathlib import Path
  22.  
  23. UPDATER_VERSION = "1.1"
  24. SERVER = "https://octowow.st"
  25. DOWNLOAD_VERSION = "latest"
  26. OUT_DIR = "OctoWoW"
  27. UA = f"OctoWoWUpdater/{UPDATER_VERSION}"
  28. DOWNLOAD_RETRY_COUNT = 3
  29.  
  30.  
  31. def ensure_dir(path):
  32. Path(path).mkdir(parents=True, exist_ok=True)
  33.  
  34.  
  35. def sha1(path):
  36. h = hashlib.sha1()
  37. with open(path, "rb") as f:
  38. for chunk in iter(lambda: f.read(65536), b""):
  39. h.update(chunk)
  40. return h.hexdigest().upper()
  41.  
  42.  
  43. def safe_remove(path):
  44. if os.path.exists(path):
  45. print(f" Removing: {path}")
  46. os.remove(path)
  47.  
  48.  
  49. def download(url, dest, size):
  50. os.makedirs(os.path.dirname(dest), exist_ok=True)
  51.  
  52. tmp = dest + ".tmp"
  53.  
  54. for attempt in range(1, DOWNLOAD_RETRY_COUNT + 1):
  55. try:
  56. print(f" Downloading ({size // 1024 // 1024} MB)...")
  57. req = urllib.request.Request(url, headers={"User-Agent": UA})
  58. with urllib.request.urlopen(req) as r:
  59. with open(tmp, "wb") as f:
  60. downloaded = 0
  61. while True:
  62. chunk_size = 1024 * 1024 # 1 MB
  63. chunk = r.read(chunk_size)
  64. if not chunk:
  65. break
  66. f.write(chunk)
  67. downloaded += len(chunk)
  68. print(
  69. f"\r {downloaded // 1024 // 1024} / {size // 1024 // 1024} MB",
  70. end="",
  71. flush=True,
  72. )
  73. print()
  74.  
  75. # atomic replace
  76. shutil.move(tmp, dest)
  77. return
  78.  
  79. except Exception as e:
  80. print(f" Download failed: {e}")
  81. if os.path.exists(tmp):
  82. os.remove(tmp)
  83. time.sleep(1)
  84.  
  85. raise Exception(f"Failed to download after {DOWNLOAD_RETRY_COUNT} attempts: {url}")
  86.  
  87.  
  88. def already_updated(dest, expected_hash):
  89. if not os.path.exists(dest):
  90. return False
  91. try:
  92. return sha1(dest) == expected_hash
  93. except:
  94. return False
  95.  
  96.  
  97. def traverse(node, path_parts):
  98. t = node["type"]
  99. name = node["name"]
  100. current = path_parts + [name]
  101.  
  102. rel_path = os.path.join(*current)
  103. rel_url = "/".join(current)
  104. dest = os.path.join(OUT_DIR, rel_path)
  105. url = f"{SERVER}/client/{DOWNLOAD_VERSION}/{rel_url}"
  106.  
  107. if t == "dir":
  108. for child in node.get("files", []):
  109. traverse(child, current)
  110.  
  111. elif t == "file":
  112. print(f"[file] {rel_path}")
  113.  
  114. if already_updated(dest, node["hash"]):
  115. print(" Already up to date.")
  116. return
  117.  
  118. download(url, dest, node["size"])
  119.  
  120. # verify after download
  121. if sha1(dest) != node["hash"]:
  122. print(" Hash mismatch -> re-downloading")
  123. safe_remove(dest)
  124. download(url, dest, node["size"])
  125.  
  126. elif t == "mpq":
  127. mpq_name = name + ".mpq"
  128. current_mpq = path_parts + [mpq_name]
  129.  
  130. rel_path = os.path.join(*current_mpq)
  131. dest = os.path.join(OUT_DIR, rel_path)
  132. url = f"{SERVER}/client/{DOWNLOAD_VERSION}/{'/'.join(current_mpq)}"
  133.  
  134. print(f"[mpq] {rel_path}")
  135.  
  136. if already_updated(dest, node["hash"]):
  137. print(" Already up to date.")
  138. return
  139.  
  140. download(url, dest, node["size"])
  141.  
  142. if sha1(dest) != node["hash"]:
  143. print(" Hash mismatch -> re-downloading")
  144. safe_remove(dest)
  145. download(url, dest, node["size"])
  146.  
  147. elif t == "del":
  148. print(f"[del] {rel_path}")
  149. safe_remove(dest)
  150.  
  151.  
  152. # TODO: rewrite this
  153. def build_tweaks(buf):
  154. DEFAULT_FOV_DEGREES = 90 # for 16:9 screen ratio
  155. fov_radians = DEFAULT_FOV_DEGREES * (math.pi / 180.0)
  156.  
  157. # largeAddress: set IMAGE_FILE_LARGE_ADDRESS_AWARE flag in PE header
  158. current_flags = struct.unpack_from("<H", buf, 0x126)[0]
  159. large_address_value = current_flags | 0x20
  160.  
  161. # fmt: off
  162. return [
  163. # (label, type, offset, value)
  164. ("largeAddress", "uint16", 0x126, large_address_value),
  165. ("fieldOfView", "float", 0x4089b4, fov_radians),
  166. ("cameraDistance", "float", 0x4089a4, 50.0), # WoW default
  167. ("farClip", "float", 0x40fed8, 777.0), # WoW default
  168. ("frillDistance", "float", 0x467958, 70.0), # WoW default
  169. ("nameplateRange", "float", 0x40c448, 20.0), # WoW default
  170. ("soundInBackground", "int8", 0x3a4869, 0x27), # enabled (0x14 = disabled)
  171. ("alwaysAutoLoot", "bytes", None, [
  172. (0x0c1ecf, bytes([0x75])),
  173. (0x0c2b25, bytes([0x75])),
  174. ]),
  175. ("crossFactionResurrect", "bytes", None, [
  176. (0x006e5fb8, bytes([0x006e5fb9 & 0xff])),
  177. (0x006e62a8, bytes([0x006e62a9 & 0xff])),
  178. ]),
  179. ("cameraSkipFix", "bytes", None, [
  180. (0x02ccd0, bytes([
  181. 0x55, 0x8b, 0x05, 0x48, 0x4e, 0x88, 0x00, 0x8b, 0x0d, 0x44, 0x4e, 0x88, 0x00, 0xe9, 0x33, 0x90,
  182. 0x32, 0x00, 0x83, 0xc0, 0x32, 0x83, 0xc1, 0x32, 0x3b, 0x0d, 0xa8, 0xeb, 0xc4, 0x00, 0x7e, 0x03,
  183. 0x83, 0xe9, 0x01, 0x3b, 0x05, 0xac, 0xeb, 0xc4, 0x00, 0x7e, 0x03, 0x83, 0xe8, 0x01, 0x83, 0xe9,
  184. 0x32, 0x83, 0xe8, 0x32, 0x89, 0x05, 0x48, 0x4e, 0x88, 0x00, 0x89, 0x0d, 0x44, 0x4e, 0x88, 0x00,
  185. 0x5d, 0xeb, 0x0d,
  186. ])),
  187. (0x02d326, bytes([0xe9, 0xb1, 0x8a, 0x32, 0x00])),
  188. (0x02d334, bytes([0x8b, 0x35, 0x48, 0x4e, 0x88, 0x00])),
  189. (0x355d15, bytes([
  190. 0x83, 0xf8, 0x32, 0x7d, 0x03, 0x83, 0xc0, 0x01, 0x83, 0xf9, 0x32,
  191. 0x7d, 0x03, 0x83, 0xc1, 0x01, 0xe9, 0xb8, 0x6f, 0xcd, 0xff,
  192. ])),
  193. (0x355ddc, bytes([
  194. 0x8d, 0x4d, 0xf0, 0x51,
  195. 0xff, 0x35, 0x00, 0x4e, 0x88, 0x00, 0xff, 0x15, 0x50, 0xf6, 0x7f, 0x00, 0x8b, 0x45, 0xf0, 0x8b,
  196. 0x15, 0x44, 0x4e, 0x88, 0x00, 0xe9, 0x35, 0x75, 0xcd, 0xff,
  197. ])),
  198. ]),
  199. ("skillUiGateHijack", "bytes", None, [
  200. (0x002ddf90, bytes([
  201. 0x55, 0x8b, 0xec, 0x83, 0xec, 0x08, 0x53, 0x56,
  202. 0x57, 0x8b, 0x3d, 0x60, 0xab, 0xce, 0x00, 0x83,
  203. 0xff, 0xff, 0x89, 0x55, 0xfc, 0x89, 0x4d, 0xf8,
  204. 0x74, 0x79, 0x8b, 0x75, 0x08, 0x8b, 0x15, 0x58,
  205. 0xab, 0xce, 0x00, 0x8b, 0xc7, 0x23, 0xc6, 0x8d,
  206. 0x04, 0x40, 0x8b, 0x4c, 0x82, 0x08, 0xf6, 0xc1,
  207. 0x01, 0x8d, 0x44, 0x82, 0x04, 0x75, 0x04, 0x85,
  208. 0xc9, 0x75, 0x05, 0x33, 0xc9, 0x8d, 0x49, 0x00,
  209. 0xf6, 0xc1, 0x01, 0x75, 0x4e, 0x85, 0xc9, 0x74,
  210. 0x4a, 0x39, 0x31, 0x74, 0x13, 0x8b, 0xc7, 0x23,
  211. 0xc6, 0x8d, 0x04, 0x40, 0x8d, 0x04, 0x82, 0x8b,
  212. 0x00, 0x03, 0xc1, 0x8b, 0x48, 0x04, 0xeb, 0xe0,
  213. 0x8b, 0x59, 0x1c, 0x8b, 0x71, 0x18, 0x33, 0xff,
  214. 0x85, 0xdb, 0x7e, 0x27, 0x8d, 0x64, 0x24, 0x00,
  215. 0x8b, 0x4e, 0x0c, 0x8b, 0x56, 0x08, 0x6a, 0x00,
  216. 0x6a, 0x00, 0x51, 0x8b, 0x4d, 0xf8, 0x52, 0x8b,
  217. 0x55, 0xfc, 0xe8, 0xb9, 0xfd, 0xff, 0xff, 0x84,
  218. 0xc0, 0x75, 0x13, 0x47, 0x83, 0xc6, 0x20, 0x3b,
  219. 0xfb, 0x7c, 0xdd, 0x5f, 0x5e, 0x33, 0xc0, 0x5b,
  220. 0x8b, 0xe5, 0x5d, 0xc2, 0x04, 0x00, 0x5f, 0x8b,
  221. 0xc6, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc2, 0x04,
  222. 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
  223. ])),
  224. ]),
  225. ]
  226. # fmt: on
  227.  
  228.  
  229. def patch_exe():
  230. exe_path = os.path.join(OUT_DIR, "WoW.exe")
  231. if not os.path.exists(exe_path):
  232. raise Exception(f"{exe_path} is not found\n")
  233.  
  234. print("\nApplying tweaks to WoW.exe...\n")
  235.  
  236. with open(exe_path, "rb") as f:
  237. buf = bytearray(f.read())
  238.  
  239. for label, kind, offset, value in build_tweaks(buf):
  240. print(f" Applying: {label}")
  241. if kind == "float":
  242. struct.pack_into("<f", buf, offset, value)
  243. elif kind == "int8":
  244. struct.pack_into("<b", buf, offset, value)
  245. elif kind == "uint16":
  246. struct.pack_into("<H", buf, offset, value)
  247. elif kind == "bytes":
  248. for off, data in value:
  249. buf[off : off + len(data)] = data
  250.  
  251. with open(exe_path, "wb") as f:
  252. f.write(buf)
  253.  
  254. print("\nWoW.exe patched successfully!")
  255.  
  256.  
  257. def get_display_info():
  258. import ctypes
  259.  
  260. ENUM_CURRENT_SETTINGS = -1
  261.  
  262. class DEVMODE(ctypes.Structure):
  263. _fields_ = [
  264. ("dmDeviceName", ctypes.c_wchar * 32),
  265. ("dmSpecVersion", ctypes.c_ushort),
  266. ("dmDriverVersion", ctypes.c_ushort),
  267. ("dmSize", ctypes.c_ushort),
  268. ("dmDriverExtra", ctypes.c_ushort),
  269. ("dmFields", ctypes.c_ulong),
  270. ("dmPositionX", ctypes.c_long),
  271. ("dmPositionY", ctypes.c_long),
  272. ("dmDisplayOrientation", ctypes.c_ulong),
  273. ("dmDisplayFixedOutput", ctypes.c_ulong),
  274. ("dmColor", ctypes.c_short),
  275. ("dmDuplex", ctypes.c_short),
  276. ("dmYResolution", ctypes.c_short),
  277. ("dmTTOption", ctypes.c_short),
  278. ("dmCollate", ctypes.c_short),
  279. ("dmFormName", ctypes.c_wchar * 32),
  280. ("dmLogPixels", ctypes.c_ushort),
  281. ("dmBitsPerPel", ctypes.c_ulong),
  282. ("dmPelsWidth", ctypes.c_ulong),
  283. ("dmPelsHeight", ctypes.c_ulong),
  284. ("dmDisplayFlags", ctypes.c_ulong),
  285. ("dmDisplayFrequency", ctypes.c_ulong),
  286. ]
  287.  
  288. devmode = DEVMODE()
  289. devmode.dmSize = ctypes.sizeof(DEVMODE)
  290.  
  291. ctypes.windll.user32.EnumDisplaySettingsW(
  292. None, ENUM_CURRENT_SETTINGS, ctypes.byref(devmode)
  293. )
  294.  
  295. return {
  296. "width": devmode.dmPelsWidth,
  297. "height": devmode.dmPelsHeight,
  298. "refresh_rate": devmode.dmDisplayFrequency,
  299. }
  300.  
  301.  
  302. def patch_config():
  303. config_dir = os.path.join(OUT_DIR, "WTF")
  304. ensure_dir(config_dir)
  305.  
  306. print("\nApplying Config.wtf variables...")
  307.  
  308. config_path = os.path.join(config_dir, "Config.wtf")
  309. if os.path.exists(config_path):
  310. print("\nConfig.wtf already exists, skipping.")
  311. return
  312.  
  313. display_info = get_display_info()
  314. server_name = "octowow.st"
  315.  
  316. variables = {
  317. "realmList": server_name,
  318. "patchList": server_name,
  319. "scriptMemory": 512000,
  320. "gxResolution": f"{display_info['width']}x{display_info['height']}",
  321. "gxVSync": 0,
  322. "gxColorBits": 24,
  323. "gxDepthBits": 24,
  324. "gxRefresh": display_info["refresh_rate"],
  325. "gxMultisampleQuality": 0,
  326. "gxMultisample": 2,
  327. "gxWindow": 1,
  328. "gxMaximize": 1,
  329. "anisotropic": 16,
  330. "trilinear": 1,
  331. "specular": 1,
  332. "shadowLevel": 0,
  333. "lod": 0,
  334. "hwDetect": 0,
  335. "pixelShaders": 1,
  336. "M2UsePixelShaders": 1,
  337. "particleDensity": 1,
  338. "unitDrawDist": 300,
  339. "weatherDensity": 3,
  340. "movieSubtitle": 1,
  341. "CameraDistanceMax": 50,
  342. "farClip": 777,
  343. "frillDensity": 48,
  344. "fullAlpha": 1,
  345. "SmallCull": 0.01,
  346. "DistCull": 888.8,
  347. "minimapZoom": 0,
  348. "minimapInsideZoom": 0,
  349. "SoundZoneMusicNoDelay": 1,
  350. "SoundMaxHardwareChannels": 64,
  351. "SoundSoftwareChannels": 64,
  352. "readTOS": 1,
  353. "readEULA": 1,
  354. }
  355.  
  356. with open(config_path, "w", encoding="utf-8") as f:
  357. for k, v in variables.items():
  358. if v is not None:
  359. f.write(f'SET {k} "{v}"\n')
  360.  
  361. print("\nConfig.wtf variables applied successfully!")
  362.  
  363.  
  364. def fetch_manifest():
  365. print("\nFetching manifest.json...")
  366.  
  367. req = urllib.request.Request(
  368. f"{SERVER}/api/file/{DOWNLOAD_VERSION}/manifest.json",
  369. headers={"User-Agent": UA},
  370. )
  371.  
  372. with urllib.request.urlopen(req) as r:
  373. manifest = json.load(r)
  374.  
  375. print("\nmanifest.json fetched successfully!")
  376. return manifest
  377.  
  378.  
  379. def download_client(manifest):
  380. print("\nStarting OctoWoW client update...\n")
  381.  
  382. root = manifest["root"]
  383. for child in root.get("files", []):
  384. traverse(child, [])
  385.  
  386. print("\nOctoWoW client update completed successfully!")
  387.  
  388.  
  389. def apply_patcher():
  390. patch_exe()
  391. patch_config()
  392.  
  393.  
  394. # main entry point
  395.  
  396. print(f"OctoWoW Updater {UPDATER_VERSION}")
  397. print(
  398. "\nFor questions or support, contact me on Discord: rebazed (discord.com/users/287467238573867018)"
  399. ) # please credit the original author
  400.  
  401. manifest = fetch_manifest()
  402. download_client(manifest)
  403. apply_patcher()
  404.  
  405. print("\nEverything is ready! You can close this window.")
  406. input()
  407.  
Advertisement
Add Comment
Please, Sign In to add comment