Exemple #1
0
 def download_libraries(self, java_info, verify_hashes=False, force=False):
     """Downloads missing libraries."""
     logger.info("Checking libraries.")
     q = DownloadQueue()
     for library in self.get_libraries(java_info):
         if not library.available:
             continue
         basedir = self.launcher.get_path(Directory.LIBRARIES)
         abspath = library.get_abspath(basedir)
         ok = abspath.is_file() and os.path.getsize(abspath) > 0
         if verify_hashes and library.sha1 is not None:
             ok = ok and file_sha1(abspath) == library.sha1
         if not ok and not library.url:
             logger.error(
                 f"Library {library.filename} is missing or corrupt "
                 "and has no download url.")
             continue
         if force or not ok:
             q.add(library.url, library.get_abspath(basedir), library.size)
     jardl = self.get_jarfile_dl(verify_hashes, force)
     if jardl is not None:
         url, size = jardl
         q.add(url, self.jarfile, size=size)
     if len(q) > 0:
         logger.info("Downloading {} libraries.".format(len(q)))
     if not q.download():
         logger.error(
             "Some libraries failed to download. If they are part of a non-vanilla "
             "profile, the original installer may need to be used.")
Exemple #2
0
def assert_java(java, wanted):
    try:
        jinfo = get_java_info(java)
        bitness = jinfo.get("sun.arch.data.model", None)
        if bitness and bitness != "64":
            logger.warning(
                "You are not using 64-bit java. Things will probably not work."
            )

        logger.info("Using java version: {} ({})".format(
            jinfo["java.version"], jinfo["java.vm.name"]))

        if not check_version_against(jinfo["java.version"], wanted):
            logger.warning("The version of Minecraft you are launching "
                           "uses java {} by default.".format(
                               wanted_to_str(wanted)))

            logger.warning(
                "You may experience issues, especially with older versions of Minecraft."
            )

            major = get_major_version(jinfo["java.version"])
            if int(major) < wanted["majorVersion"]:
                logger.error(
                    "Note that at least java {} is required to launch at all.".
                    format(wanted_to_str(wanted)))

        return jinfo

    except FileNotFoundError:
        die("Could not execute java at: {}. Have you installed it? Is it in yout PATH?"
            .format(java))
Exemple #3
0
 def prepare_assets_launch(self, gamedir):
     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 = gamedir / "resources"
         logger.debug("Resources path: {}".format(where))
         self._populate_virtual_assets(launch_asset_index, where)
Exemple #4
0
def _list(am):
    """List avaiable accounts."""
    alist = am.list()
    if alist:
        lines = ("{}{}".format("* " if am.is_default(u) else "  ", u)
                 for u in alist)
        print("\n".join(lines))
    else:
        logger.info("No accounts.")
