├── .gitignore ├── .idea ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pml.iml └── vcs.xml ├── MANIFEST.in ├── README.md ├── main.py ├── main_login.py ├── pmlauncher ├── __init__.py ├── mdownloader.py ├── mevent.py ├── minecraft.py ├── mlaunch.py ├── mlaunchoption.py ├── mlibrary.py ├── mlogin.py ├── mnative.py ├── mprofile.py ├── mprofileinfo.py ├── mrule.py └── pml.py ├── pycraft └── put_pycraft_here.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | # *.iml 35 | # *.ipr 36 | 37 | # CMake 38 | cmake-build-*/ 39 | 40 | # Mongo Explorer plugin 41 | .idea/**/mongoSettings.xml 42 | 43 | # File-based project format 44 | *.iws 45 | 46 | # IntelliJ 47 | out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Cursive Clojure plugin 56 | .idea/replstate.xml 57 | 58 | # Crashlytics plugin (for Android Studio and IntelliJ) 59 | com_crashlytics_export_strings.xml 60 | crashlytics.properties 61 | crashlytics-build.properties 62 | fabric.properties 63 | 64 | # Editor-based Rest Client 65 | .idea/httpRequests 66 | 67 | # Android studio 3.1+ serialized cache file 68 | .idea/caches/build_file_checksums.ser 69 | 70 | pycraft/networking 71 | pycraft/__pycache__ 72 | pycraft/__init__.py 73 | pycraft/authentication.py 74 | pycraft/compat.py 75 | pycraft/exceptions.py 76 | game/ 77 | logs/ 78 | __pycache__ 79 | args.txt 80 | build/ 81 | dist/ 82 | pmlauncher.egg-info 83 | .vscode/settings.json 84 | .vs 85 | venv/ 86 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pml.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include main.py 2 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pml 2 | Python Minecraft Launcher Library - crossplatform, support forge, all version 3 | this project is ported version from [CmlLib](https://github.com/AlphaBs/MinecraftLauncherLibrary) 4 | 5 | **Not Updated for long time. so if this library does not work well, please create issue.** 6 | 7 | 1.4.* 8 | tested : windows 10 / ubuntu 18.04 LTS / macOS 10.14 Mojave 9 | 10 | Contacts 11 | ------------- 12 | 13 | Email : ksi123456ab@naver.com 14 | Discord : ksi123456ab#3719 15 | 16 | License 17 | -------------- 18 | 19 | Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. 20 | 21 | ****NO COMMERCIAL**** 22 | 23 | Dependency 24 | ------------- 25 | Python 3.6.8 26 | requests 2.22 27 | 28 | Use venv 29 | 30 | How To Use 31 | ------------- 32 | install : 33 | 34 | pip install pmlauncher 35 | 36 | check sample script 37 | [main.py](https://github.com/AlphaBs/pml/blob/master/main.py) 38 | 39 | 40 | Install pycraft 41 | ------------- 42 | To launch premium minecraft, you have to add ['pyCraft' library](https://github.com/ammaraskar/pyCraft). 43 | Download release and unzip the library, and copy files of pyCraft/minecraft directory to pml/pycraft directory 44 | ![주석 2019-12-06 233922](https://user-images.githubusercontent.com/17783561/70331127-2c85c200-1882-11ea-8fc8-8ba221e0b75c.png) 45 | and check sample script 46 | [main_login.py](https://github.com/AlphaBs/pml/blob/master/main_login.py) 47 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from pmlauncher import pml, mlogin, mlaunchoption 2 | import subprocess 3 | import os 4 | import sys 5 | 6 | 7 | # initialize 8 | # p = os.environ["appdata"] + "\\.minecraft" # windows default game directory 9 | p = os.getcwd() + "/game" 10 | #p = os.path.abspath("/home/myu/.minecraft") 11 | pml.initialize(p) 12 | print("Initialized in " + pml.getGamePath()) 13 | 14 | 15 | # login 16 | print("session : test user (tester123)") 17 | session = mlogin.session() 18 | session.username = "tester123" 19 | session.uuid = "uuid" 20 | session.access_token = "access_token" 21 | 22 | 23 | # get profiles 24 | profiles = pml.updateProfiles() 25 | for item in profiles: 26 | print(item.name) 27 | 28 | inputVersion = input("input version : ") 29 | 30 | 31 | # download event handler 32 | # filekind : library , minecraft, index, resource 33 | def downloadEvent(x): 34 | print(x.filekind + " - " + x.filename + " - " + str(x.currentvalue) + "/" + str(x.maxvalue)) 35 | 36 | 37 | pml.downloadEventHandler = downloadEvent 38 | 39 | # download profile and create argument 40 | args = pml.startProfile(inputVersion, 41 | xmx_mb=1024, 42 | session=session, 43 | 44 | launcher_name="pml", # option 45 | server_ip="", 46 | jvm_args="", 47 | screen_width=0, 48 | screen_height=0) 49 | 50 | 51 | # start process 52 | with open("args.txt", "w") as f: # for debug 53 | f.write(args) 54 | print(args) 55 | 56 | mc = subprocess.Popen("java " + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=pml.getGamePath(), shell=True) 57 | 58 | print("launched!") 59 | 60 | 61 | # write output 62 | with mc.stdout as gameLog: 63 | while True: 64 | try: 65 | line = gameLog.readline() 66 | if not line: 67 | break 68 | print(line.decode(sys.getdefaultencoding())) 69 | except: 70 | pass 71 | 72 | if mc.returncode: 73 | print(f"Client returned {mc.returncode}!") 74 | 75 | -------------------------------------------------------------------------------- /main_login.py: -------------------------------------------------------------------------------- 1 | from pmlauncher import pml, mlogin, mlaunchoption 2 | import subprocess 3 | import os 4 | import sys 5 | 6 | 7 | # initialize 8 | # p = os.environ["appdata"] + "\\.minecraft" # windows default game directory 9 | p = os.getcwd() + "/game" 10 | #p = os.path.abspath("/home/myu/.minecraft") 11 | pml.initialize(p) 12 | print("Initialized in " + pml.getGamePath()) 13 | 14 | 15 | # online-mode login (need pycraft : https://github.com/ammaraskar/pyCraft) 16 | # download pyCraft and copy 'minecraft' directory to 'pycraft' directory. 17 | from pycraft import authentication 18 | 19 | mcid = input("input mojang email : ") 20 | mcpw = input("input mojang pw : ") 21 | 22 | auth = authentication.AuthenticationToken() 23 | auth.authenticate(mcid, mcpw) # input mojang email and password 24 | 25 | session = mlogin.session() # set session object 26 | session.username = auth.profile.name 27 | session.uuid = auth.profile.id_ 28 | session.access_token = auth.access_token 29 | 30 | print("login success : " + session.username) 31 | 32 | 33 | # get profiles 34 | profiles = pml.updateProfiles() 35 | for item in profiles: 36 | print(item.name) 37 | 38 | inputVersion = input("input version : ") 39 | 40 | 41 | # download event handler 42 | # filekind : library , minecraft, index, resource 43 | def downloadEvent(x): 44 | print(x.filekind + " - " + x.filename + " - " + str(x.currentvalue) + "/" + str(x.maxvalue)) 45 | 46 | 47 | pml.downloadEventHandler = downloadEvent 48 | 49 | # download profile and create argument 50 | args = pml.startProfile(inputVersion, 51 | xmx_mb=1024, 52 | session=session, 53 | 54 | launcher_name="pml", # option 55 | server_ip="", 56 | jvm_args="", 57 | screen_width=0, 58 | screen_height=0) 59 | 60 | 61 | # start process 62 | with open("args.txt", "w") as f: # for debug 63 | f.write(args) 64 | print(args) 65 | 66 | mc = subprocess.Popen("java " + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=pml.getGamePath(), shell=True) 67 | 68 | print("launched!") 69 | 70 | 71 | # write output 72 | with mc.stdout as gameLog: 73 | while True: 74 | try: 75 | line = gameLog.readline() 76 | if not line: 77 | break 78 | print(line.decode(sys.getdefaultencoding())) 79 | except: 80 | pass 81 | 82 | if mc.returncode: 83 | print(f"Client returned {mc.returncode}!") 84 | 85 | -------------------------------------------------------------------------------- /pmlauncher/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | 3 | __all__ = [ 4 | 'mdownloader', 5 | 'mevent', 6 | 'minecraft', 7 | 'mlaunchoption', 8 | 'mlaunch', 9 | 'mlibrary', 10 | 'mlogin', 11 | 'mrule', 12 | 'mnative', 13 | 'mprofile', 14 | 'mprofileinfo', 15 | 16 | ] 17 | -------------------------------------------------------------------------------- /pmlauncher/mdownloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from pmlauncher import minecraft, mevent 4 | import json 5 | from shutil import copyfile 6 | import shutil 7 | import hashlib 8 | 9 | 10 | def mkd(path): 11 | if not os.path.isdir(path): 12 | os.makedirs(path) 13 | 14 | 15 | def download(url, path): 16 | dirpath = os.path.dirname(path) 17 | mkd(dirpath) 18 | 19 | response = requests.get(url, stream=True) 20 | if int(response.status_code / 100) is not 2: 21 | return 22 | 23 | with open(path, 'wb') as f: 24 | shutil.copyfileobj(response.raw, f) 25 | 26 | 27 | class mdownload: 28 | def __init__(self, _profile): 29 | self.checkHash = True 30 | self.profile = _profile 31 | self.doFireEvents = True 32 | self.downloadFileChangedEvent = mevent.Event() 33 | 34 | def fireEvent(self, kind, name, max, current): 35 | if not self.doFireEvents: 36 | return 37 | 38 | args = mevent.MDownloadEventArgs() 39 | args.filekind = kind 40 | args.filename = name 41 | args.maxvalue = max 42 | args.currentvalue = current 43 | self.downloadFileChangedEvent(args) 44 | 45 | def checkFileSHA1(self, path, fhash): 46 | if not self.checkHash: 47 | return True 48 | if not fhash: 49 | return True 50 | 51 | f = open(path, "rb") 52 | data = f.read() 53 | f.close() 54 | 55 | return fhash == hashlib.sha1(data).hexdigest() 56 | 57 | def checkFileValidation(self, path, fhash): 58 | return os.path.isfile(path) and self.checkFileSHA1(path, fhash) 59 | 60 | def downloadAll(self, downloadAssets): 61 | self.downloadLibraries() 62 | self.downloadMinecraft() 63 | if downloadAssets: 64 | self.downloadIndex() 65 | self.downloadResources() 66 | 67 | def downloadLibraries(self): 68 | count = len(self.profile.libraries) 69 | for i in range(0, count): 70 | lib = self.profile.libraries[i] 71 | if lib.isRequire and lib.path and lib.url and not self.checkFileValidation(lib.path, lib.hash): 72 | download(lib.url, lib.path) 73 | 74 | self.fireEvent("library", lib.name, count, i + 1) 75 | 76 | def downloadIndex(self): 77 | path = os.path.normpath(minecraft.index + "/" + self.profile.assetId + ".json") 78 | if self.profile.assetUrl and not self.checkFileValidation(path, self.profile.assetHash): 79 | download(self.profile.assetUrl, path) 80 | 81 | self.fireEvent("index", self.profile.assetId, 1, 1) 82 | 83 | def downloadResources(self): 84 | indexPath = os.path.normpath(minecraft.index + "/" + self.profile.assetId + ".json") 85 | if not os.path.isfile(indexPath): 86 | return 87 | 88 | f = open(indexPath, "r") 89 | content = f.read() 90 | f.close() 91 | 92 | index = json.loads(content) 93 | 94 | isVirtual = False 95 | v = index.get("virtual") 96 | if v and v == True: 97 | isVirtual = True 98 | 99 | isMapResource = False 100 | m = index.get("map_to_resources") 101 | if m and m == True: 102 | isMapResource = True 103 | 104 | items = list(index.get("objects").items()) 105 | count = len(items) 106 | for i in range(0, count): 107 | key = items[i][0] 108 | value = items[i][1] 109 | 110 | hash = value.get("hash") 111 | hashName = hash[:2] + "/" + hash 112 | hashPath = os.path.normpath(minecraft.assetObject + "/" + hashName) 113 | hashUrl = "http://resources.download.minecraft.net/" + hashName 114 | 115 | if not os.path.isfile(hashPath): 116 | download(hashUrl, hashPath) 117 | 118 | if isVirtual: 119 | resPath = os.path.normpath(minecraft.assetLegacy + "/" + key) 120 | 121 | if not os.path.isfile(resPath): 122 | mkd(os.path.dirname(resPath)) 123 | copyfile(hashPath, resPath) 124 | 125 | if isMapResource: 126 | resPath = os.path.normpath(minecraft.resources + "/" + key) 127 | 128 | if not os.path.isfile(resPath): 129 | mkd(os.path.dirname(resPath)) 130 | copyfile(hashPath, resPath) 131 | 132 | self.fireEvent("resource", "", count, i + 1) 133 | 134 | def downloadMinecraft(self): 135 | if not self.profile.clientDownloadUrl: 136 | return 137 | 138 | id = self.profile.jar 139 | path = os.path.normpath(minecraft.version + "/" + id + "/" + id + ".jar") 140 | if not self.checkFileValidation(path, self.profile.clientHash): 141 | download(self.profile.clientDownloadUrl, path) 142 | 143 | self.fireEvent("minecraft", id, 1, 1) 144 | -------------------------------------------------------------------------------- /pmlauncher/mevent.py: -------------------------------------------------------------------------------- 1 | class Event(list): 2 | def __call__(self, *args, **kwargs): 3 | for f in self: 4 | f(*args, **kwargs) 5 | 6 | def __repr__(self): 7 | return "Event(%s)" % list.__repr__(self) 8 | 9 | 10 | class MDownloadEventArgs: 11 | def __init__(self): 12 | self.filekind = "" 13 | self.filename = "" 14 | self.maxvalue = 1 15 | self.currentvalue = 1 16 | 17 | -------------------------------------------------------------------------------- /pmlauncher/minecraft.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | path = "" 4 | library = "" 5 | version = "" 6 | assets = "" 7 | index = "" 8 | assetObject = "" 9 | assetLegacy = "" 10 | resources = "" 11 | natives = "" 12 | 13 | 14 | def initialize(_path): 15 | global path, library, version, resources, natives 16 | 17 | path = m(_path) 18 | library = m(path + "/libraries") 19 | version = m(path + "/versions") 20 | resources = m(path + "/resources") 21 | natives = m(path + "/natives") 22 | change_assets(path) 23 | 24 | 25 | def change_assets(p): 26 | global assets, assetLegacy, assetObject, index 27 | 28 | assets = os.path.normpath(p + "/assets") 29 | index = os.path.normpath(assets + "/indexes") 30 | assetObject = os.path.normpath(assets + "/objects") 31 | assetLegacy = os.path.normpath(assets + "/virtual/legacy") 32 | 33 | 34 | def m(p): 35 | p = os.path.normpath(p) 36 | if not os.path.isdir(p): 37 | os.makedirs(p) 38 | return p 39 | 40 | -------------------------------------------------------------------------------- /pmlauncher/mlaunch.py: -------------------------------------------------------------------------------- 1 | from pmlauncher import mnative, minecraft, mrule 2 | import string 3 | import os 4 | import re 5 | 6 | supportversion = "1.4" 7 | pre_compiled = re.compile("\\$\\{(.*?)}") 8 | 9 | def e(t): 10 | if " " in t: 11 | return '"' + t + '"' 12 | else: 13 | return t 14 | 15 | 16 | def ea(t): 17 | if " " in t and "=" in t: 18 | s = t.split("=") 19 | return s[0] + '="' + s[1] + '"' 20 | else: 21 | return t 22 | 23 | 24 | def arg_in(arg, dicts): 25 | args = list() 26 | for item in arg: 27 | if type(item) is str: 28 | m = pre_compiled.search(item) # check ${} str 29 | if m: 30 | arg_key = m.group() # get ${KEY} 31 | arg_value = dicts.get(arg_key[2:-1]) # get dicts value of ${KEY} 32 | 33 | if arg_value: 34 | args.append(pre_compiled.sub(arg_value.replace("\\","\\\\"), item)) # replace ${} of whole str to dicts value 35 | else: 36 | args.append(item) # if value of default arg has space, handle whitespace. 37 | # (ex) -Dos.Version=Windows 10 => -Dos.Version="Windows 10" 38 | else: 39 | args.append(ea(item)) # not ${} str 40 | 41 | return args 42 | 43 | 44 | def arg_str(arg, dicts): 45 | return string.Template(arg).safe_substitute(dicts) 46 | 47 | 48 | class launch: 49 | def __init__(self, option): 50 | self.defaultJavaParameter = " ".join([ 51 | "-XX:+UnlockExperimentalVMOptions", 52 | "-XX:+UseG1GC", 53 | "-XX:G1NewSizePercent=20", 54 | "-XX:G1ReservePercent=20", 55 | "-XX:MaxGCPauseMillis=50", 56 | "-XX:G1HeapRegionSize=16M"]) 57 | if mrule.osname == "osx": 58 | self.defaultJavaParameter += " -XstartOnFirstThread" 59 | 60 | option.checkValid() 61 | self.launchOption = option 62 | 63 | def createArg(self): 64 | profile = self.launchOption.start_profile 65 | 66 | args = list() 67 | 68 | # common jvm args 69 | if self.launchOption.jvm_arg: 70 | args.append(self.launchOption.jvm_arg) 71 | else: 72 | args.append(self.defaultJavaParameter) 73 | 74 | args.append("-Xmx" + str(self.launchOption.xmx_mb) + "m") 75 | 76 | # specific jvm args 77 | libArgs = list() 78 | 79 | for item in profile.libraries: 80 | if not item.isNative: 81 | libArgs.append(e(item.path)) 82 | 83 | libArgs.append(e(os.path.normpath(minecraft.version + "/" + profile.jar + "/" + profile.jar + ".jar"))) 84 | libs = os.pathsep.join(libArgs) 85 | 86 | jvmdict = { 87 | "natives_directory" : e(minecraft.natives), 88 | "launcher_name" : "minecraft-launcher", 89 | "launcher_version" : "2", 90 | "classpath" : libs 91 | } 92 | 93 | if profile.jvm_arguments: 94 | args.extend(arg_in(profile.jvm_arguments, jvmdict)) 95 | else: 96 | args.append("-Djava.library.path=" + e(minecraft.natives)) 97 | args.append("-cp " + libs) 98 | 99 | 100 | args.append(profile.mainclass) 101 | 102 | # game args 103 | gamedict = { 104 | "auth_player_name" : self.launchOption.session.username, 105 | "version_name" : profile.id, 106 | "game_directory" : e(minecraft.path), 107 | "assets_root" : e(minecraft.assets), 108 | "assets_index_name" : profile.assetId, 109 | "auth_uuid" : self.launchOption.session.uuid, 110 | "auth_access_token" : self.launchOption.session.access_token, 111 | "user_properties" : "{}", 112 | "user_type" : "Mojang", 113 | "game_assets" : e(minecraft.assetLegacy), 114 | "auth_session" : self.launchOption.session.access_token 115 | } 116 | 117 | if self.launchOption.launcher_name: 118 | gamedict["version_type"] = self.launchOption.launcher_name 119 | else: 120 | gamedict["version_type"] = profile.type 121 | 122 | if profile.game_arguments: # 1.3 123 | args.extend(arg_in(profile.game_arguments, gamedict)) 124 | elif profile.minecraftArguments: 125 | args.append(arg_str(profile.minecraftArguments, gamedict)) 126 | 127 | # options 128 | if self.launchOption.server_ip: 129 | args.append("--server " + self.launchOption.server_ip) 130 | 131 | if self.launchOption.screen_width and self.launchOption.screen_height: 132 | args.append("--width " + self.launchOption.screen_width) 133 | args.append("--height " + self.launchOption.screen_height) 134 | 135 | return " ".join(map(str, args)) 136 | 137 | def createProcess(self): 138 | mnative.clean_natives() 139 | mnative.extract_natives(self.launchOption.start_profile) 140 | 141 | return self.createArg() 142 | 143 | 144 | -------------------------------------------------------------------------------- /pmlauncher/mlaunchoption.py: -------------------------------------------------------------------------------- 1 | class launchoption: 2 | def __init__(self): 3 | self.xmx_mb = 1024 4 | self.start_profile = None 5 | self.session = None 6 | self.launcher_name = "" 7 | self.server_ip = "" 8 | self.jvm_arg = "" 9 | self.screen_width = 0 10 | self.screen_height = 0 11 | 12 | def checkValid(self): 13 | exMsg = "" 14 | if not self.xmx_mb: 15 | exMsg = "xmx_mb is too small" 16 | if not self.start_profile: 17 | exMsg = "start_profile was None" 18 | if not self.session: 19 | exMsg = "session was None" 20 | if " " in self.launcher_name: 21 | exMsg = "launcher_name cannot contain space character" 22 | 23 | if exMsg: 24 | raise ValueError(exMsg) 25 | 26 | -------------------------------------------------------------------------------- /pmlauncher/mlibrary.py: -------------------------------------------------------------------------------- 1 | from pmlauncher import minecraft, mrule 2 | import os 3 | 4 | class mlibrary: 5 | def __init__(self): 6 | self.isNative = False 7 | self.name = "" 8 | self.path = "" 9 | self.url = "" 10 | self.isRequire = True 11 | self.hash = "" 12 | 13 | 14 | checkOSRules = True 15 | defaultLibraryServer = "https://libraries.minecraft.net/" 16 | 17 | 18 | def nameToPath(name, native): # library name to relative path 19 | try: 20 | tmp = name.split(':') 21 | front = tmp[0].replace('.', '/') 22 | back = "" 23 | 24 | for i in range(1, len(tmp)): 25 | if i == len(tmp) - 1: 26 | back += tmp[i] 27 | else: 28 | back += tmp[i] + ":" 29 | 30 | libpath = front + "/" + back.replace(":", "/") + "/" + (back.replace(":", "-")) 31 | if native: 32 | libpath += "-" + native + ".jar" 33 | else: 34 | libpath += ".jar" 35 | return libpath 36 | except Exception: 37 | return "" 38 | 39 | 40 | def createLibrary(name, nativeId, job): 41 | path = job.get("path") 42 | if not path: 43 | path = nameToPath(name, nativeId) 44 | 45 | url = job.get("url") 46 | if not url: 47 | url = defaultLibraryServer + path 48 | elif not url.split('/')[-1]: 49 | url += path 50 | 51 | library = mlibrary() 52 | library.hash = job.get("sha1") 53 | library.name = name 54 | library.path = os.path.normpath(minecraft.library + "/" + path) 55 | library.url = url 56 | if nativeId: 57 | library.isNative = True 58 | else: 59 | library.isNative = False 60 | 61 | return library 62 | 63 | def parselist(json): 64 | list = [] 65 | 66 | for item in json: 67 | name = item.get("name") 68 | if name is None: 69 | continue 70 | 71 | # check rules 72 | rules = item.get("rules") 73 | if checkOSRules and rules: 74 | isRequire = mrule.checkAllowOS(rules) 75 | 76 | if not isRequire: 77 | continue 78 | 79 | # forge library 80 | downloads = item.get("downloads") 81 | if not downloads: # downloads == null 82 | natives = item.get("natives") 83 | 84 | nativeId = None 85 | if natives is not None: # natives != null 86 | nativeId = natives.get(mrule.osname) 87 | 88 | list.append(createLibrary(name, nativeId, item)) 89 | continue 90 | 91 | # native library 92 | classif = downloads.get("classifiers") 93 | if classif: 94 | native_id = None 95 | native_obj = item.get("natives") 96 | if native_obj: 97 | native_id = native_obj.get(mrule.osname) 98 | 99 | if native_id and classif.get(native_id): 100 | native_id = native_id.replace("${arch}", mrule.arch) 101 | job = classif.get(native_id) 102 | list.append(createLibrary(name, native_id, job)) 103 | 104 | # common library 105 | arti = downloads.get("artifact") 106 | if arti: 107 | list.append(createLibrary(name, "", arti)) 108 | 109 | return list 110 | 111 | -------------------------------------------------------------------------------- /pmlauncher/mlogin.py: -------------------------------------------------------------------------------- 1 | class session: 2 | def __init__(self): 3 | self.username = "" 4 | self.access_token = "" 5 | self.uuid = "" 6 | -------------------------------------------------------------------------------- /pmlauncher/mnative.py: -------------------------------------------------------------------------------- 1 | from pmlauncher import minecraft 2 | import zipfile 3 | import os 4 | 5 | 6 | def extract_natives(profile): 7 | for item in profile.libraries: 8 | if item.isNative: 9 | try: 10 | lib = zipfile.ZipFile(item.path) 11 | lib.extractall(minecraft.natives) 12 | except Exception as e: 13 | print(e) 14 | 15 | 16 | def clean_natives(): 17 | for item in os.listdir(minecraft.natives): 18 | path = os.path.join(minecraft.natives, item) 19 | 20 | if os.path.isfile(path): 21 | try: 22 | os.remove(path) 23 | except Exception as e: 24 | print(e) 25 | 26 | -------------------------------------------------------------------------------- /pmlauncher/mprofile.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from pmlauncher import minecraft, mlibrary, mrule 4 | import os 5 | 6 | 7 | def n(t): 8 | if t is None: 9 | return "" 10 | else: 11 | return t 12 | 13 | 14 | def arg_parse(arr): 15 | strlist = list() 16 | for item in arr: 17 | if type(item) == dict: 18 | allow = True 19 | 20 | rule = item.get("rules") 21 | if rule: 22 | allow = mrule.checkAllowOS(rule) 23 | 24 | value = item.get("value") 25 | 26 | if allow and value: 27 | if type(value) == list: 28 | strlist.extend(value) 29 | else: 30 | strlist.append(value) 31 | else: 32 | strlist.append(item) 33 | 34 | return strlist 35 | 36 | 37 | 38 | class profile: 39 | def __init__(self, info): 40 | self.id = "" 41 | self.assetId = "" 42 | self.assetUrl = "" 43 | self.assetHash = "" 44 | self.jvm_arguments = [] 45 | self.game_arguments = [] 46 | self.libraries = [] 47 | self.clientDownloadUrl = "" 48 | self.clientHash = "" 49 | self.parent_profile_id = "" 50 | self.is_inherited = False 51 | self.jar = "" 52 | self.mainclass = "" 53 | self.minecraftArguments = "" 54 | self.releaseTime = "" 55 | self.type = "" 56 | self.parse(info) 57 | 58 | 59 | 60 | def parse(self, info): 61 | if info.isweb: 62 | json = requests.get(info.path).text 63 | else: 64 | f = open(info.path) 65 | json = f.read() 66 | f.close() 67 | 68 | return self.parseFromJson(json) 69 | 70 | def parseFromJson(self, content): 71 | d = json.loads(content) 72 | 73 | self.id = d.get("id") 74 | 75 | assetIndex = d.get("assetIndex") 76 | if assetIndex: 77 | self.assetId = n(assetIndex.get("id")) 78 | self.assetUrl = n(assetIndex.get("url")) 79 | self.assetHash = n(assetIndex.get("sha1")) 80 | 81 | downloads = d.get("downloads") 82 | if downloads: 83 | client = downloads.get("client") 84 | if client: 85 | self.clientDownloadUrl = client["url"] 86 | self.clientHash = client["sha1"] 87 | 88 | self.libraries = mlibrary.parselist(d.get("libraries")) 89 | self.mainclass = n(d.get("mainClass")) 90 | 91 | self.minecraftArguments = d.get("minecraftArguments") 92 | arg = d.get("arguments") 93 | if arg: 94 | if arg.get("game"): 95 | self.game_arguments = arg_parse(arg.get("game")) 96 | if arg.get("jvm"): 97 | self.jvm_arguments = arg_parse(arg.get("jvm")) 98 | 99 | self.releaseTime = n(d.get("releaseTime")) 100 | self.type = n(d.get("type")) 101 | 102 | inherits = d.get("inheritsFrom") 103 | if inherits: 104 | self.is_inherited = True 105 | self.parent_profile_id = inherits 106 | else: 107 | self.jar = self.id 108 | 109 | profilePath = os.path.normpath(minecraft.version + "/" + self.id) 110 | 111 | if not os.path.isdir(profilePath): 112 | os.makedirs(profilePath) 113 | 114 | f = open(os.path.normpath(profilePath + "/" + self.id + ".json"), "w") 115 | f.write(content) 116 | f.close() 117 | 118 | 119 | def inhert(parent, child): 120 | # Overload : assetId, assetUrl, assetHash, clientDownloadUrl, clientHash, mainClass, minecraftArguments 121 | # Combine : libraries, game_arguments, jvm_arguments 122 | 123 | if not child.assetId: 124 | child.assetId = parent.assetId 125 | 126 | if not child.assetUrl: 127 | child.assetUrl = parent.assetUrl 128 | 129 | if not child.assetHash: 130 | child.assetHash = parent.assetHash 131 | 132 | if not child.clientDownloadUrl: 133 | child.clientDownloadUrl = parent.clientDownloadUrl 134 | 135 | if not child.clientHash: 136 | child.clientHash = parent.clientHash 137 | 138 | if not child.mainclass: 139 | child.mainclass = parent.mainclass 140 | 141 | if not child.minecraftArguments: 142 | child.minecraftArguments = parent.minecraftArguments 143 | 144 | if parent.libraries: 145 | if child.libraries: 146 | child.libraries.extend(parent.libraries) 147 | else: 148 | child.libraries = parent.libraries 149 | 150 | if parent.game_arguments: 151 | if child.game_arguments: 152 | child.game_arguments.extend(parent.game_arguments) 153 | else: 154 | child.game_arguments = parent.game_arguments 155 | 156 | if parent.jvm_arguments: 157 | if child.jvm_arguments: 158 | child.jvm_arguments.extend(parent.jvm_arguments) 159 | else: 160 | child.jvm_arguments = parent.jvm_arguments 161 | 162 | 163 | def get_profile(infos, version): 164 | start_profile = None 165 | 166 | for item in infos: 167 | if item.name == version: 168 | start_profile = profile(item) 169 | break 170 | 171 | if start_profile == None: 172 | raise ValueError("cannot find profile named " + version) 173 | 174 | if start_profile.is_inherited: 175 | parent_profile = get_profile(infos, start_profile.parent_profile_id) 176 | inhert(parent_profile, start_profile) 177 | 178 | return start_profile 179 | -------------------------------------------------------------------------------- /pmlauncher/mprofileinfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pmlauncher import minecraft 3 | import json 4 | import requests 5 | 6 | 7 | class mprofileinfo: 8 | def __init__(self): 9 | self.isweb = True 10 | self.name = "" 11 | self.path = "" 12 | 13 | 14 | def getProfilesFromLocal(): 15 | files = os.listdir(minecraft.version) 16 | arr = list() 17 | 18 | if not files: 19 | return arr 20 | 21 | for item in files: 22 | filepath = os.path.normpath(minecraft.version + "/" + item + "/" + item + ".json") 23 | if os.path.isfile(filepath): 24 | profile = mprofileinfo() 25 | profile.isweb = False 26 | profile.name = item 27 | profile.path = filepath 28 | arr.append(profile) 29 | 30 | return arr 31 | 32 | 33 | def getProfilesFromWeb(): 34 | result = list() 35 | url = "https://launchermeta.mojang.com/mc/game/version_manifest.json" 36 | jarr = json.loads(requests.get(url).text) 37 | for item in jarr.get("versions"): 38 | profile = mprofileinfo() 39 | profile.isweb = True 40 | profile.name = item.get("id") 41 | profile.path = item.get("url") 42 | result.append(profile) 43 | 44 | return result 45 | 46 | 47 | def getProfiles(): 48 | arr = getProfilesFromLocal() 49 | for item1 in getProfilesFromWeb(): 50 | exist = False 51 | for item2 in arr: 52 | if item1.name == item2.name: 53 | exist = True 54 | break 55 | if not exist: 56 | arr.append(item1) 57 | return arr -------------------------------------------------------------------------------- /pmlauncher/mrule.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import os 3 | 4 | checkOSRules = True 5 | 6 | is64bit = platform.machine().endswith('64') 7 | if is64bit: 8 | arch = "64" 9 | else: 10 | arch = "32" 11 | 12 | osversion = platform.release() 13 | osname = "" 14 | sysname = platform.system() 15 | if sysname == "Linux": 16 | osname = "linux" 17 | elif sysname == "Darwin": 18 | osname = "osx" 19 | else: 20 | osname = "windows" 21 | #osname = "osx" # fake os to debug 22 | 23 | 24 | def checkAllowOS(arr): 25 | require = True 26 | 27 | for job in arr: 28 | action = True # allow / disallow 29 | containCurrentOS = True 30 | 31 | for key, value in job.items(): 32 | 33 | if key == "action": 34 | if value == "allow": 35 | action = True 36 | else: 37 | action = False 38 | 39 | elif key == "os": 40 | containCurrentOS = check_os_contains(value.items()) 41 | 42 | elif key == "features": 43 | return False 44 | 45 | if not action and containCurrentOS: # disallow os 46 | require = False 47 | elif action and containCurrentOS: # allow os 48 | require = True 49 | elif action and not containCurrentOS: # 50 | require = False 51 | 52 | return require 53 | 54 | 55 | def check_os_contains(arr): 56 | for osKey, osValue in arr: 57 | if osKey == "name" and osValue == osname: 58 | return True 59 | return False 60 | -------------------------------------------------------------------------------- /pmlauncher/pml.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | profiles = None 4 | downloadEventHandler = None 5 | 6 | 7 | def getGamePath(): 8 | return minecraft.path 9 | 10 | 11 | def initialize(path): 12 | minecraft.initialize(path) 13 | 14 | 15 | def updateProfiles(): 16 | global profiles 17 | profiles = mprofileinfo.getProfiles() 18 | return profiles 19 | 20 | 21 | def getProfile(name): 22 | global profiles 23 | if profiles is None: 24 | updateProfiles() 25 | 26 | return mprofile.get_profile(profiles, name) 27 | 28 | 29 | def downloadProfile(profile, downloadAssets = True): 30 | if type(profile) is not mprofile.profile: 31 | raise ValueError("profile must be mprofile.profile type") 32 | 33 | downloader = mdownloader.mdownload(profile) 34 | if downloadEventHandler: 35 | downloader.downloadFileChangedEvent.append(downloadEventHandler) 36 | downloader.downloadAll(downloadAssets) 37 | 38 | 39 | def startProfile(name, **option): 40 | profile = getProfile(name) 41 | downloadProfile(profile) 42 | 43 | l = mlaunchoption.launchoption() 44 | if option.get("launchoption"): 45 | l = option.get("launchoption") 46 | else: 47 | l.__dict__.update(option) 48 | 49 | l.start_profile = profile 50 | launch = mlaunch.launch(l) 51 | return launch.createProcess() 52 | -------------------------------------------------------------------------------- /pycraft/put_pycraft_here.txt: -------------------------------------------------------------------------------- 1 | need file : 2 | 3 | networking directory 4 | __init.py__ 5 | authentication.py 6 | compat.py 7 | exceptions.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="pmlauncher", 5 | version="0.0.9", 6 | url="https://github.com/AlphaBs/pml", 7 | license="non commercial", 8 | author="ksi123456ab", 9 | author_email="ksi123456ab@naver.com", 10 | description="crossplatform Python Minecraft Launcher support forge and all version", 11 | packages=find_packages(exclude=['main.py']), 12 | long_description=open('README.md').read(), 13 | zip_safe=False, 14 | install_requires=['requests'], 15 | python_requires='>=3', 16 | keywords=["minecraft", "launcher"], 17 | classifiers=[ 18 | "Programming Language :: Python :: 3.0", 19 | "Programming Language :: Python :: 3.1", 20 | "Programming Language :: Python :: 3.2", 21 | "Programming Language :: Python :: 3.3", 22 | "Programming Language :: Python :: 3.4", 23 | "Programming Language :: Python :: 3.5", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | "Topic :: Games/Entertainment" 28 | ] 29 | ) 30 | 31 | # python setup.py sdist 32 | # twine upload --skip-existing dist/* 33 | --------------------------------------------------------------------------------