import sys, json, subprocess, configparser, shutil, os, platform, urllib.request from pathlib import Path # User Variables MAXRAM = "8G" MINRAM = "4G" JVMARGS = "" REPO_URL = "" BRANCH = "main" InstanceConfig = configparser.ConfigParser() if Path("../instance.cfg").is_file(): # Launched by Prism/MultiMC - read the instance's own metadata InstanceConfig.read("../instance.cfg") with open("../mmc-pack.json", "r") as file: Components = json.load(file)["components"] # ponytail: match on uid, not list index - component order isn't guaranteed Minecraft = next(c for c in Components if c["uid"] == "net.minecraft") Modloader = next(c for c in Components if c["uid"] != "net.minecraft" and not c.get("dependencyOnly")) MinecraftVersion = Minecraft["version"] ModloaderName = Modloader["cachedName"] ModloaderVersion = Modloader["version"] elif Path("env.txt").is_file(): # Standalone - same INI format, plus the versions mmc-pack.json would have given us InstanceConfig.read("env.txt") MinecraftVersion = InstanceConfig['General']['MinecraftVersion'] ModloaderName = InstanceConfig['General']['ModloaderName'] ModloaderVersion = InstanceConfig['General']['ModloaderVersion'] else: sys.exit("* Found neither ../instance.cfg nor env.txt - cannot identify this instance.") PackName = InstanceConfig['General']['name'] # Program Variables DO NOT TOUCH MINGIT_URL = "https://github.com/git-for-windows/git/releases/download/v2.45.2.windows.1/MinGit-2.45.2-64-bit.zip" MINGIT = Path(".mingit/cmd/git.exe") IS_NEOFORGE = ModloaderName == "NeoForge" SERVER_JAR = Path("server-installer.jar") # ponytail: version-stamped path - a modloader bump makes this vanish and the install re-runs for free ARGS_FILE = Path("libraries/net/neoforged/neoforge", ModloaderVersion, "win_args.txt" if os.name == "nt" else "unix_args.txt") SERVER_JAR_URL = ( f"https://maven.neoforged.net/releases/net/neoforged/neoforge/{ModloaderVersion}/neoforge-{ModloaderVersion}-installer.jar" if IS_NEOFORGE else f"https://meta.fabricmc.net/v2/versions/loader/{MinecraftVersion}/{ModloaderVersion}/1.0.1/server/jar" ) JAVA_DIR = Path(".Java21") JAVA = JAVA_DIR / "bin" / ("java.exe" if os.name == "nt" else "java") # ponytail: Adoptium takes os/arch as path segments - build them instead of hardcoding windows/x64 JAVA_OS = {"Windows": "windows", "Linux": "linux"}[platform.system()] JAVA_ARCH = {"AMD64": "x64", "x86_64": "x64"}[platform.machine()] JAVA_URL = f"https://api.adoptium.net/v3/binary/latest/21/ga/{JAVA_OS}/{JAVA_ARCH}/jre/hotspot/normal/eclipse" JAVA_ARCHIVE = "java.zip" if os.name == "nt" else "java.tar.gz" print("****************************************") print(f"* Launching {PackName} Server") print(f"* {MinecraftVersion} / {ModloaderName} {ModloaderVersion}") # ponytail: check the local MinGit before downloading - which() never sees it, it's not on PATH GIT = shutil.which("git") or (str(MINGIT.resolve()) if MINGIT.is_file() else None) if GIT is None: if os.name != "nt": sys.exit("* Git not found, please install Git!") print("* Git not found, downloading and installing Git.") urllib.request.urlretrieve(MINGIT_URL, "MinGit.zip") shutil.unpack_archive("MinGit.zip", ".mingit") Path("MinGit.zip").unlink() GIT = str(MINGIT.resolve()) def git(*args): with open("launchServer.log", "a") as log: subprocess.run([GIT, *args], stdout=log, stderr=subprocess.STDOUT, check=True) def AcceptEula(): eula = Path("eula.txt") # ponytail: substring check, not a properties parse - the file has one setting if eula.is_file() and "eula=true" in eula.read_text(): return eula.write_text("eula=true\n") if Path(".git").is_dir(): print("* Updating Modpack Server from Gitlab") print("* * Fetching changes from upstream") git("fetch", "origin") print("* * Resetting local to match upstream") git("reset", "--hard", f"origin/{BRANCH}") print("* * Clearing stale untracked pack files") git("clean", "-fd") print("* Server Update Complete!") else: print("* Installing Modpack Server from Gitlab") print("* * Cloning repo into temp folder") tmp = Path("repo.tmp") git("clone", "--branch", BRANCH, REPO_URL, str(tmp)) print("* * Copying repo out of temp and into current folder") shutil.copytree(tmp, ".", dirs_exist_ok=True) # ponytail: merges like robocopy /E; move refuses existing dirs shutil.rmtree(tmp) print("* Server Installation Complete!") print("****************************************") print("* Setting up server runtime") if not JAVA.is_file(): print("* * Downloading Java 21") urllib.request.urlretrieve(JAVA_URL, JAVA_ARCHIVE) print("* * Extracting Java 21") shutil.unpack_archive(JAVA_ARCHIVE) # ponytail: replaces `tar -xf`, picks zip vs tar.gz by extension Path(JAVA_ARCHIVE).unlink() shutil.move(str(next(Path(".").glob("jdk-*"))), JAVA_DIR) if IS_NEOFORGE: if not ARGS_FILE.is_file(): print(f"* * Downloading {ModloaderName} installer") urllib.request.urlretrieve(SERVER_JAR_URL, SERVER_JAR) print(f"* * Installing {ModloaderName} server") subprocess.run([str(JAVA), "-jar", str(SERVER_JAR), "--installServer", "."], check=True) # NeoForge ships an @argfile instead of a runnable jar LoaderArgs = [f"@{ARGS_FILE}"] else: if not SERVER_JAR.is_file(): print(f"* * Downloading {ModloaderName} server jar") urllib.request.urlretrieve(SERVER_JAR_URL, SERVER_JAR) LoaderArgs = ["-jar", str(SERVER_JAR)] print("* Server runtime ready!") ModsList = Path("NonServerFriendlyMods.txt") if ModsList.is_file(): print("****************************************") print("* Removing mods not friendly for servers") for line in ModsList.read_text().splitlines(): if line.strip(): Path("mods", line.strip()).unlink(missing_ok=True) print("* Removed problematic mods") print("****************************************") print("* Starting Modpack Server") AcceptEula() subprocess.run([str(JAVA), f"-Xms{MINRAM}", f"-Xmx{MAXRAM}", *JVMARGS.split(), *LoaderArgs, "nogui"]) input("Press Enter to exit...")