offline-minecraft-launcher/offline-minecraft-launcher.py

575 lines
18 KiB
Python
Raw Normal View History

2021-03-27 23:10:28 +00:00
#!/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)
2021-03-28 00:38:06 +00:00
import configparser
2021-03-27 23:10:28 +00:00
import subprocess
import json
import hashlib
import platform
import urllib.request
2021-03-29 23:19:22 +00:00
import re
2022-05-13 09:43:24 +00:00
import copy
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)
2021-03-27 23:10:28 +00:00
def readJsonFile(file):
fileh = open(file,"r")
data = fileh.read()
fileh.close()
return json.loads(data)
2021-06-17 10:59:00 +00:00
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"))
2022-05-13 09:43:24 +00:00
curPath = copy.deepcopy(clientJson)
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"))
2022-05-13 09:43:24 +00:00
curPath = copy.deepcopy(clientJson)
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"]:
2021-03-29 23:19:22 +00:00
rule["os"]["name"] = rule["os"]["name"].replace("osx","macos")
if not re.search(rule["os"]["name"],lv["osName"]): continue
2021-03-29 08:54:57 +00:00
if "arch" in rule["os"]:
2021-03-29 23:19:22 +00:00
if not re.search(rule["os"]["arch"],lv["jvmArch"]): continue
2021-03-29 23:19:41 +00:00
if "version" in rule["os"]:
if not re.search(rule["os"]["version"],lv["osVersion"]): continue
2021-03-29 10:20:57 +00:00
action = rule["action"]
2021-06-16 12:44:23 +00:00
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 = []
2021-03-29 10:20:57 +00:00
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)
2021-03-29 05:51:39 +00:00
for classifier in library["downloads"]["classifiers"]:
2022-05-13 09:43:24 +00:00
l = copy.deepcopy(lBase)
2021-03-29 05:51:39 +00:00
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))
2021-03-29 05:51:39 +00:00
if "url" in native and native["url"] != "":
l["url"] = native["url"]
2021-03-29 10:20:57 +00:00
libraries.append(l)
if "artifact" in library["downloads"]: # artifact (usually for libraries)
2022-05-13 09:43:24 +00:00
l = copy.deepcopy(lBase)
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
2022-05-13 09:43:24 +00:00
lBaseTwo = copy.deepcopy(lBase)
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"]
2021-06-16 12:44:57 +00:00
for native in library["natives"]:
2022-05-13 09:43:24 +00:00
l = copy.deepcopy(lBaseTwo)
l["nativeOS"] = native
native = "natives-" + native
2021-06-16 12:44:57 +00:00
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)
2021-06-17 13:59:54 +00:00
else: # libraries
2022-05-13 09:43:24 +00:00
l = copy.deepcopy(lBase)
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:
2021-03-29 10:20:57 +00:00
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)
2021-03-29 10:20:57 +00:00
return clientJson,libraries,arguments,jvmArguments
2021-03-27 23:10:28 +00:00
def main():
coloramaInit()
print(colored(colorama.Fore.GREEN,"Reading config..."))
2021-03-28 00:38:06 +00:00
config = configparser.ConfigParser()
2021-03-29 23:17:07 +00:00
config.optionxform = str
2021-03-28 00:38:06 +00:00
config.read(os.path.splitext(s)[0] + ".ini")
global lv
2021-03-28 00:38:06 +00:00
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]))
2021-03-28 00:38:06 +00:00
if lv["osName"] == "":
lv["osName"] = platform.system().lower()
if lv["osName"] == "darwin": lv["osName"] = "macos"
2021-03-29 23:16:49 +00:00
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"
2021-03-29 23:17:35 +00:00
print("")
for setting in lv:
print(colored(colorama.Fore.BLACK,setting+ "=" +str(lv[setting])))
2021-03-29 23:17:35 +00:00
2021-03-27 23:10:28 +00:00
if len(sys.argv) > 1:
for arg in sys.argv[1:]:
2021-06-17 16:28:09 +00:00
if arg.startswith("-"): continue
2021-03-27 23:10:28 +00:00
argSplit = arg.split("=",1)
if len(argSplit) > 1:
2021-03-28 00:38:06 +00:00
lv[argSplit[0]] = argSplit[1]
else:
lv[argSplit[0]] = True
2021-03-27 23:10:28 +00:00
json.loads(lv["jvmArguments"])
2021-03-28 00:38:06 +00:00
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("")
2021-03-28 00:38:06 +00:00
if not "version" in lv: lv["version"] = input("Version ID: ")
2021-06-17 16:28:34 +00:00
if "-downloadonly" in sys.argv: lv["name"] = "Player"
2021-03-28 00:38:06 +00:00
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)
2021-03-27 23:10:28 +00:00
launcherVariables = {}
versionsPath = p(lv["gamePath"],"versions")
libraryPath = p(lv["gamePath"],"libraries")
nativePath = p(lv["gamePath"],"natives")
versionPath = p(lv["gamePath"],"versions",lv["version"])
2021-06-17 19:52:03 +00:00
nativesOutPath = p(lv["gamePath"],"natives-extracted",lv["version"],lv["osName"]+ "." +lv["jvmArch"])
assetsPath = p(lv["gamePath"],"assets")
2021-03-27 23:10:28 +00:00
print(colored(colorama.Fore.GREEN,"Scanning .json(s)..."))
2021-06-17 16:28:49 +00:00
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)))
2021-06-16 09:11:51 +00:00
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)
2021-03-27 23:10:28 +00:00
2021-06-17 16:28:34 +00:00
if "-downloadonly" in sys.argv:
exit(0)
loadLibraries = {}
2021-03-29 05:51:39 +00:00
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:"))
2021-06-17 10:59:00 +00:00
clientJar = ""
for libraryID in loadLibraries:
library = loadLibraries[libraryID]
if os.path.isfile(library["filePathOS"]):
if library["type"] == "client":
print("Client: " +library["filePathOS"])
2021-06-17 10:59:00 +00:00
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)
2021-06-17 11:00:17 +00:00
continue
libraryList += library["filePathOS"] + separator
libraryList = libraryList[:-1]
2021-03-29 05:51:39 +00:00
print(colored(colorama.Fore.GREEN,"\nExtracting natives..."))
if not os.path.isdir(nativesOutPath): os.makedirs(nativesOutPath)
for library in libraries:
if library["type"] == "native":
2021-06-16 12:44:57 +00:00
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
2021-06-16 10:55:30 +00:00
if not os.path.isfile(library["filePathOS"]):
print(colored(colorama.Fore.RED,"> Native not found: ") +getLibraryPrettyName(library),file=sys.stderr)
2021-06-16 10:55:30 +00:00
continue
print(colored(colorama.Fore.BLACK,getLibraryPrettyName(library)))
2021-04-01 15:23:29 +00:00
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)
2021-03-27 23:10:28 +00:00
print(colored(colorama.Fore.GREEN,"\nSetting up launcher variables..."))
2021-03-28 00:38:06 +00:00
launcherVariables["auth_player_name"] = lv["name"]
launcherVariables["version_name"] = findInChainDeepest(versionsPath,lv["version"],["id"])
launcherVariables["game_directory"] = lv["gamePath"]
2021-03-27 23:10:28 +00:00
launcherVariables["assets_root"] = assetsPath
launcherVariables["auth_access_token"] = "-"
2021-03-28 00:38:06 +00:00
launcherVariables["auth_uuid"] = hashlib.md5(lv["name"].encode('utf-8')).hexdigest()
2021-03-27 23:10:28 +00:00
launcherVariables["user_type"] = "offline"
launcherVariables["version_type"] = clientJson["type"]
2021-03-29 10:20:57 +00:00
launcherVariables["natives_directory"] = nativesOutPath
launcherVariables["launcher_name"] = "offline-minecraft-launcher"
launcherVariables["launcher_version"] = "0.0"
launcherVariables["classpath"] = libraryList
2021-03-30 12:19:11 +00:00
launcherVariables["game_assets"] = assetsPath
launcherVariables["auth_session"] = "-"
2021-03-30 12:18:31 +00:00
launcherVariables["user_properties"] = "{}"
2021-03-27 23:10:28 +00:00
2021-03-30 04:24:13 +00:00
if lv["profileFolder"] == "1":
profilePath = p(lv["gamePath"],"profiles")
launcherVariables["game_directory"] = p(profilePath,lv["name"],lv["version"],".minecraft")
2021-03-30 04:24:13 +00:00
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"]
2021-03-29 10:20:57 +00:00
# JVM arguments:
args = []
2021-06-17 10:59:00 +00:00
if not findArgument(jvmArguments,"-Djava-library.path="):
2021-03-29 10:20:57 +00:00
args.append("-Djava.library.path=" +nativesOutPath)
2021-06-17 10:59:00 +00:00
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")
2021-06-17 10:59:00 +00:00
args.append("-Dminecraft.client.jar=" +clientJar)
if not findArgument(jvmArguments,"-cp"):
2021-03-29 10:20:57 +00:00
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"])
2021-03-27 23:10:28 +00:00
args.append(clientJson["mainClass"])
for arg in arguments:
for var in launcherVariables:
arg = arg.replace("${" +var+ "}",launcherVariables[var])
args.append(arg)
2021-03-27 23:10:28 +00:00
2021-06-17 10:59:00 +00:00
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..."))
2021-03-28 00:38:06 +00:00
if lv["console"] == "1":
2021-04-01 15:23:29 +00:00
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)
2021-03-28 00:38:06 +00:00
else:
pkwargs = {
"stdout": subprocess.DEVNULL,
"stdin": subprocess.DEVNULL,
2021-06-16 10:55:30 +00:00
"stderr": subprocess.DEVNULL
2021-03-28 00:38:06 +00:00
}
if lv["osName"] == "windows": pkwargs["creationflags"] = 0x00000008
subprocess.Popen([lv["java"] + "w"] + args,**pkwargs)
2021-03-27 23:10:28 +00:00
main()