现在再来分析一下我们的启动器需要哪些功能吧。


功能列表


这是HMCL的启动器首页:image.png
这款启动器的主页上布置了账户登录、游戏列表、下载游戏、多人联机、设置、启动游戏,其实这也是大多数启动器的主页布局。我们已经在前文梳理过使用代码生成启动脚本的步骤了,现在就来重新回顾一下从JSON中提取并拼接脚本的步骤吧。

  1. class func:
  2. # 线程配置
  3. threading_manager = threading.Semaphore(value=config.max_threading_count)
  4. def get_java_info(self: int):
  5. java_list = local_files.java_versions_list(0)
  6. return java_list
  7. def get_version_info(self: int, game_version: str):
  8. """
  9. 返回详细版本信息的函数。
  10. :param game_version: 目标版本的自定义名称
  11. :return: 如果版本存在,返回的元组中依次包括[0]游戏版本号、[1].minecraft文件夹路径、[2]该版本JSON文件路径、[3]版本natives库文件夹应在的路径、[4]版本libraries文件夹路径、[5]minecraft本体客户端路径,[6][7]以及两项启动参数;如果版本不存在,返回None
  12. """
  13. game_versions_list = local_files.game_versions_list(0)
  14. # 这里通过先在本地文件中缓存能启动的版本列表,然后查询列表以获取启动需要的版本信息。
  15. for i in game_versions_list:
  16. if str(game_version) == i['name']:
  17. game_version_num: str = str(i['version'])
  18. version_file_path = config.self_path + r"\.minecraft"
  19. version_json_path = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + ".json"
  20. libraries_path_1 = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + "-natives"
  21. libraries_path_2 = version_file_path + r"\libraries" + "\\"
  22. minecraft_jar_path = version_file_path + r"\versions" + "\\" + game_version + "\\" + game_version + ".jar"
  23. Djava_library_path = r"-Djava.library.path=" + libraries_path_1
  24. Dminecraft_client_jar = r"-Dminecraft.client.jar=" + minecraft_jar_path
  25. assetsindex_path_part = version_file_path + "\\assets\\indexes"
  26. return (game_version_num, version_file_path, version_json_path,
  27. libraries_path_1, libraries_path_2, minecraft_jar_path,
  28. Djava_library_path, Dminecraft_client_jar, assetsindex_path_part)
  29. return None
  30. # 这里是在解压natives库文件,目前还未先检测是否存在再解压的策略。
  31. def zip_natives(self: int, natives_file: dict, libraries_path: str, natives_path: str):
  32. file_temp_1 = natives_file['classifiers']
  33. if "natives-windows-64" in file_temp_1:
  34. file_temp_2: dict = dict(file_temp_1['natives-windows-64'])
  35. elif "natives-windows" in file_temp_1:
  36. file_temp_2: dict = dict(file_temp_1['natives-windows'])
  37. else:
  38. return None
  39. file_temp_3: str = str(file_temp_2['path'])
  40. file_temp_4 = list(file_temp_3.split("/"))
  41. dividing_str_1 = "\\"
  42. file_temp_5 = dividing_str_1.join(file_temp_4)
  43. real_path = libraries_path + file_temp_5
  44. with zipfile.ZipFile(file=real_path) as natives_temp:
  45. natives_temp.extractall(path=natives_path)
  46. return None
  47. class using:
  48. def java_setting(self: int, java_setting_info: list):
  49. """
  50. :param java_setting_info: 该版本的相关信息组成的列表,需要:[0]最大内存,[1]最小内存,[2]Djava参数(natives),[3]Dminecraft参数
  51. :return: 包含该.jar文件和其他有关参数的,可直接拼接成脚本使用的代码片段
  52. """
  53. info_max_memory = java_setting_info[0]
  54. info_min_memory = java_setting_info[1]
  55. Djava_library_path = java_setting_info[2]
  56. Dminecraft_jar_path = java_setting_info[3]
  57. output_java_setting = info_max_memory + " " + info_min_memory + " " \
  58. + Djava_library_path + " " \
  59. + Dminecraft_jar_path
  60. return output_java_setting
  61. def get_libraries_json(self: int, game_version_info: list):
  62. """
  63. 返回拼接成的“-cp”+普通库1+普通库2+……
  64. :param game_version_info: 该版本的相关信息组成的列表,需要:[0]版本名称,[1]版本JSON路径,[2]版本libraries路径,[3]版本游戏本体路径,[4]版本natives文件路径
  65. :return 从该JSON文件中提取出的,可直接拼接成脚本使用的代码片段
  66. """
  67. output_path = []
  68. game_version_name = game_version_info[0]
  69. game_version_json_path = game_version_info[1]
  70. libraries_path = game_version_info[2]
  71. minecraft_jar_path = game_version_info[3]
  72. natives_path = game_version_info[4]
  73. with open(file=game_version_json_path) as json_file:
  74. json_data = json.load(json_file)
  75. info_back_type_list = json_data['libraries']
  76. for i in info_back_type_list:
  77. if "rules" in i:
  78. data_temp_1 = list(i['rules'])
  79. if len(data_temp_1) == 1:
  80. allowing_list = list(dict(dict(data_temp_1[0])["os"])['name'])
  81. if not "windows" in allowing_list:
  82. continue
  83. if len(data_temp_1) == 2:
  84. disallowing_list = list(dict(dict(data_temp_1[1])['os'])['name'])
  85. if "windows" in disallowing_list:
  86. continue
  87. data_temp_2 = dict(i['downloads'])
  88. if "classifiers" in data_temp_2:
  89. func.zip_natives(0, data_temp_2, libraries_path, natives_path)
  90. continue
  91. elif not 'artifact' in data_temp_2:
  92. continue
  93. else:
  94. data_temp_2_ = dict(data_temp_2['artifact'])
  95. data_temp_3 = str(data_temp_2_['path'])
  96. data_temp_4 = list(data_temp_3.split("/"))
  97. dividing_str_1 = "\\"
  98. data_temp_5 = dividing_str_1.join(data_temp_4)
  99. real_path = libraries_path + data_temp_5
  100. if not real_path in output_path:
  101. output_path.append(real_path)
  102. output_path.append(minecraft_jar_path)
  103. dividing_str_2 = ";"
  104. output_path_str = dividing_str_2.join(output_path)
  105. return output_path_str
  106. def get_player_info(self: int, get_player_info: list):
  107. """
  108. 此函数可以生成启动脚本中的Minecraft参数。
  109. :param get_player_info: 需要输入一个列表,内容分别为:[0]版本JSON文件路径,[1]玩家名,[2]玩家UUID,[3].minecraft文件夹路径,[4]assets文件夹路径
  110. :return: 从该JSON文件中提取出的,以及输入的信息组成的,可直接拼接成脚本使用的代码片段
  111. """
  112. info_version_json_path = get_player_info[0]
  113. info_player_name = get_player_info[1]
  114. info_player_uuid = get_player_info[2]
  115. info_minecraft_jar_path = get_player_info[3]
  116. info_assets_path = get_player_info[4]
  117. with open(file=info_version_json_path) as json_file:
  118. json_data = json.load(json_file)
  119. main_class = json_data['mainClass']
  120. if "minecraftArguments" in json_data:
  121. minecraft_arguments_ = str(json_data['minecraftArguments'])
  122. assets_index_name = str(dict(json_data['assetIndex'])['id'])
  123. version_type = str(json_data['type'])
  124. minecraft_arguments = minecraft_arguments_.replace("${auth_player_name}", info_player_name) \
  125. .replace("${version_name}", "FanCraftLauncher") \
  126. .replace("${game_directory}", info_minecraft_jar_path) \
  127. .replace("${assets_root}", info_assets_path) \
  128. .replace("${assets_index_name}", assets_index_name) \
  129. .replace("${auth_uuid}", info_player_uuid) \
  130. .replace("${auth_access_token}", info_player_uuid) \
  131. .replace("${user_properties}", "{}") \
  132. .replace("${auth_xuid}", "{}") \
  133. .replace("${clientid}", "{}") \
  134. .replace("${user_type}", "legacy") \
  135. .replace("${version_type}", version_type)
  136. # 1.13以后,JSON中的minecraftArguments键更名为arguments,并且键的结构发生了改变,
  137. # 下面是对这种新结构的解析(并不全面,仅供参考)
  138. elif "arguments" in json_data:
  139. temp_list = []
  140. minecraft_arguments__ = list(json_data["arguments"]["game"])
  141. assets_index_name = str(dict(json_data['assetIndex'])['id'])
  142. version_type = str(json_data['type'])
  143. for i in minecraft_arguments__:
  144. if type(i) == str:
  145. temp_list.append(i)
  146. minecraft_arguments_ = " ".join(temp_list)
  147. minecraft_arguments = minecraft_arguments_.replace("${auth_player_name}", info_player_name) \
  148. .replace("${version_name}", "FanCraftLauncher") \
  149. .replace("${game_directory}", info_minecraft_jar_path) \
  150. .replace("${assets_root}", info_assets_path) \
  151. .replace("${assets_index_name}", assets_index_name) \
  152. .replace("${auth_uuid}", info_player_uuid) \
  153. .replace("${auth_access_token}", info_player_uuid) \
  154. .replace("${user_properties}", "{}") \
  155. .replace("${auth_xuid}", "{}") \
  156. .replace("${clientid}", "{}") \
  157. .replace("${user_type}", "legacy") \
  158. .replace("${version_type}", version_type)
  159. output_player_info = main_class + " " + minecraft_arguments
  160. return output_player_info

