summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--forgewrapper/.github/workflows/build.yml28
-rw-r--r--forgewrapper/.github/workflows/publication.yml60
-rw-r--r--forgewrapper/.gitignore15
-rw-r--r--forgewrapper/LICENSE21
-rw-r--r--forgewrapper/README.md30
-rw-r--r--forgewrapper/build.gradle90
-rw-r--r--forgewrapper/gradle.properties4
-rw-r--r--forgewrapper/gradle/wrapper/gradle-wrapper.jarbin0 -> 59536 bytes
-rw-r--r--forgewrapper/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xforgewrapper/gradlew234
-rw-r--r--forgewrapper/gradlew.bat89
-rw-r--r--forgewrapper/jigsaw/build.gradle28
-rw-r--r--forgewrapper/jigsaw/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java211
-rw-r--r--forgewrapper/settings.gradle3
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java75
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java182
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java74
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java35
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java99
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java52
-rw-r--r--forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java40
-rw-r--r--forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector1
22 files changed, 1376 insertions, 0 deletions
diff --git a/forgewrapper/.github/workflows/build.yml b/forgewrapper/.github/workflows/build.yml
new file mode 100644
index 0000000000..971e891add
--- /dev/null
+++ b/forgewrapper/.github/workflows/build.yml
@@ -0,0 +1,28 @@
+name: Gradle Build
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "8.0.312"
+ architecture: x64
+ - name: Build with Gradle
+ run: |
+ chmod +x ./gradlew
+ ./gradlew build -iS
+ - uses: actions/upload-artifact@v4
+ with:
+ name: Package
+ path: build/libs
diff --git a/forgewrapper/.github/workflows/publication.yml b/forgewrapper/.github/workflows/publication.yml
new file mode 100644
index 0000000000..eeaed812c5
--- /dev/null
+++ b/forgewrapper/.github/workflows/publication.yml
@@ -0,0 +1,60 @@
+name: Publication
+
+on:
+ push:
+ tags:
+ - "*"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: "8.0.312"
+ architecture: x64
+ - name: Build with Gradle
+ env:
+ IS_PUBLICATION: true
+ run: |
+ git clone -b maven https://github.com/ZekerZhayard/ForgeWrapper.git ./build/maven
+ rm -rf ./build/maven/.git/*
+ chmod +x ./gradlew
+ ./gradlew publish -iS
+ - uses: actions/upload-artifact@v4
+ with:
+ name: Package
+ path: build/libs
+ - name: Get tag version
+ id: get_version
+ uses: olegtarasov/get-tag@v2.1
+ - uses: actions/upload-artifact@v4
+ with:
+ name: Package
+ path: build/libs
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ steps.get_version.outputs.tag }}
+ release_name: ${{ steps.get_version.outputs.tag }}
+ body: ""
+ draft: false
+ prerelease: false
+ - name: Upload release binaries
+ uses: alexellis/upload-assets@0.2.2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ asset_paths: '["./build/libs/*"]'
+ - name: Upload to Maven
+ uses: JamesIves/github-pages-deploy-action@4.1.4
+ with:
+ branch: maven
+ folder: build/maven
diff --git a/forgewrapper/.gitignore b/forgewrapper/.gitignore
new file mode 100644
index 0000000000..2bcd13edda
--- /dev/null
+++ b/forgewrapper/.gitignore
@@ -0,0 +1,15 @@
+.gradle
+build
+
+# eclipse
+.settings
+.classpath
+.project
+bin
+
+# idea
+.idea
+*.iml
+*.ipr
+*.iws
+out
diff --git a/forgewrapper/LICENSE b/forgewrapper/LICENSE
new file mode 100644
index 0000000000..712ea35b1e
--- /dev/null
+++ b/forgewrapper/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) ZekerZhayard
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/forgewrapper/README.md b/forgewrapper/README.md
new file mode 100644
index 0000000000..a1e855798b
--- /dev/null
+++ b/forgewrapper/README.md
@@ -0,0 +1,30 @@
+# ForgeWrapper
+
+Allow [MultiMC](https://github.com/MultiMC/MultiMC5) to launch Minecraft 1.13+ with Forge.
+
+**ForgeWrapper has been adopted by MultiMC, you do not need to perform the following steps manually. (2020-03-29)**
+
+## For other launchers
+1. ForgeWrapper provides some java properties since 1.4.2:
+ - `forgewrapper.librariesDir` : a path to libraries folder (e.g. -Dforgewrapper.librariesDir=/home/xxx/.minecraft/libraries)
+ - `forgewrapper.installer` : a path to forge installer (e.g. -Dforgewrapper.installer=/home/xxx/forge-1.14.4-28.2.0-installer.jar)
+ - `forgewrapper.minecraft` : a path to the vanilla minecraft jar (e.g. -Dforgewrapper.minecraft=/home/xxx/.minecraft/versions/1.14.4/1.14.4.jar)
+
+2. ForgeWrapper also provides an interface [`IFileDetector`](https://github.com/ZekerZhayard/ForgeWrapper/blob/master/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java), you can implement it and custom your own detecting rules. To load it, you should make another jar which contains `META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector` within the full implementation class name and add the jar to class path.
+
+## How to use (Outdated)
+
+1. Download Forge installer for Minecraft 1.13+ [here](https://files.minecraftforge.net/).
+2. Download ForgeWrapper jar file at the [release](https://github.com/ZekerZhayard/ForgeWrapper/releases) page.
+3. Since ForgeWrapper 1.5.1, it no longer includes the json converter, so you need to build it by yourself:
+ - [Download](https://github.com/ZekerZhayard/ForgeWrapper/archive/refs/heads/master.zip) ForgeWrapper sources.
+ - Extract the zip and open terminal in the extracted folder.
+ - Run `./gradlew converter:build` command in terminal and get the jar from `./converter/build/libs`
+3. Run the below command in terminal:
+ ```
+ java -jar <ForgeWrapper.jar> --installer=<forge-installer.jar> [--instance=<instance-path>]
+ ```
+ *Notice: If you don't specify a MultiMC instance path, ForgeWrapper will create the instance folder in current working space.*
+
+4. If the instance folder which just created is not in `MultiMC/instances` folder, you just need to move to the `MultiMC/instances` folder.
+5. Run MultiMC, and you will see a new instance named `forge-<mcVersion>-<forgeVersion>`. \ No newline at end of file
diff --git a/forgewrapper/build.gradle b/forgewrapper/build.gradle
new file mode 100644
index 0000000000..244e0555ae
--- /dev/null
+++ b/forgewrapper/build.gradle
@@ -0,0 +1,90 @@
+import java.text.SimpleDateFormat
+
+plugins {
+ id "java"
+ id "eclipse"
+ id "maven-publish"
+}
+
+sourceCompatibility = targetCompatibility = 1.8
+compileJava {
+ sourceCompatibility = targetCompatibility = 1.8
+}
+
+version = "${fw_version}${-> getVersionSuffix()}"
+group = "io.github.zekerzhayard"
+archivesBaseName = rootProject.name
+
+configurations {
+ multirelase {
+ implementation.extendsFrom multirelase
+ }
+}
+
+repositories {
+ mavenCentral()
+ maven {
+ name = "forge"
+ url = "https://maven.minecraftforge.net/"
+ }
+}
+
+dependencies {
+ compileOnly "com.google.code.gson:gson:2.8.7"
+ compileOnly "cpw.mods:modlauncher:8.0.9"
+ compileOnly "net.minecraftforge:installer:2.2.7"
+ compileOnly "net.sf.jopt-simple:jopt-simple:5.0.4"
+
+ multirelase project(":jigsaw")
+}
+
+java {
+ withSourcesJar()
+}
+
+jar {
+ manifest.attributes([
+ "Specification-Title": "${project.name}",
+ "Specification-Vendor": "ZekerZhayard",
+ "Specification-Version": "${project.version}".split("-")[0],
+ "Implementation-Title": "${project.name}",
+ "Implementation-Version": "${project.version}",
+ "Implementation-Vendor" :"ZekerZhayard",
+ "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
+ "Automatic-Module-Name": "${project.group}.${project.archivesBaseName}".toString().toLowerCase(),
+ "Multi-Release": "true",
+ "GitCommit": String.valueOf(System.getenv("GITHUB_SHA"))
+ ])
+
+ into "META-INF/versions/9", {
+ from configurations.multirelase.files.collect {
+ zipTree(it)
+ }
+ exclude "META-INF/**"
+ }
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ groupId "${project.group}"
+ artifactId "${project.archivesBaseName}"
+ version "${project.version}"
+
+ from components.java
+ }
+ }
+ repositories {
+ maven {
+ url = layout.buildDirectory.dir("maven")
+ }
+ }
+}
+tasks.publish.dependsOn build
+
+static String getVersionSuffix() {
+ if (System.getenv("IS_PUBLICATION") != null || System.getenv("GITHUB_ACTIONS") == "true")
+ return new SimpleDateFormat("-yyyy-MM-dd").format(new Date())
+
+ return "-LOCAL"
+}
diff --git a/forgewrapper/gradle.properties b/forgewrapper/gradle.properties
new file mode 100644
index 0000000000..e2b1ac4ba7
--- /dev/null
+++ b/forgewrapper/gradle.properties
@@ -0,0 +1,4 @@
+
+org.gradle.daemon = false
+
+fw_version = prism
diff --git a/forgewrapper/gradle/wrapper/gradle-wrapper.jar b/forgewrapper/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..7454180f2a
--- /dev/null
+++ b/forgewrapper/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/forgewrapper/gradle/wrapper/gradle-wrapper.properties b/forgewrapper/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..669386b870
--- /dev/null
+++ b/forgewrapper/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/forgewrapper/gradlew b/forgewrapper/gradlew
new file mode 100755
index 0000000000..1b6c787337
--- /dev/null
+++ b/forgewrapper/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/forgewrapper/gradlew.bat b/forgewrapper/gradlew.bat
new file mode 100644
index 0000000000..107acd32c4
--- /dev/null
+++ b/forgewrapper/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/forgewrapper/jigsaw/build.gradle b/forgewrapper/jigsaw/build.gradle
new file mode 100644
index 0000000000..a555ec3bf3
--- /dev/null
+++ b/forgewrapper/jigsaw/build.gradle
@@ -0,0 +1,28 @@
+
+plugins {
+ id "java"
+ id "eclipse"
+}
+
+compileJava {
+ if (JavaVersion.current() < JavaVersion.VERSION_1_9) {
+ javaCompiler = javaToolchains.compilerFor {
+ languageVersion = JavaLanguageVersion.of(9)
+ }
+ }
+ sourceCompatibility = 9
+ targetCompatibility = 9
+}
+
+configurations {
+ apiElements {
+ attributes {
+ attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8
+ }
+ }
+ runtimeElements {
+ attributes {
+ attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8
+ }
+ }
+}
diff --git a/forgewrapper/jigsaw/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java b/forgewrapper/jigsaw/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java
new file mode 100644
index 0000000000..09792b1624
--- /dev/null
+++ b/forgewrapper/jigsaw/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java
@@ -0,0 +1,211 @@
+package io.github.zekerzhayard.forgewrapper.installer.util;
+
+import java.io.File;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.module.Configuration;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.lang.module.ResolvedModule;
+import java.lang.reflect.Field;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import sun.misc.Unsafe;
+
+public class ModuleUtil {
+ private final static MethodHandles.Lookup IMPL_LOOKUP = getImplLookup();
+
+ private static MethodHandles.Lookup getImplLookup() {
+ try {
+ // Get theUnsafe
+ Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ Unsafe unsafe = (Unsafe) unsafeField.get(null);
+
+ // Get IMPL_LOOKUP
+ Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
+ return (MethodHandles.Lookup) unsafe.getObject(unsafe.staticFieldBase(implLookupField), unsafe.staticFieldOffset(implLookupField));
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * add module-path at runtime
+ */
+ @SuppressWarnings("unchecked")
+ public static void addModules(String modulePath) throws Throwable {
+ // Find all extra modules, exclude all existing modules
+ ModuleFinder finder = ModuleFinder.of(Stream.of(modulePath.split(File.pathSeparator)).map(Paths::get).filter(p -> ModuleFinder.of(p).findAll().stream().noneMatch(mref -> ModuleLayer.boot().findModule(mref.descriptor().name()).isPresent())).toArray(Path[]::new));
+ MethodHandle loadModuleMH = IMPL_LOOKUP.findVirtual(Class.forName("jdk.internal.loader.BuiltinClassLoader"), "loadModule", MethodType.methodType(void.class, ModuleReference.class));
+
+ // Resolve modules to a new config and load all extra modules in system class loader (unnamed modules for now)
+ List<String> roots = new ArrayList<>();
+ for (ModuleReference mref : finder.findAll()) {
+ String name = mref.descriptor().name();
+ if (!ModuleLayer.boot().findModule(name).isPresent()) {
+ loadModuleMH.invokeWithArguments(ClassLoader.getSystemClassLoader(), mref);
+ roots.add(name);
+ }
+ }
+ Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, roots);
+
+ // Copy the new config graph to boot module layer config
+ MethodHandle graphGetter = IMPL_LOOKUP.findGetter(Configuration.class, "graph", Map.class);
+ HashMap<ResolvedModule, Set<ResolvedModule>> graphMap = new HashMap<>((Map<ResolvedModule, Set<ResolvedModule>>) graphGetter.invokeWithArguments(config));
+ MethodHandle cfSetter = IMPL_LOOKUP.findSetter(ResolvedModule.class, "cf", Configuration.class);
+ // Reset all extra resolved modules config to boot module layer config
+ for (Map.Entry<ResolvedModule, Set<ResolvedModule>> entry : graphMap.entrySet()) {
+ cfSetter.invokeWithArguments(entry.getKey(), ModuleLayer.boot().configuration());
+ for (ResolvedModule resolvedModule : entry.getValue()) {
+ cfSetter.invokeWithArguments(resolvedModule, ModuleLayer.boot().configuration());
+ }
+ }
+ graphMap.putAll((Map<ResolvedModule, Set<ResolvedModule>>) graphGetter.invokeWithArguments(ModuleLayer.boot().configuration()));
+ IMPL_LOOKUP.findSetter(Configuration.class, "graph", Map.class).invokeWithArguments(ModuleLayer.boot().configuration(), new HashMap<>(graphMap));
+
+ // Reset boot module layer resolved modules as new config resolved modules to prepare define modules
+ Set<ResolvedModule> oldBootModules = ModuleLayer.boot().configuration().modules();
+ MethodHandle modulesSetter = IMPL_LOOKUP.findSetter(Configuration.class, "modules", Set.class);
+ HashSet<ResolvedModule> modulesSet = new HashSet<>(config.modules());
+ modulesSetter.invokeWithArguments(ModuleLayer.boot().configuration(), new HashSet<>(modulesSet));
+
+ // Prepare to add all the new config "nameToModule" to boot module layer config
+ MethodHandle nameToModuleGetter = IMPL_LOOKUP.findGetter(Configuration.class, "nameToModule", Map.class);
+ HashMap<String, ResolvedModule> nameToModuleMap = new HashMap<>((Map<String, ResolvedModule>) nameToModuleGetter.invokeWithArguments(ModuleLayer.boot().configuration()));
+ nameToModuleMap.putAll((Map<String, ResolvedModule>) nameToModuleGetter.invokeWithArguments(config));
+ IMPL_LOOKUP.findSetter(Configuration.class, "nameToModule", Map.class).invokeWithArguments(ModuleLayer.boot().configuration(), new HashMap<>(nameToModuleMap));
+
+ // Define all extra modules and add all the new config "nameToModule" to boot module layer config
+ ((Map<String, Module>) IMPL_LOOKUP.findGetter(ModuleLayer.class, "nameToModule", Map.class).invokeWithArguments(ModuleLayer.boot())).putAll((Map<String, Module>) IMPL_LOOKUP.findStatic(Module.class, "defineModules", MethodType.methodType(Map.class, Configuration.class, Function.class, ModuleLayer.class)).invokeWithArguments(ModuleLayer.boot().configuration(), (Function<String, ClassLoader>) name -> ClassLoader.getSystemClassLoader(), ModuleLayer.boot()));
+
+ // Add all of resolved modules
+ modulesSet.addAll(oldBootModules);
+ modulesSetter.invokeWithArguments(ModuleLayer.boot().configuration(), new HashSet<>(modulesSet));
+
+ // Reset cache of boot module layer
+ IMPL_LOOKUP.findSetter(ModuleLayer.class, "modules", Set.class).invokeWithArguments(ModuleLayer.boot(), null);
+ IMPL_LOOKUP.findSetter(ModuleLayer.class, "servicesCatalog", Class.forName("jdk.internal.module.ServicesCatalog")).invokeWithArguments(ModuleLayer.boot(), null);
+
+ // Add reads from extra modules to jdk modules
+ MethodHandle implAddReadsMH = IMPL_LOOKUP.findVirtual(Module.class, "implAddReads", MethodType.methodType(void.class, Module.class));
+ for (ResolvedModule resolvedModule : config.modules()) {
+ Module module = ModuleLayer.boot().findModule(resolvedModule.name()).orElse(null);
+ if (module != null) {
+ for (ResolvedModule bootResolvedModule : oldBootModules) {
+ Module bootModule = ModuleLayer.boot().findModule(bootResolvedModule.name()).orElse(null);
+ if (bootModule != null) {
+ implAddReadsMH.invokeWithArguments(module, bootModule);
+ }
+ }
+ }
+ }
+ }
+
+ public static void addExports(List<String> exports) {
+ TypeToAdd.EXPORTS.implAdd(exports);
+ }
+
+ public static void addOpens(List<String> opens) {
+ TypeToAdd.OPENS.implAdd(opens);
+ }
+
+ public static ClassLoader getPlatformClassLoader() {
+ return ClassLoader.getPlatformClassLoader();
+ }
+
+ private enum TypeToAdd {
+ EXPORTS("Exports"),
+ OPENS("Opens");
+
+ private final MethodHandle implAddMH;
+ private final MethodHandle implAddToAllUnnamedMH;
+
+ TypeToAdd(String name) {
+ try {
+ this.implAddMH = IMPL_LOOKUP.findVirtual(Module.class, "implAdd" + name, MethodType.methodType(void.class, String.class, Module.class));
+ this.implAddToAllUnnamedMH = IMPL_LOOKUP.findVirtual(Module.class, "implAdd" + name + "ToAllUnnamed", MethodType.methodType(void.class, String.class));
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ void implAdd(List<String> extras) {
+ for (String extra : extras) {
+ ParserData data = ModuleUtil.parseModuleExtra(extra).orElse(null);
+ if (data != null) {
+ Module module = ModuleLayer.boot().findModule(data.module).orElse(null);
+ if (module != null) {
+ try {
+ if ("ALL-UNNAMED".equals(data.target)) {
+ this.implAddToAllUnnamedMH.invokeWithArguments(module, data.packages);
+ } else {
+ Module targetModule = ModuleLayer.boot().findModule(data.target).orElse(null);
+ if (targetModule != null) {
+ this.implAddMH.invokeWithArguments(module, data.packages, targetModule);
+ }
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // <module>/<package>=<target>
+ private static Optional<ParserData> parseModuleExtra(String extra) {
+ String[] all = extra.split("=", 2);
+ if (all.length < 2) {
+ return Optional.empty();
+ }
+
+ String[] source = all[0].split("/", 2);
+ if (source.length < 2) {
+ return Optional.empty();
+ }
+ return Optional.of(new ParserData(source[0], source[1], all[1]));
+ }
+
+ private static class ParserData {
+ final String module;
+ final String packages;
+ final String target;
+
+ ParserData(String module, String packages, String target) {
+ this.module = module;
+ this.packages = packages;
+ this.target = target;
+ }
+ }
+
+ public static void setupClassPath(Path libraryDir, List<String> paths) throws Throwable {
+ Class<?> urlClassPathClass = Class.forName("jdk.internal.loader.URLClassPath");
+ Object ucp = IMPL_LOOKUP.findGetter(Class.forName("jdk.internal.loader.BuiltinClassLoader"), "ucp", urlClassPathClass).invokeWithArguments(ClassLoader.getSystemClassLoader());
+ MethodHandle addURLMH = IMPL_LOOKUP.findVirtual(urlClassPathClass, "addURL", MethodType.methodType(void.class, URL.class));
+ for (String path : paths) {
+ addURLMH.invokeWithArguments(ucp, libraryDir.resolve(path).toUri().toURL());
+ }
+ }
+
+ // ForgeWrapper need some extra settings to invoke BootstrapLauncher.
+ public static Class<?> setupBootstrapLauncher(Class<?> mainClass) throws Throwable {
+ if (!mainClass.getModule().isOpen(mainClass.getPackageName(), ModuleUtil.class.getModule())) {
+ TypeToAdd.OPENS.implAddMH.invokeWithArguments(mainClass.getModule(), mainClass.getPackageName(), ModuleUtil.class.getModule());
+ }
+ return mainClass;
+ }
+}
diff --git a/forgewrapper/settings.gradle b/forgewrapper/settings.gradle
new file mode 100644
index 0000000000..3444232309
--- /dev/null
+++ b/forgewrapper/settings.gradle
@@ -0,0 +1,3 @@
+rootProject.name = 'ForgeWrapper'
+
+include 'jigsaw'
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java
new file mode 100644
index 0000000000..88c3a00742
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Bootstrap.java
@@ -0,0 +1,75 @@
+package io.github.zekerzhayard.forgewrapper.installer;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.github.zekerzhayard.forgewrapper.installer.util.ModuleUtil;
+
+public class Bootstrap {
+ public static void bootstrap(String[] jvmArgs, String minecraftJar, String libraryDir) throws Throwable {
+ // Replace all placeholders
+ String[] replacedJvmArgs = new String[jvmArgs.length];
+ for (int i = 0; i < jvmArgs.length; i++) {
+ String arg = jvmArgs[i];
+ replacedJvmArgs[i] = arg.replace("${classpath}", System.getProperty("java.class.path").replace(File.separator, "/")).replace("${classpath_separator}", File.pathSeparator).replace("${library_directory}", libraryDir).replace("${version_name}", minecraftJar.substring(0, minecraftJar.lastIndexOf('.')));
+ }
+ jvmArgs = replacedJvmArgs;
+
+ // Remove NewLaunch.jar from property to prevent Forge from adding it to the module path
+ StringBuilder newCP = new StringBuilder();
+ for (String path : System.getProperty("java.class.path").split(File.pathSeparator)) {
+ if (!path.endsWith("NewLaunch.jar")) {
+ newCP.append(path).append(File.pathSeparator);
+ }
+ }
+ System.setProperty("java.class.path", newCP.substring(0, newCP.length() - 1));
+
+ String modulePath = null;
+ List<String> addExports = new ArrayList<>();
+ List<String> addOpens = new ArrayList<>();
+ for (int i = 0; i < jvmArgs.length; i++) {
+ String arg = jvmArgs[i];
+
+ if (arg.equals("-p") || arg.equals("--module-path")) {
+ modulePath = jvmArgs[i + 1];
+ } else if (arg.startsWith("--module-path=")) {
+ modulePath = arg.split("=", 2)[1];
+ }
+
+ if (arg.equals("--add-exports")) {
+ addExports.add(jvmArgs[i + 1]);
+ } else if (arg.startsWith("--add-exports=")) {
+ addExports.add(arg.split("=", 2)[1]);
+ }
+
+ if (arg.equals("--add-opens")) {
+ addOpens.add(jvmArgs[i + 1]);
+ } else if (arg.startsWith("--add-opens=")) {
+ addOpens.add(arg.split("=", 2)[1]);
+ }
+
+ // Java properties
+ if (arg.startsWith("-D")) {
+ String[] prop = arg.substring(2).split("=", 2);
+
+ if (prop[0].equals("ignoreList")) {
+ // The default ignoreList may cause some problems, so we define it more precisely.
+ System.setProperty(prop[0], prop[1] + ",NewLaunch.jar,ForgeWrapper-," + minecraftJar);
+ } else {
+ System.setProperty(prop[0], prop[1]);
+ }
+ }
+ }
+
+ if (System.getProperty("libraryDirectory") == null) {
+ System.setProperty("libraryDirectory", libraryDir);
+ }
+
+ if (modulePath != null) {
+ ModuleUtil.addModules(modulePath);
+ }
+ ModuleUtil.addExports(addExports);
+ ModuleUtil.addOpens(addOpens);
+ }
+}
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java
new file mode 100644
index 0000000000..98b9d1afa3
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java
@@ -0,0 +1,182 @@
+package io.github.zekerzhayard.forgewrapper.installer;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.minecraftforge.installer.DownloadUtils;
+import net.minecraftforge.installer.actions.PostProcessors;
+import net.minecraftforge.installer.actions.ProgressCallback;
+import net.minecraftforge.installer.json.Artifact;
+import net.minecraftforge.installer.json.Install;
+import net.minecraftforge.installer.json.InstallV1;
+import net.minecraftforge.installer.json.Util;
+import net.minecraftforge.installer.json.Version;
+
+public class Installer {
+ private static InstallV1Wrapper wrapper;
+ private static InstallV1Wrapper getWrapper(File librariesDir) {
+ if (wrapper == null) {
+ wrapper = new InstallV1Wrapper(Util.loadInstallProfile(), librariesDir);
+ }
+ return wrapper;
+ }
+
+ public static Map<String, Object> getData(File librariesDir) {
+ Map<String, Object> data = new HashMap<>();
+ Version0 version = Version0.loadVersion(getWrapper(librariesDir));
+ data.put("mainClass", version.getMainClass());
+ data.put("jvmArgs", version.getArguments().getJvm());
+ data.put("extraLibraries", getExtraLibraries(version));
+ return data;
+ }
+
+ public static boolean install(File libraryDir, File minecraftJar, File installerJar) throws Throwable {
+ ProgressCallback monitor = ProgressCallback.withOutputs(System.out);
+ if (System.getProperty("java.net.preferIPv4Stack") == null) {
+ System.setProperty("java.net.preferIPv4Stack", "true");
+ }
+ String vendor = System.getProperty("java.vendor", "missing vendor");
+ String javaVersion = System.getProperty("java.version", "missing java version");
+ String jvmVersion = System.getProperty("java.vm.version", "missing jvm version");
+ monitor.message(String.format("JVM info: %s - %s - %s", vendor, javaVersion, jvmVersion));
+ monitor.message("java.net.preferIPv4Stack=" + System.getProperty("java.net.preferIPv4Stack"));
+ monitor.message("Current Time: " + new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()));
+
+ // MinecraftForge has removed all old installers since 2024/2/27, but they still exist in NeoForge.
+ PostProcessors processors = new PostProcessors(wrapper, true, monitor);
+ Method processMethod = PostProcessors.class.getMethod("process", File.class, File.class, File.class, File.class);
+ if (boolean.class.equals(processMethod.getReturnType())) {
+ return (boolean) processMethod.invoke(processors, libraryDir, minecraftJar, libraryDir.getParentFile(), installerJar);
+ } else {
+ return processMethod.invoke(processors, libraryDir, minecraftJar, libraryDir.getParentFile(), installerJar) != null;
+ }
+ }
+
+ // Some libraries in the version json are not available via direct download,
+ // so they are not available in the MultiMC meta json,
+ // so wee need to get them manually.
+ private static List<String> getExtraLibraries(Version0 version) {
+ List<String> paths = new ArrayList<>();
+ for (Version.Library library : version.getLibraries()) {
+ Version.LibraryDownload artifact = library.getDownloads().getArtifact();
+ if (artifact.getUrl().isEmpty()) {
+ paths.add(artifact.getPath());
+ }
+ }
+ return paths;
+ }
+
+ public static class InstallV1Wrapper extends InstallV1 {
+ protected Map<String, List<Processor>> processors = new HashMap<>();
+ protected File librariesDir;
+
+ public InstallV1Wrapper(InstallV1 v1, File librariesDir) {
+ super(v1);
+ this.serverJarPath = v1.getServerJarPath();
+ this.librariesDir = librariesDir;
+ }
+
+ @Override
+ public List<Processor> getProcessors(String side) {
+ List<Processor> processor = this.processors.get(side);
+ if (processor == null) {
+ checkProcessorFiles(processor = super.getProcessors(side), super.getData("client".equals(side)), this.librariesDir);
+ this.processors.put(side, processor);
+ }
+ // It can also be defined by JVM argument "-Dforgewrapper.skipHashCheck=true".
+ if (Boolean.getBoolean("forgewrapper.skipHashCheck")){
+ processor.forEach(proc -> proc.getOutputs().clear());
+ }
+ return processor;
+ }
+
+ private static void checkProcessorFiles(List<Processor> processors, Map<String, String> data, File base) {
+ Map<String, File> artifactData = new HashMap<>();
+ for (Map.Entry<String, String> entry : data.entrySet()) {
+ String value = entry.getValue();
+ if (value.charAt(0) == '[' && value.charAt(value.length() - 1) == ']') {
+ artifactData.put("{" + entry.getKey() + "}", Artifact.from(value.substring(1, value.length() - 1)).getLocalPath(base));
+ }
+ }
+
+ Map<Processor, Map<String, String>> outputsMap = new HashMap<>();
+ label:
+ for (Processor processor : processors) {
+ Map<String, String> outputs = new HashMap<>();
+ if (processor.getOutputs().isEmpty()) {
+ String[] args = processor.getArgs();
+ for (int i = 0; i < args.length; i++) {
+ for (Map.Entry<String, File> entry : artifactData.entrySet()) {
+ if (args[i].contains(entry.getKey())) {
+ // We assume that all files that exist but don't have the sha1 checksum are valid.
+ if (entry.getValue().exists()) {
+ outputs.put(entry.getKey(), DownloadUtils.getSha1(entry.getValue()));
+ } else {
+ outputsMap.clear();
+ break label;
+ }
+ }
+ }
+ }
+ outputsMap.put(processor, outputs);
+ }
+ }
+ for (Map.Entry<Processor, Map<String, String>> entry : outputsMap.entrySet()) {
+ setOutputs(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static Field outputsField;
+ private static void setOutputs(Processor processor, Map<String, String> outputs) {
+ try {
+ if (outputsField == null) {
+ outputsField = Processor.class.getDeclaredField("outputs");
+ outputsField.setAccessible(true);
+ }
+ outputsField.set(processor, outputs);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+
+ public static class Version0 extends Version {
+
+ public static Version0 loadVersion(Install profile) {
+ try (InputStream stream = Util.class.getResourceAsStream(profile.getJson())) {
+ return Util.GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Version0.class);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ protected String mainClass;
+ protected Version0.Arguments arguments;
+
+ public String getMainClass() {
+ return mainClass;
+ }
+
+ public Version0.Arguments getArguments() {
+ return arguments;
+ }
+
+ public static class Arguments {
+ protected String[] jvm;
+
+ public String[] getJvm() {
+ return jvm;
+ }
+ }
+ }
+}
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java
new file mode 100644
index 0000000000..2c0f4cfc29
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java
@@ -0,0 +1,74 @@
+package io.github.zekerzhayard.forgewrapper.installer;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import io.github.zekerzhayard.forgewrapper.installer.detector.DetectorLoader;
+import io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector;
+import io.github.zekerzhayard.forgewrapper.installer.util.ModuleUtil;
+
+public class Main {
+ @SuppressWarnings("unchecked")
+ public static void main(String[] args) throws Throwable {
+ // --fml.neoForgeVersion 20.2.20-beta --fml.fmlVersion 1.0.2 --fml.mcVersion 1.20.2 --fml.neoFormVersion 20231019.002635 --launchTarget forgeclient
+
+ List<String> argsList = Stream.of(args).collect(Collectors.toList());
+ // NOTE: this is only true for NeoForge versions past 20.2.x
+ // early versions of NeoForge (for 1.20.1) are not supposed to be covered here
+ boolean isNeoForge = argsList.contains("--fml.neoForgeVersion");
+
+ String mcVersion = argsList.get(argsList.indexOf("--fml.mcVersion") + 1);
+ String forgeGroup = argsList.contains("--fml.forgeGroup") ? argsList.get(argsList.indexOf("--fml.forgeGroup") + 1) : "net.neoforged";
+ String forgeArtifact = isNeoForge ? "neoforge" : "forge";
+ String forgeVersionKey = isNeoForge ? "--fml.neoForgeVersion" : "--fml.forgeVersion";
+ String forgeVersion = argsList.get(argsList.indexOf(forgeVersionKey) + 1);
+ String forgeFullVersion = isNeoForge ? forgeVersion : mcVersion + "-" + forgeVersion;
+
+ IFileDetector detector = DetectorLoader.loadDetector();
+ // Check installer jar.
+ Path installerJar = detector.getInstallerJar(forgeGroup, forgeArtifact, forgeFullVersion);
+ if (!isFile(installerJar)) {
+ throw new RuntimeException("Unable to detect the forge installer!");
+ }
+
+ // Check vanilla Minecraft jar.
+ Path minecraftJar = detector.getMinecraftJar(mcVersion);
+ if (!isFile(minecraftJar)) {
+ throw new RuntimeException("Unable to detect the Minecraft jar!");
+ }
+
+ try (URLClassLoader ucl = URLClassLoader.newInstance(new URL[] {
+ Main.class.getProtectionDomain().getCodeSource().getLocation(),
+ installerJar.toUri().toURL()
+ }, ModuleUtil.getPlatformClassLoader())) {
+ Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer");
+
+ Map<String, Object> data = (Map<String, Object>) installer.getMethod("getData", File.class).invoke(null, detector.getLibraryDir().toFile());
+ try {
+ Bootstrap.bootstrap((String[]) data.get("jvmArgs"), detector.getMinecraftJar(mcVersion).getFileName().toString(), detector.getLibraryDir().toAbsolutePath().toString());
+ } catch (Throwable t) {
+ // Avoid this bunch of hacks that nuke the whole wrapper.
+ t.printStackTrace();
+ }
+
+ if (!((boolean) installer.getMethod("install", File.class, File.class, File.class).invoke(null, detector.getLibraryDir().toFile(), minecraftJar.toFile(), installerJar.toFile()))) {
+ return;
+ }
+
+ ModuleUtil.setupClassPath(detector.getLibraryDir(), (List<String>) data.get("extraLibraries"));
+ Class<?> mainClass = ModuleUtil.setupBootstrapLauncher(Class.forName((String) data.get("mainClass")));
+ mainClass.getMethod("main", String[].class).invoke(null, new Object[] {args});
+ }
+ }
+
+ private static boolean isFile(Path path) {
+ return path != null && Files.isRegularFile(path);
+ }
+}
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java
new file mode 100644
index 0000000000..369c4b555d
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java
@@ -0,0 +1,35 @@
+package io.github.zekerzhayard.forgewrapper.installer.detector;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+public class DetectorLoader {
+ public static IFileDetector loadDetector() {
+ ServiceLoader<IFileDetector> sl = ServiceLoader.load(IFileDetector.class);
+ HashMap<String, IFileDetector> detectors = new HashMap<>();
+ for (IFileDetector detector : sl) {
+ detectors.put(detector.name(), detector);
+ }
+
+ boolean enabled = false;
+ IFileDetector temp = null;
+ for (Map.Entry<String, IFileDetector> detector : detectors.entrySet()) {
+ HashMap<String, IFileDetector> others = new HashMap<>(detectors);
+ others.remove(detector.getKey());
+ if (!enabled) {
+ enabled = detector.getValue().enabled(others);
+ if (enabled) {
+ temp = detector.getValue();
+ }
+ } else if (detector.getValue().enabled(others)) {
+ throw new RuntimeException("There are two or more file detectors are enabled! (" + temp.toString() + ", " + detector.toString() + ")");
+ }
+ }
+
+ if (temp == null) {
+ throw new RuntimeException("No file detector is enabled!");
+ }
+ return temp;
+ }
+}
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java
new file mode 100644
index 0000000000..6f695ff884
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java
@@ -0,0 +1,99 @@
+package io.github.zekerzhayard.forgewrapper.installer.detector;
+
+import java.net.URL;
+import java.net.URISyntaxException;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+
+public interface IFileDetector {
+ /**
+ * @return The name of the detector.
+ */
+ String name();
+
+ /**
+ * If there are two or more detectors are enabled, an exception will be thrown. Removing anything from the map is in vain.
+ * @param others Other detectors.
+ * @return True represents enabled.
+ */
+ boolean enabled(HashMap<String, IFileDetector> others);
+
+ /**
+ * @return The ".minecraft/libraries" folder for normal. It can also be defined by JVM argument "-Dforgewrapper.librariesDir=&lt;libraries-path&gt;".
+ */
+ default Path getLibraryDir() {
+ String libraryDir = System.getProperty("forgewrapper.librariesDir");
+ if (libraryDir != null) {
+ return Paths.get(libraryDir).toAbsolutePath();
+ }
+ try {
+ URL launcherLocation = null;
+ String[] classNames = {
+ "cpw/mods/modlauncher/Launcher.class",
+ "net/neoforged/fml/loading/FMLLoader.class"
+ };
+
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ for (String classResource : classNames) {
+ URL url = cl.getResource(classResource);
+ if (url != null) {
+ String path = url.toString();
+ if (path.startsWith("jar:") && path.contains("!")) {
+ path = path.substring(4, path.indexOf('!'));
+ try {
+ launcherLocation = new URL(path);
+ break;
+ } catch (MalformedURLException e) {
+ // ignore and try next
+ }
+ }
+ }
+ }
+
+ if (launcherLocation == null) {
+ throw new UnsupportedOperationException("Could not detect the libraries folder - it can be manually specified with `-Dforgewrapper.librariesDir=` (Java runtime argument)");
+ }
+ Path launcher = Paths.get(launcherLocation.toURI());
+
+ while (!launcher.getFileName().toString().equals("libraries")) {
+ launcher = launcher.getParent();
+
+ if (launcher == null || launcher.getFileName() == null) {
+ throw new UnsupportedOperationException("Could not detect the libraries folder - it can be manually specified with `-Dforgewrapper.librariesDir=` (Java runtime argument)");
+ }
+ }
+
+ return launcher.toAbsolutePath();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * @param forgeGroup Forge package group (e.g. net.minecraftforge).
+ * @param forgeArtifact Forge package artifact (e.g. forge).
+ * @param forgeFullVersion Forge full version (e.g. 1.14.4-28.2.0).
+ * @return The forge installer jar path. It can also be defined by JVM argument "-Dforgewrapper.installer=&lt;installer-path&gt;".
+ */
+ default Path getInstallerJar(String forgeGroup, String forgeArtifact, String forgeFullVersion) {
+ String installer = System.getProperty("forgewrapper.installer");
+ if (installer != null) {
+ return Paths.get(installer).toAbsolutePath();
+ }
+ return null;
+ }
+
+ /**
+ * @param mcVersion Minecraft version (e.g. 1.14.4).
+ * @return The minecraft client jar path. It can also be defined by JVM argument "-Dforgewrapper.minecraft=&lt;minecraft-path&gt;".
+ */
+ default Path getMinecraftJar(String mcVersion) {
+ String minecraft = System.getProperty("forgewrapper.minecraft");
+ if (minecraft != null) {
+ return Paths.get(minecraft).toAbsolutePath();
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java
new file mode 100644
index 0000000000..a1216f0947
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java
@@ -0,0 +1,52 @@
+package io.github.zekerzhayard.forgewrapper.installer.detector;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+
+public class MultiMCFileDetector implements IFileDetector {
+ protected Path libraryDir = null;
+ protected Path installerJar = null;
+ protected Path minecraftJar = null;
+
+ @Override
+ public String name() {
+ return "MultiMC";
+ }
+
+ @Override
+ public boolean enabled(HashMap<String, IFileDetector> others) {
+ return others.size() == 0;
+ }
+
+ @Override
+ public Path getLibraryDir() {
+ if (this.libraryDir == null) {
+ this.libraryDir = IFileDetector.super.getLibraryDir();
+ }
+ return this.libraryDir;
+ }
+
+ @Override
+ public Path getInstallerJar(String forgeGroup, String forgeArtifact, String forgeFullVersion) {
+ Path path = IFileDetector.super.getInstallerJar(forgeGroup, forgeArtifact, forgeFullVersion);
+ if (path == null) {
+ if (this.installerJar == null) {
+ Path installerBase = this.getLibraryDir();
+ for (String dir : forgeGroup.split("\\."))
+ installerBase = installerBase.resolve(dir);
+ this.installerJar = installerBase.resolve(forgeArtifact).resolve(forgeFullVersion).resolve(forgeArtifact + "-" + forgeFullVersion + "-installer.jar").toAbsolutePath();
+ }
+ return this.installerJar;
+ }
+ return path;
+ }
+
+ @Override
+ public Path getMinecraftJar(String mcVersion) {
+ Path path = IFileDetector.super.getMinecraftJar(mcVersion);
+ if (path == null) {
+ return this.minecraftJar != null ? this.minecraftJar : (this.minecraftJar = this.getLibraryDir().resolve("com").resolve("mojang").resolve("minecraft").resolve(mcVersion).resolve("minecraft-" + mcVersion + "-client.jar").toAbsolutePath());
+ }
+ return path;
+ }
+}
diff --git a/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java
new file mode 100644
index 0000000000..9eee574cf3
--- /dev/null
+++ b/forgewrapper/src/main/java/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.java
@@ -0,0 +1,40 @@
+package io.github.zekerzhayard.forgewrapper.installer.util;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+
+public class ModuleUtil {
+ public static void addModules(String modulePath) {
+ // nothing to do with Java 8
+ }
+
+ public static void addExports(List<String> exports) {
+ // nothing to do with Java 8
+ }
+
+ public static void addOpens(List<String> opens) {
+ // nothing to do with Java 8
+ }
+
+ public static void setupClassPath(Path libraryDir, List<String> paths) throws Throwable {
+ Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+ addURLMethod.setAccessible(true);
+ for (String path : paths) {
+ addURLMethod.invoke(ClassLoader.getSystemClassLoader(), libraryDir.resolve(path).toUri().toURL());
+ }
+ }
+
+ public static Class<?> setupBootstrapLauncher(Class<?> mainClass) {
+ // nothing to do with Java 8
+ return mainClass;
+ }
+
+ public static ClassLoader getPlatformClassLoader() {
+ // PlatformClassLoader does not exist in Java 8
+ return null;
+ }
+}
diff --git a/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector b/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector
new file mode 100644
index 0000000000..31f2c4e711
--- /dev/null
+++ b/forgewrapper/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector
@@ -0,0 +1 @@
+io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector