230 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			230 lines
		
	
	
		
			9.3 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
 | 
						|
 | 
						|
 | 
						|
def feedback(text: str):
 | 
						|
    print(text)
 | 
						|
    input("Press Enter to ignore and probably crash.")
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    try:
 | 
						|
        print(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.get("players", 0)
 | 
						|
        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.get("weights_file_path", "weights.yaml")
 | 
						|
        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)
 | 
						|
                print(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:
 | 
						|
            print(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}"
 | 
						|
 | 
						|
        print(command)
 | 
						|
        import time
 | 
						|
 | 
						|
        start = time.perf_counter()
 | 
						|
        text = subprocess.check_output(command, shell=True).decode()
 | 
						|
        print(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)
 | 
						|
                    print(f"Packed {file} into zipfile {zipname}")
 | 
						|
 | 
						|
 | 
						|
            def remove_zipped_file(file: str):
 | 
						|
                os.remove(os.path.join(output_path, file))
 | 
						|
                print(f"Removed {file} which is now present in the zipfile")
 | 
						|
 | 
						|
 | 
						|
            zipname = os.path.join(output_path, f"BM_{seedname}.{typical_zip_ending}")
 | 
						|
 | 
						|
            print(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")
 |