232 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			232 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| __author__ = "Berserker55"  # you can find me on discord.gg/8Z65BR2
 | |
| 
 | |
| """
 | |
| This script launches a Multiplayer "Multiworld" Mystery Game
 | |
| 
 | |
| .yaml files for all participating players should be placed in a /Players folder.
 | |
| For every player a mystery game is rolled and a ROM created.
 | |
| After generation the server is automatically launched.
 | |
| It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
 | |
| Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
 | |
| A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
 | |
| Configuration can be found in host.yaml
 | |
| """
 | |
| 
 | |
| import os
 | |
| import subprocess
 | |
| import sys
 | |
| import threading
 | |
| import concurrent.futures
 | |
| import argparse
 | |
| import logging
 | |
| 
 | |
| 
 | |
| def feedback(text: str):
 | |
|     logging.info(text)
 | |
|     input("Press Enter to ignore and probably crash.")
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     logging.basicConfig(format='%(message)s', level=logging.INFO)
 | |
|     try:
 | |
|         logging.info(f"{__author__}'s MultiMystery Launcher")
 | |
|         import ModuleUpdate
 | |
| 
 | |
|         ModuleUpdate.update()
 | |
| 
 | |
|         parser = argparse.ArgumentParser(add_help=False)
 | |
|         parser.add_argument('--disable_autohost', action='store_true')
 | |
|         args = parser.parse_args()
 | |
| 
 | |
|         from Utils import get_public_ipv4, get_options
 | |
| 
 | |
|         from Patch import create_patch_file
 | |
| 
 | |
|         options = get_options()
 | |
| 
 | |
|         multi_mystery_options = options["multi_mystery_options"]
 | |
|         output_path = options["general_options"]["output_path"]
 | |
|         enemizer_path = multi_mystery_options["enemizer_path"]
 | |
|         player_files_path = multi_mystery_options["player_files_path"]
 | |
|         target_player_count = multi_mystery_options["players"]
 | |
|         race = multi_mystery_options["race"]
 | |
|         create_spoiler = multi_mystery_options["create_spoiler"]
 | |
|         zip_roms = multi_mystery_options["zip_roms"]
 | |
|         zip_diffs = multi_mystery_options["zip_diffs"]
 | |
|         zip_spoiler = multi_mystery_options["zip_spoiler"]
 | |
|         zip_multidata = multi_mystery_options["zip_multidata"]
 | |
|         zip_format = multi_mystery_options["zip_format"]
 | |
|         # zip_password = multi_mystery_options["zip_password"] not at this time
 | |
|         player_name = multi_mystery_options["player_name"]
 | |
|         meta_file_path = multi_mystery_options["meta_file_path"]
 | |
|         weights_file_path = multi_mystery_options["weights_file_path"]
 | |
|         teams = multi_mystery_options["teams"]
 | |
|         rom_file = options["general_options"]["rom_file"]
 | |
|         host = options["server_options"]["host"]
 | |
|         port = options["server_options"]["port"]
 | |
| 
 | |
|         py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
 | |
| 
 | |
|         if not os.path.exists(enemizer_path):
 | |
