generateForge.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import os
  2. import re
  3. import sys
  4. from distutils.version import LooseVersion
  5. from operator import attrgetter
  6. from typing import Collection
  7. from meta.common import ensure_component_dir, polymc_path, upstream_path, static_path
  8. from meta.common.forge import FORGE_COMPONENT, INSTALLER_MANIFEST_DIR, VERSION_MANIFEST_DIR, DERIVED_INDEX_FILE, \
  9. STATIC_LEGACYINFO_FILE, INSTALLER_INFO_DIR, BAD_VERSIONS, FORGEWRAPPER_MAVEN
  10. from meta.common.mojang import MINECRAFT_COMPONENT
  11. from meta.model import MetaVersion, Dependency, Library, GradleSpecifier, MojangLibraryDownloads, MojangArtifact, \
  12. MetaPackage
  13. from meta.model.forge import ForgeVersion, ForgeInstallerProfile, ForgeLegacyInfo, fml_libs_for_version, \
  14. ForgeInstallerProfileV2, InstallerInfo, DerivedForgeIndex, ForgeLegacyInfoList
  15. from meta.model.mojang import MojangVersion
  16. PMC_DIR = polymc_path()
  17. UPSTREAM_DIR = upstream_path()
  18. STATIC_DIR = static_path()
  19. ensure_component_dir(FORGE_COMPONENT)
  20. def eprint(*args, **kwargs):
  21. print(*args, file=sys.stderr, **kwargs)
  22. # Contruct a set of libraries out of a Minecraft version file, for filtering.
  23. mc_version_cache = {}
  24. def load_mc_version_filter(version: str):
  25. if version in mc_version_cache:
  26. return mc_version_cache[version]
  27. v = MetaVersion.parse_file(os.path.join(PMC_DIR, MINECRAFT_COMPONENT, f"{version}.json"))
  28. libs = set(map(attrgetter("name"), v.libraries))
  29. mc_version_cache[version] = libs
  30. return libs
  31. '''
  32. Match a library coordinate to a set of library coordinates.
  33. * Block those that pass completely.
  34. * For others, block those with lower versions than in the set.
  35. '''
  36. def should_ignore_artifact(libs: Collection[GradleSpecifier], match: GradleSpecifier):
  37. for ver in libs:
  38. if ver.group == match.group and ver.artifact == match.artifact and ver.classifier == match.classifier:
  39. if ver.version == match.version:
  40. # Everything is matched perfectly - this one will be ignored
  41. return True
  42. elif LooseVersion(ver.version) > LooseVersion(match.version):
  43. return True
  44. else:
  45. # Otherwise it did not match - new version is higher and this is an upgrade
  46. return False
  47. # No match found in the set - we need to keep this
  48. return False
  49. def version_from_profile(profile: ForgeInstallerProfile, version: ForgeVersion) -> MetaVersion:
  50. v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
  51. mc_version = profile.install.minecraft
  52. v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)]
  53. v.main_class = profile.version_info.main_class
  54. v.release_time = profile.version_info.time
  55. args = profile.version_info.minecraft_arguments
  56. tweakers = []
  57. expression = re.compile(r"--tweakClass ([a-zA-Z0-9.]+)")
  58. match = expression.search(args)
  59. while match is not None:
  60. tweakers.append(match.group(1))
  61. args = args[:match.start()] + args[match.end():]
  62. match = expression.search(args)
  63. if len(tweakers) > 0:
  64. args = args.strip()
  65. v.additional_tweakers = tweakers
  66. # v.minecraftArguments = args
  67. v.libraries = []
  68. mc_filter = load_mc_version_filter(mc_version)
  69. for forge_lib in profile.version_info.libraries:
  70. if forge_lib.name.is_lwjgl() or forge_lib.name.is_log4j() or should_ignore_artifact(mc_filter, forge_lib.name):
  71. continue
  72. overridden_name = forge_lib.name
  73. if overridden_name.group == "net.minecraftforge":
  74. if overridden_name.artifact == "minecraftforge":
  75. overridden_name.artifact = "forge"
  76. overridden_name.version = "%s-%s" % (mc_version, overridden_name.version)
  77. overridden_name.classifier = "universal"
  78. elif overridden_name.artifact == "forge":
  79. overridden_name.classifier = "universal"
  80. overridden_lib = Library(name=overridden_name)
  81. if forge_lib.url == "http://maven.minecraftforge.net/":
  82. overridden_lib.url = "https://maven.minecraftforge.net/"
  83. else:
  84. overridden_lib.url = forge_lib.url
  85. # if forge_lib.checksums and len(forge_lib.checksums) == 2:
  86. # overridden_lib.mmcHint = "forge-pack-xz"
  87. v.libraries.append(overridden_lib)
  88. v.order = 5
  89. return v
  90. def version_from_modernized_installer(installer: MojangVersion, version: ForgeVersion) -> MetaVersion:
  91. v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
  92. mc_version = version.mc_version
  93. v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)]
  94. v.main_class = installer.main_class
  95. v.release_time = installer.release_time
  96. args = installer.minecraft_arguments
  97. tweakers = []
  98. expression = re.compile("--tweakClass ([a-zA-Z0-9.]+)")
  99. match = expression.search(args)
  100. while match is not None:
  101. tweakers.append(match.group(1))
  102. args = args[:match.start()] + args[match.end():]
  103. match = expression.search(args)
  104. if len(tweakers) > 0:
  105. args = args.strip()
  106. v.additional_tweakers = tweakers
  107. # v.minecraftArguments = args
  108. v.libraries = []
  109. mc_filter = load_mc_version_filter(mc_version)
  110. for upstream_lib in installer.libraries:
  111. forge_lib = Library.parse_obj(upstream_lib.dict()) # "cast" MojangLibrary to Library
  112. if forge_lib.name.is_lwjgl() or forge_lib.name.is_log4j() or should_ignore_artifact(mc_filter, forge_lib.name):
  113. continue
  114. if forge_lib.name.group == "net.minecraftforge":
  115. if forge_lib.name.artifact == "forge":
  116. overridden_name = forge_lib.name
  117. overridden_name.classifier = "universal"
  118. forge_lib.downloads.artifact.path = overridden_name.path()
  119. forge_lib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % overridden_name.path()
  120. forge_lib.name = overridden_name
  121. elif forge_lib.name.artifact == "minecraftforge":
  122. overridden_name = forge_lib.name
  123. overridden_name.artifact = "forge"
  124. overridden_name.classifier = "universal"
  125. overridden_name.version = "%s-%s" % (mc_version, overridden_name.version)
  126. forge_lib.downloads.artifact.path = overridden_name.path()
  127. forge_lib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % overridden_name.path()
  128. forge_lib.name = overridden_name
  129. v.libraries.append(forge_lib)
  130. v.order = 5
  131. return v
  132. def version_from_legacy(info: ForgeLegacyInfo, version: ForgeVersion) -> MetaVersion:
  133. v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
  134. mc_version = version.mc_version_sane
  135. v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)]
  136. v.release_time = info.release_time
  137. v.order = 5
  138. if fml_libs_for_version(mc_version): # WHY, WHY DID I WASTE MY TIME REWRITING FMLLIBSMAPPING
  139. v.additional_traits = ["legacyFML"]
  140. classifier = "client"
  141. if "universal" in version.url():
  142. classifier = "universal"
  143. main_mod = Library(name=GradleSpecifier("net.minecraftforge", "forge", version.long_version, classifier))
  144. main_mod.downloads = MojangLibraryDownloads()
  145. main_mod.downloads.artifact = MojangArtifact(url=version.url(), sha1=info.sha1, size=info.size)
  146. main_mod.downloads.artifact.path = None
  147. v.jar_mods = [main_mod]
  148. return v
  149. def version_from_build_system_installer(installer: MojangVersion, profile: ForgeInstallerProfileV2,
  150. version: ForgeVersion) -> MetaVersion:
  151. v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
  152. v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=version.mc_version_sane)]
  153. v.main_class = "io.github.zekerzhayard.forgewrapper.installer.Main"
  154. # FIXME: Add the size and hash here
  155. v.maven_files = []
  156. # load the locally cached installer file info and use it to add the installer entry in the json
  157. info = InstallerInfo.parse_file(
  158. os.path.join(UPSTREAM_DIR, INSTALLER_INFO_DIR, f"{version.long_version}.json"))
  159. installer_lib = Library(
  160. name=GradleSpecifier("net.minecraftforge", "forge", version.long_version, "installer"))
  161. installer_lib.downloads = MojangLibraryDownloads()
  162. installer_lib.downloads.artifact = MojangArtifact(
  163. url="https://maven.minecraftforge.net/%s" % (installer_lib.name.path()),
  164. sha1=info.sha1hash,
  165. size=info.size)
  166. v.maven_files.append(installer_lib)
  167. for upstream_lib in profile.libraries:
  168. forge_lib = Library.parse_obj(upstream_lib.dict())
  169. if forge_lib.name.is_log4j():
  170. continue
  171. if forge_lib.name.group == "net.minecraftforge" and forge_lib.name.artifact == "forge" \
  172. and forge_lib.name.classifier == "universal":
  173. forge_lib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % forge_lib.name.path()
  174. v.maven_files.append(forge_lib)
  175. v.libraries = []
  176. wrapper_lib = Library(name=GradleSpecifier("io.github.zekerzhayard", "ForgeWrapper", "1.5.8-poly1"))
  177. wrapper_lib.downloads = MojangLibraryDownloads()
  178. wrapper_lib.downloads.artifact = MojangArtifact(url=FORGEWRAPPER_MAVEN % (wrapper_lib.name.path()),
  179. sha1="4f5913cf7ea368b274289e35190e433716e4d844",
  180. size=35669)
  181. v.libraries.append(wrapper_lib)
  182. for upstream_lib in installer.libraries:
  183. forge_lib = Library.parse_obj(upstream_lib.dict())
  184. if forge_lib.name.is_log4j():
  185. continue
  186. if forge_lib.name.group == "net.minecraftforge":
  187. if forge_lib.name.artifact == "forge" and not forge_lib.name.classifier:
  188. forge_lib.name.classifier = "launcher"
  189. forge_lib.downloads.artifact.path = forge_lib.name.path()
  190. forge_lib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % forge_lib.name.path()
  191. forge_lib.name = forge_lib.name
  192. # net.minecraftforge.forge:client doesn't exist??? (49.0.x)
  193. if not len(forge_lib.downloads.artifact.url):
  194. continue
  195. v.libraries.append(forge_lib)
  196. v.release_time = installer.release_time
  197. v.order = 5
  198. mc_args = "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} " \
  199. "--assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} " \
  200. "--accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}"
  201. for arg in installer.arguments.game:
  202. mc_args += f" {arg}"
  203. if "--fml.forgeGroup" not in installer.arguments.game:
  204. mc_args += f" --fml.forgeGroup net.minecraftforge"
  205. if "--fml.forgeVersion" not in installer.arguments.game:
  206. mc_args += f" --fml.forgeVersion {version.rawVersion}"
  207. if "--fml.mcVersion" not in installer.arguments.game:
  208. mc_args += f" --fml.mcVersion {version.mc_version}"
  209. v.minecraft_arguments = mc_args
  210. return v
  211. def main():
  212. # load the locally cached version list
  213. remote_versions = DerivedForgeIndex.parse_file(os.path.join(UPSTREAM_DIR, DERIVED_INDEX_FILE))
  214. recommended_versions = []
  215. legacy_info_list = ForgeLegacyInfoList.parse_file(os.path.join(STATIC_DIR, STATIC_LEGACYINFO_FILE))
  216. legacy_versions = [
  217. "1.1",
  218. "1.2.3",
  219. "1.2.4",
  220. "1.2.5",
  221. "1.3.2",
  222. "1.4.1",
  223. "1.4.2",
  224. "1.4.3",
  225. "1.4.4",
  226. "1.4.5",
  227. "1.4.6",
  228. "1.4.7",
  229. "1.5",
  230. "1.5.1",
  231. "1.5.2",
  232. "1.6.1",
  233. "1.6.2",
  234. "1.6.3",
  235. "1.6.4",
  236. "1.7.10",
  237. "1.7.10-pre4",
  238. "1.7.2",
  239. "1.8",
  240. "1.8.8",
  241. "1.8.9",
  242. "1.9",
  243. "1.9.4",
  244. "1.10",
  245. "1.10.2",
  246. "1.11",
  247. "1.11.2",
  248. "1.12",
  249. "1.12.1",
  250. "1.12.2",
  251. ]
  252. for key, entry in remote_versions.versions.items():
  253. if entry.mc_version is None:
  254. eprint("Skipping %s with invalid MC version" % key)
  255. continue
  256. version = ForgeVersion(entry)
  257. if version.long_version in BAD_VERSIONS:
  258. # Version 1.12.2-14.23.5.2851 is ultra cringe, I can't imagine why you would even spend one second on
  259. # actually adding support for this version.
  260. # It is cringe, because it's installer info is broken af
  261. eprint(f"Skipping bad version {version.long_version}")
  262. continue
  263. if version.url() is None:
  264. eprint("Skipping %s with no valid files" % key)
  265. continue
  266. eprint("Processing Forge %s" % version.rawVersion)
  267. version_elements = version.rawVersion.split('.')
  268. if len(version_elements) < 1:
  269. eprint("Skipping version %s with not enough version elements" % key)
  270. continue
  271. major_version_str = version_elements[0]
  272. if not major_version_str.isnumeric():
  273. eprint("Skipping version %s with non-numeric major version %s" % (key, major_version_str))
  274. continue
  275. if entry.recommended:
  276. recommended_versions.append(version.rawVersion)
  277. # If we do not have the corresponding Minecraft version, we ignore it
  278. if not os.path.isfile(os.path.join(PMC_DIR, MINECRAFT_COMPONENT, f"{version.mc_version_sane}.json")):
  279. eprint("Skipping %s with no corresponding Minecraft version %s" % (key, version.mc_version_sane))
  280. continue
  281. # Path for new-style build system based installers
  282. installer_version_filepath = os.path.join(UPSTREAM_DIR, VERSION_MANIFEST_DIR, f"{version.long_version}.json")
  283. profile_filepath = os.path.join(UPSTREAM_DIR, INSTALLER_MANIFEST_DIR, f"{version.long_version}.json")
  284. eprint(installer_version_filepath)
  285. if os.path.isfile(installer_version_filepath):
  286. installer = MojangVersion.parse_file(installer_version_filepath)
  287. if entry.mc_version in legacy_versions:
  288. v = version_from_modernized_installer(installer, version)
  289. else:
  290. profile = ForgeInstallerProfileV2.parse_file(profile_filepath)
  291. v = version_from_build_system_installer(installer, profile, version)
  292. else:
  293. if version.uses_installer():
  294. # If we do not have the Forge json, we ignore this version
  295. if not os.path.isfile(profile_filepath):
  296. eprint("Skipping %s with missing profile json" % key)
  297. continue
  298. profile = ForgeInstallerProfile.parse_file(profile_filepath)
  299. v = version_from_profile(profile, version)
  300. else:
  301. # Generate json for legacy here
  302. if version.mc_version_sane == "1.6.1":
  303. continue
  304. build = version.build
  305. if not str(build).encode('utf-8').decode('utf8') in legacy_info_list.number:
  306. eprint("Legacy build %d is missing in legacy info. Ignoring." % build)
  307. continue
  308. v = version_from_legacy(legacy_info_list.number[str(build)], version)
  309. v.write(os.path.join(PMC_DIR, FORGE_COMPONENT, f"{v.version}.json"))
  310. recommended_versions.sort()
  311. print('Recommended versions:', recommended_versions)
  312. package = MetaPackage(uid=FORGE_COMPONENT, name="Forge", project_url="https://www.minecraftforge.net/forum/")
  313. package.recommended = recommended_versions
  314. package.write(os.path.join(PMC_DIR, FORGE_COMPONENT, "package.json"))
  315. if __name__ == '__main__':
  316. main()