#!/usr/bin/env python3 import sys oldexcepthook = sys.excepthook def newexcepthook(type,value,traceback): oldexcepthook(type,value,traceback) input("Press ENTER to quit.") sys.excepthook = newexcepthook import os p = os.path.join pUp = os.path.dirname s = False if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): s = os.path.realpath(sys.executable) else: s = os.path.realpath(__file__) sp = pUp(s) import configparser import subprocess import json import hashlib import platform import urllib.request import re class coloramaFallback: class Fore: BLACK = "" RED = "" GREEN = "" YELLOW = "" BLUE = "" MAGENTA = "" CYAN = "" WHITE = "" RESET = "" class Back: BLACK = "" RED = "" GREEN = "" YELLOW = "" BLUE = "" MAGENTA = "" CYAN = "" WHITE = "" RESET = "" class Style: DIM = "" NORMAL = "" BRIGHT = "" RESET_ALL = "" def coloramaInit(): global colorama coloramaSuccess = False if not "-nocolor" in sys.argv: try: import colorama colorama.init() coloramaSuccess = True except Exception as e: print("Could not import/init colorama: " +str(e),file=sys.stderr) print("Colors deactivated.\n",file=sys.stderr) if not coloramaSuccess: colorama = coloramaFallback def colored(color,st,bright = True): if bright: st = colorama.Style.BRIGHT + st return color + st + colorama.Style.RESET_ALL def download(url,decode = "utf-8"): request = urllib.request.Request(url,headers={"User-Agent":"Mozilla/5.0"}) response = urllib.request.urlopen(request) rt = response.read() if decode == False: return rt return response.read().decode(decode) def readFile(file,decode = "utf-8"): fileh = False fileh = open(file,"rb") data = fileh.read() fileh.close() if decode == False: return data return data.decode(decode) def fileDl(url,file,read = True,decode = "utf-8"): if os.path.isfile(file): if read == True: return readFile(file,decode) return #print(url) data = download(url,False) fileh = open(tmpFile,"wb") fileh.write(data) fileh.close() if pUp(file) != "" and not os.path.isdir(pUp(file)): os.makedirs(pUp(file)) os.rename(tmpFile,file) if read == False: return if decode == False: return data return data.decode(decode) def readJsonFile(file): fileh = open(file,"r") data = fileh.read() fileh.close() return json.loads(data) def findArgument(args,searchFor): for arg in args: if arg.startswith(searchFor): return True return False def findInChain(versionsPath,version,path): versionPath = p(versionsPath,version) clientJson = readJsonFile(p(versionPath,version + ".json")) curPath = clientJson.copy() success = True for dir in path: if dir in curPath: curPath = curPath[dir] else: success = False break if success == False: return findInChain(versionsPath,clientJson["inheritsFrom"],path) return curPath def findInChainDeepest(versionsPath,version,path): found = False while True: versionPath = p(versionsPath,version) clientJson = readJsonFile(p(versionPath,version + ".json")) curPath = clientJson.copy() for dir in path: if dir in curPath: curPath = curPath[dir] else: if "inheritsFrom" in clientJson: version = clientJson["inheritsFrom"] continue else: curPath = False break found = curPath if "inheritsFrom" in clientJson: version = clientJson["inheritsFrom"] continue else: break if found == False: raise return found def parseJavaLibraryName(libName): libSplit = libName.split(":",2) libPackage = libSplit[0] libName = libSplit[1] libVersion = libSplit[2] libFilePath = libPackage.replace(".","/") + "/" +libName+ "/" +libVersion+ "/" +libName+ "-" +libVersion+ ".jar" return libPackage,libName,libVersion,libFilePath def checkRules(ruleList): action = "disallow" for rule in ruleList: if "os" in rule: if "name" in rule["os"]: rule["os"]["name"] = rule["os"]["name"].replace("osx","macos") if not re.search(rule["os"]["name"],lv["osName"]): continue if "arch" in rule["os"]: if not re.search(rule["os"]["arch"],lv["jvmArch"]): continue if "version" in rule["os"]: if not re.search(rule["os"]["version"],lv["osVersion"]): continue action = rule["action"] else: action = rule["action"] return action def getLibraryPrettyName(library): libraryName = library["package"]+ ":" +library["name"]+ ":" +library["version"] if library["type"] == "native": libraryName += " (" +library["nativeOS"]+ ")" return libraryName def getFileHash(file): with open(file,"rb") as fileh: return hashlib.sha1(fileh.read()).hexdigest() def checkLibraryHash(library): hash = False if library["dumb"] == False: if library["type"] == "library": hash = library["data"]["downloads"]["artifact"]["sha1"] else: hash = library["data"]["downloads"]["classifiers"]["natives-" +library["nativeOS"]]["sha1"] else: if not os.path.isfile(library["filePathOS"] + ".sha1"): print(colored(colorama.fore.RED,"No .sha1 file found, can't verify."),file=sys.stderr) return False else: with open(library["filePathOS"] + ".sha1","r") as fileh: hash = fileh.read() match = (hash == getFileHash(library["filePathOS"])) if not match: print(colored(colorama.Fore.RED,library["filePathOS"]+ " is corrupt!"),file=sys.stderr) return match def processVersion(versionsPath,libraryPath,nativePath,version): versionPath = p(versionsPath,version) clientJson = readJsonFile(p(versionPath,version + ".json")) libraries = [] arguments = [] jvmArguments = [] if "inheritsFrom" in clientJson: _,libraries,_,_= processVersion(versionsPath,libraryPath,nativePath,clientJson["inheritsFrom"]) for library in clientJson["libraries"]: lBase = {} lBase["package"],lBase["name"],lBase["version"],lBase["filePath"] = parseJavaLibraryName(library["name"]) lBase["type"] = "library" lBase["data"] = library lBase["dumb"] = False if "downloads" in library: # not dumb if "classifiers" in library["downloads"]: # classifiers (usually for natives, their sources and their documentation) for classifier in library["downloads"]["classifiers"]: l = lBase.copy() native = library["downloads"]["classifiers"][classifier] if classifier.startswith("natives-"): l["type"] = "native" l["nativeOS"] = classifier.replace("natives-","",1) else: continue # TODO: add source and javadoc handling l["filePathOS"] = p(nativePath,native["path"].replace("/",os.path.sep)) if "url" in native and native["url"] != "": l["url"] = native["url"] libraries.append(l) if "artifact" in library["downloads"]: # artifact (usually for libraries) l = lBase.copy() if "path" in library["downloads"]["artifact"]: l["filePathOS"] = p(libraryPath,library["downloads"]["artifact"]["path"].replace("/",os.path.sep)) if "url" in library["downloads"]["artifact"] and library["downloads"]["artifact"]["url"] != "": l["url"] = library["downloads"]["artifact"]["url"] libraries.append(l) else: # dumb lBase["dumb"] = True if "natives" in library: # natives lBaseTwo = lBase.copy() lBaseTwo["type"] = "native" if not "url" in library: lBaseTwo["url"] = "https://libraries.minecraft.net" else: lBaseTwo["url"] = library["url"] while len(lBaseTwo["url"]) > 0 and lBaseTwo["url"][-1] == "/": lBaseTwo["url"] = lBaseTwo["url"][:-1] lBaseTwo["url"] = lBaseTwo["url"] + "/" + lBaseTwo["filePath"] for native in library["natives"]: l = lBaseTwo.copy() l["nativeOS"] = native native = "natives-" + native l["filePath"] = l["filePath"][:-4] + "-" + native + ".jar" l["url"] = l["url"][:-4] + "-" + native + ".jar" l["filePathOS"] = p(nativePath,l["filePath"].replace("/",os.path.sep)) libraries.append(l) else: # libraries l = lBase.copy() if not "url" in library: l["url"] = "https://libraries.minecraft.net" else: l["url"] = library["url"] while len(l["url"]) > 0 and l["url"][-1] == "/": l["url"] = l["url"][:-1] l["url"] = l["url"] + "/" + l["filePath"] l["filePathOS"] = p(libraryPath,l["filePath"].replace("/",os.path.sep)) libraries.append(l) if os.path.isfile(p(versionPath,version + ".jar")): libraries.append({"type":"client","filePathOS":p(versionPath,version + ".jar")}) if "arguments" in clientJson: if "game" in clientJson["arguments"]: for arg in clientJson["arguments"]["game"]: if type(arg) != str: continue arguments.append(arg) if "jvm" in clientJson["arguments"]: for arg in clientJson["arguments"]["jvm"]: if type(arg) != str: if "value" in arg: if type(arg["value"]) != list: arg["value"] = [arg["value"]] if "rules" in arg: if checkRules(arg["rules"]) == "allow": for value in arg["value"]: jvmArguments.append(value) else: for value in arg["value"]: jvmArguments.append(value) else: jvmArguments.append(arg) elif "minecraftArguments" in clientJson: margs = clientJson["minecraftArguments"].replace('"',"").split(" ") for arg in margs: if type(arg) != str: continue arguments.append(arg) return clientJson,libraries,arguments,jvmArguments def main(): coloramaInit() print(colored(colorama.Fore.GREEN,"Reading config...")) config = configparser.ConfigParser() config.optionxform = str config.read(os.path.splitext(s)[0] + ".ini") global lv lv = config["default"] for var in lv: glbs = globals() for glb in glbs: lv[var] = lv[var].replace("$+" +glb+ "$",str(glbs[glb])) lcs = locals() for lc in lcs: lv[var] = lv[var].replace("$" +lc+ "$",str(lcs[lc])) if lv["osName"] == "": lv["osName"] = platform.system().lower() if lv["osName"] == "darwin": lv["osName"] = "macos" if lv["osVersion"] == "": lv["osVersion"] = platform.version() if lv["jvmArch"] == "": if "64-Bit" in subprocess.check_output([lv["java"],"-version"]).decode("utf-8"): lv["jvmArch"] = "amd64" else: lv["jvmArch"] = "x86" print("") for setting in lv: print(colored(colorama.Fore.BLACK,setting+ "=" +str(lv[setting]))) if len(sys.argv) > 1: for arg in sys.argv[1:]: if arg.startswith("-"): continue argSplit = arg.split("=",1) if len(argSplit) > 1: lv[argSplit[0]] = argSplit[1] else: lv[argSplit[0]] = True json.loads(lv["jvmArguments"]) if not lv["osName"] in ["windows","linux","macos"]: print(colored(colorama.Fore.YELLOW,"\nWarning, unsupported OS detected: '" +lv["osName"]+ "'"),file=sys.stderr) print("Needs to be either windows, linux or macos. Define it with osName=name in the config.",file=sys.stderr) print("") if not "version" in lv: lv["version"] = input("Version ID: ") if "-downloadonly" in sys.argv: lv["name"] = "Player" if not "name" in lv: lv["name"] = input("Player name: ") global tmpFile tmpFile = p(lv["gamePath"],"file.tmp") if os.path.isfile(tmpFile): os.remove(tmpFile) launcherVariables = {} versionsPath = p(lv["gamePath"],"versions") libraryPath = p(lv["gamePath"],"libraries") nativePath = p(lv["gamePath"],"natives") versionPath = p(lv["gamePath"],"versions",lv["version"]) nativesOutPath = p(lv["gamePath"],"natives-extracted",lv["version"],lv["osName"]+ "." +lv["jvmArch"]) assetsPath = p(lv["gamePath"],"assets") print(colored(colorama.Fore.GREEN,"Scanning .json(s)...")) clientJson,libraries,arguments,jvmArguments = processVersion(versionsPath,libraryPath,nativePath,lv["version"]) try: launcherVariables["assets_index_name"] = findInChain(versionsPath,lv["version"],["assets"]) except: print(colored(colorama.Fore.YELLOW,"> Could not find assets_index_name, assuming pre-1.6"),file=sys.stderr) launcherVariables["assets_index_name"] = "pre-1.6" input() print(colored(colorama.Fore.GREEN,"\nDownloading libraries...")) for library in libraries: if "url" in library: print(colored(colorama.Fore.BLACK,getLibraryPrettyName(library))) try: fileDl(library["url"],library["filePathOS"],read = False) except Exception as e: print(colored(colorama.Fore.RED,"> Could not download: ") +str(e),file=sys.stderr) else: if library["dumb"] == True: try: fileDl(library["url"] + ".sha1",library["filePathOS"] + ".sha1",read = False) except Exception as e: print(colored(colorama.Fore.RED,"> Could not download sha1 hash: ") +str(e),file=sys.stderr) if "-verifydata" in sys.argv: checkLibraryHash(library) if "-verifydata" in sys.argv: # assets print(colored(colorama.Fore.GREEN,"\nVerifying assets...")) assetJson = False with open(p(assetsPath,"indexes",launcherVariables["assets_index_name"] + ".json"),"r") as fileh: assetJson = json.loads(fileh.read()) for asset in assetJson["objects"]: hash = assetJson["objects"][asset]["hash"] assetp = p(assetsPath,"objects",hash[:2],hash) if not os.path.isfile(assetp): print(colored(colorama.Fore.RED,assetp+ " is missing!"),file=sys.stderr) continue if hash != getFileHash(assetp): print(colored(colorama.Fore.RED,assetp+ " is corrupt!"),file=sys.stderr) if "-downloadonly" in sys.argv: exit(0) loadLibraries = {} for library in libraries: if "data" in library and "rules" in library["data"] and checkRules(library["data"]["rules"]) == "disallow": continue if library["type"] == "client": loadLibraries["client:" +library["filePathOS"]] = library elif library["type"] == "library": loadLibraries[library["package"] + ":" +library["name"]] = library separator = ";" libraryList = "" if lv["osName"] != "windows": separator = ":" print(colored(colorama.Fore.GREEN,"\nLoaded libraries:")) clientJar = "" for libraryID in loadLibraries: library = loadLibraries[libraryID] if os.path.isfile(library["filePathOS"]): if library["type"] == "client": print("Client: " +library["filePathOS"]) clientJar = library["filePathOS"] else: print(colored(colorama.Fore.BLACK,getLibraryPrettyName(library))) else: print(colored(colorama.Fore.RED,"> Lib not found: ") +getLibraryPrettyName(library),file=sys.stderr) continue libraryList += library["filePathOS"] + separator libraryList = libraryList[:-1] print(colored(colorama.Fore.GREEN,"\nExtracting natives...")) if not os.path.isdir(nativesOutPath): os.makedirs(nativesOutPath) for library in libraries: if library["type"] == "native": if "rules" in library["data"] and checkRules(library["data"]["rules"]) == "disallow": continue if lv["osName"] != "macos": if library["nativeOS"] != lv["osName"]: continue else: if not library["nativeOS"] in ["macos","osx"]: continue if not os.path.isfile(library["filePathOS"]): print(colored(colorama.Fore.RED,"> Native not found: ") +getLibraryPrettyName(library),file=sys.stderr) continue print(colored(colorama.Fore.BLACK,getLibraryPrettyName(library))) proc = subprocess.Popen(["7z","x",library["filePathOS"],"-o" +nativesOutPath,"-aos"],stdout=subprocess.DEVNULL) rtn = proc.wait() if rtn != 0: print(colored(colorama.Fore.RED,"> Native could not be extracted: ") +getLibraryPrettyName(library),file=sys.stderr) print(colored(colorama.Fore.GREEN,"\nSetting up launcher variables...")) launcherVariables["auth_player_name"] = lv["name"] launcherVariables["version_name"] = findInChainDeepest(versionsPath,lv["version"],["id"]) launcherVariables["game_directory"] = lv["gamePath"] launcherVariables["assets_root"] = assetsPath launcherVariables["auth_access_token"] = "-" launcherVariables["auth_uuid"] = hashlib.md5(lv["name"].encode('utf-8')).hexdigest() launcherVariables["user_type"] = "offline" launcherVariables["version_type"] = clientJson["type"] launcherVariables["natives_directory"] = nativesOutPath launcherVariables["launcher_name"] = "offline-minecraft-launcher" launcherVariables["launcher_version"] = "0.0" launcherVariables["classpath"] = libraryList launcherVariables["game_assets"] = assetsPath launcherVariables["auth_session"] = "-" launcherVariables["user_properties"] = "{}" if lv["profileFolder"] == "1": profilePath = p(lv["gamePath"],"profiles") launcherVariables["game_directory"] = p(profilePath,lv["name"],lv["version"],".minecraft") if not os.path.isdir(launcherVariables["game_directory"]): os.makedirs(launcherVariables["game_directory"]) os.chdir(launcherVariables["game_directory"]) if launcherVariables["game_directory"].replace(pUp(launcherVariables["game_directory"]) + os.sep,"",1) == ".minecraft": print(colored(colorama.Fore.MAGENTA,"> game_directory is called .minecraft, setting APPDATA/HOME environment variables to parent directory.")) os.environ["APPDATA"] = pUp(launcherVariables["game_directory"]) os.environ["HOME"] = os.environ["APPDATA"] # JVM arguments: args = [] if not findArgument(jvmArguments,"-Djava-library.path="): args.append("-Djava.library.path=" +nativesOutPath) if not findArgument(jvmArguments,"-Dminecraft.launcher.brand="): args.append("-Dminecraft.launcher.brand=" +launcherVariables["launcher_name"]) if not findArgument(jvmArguments,"-Dminecraft.launcher.version="): args.append("-Dminecraft.launcher.version=" +launcherVariables["launcher_version"]) if lv["osName"] == "windows": if not findArgument(jvmArguments,"-XX:HeapDumpPath="): args.append("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump") args.append("-Dminecraft.client.jar=" +clientJar) if not findArgument(jvmArguments,"-cp"): args.append("-cp") args.append(libraryList) for arg in jvmArguments: for var in launcherVariables: arg = arg.replace("${" +var+ "}",launcherVariables[var]) args.append(arg) args = args + json.loads(lv["jvmArguments"]) args.append(clientJson["mainClass"]) for arg in arguments: for var in launcherVariables: arg = arg.replace("${" +var+ "}",launcherVariables[var]) args.append(arg) if not findArgument(args,"--gameDir"): args.append("--gameDir") args.append(launcherVariables["game_directory"]) if not findArgument(args,"--assetsDir"): args.append("--assetsDir") args.append(launcherVariables["assets_root"]) print(colored(colorama.Fore.GREEN,"\nLaunching Minecraft...")) if lv["console"] == "1": proc = subprocess.Popen([lv["java"]] + args) rtn = proc.wait() if rtn != 0: print(colored(colorama.Fore.RED,"> Launch failed: ") +"return isn't 0",file=sys.stderr) else: pkwargs = { "stdout": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "stderr": subprocess.DEVNULL } if lv["osName"] == "windows": pkwargs["creationflags"] = 0x00000008 subprocess.Popen([lv["java"] + "w"] + args,**pkwargs) main()