mirror of
https://github.com/BX-Team/DivineMC.git
synced 2025-12-19 14:59:25 +00:00
implement shuttle
This commit is contained in:
4
.github/workflows/build-1217.yml
vendored
4
.github/workflows/build-1217.yml
vendored
@@ -62,8 +62,8 @@ jobs:
|
||||
PARALLELISM=$(($(nproc) * 2))
|
||||
./gradlew applyAllPatches --stacktrace --parallel --max-workers=$PARALLELISM --build-cache --no-daemon
|
||||
|
||||
- name: Build Paperclip Jar
|
||||
run: ./gradlew createMojmapPaperclipJar --stacktrace --parallel --no-daemon
|
||||
- name: Build Shuttle Jar
|
||||
run: ./gradlew createMojmapShuttleJar --stacktrace --parallel --no-daemon
|
||||
|
||||
- name: Prepare Release Info
|
||||
run: bash scripts/releaseInfo.sh
|
||||
|
||||
6
.github/workflows/build-pr.yml
vendored
6
.github/workflows/build-pr.yml
vendored
@@ -26,11 +26,11 @@ jobs:
|
||||
- name: Apply Patches
|
||||
run: ./gradlew applyAllPatches --stacktrace --no-daemon
|
||||
|
||||
- name: Build Paperclip Jar
|
||||
run: ./gradlew createMojmapPaperclipJar --stacktrace --no-daemon
|
||||
- name: Build Shuttle Jar
|
||||
run: ./gradlew createMojmapShuttleJar --stacktrace --no-daemon
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@main
|
||||
with:
|
||||
name: DivineMC
|
||||
path: divinemc-server/build/libs/divinemc-paperclip-*-mojmap.jar
|
||||
path: divinemc-server/build/libs/divinemc-shuttle-*-mojmap.jar
|
||||
|
||||
@@ -43,7 +43,7 @@ Run the following commands in the root directory:
|
||||
|
||||
```bash
|
||||
> ./gradlew applyAllPatches # apply all patches
|
||||
> ./gradlew createMojmapPaperclipJar # build the server jar
|
||||
> ./gradlew createMojmapShuttleJar # build the server jar
|
||||
```
|
||||
|
||||
For anything else you can refer to our [contribution guide](https://bxteam.org/docs/divinemc/development/contributing).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
plugins {
|
||||
java
|
||||
@@ -91,6 +92,7 @@ subprojects {
|
||||
mavenCentral()
|
||||
maven(paperMavenPublicUrl)
|
||||
maven("https://jitpack.io")
|
||||
maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
|
||||
}
|
||||
|
||||
extensions.configure<PublishingExtension> {
|
||||
@@ -105,6 +107,76 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Jar>("createMojmapShuttleJar") {
|
||||
dependsOn(":divinemc-server:createMojmapPaperclipJar", "shuttle:shadowJar")
|
||||
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
val paperclipJarTask = project(":divinemc-server").tasks.getByName("createMojmapPaperclipJar")
|
||||
val shuttleJarTask = project(":shuttle").tasks.getByName("shadowJar")
|
||||
|
||||
val paperclipJar = paperclipJarTask.outputs.files.singleFile
|
||||
val shuttleJar = shuttleJarTask.outputs.files.singleFile
|
||||
val outputDir = paperclipJar.parentFile
|
||||
val tempDir = File(outputDir, "tempJarWork")
|
||||
val newJarName = "divinemc-shuttle-${properties["version"]}-mojmap.jar"
|
||||
|
||||
doFirst {
|
||||
val time = measureTimeMillis {
|
||||
println("Recompiling Paperclip with Shuttle sources...")
|
||||
|
||||
tempDir.deleteRecursively()
|
||||
tempDir.mkdirs()
|
||||
|
||||
copy {
|
||||
from(zipTree(paperclipJar))
|
||||
into(tempDir)
|
||||
}
|
||||
|
||||
val oldPackagePath = "io/papermc/paperclip/"
|
||||
tempDir.walkTopDown()
|
||||
.filter { it.isFile && it.relativeTo(tempDir).path.startsWith(oldPackagePath) }
|
||||
.forEach { it.delete() }
|
||||
|
||||
val shuttlePackagePath = "org/bxteam/shuttle/"
|
||||
copy {
|
||||
from(zipTree(shuttleJar))
|
||||
include("$shuttlePackagePath**")
|
||||
into(tempDir)
|
||||
}
|
||||
|
||||
tempDir.walkBottomUp()
|
||||
.filter { it.isDirectory && it.listFiles().isNullOrEmpty() }
|
||||
.forEach { it.delete() }
|
||||
|
||||
val metaInfDir = File(tempDir, "META-INF")
|
||||
metaInfDir.mkdirs()
|
||||
File(metaInfDir, "main-class").writeText("net.minecraft.server.Main")
|
||||
}
|
||||
println("Finished build in ${time}ms")
|
||||
}
|
||||
|
||||
archiveFileName.set(newJarName)
|
||||
destinationDirectory.set(outputDir)
|
||||
from(tempDir)
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Main-Class" to "org.bxteam.shuttle.Shuttle",
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
"Premain-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Agent-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Launcher-Agent-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Can-Redefine-Classes" to true,
|
||||
"Can-Retransform-Classes" to true
|
||||
)
|
||||
}
|
||||
|
||||
doLast {
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("printMinecraftVersion") {
|
||||
doLast {
|
||||
println(providers.gradleProperty("mcVersion").get().trim())
|
||||
|
||||
@@ -18,10 +18,28 @@ index 394443d00e661715439be1e56dddc129947699a4..480ad57a6b7b74e6b83e9c6ceb69ea1f
|
||||
public CrashReport(String title, Throwable exception) {
|
||||
io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception); // Paper
|
||||
diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
|
||||
index b06c2c4aa77edafb374f7cf0406cf4d29c6e7f9f..a476b53e0c5b18d9b0abceb4ffeb5ab4c5d7d6d9 100644
|
||||
index b06c2c4aa77edafb374f7cf0406cf4d29c6e7f9f..9d89af099c6e0f50c73a9372e1ef7f1b0ef932c5 100644
|
||||
--- a/net/minecraft/server/Main.java
|
||||
+++ b/net/minecraft/server/Main.java
|
||||
@@ -125,7 +125,6 @@ public class Main {
|
||||
@@ -64,6 +64,17 @@ import org.slf4j.Logger;
|
||||
public class Main {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
+ public static void main(String[] arguments) {
|
||||
+ OptionSet optionSet = org.bxteam.divinemc.DivineBootstrap.bootstrap(arguments);
|
||||
+
|
||||
+ io.papermc.paper.ServerBuildInfo info = io.papermc.paper.ServerBuildInfo.buildInfo();
|
||||
+ if (io.papermc.paper.ServerBuildInfoImpl.IS_EXPERIMENTAL) {
|
||||
+ LOGGER.warn("Running an experimental version of {}, please proceed with caution.", info.brandName());
|
||||
+ }
|
||||
+
|
||||
+ main(optionSet);
|
||||
+ }
|
||||
+
|
||||
@SuppressForbidden(
|
||||
reason = "System.out needed before bootstrap"
|
||||
)
|
||||
@@ -125,7 +136,6 @@ public class Main {
|
||||
dedicatedServerSettings.forceSave();
|
||||
RegionFileVersion.configure(dedicatedServerSettings.getProperties().regionFileComression);
|
||||
Path path2 = Paths.get("eula.txt");
|
||||
@@ -29,7 +47,7 @@ index b06c2c4aa77edafb374f7cf0406cf4d29c6e7f9f..a476b53e0c5b18d9b0abceb4ffeb5ab4
|
||||
// Paper start - load config files early for access below if needed
|
||||
org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionSet.valueOf("bukkit-settings"));
|
||||
org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionSet.valueOf("spigot-settings"));
|
||||
@@ -148,19 +147,6 @@ public class Main {
|
||||
@@ -148,19 +158,6 @@ public class Main {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -231,17 +231,111 @@ index 62e2d5704c348955bc8284dc2d54c933b7bcdd06..341f13e57896f03058ea3ec68e69b7cb
|
||||
public void executeAsync(final Runnable runnable) {
|
||||
MCUtil.scheduleAsyncTask(this.catching(runnable, "asynchronous"));
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
index c92ae5af5186a7c36f685272a13cfcfad21e8e69..24013949b6646016267132396e61d6cd4af8e374 100644
|
||||
index 748bd9650da4a209743b7a5dde584b2e19c5a578..7e90142cb65937103aa99fd011540086449c45c8 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
@@ -247,7 +247,7 @@ public class Main {
|
||||
System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
|
||||
System.setProperty("jdk.console", "java.base"); // Paper - revert default console provider back to java.base so we can have our own jline
|
||||
@@ -1,14 +1,8 @@
|
||||
package org.bukkit.craftbukkit;
|
||||
|
||||
import java.io.File;
|
||||
-import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
-import java.util.Calendar;
|
||||
-import java.util.Date;
|
||||
-import java.util.logging.Level;
|
||||
-import java.util.logging.Logger;
|
||||
import joptsimple.OptionParser;
|
||||
-import joptsimple.OptionSet;
|
||||
import joptsimple.util.PathConverter;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
@@ -24,7 +18,7 @@ public class Main {
|
||||
}
|
||||
// Paper end - Reset loggers after shutdown
|
||||
|
||||
- public static void main(String[] args) {
|
||||
+ public static OptionParser main(String[] args) { // DivineMC - Rebrand
|
||||
if (System.getProperty("jdk.nio.maxCachedBufferSize") == null) System.setProperty("jdk.nio.maxCachedBufferSize", "262144"); // Paper - cap per-thread NIO cache size; https://www.evanjones.ca/java-bytebuffer-leak.html
|
||||
OptionParser parser = new OptionParser() {
|
||||
{
|
||||
@@ -180,77 +174,6 @@ public class Main {
|
||||
}
|
||||
};
|
||||
|
||||
- OptionSet options = null;
|
||||
-
|
||||
- try {
|
||||
- options = parser.parse(args);
|
||||
- } catch (joptsimple.OptionException ex) {
|
||||
- Logger.getLogger(Main.class.getName()).log(Level.SEVERE, ex.getLocalizedMessage());
|
||||
- }
|
||||
-
|
||||
- if ((options == null) || (options.has("?"))) {
|
||||
- try {
|
||||
- parser.printHelpOn(System.out);
|
||||
- } catch (IOException ex) {
|
||||
- Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
- }
|
||||
- } else if (options.has("v")) {
|
||||
- System.out.println(CraftServer.class.getPackage().getImplementationVersion());
|
||||
- } else {
|
||||
- // Do you love Java using + and ! as string based identifiers? I sure do!
|
||||
- String path = new File(".").getAbsolutePath();
|
||||
- if (path.contains("!") || path.contains("+")) {
|
||||
- System.err.println("Cannot run server in a directory with ! or + in the pathname. Please rename the affected folders and try again.");
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- // Paper start - Improve java version check
|
||||
- boolean skip = Boolean.getBoolean("Paper.IgnoreJavaVersion");
|
||||
- String javaVersionName = System.getProperty("java.version");
|
||||
- // J2SE SDK/JRE Version String Naming Convention
|
||||
- boolean isPreRelease = javaVersionName.contains("-");
|
||||
- if (isPreRelease) {
|
||||
- if (!skip) {
|
||||
- System.err.println("Unsupported Java detected (" + javaVersionName + "). You are running an unsupported, non official, version. Only general availability versions of Java are supported. Please update your Java version. See https://docs.papermc.io/paper/faq#unsupported-java-detected-what-do-i-do for more information.");
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- System.err.println("Unsupported Java detected ("+ javaVersionName + "), but the check was skipped. Proceed with caution! ");
|
||||
- }
|
||||
- // Paper end - Improve java version check
|
||||
-
|
||||
- try {
|
||||
- if (options.has("nojline")) {
|
||||
- System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false");
|
||||
- useJline = false;
|
||||
- }
|
||||
-
|
||||
- if (options.has("noconsole")) {
|
||||
- Main.useConsole = false;
|
||||
- useJline = false; // Paper
|
||||
- System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper
|
||||
- }
|
||||
-
|
||||
- if (false && Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { // Purpur - Disable outdated build check
|
||||
- Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper
|
||||
-
|
||||
- Calendar deadline = Calendar.getInstance();
|
||||
- deadline.add(Calendar.DAY_OF_YEAR, -14);
|
||||
- if (buildDate.before(deadline.getTime())) {
|
||||
- // Paper start - This is some stupid bullshit
|
||||
- System.err.println("*** Warning, you've not updated in a while! ***");
|
||||
- System.err.println("*** Please download a new build from https://papermc.io/downloads/paper ***");
|
||||
- // Paper end
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
|
||||
- System.setProperty("jdk.console", "java.base"); // Paper - revert default console provider back to java.base so we can have our own jline
|
||||
-
|
||||
- io.papermc.paper.PaperBootstrap.boot(options);
|
||||
+ org.bxteam.divinemc.DivineBootstrap.boot(options); // DivineMC - Replace with DivineBootstrap
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
- } catch (Throwable t) {
|
||||
- t.printStackTrace();
|
||||
- }
|
||||
- }
|
||||
+ return parser; // DivineMC - Rebrand
|
||||
}
|
||||
}
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
|
||||
index 2e7c3d4befeb6256ce81ecaa9ed4e8fbcb21651e..a839dbbb72f48b8f8736d9f4693c528686570732 100644
|
||||
|
||||
@@ -31,10 +31,10 @@ index 592e8a4c04ef5acda9fdfd1405d8ff4952396ada..c8c7fa0304e8eaf0d444fc0c9a36c00b
|
||||
|
||||
Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
index cc4f4fbbe1cacce5f0f5500a8dda199aa0bcdf31..03fe7d98252b93f4625359f50552e3cf60e82b9c 100644
|
||||
index 7e90142cb65937103aa99fd011540086449c45c8..7653ba0259ddc930dc4e2af84636641d3dba6e7f 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
@@ -172,6 +172,14 @@ public class Main {
|
||||
@@ -166,6 +166,14 @@ public class Main {
|
||||
.describedAs("Yml file");
|
||||
// Purpur end - Purpur config files
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ index 70413fddd23ca1165cb5090cce4fddcb1bbca93f..ae70b84e6473fa2ed94416bf4bef8849
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Path> files = ((java.util.List<File>) optionSet.valuesOf("add-plugin")).stream().map(File::toPath).toList();
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
index 03fe7d98252b93f4625359f50552e3cf60e82b9c..ff350b5744b586cad7c2f5e0a04770e76f039d39 100644
|
||||
index 7653ba0259ddc930dc4e2af84636641d3dba6e7f..1a3a5e8a9119af8ed9d13a61bc5dc7b3ee0b7a65 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
|
||||
@@ -180,6 +180,14 @@ public class Main {
|
||||
@@ -174,6 +174,14 @@ public class Main {
|
||||
.describedAs("Yml file");
|
||||
// DivineMC end - Configuration
|
||||
|
||||
|
||||
@@ -1,29 +1,73 @@
|
||||
package org.bxteam.divinemc;
|
||||
|
||||
import io.papermc.paper.ServerBuildInfo;
|
||||
import joptsimple.OptionParser;
|
||||
import joptsimple.OptionSet;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Eula;
|
||||
import net.minecraft.server.Main;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.Main;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
public class DivineBootstrap {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("DivineBootstrap");
|
||||
|
||||
public static void boot(final OptionSet options) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
public static OptionSet bootstrap(String[] args) {
|
||||
OptionParser parser = Main.main(args);
|
||||
OptionSet options = parser.parse(args);
|
||||
|
||||
io.papermc.paper.ServerBuildInfo info = io.papermc.paper.ServerBuildInfo.buildInfo();
|
||||
if (io.papermc.paper.ServerBuildInfoImpl.IS_EXPERIMENTAL) {
|
||||
LOGGER.warn("Running an experimental version of {}, please proceed with caution.", info.brandName());
|
||||
if ((options == null) || (options.has("?"))) {
|
||||
try {
|
||||
parser.printHelpOn(System.out);
|
||||
} catch (IOException ex) {
|
||||
LOGGER.warn(ex.getMessage());
|
||||
}
|
||||
} else if (options.has("v")) {
|
||||
System.out.println(CraftServer.class.getPackage().getImplementationVersion());
|
||||
} else {
|
||||
String path = new File(".").getAbsolutePath();
|
||||
if (path.contains("!") || path.contains("+")) {
|
||||
System.err.println("Cannot run server in a directory with ! or + in the pathname. Please rename the affected folders and try again.");
|
||||
System.exit(70);
|
||||
}
|
||||
|
||||
boolean skip = Boolean.getBoolean("Paper.IgnoreJavaVersion");
|
||||
String javaVersionName = System.getProperty("java.version");
|
||||
boolean isPreRelease = javaVersionName.contains("-");
|
||||
if (isPreRelease) {
|
||||
if (!skip) {
|
||||
System.err.println("Unsupported Java detected (" + javaVersionName + "). You are running an unsupported, non official, version. Only general availability versions of Java are supported. Please update your Java version. See https://docs.papermc.io/paper/faq#unsupported-java-detected-what-do-i-do for more information.");
|
||||
System.exit(70);
|
||||
}
|
||||
|
||||
System.err.println("Unsupported Java detected ("+ javaVersionName + "), but the check was skipped. Proceed with caution! ");
|
||||
}
|
||||
|
||||
try {
|
||||
if (options.has("nojline")) {
|
||||
System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false");
|
||||
Main.useJline = false;
|
||||
}
|
||||
|
||||
if (options.has("noconsole")) {
|
||||
Main.useConsole = false;
|
||||
Main.useJline = false;
|
||||
System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper
|
||||
}
|
||||
|
||||
System.setProperty("library.jansi.version", "Paper");
|
||||
System.setProperty("jdk.console", "java.base");
|
||||
|
||||
SharedConstants.tryDetectVersion();
|
||||
Path path2 = Paths.get("eula.txt");
|
||||
Eula eula = new Eula(path2);
|
||||
boolean eulaAgreed = Boolean.getBoolean("com.mojang.eula.agree");
|
||||
@@ -34,12 +78,15 @@ public class DivineBootstrap {
|
||||
}
|
||||
if (!eula.hasAgreedToEULA() && !eulaAgreed) {
|
||||
LOGGER.info("You need to agree to the EULA in order to run the server. Go to eula.txt for more info.");
|
||||
return;
|
||||
System.exit(0);
|
||||
}
|
||||
System.out.println("Loading libraries, please wait..."); // Restore CraftBukkit log
|
||||
getStartupVersionMessages().forEach(LOGGER::info);
|
||||
|
||||
Main.main(options);
|
||||
getStartupVersionMessages().forEach(LOGGER::info);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static List<String> getStartupVersionMessages() {
|
||||
|
||||
@@ -5,7 +5,7 @@ mcVersion=1.21.7
|
||||
purpurRef=c4e5604ca8e4a36d24f4eaa90de7fee768fb60b7
|
||||
experimental=false
|
||||
|
||||
org.gradle.configuration-cache=true
|
||||
#org.gradle.configuration-cache=true
|
||||
org.gradle.caching = true
|
||||
org.gradle.parallel = true
|
||||
org.gradle.vfs.watch = false
|
||||
|
||||
@@ -28,7 +28,7 @@ make_latest=$([ "$experimental" = "true" ] && echo "false" || echo "true")
|
||||
|
||||
rm -f $changelog
|
||||
|
||||
mv divinemc-server/build/libs/divinemc-paperclip-"$version"-mojmap.jar "$jarName"
|
||||
mv divinemc-server/build/libs/divinemc-shuttle-"$version"-mojmap.jar "$jarName"
|
||||
{
|
||||
echo "name=$divinemcid"
|
||||
echo "tag=$tagid"
|
||||
|
||||
@@ -35,7 +35,7 @@ if (!file(".git").exists()) {
|
||||
|
||||
rootProject.name = "DivineMC"
|
||||
|
||||
for (name in listOf("divinemc-api", "divinemc-server")) {
|
||||
for (name in listOf("divinemc-api", "divinemc-server", "shuttle")) {
|
||||
val projName = name.lowercase(Locale.ENGLISH)
|
||||
include(projName)
|
||||
findProject(":$projName")!!.projectDir = file(name)
|
||||
|
||||
57
shuttle/build.gradle.kts
Normal file
57
shuttle/build.gradle.kts
Normal file
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
id("java")
|
||||
id("application")
|
||||
id("maven-publish")
|
||||
id("com.gradleup.shadow") version "8.3.8"
|
||||
}
|
||||
|
||||
val mainClass = "org.bxteam.shuttle.Shuttle"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.sigpipe:jbsdiff:1.0")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
options.release = 21
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
val jar = tasks.named("shadowJar")
|
||||
dependsOn(jar)
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from(zipTree(jar.map { it.outputs.files.singleFile }))
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Main-Class" to mainClass,
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
"Premain-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Agent-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Launcher-Agent-Class" to "org.bxteam.shuttle.patch.InstrumentationManager",
|
||||
"Can-Redefine-Classes" to true,
|
||||
"Can-Retransform-Classes" to true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
project.setProperty("mainClassName", mainClass)
|
||||
|
||||
tasks.shadowJar {
|
||||
val prefix = "paperclip.libs"
|
||||
listOf("org.apache", "org.tukaani", "io.sigpipe").forEach { pack ->
|
||||
relocate(pack, "$prefix.$pack")
|
||||
}
|
||||
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE.txt")
|
||||
}
|
||||
169
shuttle/src/main/java/org/bxteam/shuttle/Shuttle.java
Normal file
169
shuttle/src/main/java/org/bxteam/shuttle/Shuttle.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package org.bxteam.shuttle;
|
||||
|
||||
import org.bxteam.shuttle.logger.Logger;
|
||||
import org.bxteam.shuttle.patch.LibraryLoader;
|
||||
import org.bxteam.shuttle.patch.PatchBuilder;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public final class Shuttle {
|
||||
private static final String MAIN_CLASS_RESOURCE = "/META-INF/main-class";
|
||||
private static final String VERSION_RESOURCE = "/version.json";
|
||||
private static final String PATCH_ONLY_PROPERTY = "paperclip.patchonly";
|
||||
private static final String BUNDLER_MAIN_CLASS_PROPERTY = "bundlerMainClass";
|
||||
private static final String BUNDLER_REPO_DIR_PROPERTY = "bundlerRepoDir";
|
||||
private static final String MAIN_THREAD_NAME = "main";
|
||||
|
||||
public static void main(String[] arguments) {
|
||||
new Shuttle().run(arguments);
|
||||
}
|
||||
|
||||
private void run(String[] arguments) {
|
||||
try {
|
||||
String defaultMainClassName = readMainClass();
|
||||
String mainClassName = System.getProperty(BUNDLER_MAIN_CLASS_PROPERTY, defaultMainClassName);
|
||||
String repoDir = System.getProperty(BUNDLER_REPO_DIR_PROPERTY, "");
|
||||
|
||||
setupDirectories(repoDir);
|
||||
|
||||
Provider<String> versionProvider = this::readVersionFromResource;
|
||||
|
||||
executePatchingPhase(versionProvider);
|
||||
|
||||
if (shouldExitAfterPatching()) {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
executeLibraryLoadingPhase(versionProvider);
|
||||
|
||||
startTargetApplication(mainClassName, arguments);
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to extract server libraries, exiting", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private String readMainClass() throws IOException {
|
||||
return readResourceContent(MAIN_CLASS_RESOURCE, BufferedReader::readLine);
|
||||
}
|
||||
|
||||
private String readVersionFromResource() throws IOException {
|
||||
String jsonContent = readResourceContent(VERSION_RESOURCE, this::readAllLines);
|
||||
return extractVersionFromJson(jsonContent);
|
||||
}
|
||||
|
||||
private <T> T readResourceContent(String resourcePath, ResourceParser<T> parser) throws IOException {
|
||||
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Resource not found: " + resourcePath);
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
return parser.parse(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String readAllLines(BufferedReader reader) throws IOException {
|
||||
StringBuilder content = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
private String extractVersionFromJson(String jsonContent) throws IOException {
|
||||
String prefix = "\"id\": \"";
|
||||
int startIndex = jsonContent.indexOf(prefix);
|
||||
|
||||
if (startIndex == -1) {
|
||||
throw new IOException("Version ID not found in JSON content");
|
||||
}
|
||||
|
||||
startIndex += prefix.length();
|
||||
int endIndex = jsonContent.indexOf("\"", startIndex);
|
||||
|
||||
if (endIndex == -1) {
|
||||
throw new IOException("Malformed version ID in JSON content");
|
||||
}
|
||||
|
||||
return jsonContent.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private void setupDirectories(String repoDir) throws IOException {
|
||||
if (!repoDir.isEmpty()) {
|
||||
Path outputDir = Paths.get(repoDir);
|
||||
Files.createDirectories(outputDir);
|
||||
}
|
||||
}
|
||||
private void executePatchingPhase(Provider<String> versionProvider) throws IOException {
|
||||
new PatchBuilder().start(versionProvider);
|
||||
}
|
||||
|
||||
private boolean shouldExitAfterPatching() {
|
||||
return Boolean.getBoolean(PATCH_ONLY_PROPERTY);
|
||||
}
|
||||
|
||||
private void executeLibraryLoadingPhase(Provider<String> versionProvider) throws IOException {
|
||||
new LibraryLoader().start(versionProvider);
|
||||
}
|
||||
|
||||
private void startTargetApplication(String mainClassName, String[] arguments) {
|
||||
if (mainClassName == null || mainClassName.trim().isEmpty()) {
|
||||
Logger.warn("No main class specified, exiting");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info("Starting " + mainClassName);
|
||||
|
||||
Thread applicationThread = new Thread(() -> {
|
||||
try {
|
||||
invokeMainMethod(mainClassName, arguments);
|
||||
} catch (Throwable e) {
|
||||
ExceptionHandler.INSTANCE.rethrow(e);
|
||||
}
|
||||
}, MAIN_THREAD_NAME);
|
||||
|
||||
applicationThread.start();
|
||||
}
|
||||
|
||||
private void invokeMainMethod(String mainClassName, String[] arguments) throws Throwable {
|
||||
Class<?> mainClass = Class.forName(mainClassName);
|
||||
MethodHandle mainHandle = MethodHandles.lookup()
|
||||
.findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class))
|
||||
.asFixedArity();
|
||||
|
||||
mainHandle.invoke((Object) arguments);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ResourceParser<T> {
|
||||
T parse(BufferedReader reader) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Provider<T> {
|
||||
T get() throws IOException;
|
||||
}
|
||||
|
||||
private static final class ExceptionHandler {
|
||||
static final ExceptionHandler INSTANCE = new ExceptionHandler();
|
||||
|
||||
private ExceptionHandler() {
|
||||
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||
}
|
||||
|
||||
void rethrow(Throwable exception) {
|
||||
throw new RuntimeException("Exception in target application", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
shuttle/src/main/java/org/bxteam/shuttle/logger/Logger.java
Normal file
73
shuttle/src/main/java/org/bxteam/shuttle/logger/Logger.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package org.bxteam.shuttle.logger;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Formatter;
|
||||
|
||||
public class Logger {
|
||||
private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger("Shuttle");
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
static {
|
||||
LOGGER.setUseParentHandlers(false);
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setFormatter(new ShuttleFormatter());
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
LOGGER.addHandler(consoleHandler);
|
||||
|
||||
LOGGER.setLevel(Level.INFO);
|
||||
}
|
||||
|
||||
public static void info(String message) {
|
||||
LOGGER.info(message);
|
||||
}
|
||||
|
||||
public static void warn(String message) {
|
||||
LOGGER.warning(message);
|
||||
}
|
||||
|
||||
public static void error(String message) {
|
||||
LOGGER.severe(message);
|
||||
}
|
||||
|
||||
public static void error(String message, Throwable throwable) {
|
||||
LOGGER.log(Level.SEVERE, message, throwable);
|
||||
}
|
||||
|
||||
public static void debug(String message) {
|
||||
LOGGER.fine(message);
|
||||
}
|
||||
|
||||
public static void setLevel(Level level) {
|
||||
LOGGER.setLevel(level);
|
||||
LOGGER.getHandlers()[0].setLevel(level);
|
||||
}
|
||||
|
||||
private static class ShuttleFormatter extends Formatter {
|
||||
@Override
|
||||
public String format(LogRecord record) {
|
||||
String time = LocalTime.now().format(TIME_FORMATTER);
|
||||
String level = record.getLevel().getName();
|
||||
String message = record.getMessage();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[").append(time).append(" ").append(level).append("]: ");
|
||||
sb.append("[Shuttle] ");
|
||||
sb.append(message);
|
||||
sb.append(System.lineSeparator());
|
||||
|
||||
if (record.getThrown() != null) {
|
||||
java.io.StringWriter sw = new java.io.StringWriter();
|
||||
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
|
||||
record.getThrown().printStackTrace(pw);
|
||||
sb.append(sw.toString());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.bxteam.shuttle.patch;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
public final class InstrumentationManager {
|
||||
private static volatile Instrumentation instrumentation;
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private InstrumentationManager() {
|
||||
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||
}
|
||||
|
||||
public static void premain(final String agentArgs, final Instrumentation inst) {
|
||||
agentmain(agentArgs, inst);
|
||||
}
|
||||
|
||||
public static void agentmain(final String agentArgs, final Instrumentation inst) {
|
||||
if (inst == null) {
|
||||
throw new IllegalArgumentException("Instrumentation instance cannot be null");
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
if (instrumentation == null) {
|
||||
instrumentation = inst;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Instrumentation getInstrumentation() {
|
||||
final Instrumentation inst = instrumentation;
|
||||
if (inst == null) {
|
||||
throw new IllegalStateException(
|
||||
"Instrumentation has not been initialized. " +
|
||||
"Ensure the agent is properly loaded via -javaagent or attach mechanism."
|
||||
);
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package org.bxteam.shuttle.patch;
|
||||
|
||||
import org.bxteam.shuttle.Shuttle;
|
||||
import org.bxteam.shuttle.logger.Logger;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class LibraryLoader {
|
||||
private static final String LIBRARIES_DIR = "libraries";
|
||||
private static final String CACHE_DIR = "cache";
|
||||
private static final String LIBRARIES_LIST_RESOURCE = "/META-INF/libraries.list";
|
||||
private static final String LIBRARIES_PREFIX = "META-INF/libraries/";
|
||||
private static final String PATCH_EXTENSION = ".patch";
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private static final List<String> MAVEN_REPOSITORIES = List.of(
|
||||
"https://repo.papermc.io/repository/maven-public/",
|
||||
"https://jitpack.io",
|
||||
"https://s01.oss.sonatype.org/content/repositories/snapshots/",
|
||||
"https://repo1.maven.org/maven2/",
|
||||
"https://libraries.minecraft.net/"
|
||||
);
|
||||
|
||||
private static void copyStream(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
public void start(Shuttle.Provider<String> versionProvider) throws IOException {
|
||||
try {
|
||||
start(versionProvider.get());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error during library loading process", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void start(String mcVersion) throws IOException {
|
||||
if (mcVersion == null || mcVersion.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Minecraft version cannot be null or empty");
|
||||
}
|
||||
|
||||
Logger.info("Unpacking and linking library jars");
|
||||
|
||||
try {
|
||||
createLibrariesDirectory();
|
||||
|
||||
Path currentJar = getCurrentJarPath();
|
||||
extractLibrariesFromJar(currentJar);
|
||||
|
||||
Path vanillaBundler = Paths.get(CACHE_DIR, "vanilla-bundler-" + mcVersion + ".jar");
|
||||
if (Files.exists(vanillaBundler)) {
|
||||
extractLibrariesFromJar(vanillaBundler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Failed to load libraries for version " + mcVersion, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createLibrariesDirectory() throws IOException {
|
||||
Path librariesDir = Paths.get(LIBRARIES_DIR);
|
||||
if (!Files.exists(librariesDir)) {
|
||||
Files.createDirectories(librariesDir);
|
||||
}
|
||||
}
|
||||
|
||||
private Path getCurrentJarPath() throws IOException {
|
||||
try {
|
||||
return Paths.get(LibraryLoader.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Failed to get current JAR path", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractLibrariesFromJar(Path jarPath) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(jarPath.toFile())) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
|
||||
if (entry.getName().startsWith(LIBRARIES_PREFIX)) {
|
||||
processLibraryEntry(jarFile, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processLibraryEntry(JarFile jarFile, JarEntry entry) throws IOException {
|
||||
String relativePath = entry.getName().substring(LIBRARIES_PREFIX.length());
|
||||
File extractedFile = new File(LIBRARIES_DIR, relativePath);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(extractedFile.toPath());
|
||||
} else if (entry.getName().endsWith(PATCH_EXTENSION)) {
|
||||
processPatchEntry(entry);
|
||||
} else {
|
||||
extractLibraryFile(jarFile, entry, extractedFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractLibraryFile(JarFile jarFile, JarEntry entry, File extractedFile) throws IOException {
|
||||
File parentDir = extractedFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
try (InputStream input = jarFile.getInputStream(entry);
|
||||
FileOutputStream output = new FileOutputStream(extractedFile)) {
|
||||
copyStream(input, output);
|
||||
}
|
||||
|
||||
addToClasspath(extractedFile);
|
||||
}
|
||||
|
||||
private void processPatchEntry(JarEntry entry) throws IOException {
|
||||
try (InputStream inputStream = LibraryLoader.class.getResourceAsStream(LIBRARIES_LIST_RESOURCE);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Libraries list resource not found: " + LIBRARIES_LIST_RESOURCE);
|
||||
}
|
||||
|
||||
String entryDirectoryName = extractDirectoryName(entry.getName());
|
||||
boolean foundArtifact = false;
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
LibraryInfo libraryInfo = parseLibraryLine(line);
|
||||
if (libraryInfo == null) continue;
|
||||
|
||||
String libraryDirectoryName = extractDirectoryName(libraryInfo.jarPath);
|
||||
|
||||
if (entryDirectoryName.equalsIgnoreCase(libraryDirectoryName)) {
|
||||
foundArtifact = true;
|
||||
handleLibraryFromPatch(libraryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundArtifact) {
|
||||
String artifactName = extractArtifactName(entry.getName());
|
||||
Logger.error("Unable to find library: " + artifactName);
|
||||
throw new RuntimeException("Missing library: " + artifactName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String extractDirectoryName(String path) {
|
||||
return path.replaceFirst("^" + LIBRARIES_PREFIX, "")
|
||||
.replaceFirst("/[^/]+$", "");
|
||||
}
|
||||
|
||||
private String extractArtifactName(String entryName) {
|
||||
String[] parts = entryName.split("/");
|
||||
String fileName = parts[parts.length - 1];
|
||||
return fileName.replace(PATCH_EXTENSION, "");
|
||||
}
|
||||
|
||||
private LibraryInfo parseLibraryLine(String line) {
|
||||
if (line == null || line.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new LibraryInfo(parts[1], parts[2]);
|
||||
}
|
||||
|
||||
private void handleLibraryFromPatch(LibraryInfo libraryInfo) throws IOException {
|
||||
File libraryFile = new File(LIBRARIES_DIR, libraryInfo.jarPath);
|
||||
|
||||
if (!libraryFile.exists()) {
|
||||
File downloadedFile = downloadLibrary(libraryInfo.jarPath);
|
||||
|
||||
if (downloadedFile == null) {
|
||||
throw new IOException("Failed to download missing library: " + libraryInfo.artifact);
|
||||
}
|
||||
|
||||
libraryFile = downloadedFile;
|
||||
}
|
||||
|
||||
addToClasspath(libraryFile);
|
||||
}
|
||||
|
||||
private File downloadLibrary(String artifactPath) {
|
||||
for (String repository : MAVEN_REPOSITORIES) {
|
||||
try {
|
||||
String downloadUrl = repository + artifactPath;
|
||||
File downloadedFile = new File(LIBRARIES_DIR, artifactPath);
|
||||
|
||||
if (downloadFile(downloadUrl, downloadedFile)) {
|
||||
return downloadedFile;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Continue trying other repositories
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean downloadFile(String urlString, File outputFile) {
|
||||
try {
|
||||
File parentDir = outputFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
URL url = new URL(urlString);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setConnectTimeout(30000);
|
||||
connection.setReadTimeout(60000);
|
||||
|
||||
try (InputStream input = connection.getInputStream();
|
||||
FileOutputStream output = new FileOutputStream(outputFile)) {
|
||||
copyStream(input, output);
|
||||
}
|
||||
|
||||
return outputFile.exists() && outputFile.length() > 0;
|
||||
|
||||
} catch (Exception e) {
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addToClasspath(File jarFile) throws IOException {
|
||||
try (JarFile jar = new JarFile(jarFile)) {
|
||||
InstrumentationManager.getInstrumentation().appendToSystemClassLoaderSearch(jar);
|
||||
}
|
||||
}
|
||||
|
||||
private record LibraryInfo(String artifact, String jarPath) { }
|
||||
}
|
||||
248
shuttle/src/main/java/org/bxteam/shuttle/patch/PatchBuilder.java
Normal file
248
shuttle/src/main/java/org/bxteam/shuttle/patch/PatchBuilder.java
Normal file
@@ -0,0 +1,248 @@
|
||||
package org.bxteam.shuttle.patch;
|
||||
|
||||
import io.sigpipe.jbsdiff.InvalidHeaderException;
|
||||
import io.sigpipe.jbsdiff.Patch;
|
||||
import org.apache.commons.compress.compressors.CompressorException;
|
||||
import org.bxteam.shuttle.Shuttle;
|
||||
import org.bxteam.shuttle.logger.Logger;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class PatchBuilder {
|
||||
private static final String CACHE_DIR = "cache";
|
||||
private static final String VERSIONS_DIR = "versions";
|
||||
private static final String META_INF_PREFIX = "/META-INF/";
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private static String readResourceField(int index, String resourceName) {
|
||||
final String resourcePath = META_INF_PREFIX + resourceName;
|
||||
|
||||
try (InputStream inputStream = PatchBuilder.class.getResourceAsStream(resourcePath);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Resource not found: " + resourcePath);
|
||||
}
|
||||
|
||||
String line = reader.readLine();
|
||||
if (line == null) {
|
||||
throw new IOException("Empty resource file: " + resourcePath);
|
||||
}
|
||||
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length <= index) {
|
||||
throw new IOException("Invalid resource format or index out of bounds: " + resourcePath);
|
||||
}
|
||||
|
||||
return parts[index].trim();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to read " + resourceName + " at index " + index, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String computeFileSha256(File file) throws NoSuchAlgorithmException, IOException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
BufferedInputStream bis = new BufferedInputStream(fis)) {
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = bis.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
hexString.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
public void start(Shuttle.Provider<String> versionProvider) throws IOException {
|
||||
try {
|
||||
start(versionProvider.get());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error during patch building process", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void start(String mcVersion) throws IOException {
|
||||
if (mcVersion == null || mcVersion.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Minecraft version cannot be null or empty");
|
||||
}
|
||||
|
||||
Logger.info("Loading Minecraft version " + mcVersion);
|
||||
|
||||
try {
|
||||
createDirectories();
|
||||
|
||||
String sha256Hash = readResourceField(0, "download-context");
|
||||
String vanillaUrl = readResourceField(1, "download-context");
|
||||
|
||||
Path vanillaBundler = downloadVanillaBundler(mcVersion, vanillaUrl, sha256Hash);
|
||||
String patchedJarName = extractPatchedJarName();
|
||||
|
||||
applyPatches(mcVersion, vanillaBundler, patchedJarName);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Failed to build patched jar for version " + mcVersion, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createDirectories() throws IOException {
|
||||
Files.createDirectories(Paths.get(VERSIONS_DIR));
|
||||
Files.createDirectories(Paths.get(CACHE_DIR));
|
||||
}
|
||||
|
||||
private Path downloadVanillaBundler(String mcVersion, String vanillaUrl, String expectedSha256) throws IOException {
|
||||
Path vanillaBundler = Paths.get(CACHE_DIR, "vanilla-bundler-" + mcVersion + ".jar");
|
||||
|
||||
boolean needsDownload = !Files.exists(vanillaBundler);
|
||||
|
||||
if (!needsDownload) {
|
||||
try {
|
||||
String actualSha256 = computeFileSha256(vanillaBundler.toFile());
|
||||
needsDownload = !expectedSha256.equals(actualSha256);
|
||||
if (needsDownload) {
|
||||
Logger.info("SHA-256 mismatch, re-downloading vanilla jar");
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (needsDownload) {
|
||||
Logger.info("Downloading vanilla jar...");
|
||||
downloadFile(vanillaUrl, vanillaBundler);
|
||||
}
|
||||
|
||||
return vanillaBundler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL.
|
||||
*/
|
||||
private void downloadFile(String urlString, Path outputPath) throws IOException {
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(30000);
|
||||
connection.setReadTimeout(60000);
|
||||
|
||||
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
|
||||
FileOutputStream fos = new FileOutputStream(outputPath.toFile());
|
||||
BufferedOutputStream out = new BufferedOutputStream(fos)) {
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractPatchedJarName() {
|
||||
String versionListEntry = readResourceField(2, "versions.list");
|
||||
String[] parts = versionListEntry.split("/");
|
||||
if (parts.length < 2) {
|
||||
throw new RuntimeException("Invalid versions.list format");
|
||||
}
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
private void applyPatches(String mcVersion, Path vanillaBundler, String patchedJarName) throws IOException {
|
||||
Logger.info("Applying patches...");
|
||||
|
||||
Path vanillaJar = extractVanillaJar(mcVersion, vanillaBundler);
|
||||
|
||||
File patchFile = extractPatchFile(mcVersion);
|
||||
|
||||
Path outputJar = Paths.get(VERSIONS_DIR, mcVersion, patchedJarName);
|
||||
Files.createDirectories(outputJar.getParent());
|
||||
|
||||
try {
|
||||
applyPatch(vanillaJar.toFile(), patchFile, outputJar.toFile());
|
||||
addToClasspath(outputJar.toFile());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Failed to apply patch", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path extractVanillaJar(String mcVersion, Path vanillaBundler) throws IOException {
|
||||
Path vanillaJar = Paths.get(CACHE_DIR, "vanilla-" + mcVersion + ".jar");
|
||||
|
||||
try (JarFile jarFile = new JarFile(vanillaBundler.toFile())) {
|
||||
JarEntry entry = jarFile.getJarEntry("META-INF/versions/" + mcVersion + "/server-" + mcVersion + ".jar");
|
||||
|
||||
if (entry == null) {
|
||||
throw new IOException("Vanilla jar entry not found in bundler for version " + mcVersion);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = jarFile.getInputStream(entry)) {
|
||||
Files.copy(inputStream, vanillaJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
return vanillaJar;
|
||||
}
|
||||
|
||||
private File extractPatchFile(String mcVersion) throws IOException {
|
||||
String resourcePath = "/META-INF/versions/" + mcVersion + "/server-" + mcVersion + ".jar.patch";
|
||||
return extractResourceToFile(resourcePath, CACHE_DIR);
|
||||
}
|
||||
|
||||
private File extractResourceToFile(String resourcePath, String outputDir) throws IOException {
|
||||
try (InputStream resourceStream = PatchBuilder.class.getResourceAsStream(resourcePath)) {
|
||||
if (resourceStream == null) {
|
||||
throw new IOException("Resource not found: " + resourcePath);
|
||||
}
|
||||
|
||||
Path outputDirectory = Paths.get(outputDir);
|
||||
Files.createDirectories(outputDirectory);
|
||||
|
||||
String fileName = Paths.get(resourcePath).getFileName().toString();
|
||||
Path outputPath = outputDirectory.resolve(fileName);
|
||||
|
||||
Files.copy(resourceStream, outputPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
return outputPath.toFile();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPatch(File vanillaJar, File patchFile, File outputJar) throws IOException, CompressorException, InvalidHeaderException {
|
||||
byte[] vanillaBytes = Files.readAllBytes(vanillaJar.toPath());
|
||||
byte[] patchBytes = Files.readAllBytes(patchFile.toPath());
|
||||
|
||||
try (FileOutputStream outputStream = new FileOutputStream(outputJar)) {
|
||||
Patch.patch(vanillaBytes, patchBytes, outputStream);
|
||||
}
|
||||
|
||||
if (!outputJar.exists()) {
|
||||
throw new IOException("Patched jar was not created successfully");
|
||||
}
|
||||
}
|
||||
|
||||
private void addToClasspath(File jarFile) throws IOException {
|
||||
try (JarFile jar = new JarFile(jarFile)) {
|
||||
InstrumentationManager.getInstrumentation().appendToSystemClassLoaderSearch(jar);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user