然后只需要通过另外一个函数,先后调用这些函数来获取脚本的不同片段,再首尾拼接起来即可。

  1. def launcher_playing_vanilla(config_: list):
  2. """
  3. 此函数用于创建子线程启动Minecraft。
  4. :param config_: 以元组形式,先后输入:[0]Java版本,[1]游戏版本,[2]最大内存,[3]最小内存,[4]玩家名,[5]玩家uuid。
  5. :return: 此函数无论是否顺利执行都不返回值。
  6. """
  7. launch_java_path = ""
  8. info_java_version = config_[0]
  9. info_game_version = config_[1]
  10. info_max_memory = config_[2]
  11. info_min_memory = config_[3]
  12. info_player_name = config_[4]
  13. info_player_uuid = config_[5]
  14. java_list = func.get_java_info(0)
  15. for i in java_list:
  16. if info_java_version == i['version']:
  17. launch_java_path = i['path']
  18. if not launch_java_path:
  19. logger.Logger.warning("指定Java版本不存在。")
  20. # Logger是我在别处定义的日志记录器,会将日志同时输出在控制台上和log文件里。
  21. return None
  22. back_info = func.get_version_info(0, info_game_version)
  23. # 首先获取该版本的信息,如果版本不存在就报错。
  24. if not back_info:
  25. logger.Logger.warning("指定Minecraft版本不存在。")
  26. return None
  27. info_version_name = back_info[0]
  28. info__minecraft_path = back_info[1]
  29. info_assets_path = info__minecraft_path + r"\assets"
  30. info_version_json_path = back_info[2]
  31. info_natives_path = back_info[3]
  32. info_libraries_path = back_info[4]
  33. info_minecraft_jar_path = back_info[5]
  34. info_Djava_libraries = back_info[6]
  35. info_Dminecraft_path = back_info[7]
  36. # 获取版本有关的各种信息。
  37. get_libraries_json_package = [info_version_name, info_version_json_path, info_libraries_path, info_minecraft_jar_path, info_natives_path]
  38. java_setting_package = [info_max_memory, info_min_memory, info_Djava_libraries, info_Dminecraft_path]
  39. get_player_info_package = [info_version_json_path, info_player_name, info_player_uuid, info__minecraft_path, info_assets_path]
  40. # 将各种信息组合成三个列表,准备分别调用上面的函数以获取脚本片段。这样弄比较简洁。
  41. result = '"' + launch_java_path + '" ' \
  42. + using.java_setting(0, java_setting_package) + " -cp " \
  43. + using.get_libraries_json(0, get_libraries_json_package) + " " \
  44. + using.get_player_info(0, get_player_info_package)
  45. # 开始调用函数并拼接脚本。
  46. logger.Logger.debug("成功输出启动脚本:" + result)
  47. logger.Logger.info("启动脚本已输出,准备启动。")
  48. # 创建一个子进程,并使用os库中的system来将启动脚本输入cmd控制台。
  49. func.threading_manager.acquire()
  50. threading.Thread(target=os.system, args=(result,), name="Os_cmd_launcher").start()
  51. func.threading_manager.release()

这样就能启动游戏了!