| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  | import argparse | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  | import io | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import os.path | 
					
						
							|  |  |  | import subprocess | 
					
						
							|  |  |  | import urllib.request | 
					
						
							|  |  |  | from shutil import which | 
					
						
							| 
									
										
										
										
											2024-11-27 03:28:00 +01:00
										 |  |  | from typing import Any | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  | from zipfile import ZipFile | 
					
						
							|  |  |  | from Utils import open_file | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import requests | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from Utils import is_windows, messagebox, tuplize_version | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-27 03:28:00 +01:00
										 |  |  | def ask_yes_no_cancel(title: str, text: str) -> bool | None: | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     :param title: Title to be displayed at the top of the message box. | 
					
						
							|  |  |  |     :param text: Text to be displayed inside the message box. | 
					
						
							|  |  |  |     :return: Returns True if yes, False if no, None if cancel. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     from tkinter import Tk, messagebox | 
					
						
							|  |  |  |     root = Tk() | 
					
						
							|  |  |  |     root.withdraw() | 
					
						
							|  |  |  |     ret = messagebox.askyesnocancel(title, text) | 
					
						
							|  |  |  |     root.update() | 
					
						
							|  |  |  |     return ret | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  | def launch_game(*args) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |     """Check the game installation, then launch it""" | 
					
						
							|  |  |  |     def courier_installed() -> bool: | 
					
						
							|  |  |  |         """Check if Courier is installed""" | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |         assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") | 
					
						
							|  |  |  |         with open(assembly_path, "rb") as assembly: | 
					
						
							|  |  |  |             for line in assembly: | 
					
						
							|  |  |  |                 if b"Courier" in line: | 
					
						
							|  |  |  |                     return True | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def mod_installed() -> bool: | 
					
						
							|  |  |  |         """Check if the mod is installed""" | 
					
						
							|  |  |  |         return os.path.exists(os.path.join(game_folder, "Mods", "TheMessengerRandomizerAP", "courier.toml")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def request_data(request_url: str) -> Any: | 
					
						
							|  |  |  |         """Fetches json response from given url""" | 
					
						
							|  |  |  |         logging.info(f"requesting {request_url}") | 
					
						
							|  |  |  |         response = requests.get(request_url) | 
					
						
							|  |  |  |         if response.status_code == 200:  # success | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 data = response.json() | 
					
						
							|  |  |  |             except requests.exceptions.JSONDecodeError: | 
					
						
							|  |  |  |                 raise RuntimeError(f"Unable to fetch data. (status code {response.status_code})") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise RuntimeError(f"Unable to fetch data. (status code {response.status_code})") | 
					
						
							|  |  |  |         return data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def install_courier() -> None: | 
					
						
							|  |  |  |         """Installs latest version of Courier""" | 
					
						
							|  |  |  |         # can't use latest since courier uses pre-release tags | 
					
						
							|  |  |  |         courier_url = "https://api.github.com/repos/Brokemia/Courier/releases" | 
					
						
							|  |  |  |         latest_download = request_data(courier_url)[0]["assets"][-1]["browser_download_url"] | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |         with urllib.request.urlopen(latest_download) as download: | 
					
						
							|  |  |  |             with ZipFile(io.BytesIO(download.read()), "r") as zf: | 
					
						
							|  |  |  |                 for member in zf.infolist(): | 
					
						
							|  |  |  |                     zf.extract(member, path=game_folder) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |         os.chdir(game_folder) | 
					
						
							|  |  |  |         # linux and mac handling | 
					
						
							|  |  |  |         if not is_windows: | 
					
						
							|  |  |  |             mono_exe = which("mono") | 
					
						
							|  |  |  |             if not mono_exe: | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |                 # download and use mono kickstart | 
					
						
							|  |  |  |                 # this allows steam deck support | 
					
						
							|  |  |  |                 mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip" | 
					
						
							|  |  |  |                 files = [] | 
					
						
							|  |  |  |                 with urllib.request.urlopen(mono_kick_url) as download: | 
					
						
							|  |  |  |                     with ZipFile(io.BytesIO(download.read()), "r") as zf: | 
					
						
							|  |  |  |                         for member in zf.infolist(): | 
					
						
							|  |  |  |                             if "precompiled/" not in member.filename or member.filename.endswith("/"): | 
					
						
							|  |  |  |                                 continue | 
					
						
							|  |  |  |                             member.filename = member.filename.split("/")[-1] | 
					
						
							|  |  |  |                             if member.filename.endswith("bin.x86_64"): | 
					
						
							|  |  |  |                                 member.filename = "MiniInstaller.bin.x86_64" | 
					
						
							|  |  |  |                             zf.extract(member, path=game_folder) | 
					
						
							|  |  |  |                             files.append(member.filename) | 
					
						
							|  |  |  |                 mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64") | 
					
						
							|  |  |  |                 os.chmod(mono_installer, 0o755) | 
					
						
							|  |  |  |                 installer = subprocess.Popen(mono_installer, shell=False) | 
					
						
							|  |  |  |                 failure = installer.wait() | 
					
						
							|  |  |  |                 for file in files: | 
					
						
							|  |  |  |                     os.remove(file) | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |                 installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True) | 
					
						
							|  |  |  |                 failure = installer.wait() | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |             installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True) | 
					
						
							|  |  |  |             failure = installer.wait() | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |         print(failure) | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         if failure: | 
					
						
							|  |  |  |             messagebox("Failure", "Failed to install Courier", True) | 
					
						
							|  |  |  |             os.chdir(working_directory) | 
					
						
							|  |  |  |             raise RuntimeError("Failed to install Courier") | 
					
						
							|  |  |  |         os.chdir(working_directory) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |         if courier_installed(): | 
					
						
							|  |  |  |             messagebox("Success!", "Courier successfully installed!") | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         messagebox("Failure", "Failed to install Courier", True) | 
					
						
							|  |  |  |         raise RuntimeError("Failed to install Courier") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def install_mod() -> None: | 
					
						
							|  |  |  |         """Installs latest version of the mod""" | 
					
						
							|  |  |  |         assets = request_data(MOD_URL)["assets"] | 
					
						
							|  |  |  |         if len(assets) == 1: | 
					
						
							|  |  |  |             release_url = assets[0]["browser_download_url"] | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             for asset in assets: | 
					
						
							|  |  |  |                 if "TheMessengerRandomizerAP" in asset["name"]: | 
					
						
							|  |  |  |                     release_url = asset["browser_download_url"] | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 messagebox("Failure", "Failed to find latest mod download", True) | 
					
						
							|  |  |  |                 raise RuntimeError("Failed to install Mod") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         mod_folder = os.path.join(game_folder, "Mods") | 
					
						
							|  |  |  |         os.makedirs(mod_folder, exist_ok=True) | 
					
						
							|  |  |  |         with urllib.request.urlopen(release_url) as download: | 
					
						
							|  |  |  |             with ZipFile(io.BytesIO(download.read()), "r") as zf: | 
					
						
							|  |  |  |                 for member in zf.infolist(): | 
					
						
							|  |  |  |                     zf.extract(member, path=mod_folder) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         messagebox("Success!", "Latest mod successfully installed!") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def available_mod_update(latest_version: str) -> bool: | 
					
						
							|  |  |  |         """Check if there's an available update""" | 
					
						
							|  |  |  |         latest_version = latest_version.lstrip("v") | 
					
						
							|  |  |  |         toml_path = os.path.join(game_folder, "Mods", "TheMessengerRandomizerAP", "courier.toml") | 
					
						
							|  |  |  |         with open(toml_path, "r") as f: | 
					
						
							|  |  |  |             installed_version = f.read().splitlines()[1].strip("version = \"") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         logging.info(f"Installed version: {installed_version}. Latest version: {latest_version}") | 
					
						
							|  |  |  |         # one of the alpha builds | 
					
						
							|  |  |  |         return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from . import MessengerWorld | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |     try: | 
					
						
							|  |  |  |         game_folder = os.path.dirname(MessengerWorld.settings.game_path) | 
					
						
							|  |  |  |     except ValueError as e: | 
					
						
							|  |  |  |         logging.error(e) | 
					
						
							|  |  |  |         messagebox("Invalid File", "Selected file did not match expected hash. " | 
					
						
							|  |  |  |                    "Please try again and ensure you select The Messenger.exe.") | 
					
						
							|  |  |  |         return | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |     working_directory = os.getcwd() | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |     # setup ssl context | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         import certifi | 
					
						
							|  |  |  |         import ssl | 
					
						
							|  |  |  |         context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) | 
					
						
							|  |  |  |         context.set_alpn_protocols(["http/1.1"]) | 
					
						
							|  |  |  |         https_handler = urllib.request.HTTPSHandler(context=context) | 
					
						
							|  |  |  |         opener = urllib.request.build_opener(https_handler) | 
					
						
							|  |  |  |         urllib.request.install_opener(opener) | 
					
						
							|  |  |  |     except ImportError: | 
					
						
							|  |  |  |         pass | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |     if not courier_installed(): | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |         should_install = ask_yes_no_cancel("Install Courier", | 
					
						
							|  |  |  |                                            "No Courier installation detected. Would you like to install now?") | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         if not should_install: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         logging.info("Installing Courier") | 
					
						
							|  |  |  |         install_courier() | 
					
						
							|  |  |  |     if not mod_installed(): | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |         should_install = ask_yes_no_cancel("Install Mod", | 
					
						
							|  |  |  |                                            "No randomizer mod detected. Would you like to install now?") | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         if not should_install: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         logging.info("Installing Mod") | 
					
						
							|  |  |  |         install_mod() | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         latest = request_data(MOD_URL)["tag_name"] | 
					
						
							|  |  |  |         if available_mod_update(latest): | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |             should_update = ask_yes_no_cancel("Update Mod", | 
					
						
							|  |  |  |                                               f"New mod version detected. Would you like to update to {latest} now?") | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |             if should_update: | 
					
						
							|  |  |  |                 logging.info("Updating mod") | 
					
						
							|  |  |  |                 install_mod() | 
					
						
							|  |  |  |             elif should_update is None: | 
					
						
							|  |  |  |                 return | 
					
						
							| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  |     if not args: | 
					
						
							|  |  |  |         should_launch = ask_yes_no_cancel("Launch Game", | 
					
						
							|  |  |  |                                           "Mod installed and up to date. Would you like to launch the game now?") | 
					
						
							|  |  |  |         if not should_launch: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  |     parser = argparse.ArgumentParser(description="Messenger Client Launcher") | 
					
						
							|  |  |  |     parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") | 
					
						
							|  |  |  |     args = parser.parse_args(args) | 
					
						
							| 
									
										
										
										
											2024-09-08 12:55:17 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |     if not is_windows: | 
					
						
							| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  |         if args.url: | 
					
						
							|  |  |  |             open_file(f"steam://rungameid/764790//{args.url}/") | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         else: | 
					
						
							|  |  |  |             open_file("steam://rungameid/764790") | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         os.chdir(game_folder) | 
					
						
							| 
									
										
										
										
											2024-09-07 17:03:04 -05:00
										 |  |  |         if args.url: | 
					
						
							|  |  |  |             subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)]) | 
					
						
							| 
									
										
										
										
											2024-03-11 17:23:41 -05:00
										 |  |  |         else: | 
					
						
							|  |  |  |             subprocess.Popen(MessengerWorld.settings.game_path) | 
					
						
							|  |  |  |         os.chdir(working_directory) |