Exemple #5
0
def version_cli(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 VersionResolutionError as e:
        logger.error(e)
Exemple #6
0
 def extract_natives(self):
     vobj = self.launcher.version_manager.get_version(
         self.config["version"])
     java_info = assert_java(self.get_java())
     vobj.download_libraries(java_info, verify_hashes=True)
     libs = vobj.get_libraries(java_info)
     ne = NativesExtractor(self.libraries_root, self,
                           filter(attrgetter("is_native"), libs))
     ne.extract()
     logger.info("Extracted natives to {}".format(ne.get_natives_path()))
Exemple #7
0
def list(vm, release, snapshot, alpha, beta, local, all):
    """List available Minecraft versions."""
    if all:
        release = snapshot = alpha = beta = local = True
    elif not (release or snapshot or alpha or beta):
        logger.info("Showing only locally installed versions. "
                    "Use `version list --help` to get more info.")
        local = True
    T = VersionType.create(release, snapshot, alpha, beta)
    versions = vm.version_list(vtype=T, local=local)
    print("\n".join(versions))
Exemple #8
0
    def _ms_oauth(self):
        data = {"client_id": CLIENT_ID, "scope": SCOPE}

        resp = requests.post(URL_DEVICE_AUTH, data)
        resp.raise_for_status()

        j = resp.json()
        device_code = j["device_code"]

        msg = j["message"]
        user_code = j["user_code"]
        link = j["verification_uri"]

        msg = msg.replace(
            user_code,
            colorama.Fore.RED + user_code + colorama.Fore.RESET).replace(
                link, colorama.Style.BRIGHT + link + colorama.Style.NORMAL)

        logger.info(msg)

        data = {
            "code": device_code,
            "grant_type": GRANT_TYPE,
            "client_id": CLIENT_ID
        }

        first = True
        while True:
            if first:
                input("Press enter to continue... ")
            else:
                input("Press enter to try again... ")
            first = False

            resp = requests.post(URL_TOKEN, data)
            if resp.status_code == 400:
                j = resp.json()
                logger.debug(j)
                if j["error"] == "authorization_pending":
                    logger.warning(j["error_description"])
                    logger.info(msg)
                    continue
                else:
                    raise AuthenticationError(j["error_description"])
            resp.raise_for_status()

            j = resp.json()
            break

        access_token = j["access_token"]
        refresh_token = j["refresh_token"]
        logger.debug("OAuth device code flow successful")
        return access_token, refresh_token
Exemple #9
0
    def launch(self, account, version=None, verify_hashes=False):
        vobj = self.launcher.version_manager.get_version(
            version or self.config["version"])
        logger.info("Launching instance: {}".format(self.name))
        if version or vobj.version_name == self.config["version"]:
            logger.info("Using version: {}".format(vobj.version_name))
        else:
            logger.info("Using version: {} -> {}".format(
                self.config["version"], vobj.version_name))
        logger.info("Using account: {}".format(account))
        gamedir = self.get_minecraft_dir()
        os.makedirs(gamedir, exist_ok=True)

        java = self.get_java()
        java_info = assert_java(java)

        libraries = vobj.get_libraries(java_info)
        vobj.prepare_launch(gamedir, java_info, verify_hashes)
        # Do this here so that configs are not needlessly overwritten after
        # the game quits
        self.launcher.config_manager.commit_all_dirty()
        with NativesExtractor(self.libraries_root, self,
                              filter(attrgetter("is_native"),
                                     libraries)) as natives_dir:
            self._exec_mc(
                account,
                vobj,
                java,
                java_info,
                gamedir,
                filter(attrgetter("is_classpath"), libraries),
                natives_dir,
                verify_hashes,
            )
Exemple #10
0
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 VersionResolutionError("Version mismatch")
    game_version = found_game

    return game_version, forge_version, full
Exemple #11
0
def jar(version, which, output):
    """Download the file and save."""
    dlspec = version.vspec.downloads.get(which, None)
    if not dlspec:
        die("No such dlspec exists for version {}".format(
            version.version_name))
    url = dlspec["url"]
    sha1 = dlspec["sha1"]
    ext = posixpath.basename(urllib.parse.urlsplit(url).path).split(".")[-1]
    if output is None:
        output = "{}_{}.{}".format(version.version_name, which, ext)
    if os.path.exists(output):
        die("Refusing to overwrite {}".format(output))
    logger.info("Hash (sha1) should be {}".format(sha1))
    logger.info("Downloading the {} file and saving to {}".format(
        which, output))
    urllib.request.urlretrieve(dlspec["url"], output)
    if file_sha1(output) != sha1:
        logger.warning("Hash of downloaded file does not match")
Exemple #12
0
    def download_assets(self, verify_hashes=False, force=False):
        """Downloads missing assets."""

        hashes = dict()
        for obj in self.raw_asset_index["objects"].values():
            hashes[obj["hash"]] = obj["size"]

        logger.info("Checking {} assets.".format(len(hashes)))

        is_virtual = self.raw_asset_index.get("virtual", False)

        fileset = set(recur_files(self.assets_root))
        q = DownloadQueue()
        objpath = self.launcher.get_path(Directory.ASSET_OBJECTS)
        for sha in hashes:
            abspath = objpath / sha[0:2] / sha
            ok = abspath in fileset  # file exists
            if verify_hashes:
                ok = ok and file_sha1(abspath) == sha
            if force or not ok:
                url = urllib.parse.urljoin(self.ASSETS_URL,
                                           posixpath.join(sha[0:2], sha))
                q.add(url, abspath, size=hashes[sha])

        if len(q) > 0:
            logger.info("Downloading {} assets.".format(len(q)))
        if not q.download():
            logger.warning("Some assets failed to download.")

        if is_virtual:
            logger.info("Copying virtual assets")
            where = self.get_virtual_asset_path()
            logger.debug("Virtual asset path: {}".format(where))
            self._populate_virtual_assets(self.raw_asset_index, where)
Exemple #13
0
    def get_jarfile_dl(self, verify_hashes=False, force=False):
        """Checks existence and hash of cached jar. Returns None if ok, otherwise
        returns download (url, size)"""
        logger.debug("Attempting to use jarfile: {}".format(self.jarfile))
        dlspec = self.vspec.downloads.get("client", None)
        if dlspec is None:
            logger.debug("jarfile dlspec not availble, skipping hash check.")
            if not self.jarfile.exists():
                die("jarfile does not exist and can not be downloaded.")
            return

        logger.debug("Checking jarfile.")
        if (force or not self.jarfile.exists()
                # The fabric-installer places an empty jarfile here, due to some
                # quirk of an old (git blame 2 years) version of the vanilla launcher.
                # https://github.com/FabricMC/fabric-installer/blob/master/src/main/java/net/fabricmc/installer/client/ClientInstaller.java#L49
                or os.path.getsize(self.jarfile) == 0 or
            (verify_hashes and file_sha1(self.jarfile) != dlspec["sha1"])):
            logger.info(
                "Jar file ({}) will be downloaded with libraries.".format(
                    self.jarname))
            return dlspec["url"], dlspec.get("size", None)
Exemple #14
0
def assert_java(java):
    try:
        jinfo = get_java_info(java)
        badjv = not jinfo["java.version"].startswith("1.8.0")
        bitness = jinfo.get("sun.arch.data.model", None)
        if bitness and bitness != "64":
            logger.warning(
                "You are not using 64-bit java. Things will probably not work."
            )

        logger.info("Using java version: {} ({})".format(
            jinfo["java.version"], jinfo["java.vm.name"]))

        if badjv:
            logger.warning(
                "Minecraft uses java 1.8.0 by default."
                " You may experience issues, especially with older versions of Minecraft."
            )

        return jinfo

    except FileNotFoundError:
        die("Could not execute java at: {}. Have you installed it? Is it in yout PATH?"
            .format(java))
Exemple #15
0
def install(pack_id, version, launcher, im, instance_name, use_beta):
    try:
        pack_manifest, version_manifest = resolve_pack_meta(pack_id, version, use_beta)
    except NotImplementedError as ex:
        die(ex)

    pack_name = pack_manifest["name"]
    pack_version = version_manifest["name"]

    if instance_name is None:
        instance_name = sanitize_name(f"{pack_name}-{pack_version}")

    if im.exists(instance_name):
        die("Instance {} already exists".format(instance_name))

    logger.info(f"Installing {pack_name} {pack_version} as {instance_name}")

    forge_version_name = None
    game_version = None
    for target in version_manifest["targets"]:
        if target["name"] == "forge":
            try:
                forge_version_name = forge.install(
                    versions_root=launcher.get_path(Directory.VERSIONS),
                    libraries_root=launcher.get_path(Directory.LIBRARIES),
                    forge_version=target["version"],
                )
            except forge.AlreadyInstalledError as ex:
                forge_version_name = ex.args[0]
        elif target["name"] == "minecraft":
            game_version = target["version"]
        else:
            logger.warn(f"Skipping unsupported target {target['name']}")

    inst_version = forge_version_name or game_version

    inst = im.create(instance_name, inst_version)
    inst.config["java.memory.max"] = str(version_manifest["specs"]["recommended"]) + "M"

    mcdir: Path = inst.get_minecraft_dir()
    dq = DownloadQueue()
    for f in version_manifest["files"]:
        filepath: Path = mcdir / PurePath(f["path"]) / f["name"]
        filepath.parent.mkdir(exist_ok=True, parents=True)
        dq.add(f["url"], filepath, f["size"])

    logger.info("Downloading modpack files")
    dq.download()

    logger.info(f"Installed successfully as {instance_name}")
Exemple #16
0
def install_from_zip(zipfileobj,
                     launcher,
                     instance_manager,
                     instance_name=None):
    with ZipFile(zipfileobj) as pack_zf:
        for fileinfo in pack_zf.infolist():
            fpath = PurePath(fileinfo.filename)
            if fpath.parts[-1] == "manifest.json" and len(fpath.parts) <= 2:
                manifest_zipinfo = fileinfo
                archive_prefix = fpath.parent
                break
        else:
            raise ValueError("Zip file does not contain manifest")

        with pack_zf.open(manifest_zipinfo) as fd:
            manifest = json.load(fd)

        assert manifest["manifestType"] == "minecraftModpack"
        assert manifest["manifestVersion"] == 1

        assert len(manifest["minecraft"]["modLoaders"]) == 1
        forge_ver = manifest["minecraft"]["modLoaders"][0]["id"]

        assert forge_ver.startswith(FORGE_PREFIX)
        forge_ver = forge_ver[len(FORGE_PREFIX):]
        packname = manifest["name"]
        packver = manifest["version"]
        if instance_name is None:
            instance_name = "{}-{}".format(sanitize_name(packname),
                                           sanitize_name(packver))
            logger.info(f"Installing {packname} version {packver}")
        else:
            logger.info(
                f"Installing {packname} version {packver} as instance {instance_name}"
            )

        if instance_manager.exists(instance_name):
            die("Instace {} already exists".format(instance_name))

        try:
            forge.install(
                versions_root=launcher.get_path(Directory.VERSIONS),
                libraries_root=launcher.get_path(Directory.LIBRARIES),
                forge_version=forge_ver,
            )
        except forge.AlreadyInstalledError:
            pass

        # Trusting the game version from the manifest may be a bad idea
        inst = instance_manager.create(
            instance_name,
            "{}-forge-{}".format(manifest["minecraft"]["version"], forge_ver),
        )
        # This is a random guess, but better than the vanilla 1G
        inst.config["java.memory.max"] = "4G"

        project_files = {
            mod["projectID"]: mod["fileID"]
            for mod in manifest["files"]
        }
        headers = {"User-Agent": "curl"}
        dq = DownloadQueue()

        logger.info("Retrieving mod metadata from curse")
        modcount = len(project_files)
        mcdir: Path = inst.get_minecraft_dir()
        moddir = mcdir / "mods"
        with tqdm(total=modcount) as tq:
            # Try to get as many file_infos as we can in one request
            # This endpoint only provides a few "latest" files for each project,
            # so it's not guaranteed that the response will contain the fileID
            # we are looking for. It's a gamble, but usually worth it in terms
            # of request count. The time benefit is not that great, as the endpoint
            # is slow.
            resp = requests.post(ADDON_URL,
                                 json=list(project_files.keys()),
                                 headers=headers)
            resp.raise_for_status()
            projects_meta = resp.json()
            for proj in projects_meta:
                proj_id = proj["id"]
                want_file = project_files[proj_id]
                for file_info in proj["latestFiles"]:
                    if want_file == file_info["id"]:
                        dq.add(
                            file_info["downloadUrl"],
                            moddir / file_info["fileName"],
                            size=file_info["fileLength"],
                        )
                        del project_files[proj_id]

            batch_recvd = modcount - len(project_files)
            logger.debug("Got {} batched".format(batch_recvd))
            tq.update(batch_recvd)

            with ThreadPoolExecutor(max_workers=16) as tpe:

                def dl(pid, fid):
                    resp = requests.get(GETINFO_URL.format(pid, fid),
                                        headers=headers)
                    resp.raise_for_status()
                    file_info = resp.json()
                    assert file_info["id"] == fid
                    dq.add(
                        file_info["downloadUrl"],
                        moddir / file_info["fileName"],
                        size=file_info["fileLength"],
                    )

                # Get remaining individually
                futmap = {}
                for pid, fid in project_files.items():
                    fut = tpe.submit(dl, pid, fid)
                    futmap[fut] = (pid, fid)

                for fut in concurrent.futures.as_completed(futmap.keys()):
                    try:
                        fut.result()
                    except Exception as ex:
                        pid, fid = futmap[fut]
                        logger.error(
                            "Could not get metadata for {}/{}: {}".format(
                                pid, fid, ex))
                    else:
                        tq.update(1)

        logger.info("Downloading mod jars")
        dq.download()

        logger.info("Copying overrides")
        overrides = archive_prefix / manifest["overrides"]
        for fileinfo in pack_zf.infolist():
            if fileinfo.is_dir():
                continue
            fname = fileinfo.filename
            try:
                outpath = mcdir / PurePath(fname).relative_to(overrides)
            except ValueError:
                continue
            if not outpath.parent.exists():
                outpath.parent.mkdir(parents=True, exist_ok=True)
            with pack_zf.open(fileinfo) as infile, open(outpath,
                                                        "wb") as outfile:
                shutil.copyfileobj(infile, outfile)

        logger.info("Done installing {}".format(instance_name))
Exemple #17
0
    def _exec_mc(self, account, v, java, java_info, gamedir, libraries,
                 natives, verify_hashes):
        libs = [lib.get_abspath(self.libraries_root) for lib in libraries]
        libs.append(v.jarfile)
        classpath = join_classpath(*libs)

        version_type, user_type = (("picomc", "mojang") if account.online else
                                   ("picomc/offline", "offline"))

        mc = v.vspec.mainClass

        if hasattr(v.vspec, "minecraftArguments"):
            mcargs = shlex.split(v.vspec.minecraftArguments)
            sjvmargs = [
                "-Djava.library.path={}".format(natives), "-cp", classpath
            ]
        elif hasattr(v.vspec, "arguments"):
            mcargs, jvmargs = process_arguments(v.vspec.arguments, java_info)
            sjvmargs = []
            for a in jvmargs:
                tmpl = Template(a)
                res = tmpl.substitute(
                    natives_directory=natives,
                    launcher_name="picomc",
                    launcher_version=picomc.__version__,
                    classpath=classpath,
                )
                sjvmargs.append(res)

        try:
            account.refresh()
        except requests.exceptions.ConnectionError:
            logger.warning(
                "Failed to refresh account due to a connectivity error. Continuing."
            )

        smcargs = []
        for a in mcargs:
            tmpl = Template(a)
            res = tmpl.substitute(
                auth_player_name=account.gname,
                auth_uuid=account.uuid,
                auth_access_token=account.access_token,
                # Only used in old versions.
                auth_session="token:{}:{}".format(account.access_token,
                                                  account.uuid),
                user_type=user_type,
                user_properties={},
                version_type=version_type,
                version_name=v.version_name,
                game_directory=gamedir,
                assets_root=self.assets_root,
                assets_index_name=v.vspec.assets,
                game_assets=v.get_virtual_asset_path(),
            )
            smcargs.append(res)

        my_jvm_args = [
            "-Xms{}".format(self.config["java.memory.min"]),
            "-Xmx{}".format(self.config["java.memory.max"]),
        ]

        if verify_hashes:
            my_jvm_args.append("-Dpicomc.verify=true")

        my_jvm_args += shlex.split(self.config["java.jvmargs"])

        fargs = [java] + sjvmargs + my_jvm_args + [mc] + smcargs
        if logging.debug:
            logger.debug("Launching: " + shlex.join(fargs))
        else:
            logger.info("Launching the game")
        subprocess.run(fargs, cwd=gamedir)
Exemple #18
0
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}"

    version_dir = versions_root / version_name
    if version_dir.exists():
        logger.info(f"Forge {version} already installed as {version_name}")
        raise AlreadyInstalledError(
            version_name, f"Version with name {version_name} already exists")

    logger.info(f"Installing Forge {version} as {version_name}")

    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():
            raise InstallationError("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(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(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)
            logger.info("Done installing Forge")
        except:  # noqa E722
            shutil.rmtree(version_dir, ignore_errors=True)
            raise
    return version_name