diff --git a/NonServerFriendlyMods.txt b/NonServerFriendlyMods.txt new file mode 100644 index 0000000..aff678e --- /dev/null +++ b/NonServerFriendlyMods.txt @@ -0,0 +1 @@ +DistantHorizons-2.1.2-a-1.20.1-forge-fabric.jar diff --git a/env.txt b/env.txt new file mode 100644 index 0000000..17fdd98 --- /dev/null +++ b/env.txt @@ -0,0 +1,19 @@ +# Standalone config. Only read when ../instance.cfg is missing (i.e. not launched +# by Prism/MultiMC). Delete this file on a Prism-managed instance - the launcher's +# own instance.cfg and mmc-pack.json take priority and this is ignored. +[General] + +# Shown in the launch banner. Any string. +name=My Modpack + +# Minecraft version. Used to build the Fabric server jar URL. +MinecraftVersion=1.21.1 + +# Must be exactly "NeoForge" to get the NeoForge install path. +# Anything else falls through to the Fabric branch. +ModloaderName=NeoForge + +# Modloader version, NOT the Minecraft version. This builds the NeoForge Maven URL +# and the libraries/net/neoforged/neoforge// argfile path - a wrong value +# here means a 404 on download, or a "could not find main class" at startup. +ModloaderVersion=21.1.209 \ No newline at end of file diff --git a/launchClient.py b/launchClient.py new file mode 100644 index 0000000..f03905d --- /dev/null +++ b/launchClient.py @@ -0,0 +1,86 @@ +import sys, json, subprocess, configparser, shutil, os, urllib.request +from pathlib import Path + +# User Variables +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") + +print("****************************************") +print(f"* Launching {PackName}") +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("launchClient.log", "a") as log: + subprocess.run([GIT, *args], stdout=log, stderr=subprocess.STDOUT, check=True) + +if Path(".git").is_dir(): + print("* Detected git repo. Updating Pack!") + + print("* * Fetching changes from upstream") + git("fetch", "origin") + + # ponytail: reset straight to the remote - pull-down-only means local history never matters, + # and this can't produce a merge conflict the way fetch + merge could + print("* * Resetting local to match upstream") + git("reset", "--hard", f"origin/{BRANCH}") + + # ponytail: clears leftovers that would block the reset. Respects .gitignore - no -x flag, + # so anything the game generates stays put as long as the repo ignores it + print("* * Clearing stale untracked pack files") + git("clean", "-fd") + + print("* Pack Update Complete!") +else: + print("* No .git folder was found. Installing Pack!") + + 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("* Pack Installation Complete!") + +print("****************************************") \ No newline at end of file diff --git a/launchServer.py b/launchServer.py new file mode 100644 index 0000000..cb14ec0 --- /dev/null +++ b/launchServer.py @@ -0,0 +1,152 @@ +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...") \ No newline at end of file