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.")
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))
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)
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.")
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)
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()))
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))
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
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, )
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
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")
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)
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)
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))
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}")
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))
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)
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