diff options
-rw-r--r-- | .gitattributes | 9 | ||||
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | build.gradle.kts | 39 | ||||
-rw-r--r-- | gradle.properties | 1 | ||||
-rw-r--r-- | gradle/wrapper/gradle-wrapper.properties | 6 | ||||
-rwxr-xr-x | gradlew | 244 | ||||
-rw-r--r-- | gradlew.bat | 92 | ||||
-rw-r--r-- | settings.gradle.kts | 10 | ||||
-rw-r--r-- | src/main/kotlin/mcprepack/App.kt | 374 | ||||
-rw-r--r-- | src/main/kotlin/mcprepack/CSVFile.kt | 5 | ||||
-rw-r--r-- | src/main/kotlin/mcprepack/WorkContext.kt | 97 | ||||
-rw-r--r-- | src/test/kotlin/mcprepack/AppTest.kt | 7 |
12 files changed, 892 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b068452 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +cache-mcprepack +work-mcprepack +*.jar
\ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1fa7c68 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + kotlin("jvm") version "1.8.0" + application +} + + +java.toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) +} + +repositories { + mavenCentral() + maven("https://maven.architectury.dev") + maven("https://maven.fabricmc.net") + maven("https://maven.minecraftforge.net") + maven("https://repository.ow2.org/repositories/releases/") +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.1") + implementation("dev.architectury:architectury-pack200:0.1.3") + implementation("net.fabricmc:stitch:0.6.2") + implementation("net.minecraftforge:srgutils:0.4.13") + implementation(platform("org.ow2.asm:asm-bom:9.4")) + implementation("org.ow2.asm:asm") + implementation("net.fabricmc:tiny-remapper:0.8.6") + implementation("com.github.jponge:lzma-java:1.3") + implementation("com.nothome:javaxdelta:2.0.1") // TODO maybe remove? + implementation("com.google.code.gson:gson:2.10.1") +} + +application { + mainClass.set("mcprepack.AppKt") +} + +tasks.named<Test>("test") { + useJUnitPlatform() +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fbbd9f8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.configureondemand=false
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f398c33 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,244 @@ +#!/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/HEAD/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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# 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*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + 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 \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# 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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..6452643 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html + */ + +rootProject.name = "mcprepack" diff --git a/src/main/kotlin/mcprepack/App.kt b/src/main/kotlin/mcprepack/App.kt new file mode 100644 index 0000000..86764e0 --- /dev/null +++ b/src/main/kotlin/mcprepack/App.kt @@ -0,0 +1,374 @@ +package mcprepack + +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import dev.architectury.pack200.java.Pack200 +import lzma.sdk.lzma.Decoder +import lzma.sdk.lzma.Encoder +import lzma.streams.LzmaInputStream +import lzma.streams.LzmaOutputStream +import net.fabricmc.stitch.commands.tinyv2.* +import net.minecraftforge.srgutils.IMappingFile +import net.minecraftforge.srgutils.INamedMappingFile +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.Opcodes +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarOutputStream +import kotlin.io.path.* + +val gson = GsonBuilder() + .setPrettyPrinting() + .create() + +@OptIn(ExperimentalPathApi::class) +fun main(): Unit = lifecycle("Repacking") { + val mavenModulePub = "test" + val pubVersion = "1.8.9" + + + WorkContext.setupWorkSpace() + if (true) { + WorkContext.getArtifact("net.minecraftforge", "forge", "1.19-41.1.0", "universal") + WorkContext.getArtifact("net.minecraftforge", "forge", "1.19-41.1.0", "installer") + WorkContext.getArtifact("de.oceanlabs.mcp", "mcp_config", "1.19-20220607.102129", extension = "zip") + val userdev119 = WorkContext.getArtifact("net.minecraftforge", "forge", "1.19-41.1.0", "userdev") + val patches119 = WorkContext.file("patches1.19", "jar").outputStream() + LzmaInputStream( + FileSystems.newFileSystem(userdev119).getPath("joined.lzma") + .inputStream(), Decoder() + ).copyTo(patches119) + patches119.close() + } + + val legacyForgeInstallerJar = lifecycleNonNull("Downloading Installer") { + WorkContext.getArtifact("net.minecraftforge", "forge", "1.8.9-11.15.1.2318-1.8.9", "installer") + } + + val legacyUserDev = lifecycleNonNull("Downloading userdev") { + WorkContext.getArtifact("net.minecraftforge", "forge", "1.8.9-11.15.1.2318-1.8.9", "userdev") + ?.let(FileSystems::newFileSystem) + } + val legacyForgeUniversal = lifecycleNonNull("Downloading universal") { + WorkContext.getArtifact("net.minecraftforge", "forge", "1.8.9-11.15.1.2318-1.8.9", "universal") + ?.let(FileSystems::newFileSystem) + } + val mcpStableFs = lifecycleNonNull("Downloading mcp stable") { + WorkContext.getArtifact("de.oceanlabs.mcp", "mcp_stable", "22-1.8.9", extension = "zip") + ?.let(FileSystems::newFileSystem) + } + val mcpSrgFs = lifecycleNonNull("Downloading mcp srg") { + WorkContext.getArtifact("de.oceanlabs.mcp", "mcp", "1.8.9", "srg", extension = "zip") + ?.let(FileSystems::newFileSystem) + } + + val (classesTiny, classesTsrg) = lifecycle("Generate Tiny classes") { + val classes = INamedMappingFile.load(Files.newInputStream(mcpSrgFs.getPath("joined.srg"))) + val tinyTemp = WorkContext.file("tiny-joined", "tiny") + val tsrgTemp = WorkContext.file("tsrg-joined", "tsrg") + classes.write(tinyTemp, IMappingFile.Format.TINY) + classes.write(tsrgTemp, IMappingFile.Format.TSRG) // Write tsrg not tsrg2 so mojang mappings arent involved + tinyTemp to tsrgTemp + } + + val minecraftjar = lifecycle("Load Minecraft Jar") { + FileSystems.newFileSystem(Path.of("minecraft-merged.jar")) + } + + val classCache = mutableMapOf<String, Map<String, String>>() + + fun findFieldDescriptorInMergedJar(className: String, fieldName: String): String? { + if (className in classCache) return classCache[className]!![fieldName] + val fieldDescriptors = mutableMapOf<String, String>() + val cr = ClassReader(Files.readAllBytes(minecraftjar.getPath("/$className.class"))) + cr.accept(object : ClassVisitor(Opcodes.ASM9) { + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any? + ): FieldVisitor? { + fieldDescriptors[name] = descriptor + return super.visitField(access, name, descriptor, signature, value) + } + }, 0) + classCache[className] = fieldDescriptors + return fieldDescriptors[fieldName] + } + + + val tinyV2Enhanced = lifecycle("Enhance tiny classes with methods and fields") { + val params = readCSV(mcpStableFs.getPath("/params.csv")) + val fields = readCSV(mcpStableFs.getPath("/fields.csv")) + val methods = readCSV(mcpStableFs.getPath("/methods.csv")) + val tinyFile = TinyV2Reader.read(classesTiny) + val newTiny = + TinyFile(TinyHeader(listOf("official", "intermediary", "named"), 2, 0, mapOf()), tinyFile.classEntries.map { + TinyClass(it.classNames + listOf(it.classNames[1]), it.methods.map { method -> + val mcpMethod = methods.map.find { it["searge"] == method.methodNames[1] } + TinyMethod(method.methodDescriptorInFirstNamespace, + method.methodNames + listOf(mcpMethod?.get("name") ?: method.methodNames[1]), + method.parameters, + method.localVariables, + method.comments + (mcpMethod?.get("desc")?.let { listOf(it) } ?: listOf())) + // TODO parameter names and better comments? + }, it.fields.map { field -> + val mcpField = fields.map.find { it["searge"] == field.fieldNames[1] } + TinyField(findFieldDescriptorInMergedJar(it.classNames[0], field.fieldNames[0]), + field.fieldNames + listOf(mcpField?.get("name") ?: field.fieldNames[1]), + field.comments + (mcpField?.get("desc")?.let { listOf(it) } ?: listOf())) + }, it.comments.map { it }) + }) + val newTinyFile = WorkContext.file("tiny-joined-enhanced", "tiny") + TinyV2Writer.write(newTiny, newTinyFile) + newTinyFile + } + + val yarnCompatibleJar = lifecycle("Create v2 compatible \"yarn\" zip") { + val x = WorkContext.file("yarn-1.8.9-v2", "zip") + Files.delete(x) + val fs = FileSystems.newFileSystem(x, mapOf("create" to true)) + val mappingsPath = fs.getPath("/mappings/mappings.tiny") + Files.createDirectories(mappingsPath.parent) + Files.copy(tinyV2Enhanced, mappingsPath) + fs.close() + x + } + + + val binpatchesLegacy = lifecycle("Unpack binpatches") { + val inputStream = + LzmaInputStream(legacyForgeUniversal.getPath("/binpatches.pack.lzma").inputStream(), Decoder()) + val patchJar = WorkContext.file("binpatches", "jar") + val patchOutput = JarOutputStream(patchJar.outputStream()) + Pack200.newUnpacker().unpack(inputStream, patchOutput) + patchOutput.close() + FileSystems.newFileSystem(patchJar) + } + + // TODO merge binpatches somehow? not sure how that would work, maybe look at essential loom + + fun createBinPatchSubJar(dist: String): Path { + val basePath = binpatchesLegacy.getPath("binpatch/client") + val modernPatchJar = WorkContext.file("binpatches-modern", "jar") + modernPatchJar.deleteExisting() + val patchJarFs = FileSystems.newFileSystem(modernPatchJar, mapOf("create" to true)) + basePath.listDirectoryEntries() + .filter { Files.isRegularFile(it) } + .forEach { + val to = patchJarFs.getPath("/${it.name}") + it.copyTo(to) + } + patchJarFs.close() + return modernPatchJar + + } + + val legacyDevJson = + gson.fromJson(legacyUserDev.getPath("/dev.json").readText(), JsonObject::class.java) + + val binpatchesModernClient = lifecycle("Modernize client binpatches") { + createBinPatchSubJar("client") + } + + val binpatchesModernServer = lifecycle("Modernize server binpatches") { + createBinPatchSubJar("server") + } + + val modernForgeInstaller = lifecycle("Create Modern Forge Installer") { + val x = WorkContext.file("modern-forge-installer", "jar") + x.deleteExisting() + legacyForgeInstallerJar.copyTo(x) + val fs = FileSystems.newFileSystem(x) + legacyForgeUniversal.getPath("version.json").copyTo(fs.getPath("version.json")) + + fs.getPath("/data").createDirectories() + fs.getPath("/data/client.lzma").outputStream().use { + binpatchesModernClient.inputStream() + .copyTo(LzmaOutputStream(it, Encoder())) + } + fs.getPath("/data/server.lzma").outputStream().use { + binpatchesModernServer.inputStream() + .copyTo(LzmaOutputStream(it, Encoder())) + } + fs.close() + // TODO args and run scripts also potentially specify mappings in install_profile.json + x + } + + val modernForgeUserdev = lifecycle("Create Modern Forge Userdev") { + val x = WorkContext.file("forge-1.8.9-userdev", "jar") + x.deleteExisting() + val userdevModern = FileSystems.newFileSystem(x, mapOf("create" to true)) + val config = userdevModern.getPath("config.json") + config.writeText(gson.toJson(JsonObject().apply { + addProperty("mcp", "$mavenModulePub:mcp_config:$pubVersion") + addProperty("binpatches", "joined.lzma") + add("binpatcher", JsonObject().apply { + addProperty("version", "net.minecraftforge:binarypatcher:1.1.1:fatjar") + add("args", JsonArray().apply { + add("--clean") + add("{clean}") + add("--output") + add("{output}") + add("--apply") + add("{patch}") + }) + }) + addProperty("universal", "net.minecraftforge:forge:1.8.9-11.15.1.2318-1.8.9:universal@jar") + addProperty("sources", "net.minecraftforge:forge:1.8.9-11.15.1.2318-1.8.9:sources@jar") + addProperty("spec", 2)//Hahaha, yes, I follow the spec, so true + add("libraries", JsonArray().apply { + legacyDevJson["libraries"].asJsonArray.forEach { + add(it.asJsonObject["name"]) + } + }) + })) + userdevModern.getPath("/joined.lzma").outputStream().use { + binpatchesModernClient.inputStream().copyTo(LzmaOutputStream(it, Encoder())) + } + userdevModern.close() + x + } + + val mcpConfig = lifecycle("Create modern mcp_config") { + val x = WorkContext.file("mcp_config-1.9.9", "jar") + x.deleteExisting() + val mcpConfigModern = FileSystems.newFileSystem(x, mapOf("create" to true)) + mcpConfigModern.getPath("config.json").writeText(gson.toJson(JsonObject().apply { + addProperty("spec", 3) + addProperty("version", "1.8.9") + addProperty("official", false) + addProperty("encoding", "UTF-8") + add("data", JsonObject().apply { + addProperty("mappings", "config/joined.tsrg") + // TODO Inject and Patches + }) + add("steps", JsonObject().apply { + add("joined", JsonArray().apply { + add(JsonObject().apply { addProperty("type", "downloadClient") }) + add(JsonObject().apply { addProperty("type", "downloadServer") }) + add(JsonObject().apply { addProperty("type", "listLibraries") }) + add(JsonObject().apply { + addProperty("type", "merge") + addProperty("client", "{downloadClientOutput}") + addProperty("server", "{downloadServerOutput}") + addProperty("version", "1.8.9") + addProperty("name", "rename") + }) + /* + add(JsonObject().apply { + addProperty("type", "rename") + addProperty("input", "{mergeOutput}")// TODO maybe strip? honestly not sure + addProperty("libraries", "{listLibrariesOutput}") + addProperty("mappings", "{mappings}") + })*/ + }) + for (dist in listOf("client", "server")) { + add(dist, JsonArray().apply { + add(JsonObject().apply { addProperty("type", "listLibraries") }) + add(JsonObject().apply { + addProperty( + "type", + "download" + dist.replaceFirstChar { it.uppercase() }) + }) + }) + // TODO rename in specific distro + } + }) + add("functions", JsonObject().apply { + add("merge", JsonObject().apply { + addProperty("version", "net.minecraftforge:mergetool:1.1.5:fatjar") + add("jvmargs", JsonArray()) + addProperty("repo", "https://maven.minecraftforge.net/") + add("args", JsonArray().apply { + listOf( + "--client", + "{client}", + "--server", + "{server}", + "--ann", + "{version}", + "--output", + "{output}", + "--inject", + "false" + ).forEach { add(it) } + }) + }) + add("rename", JsonObject().apply { + addProperty("version", "net.minecraftforge:ForgeAutoRenamingTool:0.1.22:all") + add("jvmargs", JsonArray()) + addProperty("repo", "https://maven.minecraftforge.net/") + add("args", JsonArray().apply { + listOf( + "--input", + "{input}", + "--output", + "{output}", + "--map", + "{mappings}", + "--cfg", + "{libraries}", + "--ann-fix", + "--ids-fix", + "--src-fix", + "--record-fix" + ).forEach { add(it) } + }) + }) + }) + })) + mcpConfigModern.getPath("/config").createDirectories() + classesTsrg.copyTo(mcpConfigModern.getPath("/config/joined.tsrg")) + mcpConfigModern.close() + x + } + + val mavenBasePath = Path.of(System.getProperty("user.home"), ".m2/repository") + lifecycle("Publish to $mavenBasePath") { + val modBasePath = mavenBasePath.resolve(mavenModulePub.replace(".", "/")) + modBasePath.deleteRecursively() + fun mavenFile(artifact: String, ext: String, classifier: String? = null) = + modBasePath.resolve("$artifact/$pubVersion/$artifact-$pubVersion${if (classifier != null) "-$classifier" else ""}.$ext") + .also { it.parent.createDirectories() } + + fun mkPom(artifact: String) = + """ + <?xml version="1.0" encoding="UTF-8"?> + <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <modelVersion>4.0.0</modelVersion> + <groupId>$mavenModulePub</groupId> + <artifactId>$artifact</artifactId> + <version>$pubVersion</version> + </project> + """.trimIndent() + + mavenFile("yarn", "pom").writeText(mkPom("yarn")) + yarnCompatibleJar.copyTo(mavenFile("yarn", "jar")) + yarnCompatibleJar.copyTo(mavenFile("yarn", "jar", "v2")) + + mavenFile("mcp_config", "pom").writeText(mkPom("mcp_config")) + mcpConfig.copyTo(mavenFile("mcp_config", "jar")) + + mavenFile("forge", "pom").writeText(mkPom("forge")) + modernForgeUserdev.copyTo(mavenFile("forge", "jar", "userdev")) + modernForgeUserdev.copyTo(mavenFile("forge", "jar")) + modernForgeInstaller.copyTo(mavenFile("forge", "jar", "installer")) + } + +} + +fun readCSV(path: Path): CSVFile { + // TODO proper "" handling + val lines = Files.readAllLines(path) + val headers = lines.first().split(",") + val entries = lines.drop(1).map { it.split(",", limit = headers.size) } + return CSVFile(headers, entries) +} diff --git a/src/main/kotlin/mcprepack/CSVFile.kt b/src/main/kotlin/mcprepack/CSVFile.kt new file mode 100644 index 0000000..6463fc7 --- /dev/null +++ b/src/main/kotlin/mcprepack/CSVFile.kt @@ -0,0 +1,5 @@ +package mcprepack + +data class CSVFile(val headers: List<String>, val entries: List<List<String>>) { + val map = entries.map { headers.zip(it).toMap() } +} diff --git a/src/main/kotlin/mcprepack/WorkContext.kt b/src/main/kotlin/mcprepack/WorkContext.kt new file mode 100644 index 0000000..74e929d --- /dev/null +++ b/src/main/kotlin/mcprepack/WorkContext.kt @@ -0,0 +1,97 @@ +package mcprepack + +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.system.exitProcess +import kotlin.time.ExperimentalTime +import kotlin.time.measureTime + +object WorkContext { + val workDir = Paths.get("work-mcprepack").toAbsolutePath().normalize() + val cacheDir = Paths.get("cache-mcprepack").toAbsolutePath().normalize() + + fun setupWorkSpace() { + println("# Setting up $workDir") + workDir.toFile().deleteRecursively() + Files.createDirectories(workDir) + } + + fun file(name: String, ext: String? = null): Path { + return Files.createTempFile(workDir, "$name-", if(ext != null) ".$ext" else "") + } + + fun dir(name: String): Path { + return Files.createTempFile(workDir, "$name-", "") + } + + val mavens = listOf( + "https://maven.minecraftforge.net/" + ) + + fun httpGet(url: String, into: Path): Boolean { + val conn = URL(url).openConnection() as HttpURLConnection + conn.connect() + val os = conn.inputStream + if (conn.responseCode != 200) { + conn.disconnect() + return false + } + Files.createDirectories(into.parent) + os.use { input -> + Files.newOutputStream(into).use { output -> + input.copyTo(output) + return true + } + } + } + + fun getArtifact( + module: String, + artifact: String, + version: String, + classifier: String = "", + extension: String = "jar" + ): Path? { + val moduleDir = module.replace(".", "/") + val extClassifier = if (classifier.isEmpty()) "" else "-$classifier" + val path = "$moduleDir/$artifact/$version/$artifact-$version$extClassifier.$extension" + val localSave = cacheDir.resolve(path) + if (Files.exists(localSave)) return localSave + for (maven in mavens) { + if (httpGet("$maven/$path", localSave)) + return localSave + } + return null + } + +} + +@OptIn(ExperimentalTime::class) +fun <T> lifecycle(name: String, block: () -> T): T { + var x: T + println("> $name") + val time = measureTime { + x = block() + } + println("> $name done. Took $time") + return x +} + +@OptIn(ExperimentalTime::class) + +fun <T : Any> lifecycleNonNull(name: String, block: () -> T?): T { + var x: T? + println("> $name") + val time = measureTime { + x = block() + } + if (x == null) { + println("! $name failed. Took $time") + exitProcess(1) + } + println("> $name done. Took $time") + return x as T +} diff --git a/src/test/kotlin/mcprepack/AppTest.kt b/src/test/kotlin/mcprepack/AppTest.kt new file mode 100644 index 0000000..265c852 --- /dev/null +++ b/src/test/kotlin/mcprepack/AppTest.kt @@ -0,0 +1,7 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package mcprepack + +import kotlin.test.Test +import kotlin.test.assertNotNull |