现在再来分析一下我们的启动器需要哪些功能吧。
功能列表
这是HMCL的启动器首页:
这款启动器的主页上布置了账户登录、游戏列表、下载游戏、多人联机、设置、启动游戏,其实这也是大多数启动器的主页布局。我们已经在前文梳理过使用代码生成启动脚本的步骤了,现在就来重新回顾一下从JSON中提取并拼接脚本的步骤吧。
class func:# 线程配置threading_manager = threading.Semaphore(value=config.max_threading_count)def get_java_info(self: int):java_list = local_files.java_versions_list(0)return java_listdef get_version_info(self: int, game_version: str):"""返回详细版本信息的函数。:param game_version: 目标版本的自定义名称:return: 如果版本存在,返回的元组中依次包括[0]游戏版本号、[1].minecraft文件夹路径、[2]该版本JSON文件路径、[3]版本natives库文件夹应在的路径、[4]版本libraries文件夹路径、[5]minecraft本体客户端路径,[6][7]以及两项启动参数;如果版本不存在,返回None"""game_versions_list = local_files.game_versions_list(0)# 这里通过先在本地文件中缓存能启动的版本列表,然后查询列表以获取启动需要的版本信息。for i in game_versions_list:if str(game_version) == i['name']:game_version_num: str = str(i['version'])version_file_path = config.self_path + r"\.minecraft"version_json_path = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + ".json"libraries_path_1 = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + "-natives"libraries_path_2 = version_file_path + r"\libraries" + "\\"minecraft_jar_path = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + ".jar"Djava_library_path = r"-Djava.library.path=" + libraries_path_1Dminecraft_client_jar = r"-Dminecraft.client.jar=" + minecraft_jar_pathassetsindex_path_part = version_file_path + "\\assets\\indexes"return (game_version_num, version_file_path, version_json_path,libraries_path_1, libraries_path_2, minecraft_jar_path,Djava_library_path, Dminecraft_client_jar, assetsindex_path_part)return None# 这里是在解压natives库文件,目前还未先检测是否存在再解压的策略。def zip_natives(self: int, natives_file: dict, libraries_path: str, natives_path: str):file_temp_1 = natives_file['classifiers']if "natives-windows-64" in file_temp_1:file_temp_2: dict = dict(file_temp_1['natives-windows-64'])elif "natives-windows" in file_temp_1:file_temp_2: dict = dict(file_temp_1['natives-windows'])else:return Nonefile_temp_3: str = str(file_temp_2['path'])file_temp_4 = list(file_temp_3.split("/"))dividing_str_1 = "\\"file_temp_5 = dividing_str_1.join(file_temp_4)real_path = libraries_path + file_temp_5with zipfile.ZipFile(file=real_path) as natives_temp:natives_temp.extractall(path=natives_path)return Noneclass using:def java_setting(self: int, java_setting_info: list):""":param java_setting_info: 该版本的相关信息组成的列表,需要:[0]最大内存,[1]最小内存,[2]Djava参数(natives),[3]Dminecraft参数:return: 包含该.jar文件和其他有关参数的,可直接拼接成脚本使用的代码片段"""info_max_memory = java_setting_info[0]info_min_memory = java_setting_info[1]Djava_library_path = java_setting_info[2]Dminecraft_jar_path = java_setting_info[3]output_java_setting = info_max_memory + " " + info_min_memory + " " \+ Djava_library_path + " " \+ Dminecraft_jar_pathreturn output_java_settingdef get_libraries_json(self: int, game_version_info: list):"""返回拼接成的“-cp”+普通库1+普通库2+……:param game_version_info: 该版本的相关信息组成的列表,需要:[0]版本名称,[1]版本JSON路径,[2]版本libraries路径,[3]版本游戏本体路径,[4]版本natives文件路径:return 从该JSON文件中提取出的,可直接拼接成脚本使用的代码片段"""output_path = []game_version_name = game_version_info[0]game_version_json_path = game_version_info[1]libraries_path = game_version_info[2]minecraft_jar_path = game_version_info[3]natives_path = game_version_info[4]with open(file=game_version_json_path) as json_file:json_data = json.load(json_file)info_back_type_list = json_data['libraries']for i in info_back_type_list:if "rules" in i:data_temp_1 = list(i['rules'])if len(data_temp_1) == 1:allowing_list = list(dict(dict(data_temp_1[0])["os"])['name'])if not "windows" in allowing_list:continueif len(data_temp_1) == 2:disallowing_list = list(dict(dict(data_temp_1[1])['os'])['name'])if "windows" in disallowing_list:continuedata_temp_2 = dict(i['downloads'])if "classifiers" in data_temp_2:func.zip_natives(0, data_temp_2, libraries_path, natives_path)continueelif not 'artifact' in data_temp_2:continueelse:data_temp_2_ = dict(data_temp_2['artifact'])data_temp_3 = str(data_temp_2_['path'])data_temp_4 = list(data_temp_3.split("/"))dividing_str_1 = "\\"data_temp_5 = dividing_str_1.join(data_temp_4)real_path = libraries_path + data_temp_5if not real_path in output_path:output_path.append(real_path)output_path.append(minecraft_jar_path)dividing_str_2 = ";"output_path_str = dividing_str_2.join(output_path)return output_path_strdef get_player_info(self: int, get_player_info: list):"""此函数可以生成启动脚本中的Minecraft参数。:param get_player_info: 需要输入一个列表,内容分别为:[0]版本JSON文件路径,[1]玩家名,[2]玩家UUID,[3].minecraft文件夹路径,[4]assets文件夹路径:return: 从该JSON文件中提取出的,以及输入的信息组成的,可直接拼接成脚本使用的代码片段"""info_version_json_path = get_player_info[0]info_player_name = get_player_info[1]info_player_uuid = get_player_info[2]info_minecraft_jar_path = get_player_info[3]info_assets_path = get_player_info[4]with open(file=info_version_json_path) as json_file:json_data = json.load(json_file)main_class = json_data['mainClass']if "minecraftArguments" in json_data:minecraft_arguments_ = str(json_data['minecraftArguments'])assets_index_name = str(dict(json_data['assetIndex'])['id'])version_type = str(json_data['type'])minecraft_arguments = minecraft_arguments_.replace("${auth_player_name}", info_player_name) \.replace("${version_name}", "FanCraftLauncher") \.replace("${game_directory}", info_minecraft_jar_path) \.replace("${assets_root}", info_assets_path) \.replace("${assets_index_name}", assets_index_name) \.replace("${auth_uuid}", info_player_uuid) \.replace("${auth_access_token}", info_player_uuid) \.replace("${user_properties}", "{}") \.replace("${auth_xuid}", "{}") \.replace("${clientid}", "{}") \.replace("${user_type}", "legacy") \.replace("${version_type}", version_type)# 1.13以后,JSON中的minecraftArguments键更名为arguments,并且键的结构发生了改变,# 下面是对这种新结构的解析(并不全面,仅供参考)elif "arguments" in json_data:temp_list = []minecraft_arguments__ = list(json_data["arguments"]["game"])assets_index_name = str(dict(json_data['assetIndex'])['id'])version_type = str(json_data['type'])for i in minecraft_arguments__:if type(i) == str:temp_list.append(i)minecraft_arguments_ = " ".join(temp_list)minecraft_arguments = minecraft_arguments_.replace("${auth_player_name}", info_player_name) \.replace("${version_name}", "FanCraftLauncher") \.replace("${game_directory}", info_minecraft_jar_path) \.replace("${assets_root}", info_assets_path) \.replace("${assets_index_name}", assets_index_name) \.replace("${auth_uuid}", info_player_uuid) \.replace("${auth_access_token}", info_player_uuid) \.replace("${user_properties}", "{}") \.replace("${auth_xuid}", "{}") \.replace("${clientid}", "{}") \.replace("${user_type}", "legacy") \.replace("${version_type}", version_type)output_player_info = main_class + " " + minecraft_argumentsreturn output_player_info
然后只需要通过另外一个函数,先后调用这些函数来获取脚本的不同片段,再首尾拼接起来即可。
def launcher_playing_vanilla(config_: list):"""此函数用于创建子线程启动Minecraft。:param config_: 以元组形式,先后输入:[0]Java版本,[1]游戏版本,[2]最大内存,[3]最小内存,[4]玩家名,[5]玩家uuid。:return: 此函数无论是否顺利执行都不返回值。"""launch_java_path = ""info_java_version = config_[0]info_game_version = config_[1]info_max_memory = config_[2]info_min_memory = config_[3]info_player_name = config_[4]info_player_uuid = config_[5]java_list = func.get_java_info(0)for i in java_list:if info_java_version == i['version']:launch_java_path = i['path']if not launch_java_path:logger.Logger.warning("指定Java版本不存在。")# Logger是我在别处定义的日志记录器,会将日志同时输出在控制台上和log文件里。return Noneback_info = func.get_version_info(0, info_game_version)# 首先获取该版本的信息,如果版本不存在就报错。if not back_info:logger.Logger.warning("指定Minecraft版本不存在。")return Noneinfo_version_name = back_info[0]info__minecraft_path = back_info[1]info_assets_path = info__minecraft_path + r"\assets"info_version_json_path = back_info[2]info_natives_path = back_info[3]info_libraries_path = back_info[4]info_minecraft_jar_path = back_info[5]info_Djava_libraries = back_info[6]info_Dminecraft_path = back_info[7]# 获取版本有关的各种信息。get_libraries_json_package = [info_version_name, info_version_json_path, info_libraries_path, info_minecraft_jar_path, info_natives_path]java_setting_package = [info_max_memory, info_min_memory, info_Djava_libraries, info_Dminecraft_path]get_player_info_package = [info_version_json_path, info_player_name, info_player_uuid, info__minecraft_path, info_assets_path]# 将各种信息组合成三个列表,准备分别调用上面的函数以获取脚本片段。这样弄比较简洁。result = '"' + launch_java_path + '" ' \+ using.java_setting(0, java_setting_package) + " -cp " \+ using.get_libraries_json(0, get_libraries_json_package) + " " \+ using.get_player_info(0, get_player_info_package)# 开始调用函数并拼接脚本。logger.Logger.debug("成功输出启动脚本:" + result)logger.Logger.info("启动脚本已输出,准备启动。")# 创建一个子进程,并使用os库中的system来将启动脚本输入cmd控制台。func.threading_manager.acquire()threading.Thread(target=os.system, args=(result,), name="Os_cmd_launcher").start()func.threading_manager.release()
这样就能启动游戏了!
