diff --git a/offline-minecraft-launcher.py b/offline-minecraft-launcher.py index 373cad8..bd71064 100644 --- a/offline-minecraft-launcher.py +++ b/offline-minecraft-launcher.py @@ -25,6 +25,56 @@ 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) @@ -46,7 +96,7 @@ def fileDl(url,file,read = True,decode = "utf-8"): return readFile(file,decode) return - print(url) + #print(url) data = download(url,False) fileh = open(tmpFile,"wb") fileh.write(data) @@ -139,6 +189,37 @@ def checkRules(ruleList): 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")) @@ -154,7 +235,7 @@ def processVersion(versionsPath,libraryPath,nativePath,version): lBase["package"],lBase["name"],lBase["version"],lBase["filePath"] = parseJavaLibraryName(library["name"]) lBase["type"] = "library" lBase["data"] = library - lBase["dumb"] = True + lBase["dumb"] = False if "downloads" in library: # not dumb if "classifiers" in library["downloads"]: # classifiers (usually for natives, their sources and their documentation) @@ -183,6 +264,7 @@ def processVersion(versionsPath,libraryPath,nativePath,version): libraries.append(l) else: # dumb + lBase["dumb"] = True if "natives" in library: # natives lBaseTwo = lBase.copy() lBaseTwo["type"] = "native" @@ -247,7 +329,8 @@ def processVersion(versionsPath,libraryPath,nativePath,version): return clientJson,libraries,arguments,jvmArguments def main(): - print("Reading config...") + coloramaInit() + print(colored(colorama.Fore.GREEN,"Reading config...")) config = configparser.ConfigParser() config.optionxform = str config.read(os.path.splitext(s)[0] + ".ini") @@ -279,7 +362,7 @@ def main(): print("") for setting in lv: - print(setting+ "=" +str(lv[setting])) + print(colored(colorama.Fore.BLACK,setting+ "=" +str(lv[setting]))) if len(sys.argv) > 1: for arg in sys.argv[1:]: @@ -293,8 +376,8 @@ def main(): json.loads(lv["jvmArguments"]) if not lv["osName"] in ["windows","linux","macos"]: - print("\nWarning, unsupported OS detected: '" +lv["osName"]+ "'") - print("Needs to be either windows, linux or macos. Define it with osName=name in the config.") + 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: ") @@ -304,6 +387,7 @@ def main(): 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") @@ -311,32 +395,62 @@ def main(): nativesOutPath = p(lv["gamePath"],"natives-extracted",lv["version"],lv["osName"]+ "." +lv["jvmArch"]) assetsPath = p(lv["gamePath"],"assets") - print("Scanning .json(s)...") + print(colored(colorama.Fore.GREEN,"Scanning .json(s)...")) clientJson,libraries,arguments,jvmArguments = processVersion(versionsPath,libraryPath,nativePath,lv["version"]) - print("\nDownloading libraries...") + 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("Could not download: " +str(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": - if "rules" in library["data"] and checkRules(library["data"]["rules"]) == "disallow": continue loadLibraries[library["package"] + ":" +library["name"]] = library separator = ";" libraryList = "" if lv["osName"] != "windows": separator = ":" - print("\nLoaded libraries:") + print(colored(colorama.Fore.GREEN,"\nLoaded libraries:")) clientJar = "" for libraryID in loadLibraries: library = loadLibraries[libraryID] @@ -345,15 +459,15 @@ def main(): print("Client: " +library["filePathOS"]) clientJar = library["filePathOS"] else: - print("- " +library["package"]+ ":" +library["name"]+ ":" +library["version"]) + print(colored(colorama.Fore.BLACK,getLibraryPrettyName(library))) else: - print("Lib not found: " +library["package"]+ ":" + +library["name"]+ ":" +library["version"]) + print(colored(colorama.Fore.RED,"> Lib not found: ") +getLibraryPrettyName(library),file=sys.stderr) continue libraryList += library["filePathOS"] + separator libraryList = libraryList[:-1] - print("\nExtracting natives...") + print(colored(colorama.Fore.GREEN,"\nExtracting natives...")) if not os.path.isdir(nativesOutPath): os.makedirs(nativesOutPath) for library in libraries: if library["type"] == "native": @@ -364,23 +478,20 @@ def main(): if not library["nativeOS"] in ["macos","osx"]: continue if not os.path.isfile(library["filePathOS"]): - print("Native not found: " +library["package"]+ ":" +library["name"]+ ":" +library["version"]) + 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: raise Exception("process","return code isn't 0") + if rtn != 0: + print(colored(colorama.Fore.RED,"> Native could not be extracted: ") +getLibraryPrettyName(library),file=sys.stderr) - print("\nSetting up launcher variables...") - launcherVariables = {} + 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 - try: - launcherVariables["assets_index_name"] = findInChain(versionsPath,lv["version"],["assets"]) - except: - print("> Could not find assets_index_name, assuming pre-1.6") - launcherVariables["assets_index_name"] = "pre-1.6" launcherVariables["auth_access_token"] = "-" launcherVariables["auth_uuid"] = hashlib.md5(lv["name"].encode('utf-8')).hexdigest() launcherVariables["user_type"] = "offline" @@ -400,7 +511,7 @@ def main(): os.chdir(launcherVariables["game_directory"]) if launcherVariables["game_directory"].replace(pUp(launcherVariables["game_directory"]) + os.sep,"",1) == ".minecraft": - print("> game_directory is called .minecraft, setting APPDATA/HOME environment variables to parent directory.") + 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"] @@ -442,11 +553,11 @@ def main(): args.append("--assetsDir") args.append(launcherVariables["assets_root"]) - print("\nLaunching Minecraft...") + print(colored(colorama.Fore.GREEN,"\nLaunching Minecraft...")) if lv["console"] == "1": proc = subprocess.Popen([lv["java"]] + args) rtn = proc.wait() - if rtn != 0: raise Exception("process","return code isn't 0") + if rtn != 0: print(colored(colorama.Fore.RED,"> Launch failed: ") +"return isn't 0",file=sys.stderr) else: pkwargs = { "stdout": subprocess.DEVNULL,