├── config ├── .gitignore ├── readme.txt └── config_local.sh.example ├── .gitignore ├── jsonobject ├── exceptions.py ├── __init__.py ├── utils.py ├── api.py ├── properties.py ├── containers.py ├── base_properties.py └── base.py ├── clone.sh ├── status.sh ├── config.sh ├── quiltutil.py ├── updateLiteloader.py ├── updateJavas.py ├── updateMojang.py ├── COPYING ├── properties.py ├── fabricutil.py ├── javasutil.py ├── index.py ├── updateFabric.py ├── generateLiteloader.py ├── liteloaderutil.py ├── updateQuilt.py ├── enumerateForge.py ├── generateQuilt.py ├── update.sh ├── neoforgeutil.py ├── generateFabric.py ├── generateNeoforge.py ├── updateNeoforge.py ├── forgeutil.py ├── static ├── minecraft.json └── lwjgl-3.2.2.json ├── metautil.py ├── updateForge.py └── generateForge.py /config/.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | *.pub 3 | s3cmd.cfg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | http_cache 2 | multimc 3 | upstream 4 | forgemaven 5 | forge_cache 6 | __pycache__ 7 | *.kdev4 8 | config_local.sh 9 | -------------------------------------------------------------------------------- /config/readme.txt: -------------------------------------------------------------------------------- 1 | This is where you should put the git deploy keys. 2 | 3 | These are expected: 4 | * meta-multimc.key 5 | * meta-multimc.key.pub 6 | * meta-upstream.key 7 | * meta-upstream.key.pub 8 | * s3cmd.cfg -------------------------------------------------------------------------------- /jsonobject/exceptions.py: -------------------------------------------------------------------------------- 1 | class DeleteNotAllowed(Exception): 2 | pass 3 | 4 | 5 | class BadValueError(Exception): 6 | """raised when a value can't be validated or is required""" 7 | 8 | 9 | class WrappingAttributeError(AttributeError): 10 | pass 11 | -------------------------------------------------------------------------------- /clone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR=$(dirname "$0") 4 | cd "${BASEDIR}" 5 | BASEDIR=`pwd` 6 | 7 | source config.sh 8 | 9 | set -x 10 | 11 | if [ ! -d "${UPSTREAM_DIR}" ]; then 12 | git clone ${UPSTREAM_REPO} ${UPSTREAM_DIR} 13 | fi 14 | 15 | if [ ! -d "${MMC_DIR}" ]; then 16 | git clone ${MMC_REPO} ${MMC_DIR} 17 | fi 18 | -------------------------------------------------------------------------------- /status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR=$(dirname "$0") 4 | cd "${BASEDIR}" 5 | BASEDIR=`pwd` 6 | 7 | source config.sh 8 | 9 | echo "Upstream:" 10 | pushd "${UPSTREAM_DIR}" 11 | git status 12 | popd 13 | echo 14 | 15 | 16 | echo "MultiMC:" 17 | pushd "${MMC_DIR}" 18 | git status 19 | popd 20 | echo 21 | 22 | echo "Scripts:" 23 | git status 24 | echo -------------------------------------------------------------------------------- /jsonobject/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .base import JsonObjectMeta 3 | from .containers import JsonArray 4 | from .properties import * 5 | from .base_properties import * 6 | from .api import JsonObject 7 | 8 | __all__ = [ 9 | 'IntegerProperty', 'FloatProperty', 'DecimalProperty', 10 | 'StringProperty', 'BooleanProperty', 11 | 'DateProperty', 'DateTimeProperty', 'TimeProperty', 12 | 'ObjectProperty', 'ListProperty', 'DictProperty', 'SetProperty', 13 | 'JsonObject', 'JsonArray', 'AbstractDateProperty', 'JsonProperty' 14 | ] 15 | -------------------------------------------------------------------------------- /config/config_local.sh.example: -------------------------------------------------------------------------------- 1 | export MODE=master 2 | export GIT_AUTHOR_NAME="Petr Mrázek" 3 | export GIT_AUTHOR_EMAIL="peterix@gmail.com" 4 | export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" 5 | export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" 6 | export DEPLOY_TO_S3=false 7 | export DEPLOY_TO_FOLDER=false 8 | export DEPLOY_TO_GIT=false 9 | 10 | export DISCORD_NOTIFY=false 11 | export DISCORD_NOTIFY_USERNAME="Meta Updater" 12 | export DISCORD_NOTIFY_HOOK_OK="https://discord.com/api/webhooks/.../..." 13 | export DISCORD_NOTIFY_HOOK_ERROR="https://discord.com/api/webhooks/.../..." 14 | 15 | export DEPLOY_FORGE_MAVEN=false 16 | export DEPLOY_FORGE_MAVEN_S3=false -------------------------------------------------------------------------------- /config.sh: -------------------------------------------------------------------------------- 1 | export UPSTREAM_DIR=upstream 2 | export UPSTREAM_REPO=git@github.com:MultiMC/meta-upstream.git 3 | export MMC_DIR=multimc 4 | export MMC_REPO=git@github.com:MultiMC/meta-multimc.git 5 | export S3_master=s3://meta.multimc.org/v1/ 6 | export S3_develop=s3://meta.multimc.org/dev/ 7 | export BRANCH_master=master 8 | export BRANCH_develop=develop 9 | export DEPLOY_TO_S3=false 10 | export DEPLOY_TO_FOLDER=false 11 | export DEPLOY_TO_GIT=false 12 | export DEPLOY_FOLDER_master=/var/www/meta/v1/ 13 | export DEPLOY_FOLDER_develop=/var/www/meta/dev/ 14 | export DEPLOY_FOLDER_USER=http 15 | export DEPLOY_FOLDER_GROUP=http 16 | 17 | export UPDATE_FORGE_MAVEN=true 18 | export DEPLOY_FORGE_MAVEN=false 19 | export DEPLOY_FORGE_MAVEN_S3=false 20 | export S3_FORGE_MAVEN=s3://forgemaven.multimc.org/ 21 | -------------------------------------------------------------------------------- /quiltutil.py: -------------------------------------------------------------------------------- 1 | from metautil import * 2 | import jsonobject 3 | 4 | # barebones semver-like parser 5 | def isQuiltVerStable(ver): 6 | s = ver.split("+") 7 | return ("-" not in s[0]) 8 | 9 | class QuiltInstallerArguments(JsonObject): 10 | client = ListProperty(StringProperty) 11 | common = ListProperty(StringProperty) 12 | server = ListProperty(StringProperty) 13 | 14 | class QuiltInstallerLibraries(JsonObject): 15 | client = ListProperty(MultiMCLibrary) 16 | common = ListProperty(MultiMCLibrary) 17 | server = ListProperty(MultiMCLibrary) 18 | development = ListProperty(MultiMCLibrary, required=False) 19 | 20 | class QuiltInstallerDataV1(JsonObject): 21 | version = IntegerProperty(required=True) 22 | libraries = ObjectProperty(QuiltInstallerLibraries, required=True) 23 | mainClass = jsonobject.DefaultProperty() 24 | arguments = ObjectProperty(QuiltInstallerArguments, required=False) 25 | min_java_version = IntegerProperty(required=False) 26 | 27 | class QuiltJarInfo(JsonObject): 28 | releaseTime = ISOTimestampProperty() 29 | size = IntegerProperty() 30 | sha256 = StringProperty() 31 | sha1 = StringProperty() 32 | -------------------------------------------------------------------------------- /updateLiteloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Get the source files necessary for generating Forge versions 4 | ''' 5 | from __future__ import print_function 6 | import sys 7 | 8 | import requests 9 | from cachecontrol import CacheControl 10 | from cachecontrol.caches import FileCache 11 | 12 | import json 13 | from jsonobject import * 14 | from liteloaderutil import * 15 | import os.path 16 | import copy 17 | 18 | def eprint(*args, **kwargs): 19 | print(*args, file=sys.stderr, **kwargs) 20 | 21 | forever_cache = FileCache('http_cache', forever=True) 22 | sess = CacheControl(requests.Session(), forever_cache) 23 | 24 | 25 | # get the remote version list 26 | r = sess.get('http://dl.liteloader.com/versions/versions.json') 27 | r.raise_for_status() 28 | 29 | # make sure it's JSON 30 | main_json = r.json() 31 | 32 | # make sure we understand the schema 33 | remoteVersionlist = LiteloaderIndex(copy.deepcopy(main_json)) 34 | newStr = json.dumps(remoteVersionlist.to_json(), sort_keys=True) 35 | origStr = json.dumps(main_json, sort_keys=True) 36 | assert newStr == origStr 37 | 38 | # save the json it to file 39 | with open("upstream/liteloader/versions.json", 'w', encoding='utf-8') as f: 40 | json.dump(main_json, f, sort_keys=True, indent=4) 41 | -------------------------------------------------------------------------------- /jsonobject/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .exceptions import BadValueError 3 | 4 | 5 | def check_type(obj, item_type, message): 6 | if obj is None: 7 | return item_type() 8 | elif not isinstance(obj, item_type): 9 | raise BadValueError('{}. Found object of type: {}'.format(message, type(obj))) 10 | else: 11 | return obj 12 | 13 | 14 | class SimpleDict(dict): 15 | """ 16 | Re-implements destructive methods of dict 17 | to use only setitem and getitem and delitem 18 | """ 19 | def update(self, E=None, **F): 20 | for dct in (E, F): 21 | if dct: 22 | for key, value in dct.items(): 23 | self[key] = value 24 | 25 | def clear(self): 26 | for key in list(self.keys()): 27 | del self[key] 28 | 29 | def pop(self, key, *args): 30 | if len(args) > 1: 31 | raise TypeError('pop expected at most 2 arguments, got 3') 32 | try: 33 | val = self[key] 34 | del self[key] 35 | return val 36 | except KeyError: 37 | try: 38 | return args[0] 39 | except IndexError: 40 | raise KeyError(key) 41 | 42 | def popitem(self): 43 | try: 44 | arbitrary_key = list(self.keys())[0] 45 | except IndexError: 46 | raise KeyError('popitem(): dictionary is empty') 47 | val = self[arbitrary_key] 48 | del self[arbitrary_key] 49 | return (arbitrary_key, val) 50 | 51 | def setdefault(self, key, default=None): 52 | try: 53 | return self[key] 54 | except KeyError: 55 | self[key] = default 56 | return default 57 | -------------------------------------------------------------------------------- /jsonobject/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .base import JsonObjectBase, _LimitedDictInterfaceMixin 3 | 4 | import six 5 | import decimal 6 | import datetime 7 | 8 | from . import properties 9 | import re 10 | 11 | 12 | re_date = re.compile(r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$') 13 | re_time = re.compile( 14 | r'^([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?$') 15 | re_datetime = re.compile( 16 | r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])' 17 | r'(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?' 18 | r'([zZ]|([\+-])([01]\d|2[0-3])\D?([0-5]\d)?)?)?$' 19 | ) 20 | re_decimal = re.compile('^(\d+)\.(\d+)$') 21 | if six.PY3: 22 | unicode = str 23 | long = int 24 | 25 | 26 | class JsonObject(JsonObjectBase, _LimitedDictInterfaceMixin): 27 | def __getstate__(self): 28 | return self.to_json() 29 | 30 | def __setstate__(self, dct): 31 | self.__init__(dct) 32 | 33 | class Meta(object): 34 | properties = { 35 | decimal.Decimal: properties.DecimalProperty, 36 | datetime.datetime: properties.DateTimeProperty, 37 | datetime.date: properties.DateProperty, 38 | datetime.time: properties.TimeProperty, 39 | str: properties.StringProperty, 40 | unicode: properties.StringProperty, 41 | bool: properties.BooleanProperty, 42 | int: properties.IntegerProperty, 43 | long: properties.IntegerProperty, 44 | float: properties.FloatProperty, 45 | list: properties.ListProperty, 46 | dict: properties.DictProperty, 47 | set: properties.SetProperty, 48 | } 49 | string_conversions = ( 50 | (re_date, datetime.date), 51 | (re_time, datetime.time), 52 | (re_datetime, datetime.datetime), 53 | (re_decimal, decimal.Decimal), 54 | ) 55 | -------------------------------------------------------------------------------- /updateJavas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import json 3 | 4 | from pprint import pprint 5 | from javasutil import * 6 | 7 | import requests 8 | from cachecontrol import CacheControl 9 | from cachecontrol.caches import FileCache 10 | 11 | forever_cache = FileCache('http_cache', forever=True) 12 | sess = CacheControl(requests.Session(), forever_cache) 13 | 14 | def get_version_file(path, url): 15 | with open(path, 'w', encoding='utf-8') as f: 16 | r = sess.get(url) 17 | r.raise_for_status() 18 | version_json = r.json() 19 | assetId = version_json["assetIndex"]["id"] 20 | assetUrl = version_json["assetIndex"]["url"] 21 | json.dump(version_json, f, sort_keys=True, indent=4) 22 | return assetId, assetUrl 23 | 24 | def get_file(path, url): 25 | with open(path, 'w', encoding='utf-8') as f: 26 | r = sess.get(url) 27 | r.raise_for_status() 28 | version_json = r.json() 29 | json.dump(version_json, f, sort_keys=True, indent=4) 30 | 31 | # get the local version list 32 | localJRElist = None 33 | try: 34 | with open("upstream/jres/all.json", 'r', encoding='utf-8') as localIndexFile: 35 | localJRElist = JREIndexWrap(json.load(localIndexFile)) 36 | except: 37 | localJRElist = JREIndexWrap({}) 38 | # localIDs = set(localJRElist.versions.keys()) 39 | 40 | # get the remote package list 41 | r = sess.get('https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json') 42 | r.raise_for_status() 43 | main_json = r.json() 44 | remoteJRElist = JREIndexWrap(main_json) 45 | # remoteIDs = set(remoteJRElist.versions.keys()) 46 | 47 | # versions not present locally but present remotely are new 48 | # newIDs = remoteIDs.difference(localIDs) 49 | 50 | # versions present both locally and remotely need to be checked 51 | # checkedIDs = remoteIDs.difference(newIDs) 52 | 53 | # versions that actually need to be updated have updated timestamps or are new 54 | # updatedIDs = newIDs 55 | # for id in checkedIDs: 56 | # remoteVersion = remoteJRElist.versions[id] 57 | # localVersion = localJRElist.versions[id] 58 | # if remoteVersion.time > localVersion.time: 59 | # updatedIDs.add(id) 60 | 61 | # update versions 62 | # for id in updatedIDs: 63 | # version = remoteJRElist.versions[id] 64 | # print("Updating " + version.id + " to timestamp " + version.releaseTime.strftime('%s')) 65 | # assetId, assetUrl = get_version_file( "upstream/jres/versions/" + id + '.json', version.url) 66 | # assets[assetId] = assetUrl 67 | 68 | with open("upstream/jres/all.json", 'w', encoding='utf-8') as f: 69 | json.dump(main_json, f, sort_keys=True, indent=4) 70 | -------------------------------------------------------------------------------- /updateMojang.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import requests 3 | from cachecontrol import CacheControl 4 | import json 5 | from metautil import * 6 | 7 | from cachecontrol.caches import FileCache 8 | 9 | forever_cache = FileCache('http_cache', forever=True) 10 | sess = CacheControl(requests.Session(), forever_cache) 11 | 12 | def get_version_file(path, url): 13 | with open(path, 'w', encoding='utf-8') as f: 14 | r = sess.get(url) 15 | r.raise_for_status() 16 | version_json = r.json() 17 | assetId = version_json["assetIndex"]["id"] 18 | assetUrl = version_json["assetIndex"]["url"] 19 | json.dump(version_json, f, sort_keys=True, indent=4) 20 | return assetId, assetUrl 21 | 22 | def get_file(path, url): 23 | with open(path, 'w', encoding='utf-8') as f: 24 | r = sess.get(url) 25 | r.raise_for_status() 26 | version_json = r.json() 27 | json.dump(version_json, f, sort_keys=True, indent=4) 28 | 29 | # get the local version list 30 | localVersionlist = None 31 | try: 32 | with open("upstream/mojang/version_manifest_v2.json", 'r', encoding='utf-8') as localIndexFile: 33 | localVersionlist = MojangIndexWrap(json.load(localIndexFile)) 34 | except: 35 | localVersionlist = MojangIndexWrap({}) 36 | localIDs = set(localVersionlist.versions.keys()) 37 | 38 | # get the remote version list 39 | r = sess.get('https://launchermeta.mojang.com/mc/game/version_manifest_v2.json') 40 | r.raise_for_status() 41 | main_json = r.json() 42 | remoteVersionlist = MojangIndexWrap(main_json) 43 | remoteIDs = set(remoteVersionlist.versions.keys()) 44 | 45 | # versions not present locally but present remotely are new 46 | newIDs = remoteIDs.difference(localIDs) 47 | 48 | # versions present both locally and remotely need to be checked 49 | checkedIDs = remoteIDs.difference(newIDs) 50 | 51 | # versions that actually need to be updated have updated timestamps or are new 52 | updatedIDs = newIDs 53 | for id in checkedIDs: 54 | remoteVersion = remoteVersionlist.versions[id] 55 | localVersion = localVersionlist.versions[id] 56 | if remoteVersion.time > localVersion.time: 57 | updatedIDs.add(id) 58 | 59 | # update versions and the linked assets files 60 | assets = {} 61 | for id in updatedIDs: 62 | version = remoteVersionlist.versions[id] 63 | print("Updating " + version.id + " to timestamp " + version.releaseTime.strftime('%s')) 64 | assetId, assetUrl = get_version_file( "upstream/mojang/versions/" + id + '.json', version.url) 65 | assets[assetId] = assetUrl 66 | 67 | for assetId, assetUrl in iter(assets.items()): 68 | print("assets", assetId, assetUrl) 69 | get_file( "upstream/mojang/assets/" + assetId + '.json', assetUrl) 70 | 71 | with open("upstream/mojang/version_manifest_v2.json", 'w', encoding='utf-8') as f: 72 | json.dump(main_json, f, sort_keys=True, indent=4) 73 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | 3 | This license governs use of the accompanying software. If you use the 4 | software, you accept this license. If you do not accept the license, do not 5 | use the software. 6 | 7 | 1. Definitions 8 | The terms "reproduce," "reproduction," "derivative works," and "distribution" 9 | have the same meaning here as under U.S. copyright law. A "contribution" is 10 | the original software, or any additions or changes to the software. A 11 | "contributor" is any person that distributes its contribution under this 12 | license. "Licensed patents" are a contributor's patent claims that read 13 | directly on its contribution. 14 | 15 | 2. Grant of Rights 16 | (A) Copyright Grant- Subject to the terms of this license, including the 17 | license conditions and limitations in section 3, each contributor grants 18 | you a non-exclusive, worldwide, royalty-free copyright license to 19 | reproduce its contribution, prepare derivative works of its contribution, 20 | and distribute its contribution or any derivative works that you create. 21 | 22 | (B) Patent Grant- Subject to the terms of this license, including the 23 | license conditions and limitations in section 3, each contributor grants 24 | you a non-exclusive, worldwide, royalty-free license under its licensed 25 | patents to make, have made, use, sell, offer for sale, import, and/or 26 | otherwise dispose of its contribution in the software or derivative works 27 | of the contribution in the software. 28 | 29 | 3. Conditions and Limitations 30 | (A) No Trademark License- This license does not grant you rights to use 31 | any contributors' name, logo, or trademarks. 32 | 33 | (B) If you bring a patent claim against any contributor over patents that 34 | you claim are infringed by the software, your patent license from such 35 | contributor to the software ends automatically. 36 | 37 | (C) If you distribute any portion of the software, you must retain all 38 | copyright, patent, trademark, and attribution notices that are present in 39 | the software. 40 | 41 | (D) If you distribute any portion of the software in source code form, 42 | you may do so only under this license by including a complete copy of 43 | this license with your distribution. If you distribute any portion of the 44 | software in compiled or object code form, you may only do so under a 45 | license that complies with this license. 46 | 47 | (E) The software is licensed "as-is." You bear the risk of using it. The 48 | contributors give no express warranties, guarantees, or conditions. You 49 | may have additional consumer rights under your local laws which this 50 | license cannot change. To the extent permitted under your local laws, the 51 | contributors exclude the implied warranties of merchantability, fitness 52 | for a particular purpose and non-infringement. -------------------------------------------------------------------------------- /properties.py: -------------------------------------------------------------------------------- 1 | from jsonobject import * 2 | import datetime 3 | import iso8601 4 | 5 | class ISOTimestampProperty(AbstractDateProperty): 6 | _type = datetime.datetime 7 | 8 | def _wrap(self, value): 9 | try: 10 | return iso8601.parse_date(value) 11 | except ValueError as e: 12 | raise ValueError( 13 | 'Invalid ISO date/time {0!r} [{1}]'.format(value, e)) 14 | 15 | def _unwrap(self, value): 16 | return value, value.isoformat() 17 | 18 | 19 | class GradleSpecifier: 20 | ''' 21 | A gradle specifier - a maven coordinate. Like one of these: 22 | "org.lwjgl.lwjgl:lwjgl:2.9.0" 23 | "net.java.jinput:jinput:2.0.5" 24 | "net.minecraft:launchwrapper:1.5" 25 | ''' 26 | 27 | def __init__(self, name): 28 | atSplit = name.split('@') 29 | 30 | components = atSplit[0].split(':') 31 | self.group = components[0] 32 | self.artifact = components[1] 33 | self.version = components[2] 34 | 35 | self.extension = 'jar' 36 | if len(atSplit) == 2: 37 | self.extension = atSplit[1] 38 | 39 | if len(components) == 4: 40 | self.classifier = components[3] 41 | else: 42 | self.classifier = None 43 | 44 | def toString(self): 45 | extensionStr = '' 46 | if self.extension != 'jar': 47 | extensionStr = "@%s" % self.extension 48 | if self.classifier: 49 | return "%s:%s:%s:%s%s" % (self.group, self.artifact, self.version, self.classifier, extensionStr) 50 | else: 51 | return "%s:%s:%s%s" % (self.group, self.artifact, self.version, extensionStr) 52 | 53 | def getFilename(self): 54 | if self.classifier: 55 | return "%s-%s-%s.%s" % (self.artifact, self.version, self.classifier, self.extension) 56 | else: 57 | return "%s-%s.%s" % (self.artifact, self.version, self.extension) 58 | 59 | def getBase(self): 60 | return "%s/%s/%s/" % (self.group.replace('.','/'), self.artifact, self.version) 61 | 62 | def getPath(self): 63 | return self.getBase() + self.getFilename() 64 | 65 | 66 | def __repr__(self): 67 | return "GradleSpecifier('" + self.toString() + "')" 68 | 69 | def isLwjgl(self): 70 | return self.group in ("org.lwjgl", "org.lwjgl.lwjgl", "net.java.jinput", "net.java.jutils") 71 | 72 | def isLog4j(self): 73 | return self.group == "org.apache.logging.log4j" 74 | 75 | 76 | def __lt__(self, other): 77 | return self.toString() < other.toString() 78 | 79 | def __eq__(self, other): 80 | return self.group == other.group and self.artifact == other.artifact and self.version == other.version and self.classifier == other.classifier 81 | 82 | def __ne__(self, other): 83 | return not self.__eq__(other) 84 | 85 | def __hash__(self): 86 | return self.toString().__hash__() 87 | 88 | class GradleSpecifierProperty(JsonProperty): 89 | def wrap(self, value): 90 | return GradleSpecifier(value) 91 | 92 | def unwrap(self, value): 93 | return value, value.toString() 94 | -------------------------------------------------------------------------------- /fabricutil.py: -------------------------------------------------------------------------------- 1 | from metautil import * 2 | import jsonobject 3 | 4 | class FabricInstallerArguments(JsonObject): 5 | client = ListProperty(StringProperty) 6 | common = ListProperty(StringProperty) 7 | server = ListProperty(StringProperty) 8 | 9 | class FabricInstallerLaunchwrapper(JsonObject): 10 | tweakers = ObjectProperty(FabricInstallerArguments, required=True) 11 | 12 | class FabricInstallerLibrariesV1(JsonObject): 13 | client = ListProperty(MultiMCLibrary) 14 | common = ListProperty(MultiMCLibrary) 15 | server = ListProperty(MultiMCLibrary) 16 | development = ListProperty(MultiMCLibrary, required=False) 17 | 18 | class FabricInstallerDataV1(JsonObject): 19 | version = IntegerProperty(required=True) 20 | libraries = ObjectProperty(FabricInstallerLibrariesV1, required=True) 21 | mainClass = jsonobject.DefaultProperty() 22 | arguments = ObjectProperty(FabricInstallerArguments, required=False) 23 | launchwrapper = ObjectProperty(FabricInstallerLaunchwrapper, required=False) 24 | 25 | ''' 26 | 27 | This format was introduced with version 0.15.0 28 | { 29 | "name": "org.ow2.asm:asm:9.6", 30 | "md5": "6f8bccf756f170d4185bb24c8c2d2020", 31 | "sha1": "aa205cf0a06dbd8e04ece91c0b37c3f5d567546a", 32 | "sha256": "3c6fac2424db3d4a853b669f4e3d1d9c3c552235e19a319673f887083c2303a1", 33 | "sha512": "01a5ea6f5b43bf094c52a50e18325a60af7bb02e74d24f9bc2c727d43e514578fd968b30ff22f9d2720caec071458f9ff82d11a21fbb1ebc42d8203e737c4b52", 34 | "size": 123598, 35 | "url": "https://maven.fabricmc.net/" 36 | }, 37 | ''' 38 | 39 | class FabricLibrary (JsonObject): 40 | name = GradleSpecifierProperty(required = True) 41 | url = StringProperty(exclude_if_none=True, default=None) 42 | size = IntegerProperty() 43 | md5 = StringProperty(exclude_if_none=True, default=None) 44 | sha1 = StringProperty(exclude_if_none=True, default=None) 45 | sha256 = StringProperty(exclude_if_none=True, default=None) 46 | sha512 = StringProperty(exclude_if_none=True, default=None) 47 | 48 | def toMmcLibrary(self) -> MultiMCLibrary: 49 | return MultiMCLibrary( 50 | name = self.name, 51 | downloads = MojangLibraryDownloads( 52 | artifact = MojangArtifact( 53 | sha1 = self.sha1, 54 | size = self.size, 55 | url = self.url + self.name.getPath(), 56 | ), 57 | ), 58 | ) 59 | 60 | 61 | class FabricInstallerLibrariesV2(JsonObject): 62 | client = ListProperty(FabricLibrary) 63 | common = ListProperty(FabricLibrary) 64 | server = ListProperty(FabricLibrary) 65 | development = ListProperty(FabricLibrary, required=False) 66 | 67 | class FabricInstallerDataV2(JsonObject): 68 | version = IntegerProperty(required=True) 69 | libraries = ObjectProperty(FabricInstallerLibrariesV2, required=True) 70 | mainClass = jsonobject.DefaultProperty() 71 | min_java_version = IntegerProperty(required=True) 72 | 73 | 74 | class FabricJarInfo(JsonObject): 75 | releaseTime = ISOTimestampProperty() 76 | size = IntegerProperty() 77 | sha256 = StringProperty() 78 | sha1 = StringProperty() 79 | -------------------------------------------------------------------------------- /javasutil.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pprint import pprint 3 | from jsonobject import * 4 | from properties import * 5 | 6 | class PistonAvailability(JsonObject): 7 | group = IntegerProperty() 8 | progress = IntegerProperty() 9 | 10 | class PistonArtifact(JsonObject): 11 | sha1 = StringProperty() 12 | size = IntegerProperty() 13 | url = StringProperty() 14 | 15 | class PistonVersion(JsonObject): 16 | name = StringProperty() 17 | released = ISOTimestampProperty() 18 | 19 | class PistonRelease(JsonObject): 20 | availability = ObjectProperty(PistonAvailability) 21 | manifest = ObjectProperty(PistonArtifact) 22 | version = ObjectProperty(PistonVersion) 23 | 24 | # NOTE: this will blow up when Mojang adds a JRE-like product 25 | class PlatformJREManifest(JsonObject): 26 | jre_legacy = ListProperty(PistonRelease, name="jre-legacy") 27 | java_runtime_alpha = ListProperty(PistonRelease, name="java-runtime-alpha") 28 | java_runtime_beta = ListProperty(PistonRelease, name="java-runtime-beta") 29 | java_runtime_gamma = ListProperty(PistonRelease, name="java-runtime-gamma") 30 | java_runtime_gamma_snapshot = ListProperty(PistonRelease, name="java-runtime-gamma-snapshot") 31 | java_runtime_delta = ListProperty(PistonRelease, name="java-runtime-delta") 32 | minecraft_java_exe = ListProperty(PistonRelease, name="minecraft-java-exe") 33 | 34 | # NOTE: this will blow up when Mojang adds a new platform 35 | class AllPlatformsJREManifest(JsonObject): 36 | gamecore = ObjectProperty(PlatformJREManifest, name="gamecore") 37 | linux_amd64 = ObjectProperty(PlatformJREManifest, name="linux") 38 | linux_x86 = ObjectProperty(PlatformJREManifest, name="linux-i386") 39 | macos_amd64 = ObjectProperty(PlatformJREManifest, name="mac-os") 40 | macos_arm64 = ObjectProperty(PlatformJREManifest, name="mac-os-arm64") 41 | windows_arm64 = ObjectProperty(PlatformJREManifest, name="windows-arm64") 42 | windows_amd64 = ObjectProperty(PlatformJREManifest, name="windows-x64") 43 | windows_x86 = ObjectProperty(PlatformJREManifest, name="windows-x86") 44 | 45 | class JavaVersion(JsonObject): 46 | download = ObjectProperty(PistonArtifact) 47 | 48 | 49 | class VersionIndex(JsonObject): 50 | java8 = ObjectProperty(JavaVersion, name="8") # legacy -> 8 51 | java16 = ObjectProperty(JavaVersion, name="16") # alpha -> 16 52 | java17 = ObjectProperty(JavaVersion, name="17") # beta, gamma-snapshot, gamma -> 17 53 | java21 = ObjectProperty(JavaVersion, name="21") # delta -> 21 54 | 55 | class PlatformIndex(JsonObject): 56 | arm64 = ObjectProperty() 57 | amd64 = ObjectProperty() 58 | x86 = ObjectProperty() 59 | 60 | class AllIndex(JsonObject): 61 | linux = ObjectProperty(PlatformIndex) 62 | windows = ObjectProperty(PlatformIndex) 63 | mac = ObjectProperty(PlatformIndex) 64 | 65 | class JREIndexWrap: 66 | def __init__(self, json): 67 | self.original = AllPlatformsJREManifest.wrap(json) 68 | # self.latest = self.index.latest 69 | # versionsDict = {} 70 | # for version in self.index.versions: 71 | # versionsDict[version.id] = version 72 | # self.versions = versionsDict 73 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import hashlib 4 | import os 5 | import json 6 | 7 | from metautil import * 8 | from operator import itemgetter 9 | 10 | # take the hash type (like hashlib.md5) and filename, return hex string of hash 11 | def HashFile(hash, fname): 12 | hash_instance = hash() 13 | with open(fname, "rb") as f: 14 | for chunk in iter(lambda: f.read(4096), b""): 15 | hash_instance.update(chunk) 16 | return hash_instance.hexdigest() 17 | 18 | # ignore these files when indexing versions 19 | ignore = set(["index.json", "package.json", ".git"]) 20 | 21 | # initialize output structures - package list level 22 | packages = MultiMCPackageIndex() 23 | 24 | # walk thorugh all the package folders 25 | for package in sorted(os.listdir('multimc')): 26 | if package in ignore: 27 | continue 28 | 29 | sharedData = readSharedPackageData(package) 30 | recommendedVersions = set() 31 | if sharedData.recommended: 32 | recommendedVersions = set(sharedData.recommended) 33 | 34 | # initialize output structures - version list level 35 | versionList = MultiMCVersionIndex() 36 | versionList.uid = package 37 | versionList.name = sharedData.name 38 | 39 | # walk through all the versions of the package 40 | for filename in os.listdir("multimc/%s" % (package)): 41 | if filename in ignore: 42 | continue 43 | 44 | # parse and hash the version file 45 | filepath = "multimc/%s/%s" % (package, filename) 46 | filehash = HashFile(hashlib.sha256, filepath) 47 | versionFile = None 48 | with open(filepath) as json_file: 49 | versionFile = MultiMCVersionFile(json.load(json_file)) 50 | 51 | # pull information from the version file 52 | versionEntry = MultiMCVersionIndexEntry() 53 | if versionFile.version in recommendedVersions: 54 | versionEntry.recommended = True 55 | versionEntry.version = versionFile.version 56 | versionEntry.type = versionFile.type 57 | versionEntry.releaseTime = versionFile.releaseTime 58 | versionEntry.sha256 = filehash 59 | versionEntry.requires = versionFile.requires 60 | versionEntry.conflicts = versionFile.conflicts 61 | versionEntry.volatile = versionFile.volatile 62 | versionList.versions.append(versionEntry) 63 | 64 | # sort the versions in descending order by time of release 65 | versionList.versions = sorted(versionList.versions, key=itemgetter('releaseTime'), reverse=True) 66 | 67 | # write the version index for the package 68 | outFilePath = "multimc/%s/index.json" % (package) 69 | with open(outFilePath, 'w') as outfile: 70 | json.dump(versionList.to_json(), outfile, sort_keys=True, indent=4) 71 | 72 | # insert entry into the package index 73 | packageEntry = MultiMCPackageIndexEntry( 74 | { 75 | "uid" : package, 76 | "name" : sharedData.name, 77 | "sha256": HashFile(hashlib.sha256, outFilePath) 78 | } 79 | ) 80 | packages.packages.append(packageEntry) 81 | 82 | # write the repository package index 83 | with open("multimc/index.json", 'w') as outfile: 84 | json.dump(packages.to_json(), outfile, sort_keys=True, indent=4) 85 | -------------------------------------------------------------------------------- /updateFabric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, requests 3 | from cachecontrol import CacheControl 4 | import datetime 5 | import hashlib, json 6 | import zipfile 7 | from fabricutil import * 8 | 9 | from cachecontrol.caches import FileCache 10 | 11 | forever_cache = FileCache('http_cache', forever=True) 12 | sess = CacheControl(requests.Session(), forever_cache) 13 | 14 | def mkdirs(path): 15 | if not os.path.exists(path): 16 | os.makedirs(path) 17 | 18 | def filehash(filename, hashtype, blocksize=65536): 19 | hash = hashtype() 20 | with open(filename, "rb") as f: 21 | for block in iter(lambda: f.read(blocksize), b""): 22 | hash.update(block) 23 | return hash.hexdigest() 24 | 25 | def get_maven_url(mavenKey, server, ext): 26 | mavenParts = mavenKey.split(":", 3) 27 | mavenVerUrl = server + mavenParts[0].replace(".", "/") + "/" + mavenParts[1] + "/" + mavenParts[2] + "/" 28 | mavenUrl = mavenVerUrl + mavenParts[1] + "-" + mavenParts[2] + ext 29 | return mavenUrl 30 | 31 | def get_json_file(path, url): 32 | with open(path, 'w', encoding='utf-8') as f: 33 | r = sess.get(url) 34 | r.raise_for_status() 35 | version_json = r.json() 36 | json.dump(version_json, f, sort_keys=True, indent=4) 37 | return version_json 38 | 39 | def get_binary_file(path, url): 40 | with open(path, 'w', encoding='utf-8') as f: 41 | r = sess.get(url) 42 | r.raise_for_status() 43 | with open(path, 'wb') as f: 44 | for chunk in r.iter_content(chunk_size=128): 45 | f.write(chunk) 46 | 47 | def compute_jar_file(path, url): 48 | jarPath = path + ".jar" 49 | get_binary_file(jarPath, url) 50 | tstamp = datetime.datetime.fromtimestamp(0) 51 | with zipfile.ZipFile(jarPath, 'r') as jar: 52 | allinfo = jar.infolist() 53 | for info in allinfo: 54 | tstampNew = datetime.datetime(*info.date_time) 55 | if tstampNew > tstamp: 56 | tstamp = tstampNew 57 | data = FabricJarInfo() 58 | data.releaseTime = tstamp 59 | data.sha1 = filehash(jarPath, hashlib.sha1) 60 | data.sha256 = filehash(jarPath, hashlib.sha256) 61 | data.size = os.path.getsize(jarPath) 62 | with open(path + ".json", 'w') as outfile: 63 | json.dump(data.to_json(), outfile, sort_keys=True, indent=4) 64 | 65 | mkdirs("upstream/fabric/meta-v2") 66 | mkdirs("upstream/fabric/loader-installer-json") 67 | mkdirs("upstream/fabric/jars") 68 | 69 | # get the version list for each component we are interested in 70 | for component in ["intermediary", "loader"]: 71 | index = get_json_file("upstream/fabric/meta-v2/" + component + ".json", "https://meta.fabricmc.net/v2/versions/" + component) 72 | for it in index: 73 | jarMavenUrl = get_maven_url(it["maven"], "https://maven.fabricmc.net/", ".jar") 74 | compute_jar_file("upstream/fabric/jars/" + it["maven"].replace(":", "."), jarMavenUrl) 75 | 76 | # for each loader, download installer JSON file from maven 77 | with open("upstream/fabric/meta-v2/loader.json", 'r', encoding='utf-8') as loaderVersionIndexFile: 78 | loaderVersionIndex = json.load(loaderVersionIndexFile) 79 | for it in loaderVersionIndex: 80 | mavenUrl = get_maven_url(it["maven"], "https://maven.fabricmc.net/", ".json") 81 | get_json_file("upstream/fabric/loader-installer-json/" + it["version"] + ".json", mavenUrl) 82 | -------------------------------------------------------------------------------- /generateLiteloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from liteloaderutil import * 3 | from jsonobject import * 4 | from datetime import datetime 5 | from pprint import pprint 6 | import copy 7 | 8 | # load the locally cached version list 9 | def loadLiteloaderJson(): 10 | with open("upstream/liteloader/versions.json", 'r', encoding='utf-8') as f: 11 | return LiteloaderIndex(json.load(f)) 12 | 13 | remoteVersionlist = loadLiteloaderJson() 14 | 15 | def processArtefacts(mcVersion, liteloader, notSnapshots): 16 | versions = [] 17 | lookup = {} 18 | latestVersion = None 19 | latest = None 20 | for id, artefact in liteloader.items(): 21 | if id == 'latest': 22 | latestVersion = artefact.version 23 | continue 24 | version = MultiMCVersionFile(name="LiteLoader", uid="com.mumfrey.liteloader", version=artefact.version) 25 | version.requires = [DependencyEntry(uid='net.minecraft', equals=mcVersion)] 26 | version.releaseTime = datetime.utcfromtimestamp(int(artefact.timestamp)) 27 | version.addTweakers = [artefact.tweakClass] 28 | version.mainClass = "net.minecraft.launchwrapper.Launch" 29 | version.order = 10 30 | if notSnapshots: 31 | version.type = "release" 32 | else: 33 | version.type = "snapshot" 34 | lookup[version.version] = version 35 | libraries = artefact.libraries 36 | # hack to make broken liteloader versions work 37 | for lib in libraries: 38 | if lib.name == GradleSpecifier("org.ow2.asm:asm-all:5.0.3"): 39 | lib.url = "https://repo.maven.apache.org/maven2/" 40 | if lib.name == GradleSpecifier("org.ow2.asm:asm-all:5.2"): 41 | lib.url = "http://repo.liteloader.com/" 42 | liteloaderLib = MultiMCLibrary( 43 | name=GradleSpecifier("com.mumfrey:liteloader:%s" % version.version), 44 | url = "http://dl.liteloader.com/versions/" 45 | ) 46 | if not notSnapshots: 47 | liteloaderLib.mmcHint = "always-stale" 48 | libraries.append(liteloaderLib) 49 | version.libraries = libraries 50 | versions.append(version) 51 | if latestVersion: 52 | latest = lookup[latestVersion] 53 | return versions, latest 54 | 55 | allVersions = [] 56 | recommended = [] 57 | for mcVersion, versionObject in remoteVersionlist.versions.items(): 58 | # ignore this for now. It should be a jar mod or something. 59 | if mcVersion == "1.5.2": 60 | continue 61 | latestSnapshot = None 62 | latestRelease = None 63 | version = [] 64 | if versionObject.artefacts: 65 | versions, latestRelease = processArtefacts(mcVersion, versionObject.artefacts.liteloader, True) 66 | allVersions.extend(versions) 67 | if versionObject.snapshots: 68 | versions, latestSnapshot = processArtefacts(mcVersion, versionObject.snapshots.liteloader, False) 69 | allVersions.extend(versions) 70 | 71 | if latestRelease: 72 | recommended.append(latestRelease.version) 73 | 74 | recommended.sort() 75 | 76 | allVersions.sort(key=lambda x: x.releaseTime, reverse=True) 77 | 78 | for version in allVersions: 79 | outFilepath = "multimc/com.mumfrey.liteloader/%s.json" % version.version 80 | with open(outFilepath, 'w') as outfile: 81 | json.dump(version.to_json(), outfile, sort_keys=True, indent=4) 82 | 83 | sharedData = MultiMCSharedPackageData(uid = 'com.mumfrey.liteloader', name = 'LiteLoader') 84 | sharedData.recommended = recommended 85 | sharedData.description = remoteVersionlist.meta.description 86 | sharedData.projectUrl = remoteVersionlist.meta.url 87 | sharedData.authors = [remoteVersionlist.meta.authors] 88 | sharedData.write() 89 | -------------------------------------------------------------------------------- /liteloaderutil.py: -------------------------------------------------------------------------------- 1 | from metautil import * 2 | 3 | ''' 4 | "repo":{ 5 | "stream":"RELEASE", 6 | "type":"m2", 7 | "url":"http:\/\/dl.liteloader.com\/repo\/", 8 | "classifier":"" 9 | }, 10 | ''' 11 | class LiteloaderRepo(JsonObject): 12 | stream = StringProperty(required=True) 13 | type = StringProperty(required=True) 14 | url = StringProperty(required=True) 15 | classifier = StringProperty(required=True) 16 | 17 | ''' 18 | "53639d52340479ccf206a04f5e16606f":{ 19 | "tweakClass":"com.mumfrey.liteloader.launch.LiteLoaderTweaker", 20 | "libraries":[ 21 | { 22 | "name":"net.minecraft:launchwrapper:1.5" 23 | }, 24 | { 25 | "name":"net.sf.jopt-simple:jopt-simple:4.5" 26 | }, 27 | { 28 | "name":"org.ow2.asm:asm-all:4.1" 29 | } 30 | ], 31 | "stream":"RELEASE", 32 | "file":"liteloader-1.5.2_01.jar", 33 | "version":"1.5.2_01", 34 | "md5":"53639d52340479ccf206a04f5e16606f", 35 | "timestamp":"1367366420" 36 | }, 37 | ''' 38 | class LiteloaderArtefact(JsonObject): 39 | tweakClass = StringProperty(required=True) 40 | libraries = ListProperty(MultiMCLibrary, required=True) 41 | stream = StringProperty(required=True) 42 | file = StringProperty(required=True) 43 | version = StringProperty(required=True) 44 | build = StringProperty(default=None, exclude_if_none=True) 45 | md5 = StringProperty(required=True) 46 | timestamp = StringProperty(required=True) 47 | srcJar = StringProperty(default=None, exclude_if_none=True) 48 | mcpJar = StringProperty(default=None, exclude_if_none=True) 49 | 50 | class LiteloaderDev(JsonObject): 51 | fgVersion = StringProperty(default=None ,exclude_if_none=True) 52 | mappings = StringProperty(required=None, exclude_if_none=True) 53 | mcp = StringProperty(default=None, exclude_if_none=True) 54 | 55 | class LiteloaderArtefacts(JsonObject): 56 | liteloader = DictProperty(LiteloaderArtefact, name="com.mumfrey:liteloader", required=True) 57 | 58 | class LiteloaderSnapshot(LiteloaderArtefact): 59 | lastSuccessfulBuild = IntegerProperty() 60 | 61 | class LiteloaderSnapshots(JsonObject): 62 | libraries = ListProperty(MultiMCLibrary, required=True) 63 | liteloader = DictProperty(LiteloaderSnapshot, name="com.mumfrey:liteloader", required=True) 64 | 65 | ''' 66 | "1.10.2":{ 67 | "dev": { ... }, 68 | "repo":{ ... }, 69 | "artefacts":{ 70 | "com.mumfrey:liteloader":{ }, 71 | ... 72 | }, 73 | "snapshots":{ 74 | ... 75 | } 76 | ''' 77 | class LiteloaderEntry(JsonObject): 78 | dev = ObjectProperty(LiteloaderDev, default=None, exclude_if_none=True) 79 | repo = ObjectProperty(LiteloaderRepo, required=True) 80 | artefacts = ObjectProperty(LiteloaderArtefacts, default=None, exclude_if_none=True) 81 | snapshots = ObjectProperty(LiteloaderSnapshots, default=None, exclude_if_none=True) 82 | 83 | ''' 84 | "meta":{ 85 | "description":"LiteLoader is a lightweight mod bootstrap designed to provide basic loader functionality for mods which don't need to modify game mechanics.", 86 | "authors":"Mumfrey", 87 | "url":"http:\/\/dl.liteloader.com", 88 | "updated":"2017-02-22T11:34:07+00:00", 89 | "updatedTime":1487763247 90 | }, 91 | ''' 92 | class LiteloaderMeta(JsonObject): 93 | description = StringProperty(required=True) 94 | authors = StringProperty(required=True) 95 | url = StringProperty(required=True) 96 | updated = ISOTimestampProperty(required=True) 97 | updatedTime = IntegerProperty(required=True) 98 | 99 | # The raw Forge version index 100 | class LiteloaderIndex(JsonObject): 101 | meta = ObjectProperty(LiteloaderMeta, required=True) 102 | versions = DictProperty(LiteloaderEntry) 103 | 104 | -------------------------------------------------------------------------------- /updateQuilt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, requests 3 | from cachecontrol import CacheControl 4 | import datetime 5 | import hashlib, json 6 | import zipfile 7 | from quiltutil import * 8 | 9 | from cachecontrol.caches import FileCache 10 | 11 | forever_cache = FileCache('http_cache', forever=True) 12 | sess = CacheControl(requests.Session(), forever_cache) 13 | 14 | blacklist = ("0.17.5-beta.5", "0.17.5-beta.4") 15 | 16 | def mkdirs(path): 17 | if not os.path.exists(path): 18 | os.makedirs(path) 19 | 20 | def filehash(filename, hashtype, blocksize=65536): 21 | hash = hashtype() 22 | with open(filename, "rb") as f: 23 | for block in iter(lambda: f.read(blocksize), b""): 24 | hash.update(block) 25 | return hash.hexdigest() 26 | 27 | class MavenSpecifier: 28 | def __init__(self, mavenKey): 29 | mavenParts = mavenKey.split(":", 3) 30 | self.group = mavenParts[0] 31 | self.name = mavenParts[1] 32 | self.version = mavenParts[2] 33 | 34 | def path(self): 35 | return self.group.replace(".", "/") + "/" + self.name + "/" + self.version + "/" 36 | 37 | def filename(self, ext): 38 | return self.name + "-" + self.version + ext 39 | 40 | def url(self, server, ext): 41 | return server + self.path() + self.filename(ext) 42 | 43 | 44 | def get_maven_url(mavenKey, server, ext): 45 | mavenParts = mavenKey.split(":", 3) 46 | mavenVerUrl = server + mavenParts[0].replace(".", "/") + "/" + mavenParts[1] + "/" + mavenParts[2] + "/" 47 | mavenUrl = mavenVerUrl + mavenParts[1] + "-" + mavenParts[2] + ext 48 | return mavenUrl 49 | 50 | def get_json_file(path, url): 51 | with open(path, 'w', encoding='utf-8') as f: 52 | r = sess.get(url) 53 | r.raise_for_status() 54 | version_json = r.json() 55 | json.dump(version_json, f, sort_keys=True, indent=4) 56 | return version_json 57 | 58 | def get_binary_file(path, url): 59 | with open(path, 'w', encoding='utf-8') as f: 60 | r = sess.get(url) 61 | r.raise_for_status() 62 | with open(path, 'wb') as f: 63 | for chunk in r.iter_content(chunk_size=128): 64 | f.write(chunk) 65 | 66 | def compute_jar_file(path, url): 67 | jarPath = path + ".jar" 68 | get_binary_file(jarPath, url) 69 | tstamp = datetime.datetime.fromtimestamp(0) 70 | with zipfile.ZipFile(jarPath, 'r') as jar: 71 | allinfo = jar.infolist() 72 | for info in allinfo: 73 | tstampNew = datetime.datetime(*info.date_time) 74 | if tstampNew > tstamp: 75 | tstamp = tstampNew 76 | data = QuiltJarInfo() 77 | data.releaseTime = tstamp 78 | data.sha1 = filehash(jarPath, hashlib.sha1) 79 | data.sha256 = filehash(jarPath, hashlib.sha256) 80 | data.size = os.path.getsize(jarPath) 81 | with open(path + ".json", 'w') as outfile: 82 | json.dump(data.to_json(), outfile, sort_keys=True, indent=4) 83 | 84 | mkdirs("upstream/quilt/meta-v3") 85 | mkdirs("upstream/quilt/loader-installer-json") 86 | mkdirs("upstream/quilt/jars") 87 | 88 | # get the version list for each component we are interested in 89 | #for component in ["intermediary", "loader"]: 90 | for component in ["loader"]: 91 | index = get_json_file("upstream/quilt/meta-v3/" + component + ".json", "https://meta.quiltmc.org/v3/versions/" + component) 92 | for it in index: 93 | spec = MavenSpecifier(it["maven"]) 94 | jarMavenUrl = spec.url("https://maven.quiltmc.org/repository/release/", ".jar"); 95 | if spec.version in blacklist: 96 | print("Ignoring ", jarMavenUrl) 97 | continue 98 | print("Looking up", jarMavenUrl) 99 | compute_jar_file("upstream/quilt/jars/" + it["maven"].replace(":", "."), jarMavenUrl) 100 | 101 | # for each loader, download installer JSON file from maven 102 | with open("upstream/quilt/meta-v3/loader.json", 'r', encoding='utf-8') as loaderVersionIndexFile: 103 | loaderVersionIndex = json.load(loaderVersionIndexFile) 104 | for it in loaderVersionIndex: 105 | spec = MavenSpecifier(it["maven"]) 106 | mavenUrl = spec.url("https://maven.quiltmc.org/repository/release/", ".json") 107 | if spec.version in blacklist: 108 | print("Ignoring metadata from", mavenUrl) 109 | continue 110 | print("Getting metadata from", mavenUrl) 111 | get_json_file("upstream/quilt/loader-installer-json/" + it["version"] + ".json", mavenUrl) 112 | -------------------------------------------------------------------------------- /enumerateForge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | import sys 4 | import os 5 | import re 6 | from metautil import * 7 | from forgeutil import * 8 | from jsonobject import * 9 | from distutils.version import LooseVersion 10 | from enum import Enum 11 | 12 | import requests 13 | from cachecontrol import CacheControl 14 | from cachecontrol.caches import FileCache 15 | 16 | #with open('multimc/index.json', 'r', encoding='utf-8') as index: 17 | #packages = MultiMCPackageIndex(json.load(index)) 18 | 19 | #for entry in packages.packages: 20 | #print (entry) 21 | 22 | class DownloadType(Enum): 23 | NORMAL = 1 24 | FORGE_XZ = 2 25 | 26 | class DownloadEntry: 27 | def __init__(self, url : str, kind : DownloadType, name : GradleSpecifier): 28 | self.name = name 29 | self.url = url 30 | self.kind = kind 31 | 32 | def __lt__(self, other): 33 | return self.name < other.name 34 | 35 | def __eq__(self, other): 36 | return self.name == other.name 37 | 38 | def __ne__(self, other): 39 | return not self.__eq__(other) 40 | 41 | def __hash__(self): 42 | return self.name.__hash__() 43 | 44 | def toString(self): 45 | return "%s %s" % (self.name.toString(), self.url) 46 | 47 | def __repr__(self): 48 | return "DownloadEntry('" + self.toString() + "')" 49 | 50 | class MojangLibrary (JsonObject): 51 | extract = ObjectProperty(MojangLibraryExtractRules, exclude_if_none=True, default=None) 52 | name = GradleSpecifierProperty(required = True) 53 | downloads = ObjectProperty(MojangLibraryDownloads, exclude_if_none=True, default=None) 54 | natives = DictProperty(StringProperty, exclude_if_none=True, default=None) 55 | rules = ListProperty(MojangRule, exclude_if_none=True, default=None) 56 | 57 | class MultiMCLibrary (MojangLibrary): 58 | url = StringProperty(exclude_if_none=True, default=None) 59 | mmcHint = StringProperty(name="MMC-hint", exclude_if_none=True, default=None) 60 | 61 | 62 | def GetLibraryDownload (library : MultiMCLibrary): 63 | if library.natives: 64 | raise Exception('Natives are not handled yet') 65 | 66 | name = library.name 67 | if library.mmcHint == 'forge-pack-xz': 68 | kind = DownloadType.FORGE_XZ 69 | name.extension = 'jar.pack.xz' 70 | else: 71 | kind = DownloadType.NORMAL 72 | 73 | if library.downloads: 74 | url = library.downloads.artifact.url 75 | if url.endswith('.zip'): 76 | name.extension = 'zip' 77 | 78 | 79 | if library.downloads: 80 | url = library.downloads.artifact.url 81 | else: 82 | if library.url: 83 | url = library.url + name.getPath() 84 | else: 85 | url = 'https://libraries.minecraft.net/' + name.getPath() 86 | 87 | return DownloadEntry(url, kind, name) 88 | 89 | with open('multimc/net.minecraftforge/index.json', 'r', encoding='utf-8') as forgeIndex: 90 | forgeVersions = MultiMCVersionIndex(json.load(forgeIndex)) 91 | 92 | urlSet = set() 93 | 94 | for entry in forgeVersions.versions: 95 | versionString = entry.version 96 | versionPath = "multimc/net.minecraftforge/%s.json" % versionString 97 | with open(versionPath, 'r') as infile: 98 | forgeVersion = MultiMCVersionFile(json.load(infile)) 99 | if forgeVersion.libraries: 100 | for entry in forgeVersion.libraries: 101 | urlSet.add(GetLibraryDownload(entry)) 102 | 103 | if forgeVersion.jarMods: 104 | for entry in forgeVersion.jarMods: 105 | urlSet.add(GetLibraryDownload(entry)) 106 | 107 | if forgeVersion.mavenFiles: 108 | for entry in forgeVersion.mavenFiles: 109 | urlSet.add(GetLibraryDownload(entry)) 110 | 111 | forever_cache = FileCache('forge_cache', forever=True) 112 | sess = CacheControl(requests.Session(), forever_cache) 113 | 114 | for entry in urlSet: 115 | libraryName = entry.name 116 | folderPath = "forgemaven/%s" % libraryName.getBase() 117 | filePath = "forgemaven/%s" % libraryName.getPath() 118 | if not os.path.isfile(filePath): 119 | os.makedirs(folderPath, exist_ok=True) 120 | rfile = sess.get(entry.url, stream=True) 121 | try: 122 | rfile.raise_for_status() 123 | except requests.exceptions.HTTPError as exc: 124 | print('Missing: %s %s' % (entry.name, entry.url)) 125 | continue 126 | print('Downloading %s' % entry.name) 127 | print('To %s' % filePath) 128 | with open(filePath, 'wb') as f: 129 | for chunk in rfile.iter_content(chunk_size=4096): 130 | f.write(chunk) 131 | -------------------------------------------------------------------------------- /jsonobject/properties.py: -------------------------------------------------------------------------------- 1 | # DateTimeProperty, DateProperty, and TimeProperty 2 | # include code copied from couchdbkit 3 | from __future__ import absolute_import 4 | import sys 5 | import datetime 6 | import time 7 | import decimal 8 | from .base_properties import ( 9 | AbstractDateProperty, 10 | AssertTypeProperty, 11 | JsonContainerProperty, 12 | JsonProperty, 13 | DefaultProperty, 14 | ) 15 | from .containers import JsonArray, JsonDict, JsonSet 16 | 17 | 18 | if sys.version > '3': 19 | unicode = str 20 | long = int 21 | 22 | 23 | class StringProperty(AssertTypeProperty): 24 | _type = (unicode, str) 25 | 26 | def selective_coerce(self, obj): 27 | if isinstance(obj, str): 28 | obj = unicode(obj) 29 | return obj 30 | 31 | 32 | class BooleanProperty(AssertTypeProperty): 33 | _type = bool 34 | 35 | 36 | class IntegerProperty(AssertTypeProperty): 37 | _type = (int, long) 38 | 39 | 40 | class FloatProperty(AssertTypeProperty): 41 | _type = float 42 | 43 | def selective_coerce(self, obj): 44 | if isinstance(obj, (int, long)): 45 | obj = float(obj) 46 | return obj 47 | 48 | 49 | class DecimalProperty(JsonProperty): 50 | 51 | def wrap(self, obj): 52 | return decimal.Decimal(obj) 53 | 54 | def unwrap(self, obj): 55 | if isinstance(obj, (int, long)): 56 | obj = decimal.Decimal(obj) 57 | elif isinstance(obj, float): 58 | # python 2.6 doesn't allow a float to Decimal 59 | obj = decimal.Decimal(unicode(obj)) 60 | assert isinstance(obj, decimal.Decimal) 61 | return obj, unicode(obj) 62 | 63 | 64 | class DateProperty(AbstractDateProperty): 65 | 66 | _type = datetime.date 67 | 68 | def _wrap(self, value): 69 | fmt = '%Y-%m-%d' 70 | try: 71 | return datetime.date(*time.strptime(value, fmt)[:3]) 72 | except ValueError as e: 73 | raise ValueError('Invalid ISO date {0!r} [{1}]'.format(value, e)) 74 | 75 | def _unwrap(self, value): 76 | return value, value.isoformat() 77 | 78 | 79 | class DateTimeProperty(AbstractDateProperty): 80 | 81 | _type = datetime.datetime 82 | 83 | def _wrap(self, value): 84 | if not self.exact: 85 | value = value.split('.', 1)[0] # strip out microseconds 86 | value = value[0:19] # remove timezone 87 | fmt = '%Y-%m-%dT%H:%M:%S' 88 | else: 89 | fmt = '%Y-%m-%dT%H:%M:%S.%fZ' 90 | try: 91 | return datetime.datetime.strptime(value, fmt) 92 | except ValueError as e: 93 | raise ValueError( 94 | 'Invalid ISO date/time {0!r} [{1}]'.format(value, e)) 95 | 96 | def _unwrap(self, value): 97 | if not self.exact: 98 | value = value.replace(microsecond=0) 99 | padding = '' 100 | else: 101 | padding = '' if value.microsecond else '.000000' 102 | return value, value.isoformat() + padding + 'Z' 103 | 104 | 105 | class TimeProperty(AbstractDateProperty): 106 | 107 | _type = datetime.time 108 | 109 | def _wrap(self, value): 110 | if not self.exact: 111 | value = value.split('.', 1)[0] # strip out microseconds 112 | fmt = '%H:%M:%S' 113 | else: 114 | fmt = '%H:%M:%S.%f' 115 | try: 116 | return datetime.time(*time.strptime(value, fmt)[3:6]) 117 | except ValueError as e: 118 | raise ValueError('Invalid ISO time {0!r} [{1}]'.format(value, e)) 119 | 120 | def _unwrap(self, value): 121 | if not self.exact: 122 | value = value.replace(microsecond=0) 123 | return value, value.isoformat() 124 | 125 | 126 | class ObjectProperty(JsonContainerProperty): 127 | 128 | default = lambda self: self.item_type() 129 | 130 | def wrap(self, obj, string_conversions=None): 131 | return self.item_type.wrap(obj) 132 | 133 | def unwrap(self, obj): 134 | assert isinstance(obj, self.item_type), \ 135 | '{0} is not an instance of {1}'.format(obj, self.item_type) 136 | return obj, obj._obj 137 | 138 | 139 | class ListProperty(JsonContainerProperty): 140 | 141 | _type = default = list 142 | container_class = JsonArray 143 | 144 | def _update(self, container, extension): 145 | container.extend(extension) 146 | 147 | 148 | class DictProperty(JsonContainerProperty): 149 | 150 | _type = default = dict 151 | container_class = JsonDict 152 | 153 | def _update(self, container, extension): 154 | container.update(extension) 155 | 156 | 157 | class SetProperty(JsonContainerProperty): 158 | 159 | _type = default = set 160 | container_class = JsonSet 161 | 162 | def _update(self, container, extension): 163 | container.update(extension) 164 | -------------------------------------------------------------------------------- /generateQuilt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from quiltutil import * 3 | from jsonobject import * 4 | from datetime import datetime 5 | from pprint import pprint 6 | import os, copy 7 | 8 | # turn loader versions into packages 9 | loaderRecommended = [] 10 | loaderVersions = [] 11 | #intermediaryRecommended = [] 12 | #intermediaryVersions = [] 13 | 14 | def mkdirs(path): 15 | if not os.path.exists(path): 16 | os.makedirs(path) 17 | 18 | mkdirs("multimc/org.quiltmc.quilt-loader") 19 | #mkdirs("multimc/org.quiltmc.intermediary") 20 | 21 | def loadJarInfo(mavenKey): 22 | with open("upstream/quilt/jars/" + mavenKey.replace(":", ".") + ".json", 'r', encoding='utf-8') as jarInfoFile: 23 | return QuiltJarInfo(json.load(jarInfoFile)) 24 | 25 | def processLoaderVersion(loaderVersion, it, loaderData): 26 | verStable = isQuiltVerStable(loaderVersion) 27 | if (len(loaderRecommended) < 1) and verStable: 28 | loaderRecommended.append(loaderVersion) 29 | versionJarInfo = loadJarInfo(it["maven"]) 30 | version = MultiMCVersionFile(name="Quilt Loader", uid="org.quiltmc.quilt-loader", version=loaderVersion) 31 | version.releaseTime = versionJarInfo.releaseTime 32 | # version.requires = [DependencyEntry(uid='org.quiltmc.intermediary')] 33 | version.requires = [DependencyEntry(uid='net.fabricmc.intermediary')] 34 | version.order = 10 35 | if verStable: 36 | version.type = "release" 37 | else: 38 | version.type = "snapshot" 39 | if isinstance(loaderData.mainClass, dict): 40 | version.mainClass = loaderData.mainClass["client"] 41 | else: 42 | version.mainClass = loaderData.mainClass 43 | version.libraries = [] 44 | version.libraries.extend(loaderData.libraries.common) 45 | version.libraries.extend(loaderData.libraries.client) 46 | loaderLib = MultiMCLibrary(name=GradleSpecifier(it["maven"]), url="https://maven.quiltmc.org/repository/release/") 47 | version.libraries.append(loaderLib) 48 | loaderVersions.append(version) 49 | 50 | #def processIntermediaryVersion(it): 51 | # intermediaryRecommended.append(it["version"]) 52 | # versionJarInfo = loadJarInfo(it["maven"]) 53 | # version = MultiMCVersionFile(name="Intermediary Mappings", uid="org.quiltmc.intermediary", version=it["version"]) 54 | # version.releaseTime = versionJarInfo.releaseTime 55 | # version.requires = [DependencyEntry(uid='net.minecraft', equals=it["version"])] 56 | # version.order = 11 57 | # version.type = "release" 58 | # version.libraries = [] 59 | # version.volatile = True 60 | # mappingLib = MultiMCLibrary(name=GradleSpecifier(it["maven"]), url="https://maven.quiltmc.org/repository/release/") 61 | # version.libraries.append(mappingLib) 62 | # intermediaryVersions.append(version) 63 | 64 | with open("upstream/quilt/meta-v3/loader.json", 'r', encoding='utf-8') as loaderVersionIndexFile: 65 | loaderVersionIndex = json.load(loaderVersionIndexFile) 66 | for it in loaderVersionIndex: 67 | version = it["version"] 68 | with open("upstream/quilt/loader-installer-json/" + version + ".json", 'r', encoding='utf-8') as loaderVersionFile: 69 | ldata = json.load(loaderVersionFile) 70 | ldata = QuiltInstallerDataV1(ldata) 71 | processLoaderVersion(version, it, ldata) 72 | 73 | #with open("upstream/quilt/meta-v3/intermediary.json", 'r', encoding='utf-8') as intermediaryVersionIndexFile: 74 | # intermediaryVersionIndex = json.load(intermediaryVersionIndexFile) 75 | # for it in intermediaryVersionIndex: 76 | # processIntermediaryVersion(it) 77 | 78 | for version in loaderVersions: 79 | outFilepath = "multimc/org.quiltmc.quilt-loader/%s.json" % version.version 80 | with open(outFilepath, 'w') as outfile: 81 | json.dump(version.to_json(), outfile, sort_keys=True, indent=4) 82 | 83 | sharedData = MultiMCSharedPackageData(uid = 'org.quiltmc.quilt-loader', name = 'Quilt Loader') 84 | sharedData.recommended = loaderRecommended 85 | sharedData.description = "Quilt Loader is a tool to load Quilt-compatible mods in game environments." 86 | sharedData.projectUrl = "https://quiltmc.org" 87 | sharedData.authors = ["Quilt Developers"] 88 | sharedData.write() 89 | 90 | #for version in intermediaryVersions: 91 | # outFilepath = "multimc/org.quiltmc.intermediary/%s.json" % version.version 92 | # with open(outFilepath, 'w') as outfile: 93 | # json.dump(version.to_json(), outfile, sort_keys=True, indent=4) 94 | 95 | #sharedData = MultiMCSharedPackageData(uid = 'org.quiltmc.intermediary', name = 'Intermediary Mappings') 96 | #sharedData.recommended = intermediaryRecommended 97 | #sharedData.description = "Intermediary mappings allow using Quilt Loader with mods for Minecraft in a more compatible manner." 98 | #sharedData.projectUrl = "https://quiltmc.org" 99 | #sharedData.authors = ["Quilt Developers"] 100 | #sharedData.write() 101 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR=$(dirname "$0") 4 | cd "${BASEDIR}" 5 | BASEDIR=`pwd` 6 | 7 | set -x 8 | 9 | source config.sh 10 | if [ -f config/config_local.sh ]; then 11 | source config/config_local.sh 12 | fi 13 | 14 | MODE=${MODE:-develop} 15 | 16 | S3_BUCKET_var="S3_$MODE" 17 | S3_BUCKET="${!S3_BUCKET_var}" 18 | 19 | BRANCH_var="BRANCH_$MODE" 20 | BRANCH="${!BRANCH_var}" 21 | 22 | function fail_in { 23 | if [ "${DISCORD_NOTIFY}" = true ] ; then 24 | discord_webhook -u "${DISCORD_NOTIFY_HOOK_ERROR}" -c "Meta failed to update: $1" --username "Meta Error" 25 | fi 26 | cd "${BASEDIR}/${UPSTREAM_DIR}" 27 | git reset --hard HEAD 28 | exit 1 29 | } 30 | 31 | function fail_out { 32 | if [ "${DISCORD_NOTIFY}" = true ] ; then 33 | discord_webhook -u "${DISCORD_NOTIFY_HOOK_ERROR}" -c "Meta failed to output: $1" --username "Meta Error" 34 | fi 35 | cd "${BASEDIR}/${MMC_DIR}" 36 | git reset --hard HEAD 37 | exit 1 38 | } 39 | 40 | function fail_generic { 41 | if [ "${DISCORD_NOTIFY}" = true ] ; then 42 | discord_webhook -u "${DISCORD_NOTIFY_HOOK_ERROR}" -c "Meta failed to $1" --username "Meta Error" 43 | fi 44 | exit 1 45 | } 46 | 47 | currentDate=`date --iso-8601` 48 | 49 | cd "${BASEDIR}/${UPSTREAM_DIR}" 50 | git reset --hard HEAD || fail_generic "git reset upstream" 51 | git checkout ${BRANCH} || fail_generic "git checkout upstream" 52 | cd "${BASEDIR}" 53 | 54 | ./updateMojang.py || fail_in "Mojang" 55 | ./updateForge.py || fail_in "Forge" 56 | ./updateNeoforge.py || fail_in "Neoforge" 57 | ./updateFabric.py || fail_in "Fabric" 58 | ./updateQuilt.py || fail_in "Quilt" 59 | ./updateLiteloader.py || fail_in "Liteloader" 60 | 61 | if [ "${DEPLOY_TO_GIT}" = true ] ; then 62 | cd "${BASEDIR}/${UPSTREAM_DIR}" 63 | git add mojang/version_manifest_v2.json mojang/versions/* mojang/assets/* || fail_in "git add" 64 | git add forge/*.json forge/version_manifests/*.json forge/installer_manifests/*.json forge/files_manifests/*.json forge/installer_info/*.json || fail_in "git add" 65 | git add neoforge/*.json neoforge/version_manifests/*.json neoforge/installer_manifests/*.json neoforge/files_manifests/*.json neoforge/installer_info/*.json || fail_in "git add" 66 | git add fabric/loader-installer-json/*.json fabric/meta-v2/*.json fabric/jars/*.json || fail_in "git add" 67 | git add quilt/loader-installer-json/*.json quilt/meta-v3/*.json quilt/jars/*.json || fail_in "git add" 68 | git add liteloader/*.json || fail_in "git add" 69 | if ! git diff --cached --exit-code ; then 70 | git commit -a -m "Update ${currentDate}" || fail_in "git commit" 71 | GIT_SSH_COMMAND="ssh -i ${BASEDIR}/config/meta-upstream.key" git push || fail_generic "git push upstream" 72 | fi 73 | cd "${BASEDIR}" 74 | fi 75 | 76 | cd "${BASEDIR}/${MMC_DIR}" 77 | git reset --hard HEAD || fail_generic "git reset multimc" 78 | git checkout ${BRANCH} || fail_generic "git checkout multimc" 79 | cd "${BASEDIR}" 80 | 81 | ./generateMojang.py || fail_out "Mojang" 82 | ./generateForge.py || fail_out "Forge" 83 | ./generateNeoforge.py || fail_out "Neoforge" 84 | ./generateFabric.py || fail_out "Fabric" 85 | ./generateQuilt.py || fail_out "Quilt" 86 | ./generateLiteloader.py || fail_out "Liteloader" 87 | ./index.py || fail_out "Generating Index" 88 | 89 | if [ "${DEPLOY_TO_GIT}" = true ] ; then 90 | cd "${BASEDIR}/${MMC_DIR}" 91 | git add index.json org.lwjgl/* net.minecraft/* || fail_out "git add" 92 | git add net.minecraftforge/* || fail_out "git add" 93 | git add net.neoforged/* || fail_out "git add" 94 | git add net.fabricmc.fabric-loader/* net.fabricmc.intermediary/* || fail_out "git add" 95 | git add org.quiltmc.quilt-loader/* || fail_out "git add" 96 | git add com.mumfrey.liteloader/* || fail_out "git add" 97 | if [ -d "org.lwjgl3" ]; then 98 | git add org.lwjgl3/* || fail_out "git add" 99 | fi 100 | 101 | if ! git diff --cached --exit-code ; then 102 | git commit -a -m "Update ${currentDate}" || fail_out "git commit" 103 | GIT_SSH_COMMAND="ssh -i ${BASEDIR}/config/meta-multimc.key" git push || fail_generic "git push multimc" 104 | fi 105 | fi 106 | 107 | if [ "${UPDATE_FORGE_MAVEN}" = true ] ; then 108 | echo "Updating the copy of Forge maven" 109 | cd "${BASEDIR}" 110 | ./enumerateForge.py || fail_generic "Enumerate Forge" 111 | if [ "${DEPLOY_FORGE_MAVEN}" = true ] ; then 112 | chown -RL ${DEPLOY_FOLDER_USER}:${DEPLOY_FOLDER_GROUP} ${BASEDIR}/forgemaven/ 113 | if [ "${DEPLOY_FORGE_MAVEN_S3}" = true ] ; then 114 | s3cmd -c ${BASEDIR}/config/s3cmd.cfg --exclude=".git*" --delete-removed sync ${BASEDIR}/forgemaven/ ${S3_FORGE_MAVEN} || fail_generic "Deploy Forge Maven to S3" 115 | fi 116 | fi 117 | fi 118 | 119 | cd "${BASEDIR}" 120 | if [ "${DEPLOY_TO_FOLDER}" = true ] ; then 121 | DEPLOY_FOLDER_var="DEPLOY_FOLDER_$MODE" 122 | DEPLOY_FOLDER="${!DEPLOY_FOLDER_var}" 123 | echo "Deploying to ${DEPLOY_FOLDER}" 124 | rsync -rvog --chown=${DEPLOY_FOLDER_USER}:${DEPLOY_FOLDER_GROUP} --exclude=.git ${BASEDIR}/${MMC_DIR}/ ${DEPLOY_FOLDER} 125 | fi 126 | if [ "${DEPLOY_TO_S3}" = true ] ; then 127 | s3cmd -c ${BASEDIR}/config/s3cmd.cfg --exclude=".git*" --delete-removed sync ${BASEDIR}/${MMC_DIR}/ ${S3_BUCKET} || fail_generic "Deploy Meta to S3" 128 | fi 129 | 130 | if [ "${DISCORD_NOTIFY}" = true ] ; then 131 | discord_webhook -u "${DISCORD_NOTIFY_HOOK_OK}" -c "Meta update succeeded!" --username "Meta" 132 | fi 133 | 134 | exit 0 135 | -------------------------------------------------------------------------------- /neoforgeutil.py: -------------------------------------------------------------------------------- 1 | from metautil import * 2 | from collections import namedtuple 3 | 4 | # A post-processed entry constructed from the reconstructed Neoforge version index 5 | class NeoforgeVersion: 6 | def __init__(self, entry): 7 | self.package = entry.package 8 | self.build = entry.build 9 | self.rawVersion = entry.version 10 | if self.package == "neoforge": 11 | self.rawVersion = entry.longversion 12 | self.mcversion = entry.mcversion 13 | self.mcversion_sane = self.mcversion.replace("_pre", "-pre", 1) 14 | self.branch = entry.branch 15 | self.installer_filename = None 16 | self.installer_url = None 17 | self.universal_filename = None 18 | self.universal_url = None 19 | self.changelog_url = None 20 | self.longVersion = entry.longversion 21 | 22 | for classifier, fileentry in entry.files.items(): 23 | extension = fileentry.extension 24 | filename = fileentry.filename(self.longVersion) 25 | url = fileentry.url(self.longVersion) 26 | if (classifier == "installer") and (extension == "jar"): 27 | self.installer_filename = filename 28 | self.installer_url = url 29 | if (classifier == "universal" or classifier == "client") and (extension == "jar" or extension == "zip"): 30 | self.universal_filename = filename 31 | self.universal_url = url 32 | if (classifier == "changelog") and (extension == "txt"): 33 | self.changelog_url = url 34 | 35 | def name(self): 36 | return "Neoforge %d" % (self.build) 37 | 38 | def filename(self): 39 | return self.installer_filename 40 | 41 | def url(self): 42 | return self.installer_url 43 | 44 | def isSupported(self): 45 | if self.url() == None: 46 | return False 47 | 48 | versionElements = self.rawVersion.split('.') 49 | if len(versionElements) < 1: 50 | return False 51 | 52 | majorVersionStr = versionElements[0] 53 | if not majorVersionStr.isnumeric(): 54 | return False 55 | 56 | return True 57 | 58 | class NeoforgeFile(JsonObject): 59 | classifier = StringProperty(required=True) 60 | extension = StringProperty(required=True) 61 | legacy = BooleanProperty(required=True) 62 | 63 | def filename(self, longversion): 64 | return "%s-%s-%s.%s" % (self.package(), longversion, self.classifier, self.extension) 65 | 66 | def url(self, longversion): 67 | return "https://maven.neoforged.net/releases/net/neoforged/%s/%s/%s" % (self.package(), longversion, self.filename(longversion)) 68 | 69 | def package(self): 70 | return "forge" if self.legacy else "neoforge" 71 | 72 | class NeoforgeEntry(JsonObject): 73 | package = StringProperty(required=True,choices=["neoforge","forge"]) 74 | longversion = StringProperty(required=True) 75 | mcversion = StringProperty(required=True) 76 | version = StringProperty(required=True) 77 | build = IntegerProperty(required=True) 78 | branch = StringProperty() 79 | latest = BooleanProperty() 80 | recommended = BooleanProperty() 81 | files = DictProperty(NeoforgeFile) 82 | 83 | class NeoforgeMcVersionInfo(JsonObject): 84 | latest = StringProperty() 85 | recommended = StringProperty() 86 | versions = ListProperty(StringProperty()) 87 | 88 | class DerivedNeoforgeIndex(JsonObject): 89 | versions = DictProperty(NeoforgeEntry) 90 | by_mcversion = DictProperty(NeoforgeMcVersionInfo) 91 | 92 | class NeoforgeLibrary(MojangLibrary): 93 | url = StringProperty(exclude_if_none=True) 94 | serverreq = BooleanProperty(exclude_if_none=True, default=None) 95 | clientreq = BooleanProperty(exclude_if_none=True, default=None) 96 | checksums = ListProperty(StringProperty) 97 | comment = StringProperty() 98 | 99 | class NeoforgeVersionFile(MojangVersionFile): 100 | libraries = ListProperty(NeoforgeLibrary, exclude_if_none=True, default=None) # overrides Mojang libraries 101 | inheritsFrom = StringProperty() 102 | jar = StringProperty() 103 | 104 | class DataSpec(JsonObject): 105 | client = StringProperty() 106 | server = StringProperty() 107 | 108 | class ProcessorSpec(JsonObject): 109 | jar = StringProperty() 110 | classpath = ListProperty(StringProperty) 111 | args = ListProperty(StringProperty) 112 | outputs = DictProperty(StringProperty) 113 | sides = ListProperty(StringProperty, exclude_if_none=True, default=None) 114 | 115 | class NeoforgeInstallerProfileV1(JsonObject): 116 | _comment = ListProperty(StringProperty) 117 | spec = DecimalProperty(required=True, choices=[1]) 118 | profile = StringProperty() 119 | version = StringProperty() 120 | icon = StringProperty() 121 | json = StringProperty() 122 | path = GradleSpecifierProperty() 123 | logo = StringProperty() 124 | minecraft = StringProperty() 125 | welcome = StringProperty() 126 | data = DictProperty(DataSpec) 127 | processors = ListProperty(ProcessorSpec) 128 | libraries = ListProperty(MojangLibrary) 129 | hideExtract = BooleanProperty() 130 | serverJarPath = StringProperty() 131 | mirrorList = StringProperty(exclude_if_none=True, default=None) 132 | serverJarPath = StringProperty(exclude_if_none=True, default=None) 133 | 134 | class InstallerInfo(JsonObject): 135 | sha1hash = StringProperty() 136 | sha256hash = StringProperty() 137 | size = IntegerProperty() 138 | -------------------------------------------------------------------------------- /generateFabric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from fabricutil import * 3 | from jsonobject import * 4 | from datetime import datetime 5 | from pprint import pprint 6 | import os, copy 7 | 8 | # turn loader versions into packages 9 | loaderRecommended = [] 10 | loaderVersions = [] 11 | intermediaryRecommended = [] 12 | intermediaryVersions = [] 13 | 14 | def mkdirs(path): 15 | if not os.path.exists(path): 16 | os.makedirs(path) 17 | 18 | mkdirs("multimc/net.fabricmc.fabric-loader") 19 | mkdirs("multimc/net.fabricmc.intermediary") 20 | 21 | def loadJarInfo(mavenKey): 22 | with open("upstream/fabric/jars/" + mavenKey.replace(":", ".") + ".json", 'r', encoding='utf-8') as jarInfoFile: 23 | return FabricJarInfo(json.load(jarInfoFile)) 24 | 25 | def processLoaderVersion(loaderVersion, it, loaderData, metadataVersion): 26 | 'TODO: use min_java_version to fill in the equivalent of it in the final file(s) bonce we have that in place' 27 | verStable = it["stable"] 28 | if (len(loaderRecommended) < 1) and verStable: 29 | loaderRecommended.append(loaderVersion) 30 | versionJarInfo = loadJarInfo(it["maven"]) 31 | version = MultiMCVersionFile(name="Fabric Loader", uid="net.fabricmc.fabric-loader", version=loaderVersion) 32 | version.releaseTime = versionJarInfo.releaseTime 33 | version.requires = [DependencyEntry(uid='net.fabricmc.intermediary')] 34 | version.order = 10 35 | version.type = "release" 36 | if isinstance(loaderData.mainClass, dict): 37 | version.mainClass = loaderData.mainClass["client"] 38 | else: 39 | version.mainClass = loaderData.mainClass 40 | version.libraries = [] 41 | if metadataVersion == 1: 42 | version.libraries.extend(loaderData.libraries.common) 43 | version.libraries.extend(loaderData.libraries.client) 44 | elif metadataVersion == 2: 45 | version.libraries.extend(list(map(FabricLibrary.toMmcLibrary, loaderData.libraries.common))) 46 | version.libraries.extend(list(map(FabricLibrary.toMmcLibrary, loaderData.libraries.client))) 47 | 48 | loaderLibName = GradleSpecifier(it["maven"]) 49 | loaderLib = MultiMCLibrary( 50 | name=loaderLibName, 51 | downloads = MojangLibraryDownloads( 52 | artifact = MojangArtifact( 53 | sha1 = versionJarInfo.sha1, 54 | size = versionJarInfo.size, 55 | url="https://maven.fabricmc.net/" + loaderLibName.getPath(), 56 | ), 57 | ), 58 | ) 59 | version.libraries.append(loaderLib) 60 | loaderVersions.append(version) 61 | 62 | def processIntermediaryVersion(it): 63 | intermediaryRecommended.append(it["version"]) 64 | versionJarInfo = loadJarInfo(it["maven"]) 65 | version = MultiMCVersionFile(name="Intermediary Mappings", uid="net.fabricmc.intermediary", version=it["version"]) 66 | version.releaseTime = versionJarInfo.releaseTime 67 | version.requires = [DependencyEntry(uid='net.minecraft', equals=it["version"])] 68 | version.order = 11 69 | version.type = "release" 70 | version.libraries = [] 71 | version.volatile = True 72 | mappingLibName = GradleSpecifier(it["maven"]) 73 | mappingLib = MultiMCLibrary( 74 | name=mappingLibName, 75 | downloads = MojangLibraryDownloads( 76 | artifact = MojangArtifact( 77 | sha1 = versionJarInfo.sha1, 78 | size = versionJarInfo.size, 79 | url="https://maven.fabricmc.net/" + mappingLibName.getPath(), 80 | ), 81 | ), 82 | ) 83 | version.libraries.append(mappingLib) 84 | intermediaryVersions.append(version) 85 | 86 | with open("upstream/fabric/meta-v2/loader.json", 'r', encoding='utf-8') as loaderVersionIndexFile: 87 | loaderVersionIndex = json.load(loaderVersionIndexFile) 88 | for it in loaderVersionIndex: 89 | version = it["version"] 90 | with open("upstream/fabric/loader-installer-json/" + version + ".json", 'r', encoding='utf-8') as loaderVersionFile: 91 | ldata = json.load(loaderVersionFile) 92 | metadataVersion = ldata['version'] 93 | if metadataVersion == 1: 94 | ldata = FabricInstallerDataV1(ldata) 95 | elif metadataVersion == 2: 96 | ldata = FabricInstallerDataV2(ldata) 97 | else: 98 | raise UnknownVersionException("Unsupported Fabric format version: %d. Max supported is: 2" % (metadataVersion)) 99 | processLoaderVersion(version, it, ldata, metadataVersion) 100 | 101 | with open("upstream/fabric/meta-v2/intermediary.json", 'r', encoding='utf-8') as intermediaryVersionIndexFile: 102 | intermediaryVersionIndex = json.load(intermediaryVersionIndexFile) 103 | for it in intermediaryVersionIndex: 104 | processIntermediaryVersion(it) 105 | 106 | for version in loaderVersions: 107 | outFilepath = "multimc/net.fabricmc.fabric-loader/%s.json" % version.version 108 | with open(outFilepath, 'w') as outfile: 109 | json.dump(version.to_json(), outfile, sort_keys=True, indent=4) 110 | 111 | sharedData = MultiMCSharedPackageData(uid = 'net.fabricmc.fabric-loader', name = 'Fabric Loader') 112 | sharedData.recommended = loaderRecommended 113 | sharedData.description = "Fabric Loader is a tool to load Fabric-compatible mods in game environments." 114 | sharedData.projectUrl = "https://fabricmc.net" 115 | sharedData.authors = ["Fabric Developers"] 116 | sharedData.write() 117 | 118 | for version in intermediaryVersions: 119 | outFilepath = "multimc/net.fabricmc.intermediary/%s.json" % version.version 120 | with open(outFilepath, 'w') as outfile: 121 | json.dump(version.to_json(), outfile, sort_keys=True, indent=4) 122 | 123 | sharedData = MultiMCSharedPackageData(uid = 'net.fabricmc.intermediary', name = 'Intermediary Mappings') 124 | sharedData.recommended = intermediaryRecommended 125 | sharedData.description = "Intermediary mappings allow using Fabric Loader with mods for Minecraft in a more compatible manner." 126 | sharedData.projectUrl = "https://fabricmc.net" 127 | sharedData.authors = ["Fabric Developers"] 128 | sharedData.write() 129 | -------------------------------------------------------------------------------- /generateNeoforge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | import sys 4 | import os 5 | import re 6 | from metautil import * 7 | from neoforgeutil import * 8 | from jsonobject import * 9 | from distutils.version import LooseVersion 10 | 11 | def eprint(*args, **kwargs): 12 | print(*args, file=sys.stderr, **kwargs) 13 | 14 | def versionFromBuildSystemInstaller(installerVersion : MojangVersionFile, installerProfile: NeoforgeInstallerProfileV1, version: NeoforgeVersion): 15 | eprint("Generating NeoForge %s." % version.longVersion) 16 | result = MultiMCVersionFile({"name": "NeoForge", "version": version.rawVersion, "uid": "net.neoforged" }) 17 | result.requires = [DependencyEntry(uid='net.minecraft', equals=version.mcversion_sane)] 18 | result.mainClass = "io.github.zekerzhayard.forgewrapper.installer.Main" 19 | 20 | mavenLibs = [] 21 | 22 | # load the locally cached installer file info and use it to add the installer entry in the json 23 | with open("upstream/neoforge/installer_info/%s.json" % version.longVersion, 'r', encoding='utf-8') as f: 24 | installerInfo = InstallerInfo(json.load(f)) 25 | InstallerLib = MultiMCLibrary(name=GradleSpecifier("net.neoforged:%s:%s:installer" % (version.package, version.longVersion))) 26 | InstallerLib.downloads = MojangLibraryDownloads() 27 | InstallerLib.downloads.artifact = MojangArtifact() 28 | InstallerLib.downloads.artifact.url = "https://maven.neoforged.net/%s" % (InstallerLib.name.getPath()) 29 | InstallerLib.downloads.artifact.sha1 = installerInfo.sha1hash 30 | InstallerLib.downloads.artifact.size = installerInfo.size 31 | mavenLibs.append(InstallerLib) 32 | 33 | for upstreamLib in installerProfile.libraries: 34 | mmcLib = MultiMCLibrary(upstreamLib.to_json()) 35 | if mmcLib.name.isLog4j(): 36 | continue 37 | mavenLibs.append(mmcLib) 38 | 39 | result.mavenFiles = mavenLibs 40 | 41 | libraries = [] 42 | 43 | # wrapperLib = MultiMCLibrary(name=GradleSpecifier("io.github.zekerzhayard:ForgeWrapper:mmc5")) 44 | # wrapperLib.downloads = MojangLibraryDownloads() 45 | # wrapperLib.downloads.artifact = MojangArtifact() 46 | # wrapperLib.downloads.artifact.url = "https://files.multimc.org/maven/%s" % (wrapperLib.name.getPath()) 47 | # wrapperLib.downloads.artifact.sha1 = "d82cb39636a5092a8e5b5de82ccfe5f7e70e8d49" 48 | # wrapperLib.downloads.artifact.size = 35390 49 | # libraries.append(wrapperLib) 50 | wrapperLib = MultiMCLibrary(name=GradleSpecifier("io.github.zekerzhayard:ForgeWrapper:mmc7")) 51 | wrapperLib.downloads = MojangLibraryDownloads() 52 | wrapperLib.downloads.artifact = MojangArtifact() 53 | wrapperLib.downloads.artifact.url = "https://files.multimc.org/maven/%s" % (wrapperLib.name.getPath()) 54 | wrapperLib.downloads.artifact.sha1 = "0c99747406998c933be78a368dfd8386949d1935" 55 | wrapperLib.downloads.artifact.size = 29346 56 | libraries.append(wrapperLib) 57 | 58 | for upstreamLib in installerVersion.libraries: 59 | mmcLib = MultiMCLibrary(upstreamLib.to_json()) 60 | if mmcLib.name.isLog4j(): 61 | continue 62 | libraries.append(mmcLib) 63 | result.libraries = libraries 64 | 65 | result.releaseTime = installerVersion.releaseTime 66 | result.order = 5 67 | mcArgs = "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}" 68 | for arg in installerVersion.arguments.game: 69 | mcArgs += " %s" % arg 70 | result.minecraftArguments = mcArgs 71 | return result 72 | 73 | print("") 74 | print("Making dirs...") 75 | os.makedirs("multimc/net.neoforged/", exist_ok=True) 76 | 77 | # load the locally cached version list 78 | with open("upstream/neoforge/derived_index.json", 'r', encoding='utf-8') as f: 79 | main_json = json.load(f) 80 | remoteVersionlist = DerivedNeoforgeIndex(main_json) 81 | 82 | recommendedVersions = [] 83 | 84 | for id, entry in remoteVersionlist.versions.items(): 85 | if entry.mcversion == None: 86 | eprint ("Skipping %s with invalid MC version" % id) 87 | continue 88 | 89 | version = NeoforgeVersion(entry) 90 | if version.url() == None: 91 | eprint ("Skipping %s with no valid files" % id) 92 | continue 93 | eprint ("Processing NeoForge %s" % version.rawVersion) 94 | versionElements = version.rawVersion.split('.') 95 | if len(versionElements) < 1: 96 | eprint ("Skipping version %s with not enough version elements" % (id)) 97 | continue 98 | 99 | majorVersionStr = versionElements[0] 100 | if not majorVersionStr.isnumeric(): 101 | eprint ("Skipping version %s with non-numeric major version %s" % (id, majorVersionStr)) 102 | continue 103 | 104 | majorVersion = int(majorVersionStr) 105 | 106 | if entry.recommended: 107 | recommendedVersions.append(version.longVersion) 108 | 109 | # If we do not have the corresponding Minecraft version, we ignore it 110 | if not os.path.isfile("multimc/net.minecraft/%s.json" % version.mcversion_sane): 111 | eprint ("Skipping %s with no corresponding Minecraft version %s" % (id, version.mcversion_sane)) 112 | continue 113 | 114 | outVersion = None 115 | 116 | # Path for new-style build system based installers 117 | installerVersionFilepath = "upstream/neoforge/version_manifests/%s.json" % version.longVersion 118 | profileFilepath = "upstream/neoforge/installer_manifests/%s.json" % version.longVersion 119 | 120 | eprint(installerVersionFilepath) 121 | if os.path.isfile(installerVersionFilepath): 122 | with open(installerVersionFilepath, 'r', encoding='utf-8') as installerVersionFile: 123 | installerVersion = MojangVersionFile(json.load(installerVersionFile)) 124 | with open(profileFilepath, 'r', encoding='utf-8') as profileFile: 125 | installerProfile = NeoforgeInstallerProfileV1(json.load(profileFile)) 126 | outVersion = versionFromBuildSystemInstaller(installerVersion, installerProfile, version) 127 | else: 128 | # If we do not have the Neoforge json, we ignore this version 129 | eprint ("Skipping %s with missing profile json" % id) 130 | continue 131 | 132 | outFilepath = "multimc/net.neoforged/%s.json" % outVersion.version 133 | with open(outFilepath, 'w') as outfile: 134 | json.dump(outVersion.to_json(), outfile, sort_keys=True, indent=4) 135 | 136 | recommendedVersions.sort() 137 | 138 | print ('Recommended versions:', recommendedVersions) 139 | 140 | sharedData = MultiMCSharedPackageData(uid = 'net.neoforged', name = "NeoForge") 141 | sharedData.projectUrl = 'https://neoforged.net/' 142 | sharedData.recommended = recommendedVersions 143 | sharedData.write() 144 | -------------------------------------------------------------------------------- /jsonobject/containers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .base_properties import DefaultProperty 3 | from .utils import check_type, SimpleDict 4 | import copy 5 | 6 | class JsonArray(list): 7 | def __init__(self, _obj=None, wrapper=None, type_config=None): 8 | super(JsonArray, self).__init__() 9 | self._obj = check_type(_obj, list, 10 | 'JsonArray must wrap a list or None') 11 | 12 | assert type_config is not None 13 | self._type_config = type_config 14 | self._wrapper = ( 15 | wrapper or 16 | DefaultProperty(type_config=self._type_config) 17 | ) 18 | for item in self._obj: 19 | super(JsonArray, self).append(self._wrapper.wrap(item)) 20 | 21 | def validate(self, required=True): 22 | for obj in self: 23 | self._wrapper.validate(obj, required=required) 24 | 25 | def to_json(self): 26 | self.validate() 27 | return copy.deepcopy(self._obj) 28 | 29 | def append(self, wrapped): 30 | wrapped, unwrapped = self._wrapper.unwrap(wrapped) 31 | self._obj.append(unwrapped) 32 | super(JsonArray, self).append(wrapped) 33 | 34 | def __delitem__(self, i): 35 | super(JsonArray, self).__delitem__(i) 36 | del self._obj[i] 37 | 38 | def __setitem__(self, i, wrapped): 39 | wrapped, unwrapped = self._wrapper.unwrap(wrapped) 40 | self._obj[i] = unwrapped 41 | super(JsonArray, self).__setitem__(i, wrapped) 42 | 43 | def extend(self, wrapped_list): 44 | if wrapped_list: 45 | wrapped_list, unwrapped_list = zip( 46 | *map(self._wrapper.unwrap, wrapped_list) 47 | ) 48 | else: 49 | unwrapped_list = [] 50 | self._obj.extend(unwrapped_list) 51 | super(JsonArray, self).extend(wrapped_list) 52 | 53 | def insert(self, index, wrapped): 54 | wrapped, unwrapped = self._wrapper.unwrap(wrapped) 55 | self._obj.insert(index, unwrapped) 56 | super(JsonArray, self).insert(index, wrapped) 57 | 58 | def remove(self, value): 59 | i = self.index(value) 60 | super(JsonArray, self).remove(value) 61 | self._obj.pop(i) 62 | 63 | def pop(self, index=-1): 64 | self._obj.pop(index) 65 | return super(JsonArray, self).pop(index) 66 | 67 | def sort(self, cmp=None, key=None, reverse=False): 68 | zipped = zip(self, self._obj) 69 | if key: 70 | new_key = lambda pair: key(pair[0]) 71 | zipped.sort(key=new_key, reverse=reverse) 72 | elif cmp: 73 | new_cmp = lambda pair1, pair2: cmp(pair1[0], pair2[0]) 74 | zipped.sort(cmp=new_cmp, reverse=reverse) 75 | else: 76 | zipped.sort(reverse=reverse) 77 | 78 | wrapped_list, unwrapped_list = zip(*zipped) 79 | while self: 80 | self.pop() 81 | super(JsonArray, self).extend(wrapped_list) 82 | self._obj.extend(unwrapped_list) 83 | 84 | def reverse(self): 85 | self._obj.reverse() 86 | super(JsonArray, self).reverse() 87 | 88 | def __fix_slice(self, i, j): 89 | length = len(self) 90 | if j < 0: 91 | j += length 92 | if i < 0: 93 | i += length 94 | if i > length: 95 | i = length 96 | if j > length: 97 | j = length 98 | return i, j 99 | 100 | def __setslice__(self, i, j, sequence): 101 | i, j = self.__fix_slice(i, j) 102 | for _ in range(j - i): 103 | self.pop(i) 104 | for k, wrapped in enumerate(sequence): 105 | self.insert(i + k, wrapped) 106 | 107 | def __delslice__(self, i, j): 108 | i, j = self.__fix_slice(i, j) 109 | for _ in range(j - i): 110 | self.pop(i) 111 | 112 | 113 | class JsonDict(SimpleDict): 114 | 115 | def __init__(self, _obj=None, wrapper=None, type_config=None): 116 | super(JsonDict, self).__init__() 117 | self._obj = check_type(_obj, dict, 'JsonDict must wrap a dict or None') 118 | assert type_config is not None 119 | self._type_config = type_config 120 | self._wrapper = ( 121 | wrapper or 122 | DefaultProperty(type_config=self._type_config) 123 | ) 124 | for key, value in self._obj.items(): 125 | self[key] = self.__wrap(key, value) 126 | 127 | def validate(self, required=True): 128 | for obj in self.values(): 129 | self._wrapper.validate(obj, required=required) 130 | 131 | def __wrap(self, key, unwrapped): 132 | return self._wrapper.wrap(unwrapped) 133 | 134 | def __unwrap(self, key, wrapped): 135 | return self._wrapper.unwrap(wrapped) 136 | 137 | def __setitem__(self, key, value): 138 | if isinstance(key, int): 139 | key = str(key) 140 | 141 | wrapped, unwrapped = self.__unwrap(key, value) 142 | self._obj[key] = unwrapped 143 | super(JsonDict, self).__setitem__(key, wrapped) 144 | 145 | def __delitem__(self, key): 146 | del self._obj[key] 147 | super(JsonDict, self).__delitem__(key) 148 | 149 | def __getitem__(self, key): 150 | if isinstance(key, int): 151 | key = str(key) 152 | return super(JsonDict, self).__getitem__(key) 153 | 154 | 155 | class JsonSet(set): 156 | def __init__(self, _obj=None, wrapper=None, type_config=None): 157 | super(JsonSet, self).__init__() 158 | if isinstance(_obj, set): 159 | _obj = list(_obj) 160 | self._obj = check_type(_obj, list, 'JsonSet must wrap a list or None') 161 | assert type_config is not None 162 | self._type_config = type_config 163 | self._wrapper = ( 164 | wrapper or 165 | DefaultProperty(type_config=self._type_config) 166 | ) 167 | for item in self._obj: 168 | super(JsonSet, self).add(self._wrapper.wrap(item)) 169 | 170 | def validate(self, required=True): 171 | for obj in self: 172 | self._wrapper.validate(obj, required=required) 173 | 174 | def add(self, wrapped): 175 | wrapped, unwrapped = self._wrapper.unwrap(wrapped) 176 | if wrapped not in self: 177 | self._obj.append(unwrapped) 178 | super(JsonSet, self).add(wrapped) 179 | 180 | def remove(self, wrapped): 181 | wrapped, unwrapped = self._wrapper.unwrap(wrapped) 182 | if wrapped in self: 183 | self._obj.remove(unwrapped) 184 | super(JsonSet, self).remove(wrapped) 185 | else: 186 | raise KeyError(wrapped) 187 | 188 | def discard(self, wrapped): 189 | try: 190 | self.remove(wrapped) 191 | except KeyError: 192 | pass 193 | 194 | def pop(self): 195 | # get first item 196 | for wrapped in self: 197 | break 198 | else: 199 | raise KeyError() 200 | wrapped_, unwrapped = self._wrapper.unwrap(wrapped) 201 | assert wrapped is wrapped_ 202 | self.remove(unwrapped) 203 | return wrapped 204 | 205 | def clear(self): 206 | while self: 207 | self.pop() 208 | 209 | def __ior__(self, other): 210 | for wrapped in other: 211 | self.add(wrapped) 212 | return self 213 | 214 | def update(self, *args): 215 | for wrapped_list in args: 216 | self |= set(wrapped_list) 217 | 218 | union_update = update 219 | 220 | def __iand__(self, other): 221 | for wrapped in list(self): 222 | if wrapped not in other: 223 | self.remove(wrapped) 224 | return self 225 | 226 | def intersection_update(self, *args): 227 | for wrapped_list in args: 228 | self &= set(wrapped_list) 229 | 230 | def __isub__(self, other): 231 | for wrapped in list(self): 232 | if wrapped in other: 233 | self.remove(wrapped) 234 | return self 235 | 236 | def difference_update(self, *args): 237 | for wrapped_list in args: 238 | self -= set(wrapped_list) 239 | 240 | def __ixor__(self, other): 241 | removed = set() 242 | for wrapped in list(self): 243 | if wrapped in other: 244 | self.remove(wrapped) 245 | removed.add(wrapped) 246 | self.update(other - removed) 247 | return self 248 | 249 | def symmetric_difference_update(self, *args): 250 | for wrapped_list in args: 251 | self ^= set(wrapped_list) 252 | -------------------------------------------------------------------------------- /updateNeoforge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Get the source files necessary for generating NeoForge versions 4 | ''' 5 | from __future__ import print_function 6 | import sys 7 | 8 | import requests 9 | from cachecontrol import CacheControl 10 | from cachecontrol.caches import FileCache 11 | 12 | import json 13 | import copy 14 | import re 15 | import zipfile 16 | from metautil import * 17 | from jsonobject import * 18 | from neoforgeutil import * 19 | import os.path 20 | import datetime 21 | import hashlib 22 | from pathlib import Path 23 | from contextlib import suppress 24 | 25 | def eprint(*args, **kwargs): 26 | print(*args, file=sys.stderr, **kwargs) 27 | 28 | def filehash(filename, hashtype, blocksize=65536): 29 | hash = hashtype() 30 | with open(filename, "rb") as f: 31 | for block in iter(lambda: f.read(blocksize), b""): 32 | hash.update(block) 33 | return hash.hexdigest() 34 | 35 | forever_cache = FileCache('http_cache', forever=True) 36 | sess = CacheControl(requests.Session(), forever_cache) 37 | 38 | # get the remote version list fragments 39 | r = sess.get('https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge') 40 | r.raise_for_status() 41 | main_json = r.json()["versions"] 42 | assert type(main_json) == list 43 | 44 | # get the remote version list fragments 45 | r = sess.get("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge") 46 | r.raise_for_status() 47 | legacy_json = r.json()["versions"] 48 | assert type(legacy_json) == list 49 | 50 | main_json = legacy_json + main_json 51 | 52 | newIndex = DerivedNeoforgeIndex() 53 | 54 | versionExpression = re.compile("^(?P[0-9]+)\\.(?P[0-9]+)\\.(?P[0-9]+)(?:-(?P[a-zA-Z0-9_]+))?$") 55 | legacyVersionExpression = re.compile("^(?P[0-9a-zA-Z_\\.]+)-(?P[0-9\\.]+\\.(?P[0-9]+))(-(?P[a-zA-Z0-9\\.]+))?$") 56 | 57 | def getSingleNeoforgeFilesManifest(longversion, legacy): 58 | pathThing = "upstream/neoforge/files_manifests/%s.json" % longversion 59 | files_manifest_file = Path(pathThing) 60 | from_file = False 61 | if files_manifest_file.is_file(): 62 | with open(pathThing, 'r') as f: 63 | files_json=json.load(f) 64 | from_file = True 65 | else: 66 | fileUrl = 'https://maven.neoforged.net/api/maven/details/releases/net/neoforged/%s/%s' % ("forge" if legacy else "neoforge", longversion) 67 | r = sess.get(fileUrl) 68 | r.raise_for_status() 69 | files_json = r.json() 70 | 71 | retDict = dict() 72 | 73 | for file in files_json.get('files'): 74 | assert type(file) == dict 75 | 76 | if file["type"] != 'FILE': 77 | continue 78 | if file["name"].endswith((".md5", ".sha1", ".sha256", ".sha512", ".pom", ".asc", ".module")): 79 | continue 80 | 81 | fileName = file["name"] 82 | filePrefix = '%s-%s-' % ("forge" if legacy else "neoforge", longversion) 83 | 84 | if not fileName.startswith(filePrefix): 85 | print("fileName", fileName, "does not start with", filePrefix) 86 | assert False 87 | 88 | fileSuffix = fileName[len(filePrefix):] 89 | classifier, extension = os.path.splitext(fileSuffix) 90 | 91 | fileObj = NeoforgeFile( 92 | classifier=classifier, 93 | extension=extension[1:], 94 | legacy=legacy 95 | ) 96 | retDict[classifier] = fileObj 97 | 98 | if not from_file: 99 | with open(pathThing, 'w', encoding='utf-8') as f: 100 | json.dump(files_json, f, sort_keys=True, indent=4) 101 | 102 | return retDict 103 | 104 | print("") 105 | print("Making dirs...") 106 | os.makedirs("upstream/neoforge/jars/", exist_ok=True) 107 | os.makedirs("upstream/neoforge/installer_info/", exist_ok=True) 108 | os.makedirs("upstream/neoforge/installer_manifests/", exist_ok=True) 109 | os.makedirs("upstream/neoforge/version_manifests/", exist_ok=True) 110 | os.makedirs("upstream/neoforge/files_manifests/", exist_ok=True) 111 | 112 | print("") 113 | print("Processing versions:") 114 | for longversion in main_json: 115 | assert type(longversion) == str 116 | match = versionExpression.match(longversion) 117 | legacy = False 118 | 119 | if not match: 120 | match = legacyVersionExpression.match(longversion) 121 | if not match: 122 | pprint(longversion) 123 | continue 124 | # assert match 125 | legacy = True 126 | package = "forge" 127 | branch = match.group("branch") 128 | build = int(match.group("build")) 129 | mcversion = match.group("mc_version") 130 | ver = match.group("version") 131 | else: 132 | package = "neoforge" 133 | branch = match.group("branch") 134 | build = int(match.group("build")) 135 | if match.group('mcpatch') == '0': 136 | mcversion = '1.%s' % (match.group('mcminor')) 137 | else: 138 | mcversion = '1.%s.%s' % (match.group('mcminor'), match.group('mcpatch')) 139 | ver = match.group("build") 140 | 141 | try: 142 | files = getSingleNeoforgeFilesManifest(longversion, legacy) 143 | except requests.exceptions.HTTPError as err: 144 | if err.response.status_code == 404: 145 | continue 146 | else: 147 | raise 148 | except: 149 | raise 150 | 151 | entry = NeoforgeEntry( 152 | package=package, 153 | longversion=longversion, 154 | mcversion=mcversion, 155 | version=ver, 156 | build=build, 157 | branch=branch, 158 | latest=False, 159 | # TODO: 160 | recommended=False, 161 | files=files 162 | ) 163 | newIndex.versions[longversion] = entry 164 | if not newIndex.by_mcversion: 165 | newIndex.by_mcversion = dict() 166 | if not mcversion in newIndex.by_mcversion: 167 | newIndex.by_mcversion.setdefault(mcversion, NeoforgeMcVersionInfo()) 168 | newIndex.by_mcversion[mcversion].versions.append(longversion) 169 | 170 | if entry.recommended: 171 | newIndex.by_mcversion[mcversion].recommended = longversion 172 | 173 | print("") 174 | print("Post processing promotions and adding missing 'latest':") 175 | for mcversion, info in newIndex.by_mcversion.items(): 176 | latestVersion = info.versions[-1] 177 | info.latest = latestVersion 178 | newIndex.versions[latestVersion].latest = True 179 | print("Added %s as latest for %s" % (latestVersion, mcversion)) 180 | 181 | print("") 182 | print("Dumping index files...") 183 | 184 | with open("upstream/neoforge/maven-metadata.json", 'w', encoding='utf-8') as f: 185 | json.dump(main_json, f, sort_keys=True, indent=4) 186 | 187 | with open("upstream/neoforge/derived_index.json", 'w', encoding='utf-8') as f: 188 | json.dump(newIndex.to_json(), f, sort_keys=True, indent=4) 189 | 190 | versions = [] 191 | 192 | print("Grabbing installers and dumping installer profiles...") 193 | # get the installer jars - if needed - and get the installer profiles out of them 194 | for id, entry in newIndex.versions.items(): 195 | eprint ("Updating NeoForge %s" % id) 196 | if entry.mcversion == None: 197 | eprint ("Skipping %d with invalid MC version" % entry.build) 198 | continue 199 | 200 | version = NeoforgeVersion(entry) 201 | 202 | if version.url() == None: 203 | eprint ("Skipping %d with no valid files" % version.build) 204 | continue 205 | 206 | jarFilepath = "upstream/neoforge/jars/%s" % version.filename() 207 | 208 | installerInfoFilepath = "upstream/neoforge/installer_info/%s.json" % version.longVersion 209 | profileFilepath = "upstream/neoforge/installer_manifests/%s.json" % version.longVersion 210 | versionJsonFilepath = "upstream/neoforge/version_manifests/%s.json" % version.longVersion 211 | installerRefreshRequired = False 212 | if not os.path.isfile(profileFilepath): 213 | installerRefreshRequired = True 214 | if not os.path.isfile(installerInfoFilepath): 215 | installerRefreshRequired = True 216 | 217 | if installerRefreshRequired: 218 | # grab the installer if it's not there 219 | if not os.path.isfile(jarFilepath): 220 | eprint ("Downloading %s" % version.url()) 221 | rfile = sess.get(version.url(), stream=True) 222 | rfile.raise_for_status() 223 | with open(jarFilepath, 'wb') as f: 224 | for chunk in rfile.iter_content(chunk_size=128): 225 | f.write(chunk) 226 | 227 | eprint ("Processing %s" % version.url()) 228 | # harvestables from the installer 229 | if not os.path.isfile(profileFilepath): 230 | print(jarFilepath) 231 | with zipfile.ZipFile(jarFilepath, 'r') as jar: 232 | with suppress(KeyError): 233 | with jar.open('version.json', 'r') as profileZipEntry: 234 | versionJsonData = profileZipEntry.read(); 235 | versionJsonJson = json.loads(versionJsonData) 236 | profileZipEntry.close() 237 | 238 | # Process: does it parse? 239 | doesItParse = MojangVersionFile(versionJsonJson) 240 | 241 | with open(versionJsonFilepath, 'wb') as versionJsonFile: 242 | versionJsonFile.write(versionJsonData) 243 | versionJsonFile.close() 244 | 245 | with jar.open('install_profile.json', 'r') as profileZipEntry: 246 | installProfileJsonData = profileZipEntry.read() 247 | profileZipEntry.close() 248 | 249 | # Process: does it parse? 250 | installProfileJsonJson = json.loads(installProfileJsonData) 251 | atLeastOneFormatWorked = False 252 | exception = None 253 | try: 254 | doesItParseV1 = NeoforgeInstallerProfileV1(installProfileJsonJson) 255 | atLeastOneFormatWorked = True 256 | except BaseException as err: 257 | exception = err 258 | 259 | if not atLeastOneFormatWorked: 260 | if version.isSupported(): 261 | raise exception 262 | else: 263 | eprint ("Version %s is not supported and won't be generated later." % version.longVersion) 264 | 265 | with open(profileFilepath, 'wb') as profileFile: 266 | profileFile.write(installProfileJsonData) 267 | profileFile.close() 268 | 269 | # installer info v1 270 | if not os.path.isfile(installerInfoFilepath): 271 | installerInfo = InstallerInfo() 272 | eprint ("SHA1 %s" % jarFilepath) 273 | installerInfo.sha1hash = filehash(jarFilepath, hashlib.sha1) 274 | eprint ("SHA256 %s" % jarFilepath) 275 | installerInfo.sha256hash = filehash(jarFilepath, hashlib.sha256) 276 | eprint ("SIZE %s" % jarFilepath) 277 | installerInfo.size = os.path.getsize(jarFilepath) 278 | eprint ("DUMP %s" % jarFilepath) 279 | with open(installerInfoFilepath, 'w', encoding='utf-8') as installerInfoFile: 280 | json.dump(installerInfo.to_json(), installerInfoFile, sort_keys=True, indent=4) 281 | installerInfoFile.close() 282 | 283 | -------------------------------------------------------------------------------- /jsonobject/base_properties.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import six 3 | import inspect 4 | from .exceptions import BadValueError 5 | 6 | function_name = None 7 | if six.PY3: 8 | def function_name(f): 9 | return f.__name__ 10 | else: 11 | def function_name(f): 12 | return f.func_name 13 | 14 | 15 | class JsonProperty(object): 16 | 17 | default = None 18 | type_config = None 19 | 20 | def __init__(self, default=Ellipsis, name=None, choices=None, 21 | required=False, exclude_if_none=False, validators=None, 22 | verbose_name=None, type_config=None): 23 | validators = validators or () 24 | self.name = name 25 | if default is Ellipsis: 26 | default = self.default 27 | if callable(default): 28 | self.default = default 29 | else: 30 | self.default = lambda: default 31 | self.choices = choices 32 | self.choice_keys = [] 33 | if choices: 34 | for choice in choices: 35 | if isinstance(choice, tuple): 36 | choice, _ = choice 37 | self.choice_keys.append(choice) 38 | self.required = required 39 | self.exclude_if_none = exclude_if_none 40 | self._validators = validators 41 | self.verbose_name = verbose_name 42 | if type_config: 43 | self.type_config = type_config 44 | 45 | def init_property(self, default_name, type_config): 46 | self.name = self.name or default_name 47 | self.type_config = self.type_config or type_config 48 | 49 | def wrap(self, obj): 50 | raise NotImplementedError() 51 | 52 | def unwrap(self, obj): 53 | """ 54 | must return tuple of (wrapped, unwrapped) 55 | 56 | If obj is already a fully wrapped object, 57 | it must be returned as the first element. 58 | 59 | For an example where the first element is relevant see ListProperty 60 | 61 | """ 62 | raise NotImplementedError() 63 | 64 | def to_json(self, value): 65 | _, unwrapped = self.unwrap(value) 66 | return unwrapped 67 | 68 | def to_python(self, value): 69 | return self.wrap(value) 70 | 71 | def __get__(self, instance, owner): 72 | if instance: 73 | assert self.name in instance 74 | return instance[self.name] 75 | else: 76 | return self 77 | 78 | def __set__(self, instance, value): 79 | instance[self.name] = value 80 | 81 | def __call__(self, method): 82 | """ 83 | use a property as a decorator to set its default value 84 | 85 | class Document(JsonObject): 86 | @StringProperty() 87 | def doc_type(self): 88 | return self.__class__.__name__ 89 | """ 90 | assert self.default() is None 91 | self.default = method 92 | self.name = self.name or function_name(method) 93 | return self 94 | 95 | def exclude(self, value): 96 | return self.exclude_if_none and value == None 97 | 98 | def empty(self, value): 99 | return value is None 100 | 101 | def validate(self, value, required=True, recursive=True): 102 | if (self.choice_keys and value not in self.choice_keys 103 | and value is not None): 104 | raise BadValueError( 105 | '{0!r} not in choices: {1!r}'.format(value, self.choice_keys) 106 | ) 107 | 108 | if not self.empty(value): 109 | self._custom_validate(value) 110 | elif required and self.required: 111 | raise BadValueError( 112 | 'Property {0} is required.'.format(self.name) 113 | ) 114 | if recursive and hasattr(value, 'validate'): 115 | value.validate(required=required) 116 | 117 | def _custom_validate(self, value): 118 | if self._validators: 119 | if hasattr(self._validators, '__iter__'): 120 | for validator in self._validators: 121 | validator(value) 122 | else: 123 | self._validators(value) 124 | 125 | 126 | class JsonContainerProperty(JsonProperty): 127 | 128 | _type = default = None 129 | container_class = None 130 | 131 | def __init__(self, item_type=None, **kwargs): 132 | self._item_type_deferred = item_type 133 | super(JsonContainerProperty, self).__init__(**kwargs) 134 | 135 | def init_property(self, **kwargs): 136 | super(JsonContainerProperty, self).init_property(**kwargs) 137 | if not inspect.isfunction(self._item_type_deferred): 138 | # trigger validation 139 | self.item_type 140 | 141 | def set_item_type(self, item_type): 142 | from jsonobject.base import JsonObjectMeta 143 | if hasattr(item_type, '_type'): 144 | item_type = item_type._type 145 | if isinstance(item_type, tuple): 146 | # this is for the case where item_type = (int, long) 147 | item_type = item_type[0] 148 | allowed_types = set(self.type_config.properties.keys()) 149 | if isinstance(item_type, JsonObjectMeta) \ 150 | or not item_type or item_type in allowed_types: 151 | self._item_type = item_type 152 | else: 153 | raise ValueError("item_type {0!r} not in {1!r}".format( 154 | item_type, 155 | allowed_types, 156 | )) 157 | 158 | @property 159 | def item_type(self): 160 | if hasattr(self, '_item_type_deferred'): 161 | if inspect.isfunction(self._item_type_deferred): 162 | self.set_item_type(self._item_type_deferred()) 163 | else: 164 | self.set_item_type(self._item_type_deferred) 165 | del self._item_type_deferred 166 | return self._item_type 167 | 168 | def empty(self, value): 169 | return not value 170 | 171 | def wrap(self, obj): 172 | wrapper = self.type_to_property(self.item_type) if self.item_type else None 173 | return self.container_class(obj, wrapper=wrapper, 174 | type_config=self.type_config) 175 | 176 | def type_to_property(self, item_type): 177 | map_types_properties = self.type_config.properties 178 | from .properties import ObjectProperty 179 | from .base import JsonObjectBase 180 | if issubclass(item_type, JsonObjectBase): 181 | return ObjectProperty(item_type, type_config=self.type_config) 182 | elif item_type in map_types_properties: 183 | return map_types_properties[item_type](type_config=self.type_config) 184 | else: 185 | for key, value in map_types_properties.items(): 186 | if issubclass(item_type, key): 187 | return value(type_config=self.type_config) 188 | raise TypeError('Type {0} not recognized'.format(item_type)) 189 | 190 | def unwrap(self, obj): 191 | if not isinstance(obj, self._type): 192 | raise BadValueError( 193 | '{0!r} is not an instance of {1!r}'.format( 194 | obj, self._type.__name__) 195 | ) 196 | if isinstance(obj, self.container_class): 197 | return obj, obj._obj 198 | else: 199 | wrapped = self.wrap(self._type()) 200 | self._update(wrapped, obj) 201 | return self.unwrap(wrapped) 202 | 203 | def _update(self, container, extension): 204 | raise NotImplementedError() 205 | 206 | 207 | class DefaultProperty(JsonProperty): 208 | 209 | def wrap(self, obj): 210 | assert self.type_config.string_conversions is not None 211 | value = self.value_to_python(obj) 212 | property_ = self.value_to_property(value) 213 | 214 | if property_: 215 | return property_.wrap(obj) 216 | 217 | def unwrap(self, obj): 218 | property_ = self.value_to_property(obj) 219 | if property_: 220 | return property_.unwrap(obj) 221 | else: 222 | return obj, None 223 | 224 | def value_to_property(self, value): 225 | map_types_properties = self.type_config.properties 226 | if value is None: 227 | return None 228 | elif type(value) in map_types_properties: 229 | return map_types_properties[type(value)]( 230 | type_config=self.type_config) 231 | else: 232 | for value_type, prop_class in map_types_properties.items(): 233 | if isinstance(value, value_type): 234 | return prop_class(type_config=self.type_config) 235 | else: 236 | raise BadValueError( 237 | 'value {0!r} not in allowed types: {1!r}'.format( 238 | value, map_types_properties.keys()) 239 | ) 240 | 241 | def value_to_python(self, value): 242 | """ 243 | convert encoded string values to the proper python type 244 | 245 | ex: 246 | >>> DefaultProperty().value_to_python('2013-10-09T10:05:51Z') 247 | datetime.datetime(2013, 10, 9, 10, 5, 51) 248 | 249 | other values will be passed through unmodified 250 | Note: containers' items are NOT recursively converted 251 | 252 | """ 253 | if isinstance(value, six.string_types): 254 | convert = None 255 | for pattern, _convert in self.type_config.string_conversions: 256 | if pattern.match(value): 257 | convert = _convert 258 | break 259 | 260 | if convert is not None: 261 | try: 262 | #sometimes regex fail so return value 263 | value = convert(value) 264 | except Exception: 265 | pass 266 | return value 267 | 268 | 269 | class AssertTypeProperty(JsonProperty): 270 | _type = None 271 | 272 | def assert_type(self, obj): 273 | if not isinstance(obj, self._type): 274 | raise BadValueError( 275 | '{0!r} not of type {1!r}'.format(obj, self._type) 276 | ) 277 | 278 | def selective_coerce(self, obj): 279 | return obj 280 | 281 | def wrap(self, obj): 282 | obj = self.selective_coerce(obj) 283 | self.assert_type(obj) 284 | return obj 285 | 286 | def unwrap(self, obj): 287 | obj = self.selective_coerce(obj) 288 | self.assert_type(obj) 289 | return obj, obj 290 | 291 | 292 | class AbstractDateProperty(JsonProperty): 293 | 294 | _type = None 295 | 296 | def __init__(self, exact=False, *args, **kwargs): 297 | super(AbstractDateProperty, self).__init__(*args, **kwargs) 298 | self.exact = exact 299 | 300 | def wrap(self, obj): 301 | try: 302 | if not isinstance(obj, six.string_types): 303 | raise ValueError() 304 | return self._wrap(obj) 305 | except ValueError: 306 | raise BadValueError('{0!r} is not a {1}-formatted string'.format( 307 | obj, 308 | self._type.__name__, 309 | )) 310 | 311 | def unwrap(self, obj): 312 | if not isinstance(obj, self._type): 313 | raise BadValueError('{0!r} is not a {1} object'.format( 314 | obj, 315 | self._type.__name__, 316 | )) 317 | return self._unwrap(obj) 318 | 319 | def _wrap(self, obj): 320 | raise NotImplementedError() 321 | 322 | def _unwrap(self, obj): 323 | raise NotImplementedError() 324 | -------------------------------------------------------------------------------- /forgeutil.py: -------------------------------------------------------------------------------- 1 | from metautil import * 2 | from collections import namedtuple 3 | 4 | # A post-processed entry constructed from the reconstructed Forge version index 5 | class ForgeVersion: 6 | def __init__(self, entry): 7 | self.build = entry.build 8 | self.rawVersion = entry.version 9 | self.mcversion = entry.mcversion 10 | self.mcversion_sane = self.mcversion.replace("_pre", "-pre", 1) 11 | self.branch = entry.branch 12 | self.installer_filename = None 13 | self.installer_url = None 14 | self.universal_filename = None 15 | self.universal_url = None 16 | self.changelog_url = None 17 | self.longVersion = "%s-%s" % (self.mcversion, self.rawVersion) 18 | if self.branch != None: 19 | self.longVersion = self.longVersion + "-%s" % (self.branch) 20 | for classifier, fileentry in entry.files.items(): 21 | extension = fileentry.extension 22 | checksum = fileentry.hash 23 | filename = fileentry.filename(self.longVersion) 24 | url = fileentry.url(self.longVersion) 25 | if (classifier == "installer") and (extension == "jar"): 26 | self.installer_filename = filename 27 | self.installer_url = url 28 | if (classifier == "universal" or classifier == "client") and (extension == "jar" or extension == "zip"): 29 | self.universal_filename = filename 30 | self.universal_url = url 31 | if (classifier == "changelog") and (extension == "txt"): 32 | self.changelog_url = url 33 | 34 | def name(self): 35 | return "Forge %d" % (self.build) 36 | 37 | def usesInstaller(self): 38 | if self.installer_url == None: 39 | return False 40 | if self.mcversion == "1.5.2": 41 | return False 42 | return True 43 | 44 | def filename(self): 45 | if self.usesInstaller(): 46 | return self.installer_filename 47 | else: 48 | return self.universal_filename 49 | 50 | def url(self): 51 | if self.usesInstaller(): 52 | return self.installer_url 53 | else: 54 | return self.universal_url 55 | 56 | def isSupported(self): 57 | if self.url() == None: 58 | return False 59 | 60 | versionElements = self.rawVersion.split('.') 61 | if len(versionElements) < 1: 62 | return False 63 | 64 | majorVersionStr = versionElements[0] 65 | if not majorVersionStr.isnumeric(): 66 | return False 67 | 68 | #majorVersion = int(majorVersionStr) 69 | #if majorVersion >= 37: 70 | # return False 71 | 72 | return True 73 | 74 | class ForgeFile(JsonObject): 75 | classifier = StringProperty(required=True) 76 | hash = StringProperty(required=True) 77 | extension = StringProperty(required=True) 78 | 79 | def filename(self, longversion): 80 | return "%s-%s-%s.%s" % ("forge", longversion, self.classifier, self.extension) 81 | 82 | def url(self, longversion): 83 | return "https://files.minecraftforge.net/maven/net/minecraftforge/forge/%s/%s" % (longversion, self.filename(longversion)) 84 | 85 | class ForgeEntry(JsonObject): 86 | longversion = StringProperty(required=True) 87 | mcversion = StringProperty(required=True) 88 | version = StringProperty(required=True) 89 | build = IntegerProperty(required=True) 90 | branch = StringProperty() 91 | latest = BooleanProperty() 92 | recommended = BooleanProperty() 93 | files = DictProperty(ForgeFile) 94 | 95 | class ForgeMcVersionInfo(JsonObject): 96 | latest = StringProperty() 97 | recommended = StringProperty() 98 | versions = ListProperty(StringProperty()) 99 | 100 | class DerivedForgeIndex(JsonObject): 101 | versions = DictProperty(ForgeEntry) 102 | by_mcversion = DictProperty(ForgeMcVersionInfo) 103 | 104 | ''' 105 | FML library mappings - these are added to legacy Forge versions because Forge no longer can download these 106 | by itself - the locations have changed and some of this has to be rehosted on MultiMC servers. 107 | ''' 108 | 109 | FMLLib = namedtuple('FMLLib', ('filename', 'checksum', 'ours')) 110 | 111 | fmlLibsMapping = {} 112 | 113 | fmlLibsMapping["1.3.2"] = [ 114 | FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False), 115 | FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False), 116 | FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False) 117 | ] 118 | 119 | fml14 = [ 120 | FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False), 121 | FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False), 122 | FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False), 123 | FMLLib("bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", False) 124 | ] 125 | fmlLibsMapping["1.4"] = fml14; 126 | fmlLibsMapping["1.4.1"] = fml14; 127 | fmlLibsMapping["1.4.2"] = fml14; 128 | fmlLibsMapping["1.4.3"] = fml14; 129 | fmlLibsMapping["1.4.4"] = fml14; 130 | fmlLibsMapping["1.4.5"] = fml14; 131 | fmlLibsMapping["1.4.6"] = fml14; 132 | fmlLibsMapping["1.4.7"] = fml14; 133 | 134 | fmlLibsMapping["1.5"] = [ 135 | FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), 136 | FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), 137 | FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), 138 | FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), 139 | FMLLib("deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", False), 140 | FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) 141 | ] 142 | 143 | fmlLibsMapping["1.5.1"] = [ 144 | FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), 145 | FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), 146 | FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), 147 | FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), 148 | FMLLib("deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", False), 149 | FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) 150 | ] 151 | 152 | fmlLibsMapping["1.5.2"] = [ 153 | FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), 154 | FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), 155 | FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), 156 | FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), 157 | FMLLib("deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", False), 158 | FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) 159 | ] 160 | 161 | ''' 162 | "install": { 163 | "profileName": "Forge", 164 | "target":"Forge8.9.0.753", 165 | "path":"net.minecraftforge:minecraftforge:8.9.0.753", 166 | "version":"Forge 8.9.0.753", 167 | "filePath":"minecraftforge-universal-1.6.1-8.9.0.753.jar", 168 | "welcome":"Welcome to the simple Forge installer.", 169 | "minecraft":"1.6.1", 170 | "logo":"/big_logo.png", 171 | "mirrorList": "http://files.minecraftforge.net/mirror-brand.list" 172 | }, 173 | "install": { 174 | "profileName": "forge", 175 | "target":"1.11-forge1.11-13.19.0.2141", 176 | "path":"net.minecraftforge:forge:1.11-13.19.0.2141", 177 | "version":"forge 1.11-13.19.0.2141", 178 | "filePath":"forge-1.11-13.19.0.2141-universal.jar", 179 | "welcome":"Welcome to the simple forge installer.", 180 | "minecraft":"1.11", 181 | "mirrorList" : "http://files.minecraftforge.net/mirror-brand.list", 182 | "logo":"/big_logo.png", 183 | "modList":"none" 184 | }, 185 | ''' 186 | class ForgeInstallerProfileInstallSection(JsonObject): 187 | profileName = StringProperty(required = True) 188 | target = StringProperty(required = True) 189 | path = GradleSpecifierProperty(required = True) 190 | version = StringProperty(required = True) 191 | filePath = StringProperty(required = True) 192 | welcome = StringProperty(required = True) 193 | minecraft = StringProperty(required = True) 194 | logo = StringProperty(required = True) 195 | mirrorList = StringProperty(required = True) 196 | modList = StringProperty(exclude_if_none=True, default=None) 197 | 198 | class ForgeLibrary (MojangLibrary): 199 | url = StringProperty(exclude_if_none=True) 200 | serverreq = BooleanProperty(exclude_if_none=True, default=None) 201 | clientreq = BooleanProperty(exclude_if_none=True, default=None) 202 | checksums = ListProperty(StringProperty) 203 | comment = StringProperty() 204 | 205 | class ForgeVersionFile (MojangVersionFile): 206 | libraries = ListProperty(ForgeLibrary, exclude_if_none=True, default=None) # overrides Mojang libraries 207 | inheritsFrom = StringProperty() 208 | jar = StringProperty() 209 | 210 | ''' 211 | "optionals": [ 212 | { 213 | "name": "Mercurius", 214 | "client": true, 215 | "server": true, 216 | "default": true, 217 | "inject": true, 218 | "desc": "A mod that collects statistics about Minecraft and your system.
Useful for Forge to understand how Minecraft/Forge are used.", 219 | "url": "http://www.minecraftforge.net/forum/index.php?topic=43278.0", 220 | "artifact": "net.minecraftforge:MercuriusUpdater:1.11.2", 221 | "maven": "http://files.minecraftforge.net/maven/" 222 | } 223 | ] 224 | ''' 225 | class ForgeOptional (JsonObject): 226 | name = StringProperty() 227 | client = BooleanProperty() 228 | server = BooleanProperty() 229 | default = BooleanProperty() 230 | inject = BooleanProperty() 231 | desc = StringProperty() 232 | url = StringProperty() 233 | artifact = GradleSpecifierProperty() 234 | maven = StringProperty() 235 | 236 | class ForgeInstallerProfile(JsonObject): 237 | install = ObjectProperty(ForgeInstallerProfileInstallSection, required = True) 238 | versionInfo = ObjectProperty(ForgeVersionFile, required = True) 239 | optionals = ListProperty(ForgeOptional) 240 | 241 | class ForgeLegacyInfo(JsonObject): 242 | releaseTime = ISOTimestampProperty() 243 | size = IntegerProperty() 244 | sha256 = StringProperty() 245 | sha1 = StringProperty() 246 | 247 | class ForgeLegacyInfoList(JsonObject): 248 | number = DictProperty(ForgeLegacyInfo) 249 | 250 | class DataSpec(JsonObject): 251 | client = StringProperty() 252 | server = StringProperty() 253 | 254 | class ProcessorSpec(JsonObject): 255 | jar = StringProperty() 256 | classpath = ListProperty(StringProperty) 257 | args = ListProperty(StringProperty) 258 | outputs = DictProperty(StringProperty) 259 | sides = ListProperty(StringProperty, exclude_if_none=True, default=None) 260 | 261 | # Note: This is only used in one version (1.12.2-14.23.5.2851) and we don't even use the installer profile in it. 262 | # It's here just so it parses and we can continue... 263 | class ForgeInstallerProfileV1_5(JsonObject): 264 | _comment = ListProperty(StringProperty) 265 | spec = IntegerProperty() 266 | profile = StringProperty() 267 | version = StringProperty() 268 | icon = StringProperty() 269 | json = StringProperty() 270 | path = GradleSpecifierProperty() 271 | logo = StringProperty() 272 | minecraft = StringProperty() 273 | welcome = StringProperty() 274 | # We don't know what 'data' actually is in this one. It's an empty array 275 | data = ListProperty(StringProperty) 276 | processors = ListProperty(ProcessorSpec) 277 | libraries = ListProperty(MojangLibrary) 278 | mirrorList = StringProperty(exclude_if_none=True, default=None) 279 | 280 | class ForgeInstallerProfileV2(JsonObject): 281 | _comment = ListProperty(StringProperty) 282 | spec = IntegerProperty() 283 | profile = StringProperty() 284 | version = StringProperty() 285 | icon = StringProperty() 286 | json = StringProperty() 287 | path = GradleSpecifierProperty() 288 | logo = StringProperty() 289 | minecraft = StringProperty() 290 | welcome = StringProperty() 291 | data = DictProperty(DataSpec) 292 | processors = ListProperty(ProcessorSpec) 293 | libraries = ListProperty(MojangLibrary) 294 | hideExtract = BooleanProperty() 295 | serverJarPath = StringProperty() 296 | mirrorList = StringProperty(exclude_if_none=True, default=None) 297 | serverJarPath = StringProperty(exclude_if_none=True, default=None) 298 | 299 | class InstallerInfo(JsonObject): 300 | sha1hash = StringProperty() 301 | sha256hash = StringProperty() 302 | size = IntegerProperty() 303 | -------------------------------------------------------------------------------- /static/minecraft.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": { 3 | "1.5.2": { 4 | "releaseTime": "2013-04-25T17:45:00+02:00", 5 | "+traits": ["legacyLaunch", "texturepacks"] 6 | }, 7 | "1.5.1": { 8 | "releaseTime": "2013-03-20T12:00:00+02:00", 9 | "+traits": ["legacyLaunch", "texturepacks"] 10 | }, 11 | "1.5": { 12 | "releaseTime": "2013-03-07T00:00:00+02:00", 13 | "+traits": ["legacyLaunch", "texturepacks"] 14 | }, 15 | "1.4.7": { 16 | "releaseTime": "2012-12-28T00:00:00+02:00", 17 | "+traits": ["legacyLaunch", "texturepacks"] 18 | }, 19 | "1.4.6": { 20 | "releaseTime": "2012-12-20T00:00:00+02:00", 21 | "+traits": ["legacyLaunch", "texturepacks"] 22 | }, 23 | "1.4.5": { 24 | "releaseTime": "2012-11-20T00:00:00+02:00", 25 | "+traits": ["legacyLaunch", "texturepacks"] 26 | }, 27 | "1.4.4": { 28 | "releaseTime": "2012-11-14T00:00:00+02:00", 29 | "+traits": ["legacyLaunch", "texturepacks"] 30 | }, 31 | "1.4.3": { 32 | "releaseTime": "2012-11-01T00:00:00+02:00", 33 | "+traits": ["legacyLaunch", "texturepacks"] 34 | }, 35 | "1.4.2": { 36 | "releaseTime": "2012-10-25T00:00:00+02:00", 37 | "+traits": ["legacyLaunch", "texturepacks"] 38 | }, 39 | "1.4.1": { 40 | "releaseTime": "2012-10-23T00:00:00+02:00", 41 | "+traits": ["legacyLaunch", "texturepacks"] 42 | }, 43 | "1.4": { 44 | "releaseTime": "2012-10-19T00:00:00+02:00", 45 | "+traits": ["legacyLaunch", "texturepacks"] 46 | }, 47 | "1.3.2": { 48 | "releaseTime": "2012-08-16T00:00:00+02:00", 49 | "+traits": ["legacyLaunch", "texturepacks"] 50 | }, 51 | "1.3.1": { 52 | "releaseTime": "2012-08-01T00:00:00+02:00", 53 | "+traits": ["legacyLaunch", "texturepacks"] 54 | }, 55 | "1.3": { 56 | "releaseTime": "2012-07-26T00:00:00+02:00", 57 | "+traits": ["legacyLaunch", "texturepacks"] 58 | }, 59 | "1.2.5": { 60 | "releaseTime": "2012-03-30T00:00:00+02:00", 61 | "+traits": ["legacyLaunch", "texturepacks"] 62 | }, 63 | "1.2.4": { 64 | "releaseTime": "2012-03-22T00:00:00+02:00", 65 | "+traits": ["legacyLaunch", "texturepacks"] 66 | }, 67 | "1.2.3": { 68 | "releaseTime": "2012-03-02T00:00:00+02:00", 69 | "+traits": ["legacyLaunch", "texturepacks"] 70 | }, 71 | "1.2.2": { 72 | "releaseTime": "2012-03-01T00:00:01+02:00", 73 | "+traits": ["legacyLaunch", "texturepacks"] 74 | }, 75 | "1.2.1": { 76 | "releaseTime": "2012-03-01T00:00:00+02:00", 77 | "+traits": ["legacyLaunch", "texturepacks"] 78 | }, 79 | "1.1": { 80 | "releaseTime": "2012-01-12T00:00:00+02:00", 81 | "+traits": ["legacyLaunch", "texturepacks"] 82 | }, 83 | "1.0": { 84 | "releaseTime": "2011-11-18T00:00:00+02:00", 85 | "+traits": ["legacyLaunch", "texturepacks"] 86 | }, 87 | "b1.8.1": { 88 | "releaseTime": "2011-09-19T00:00:00+02:00", 89 | "+traits": ["legacyLaunch", "texturepacks"] 90 | }, 91 | "b1.8": { 92 | "releaseTime": "2011-09-15T00:00:00+02:00", 93 | "+traits": ["legacyLaunch", "texturepacks"] 94 | }, 95 | "b1.7.3": { 96 | "releaseTime": "2011-07-08T00:00:00+02:00", 97 | "+traits": ["legacyLaunch", "texturepacks"] 98 | }, 99 | "b1.7.2": { 100 | "releaseTime": "2011-07-01T00:00:00+02:00", 101 | "+traits": ["legacyLaunch", "texturepacks"] 102 | }, 103 | "b1.7": { 104 | "releaseTime": "2011-06-30T00:00:00+02:00", 105 | "+traits": ["legacyLaunch", "texturepacks"] 106 | }, 107 | "b1.6.6": { 108 | "releaseTime": "2011-05-31T00:00:00+02:00", 109 | "+traits": ["legacyLaunch", "texturepacks"] 110 | }, 111 | "b1.6.5": { 112 | "releaseTime": "2011-05-28T00:00:00+02:00", 113 | "+traits": ["legacyLaunch", "texturepacks"] 114 | }, 115 | "b1.6.4": { 116 | "releaseTime": "2011-05-26T00:00:04+02:00", 117 | "+traits": ["legacyLaunch", "texturepacks"] 118 | }, 119 | "b1.6.3": { 120 | "releaseTime": "2011-05-26T00:00:03+02:00", 121 | "+traits": ["legacyLaunch", "texturepacks"] 122 | }, 123 | "b1.6.2": { 124 | "releaseTime": "2011-05-26T00:00:02+02:00", 125 | "+traits": ["legacyLaunch", "texturepacks"] 126 | }, 127 | "b1.6.1": { 128 | "releaseTime": "2011-05-26T00:00:01+02:00", 129 | "+traits": ["legacyLaunch", "texturepacks"] 130 | }, 131 | "b1.6": { 132 | "releaseTime": "2011-05-26T00:00:00+02:00", 133 | "+traits": ["legacyLaunch", "texturepacks"] 134 | }, 135 | "b1.5_01": { 136 | "releaseTime": "2011-04-20T00:00:00+02:00", 137 | "+traits": ["legacyLaunch", "texturepacks"] 138 | }, 139 | "b1.5": { 140 | "releaseTime": "2011-04-19T00:00:00+02:00", 141 | "+traits": ["legacyLaunch", "texturepacks"] 142 | }, 143 | "b1.4_01": { 144 | "releaseTime": "2011-04-05T00:00:00+02:00", 145 | "+traits": ["legacyLaunch", "texturepacks"] 146 | }, 147 | "b1.4": { 148 | "releaseTime": "2011-03-31T00:00:00+02:00", 149 | "+traits": ["legacyLaunch", "texturepacks"] 150 | }, 151 | "b1.3_01": { 152 | "releaseTime": "2011-02-23T00:00:00+02:00", 153 | "+traits": ["legacyLaunch", "texturepacks"] 154 | }, 155 | "b1.3b": { 156 | "releaseTime": "2011-02-22T00:00:00+02:00", 157 | "+traits": ["legacyLaunch", "texturepacks"] 158 | }, 159 | "b1.2_02": { 160 | "releaseTime": "2011-01-21T00:00:00+02:00", 161 | "+traits": ["legacyLaunch", "texturepacks"] 162 | }, 163 | "b1.2_01": { 164 | "releaseTime": "2011-01-14T00:00:00+02:00", 165 | "+traits": ["legacyLaunch", "texturepacks"] 166 | }, 167 | "b1.2": { 168 | "releaseTime": "2011-01-13T00:00:00+02:00", 169 | "+traits": ["legacyLaunch", "texturepacks"] 170 | }, 171 | "b1.1_02": { 172 | "releaseTime": "2010-12-22T00:00:01+02:00", 173 | "+traits": ["legacyLaunch", "texturepacks"] 174 | }, 175 | "b1.1_01": { 176 | "releaseTime": "2010-12-22T00:00:00+02:00", 177 | "+traits": ["legacyLaunch", "texturepacks"] 178 | }, 179 | "b1.0.2": { 180 | "releaseTime": "2010-12-21T00:00:00+02:00", 181 | "+traits": ["legacyLaunch", "texturepacks"] 182 | }, 183 | "b1.0_01": { 184 | "releaseTime": "2010-12-20T00:00:01+02:00", 185 | "+traits": ["legacyLaunch", "texturepacks"] 186 | }, 187 | "b1.0": { 188 | "releaseTime": "2010-12-20T00:00:00+02:00", 189 | "+traits": ["legacyLaunch", "texturepacks"] 190 | }, 191 | "a1.2.6": { 192 | "releaseTime": "2010-12-03T00:00:00+02:00", 193 | "+traits": ["legacyLaunch", "texturepacks"] 194 | }, 195 | "a1.2.5": { 196 | "releaseTime": "2010-12-01T00:00:00+02:00", 197 | "+traits": ["legacyLaunch", "texturepacks"] 198 | }, 199 | "a1.2.4_01": { 200 | "releaseTime": "2010-11-30T00:00:00+02:00", 201 | "+traits": ["legacyLaunch", "texturepacks"] 202 | }, 203 | "a1.2.3_04": { 204 | "releaseTime": "2010-11-26T00:00:00+02:00", 205 | "+traits": ["legacyLaunch", "texturepacks"] 206 | }, 207 | "a1.2.3_02": { 208 | "releaseTime": "2010-11-25T00:00:00+02:00", 209 | "+traits": ["legacyLaunch", "texturepacks"] 210 | }, 211 | "a1.2.3_01": { 212 | "releaseTime": "2010-11-24T00:00:01+02:00", 213 | "+traits": ["legacyLaunch", "texturepacks"] 214 | }, 215 | "a1.2.3": { 216 | "releaseTime": "2010-11-24T00:00:00+02:00", 217 | "+traits": ["legacyLaunch", "texturepacks"] 218 | }, 219 | "a1.2.2b": { 220 | "releaseTime": "2010-11-10T00:00:01+02:00", 221 | "+traits": ["legacyLaunch", "texturepacks"] 222 | }, 223 | "a1.2.2a": { 224 | "releaseTime": "2010-11-10T00:00:00+02:00", 225 | "+traits": ["legacyLaunch", "texturepacks"] 226 | }, 227 | "a1.2.1_01": { 228 | "releaseTime": "2010-11-05T00:00:01+02:00", 229 | "+traits": ["legacyLaunch", "no-texturepacks"] 230 | }, 231 | "a1.2.1": { 232 | "releaseTime": "2010-11-05T00:00:00+02:00", 233 | "+traits": ["legacyLaunch", "no-texturepacks"] 234 | }, 235 | "a1.2.0_02": { 236 | "releaseTime": "2010-11-04T00:00:00+02:00", 237 | "+traits": ["legacyLaunch", "no-texturepacks"] 238 | }, 239 | "a1.2.0_01": { 240 | "releaseTime": "2010-10-31T00:00:00+02:00", 241 | "+traits": ["legacyLaunch", "no-texturepacks"] 242 | }, 243 | "a1.2.0": { 244 | "releaseTime": "2010-10-30T00:00:00+02:00", 245 | "+traits": ["legacyLaunch", "no-texturepacks"] 246 | }, 247 | "a1.1.2_01": { 248 | "releaseTime": "2010-09-23T00:00:00+02:00", 249 | "+traits": ["legacyLaunch", "no-texturepacks"] 250 | }, 251 | "a1.1.2": { 252 | "releaseTime": "2010-09-20T00:00:00+02:00", 253 | "+traits": ["legacyLaunch", "no-texturepacks"] 254 | }, 255 | "a1.1.0": { 256 | "releaseTime": "2010-09-13T00:00:00+02:00", 257 | "+traits": ["legacyLaunch", "no-texturepacks"] 258 | }, 259 | "a1.0.17_04": { 260 | "releaseTime": "2010-08-23T00:00:00+02:00", 261 | "+traits": ["legacyLaunch", "no-texturepacks"] 262 | }, 263 | "a1.0.17_02": { 264 | "releaseTime": "2010-08-20T00:00:00+02:00", 265 | "+traits": ["legacyLaunch", "no-texturepacks"] 266 | }, 267 | "a1.0.16": { 268 | "releaseTime": "2010-08-12T00:00:00+02:00", 269 | "+traits": ["legacyLaunch", "no-texturepacks"] 270 | }, 271 | "a1.0.15": { 272 | "releaseTime": "2010-08-04T00:00:00+02:00", 273 | "+traits": ["legacyLaunch", "no-texturepacks"] 274 | }, 275 | "a1.0.14": { 276 | "releaseTime": "2010-07-30T00:00:00+02:00", 277 | "+traits": ["legacyLaunch", "no-texturepacks"] 278 | }, 279 | "a1.0.11": { 280 | "releaseTime": "2010-07-23T00:00:00+02:00", 281 | "+traits": ["legacyLaunch", "no-texturepacks"] 282 | }, 283 | "a1.0.5_01": { 284 | "releaseTime": "2010-07-13T00:00:00+02:00", 285 | "mainClass": "y", 286 | "+traits": ["legacyLaunch", "no-texturepacks"] 287 | }, 288 | "a1.0.4": { 289 | "releaseTime": "2010-07-09T00:00:00+02:00", 290 | "mainClass": "ax", 291 | "+traits": ["legacyLaunch", "no-texturepacks"] 292 | }, 293 | "inf-20100618": { 294 | "releaseTime": "2010-06-16T00:00:00+02:00", 295 | "mainClass": "net.minecraft.client.d", 296 | "appletClass": "net.minecraft.client.MinecraftApplet", 297 | "+traits": ["legacyLaunch", "no-texturepacks"] 298 | }, 299 | "c0.30_01c": { 300 | "releaseTime": "2009-12-22T00:00:00+02:00", 301 | "mainClass": "com.mojang.minecraft.l", 302 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 303 | "+traits": ["legacyLaunch", "no-texturepacks"] 304 | }, 305 | "c0.0.13a_03": { 306 | "releaseTime": "2009-05-22T00:00:00+02:00", 307 | "mainClass": "com.mojang.minecraft.c", 308 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 309 | "+traits": ["legacyLaunch", "no-texturepacks"] 310 | }, 311 | "c0.0.13a": { 312 | "releaseTime": "2009-05-31T00:00:00+02:00", 313 | "mainClass": "com.mojang.minecraft.Minecraft", 314 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 315 | "+traits": ["legacyLaunch", "no-texturepacks"] 316 | }, 317 | "c0.0.11a": { 318 | "releaseTime": "2009-05-17T00:00:00+02:00", 319 | "mainClass": "com.mojang.minecraft.Minecraft", 320 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 321 | "+traits": ["legacyLaunch", "no-texturepacks"] 322 | }, 323 | "rd-161348": { 324 | "releaseTime": "2009-05-16T13:48:00+02:00", 325 | "mainClass": "com.mojang.minecraft.RubyDung", 326 | "+traits": ["no-texturepacks"] 327 | }, 328 | "rd-160052": { 329 | "releaseTime": "2009-05-16T00:52:00+02:00", 330 | "mainClass": "com.mojang.rubydung.RubyDung", 331 | "+traits": ["no-texturepacks"] 332 | }, 333 | "rd-20090515": { 334 | "mainClass": "com.mojang.rubydung.RubyDung", 335 | "+traits": ["no-texturepacks"] 336 | }, 337 | "rd-132328": { 338 | "releaseTime": "2009-05-13T23:28:00+02:00", 339 | "mainClass": "com.mojang.rubydung.RubyDung", 340 | "+traits": ["no-texturepacks"] 341 | }, 342 | "rd-132211": { 343 | "releaseTime": "2009-05-13T22:11:00+02:00", 344 | "mainClass": "com.mojang.rubydung.RubyDung", 345 | "+traits": ["no-texturepacks"] 346 | } 347 | } 348 | } -------------------------------------------------------------------------------- /metautil.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pprint import pprint 3 | from jsonobject import * 4 | from properties import * 5 | 6 | ''' 7 | Mojang index files look like this: 8 | { 9 | "latest": { 10 | "release": "1.11.2", 11 | "snapshot": "17w06a" 12 | }, 13 | "versions": [ 14 | ... 15 | { 16 | "id": "17w06a", 17 | "releaseTime": "2017-02-08T13:16:29+00:00", 18 | "time": "2017-02-08T13:17:20+00:00", 19 | "type": "snapshot", 20 | "url": "https://launchermeta.mojang.com/mc/game/7db0c61afa278d016cf1dae2fba0146edfbf2f8e/17w06a.json" 21 | }, 22 | ... 23 | ] 24 | } 25 | ''' 26 | 27 | class MojangIndexEntry(JsonObject): 28 | id = StringProperty() 29 | releaseTime = ISOTimestampProperty() 30 | time = ISOTimestampProperty() 31 | type = StringProperty() 32 | url = StringProperty() 33 | sha1 = StringProperty(exclude_if_none=True, default=None) 34 | complianceLevel = IntegerProperty(exclude_if_none=True, default=None) 35 | 36 | class MojangIndex(JsonObject): 37 | latest = DictProperty(StringProperty) 38 | versions = ListProperty(MojangIndexEntry) 39 | 40 | class MojangIndexWrap: 41 | def __init__(self, json): 42 | self.index = MojangIndex.wrap(json) 43 | self.latest = self.index.latest 44 | versionsDict = {} 45 | for version in self.index.versions: 46 | versionsDict[version.id] = version 47 | self.versions = versionsDict 48 | 49 | 50 | class MojangArtifactBase (JsonObject): 51 | sha1 = StringProperty(exclude_if_none=True, default=None) 52 | size = IntegerProperty(exclude_if_none=True, default=None) 53 | url = StringProperty() 54 | 55 | class MojangArtifact (MojangArtifactBase): 56 | path = StringProperty(exclude_if_none=True, default=None) 57 | 58 | class MojangAssets (MojangArtifactBase): 59 | id = StringProperty() 60 | totalSize = IntegerProperty() 61 | 62 | class MojangLibraryDownloads(JsonObject): 63 | artifact = ObjectProperty(MojangArtifact, exclude_if_none=True, default=None) 64 | classifiers = DictProperty(MojangArtifact, exclude_if_none=True, default=None) 65 | 66 | class MojangLibraryExtractRules(JsonObject): 67 | exclude = ListProperty(StringProperty) 68 | 69 | ''' 70 | "rules": [ 71 | { 72 | "action": "allow" 73 | }, 74 | { 75 | "action": "disallow", 76 | "os": { 77 | "name": "osx" 78 | } 79 | } 80 | ] 81 | ''' 82 | 83 | class OSRule (JsonObject): 84 | name = StringProperty(choices=["osx", "linux", "windows"], required = True) 85 | version = StringProperty(exclude_if_none=True, default=None) 86 | 87 | class MojangRule (JsonObject): 88 | action = StringProperty(choices=["allow", "disallow"], required = True) 89 | os = ObjectProperty(OSRule, exclude_if_none=True, default=None) 90 | 91 | class MojangLibrary (JsonObject): 92 | extract = ObjectProperty(MojangLibraryExtractRules, exclude_if_none=True, default=None) 93 | name = GradleSpecifierProperty(required = True) 94 | downloads = ObjectProperty(MojangLibraryDownloads, exclude_if_none=True, default=None) 95 | natives = DictProperty(StringProperty, exclude_if_none=True, default=None) 96 | rules = ListProperty(MojangRule, exclude_if_none=True, default=None) 97 | 98 | class MojangLoggingArtifact (MojangArtifactBase): 99 | id = StringProperty() 100 | 101 | class MojangLogging (JsonObject): 102 | file = ObjectProperty(MojangLoggingArtifact, required = True) 103 | argument = StringProperty(required = True) 104 | type = StringProperty(required = True, choices=["log4j2-xml"]) 105 | 106 | class MojangArguments (JsonObject): 107 | game = ListProperty(exclude_if_none=True, default=None) 108 | jvm = ListProperty(exclude_if_none=True, default=None) 109 | 110 | class JavaVersion (JsonObject): 111 | component = StringProperty(default="jre-legacy") 112 | majorVersion = IntegerProperty(default=8) 113 | 114 | class UnknownVersionException(Exception): 115 | """Exception raised for unknown Mojang version file format versions. 116 | 117 | Attributes: 118 | message -- explanation of the error 119 | """ 120 | def __init__(self, message): 121 | self.message = message 122 | 123 | def validateSupportedMojangVersion(version): 124 | supportedVersion = 21 125 | if version > supportedVersion: 126 | raise UnknownVersionException("Unsupported Mojang format version: %d. Max supported is: %d" % (version, supportedVersion)) 127 | 128 | class MojangVersionFile (JsonObject): 129 | arguments = ObjectProperty(MojangArguments, exclude_if_none=True, default=None) 130 | assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None) 131 | assets = StringProperty(exclude_if_none=True, default=None) 132 | downloads = DictProperty(MojangArtifactBase, exclude_if_none=True, default=None) 133 | id = StringProperty(exclude_if_none=True, default=None) 134 | libraries = ListProperty(MojangLibrary, exclude_if_none=True, default=None) 135 | mainClass = StringProperty(exclude_if_none=True, default=None) 136 | processArguments = StringProperty(exclude_if_none=True, default=None) 137 | minecraftArguments = StringProperty(exclude_if_none=True, default=None) 138 | minimumLauncherVersion = IntegerProperty(exclude_if_none=True, default=None, validators=validateSupportedMojangVersion) 139 | releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) 140 | time = ISOTimestampProperty(exclude_if_none=True, default=None) 141 | type = StringProperty(exclude_if_none=True, default=None) 142 | inheritsFrom = StringProperty(exclude_if_none=True, default=None) 143 | logging = DictProperty(MojangLogging, exclude_if_none=True, default=None) 144 | complianceLevel = IntegerProperty(exclude_if_none=True, default=None) 145 | javaVersion = ObjectProperty(JavaVersion, exclude_if_none=True, default=None) 146 | 147 | CurrentMultiMCFormatVersion = 1 148 | def validateSupportedMultiMCVersion(version): 149 | if version > CurrentMultiMCFormatVersion: 150 | raise UnknownVersionException("Unsupported MultiMC format version: %d. Max supported is: %d" % (version, CurrentMultiMCFormatVersion)) 151 | 152 | class MultiMCLibrary (MojangLibrary): 153 | url = StringProperty(exclude_if_none=True, default=None) 154 | mmcHint = StringProperty(name="MMC-hint", exclude_if_none=True, default=None) 155 | 156 | class VersionedJsonObject(JsonObject): 157 | formatVersion = IntegerProperty(default=CurrentMultiMCFormatVersion, validators=validateSupportedMultiMCVersion) 158 | 159 | class DependencyEntry (JsonObject): 160 | uid = StringProperty(required=True) 161 | equals = StringProperty(exclude_if_none=True, default=None) 162 | suggests = StringProperty(exclude_if_none=True, default=None) 163 | 164 | class MultiMCVersionFile (VersionedJsonObject): 165 | name = StringProperty(required=True) 166 | version = StringProperty(required=True) 167 | uid = StringProperty(required=True) 168 | requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None) 169 | conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None) 170 | volatile = BooleanProperty(exclude_if_none=True, default=None) 171 | assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None) 172 | libraries = ListProperty(MultiMCLibrary, exclude_if_none=True, default=None) 173 | mavenFiles = ListProperty(MultiMCLibrary, exclude_if_none=True, default=None) 174 | mainJar = ObjectProperty(MultiMCLibrary, exclude_if_none=True, default=None) 175 | jarMods = ListProperty(MultiMCLibrary, exclude_if_none=True, default=None) 176 | mainClass = StringProperty(exclude_if_none=True, default=None) 177 | appletClass = StringProperty(exclude_if_none=True, default=None) 178 | minecraftArguments = StringProperty(exclude_if_none=True, default=None) 179 | releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) 180 | type = StringProperty(exclude_if_none=True, default=None) 181 | addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None) 182 | addTweakers = ListProperty(StringProperty, name="+tweakers", exclude_if_none=True, default=None) 183 | order = IntegerProperty(exclude_if_none=True, default=None) 184 | 185 | class UnknownComplianceLevelException(Exception): 186 | """Exception raised for unknown Mojang compliance level 187 | 188 | Attributes: 189 | message -- explanation of the error 190 | """ 191 | def __init__(self, message): 192 | self.message = message 193 | 194 | 195 | # Convert Mojang version file object to a MultiMC version file object 196 | def MojangToMultiMC (file, name, uid, version): 197 | mmcFile = MultiMCVersionFile( 198 | { 199 | "name": name, 200 | "uid": uid, 201 | "version": version 202 | } 203 | ) 204 | mmcFile.assetIndex = file.assetIndex 205 | mmcFile.libraries = file.libraries 206 | mmcFile.mainClass = file.mainClass 207 | if file.id: 208 | mainJar = MultiMCLibrary( 209 | { 210 | "name": "com.mojang:minecraft:%s:client" % file.id, 211 | } 212 | ) 213 | cldl = file.downloads['client'] 214 | mainJar.downloads = MojangLibraryDownloads() 215 | mainJar.downloads.artifact = MojangArtifact() 216 | mainJar.downloads.artifact.path = None 217 | mainJar.downloads.artifact.url = cldl.url 218 | mainJar.downloads.artifact.sha1 = cldl.sha1 219 | mainJar.downloads.artifact.size = cldl.size 220 | mmcFile.mainJar = mainJar 221 | 222 | mmcFile.minecraftArguments = file.minecraftArguments 223 | mmcFile.releaseTime = file.releaseTime 224 | # time should not be set. 225 | mmcFile.type = file.type 226 | maxSupportedLevel = 1 227 | if file.complianceLevel: 228 | if file.complianceLevel == 0: 229 | pass 230 | elif file.complianceLevel == 1: 231 | if not mmcFile.addTraits: 232 | mmcFile.addTraits = [] 233 | mmcFile.addTraits.append("XR:Initial") 234 | else: 235 | raise UnknownComplianceLevelException("Unsupported Mojang compliance level: %d. Max supported is: %d" % (file.complianceLevel, maxSupportedLevel)) 236 | return mmcFile 237 | 238 | class MultiMCSharedPackageData(VersionedJsonObject): 239 | name = StringProperty(required=True) 240 | uid = StringProperty(required=True) 241 | recommended = ListProperty(StringProperty, exclude_if_none=True, default=None) 242 | authors = ListProperty(StringProperty, exclude_if_none=True, default=None) 243 | description = StringProperty(exclude_if_none=True, default=None) 244 | projectUrl = StringProperty(exclude_if_none=True, default=None) 245 | 246 | def write(self): 247 | try: 248 | with open("multimc/%s/package.json" % self.uid, 'w') as file: 249 | json.dump(self.to_json(), file, sort_keys=True, indent=4) 250 | except EnvironmentError as e: 251 | print("Error while trying to save shared packaged data for %s:" % self.uid, e) 252 | 253 | def writeSharedPackageData(uid, name): 254 | desc = MultiMCSharedPackageData({ 255 | 'name': name, 256 | 'uid': uid 257 | }) 258 | with open("multimc/%s/package.json" % uid, 'w') as file: 259 | json.dump(desc.to_json(), file, sort_keys=True, indent=4) 260 | 261 | def readSharedPackageData(uid): 262 | with open("multimc/%s/package.json" % uid, 'r') as file: 263 | return MultiMCSharedPackageData(json.load(file)) 264 | 265 | class MultiMCVersionIndexEntry(JsonObject): 266 | version = StringProperty() 267 | type = StringProperty(exclude_if_none=True, default=None) 268 | releaseTime = ISOTimestampProperty() 269 | requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None) 270 | conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None) 271 | recommended = BooleanProperty(exclude_if_none=True, default=None) 272 | volatile = BooleanProperty(exclude_if_none=True, default=None) 273 | sha256 = StringProperty() 274 | 275 | class MultiMCVersionIndex(VersionedJsonObject): 276 | name = StringProperty() 277 | uid = StringProperty() 278 | versions = ListProperty(MultiMCVersionIndexEntry) 279 | 280 | class MultiMCPackageIndexEntry(JsonObject): 281 | name = StringProperty() 282 | uid = StringProperty() 283 | sha256 = StringProperty() 284 | 285 | class MultiMCPackageIndex(VersionedJsonObject): 286 | packages = ListProperty(MultiMCPackageIndexEntry) 287 | 288 | ''' 289 | The MultiMC static override file for legacy looks like this: 290 | { 291 | "versions": [ 292 | ... 293 | { 294 | "id": "c0.0.13a", 295 | "checksum": "3617fbf5fbfd2b837ebf5ceb63584908", 296 | "releaseTime": "2009-05-31T00:00:00+02:00", 297 | "type": "old_alpha", 298 | "mainClass": "com.mojang.minecraft.Minecraft", 299 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 300 | "+traits": ["legacyLaunch", "no-texturepacks"] 301 | }, 302 | ... 303 | ] 304 | } 305 | ''' 306 | 307 | class LegacyOverrideEntry(JsonObject): 308 | releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) 309 | mainClass = StringProperty(exclude_if_none=True, default=None) 310 | appletClass = StringProperty(exclude_if_none=True, default=None) 311 | addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None) 312 | 313 | class LegacyOverrideIndex(JsonObject): 314 | versions = DictProperty(LegacyOverrideEntry) 315 | 316 | def ApplyLegacyOverride (mmcFile, legacyOverride): 317 | # simply hard override classes 318 | mmcFile.mainClass = legacyOverride.mainClass 319 | mmcFile.appletClass = legacyOverride.appletClass 320 | # if we have an updated release time (more correct than Mojang), use it 321 | if legacyOverride.releaseTime != None: 322 | mmcFile.releaseTime = legacyOverride.releaseTime 323 | # add traits, if any 324 | if legacyOverride.addTraits: 325 | if not mmcFile.addTraits: 326 | mmcFile.addTraits = [] 327 | mmcFile.addTraits = mmcFile.addTraits + legacyOverride.addTraits 328 | # remove all libraries - they are not needed for legacy 329 | mmcFile.libraries = None 330 | # remove minecraft arguments - we use our own hardcoded ones 331 | mmcFile.minecraftArguments = None 332 | -------------------------------------------------------------------------------- /jsonobject/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from collections import namedtuple, OrderedDict 3 | import copy 4 | import six 5 | import inspect 6 | from .exceptions import ( 7 | DeleteNotAllowed, 8 | WrappingAttributeError, 9 | ) 10 | from .base_properties import JsonProperty, DefaultProperty 11 | from .utils import check_type 12 | 13 | 14 | JsonObjectClassSettings = namedtuple('JsonObjectClassSettings', ['type_config']) 15 | 16 | CLASS_SETTINGS_ATTR = '_$_class_settings' 17 | 18 | 19 | def get_settings(cls): 20 | return getattr(cls, CLASS_SETTINGS_ATTR, 21 | JsonObjectClassSettings(type_config=TypeConfig())) 22 | 23 | 24 | def set_settings(cls, settings): 25 | setattr(cls, CLASS_SETTINGS_ATTR, settings) 26 | 27 | 28 | class TypeConfig(object): 29 | """ 30 | This class allows the user to configure dynamic 31 | type handlers and string conversions for their JsonObject. 32 | 33 | properties is a map from python types to JsonProperty subclasses 34 | string_conversions is a list or tuple of (regex, python type)-tuples 35 | 36 | This class is used to store the configuration but is not part of the API. 37 | To configure: 38 | 39 | class Foo(JsonObject): 40 | # property definitions go here 41 | # ... 42 | 43 | class Meta(object): 44 | update_properties = { 45 | datetime.datetime: MySpecialDateTimeProperty 46 | } 47 | # this is already set by default 48 | # but you can override with your own modifications 49 | string_conversions = ((date_re, datetime.date), 50 | (datetime_re, datetime.datetime), 51 | (time_re, datetime.time), 52 | (decimal_re, decimal.Decimal)) 53 | 54 | If you now do 55 | 56 | foo = Foo() 57 | foo.timestamp = datetime.datetime(1988, 7, 7, 11, 8, 0) 58 | 59 | timestamp will be governed by a MySpecialDateTimeProperty 60 | instead of the default. 61 | 62 | """ 63 | def __init__(self, properties=None, string_conversions=None): 64 | self._properties = properties if properties is not None else {} 65 | 66 | self._string_conversions = ( 67 | OrderedDict(string_conversions) if string_conversions is not None 68 | else OrderedDict() 69 | ) 70 | # cache this 71 | self.string_conversions = self._get_string_conversions() 72 | self.properties = self._properties 73 | 74 | def replace(self, properties=None, string_conversions=None): 75 | return TypeConfig( 76 | properties=(properties if properties is not None 77 | else self._properties), 78 | string_conversions=(string_conversions if string_conversions is not None 79 | else self._string_conversions) 80 | ) 81 | 82 | def updated(self, properties=None, string_conversions=None): 83 | """ 84 | update properties and string_conversions with the paramenters 85 | keeping all non-mentioned items the same as before 86 | returns a new TypeConfig with these changes 87 | (does not modify original) 88 | 89 | """ 90 | _properties = self._properties.copy() 91 | _string_conversions = self.string_conversions[:] 92 | if properties: 93 | _properties.update(properties) 94 | if string_conversions: 95 | _string_conversions.extend(string_conversions) 96 | return TypeConfig( 97 | properties=_properties, 98 | string_conversions=_string_conversions, 99 | ) 100 | 101 | def _get_string_conversions(self): 102 | result = [] 103 | for pattern, conversion in self._string_conversions.items(): 104 | conversion = ( 105 | conversion if conversion not in self._properties 106 | else self._properties[conversion](type_config=self).to_python 107 | ) 108 | result.append((pattern, conversion)) 109 | return result 110 | 111 | META_ATTRS = ('properties', 'string_conversions', 'update_properties') 112 | 113 | 114 | class JsonObjectMeta(type): 115 | 116 | class Meta(object): 117 | pass 118 | 119 | def __new__(mcs, name, bases, dct): 120 | cls = super(JsonObjectMeta, mcs).__new__(mcs, name, bases, dct) 121 | 122 | cls.__configure(**{key: value 123 | for key, value in cls.Meta.__dict__.items() 124 | if key in META_ATTRS}) 125 | cls_settings = get_settings(cls) 126 | 127 | properties = {} 128 | properties_by_name = {} 129 | for key, value in dct.items(): 130 | if isinstance(value, JsonProperty): 131 | properties[key] = value 132 | elif key.startswith('_'): 133 | continue 134 | elif type(value) in cls_settings.type_config.properties: 135 | property_ = cls_settings.type_config.properties[type(value)](default=value) 136 | properties[key] = dct[key] = property_ 137 | setattr(cls, key, property_) 138 | 139 | for key, property_ in properties.items(): 140 | property_.init_property(default_name=key, 141 | type_config=cls_settings.type_config) 142 | assert property_.name is not None, property_ 143 | assert property_.name not in properties_by_name, \ 144 | 'You can only have one property named {0}'.format( 145 | property_.name) 146 | properties_by_name[property_.name] = property_ 147 | 148 | for base in bases: 149 | if getattr(base, '_properties_by_attr', None): 150 | for key, value in base._properties_by_attr.items(): 151 | if key not in properties: 152 | properties[key] = value 153 | properties_by_name[value.name] = value 154 | 155 | cls._properties_by_attr = properties 156 | cls._properties_by_key = properties_by_name 157 | return cls 158 | 159 | def __configure(cls, properties=None, string_conversions=None, 160 | update_properties=None): 161 | super_settings = get_settings(super(cls, cls)) 162 | assert not properties or not update_properties, \ 163 | "{} {}".format(properties, update_properties) 164 | type_config = super_settings.type_config 165 | if update_properties is not None: 166 | type_config = type_config.updated(properties=update_properties) 167 | elif properties is not None: 168 | type_config = type_config.replace(properties=properties) 169 | if string_conversions is not None: 170 | type_config = type_config.replace( 171 | string_conversions=string_conversions) 172 | set_settings(cls, super_settings._replace(type_config=type_config)) 173 | return cls 174 | 175 | 176 | class _JsonObjectPrivateInstanceVariables(object): 177 | 178 | def __init__(self, dynamic_properties=None): 179 | self.dynamic_properties = dynamic_properties or {} 180 | 181 | 182 | @six.add_metaclass(JsonObjectMeta) 183 | class JsonObjectBase(object): 184 | 185 | _allow_dynamic_properties = False 186 | _validate_required_lazily = False 187 | 188 | _properties_by_attr = None 189 | _properties_by_key = None 190 | 191 | _string_conversions = () 192 | 193 | def __init__(self, _obj=None, **kwargs): 194 | setattr(self, '_$', _JsonObjectPrivateInstanceVariables()) 195 | 196 | self._obj = check_type(_obj, dict, 197 | 'JsonObject must wrap a dict or None') 198 | self._wrapped = {} 199 | 200 | for key, value in self._obj.items(): 201 | try: 202 | self.set_raw_value(key, value) 203 | except AttributeError: 204 | raise WrappingAttributeError( 205 | "can't set attribute corresponding to {key!r} " 206 | "on a {cls} while wrapping {data!r}".format( 207 | cls=self.__class__, 208 | key=key, 209 | data=_obj, 210 | ) 211 | ) 212 | 213 | for attr, value in kwargs.items(): 214 | try: 215 | setattr(self, attr, value) 216 | except AttributeError: 217 | raise WrappingAttributeError( 218 | "can't set attribute {key!r} " 219 | "on a {cls} while wrapping {data!r}".format( 220 | cls=self.__class__, 221 | key=attr, 222 | data=_obj, 223 | ) 224 | ) 225 | 226 | for key, value in self._properties_by_key.items(): 227 | if key not in self._obj: 228 | try: 229 | d = value.default() 230 | except TypeError: 231 | d = value.default(self) 232 | self[key] = d 233 | 234 | def set_raw_value(self, key, value): 235 | wrapped = self.__wrap(key, value) 236 | if key in self._properties_by_key: 237 | self[key] = wrapped 238 | else: 239 | setattr(self, key, wrapped) 240 | 241 | @classmethod 242 | def properties(cls): 243 | return cls._properties_by_attr.copy() 244 | 245 | @property 246 | def __dynamic_properties(self): 247 | return getattr(self, '_$').dynamic_properties 248 | 249 | @classmethod 250 | def wrap(cls, obj): 251 | self = cls(obj) 252 | return self 253 | 254 | def validate(self, required=True): 255 | for key, value in self._wrapped.items(): 256 | self.__get_property(key).validate(value, required=required) 257 | 258 | def to_json(self): 259 | self.validate() 260 | return copy.deepcopy(self._obj) 261 | 262 | def __get_property(self, key): 263 | try: 264 | return self._properties_by_key[key] 265 | except KeyError: 266 | return DefaultProperty(type_config=get_settings(self).type_config) 267 | 268 | def __wrap(self, key, value): 269 | property_ = self.__get_property(key) 270 | 271 | if value is None: 272 | return None 273 | 274 | return property_.wrap(value) 275 | 276 | def __unwrap(self, key, value): 277 | property_ = self.__get_property(key) 278 | try: 279 | property_.validate( 280 | value, 281 | required=not self._validate_required_lazily, 282 | recursive=False, 283 | ) 284 | except TypeError: 285 | property_.validate( 286 | value, 287 | required=not self._validate_required_lazily, 288 | ) 289 | if value is None: 290 | return None, None 291 | 292 | return property_.unwrap(value) 293 | 294 | def __setitem__(self, key, value): 295 | wrapped, unwrapped = self.__unwrap(key, value) 296 | self._wrapped[key] = wrapped 297 | if self.__get_property(key).exclude(unwrapped): 298 | self._obj.pop(key, None) 299 | else: 300 | self._obj[key] = unwrapped 301 | if key not in self._properties_by_key: 302 | assert key not in self._properties_by_attr 303 | self.__dynamic_properties[key] = wrapped 304 | super(JsonObjectBase, self).__setattr__(key, wrapped) 305 | 306 | def __is_dynamic_property(self, name): 307 | return ( 308 | name not in self._properties_by_attr and 309 | not name.startswith('_') and 310 | not inspect.isdatadescriptor(getattr(self.__class__, name, None)) 311 | ) 312 | 313 | def __setattr__(self, name, value): 314 | if self.__is_dynamic_property(name): 315 | if self._allow_dynamic_properties: 316 | self[name] = value 317 | else: 318 | raise AttributeError( 319 | "{0!r} is not defined in schema " 320 | "(not a valid property)".format(name) 321 | ) 322 | else: 323 | super(JsonObjectBase, self).__setattr__(name, value) 324 | 325 | def __delitem__(self, key): 326 | if key in self._properties_by_key: 327 | raise DeleteNotAllowed(key) 328 | else: 329 | if not self.__is_dynamic_property(key): 330 | raise KeyError(key) 331 | del self._obj[key] 332 | del self._wrapped[key] 333 | del self.__dynamic_properties[key] 334 | super(JsonObjectBase, self).__delattr__(key) 335 | 336 | def __delattr__(self, name): 337 | if name in self._properties_by_attr: 338 | raise DeleteNotAllowed(name) 339 | elif self.__is_dynamic_property(name): 340 | del self[name] 341 | else: 342 | super(JsonObjectBase, self).__delattr__(name) 343 | 344 | def __repr__(self): 345 | name = self.__class__.__name__ 346 | predefined_properties = self._properties_by_attr.keys() 347 | predefined_property_keys = set(self._properties_by_attr[p].name 348 | for p in predefined_properties) 349 | dynamic_properties = (set(self._wrapped.keys()) 350 | - predefined_property_keys) 351 | properties = sorted(predefined_properties) + sorted(dynamic_properties) 352 | return u'{name}({keyword_args})'.format( 353 | name=name, 354 | keyword_args=', '.join('{key}={value!r}'.format( 355 | key=key, 356 | value=getattr(self, key) 357 | ) for key in properties), 358 | ) 359 | 360 | 361 | class _LimitedDictInterfaceMixin(object): 362 | """ 363 | mindlessly farms selected dict methods out to an internal dict 364 | 365 | really only a separate class from JsonObject 366 | to keep this mindlessness separate from the methods 367 | that need to be more carefully understood 368 | 369 | """ 370 | _wrapped = None 371 | 372 | def keys(self): 373 | return self._wrapped.keys() 374 | 375 | def items(self): 376 | return self._wrapped.items() 377 | 378 | def iteritems(self): 379 | return self._wrapped.iteritems() 380 | 381 | def __contains__(self, item): 382 | return item in self._wrapped 383 | 384 | def __getitem__(self, item): 385 | return self._wrapped[item] 386 | 387 | def __iter__(self): 388 | return iter(self._wrapped) 389 | 390 | def __len__(self): 391 | return len(self._wrapped) 392 | 393 | 394 | def get_dynamic_properties(obj): 395 | return getattr(obj, '_$').dynamic_properties.copy() 396 | -------------------------------------------------------------------------------- /updateForge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Get the source files necessary for generating Forge versions 4 | ''' 5 | from __future__ import print_function 6 | import sys 7 | 8 | import requests 9 | from cachecontrol import CacheControl 10 | from cachecontrol.caches import FileCache 11 | 12 | import json 13 | import copy 14 | import re 15 | import zipfile 16 | from metautil import * 17 | from jsonobject import * 18 | from forgeutil import * 19 | import os.path 20 | import datetime 21 | import hashlib 22 | from pathlib import Path 23 | from contextlib import suppress 24 | 25 | def eprint(*args, **kwargs): 26 | print(*args, file=sys.stderr, **kwargs) 27 | 28 | def filehash(filename, hashtype, blocksize=65536): 29 | hash = hashtype() 30 | with open(filename, "rb") as f: 31 | for block in iter(lambda: f.read(blocksize), b""): 32 | hash.update(block) 33 | return hash.hexdigest() 34 | 35 | forever_cache = FileCache('http_cache', forever=True) 36 | sess = CacheControl(requests.Session(), forever_cache) 37 | 38 | # get the remote version list fragments 39 | r = sess.get('https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json') 40 | r.raise_for_status() 41 | main_json = r.json() 42 | assert type(main_json) == dict 43 | 44 | r = sess.get('https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json') 45 | r.raise_for_status() 46 | promotions_json = r.json() 47 | assert type(promotions_json) == dict 48 | 49 | promotedKeyExpression = re.compile("(?P[^-]+)-(?P(latest)|(recommended))(-(?P[a-zA-Z0-9\\.]+))?") 50 | 51 | recommendedSet = set() 52 | 53 | newIndex = DerivedForgeIndex() 54 | 55 | # FIXME: does not fully validate that the file has not changed format 56 | # NOTE: For some insane reason, the format of the versions here is special. It having a branch at the end means it affects that particular branch 57 | # We don't care about Forge having branches. 58 | # Therefore we only use the short version part for later identification and filter out the branch-specific promotions (among other errors). 59 | print("Processing promotions:") 60 | for promoKey, shortversion in promotions_json.get('promos').items(): 61 | match = promotedKeyExpression.match(promoKey) 62 | if not match: 63 | print('Skipping promotion %s, the key did not parse:' % promoKey) 64 | pprint(promoKey) 65 | assert match 66 | if not match.group('mc'): 67 | print('Skipping promotion %s, because it has no Minecraft version.' % promoKey) 68 | continue 69 | if match.group('branch'): 70 | print('Skipping promotion %s, because it on a branch only.' % promoKey) 71 | continue 72 | elif match.group('promotion') == 'recommended': 73 | recommendedSet.add(shortversion) 74 | print ('%s added to recommended set' % shortversion) 75 | elif match.group('promotion') == 'latest': 76 | pass 77 | else: 78 | assert False 79 | 80 | versionExpression = re.compile("^(?P[0-9a-zA-Z_\\.]+)-(?P[0-9\\.]+\\.(?P[0-9]+))(-(?P[a-zA-Z0-9\\.]+))?$") 81 | 82 | def getSingleForgeFilesManifest(longversion): 83 | pathThing = "upstream/forge/files_manifests/%s.json" % longversion 84 | files_manifest_file = Path(pathThing) 85 | from_file = False 86 | if files_manifest_file.is_file(): 87 | with open(pathThing, 'r') as f: 88 | files_json=json.load(f) 89 | from_file = True 90 | else: 91 | fileUrl = 'https://files.minecraftforge.net/net/minecraftforge/forge/%s/meta.json' % longversion 92 | r = sess.get(fileUrl) 93 | r.raise_for_status() 94 | files_json = r.json() 95 | 96 | retDict = dict() 97 | 98 | for classifier, extensionObj in files_json.get('classifiers').items(): 99 | assert type(classifier) == str 100 | assert type(extensionObj) == dict 101 | 102 | # assert len(extensionObj.items()) == 1 103 | index = 0 104 | count = 0 105 | while index < len(extensionObj.items()): 106 | mutableCopy = copy.deepcopy(extensionObj) 107 | extension, hash = mutableCopy.popitem() 108 | if not type(classifier) == str: 109 | pprint(classifier) 110 | pprint(extensionObj) 111 | if not type(hash) == str: 112 | pprint(classifier) 113 | pprint(extensionObj) 114 | print('%s: Skipping missing hash for extension %s:' % (longversion, extension)) 115 | index = index + 1 116 | continue 117 | assert type(classifier) == str 118 | processedHash = re.sub(r"\W", "", hash) 119 | if not len(processedHash) == 32: 120 | print('%s: Skipping invalid hash for extension %s:' % (longversion, extension)) 121 | pprint(extensionObj) 122 | index = index + 1 123 | continue 124 | 125 | fileObj = ForgeFile( 126 | classifier=classifier, 127 | hash=processedHash, 128 | extension=extension 129 | ) 130 | if count == 0: 131 | retDict[classifier] = fileObj 132 | index = index + 1 133 | count = count + 1 134 | else: 135 | print('%s: Multiple objects detected for classifier %s:' % (longversion, classifier)) 136 | pprint(extensionObj) 137 | assert False 138 | 139 | if not from_file: 140 | with open(pathThing, 'w', encoding='utf-8') as f: 141 | json.dump(files_json, f, sort_keys=True, indent=4) 142 | 143 | return retDict 144 | 145 | print("") 146 | print("Making dirs...") 147 | os.makedirs("upstream/forge/jars/", exist_ok=True) 148 | os.makedirs("upstream/forge/installer_info/", exist_ok=True) 149 | os.makedirs("upstream/forge/installer_manifests/", exist_ok=True) 150 | os.makedirs("upstream/forge/version_manifests/", exist_ok=True) 151 | os.makedirs("upstream/forge/files_manifests/", exist_ok=True) 152 | 153 | print("") 154 | print("Processing versions:") 155 | for mcversion, value in main_json.items(): 156 | assert type(mcversion) == str 157 | assert type(value) == list 158 | for longversion in value: 159 | assert type(longversion) == str 160 | match = versionExpression.match(longversion) 161 | if not match: 162 | pprint(longversion) 163 | assert match 164 | assert match.group('mc') == mcversion 165 | 166 | files = getSingleForgeFilesManifest(longversion) 167 | 168 | build = int(match.group('build')) 169 | version = match.group('ver') 170 | branch = match.group('branch') 171 | 172 | isRecommended = (version in recommendedSet) 173 | 174 | entry = ForgeEntry( 175 | longversion=longversion, 176 | mcversion=mcversion, 177 | version=version, 178 | build=build, 179 | branch=branch, 180 | # NOTE: we add this later after the fact. The forge promotions file lies about these. 181 | latest=False, 182 | recommended=isRecommended, 183 | files=files 184 | ) 185 | newIndex.versions[longversion] = entry 186 | if not newIndex.by_mcversion: 187 | newIndex.by_mcversion = dict() 188 | if not mcversion in newIndex.by_mcversion: 189 | newIndex.by_mcversion.setdefault(mcversion, ForgeMcVersionInfo()) 190 | newIndex.by_mcversion[mcversion].versions.append(longversion) 191 | # NOTE: we add this later after the fact. The forge promotions file lies about these. 192 | #if entry.latest: 193 | #newIndex.by_mcversion[mcversion].latest = longversion 194 | if entry.recommended: 195 | newIndex.by_mcversion[mcversion].recommended = longversion 196 | 197 | print("") 198 | print("Post processing promotions and adding missing 'latest':") 199 | for mcversion, info in newIndex.by_mcversion.items(): 200 | latestVersion = info.versions[-1] 201 | info.latest = latestVersion 202 | newIndex.versions[latestVersion].latest = True 203 | print("Added %s as latest for %s" % (latestVersion, mcversion)) 204 | 205 | print("") 206 | print("Dumping index files...") 207 | 208 | with open("upstream/forge/maven-metadata.json", 'w', encoding='utf-8') as f: 209 | json.dump(main_json, f, sort_keys=True, indent=4) 210 | 211 | with open("upstream/forge/promotions_slim.json", 'w', encoding='utf-8') as f: 212 | json.dump(promotions_json, f, sort_keys=True, indent=4) 213 | 214 | with open("upstream/forge/derived_index.json", 'w', encoding='utf-8') as f: 215 | json.dump(newIndex.to_json(), f, sort_keys=True, indent=4) 216 | 217 | versions = [] 218 | legacyinfolist = ForgeLegacyInfoList() 219 | tsPath = "static/forge-legacyinfo.json" 220 | 221 | fuckedVersions = [] 222 | 223 | print("Grabbing installers and dumping installer profiles...") 224 | # get the installer jars - if needed - and get the installer profiles out of them 225 | for id, entry in newIndex.versions.items(): 226 | eprint ("Updating Forge %s" % id) 227 | if entry.mcversion == None: 228 | eprint ("Skipping %d with invalid MC version" % entry.build) 229 | continue 230 | 231 | version = ForgeVersion(entry) 232 | if version.url() == None: 233 | eprint ("Skipping %d with no valid files" % version.build) 234 | continue 235 | 236 | jarFilepath = "upstream/forge/jars/%s" % version.filename() 237 | 238 | if version.usesInstaller(): 239 | installerInfoFilepath = "upstream/forge/installer_info/%s.json" % version.longVersion 240 | profileFilepath = "upstream/forge/installer_manifests/%s.json" % version.longVersion 241 | versionJsonFilepath = "upstream/forge/version_manifests/%s.json" % version.longVersion 242 | installerRefreshRequired = False 243 | if not os.path.isfile(profileFilepath): 244 | installerRefreshRequired = True 245 | if not os.path.isfile(installerInfoFilepath): 246 | installerRefreshRequired = True 247 | 248 | if installerRefreshRequired: 249 | # grab the installer if it's not there 250 | if not os.path.isfile(jarFilepath): 251 | eprint ("Downloading %s" % version.url()) 252 | rfile = sess.get(version.url(), stream=True) 253 | rfile.raise_for_status() 254 | with open(jarFilepath, 'wb') as f: 255 | for chunk in rfile.iter_content(chunk_size=128): 256 | f.write(chunk) 257 | 258 | eprint ("Processing %s" % version.url()) 259 | # harvestables from the installer 260 | if not os.path.isfile(profileFilepath): 261 | print(jarFilepath) 262 | with zipfile.ZipFile(jarFilepath, 'r') as jar: 263 | with suppress(KeyError): 264 | with jar.open('version.json', 'r') as profileZipEntry: 265 | versionJsonData = profileZipEntry.read(); 266 | versionJsonJson = json.loads(versionJsonData) 267 | profileZipEntry.close() 268 | 269 | # Process: does it parse? 270 | doesItParse = MojangVersionFile(versionJsonJson) 271 | 272 | with open(versionJsonFilepath, 'wb') as versionJsonFile: 273 | versionJsonFile.write(versionJsonData) 274 | versionJsonFile.close() 275 | 276 | with jar.open('install_profile.json', 'r') as profileZipEntry: 277 | installProfileJsonData = profileZipEntry.read() 278 | profileZipEntry.close() 279 | 280 | # Process: does it parse? 281 | installProfileJsonJson = json.loads(installProfileJsonData) 282 | atLeastOneFormatWorked = False 283 | exception = None 284 | try: 285 | doesItParseV1 = ForgeInstallerProfile(installProfileJsonJson) 286 | atLeastOneFormatWorked = True 287 | except BaseException as err: 288 | exception = err 289 | try: 290 | doesItParseV2 = ForgeInstallerProfileV2(installProfileJsonJson) 291 | atLeastOneFormatWorked = True 292 | except BaseException as err: 293 | exception = err 294 | 295 | # NOTE: Only here for 1.12.2-14.23.5.2851 296 | try: 297 | doesItParseV1_5 = ForgeInstallerProfileV1_5(installProfileJsonJson) 298 | atLeastOneFormatWorked = True 299 | except BaseException as err: 300 | exception = err 301 | 302 | if not atLeastOneFormatWorked: 303 | if version.isSupported(): 304 | raise exception 305 | else: 306 | eprint ("Version %s is not supported and won't be generated later." % version.longVersion) 307 | 308 | with open(profileFilepath, 'wb') as profileFile: 309 | profileFile.write(installProfileJsonData) 310 | profileFile.close() 311 | 312 | # installer info v1 313 | if not os.path.isfile(installerInfoFilepath): 314 | installerInfo = InstallerInfo() 315 | eprint ("SHA1 %s" % jarFilepath) 316 | installerInfo.sha1hash = filehash(jarFilepath, hashlib.sha1) 317 | eprint ("SHA256 %s" % jarFilepath) 318 | installerInfo.sha256hash = filehash(jarFilepath, hashlib.sha256) 319 | eprint ("SIZE %s" % jarFilepath) 320 | installerInfo.size = os.path.getsize(jarFilepath) 321 | eprint ("DUMP %s" % jarFilepath) 322 | with open(installerInfoFilepath, 'w', encoding='utf-8') as installerInfoFile: 323 | json.dump(installerInfo.to_json(), installerInfoFile, sort_keys=True, indent=4) 324 | installerInfoFile.close() 325 | else: 326 | pass 327 | # ignore the two versions without install manifests and jar mod class files 328 | # TODO: fix those versions? 329 | if version.mcversion_sane == "1.6.1": 330 | continue 331 | 332 | # only gather legacy info if it's missing 333 | if not os.path.isfile(tsPath): 334 | # grab the jar/zip if it's not there 335 | if not os.path.isfile(jarFilepath): 336 | rfile = sess.get(version.url(), stream=True) 337 | rfile.raise_for_status() 338 | with open(jarFilepath, 'wb') as f: 339 | for chunk in rfile.iter_content(chunk_size=128): 340 | f.write(chunk) 341 | # find the latest timestamp in the zip file 342 | tstamp = datetime.datetime.fromtimestamp(0) 343 | with zipfile.ZipFile(jarFilepath, 'r') as jar: 344 | allinfo = jar.infolist() 345 | for info in allinfo: 346 | tstampNew = datetime.datetime(*info.date_time) 347 | if tstampNew > tstamp: 348 | tstamp = tstampNew 349 | legacyInfo = ForgeLegacyInfo() 350 | legacyInfo.releaseTime = tstamp 351 | legacyInfo.sha1 = filehash(jarFilepath, hashlib.sha1) 352 | legacyInfo.sha256 = filehash(jarFilepath, hashlib.sha256) 353 | legacyInfo.size = os.path.getsize(jarFilepath) 354 | legacyinfolist.number[id] = legacyInfo 355 | 356 | # only write legacy info if it's missing 357 | if not os.path.isfile(tsPath): 358 | with open(tsPath, 'w') as outfile: 359 | json.dump(legacyinfolist.to_json(), outfile, sort_keys=True, indent=4) 360 | -------------------------------------------------------------------------------- /static/lwjgl-3.2.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "libraries": [ 4 | { 5 | "downloads": { 6 | "artifact": { 7 | "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4", 8 | "size": 208338, 9 | "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar" 10 | } 11 | }, 12 | "name": "net.java.jinput:jinput:2.0.5" 13 | }, 14 | { 15 | "downloads": { 16 | "artifact": { 17 | "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6", 18 | "size": 7508, 19 | "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar" 20 | } 21 | }, 22 | "name": "net.java.jutils:jutils:1.0.0" 23 | }, 24 | { 25 | "downloads": { 26 | "artifact": { 27 | "sha1": "d3ad4df38e400b8afba1de63f84338809399df5b", 28 | "size": 108907, 29 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2.jar" 30 | } 31 | }, 32 | "name": "org.lwjgl:lwjgl-glfw:3.2.2" 33 | }, 34 | { 35 | "downloads": { 36 | "artifact": { 37 | "sha1": "d3ad4df38e400b8afba1de63f84338809399df5b", 38 | "size": 108907, 39 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2.jar" 40 | }, 41 | "classifiers": { 42 | "natives-linux": { 43 | "sha1": "0957733f26a6661d4883da0335f7ef46d3bbbd7d", 44 | "size": 159198, 45 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2-natives-linux.jar" 46 | }, 47 | "natives-macos": { 48 | "sha1": "98f745038d17ac3192fcd01dc44126b03ec1570d", 49 | "size": 67311, 50 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2-natives-macos.jar" 51 | }, 52 | "natives-windows": { 53 | "sha1": "dc6826d636bf796b33a49038c354210e661bfc17", 54 | "size": 266648, 55 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2-natives-windows.jar" 56 | } 57 | } 58 | }, 59 | "name": "org.lwjgl:lwjgl-glfw:3.2.2", 60 | "natives": { 61 | "linux": "natives-linux", 62 | "osx": "natives-macos", 63 | "windows": "natives-windows" 64 | } 65 | }, 66 | { 67 | "downloads": { 68 | "artifact": { 69 | "sha1": "ee8e57a79300f78294576d87c4a587f8c99402e2", 70 | "size": 34848, 71 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-jemalloc/3.2.2/lwjgl-jemalloc-3.2.2.jar" 72 | } 73 | }, 74 | "name": "org.lwjgl:lwjgl-jemalloc:3.2.2" 75 | }, 76 | { 77 | "downloads": { 78 | "artifact": { 79 | "sha1": "ee8e57a79300f78294576d87c4a587f8c99402e2", 80 | "size": 34848, 81 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-jemalloc/3.2.2/lwjgl-jemalloc-3.2.2.jar" 82 | }, 83 | "classifiers": { 84 | "natives-linux": { 85 | "sha1": "268c08a150347e04e44ba56e359d62c9b78669df", 86 | "size": 156173, 87 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-jemalloc/3.2.2/lwjgl-jemalloc-3.2.2-natives-linux.jar" 88 | }, 89 | "natives-macos": { 90 | "sha1": "805f5a10465375ba034b27b72331912fd2846690", 91 | "size": 117127, 92 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-jemalloc/3.2.2/lwjgl-jemalloc-3.2.2-natives-macos.jar" 93 | }, 94 | "natives-windows": { 95 | "sha1": "338b25b99da3ba5f441f6492f2ce2a9c608860ed", 96 | "size": 220623, 97 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-jemalloc/3.2.2/lwjgl-jemalloc-3.2.2-natives-windows.jar" 98 | } 99 | } 100 | }, 101 | "name": "org.lwjgl:lwjgl-jemalloc:3.2.2", 102 | "natives": { 103 | "linux": "natives-linux", 104 | "osx": "natives-macos", 105 | "windows": "natives-windows" 106 | } 107 | }, 108 | { 109 | "downloads": { 110 | "artifact": { 111 | "sha1": "2b772a102b0a11ee5f2109a5b136f4dc7c630827", 112 | "size": 80012, 113 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-openal/3.2.2/lwjgl-openal-3.2.2.jar" 114 | } 115 | }, 116 | "name": "org.lwjgl:lwjgl-openal:3.2.2" 117 | }, 118 | { 119 | "downloads": { 120 | "artifact": { 121 | "sha1": "2b772a102b0a11ee5f2109a5b136f4dc7c630827", 122 | "size": 80012, 123 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-openal/3.2.2/lwjgl-openal-3.2.2.jar" 124 | }, 125 | "classifiers": { 126 | "natives-linux": { 127 | "sha1": "0364f9f5c3947393083ab5f37a571f5603aadd0b", 128 | "size": 590997, 129 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-openal/3.2.2/lwjgl-openal-3.2.2-natives-linux.jar" 130 | }, 131 | "natives-macos": { 132 | "sha1": "a97b6345d5a9ddf889e262bd7ad8eed43b1bb063", 133 | "size": 528006, 134 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-openal/3.2.2/lwjgl-openal-3.2.2-natives-macos.jar" 135 | }, 136 | "natives-windows": { 137 | "sha1": "ec20a7d42a2438528fca87e60b1705f1e2339ddb", 138 | "size": 1310102, 139 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-openal/3.2.2/lwjgl-openal-3.2.2-natives-windows.jar" 140 | } 141 | } 142 | }, 143 | "name": "org.lwjgl:lwjgl-openal:3.2.2", 144 | "natives": { 145 | "linux": "natives-linux", 146 | "osx": "natives-macos", 147 | "windows": "natives-windows" 148 | } 149 | }, 150 | { 151 | "downloads": { 152 | "artifact": { 153 | "sha1": "6ac5bb88b44c43ea195a570aab059f63da004cd8", 154 | "size": 929780, 155 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-opengl/3.2.2/lwjgl-opengl-3.2.2.jar" 156 | } 157 | }, 158 | "name": "org.lwjgl:lwjgl-opengl:3.2.2" 159 | }, 160 | { 161 | "downloads": { 162 | "artifact": { 163 | "sha1": "6ac5bb88b44c43ea195a570aab059f63da004cd8", 164 | "size": 929780, 165 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-opengl/3.2.2/lwjgl-opengl-3.2.2.jar" 166 | }, 167 | "classifiers": { 168 | "natives-linux": { 169 | "sha1": "338d33387919cb3f4cdba143c2b738a71ccfda60", 170 | "size": 77392, 171 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-opengl/3.2.2/lwjgl-opengl-3.2.2-natives-linux.jar" 172 | }, 173 | "natives-macos": { 174 | "sha1": "cf4f43e69ee70d8ebfbb6ba93dec9016339e4fdc", 175 | "size": 38989, 176 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-opengl/3.2.2/lwjgl-opengl-3.2.2-natives-macos.jar" 177 | }, 178 | "natives-windows": { 179 | "sha1": "d8dcdc91066cae2d2d8279cb4a9f9f05d9525826", 180 | "size": 170798, 181 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-opengl/3.2.2/lwjgl-opengl-3.2.2-natives-windows.jar" 182 | } 183 | } 184 | }, 185 | "name": "org.lwjgl:lwjgl-opengl:3.2.2", 186 | "natives": { 187 | "linux": "natives-linux", 188 | "osx": "natives-macos", 189 | "windows": "natives-windows" 190 | } 191 | }, 192 | { 193 | "downloads": { 194 | "artifact": { 195 | "sha1": "3b8e6ebc5851dd3d17e37e5cadce2eff2a429f0f", 196 | "size": 104469, 197 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-stb/3.2.2/lwjgl-stb-3.2.2.jar" 198 | } 199 | }, 200 | "name": "org.lwjgl:lwjgl-stb:3.2.2" 201 | }, 202 | { 203 | "downloads": { 204 | "artifact": { 205 | "sha1": "3b8e6ebc5851dd3d17e37e5cadce2eff2a429f0f", 206 | "size": 104469, 207 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-stb/3.2.2/lwjgl-stb-3.2.2.jar" 208 | }, 209 | "classifiers": { 210 | "natives-linux": { 211 | "sha1": "172c52e586fecf43f759bc4f70a778c01f6fdcc1", 212 | "size": 203476, 213 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-stb/3.2.2/lwjgl-stb-3.2.2-natives-linux.jar" 214 | }, 215 | "natives-macos": { 216 | "sha1": "ee059b129b09fdecbd8595273926ae930bf5a5d7", 217 | "size": 196796, 218 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-stb/3.2.2/lwjgl-stb-3.2.2-natives-macos.jar" 219 | }, 220 | "natives-windows": { 221 | "sha1": "811f705cbb29e8ae8d60bdf8fdd38c0c123ad3ef", 222 | "size": 465810, 223 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-stb/3.2.2/lwjgl-stb-3.2.2-natives-windows.jar" 224 | } 225 | } 226 | }, 227 | "name": "org.lwjgl:lwjgl-stb:3.2.2", 228 | "natives": { 229 | "linux": "natives-linux", 230 | "osx": "natives-macos", 231 | "windows": "natives-windows" 232 | } 233 | }, 234 | { 235 | "downloads": { 236 | "artifact": { 237 | "sha1": "fcbe606c8f8da6f8f9a05e2c540eb1ee8632b0e9", 238 | "size": 7092, 239 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2.jar" 240 | } 241 | }, 242 | "name": "org.lwjgl:lwjgl-tinyfd:3.2.2" 243 | }, 244 | { 245 | "downloads": { 246 | "artifact": { 247 | "sha1": "fcbe606c8f8da6f8f9a05e2c540eb1ee8632b0e9", 248 | "size": 7092, 249 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2.jar" 250 | }, 251 | "classifiers": { 252 | "javadoc": { 253 | "sha1": "ba657a222ee267b75fa81ae5ab29ae29b50f725f", 254 | "size": 368913, 255 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2-javadoc.jar" 256 | }, 257 | "natives-linux": { 258 | "sha1": "39e35b161c130635d9c8918ce04e887a30c5b687", 259 | "size": 38804, 260 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2-natives-linux.jar" 261 | }, 262 | "natives-macos": { 263 | "sha1": "46d0798228b8a28e857a2a0f02310fd6ba2a4eab", 264 | "size": 42136, 265 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2-natives-macos.jar" 266 | }, 267 | "natives-windows": { 268 | "sha1": "e9115958773644e863332a6a06488d26f9e1fc9f", 269 | "size": 208314, 270 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2-natives-windows.jar" 271 | }, 272 | "sources": { 273 | "sha1": "2fe76dcf2ca02ae0e64ac7c69eb251c09df0e922", 274 | "size": 5034, 275 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-tinyfd/3.2.2/lwjgl-tinyfd-3.2.2-sources.jar" 276 | } 277 | } 278 | }, 279 | "name": "org.lwjgl:lwjgl-tinyfd:3.2.2", 280 | "natives": { 281 | "linux": "natives-linux", 282 | "osx": "natives-macos", 283 | "windows": "natives-windows" 284 | } 285 | }, 286 | { 287 | "downloads": { 288 | "artifact": { 289 | "sha1": "8ad6294407e15780b43e84929c40e4c5e997972e", 290 | "size": 321900, 291 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2.jar" 292 | } 293 | }, 294 | "name": "org.lwjgl:lwjgl:3.2.2" 295 | }, 296 | { 297 | "downloads": { 298 | "artifact": { 299 | "sha1": "8ad6294407e15780b43e84929c40e4c5e997972e", 300 | "size": 321900, 301 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2.jar" 302 | }, 303 | "classifiers": { 304 | "natives-linux": { 305 | "sha1": "ae7976827ca2a3741f6b9a843a89bacd637af350", 306 | "size": 124776, 307 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2-natives-linux.jar" 308 | }, 309 | "natives-macos": { 310 | "sha1": "bbfb75693bdb714c0c69c2c9f9be73d259b43b62", 311 | "size": 48462, 312 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2-natives-macos.jar" 313 | }, 314 | "natives-windows": { 315 | "sha1": "05359f3aa50d36352815fc662ea73e1c00d22170", 316 | "size": 279593, 317 | "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2-natives-windows.jar" 318 | } 319 | } 320 | }, 321 | "name": "org.lwjgl:lwjgl:3.2.2", 322 | "natives": { 323 | "linux": "natives-linux", 324 | "osx": "natives-macos", 325 | "windows": "natives-windows" 326 | } 327 | } 328 | ], 329 | "name": "LWJGL", 330 | "releaseTime": "2019-06-24T12:52:52+00:00", 331 | "type": "release", 332 | "uid": "org.lwjgl", 333 | "version": "3.2.2" 334 | } 335 | -------------------------------------------------------------------------------- /generateForge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | import sys 4 | import os 5 | import re 6 | from metautil import * 7 | from forgeutil import * 8 | from jsonobject import * 9 | from distutils.version import LooseVersion 10 | 11 | def eprint(*args, **kwargs): 12 | print(*args, file=sys.stderr, **kwargs) 13 | 14 | # Contruct a set of libraries out of a Minecraft version file, for filtering. 15 | mcVersionCache = {} 16 | def loadMcVersionFilter(version): 17 | if version in mcVersionCache: 18 | return mcVersionCache[version] 19 | libSet = set() 20 | with open("multimc/net.minecraft/%s.json" % version, 'r', encoding='utf-8') as mcFile: 21 | mcVersion = MultiMCVersionFile(json.load(mcFile)) 22 | for lib in mcVersion.libraries: 23 | libSet.add(lib.name) 24 | mcVersionCache[version] = libSet 25 | return libSet 26 | 27 | ''' 28 | Match a library coordinate to a set of library coordinates. 29 | * Block those that pass completely. 30 | * For others, block those with lower versions than in the set. 31 | ''' 32 | def shouldIgnoreArtifact(libSet, match): 33 | for ver in libSet: 34 | if ver.group == match.group and ver.artifact == match.artifact and ver.classifier == match.classifier: 35 | if ver.version == match.version: 36 | # Everything is matched perfectly - this one will be ignored 37 | return True 38 | else: 39 | # We say the lib matches (is the same) also when the new version is lower than the old one 40 | if LooseVersion(ver.version) > LooseVersion(match.version): 41 | # eprint ("Lower version on %s:%s:%s: OLD=%s NEW=%s" % (ver.group, ver.artifact, ver.classifier, ver.version, match.version)) 42 | return True 43 | # Otherwise it did not match - new version is higher and this is an upgrade 44 | return False 45 | # No match found in the set - we need to keep this 46 | return False 47 | 48 | def versionFromProfile(profile, version): 49 | result = MultiMCVersionFile({"name":"Forge", "version":version.rawVersion, "uid":"net.minecraftforge" }) 50 | mcversion = profile.install.minecraft 51 | result.requires = [DependencyEntry(uid='net.minecraft', equals=mcversion)] 52 | result.mainClass = profile.versionInfo.mainClass 53 | args = profile.versionInfo.minecraftArguments 54 | tweakers = [] 55 | expression = re.compile("--tweakClass ([a-zA-Z0-9\\.]+)") 56 | match = expression.search(args) 57 | while match != None: 58 | tweakers.append(match.group(1)); 59 | args = args[:match.start()] + args[match.end():] 60 | match = expression.search(args); 61 | if len(tweakers) > 0: 62 | args = args.strip() 63 | result.addTweakers = tweakers; 64 | # result.minecraftArguments = args 65 | result.releaseTime = profile.versionInfo.time 66 | libs = [] 67 | mcFilter = loadMcVersionFilter(mcversion) 68 | for forgeLib in profile.versionInfo.libraries: 69 | if forgeLib.name.isLwjgl(): 70 | continue 71 | if forgeLib.name.isLog4j(): 72 | continue 73 | if shouldIgnoreArtifact(mcFilter, forgeLib.name): 74 | continue 75 | fixedName = forgeLib.name 76 | if fixedName.group == "net.minecraftforge": 77 | if fixedName.artifact == "minecraftforge": 78 | fixedName.artifact = "forge" 79 | fixedName.classifier = "universal" 80 | fixedName.version = "%s-%s" % (mcversion, fixedName.version) 81 | elif fixedName.artifact == "forge": 82 | fixedName.classifier = "universal" 83 | ourLib = MultiMCLibrary(name=fixedName) 84 | if forgeLib.url == "http://files.minecraftforge.net/maven/": 85 | ourLib.url = "https://maven.minecraftforge.net/" 86 | else: 87 | ourLib.url = forgeLib.url 88 | #if forgeLib.checksums and len(forgeLib.checksums) == 2: 89 | # ourLib.mmcHint = "forge-pack-xz" 90 | libs.append(ourLib) 91 | result.libraries = libs 92 | result.order = 5 93 | return result 94 | 95 | def versionFromModernizedInstaller(installerVersion : MojangVersionFile, version: ForgeVersion): 96 | eprint("Generating Modernized Forge %s." % version.longVersion) 97 | result = MultiMCVersionFile({"name":"Forge", "version":version.rawVersion, "uid":"net.minecraftforge" }) 98 | mcversion = version.mcversion 99 | result.requires = [DependencyEntry(uid='net.minecraft', equals=mcversion)] 100 | result.mainClass = installerVersion.mainClass 101 | args = installerVersion.minecraftArguments 102 | tweakers = [] 103 | expression = re.compile("--tweakClass ([a-zA-Z0-9\\.]+)") 104 | match = expression.search(args) 105 | while match != None: 106 | tweakers.append(match.group(1)); 107 | args = args[:match.start()] + args[match.end():] 108 | match = expression.search(args); 109 | if len(tweakers) > 0: 110 | args = args.strip() 111 | result.addTweakers = tweakers; 112 | # result.minecraftArguments = args 113 | result.releaseTime = installerVersion.releaseTime 114 | libs = [] 115 | mcFilter = loadMcVersionFilter(mcversion) 116 | for upstreamLib in installerVersion.libraries: 117 | mmcLib = MultiMCLibrary(upstreamLib.to_json()) 118 | if mmcLib.name.isLwjgl(): 119 | continue 120 | if mmcLib.name.isLog4j(): 121 | continue 122 | if shouldIgnoreArtifact(mcFilter, mmcLib.name): 123 | continue 124 | if mmcLib.name.group == "net.minecraftforge": 125 | if mmcLib.name.artifact == "forge": 126 | fixedName = mmcLib.name 127 | fixedName.classifier = "universal" 128 | mmcLib.downloads.artifact.path = fixedName.getPath() 129 | mmcLib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % fixedName.getPath() 130 | mmcLib.name = fixedName 131 | libs.append(mmcLib) 132 | continue 133 | elif mmcLib.name.artifact == "minecraftforge": 134 | fixedName = mmcLib.name 135 | fixedName.artifact = "forge" 136 | fixedName.classifier = "universal" 137 | fixedName.version = "%s-%s" % (mcversion, fixedName.version) 138 | mmcLib.downloads.artifact.path = fixedName.getPath() 139 | mmcLib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % fixedName.getPath() 140 | mmcLib.name = fixedName 141 | libs.append(mmcLib) 142 | continue 143 | libs.append(mmcLib) 144 | 145 | result.libraries = libs 146 | result.order = 5 147 | return result 148 | 149 | def versionFromLegacy(version, legacyinfo : ForgeLegacyInfo): 150 | result = MultiMCVersionFile({"name":"Forge", "version":version.rawVersion, "uid":"net.minecraftforge" }) 151 | mcversion = version.mcversion_sane 152 | result.requires = [DependencyEntry(uid='net.minecraft', equals=mcversion)] 153 | result.releaseTime = legacyinfo.releaseTime 154 | result.order = 5 155 | if mcversion in fmlLibsMapping: 156 | result.addTraits = ["legacyFML"] 157 | url = version.url() 158 | classifier = None 159 | if "universal" in url: 160 | classifier = "universal" 161 | else: 162 | classifier = "client" 163 | coord = GradleSpecifier("net.minecraftforge:forge:%s:%s" % (version.longVersion,classifier)) 164 | mainmod = MultiMCLibrary(name = coord) 165 | mainmod.downloads = MojangLibraryDownloads() 166 | mainmod.downloads.artifact = MojangArtifact() 167 | mainmod.downloads.artifact.path = None 168 | mainmod.downloads.artifact.url = version.url() 169 | mainmod.downloads.artifact.sha1 = legacyinfo.sha1 170 | mainmod.downloads.artifact.size = legacyinfo.size 171 | result.jarMods = [mainmod] 172 | return result 173 | 174 | def versionFromBuildSystemInstaller(installerVersion : MojangVersionFile, installerProfile: ForgeInstallerProfileV2, version: ForgeVersion): 175 | eprint("Generating Forge %s." % version.longVersion) 176 | result = MultiMCVersionFile({"name":"Forge", "version":version.rawVersion, "uid":"net.minecraftforge" }) 177 | result.requires = [DependencyEntry(uid='net.minecraft', equals=version.mcversion_sane)] 178 | result.mainClass = "io.github.zekerzhayard.forgewrapper.installer.Main" 179 | 180 | # FIXME: Add the size and hash here 181 | mavenLibs = [] 182 | 183 | # load the locally cached installer file info and use it to add the installer entry in the json 184 | with open("upstream/forge/installer_info/%s.json" % version.longVersion, 'r', encoding='utf-8') as f: 185 | installerInfo = InstallerInfo(json.load(f)) 186 | InstallerLib = MultiMCLibrary(name=GradleSpecifier("net.minecraftforge:forge:%s:installer" % (version.longVersion))) 187 | InstallerLib.downloads = MojangLibraryDownloads() 188 | InstallerLib.downloads.artifact = MojangArtifact() 189 | InstallerLib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % (InstallerLib.name.getPath()) 190 | InstallerLib.downloads.artifact.sha1 = installerInfo.sha1hash 191 | InstallerLib.downloads.artifact.size = installerInfo.size 192 | mavenLibs.append(InstallerLib) 193 | 194 | for upstreamLib in installerProfile.libraries: 195 | mmcLib = MultiMCLibrary(upstreamLib.to_json()) 196 | if mmcLib.name.group == "net.minecraftforge": 197 | if mmcLib.name.artifact == "forge": 198 | if mmcLib.name.classifier == "universal": 199 | mmcLib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % mmcLib.name.getPath() 200 | mavenLibs.append(mmcLib) 201 | continue 202 | if mmcLib.name.isLog4j(): 203 | continue 204 | mavenLibs.append(mmcLib) 205 | 206 | result.mavenFiles = mavenLibs 207 | 208 | libraries = [] 209 | 210 | # wrapperLib = MultiMCLibrary(name=GradleSpecifier("io.github.zekerzhayard:ForgeWrapper:mmc4")) 211 | # wrapperLib.downloads = MojangLibraryDownloads() 212 | # wrapperLib.downloads.artifact = MojangArtifact() 213 | # wrapperLib.downloads.artifact.url = "https://files.multimc.org/maven/%s" % (wrapperLib.name.getPath()) 214 | # wrapperLib.downloads.artifact.sha1 = "9a7d2f13be2070525909f30a26f3832db611009a" 215 | # wrapperLib.downloads.artifact.size = 36203 216 | # libraries.append(wrapperLib) 217 | 218 | # wrapperLib = MultiMCLibrary(name=GradleSpecifier("io.github.zekerzhayard:ForgeWrapper:mmc5")) 219 | # wrapperLib.downloads = MojangLibraryDownloads() 220 | # wrapperLib.downloads.artifact = MojangArtifact() 221 | # wrapperLib.downloads.artifact.url = "https://files.multimc.org/maven/%s" % (wrapperLib.name.getPath()) 222 | # wrapperLib.downloads.artifact.sha1 = "d82cb39636a5092a8e5b5de82ccfe5f7e70e8d49" 223 | # wrapperLib.downloads.artifact.size = 35390 224 | # libraries.append(wrapperLib) 225 | 226 | wrapperLib = MultiMCLibrary(name=GradleSpecifier("io.github.zekerzhayard:ForgeWrapper:mmc6")) 227 | wrapperLib.downloads = MojangLibraryDownloads() 228 | wrapperLib.downloads.artifact = MojangArtifact() 229 | wrapperLib.downloads.artifact.url = "https://files.multimc.org/maven/%s" % (wrapperLib.name.getPath()) 230 | wrapperLib.downloads.artifact.sha1 = "8dfb7d1151a7260ecb7ab7c01ac362df80469261" 231 | wrapperLib.downloads.artifact.size = 28912 232 | libraries.append(wrapperLib) 233 | 234 | for upstreamLib in installerVersion.libraries: 235 | mmcLib = MultiMCLibrary(upstreamLib.to_json()) 236 | if mmcLib.name.group == "net.minecraftforge": 237 | if mmcLib.name.artifact == "forge" and not mmcLib.name.classifier: 238 | fixedName = mmcLib.name 239 | fixedName.classifier = "launcher" 240 | mmcLib.downloads.artifact.path = fixedName.getPath() 241 | mmcLib.downloads.artifact.url = "https://maven.minecraftforge.net/%s" % fixedName.getPath() 242 | mmcLib.name = fixedName 243 | libraries.append(mmcLib) 244 | continue 245 | # forge 49.0.4+ sets an empty client download https://github.com/MinecraftForge/MinecraftForge/commit/5c15aa3322db8a3f95e97390638f80eb6d4e5d15 246 | # so we need clear the url to stop mmc from downloading it 247 | if not mmcLib.downloads.artifact.url and mmcLib.name.classifier == "client": 248 | continue 249 | if mmcLib.name.isLog4j(): 250 | continue 251 | libraries.append(mmcLib) 252 | result.libraries = libraries 253 | 254 | result.releaseTime = installerVersion.releaseTime 255 | result.order = 5 256 | mcArgs = "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}" 257 | for arg in installerVersion.arguments.game: 258 | mcArgs += " %s" % arg 259 | if "--fml.forgeVersion" not in installerVersion.arguments.game: 260 | mcArgs += " --fml.forgeVersion %s" % version.rawVersion 261 | if "--fml.mcVersion" not in installerVersion.arguments.game: 262 | mcArgs += " --fml.mcVersion %s" % version.mcversion 263 | if "--fml.forgeGroup" not in installerVersion.arguments.game: 264 | mcArgs += " --fml.forgeGroup net.minecraftforge" 265 | result.minecraftArguments = mcArgs 266 | return result 267 | 268 | 269 | # load the locally cached version list 270 | with open("upstream/forge/derived_index.json", 'r', encoding='utf-8') as f: 271 | main_json = json.load(f) 272 | remoteVersionlist = DerivedForgeIndex(main_json) 273 | 274 | recommendedVersions = [] 275 | 276 | tsPath = "static/forge-legacyinfo.json" 277 | 278 | legacyinfolist = None 279 | with open(tsPath, 'r', encoding='utf-8') as tsFile: 280 | legacyinfolist = ForgeLegacyInfoList(json.load(tsFile)) 281 | 282 | legacyVersions = [ 283 | "1.1", 284 | "1.2.3", 285 | "1.2.4", 286 | "1.2.5", 287 | "1.3.2", 288 | "1.4.1", 289 | "1.4.2", 290 | "1.4.3", 291 | "1.4.4", 292 | "1.4.5", 293 | "1.4.6", 294 | "1.4.7", 295 | "1.5", 296 | "1.5.1", 297 | "1.5.2", 298 | "1.6.1", 299 | "1.6.2", 300 | "1.6.3", 301 | "1.6.4", 302 | "1.7.10", 303 | "1.7.10-pre4", 304 | "1.7.2", 305 | "1.8", 306 | "1.8.8", 307 | "1.8.9", 308 | "1.9", 309 | "1.9.4", 310 | "1.10", 311 | "1.10.2", 312 | "1.11", 313 | "1.11.2", 314 | "1.12", 315 | "1.12.1", 316 | "1.12.2", 317 | ] 318 | 319 | for id, entry in remoteVersionlist.versions.items(): 320 | if entry.mcversion == None: 321 | eprint ("Skipping %s with invalid MC version" % id) 322 | continue 323 | 324 | version = ForgeVersion(entry) 325 | if version.url() == None: 326 | eprint ("Skipping %s with no valid files" % id) 327 | continue 328 | eprint ("Processing Forge %s" % version.rawVersion) 329 | versionElements = version.rawVersion.split('.') 330 | if len(versionElements) < 1: 331 | eprint ("Skipping version %s with not enough version elements" % (id)) 332 | continue 333 | 334 | majorVersionStr = versionElements[0] 335 | if not majorVersionStr.isnumeric(): 336 | eprint ("Skipping version %s with non-numeric major version %s" % (id, majorVersionStr)) 337 | continue 338 | 339 | majorVersion = int(majorVersionStr) 340 | #if majorVersion >= 37: 341 | # eprint ("Skipping unsupported major version %d (%s)" % (majorVersion, id)) 342 | # continue 343 | 344 | if entry.recommended: 345 | recommendedVersions.append(version.rawVersion) 346 | 347 | # If we do not have the corresponding Minecraft version, we ignore it 348 | if not os.path.isfile("multimc/net.minecraft/%s.json" % version.mcversion_sane): 349 | eprint ("Skipping %s with no corresponding Minecraft version %s" % (id, version.mcversion_sane)) 350 | continue 351 | 352 | outVersion = None 353 | 354 | # Path for new-style build system based installers 355 | installerVersionFilepath = "upstream/forge/version_manifests/%s.json" % version.longVersion 356 | profileFilepath = "upstream/forge/installer_manifests/%s.json" % version.longVersion 357 | 358 | eprint(installerVersionFilepath) 359 | if os.path.isfile(installerVersionFilepath): 360 | with open(installerVersionFilepath, 'r', encoding='utf-8') as installerVersionFile: 361 | installerVersion = MojangVersionFile(json.load(installerVersionFile)) 362 | if entry.mcversion in legacyVersions: 363 | outVersion = versionFromModernizedInstaller(installerVersion, version) 364 | else: 365 | with open(profileFilepath, 'r', encoding='utf-8') as profileFile: 366 | installerProfile = ForgeInstallerProfileV2(json.load(profileFile)) 367 | outVersion = versionFromBuildSystemInstaller(installerVersion, installerProfile, version) 368 | else: 369 | if version.usesInstaller(): 370 | 371 | # If we do not have the Forge json, we ignore this version 372 | if not os.path.isfile(profileFilepath): 373 | eprint ("Skipping %s with missing profile json" % id) 374 | continue 375 | with open(profileFilepath, 'r', encoding='utf-8') as profileFile: 376 | profile = ForgeInstallerProfile(json.load(profileFile)) 377 | outVersion = versionFromProfile(profile, version) 378 | else: 379 | # Generate json for legacy here 380 | if version.mcversion_sane == "1.6.1": 381 | continue 382 | build = version.build 383 | if not str(build).encode('utf-8').decode('utf8') in legacyinfolist.number: 384 | eprint("Legacy build %d is missing in legacy info. Ignoring." % build) 385 | continue 386 | 387 | outVersion = versionFromLegacy(version, legacyinfolist.number[build]) 388 | 389 | outFilepath = "multimc/net.minecraftforge/%s.json" % outVersion.version 390 | with open(outFilepath, 'w') as outfile: 391 | json.dump(outVersion.to_json(), outfile, sort_keys=True, indent=4) 392 | 393 | recommendedVersions.sort() 394 | 395 | print ('Recommended versions:', recommendedVersions) 396 | 397 | sharedData = MultiMCSharedPackageData(uid = 'net.minecraftforge', name = "Forge") 398 | sharedData.projectUrl = 'https://www.minecraftforge.net/forum/' 399 | sharedData.recommended = recommendedVersions 400 | sharedData.write() 401 | --------------------------------------------------------------------------------