aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore11
-rwxr-xr-xinstaller.py111
-rw-r--r--installer.spec48
-rwxr-xr-xmodpackman.py171
-rw-r--r--mods.txt48
-rw-r--r--packs/jeffrey-2/icon.icobin0 -> 1662 bytes
-rw-r--r--packs/jeffrey-2/icon.pngbin0 -> 1195 bytes
-rw-r--r--packs/jeffrey-2/pack-lock.ini52
-rw-r--r--packs/jeffrey-2/pack.ini54
-rw-r--r--packs/jeffrey-2/version.txt (renamed from version.txt)0
-rw-r--r--packs/jeffrey-3/.gitignore8
-rw-r--r--packs/jeffrey-3/config/creeperconfetti-common.toml8
-rw-r--r--packs/jeffrey-3/icon.icobin0 -> 9662 bytes
-rw-r--r--packs/jeffrey-3/icon.pngbin0 -> 923 bytes
-rw-r--r--packs/jeffrey-3/local-config.ini9
-rw-r--r--packs/jeffrey-3/pack-lock.ini86
-rw-r--r--packs/jeffrey-3/pack.ini100
-rw-r--r--readme.md63
-rw-r--r--todo.md7
-rwxr-xr-xupdate.py248
-rw-r--r--util.py309
-rw-r--r--whitelist.txt1
22 files changed, 1028 insertions, 306 deletions
diff --git a/.gitignore b/.gitignore
index f14d830..fcb831d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,10 @@
-pack-location.txt
+local-config.ini
+geckodriver
+geckodriver.exe
+geckodriver.log
+__pycache__/
+*.swp
+*.swo
+*.log
+build/
+dist/
diff --git a/installer.py b/installer.py
new file mode 100755
index 0000000..bdad7a6
--- /dev/null
+++ b/installer.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+import os
+import sys
+
+if hasattr(sys, '_MEIPASS'): # we're running in a bundle, go where we have our bundled assets
+ os.chdir(sys._MEIPASS)
+
+import subprocess
+import requests
+import tempfile
+import shutil
+import subprocess
+import json
+import uuid
+import pathlib
+
+from modpackman import install
+from util import config
+import util
+
+
+def install_forge():
+ """
+ :param java_path: path to a working Java executable
+ Downloads and runs the Forge installer specified in pack.ini.
+ """
+ with tempfile.TemporaryDirectory() as working_dir:
+ forge_path = os.path.join(working_dir, "forge_installer.jar")
+ util.download_file(config['pack']['forge_url'], forge_path)
+ try:
+ subprocess.check_output([util.find_jre(), "-jar", forge_path])
+ except RuntimeError:
+ if sys.platform == 'win32':
+ # if we can't find java, see if Windows can...
+ subprocess.check_output([f'cmd /C start "" "{forge_path}"'])
+ else:
+ raise
+
+
+def setup_forge(profile_id):
+ path_to_profiles = os.path.join(util.find_minecraft_directory(), "launcher_profiles.json")
+ # first, find current profiles so we can figure out which forge installs
+ with open(path_to_profiles, "r") as f:
+ profiles = json.load(f)
+ old_profile_ids = set(profiles["profiles"].keys())
+
+ # install forge, should add a new profile
+ install_forge()
+
+ with open(path_to_profiles, "r") as f:
+ profiles = json.load(f)
+ difference = set(profiles["profiles"].keys()) - old_profile_ids
+ if difference:
+ forge_profile_id = next(difference)
+ forge_game_version = profiles["profiles"][forge_profile_id]["lastVersionId"]
+ del profiles["profiles"][forge_profile_id]
+ else:
+ # this will probably break soon :(
+ game_version, forge_version = config["pack"]["forge_url"].split("/")[-2].split('-')
+ forge_game_version = f"{game_version}-forge-{forge_version}"
+
+ if profile_id not in profiles["profiles"]:
+ profile = {
+ "name": config["pack"]["name"],
+ "gameDir": config["pack"]["location"],
+ "lastVersionId": forge_game_version,
+ "type": "custom",
+ "javaArgs": config["pack"]["java_args"],
+ "icon": util.generate_base64_icon("icon.png")
+ }
+ profiles["profiles"][profile_id] = profile
+ else:
+ profile = profiles["profiles"][profile_id]
+ profile["lastVersionId"] = forge_game_version
+ profile["icon"] = util.generate_base64_icon("icon.png")
+
+ with open(path_to_profiles, "w") as f:
+ json.dump(profiles, f, indent=2)
+
+
+def main():
+ # if we're in a bundle, download the latest pack data from remote source
+ if hasattr(sys, "_MEIPASS"):
+ util.update_self()
+
+ persistent_data_path = os.path.join(config["pack"]["location"], "modpackman.json")
+ if os.path.exists(persistent_data_path):
+ with open(persistent_data_path, "r") as f:
+ persistent_data = json.load(f)
+ else:
+ # this is the first time this pack is installed
+ pathlib.Path(config["pack"]["location"]).mkdir(parents=True, exist_ok=True)
+ persistent_data = {"last_forge_url": "no", "profile_id": str(uuid.uuid4()).replace('-', '')}
+ if os.path.exists(os.path.join(util.find_minecraft_directory(), 'options.txt')):
+ shutil.copyfile(os.path.join(util.find_minecraft_directory(), 'options.txt'), os.path.join(config["pack"]["location"], "options.txt"))
+
+ if config["pack"]["forge_url"] != persistent_data["last_forge_url"]:
+ setup_forge(persistent_data["profile_id"])
+ persistent_data["last_forge_url"] = config["pack"]["forge_url"]
+ with open(persistent_data_path, "w") as f:
+ json.dump(persistent_data, f, indent=2)
+
+ ##todo install mods
+ install()
+
+
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/installer.spec b/installer.spec
new file mode 100644
index 0000000..dca793c
--- /dev/null
+++ b/installer.spec
@@ -0,0 +1,48 @@
+# -*- mode: python ; coding: utf-8 -*-
+import os
+import sys
+
+# included data is based on cwd
+cwd = os.getcwd()
+pack_name = cwd.split(os.path.sep)[-1]
+j = os.path.join
+
+block_cipher = None
+
+
+a = Analysis(['installer.py'],
+ pathex=[cwd],
+ binaries=[],
+ datas=[
+ (j(cwd, 'pack-lock.ini'), '.'),
+ (j(cwd, 'pack.ini'), '.'),
+ (j(cwd, 'icon.png'), '.'),
+ (j(cwd, 'config'), 'config')
+ ],
+ hiddenimports=[],
+ hookspath=[],
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher,
+ noarchive=False)
+
+pyz = PYZ(a.pure, a.zipped_data,
+ cipher=block_cipher)
+
+exe = EXE(pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name=f'{pack_name}-installer',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=True,
+ icon=j(cwd, 'icon.ico') + ",0")
diff --git a/modpackman.py b/modpackman.py
new file mode 100755
index 0000000..e5e5fdc
--- /dev/null
+++ b/modpackman.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+import os
+import sys
+
+if hasattr(sys, '_MEIPASS'): # we're running in a bundle, go to where we have our bundled assets
+ os.chdir(sys._MEIPASS)
+
+import argparse
+import shutil
+import requests
+import pathlib
+import hashlib
+from configparser import RawConfigParser
+
+import util
+from util import config
+
+# Apply updates to the actual mod pack
+def install():
+ mods_location = os.path.join(config["pack"]["location"], "mods")
+ whitelist = config["pack"]["whitelist"]
+ blacklist = config["pack"]["blacklist"]
+
+ # Actual download links are stored in pack-lock.ini, so we need to read it in here
+ pack_lock = RawConfigParser()
+ pack_lock.read('pack-lock.ini')
+
+ print(f"Updating pack with version {pack_lock['global']['pack_version']}...")
+
+ # create the mods folder if it doesn't already exist
+ pathlib.Path(mods_location).mkdir(parents=True, exist_ok=True)
+
+ names = set(f'{mod}.jar' for mod in pack_lock['mod_versions'].keys())
+ # whitelist client mods (e.g. optifine)
+ names.update(whitelist)
+
+ i = 0
+ for entry in pack_lock['mod_versions'].items():
+ name = f'{entry[0]}.jar'
+ checksum, url = entry[1].split(',')
+ mod_path = os.path.join(mods_location, name)
+ i += 1
+ if (os.path.exists(mod_path) and os.path.isfile(mod_path) and \
+ hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == checksum) or \
+ name in blacklist:
+ print(f"Skipping {name}, already up to date")
+ else:
+ print(f'Installing {name} from {url}...')
+ print(f' ({i} of {len(pack_lock["mod_versions"])})', end='\r')
+ util.download_file(url, mod_path)
+ print("Done!" + " " * 8)
+
+ # Remove any old mods that might be stuck in the mods folder
+ print()
+ print("Removing old mods...")
+ for jar in os.listdir(mods_location):
+ if jar not in names and os.path.splitext(jar)[1] == ".jar":
+ os.remove(os.path.join(mods_location, jar))
+ print(f"Removing '{jar}'")
+
+
+ print('\nInstalling configs...')
+ # For config files, we don't need to remove any extras, as they
+ # will simply be ignored by Forge
+ if not os.path.exists('config'):
+ raise RuntimeError("Error: config folder must exist!")
+
+ mc_config_folder = os.path.join(config['pack']['location'], 'config')
+ if not os.path.exists(mc_config_folder):
+ os.mkdir(mc_config_folder)
+ for cfg in os.listdir('config'):
+ shutil.copyfile(os.path.join('config', cfg), os.path.join(mc_config_folder, cfg))
+
+ print()
+ print("Finished installing mods!")
+
+
+
+def apply_updates():
+ """
+ Using the URLs defined in pack.ini, update all mods to the latest
+ compatible version and write them out to pack-lock.ini
+ """
+ print("Populating version file...")
+ print("Getting new versions of all mods...")
+ mod_urls = util.find_updated_urls([x for x in config['mods'].values()], config['pack']['game_version'], threads=16)
+ print("Downloading and checksumming all mods...")
+ if None in mod_urls:
+ print("[!] Checksum generation aborted due to invalid URLs. Please fix them and try again.")
+ exit(1)
+ checksums = util.find_checksums(mod_urls)
+
+ # Write the updated mods list out to pack-lock.ini
+ pack_lock = RawConfigParser()
+ pack_lock.read('pack-lock.ini')
+ pack_lock['global']['pack_version'] = str(int(pack_lock['global']['pack_version']) + 1)
+ # This is only needed for the self-update feature
+ pack_lock['global']['config_files'] = ','.join(os.listdir('config'))
+ pack_lock['mod_versions'] = {name: f'{checksum},{url}' for name, checksum, url in zip(config['mods'].keys(), checksums, mod_urls)}
+ with open('pack-lock.ini', 'w') as f:
+ pack_lock.write(f)
+
+ print()
+ print("Done!")
+ print("Updates applied to pack-lock.ini")
+ print(f"New pack version is {pack_lock['global']['pack_version']}")
+ print("[!] No mods were installed. To update your mods folder, run 'update.py install'")
+
+
+# Find if any updates are available
+def check_updates(mods, version_file, version=(2, 0, 0)):
+ pack_version = util.get_version_from_file(version_file)
+ print("Checking for updates to version " + str(pack_version) + "...")
+ latest = [(k, mods[k]) for k in mods.keys()]
+ old = util.read_file(version_file)
+ old_urls = [mod[2] for mod in old]
+ num_updates = 0
+
+ print("Checking updates...")
+ ffx = util.firefox()
+
+ for mod in latest:
+ print("Checking for updates to {mod[0]}...".format(mod=mod), end="")
+ sys.stdout.flush() # takes care of line-buffered terminals
+ if 'curseforge' in mod[1]:
+ url = util.find_cdn(ffx, mod[1], version)
+ else:
+ url = requests.get(mod[1]).url
+ if url in old_urls:
+ print(" No updates")
+ else:
+ print(" Found update: " + url.split('/')[-1])
+ num_updates += 1
+ ffx.close()
+
+ print("Finished checking for updates. {num} mods can be updated".format(num=num_updates))
+ if num_updates >= 0:
+ print("Run 'python update.py apply_updates' to create a new version with these updates applied.")
+
+
+parser = argparse.ArgumentParser(
+ description="A Simple Git-Based Modpack Manager",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog='''\
+Available commands:
+ install : Downloads mods listed in version.txt and populates the mods folder specified in pack-location.txt
+ apply_updates : Using the urls in mods.txt, repopulates version.txt to reflect the most recent mod versions
+ check_updates : Compares version.txt and mods.txt to see if any mods can be updated
+''')
+
+parser.add_argument('command',
+ nargs='?',
+ default='install',
+ help="The action to perform (default: install)")
+
+if __name__ == "__main__":
+ args = parser.parse_args()
+ mods = config['mods']
+ pack = config['pack']
+
+ # run the command
+ if args.command == "install":
+ install()
+ elif args.command == "apply_updates":
+ apply_updates()
+ elif args.command == "check_updates":
+ check_updates(config['mods'], "version.txt", config['pack']["game_version"])
+ else:
+ print("Error: command \"" + args.command + "\" does not exist")
+ parser.print_help()
+ sys.exit(1)
diff --git a/mods.txt b/mods.txt
deleted file mode 100644
index ee5337d..0000000
--- a/mods.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-# fill this file with the download links for mods, or the curseforge mod main page
-# e.g.
-twilightforest.jar https://www.curseforge.com/minecraft/mc-mods/the-twilight-forest
-tconstruct.jar https://www.curseforge.com/minecraft/mc-mods/tinkers-construct
-baubles.jar https://www.curseforge.com/minecraft/mc-mods/baubles
-jei.jar https://www.curseforge.com/minecraft/mc-mods/jei
-psi.jar https://www.curseforge.com/minecraft/mc-mods/psi
-opencomputers.jar https://www.curseforge.com/minecraft/mc-mods/opencomputers
-chisel-and-bits.jar https://www.curseforge.com/minecraft/mc-mods/chisels-bits
-chisel.jar https://www.curseforge.com/minecraft/mc-mods/chisel
-dynamictrees.jar https://www.curseforge.com/minecraft/mc-mods/dynamictrees
-dynamictrees-pams-compat.jar https://www.curseforge.com/minecraft/mc-mods/dtphc
-dynamictreestraverse.jar https://f-1.karel.pw/dttraverse-1.4.1e.jar
-traverse.jar https://media.forgecdn.net/files/2613/657/Traverse-1.12.2-1.6.0-69.jar
-roots.jar https://www.curseforge.com/minecraft/mc-mods/roots
-ae2.jar https://www.curseforge.com/minecraft/mc-mods/applied-energistics-2
-gravestones.jar https://www.curseforge.com/minecraft/mc-mods/gravestone-mod
-enderstorage.jar https://www.curseforge.com/minecraft/mc-mods/ender-storage-1-8
-waila.jar https://media.forgecdn.net/files/2568/751/Hwyla-1.8.26-B41_1.12.2.jar
-harvestcraft.jar https://www.curseforge.com/minecraft/mc-mods/pams-harvestcraft
-akashictome.jar https://www.curseforge.com/minecraft/mc-mods/akashic-tome
-railcraft.jar https://www.curseforge.com/minecraft/mc-mods/railcraft
-optifine.jar https://karel.pw/optifine-1.12.2.jar
-autoreglib.jar https://media.forgecdn.net/files/2746/11/AutoRegLib-1.3-32.jar
-ctm.jar https://www.curseforge.com/minecraft/mc-mods/ctm
-codechickenlib.jar https://www.curseforge.com/minecraft/mc-mods/codechicken-lib-1-8
-mysticallib.jar https://media.forgecdn.net/files/2865/499/mysticallib-1.12.2-1.6.0.jar
-patchouli.jar https://media.forgecdn.net/files/2731/963/Patchouli-1.0-20.jar
-mysticalworld.jar https://media.forgecdn.net/files/2838/234/mysticalworld-1.12.2-1.6.1.jar
-diet-hoppers.jar https://www.curseforge.com/minecraft/mc-mods/diet-hoppers
-worsebarrels.jar https://www.curseforge.com/minecraft/mc-mods/worse-barrels
-wearable-backpacks.jar https://www.curseforge.com/minecraft/mc-mods/wearable-backpacks
-eerie-entities.jar https://www.curseforge.com/minecraft/mc-mods/eerie-entities
-bookshelf.jar https://media.forgecdn.net/files/2836/960/Bookshelf-1.12.2-2.3.590.jar
-veining.jar https://www.curseforge.com/minecraft/mc-mods/veining
-minecoprocessors.jar https://www.curseforge.com/minecraft/mc-mods/minecoprocessors
-librarianlib.jar https://www.curseforge.com/minecraft/mc-mods/librarianlib
-forgelin.jar https://media.forgecdn.net/files/2785/465/Forgelin-1.8.4.jar
-dimdoors.jar https://www.curseforge.com/minecraft/mc-mods/dimensionaldoors
-translocators.jar https://www.curseforge.com/minecraft/mc-mods/translocators-1-8
-forgemultipartcbe.jar https://www.curseforge.com/minecraft/mc-mods/forge-multipart-cbe
-magneticraft.jar https://www.curseforge.com/minecraft/mc-mods/magneticraft
-ic2.jar https://www.curseforge.com/minecraft/mc-mods/industrial-craft
-modelloader.jar https://www.curseforge.com/minecraft/mc-mods/modelloader
-mtlib.jar https://www.curseforge.com/minecraft/mc-mods/mtlib
-appleskin.jar https://media.forgecdn.net/files/2496/585/AppleSkin-mc1.12-1.0.9.jar
-mantle.jar https://www.curseforge.com/minecraft/mc-mods/mantle
-worleys-caves.jar https://www.curseforge.com/minecraft/mc-mods/worleys-caves
diff --git a/packs/jeffrey-2/icon.ico b/packs/jeffrey-2/icon.ico
new file mode 100644
index 0000000..0525305
--- /dev/null
+++ b/packs/jeffrey-2/icon.ico
Binary files differ
diff --git a/packs/jeffrey-2/icon.png b/packs/jeffrey-2/icon.png
new file mode 100644
index 0000000..0dfbb45
--- /dev/null
+++ b/packs/jeffrey-2/icon.png
Binary files differ
diff --git a/packs/jeffrey-2/pack-lock.ini b/packs/jeffrey-2/pack-lock.ini
new file mode 100644
index 0000000..abaf013
--- /dev/null
+++ b/packs/jeffrey-2/pack-lock.ini
@@ -0,0 +1,52 @@
+[global]
+pack_version = 33
+config_files =
+
+[mod_versions]
+twilightforest = 23521ca2f42916cc6fe112464b6647d91b0e22cb,https://media.forgecdn.net/files/2756/932/twilightforest-1.12.2-3.9.984-universal.jar
+tconstruct = e37e1f05ad0eaf567497ce655bc877dc1778bd0f,https://media.forgecdn.net/files/2902/483/TConstruct-1.12.2-2.13.0.183.jar
+baubles = cb13fcfb18a9cb0cbd825fd5fe8d813c77368549,https://media.forgecdn.net/files/2518/667/Baubles-1.12-1.5.2.jar
+jei = 3e88d2896ca868c3cedb65e117ad3a1b82488fa8,https://media.forgecdn.net/files/3043/174/jei_1.12.2-4.16.1.302.jar
+psi = c76503880249a7e92f99d0ef68637076c6844218,https://media.forgecdn.net/files/3085/917/Psi-r1.1-78.2.jar
+opencomputers = 8eff5dc6b00a50b13ad6f3e6a838049bbf8306f8,https://media.forgecdn.net/files/2828/357/OpenComputers-MC1.12.2-1.7.5.192.jar
+chisel-bits = 0e6f159254e6899651087e5b1464bac91698d1d3,https://media.forgecdn.net/files/2720/655/chiselsandbits-14.33.jar
+chisel = 1dda45074e17128451b3c8f66172bfaddf84f443,https://media.forgecdn.net/files/2619/468/Chisel-MC1.12.2-0.2.1.35.jar
+dynamictrees = 7598b342c7585a64ab57a1bb7348bc95114a7d75,https://media.forgecdn.net/files/2960/958/DynamicTrees-1.12.2-0.9.8.jar
+dynamictrees-compat = 2e429e9705cb49afb72e2650001d003fd755ce42,https://media.forgecdn.net/files/2656/685/DynamicTreesPHC-1.12.2-1.4.2.jar
+dynamictreestraverse = 19c1e2e6b9c6ee4a9a93b8b50969be7bb6e13011,https://f-1.karel.pw/dttraverse-1.4.1e.jar
+traverse = b16aaff09a6e77ba193e42f88f5d5bb8a85e063a,https://media.forgecdn.net/files/2613/657/Traverse-1.12.2-1.6.0-69.jar
+roots = f954f42522ad35355e5215caa0a8b61904f29cd2,https://media.forgecdn.net/files/3056/896/Roots-1.12.2-3.0.32.jar
+ae2 = e5c3c11eafc5daf73652a2dabe855e3759caa8b5,https://media.forgecdn.net/files/2747/63/appliedenergistics2-rv6-stable-7.jar
+gravestones = 3daa7d4563965f6ec1954c8176e11fa3ba0b85ee,https://media.forgecdn.net/files/2608/278/gravestone-1.10.2.jar
+enderstorage = 7a872baf72b1da038704056a0cf7bbcc40bfa4d6,https://media.forgecdn.net/files/2755/787/EnderStorage-1.12.2-2.4.6.137-universal.jar
+waila = 7280d5c0dab42436549bcefc63ff64a1049e5501,https://media.forgecdn.net/files/2568/751/Hwyla-1.8.26-B41_1.12.2.jar
+harvestcraft = fb4df84de5f52125d0339b614787222b9ee45442,https://media.forgecdn.net/files/2751/199/Pam%27s%20HarvestCraft%201.12.2ze.jar
+akashictome = b11bf9d93f4bd7a2eeb8cfe49c1b30ce1a2f5a37,https://media.forgecdn.net/files/2648/656/AkashicTome-1.2-12.jar
+railcraft = ea2085a509b816bb9a3cdd79f2f44175b588737a,https://media.forgecdn.net/files/2687/757/railcraft-12.0.0.jar
+optifine = e805d4be5c2a3343488c573145606e90bb13816d,https://karel.pw/optifine-1.12.2.jar
+autoreglib = 267269ca7f1a71fb3bb35bdb8e61702a4da6263e,https://media.forgecdn.net/files/2746/11/AutoRegLib-1.3-32.jar
+ctm = 03be3e20dacf6b52abcee09436b2d06c06f2add0,https://media.forgecdn.net/files/2642/375/CTM-MC1.12.2-0.3.3.22.jar
+codechickenlib = b6a7e3b889c354216059a3bfad298e30a1e46a2d,https://media.forgecdn.net/files/2779/848/CodeChickenLib-1.12.2-3.2.3.358-universal.jar
+mysticallib = cb8c57761ca503c7ca4985991106d5df5ae2dd1a,https://media.forgecdn.net/files/3040/592/mysticallib-1.12.2-1.9.0.jar
+patchouli = 9804876a655365926757eda750189cd97b5bad69,https://media.forgecdn.net/files/2731/963/Patchouli-1.0-20.jar
+mysticalworld = df31d6c6777ff03a643425fbb9114421e58aef95,https://media.forgecdn.net/files/3054/945/mysticalworld-1.12.2-1.9.2.jar
+diet = 2aea3d9a64551cefe0a1b6f5c5edf57959796b66,https://media.forgecdn.net/files/2482/543/diethopper-1.1.jar
+worsebarrels = bd81a26550b4dc107f6781768973b650b4ffd6f4,https://media.forgecdn.net/files/2729/49/worsebarrels-1.2.0.jar
+wearable = 195614d96ebab5758605c7d89f95877bd2b7bbcc,https://media.forgecdn.net/files/2576/699/WearableBackpacks-1.12.2-3.1.3.jar
+eerie-entities = 30713af2e103899250239cbdfd2f7afb75e29f81,https://media.forgecdn.net/files/2872/906/EerieEntities-1.12.2-1.0.8.jar
+bookshelf = 83a1864dd78f48102609849dd36866d6cf32b907,https://media.forgecdn.net/files/2836/960/Bookshelf-1.12.2-2.3.590.jar
+veining = a0dbc6ad8021c8fa2f7d00de8058499268000232,https://media.forgecdn.net/files/2578/505/veining-1.3.2-1.12.x.jar
+minecoprocessors = aa953ffcfb6935669392c95cab68f552f9b1942c,https://media.forgecdn.net/files/2599/694/minecoprocessors-1.12.2-5.jar
+librarianlib = f2e75d6899a26fe32b0b0ee2ad33c68a70bd91e6,https://media.forgecdn.net/files/3041/340/librarianlib-1.12.2-4.22.jar
+forgelin = 7a87553fcb808a45d9c7e03f113b355ac7fd10d7,https://media.forgecdn.net/files/2785/465/Forgelin-1.8.4.jar
+dimdoors = 30c939f2305b862ed70b2144912ba25418430c36,https://media.forgecdn.net/files/2558/528/DimensionalDoors-3.0.9-287.jar
+translocators = b008cc099c15e0cb121c43c889504fc367b3dedd,https://media.forgecdn.net/files/2755/795/Translocators-1.12.2-2.5.2.81-universal.jar
+forgemultipartcbe = 3306ea22380bc9b6a0170b23fa0251085d5a6e25,https://media.forgecdn.net/files/2755/790/ForgeMultipart-1.12.2-2.6.2.83-universal.jar
+magneticraft = 1e32b4d2e0c38a3f129605ed945e55af19d17d13,https://media.forgecdn.net/files/2807/901/Magneticraft_1.12-2.8.2-dev.jar
+ic2 = 43e4af33528087ac448b9fcb5b31c6a6cd3a10ce,https://media.forgecdn.net/files/3078/604/industrialcraft-2-2.8.221-ex112.jar
+modelloader = 8f77152980cafb1be5a4204f5773daef74de8627,https://media.forgecdn.net/files/2744/735/modelloader-1.1.7.jar
+mtlib = a4625a61c9ef53412e0e467d23f25c5543658677,https://media.forgecdn.net/files/2684/561/MTLib-3.0.6.jar
+appleskin = 23162a97cab0adb4be2fc6d3937c613929d1d5c7,https://media.forgecdn.net/files/2496/585/AppleSkin-mc1.12-1.0.9.jar
+mantle = a1e5d5c197dae3e92637cafb8cd996185191165b,https://media.forgecdn.net/files/2713/386/Mantle-1.12-1.3.3.55.jar
+worleys0caves = ad8d5845683a65981b54d9737d9dcccaf8ae9b9c,https://media.forgecdn.net/files/3038/801/worleycaves-1.12.2-1.5.2.jar
+
diff --git a/packs/jeffrey-2/pack.ini b/packs/jeffrey-2/pack.ini
new file mode 100644
index 0000000..445cfa5
--- /dev/null
+++ b/packs/jeffrey-2/pack.ini
@@ -0,0 +1,54 @@
+[pack]
+name = J.E.F.F.R.E.Y. 2
+pack_base_url = https://gitlab
+forge_url = https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.12.2-14.23.5.2854/forge-1.12.2-14.23.5.2854-installer.jar
+game_version = 1.12.2
+java_args = -Xmx6G -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M
+
+[mods]
+twilightforest = https://www.curseforge.com/minecraft/mc-mods/the-twilight-forest
+tconstruct = https://www.curseforge.com/minecraft/mc-mods/tinkers-construct
+baubles = https://www.curseforge.com/minecraft/mc-mods/baubles
+jei = https://www.curseforge.com/minecraft/mc-mods/jei
+psi = https://www.curseforge.com/minecraft/mc-mods/psi
+opencomputers = https://www.curseforge.com/minecraft/mc-mods/opencomputers
+chisel-bits = https://www.curseforge.com/minecraft/mc-mods/chisels-bits
+chisel = https://www.curseforge.com/minecraft/mc-mods/chisel
+dynamictrees = https://www.curseforge.com/minecraft/mc-mods/dynamictrees
+dynamictrees-compat = https://www.curseforge.com/minecraft/mc-mods/dtphc
+dynamictreestraverse = https://f-1.karel.pw/dttraverse-1.4.1e.jar
+traverse = https://media.forgecdn.net/files/2613/657/Traverse-1.12.2-1.6.0-69.jar
+roots = https://www.curseforge.com/minecraft/mc-mods/roots
+ae2 = https://www.curseforge.com/minecraft/mc-mods/applied-energistics-2
+gravestones = https://media.forgecdn.net/files/2608/278/gravestone-1.10.2.jar
+enderstorage = https://www.curseforge.com/minecraft/mc-mods/ender-storage-1-8
+waila = https://media.forgecdn.net/files/2568/751/Hwyla-1.8.26-B41_1.12.2.jar
+harvestcraft = https://www.curseforge.com/minecraft/mc-mods/pams-harvestcraft
+akashictome = https://www.curseforge.com/minecraft/mc-mods/akashic-tome
+railcraft = https://www.curseforge.com/minecraft/mc-mods/railcraft
+optifine = https://karel.pw/optifine-1.12.2.jar
+autoreglib = https://media.forgecdn.net/files/2746/11/AutoRegLib-1.3-32.jar
+ctm = https://www.curseforge.com/minecraft/mc-mods/ctm
+codechickenlib = https://www.curseforge.com/minecraft/mc-mods/codechicken-lib-1-8
+mysticallib = https://media.forgecdn.net/files/3040/592/mysticallib-1.12.2-1.9.0.jar
+patchouli = https://media.forgecdn.net/files/2731/963/Patchouli-1.0-20.jar
+mysticalworld = https://media.forgecdn.net/files/3054/945/mysticalworld-1.12.2-1.9.2.jar
+diet = https://www.curseforge.com/minecraft/mc-mods/diet-hoppers
+worsebarrels = https://www.curseforge.com/minecraft/mc-mods/worse-barrels
+wearable = https://www.curseforge.com/minecraft/mc-mods/wearable-backpacks
+eerie-entities = https://www.curseforge.com/minecraft/mc-mods/eerie-entities
+bookshelf = https://media.forgecdn.net/files/2836/960/Bookshelf-1.12.2-2.3.590.jar
+veining = https://www.curseforge.com/minecraft/mc-mods/veining
+minecoprocessors = https://www.curseforge.com/minecraft/mc-mods/minecoprocessors
+librarianlib = https://www.curseforge.com/minecraft/mc-mods/librarianlib
+forgelin = https://media.forgecdn.net/files/2785/465/Forgelin-1.8.4.jar
+dimdoors = https://www.curseforge.com/minecraft/mc-mods/dimensionaldoors
+translocators = https://www.curseforge.com/minecraft/mc-mods/translocators-1-8
+forgemultipartcbe = https://www.curseforge.com/minecraft/mc-mods/forge-multipart-cbe
+magneticraft = https://www.curseforge.com/minecraft/mc-mods/magneticraft
+ic2 = https://www.curseforge.com/minecraft/mc-mods/industrial-craft
+modelloader = https://www.curseforge.com/minecraft/mc-mods/modelloader
+mtlib = https://www.curseforge.com/minecraft/mc-mods/mtlib
+appleskin = https://media.forgecdn.net/files/2496/585/AppleSkin-mc1.12-1.0.9.jar
+mantle = https://www.curseforge.com/minecraft/mc-mods/mantle
+worleys0caves = https://www.curseforge.com/minecraft/mc-mods/worleys-caves
diff --git a/version.txt b/packs/jeffrey-2/version.txt
index 1cc2ba4..1cc2ba4 100644
--- a/version.txt
+++ b/packs/jeffrey-2/version.txt
diff --git a/packs/jeffrey-3/.gitignore b/packs/jeffrey-3/.gitignore
new file mode 100644
index 0000000..8f39806
--- /dev/null
+++ b/packs/jeffrey-3/.gitignore
@@ -0,0 +1,8 @@
+local-config.ini
+geckodriver
+geckodriver.exe
+geckodriver.log
+__pycache__/
+*.swp
+*.swo
+*.log
diff --git a/packs/jeffrey-3/config/creeperconfetti-common.toml b/packs/jeffrey-3/config/creeperconfetti-common.toml
new file mode 100644
index 0000000..d049bda
--- /dev/null
+++ b/packs/jeffrey-3/config/creeperconfetti-common.toml
@@ -0,0 +1,8 @@
+
+[General]
+ #The %chance that any given creeper will explode into confetti [0..100|default:100]
+ #Range: 0 ~ 100
+ confettiChance = 5
+ #Confetti Explosions Damage Players [false/true|default:false]
+ damagePlayers = false
+
diff --git a/packs/jeffrey-3/icon.ico b/packs/jeffrey-3/icon.ico
new file mode 100644
index 0000000..3a75d9c
--- /dev/null
+++ b/packs/jeffrey-3/icon.ico
Binary files differ
diff --git a/packs/jeffrey-3/icon.png b/packs/jeffrey-3/icon.png
new file mode 100644
index 0000000..d60084b
--- /dev/null
+++ b/packs/jeffrey-3/icon.png
Binary files differ
diff --git a/packs/jeffrey-3/local-config.ini b/packs/jeffrey-3/local-config.ini
new file mode 100644
index 0000000..5781ccf
--- /dev/null
+++ b/packs/jeffrey-3/local-config.ini
@@ -0,0 +1,9 @@
+# this file is local configuration merged into the pack configuration at runtime
+# (values here override those in pack.ini)
+[pack]
+# Uncomment this to override the default automatic selection of the mod install directory.
+#location = /home/example/.minecraft/instances/jeffrey
+# A comma-separated list of mods that won't be deleted during the install process.
+whitelist = example_mod.jar,example_extra_mod.jar
+# A comma-separated list of mods that won't be installed during the install process
+blacklist = server_only_mod.jar,client_only_mod.jar
diff --git a/packs/jeffrey-3/pack-lock.ini b/packs/jeffrey-3/pack-lock.ini
new file mode 100644
index 0000000..3bffe20
--- /dev/null
+++ b/packs/jeffrey-3/pack-lock.ini
@@ -0,0 +1,86 @@
+[global]
+config_files = creeperconfetti-common.toml
+pack_version = 7
+
+[mod_versions]
+building-gadgets = 6ec24f6a51cb8e0d4ef53b2ed7167328fcae3738,https://media.forgecdn.net/files/3144/138/buildinggadgets-1.16.4-3.7.3.jar
+item-collectors = bb5f774df00908859f9d66883c9c822c9b2f5d88,https://media.forgecdn.net/files/3145/217/itemcollectors-1.0.7-mc1.16.4.jar
+natures-compass = 8e4c83dcdcb088382e47ce858818504ef053ced2,https://media.forgecdn.net/files/3133/521/NaturesCompass-1.16.4-1.8.6.jar
+packing-tape = 46060f5793a82a87967bf8137cbff73f34c40ac7,https://media.forgecdn.net/files/3082/552/PackingTape-1.16.3-0.10.0.jar
+seals = 8f83c9e18cae5929a24c528c4a0703d4f5202cac,https://media.forgecdn.net/files/3073/361/seals-1.16.3-2.0.0.jar
+simple-planes = 016f91f2c8b21620061f001008310b397fd63806,https://media.forgecdn.net/files/3134/471/simpleplanes-1.16.3-3.2.0.3.jar
+ender-chests = c30300af94311a46cc533cf9549f93432bbbcbe1,https://media.forgecdn.net/files/3106/960/enderchests-1.16-1.7.5.jar
+ender-tanks = a750b0d7fe4d50c23c647818b2bd11ea63b95ce9,https://media.forgecdn.net/files/3055/892/endertanks-1.16-1.9.3.jar
+gauges-and-switches = dd07cf0b5716d782cf9ebf2ea5e43c8c2f5a74d1,https://media.forgecdn.net/files/3142/879/rsgauges-1.16.4-1.2.6.jar
+moving-elevators = 72976360ddb40cd49dcb93c3036b281c13d693c2,https://media.forgecdn.net/files/3145/251/movingelevators-1.2.30-mc1.16.4.jar
+cooking-for-blockheads = cb9d33c55eefc4d876b2590a9bd099c0ef1cea77,https://media.forgecdn.net/files/3098/223/CookingForBlockheads_1.16.3-9.2.2.jar
+discord-presence = cf7250f8cdd6dcbd0a3048a4bc992cb0f3964671,https://media.forgecdn.net/files/3103/834/SimpleDiscordRichPresence-1.16.4-1.3.5.jar
+shetiphiancore = b9e612714fc2bb7a6bd1be8b04a143b45d76b121,https://media.forgecdn.net/files/3090/382/shetiphiancore-1.16-3.8.4.jar
+quark = 5aa63e44e03b0e5bef944169a7287134ce375504,https://media.forgecdn.net/files/3146/131/Quark-r2.4-283.jar
+blood-magic = 85f1d49cd69cefe67b86a80e47f16124f43bd7c5,https://media.forgecdn.net/files/3132/991/BloodMagic-1.16.3-3.0.2-7.jar
+astral-sorcery = 167558fd1ae9f9605fb7c1c9dd3c80e77f55174d,https://media.forgecdn.net/files/3144/866/astralsorcery-1.16.4-1.13.8.jar
+ae2 = 7d2b29fceaed530813aae3b60bef27a28ecc3257,https://media.forgecdn.net/files/3118/473/appliedenergistics2-8.2.0-alpha.2.jar
+mekanism = 8b2ffd61cc121d5e342d49633a0696c34877a273,https://media.forgecdn.net/files/3134/211/Mekanism-1.16.4-10.0.18.445.jar
+mekanism-tools = f72c31c26785cd867e32dbde30fdfe65429df6c2,https://media.forgecdn.net/files/3134/214/MekanismTools-1.16.4-10.0.18.445.jar
+mekanism-generators = 140700335ae7ccca3e4a78fd0199e90429aabeca,https://media.forgecdn.net/files/3134/213/MekanismGenerators-1.16.4-10.0.18.445.jar
+mekanism-additions = b3abe12303f57967b37f391830742becfe9401ae,https://media.forgecdn.net/files/3134/215/MekanismAdditions-1.16.4-10.0.18.445.jar
+psi = 35ff69060bbdf7514da60d224ffd9db8bdd0f772,https://media.forgecdn.net/files/3106/707/Psi%201.16-88.jar
+cc-tweaked = 569a119e39aa59d03d10189bec829f7bc8d6fb9b,https://media.forgecdn.net/files/3104/639/cc-tweaked-1.16.4-1.94.0.jar
+immersive-engineering = 74051c20e6f1d9713daac60fda0852016544b9dd,https://media.forgecdn.net/files/3141/693/ImmersiveEngineering-1.16.4-4.1.2-129.jar
+botania = 2fcf9ca0f76a5e9ab3c09b5140c94555b7514ba9,https://media.forgecdn.net/files/3134/409/Botania-1.16.4-410.jar
+jei = 153ba78db1142db5dd0b4e3a5cd10ab9d2141f45,https://media.forgecdn.net/files/3136/600/jei-1.16.4-7.6.0.62.jar
+tetra = 4b309ec6a02c024baff730c363ac5d6a1071b0a6,https://media.forgecdn.net/files/3147/835/tetra-1.16.4-3.4.0.jar
+chickenchunks = 0b7110a4668da041debb43b8a1ec200b61c13d82,https://media.forgecdn.net/files/3125/159/ChickenChunks-1.16.4-2.7.0.85-universal.jar
+chisel-and-bits = 6922303111dfc8d3b12f29372f35f000276e329a,https://media.forgecdn.net/files/3133/859/chiselsandbits-0.2.8-RELEASE.jar
+inventory-sorter = fb538c5a8eb1e8a7a2bc35f3344b816634d53e4e,https://media.forgecdn.net/files/3077/903/inventorysorter-1.16.1-18.1.0.jar
+creeper-confetti = c1d06621ba453680fe31dd30fe11f6ea9bcc8e93,https://media.forgecdn.net/files/3063/500/creeperconfetti-3.4.jar
+hwyla = 79123ef4c447affe57e77ee241d78c494bd8948d,https://media.forgecdn.net/files/3033/593/Hwyla-forge-1.10.11-B78_1.16.2.jar
+jer = dcede97c594d1f89a071bd2f904f232f789aea44,https://media.forgecdn.net/files/3109/962/JustEnoughResources-1.16.4-0.12.0.103.jar
+ender-tendril = caa72968398bf124e5c2fcb03833547b74b68056,https://media.forgecdn.net/files/3069/322/EnderTendril-1.16.3-1.1.1%2B4.jar
+discord-integration = d144c5a7918f3ccd7ab01b8768893c0562388cb3,https://media.forgecdn.net/files/3118/761/dcintegration-2.0.2-1.16.jar
+step = dcc4854053a27c8c893fb3809b6d2a2cda0741ab,https://media.forgecdn.net/files/3100/371/step-1.16.4-1.0.3.jar
+simply-backpacks = d4bb5d2d4d8078debef3e7f0788f21db3f637bfa,https://media.forgecdn.net/files/3076/926/simplybackpacks-1.16.3-1.4.13.jar
+better-mineshafts = 6ba857968964a3479a8df4c15a1ba120f2c3e978,https://media.forgecdn.net/files/3145/910/BetterMineshafts-Forge-1.16.3-1.1.1.jar
+comforts = e3e827f0c6fe892c88dc865b4c6a4f0924f034ec,https://media.forgecdn.net/files/3143/312/comforts-forge-1.16.4-4.0.0.3.jar
+better-than-llamas = 29cecac936279cc87cc4a957acfbcca8d5551b2e,https://media.forgecdn.net/files/3062/115/BetterThanLlamas-1.16.3-1.1.1.jar
+better-than-bunnies = 58640158f2aedbb0547d37568c0ccca66fd49944,https://media.forgecdn.net/files/3062/114/BetterThanBunnies-1.16.3-1.2.0.jar
+corpse = d5bf41cad1a7c7d90972697e3f5494dd0444e15c,https://media.forgecdn.net/files/3123/623/corpse-1.16.4-1.0.4.jar
+gilded-armor = 2eae828bcf1ef89c302f7100fe02505ac048c5e1,https://media.forgecdn.net/files/3069/303/gildedarmor-1.16.3-1.0.3.jar
+jei-integration = e9368114e66bf4fc23cd21dd97b95c469115dd55,https://media.forgecdn.net/files/3122/292/jeiintegration_1.16.4-6.1.1.11.jar
+openblocks-elevator = 6e3e440447a57b7b2b3256d4acbf1519be3a09c8,https://media.forgecdn.net/files/3110/386/elevatorid-1.16.4-1.7.8.jar
+ranged-pumps = 202c2aba870b852638d6ed28074dd0e91b2f76c1,https://media.forgecdn.net/files/3065/697/rangedpumps-0.8.2.jar
+the-conjurer = 3dd29349f2190d2f5b40cebed4fa67f6bb716324,https://media.forgecdn.net/files/3107/953/the-conjurer-1.16.4-1.0.13.jar
+trashcans = c251a09d6244c821bbd5d9b46cd2ad5c7b5a0c9e,https://media.forgecdn.net/files/3144/215/trashcans-1.0.5-mc1.16.4.jar
+controlling = d7d04f585795b3d3040a6ad863a8737cf2efa6ac,https://media.forgecdn.net/files/3110/995/Controlling-7.0.0.11.jar
+ding = d984f82495e1ed12236aeea6e6be20d2bd094d72,https://media.forgecdn.net/files/3062/74/Ding-1.16.3-1.2.0.jar
+appleskin = d0e2d9d9bf17806d3fbd263a8702822ab33edeb1,https://media.forgecdn.net/files/3035/787/AppleSkin-mc1.16.2-forge-1.0.14.jar
+enchantment-descriptions = 30439e14507a5b942d4b696af8a1dbc463134199,https://media.forgecdn.net/files/3112/901/EnchantmentDescriptions-1.16.4-6.0.2.jar
+emojiful = 994b1e4de03a22880a904339c64d35db93d09732,https://media.forgecdn.net/files/3099/65/emojiful-1.16.3-2.1.1.jar
+clumps = f84965303d0c8b6320435cf88dd0dcf403a7098d,https://media.forgecdn.net/files/3137/103/Clumps-6.0.0.13.jar
+toast-control = 6ebe524115df53b9d9a8068e1490ce20b9e139dc,https://media.forgecdn.net/files/3069/51/Toast-Control-1.16.3-4.3.0.jar
+mouse-tweaks = b9c5ac6c2183eee2c4acda20dc1dd7fc2c387fa2,https://media.forgecdn.net/files/3035/780/MouseTweaks-2.13-mc1.16.2.jar
+recipe-buffers = 03a01185d7ab325ba14f56a41b0a424b2d6a580c,https://media.forgecdn.net/files/3126/130/recipebuffers-1.1.jar
+harvest = f9618079d9456c23133ab89909fdd1c9fbae56b8,https://media.forgecdn.net/files/3087/381/harvest-1.16.3-1.0.3.jar
+light-overlay = a3caff7768581d5d97b5433dd834dfa6d676aea9,https://media.forgecdn.net/files/3091/376/light-overlay-5.5.4.jar
+morpheus = da30b18acd23c270a7da07ed8feb693291efcf90,https://media.forgecdn.net/files/3114/135/Morpheus-1.16.4-4.2.68.jar
+extreme-sound-muffler = 0e7d143dac36cb1e744b450217313e7abb6ea24f,https://media.forgecdn.net/files/3136/803/extremeSoundMuffler-3.2_Forge-1.16.4.jar
+fast-workbench = c7f0296b5c569f1fe11aa6368b87bebfa01c9ddc,https://media.forgecdn.net/files/3112/661/FastWorkbench-1.16.3-4.4.1.jar
+findme = 7f18ff3af51b46c388d780c0b0afb9b400cc7962,https://media.forgecdn.net/files/3073/336/findme-1.16.3-2.1.0.0.jar
+wawla = 9aeea4cc8ae5bfa89701435b7c71bffdc43cc49d,https://media.forgecdn.net/files/3124/964/WAWLA-1.16.4-7.0.2.jar
+iron-chests = 3e7a9131d1ea836527d5cf7a1761d8b4dc7026a3,https://media.forgecdn.net/files/3105/315/ironchest-1.16.4-11.2.10.jar
+reauth = f5bdf682d64bcde250c8a2a9c1261e285540ac9b,https://media.forgecdn.net/files/3105/779/ReAuth-1.16-Forge-3.9.3.jar
+yungs-caves = 159c5a81f6cdcf9a541ea499152cbe8aedcfcfad,https://media.forgecdn.net/files/3128/132/BetterCaves-1.16.3-1.0.6.jar
+mgui = 847a29f1913b3957d384e57be8ee415a51c3e8ff,https://media.forgecdn.net/files/3104/239/mgui-1.16.4-3.1.3.jar
+obfuscate = 6065de3d3cfda408db086ca1bbb2a0882630dee4,https://media.forgecdn.net/files/3148/131/obfuscate-0.5.1-1.16.3.jar
+placebo = 1513d08e468654c8893f00f6b844fa374fd56620,https://media.forgecdn.net/files/3092/113/Placebo-1.16.3-4.3.3.jar
+cloth-config = 1ee61a4974ce5c720b495a22d1221a4906a4334b,https://media.forgecdn.net/files/3112/227/cloth-config-forge-4.1.1.jar
+structure-jel = 3145fde7a369c5214e375501d98a5d9a2b4c44bd,https://media.forgecdn.net/files/3133/625/structure-gel-api-1.16.4-1.7.2.jar
+bookshelf = eadd284eee4e26bcdf301f5b03d66abbc58e199f,https://media.forgecdn.net/files/3133/712/Bookshelf-1.16.4-9.3.18.jar
+ichunutil = 902ee274d713242aae0339a3ddefabc277b65632,https://media.forgecdn.net/files/3062/89/iChunUtil-1.16.3-10.0.0.jar
+titanium = 5d266ccfedd28a7831b7491cd59a8fa4f7c2eb69,https://media.forgecdn.net/files/3143/177/titanium-1.16.4-3.2.3.jar
+curios = 744de4d8889fbb0c5af43416e3bec16df056fe70,https://media.forgecdn.net/files/3122/651/curios-forge-1.16.4-4.0.3.0.jar
+autoreglib = b581dda2655a47c84ef15013784160b07f6d5c97,https://media.forgecdn.net/files/3128/555/AutoRegLib-1.6-47.jar
+observerlib = c3154cb3f0e78ff9ef526f6de80dca802a70d31a,https://media.forgecdn.net/files/3123/187/observerlib-1.16.4-1.4.4.jar
+patchouli = 5d27235ad0e644dcfafadf439f15134c966b54e2,https://media.forgecdn.net/files/3126/931/Patchouli-1.16.4-48.jar
+codechickenlib = 67e93ac61ae70eec8fa3086032d3a47a0cfd52e6,https://media.forgecdn.net/files/3125/157/CodeChickenLib-1.16.4-3.5.0.398-universal.jar
+
diff --git a/packs/jeffrey-3/pack.ini b/packs/jeffrey-3/pack.ini
new file mode 100644
index 0000000..9d6b892
--- /dev/null
+++ b/packs/jeffrey-3/pack.ini
@@ -0,0 +1,100 @@
+[pack]
+name = J.E.F.F.R.E.Y. 3
+pack_base_url = https://gitlab.com/1F335/modpackman/-/raw/refactor-fix/packs/jeffrey-3/
+forge_url = https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.16.4-35.1.13/forge-1.16.4-35.1.13-installer.jar
+game_version = 1.16.4
+java_args = -Xmx6G -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=32M
+
+[mods]
+# Possibly controversial mods - subject to further review
+building-gadgets = https://www.curseforge.com/minecraft/mc-mods/building-gadgets
+item-collectors = https://www.curseforge.com/minecraft/mc-mods/item-collectors
+natures-compass = https://www.curseforge.com/minecraft/mc-mods/natures-compass
+packing-tape = https://www.curseforge.com/minecraft/mc-mods/packing-tape
+seals = https://www.curseforge.com/minecraft/mc-mods/seals
+simple-planes = https://www.curseforge.com/minecraft/mc-mods/simple-planes
+ender-chests = https://www.curseforge.com/minecraft/mc-mods/enderchests
+ender-tanks = https://www.curseforge.com/minecraft/mc-mods/endertanks
+gauges-and-switches = https://www.curseforge.com/minecraft/mc-mods/redstone-gauges-and-switches
+moving-elevators = https://www.curseforge.com/minecraft/mc-mods/moving-elevators
+cooking-for-blockheads = https://www.curseforge.com/minecraft/mc-mods/cooking-for-blockheads
+discord-presence = https://www.curseforge.com/minecraft/mc-mods/simple-discord-rich-presence
+# Minecolonies doesn't work, citing version incompatibilites with the latest structurize
+#minecolonies = https://www.curseforge.com/minecraft/mc-mods/minecolonies
+# library mod for minecolonies
+#structurize = https://www.curseforge.com/minecraft/mc-mods/structurize
+# library mod for enderchests/endertanks
+shetiphiancore = https://www.curseforge.com/minecraft/mc-mods/shetiphiancore
+quark = https://www.curseforge.com/minecraft/mc-mods/quark
+blood-magic = https://www.curseforge.com/minecraft/mc-mods/blood-magic
+astral-sorcery = https://www.curseforge.com/minecraft/mc-mods/astral-sorcery
+
+# Fight me mods
+ae2 = https://www.curseforge.com/minecraft/mc-mods/applied-energistics-2
+mekanism = https://www.curseforge.com/minecraft/mc-mods/mekanism
+mekanism-tools = https://www.curseforge.com/minecraft/mc-mods/mekanism-tools
+mekanism-generators = https://www.curseforge.com/minecraft/mc-mods/mekanism-generators
+mekanism-additions = https://www.curseforge.com/minecraft/mc-mods/mekanism-additions
+psi = https://www.curseforge.com/minecraft/mc-mods/psi
+cc-tweaked = https://www.curseforge.com/minecraft/mc-mods/cc-tweaked
+immersive-engineering = https://www.curseforge.com/minecraft/mc-mods/immersive-engineering
+botania = https://www.curseforge.com/minecraft/mc-mods/botania
+jei = https://www.curseforge.com/minecraft/mc-mods/jei
+tetra = https://www.curseforge.com/minecraft/mc-mods/tetra
+
+# Smaller yes mods
+chickenchunks = https://www.curseforge.com/minecraft/mc-mods/chicken-chunks-1-8
+chisel-and-bits = https://www.curseforge.com/minecraft/mc-mods/chisels-bits
+inventory-sorter = https://www.curseforge.com/minecraft/mc-mods/inventory-sorter
+creeper-confetti = https://www.curseforge.com/minecraft/mc-mods/creeper-confetti
+hwyla = https://www.curseforge.com/minecraft/mc-mods/hwyla
+jer = https://www.curseforge.com/minecraft/mc-mods/just-enough-resources-jer
+ender-tendril = https://www.curseforge.com/minecraft/mc-mods/ender-tendril
+discord-integration = https://www.curseforge.com/minecraft/mc-mods/dcintegration
+step = https://www.curseforge.com/minecraft/mc-mods/step
+simply-backpacks = https://www.curseforge.com/minecraft/mc-mods/simply-backpacks
+better-mineshafts = https://www.curseforge.com/minecraft/mc-mods/yungs-better-mineshafts-forge
+comforts = https://www.curseforge.com/minecraft/mc-mods/comforts
+better-than-llamas = https://www.curseforge.com/minecraft/mc-mods/better-than-llamas
+better-than-bunnies = https://www.curseforge.com/minecraft/mc-mods/better-than-bunnies
+corpse = https://www.curseforge.com/minecraft/mc-mods/corpse
+gilded-armor = https://www.curseforge.com/minecraft/mc-mods/gildedarmor
+jei-integration = https://www.curseforge.com/minecraft/mc-mods/jei-integration
+openblocks-elevator = https://www.curseforge.com/minecraft/mc-mods/openblocks-elevator
+ranged-pumps = https://www.curseforge.com/minecraft/mc-mods/ranged-pumps
+the-conjurer = https://www.curseforge.com/minecraft/mc-mods/the-conjurer
+trashcans = https://www.curseforge.com/minecraft/mc-mods/trash-cans
+controlling = https://www.curseforge.com/minecraft/mc-mods/controlling
+ding = https://www.curseforge.com/minecraft/mc-mods/ding
+appleskin = https://www.curseforge.com/minecraft/mc-mods/appleskin
+enchantment-descriptions = https://www.curseforge.com/minecraft/mc-mods/enchantment-descriptions
+emojiful = https://www.curseforge.com/minecraft/mc-mods/emojiful
+clumps = https://www.curseforge.com/minecraft/mc-mods/clumps
+toast-control = https://www.curseforge.com/minecraft/mc-mods/toast-control
+mouse-tweaks = https://www.curseforge.com/minecraft/mc-mods/mouse-tweaks
+recipe-buffers = https://www.curseforge.com/minecraft/mc-mods/recipebuffers
+harvest = https://www.curseforge.com/minecraft/mc-mods/harvest
+light-overlay = https://www.curseforge.com/minecraft/mc-mods/light-overlay
+morpheus = https://www.curseforge.com/minecraft/mc-mods/morpheus
+extreme-sound-muffler = https://www.curseforge.com/minecraft/mc-mods/extreme-sound-muffler
+fast-workbench = https://www.curseforge.com/minecraft/mc-mods/fastworkbench
+findme = https://www.curseforge.com/minecraft/mc-mods/findme
+wawla = https://www.curseforge.com/minecraft/mc-mods/wawla
+iron-chests = https://www.curseforge.com/minecraft/mc-mods/iron-chests
+reauth = https://www.curseforge.com/minecraft/mc-mods/reauth
+yungs-caves = https://www.curseforge.com/minecraft/mc-mods/yungs-better-caves
+
+# Library mods
+mgui = https://www.curseforge.com/minecraft/mc-mods/mgui
+obfuscate = https://www.curseforge.com/minecraft/mc-mods/obfuscate
+placebo = https://www.curseforge.com/minecraft/mc-mods/placebo
+cloth-config = https://www.curseforge.com/minecraft/mc-mods/cloth-config-forge
+structure-jel = https://www.curseforge.com/minecraft/mc-mods/structure-gel-api
+bookshelf = https://www.curseforge.com/minecraft/mc-mods/bookshelf
+ichunutil = https://www.curseforge.com/minecraft/mc-mods/ichunutil
+titanium = https://www.curseforge.com/minecraft/mc-mods/titanium
+curios = https://www.curseforge.com/minecraft/mc-mods/curios
+autoreglib = https://www.curseforge.com/minecraft/mc-mods/autoreglib
+observerlib = https://www.curseforge.com/minecraft/mc-mods/observerlib
+patchouli = https://www.curseforge.com/minecraft/mc-mods/patchouli
+codechickenlib = https://www.curseforge.com/minecraft/mc-mods/codechicken-lib-1-8
diff --git a/readme.md b/readme.md
index 9149dfa..0f219bb 100644
--- a/readme.md
+++ b/readme.md
@@ -3,20 +3,67 @@
Script to update modpacks automatically
-#### To Use
+### Installation
-First, install [Python 3](https://www.python.org/downloads/) and [Git](https://git-scm.com/downloads) and add them to your `$PATH`.
-Then, run `pip install requests` to install the Python Requests module (required to run the script).
+#### For *Windows* users:
-Simply put the location of your `mods` folder in `pack-location.txt` and run `python update.py install`
+1. Close *Minecraft* and *Minecraft Launcher*.
-#### Maintenance:
+2. Download the latest installer from [the releases page](https://gitlab.com/1F335/modpackman/-/releases) and run it, completing the Forge prompt.
-To check `version.txt` modlist for updates against `mods.txt` modlist, run `python update.py check_updates`.
+3. Start *Minecraft Launcher* and launch the newly installed modpack profile.
-To automatically populate `version.txt` with the most recent versions of mods listed in `mods.txt` run `python update.py apply_updates`.
-Finally, to actually install mods from the list in `version.txt`, run `python update.py install`
+#### For other platforms:
+1. Install Git, Python 3, Java, and *Minecraft*.
+
+2. Close *Minecraft* and *Minecraft Launcher*.
+
+3. Install the Python `requests` module with `pip install requests` (or `pip install --user requests` if the first one fails).
+
+4. Clone or download this repository.
+
+5. In a shell, navigate to the directory of your desired pack (e.g. `cd packs/jeffrey-3` for the J.E.F.F.R.E.Y. 3 modpack).
+
+6. Run the installer with `python ../../installer.py`
+
+7. Start *Minecraft Launcher* and launch the newly installed modpack profile.
+
+
+### Maintenance:
+
+To select a pack to work on, navigate to its directory (e.g. `cd packs/jeffrey-3` for the J.E.F.F.R.E.Y. 3 modpack).
+
+Run `python ../../modpackman.py apply_updates` to update `pack-lock.ini` with the most recent compatible versions of every mod specified in this pack's `pack.ini`. This will also update the list of bundled config files and increment the pack version.
+
+Run `python ../../modpackman.py check_updates` to print out available updates for all the mods specified in this pack.
+
+To bypass everything except the mod downloads, run `python ../../modpackman.py install`. This will install all the mods specified in this pack's `pack-lock.ini`
***NOTE***: `check_updates` and `apply_updates` require you to have the `selenium` module installed
+
+
+### Configuration:
+
+All of the data related to specific pack is stored in its folder in `packs/`. This includes:
+ - The icon for the launcher profile (`icon.png`)
+ - The icon for the executable installer (`icon.ico`)
+ - The default mod config files (`config/`)
+ - The modpackman pack configuration (`pack.ini`)
+ - The current pack version information (`pack-lock.ini`)
+
+Note: you can create a file `local-config.ini` in this folder on your local machine that will override any specified values in `pack.ini`
+
+`pack.ini` has two sections:
+
+ - `pack`, with these options:
+ - `name`: the user-friendly name of the modpack
+ - `pack_base_url`: the web url from whence the pack's data may be retrieved
+ - `forge_url`: the download url for the forge installer for this pack. (note: this skips the Forge ads, consider supporting the project directly instead)
+ - `game_version`: the maximum *Minecraft* version to update mods to
+ - `java_args`: Java arguments to be added to the pack launcher profile when it is created.
+
+ - `mods`, which is a collection of `mod_name = download_url` pairs. For mods hosted on curseforge, the download url is the project's homepage url.
+
+
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..1d7eb4c
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,7 @@
+ - version.txt -> pack-lock.ini
+ - also add file_names for config
+ - pack_version in there as well
+ - copy config folder
+ - download config from url
+ - download everything from specified URL
+ - only when in a bundle
diff --git a/update.py b/update.py
deleted file mode 100755
index 2851c43..0000000
--- a/update.py
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import os
-import sys
-import hashlib
-import shutil
-import re
-import collections
-import urllib.parse
-
-import requests
-
-parser = argparse.ArgumentParser(
- description="A Simple Git-Based Modpack Manager",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog='''\
-Available commands:
- install : Downloads mods listed in version.txt and populates the mods folder specified in pack-location.txt
- apply_updates : Using the urls in mods.txt, repopulates version.txt to reflect the most recent mod versions
- check_updates : Compares version.txt and mods.txt to see if any mods can be updated
-''')
-
-parser.add_argument('command',
- nargs='?',
- default='install',
- help="The action to perform (default: install)")
-parser.add_argument('filename',
- nargs='?',
- default="mods.txt",
- help="Optional filename to specify latest mods (default: mods.txt)")
-parser.add_argument('--version-file',
- type=str,
- default="version.txt",
- help="Optional custom version file to download mods from (default: version.txt)")
-parser.add_argument('--pack-location',
- type=str,
- help="Optional custom modpack folder location (default: read from pack-location.txt)")
-parser.add_argument('--whitelist-file',
- type=str,
- default="whitelist.txt",
- help="Optional custom whitelist file that tells 'install' which files not to remove (default: whitelist.txt)")
-parser.add_argument("--game-version",
- type=str,
- default=None,
- help="The maximum game version to update mods to")
-
-## loaded from version.txt
-VERSION = 0
-
-def read_file(fil):
- strings = []
- with open(fil) as f:
- for line in f:
- string = line.strip().split()
- if len(line) > 1 and line[0] != '#':
- # run strip on each element
- string = tuple(map(lambda x: x.strip(), string))
- strings.append(string)
-
- return strings
-
-# Apply updates to the actual mod pack
-def install(args):
- print("Updating pack with version " + str(VERSION) + "...")
- print()
- # (fname, checksum, url)
- mods = read_file(args.version_file)
- names = [mod[0] for mod in mods]
- # whitelist client mods (e.g. optifine)
- names += [line[0] for line in read_file(args.whitelist_file)]
-
- i = 0
- for mod in mods:
- mod_path = os.path.join(args.pack_location, mod[0])
- i += 1
- if os.path.exists(mod_path) and os.path.isfile(mod_path) and \
- hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == mod[1]:
- print("Skipping {mod[0]}, already up to date".format(mod=mod))
- else:
- print('Installing {mod[0]} from {mod[2]}...'.format(mod=mod))
- print(' ({i} of {x})'.format(i=i,x=len(mods)), end='\r')
- download_obj = requests.get(mod[2], stream=True)
- with open(mod_path, "wb") as write_file:
- shutil.copyfileobj(download_obj.raw, write_file)
- print("Done!" + " " * 8)
-
- print()
- print("Removing old mods...")
- for jar in os.listdir(args.pack_location):
- if jar not in names and os.path.splitext(jar)[1] == ".jar":
- os.remove(os.path.join(args.pack_location, jar))
- print("Removing '{jar}'".format(jar=jar))
-
- print()
- print("Finished installing mods!")
-
-
-# Using the latest urls, update downloads.txt to match and have the correct sha1
-def apply_updates(args):
- if args.game_version is not None:
- version = tuple(int(x) for x in args.game_version.split('.'))
- else:
- version = (2, 0, 0)
- print("Populating version File...")
- mods = read_file(args.filename)
- print("Getting new versions of all mods...")
- ffx = firefox()
- with open(args.version_file, 'w') as f:
- f.write('# Format: <jarname> <hex digested sha1> <direct download url>\n')
- f.write("#VERSION " + str(VERSION + 1) + "\n")
- for mod in mods:
- print("Fetching {mod[0]}...".format(mod=mod))
- if 'curseforge' in mod[1]:
- url = find_cdn(ffx, mod[1], version)
- else:
- url = requests.get(mod[1]).url
- if url is None:
- print('[!]Failed to fetch {mod[0]}!'.format(mod=mod))
- continue
- resp = requests.get(url)
- hsh = hashlib.sha1(resp.content).hexdigest()
- f.write('{mod[0]} {hsh} {resp.url}\n'.format(mod=mod, hsh=hsh, resp=resp))
- ffx.close()
- print()
- print("Done!")
- print("Updates applied to {args.version_file}".format(args=args))
- print("New pack version is " + str(VERSION + 1))
- print("[!] No mods were installed. To update your mods folder, run 'update.py install'")
-
-# Find if any updates are available
-def check_updates(args):
- if args.game_version is not None:
- version = tuple(int(x) for x in args.game_version.split('.'))
- else:
- version = (2, 0, 0)
- print("Checking for updates to version " + str(VERSION) + "...")
- latest = read_file(args.filename)
- old = read_file(args.version_file)
- old_urls = [mod[2] for mod in old]
- num_updates = 0
-
- print("Checking updates...")
- ffx = firefox()
-
- for mod in latest:
- print("Checking for updates to {mod[0]}...".format(mod=mod), end="")
- sys.stdout.flush() # takes care of line-buffered terminals
- if 'curseforge' in mod[1]:
- url = find_cdn(ffx, mod[1], version)
- else:
- url = requests.get(mod[1]).url
- if url in old_urls:
- print(" No updates")
- else:
- print(" Found update: " + url.split('/')[-1])
- num_updates += 1
- ffx.close()
-
- print("Finished checking for updates. {num} mods can be updated".format(num=num_updates))
- if num_updates >= 0:
- print("Run 'python update.py apply_updates' to create a new version with these updates applied.")
-
-
-def find_cdn(ffx, url, version):
- """
- Given a mod home URL, finds the most up-to-date mod version compatible with the given game version.
- Returns the direct Forge CDN download URL
- """
- #TODO filter mods by forge/fabric compatibility
- try:
- ffx.get(url + '/files/all')
- mod_versions = ffx.find_elements_by_class_name("listing")[0].find_elements_by_xpath("tbody/tr") # extract the table of files from the page
- row_info = collections.namedtuple("row_info", ["type", "filename", "cdn_id", "game_version"]) # create a custom tuple because data
- rows = []
- for version_entry in mod_versions:
- # parse out the four fields that we use
- entry_cells = version_entry.find_elements_by_tag_name("td")
- release_type = entry_cells[0].text
- filename = urllib.parse.quote(entry_cells[1].find_elements_by_tag_name("a")[0].text)
- try:
- game_version = tuple([int(x) for x in entry_cells[4].find_element_by_class_name("mr-2").text.split(".")]) # get game version and convert to tuple
- except:
- game_version = (0, 0, 0)
- cdn_id = entry_cells[1].find_element_by_tag_name("a").get_property("href").split("/")[-1]
- rows.append(row_info(release_type, filename, cdn_id, game_version))
- rows.sort(key=lambda x: x.game_version, reverse=True)
- best_row = next(x for x in rows if x.game_version <= version)
-
- return f'https://media.forgecdn.net/files/{best_row.cdn_id[:4]}/{best_row.cdn_id[4:]}/{best_row.filename}'
-
- except:
- import traceback; traceback.print_exc()
- return None
-
-
-def firefox():
- print("Starting Selenium...")
- try:
- from selenium.webdriver import Firefox
- except:
- print("Applying updates requires the `selenium` package")
- sys.exit(0)
- return Firefox()
-
-COMMAND_MAP = {
- 'install': install,
- 'apply_updates': apply_updates,
- 'check_updates': check_updates,
-}
-
-if __name__ == "__main__":
- args = parser.parse_args()
-
- if not args.pack_location:
- # initialize from config
- with open("pack-location.txt", "r") as f:
- args.pack_location = f.read().strip()
-
- if not os.path.exists(args.version_file):
- print("Error: version file\"" + args.version_file + "\" does not exist.")
- parser.print_help()
- sys.exit(1)
- if not os.path.exists(args.pack_location):
- print("Error: mod folder \"" + args.pack_location + "\" does not exist.")
- parser.print_help()
- sys.exit(1)
- elif not os.path.isdir(args.pack_location):
- print("Error: mod folder \"" + args.pack_location + "\" is not actually a folder.")
- parser.print_help()
- sys.exit(1)
- if not os.path.exists(args.whitelist_file):
- print("Error: whitelist file \"" + args.whitelist_file + "\" does not exist.")
- sys.exit(1)
-
- if not (args.command in COMMAND_MAP):
- print("Error: command \"" + args.command + "\" does not exist")
- parser.print_help()
- sys.exit(1)
-
- # fetch version
- with open(args.version_file) as f:
- for line in f:
- if line.strip().split()[0] == "#VERSION":
- VERSION = int(line.strip().split()[1])
- break
- # run the command
- COMMAND_MAP[args.command](args)
diff --git a/util.py b/util.py
new file mode 100644
index 0000000..3343434
--- /dev/null
+++ b/util.py
@@ -0,0 +1,309 @@
+#!/usr/bin/env python3
+import os
+import sys
+import hashlib
+import shutil
+import re
+import collections
+import urllib.parse
+import multiprocessing
+import pathlib
+import base64
+from configparser import RawConfigParser
+
+import requests
+
+
+def load_config():
+ """
+ Load configuarion from pack and local configuration files
+ Fill in reasonable defaults where applicable.
+ """
+ config_p = RawConfigParser()
+ config_p.read(["pack.ini", "local-config.ini"])
+ config = config_p._sections
+ config["pack"]["sanitized_name"] = sanitize_text(config["pack"]["name"])
+
+ if "location" not in config["pack"]:
+ config['pack']['location'] = os.path.join(find_minecraft_directory(), config['pack']['sanitized_name'])
+
+ if "whitelist" not in config["pack"]:
+ config["pack"]["whitelist"] = []
+ else:
+ config["pack"]["whitelist"] = config["pack"]["whitelist"].split(",")
+
+ if "blacklist" not in config["pack"]:
+ config["pack"]["blacklist"] = []
+ else:
+ config["pack"]["blacklist"] = config["pack"]["blacklist"].split(",")
+
+ config["pack"]["game_version"] = game_version_from_string(config["pack"]["game_version"])
+
+ # return the whole config file, pack configuration and modlist
+ return config
+
+
+def update_self():
+ """
+ Try to download new versions of all of the pack configuration and data files
+ in order to update to the latest version. Will overwrite any existing pack.ini and other
+ config files, so be careful!
+ """
+ global config
+
+ base_url = config["pack"]["pack_base_url"].strip("/") + "/"
+ download_text_file(base_url + "pack.ini?inline=false", "pack.ini")
+ download_text_file(base_url + "pack-lock.ini?inline=false", "pack-lock.ini")
+ download_file(base_url + "icon.png?inline=false", "icon.png")
+
+ pack_lock = RawConfigParser()
+ pack_lock.read(["pack-lock.ini"])
+ for path in pack_lock["global"]["config_files"].split(","):
+ if not path:
+ continue
+ download_text_file(f"{base_url}config/{path}", os.path.join("config", path))
+
+ config = load_config()
+
+
+def find_minecraft_directory():
+ """
+ Find the location of the user's .minecraft folder based on
+ their operating system.
+ :returns: the absolute path to the .minecraft directory
+ """
+ if sys.platform == "linux":
+ return os.path.join(os.path.expanduser('~'), ".minecraft")
+ elif sys.platform == "win32":
+ return os.path.join(os.environ["APPDATA"], ".minecraft")
+ elif sys.platform == "darwin":
+ return os.path.join(os.path.expanduser('~'), "Library", "Application Support", "minecraft")
+ else:
+ raise RuntimeError(f"Unsupported operating system `{sys.platform}`. Please define a location for the pack in your `local-config.ini` file")
+
+
+def find_jre():
+ """
+ Find a usable install of Java, either from a user-installed JRE or
+ from the Minecraft Launcher's integrated JRE.
+
+ :return: the absolute path of a working Java executable
+ """
+ if shutil.which("java") is not None:
+ return shutil.which("java")
+ if sys.platform == 'win32': # We can try and use the Minecraft Launcher's integrated JRE on Windows
+ if os.path.exists("C:\\Program Files (x86)\\Minecraft Launcher\\runtime\\jre-x64\\java.exe"):
+ return "C:\\Program Files (x86)\\Minecraft Launcher\\runtime\\jre-x64\\java.exe"
+ raise RuntimeError("Unable to detect an installed JRE. Please install Java in order to use modpackman.")
+
+
+def download_file(url, destination):
+ """
+ Given a url, performs a requests request to get the remote object
+ and write it to destination.
+ Note that this only works on binary files.
+ """
+ with open(destination, "wb") as f:
+ with requests.get(url, stream=True) as dl:
+ shutil.copyfileobj(dl.raw, f)
+
+def download_text_file(url, destination):
+ """
+ Given the URL to a text file, download it to the file named
+ by `destination`. Note that this only works for text files, not binary files.
+ """
+ with open(destination, "w") as f:
+ f.write(requests.get(url).text)
+
+
+# take a string and only keep filename-friendly parts
+def sanitize_text(text):
+ sanitized = ""
+ replacement_map = {" ": "-"}
+ for char in text:
+ if char.isalnum():
+ sanitized += char.lower()
+ elif char in replacement_map:
+ sanitized += replacement_map[char]
+ return sanitized
+
+
+def generate_base64_icon(filename):
+ with open(filename, "rb") as f:
+ return "data:image/png;base64," + base64.b64encode(f.read()).decode("utf8")
+
+
+def read_file(fil):
+ """
+ Given a filename, read its contents in as a list of tuples.
+ This function strips out comment lines and whitespaces.
+ """
+ strings = []
+ with open(fil) as f:
+ for line in f:
+ string = line.strip().split()
+ if len(line) > 1 and line[0] != '#':
+ # run strip on each element
+ string = tuple(map(lambda x: x.strip(), string))
+ strings.append(string)
+
+ return strings
+
+
+def game_version_from_string(string):
+ if string is not None:
+ try:
+ return tuple(int(x) for x in string.split('.'))
+ except:
+ pass
+ return (2, 0, 0)
+
+
+
+def threaded_find_url(homepage_url, game_version):
+ """
+ Helper function that finds a single mod URL based on the homepage.
+ """
+ if 'curseforge' in homepage_url:
+ ffx = firefox()
+ final_url = find_cdn(ffx, homepage_url, game_version)
+ ffx.close()
+ else:
+ final_url = requests.get(homepage_url).url
+ return final_url
+
+
+def find_updated_urls(forge_urls, game_version, threads=20):
+ """
+ Given a list of mod homepage URLs, find all of their direct download links in parallel.
+ """
+
+ # First, check that we can successfully open a Firefox instance in the main thread.
+ # This provides us with a much nicer error message and quicker feedback.
+ f = firefox()
+ f.close()
+
+ with multiprocessing.Pool(threads) as pool:
+ # No progress indicator possible
+ # return pool.map(threaded_find_url, forge_urls)
+
+ # Much longer, but allows us to do a nice progress indicator
+ result_futures = []
+ for url in forge_urls:
+ result_futures.append(pool.apply_async(threaded_find_url, (url, game_version)))
+
+ results = []
+ for i,f in enumerate(result_futures):
+ results.append(f.get())
+ print(f'\r{i+1}/{len(result_futures)} URLs updated ({round((i+1)/len(result_futures)*100)}%)', end='')
+ print()
+
+ return results
+
+
+def threaded_calc_sha1(direct_url):
+ """
+ Helper function that downloads and calculates a single SHA1 hash from a direct download URL.
+ """
+ resp = requests.get(direct_url)
+ hsh = hashlib.sha1(resp.content).hexdigest()
+ return hsh
+
+
+def find_checksums(direct_urls, threads=8):
+ """
+ Given a list of direct download URLs, download them all and calculate the SHA1 checksum of the file at that location.
+ """
+
+ with multiprocessing.Pool(threads) as pool:
+ # Much longer, but allows us to do a nice progress indicator
+ result_futures = []
+ for url in direct_urls:
+ result_futures.append(pool.apply_async(threaded_calc_sha1, (url,)))
+
+ results = []
+ for i,f in enumerate(result_futures):
+ results.append(f.get())
+ print(f'\r{i+1}/{len(result_futures)} checksums calculated ({round((i+1)/len(result_futures)*100)}%)', end='')
+ print()
+
+ return results
+
+
+def find_cdn(ffx, url, version):
+ """
+ Given a mod home URL, finds the most up-to-date mod version compatible with the given game version.
+ Returns the direct Forge CDN download URL
+ """
+ try:
+ # This goes to the "all files" page, where we get a table view of all
+ page_index = 0;
+ while True:
+ ffx.get(url + f'/files/all?page={page_index}')
+ mod_versions = ffx.find_elements_by_class_name("listing")[0].find_elements_by_xpath("tbody/tr") # extract the table of files from the page
+ row_info = collections.namedtuple("row_info", ["type", "filename", "cdn_id", "game_version"]) # create a custom tuple because data
+ rows = []
+ for version_entry in mod_versions:
+ # parse out the four fields that we use
+ entry_cells = version_entry.find_elements_by_tag_name("td")
+ release_type = entry_cells[0].text
+ # Note that this is NOT the final filename - this is just the "release name".
+ filename = urllib.parse.quote(entry_cells[1].find_elements_by_tag_name("a")[0].text)
+ try:
+ game_version = tuple([int(x) for x in entry_cells[4].find_element_by_class_name("mr-2").text.split(".")]) # get game version and convert to tuple
+ except:
+ game_version = (0, 0, 0)
+ cdn_id = entry_cells[1].find_element_by_tag_name("a").get_property("href").split("/")[-1]
+
+ #TODO make this configurable
+ if 'fabric' not in filename.lower() or 'forge' in filename.lower():
+ rows.append(row_info(release_type, filename, cdn_id, game_version))
+ rows.sort(key=lambda x: x.game_version, reverse=True)
+ try:
+ best_row = next(x for x in rows if x.game_version <= version)
+ break
+ except StopIteration:
+ if len(ffx.find_elements_by_class_name("pagination-next--inactive")) != 0:
+ raise
+ page_index += 1
+
+
+ # We need to find the real, ForgeCDN compatible filename now by going to the file page.
+ ffx.get(f'{url}/files/{best_row.cdn_id}')
+ # This will probably break in the future
+ filename = ffx.find_elements_by_xpath("html/body/div/main/div/div/section/div/div/div/section/section/article/div/div/span")[1].text
+ # URL escape the filename!
+ filename = urllib.parse.quote(filename)
+
+ # ForgeCDN requires that the leading zeroes are stripped from each portion of the CDN ID, hence the int() cast.
+ return f'https://media.forgecdn.net/files/{int(best_row.cdn_id[:4])}/{int(best_row.cdn_id[4:])}/{filename}'
+
+ except:
+ # import traceback; traceback.print_exc()
+ print(f"[!] Failed to retrieve valid CDN URL for {url}")
+ return None
+
+
+def firefox():
+ """
+ Start a headless Firefox instance and return the Selenium refrence to it.
+ """
+ try:
+ from selenium.webdriver import Firefox
+ from selenium.webdriver.firefox.options import Options
+ except:
+ print("Applying updates requires the `selenium` package")
+ exit(0)
+ options = Options()
+ options.add_argument('-headless')
+ options.add_argument('--window-size 1920,1080')
+
+ # for ~~cursed~~ windows people, put geckodriver in the folder next to modpackman.py
+ if(os.path.exists("../../geckodriver.exe")):
+ return Firefox(executable_path='../../geckodriver', options=options)
+ return Firefox(options=options)
+
+
+# Configuration is automatically loaded from pack.ini and local-config.ini,
+# and made accessible here as a global
+config = load_config()
diff --git a/whitelist.txt b/whitelist.txt
deleted file mode 100644
index 76c8d1f..0000000
--- a/whitelist.txt
+++ /dev/null
@@ -1 +0,0 @@
-# file names in here are NOT removed from mods folder during 'install'