├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── changelog-next ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── eu └── pb4 ├── forgefx ├── ByteSwappingFix.java ├── ForgeInstallerFix.java └── S.java └── mrpackserver ├── Create.java ├── Main.java ├── format ├── FabricInstallerVersion.java ├── InstanceInfo.java ├── ModpackIndex.java ├── ModpackInfo.java ├── ModrinthModpackVersion.java ├── ModrinthProjectData.java ├── ModrinthSearchData.java ├── VanillaVersionData.java └── VanillaVersionList.java ├── installer ├── FabricInstallerLookup.java ├── FileDownloader.java ├── ForgeInstallerLookup.java ├── ForgeStarterLookup.java ├── ModrinthModpackLookup.java ├── MrPackInstaller.java ├── NeoForgeInstallerLookup.java ├── QuiltInstallerLookup.java └── VanillaInstallerLookup.java ├── launch ├── ExitError.java ├── FileSystemProviderHijacker.java ├── Launcher.java └── LegacyExitBlocker.java └── util ├── Constants.java ├── FlexVerComparator.java ├── HashData.java ├── InstallerGui.java ├── InstrumentationCatcher.java ├── JavaVersion.java ├── Logger.java └── Utils.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: Patbox 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: 8 | pull_request: 9 | push: 10 | paths-ignore: [ '.github/', 'docs/', 'mkdocs.yml' ] 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | # Use these Java versions 17 | java: [ 18 | 21 # Latest version 19 | ] 20 | # and run on both Linux and Windows 21 | os: [ubuntu-latest] 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: checkout repository 25 | uses: actions/checkout@v2 26 | - name: setup jdk ${{ matrix.java }} 27 | uses: actions/setup-java@v1 28 | with: 29 | java-version: ${{ matrix.java }} 30 | - name: make gradle wrapper executable 31 | if: ${{ runner.os != 'Windows' }} 32 | run: chmod +x ./gradlew 33 | - name: build 34 | run: ./gradlew publish 35 | - name: capture build artifacts 36 | if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from LTS java on one OS 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: Artifacts 40 | path: build/libs/ -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: write-all 13 | steps: 14 | - uses: actions/cache@v2 15 | with: 16 | path: | 17 | ~/.gradle/loom-cache 18 | ~/.gradle/caches 19 | ~/.gradle/wrapper 20 | key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 21 | restore-keys: | 22 | gradle- 23 | - uses: actions/checkout@v2 24 | - name: Set up JDK 25 | uses: actions/setup-java@v1 26 | with: 27 | java-version: 21 28 | 29 | - name: Grant execute permission for gradlew 30 | run: chmod +x gradlew 31 | - name: Build and publish with Gradle 32 | run: ./gradlew publish 33 | env: 34 | MAVEN_URL: ${{ secrets.MAVEN_URL }} 35 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 36 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 37 | CURSEFORGE: ${{ secrets.CURSEFORGE }} 38 | MODRINTH: ${{ secrets.MODRINTH }} 39 | CHANGELOG: ${{ github.event.release.body }} 40 | - name: Upload GitHub release 41 | uses: AButler/upload-release-assets@v3.0 42 | with: 43 | files: 'build/libs/*.jar;!build/libs/*-sources.jar;!build/libs/*-dev.jar' 44 | repo-token: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Patbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mrpack4server 2 | mrpack4server is a "server launcher" that allows you to easily install and run any modpack from Modrinth 3 | (or one that's exported in `.mrpack` format) as a Minecraft Server on your local machine or any hosting provider that supports custom jars. 4 | This tool doesn't require any additional arguments and can work as any other server jar (like vanilla provided one). 5 | 6 | ## Features: 7 | - For any users, usable standalone, for setup of any modpack by defining a single file. 8 | - For modpack makers, allowing quick server setup by having to just download and run a single file. 9 | - Automatically downloads required mrpack files and any mods / external assets defined by modpack. 10 | - Automatically downloads and starts the server/modloader files, without requiring renaming of jars, supporting Fabric Loader, 11 | Quilt Loader, Forge and NeoForge. 12 | If used with modpack for other unsupported platforms, it will still install everything, but won't be able to launch. 13 | 14 | ## Usage: 15 | The file is run just like any other Minecraft server (`java -jar mrpack4server.jar`) and will use / pass 16 | through any arguments given to it. When used on its own, it first looks in 3 places for modpack definition: 17 | - `modpack-info.json` included within jar itself, useful for modpack makers. See below for definition, 18 | - `modpack-info.json` in server's root directory (alongside jar), defined for general user setups, 19 | - `local.mrpack` in server's root directory (alongside jar), making it directly use provided mrpack file instead of 20 | pulling one from Modrinth. 21 | 22 | If neither of these is found, it will ask you to either provide link, project id or name of the modpack you want to install 23 | and then the version of it (suggesting latest one), which will be used to create local `modpack-info.json` file. 24 | You can then either update target version by editing it or by removing the file and rerunning the initial setup. 25 | 26 | Default/main jar only supports Java 21 (`mrpack4server-X.Y.Z.jar`). If you want to run it on older Java version you can use: 27 | - `mrpack4server-X.Y.Z-jvm8.jar` for Java 8 and above, 28 | - `mrpack4server-X.Y.Z-jvm16.jar` for Java 16 and above, 29 | - `mrpack4server-X.Y.Z-jvm17.jar` for Java 17 and above. 30 | 31 | These versions however don't override requirements of Minecraft/Loader itself. 32 | For example, you still need Java to use Java 17 or above to run Minecraft 1.20.1 and Java 8 (but not newer!) to run Forge 1.16.5. 33 | 34 | You can get download preconfigured jar by downloading it from `https://mrpack4server.pb4.eu/download////server.jar` url, 35 | where you replace `` with project id or slug, `` for modpack version, `` for java base used. 36 | For example `https://mrpack4server.pb4.eu/download/polymania/0.2.1/jvm21/server.jar` will download launcher for Polymania 0.2.1, using Java 21 variant 37 | of the mrpack4server. Currently supported variants are `jvm21`, `jvm17`, `jvm16` and `jvm8`. 38 | 39 | You can also create bundled variant by hand with some zip software or by running `java -cp mrpack4server.jar eu.pb4.mrpackserver.Create`. 40 | By default, without any arguments, it will copy currently provided `modpack-info.json` file, but you can also set it with arguments (`--arg value`), 41 | where it mirrors all arguments from `modpack-info.json` (aka `--project_id my_modpack --version_id 1.2.3` will create jar with these defined). 42 | Additionally, you can use the `--out` argument to set output file path, by default being set to `--out server.jar`. 43 | 44 | ### `modpack-info.json` format: 45 | `modpack-info.json` is a regular json file without support for comments. Ones provided below are purely 46 | to make easier to describe things. 47 | ```json5 48 | { 49 | // (Optional) Display name, used to display as information while starting / download files. 50 | "display_name": "My Modpack", 51 | // (Optional) Display version, used to display as information while starting / download files. 52 | "display_version": "1.0.0 for 1.21.1", 53 | // Project id used on Modrinth and locally, identifying modpack as unique. Can use slug or ID 54 | "project_id": "my_modpack_by_patbox", 55 | // Version id used on Modrinth and locally, identifying used version. Can be a version number, version id or prefixed version type. 56 | // As version type, you can set it to ";;release", ";;beta" or ";;alpha", making it download latest version with highest 57 | // version number! For most use cases, I would recommend not using this functionality, unless you are 100% modpack's version is consistent 58 | // and non-hard breaking. For stability, you should use version numbers directly. 59 | "version_id": "1.0.0", 60 | // (Optional) Overrides url used to download the modpack, can download it from anywhere. 61 | "url": "https://files.pb4.eu/modpacks/my_modpack.mrpack", 62 | // (Optional) Size of the file downloaded from "url", in bytes. It's not required even with "url" used. 63 | "size": 1000, 64 | // (Optional) Value of sha512 hash for file downloaded from "url", used for validation. It's not required even with "url" used. 65 | "sha512": 1000, 66 | // (Optional) Additional list of whitelisted domains, only useful for modpacks hosted outside Modrinth. 67 | "whitelisted_domains": [ 68 | "files.pb4.eu" // Note it's just a domain, no protocol/ports/paths. 69 | ], 70 | // (Optional) Additional list of paths that can't be overwritten if they already exist. 71 | "non_overwritable_paths": [ 72 | "global-player-data" // Note it's just path, always relative to server root. 73 | ], 74 | // (Optional) Allows to url used for requesting list of available versions, used by auto-updating feature (the ;; versions). 75 | "version_list_url": "https://api.modrinth.com/v2/project/{PROJECT_ID}/versions", 76 | // (Optional) Allows to skip java version check. By default is set to false 77 | "skip_java_version_check": false 78 | } 79 | ``` 80 | 81 | Examples: 82 | - Installing Adrenaline version 1.25.0+1.21.1.fabric. 83 | ```json 84 | { 85 | "project_id": "adrenaline", 86 | "version_id": "1.25.0+1.21.1.fabric" 87 | } 88 | ``` 89 | - Installing Cobblemon Official Modpack v1.5.2 (using id's copied from website). 90 | ```json 91 | { 92 | "project_id": "5FFgwNNP", 93 | "version_id": "bpaivauC" 94 | } 95 | ``` 96 | 97 | ## Securing folders/files from overriding. 98 | If you use mrpack4server 0.5.0 or newer, you can now create a `lockedpaths.txt` file in your main server directory. 99 | It will then be used to select which paths are blocked from getting updates/being overwritten. 100 | 101 | You can either provide a folder (for example `mods/luckperms`) or direct file (for example `config/styledchat.json`). 102 | Every new entry should be provided on a new line. 103 | 104 | By default, these paths are secured: 105 | - `world` folder 106 | - `server.properties` 107 | - `whitelist.json` 108 | - `banned-ips.json` 109 | - `banned-players.json` 110 | - `ops.json` 111 | - `.mrpack4server` folder 112 | - `lockedpaths.txt` -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import kotlin.jvm.functions.Function1 2 | 3 | plugins { 4 | id 'com.gradleup.shadow' version '8.3.5' 5 | id 'xyz.wagyourtail.jvmdowngrader' version '1.2.2' 6 | id 'java' 7 | id 'maven-publish' 8 | } 9 | 10 | group = project.maven_group 11 | version = project.version 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | implementation shadow('com.google.code.gson:gson:2.2.4') 19 | compileOnly 'org.jetbrains:annotations:24.1.0' 20 | } 21 | 22 | shadowJar { 23 | minimize() 24 | } 25 | 26 | shadowJar { 27 | manifest { 28 | attributes 'Implementation-Title': 'mrpack4server', 29 | 'Implementation-Version': project.version, 30 | 'Implementation-Vendor': 'pb4.eu', 31 | 'Main-Class': 'eu.pb4.mrpackserver.Main', 32 | 'Launcher-Agent-Class': 'eu.pb4.mrpackserver.util.InstrumentationCatcher', 33 | 'Agent-Class': 'eu.pb4.mrpackserver.util.InstrumentationCatcher', 34 | 'Premain-Class': 'eu.pb4.mrpackserver.util.InstrumentationCatcher' 35 | } 36 | relocate 'com.google.gson', 'eu.pb4.mrpackserver.lib.gson' 37 | minimize() 38 | archiveClassifier.set(null) 39 | } 40 | 41 | task downgradeJar8(type: xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar) { 42 | inputFile = tasks.shadowJar.archiveFile 43 | downgradeTo = JavaVersion.VERSION_1_8 // default 44 | archiveClassifier.set("jvm8-dev") 45 | } 46 | 47 | task shadeDowngradedApi8(type: xyz.wagyourtail.jvmdg.gradle.task.ShadeJar) { 48 | inputFile = downgradeJar8.archiveFile 49 | archiveClassifier.set("jvm8") 50 | } 51 | 52 | task downgradeJar16(type: xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar) { 53 | inputFile = tasks.shadowJar.archiveFile 54 | downgradeTo = JavaVersion.VERSION_16 // default 55 | archiveClassifier.set("jvm16-dev") 56 | } 57 | 58 | task shadeDowngradedApi16(type: xyz.wagyourtail.jvmdg.gradle.task.ShadeJar) { 59 | inputFile = downgradeJar16.archiveFile 60 | archiveClassifier.set("jvm16") 61 | } 62 | 63 | task downgradeJar17(type: xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar) { 64 | inputFile = tasks.shadowJar.archiveFile 65 | downgradeTo = JavaVersion.VERSION_17 // default 66 | archiveClassifier.set("jvm17-dev") 67 | } 68 | 69 | task shadeDowngradedApi17(type: xyz.wagyourtail.jvmdg.gradle.task.ShadeJar) { 70 | inputFile = downgradeJar17.archiveFile 71 | archiveClassifier.set("jvm17") 72 | } 73 | 74 | 75 | 76 | var env = System.getenv(); 77 | 78 | // configure the maven publication 79 | publishing { 80 | publications { 81 | mavenJava(MavenPublication) { 82 | // add all the jars that should be included when publishing to maven 83 | artifact(shadowJar) { 84 | builtBy shadowJar 85 | } 86 | artifact(shadeDowngradedApi8) { 87 | builtBy shadeDowngradedApi8 88 | } 89 | 90 | artifact(shadeDowngradedApi16) { 91 | builtBy shadeDowngradedApi16 92 | } 93 | 94 | artifact(shadeDowngradedApi17) { 95 | builtBy shadeDowngradedApi17 96 | } 97 | } 98 | } 99 | 100 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 101 | repositories { 102 | // Add repositories to publish to here. 103 | // Notice: This block does NOT have the same function as the block in the top level. 104 | // The repositories here will be used for publishing your artifact, not for 105 | // retrieving dependencies. 106 | repositories { 107 | if (env.MAVEN_URL) { 108 | maven { 109 | credentials { 110 | username env.MAVEN_USERNAME 111 | password env.MAVEN_PASSWORD 112 | } 113 | url env.MAVEN_URL 114 | } 115 | } else { 116 | mavenLocal() 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /changelog-next: -------------------------------------------------------------------------------- 1 | - Added simple UI visible when running the jar directly on systems with graphics. 2 | - Pretty much just a console window, with input still being text based. 3 | - It will close once Minecraft opens it's own server window. 4 | - Can be disabled by providing `--noGui` or `noGui` option, just like vanilla server. 5 | - Modpack info files can now define non-overwritable files (prevents modification if they already exist). 6 | - You can now add custom non-overridable files/folders as a new-line separated list in `lockedpaths.txt` file in main server directory. 7 | - When running standalone, selecting a project will now validate if it's actually a modpack. 8 | - When providing modpack slug/link, you can now force it to always search by prefixing the input with question mark (for example providing `polymania` will auto-select the Polymania modpack, while `?polymania` will search for the modpacks). 9 | - Removed `mrpack4server` text from log prefix, as it was redundant. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Mod Properties 5 | version = 0.5.0 6 | maven_group = eu.pb4 7 | archives_base_name = mrpack4server 8 | 9 | # Dependencies 10 | # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patbox/mrpack4server/5c7cba9a2c9f329974926b1136a5b168f24ac33a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 08 20:46:14 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | maven { 5 | url = "https://maven.wagyourtail.xyz/releases" 6 | } 7 | maven { 8 | url = "https://maven.wagyourtail.xyz/snapshots" 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | 15 | rootProject.name = 'mrpack4server' -------------------------------------------------------------------------------- /src/main/java/eu/pb4/forgefx/ByteSwappingFix.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.forgefx; 2 | 3 | import java.lang.instrument.ClassFileTransformer; 4 | import java.lang.instrument.IllegalClassFormatException; 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.ProtectionDomain; 7 | import java.util.Arrays; 8 | import java.util.Set; 9 | 10 | public class ByteSwappingFix implements ClassFileTransformer { 11 | private final ClassLoader targetLoader; 12 | private final Pair[] replacements; 13 | private final Set classes; 14 | 15 | public ByteSwappingFix(ClassLoader loader, Set classes, Pair... replacements) { 16 | this.targetLoader = loader; 17 | this.classes = classes; 18 | this.replacements = replacements; 19 | } 20 | 21 | @Override 22 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 23 | if (loader != this.targetLoader || !this.classes.contains(className)) { 24 | return null; 25 | } 26 | 27 | byte[] out = null; 28 | 29 | for (int i = 0; i < classfileBuffer.length; i++) { 30 | for (var rep : this.replacements) { 31 | var found = true; 32 | for (int c = 0; c < rep.from.length; c++) { 33 | if (classfileBuffer[i + c] != rep.from[c]) { 34 | found = false; 35 | break; 36 | } 37 | } 38 | 39 | if (found) { 40 | if (out == null) { 41 | out = Arrays.copyOf(classfileBuffer, classfileBuffer.length); 42 | } 43 | System.arraycopy(rep.to, 0, out, i, rep.to.length); 44 | } 45 | } 46 | } 47 | 48 | return out; 49 | } 50 | 51 | public record Pair(byte[] from, byte[] to) { 52 | public static Pair of(String from, String to) { 53 | return new Pair(from.getBytes(StandardCharsets.UTF_8), to.getBytes(StandardCharsets.UTF_8)); 54 | } 55 | public Pair { 56 | assert from.length == to.length; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/forgefx/ForgeInstallerFix.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.forgefx; 2 | 3 | import java.util.Set; 4 | 5 | public class ForgeInstallerFix extends ByteSwappingFix { 6 | public ForgeInstallerFix(ClassLoader loader) { 7 | super(loader, Set.of("net/minecraftforge/installer/SimpleInstaller"), 8 | Pair.of("java/lang/System", "eu/pb4/forgefx/S") 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/forgefx/S.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.forgefx; 2 | 3 | 4 | import eu.pb4.mrpackserver.launch.ExitError; 5 | 6 | import java.io.Console; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.PrintStream; 10 | import java.nio.channels.Channel; 11 | import java.util.Properties; 12 | 13 | public class S { 14 | public static final InputStream in = System.in; 15 | public static final PrintStream out = System.out; 16 | public static final PrintStream err = System.err; 17 | 18 | public static void setIn(InputStream in) { 19 | } 20 | 21 | public static void setOut(PrintStream out) { 22 | } 23 | 24 | public static void setErr(PrintStream err) { 25 | } 26 | 27 | 28 | public static Console console() { 29 | return System.console(); 30 | } 31 | 32 | 33 | public static Channel inheritedChannel() throws IOException { 34 | return System.inheritedChannel(); 35 | } 36 | @SuppressWarnings("removal") 37 | @Deprecated(forRemoval = true) 38 | public static void setSecurityManager(@SuppressWarnings("removal") SecurityManager sm) { 39 | System.setSecurityManager(sm); 40 | } 41 | 42 | @SuppressWarnings("removal") 43 | @Deprecated(forRemoval = true) 44 | public static SecurityManager getSecurityManager() { 45 | return System.getSecurityManager(); 46 | } 47 | 48 | public static long currentTimeMillis() { 49 | return System.currentTimeMillis(); 50 | } 51 | 52 | public static long nanoTime() { 53 | return System.nanoTime(); 54 | } 55 | 56 | public static void arraycopy(Object src, int srcPos, 57 | Object dest, int destPos, 58 | int length) { 59 | System.arraycopy(src, srcPos, dest, destPos, length); 60 | } 61 | 62 | public static int identityHashCode(Object x) { 63 | return System.identityHashCode(x); 64 | } 65 | 66 | public static Properties getProperties() { 67 | return System.getProperties(); 68 | } 69 | 70 | public static String lineSeparator() { 71 | return System.lineSeparator(); 72 | } 73 | 74 | public static void setProperties(Properties props) { 75 | System.setProperties(props); 76 | } 77 | 78 | public static String getProperty(String key) { 79 | return System.getProperty(key); 80 | } 81 | 82 | public static String getProperty(String key, String def) { 83 | return System.getProperty(key, def); 84 | } 85 | 86 | public static String setProperty(String key, String value) { 87 | return System.setProperty(key, value); 88 | } 89 | 90 | public static String clearProperty(String key) { 91 | return System.clearProperty(key); 92 | } 93 | 94 | public static String getenv(String name) { 95 | return System.getenv(name); 96 | } 97 | 98 | public static java.util.Map getenv() { 99 | return System.getenv(); 100 | } 101 | 102 | //public static System.Logger getLogger(String name) { 103 | // return System.getLogger(name); 104 | //} 105 | 106 | //public static System.Logger getLogger(String name, ResourceBundle bundle) { 107 | // return System.getLogger(name, bundle); 108 | //} 109 | 110 | public static void exit(int status) { 111 | if (status != 0) { 112 | System.exit(status); 113 | } else { 114 | throw new ExitError(status); 115 | } 116 | } 117 | 118 | public static void gc() { 119 | System.gc(); 120 | } 121 | 122 | public static void runFinalization() { 123 | //noinspection removal 124 | System.runFinalization(); 125 | } 126 | 127 | public static void load(String filename) { 128 | System.load(filename); 129 | } 130 | 131 | public static void loadLibrary(String libname) { 132 | System.loadLibrary(libname); 133 | } 134 | 135 | public static String mapLibraryName(String libname) { 136 | return System.mapLibraryName(libname); 137 | }; 138 | } -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/Create.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver; 2 | 3 | import eu.pb4.mrpackserver.format.ModpackInfo; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | 7 | import java.net.URI; 8 | import java.nio.file.FileSystems; 9 | import java.nio.file.Files; 10 | import java.nio.file.Paths; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Objects; 14 | 15 | public class Create { 16 | public static void main(String[] args) throws Throwable { 17 | var argMap = new HashMap(); 18 | 19 | for (int i = 0; i < args.length; i++) { 20 | var arg = args[i]; 21 | if (arg.startsWith("--")) { 22 | if (i + 1 < args.length && !args[i + 1].startsWith("--")) { 23 | argMap.put(arg.substring(2), args[i+1]); 24 | i++; 25 | } 26 | } 27 | } 28 | Logger.info("Creating jar with bundled modpack-info."); 29 | 30 | var runPath = Paths.get(""); 31 | ModpackInfo info; 32 | if (argMap.containsKey("project_id")) { 33 | info = new ModpackInfo(); 34 | info.versionId = argMap.getOrDefault("version_id", ";;release"); 35 | info.projectId = argMap.get("project_id"); 36 | info.displayName = argMap.get("display_name"); 37 | info.displayVersion = argMap.get("display_version"); 38 | info.url = argMap.containsKey("url") ? URI.create(argMap.get("url")) : null; 39 | if (info.url != null) { 40 | info.size = argMap.containsKey("size") ? Long.parseLong(argMap.get("size")) : null; 41 | info.sha512 = argMap.get("sha512"); 42 | } 43 | if (argMap.containsKey("whitelist") || argMap.containsKey("whitelisted_domains")) { 44 | info.whitelistedDomains.addAll(List.of(argMap.getOrDefault("whitelist", argMap.getOrDefault("whitelisted_domains", "")).split(","))); 45 | } 46 | } else { 47 | info = Utils.resolveModpackInfo(runPath); 48 | if (info == null) { 49 | Logger.error("Couldn't find 'modpack-info.json'!"); 50 | return; 51 | } 52 | } 53 | Logger.info("== Modpack Info =="); 54 | Logger.info("project_id: %s", info.projectId); 55 | Logger.info("version_id: %s", info.versionId); 56 | Logger.info("display_name: %s", Objects.requireNonNullElse(info.displayName, "")); 57 | Logger.info("display_version: %s", Objects.requireNonNullElse(info.displayVersion, "")); 58 | if (info.url != null) { 59 | Logger.info("url: %s", info.url); 60 | Logger.info("sha512: %s", Objects.requireNonNullElse(info.sha512, "")); 61 | Logger.info("size: %s", Objects.requireNonNullElse(info.size, "")); 62 | } 63 | Logger.info("whitelisted_domains: %s", String.join(", ", info.whitelistedDomains)); 64 | Logger.info("== Modpack Info =="); 65 | 66 | Logger.info("Creating jar file."); 67 | var inputJarPath = Paths.get(Create.class.getProtectionDomain().getCodeSource().getLocation().toURI()); 68 | var outName = argMap.getOrDefault("out", "server.jar"); 69 | var outputJarPath = runPath.resolve(outName); 70 | Files.deleteIfExists(outputJarPath); 71 | Files.copy(inputJarPath, outputJarPath); 72 | 73 | try (var fs = FileSystems.newFileSystem(outputJarPath)) { 74 | Files.writeString(fs.getPath("modpack-info.json"), info.toJson()); 75 | } 76 | 77 | Logger.info("Done! Bundled file exists as '%s'", outName); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/Main.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver; 2 | 3 | import eu.pb4.mrpackserver.format.InstanceInfo; 4 | import eu.pb4.mrpackserver.installer.ModrinthModpackLookup; 5 | import eu.pb4.mrpackserver.launch.Launcher; 6 | import eu.pb4.mrpackserver.launch.LegacyExitBlocker; 7 | import eu.pb4.mrpackserver.util.*; 8 | 9 | import java.awt.*; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.Objects; 13 | 14 | public class Main { 15 | public static boolean isLaunched = false; 16 | public static void main(String[] args) throws Throwable { 17 | System.setProperty("log4j2.formatMsgNoLookups", "true"); 18 | 19 | boolean noGui = false; 20 | for (var arg : args) { 21 | if (arg.equals("nogui") || arg.equals("--nogui")) { 22 | noGui = true; 23 | break; 24 | } 25 | } 26 | 27 | if (!noGui) { 28 | noGui = GraphicsEnvironment.isHeadless(); 29 | } 30 | 31 | var runPath = Paths.get(""); 32 | var modpackInfo = Utils.resolveModpackInfo(runPath); 33 | 34 | if (!noGui) { 35 | try { 36 | InstallerGui.setup(modpackInfo); 37 | } catch (Throwable e) { 38 | Logger.error("Tried to open terminal ui, but it failed!", e); 39 | } 40 | } 41 | 42 | if (modpackInfo == null) { 43 | Logger.info("Couldn't find modpack definition! Creating a new one..."); 44 | Utils.configureModpack(runPath); 45 | 46 | modpackInfo = Utils.resolveModpackInfo(runPath); 47 | if (modpackInfo == null) { 48 | Logger.error("Couldn't find modpack definition! Exiting..."); 49 | return; 50 | } 51 | } 52 | 53 | var instanceData = runPath.resolve(Constants.DATA_FOLDER); 54 | var instanceDataPath = instanceData.resolve("instance.json"); 55 | var instanceInfo = new InstanceInfo(); 56 | if (Files.exists(instanceDataPath)) { 57 | instanceInfo = InstanceInfo.read(Files.readString(instanceDataPath)); 58 | } 59 | 60 | try { 61 | boolean runLogic = true; 62 | 63 | if (modpackInfo.versionId.startsWith(";;")) { 64 | Logger.info("Checking for updates for %s...", modpackInfo.getDisplayName()); 65 | 66 | var type = modpackInfo.versionId.substring(2); 67 | var finalInstanceInfo = instanceInfo; 68 | var version = ModrinthModpackLookup.findVersion(modpackInfo.getVersionListUrl(), modpackInfo.projectId, modpackInfo.getDisplayName(), type, false, 69 | x -> x.versionType.equals(type) && (finalInstanceInfo.versionId.isEmpty() || FlexVerComparator.compare(finalInstanceInfo.versionId, x.versionNumber) < 0)); 70 | 71 | if (version == null) { 72 | if (instanceInfo.versionId.isEmpty()) { 73 | Logger.error("Failed to find %s version of %s! Quitting...", type, modpackInfo.getDisplayName()); 74 | return; 75 | } 76 | runLogic = false; 77 | } else { 78 | modpackInfo.versionId = version.versionNumber(); 79 | modpackInfo.url = version.uri(); 80 | modpackInfo.size = version.size(); 81 | modpackInfo.sha512 = version.hashes().get(Constants.MODRINTH_HASH); 82 | } 83 | } 84 | 85 | if (runLogic && (!modpackInfo.projectId.equals(instanceInfo.projectId) || !modpackInfo.versionId.equals(instanceInfo.versionId) || instanceInfo.runnablePath.isEmpty())) { 86 | Files.createDirectories(instanceData); 87 | var newInstance = Utils.checkAndSetupModpack(modpackInfo, instanceInfo, runPath, instanceData); 88 | if (newInstance != null) { 89 | Files.deleteIfExists(instanceDataPath); 90 | Files.writeString(instanceDataPath, Utils.GSON_MAIN.toJson(newInstance.info())); 91 | instanceInfo = newInstance.info(); 92 | 93 | Logger.useFullName(); 94 | LegacyExitBlocker.run(newInstance.installer()); 95 | Logger.useShortName(); 96 | Logger.info("Installation of %s (%s) finished!", modpackInfo.getDisplayName(), modpackInfo.getDisplayVersion()); 97 | } else if (!instanceInfo.runnablePath.isEmpty()) { 98 | Logger.error("Failed to install modpack! Quitting..."); 99 | return; 100 | } else { 101 | return; 102 | } 103 | } 104 | } catch (Throwable e) { 105 | Logger.error("Exception occurred while installing modpack!", e); 106 | return; 107 | } 108 | 109 | if (instanceInfo.runnablePath.isEmpty()) { 110 | Logger.warn("Server was installed successfully, but there is no server launcher defined!"); 111 | Logger.warn("This means the platform used by this modpack isn't supported!"); 112 | Logger.warn("Refer to platforms installation guide for more information!"); 113 | return; 114 | } 115 | 116 | isLaunched = true; 117 | 118 | if (!noGui && instanceInfo.dependencies.get(Constants.FORGE) != null && FlexVerComparator.compare(instanceInfo.dependencies.get(Constants.MINECRAFT), "1.17") < 0) { 119 | if (InstallerGui.instance != null) { 120 | InstallerGui.instance.close(); 121 | //InstallerGui.instance.handleForgeFix(); 122 | } 123 | } 124 | 125 | if (instanceInfo.forceSystemClasspath) { 126 | Launcher.launchFinalInject(runPath.resolve(instanceInfo.runnablePath), args); 127 | } else { 128 | Launcher.launchFinal(Objects.requireNonNull(Launcher.getFromPath(runPath.resolve(instanceInfo.runnablePath))), (x) -> null, args); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/FabricInstallerVersion.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import com.google.gson.reflect.TypeToken; 4 | import eu.pb4.mrpackserver.util.Utils; 5 | 6 | import java.util.List; 7 | 8 | public class FabricInstallerVersion { 9 | private static final TypeToken> TYPE = new TypeToken<>() {}; 10 | public String url = ""; 11 | public String version = ""; 12 | public boolean stable = false; 13 | 14 | public static List read(String s) { 15 | return Utils.GSON_MAIN.fromJson(s, TYPE.getType()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/InstanceInfo.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import eu.pb4.mrpackserver.util.Utils; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class InstanceInfo { 9 | public String projectId = ""; 10 | public String versionId = ""; 11 | public String runnablePath = ""; 12 | 13 | public boolean forceSystemClasspath = false; 14 | 15 | public Map dependencies = new HashMap<>(); 16 | 17 | public static InstanceInfo read(String s) { 18 | return Utils.GSON_MAIN.fromJson(s, InstanceInfo.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/ModpackIndex.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.Utils; 5 | 6 | import java.net.URI; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | public class ModpackIndex { 11 | public int formatVersion = 1; 12 | public String game = Constants.MINECRAFT; 13 | public String versionId = ""; 14 | public String name = ""; 15 | public String summary = ""; 16 | public List files = List.of(); 17 | public HashMap dependencies = new HashMap<>(); 18 | 19 | public static ModpackIndex read(String s) { 20 | return Utils.GSON_MAIN.fromJson(s, ModpackIndex.class); 21 | } 22 | public static class FileEntry { 23 | public String path = ""; 24 | public HashMap hashes = new HashMap<>(); 25 | public HashMap env = new HashMap<>(); 26 | public List downloads = List.of(); 27 | public long fileSize = 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/ModpackInfo.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import eu.pb4.mrpackserver.util.Constants; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.net.URI; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class ModpackInfo { 13 | @Nullable 14 | @SerializedName("display_name") 15 | public String displayName; 16 | @Nullable 17 | @SerializedName("display_version") 18 | public String displayVersion; 19 | @SerializedName("project_id") 20 | public String projectId = ""; 21 | @SerializedName("version_id") 22 | public String versionId = ""; 23 | @SerializedName("whitelisted_domains") 24 | public List whitelistedDomains = new ArrayList<>(); 25 | @SerializedName("non_overwritable_paths") 26 | public List nonOverwritablePaths = new ArrayList<>(); 27 | @Nullable 28 | public String sha512 = null; 29 | @Nullable 30 | public URI url = null; 31 | @Nullable 32 | public Long size = null; 33 | 34 | @SerializedName("version_list_url") 35 | @Nullable 36 | public String versionListUrl = null; 37 | 38 | @SerializedName("internal_flavor") 39 | @Nullable 40 | public String internalFlavor = null; 41 | 42 | @SerializedName("skip_java_version_check") 43 | public Boolean skipJavaVersionCheck = null; 44 | 45 | public boolean isValid() { 46 | return !this.projectId.isBlank() && !this.versionId.isBlank(); 47 | } 48 | 49 | public String getDisplayName() { 50 | return this.displayName != null ? this.displayName : this.projectId; 51 | } 52 | 53 | public String getDisplayVersion() { 54 | return this.displayVersion != null ? this.displayVersion : this.versionId; 55 | } 56 | 57 | 58 | public String getVersionListUrl() { 59 | return (this.versionListUrl != null ? this.versionListUrl : Constants.MODRINTH_API_VERSIONS).replace("{PROJECT_ID}", this.projectId); 60 | } 61 | 62 | public static ModpackInfo read(String s) { 63 | return Utils.GSON_MAIN.fromJson(s, ModpackInfo.class); 64 | } 65 | 66 | public String toJson() { 67 | return Utils.GSON_PRETTY.toJson(this); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/ModrinthModpackVersion.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.google.gson.reflect.TypeToken; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | 7 | import java.net.URI; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class ModrinthModpackVersion { 13 | private static final TypeToken> TYPE = new TypeToken<>() {}; 14 | 15 | public String name = ""; 16 | @SerializedName("version_number") 17 | public String versionNumber = ""; 18 | 19 | @SerializedName("version_type") 20 | public String versionType = ""; 21 | public String id = ""; 22 | 23 | public List files = List.of(); 24 | 25 | public static List read(String s) { 26 | return Utils.GSON_MAIN.fromJson(s, TYPE.getType()); 27 | } 28 | 29 | public static class File { 30 | public Map hashes = new HashMap<>(); 31 | public long size = -1; 32 | 33 | public URI url; 34 | public boolean primary = false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/ModrinthProjectData.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import eu.pb4.mrpackserver.util.Utils; 5 | 6 | public class ModrinthProjectData { 7 | @SerializedName("slug") 8 | public String slug = ""; 9 | @SerializedName("title") 10 | public String title = ""; 11 | @SerializedName("project_type") 12 | public String projectType = ""; 13 | 14 | @SerializedName("description") 15 | public String description = ""; 16 | 17 | public static ModrinthProjectData read(String s) { 18 | return Utils.GSON_MAIN.fromJson(s, ModrinthProjectData.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/ModrinthSearchData.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import eu.pb4.mrpackserver.util.Utils; 5 | 6 | import java.util.List; 7 | 8 | public class ModrinthSearchData { 9 | @SerializedName("hits") 10 | public List hits = List.of(); 11 | 12 | public static ModrinthSearchData read(String s) { 13 | return Utils.GSON_MAIN.fromJson(s, ModrinthSearchData.class); 14 | } 15 | 16 | 17 | public static class Project { 18 | @SerializedName("slug") 19 | public String slug = ""; 20 | @SerializedName("title") 21 | public String title = ""; 22 | @SerializedName("author") 23 | public String author = ""; 24 | @SerializedName("description") 25 | public String description = ""; 26 | @SerializedName("project_type") 27 | public String projectType = ""; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/VanillaVersionData.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import eu.pb4.mrpackserver.util.Utils; 4 | 5 | import java.util.Map; 6 | 7 | public class VanillaVersionData { 8 | public Map downloads = Map.of(); 9 | 10 | public static VanillaVersionData read(String s) { 11 | return Utils.GSON_MAIN.fromJson(s, VanillaVersionData.class); 12 | } 13 | 14 | public static class File { 15 | public String sha1 = ""; 16 | public long size = -1; 17 | public String url; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/format/VanillaVersionList.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.format; 2 | 3 | import eu.pb4.mrpackserver.util.Utils; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class VanillaVersionList { 9 | public Map latest; 10 | public List versions = List.of(); 11 | 12 | public static VanillaVersionList read(String s) { 13 | return Utils.GSON_MAIN.fromJson(s, VanillaVersionList.class); 14 | } 15 | public static class Version { 16 | public String id = ""; 17 | public String type = ""; 18 | public String url = ""; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/FabricInstallerLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | import eu.pb4.mrpackserver.format.FabricInstallerVersion; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.net.http.HttpResponse; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.util.List; 15 | 16 | public interface FabricInstallerLookup { 17 | static String createName(String mcVersion, String fabricVersion, String installerVersion) { 18 | return Constants.DATA_FOLDER + "/server/mc-" + mcVersion + "_fab-" + fabricVersion + "_inst_" + installerVersion + ".jar"; 19 | } 20 | 21 | static String createName(String installerVersion) { 22 | return Constants.DATA_FOLDER + "/server/fabric_installer_" + installerVersion + ".jar"; 23 | } 24 | 25 | @Nullable 26 | static Result downloadGeneric(FileDownloader downloader, Path path) { 27 | try { 28 | var version = getInstallerVersion(); 29 | var name = createName(version); 30 | var file = path.resolve(name); 31 | if (!Files.exists(file)) { 32 | var display = "Fabric Installer " + version; 33 | Files.createDirectories(file.getParent()); 34 | downloader.request(file, name, display, -1, null, List.of( 35 | URI.create("https://maven.fabricmc.net/net/fabricmc/fabric-installer/" + version + "/fabric-installer-" + version + ".jar") 36 | )); 37 | } 38 | return new Result(version, name, file); 39 | } catch (Throwable e) { 40 | Logger.warn("Failed to lookup Fabric installer!", e); 41 | } 42 | return null; 43 | } 44 | 45 | @Nullable 46 | static Result download(FileDownloader downloader, Path path, String mcVersion, String fabricVersion) { 47 | try { 48 | var version = getInstallerVersion(); 49 | var name = createName(mcVersion, fabricVersion, version); 50 | var file = path.resolve(name); 51 | if (!Files.exists(file)) { 52 | var display = "Fabric Installer " + version + " for Loader " + fabricVersion + " and Minecraft " + mcVersion; 53 | 54 | Files.createDirectories(file.getParent()); 55 | downloader.request(file, name, display, -1, null, List.of( 56 | URI.create("https://meta.fabricmc.net/v2/versions/loader/" + mcVersion + "/" + fabricVersion + "/" + version + "/server/jar") 57 | )); 58 | } 59 | return new Result(version, name, file); 60 | } catch (Throwable e) { 61 | Logger.warn("Failed to lookup Fabric installer!", e); 62 | } 63 | return null; 64 | } 65 | 66 | static String getInstallerVersion() throws IOException, InterruptedException { 67 | var client = Utils.createHttpClient(); 68 | var res = client.send(Utils.createGetRequest(URI.create(Constants.FABRIC_INSTALLER_VERSIONS)), HttpResponse.BodyHandlers.ofString()); 69 | var versions = FabricInstallerVersion.read(res.body()); 70 | return versions.stream().filter(fabricInstallerVersion -> fabricInstallerVersion.stable).findFirst().map(x -> x.version).orElse("1.0.1"); 71 | } 72 | 73 | record Result(String loaderVersion, String name, Path path) {}; 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/FileDownloader.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.HashData; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.net.URI; 9 | import java.net.http.HttpClient; 10 | import java.net.http.HttpResponse; 11 | import java.nio.file.Path; 12 | import java.util.*; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.ExecutionException; 15 | 16 | public final class FileDownloader { 17 | private final List entries = new ArrayList<>(); 18 | public void request(Path out, String path, long fileSize, @Nullable HashData hashData, List downloads) { 19 | request(out, path, path, fileSize, hashData, downloads); 20 | } 21 | public void request(Path out, String path, String displayName, long fileSize, @Nullable HashData hashData, List downloads) { 22 | this.entries.add(new DownloadableEntry(out, path, displayName, fileSize, hashData, downloads)); 23 | } 24 | 25 | public boolean isEmpty() { 26 | return this.entries.isEmpty(); 27 | } 28 | 29 | public List downloadFiles(Map hashes) throws InterruptedException, ExecutionException { 30 | if (this.entries.isEmpty()) { 31 | return List.of(); 32 | } 33 | 34 | var clients = new ArrayList(); 35 | var requests = new ArrayList>(); 36 | this.entries.sort(Comparator.comparingLong(DownloadableEntry::fileSize)); 37 | 38 | var failedDownloads = new ArrayList(); 39 | 40 | for (var i = 0; i < Constants.DOWNLOAD_PARRALEL_CLIENTS; i++) { 41 | clients.add(Utils.createHttpClient()); 42 | } 43 | 44 | int i = 0; 45 | 46 | for (var entry : entries) { 47 | requests.add(clients.get(i++ % clients.size()).sendAsync( 48 | Utils.createGetRequest(entry.downloads.get(0)), 49 | HttpResponse.BodyHandlers.ofInputStream() 50 | ).thenAccept(x -> { 51 | var fileSize = entry.fileSize; 52 | if (fileSize == -1) { 53 | var headerSize = x.headers().firstValue("Content-Length"); 54 | if (headerSize.isPresent()) { 55 | try { 56 | fileSize = Long.parseLong(headerSize.get()); 57 | } catch (Throwable e) { 58 | // ignored 59 | } 60 | } 61 | } 62 | 63 | var hash = Utils.handleDownloadedFile(entry.out, x.body(), entry.displayName, fileSize, entry.hashData); 64 | if (hash != null) { 65 | synchronized (hashes) { 66 | hashes.put(entry.path, hash); 67 | } 68 | } else { 69 | synchronized (failedDownloads) { 70 | failedDownloads.add(entry.displayName); 71 | } 72 | } 73 | })); 74 | } 75 | 76 | CompletableFuture.allOf(requests.toArray(new CompletableFuture[0])).get(); 77 | return failedDownloads; 78 | } 79 | 80 | public record DownloadableEntry(Path out, String path, String displayName, long fileSize, @Nullable HashData hashData, List downloads) {} 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/ForgeInstallerLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.net.URI; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.List; 11 | 12 | public interface ForgeInstallerLookup { 13 | static String createName(String version) { 14 | return Constants.DATA_FOLDER + "/server/forge_installer_" + version + ".jar"; 15 | } 16 | 17 | @Nullable 18 | static Result download(FileDownloader downloader, Path path, String mcVersion, String version) { 19 | try { 20 | var name = createName(mcVersion + "-" + version); 21 | var file = path.resolve(name); 22 | if (!Files.exists(file)) { 23 | var display = "Forge Server Installer " + mcVersion + "-" + version; 24 | Files.createDirectories(file.getParent()); 25 | downloader.request(file, name, display, -1, null, List.of( 26 | URI.create("https://maven.minecraftforge.net/net/minecraftforge/forge/" + 27 | mcVersion + "-" + version + "/forge-" + mcVersion + "-" + version + "-installer.jar") 28 | )); 29 | } 30 | return new Result(name, file); 31 | } catch (Throwable e) { 32 | Logger.warn("Failed to lookup Forge Server Installer installer!", e); 33 | } 34 | return null; 35 | } 36 | 37 | record Result(String name, Path path) {}; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/ForgeStarterLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.format.FabricInstallerVersion; 4 | import eu.pb4.mrpackserver.util.Constants; 5 | import eu.pb4.mrpackserver.util.Logger; 6 | import eu.pb4.mrpackserver.util.Utils; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.net.URI; 10 | import java.net.http.HttpResponse; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.List; 14 | 15 | public interface ForgeStarterLookup { 16 | String VERSION = "0.1.25"; 17 | String STARTER_URL = "https://github.com/neoforged/ServerStarterJar/releases/download/0.1.25/server.jar"; 18 | 19 | static String createName() { 20 | return Constants.DATA_FOLDER + "/server/forge_starter_" + VERSION + ".jar"; 21 | } 22 | 23 | @Nullable 24 | static Result download(FileDownloader downloader, Path path) { 25 | try { 26 | var name = createName(); 27 | var file = path.resolve(name); 28 | if (!Files.exists(file)) { 29 | var display = "(Neo)Forge Server Starter " + VERSION; 30 | Files.createDirectories(file.getParent()); 31 | downloader.request(file, name, display, -1, null, List.of( 32 | URI.create(STARTER_URL) 33 | )); 34 | } 35 | return new Result(name, file); 36 | } catch (Throwable e) { 37 | Logger.warn("Failed to lookup (Neo)Forge Server Starter!", e); 38 | } 39 | return null; 40 | } 41 | 42 | record Result(String name, Path path) {}; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/ModrinthModpackLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.format.ModrinthModpackVersion; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import eu.pb4.mrpackserver.util.Utils; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.net.URI; 9 | import java.net.http.HttpResponse; 10 | import java.util.List; 11 | import java.util.function.BiPredicate; 12 | import java.util.function.Predicate; 13 | 14 | public interface ModrinthModpackLookup { 15 | @Nullable 16 | static Result findVersion(String api, String projectId, String displayName, String displayVersion, boolean logError, Predicate versionPredicate) { 17 | var versions = getVersions(api, projectId, displayName); 18 | if (versions == null) { 19 | return null; 20 | } 21 | 22 | var version = versions.stream().filter(versionPredicate).findFirst(); 23 | if (version.isEmpty()) { 24 | if (logError) { 25 | Logger.error("Failed to find version %s of modpack %s (%s)!", displayVersion, displayName, projectId); 26 | } 27 | return null; 28 | } 29 | 30 | var file = version.get().files.stream().filter(x -> x.primary).findFirst(); 31 | if (file.isEmpty()) { 32 | if (logError) { 33 | Logger.error("Failed to find files for version %s of modpack %s (%s)!", displayVersion, displayName, projectId); 34 | } 35 | return null; 36 | } 37 | return new Result(version.get().id, version.get().versionNumber, file.get().url, file.get().size, file.get().hashes); 38 | } 39 | 40 | @Nullable 41 | static List getVersions(String requestUrl, String projectId, String displayName) { 42 | try { 43 | var client = Utils.createHttpClient(); 44 | var res = client.send(Utils.createGetRequest(URI.create(requestUrl)), HttpResponse.BodyHandlers.ofString()); 45 | if (res.statusCode() != 200) { 46 | Logger.warn("Failed to lookup modpack files for %s (%s)! Got code response %s | %s", displayName, projectId, res.statusCode(), res.body()); 47 | return null; 48 | } 49 | 50 | return ModrinthModpackVersion.read(res.body()); 51 | } catch (Throwable e) { 52 | Logger.error("Failed to lookup modpack files for %s (%s)!", displayName, projectId, e); 53 | return null; 54 | } 55 | } 56 | 57 | record Result(String versionId, String versionNumber, URI uri, long size, java.util.Map hashes) {}; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/MrPackInstaller.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.*; 4 | import eu.pb4.mrpackserver.format.InstanceInfo; 5 | import eu.pb4.mrpackserver.format.ModpackIndex; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.*; 10 | import java.nio.file.attribute.BasicFileAttributes; 11 | import java.security.MessageDigest; 12 | import java.util.*; 13 | 14 | public class MrPackInstaller { 15 | private final Path source; 16 | private final ModpackIndex index; 17 | private final Path destination; 18 | private final HashMap oldHashes; 19 | private final HashMap newHashes; 20 | private final Path destinationOldModified; 21 | private final InstanceInfo currentInstanceData; 22 | private final Set whitelistedDomains; 23 | private final HashSet nonOverwritablePaths; 24 | @Nullable 25 | private String newLauncher; 26 | @Nullable 27 | private Installer installer; 28 | private boolean forceSystemClasspath = false; 29 | 30 | public MrPackInstaller(Path source, ModpackIndex index, Path destination, InstanceInfo data, HashMap hashes, Set whitelistedDomains, HashSet nonOverwritablePaths) throws IOException { 31 | this.source = source; 32 | this.index = index; 33 | this.currentInstanceData = data; 34 | this.destination = destination.toAbsolutePath(); 35 | this.destinationOldModified = this.destination.resolve("old_modified_files"); 36 | 37 | 38 | this.oldHashes = hashes; 39 | this.newHashes = new HashMap<>(); 40 | 41 | this.whitelistedDomains = whitelistedDomains; 42 | this.nonOverwritablePaths = nonOverwritablePaths; 43 | } 44 | 45 | public void prepareFolders() throws IOException { 46 | Files.createDirectories(this.destination); 47 | } 48 | 49 | public void extractIncluded(Map existingHashes) throws IOException { 50 | Logger.info("Extracting included files."); 51 | for (var base : Constants.OVERWRITES) { 52 | var path = this.source.resolve(base); 53 | if (Files.isDirectory(path)) { 54 | Files.walkFileTree(path, new FileVisitor() { 55 | @Override 56 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 57 | var local = path.relativize(dir).toString(); 58 | var p = destination.resolve(local).normalize(); 59 | if (!p.startsWith(destination.toAbsolutePath())) { 60 | Logger.error("Modpack contains files, that are placed outside of server's root! Found '%s'", local); 61 | return FileVisitResult.TERMINATE; 62 | } 63 | 64 | if (nonOverwritablePaths.contains(local) && Files.exists(p)) { 65 | Logger.warn("Skipping non-overwritable path: %s", path); 66 | return FileVisitResult.SKIP_SUBTREE; 67 | } 68 | Files.createDirectories(p); 69 | return FileVisitResult.CONTINUE; 70 | } 71 | 72 | @Override 73 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 74 | var local = path.relativize(file).toString(); 75 | var outPath = destination.resolve(local); 76 | if (!outPath.startsWith(destination.toAbsolutePath())) { 77 | Logger.error("Modpack contains files, that are placed outside of server's root! Found '%s'", local); 78 | return FileVisitResult.TERMINATE; 79 | } 80 | 81 | if (nonOverwritablePaths.contains(local) && Files.exists(file)) { 82 | Logger.warn("Skipping non-overwritable file: %s", path); 83 | return FileVisitResult.CONTINUE; 84 | } 85 | 86 | var oldHash = oldHashes.get(local); 87 | var hashType = oldHash != null ? oldHash.type() : Constants.DEFAULT_HASH; 88 | var ext = existingHashes.remove(local); 89 | var existingHash = ext != null ? ext : getHash(hashType, outPath); 90 | var newHash = getHash(hashType, file); 91 | 92 | assert newHash != null; 93 | 94 | if (existingHash != null) { 95 | if ((oldHash != null && oldHash.equals(newHash) || newHash.equals(existingHash))) { 96 | newHashes.put(local, newHash); 97 | return FileVisitResult.CONTINUE; 98 | } else if (oldHash != null && !oldHash.equals(existingHash)) { 99 | var oldFilePath = destinationOldModified.resolve(local); 100 | Files.createDirectories(oldFilePath.getParent()); 101 | Files.deleteIfExists(oldFilePath); 102 | Files.move(outPath, oldFilePath); 103 | Logger.info("File '%s' was modified, but modpack required it to be updated! Moving it to '%s'", local, "old_modified_files/" + local); 104 | } 105 | Files.deleteIfExists(outPath); 106 | } 107 | 108 | Files.copy(file, outPath, StandardCopyOption.REPLACE_EXISTING); 109 | newHashes.put(local, newHash); 110 | return FileVisitResult.CONTINUE; 111 | } 112 | 113 | @Override 114 | public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 115 | return FileVisitResult.TERMINATE; 116 | } 117 | 118 | @Override 119 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 120 | return FileVisitResult.CONTINUE; 121 | } 122 | }); 123 | } 124 | } 125 | Logger.info("Finished extracting files from mrpack!"); 126 | } 127 | 128 | public boolean checkJavaVersion() { 129 | var minecraft = this.index.dependencies.get(Constants.MINECRAFT); 130 | if (!minecraft.startsWith("1.")) { 131 | return true; 132 | } 133 | 134 | if (FlexVerComparator.compare(minecraft, "1.20.5-") >= 0 && !JavaVersion.IS_JAVA_21) { 135 | Logger.error("Minecraft %s! only supports Java 21 or newer! You are currently using Java %s!", minecraft, Runtime.version().feature()); 136 | return false; 137 | }else if (FlexVerComparator.compare(minecraft, "1.18-") >= 0 && !JavaVersion.IS_JAVA_17) { 138 | Logger.error("Minecraft %s only supports Java 17 or newer! You are currently using Java %s!", minecraft, Runtime.version().feature()); 139 | return false; 140 | } else if (FlexVerComparator.compare(minecraft, "1.17-") >= 0 && !JavaVersion.IS_JAVA_16) { 141 | Logger.error("Minecraft %s only supports Java 16 or newer! You are currently using Java %s!", minecraft, Runtime.version().feature()); 142 | return false; 143 | } else if (FlexVerComparator.compare(minecraft, "1.13-") > 0 && FlexVerComparator.compare(minecraft, "1.17-") < 0 && this.index.dependencies.containsKey(Constants.FORGE) && JavaVersion.IS_JAVA_9) { 144 | Logger.error("Minecraft with Forge on %s only supports Java 8! You are currently using Java %s!", minecraft, Runtime.version().feature()); 145 | return false; 146 | } 147 | 148 | 149 | return true; 150 | } 151 | 152 | public void requestDownloads(FileDownloader downloader, Map hashExisting) throws Exception { 153 | if (this.currentInstanceData.runnablePath.isEmpty() || !this.currentInstanceData.dependencies.equals(this.index.dependencies) || !Files.exists(this.destination.resolve(this.currentInstanceData.runnablePath))) { 154 | var mcVersion = this.index.dependencies.get(Constants.MINECRAFT); 155 | if (mcVersion == null) { 156 | Logger.warn("Minecraft version is not set!"); 157 | Logger.warn("The modpack will be still installed, but it won't be able to start!"); 158 | } else if (this.index.dependencies.containsKey(Constants.FABRIC)) { 159 | var version = this.index.dependencies.get(Constants.FABRIC); 160 | if (FlexVerComparator.compare(version, "0.12.0") >= 0) { 161 | var starter = FabricInstallerLookup.download(downloader, this.destination, mcVersion, version); 162 | if (starter != null) { 163 | this.newLauncher = starter.name(); 164 | } 165 | } else { 166 | this.forceSystemClasspath = true; 167 | var installer = FabricInstallerLookup.downloadGeneric(downloader, this.destination); 168 | if (installer != null) { 169 | this.installer = new Installer("Fabric Installer", installer.name(), "server", "-mcversion", mcVersion, "-loader", version); 170 | } 171 | 172 | this.newLauncher = "fabric-server-launch.jar"; 173 | var propPath = this.destination.resolve("fabric-server-launcher.properties"); 174 | Files.deleteIfExists(propPath); 175 | var vanilla = VanillaInstallerLookup.download(downloader, this.destination, mcVersion); 176 | if (vanilla != null) { 177 | Files.writeString(propPath, "serverJar=" + vanilla.name()); 178 | } 179 | } 180 | } else if (this.index.dependencies.containsKey(Constants.NEOFORGE)) { 181 | this.forceSystemClasspath = true; 182 | var version = this.index.dependencies.get(Constants.NEOFORGE); 183 | var installer = mcVersion.equals("1.20.1") 184 | ? NeoForgeInstallerLookup.downloadLegacy(downloader, this.destination, mcVersion, version) 185 | : NeoForgeInstallerLookup.download(downloader, this.destination, version); 186 | if (installer != null) { 187 | this.installer = new Installer("NeoForge Server Installer", installer.name(), "--installServer"); 188 | } 189 | var starter = ForgeStarterLookup.download(downloader, this.destination); 190 | if (starter != null) { 191 | this.newLauncher = starter.name(); 192 | } 193 | } else if (this.index.dependencies.containsKey(Constants.FORGE)) { 194 | this.forceSystemClasspath = true; 195 | var version = this.index.dependencies.get(Constants.FORGE); 196 | 197 | var installer = ForgeInstallerLookup.download(downloader, this.destination, mcVersion, version); 198 | if (installer != null) { 199 | this.installer = new Installer("Forge Server Installer", installer.name(), "--installServer"); 200 | } 201 | if (FlexVerComparator.compare(mcVersion, "1.17.0") >= 0) { 202 | var starter = ForgeStarterLookup.download(downloader, this.destination); 203 | if (starter != null) { 204 | this.newLauncher = starter.name(); 205 | } 206 | } else { 207 | this.newLauncher = "forge-" + mcVersion + "-" + version + ".jar"; 208 | } 209 | } else if (this.index.dependencies.containsKey(Constants.QUILT)) { 210 | this.forceSystemClasspath = true; 211 | var version = this.index.dependencies.get(Constants.QUILT); 212 | var installer = QuiltInstallerLookup.download(downloader, this.destination); 213 | if (installer != null) { 214 | this.installer = new Installer("Quilt Installer", installer.name(), "install", "server", mcVersion, version, "--install-dir=./"); 215 | } 216 | 217 | this.newLauncher = "quilt-server-launch.jar"; 218 | 219 | var propPath = this.destination.resolve("quilt-server-launcher.properties"); 220 | Files.deleteIfExists(propPath); 221 | var vanilla = VanillaInstallerLookup.download(downloader, this.destination, mcVersion); 222 | if (vanilla != null) { 223 | Files.writeString(propPath, "serverJar=" + vanilla.name()); 224 | } 225 | } else if (this.index.dependencies.size() == 1) { 226 | var starter = VanillaInstallerLookup.download(downloader, this.destination, mcVersion); 227 | if (starter != null) { 228 | this.newLauncher = starter.name(); 229 | } 230 | } else { 231 | Logger.warn("Modpack requires a modloader, which is not yet supported!"); 232 | Logger.warn("The modpack will be still installed, but it won't be able to start!"); 233 | } 234 | 235 | if (this.installer != null) { 236 | hashExisting.remove(this.installer.path); 237 | } 238 | if (this.newLauncher != null) { 239 | hashExisting.remove(this.newLauncher); 240 | } 241 | } else { 242 | hashExisting.remove(this.currentInstanceData.runnablePath); 243 | } 244 | 245 | for (var file : this.index.files) { 246 | var currentHash = hashExisting.remove(file.path); 247 | var path = this.destination.resolve(file.path); 248 | 249 | if (currentHash != null) { 250 | var newHash = HashData.read(Constants.MODRINTH_HASH, file.hashes); 251 | 252 | if (newHash.equals(this.oldHashes.get(file.path))) { 253 | this.newHashes.put(file.path, this.oldHashes.get(file.path)); 254 | continue; 255 | } else if (currentHash.equals(newHash)) { 256 | this.newHashes.put(file.path, newHash); 257 | continue; 258 | } else if (nonOverwritablePaths.contains(file.path) && Files.exists(path)) { 259 | Logger.warn("Skipping non-overwritable file: %s", file.path); 260 | this.newHashes.put(file.path, this.oldHashes.get(file.path)); 261 | continue; 262 | } else if (currentHash.equals(this.oldHashes.get(file.path))) { 263 | Files.deleteIfExists(this.destination.resolve(file.path)); 264 | } else { 265 | Files.createDirectories(this.destinationOldModified.resolve(file.path).getParent()); 266 | Files.move(this.destination.resolve(file.path), this.destinationOldModified.resolve(file.path)); 267 | Logger.info("File '%s' was modified, but modpack required it to be updated! Moving it to '%s'", file.path, "old_modified_files/" + file.path); 268 | Files.deleteIfExists(this.destination.resolve(file.path)); 269 | } 270 | } else if (nonOverwritablePaths.contains(file.path) && Files.exists(path)) { 271 | Logger.warn("Skipping non-overwritable file: %s", file.path); 272 | continue; 273 | } 274 | 275 | for (var url : file.downloads) { 276 | if (!this.whitelistedDomains.contains(url.getHost())) { 277 | throw new RuntimeException("Non-whitelisted domain! " + url); 278 | } 279 | } 280 | 281 | if (Files.exists(path)) { 282 | Files.createDirectories(this.destinationOldModified.resolve(file.path).getParent()); 283 | Files.deleteIfExists(this.destinationOldModified.resolve(file.path)); 284 | Files.move(this.destination.resolve(file.path), this.destinationOldModified.resolve(file.path)); 285 | } 286 | 287 | if (!file.env.getOrDefault("server", "required").equals("unsupported")) { 288 | Files.createDirectories(path.getParent()); 289 | downloader.request(path, file.path, file.fileSize, HashData.read(Constants.MODRINTH_HASH, file.hashes.get(Constants.MODRINTH_HASH)), file.downloads); 290 | } 291 | } 292 | } 293 | public Map getLocalFileUpdatedHashes() throws Exception { 294 | if (this.oldHashes.isEmpty()) { 295 | return new HashMap<>(); 296 | } 297 | 298 | var hashExisting = new HashMap(); 299 | for (var entry : this.oldHashes.entrySet()) { 300 | var out = this.destination.resolve(entry.getKey()); 301 | if (!Files.exists(out)) { 302 | continue; 303 | } 304 | try (var stream = Files.newInputStream(out)) { 305 | var digest = MessageDigest.getInstance(entry.getValue().type()); 306 | byte[] dataBytes = new byte[1024]; 307 | int nread = 0; 308 | while ((nread = stream.read(dataBytes)) != -1) { 309 | digest.update(dataBytes, 0, nread); 310 | } 311 | hashExisting.put(entry.getKey(), new HashData(entry.getValue().type(), digest.digest())); 312 | } 313 | } 314 | return hashExisting; 315 | } 316 | 317 | public void cleanupLeftoverFiles(Map hashExisting) throws Exception { 318 | for (var entry : hashExisting.keySet()) { 319 | var currentHash = hashExisting.get(entry); 320 | if (currentHash.equals(this.oldHashes.get(entry))) { 321 | Files.deleteIfExists(this.destination.resolve(entry)); 322 | } else { 323 | Files.createDirectories(this.destinationOldModified.resolve(entry).getParent()); 324 | Files.deleteIfExists(this.destinationOldModified.resolve(entry)); 325 | Files.move(this.destination.resolve(entry), this.destinationOldModified.resolve(entry)); 326 | Logger.info("File '%s' was modified, but modpack required it to be removed! Moving it to '%s'", entry, "old_modified_files/" + entry); 327 | Files.deleteIfExists(this.destination.resolve(entry)); 328 | } 329 | } 330 | } 331 | 332 | public Map getHashes() { 333 | return this.newHashes; 334 | } 335 | 336 | private @Nullable HashData getHash(String type, Path path) { 337 | if (!Files.exists(path)) { 338 | return null; 339 | } 340 | 341 | try (var stream = Files.newInputStream(path)) { 342 | var digest = MessageDigest.getInstance(type); 343 | byte[] dataBytes = new byte[1024]; 344 | int nread = 0; 345 | while ((nread = stream.read(dataBytes)) != -1) { 346 | digest.update(dataBytes, 0, nread); 347 | } 348 | return new HashData(type, digest.digest()); 349 | } catch (Throwable e) { 350 | Logger.error("Error occurred while creating hash for '%s'!", path, e); 351 | } 352 | return null; 353 | } 354 | 355 | @Nullable 356 | public String getNewLauncher() { 357 | return this.newLauncher; 358 | } 359 | 360 | 361 | @Nullable 362 | public Installer getInstaller() { 363 | return this.installer; 364 | } 365 | 366 | public boolean forceSystemClasspath() { 367 | return this.forceSystemClasspath; 368 | } 369 | 370 | public record Installer(String name, String path, String... args) {} 371 | } 372 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/NeoForgeInstallerLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.net.URI; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.List; 11 | 12 | public interface NeoForgeInstallerLookup { 13 | static String createName(String version) { 14 | return Constants.DATA_FOLDER + "/server/neoforge_installer_" + version + ".jar"; 15 | } 16 | 17 | @Nullable 18 | static Result download(FileDownloader downloader, Path path, String version) { 19 | try { 20 | var name = createName(version); 21 | var file = path.resolve(name); 22 | if (!Files.exists(file)) { 23 | var display = "NeoForge Server Installer " + version; 24 | Files.createDirectories(file.getParent()); 25 | downloader.request(file, name, display, -1, null, List.of( 26 | URI.create("https://maven.neoforged.net/releases/net/neoforged/neoforge/" + version + "/neoforge-" + version + "-installer.jar") 27 | )); 28 | } 29 | return new Result(name, file); 30 | } catch (Throwable e) { 31 | Logger.warn("Failed to lookup NeoForge Server Installer!", e); 32 | } 33 | return null; 34 | } 35 | 36 | @Nullable 37 | static Result downloadLegacy(FileDownloader downloader, Path path, String mcVersion, String version) { 38 | try { 39 | var name = createName(mcVersion + "-" + version); 40 | var file = path.resolve(name); 41 | if (!Files.exists(file)) { 42 | var display = "NeoForge Server Installer " + version; 43 | Files.createDirectories(file.getParent()); 44 | downloader.request(file, name, display, -1, null, List.of( 45 | URI.create("https://maven.neoforged.net/releases/net/neoforged/forge/" + 46 | mcVersion + "-" + version + "/forge-" + mcVersion + "-" + version + "-installer.jar") 47 | )); 48 | } 49 | return new Result(name, file); 50 | } catch (Throwable e) { 51 | Logger.warn("Failed to lookup NeoForge Server Installer!", e); 52 | } 53 | return null; 54 | } 55 | 56 | record Result(String name, Path path) {}; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/QuiltInstallerLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.util.Constants; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.net.URI; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.List; 11 | 12 | public interface QuiltInstallerLookup { 13 | String VERSION = "0.9.2"; 14 | String STARTER_URL = "https://quiltmc.org/api/v1/download-latest-installer/java-universal"; 15 | 16 | static String createName() { 17 | return Constants.DATA_FOLDER + "/server/quilt_installer_" + VERSION + ".jar"; 18 | } 19 | 20 | @Nullable 21 | static Result download(FileDownloader downloader, Path path) { 22 | try { 23 | var name = createName(); 24 | var file = path.resolve(name); 25 | if (!Files.exists(file)) { 26 | var display = "Quilt Installer " + VERSION; 27 | Files.createDirectories(file.getParent()); 28 | downloader.request(file, name, display, -1, null, List.of( 29 | URI.create(STARTER_URL) 30 | )); 31 | } 32 | return new Result(name, file); 33 | } catch (Throwable e) { 34 | Logger.warn("Failed to lookup Quilt Installer!", e); 35 | } 36 | return null; 37 | } 38 | 39 | record Result(String name, Path path) {}; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/installer/VanillaInstallerLookup.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.installer; 2 | 3 | import eu.pb4.mrpackserver.format.VanillaVersionData; 4 | import eu.pb4.mrpackserver.format.VanillaVersionList; 5 | import eu.pb4.mrpackserver.util.Constants; 6 | import eu.pb4.mrpackserver.util.HashData; 7 | import eu.pb4.mrpackserver.util.Logger; 8 | import eu.pb4.mrpackserver.util.Utils; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.net.URI; 12 | import java.net.http.HttpResponse; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.List; 16 | 17 | public interface VanillaInstallerLookup { 18 | String VERSION_LIST = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; 19 | 20 | static String createName(String mcVersion) { 21 | return Constants.DATA_FOLDER + "/server/vanilla_" + mcVersion + ".jar"; 22 | } 23 | 24 | @Nullable 25 | static Result download(FileDownloader downloader, Path path, String mcVersion) { 26 | try { 27 | var display = "Minecraft Vanilla " + mcVersion; 28 | var client = Utils.createHttpClient(); 29 | var res = client.send(Utils.createGetRequest(URI.create(VERSION_LIST)), HttpResponse.BodyHandlers.ofString()); 30 | var versions = VanillaVersionList.read(res.body()); 31 | var version = versions.versions.stream().filter(x -> x.id.equals(mcVersion)).findFirst(); 32 | if (version.isEmpty()) { 33 | Logger.error("Failed to find %s!", display); 34 | return null; 35 | } 36 | var res2 = client.send(Utils.createGetRequest(URI.create(version.get().url)), HttpResponse.BodyHandlers.ofString()); 37 | var versionData = VanillaVersionData.read(res2.body()).downloads.get("server"); 38 | if (versionData == null) { 39 | Logger.error("Failed to find server file for %s!", display); 40 | return null; 41 | } 42 | 43 | var name = createName(mcVersion); 44 | var file = path.resolve(name); 45 | if (!Files.exists(file)) { 46 | Files.createDirectories(file.getParent()); 47 | downloader.request(file, name, display, versionData.size, HashData.read("SHA-1", versionData.sha1), List.of( 48 | URI.create(versionData.url) 49 | )); 50 | } 51 | return new Result(name, file); 52 | } catch (Throwable e) { 53 | Logger.warn("Failed to lookup Minecraft Vanilla!", e); 54 | } 55 | return null; 56 | } 57 | 58 | record Result(String name, Path path) {}; 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/launch/ExitError.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.launch; 2 | 3 | public class ExitError extends Error { 4 | public final int code; 5 | 6 | public ExitError(int code) { 7 | this.code = code; 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/launch/FileSystemProviderHijacker.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.launch; 2 | 3 | import eu.pb4.mrpackserver.util.InstrumentationCatcher; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | 6 | import java.nio.file.spi.FileSystemProvider; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public interface FileSystemProviderHijacker { 12 | static List addProviders(ClassLoader loader, List providers) { 13 | if (providers.isEmpty()) { 14 | return List.of(); 15 | } 16 | 17 | var list = new ArrayList(); 18 | 19 | for (var className : providers) { 20 | try { 21 | var clazz = loader.loadClass(className); 22 | list.add((FileSystemProvider) clazz.getConstructor().newInstance()); 23 | } catch (Throwable ignored) {} 24 | } 25 | var prov = new ArrayList<>(FileSystemProvider.installedProviders()); 26 | prov.addAll(list); 27 | 28 | InstrumentationCatcher.open("java.base", "java.nio.file.spi"); 29 | 30 | try { 31 | var field = FileSystemProvider.class.getDeclaredField("installedProviders"); 32 | field.setAccessible(true); 33 | field.set(null, Collections.unmodifiableList(prov)); 34 | field.setAccessible(false); 35 | } catch (Throwable e) { 36 | Logger.error("Failed to add FileSystemProviders!", e); 37 | return List.of(); 38 | } 39 | return list; 40 | } 41 | 42 | static void removeProviders(List providers) { 43 | if (providers.isEmpty()) { 44 | return; 45 | } 46 | 47 | var prov = new ArrayList<>(FileSystemProvider.installedProviders()); 48 | prov.removeAll(providers); 49 | 50 | try { 51 | var field = FileSystemProvider.class.getDeclaredField("installedProviders"); 52 | field.setAccessible(true); 53 | field.set(null, Collections.unmodifiableList(prov)); 54 | field.setAccessible(false); 55 | } catch (Throwable e) { 56 | Logger.error("Failed to remove FileSystemProviders!", e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/launch/Launcher.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.launch; 2 | 3 | import eu.pb4.mrpackserver.util.InstrumentationCatcher; 4 | import eu.pb4.mrpackserver.util.Logger; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.io.File; 8 | import java.lang.instrument.ClassFileTransformer; 9 | import java.lang.instrument.Instrumentation; 10 | import java.lang.invoke.MethodHandles; 11 | import java.lang.invoke.MethodType; 12 | import java.lang.reflect.Method; 13 | import java.net.URL; 14 | import java.net.URLClassLoader; 15 | import java.nio.file.FileSystems; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.*; 19 | import java.util.function.Function; 20 | import java.util.jar.Attributes; 21 | import java.util.jar.JarFile; 22 | 23 | public interface Launcher { 24 | static boolean launchExec(Path path, Function transformer, String... args) { 25 | return launchExec(getFromPath(path), transformer, args); 26 | } 27 | @Nullable 28 | static Launcher.Target getFromPath(Path path) { 29 | String mainClass = null; 30 | String launcherAgentClass = null; 31 | var jarUrl = new ArrayList(); 32 | try { 33 | jarUrl.add(path.toUri().toURL()); 34 | } catch (Throwable e) { 35 | Logger.error("Invalid path '%s'!", path); 36 | return null; 37 | } 38 | 39 | try (var jarFile = new JarFile(path.toFile())) { 40 | mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); 41 | launcherAgentClass = jarFile.getManifest().getMainAttributes().getValue("Launcher-Agent-Class"); 42 | var classPath = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); 43 | 44 | if (mainClass == null) { 45 | Logger.error("The '%s' is missing main class!!", path); 46 | return null; 47 | } 48 | 49 | if (classPath != null) { 50 | var tokenizer = new StringTokenizer(classPath, " "); 51 | while (tokenizer.hasMoreTokens()) { 52 | var t = tokenizer.nextToken(); 53 | jarUrl.add(Path.of(t.replace("\n", "").replace("\r", "")).toUri().toURL()); 54 | } 55 | } 56 | var fsProviders = new ArrayList(); 57 | for (var url : jarUrl) { 58 | try (var fs = FileSystems.newFileSystem(Path.of(url.toURI()))) { 59 | var x = fs.getPath("META-INF/services/java.nio.file.spi.FileSystemProvider"); 60 | if (Files.exists(x)) { 61 | fsProviders.addAll(Files.readAllLines(x)); 62 | } 63 | } 64 | } 65 | 66 | return new Target(jarUrl, mainClass, launcherAgentClass, fsProviders); 67 | } catch (Throwable e) { 68 | Logger.error("Failed to read data of '%s'!", path, e); 69 | return null; 70 | } 71 | } 72 | 73 | static boolean launchExec(Target target, Function transformer, String... args) { 74 | ClassFileTransformer transformer1 = null; 75 | try (var launchClassLoader = new URLClassLoader(target.classPath.toArray(URL[]::new))) { 76 | transformer1 = transformer.apply(launchClassLoader); 77 | if (transformer1 != null) { 78 | if (InstrumentationCatcher.exists()) { 79 | InstrumentationCatcher.get().addTransformer(transformer1); 80 | } else { 81 | transformer1 = null; 82 | Logger.warn("The executed jar (%s) requires patches, but they can't be executed!", target); 83 | Logger.warn("Update to Java 9 or newer to fix this! Through in some cases it's safe to ignore!"); 84 | } 85 | } 86 | 87 | return execute(launchClassLoader, target, args, true); 88 | } catch (Throwable e) { 89 | Logger.error("Exception occurred while executing '%s' with arguments '%s'!", target, String.join(" ", args), e); 90 | return false; 91 | } finally { 92 | if (transformer1 != null) { 93 | InstrumentationCatcher.get().removeTransformer(transformer1); 94 | } 95 | } 96 | } 97 | 98 | private static boolean execute(ClassLoader loader, Target target, String[] args, boolean cleanup) throws Throwable { 99 | var fs = FileSystemProviderHijacker.addProviders(loader, target.fileSystemProviders); 100 | if (target.launcherAgentClass != null) { 101 | if (InstrumentationCatcher.exists()) { 102 | try { 103 | var m = loader.loadClass(target.launcherAgentClass).getDeclaredMethod("agentmain", String.class, Instrumentation.class); 104 | m.setAccessible(true); 105 | var handle = MethodHandles.lookup().unreflect(m); 106 | handle.invoke("", InstrumentationCatcher.get()); 107 | } catch (Throwable e) { 108 | Logger.error("Error occurred while invoking Launcher-Agent-Class!", e); 109 | } 110 | } else { 111 | Logger.warn("The executed jar (%s) requires Instrumentation, but it's not set up!"); 112 | Logger.warn("Update to Java 9 or newer to fix this! Through in some cases it's safe to ignore!"); 113 | } 114 | } 115 | 116 | try { 117 | var handle = MethodHandles.publicLookup().findStatic(loader.loadClass(target.mainClass), "main", MethodType.methodType(void.class, String[].class)); 118 | handle.invoke((Object) args); 119 | if (cleanup) { 120 | FileSystemProviderHijacker.removeProviders(fs); 121 | } 122 | return true; 123 | } catch (ExitError exit) { 124 | return exit.code == 0; 125 | } 126 | } 127 | 128 | static void launchFinal(Target target, Function transformer, String... args) throws Throwable { 129 | ClassFileTransformer transformer1 = null; 130 | var launchClassLoader = new URLClassLoader(target.classPath.toArray(URL[]::new)); 131 | transformer1 = transformer.apply(launchClassLoader); 132 | if (transformer1 != null) { 133 | if (InstrumentationCatcher.exists()) { 134 | InstrumentationCatcher.get().addTransformer(transformer1); 135 | } else { 136 | Logger.warn("The executed jar (%s) requires patches, but they can't be executed!", target); 137 | Logger.warn("Update to Java 9 or newer to fix this! Through in some cases it's safe to ignore!"); 138 | } 139 | } 140 | 141 | execute(launchClassLoader, target, args, false); 142 | } 143 | 144 | static void launchFinalInject(Path jarPath, String... args) throws Throwable { 145 | launchFinalInject(Objects.requireNonNull(getFromPath(jarPath)), args); 146 | } 147 | 148 | static void launchFinalInject(Target target, String... args) throws Throwable { 149 | injectSystemClassPath(target.classPath); 150 | execute(ClassLoader.getSystemClassLoader(), target, args, false); 151 | } 152 | 153 | static void injectSystemClassPath(Collection classPath) throws Throwable { 154 | var systemClassLoader = ClassLoader.getSystemClassLoader(); 155 | 156 | if (systemClassLoader instanceof URLClassLoader) { // This is true for Java 8 157 | Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); 158 | method.setAccessible(true); 159 | for (var url : classPath) { 160 | method.invoke(systemClassLoader, url); 161 | } 162 | } else if (InstrumentationCatcher.exists()) { // And this for Java 9+ 163 | var instr = InstrumentationCatcher.get(); 164 | for (var url : classPath) { 165 | var jar = new JarFile(Path.of(url.toURI()).toFile()); 166 | instr.appendToSystemClassLoaderSearch(jar); 167 | } 168 | } else { 169 | throw new RuntimeException("Couldn't add files to classpath"); 170 | } 171 | 172 | var currentClassPath = new StringBuilder(System.getProperty("java.class.path")); 173 | for (var url : classPath) { 174 | var path = Path.of(url.toURI()).toString(); 175 | currentClassPath.append(File.pathSeparatorChar).append(path); 176 | } 177 | System.setProperty("java.class.path", currentClassPath.toString()); 178 | } 179 | 180 | record Target(Collection classPath, String mainClass, @Nullable String launcherAgentClass, List fileSystemProviders) { 181 | public static Target of(URL path, String mainClass) { 182 | return new Target(List.of(path), mainClass, null, List.of()); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/launch/LegacyExitBlocker.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.launch; 2 | 3 | import eu.pb4.mrpackserver.util.FlexVerComparator; 4 | 5 | import java.security.Permission; 6 | 7 | public class LegacyExitBlocker { 8 | @SuppressWarnings("removal") 9 | public static void run(Runnable runnable) { 10 | if (FlexVerComparator.compare(System.getProperty("java.version"), "16") < 0) { 11 | var x = System.getSecurityManager(); 12 | System.setSecurityManager(new Preventer()); 13 | runnable.run(); 14 | System.setSecurityManager(x); 15 | } else { 16 | runnable.run(); 17 | } 18 | } 19 | 20 | 21 | 22 | @SuppressWarnings("removal") 23 | private static final class Preventer extends SecurityManager { 24 | private int lastCode = -1; 25 | @Override 26 | public void checkExit(int status) { 27 | if (lastCode == -1) { 28 | lastCode = status; 29 | throw new ExitError(status); 30 | } 31 | 32 | throw new ExitError(lastCode); 33 | } 34 | 35 | @Override 36 | public void checkPermission(Permission perm) {} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/Constants.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | public class Constants { 7 | public static final String FABRIC = "fabric-loader"; 8 | public static final String QUILT = "quilt-loader"; 9 | public static final String FORGE = "forge"; 10 | public static final String NEOFORGE = "neoforge"; 11 | public static final String MINECRAFT = "minecraft"; 12 | 13 | public static final String MODRINTH_HASH = "sha512"; 14 | public static final String DEFAULT_HASH = "SHA-512"; 15 | 16 | public static final String USER_AGENT; 17 | public static final int DOWNLOAD_PARRALEL_CLIENTS = 5; 18 | public static final int DOWNLOAD_UPDATE_TIME = 1500; 19 | public static final int DOWNLOAD_CHUNK_SIZE = 512; 20 | public static final String DATA_FOLDER = ".mrpack4server"; 21 | public static final String CUSTOM_NON_OVERWRITABLE_LIST = "lockedpaths.txt"; 22 | 23 | public static final String FABRIC_INSTALLER_VERSIONS = "https://meta.fabricmc.net/v2/versions/installer"; 24 | public static final String MODRINTH_API = "https://api.modrinth.com/v2"; 25 | public static final String MODRINTH_API_VERSIONS = MODRINTH_API + "/project/{PROJECT_ID}/version"; 26 | public static final List OVERWRITES = List.of("/overrides", "/server_overrides"); 27 | public static final List DEFAULT_NON_OVERWRITABLE = List.of( 28 | "server.properties", 29 | "world", 30 | "whitelist.json", 31 | "banned-ips.json", 32 | "banned-players.json", 33 | "ops.json", 34 | DATA_FOLDER, 35 | CUSTOM_NON_OVERWRITABLE_LIST 36 | ); 37 | 38 | public static final Set DEFAULT_WHITELISTED_URLS = Set.of( 39 | "cdn.modrinth.com", 40 | "github.com", 41 | "raw.githubusercontent.com", 42 | "gitlab.com" 43 | ); 44 | public static final String LOG_PREFIX = "[mrpack4server] "; 45 | public static final String LOG_PREFIX_SMALL = ""; 46 | public static final String LOG_WARN_PREFIX = "[mrpack4server | WARN] "; 47 | public static final String LOG_WARN_PREFIX_SMALL = "[WARN] "; 48 | public static final String LOG_ERROR_PREFIX = "[mrpack4server | ERROR] "; 49 | public static final String LOG_ERROR_PREFIX_SMALL = "[ERROR] "; 50 | public static final int SEARCH_QUERY_MAX_SIZE = 20; 51 | 52 | static { 53 | String extraFlavor = ""; 54 | try { 55 | var x = Utils.resolveModpackInfoInternal(); 56 | 57 | if (x != null) { 58 | if (x.internalFlavor != null) { 59 | extraFlavor = " / " + x.internalFlavor; 60 | } else if (x.isValid()) { 61 | extraFlavor = " / conf: " + x.getDisplayName() + " ver: " + x.getDisplayVersion(); 62 | } 63 | } 64 | } catch (Throwable throwable) { 65 | // ignored 66 | } 67 | 68 | var x = Constants.class.getPackage(); 69 | USER_AGENT = x.getImplementationTitle() + " v" + x.getImplementationVersion() + " (" + x.getImplementationVendor() + extraFlavor + ")"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/FlexVerComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To the extent possible under law, the author has dedicated all copyright 3 | * and related and neighboring rights to this software to the public domain 4 | * worldwide. This software is distributed without any warranty. 5 | * 6 | * See 7 | */ 8 | 9 | package eu.pb4.mrpackserver.util; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | /** 17 | * Implements FlexVer, a SemVer-compatible intuitive comparator for free-form versioning strings as 18 | * seen in the wild. It's designed to sort versions like people do, rather than attempting to force 19 | * conformance to a rigid and limited standard. As such, it imposes no restrictions. Comparing two 20 | * versions with differing formats will likely produce nonsensical results (garbage in, garbage out), 21 | * but best effort is made to correct for basic structural changes, and versions of differing length 22 | * will be parsed in a logical fashion. 23 | */ 24 | public class FlexVerComparator { 25 | 26 | /** 27 | * Parse the given strings as freeform version strings, and compare them according to FlexVer. 28 | * @param a the first version string 29 | * @param b the second version string 30 | * @return {@code 0} if the two versions are equal, a negative number if {@code a < b}, or a positive number if {@code a > b} 31 | */ 32 | public static int compare(String a, String b) { 33 | List ad = decompose(a); 34 | List bd = decompose(b); 35 | for (int i = 0; i < Math.max(ad.size(), bd.size()); i++) { 36 | int c = get(ad, i).compareTo(get(bd, i)); 37 | if (c != 0) return c; 38 | } 39 | return 0; 40 | } 41 | 42 | 43 | private static final VersionComponent NULL = new VersionComponent(new int[0]) { 44 | @Override 45 | public int compareTo(VersionComponent other) { return other == NULL ? 0 : -other.compareTo(this); } 46 | }; 47 | 48 | // @VisibleForTesting 49 | static class VersionComponent { 50 | private final int[] codepoints; 51 | 52 | public VersionComponent(int[] codepoints) { 53 | this.codepoints = codepoints; 54 | } 55 | 56 | public int[] codepoints() { 57 | return codepoints; 58 | } 59 | 60 | public int compareTo(VersionComponent that) { 61 | if (that == NULL) return 1; 62 | int[] a = this.codepoints(); 63 | int[] b = that.codepoints(); 64 | 65 | for (int i = 0; i < Math.min(a.length, b.length); i++) { 66 | int c1 = a[i]; 67 | int c2 = b[i]; 68 | if (c1 != c2) return c1 - c2; 69 | } 70 | 71 | return a.length - b.length; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return new String(codepoints, 0, codepoints.length); 77 | } 78 | 79 | } 80 | 81 | // @VisibleForTesting 82 | static class SemVerPrereleaseVersionComponent extends VersionComponent { 83 | public SemVerPrereleaseVersionComponent(int[] codepoints) { super(codepoints); } 84 | 85 | @Override 86 | public int compareTo(VersionComponent that) { 87 | if (that == NULL) return -1; // opposite order 88 | return super.compareTo(that); 89 | } 90 | 91 | } 92 | 93 | // @VisibleForTesting 94 | static class NumericVersionComponent extends VersionComponent { 95 | public NumericVersionComponent(int[] codepoints) { super(codepoints); } 96 | 97 | @Override 98 | public int compareTo(VersionComponent that) { 99 | if (that == NULL) return 1; 100 | if (that instanceof NumericVersionComponent) { 101 | int[] a = removeLeadingZeroes(this.codepoints()); 102 | int[] b = removeLeadingZeroes(that.codepoints()); 103 | if (a.length != b.length) return a.length-b.length; 104 | for (int i = 0; i < a.length; i++) { 105 | int ad = a[i]; 106 | int bd = b[i]; 107 | if (ad != bd) return ad-bd; 108 | } 109 | return 0; 110 | } 111 | return super.compareTo(that); 112 | } 113 | 114 | private int[] removeLeadingZeroes(int[] a) { 115 | if (a.length == 1) return a; 116 | int i = 0; 117 | int stopIdx = a.length - 1; 118 | while (i < stopIdx && a[i] == '0') { 119 | i++; 120 | } 121 | return Arrays.copyOfRange(a, i, a.length); 122 | } 123 | 124 | } 125 | 126 | /* 127 | * Break apart a string into intuitive version components, by splitting it where a run of 128 | * characters changes from numeric to non-numeric. 129 | */ 130 | // @VisibleForTesting 131 | static List decompose(String str) { 132 | if (str.isEmpty()) return Collections.emptyList(); 133 | boolean lastWasNumber = isAsciiDigit(str.codePointAt(0)); 134 | int totalCodepoints = str.codePointCount(0, str.length()); 135 | int[] accum = new int[totalCodepoints]; 136 | List out = new ArrayList<>(); 137 | int j = 0; 138 | for (int i = 0; i < str.length(); i++) { 139 | int cp = str.codePointAt(i); 140 | if (Character.charCount(cp) == 2) i++; 141 | if (cp == '+') break; // remove appendices 142 | boolean number = isAsciiDigit(cp); 143 | if (number != lastWasNumber || (cp == '-' && j > 0 && accum[0] != '-')) { 144 | out.add(createComponent(lastWasNumber, accum, j)); 145 | j = 0; 146 | lastWasNumber = number; 147 | } 148 | accum[j] = cp; 149 | j++; 150 | } 151 | out.add(createComponent(lastWasNumber, accum, j)); 152 | return out; 153 | } 154 | 155 | private static boolean isAsciiDigit(int cp) { 156 | return cp >= '0' && cp <= '9'; 157 | } 158 | 159 | private static VersionComponent createComponent(boolean number, int[] s, int j) { 160 | s = Arrays.copyOfRange(s, 0, j); 161 | if (number) { 162 | return new NumericVersionComponent(s); 163 | } else if (s.length > 1 && s[0] == '-') { 164 | return new SemVerPrereleaseVersionComponent(s); 165 | } else { 166 | return new VersionComponent(s); 167 | } 168 | } 169 | 170 | private static VersionComponent get(List li, int i) { 171 | return i >= li.size() ? NULL : li.get(i); 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/HashData.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.*; 7 | 8 | public record HashData(String type, byte[] hash) { 9 | public static HashData read(String string) { 10 | var splitter = string.indexOf(';'); 11 | if (splitter == -1) { 12 | return new HashData("SHA-512", HexFormat.of().parseHex(string)); 13 | } else { 14 | return new HashData(getJavaHash(string.substring(0, splitter)), HexFormat.of().parseHex(string.substring(splitter + 1))); 15 | } 16 | } 17 | 18 | public static HashData read(String type, String string) { 19 | return new HashData(getJavaHash(type), HexFormat.of().parseHex(string)); 20 | } 21 | 22 | public static HashData read(String type, Map hashes) { 23 | return read(type, hashes.get(type)); 24 | } 25 | 26 | public String inline() { 27 | return type + ";" + HexFormat.of().formatHex(hash); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "HashData[type=" + type +", data=" + HexFormat.of().formatHex(hash) + "]"; 33 | } 34 | 35 | private static String getJavaHash(String type) { 36 | return switch (type) { 37 | case "sha512" -> "SHA-512"; 38 | case "sha1" -> "SHA-1"; 39 | default -> type; 40 | }; 41 | } 42 | 43 | public String hashString() { 44 | return HexFormat.of().formatHex(hash); 45 | } 46 | 47 | @Override 48 | public boolean equals(Object object) { 49 | if (this == object) return true; 50 | if (object == null || getClass() != object.getClass()) return false; 51 | HashData hashData = (HashData) object; 52 | return Objects.equals(type, hashData.type) && Arrays.equals(hash, hashData.hash); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | int result = Objects.hash(type); 58 | result = 31 * result + Arrays.hashCode(hash); 59 | return result; 60 | } 61 | 62 | public static class Serializer implements JsonSerializer, JsonDeserializer { 63 | @Override 64 | public HashData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 65 | return HashData.read(json.getAsString()); 66 | } 67 | 68 | @Override 69 | public JsonElement serialize(HashData src, Type typeOfSrc, JsonSerializationContext context) { 70 | return new JsonPrimitive(src.inline()); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/InstallerGui.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import eu.pb4.mrpackserver.Main; 4 | import eu.pb4.mrpackserver.format.ModpackInfo; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import javax.swing.*; 9 | import javax.swing.border.EtchedBorder; 10 | import javax.swing.border.TitledBorder; 11 | import javax.swing.text.DefaultCaret; 12 | import java.awt.*; 13 | import java.awt.event.WindowAdapter; 14 | import java.awt.event.WindowEvent; 15 | import java.io.*; 16 | import java.nio.charset.Charset; 17 | import java.util.ArrayList; 18 | 19 | public class InstallerGui extends JComponent { 20 | private static final Font FONT_MONOSPACE = new Font("Monospaced", Font.PLAIN, 12); 21 | private final OutputStream logger; 22 | private final Charset charset; 23 | private final PrintStream oldOut; 24 | private final PrintStream oldErr; 25 | private final InputStream oldIn; 26 | private final AppendableInputStream in = new AppendableInputStream(); 27 | private final JFrame frame; 28 | private JTextArea logBox; 29 | private boolean closed; 30 | 31 | public static InstallerGui instance = null; 32 | 33 | public InstallerGui(JFrame jFrame) { 34 | this.frame = jFrame; 35 | //this.setPreferredSize(new Dimension(954, 480)); 36 | this.setLayout(new BorderLayout()); 37 | this.add(this.createConsole()); 38 | this.charset = getCharset(System.out); 39 | this.oldIn = System.in; 40 | this.oldOut = System.out; 41 | this.oldErr = System.err; 42 | this.logger = new OutputStream() { 43 | @Override 44 | public void write(byte @NotNull [] bytes, int offset, int length) { 45 | var text = new String(bytes, offset, length, charset); 46 | writeToConsole(text); 47 | } 48 | 49 | @Override 50 | public void write(int b) { 51 | write(new byte[]{(byte) b}, 0, 1); 52 | } 53 | 54 | }; 55 | 56 | 57 | System.setOut(new PrintStream(new DoubleOutputStream(System.out, logger), false, this.charset)); 58 | System.setErr(new PrintStream(new DoubleOutputStream(System.err, logger), false, this.charset)); 59 | System.setIn(System.console() != null ? new DoubleWaitingInputStream(System.in, this.in) : this.in); 60 | 61 | instance = this; 62 | } 63 | 64 | private void writeToConsole(String text) { 65 | SwingUtilities.invokeLater(() -> { 66 | if (text.contains("Starting minecraft server version")) { 67 | InstallerGui.this.close(); 68 | return; 69 | } 70 | if (this.closed) { 71 | return; 72 | } 73 | 74 | logBox.append(text); 75 | }); 76 | } 77 | 78 | private Charset getCharset(PrintStream err) { 79 | try { 80 | return err.charset(); 81 | } catch (Throwable e) { 82 | try { 83 | return System.console().charset(); 84 | } catch (Throwable e2) { 85 | try { 86 | return Charset.forName(System.getProperty("stdout.encoding")); 87 | } catch (Throwable e3) { 88 | return Charset.defaultCharset(); 89 | } 90 | } 91 | } 92 | } 93 | 94 | public static boolean setup(@Nullable ModpackInfo info) { 95 | try { 96 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 97 | } catch (Exception var3) { 98 | } 99 | 100 | String name; 101 | if (info != null) { 102 | name = "mrpack4server | " + info.getDisplayName() + " " + info.getDisplayVersion(); 103 | } else { 104 | name = "mrpack4server"; 105 | } 106 | 107 | var jFrame = new JFrame(name); 108 | jFrame.setPreferredSize(new Dimension(800, 480)); 109 | var gui = new InstallerGui(jFrame); 110 | jFrame.add(gui); 111 | jFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 112 | jFrame.pack(); 113 | jFrame.setLocationRelativeTo(null); 114 | jFrame.setVisible(true); 115 | jFrame.addWindowListener(new WindowAdapter() { 116 | public void windowClosing(WindowEvent event) { 117 | if (!Main.isLaunched) { 118 | System.exit(0); 119 | } 120 | } 121 | }); 122 | return true; 123 | } 124 | 125 | private Component createConsole() { 126 | var panel = new JPanel(new BorderLayout()); 127 | this.logBox = new JTextArea(); 128 | this.logBox.setFocusable(false); 129 | this.logBox.setLineWrap(true); 130 | var caret = new DefaultCaret(); 131 | caret.setVisible(false); 132 | //caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); 133 | this.logBox.setCaret(caret); 134 | 135 | var scroll = new JScrollPane(logBox, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 136 | 137 | logBox.setEditable(false); 138 | logBox.setFont(FONT_MONOSPACE); 139 | var input = new JTextField(); 140 | input.addActionListener((event) -> { 141 | String string = input.getText(); 142 | this.in.append((string + "\n").getBytes(this.charset)); 143 | writeToConsole(string + "\n"); 144 | input.setText(""); 145 | }); 146 | panel.add(scroll, "Center"); 147 | panel.add(input, "South"); 148 | panel.setBorder(new TitledBorder(new EtchedBorder(), "Console")); 149 | 150 | return panel; 151 | } 152 | 153 | public void close() { 154 | if (this.closed) { 155 | return; 156 | } 157 | this.closed = true; 158 | 159 | System.setOut(this.oldOut); 160 | System.setErr(this.oldErr); 161 | System.setIn(this.oldIn); 162 | this.frame.dispose(); 163 | instance = null; 164 | } 165 | 166 | private static class AppendableInputStream extends InputStream { 167 | private final ArrayList list = new ArrayList<>(); 168 | private int pos = 0; 169 | private byte[] curr = null; 170 | 171 | @Override 172 | public int read() throws IOException { 173 | if (curr == null) { 174 | while (true) { 175 | synchronized (list) { 176 | if (!list.isEmpty()) { 177 | break; 178 | } 179 | } 180 | try { 181 | Thread.sleep(1); 182 | } catch (Throwable e) { 183 | return -1; 184 | } 185 | } 186 | curr = this.list.removeFirst(); 187 | } 188 | if (curr.length == this.pos) { 189 | curr = null; 190 | this.pos = 0; 191 | return -1; 192 | } 193 | 194 | var val = Byte.toUnsignedInt(this.curr[this.pos++]); 195 | return val; 196 | } 197 | 198 | @Override 199 | public int available() throws IOException { 200 | if (curr == null) { 201 | synchronized (list) { 202 | if (list.isEmpty()) { 203 | return 0; 204 | } 205 | return list.getFirst().length; 206 | } 207 | } 208 | return curr.length; 209 | } 210 | 211 | public void append(byte[] arr) { 212 | synchronized (this.list) { 213 | this.list.add(arr); 214 | } 215 | } 216 | } 217 | 218 | private static class DoubleWaitingInputStream extends InputStream { 219 | private final InputStream out; 220 | private final InputStream out2; 221 | 222 | public DoubleWaitingInputStream(InputStream out, InputStream out2) { 223 | this.out = out; 224 | this.out2 = out2; 225 | } 226 | 227 | @Override 228 | public int read() throws IOException { 229 | var stream = getStream(); 230 | if (stream != null) { 231 | return stream.read(); 232 | } 233 | return -1; 234 | } 235 | 236 | @Override 237 | public int read(@NotNull byte[] b) throws IOException { 238 | var stream = getStream(); 239 | if (stream != null) { 240 | return stream.read(b); 241 | } 242 | return 0; 243 | } 244 | 245 | @Override 246 | public int read(@NotNull byte[] b, int off, int len) throws IOException { 247 | var stream = getStream(); 248 | if (stream != null) { 249 | return stream.read(b, off, len); 250 | } 251 | return 0; 252 | } 253 | 254 | private InputStream getStream() throws IOException { 255 | while (true) { 256 | if (this.out.available() > 0) { 257 | return this.out; 258 | } 259 | if (this.out2.available() > 0) { 260 | return this.out2; 261 | } 262 | try { 263 | Thread.sleep(1); 264 | } catch (InterruptedException e) { 265 | return null; 266 | } 267 | } 268 | } 269 | 270 | @Override 271 | public int available() throws IOException { 272 | return Math.max(this.out.available(), this.out2.available()); 273 | } 274 | } 275 | 276 | private static class DoubleOutputStream extends OutputStream { 277 | private final OutputStream out; 278 | private final OutputStream out2; 279 | 280 | public DoubleOutputStream(OutputStream out, OutputStream out2) { 281 | this.out = out; 282 | this.out2 = out2; 283 | } 284 | 285 | @Override 286 | public void write(int i) throws IOException { 287 | this.out.write(i); 288 | this.out2.write(i); 289 | } 290 | 291 | @Override 292 | public void write(@NotNull byte[] b) throws IOException { 293 | this.out.write(b); 294 | this.out2.write(b); 295 | } 296 | 297 | @Override 298 | public void write(@NotNull byte[] b, int off, int len) throws IOException { 299 | this.out.write(b, off, len); 300 | this.out2.write(b, off, len); 301 | } 302 | 303 | @Override 304 | public void flush() throws IOException { 305 | this.out.flush(); 306 | this.out2.flush(); 307 | } 308 | 309 | @Override 310 | public void close() throws IOException { 311 | this.out.close(); 312 | this.out2.close(); 313 | } 314 | } 315 | 316 | /*public void handleForgeFix() { 317 | Logger.warn("Forge on 1.16.5 and older breaks mrpack4server's initial log screen."); 318 | Logger.warn("You can safely close it without stopping the server."); 319 | var currOut = System.out; 320 | var currErr = System.err; 321 | 322 | try { 323 | var thread = new Thread(() -> { 324 | while (currErr == System.err && !this.closed) { 325 | Thread.yield(); 326 | } 327 | if (this.closed) { 328 | return; 329 | } 330 | 331 | this.oldErr = System.err; 332 | System.setErr(new PrintStream(new DoubleOutputStream(System.err, logger), false, getCharset(System.err))); 333 | }); 334 | thread.setDaemon(true); 335 | thread.start(); 336 | thread = new Thread(() -> { 337 | while (currOut == System.out && !this.closed) { 338 | Thread.yield(); 339 | } 340 | if (this.closed) { 341 | return; 342 | } 343 | 344 | this.oldOut = System.out; 345 | System.setOut(new PrintStream(new DoubleOutputStream(System.out, logger), false, getCharset(System.out))); 346 | }); 347 | thread.setDaemon(true); 348 | thread.start(); 349 | } catch (Throwable e) { 350 | Logger.error("Failed to apply forge logging fix!", e); 351 | } 352 | }*/ 353 | } 354 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/InstrumentationCatcher.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | import java.util.Collections; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | public class InstrumentationCatcher { 9 | private static Instrumentation instrumentation; 10 | public static void agentmain(String arg, Instrumentation instrumentation) { 11 | premain(arg, instrumentation); 12 | } 13 | 14 | public static void premain(String arg, Instrumentation instrumentation) { 15 | InstrumentationCatcher.instrumentation = instrumentation; 16 | } 17 | 18 | public static void open(String module, String pkg) { 19 | if (instrumentation != null && JavaVersion.IS_JAVA_9) { 20 | instrumentation.redefineModule( 21 | ModuleLayer.boot().findModule(module).orElseThrow(), 22 | Collections.emptySet(), 23 | Collections.emptyMap(), 24 | Map.of(pkg, Set.of(InstrumentationCatcher.class.getModule())), 25 | Collections.emptySet(), 26 | Collections.emptyMap() 27 | ); 28 | } 29 | } 30 | 31 | public static boolean exists() { 32 | return instrumentation != null; 33 | } 34 | 35 | public static Instrumentation get() { 36 | return instrumentation; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/JavaVersion.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | public interface JavaVersion { 4 | boolean IS_JAVA_9 = Runtime.version().feature() >= 9; 5 | boolean IS_JAVA_16 = Runtime.version().feature() >= 16; 6 | boolean IS_JAVA_17 = Runtime.version().feature() >= 17; 7 | boolean IS_JAVA_21 = Runtime.version().feature() >= 21; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/Logger.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import java.util.Arrays; 4 | 5 | public class Logger { 6 | 7 | private static boolean fullName = false; 8 | 9 | public static void useFullName() { 10 | fullName = true; 11 | } 12 | 13 | public static void useShortName() { 14 | fullName = false; 15 | } 16 | 17 | public static void info(String text, Object... objects) { 18 | System.out.printf((fullName ? Constants.LOG_PREFIX : Constants.LOG_PREFIX_SMALL) + (text) + " %n", objects); 19 | } 20 | 21 | public static void label(String text, Object... objects) { 22 | System.out.append((text + " ").formatted(objects)); 23 | } 24 | 25 | public static void warn(String text, Object... objects) { 26 | System.out.printf((fullName ? Constants.LOG_WARN_PREFIX : Constants.LOG_WARN_PREFIX_SMALL) + (text) + " %n", objects); 27 | } 28 | 29 | public static void error(String text, Object... objects) { 30 | Throwable throwable = null; 31 | if (objects.length > 0 && objects[objects.length - 1] instanceof Throwable x) { 32 | objects = Arrays.copyOf(objects, objects.length - 1); 33 | throwable = x; 34 | } 35 | 36 | System.err.printf((fullName ? Constants.LOG_ERROR_PREFIX : Constants.LOG_ERROR_PREFIX_SMALL) + (text) + " %n", objects); 37 | if (throwable != null) { 38 | try { 39 | throwable.printStackTrace(System.err); 40 | } catch (Throwable e) { 41 | System.err.println("Failed to write exception! Using a fallback!"); 42 | System.err.println("Class: " + throwable.getClass().getName()); 43 | for (var traceElement : e.getStackTrace()) { 44 | System.err.println("\tat " + traceElement); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/eu/pb4/mrpackserver/util/Utils.java: -------------------------------------------------------------------------------- 1 | package eu.pb4.mrpackserver.util; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.reflect.TypeToken; 6 | import eu.pb4.forgefx.ForgeInstallerFix; 7 | import eu.pb4.mrpackserver.format.*; 8 | import eu.pb4.mrpackserver.installer.FileDownloader; 9 | import eu.pb4.mrpackserver.installer.ModrinthModpackLookup; 10 | import eu.pb4.mrpackserver.installer.MrPackInstaller; 11 | import eu.pb4.mrpackserver.launch.Launcher; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.PrintStream; 17 | import java.net.URI; 18 | import java.net.URLEncoder; 19 | import java.net.http.HttpClient; 20 | import java.net.http.HttpRequest; 21 | import java.net.http.HttpResponse; 22 | import java.nio.charset.StandardCharsets; 23 | import java.nio.file.FileSystems; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.security.DigestInputStream; 27 | import java.security.MessageDigest; 28 | import java.util.*; 29 | 30 | public interface Utils { 31 | Gson GSON_MAIN = new GsonBuilder().disableHtmlEscaping().registerTypeHierarchyAdapter(HashData.class, new HashData.Serializer()).create(); 32 | Gson GSON_PRETTY = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().registerTypeHierarchyAdapter(HashData.class, new HashData.Serializer()).create(); 33 | 34 | private static int indexOf(byte[] data, byte[] pattern) { 35 | int j = 0; 36 | 37 | for (int i = 0; i < data.length; i++) { 38 | if (pattern[j] != data[i]) { 39 | j = 0; 40 | } 41 | if (pattern[j] == data[i]) { j++; } 42 | if (j == pattern.length) { 43 | return i - pattern.length + 1; 44 | } 45 | } 46 | return -1; 47 | } 48 | @Nullable 49 | static ModpackInfo resolveModpackInfo(Path currentDir) throws IOException { 50 | var x = resolveModpackInfoInternal(); 51 | if (x != null) { 52 | return x; 53 | } 54 | 55 | return resolveModpackInfoExternal(currentDir); 56 | } 57 | @Nullable 58 | static ModpackInfo resolveModpackInfoInternal() throws IOException { 59 | try (var jarFile = Utils.class.getProtectionDomain().getCodeSource().getLocation().openStream();) { 60 | var bytes = jarFile.readAllBytes(); 61 | var jarStart = indexOf(bytes, new byte[]{0x50, 0x4b, 0x03, 0x04}); 62 | if (jarStart > 0) { 63 | var jsonPart = Arrays.copyOf(bytes, jarStart); 64 | var x = ModpackInfo.read(new String(jsonPart)); 65 | 66 | if (x.isValid()) { 67 | return x; 68 | } 69 | } 70 | } catch (IOException e) { 71 | // ignored 72 | } 73 | 74 | try (var data = Utils.class.getResourceAsStream("/modpack-info.json")) { 75 | var x = ModpackInfo.read(new String(Objects.requireNonNull(data).readAllBytes())); 76 | 77 | if (x.isValid()) { 78 | return x; 79 | } 80 | } catch (NullPointerException e) { 81 | // ignored 82 | } 83 | 84 | return null; 85 | } 86 | @Nullable 87 | static ModpackInfo resolveModpackInfoExternal(Path currentDir) throws IOException { 88 | var possibleLocal = currentDir.resolve("modpack-info.json"); 89 | if (Files.exists(possibleLocal)) { 90 | var x = ModpackInfo.read(Files.readString(possibleLocal)); 91 | 92 | if (x.isValid()) { 93 | return x; 94 | } 95 | } 96 | 97 | var possibleMrpack = currentDir.resolve("local.mrpack"); 98 | if (Files.exists(possibleMrpack)) { 99 | try (var zip = FileSystems.newFileSystem(possibleMrpack)) { 100 | var index = ModpackIndex.read(Files.readString(zip.getPath("modrinth.index.json"))); 101 | if (!index.versionId.isEmpty() && !index.name.isEmpty()) { 102 | var info = new ModpackInfo(); 103 | info.projectId = ""; 104 | info.displayName = index.name; 105 | info.versionId = index.versionId; 106 | info.url = possibleMrpack.toUri(); 107 | return info; 108 | } 109 | 110 | } catch (Throwable e) { 111 | // ignored 112 | } 113 | } 114 | return null; 115 | } 116 | 117 | static InstallResult checkAndSetupModpack(ModpackInfo modpackInfo, InstanceInfo instance, Path currentDir, Path instanceDataDir) throws Throwable { 118 | var mrpackFile = getMrPackFile(modpackInfo, instanceDataDir); 119 | if (mrpackFile == null) { 120 | return null; 121 | } 122 | 123 | try (var zip = FileSystems.newFileSystem(mrpackFile)) { 124 | var index = ModpackIndex.read(Files.readString(zip.getPath("modrinth.index.json"))); 125 | 126 | { // Safety validation 127 | var check = currentDir.toAbsolutePath(); 128 | for (var file : index.files) { 129 | var filePath = check.resolve(file.path).normalize(); 130 | if (!filePath.startsWith(check)) { 131 | Logger.error("Modpack contains files, that are placed outside of server's root! Found '%s'", file.path); 132 | return null; 133 | } 134 | } 135 | } 136 | var hashPath = instanceDataDir.resolve("hashes.json"); 137 | var hashes = new HashMap(); 138 | if (Files.exists(hashPath)) { 139 | hashes = Utils.GSON_MAIN.fromJson(Files.readString(hashPath), new TypeToken>() {}.getType()); 140 | } 141 | var whitelistedDomains = new HashSet(); 142 | whitelistedDomains.addAll(Constants.DEFAULT_WHITELISTED_URLS); 143 | whitelistedDomains.addAll(modpackInfo.whitelistedDomains); 144 | var nonOverwritablePaths = new HashSet(); 145 | nonOverwritablePaths.addAll(Constants.DEFAULT_NON_OVERWRITABLE); 146 | nonOverwritablePaths.addAll(modpackInfo.nonOverwritablePaths); 147 | 148 | var customNonOverwritable = currentDir.resolve(Constants.CUSTOM_NON_OVERWRITABLE_LIST); 149 | try { 150 | if (Files.exists(customNonOverwritable)) { 151 | for (var x : Files.readAllLines(customNonOverwritable)) { 152 | if (x.startsWith("/")) { 153 | x = x.substring(1); 154 | } 155 | if (x.endsWith("/")) { 156 | x = x.substring(0, x.length() - 1); 157 | } 158 | 159 | if (x.isBlank()) { 160 | continue; 161 | } 162 | 163 | nonOverwritablePaths.add(x); 164 | } 165 | } 166 | } catch (Throwable ignored) { 167 | 168 | } 169 | 170 | Logger.info("Starting %s of %s (%s)", hashes.isEmpty() ? "installation" : "update", modpackInfo.getDisplayName(), modpackInfo.getDisplayVersion()); 171 | 172 | var handler = new MrPackInstaller(zip.getPath(""), index, currentDir, instance, hashes, whitelistedDomains, nonOverwritablePaths); 173 | 174 | if (modpackInfo.skipJavaVersionCheck != Boolean.TRUE && !handler.checkJavaVersion()) { 175 | return null; 176 | } 177 | 178 | handler.prepareFolders(); 179 | var localExistingHashes = handler.getLocalFileUpdatedHashes(); 180 | var x = new FileDownloader(); 181 | handler.requestDownloads(x, localExistingHashes); 182 | if (!x.isEmpty()) { 183 | Logger.info("Downloading remote modpack files..."); 184 | var failed = x.downloadFiles(handler.getHashes()); 185 | if (!failed.isEmpty()) { 186 | Logger.error("Failed to download provided files: \n- " + String.join("\n- ", failed)); 187 | return null; 188 | } 189 | Logger.info("Finished downloading remote modpack files!"); 190 | } 191 | handler.extractIncluded(localExistingHashes); 192 | 193 | handler.cleanupLeftoverFiles(localExistingHashes); 194 | 195 | Files.deleteIfExists(hashPath); 196 | Files.writeString(hashPath, Utils.GSON_MAIN.toJson(handler.getHashes())); 197 | 198 | var newInstance = new InstanceInfo(); 199 | newInstance.projectId = modpackInfo.projectId; 200 | newInstance.versionId = modpackInfo.versionId; 201 | newInstance.forceSystemClasspath = handler.getNewLauncher() != null ? handler.forceSystemClasspath() : instance.forceSystemClasspath; 202 | newInstance.runnablePath = Objects.requireNonNullElse(handler.getNewLauncher(), instance.runnablePath); 203 | newInstance.dependencies.putAll(index.dependencies); 204 | 205 | if (handler.getInstaller() != null) { 206 | var installer = handler.getInstaller(); 207 | return new InstallResult(newInstance, createInstallerRunner(installer.name(), currentDir.resolve(installer.path()), installer.args())); 208 | } 209 | 210 | return new InstallResult(newInstance, () -> {}); 211 | } 212 | } 213 | 214 | static Runnable createInstallerRunner(String name, Path path, String[] args) { 215 | return () -> { 216 | var t = new Thread(() -> { 217 | for (var i = 0; i < 10; i ++) { 218 | System.out.println(); 219 | } 220 | Logger.info("Installer finished, but forces the mrpack4server to exit! Start the server again to run it!"); 221 | Logger.info("You should ignore instructions about deleting installer files, as it is not needed!"); 222 | 223 | }); 224 | t.setDaemon(true); 225 | Runtime.getRuntime().addShutdownHook(t); 226 | var oldOut = System.out; 227 | var oldErr = System.err; 228 | System.setOut(new PrintStream(oldOut) { 229 | private static final byte[] M1 = ("You can delete this installer file now if you wish" + System.lineSeparator()).getBytes(); 230 | private static final byte[] M2 = ("A problem installing was detected, install cannot continue" + System.lineSeparator()).getBytes(); 231 | @Override 232 | public void write(byte[] buf) throws IOException { 233 | if (!Arrays.equals(buf, M1) && !Arrays.equals(buf, M2)) { 234 | super.write(buf); 235 | } 236 | } 237 | }); 238 | 239 | Logger.info("Starting %s!", name); 240 | if (!Launcher.launchExec(path, ForgeInstallerFix::new, args)) { 241 | Logger.warn("Failed to execute the installer! See errors above."); 242 | } 243 | System.setOut(oldOut); 244 | System.setErr(oldErr); 245 | Runtime.getRuntime().removeShutdownHook(t); 246 | }; 247 | } 248 | 249 | @Nullable 250 | static Path getMrPackFile(ModpackInfo modpackInfo, Path instanceDataDir) { 251 | if (modpackInfo.url != null && modpackInfo.url.getScheme().equals("file")) { 252 | return Path.of(modpackInfo.url); 253 | } 254 | 255 | var name = "modpack/" + modpackInfo.projectId + "_" + modpackInfo.versionId + ".mrpack"; 256 | 257 | var modpackFile = instanceDataDir.resolve(name); 258 | if (Files.exists(modpackFile)) { 259 | return modpackFile; 260 | } 261 | URI uri; 262 | long size; 263 | String hash; 264 | if (modpackInfo.url != null) { 265 | uri = modpackInfo.url; 266 | size = modpackInfo.size != null ? modpackInfo.size : -1; 267 | hash = modpackInfo.sha512; 268 | } else { 269 | var result = ModrinthModpackLookup.findVersion(modpackInfo.getVersionListUrl(), modpackInfo.projectId, modpackInfo.getDisplayName(), modpackInfo.getDisplayVersion(), true, 270 | x -> x.versionNumber.equals(modpackInfo.versionId) || x.id.equals(modpackInfo.versionId)); 271 | if (result == null) { 272 | return null; 273 | } 274 | uri = result.uri(); 275 | size = result.size(); 276 | hash = result.hashes().get(Constants.MODRINTH_HASH); 277 | } 278 | 279 | try { 280 | Files.createDirectories(modpackFile.getParent()); 281 | var req = createHttpClient().send(createGetRequest(uri), HttpResponse.BodyHandlers.ofInputStream()); 282 | var x = handleDownloadedFile(modpackFile, req.body(), name, size, hash != null ? HashData.read(Constants.DEFAULT_HASH, hash) : null); 283 | if (x != null) { 284 | return modpackFile; 285 | } 286 | } catch (Throwable e) { 287 | Logger.error("Failed to locate source mrpack file!", e); 288 | } 289 | 290 | return null; 291 | } 292 | 293 | 294 | 295 | static @Nullable HashData handleDownloadedFile(Path out, InputStream body, String displayName, long fileSize, @Nullable HashData hashData) { 296 | var tmpOut = out.getParent().resolve(out.getFileName().toString() + ".mrpack4server.tmp"); 297 | try { 298 | Files.deleteIfExists(tmpOut); 299 | } catch (Throwable e) { 300 | Logger.error("Failed to remove leftover temporary file '%s'!", tmpOut, e); 301 | } 302 | boolean success = false; 303 | Logger.info("Downloading file '%s': 0%%", displayName); 304 | try (var file = Files.newOutputStream(tmpOut)) { 305 | var hashType = hashData != null ? hashData.type() : Constants.DEFAULT_HASH; 306 | var stream = new DigestInputStream(body, MessageDigest.getInstance(hashType)); 307 | stream.on(true); 308 | long current = 0; 309 | long lastSent = System.currentTimeMillis(); 310 | 311 | while (true) { 312 | var dat = stream.readNBytes(Math.max(stream.available(), Constants.DOWNLOAD_CHUNK_SIZE)); 313 | if (dat == null || dat.length == 0) { 314 | break; 315 | } 316 | current += dat.length; 317 | file.write(dat); 318 | 319 | if ((System.currentTimeMillis() - lastSent) > Constants.DOWNLOAD_UPDATE_TIME) { 320 | lastSent = System.currentTimeMillis(); 321 | Logger.info("Downloading file '%s': %s%%", displayName, fileSize != -1 ? String.valueOf(current * 100 / fileSize) : '?'); 322 | } 323 | } 324 | file.flush(); 325 | var hash = stream.getMessageDigest().digest(); 326 | if (hashData == null || Arrays.equals(hashData.hash(), hash)) { 327 | success = true; 328 | Logger.info("Finished downloading file '%s' successfully!", displayName); 329 | return new HashData(hashType, hash); 330 | } else { 331 | Logger.warn("Couldn't validate file '%s'! Expected hash: %s, got: %s", displayName, hashData.hashString(), HexFormat.of().formatHex(hash)); 332 | return null; 333 | } 334 | } catch (Throwable e) { 335 | Logger.error("Couldn't download file '%s' correctly!", displayName, e); 336 | return null; 337 | } finally { 338 | try { 339 | if (success) { 340 | Files.move(tmpOut, out); 341 | } else { 342 | Files.deleteIfExists(tmpOut); 343 | } 344 | } catch (Throwable e) { 345 | if (success) { 346 | Logger.error("Failed to move temporary file to correct location '%s'!", out, e); 347 | } else { 348 | Logger.error("Failed to remove temporary file '%s'!", tmpOut, e); 349 | } 350 | } 351 | } 352 | } 353 | 354 | static HttpClient createHttpClient() { 355 | return HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); 356 | } 357 | 358 | static HttpRequest createGetRequest(URI uri) { 359 | return HttpRequest.newBuilder(uri) 360 | .setHeader("User-Agent", Constants.USER_AGENT) 361 | .GET() 362 | .build(); 363 | } 364 | 365 | static void configureModpack(Path runPath) throws IOException, InterruptedException { 366 | var scanner = new Scanner(System.in); 367 | while (true) { 368 | var newInfo = new ModpackInfo(); 369 | String description = ""; 370 | Logger.info("Provide modpack name, it's id or url linking to it."); 371 | Logger.info("Prefix with ? to search."); 372 | Logger.label(">"); 373 | var data = scanner.nextLine(); 374 | boolean requestVersion = false; 375 | 376 | if (data.isBlank()) { 377 | continue; 378 | } 379 | 380 | if (data.startsWith("http://") || data.startsWith("https://")) { 381 | try { 382 | var uri = URI.create(data); 383 | var parts = uri.getPath().split("/"); 384 | 385 | var requestName = false; 386 | if (uri.getPath().endsWith(".mrpack")) { 387 | newInfo.projectId = parts[parts.length - 1].substring(".mrpack".length()); 388 | newInfo.versionId = data; 389 | newInfo.url = uri; 390 | } else if (parts.length == 3 || parts.length == 4) { 391 | newInfo.projectId = parts[2]; 392 | requestVersion = true; 393 | requestName = true; 394 | } else if (parts.length >= 5) { 395 | newInfo.projectId = parts[2]; 396 | newInfo.versionId = parts[4]; 397 | requestName = true; 398 | } else { 399 | Logger.error("Invalid url! %s"); 400 | continue; 401 | } 402 | 403 | if (requestName) { 404 | var client = Utils.createHttpClient(); 405 | var res = client.send(Utils.createGetRequest(URI.create(Constants.MODRINTH_API + "/project/" + newInfo.projectId)), HttpResponse.BodyHandlers.ofString()); 406 | if (res.statusCode() == 200) { 407 | var project = ModrinthProjectData.read(res.body()); 408 | if (!project.projectType.equals("modpack")) { 409 | Logger.error("Project %s (%s) is not a modpack!", project.title, project.slug); 410 | continue; 411 | } 412 | newInfo.displayName = project.title; 413 | description = project.description; 414 | } 415 | } 416 | } catch (Throwable e) { 417 | Logger.error("Invalid url! %s"); 418 | continue; 419 | } 420 | } else { 421 | var client = Utils.createHttpClient(); 422 | HttpResponse res; 423 | if (data.charAt(0) == '?') { 424 | data = data.substring(1); 425 | res = null; 426 | } else { 427 | res = client.send(Utils.createGetRequest(URI.create(Constants.MODRINTH_API + "/project/" + URLEncoder.encode(data, StandardCharsets.UTF_8))), HttpResponse.BodyHandlers.ofString()); 428 | } 429 | boolean foundProject = false; 430 | if (res != null && res.statusCode() == 200) { 431 | var project = ModrinthProjectData.read(res.body()); 432 | if (project.projectType.equals("modpack")) { 433 | newInfo.projectId = project.slug; 434 | newInfo.displayName = project.title; 435 | description = project.description; 436 | requestVersion = true; 437 | foundProject = true; 438 | } 439 | } 440 | 441 | if (!foundProject) { 442 | res = client.send(Utils.createGetRequest(URI.create(Constants.MODRINTH_API + "/search?query=" 443 | + URLEncoder.encode(data, StandardCharsets.UTF_8) + "&facets=[[%22project_type:modpack%22]]&limit=" + Constants.SEARCH_QUERY_MAX_SIZE)), HttpResponse.BodyHandlers.ofString()); 444 | if (res.statusCode() != 200) { 445 | Logger.error("Failed to request Modrinth search!"); 446 | return; 447 | } 448 | var search = ModrinthSearchData.read(res.body()); 449 | 450 | if (search.hits.isEmpty()) { 451 | Logger.warn("Couldn't find any modpacks matching your description!"); 452 | continue; 453 | } 454 | Logger.info("Found Modpacks:"); 455 | 456 | for (int i = 0; i < Math.min(search.hits.size(), Constants.SEARCH_QUERY_MAX_SIZE); i++) { 457 | var modpack = search.hits.get(i); 458 | Logger.info("%s > %s (%s) by %s", i + 1, modpack.title, modpack.slug, modpack.author); 459 | } 460 | boolean selected = false; 461 | while (true) { 462 | Logger.info("Select Modpack by number [1] or 0 to go back:"); 463 | Logger.label(">"); 464 | data = scanner.nextLine(); 465 | int id; 466 | if (data.isEmpty()) { 467 | id = 1; 468 | } else { 469 | try { 470 | id = Integer.parseInt(data); 471 | } catch (Throwable e) { 472 | id = Integer.MAX_VALUE; 473 | } 474 | 475 | if (id > search.hits.size()) { 476 | Logger.error("Invalid id, try again!"); 477 | continue; 478 | } else if (id == 0) { 479 | break; 480 | } 481 | } 482 | selected = true; 483 | var pack = search.hits.get(id - 1); 484 | newInfo.projectId = pack.slug; 485 | newInfo.displayName = pack.title; 486 | description = pack.description; 487 | requestVersion = true; 488 | break; 489 | } 490 | if (!selected) { 491 | continue; 492 | } 493 | } 494 | } 495 | Logger.info("Selected Modpack: %s (%s)", newInfo.getDisplayName(), newInfo.projectId); 496 | 497 | if (description != null && !description.isEmpty()) { 498 | Logger.info("Description: %s", description); 499 | } 500 | 501 | if (requestVersion) { 502 | var versions = ModrinthModpackLookup.getVersions(newInfo.getVersionListUrl(), newInfo.projectId, newInfo.getDisplayName()); 503 | if (versions == null || versions.isEmpty()) { 504 | Logger.error("No versions found for %s (%s)", newInfo.getDisplayName(), newInfo.projectId); 505 | continue; 506 | } 507 | 508 | while (true) { 509 | Logger.info("Select Version [%s]", versions.get(0).versionNumber); 510 | Logger.label(">"); 511 | data = scanner.nextLine(); 512 | if (data.isEmpty()) { 513 | data = versions.get(0).versionNumber; 514 | break; 515 | } 516 | 517 | if (data.equals(";;release") || data.equals(";;beta") || data.equals(";;alpha")) { 518 | break; 519 | } 520 | 521 | ModrinthModpackVersion ver = null; 522 | for (var version : versions) { 523 | if (version.versionNumber.equals(data) || version.name.equals(data) || version.id.equals(data)) { 524 | ver = version; 525 | break; 526 | } 527 | } 528 | 529 | if (ver != null) { 530 | data = ver.versionNumber; 531 | break; 532 | } 533 | Logger.error("%s is not a valid version of this modpack!: ", data); 534 | } 535 | 536 | newInfo.versionId = data; 537 | } 538 | 539 | if (!newInfo.versionId.isEmpty()) { 540 | Logger.info("Selected Version: %s (%s)", newInfo.getDisplayVersion(), newInfo.versionId); 541 | } 542 | 543 | 544 | Files.writeString(runPath.resolve("modpack-info.json"), newInfo.toJson()); 545 | break; 546 | } 547 | } 548 | 549 | record InstallResult(InstanceInfo info, Runnable installer) {} 550 | } 551 | --------------------------------------------------------------------------------