diff --git a/picomc/cli/__init__.py b/picomc/cli/__init__.py index 7b58ae5..67cf0cf 100644 --- a/picomc/cli/__init__.py +++ b/picomc/cli/__init__.py @@ -2,9 +2,11 @@ from .config import register_config_cli from .instance import register_instance_cli from .main import picomc_cli +from .mod import register_mod_cli from .version import register_version_cli register_account_cli(picomc_cli) register_version_cli(picomc_cli) register_instance_cli(picomc_cli) register_config_cli(picomc_cli) +register_mod_cli(picomc_cli) diff --git a/picomc/cli/mod.py b/picomc/cli/mod.py new file mode 100644 index 0000000..c684acc --- /dev/null +++ b/picomc/cli/mod.py @@ -0,0 +1,52 @@ +from pathlib import Path + +import click + +from picomc import mod +from picomc.env import get_filepath + + +@click.group() +def mod_cli(): + """Helpers to install modded Minecraft.""" + pass + + +def list_loaders(ctx, param, value): + if not value or ctx.resilient_parsing: + return + for loader in mod.LOADERS: + print(loader._loader_name) + ctx.exit() + + +@mod_cli.group("loader") +@click.option( + "--list", + "-l", + is_eager=True, + is_flag=True, + expose_value=False, + callback=list_loaders, + help="List available mod loaders", +) +@click.pass_context +def loader_cli(ctx): + """Manage mod loaders. + + Loaders are customized Minecraft + versions which can load other mods, e.g. Forge or Fabric. + Installing a loader creates a new version which can be used by instances.""" + ctx.ensure_object(dict) + + ctx.obj["VERSIONS_ROOT"] = Path(get_filepath("versions")) + ctx.obj["LIBRARIES_ROOT"] = Path(get_filepath("libraries")) + pass + + +for loader in mod.LOADERS: + loader.register_cli(loader_cli) + + +def register_mod_cli(picomc_cli): + picomc_cli.add_command(mod_cli, name="mod") diff --git a/picomc/instance.py b/picomc/instance.py index 8183358..a3dead8 100644 --- a/picomc/instance.py +++ b/picomc/instance.py @@ -119,7 +119,13 @@ def launch(self, account, version=None, verify_hashes=False): self, filter(attrgetter("is_native"), libraries) ) as natives_dir: self._exec_mc( - account, vobj, java, java_info, gamedir, libraries, natives_dir + account, + vobj, + java, + java_info, + gamedir, + filter(attrgetter("is_classpath"), libraries), + natives_dir, ) def extract_natives(self): @@ -131,11 +137,7 @@ def extract_natives(self): logger.info("Extracted natives to {}".format(ne.get_natives_path())) def _exec_mc(self, account, v, java, java_info, gamedir, libraries, natives): - libs = [ - lib.get_abspath(get_filepath("libraries")) - for lib in libraries - if not lib.is_native - ] + libs = [lib.get_abspath(get_filepath("libraries")) for lib in libraries] libs.append(v.jarfile) classpath = join_classpath(*libs) diff --git a/picomc/library.py b/picomc/library.py index 50e4e36..edcdc53 100644 --- a/picomc/library.py +++ b/picomc/library.py @@ -1,6 +1,6 @@ -import os import urllib.parse from dataclasses import dataclass +from pathlib import Path, PurePosixPath from platform import architecture from string import Template @@ -9,23 +9,48 @@ @dataclass -class LibraryArtifact: +class Artifact: url: str - path: str + path: PurePosixPath sha1: str size: int filename: str @classmethod def from_json(cls, obj): + path = None + if "path" in obj: + path = PurePosixPath(obj["path"]) + filename = None + if path: + filename = path.name return cls( - url=obj["url"], - path=obj["path"], + url=obj.get("url", None), + path=path, sha1=obj["sha1"], size=obj["size"], - filename=None, + filename=filename, ) + @classmethod + def make(cls, descriptor): + descriptor, *ext = descriptor.split("@") + ext = ext[0] if ext else "jar" + group, art_id, version, *class_ = descriptor.split(":") + classifier = None + if class_: + classifier = class_[0] + group = group.replace(".", "/") + v2 = "-".join([version] + ([classifier] if classifier else [])) + + filename = f"{art_id}-{v2}.{ext}" + path = PurePosixPath(group) / art_id / version / filename + + return cls(url=None, path=path, sha1=None, size=None, filename=filename) + + def get_localpath(self, base): + return Path(base) / self.path + class Library: MOJANG_BASE_URL = "https://libraries.minecraft.net/" @@ -36,42 +61,43 @@ def __init__(self, json_lib): def _populate(self): js = self.json_lib - self.libname = js["name"] + self.descriptor = js["name"] self.is_native = "natives" in js + self.is_classpath = not (self.is_native or js.get("presenceOnly", False)) self.base_url = js.get("url", Library.MOJANG_BASE_URL) self.available = True - self.native_suffix = "" + self.native_classifier = None if self.is_native: try: classifier_tmpl = self.json_lib["natives"][Env.platform] arch = architecture()[0][:2] self.native_classifier = Template(classifier_tmpl).substitute(arch=arch) - self.native_suffix = "-" + self.native_classifier + self.descriptor = self.descriptor + ":" + self.native_classifier except KeyError: logger.warning( - f"Native {self.libname} is not available for current platform {Env.platform}." + f"Native {self.descriptor} is not available for current platform {Env.platform}." ) - self.native_classifier = None self.available = False return - self.virt_artifact = self.make_virtual_artifact() + self.virt_artifact = Artifact.make(self.descriptor) + self.virt_artifact.url = urllib.parse.urljoin( + self.base_url, self.virt_artifact.path.as_posix() + ) self.artifact = self.resolve_artifact() # Just use filename and path derived from the name. - self.filename = self.virt_artifact.url + self.filename = self.virt_artifact.filename self.path = self.virt_artifact.path - # Actual fs path - self.relpath = os.path.join(*self.path.split("/")) - if self.artifact: final_art = self.artifact # Sanity check - assert self.virt_artifact.path == self.artifact.path + if self.artifact.path is not None: + assert self.virt_artifact.path == self.artifact.path else: final_art = self.virt_artifact @@ -79,37 +105,24 @@ def _populate(self): self.sha1 = final_art.sha1 self.size = final_art.size - def make_virtual_artifact(self): - # I don't know where the *va part comes from, it was already implemented - # before a refactor, unfortunately the reason was not documented. - # Currently I don't have a version in my versions directory which - # utilizes that. - # I am leaving it implemented, as there probably was motivation to do it. - group, art_id, version, *va = self.libname.split(":") - group = group.replace(".", "/") - v2 = "-".join([version] + va) - - filename = f"{art_id}-{v2}{self.native_suffix}.jar" - path = f"{group}/{art_id}/{version}/{filename}" - url = urllib.parse.urljoin(self.base_url, path) - - return LibraryArtifact( - url=url, path=path, sha1=None, size=None, filename=filename - ) - def resolve_artifact(self): if self.is_native: if self.native_classifier is None: # Native not available for current platform return None else: - art = self.json_lib["downloads"]["classifiers"][self.native_classifier] - return LibraryArtifact.from_json(art) + try: + art = self.json_lib["downloads"]["classifiers"][ + self.native_classifier + ] + return Artifact.from_json(art) + except KeyError: + return None else: try: - return LibraryArtifact.from_json(self.json_lib["downloads"]["artifact"]) + return Artifact.from_json(self.json_lib["downloads"]["artifact"]) except KeyError: return None def get_abspath(self, library_root): - return os.path.join(library_root, self.relpath) + return self.virt_artifact.get_localpath(library_root) diff --git a/picomc/mod/__init__.py b/picomc/mod/__init__.py new file mode 100644 index 0000000..e57a5aa --- /dev/null +++ b/picomc/mod/__init__.py @@ -0,0 +1,3 @@ +from . import fabric, forge + +LOADERS = [fabric, forge] diff --git a/picomc/mod/fabric.py b/picomc/mod/fabric.py new file mode 100644 index 0000000..86d7cca --- /dev/null +++ b/picomc/mod/fabric.py @@ -0,0 +1,155 @@ +import json +import os +import urllib.parse +from datetime import datetime, timezone + +import click +import requests + +from picomc.logging import logger +from picomc.utils import die + +_loader_name = "fabric" + +PACKAGE = "net.fabricmc" +MAVEN_BASE = "https://maven.fabricmc.net/" +LOADER_NAME = "fabric-loader" +MAPPINGS_NAME = "intermediary" + +__all__ = ["register_cli"] + + +class VersionError(Exception): + pass + + +def latest_game_version(): + url = "https://meta.fabricmc.net/v2/versions/game" + obj = requests.get(url).json() + for ver in obj: + if ver["stable"]: + return ver["version"] + + +def get_loader_meta(game_version, loader_version): + url = "https://meta.fabricmc.net/v2/versions/loader/{}".format( + urllib.parse.quote(game_version) + ) + obj = requests.get(url).json() + if len(obj) == 0: + raise VersionError("Specified game version is unsupported") + if loader_version is None: + ver = next(v for v in obj if v["loader"]["stable"]) + else: + try: + ver = next(v for v in obj if v["loader"]["version"] == loader_version) + except StopIteration: + raise VersionError("Specified loader version is not available") from None + return ver["loader"]["version"], ver["launcherMeta"] + + +def resolve_version(game_version=None, loader_version=None): + if game_version is None: + game_version = latest_game_version() + + loader_version, loader_obj = get_loader_meta(game_version, loader_version) + return game_version, loader_version, loader_obj + + +def generate_vspec_obj(version_name, loader_obj, loader_version, game_version): + out = dict() + + out["id"] = version_name + out["inheritsFrom"] = game_version + out["jar"] = game_version # Prevent the jar from being duplicated + + current_time = datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") + out["time"] = current_time + + mainClass = loader_obj["mainClass"] + if type(mainClass) is dict: + mainClass = mainClass["client"] + out["mainClass"] = mainClass + + libs = [] + for side in ["common", "client"]: + libs.extend(loader_obj["libraries"][side]) + + for artifact, version in [ + (MAPPINGS_NAME, game_version), + (LOADER_NAME, loader_version), + ]: + libs.append( + {"name": "{}:{}:{}".format(PACKAGE, artifact, version), "url": MAVEN_BASE} + ) + + out["libraries"] = libs + + return out + + +def install(versions_root, game_version=None, loader_version=None, version_name=None): + game_version, loader_version, loader_obj = resolve_version( + game_version, loader_version + ) + + if version_name is None: + version_name = "{}-{}-{}".format(LOADER_NAME, loader_version, game_version) + + version_dir = os.path.join(versions_root, version_name) + if os.path.exists(version_dir): + die(f"Version with name {version_name} already exists") + + msg = f"Installing Fabric version {loader_version}-{game_version}" + if version_name: + logger.info(msg + f" as {version_name}") + else: + logger.info(msg) + + vspec_obj = generate_vspec_obj( + version_name, loader_obj, loader_version, game_version + ) + + os.mkdir(version_dir) + with open(os.path.join(version_dir, f"{version_name}.json"), "w") as fd: + json.dump(vspec_obj, fd, indent=2) + + +@click.group("fabric") +def fabric_cli(): + """The Fabric loader. + + Find out more about Fabric at https://fabricmc.net/""" + pass + + +@fabric_cli.command("install") +@click.argument("game_version", required=False) +@click.argument("loader_version", required=False) +@click.option("--name", default=None) +@click.pass_obj +def install_cli(ctxo, game_version, loader_version, name): + """Install Fabric. If no additional arguments are specified, the latest + supported stable (non-snapshot) game version is chosen. The most recent + loader version for the given game version is selected automatically. Both + the game version and the loader version may be overridden.""" + try: + install(ctxo["VERSIONS_ROOT"], game_version, loader_version, version_name=name) + except VersionError as e: + logger.error(e) + + +@fabric_cli.command("version") +@click.argument("game_version", required=False) +def version_cli(game_version): + """Resolve the loader version. If game version is not specified, the latest + supported stable (non-snapshot) is chosen automatically.""" + try: + game_version, loader_version, _ = resolve_version(game_version) + logger.info(f"{loader_version}-{game_version}") + except VersionError as e: + logger.error(e) + + +def register_cli(root): + root.add_command(fabric_cli) diff --git a/picomc/mod/forge.py b/picomc/mod/forge.py new file mode 100644 index 0000000..b064e25 --- /dev/null +++ b/picomc/mod/forge.py @@ -0,0 +1,344 @@ +import json +import os +import posixpath +import shutil +import urllib.parse +from dataclasses import dataclass +from operator import itemgetter +from pathlib import Path +from tempfile import TemporaryDirectory +from xml.etree import ElementTree +from zipfile import ZipFile + +import click +import requests + +from picomc.downloader import DownloadQueue +from picomc.library import Artifact +from picomc.logging import logger +from picomc.utils import die + +_loader_name = "forge" + +MAVEN_URL = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/" +PROMO_FILE = "promotions.json" +META_FILE = "maven-metadata.xml" +INSTALLER_FILE = "forge-{}-installer.jar" +INSTALL_PROFILE_FILE = "install_profile.json" +VERSION_INFO_FILE = "version.json" + +FORGE_WRAPPER = { + "mainClass": "net.cavoj.picoforgewrapper.Main", + "library": { + "name": "net.cavoj:PicoForgeWrapper:1.2", + "downloads": { + "artifact": { + "url": f"https://mvn.cavoj.net/net/cavoj/PicoForgeWrapper/1.2/PicoForgeWrapper-1.2.jar", + "sha1": "0ad36756c05cf910bc596dd1a8239d20daa8e481", + "size": 6177, + } + }, + }, +} + + +class VersionError(Exception): + pass + + +def get_all_versions(): + resp = requests.get(urllib.parse.urljoin(MAVEN_URL, META_FILE)) + X = ElementTree.fromstring(resp.content) + return (v.text for v in X.findall("./versioning/versions/")) + + +def _version_as_tuple(ver): + return tuple(map(int, ver.split("."))) + + +def get_applicable_promos(latest=False): + resp = requests.get(urllib.parse.urljoin(MAVEN_URL, PROMO_FILE)) + promo_obj = resp.json() + + for id_, ver_obj in promo_obj["promos"].items(): + is_latest = id_.endswith("latest") + if is_latest and not latest: + continue + yield ver_obj + + +def best_version_from_promos(promos, game_version=None): + if game_version is None: + bestmcobj = max(promos, key=lambda obj: _version_as_tuple(obj["mcversion"])) + game_version = bestmcobj["mcversion"] + versions_for_game = list( + filter(lambda obj: obj["mcversion"] == game_version, promos) + ) + if len(versions_for_game) == 0: + raise VersionError("No forge available for game version. Try using --latest.") + forge_version = max( + map(itemgetter("version"), versions_for_game), key=_version_as_tuple + ) + + return game_version, forge_version + + +def full_from_forge(all_versions, forge_version): + for v in all_versions: + gv, fv, *_ = v.split("-") + if fv == forge_version: + return gv, v + raise VersionError(f"Given Forge version ({forge_version}) does not exist") + + +def resolve_version(game_version=None, forge_version=None, latest=False): + logger.info("Fetching Forge metadata") + promos = list(get_applicable_promos(latest)) + all_versions = set(get_all_versions()) + + logger.info("Resolving version") + + if forge_version is None: + game_version, forge_version = best_version_from_promos(promos, game_version) + + found_game, full = full_from_forge(all_versions, forge_version) + if game_version and found_game != game_version: + raise VersionError("Version mismatch") + game_version = found_game + + return game_version, forge_version, full + + +@dataclass +class ForgeInstallContext: + version: str # The full Forge version string + version_info: dict # The version.json file from installer package + game_version: str + forge_version: str + version_dir: Path + libraries_dir: Path + version_name: str # Name of the output picomc profile + extract_dir: Path # Root of extracted installer + installer_file: Path + install_profile: dict + + +def install_classic(ctx: ForgeInstallContext): + # TODO Some processing of the libraries should be done to remove duplicates. + vspec = make_base_vspec(ctx) + save_vspec(ctx, vspec) + install_meta = ctx.install_profile["install"] + src_file = ctx.extract_dir / install_meta["filePath"] + dst_file = ctx.libraries_dir / Artifact.make(install_meta["path"]).path + os.makedirs(dst_file.parent, exist_ok=True) + shutil.copy(src_file, dst_file) + + +def make_base_vspec(ctx: ForgeInstallContext): + vi = ctx.version_info + vspec = {} + for key in [ + "arguments", + "minecraftArguments", + "inheritsFrom", + "type", + "releaseTime", + "time", + "mainClass", + ]: + if key in vi: + vspec[key] = vi[key] + + vspec["id"] = ctx.version_name + if "inheritsFrom" in vi: + vspec["jar"] = vi["inheritsFrom"] # Prevent vanilla jar duplication + else: + # This is the case for som really old forge versions, before the + # launcher supported inheritsFrom. Libraries should also be filtered + # in this case, as they contain everything from the vanilla vspec as well. + # TODO + logger.warning( + "Support for this version of Forge is not epic yet. Problems may arise." + ) + vspec["jar"] = ctx.game_version + vspec["inheritsFrom"] = ctx.game_version + vspec["libraries"] = vi["libraries"] + + return vspec + + +def save_vspec(ctx, vspec): + with open(ctx.version_dir / f"{ctx.version_name}.json", "w") as fd: + json.dump(vspec, fd, indent=2) + + +def copy_libraries(ctx): + libdir_relative = Artifact.make(ctx.install_profile["path"]).path.parent + srcdir = ctx.extract_dir / "maven" / libdir_relative + dstdir = ctx.libraries_dir / libdir_relative + dstdir.mkdir(parents=True, exist_ok=True) + for f in srcdir.iterdir(): + shutil.copy2(f, dstdir) + + +def install_newstyle(ctx: ForgeInstallContext): + vspec = make_base_vspec(ctx) + save_vspec(ctx, vspec) + copy_libraries(ctx) + + +def install_113(ctx: ForgeInstallContext): + vspec = make_base_vspec(ctx) + + vspec["libraries"] = [FORGE_WRAPPER["library"]] + vspec["libraries"] + vspec["mainClass"] = FORGE_WRAPPER["mainClass"] + + for install_lib in ctx.install_profile["libraries"]: + install_lib["presenceOnly"] = True + vspec["libraries"].append(install_lib) + + save_vspec(ctx, vspec) + + copy_libraries(ctx) + + installer_descriptor = f"net.minecraftforge:forge:{ctx.version}:installer" + installer_libpath = ctx.libraries_dir / Artifact.make(installer_descriptor).path + os.makedirs(installer_libpath.parent, exist_ok=True) + shutil.copy(ctx.installer_file, installer_libpath) + + +def install( + versions_root: Path, + libraries_root, + game_version=None, + forge_version=None, + latest=False, + version_name=None, +): + game_version, forge_version, version = resolve_version( + game_version, forge_version, latest + ) + + if version_name is None: + version_name = f"{game_version}-forge-{forge_version}" + + logger.info(f"Installing Forge {version} as {version_name}") + + version_dir = os.path.join(versions_root, version_name) + if os.path.exists(version_dir): + die(f"Version with name {version_name} already exists") + + for line in ( + "As the Forge project is kept alive mostly thanks to ads on their downloads\n" + "site, please consider supporting them at https://www.patreon.com/LexManos/\n" + "or by visiting their website and looking at some ads." + ).splitlines(): + logger.warn(line) + + installer_url = urllib.parse.urljoin( + MAVEN_URL, posixpath.join(version, INSTALLER_FILE.format(version)) + ) + # TODO Legacy forge versions don't have an installer + with TemporaryDirectory(prefix=".forge-installer-", dir=versions_root) as tempdir: + tempdir = Path(tempdir) + installer_file = tempdir / "installer.jar" + extract_dir = tempdir / "installer" + + dq = DownloadQueue() + dq.add(installer_url, installer_file) + logger.info("Downloading installer") + if not dq.download(): + die("Failed to download installer.") + os.mkdir(version_dir) + try: + os.mkdir(extract_dir) + ctx = ForgeInstallContext( + version=version, + version_info=None, + game_version=game_version, + forge_version=forge_version, + version_dir=versions_root / version_name, + libraries_dir=libraries_root, + version_name=version_name, + extract_dir=extract_dir, + installer_file=installer_file, + install_profile=None, + ) + with ZipFile(installer_file) as zf: + zf.extractall(path=extract_dir) + with open(os.path.join(extract_dir, INSTALL_PROFILE_FILE)) as fd: + ctx.install_profile = json.load(fd) + if "install" in ctx.install_profile: + ctx.version_info = ctx.install_profile["versionInfo"] + logger.info("Installing from classic installer") + install_classic(ctx) + else: + with open(os.path.join(extract_dir, VERSION_INFO_FILE)) as fd: + ctx.version_info = json.load(fd) + if len(ctx.install_profile["processors"]) == 0: + logger.info("Installing legacy version from newstyle installer") + # A legacy version with an updated installer + install_newstyle(ctx) + else: + logger.info("Installing with PicoForgeWrapper") + install_113(ctx) + except: # noqa E722 + shutil.rmtree(version_dir, ignore_errors=True) + raise + + +@click.group("forge") +def forge_cli(): + """The Forge loader. + + Get more information about Forge at https://minecraftforge.net/""" + pass + + +@forge_cli.command("install") +@click.option("--name", default=None) +@click.argument("forge_version", required=False) +@click.option("--game", "-g", default=None) +@click.option("--latest", "-l", is_flag=True) +@click.pass_obj +def install_cli(ctxo, name, forge_version, game, latest): + """Installs Forge. + + The best version is selected automatically based on the given parameters. + By default, only stable Forge versions are considered, use --latest to + enable beta versions as well. + + You can install a specific version of forge using the FORGE_VERSION argument. + You can also choose the newest version for a specific version of Minecraft + using --game.""" + try: + install( + ctxo["VERSIONS_ROOT"], + ctxo["LIBRARIES_ROOT"], + game, + forge_version, + latest, + version_name=name, + ) + except VersionError as e: + logger.error(e) + + +@forge_cli.command("version") +@click.argument("forge_version", required=False) +@click.option("--game", "-g", default=None) +@click.option("--latest", "-l", is_flag=True) +@click.pass_obj +def version_cli(ctxo, forge_version, game, latest): + """Resolve version without installing.""" + try: + game_version, forge_version, version = resolve_version( + game, forge_version, latest + ) + logger.info(f"Found Forge version {forge_version} for Minecraft {game_version}") + except VersionError as e: + logger.error(e) + + +def register_cli(root): + root.add_command(forge_cli) diff --git a/picomc/utils.py b/picomc/utils.py index ab93a08..cea830d 100644 --- a/picomc/utils.py +++ b/picomc/utils.py @@ -7,7 +7,7 @@ def join_classpath(*cp): - return os.pathsep.join(cp) + return os.pathsep.join(map(str, cp)) def file_sha1(filename): diff --git a/picomc/version.py b/picomc/version.py index c787741..31cbe69 100644 --- a/picomc/version.py +++ b/picomc/version.py @@ -57,6 +57,17 @@ def argumentadd(d1, d2): return d +_sentinel = object() + +LEGACY_ASSETS = { + "id": "legacy", + "sha1": "770572e819335b6c0a053f8378ad88eda189fc14", + "size": 109634, + "totalSize": 153475165, + "url": "https://launchermeta.mojang.com/v1/packages/770572e819335b6c0a053f8378ad88eda189fc14/legacy.json", +} + + class VersionSpec: def __init__(self, vobj, version_manager): self.vobj = vobj @@ -72,11 +83,11 @@ def resolve_chain(self, version_manager): chain.append(cv) return chain - def attr_override(self, attr, default=None): + def attr_override(self, attr, default=_sentinel): for v in self.chain: if attr in v.raw_vspec: return v.raw_vspec[attr] - if default is None: + if default is _sentinel: raise AttributeError(attr) return default @@ -96,8 +107,10 @@ def initialize_fields(self): except AttributeError: pass self.mainClass = self.attr_override("mainClass") - self.assetIndex = self.attr_override("assetIndex") - self.assets = self.attr_override("assets") + self.assetIndex = self.attr_override("assetIndex", default=None) + self.assets = self.attr_override("assets", default="legacy") + if self.assetIndex is None and self.assets == "legacy": + self.assetIndex = LEGACY_ASSETS self.libraries = self.attr_reduce("libraries", lambda x, y: y + x) self.jar = self.attr_override("jar", default=self.vobj.version_name) self.downloads = self.attr_override("downloads", default={}) @@ -115,10 +128,13 @@ def __init__(self, version_name, version_manager, version_manifest): self.raw_vspec = self.get_raw_vspec() self.vspec = VersionSpec(self, self.version_manager) - self.raw_asset_index = self.get_raw_asset_index(self.vspec) + if self.vspec.assetIndex is not None: + self.raw_asset_index = self.get_raw_asset_index(self.vspec.assetIndex) - jarname = self.vspec.jar - self.jarfile = get_filepath("versions", jarname, "{}.jar".format(jarname)) + self.jarname = self.vspec.jar + self.jarfile = get_filepath( + "versions", self.jarname, "{}.jar".format(self.jarname) + ) def get_raw_vspec(self): vspec_path = get_filepath( @@ -159,10 +175,10 @@ def get_raw_vspec(self): except requests.ConnectionError: die("Failed to retrieve version json file. Check your internet connection.") - def get_raw_asset_index(self, vspec): - iid = vspec.assetIndex["id"] - url = vspec.assetIndex["url"] - sha1 = vspec.assetIndex["sha1"] + def get_raw_asset_index(self, asset_index_spec): + iid = asset_index_spec["id"] + url = asset_index_spec["url"] + sha1 = asset_index_spec["sha1"] fpath = get_filepath("assets", "indexes", "{}.json".format(iid)) if os.path.exists(fpath) and file_sha1(fpath) == sha1: logger.debug("Using cached asset index, hash matches vspec") @@ -177,6 +193,14 @@ def get_raw_asset_index(self, vspec): except requests.ConnectionError: die("Failed to retrieve asset index.") + def get_raw_asset_index_nodl(self, id_): + fpath = get_filepath("assets", "indexes", "{}.json".format(id_)) + if os.path.exists(fpath): + with open(fpath) as fp: + return json.load(fp) + else: + die("Asset index specified in 'assets' not available.") + def get_libraries(self, java_info): if java_info is not None: key = java_info.get("java.home", None) @@ -219,9 +243,7 @@ def get_jarfile_dl(self, verify_hashes=False, force=False): or (verify_hashes and file_sha1(self.jarfile) != dlspec["sha1"]) ): logger.info( - "Jar file ({}) will be downloaded with libraries.".format( - self.version_name - ) + "Jar file ({}) will be downloaded with libraries.".format(self.jarname) ) return dlspec["url"], dlspec.get("size", None) @@ -255,8 +277,8 @@ def download_libraries(self, java_info, verify_hashes=False, force=False): "Some libraries failed to download. If they are part of a non-vanilla profile, the original installer may need to be used." ) - def _populate_virtual_assets(self, where): - for name, obj in self.raw_asset_index["objects"].items(): + def _populate_virtual_assets(self, asset_index, where): + for name, obj in asset_index["objects"].items(): sha = obj["hash"] objpath = get_filepath("assets", "objects", sha[0:2], sha) path = os.path.join(where, *name.split("/")) @@ -268,12 +290,13 @@ def get_virtual_asset_path(self): return get_filepath("assets", "virtual", self.vspec.assetIndex["id"]) def prepare_assets_launch(self, gamedir): - is_map_resources = self.raw_asset_index.get("map_to_resources", False) + launch_asset_index = self.get_raw_asset_index_nodl(self.vspec.assets) + is_map_resources = launch_asset_index.get("map_to_resources", False) if is_map_resources: logger.info("Mapping resources") where = os.path.join(gamedir, "resources") logger.debug("Resources path: {}".format(where)) - self._populate_virtual_assets(where) + self._populate_virtual_assets(launch_asset_index, where) def download_assets(self, verify_hashes=False, force=False): """Downloads missing assets.""" @@ -309,13 +332,14 @@ def download_assets(self, verify_hashes=False, force=False): logger.info("Copying virtual assets") where = self.get_virtual_asset_path() logger.debug("Virtual asset path: {}".format(where)) - self._populate_virtual_assets(where) + self._populate_virtual_assets(self.raw_asset_index, where) def prepare(self, java_info=None, verify_hashes=False): if not java_info: java_info = get_java_info(Env.gconf.get("java.path")) self.download_libraries(java_info, verify_hashes) - self.download_assets(verify_hashes) + if hasattr(self, "raw_asset_index"): + self.download_assets(verify_hashes) def prepare_launch(self, gamedir, java_info, verify_hahes=False): self.prepare(java_info, verify_hahes) @@ -363,10 +387,12 @@ def version_list(self, vtype=VersionType.RELEASE, local=False): if local: import os + version_dir = get_filepath("versions") r += sorted( "{} [local]".format(name) - for name in os.listdir(get_filepath("versions")) - if os.path.isdir(get_filepath("versions", name)) + for name in os.listdir(version_dir) + if not name.startswith(".") + and os.path.isdir(os.path.join(version_dir, name)) ) return r