|             feedback(
 | |
|                 f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
 | |
|         if not os.path.exists(rom_file):
 | |
|             feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
 | |
|         player_files = []
 | |
|         os.makedirs(player_files_path, exist_ok=True)
 | |
|         for file in os.listdir(player_files_path):
 | |
|             lfile = file.lower()
 | |
|             if lfile.endswith(".yaml") and lfile != meta_file_path.lower() and lfile != weights_file_path.lower():
 | |
|                 player_files.append(file)
 | |
|                 logging.info(f"Found player's file {file}.")
 | |
| 
 | |
|         player_string = ""
 | |
|         for i, file in enumerate(player_files, 1):
 | |
|             player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
 | |
| 
 | |
|         if os.path.exists("BerserkerMultiServer.exe"):
 | |
|             basemysterycommand = "BerserkerMystery.exe"  # compiled windows
 | |
|         elif os.path.exists("BerserkerMultiServer"):
 | |
|             basemysterycommand = "BerserkerMystery"  # compiled linux
 | |
|         else:
 | |
|             basemysterycommand = f"py -{py_version} Mystery.py"  # source
 | |
| 
 | |
|         weights_file_path = os.path.join(player_files_path, weights_file_path)
 | |
|         if os.path.exists(weights_file_path):
 | |
|             target_player_count = max(len(player_files), target_player_count)
 | |
|         else:
 | |
|             target_player_count = len(player_files)
 | |
| 
 | |
|         if target_player_count == 0:
 | |
|             feedback(f"No player files found. Please put them in a {player_files_path} folder.")
 | |
|         else:
 | |
|             logging.info(f"{target_player_count} Players found.")
 | |
| 
 | |
|         command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
 | |
|                   f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
 | |
|                   f"--outputpath \"{output_path}\" --teams {teams}"
 | |
| 
 | |
|         if create_spoiler:
 | |
|             command += " --create_spoiler"
 | |
|             if create_spoiler == 2:
 | |
|                 command += " --skip_playthrough"
 | |
|         if zip_diffs:
 | |
|             command += " --create_diff"
 | |
|         if race:
 | |
|             command += " --race"
 | |
|         if os.path.exists(os.path.join(player_files_path, meta_file_path)):
 | |
|             command += f" --meta {os.path.join(player_files_path, meta_file_path)}"
 | |
|         if os.path.exists(weights_file_path):
 | |
|             command += f" --weights {weights_file_path}"
 | |
| 
 | |
|         logging.info(command)
 | |
|         import time
 | |
| 
 | |
|         start = time.perf_counter()
 | |
|         text = subprocess.check_output(command, shell=True).decode()
 | |
|         logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
 | |
|         seedname = ""
 | |
| 
 | |
|         for segment in text.split():
 | |
|             if segment.startswith("M"):
 | |
|                 seedname = segment
 | |
|                 break
 | |
| 
 | |
|         multidataname = f"BM_{seedname}.multidata"
 | |
|         spoilername = f"BM_{seedname}_Spoiler.txt"
 | |
|         romfilename = ""
 | |
| 
 | |
|         if player_name:
 | |
|             for file in os.listdir(output_path):
 | |
|                 if player_name in file:
 | |
|                     import MultiClient
 | |
|                     import asyncio
 | |
| 
 | |
|                     asyncio.run(MultiClient.run_game(os.path.join(output_path, file)))
 | |
|                     break
 | |
| 
 | |
|         if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
 | |
|             import zipfile
 | |
| 
 | |
|             compression = {1: zipfile.ZIP_DEFLATED,
 | |
|                            2: zipfile.ZIP_LZMA,
 | |
|                            3: zipfile.ZIP_BZIP2}[zip_format]
 | |
| 
 | |
|             typical_zip_ending = {1: "zip",
 | |
|                                   2: "7z",
 | |
|                                   3: "bz2"}[zip_format]
 | |
| 
 | |
|             ziplock = threading.Lock()
 | |
| 
 | |
| 
 | |
|             def pack_file(file: str):
 | |
|                 with ziplock:
 | |
|                     zf.write(os.path.join(output_path, file), file)
 | |
|                     logging.info(f"Packed {file} into zipfile {zipname}")
 | |
| 
 | |
| 
 | |
|             def remove_zipped_file(file: str):
 | |
|                 os.remove(os.path.join(output_path, file))
 | |
|                 logging.info(f"Removed {file} which is now present in the zipfile")
 | |
| 
 | |
| 
 | |
|             zipname = os.path.join(output_path, f"BM_{seedname}.{typical_zip_ending}")
 | |
| 
 | |
|             logging.info(f"Creating zipfile {zipname}")
 | |
|             ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
 | |
| 
 | |
| 
 | |
|             def _handle_sfc_file(file: str):
 | |
|                 if zip_roms:
 | |
|                     pack_file(file)
 | |
|                     if zip_roms == 2 and player_name.lower() not in file.lower():
 | |
|                         remove_zipped_file(file)
 | |
| 
 | |
| 
 | |
|             def _handle_diff_file(file: str):
 | |
|                 if zip_diffs > 0:
 | |
|                     pack_file(file)
 | |
|                     if zip_diffs == 2:
 | |
|                         remove_zipped_file(file)
 | |
| 
 | |
| 
 | |
|             with concurrent.futures.ThreadPoolExecutor() as pool:
 | |
|                 futures = []
 | |
|                 with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
 | |
|                     for file in os.listdir(output_path):
 | |
|                         if seedname in file:
 | |
|                             if file.endswith(".sfc"):
 | |
|                                 futures.append(pool.submit(_handle_sfc_file, file))
 | |
|                             elif file.endswith(".bmbp"):
 | |
|                                 futures.append(pool.submit(_handle_diff_file, file))
 | |
| 
 | |
|                     if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
 | |
|                         pack_file(multidataname)
 | |
|                         if zip_multidata == 2:
 | |
|                             remove_zipped_file(multidataname)
 | |
| 
 | |
|                     if zip_spoiler and create_spoiler:
 | |
|                         pack_file(spoilername)
 | |
|                         if zip_spoiler == 2:
 | |
|                             remove_zipped_file(spoilername)
 | |
| 
 | |
|                     for future in futures:
 | |
|                         future.result()  # make sure we close the zip AFTER any packing is done
 | |
| 
 | |
|         if not args.disable_autohost:
 | |
|             if os.path.exists(os.path.join(output_path, multidataname)):
 | |
|                 if os.path.exists("BerserkerMultiServer.exe"):
 | |
|                     baseservercommand = "BerserkerMultiServer.exe"  # compiled windows
 | |
|                 elif os.path.exists("BerserkerMultiServer"):
 | |
|                     baseservercommand = "BerserkerMultiServer"  # compiled linux
 | |
|                 else:
 | |
|                     baseservercommand = f"py -{py_version} MultiServer.py"  # source
 | |
|                 # don't have a mac to test that. If you try to run compiled on mac, good luck.
 | |
| 
 | |
|                 subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
 | |
|     except:
 | |
|         import traceback
 | |
| 
 | |
|         traceback.print_exc()
 | |
|         input("Press enter to close")
 | 
